From a431ca0f8b7f8967e89a35caddf1e41e53eee290 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski Date: Wed, 2 Nov 2016 11:39:12 +0100 Subject: Don't execute git hooks if you create branch as part of other change Currently, our procedure for adding a commit requires us to execute `CreateBranchService` before file creation. It's OK, but also we do execute `git hooks` (the `PostReceive` sidekiq job) as part of this process. However, this hook is execute before the file is actually committed, so the ref is updated. Secondly, we do execute a `git hooks` after committing file and updating ref. This results in duplicate `PostReceive` jobs, where the first one is completely invalid. This change makes the branch creation, something that is intermediate step of bigger process (file creation or update, commit cherry pick or revert) to not execute git hooks. --- app/models/repository.rb | 8 ++++++-- app/services/commits/change_service.rb | 2 +- app/services/create_branch_service.rb | 4 ++-- app/services/files/base_service.rb | 2 +- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 30be7262438..0776c7ccc5d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -160,14 +160,18 @@ class Repository tags.find { |tag| tag.name == name } end - def add_branch(user, branch_name, target) + def add_branch(user, branch_name, target, with_hooks: true) oldrev = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name target = commit(target).try(:id) return false unless target - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do + if with_hooks + GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do + update_ref!(ref, target, oldrev) + end + else update_ref!(ref, target, oldrev) end diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 1c82599c579..2d4c9788d02 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -55,7 +55,7 @@ module Commits return success if repository.find_branch(new_branch) result = CreateBranchService.new(@project, current_user) - .execute(new_branch, @target_branch, source_project: @source_project) + .execute(new_branch, @target_branch, source_project: @source_project, with_hooks: false) if result[:status] == :error raise ChangeError, "There was an error creating the source branch: #{result[:message]}" diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 757fc35a78f..a6a3461e17b 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -1,7 +1,7 @@ require_relative 'base_service' class CreateBranchService < BaseService - def execute(branch_name, ref, source_project: @project) + def execute(branch_name, ref, source_project: @project, with_hooks: true) valid_branch = Gitlab::GitRefValidator.validate(branch_name) unless valid_branch @@ -26,7 +26,7 @@ class CreateBranchService < BaseService repository.find_branch(branch_name) else - repository.add_branch(current_user, branch_name, ref) + repository.add_branch(current_user, branch_name, ref, with_hooks: with_hooks) end if new_branch diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 9bd4bd464f7..1802b932e03 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -74,7 +74,7 @@ module Files end def create_target_branch - result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project) + result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project, with_hooks: false) unless result[:status] == :success raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}") -- cgit v1.2.1 From 92aa402882cb9e2badc8213dd88913ad21b49857 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Sat, 5 Nov 2016 02:48:58 +0800 Subject: Add a test to make sure hooks are fire only once when updating a file to a different branch. --- spec/services/files/update_service_spec.rb | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb index d3c37c7820f..6fadee9304b 100644 --- a/spec/services/files/update_service_spec.rb +++ b/spec/services/files/update_service_spec.rb @@ -6,7 +6,10 @@ describe Files::UpdateService do let(:project) { create(:project) } let(:user) { create(:user) } let(:file_path) { 'files/ruby/popen.rb' } - let(:new_contents) { "New Content" } + let(:new_contents) { 'New Content' } + let(:target_branch) { project.default_branch } + let(:last_commit_sha) { nil } + let(:commit_params) do { file_path: file_path, @@ -16,7 +19,7 @@ describe Files::UpdateService do last_commit_sha: last_commit_sha, source_project: project, source_branch: project.default_branch, - target_branch: project.default_branch, + target_branch: target_branch } end @@ -54,18 +57,6 @@ describe Files::UpdateService do end context "when the last_commit_sha is not supplied" do - let(:commit_params) do - { - file_path: file_path, - commit_message: "Update File", - file_content: new_contents, - file_content_encoding: "text", - source_project: project, - source_branch: project.default_branch, - target_branch: project.default_branch, - } - end - it "returns a hash with the :success status " do results = subject.execute @@ -80,5 +71,15 @@ describe Files::UpdateService do expect(results.data).to eq(new_contents) end end + + context 'when target branch is different than source branch' do + let(:target_branch) { "#{project.default_branch}-new" } + + it 'fires hooks only once' do + expect(GitHooksService).to receive(:new).once.and_call_original + + subject.execute + end + end end end -- cgit v1.2.1 From 3128641f7eb93fec0930ebfb83a93dfa5e0b343a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 01:41:14 +0800 Subject: Revert "Don't execute git hooks if you create branch as part of other change" This reverts commit a431ca0f8b7f8967e89a35caddf1e41e53eee290. --- app/models/repository.rb | 8 ++------ app/services/commits/change_service.rb | 2 +- app/services/create_branch_service.rb | 4 ++-- app/services/files/base_service.rb | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index feaaacd02a9..063dc74021d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -162,18 +162,14 @@ class Repository tags.find { |tag| tag.name == name } end - def add_branch(user, branch_name, target, with_hooks: true) + def add_branch(user, branch_name, target) oldrev = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name target = commit(target).try(:id) return false unless target - if with_hooks - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do - update_ref!(ref, target, oldrev) - end - else + GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do update_ref!(ref, target, oldrev) end diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 2d4c9788d02..1c82599c579 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -55,7 +55,7 @@ module Commits return success if repository.find_branch(new_branch) result = CreateBranchService.new(@project, current_user) - .execute(new_branch, @target_branch, source_project: @source_project, with_hooks: false) + .execute(new_branch, @target_branch, source_project: @source_project) if result[:status] == :error raise ChangeError, "There was an error creating the source branch: #{result[:message]}" diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index a6a3461e17b..757fc35a78f 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -1,7 +1,7 @@ require_relative 'base_service' class CreateBranchService < BaseService - def execute(branch_name, ref, source_project: @project, with_hooks: true) + def execute(branch_name, ref, source_project: @project) valid_branch = Gitlab::GitRefValidator.validate(branch_name) unless valid_branch @@ -26,7 +26,7 @@ class CreateBranchService < BaseService repository.find_branch(branch_name) else - repository.add_branch(current_user, branch_name, ref, with_hooks: with_hooks) + repository.add_branch(current_user, branch_name, ref) end if new_branch diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 1802b932e03..9bd4bd464f7 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -74,7 +74,7 @@ module Files end def create_target_branch - result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project, with_hooks: false) + result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project) unless result[:status] == :success raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}") -- cgit v1.2.1 From 0b5a2eef8fa5ff4976f97883b631ec28f0553f6a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 04:02:10 +0800 Subject: Add `source_branch` option for various git operations If `source_branch` option is passed, and target branch cannot be found, `Repository#update_branch_with_hooks` would try to create a new branch from `source_branch`. This way, we could make changes in the new branch while only firing the hooks once for the changes. Previously, we can only create a new branch first then make changes to the new branch, firing hooks twice. This behaviour is bad for CI. Fixes #7237 --- app/models/repository.rb | 98 +++++++++++++++++++++-------- app/services/commits/change_service.rb | 10 +-- app/services/create_branch_service.rb | 22 +++---- app/services/files/base_service.rb | 11 ++-- app/services/files/create_dir_service.rb | 9 ++- app/services/files/create_service.rb | 11 +++- app/services/files/delete_service.rb | 9 ++- app/services/files/multi_service.rb | 3 +- app/services/files/update_service.rb | 3 +- app/services/validate_new_branch_service.rb | 22 +++++++ 10 files changed, 145 insertions(+), 53 deletions(-) create mode 100644 app/services/validate_new_branch_service.rb diff --git a/app/models/repository.rb b/app/models/repository.rb index 063dc74021d..b6581486983 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -786,8 +786,12 @@ class Repository @root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref } end - def commit_dir(user, path, message, branch, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| + def commit_dir(user, path, message, branch, + author_email: nil, author_name: nil, source_branch: nil) + update_branch_with_hooks( + user, + branch, + source_branch: source_branch) do |ref| options = { commit: { branch: ref, @@ -802,8 +806,12 @@ class Repository end end - def commit_file(user, path, content, message, branch, update, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| + def commit_file(user, path, content, message, branch, update, + author_email: nil, author_name: nil, source_branch: nil) + update_branch_with_hooks( + user, + branch, + source_branch: source_branch) do |ref| options = { commit: { branch: ref, @@ -823,8 +831,13 @@ class Repository end end - def update_file(user, path, content, branch:, previous_path:, message:, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| + def update_file(user, path, content, + branch:, previous_path:, message:, + author_email: nil, author_name: nil, source_branch: nil) + update_branch_with_hooks( + user, + branch, + source_branch: source_branch) do |ref| options = { commit: { branch: ref, @@ -849,8 +862,12 @@ class Repository end end - def remove_file(user, path, message, branch, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| + def remove_file(user, path, message, branch, + author_email: nil, author_name: nil, source_branch: nil) + update_branch_with_hooks( + user, + branch, + source_branch: source_branch) do |ref| options = { commit: { branch: ref, @@ -868,17 +885,18 @@ class Repository end end - def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil) - update_branch_with_hooks(user, branch) do |ref| + def multi_action(user:, branch:, message:, actions:, + author_email: nil, author_name: nil, source_branch: nil) + update_branch_with_hooks( + user, + branch, + source_branch: source_branch) do |ref| index = rugged.index parents = [] - branch = find_branch(ref) - if branch - last_commit = branch.dereferenced_target - index.read_tree(last_commit.raw_commit.tree) - parents = [last_commit.sha] - end + last_commit = find_branch(ref).dereferenced_target + index.read_tree(last_commit.raw_commit.tree) + parents = [last_commit.sha] actions.each do |action| case action[:action] @@ -967,7 +985,10 @@ class Repository return false unless revert_tree_id - update_branch_with_hooks(user, base_branch) do + update_branch_with_hooks( + user, + base_branch, + source_branch: revert_tree_id) do committer = user_to_committer(user) source_sha = Rugged::Commit.create(rugged, message: commit.revert_message, @@ -984,7 +1005,10 @@ class Repository return false unless cherry_pick_tree_id - update_branch_with_hooks(user, base_branch) do + update_branch_with_hooks( + user, + base_branch, + source_branch: cherry_pick_tree_id) do committer = user_to_committer(user) source_sha = Rugged::Commit.create(rugged, message: commit.message, @@ -1082,11 +1106,11 @@ class Repository fetch_ref(path_to_repo, ref, ref_path) end - def update_branch_with_hooks(current_user, branch) + def update_branch_with_hooks(current_user, branch, source_branch: nil) update_autocrlf_option ref = Gitlab::Git::BRANCH_REF_PREFIX + branch - target_branch = find_branch(branch) + target_branch, new_branch_added = raw_ensure_branch(branch, source_branch) was_empty = empty? # Make commit @@ -1096,7 +1120,7 @@ class Repository raise CommitError.new('Failed to create commit') end - if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil? + if rugged.lookup(newrev).parent_ids.empty? oldrev = Gitlab::Git::BLANK_SHA else oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha) @@ -1105,11 +1129,9 @@ class Repository GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do update_ref!(ref, newrev, oldrev) - if was_empty || !target_branch - # If repo was empty expire cache - after_create if was_empty - after_create_branch - end + # If repo was empty expire cache + after_create if was_empty + after_create_branch if was_empty || new_branch_added end newrev @@ -1169,4 +1191,28 @@ class Repository def repository_event(event, tags = {}) Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags)) end + + def raw_ensure_branch(branch_name, source_branch) + old_branch = find_branch(branch_name) + + if old_branch + [old_branch, false] + elsif source_branch + oldrev = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + target = commit(source_branch).try(:id) + + unless target + raise CommitError.new( + "Cannot find branch #{branch_name} nor #{source_branch}") + end + + update_ref!(ref, target, oldrev) + + [find_branch(branch_name), true] + else + raise CommitError.new( + "Cannot find branch #{branch_name} and source_branch is not set") + end + end end diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 1c82599c579..04b28cfaed8 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -29,7 +29,7 @@ module Commits tree_id = repository.public_send("check_#{action}_content", @commit, @target_branch) if tree_id - create_target_branch(into) if @create_merge_request + validate_target_branch(into) if @create_merge_request repository.public_send(action, current_user, @commit, into, tree_id) success @@ -50,12 +50,12 @@ module Commits true end - def create_target_branch(new_branch) + def validate_target_branch(new_branch) # Temporary branch exists and contains the change commit - return success if repository.find_branch(new_branch) + return if repository.find_branch(new_branch) - result = CreateBranchService.new(@project, current_user) - .execute(new_branch, @target_branch, source_project: @source_project) + result = ValidateNewBranchService.new(@project, current_user). + execute(new_branch) if result[:status] == :error raise ChangeError, "There was an error creating the source branch: #{result[:message]}" diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 757fc35a78f..f4270a09928 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -2,18 +2,9 @@ require_relative 'base_service' class CreateBranchService < BaseService def execute(branch_name, ref, source_project: @project) - valid_branch = Gitlab::GitRefValidator.validate(branch_name) + failure = validate_new_branch(branch_name) - unless valid_branch - return error('Branch name is invalid') - end - - repository = project.repository - existing_branch = repository.find_branch(branch_name) - - if existing_branch - return error('Branch already exists') - end + return failure if failure new_branch = if source_project != @project repository.fetch_ref( @@ -41,4 +32,13 @@ class CreateBranchService < BaseService def success(branch) super().merge(branch: branch) end + + private + + def validate_new_branch(branch_name) + result = ValidateNewBranchService.new(project, current_user). + execute(branch_name) + + error(result[:message]) if result[:status] == :error + end end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 9bd4bd464f7..6779bd2818a 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -23,9 +23,7 @@ module Files validate # Create new branch if it different from source_branch - if different_branch? - create_target_branch - end + validate_target_branch if different_branch? result = commit if result @@ -73,10 +71,11 @@ module Files end end - def create_target_branch - result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project) + def validate_target_branch + result = ValidateNewBranchService.new(project, current_user). + execute(@target_branch) - unless result[:status] == :success + if result[:status] == :error raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}") end end diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index d00d78cee7e..c59b3f8c70c 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -3,7 +3,14 @@ require_relative "base_service" module Files class CreateDirService < Files::BaseService def commit - repository.commit_dir(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name) + repository.commit_dir( + current_user, + @file_path, + @commit_message, + @target_branch, + author_email: @author_email, + author_name: @author_name, + source_branch: @source_branch) end def validate diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index bf127843d55..6d0a0f2629d 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -3,7 +3,16 @@ require_relative "base_service" module Files class CreateService < Files::BaseService def commit - repository.commit_file(current_user, @file_path, @file_content, @commit_message, @target_branch, false, author_email: @author_email, author_name: @author_name) + repository.commit_file( + current_user, + @file_path, + @file_content, + @commit_message, + @target_branch, + false, + author_email: @author_email, + author_name: @author_name, + source_branch: @source_branch) end def validate diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index 8b27ad51789..79d592731e9 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -3,7 +3,14 @@ require_relative "base_service" module Files class DeleteService < Files::BaseService def commit - repository.remove_file(current_user, @file_path, @commit_message, @target_branch, author_email: @author_email, author_name: @author_name) + repository.remove_file( + current_user, + @file_path, + @commit_message, + @target_branch, + author_email: @author_email, + author_name: @author_name, + source_branch: @source_branch) end end end diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index d28912e1301..0550dec15a6 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -11,7 +11,8 @@ module Files message: @commit_message, actions: params[:actions], author_email: @author_email, - author_name: @author_name + author_name: @author_name, + source_branch: @source_branch ) end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index c17fdb8d1f1..f3a766ed9fd 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -10,7 +10,8 @@ module Files previous_path: @previous_path, message: @commit_message, author_email: @author_email, - author_name: @author_name) + author_name: @author_name, + source_branch: @source_branch) end private diff --git a/app/services/validate_new_branch_service.rb b/app/services/validate_new_branch_service.rb new file mode 100644 index 00000000000..2f61be184ce --- /dev/null +++ b/app/services/validate_new_branch_service.rb @@ -0,0 +1,22 @@ +require_relative 'base_service' + +class ValidateNewBranchService < BaseService + def execute(branch_name) + valid_branch = Gitlab::GitRefValidator.validate(branch_name) + + unless valid_branch + return error('Branch name is invalid') + end + + repository = project.repository + existing_branch = repository.find_branch(branch_name) + + if existing_branch + return error('Branch already exists') + end + + success + rescue GitHooksService::PreReceiveError => ex + error(ex.message) + end +end -- cgit v1.2.1 From 92a438263fafdd7c4163f09e63815dc805cb8d12 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 04:27:21 +0800 Subject: Fix issues found by rubocop --- app/models/repository.rb | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index b6581486983..beafe9060bf 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -786,7 +786,8 @@ class Repository @root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref } end - def commit_dir(user, path, message, branch, + def commit_dir( + user, path, message, branch, author_email: nil, author_name: nil, source_branch: nil) update_branch_with_hooks( user, @@ -806,7 +807,9 @@ class Repository end end - def commit_file(user, path, content, message, branch, update, + # rubocop:disable Metrics/ParameterLists + def commit_file( + user, path, content, message, branch, update, author_email: nil, author_name: nil, source_branch: nil) update_branch_with_hooks( user, @@ -830,8 +833,11 @@ class Repository Gitlab::Git::Blob.commit(raw_repository, options) end end + # rubocop:enable Metrics/ParameterLists - def update_file(user, path, content, + # rubocop:disable Metrics/ParameterLists + def update_file( + user, path, content, branch:, previous_path:, message:, author_email: nil, author_name: nil, source_branch: nil) update_branch_with_hooks( @@ -861,8 +867,10 @@ class Repository end end end + # rubocop:enable Metrics/ParameterLists - def remove_file(user, path, message, branch, + def remove_file( + user, path, message, branch, author_email: nil, author_name: nil, source_branch: nil) update_branch_with_hooks( user, @@ -885,14 +893,14 @@ class Repository end end - def multi_action(user:, branch:, message:, actions:, + def multi_action( + user:, branch:, message:, actions:, author_email: nil, author_name: nil, source_branch: nil) update_branch_with_hooks( user, branch, source_branch: source_branch) do |ref| index = rugged.index - parents = [] last_commit = find_branch(ref).dereferenced_target index.read_tree(last_commit.raw_commit.tree) -- cgit v1.2.1 From 8fca786bdee6fa23a5e000bf23e65b153fc1bf73 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 04:46:54 +0800 Subject: They're never referred --- app/models/repository.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index beafe9060bf..b2b5d528840 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -998,7 +998,7 @@ class Repository base_branch, source_branch: revert_tree_id) do committer = user_to_committer(user) - source_sha = Rugged::Commit.create(rugged, + Rugged::Commit.create(rugged, message: commit.revert_message, author: committer, committer: committer, @@ -1018,7 +1018,7 @@ class Repository base_branch, source_branch: cherry_pick_tree_id) do committer = user_to_committer(user) - source_sha = Rugged::Commit.create(rugged, + Rugged::Commit.create(rugged, message: commit.message, author: { email: commit.author_email, -- cgit v1.2.1 From 30d7b5c32cfb97d7c1e57fb8874069077097c89d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 05:29:24 +0800 Subject: Fix the case when it's a whole new branch --- app/models/repository.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index b2b5d528840..5e7bb309967 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1128,7 +1128,7 @@ class Repository raise CommitError.new('Failed to create commit') end - if rugged.lookup(newrev).parent_ids.empty? + if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil? oldrev = Gitlab::Git::BLANK_SHA else oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha) @@ -1219,8 +1219,7 @@ class Repository [find_branch(branch_name), true] else - raise CommitError.new( - "Cannot find branch #{branch_name} and source_branch is not set") + [nil, true] # Empty branch end end end -- cgit v1.2.1 From eddee5fe8770d79c80fdb0d91731f866c14c9b8d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 06:01:54 +0800 Subject: Make sure we create target branch for cherry/revert --- app/models/repository.rb | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 5e7bb309967..0f3e98db420 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -988,7 +988,8 @@ class Repository end def revert(user, commit, base_branch, revert_tree_id = nil) - source_sha = find_branch(base_branch).dereferenced_target.sha + source_sha = raw_ensure_branch(base_branch, source_commit: commit). + first.dereferenced_target.sha revert_tree_id ||= check_revert_content(commit, base_branch) return false unless revert_tree_id @@ -1008,7 +1009,8 @@ class Repository end def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) - source_sha = find_branch(base_branch).dereferenced_target.sha + source_sha = raw_ensure_branch(base_branch, source_commit: commit). + first.dereferenced_target.sha cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) return false unless cherry_pick_tree_id @@ -1118,7 +1120,8 @@ class Repository update_autocrlf_option ref = Gitlab::Git::BRANCH_REF_PREFIX + branch - target_branch, new_branch_added = raw_ensure_branch(branch, source_branch) + target_branch, new_branch_added = + raw_ensure_branch(branch, source_branch: source_branch) was_empty = empty? # Make commit @@ -1200,19 +1203,19 @@ class Repository Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags)) end - def raw_ensure_branch(branch_name, source_branch) + def raw_ensure_branch(branch_name, source_commit: nil, source_branch: nil) old_branch = find_branch(branch_name) if old_branch [old_branch, false] - elsif source_branch + elsif source_commit || source_branch oldrev = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - target = commit(source_branch).try(:id) + target = (source_commit || commit(source_branch)).try(:sha) unless target raise CommitError.new( - "Cannot find branch #{branch_name} nor #{source_branch}") + "Cannot find branch #{branch_name} nor #{source_commit.try(:sha) ||source_branch}") end update_ref!(ref, target, oldrev) -- cgit v1.2.1 From 30bcc3de41d86e02c27940e0e8e4a1a82183520e Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 06:18:24 +0800 Subject: Add missing space due to Sublime Text It's Sublime Text's fault. Its word wrapping is not code friendly --- app/models/repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 0f3e98db420..89293fa8b4d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1215,7 +1215,7 @@ class Repository unless target raise CommitError.new( - "Cannot find branch #{branch_name} nor #{source_commit.try(:sha) ||source_branch}") + "Cannot find branch #{branch_name} nor #{source_commit.try(:sha) || source_branch}") end update_ref!(ref, target, oldrev) -- cgit v1.2.1 From 5de74551ace7b6df9fdb2a3c8aa30c836d693728 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 06:55:54 +0800 Subject: Branch could be nil if it's an empty repo --- app/models/repository.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 89293fa8b4d..c4bdc84348e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -901,10 +901,15 @@ class Repository branch, source_branch: source_branch) do |ref| index = rugged.index - - last_commit = find_branch(ref).dereferenced_target - index.read_tree(last_commit.raw_commit.tree) - parents = [last_commit.sha] + branch_commit = find_branch(ref) + + parents = if branch_commit + last_commit = branch_commit.dereferenced_target + index.read_tree(last_commit.raw_commit.tree) + [last_commit.sha] + else + [] + end actions.each do |action| case action[:action] -- cgit v1.2.1 From d8fe2fac7e681ddbff3c7a5338f939eb2d540e38 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 07:22:50 +0800 Subject: Make sure we have the branch on the other project --- app/models/project.rb | 11 +++++++++++ app/services/create_branch_service.rb | 10 +--------- app/services/files/base_service.rb | 10 +++++++++- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 94aabafce20..1208e5da6fa 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -988,6 +988,17 @@ class Project < ActiveRecord::Base Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path) end + def fetch_ref(source_project, branch_name, ref) + repository.fetch_ref( + source_project.repository.path_to_repo, + "refs/heads/#{ref}", + "refs/heads/#{branch_name}" + ) + + repository.after_create_branch + repository.find_branch(branch_name) + end + # Expires various caches before a project is renamed. def expire_caches_before_rename(old_path) repo = Repository.new(old_path, self) diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index f4270a09928..ecb5994fab8 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -7,15 +7,7 @@ class CreateBranchService < BaseService return failure if failure new_branch = if source_project != @project - repository.fetch_ref( - source_project.repository.path_to_repo, - "refs/heads/#{ref}", - "refs/heads/#{branch_name}" - ) - - repository.after_create_branch - - repository.find_branch(branch_name) + @project.fetch_ref(source_project, branch_name, ref) else repository.add_branch(current_user, branch_name, ref) end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 6779bd2818a..fd62246eddb 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -23,7 +23,7 @@ module Files validate # Create new branch if it different from source_branch - validate_target_branch if different_branch? + ensure_target_branch if different_branch? result = commit if result @@ -71,6 +71,14 @@ module Files end end + def ensure_target_branch + validate_target_branch + + if @source_project != project + @project.fetch_ref(@source_project, @target_branch, @source_branch) + end + end + def validate_target_branch result = ValidateNewBranchService.new(project, current_user). execute(@target_branch) -- cgit v1.2.1 From a68a62011d03c15d6116dc1e6dcb9514040a51f5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 07:53:36 +0800 Subject: Don't pass source_branch if it doesn't exist --- app/controllers/concerns/creates_commit.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index dacb5679dd3..643b61af1b2 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,9 +4,10 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables + source_branch = @ref if @repository.find_branch(@ref) commit_params = @commit_params.merge( source_project: @project, - source_branch: @ref, + source_branch: source_branch, target_branch: @target_branch ) -- cgit v1.2.1 From 39d83f72e7af78d503ef278e22eda25f90322f4b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 08:10:32 +0800 Subject: Add a few comments to explain implementation detail --- app/models/repository.rb | 2 ++ app/services/files/base_service.rb | 3 +++ 2 files changed, 5 insertions(+) diff --git a/app/models/repository.rb b/app/models/repository.rb index c4bdc84348e..4e3e70192ab 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1121,6 +1121,8 @@ class Repository fetch_ref(path_to_repo, ref, ref_path) end + # Whenever `source_branch` is passed, if `branch` doesn't exist, it would + # be created from `source_branch`. def update_branch_with_hooks(current_user, branch, source_branch: nil) update_autocrlf_option diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index fd62246eddb..8689c83d40e 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -75,6 +75,9 @@ module Files validate_target_branch if @source_project != project + # Make sure we have the source_branch in target project, + # and use source_branch as target_branch directly to avoid + # unnecessary source branch in target project. @project.fetch_ref(@source_project, @target_branch, @source_branch) end end -- cgit v1.2.1 From cd22fdd531c151cf1bbc307e52c565e86070fe3b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 08:21:16 +0800 Subject: @ref might not exist --- app/controllers/concerns/creates_commit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 643b61af1b2..5d11f286e9a 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,7 +4,7 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables - source_branch = @ref if @repository.find_branch(@ref) + source_branch = @ref if @ref && @repository.find_branch(@ref) commit_params = @commit_params.merge( source_project: @project, source_branch: source_branch, -- cgit v1.2.1 From 3e69c716f0b81b2c3b863d3c8a5e6346b9978fc1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 15 Nov 2016 20:03:18 +0800 Subject: Try if branch_exists? would work, feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_18424135 --- app/controllers/concerns/creates_commit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 5d11f286e9a..e5c40446314 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,7 +4,7 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables - source_branch = @ref if @ref && @repository.find_branch(@ref) + source_branch = @ref if @ref && @repository.branch_exists?(@ref) commit_params = @commit_params.merge( source_project: @project, source_branch: source_branch, -- cgit v1.2.1 From 1a21fa26f6f4ef70157c58329687976fc3f555f7 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 2 Nov 2016 11:11:47 +0000 Subject: Improved ref switcher dropdown performance Closes #18202 --- app/assets/javascripts/project.js | 21 ++++++++++++++++----- app/controllers/projects_controller.rb | 7 +++++-- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 016d999d77e..78ab69206af 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -63,7 +63,8 @@ return $.ajax({ url: $dropdown.data('refs-url'), data: { - ref: $dropdown.data('ref') + ref: $dropdown.data('ref'), + search: term }, dataType: "json" }).done(function(refs) { @@ -72,16 +73,26 @@ }, selectable: true, filterable: true, + filterRemote: true, filterByText: true, fieldName: $dropdown.data('field-name'), renderRow: function(ref) { - var link; + var li = document.createElement('li'); + if (ref.header != null) { - return $('
  • ').addClass('dropdown-header').text(ref.header); + li.className = 'dropdown-header'; + li.textContent = ref.header; } else { - link = $('').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref); - return $('
  • ').append(link); + var link = document.createElement('a'); + link.href = '#'; + link.className = ref.name === selected ? 'is-active' : ''; + link.textContent = ref.name; + link.dataset.ref = ref.name; + + li.appendChild(link); } + + return li; }, id: function(obj, $el) { return $el.attr('data-ref'); diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index a8a18b4fa16..fe0670aa246 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -267,12 +267,15 @@ class ProjectsController < Projects::ApplicationController end def refs + branches = BranchesFinder.new(@repository, params).execute + options = { - 'Branches' => @repository.branch_names, + 'Branches' => Kaminari.paginate_array(branches).page(params[:page]).per(100), } unless @repository.tag_count.zero? - options['Tags'] = VersionSorter.rsort(@repository.tag_names) + tags = TagsFinder.new(@repository, params).execute + options['Tags'] = Kaminari.paginate_array(tags).page(params[:page]).per(100) end # If reference is commit id - we should add it to branch/tag selectbox -- cgit v1.2.1 From af02f6ae9d500b0174cae106891b626d1dcae351 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 2 Nov 2016 11:31:00 +0000 Subject: Use cloneNode instead of createElement --- app/assets/javascripts/project.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 78ab69206af..7ac070a9c37 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -54,6 +54,11 @@ }; Project.prototype.initRefSwitcher = function() { + var refListItem = document.createElement('li'), + refLink = document.createElement('a'); + + refLink.href = '#'; + return $('.js-project-refs-dropdown').each(function() { var $dropdown, selected; $dropdown = $(this); @@ -77,21 +82,24 @@ filterByText: true, fieldName: $dropdown.data('field-name'), renderRow: function(ref) { - var li = document.createElement('li'); + var li = refListItem.cloneNode(false); if (ref.header != null) { li.className = 'dropdown-header'; li.textContent = ref.header; } else { - var link = document.createElement('a'); - link.href = '#'; - link.className = ref.name === selected ? 'is-active' : ''; + var link = refLink.cloneNode(false); + + if (ref.name === selected) { + link.className = 'is-active'; + } + link.textContent = ref.name; link.dataset.ref = ref.name; li.appendChild(link); } - + return li; }, id: function(obj, $el) { -- cgit v1.2.1 From ba2089e01711eb3f97770235b86ae9e59862ee8b Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Wed, 2 Nov 2016 12:15:25 +0000 Subject: Uses take rather than Kaminari --- app/controllers/projects_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index fe0670aa246..6affadfa0a6 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -270,12 +270,12 @@ class ProjectsController < Projects::ApplicationController branches = BranchesFinder.new(@repository, params).execute options = { - 'Branches' => Kaminari.paginate_array(branches).page(params[:page]).per(100), + 'Branches' => branches.take(100), } unless @repository.tag_count.zero? tags = TagsFinder.new(@repository, params).execute - options['Tags'] = Kaminari.paginate_array(tags).page(params[:page]).per(100) + options['Tags'] = tags.take(100) end # If reference is commit id - we should add it to branch/tag selectbox -- cgit v1.2.1 From f9750b4912c6f0e4c7b0b7d213f95223d5d1a593 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 3 Nov 2016 12:16:15 +0000 Subject: Changed how the data is returned - we only care about the branch/tag name --- app/assets/javascripts/project.js | 4 ++-- app/controllers/projects_controller.rb | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 7ac070a9c37..e7db0620848 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -94,8 +94,8 @@ link.className = 'is-active'; } - link.textContent = ref.name; - link.dataset.ref = ref.name; + link.textContent = ref; + link.dataset.ref = ref; li.appendChild(link); } diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 6affadfa0a6..d7bc31b0718 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -267,14 +267,15 @@ class ProjectsController < Projects::ApplicationController end def refs - branches = BranchesFinder.new(@repository, params).execute + branches = BranchesFinder.new(@repository, params).execute.map(&:name) options = { 'Branches' => branches.take(100), } unless @repository.tag_count.zero? - tags = TagsFinder.new(@repository, params).execute + tags = TagsFinder.new(@repository, params).execute.map(&:name) + options['Tags'] = tags.take(100) end -- cgit v1.2.1 From 08dd831f02daeeefedf65302a9a9575733943357 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 3 Nov 2016 12:25:08 +0000 Subject: Fixed the active branch selected indicator --- app/assets/javascripts/project.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index e7db0620848..04e18098544 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -90,7 +90,7 @@ } else { var link = refLink.cloneNode(false); - if (ref.name === selected) { + if (ref === selected) { link.className = 'is-active'; } -- cgit v1.2.1 From e95d43dfc5303dcb9e86359049b5f798e8351431 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 7 Nov 2016 16:58:50 +0000 Subject: Fixed up tests --- .../javascripts/protected_branches/protected_branch_dropdown.js.es6 | 2 +- spec/features/projects/ref_switcher_spec.rb | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 index e3f226e9a2a..9b551a1ed8c 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 @@ -48,7 +48,7 @@ class ProtectedBranchDropdown { onClickCreateWildcard() { // Refresh the dropdown's data, which ends up calling `getProtectedBranches` this.$dropdown.data('glDropdown').remote.execute(); - this.$dropdown.data('glDropdown').selectRowAtIndex(0); + this.$dropdown.data('glDropdown').selectRowAtIndex(gon.open_branches.length); } getProtectedBranches(term, callback) { diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index 472491188c9..38fe2d92885 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -17,14 +17,15 @@ feature 'Ref switcher', feature: true, js: true do page.within '.project-refs-form' do input = find('input[type="search"]') - input.set 'expand' + input.set 'binary' + wait_for_ajax input.native.send_keys :down input.native.send_keys :down input.native.send_keys :enter end - expect(page).to have_title 'expand-collapse-files' + expect(page).to have_title 'binary-encoding' end it "user selects ref with special characters" do -- cgit v1.2.1 From 50d58c14cd2cd1b8fb1bb9e4a7a1091b5af90c04 Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Mon, 21 Nov 2016 17:45:27 +0000 Subject: Fixed protected branches dropdown --- .../javascripts/protected_branches/protected_branch_dropdown.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 index 9b551a1ed8c..1ab72ee49e4 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 +++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 @@ -48,7 +48,7 @@ class ProtectedBranchDropdown { onClickCreateWildcard() { // Refresh the dropdown's data, which ends up calling `getProtectedBranches` this.$dropdown.data('glDropdown').remote.execute(); - this.$dropdown.data('glDropdown').selectRowAtIndex(gon.open_branches.length); + this.$dropdown.data('glDropdown').selectRowAtIndex(); } getProtectedBranches(term, callback) { -- cgit v1.2.1 From b82f415f09dd67da010a8c08397a13499e70efeb Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 22 Nov 2016 18:14:41 +0800 Subject: Move all branch creation to raw_ensure_branch, and keep it only called in update_branch_with_hooks. --- app/models/project.rb | 11 ----- app/models/repository.rb | 85 ++++++++++++++++++++++++++--------- app/services/create_branch_service.rb | 8 +--- app/services/files/base_service.rb | 13 +----- app/services/files/update_service.rb | 1 + 5 files changed, 69 insertions(+), 49 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 5346e18b051..f8a54324341 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -998,17 +998,6 @@ class Project < ActiveRecord::Base Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path) end - def fetch_ref(source_project, branch_name, ref) - repository.fetch_ref( - source_project.repository.path_to_repo, - "refs/heads/#{ref}", - "refs/heads/#{branch_name}" - ) - - repository.after_create_branch - repository.find_branch(branch_name) - end - # Expires various caches before a project is renamed. def expire_caches_before_rename(old_path) repo = Repository.new(old_path, self) diff --git a/app/models/repository.rb b/app/models/repository.rb index a029993db7f..9162f494a60 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -784,13 +784,16 @@ class Repository @tags ||= raw_repository.tags end + # rubocop:disable Metrics/ParameterLists def commit_dir( user, path, message, branch, - author_email: nil, author_name: nil, source_branch: nil) + author_email: nil, author_name: nil, + source_branch: nil, source_project: project) update_branch_with_hooks( user, branch, - source_branch: source_branch) do |ref| + source_branch: source_branch, + source_project: source_project) do |ref| options = { commit: { branch: ref, @@ -804,15 +807,18 @@ class Repository raw_repository.mkdir(path, options) end end + # rubocop:enable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists def commit_file( user, path, content, message, branch, update, - author_email: nil, author_name: nil, source_branch: nil) + author_email: nil, author_name: nil, + source_branch: nil, source_project: project) update_branch_with_hooks( user, branch, - source_branch: source_branch) do |ref| + source_branch: source_branch, + source_project: source_project) do |ref| options = { commit: { branch: ref, @@ -837,11 +843,13 @@ class Repository def update_file( user, path, content, branch:, previous_path:, message:, - author_email: nil, author_name: nil, source_branch: nil) + author_email: nil, author_name: nil, + source_branch: nil, source_project: project) update_branch_with_hooks( user, branch, - source_branch: source_branch) do |ref| + source_branch: source_branch, + source_project: source_project) do |ref| options = { commit: { branch: ref, @@ -867,13 +875,16 @@ class Repository end # rubocop:enable Metrics/ParameterLists + # rubocop:disable Metrics/ParameterLists def remove_file( user, path, message, branch, - author_email: nil, author_name: nil, source_branch: nil) + author_email: nil, author_name: nil, + source_branch: nil, source_project: project) update_branch_with_hooks( user, branch, - source_branch: source_branch) do |ref| + source_branch: source_branch, + source_project: source_project) do |ref| options = { commit: { branch: ref, @@ -890,14 +901,18 @@ class Repository Gitlab::Git::Blob.remove(raw_repository, options) end end + # rubocop:enable Metrics/ParameterLists + # rubocop:disable Metrics/ParameterLists def multi_action( user:, branch:, message:, actions:, - author_email: nil, author_name: nil, source_branch: nil) + author_email: nil, author_name: nil, + source_branch: nil, source_project: project) update_branch_with_hooks( user, branch, - source_branch: source_branch) do |ref| + source_branch: source_branch, + source_project: source_project) do |ref| index = rugged.index branch_commit = find_branch(ref) @@ -942,6 +957,7 @@ class Repository Rugged::Commit.create(rugged, options) end end + # rubocop:enable Metrics/ParameterLists def get_committer_and_author(user, email: nil, name: nil) committer = user_to_committer(user) @@ -991,8 +1007,6 @@ class Repository end def revert(user, commit, base_branch, revert_tree_id = nil) - source_sha = raw_ensure_branch(base_branch, source_commit: commit). - first.dereferenced_target.sha revert_tree_id ||= check_revert_content(commit, base_branch) return false unless revert_tree_id @@ -1000,8 +1014,11 @@ class Repository update_branch_with_hooks( user, base_branch, - source_branch: revert_tree_id) do + source_commit: commit) do + + source_sha = find_branch(base_branch).dereferenced_target.sha committer = user_to_committer(user) + Rugged::Commit.create(rugged, message: commit.revert_message, author: committer, @@ -1012,8 +1029,6 @@ class Repository end def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) - source_sha = raw_ensure_branch(base_branch, source_commit: commit). - first.dereferenced_target.sha cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) return false unless cherry_pick_tree_id @@ -1021,8 +1036,11 @@ class Repository update_branch_with_hooks( user, base_branch, - source_branch: cherry_pick_tree_id) do + source_commit: commit) do + + source_sha = find_branch(base_branch).dereferenced_target.sha committer = user_to_committer(user) + Rugged::Commit.create(rugged, message: commit.message, author: { @@ -1130,12 +1148,19 @@ class Repository # Whenever `source_branch` is passed, if `branch` doesn't exist, it would # be created from `source_branch`. - def update_branch_with_hooks(current_user, branch, source_branch: nil) + def update_branch_with_hooks( + current_user, branch, + source_branch: nil, source_commit: nil, source_project: project) update_autocrlf_option - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch target_branch, new_branch_added = - raw_ensure_branch(branch, source_branch: source_branch) + raw_ensure_branch( + branch, + source_branch: source_branch, + source_project: source_project + ) + + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch was_empty = empty? # Make commit @@ -1244,11 +1269,31 @@ class Repository Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags)) end - def raw_ensure_branch(branch_name, source_commit: nil, source_branch: nil) + def raw_ensure_branch( + branch_name, source_commit: nil, source_branch: nil, source_project: nil) old_branch = find_branch(branch_name) + if source_commit && source_branch + raise ArgumentError, + 'Should only pass either :source_branch or :source_commit, not both' + end + if old_branch [old_branch, false] + elsif project != source_project + unless source_branch + raise ArgumentError, + 'Should also pass :source_branch if' + + ' :source_project is different from current project' + end + + fetch_ref( + source_project.repository.path_to_repo, + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch}", + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch}" + ) + + [find_branch(branch_name), true] elsif source_commit || source_branch oldrev = Gitlab::Git::BLANK_SHA ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index ecb5994fab8..b2bc3626c0f 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -1,16 +1,12 @@ require_relative 'base_service' class CreateBranchService < BaseService - def execute(branch_name, ref, source_project: @project) + def execute(branch_name, ref) failure = validate_new_branch(branch_name) return failure if failure - new_branch = if source_project != @project - @project.fetch_ref(source_project, branch_name, ref) - else - repository.add_branch(current_user, branch_name, ref) - end + new_branch = repository.add_branch(current_user, branch_name, ref) if new_branch success(new_branch) diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 8689c83d40e..6779bd2818a 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -23,7 +23,7 @@ module Files validate # Create new branch if it different from source_branch - ensure_target_branch if different_branch? + validate_target_branch if different_branch? result = commit if result @@ -71,17 +71,6 @@ module Files end end - def ensure_target_branch - validate_target_branch - - if @source_project != project - # Make sure we have the source_branch in target project, - # and use source_branch as target_branch directly to avoid - # unnecessary source branch in target project. - @project.fetch_ref(@source_project, @target_branch, @source_branch) - end - end - def validate_target_branch result = ValidateNewBranchService.new(project, current_user). execute(@target_branch) diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index f3a766ed9fd..14e5af4d8c6 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -11,6 +11,7 @@ module Files message: @commit_message, author_email: @author_email, author_name: @author_name, + source_project: @source_project, source_branch: @source_branch) end -- cgit v1.2.1 From 6cd2256c84c9a785a6b15864ab170084fdebf45f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 24 Nov 2016 17:56:26 +0800 Subject: Should also pass source_commit to raw_ensure_branch --- app/models/repository.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/repository.rb b/app/models/repository.rb index 9162f494a60..276d8829873 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1157,6 +1157,7 @@ class Repository raw_ensure_branch( branch, source_branch: source_branch, + source_commit: source_commit, source_project: source_project ) -- cgit v1.2.1 From e866985be81f54002667117e03dae6680829a462 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 24 Nov 2016 23:35:28 +0800 Subject: Fix local name from branch to branch_name --- app/models/repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 276d8829873..6abe498ebde 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1291,7 +1291,7 @@ class Repository fetch_ref( source_project.repository.path_to_repo, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch}", - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch}" + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch_name}" ) [find_branch(branch_name), true] -- cgit v1.2.1 From c916f293cecf3d112f0338aec0a9dc9e3577f17e Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 25 Nov 2016 15:07:27 +0800 Subject: Add some explanation to Repository#update_branch_with_hooks --- app/models/repository.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 6abe498ebde..09e8cc060c7 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1146,8 +1146,11 @@ class Repository fetch_ref(path_to_repo, ref, ref_path) end - # Whenever `source_branch` is passed, if `branch` doesn't exist, it would - # be created from `source_branch`. + # Whenever `source_branch` or `source_commit` is passed, if `branch` + # doesn't exist, it would be created from `source_branch` or + # `source_commit`. Should only pass one of them, not both. + # If `source_project` is passed, and the branch doesn't exist, + # it would try to find the source from it instead of current repository. def update_branch_with_hooks( current_user, branch, source_branch: nil, source_commit: nil, source_project: project) -- cgit v1.2.1 From a52dc7cec70ef97b2755fb9cef7d6b489062310c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 6 Dec 2016 03:13:15 +0800 Subject: Introduce GitOperationService and wrap every git operation inside GitHooksService. Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_19210942 TODO: Fix tests for update_branch_with_hooks --- app/models/repository.rb | 174 +++++----------------------------- app/services/git_operation_service.rb | 168 ++++++++++++++++++++++++++++++++ spec/models/repository_spec.rb | 12 +-- 3 files changed, 197 insertions(+), 157 deletions(-) create mode 100644 app/services/git_operation_service.rb diff --git a/app/models/repository.rb b/app/models/repository.rb index 6bfa24f6329..491d2ab69b2 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -5,7 +5,7 @@ class Repository attr_accessor :path_with_namespace, :project - class CommitError < StandardError; end + CommitError = Class.new(StandardError) # Methods that cache data from the Git repository. # @@ -64,10 +64,6 @@ class Repository @raw_repository ||= Gitlab::Git::Repository.new(path_to_repo) end - def update_autocrlf_option - raw_repository.autocrlf = :input if raw_repository.autocrlf != :input - end - # Return absolute path to repository def path_to_repo @path_to_repo ||= File.expand_path( @@ -172,54 +168,39 @@ class Repository tags.find { |tag| tag.name == name } end - def add_branch(user, branch_name, target) - oldrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - target = commit(target).try(:id) + def add_branch(user, branch_name, ref) + newrev = commit(ref).try(:sha) - return false unless target + return false unless newrev - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do - update_ref!(ref, target, oldrev) - end + GitOperationService.new(user, self).add_branch(branch_name, newrev) after_create_branch find_branch(branch_name) end def add_tag(user, tag_name, target, message = nil) - oldrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::TAG_REF_PREFIX + tag_name - target = commit(target).try(:id) - - return false unless target - + newrev = commit(target).try(:id) options = { message: message, tagger: user_to_committer(user) } if message - GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do |service| - raw_tag = rugged.tags.create(tag_name, target, options) - service.newrev = raw_tag.target_id - end + return false unless newrev + + GitOperationService.new(user, self).add_tag(tag_name, newrev, options) find_tag(tag_name) end def rm_branch(user, branch_name) before_remove_branch - branch = find_branch(branch_name) - oldrev = branch.try(:dereferenced_target).try(:id) - newrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do - update_ref!(ref, newrev, oldrev) - end + GitOperationService.new(user, self).rm_branch(branch) after_remove_branch true end + # TODO: why we don't pass user here? def rm_tag(tag_name) before_remove_tag @@ -245,21 +226,6 @@ class Repository false end - def update_ref!(name, newrev, oldrev) - # We use 'git update-ref' because libgit2/rugged currently does not - # offer 'compare and swap' ref updates. Without compare-and-swap we can - # (and have!) accidentally reset the ref to an earlier state, clobbering - # commits. See also https://github.com/libgit2/libgit2/issues/1534. - command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z) - _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin| - stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00") - end - - return if status.zero? - - raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.") - end - # Makes sure a commit is kept around when Git garbage collection runs. # Git GC will delete commits from the repository that are no longer in any # branches or tags, but we want to keep some of these commits around, for @@ -783,8 +749,7 @@ class Repository user, path, message, branch, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( branch, source_branch: source_branch, source_project: source_project) do |ref| @@ -808,8 +773,7 @@ class Repository user, path, content, message, branch, update, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( branch, source_branch: source_branch, source_project: source_project) do |ref| @@ -839,8 +803,7 @@ class Repository branch:, previous_path:, message:, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( branch, source_branch: source_branch, source_project: source_project) do |ref| @@ -874,8 +837,7 @@ class Repository user, path, message, branch, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( branch, source_branch: source_branch, source_project: source_project) do |ref| @@ -902,8 +864,7 @@ class Repository user:, branch:, message:, actions:, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( branch, source_branch: source_branch, source_project: source_project) do |ref| @@ -964,7 +925,7 @@ class Repository end def user_to_committer(user) - Gitlab::Git::committer_hash(email: user.email, name: user.name) + Gitlab::Git.committer_hash(email: user.email, name: user.name) end def can_be_merged?(source_sha, target_branch) @@ -988,7 +949,8 @@ class Repository merge_index = rugged.merge_commits(our_commit, their_commit) return false if merge_index.conflicts? - update_branch_with_hooks(user, merge_request.target_branch) do + GitOperationService.new(user, self).with_branch( + merge_request.target_branch) do actual_options = options.merge( parents: [our_commit, their_commit], tree: merge_index.write_tree(rugged), @@ -1005,8 +967,7 @@ class Repository return false unless revert_tree_id - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( base_branch, source_commit: commit) do @@ -1027,8 +988,7 @@ class Repository return false unless cherry_pick_tree_id - update_branch_with_hooks( - user, + GitOperationService.new(user, self).with_branch( base_branch, source_commit: commit) do @@ -1048,8 +1008,8 @@ class Repository end end - def resolve_conflicts(user, branch, params) - update_branch_with_hooks(user, branch) do + def resolve_conflicts(user, branch_name, params) + GitOperationService.new(user, self).with_branch(branch_name) do committer = user_to_committer(user) Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer)) @@ -1140,51 +1100,6 @@ class Repository fetch_ref(path_to_repo, ref, ref_path) end - # Whenever `source_branch` or `source_commit` is passed, if `branch` - # doesn't exist, it would be created from `source_branch` or - # `source_commit`. Should only pass one of them, not both. - # If `source_project` is passed, and the branch doesn't exist, - # it would try to find the source from it instead of current repository. - def update_branch_with_hooks( - current_user, branch, - source_branch: nil, source_commit: nil, source_project: project) - update_autocrlf_option - - target_branch, new_branch_added = - raw_ensure_branch( - branch, - source_branch: source_branch, - source_commit: source_commit, - source_project: source_project - ) - - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch - was_empty = empty? - - # Make commit - newrev = yield(ref) - - unless newrev - raise CommitError.new('Failed to create commit') - end - - if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil? - oldrev = Gitlab::Git::BLANK_SHA - else - oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha) - end - - GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do - update_ref!(ref, newrev, oldrev) - - # If repo was empty expire cache - after_create if was_empty - after_create_branch if was_empty || new_branch_added - end - - newrev - end - def ls_files(ref) actual_ref = ref || root_ref raw_repository.ls_files(actual_ref) @@ -1266,47 +1181,4 @@ class Repository def repository_event(event, tags = {}) Gitlab::Metrics.add_event(event, { path: path_with_namespace }.merge(tags)) end - - def raw_ensure_branch( - branch_name, source_commit: nil, source_branch: nil, source_project: nil) - old_branch = find_branch(branch_name) - - if source_commit && source_branch - raise ArgumentError, - 'Should only pass either :source_branch or :source_commit, not both' - end - - if old_branch - [old_branch, false] - elsif project != source_project - unless source_branch - raise ArgumentError, - 'Should also pass :source_branch if' + - ' :source_project is different from current project' - end - - fetch_ref( - source_project.repository.path_to_repo, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch}", - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch_name}" - ) - - [find_branch(branch_name), true] - elsif source_commit || source_branch - oldrev = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - target = (source_commit || commit(source_branch)).try(:sha) - - unless target - raise CommitError.new( - "Cannot find branch #{branch_name} nor #{source_commit.try(:sha) || source_branch}") - end - - update_ref!(ref, target, oldrev) - - [find_branch(branch_name), true] - else - [nil, true] # Empty branch - end - end end diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb new file mode 100644 index 00000000000..88175c6931d --- /dev/null +++ b/app/services/git_operation_service.rb @@ -0,0 +1,168 @@ + +GitOperationService = Struct.new(:user, :repository) do + def add_branch(branch_name, newrev) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + oldrev = Gitlab::Git::BLANK_SHA + + with_hooks_and_update_ref(ref, oldrev, newrev) + end + + def rm_branch(branch) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name + oldrev = branch.dereferenced_target.id + newrev = Gitlab::Git::BLANK_SHA + + with_hooks_and_update_ref(ref, oldrev, newrev) + end + + def add_tag(tag_name, newrev, options = {}) + ref = Gitlab::Git::TAG_REF_PREFIX + tag_name + oldrev = Gitlab::Git::BLANK_SHA + + with_hooks(ref, oldrev, newrev) do |service| + raw_tag = repository.rugged.tags.create(tag_name, newrev, options) + service.newrev = raw_tag.target_id + end + end + + # Whenever `source_branch` or `source_commit` is passed, if `branch` + # doesn't exist, it would be created from `source_branch` or + # `source_commit`. Should only pass one of them, not both. + # If `source_project` is passed, and the branch doesn't exist, + # it would try to find the source from it instead of current repository. + def with_branch( + branch_name, + source_branch: nil, + source_commit: nil, + source_project: repository.project) + + if source_commit && source_branch + raise ArgumentError, 'Should pass only :source_branch or :source_commit' + end + + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + oldrev = Gitlab::Git::BLANK_SHA + + if repository.branch_exists?(branch_name) + oldrev = newrev = repository.commit(branch_name).sha + + elsif repository.project != source_project + unless source_branch + raise ArgumentError, + 'Should also pass :source_branch if' + + ' :source_project is different from current project' + end + + newrev = source_project.repository.commit(source_branch).try(:sha) + + unless newrev + raise Repository::CommitError.new( + "Cannot find branch #{branch_name} nor" \ + " #{source_branch} from" \ + " #{source_project.path_with_namespace}") + end + + elsif source_commit || source_branch + newrev = (source_commit || repository.commit(source_branch)).try(:sha) + + unless newrev + raise Repository::CommitError.new( + "Cannot find branch #{branch_name} nor" \ + " #{source_commit.try(:sha) || source_branch} from" \ + " #{repository.project.path_with_namespace}") + end + + else # we want an orphan empty branch + newrev = Gitlab::Git::BLANK_SHA + end + + commit_with_hooks(ref, oldrev, newrev) do + if repository.project != source_project + repository.fetch_ref( + source_project.repository.path_to_repo, + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch}", + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch_name}" + ) + end + + yield(ref) + end + end + + private + + def commit_with_hooks(ref, oldrev, newrev) + with_hooks_and_update_ref(ref, oldrev, newrev) do |service| + was_empty = repository.empty? + + # Make commit + nextrev = yield(ref) + + unless nextrev + raise Repository::CommitError.new('Failed to create commit') + end + + service.newrev = nextrev + + update_ref!(ref, nextrev, newrev) + + # If repo was empty expire cache + repository.after_create if was_empty + repository.after_create_branch if was_empty || + oldrev == Gitlab::Git::BLANK_SHA + + nextrev + end + end + + def with_hooks_and_update_ref(ref, oldrev, newrev) + with_hooks(ref, oldrev, newrev) do |service| + update_ref!(ref, newrev, oldrev) + + yield(service) if block_given? + end + end + + def with_hooks(ref, oldrev, newrev) + update_autocrlf_option + + result = nil + + GitHooksService.new.execute( + user, + repository.path_to_repo, + oldrev, + newrev, + ref) do |service| + + result = yield(service) if block_given? + end + + result + end + + def update_ref!(name, newrev, oldrev) + # We use 'git update-ref' because libgit2/rugged currently does not + # offer 'compare and swap' ref updates. Without compare-and-swap we can + # (and have!) accidentally reset the ref to an earlier state, clobbering + # commits. See also https://github.com/libgit2/libgit2/issues/1534. + command = %W[#{Gitlab.config.git.bin_path} update-ref --stdin -z] + _, status = Gitlab::Popen.popen( + command, + repository.path_to_repo) do |stdin| + stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00") + end + + unless status.zero? + raise Repository::CommitError.new( + "Could not update branch #{name.sub('refs/heads/', '')}." \ + " Please refresh and try again.") + end + end + + def update_autocrlf_option + if repository.raw_repository.autocrlf != :input + repository.raw_repository.autocrlf = :input + end + end +end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index b797d19161d..3ce3c4dec2a 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -667,7 +667,7 @@ describe Repository, models: true do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - repository.rm_branch(user, 'new_feature') + repository.rm_branch(user, 'feature') end.to raise_error(GitHooksService::PreReceiveError) end @@ -682,7 +682,7 @@ describe Repository, models: true do end end - describe '#update_branch_with_hooks' do + xdescribe '#update_branch_with_hooks' do let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev @@ -848,7 +848,7 @@ describe Repository, models: true do end it 'sets autocrlf to :input' do - repository.update_autocrlf_option + GitOperationService.new(nil, repository).send(:update_autocrlf_option) expect(repository.raw_repository.autocrlf).to eq(:input) end @@ -863,7 +863,7 @@ describe Repository, models: true do expect(repository.raw_repository).not_to receive(:autocrlf=). with(:input) - repository.update_autocrlf_option + GitOperationService.new(nil, repository).send(:update_autocrlf_option) end end end @@ -1429,14 +1429,14 @@ describe Repository, models: true do describe '#update_ref!' do it 'can create a ref' do - repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + GitOperationService.new(nil, repository).send(:update_ref!, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) expect(repository.find_branch('foobar')).not_to be_nil end it 'raises CommitError when the ref update fails' do expect do - repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + GitOperationService.new(nil, repository).send(:update_ref!, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) end.to raise_error(Repository::CommitError) end end -- cgit v1.2.1 From 444da6f47ed77172471a27386b969e6401d7cf84 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 6 Dec 2016 15:20:50 +0800 Subject: Fix update_ref! call in the test --- spec/workers/git_garbage_collect_worker_spec.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index e471a68a49a..2b31efbf631 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -107,7 +107,8 @@ describe GitGarbageCollectWorker do tree: old_commit.tree, parents: [old_commit], ) - project.repository.update_ref!( + GitOperationService.new(nil, project.repository).send( + :update_ref!, "refs/heads/#{SecureRandom.hex(6)}", new_commit_sha, Gitlab::Git::BLANK_SHA -- cgit v1.2.1 From 65806ec632f2ea1e2087b7cdc64f13e6db49c88a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 6 Dec 2016 18:47:24 +0800 Subject: Re-enable tests for update_branch_with_hooks and Add back check if we're losing commits in a merge. --- app/services/git_operation_service.rb | 15 +++++++++-- spec/models/repository_spec.rb | 50 ++++++++++++++++++++++++++--------- 2 files changed, 51 insertions(+), 14 deletions(-) diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 88175c6931d..c34d4bde150 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -102,9 +102,20 @@ GitOperationService = Struct.new(:user, :repository) do raise Repository::CommitError.new('Failed to create commit') end - service.newrev = nextrev + branch = + repository.find_branch(ref[Gitlab::Git::BRANCH_REF_PREFIX.size..-1]) + + prevrev = if branch && + !repository.rugged.lookup(nextrev).parent_ids.empty? + repository.rugged.merge_base( + nextrev, branch.dereferenced_target.sha) + else + newrev + end - update_ref!(ref, nextrev, newrev) + update_ref!(ref, nextrev, prevrev) + + service.newrev = nextrev # If repo was empty expire cache repository.after_create if was_empty diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 3ce3c4dec2a..e3ec4c85a74 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -682,33 +682,48 @@ describe Repository, models: true do end end - xdescribe '#update_branch_with_hooks' do + describe '#update_branch_with_hooks' do let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev context 'when pre hooks were successful' do before do - expect_any_instance_of(GitHooksService).to receive(:execute). - with(user, repository.path_to_repo, old_rev, new_rev, 'refs/heads/feature'). - and_yield.and_return(true) + service = GitHooksService.new + expect(GitHooksService).to receive(:new).and_return(service) + expect(service).to receive(:execute). + with( + user, + repository.path_to_repo, + old_rev, + old_rev, + 'refs/heads/feature'). + and_yield(service).and_return(true) end it 'runs without errors' do expect do - repository.update_branch_with_hooks(user, 'feature') { new_rev } + GitOperationService.new(user, repository).with_branch('feature') do + new_rev + end end.not_to raise_error end it 'ensures the autocrlf Git option is set to :input' do - expect(repository).to receive(:update_autocrlf_option) + service = GitOperationService.new(user, repository) + + expect(service).to receive(:update_autocrlf_option) - repository.update_branch_with_hooks(user, 'feature') { new_rev } + service.with_branch('feature') { new_rev } end context "when the branch wasn't empty" do it 'updates the head' do expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev) - repository.update_branch_with_hooks(user, 'feature') { new_rev } + + GitOperationService.new(user, repository).with_branch('feature') do + new_rev + end + expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev) end end @@ -727,7 +742,11 @@ describe Repository, models: true do branch = 'feature-ff-target' repository.add_branch(user, branch, old_rev) - expect { repository.update_branch_with_hooks(user, branch) { new_rev } }.not_to raise_error + expect do + GitOperationService.new(user, repository).with_branch(branch) do + new_rev + end + end.not_to raise_error end end @@ -742,7 +761,9 @@ describe Repository, models: true do # Updating 'master' to new_rev would lose the commits on 'master' that # are not contained in new_rev. This should not be allowed. expect do - repository.update_branch_with_hooks(user, branch) { new_rev } + GitOperationService.new(user, repository).with_branch(branch) do + new_rev + end end.to raise_error(Repository::CommitError) end end @@ -752,7 +773,9 @@ describe Repository, models: true do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) expect do - repository.update_branch_with_hooks(user, 'feature') { new_rev } + GitOperationService.new(user, repository).with_branch('feature') do + new_rev + end end.to raise_error(GitHooksService::PreReceiveError) end end @@ -770,7 +793,10 @@ describe Repository, models: true do expect(repository).to receive(:expire_branches_cache) expect(repository).to receive(:expire_has_visible_content_cache) - repository.update_branch_with_hooks(user, 'new-feature') { new_rev } + GitOperationService.new(user, repository). + with_branch('new-feature') do + new_rev + end end end -- cgit v1.2.1 From 6ae1a73cfdad4b98176bb99846042d4378119de2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 7 Dec 2016 19:50:08 +0800 Subject: Pass source_branch properly for cherry-pick/revert Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237/diffs#note_19210818 --- app/models/repository.rb | 12 ++++++++---- app/services/commits/change_service.rb | 9 ++++++++- app/services/git_operation_service.rb | 16 +++++----------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 491d2ab69b2..36cbb0d051e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -962,14 +962,16 @@ class Repository end end - def revert(user, commit, base_branch, revert_tree_id = nil) + def revert( + user, commit, base_branch, revert_tree_id = nil, + source_branch: nil, source_project: project) revert_tree_id ||= check_revert_content(commit, base_branch) return false unless revert_tree_id GitOperationService.new(user, self).with_branch( base_branch, - source_commit: commit) do + source_branch: source_branch, source_project: source_project) do source_sha = find_branch(base_branch).dereferenced_target.sha committer = user_to_committer(user) @@ -983,14 +985,16 @@ class Repository end end - def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil) + def cherry_pick( + user, commit, base_branch, cherry_pick_tree_id = nil, + source_branch: nil, source_project: project) cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) return false unless cherry_pick_tree_id GitOperationService.new(user, self).with_branch( base_branch, - source_commit: commit) do + source_branch: source_branch, source_project: source_project) do source_sha = find_branch(base_branch).dereferenced_target.sha committer = user_to_committer(user) diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 04b28cfaed8..5458f7a6790 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -31,7 +31,14 @@ module Commits if tree_id validate_target_branch(into) if @create_merge_request - repository.public_send(action, current_user, @commit, into, tree_id) + repository.public_send( + action, + current_user, + @commit, + into, + tree_id, + source_branch: @target_branch) + success else error_msg = "Sorry, we cannot #{action.to_s.dasherize} this #{@commit.change_type_title} automatically. diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index c34d4bde150..c9e2c21737a 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -25,21 +25,15 @@ GitOperationService = Struct.new(:user, :repository) do end end - # Whenever `source_branch` or `source_commit` is passed, if `branch` - # doesn't exist, it would be created from `source_branch` or - # `source_commit`. Should only pass one of them, not both. + # Whenever `source_branch` is passed, if `branch` doesn't exist, + # it would be created from `source_branch`. # If `source_project` is passed, and the branch doesn't exist, # it would try to find the source from it instead of current repository. def with_branch( branch_name, source_branch: nil, - source_commit: nil, source_project: repository.project) - if source_commit && source_branch - raise ArgumentError, 'Should pass only :source_branch or :source_commit' - end - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name oldrev = Gitlab::Git::BLANK_SHA @@ -62,13 +56,13 @@ GitOperationService = Struct.new(:user, :repository) do " #{source_project.path_with_namespace}") end - elsif source_commit || source_branch - newrev = (source_commit || repository.commit(source_branch)).try(:sha) + elsif source_branch + newrev = repository.commit(source_branch).try(:sha) unless newrev raise Repository::CommitError.new( "Cannot find branch #{branch_name} nor" \ - " #{source_commit.try(:sha) || source_branch} from" \ + " #{source_branch} from" \ " #{repository.project.path_with_namespace}") end -- cgit v1.2.1 From 5ecd0c81af85476c2328d3836cc68b17ebd5a8a6 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 7 Dec 2016 22:37:43 +0800 Subject: Commit outside the hooks if possible: So we still commit outside the hooks, and only update ref inside the hooks. There are only two exceptions: * Whenever it's adding a tag. We can't add a tag without committing, unfortunately. See !7700 * Whenever source project is in another repository. We'll need to fetch ref otherwise commits can't be made. See the whole discussion starting from: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_19210942 --- app/models/repository.rb | 16 ++++- app/services/git_operation_service.rb | 111 +++++++++++++++------------------- spec/models/repository_spec.rb | 2 +- 3 files changed, 65 insertions(+), 64 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 36cbb0d051e..9393d6b461e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -973,7 +973,8 @@ class Repository base_branch, source_branch: source_branch, source_project: source_project) do - source_sha = find_branch(base_branch).dereferenced_target.sha + source_sha = source_project.repository.find_source_sha( + source_branch || base_branch) committer = user_to_committer(user) Rugged::Commit.create(rugged, @@ -996,7 +997,8 @@ class Repository base_branch, source_branch: source_branch, source_project: source_project) do - source_sha = find_branch(base_branch).dereferenced_target.sha + source_sha = source_project.repository.find_source_sha( + source_branch || base_branch) committer = user_to_committer(user) Rugged::Commit.create(rugged, @@ -1162,6 +1164,16 @@ class Repository end end + protected + + def find_source_sha(branch_name) + if branch_exists?(branch_name) + find_branch(branch_name).dereferenced_target.sha + else + Gitlab::Git::BLANK_SHA + end + end + private def refs_directory_exists? diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index c9e2c21737a..3a102a9276b 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -34,43 +34,10 @@ GitOperationService = Struct.new(:user, :repository) do source_branch: nil, source_project: repository.project) - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - oldrev = Gitlab::Git::BLANK_SHA + check_with_branch_arguments!(branch_name, source_branch, source_project) - if repository.branch_exists?(branch_name) - oldrev = newrev = repository.commit(branch_name).sha - - elsif repository.project != source_project - unless source_branch - raise ArgumentError, - 'Should also pass :source_branch if' + - ' :source_project is different from current project' - end - - newrev = source_project.repository.commit(source_branch).try(:sha) - - unless newrev - raise Repository::CommitError.new( - "Cannot find branch #{branch_name} nor" \ - " #{source_branch} from" \ - " #{source_project.path_with_namespace}") - end - - elsif source_branch - newrev = repository.commit(source_branch).try(:sha) - - unless newrev - raise Repository::CommitError.new( - "Cannot find branch #{branch_name} nor" \ - " #{source_branch} from" \ - " #{repository.project.path_with_namespace}") - end - - else # we want an orphan empty branch - newrev = Gitlab::Git::BLANK_SHA - end - - commit_with_hooks(ref, oldrev, newrev) do + update_branch_with_hooks( + branch_name, source_branch, source_project) do |ref| if repository.project != source_project repository.fetch_ref( source_project.repository.path_to_repo, @@ -85,39 +52,37 @@ GitOperationService = Struct.new(:user, :repository) do private - def commit_with_hooks(ref, oldrev, newrev) - with_hooks_and_update_ref(ref, oldrev, newrev) do |service| - was_empty = repository.empty? - - # Make commit - nextrev = yield(ref) - - unless nextrev - raise Repository::CommitError.new('Failed to create commit') - end + def update_branch_with_hooks(branch_name, source_branch, source_project) + update_autocrlf_option - branch = - repository.find_branch(ref[Gitlab::Git::BRANCH_REF_PREFIX.size..-1]) + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name + oldrev = Gitlab::Git::BLANK_SHA + was_empty = repository.empty? - prevrev = if branch && - !repository.rugged.lookup(nextrev).parent_ids.empty? - repository.rugged.merge_base( - nextrev, branch.dereferenced_target.sha) - else - newrev - end + # Make commit + newrev = yield(ref) - update_ref!(ref, nextrev, prevrev) + unless newrev + raise Repository::CommitError.new('Failed to create commit') + end - service.newrev = nextrev + branch = repository.find_branch(branch_name) + oldrev = if repository.rugged.lookup(newrev).parent_ids.empty? || + branch.nil? + Gitlab::Git::BLANK_SHA + else + repository.rugged.merge_base( + newrev, branch.dereferenced_target.sha) + end + with_hooks_and_update_ref(ref, oldrev, newrev) do # If repo was empty expire cache repository.after_create if was_empty repository.after_create_branch if was_empty || oldrev == Gitlab::Git::BLANK_SHA - - nextrev end + + newrev end def with_hooks_and_update_ref(ref, oldrev, newrev) @@ -129,8 +94,6 @@ GitOperationService = Struct.new(:user, :repository) do end def with_hooks(ref, oldrev, newrev) - update_autocrlf_option - result = nil GitHooksService.new.execute( @@ -170,4 +133,30 @@ GitOperationService = Struct.new(:user, :repository) do repository.raw_repository.autocrlf = :input end end + + def check_with_branch_arguments!(branch_name, source_branch, source_project) + return if repository.branch_exists?(branch_name) + + if repository.project != source_project + unless source_branch + raise ArgumentError, + 'Should also pass :source_branch if' + + ' :source_project is different from current project' + end + + unless source_project.repository.commit(source_branch).try(:sha) + raise Repository::CommitError.new( + "Cannot find branch #{branch_name} nor" \ + " #{source_branch} from" \ + " #{source_project.path_with_namespace}") + end + elsif source_branch + unless repository.commit(source_branch).try(:sha) + raise Repository::CommitError.new( + "Cannot find branch #{branch_name} nor" \ + " #{source_branch} from" \ + " #{repository.project.path_with_namespace}") + end + end + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index e3ec4c85a74..ebd05c946cc 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -695,7 +695,7 @@ describe Repository, models: true do user, repository.path_to_repo, old_rev, - old_rev, + new_rev, 'refs/heads/feature'). and_yield(service).and_return(true) end -- cgit v1.2.1 From 5ba468efde94ed823aaccabf2405e63cecfbf9d6 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 7 Dec 2016 23:03:58 +0800 Subject: Not sure why rubocop prefers this but anyway --- app/models/repository.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 9393d6b461e..038ab5e104a 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -974,7 +974,7 @@ class Repository source_branch: source_branch, source_project: source_project) do source_sha = source_project.repository.find_source_sha( - source_branch || base_branch) + source_branch || base_branch) committer = user_to_committer(user) Rugged::Commit.create(rugged, @@ -998,7 +998,7 @@ class Repository source_branch: source_branch, source_project: source_project) do source_sha = source_project.repository.find_source_sha( - source_branch || base_branch) + source_branch || base_branch) committer = user_to_committer(user) Rugged::Commit.create(rugged, -- cgit v1.2.1 From fff3c5262857ee8c8dbf4ba7159032f836eba1bd Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 7 Dec 2016 23:33:33 +0800 Subject: Use multi_action to commit which doesn't need to have the branch existed upfront. That is, `Rugged::Commit.create` rather than `Gitlab::Git::Blob.commit` which the former doesn't need to have the branch but the latter needs. --- app/models/repository.rb | 99 ++++++++++++++++++------------------------------ 1 file changed, 36 insertions(+), 63 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 038ab5e104a..c05cfb271c7 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -773,27 +773,17 @@ class Repository user, path, content, message, branch, update, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - GitOperationService.new(user, self).with_branch( - branch, + multi_action( + user: user, + branch: branch, + message: message, + author_email: author_email, + author_name: author_name, source_branch: source_branch, - source_project: source_project) do |ref| - options = { - commit: { - branch: ref, - message: message, - update_ref: false - }, - file: { - content: content, - path: path, - update: update - } - } - - options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) - - Gitlab::Git::Blob.commit(raw_repository, options) - end + source_project: source_project, + actions: [{action: :create, + file_path: path, + content: content}]) end # rubocop:enable Metrics/ParameterLists @@ -803,32 +793,24 @@ class Repository branch:, previous_path:, message:, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - GitOperationService.new(user, self).with_branch( - branch, + action = if previous_path && previous_path != path + :move + else + :update + end + + multi_action( + user: user, + branch: branch, + message: message, + author_email: author_email, + author_name: author_name, source_branch: source_branch, - source_project: source_project) do |ref| - options = { - commit: { - branch: ref, - message: message, - update_ref: false - }, - file: { - content: content, - path: path, - update: true - } - } - - options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) - - if previous_path && previous_path != path - options[:file][:previous_path] = previous_path - Gitlab::Git::Blob.rename(raw_repository, options) - else - Gitlab::Git::Blob.commit(raw_repository, options) - end - end + source_project: source_project, + actions: [{action: action, + file_path: path, + content: content, + previous_path: previous_path}]) end # rubocop:enable Metrics/ParameterLists @@ -837,25 +819,16 @@ class Repository user, path, message, branch, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - GitOperationService.new(user, self).with_branch( - branch, + multi_action( + user: user, + branch: branch, + message: message, + author_email: author_email, + author_name: author_name, source_branch: source_branch, - source_project: source_project) do |ref| - options = { - commit: { - branch: ref, - message: message, - update_ref: false - }, - file: { - path: path - } - } - - options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) - - Gitlab::Git::Blob.remove(raw_repository, options) - end + source_project: source_project, + actions: [{action: :delete, + file_path: path}]) end # rubocop:enable Metrics/ParameterLists -- cgit v1.2.1 From a51f26e5142d6c5f40984cd374f0dea7580a4235 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 01:12:49 +0800 Subject: Use commit_file for commit_dir --- app/models/repository.rb | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index c05cfb271c7..6246630300c 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -749,22 +749,32 @@ class Repository user, path, message, branch, author_email: nil, author_name: nil, source_branch: nil, source_project: project) - GitOperationService.new(user, self).with_branch( + if branch_exists?(branch) + # tree_entry is private + entry = raw_repository.send(:tree_entry, commit(branch), path) + + if entry + if entry[:type] == :blob + raise Gitlab::Git::Repository::InvalidBlobName.new( + "Directory already exists as a file") + else + raise Gitlab::Git::Repository::InvalidBlobName.new( + "Directory already exists") + end + end + end + + commit_file( + user, + "#{Gitlab::Git::PathHelper.normalize_path(path)}/.gitkeep", + '', + message, branch, + false, + author_email: author_email, + author_name: author_name, source_branch: source_branch, - source_project: source_project) do |ref| - options = { - commit: { - branch: ref, - message: message, - update_ref: false - } - } - - options.merge!(get_committer_and_author(user, email: author_email, name: author_name)) - - raw_repository.mkdir(path, options) - end + source_project: source_project) end # rubocop:enable Metrics/ParameterLists -- cgit v1.2.1 From e76173038b03c660a498d9f38b07797b453f1e7f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 01:13:16 +0800 Subject: Restore the check for update in commit_file --- app/models/repository.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/models/repository.rb b/app/models/repository.rb index 6246630300c..8a94fbf3ecc 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -783,6 +783,14 @@ class Repository user, path, content, message, branch, update, author_email: nil, author_name: nil, source_branch: nil, source_project: project) + if branch_exists?(branch) && update == false + # tree_entry is private + if raw_repository.send(:tree_entry, commit(branch), path) + raise Gitlab::Git::Repository::InvalidBlobName.new( + "Filename already exists; update not allowed") + end + end + multi_action( user: user, branch: branch, -- cgit v1.2.1 From 4b3c18ce5cbd88156423d89f26b0869f45e2225e Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 01:13:33 +0800 Subject: Use branch_name to find the branch rather than ref --- app/models/repository.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 8a94fbf3ecc..4d74ac6f585 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -858,9 +858,9 @@ class Repository GitOperationService.new(user, self).with_branch( branch, source_branch: source_branch, - source_project: source_project) do |ref| + source_project: source_project) do index = rugged.index - branch_commit = find_branch(ref) + branch_commit = find_branch(branch) parents = if branch_commit last_commit = branch_commit.dereferenced_target -- cgit v1.2.1 From e36088dd98c1c00a8884684158d47f479940346e Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 01:30:10 +0800 Subject: We need to normalize the path for all actions --- app/models/repository.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 4d74ac6f585..cf2c08f618e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -766,7 +766,7 @@ class Repository commit_file( user, - "#{Gitlab::Git::PathHelper.normalize_path(path)}/.gitkeep", + "#{path}/.gitkeep", '', message, branch, @@ -871,12 +871,14 @@ class Repository end actions.each do |action| + path = Gitlab::Git::PathHelper.normalize_path(action[:file_path]).to_s + case action[:action] when :create, :update, :move mode = case action[:action] when :update - index.get(action[:file_path])[:mode] + index.get(path)[:mode] when :move index.get(action[:previous_path])[:mode] end @@ -887,9 +889,9 @@ class Repository content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content] oid = rugged.write(content, :blob) - index.add(path: action[:file_path], oid: oid, mode: mode) + index.add(path: path, oid: oid, mode: mode) when :delete - index.remove(action[:file_path]) + index.remove(path) end end -- cgit v1.2.1 From 56e0dcab97f43e14ae88a66031e3cd71cd062b23 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 02:15:09 +0800 Subject: Spaces around hash braces --- app/models/repository.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index cf2c08f618e..4d350f937a6 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -799,9 +799,9 @@ class Repository author_name: author_name, source_branch: source_branch, source_project: source_project, - actions: [{action: :create, - file_path: path, - content: content}]) + actions: [{ action: :create, + file_path: path, + content: content }]) end # rubocop:enable Metrics/ParameterLists @@ -825,10 +825,10 @@ class Repository author_name: author_name, source_branch: source_branch, source_project: source_project, - actions: [{action: action, - file_path: path, - content: content, - previous_path: previous_path}]) + actions: [{ action: action, + file_path: path, + content: content, + previous_path: previous_path }]) end # rubocop:enable Metrics/ParameterLists @@ -845,8 +845,8 @@ class Repository author_name: author_name, source_branch: source_branch, source_project: source_project, - actions: [{action: :delete, - file_path: path}]) + actions: [{ action: :delete, + file_path: path }]) end # rubocop:enable Metrics/ParameterLists -- cgit v1.2.1 From cdb598f3973bb7643d8d7f85f6109d84aea759ec Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 14:15:50 +0800 Subject: We probably don't need this anymore, not sure why --- app/controllers/concerns/creates_commit.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index e5c40446314..dacb5679dd3 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,10 +4,9 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables - source_branch = @ref if @ref && @repository.branch_exists?(@ref) commit_params = @commit_params.merge( source_project: @project, - source_branch: source_branch, + source_branch: @ref, target_branch: @target_branch ) -- cgit v1.2.1 From ae5b935b2888f7721e424cf41e2963e1483d8bb5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 14:16:06 +0800 Subject: find the commit properly and replicate gitlab_git by checking filename as well --- app/models/repository.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 4d350f937a6..50f347b58c8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -860,7 +860,8 @@ class Repository source_branch: source_branch, source_project: source_project) do index = rugged.index - branch_commit = find_branch(branch) + branch_commit = source_project.repository.find_branch( + source_branch || branch) parents = if branch_commit last_commit = branch_commit.dereferenced_target @@ -873,6 +874,9 @@ class Repository actions.each do |action| path = Gitlab::Git::PathHelper.normalize_path(action[:file_path]).to_s + raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if + path.split('/').include?('..') + case action[:action] when :create, :update, :move mode = -- cgit v1.2.1 From 4f94053c21196b3445c117130e3556463e4ca1d0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 15:22:32 +0800 Subject: We still need it for empty repo for web UI! We shouldn't pass a non-existing branch to source_branch. Checkout test for: * spec/features/tags/master_views_tags_spec.rb:24 * spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb:13 --- app/controllers/concerns/creates_commit.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index dacb5679dd3..5d11f286e9a 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,9 +4,10 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables + source_branch = @ref if @ref && @repository.find_branch(@ref) commit_params = @commit_params.merge( source_project: @project, - source_branch: @ref, + source_branch: source_branch, target_branch: @target_branch ) -- cgit v1.2.1 From cf677378ee8104e7505cf0670ad45a51613af575 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 15:23:41 +0800 Subject: Prefer repository.branch_exists? --- app/services/files/base_service.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 6779bd2818a..80e1d1d60f2 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -5,7 +5,7 @@ module Files def execute @source_project = params[:source_project] || @project @source_branch = params[:source_branch] - @target_branch = params[:target_branch] + @target_branch = params[:target_branch] @commit_message = params[:commit_message] @file_path = params[:file_path] @@ -59,12 +59,12 @@ module Files end unless project.empty_repo? - unless @source_project.repository.branch_names.include?(@source_branch) + unless @source_project.repository.branch_exists?(@source_branch) raise_error('You can only create or edit files when you are on a branch') end if different_branch? - if repository.branch_names.include?(@target_branch) + if repository.branch_exists?(@target_branch) raise_error('Branch with such name already exists. You need to switch to this branch in order to make changes') end end -- cgit v1.2.1 From 691f1c496834078ba41209597558259d20790a0b Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 15:31:42 +0800 Subject: Simply give result if result[:status] == :error --- app/services/create_branch_service.rb | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 076f976ed06..1b5e504573a 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -1,8 +1,9 @@ class CreateBranchService < BaseService def execute(branch_name, ref) - failure = validate_new_branch(branch_name) + result = ValidateNewBranchService.new(project, current_user). + execute(branch_name) - return failure if failure + return result if result[:status] == :error new_branch = repository.add_branch(current_user, branch_name, ref) @@ -18,13 +19,4 @@ class CreateBranchService < BaseService def success(branch) super().merge(branch: branch) end - - private - - def validate_new_branch(branch_name) - result = ValidateNewBranchService.new(project, current_user). - execute(branch_name) - - error(result[:message]) if result[:status] == :error - end end -- cgit v1.2.1 From 3fa3fcd7876262bb63966debd04d16ea219fad73 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 17:08:25 +0800 Subject: Cleanup parameters, easier to understand and more consistent across different methodst --- app/models/repository.rb | 91 +++++++++++++++++--------------- app/services/commits/change_service.rb | 2 +- app/services/files/create_dir_service.rb | 6 +-- app/services/files/create_service.rb | 8 +-- app/services/files/delete_service.rb | 6 +-- app/services/files/multi_service.rb | 4 +- app/services/files/update_service.rb | 6 +-- app/services/git_operation_service.rb | 16 +++--- spec/models/repository_spec.rb | 86 ++++++++++++++++++++---------- 9 files changed, 130 insertions(+), 95 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 50f347b58c8..73a9e269a65 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -746,12 +746,13 @@ class Repository # rubocop:disable Metrics/ParameterLists def commit_dir( - user, path, message, branch, + user, path, + message:, branch_name:, author_email: nil, author_name: nil, - source_branch: nil, source_project: project) - if branch_exists?(branch) + source_branch_name: nil, source_project: project) + if branch_exists?(branch_name) # tree_entry is private - entry = raw_repository.send(:tree_entry, commit(branch), path) + entry = raw_repository.send(:tree_entry, commit(branch_name), path) if entry if entry[:type] == :blob @@ -768,24 +769,25 @@ class Repository user, "#{path}/.gitkeep", '', - message, - branch, - false, + message: message, + branch_name: branch_name, + update: false, author_email: author_email, author_name: author_name, - source_branch: source_branch, + source_branch_name: source_branch_name, source_project: source_project) end # rubocop:enable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists def commit_file( - user, path, content, message, branch, update, + user, path, content, + message:, branch_name:, update: true, author_email: nil, author_name: nil, - source_branch: nil, source_project: project) - if branch_exists?(branch) && update == false + source_branch_name: nil, source_project: project) + if branch_exists?(branch_name) && update == false # tree_entry is private - if raw_repository.send(:tree_entry, commit(branch), path) + if raw_repository.send(:tree_entry, commit(branch_name), path) raise Gitlab::Git::Repository::InvalidBlobName.new( "Filename already exists; update not allowed") end @@ -793,11 +795,11 @@ class Repository multi_action( user: user, - branch: branch, message: message, + branch_name: branch_name, author_email: author_email, author_name: author_name, - source_branch: source_branch, + source_branch_name: source_branch_name, source_project: source_project, actions: [{ action: :create, file_path: path, @@ -808,9 +810,9 @@ class Repository # rubocop:disable Metrics/ParameterLists def update_file( user, path, content, - branch:, previous_path:, message:, + message:, branch_name:, previous_path:, author_email: nil, author_name: nil, - source_branch: nil, source_project: project) + source_branch_name: nil, source_project: project) action = if previous_path && previous_path != path :move else @@ -819,11 +821,11 @@ class Repository multi_action( user: user, - branch: branch, message: message, + branch_name: branch_name, author_email: author_email, author_name: author_name, - source_branch: source_branch, + source_branch_name: source_branch_name, source_project: source_project, actions: [{ action: action, file_path: path, @@ -834,16 +836,17 @@ class Repository # rubocop:disable Metrics/ParameterLists def remove_file( - user, path, message, branch, + user, path, + message:, branch_name:, author_email: nil, author_name: nil, - source_branch: nil, source_project: project) + source_branch_name: nil, source_project: project) multi_action( user: user, - branch: branch, message: message, + branch_name: branch_name, author_email: author_email, author_name: author_name, - source_branch: source_branch, + source_branch_name: source_branch_name, source_project: source_project, actions: [{ action: :delete, file_path: path }]) @@ -852,16 +855,16 @@ class Repository # rubocop:disable Metrics/ParameterLists def multi_action( - user:, branch:, message:, actions:, + user:, branch_name:, message:, actions:, author_email: nil, author_name: nil, - source_branch: nil, source_project: project) + source_branch_name: nil, source_project: project) GitOperationService.new(user, self).with_branch( - branch, - source_branch: source_branch, + branch_name, + source_branch_name: source_branch_name, source_project: source_project) do index = rugged.index branch_commit = source_project.repository.find_branch( - source_branch || branch) + source_branch_name || branch_name) parents = if branch_commit last_commit = branch_commit.dereferenced_target @@ -960,18 +963,19 @@ class Repository end def revert( - user, commit, base_branch, revert_tree_id = nil, - source_branch: nil, source_project: project) - revert_tree_id ||= check_revert_content(commit, base_branch) + user, commit, branch_name, revert_tree_id = nil, + source_branch_name: nil, source_project: project) + revert_tree_id ||= check_revert_content(commit, branch_name) return false unless revert_tree_id GitOperationService.new(user, self).with_branch( - base_branch, - source_branch: source_branch, source_project: source_project) do + branch_name, + source_branch_name: source_branch_name, + source_project: source_project) do source_sha = source_project.repository.find_source_sha( - source_branch || base_branch) + source_branch_name || branch_name) committer = user_to_committer(user) Rugged::Commit.create(rugged, @@ -984,18 +988,19 @@ class Repository end def cherry_pick( - user, commit, base_branch, cherry_pick_tree_id = nil, - source_branch: nil, source_project: project) - cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch) + user, commit, branch_name, cherry_pick_tree_id = nil, + source_branch_name: nil, source_project: project) + cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name) return false unless cherry_pick_tree_id GitOperationService.new(user, self).with_branch( - base_branch, - source_branch: source_branch, source_project: source_project) do + branch_name, + source_branch_name: source_branch_name, + source_project: source_project) do source_sha = source_project.repository.find_source_sha( - source_branch || base_branch) + source_branch_name || branch_name) committer = user_to_committer(user) Rugged::Commit.create(rugged, @@ -1019,8 +1024,8 @@ class Repository end end - def check_revert_content(commit, base_branch) - source_sha = find_branch(base_branch).dereferenced_target.sha + def check_revert_content(commit, branch_name) + source_sha = find_branch(branch_name).dereferenced_target.sha args = [commit.id, source_sha] args << { mainline: 1 } if commit.merge_commit? @@ -1033,8 +1038,8 @@ class Repository tree_id end - def check_cherry_pick_content(commit, base_branch) - source_sha = find_branch(base_branch).dereferenced_target.sha + def check_cherry_pick_content(commit, branch_name) + source_sha = find_branch(branch_name).dereferenced_target.sha args = [commit.id, source_sha] args << 1 if commit.merge_commit? diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 5458f7a6790..d49fcd42a08 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -37,7 +37,7 @@ module Commits @commit, into, tree_id, - source_branch: @target_branch) + source_branch_name: @target_branch) success else diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index f0bb3333db8..4a2b2e8fcaf 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -4,11 +4,11 @@ module Files repository.commit_dir( current_user, @file_path, - @commit_message, - @target_branch, + message: @commit_message, + branch_name: @target_branch, author_email: @author_email, author_name: @author_name, - source_branch: @source_branch) + source_branch_name: @source_branch) end def validate diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 65f7baf56fd..c95cb75f7cb 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -5,12 +5,12 @@ module Files current_user, @file_path, @file_content, - @commit_message, - @target_branch, - false, + message: @commit_message, + branch_name: @target_branch, + update: false, author_email: @author_email, author_name: @author_name, - source_branch: @source_branch) + source_branch_name: @source_branch) end def validate diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index b3f323d0173..45a9a559469 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -4,11 +4,11 @@ module Files repository.remove_file( current_user, @file_path, - @commit_message, - @target_branch, + message: @commit_message, + branch_name: @target_branch, author_email: @author_email, author_name: @author_name, - source_branch: @source_branch) + source_branch_name: @source_branch) end end end diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index 6f5f25f88fd..42ed97ca3c0 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -5,12 +5,12 @@ module Files def commit repository.multi_action( user: current_user, - branch: @target_branch, message: @commit_message, + branch_name: @target_branch, actions: params[:actions], author_email: @author_email, author_name: @author_name, - source_branch: @source_branch + source_branch_name: @source_branch ) end diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index 67d473d4978..5f671817cdb 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -4,13 +4,13 @@ module Files def commit repository.update_file(current_user, @file_path, @file_content, - branch: @target_branch, - previous_path: @previous_path, message: @commit_message, + branch_name: @target_branch, + previous_path: @previous_path, author_email: @author_email, author_name: @author_name, source_project: @source_project, - source_branch: @source_branch) + source_branch_name: @source_branch) end private diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 3a102a9276b..c8504ecf3cd 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -25,23 +25,23 @@ GitOperationService = Struct.new(:user, :repository) do end end - # Whenever `source_branch` is passed, if `branch` doesn't exist, - # it would be created from `source_branch`. + # Whenever `source_branch_name` is passed, if `branch_name` doesn't exist, + # it would be created from `source_branch_name`. # If `source_project` is passed, and the branch doesn't exist, # it would try to find the source from it instead of current repository. def with_branch( branch_name, - source_branch: nil, + source_branch_name: nil, source_project: repository.project) - check_with_branch_arguments!(branch_name, source_branch, source_project) + check_with_branch_arguments!( + branch_name, source_branch_name, source_project) - update_branch_with_hooks( - branch_name, source_branch, source_project) do |ref| + update_branch_with_hooks(branch_name) do |ref| if repository.project != source_project repository.fetch_ref( source_project.repository.path_to_repo, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch}", + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch_name}", "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch_name}" ) end @@ -52,7 +52,7 @@ GitOperationService = Struct.new(:user, :repository) do private - def update_branch_with_hooks(branch_name, source_branch, source_project) + def update_branch_with_hooks(branch_name) update_autocrlf_option ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index ebd05c946cc..78596346b91 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -249,7 +249,8 @@ describe Repository, models: true do describe "#commit_dir" do it "commits a change that creates a new directory" do expect do - repository.commit_dir(user, 'newdir', 'Create newdir', 'master') + repository.commit_dir(user, 'newdir', + message: 'Create newdir', branch_name: 'master') end.to change { repository.commits('master').count }.by(1) newdir = repository.tree('master', 'newdir') @@ -259,7 +260,10 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do expect do - repository.commit_dir(user, "newdir", "Add newdir", 'master', author_email: author_email, author_name: author_name) + repository.commit_dir(user, 'newdir', + message: 'Add newdir', + branch_name: 'master', + author_email: author_email, author_name: author_name) end.to change { repository.commits('master').count }.by(1) last_commit = repository.commit @@ -274,8 +278,9 @@ describe Repository, models: true do it 'commits change to a file successfully' do expect do repository.commit_file(user, 'CHANGELOG', 'Changelog!', - 'Updates file content', - 'master', true) + message: 'Updates file content', + branch_name: 'master', + update: true) end.to change { repository.commits('master').count }.by(1) blob = repository.blob_at('master', 'CHANGELOG') @@ -286,8 +291,12 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do expect do - repository.commit_file(user, "README", 'README!', 'Add README', - 'master', true, author_email: author_email, author_name: author_name) + repository.commit_file(user, 'README', 'README!', + message: 'Add README', + branch_name: 'master', + update: true, + author_email: author_email, + author_name: author_name) end.to change { repository.commits('master').count }.by(1) last_commit = repository.commit @@ -302,7 +311,7 @@ describe Repository, models: true do it 'updates filename successfully' do expect do repository.update_file(user, 'NEWLICENSE', 'Copyright!', - branch: 'master', + branch_name: 'master', previous_path: 'LICENSE', message: 'Changes filename') end.to change { repository.commits('master').count }.by(1) @@ -315,15 +324,16 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do - repository.commit_file(user, "README", 'README!', 'Add README', 'master', true) + repository.commit_file(user, 'README', 'README!', + message: 'Add README', branch_name: 'master', update: true) expect do - repository.update_file(user, 'README', "Updated README!", - branch: 'master', - previous_path: 'README', - message: 'Update README', - author_email: author_email, - author_name: author_name) + repository.update_file(user, 'README', 'Updated README!', + branch_name: 'master', + previous_path: 'README', + message: 'Update README', + author_email: author_email, + author_name: author_name) end.to change { repository.commits('master').count }.by(1) last_commit = repository.commit @@ -336,10 +346,12 @@ describe Repository, models: true do describe "#remove_file" do it 'removes file successfully' do - repository.commit_file(user, "README", 'README!', 'Add README', 'master', true) + repository.commit_file(user, 'README', 'README!', + message: 'Add README', branch_name: 'master', update: true) expect do - repository.remove_file(user, "README", "Remove README", 'master') + repository.remove_file(user, 'README', + message: 'Remove README', branch_name: 'master') end.to change { repository.commits('master').count }.by(1) expect(repository.blob_at('master', 'README')).to be_nil @@ -347,10 +359,13 @@ describe Repository, models: true do context "when an author is specified" do it "uses the given email/name to set the commit's author" do - repository.commit_file(user, "README", 'README!', 'Add README', 'master', true) + repository.commit_file(user, 'README', 'README!', + message: 'Add README', branch_name: 'master', update: true) expect do - repository.remove_file(user, "README", "Remove README", 'master', author_email: author_email, author_name: author_name) + repository.remove_file(user, 'README', + message: 'Remove README', branch_name: 'master', + author_email: author_email, author_name: author_name) end.to change { repository.commits('master').count }.by(1) last_commit = repository.commit @@ -498,11 +513,14 @@ describe Repository, models: true do describe "#license_blob", caching: true do before do - repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') + repository.remove_file( + user, 'LICENSE', message: 'Remove LICENSE', branch_name: 'master') end it 'handles when HEAD points to non-existent ref' do - repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) + repository.commit_file( + user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master', update: false) allow(repository).to receive(:file_on_head). and_raise(Rugged::ReferenceError) @@ -511,21 +529,27 @@ describe Repository, models: true do end it 'looks in the root_ref only' do - repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown') - repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false) + repository.remove_file(user, 'LICENSE', + message: 'Remove LICENSE', branch_name: 'markdown') + repository.commit_file(user, 'LICENSE', + Licensee::License.new('mit').content, + message: 'Add LICENSE', branch_name: 'markdown', update: false) expect(repository.license_blob).to be_nil end it 'detects license file with no recognizable open-source license content' do - repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) + repository.commit_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master', update: false) expect(repository.license_blob.name).to eq('LICENSE') end %w[LICENSE LICENCE LiCensE LICENSE.md LICENSE.foo COPYING COPYING.md].each do |filename| it "detects '#{filename}'" do - repository.commit_file(user, filename, Licensee::License.new('mit').content, "Add #{filename}", 'master', false) + repository.commit_file(user, filename, + Licensee::License.new('mit').content, + message: "Add #{filename}", branch_name: 'master', update: false) expect(repository.license_blob.name).to eq(filename) end @@ -534,7 +558,8 @@ describe Repository, models: true do describe '#license_key', caching: true do before do - repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') + repository.remove_file(user, 'LICENSE', + message: 'Remove LICENSE', branch_name: 'master') end it 'returns nil when no license is detected' do @@ -548,13 +573,16 @@ describe Repository, models: true do end it 'detects license file with no recognizable open-source license content' do - repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) + repository.commit_file(user, 'LICENSE', 'Copyright!', + message: 'Add LICENSE', branch_name: 'master', update: false) expect(repository.license_key).to be_nil end it 'returns the license key' do - repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false) + repository.commit_file(user, 'LICENSE', + Licensee::License.new('mit').content, + message: 'Add LICENSE', branch_name: 'master', update: false) expect(repository.license_key).to eq('mit') end @@ -815,7 +843,9 @@ describe Repository, models: true do expect(empty_repository).to receive(:expire_has_visible_content_cache) empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', - 'Updates file content', 'master', false) + message: 'Updates file content', + branch_name: 'master', + update: false) end end end -- cgit v1.2.1 From 23032467d4a1282f69e76bba921bd71c0083f7a8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 17:27:50 +0800 Subject: source_branch -> source_branch_name --- app/services/git_operation_service.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index c8504ecf3cd..36c8b8ff575 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -134,27 +134,28 @@ GitOperationService = Struct.new(:user, :repository) do end end - def check_with_branch_arguments!(branch_name, source_branch, source_project) + def check_with_branch_arguments!( + branch_name, source_branch_name, source_project) return if repository.branch_exists?(branch_name) if repository.project != source_project - unless source_branch + unless source_branch_name raise ArgumentError, - 'Should also pass :source_branch if' + + 'Should also pass :source_branch_name if' + ' :source_project is different from current project' end - unless source_project.repository.commit(source_branch).try(:sha) + unless source_project.repository.commit(source_branch_name).try(:sha) raise Repository::CommitError.new( "Cannot find branch #{branch_name} nor" \ - " #{source_branch} from" \ + " #{source_branch_name} from" \ " #{source_project.path_with_namespace}") end - elsif source_branch - unless repository.commit(source_branch).try(:sha) + elsif source_branch_name + unless repository.commit(source_branch_name).try(:sha) raise Repository::CommitError.new( "Cannot find branch #{branch_name} nor" \ - " #{source_branch} from" \ + " #{source_branch_name} from" \ " #{repository.project.path_with_namespace}") end end -- cgit v1.2.1 From 8384d0d8d528ffdd60c9ba9e3c0c9f688cb560ef Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 17:57:52 +0800 Subject: Introduce Repository#with_tmp_ref which we need commits from the other repository. We'll cleanup the tmp ref after we're done with our business. --- app/controllers/projects/compare_controller.rb | 3 ++- app/models/merge_request_diff.rb | 3 ++- app/models/repository.rb | 15 +++++++++++++ app/services/compare_service.rb | 30 +++++++++++++++----------- app/services/git_operation_service.rb | 15 ++++++------- app/services/merge_requests/build_service.rb | 5 +++-- app/workers/emails_on_push_worker.rb | 6 ++++-- spec/services/compare_service_spec.rb | 6 +++--- 8 files changed, 54 insertions(+), 29 deletions(-) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index bee3d56076c..e2b178314c0 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -37,7 +37,8 @@ class Projects::CompareController < Projects::ApplicationController end def define_diff_vars - @compare = CompareService.new.execute(@project, @head_ref, @project, @start_ref) + @compare = CompareService.new(@project, @head_ref). + execute(@project, @start_ref) if @compare @commits = @compare.commits diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index b8f36a2c958..7946d8e123e 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -169,7 +169,8 @@ class MergeRequestDiff < ActiveRecord::Base # When compare merge request versions we want diff A..B instead of A...B # so we handle cases when user does squash and rebase of the commits between versions. # For this reason we set straight to true by default. - CompareService.new.execute(project, head_commit_sha, project, sha, straight: straight) + CompareService.new(project, head_commit_sha). + execute(project, sha, straight: straight) end def commits_count diff --git a/app/models/repository.rb b/app/models/repository.rb index 73a9e269a65..ced68b9d274 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1099,6 +1099,21 @@ class Repository Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip) end + def with_tmp_ref(source_repository, source_branch_name) + random_string = SecureRandom.hex + + fetch_ref( + source_repository.path_to_repo, + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch_name}", + "refs/tmp/#{random_string}/head" + ) + + yield + + ensure + FileUtils.rm_rf("#{path_to_repo}/refs/tmp/#{random_string}") + end + def fetch_ref(source_path, source_ref, target_ref) args = %W(#{Gitlab.config.git.bin_path} fetch --no-tags -f #{source_path} #{source_ref}:#{target_ref}) Gitlab::Popen.popen(args, path_to_repo) diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 5e8fafca98c..4367cb5f615 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,23 +3,29 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::Git::Compare object that responds to commits and diffs class CompareService - def execute(source_project, source_branch, target_project, target_branch, straight: false) - source_commit = source_project.commit(source_branch) - return unless source_commit + attr_reader :source_project, :source_sha - source_sha = source_commit.sha + def initialize(new_source_project, source_branch) + @source_project = new_source_project + @source_sha = new_source_project.commit(source_branch).try(:sha) + end - # If compare with other project we need to fetch ref first - unless target_project == source_project - random_string = SecureRandom.hex + def execute(target_project, target_branch, straight: false) + return unless source_sha - target_project.repository.fetch_ref( - source_project.repository.path_to_repo, - "refs/heads/#{source_branch}", - "refs/tmp/#{random_string}/head" - ) + # If compare with other project we need to fetch ref first + if target_project == source_project + compare(target_project, target_branch, straight) + else + target_project.repository.with_tmp_ref(source_project, source_branch) do + compare(target_project, target_branch, straight) + end end + end + + private + def compare(target_project, target_branch, straight) raw_compare = Gitlab::Git::Compare.new( target_project.repository.raw_repository, target_branch, diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 36c8b8ff575..a7d267cd6b4 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -38,15 +38,14 @@ GitOperationService = Struct.new(:user, :repository) do branch_name, source_branch_name, source_project) update_branch_with_hooks(branch_name) do |ref| - if repository.project != source_project - repository.fetch_ref( - source_project.repository.path_to_repo, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch_name}", - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch_name}" - ) + if repository.project == source_project + yield(ref) + else + repository.with_tmp_ref( + source_project.repository, source_branch_name) do + yield(ref) + end end - - yield(ref) end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index bebfca7537b..a52a94c5ffa 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -16,9 +16,10 @@ module MergeRequests messages = validate_branches(merge_request) return build_failed(merge_request, messages) unless messages.empty? - compare = CompareService.new.execute( + compare = CompareService.new( merge_request.source_project, - merge_request.source_branch, + merge_request.source_branch + ).execute( merge_request.target_project, merge_request.target_branch, ) diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index b9cd49985dc..d4c3f14ec06 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -33,13 +33,15 @@ class EmailsOnPushWorker reverse_compare = false if action == :push - compare = CompareService.new.execute(project, after_sha, project, before_sha) + compare = CompareService.new(project, after_sha). + execute(project, before_sha) diff_refs = compare.diff_refs return false if compare.same if compare.commits.empty? - compare = CompareService.new.execute(project, before_sha, project, after_sha) + compare = CompareService.new(project, before_sha). + execute(project, after_sha) diff_refs = compare.diff_refs reverse_compare = true diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb index 3760f19aaa2..0a7fc58523f 100644 --- a/spec/services/compare_service_spec.rb +++ b/spec/services/compare_service_spec.rb @@ -3,17 +3,17 @@ require 'spec_helper' describe CompareService, services: true do let(:project) { create(:project) } let(:user) { create(:user) } - let(:service) { described_class.new } + let(:service) { described_class.new(project, 'feature') } describe '#execute' do context 'compare with base, like feature...fix' do - subject { service.execute(project, 'feature', project, 'fix', straight: false) } + subject { service.execute(project, 'fix', straight: false) } it { expect(subject.diffs.size).to eq(1) } end context 'straight compare, like feature..fix' do - subject { service.execute(project, 'feature', project, 'fix', straight: true) } + subject { service.execute(project, 'fix', straight: true) } it { expect(subject.diffs.size).to eq(3) } end -- cgit v1.2.1 From 07b9b80a8833cf44ba804c9b8dfdf1550785fe83 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 19:11:52 +0800 Subject: Fix tests to use the new API --- app/services/compare_service.rb | 14 ++++---- .../projects/templates_controller_spec.rb | 3 +- spec/factories/projects.rb | 34 +++++++++++++++++- .../project_owner_creates_license_file_spec.rb | 3 +- spec/features/projects/issuable_templates_spec.rb | 40 +++++++++++++++++++--- spec/lib/gitlab/git_access_spec.rb | 8 ++++- spec/lib/gitlab/template/issue_template_spec.rb | 19 +++++----- .../gitlab/template/merge_request_template_spec.rb | 19 +++++----- spec/models/cycle_analytics/production_spec.rb | 8 ++++- spec/models/cycle_analytics/staging_spec.rb | 8 ++--- .../merge_requests/resolve_service_spec.rb | 8 ++++- spec/support/cycle_analytics_helpers.rb | 8 ++++- 12 files changed, 130 insertions(+), 42 deletions(-) diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 4367cb5f615..199a015622e 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,29 +3,31 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::Git::Compare object that responds to commits and diffs class CompareService - attr_reader :source_project, :source_sha + attr_reader :source_project, :source_branch - def initialize(new_source_project, source_branch) + def initialize(new_source_project, source_branch_name) @source_project = new_source_project - @source_sha = new_source_project.commit(source_branch).try(:sha) + @source_branch = new_source_project.commit(source_branch_name) end def execute(target_project, target_branch, straight: false) + source_sha = source_branch.try(:sha) + return unless source_sha # If compare with other project we need to fetch ref first if target_project == source_project - compare(target_project, target_branch, straight) + compare(source_sha, target_project, target_branch, straight) else target_project.repository.with_tmp_ref(source_project, source_branch) do - compare(target_project, target_branch, straight) + compare(source_sha, target_project, target_branch, straight) end end end private - def compare(target_project, target_branch, straight) + def compare(source_sha, target_project, target_branch, straight) raw_compare = Gitlab::Git::Compare.new( target_project.repository.raw_repository, target_branch, diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb index 19a152bcb05..78e54e92a56 100644 --- a/spec/controllers/projects/templates_controller_spec.rb +++ b/spec/controllers/projects/templates_controller_spec.rb @@ -14,7 +14,8 @@ describe Projects::TemplatesController do before do project.add_user(user, Gitlab::Access::MASTER) - project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) + project.repository.commit_file(user, file_path_1, 'something valid', + message: 'test 3', branch_name: 'master', update: false) end describe '#show' do diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1166498ddff..aa971e12a2e 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -91,8 +91,40 @@ FactoryGirl.define do factory :project, parent: :empty_project do path { 'gitlabhq' } - after :create do |project| + transient do + create_template nil + end + + after :create do |project, evaluator| TestEnv.copy_repo(project) + + if evaluator.create_template + args = evaluator.create_template + + project.add_user(args[:user], args[:access]) + + project.repository.commit_file( + args[:user], + ".gitlab/#{args[:path]}/bug.md", + 'something valid', + message: 'test 3', + branch_name: 'master', + update: false) + project.repository.commit_file( + args[:user], + ".gitlab/#{args[:path]}/template_test.md", + 'template_test', + message: 'test 1', + branch_name: 'master', + update: false) + project.repository.commit_file( + args[:user], + ".gitlab/#{args[:path]}/feature_proposal.md", + 'feature_proposal', + message: 'test 2', + branch_name: 'master', + update: false) + end end end diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index a521ce50f35..64094af29c0 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -6,7 +6,8 @@ feature 'project owner creates a license file', feature: true, js: true do let(:project_master) { create(:user) } let(:project) { create(:project) } background do - project.repository.remove_file(project_master, 'LICENSE', 'Remove LICENSE', 'master') + project.repository.remove_file(project_master, 'LICENSE', + message: 'Remove LICENSE', branch_name: 'master') project.team << [project_master, :master] login_as(project_master) visit namespace_project_path(project.namespace, project) diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 2f377312ea5..daf08067ed1 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -18,8 +18,20 @@ feature 'issuable templates', feature: true, js: true do let(:description_addition) { ' appending to description' } background do - project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false) - project.repository.commit_file(user, '.gitlab/issue_templates/test.md', longtemplate_content, 'added issue template', 'master', false) + project.repository.commit_file( + user, + '.gitlab/issue_templates/bug.md', + template_content, + message: 'added issue template', + branch_name: 'master', + update: false) + project.repository.commit_file( + user, + '.gitlab/issue_templates/test.md', + longtemplate_content, + message: 'added issue template', + branch_name: 'master', + update: false) visit edit_namespace_project_issue_path project.namespace, project, issue fill_in :'issue[title]', with: 'test issue title' end @@ -68,7 +80,13 @@ feature 'issuable templates', feature: true, js: true do let(:issue) { create(:issue, author: user, assignee: user, project: project) } background do - project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false) + project.repository.commit_file( + user, + '.gitlab/issue_templates/bug.md', + template_content, + message: 'added issue template', + branch_name: 'master', + update: false) visit edit_namespace_project_issue_path project.namespace, project, issue fill_in :'issue[title]', with: 'test issue title' fill_in :'issue[description]', with: prior_description @@ -87,7 +105,13 @@ feature 'issuable templates', feature: true, js: true do let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) } background do - project.repository.commit_file(user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false) + project.repository.commit_file( + user, + '.gitlab/merge_request_templates/feature-proposal.md', + template_content, + message: 'added merge request template', + branch_name: 'master', + update: false) visit edit_namespace_project_merge_request_path project.namespace, project, merge_request fill_in :'merge_request[title]', with: 'test merge request title' end @@ -112,7 +136,13 @@ feature 'issuable templates', feature: true, js: true do fork_project.team << [fork_user, :master] create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project) login_as fork_user - project.repository.commit_file(fork_user, '.gitlab/merge_request_templates/feature-proposal.md', template_content, 'added merge request template', 'master', false) + project.repository.commit_file( + fork_user, + '.gitlab/merge_request_templates/feature-proposal.md', + template_content, + message: 'added merge request template', + branch_name: 'master', + update: false) visit edit_namespace_project_merge_request_path project.namespace, project, merge_request fill_in :'merge_request[title]', with: 'test merge request title' end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index f1d0a190002..a48287b7a75 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -209,7 +209,13 @@ describe Gitlab::GitAccess, lib: true do stub_git_hooks project.repository.add_branch(user, unprotected_branch, 'feature') target_branch = project.repository.lookup('feature') - source_branch = project.repository.commit_file(user, FFaker::InternetSE.login_user_name, FFaker::HipsterIpsum.paragraph, FFaker::HipsterIpsum.sentence, unprotected_branch, false) + source_branch = project.repository.commit_file( + user, + FFaker::InternetSE.login_user_name, + FFaker::HipsterIpsum.paragraph, + message: FFaker::HipsterIpsum.sentence, + branch_name: unprotected_branch, + update: false) rugged = project.repository.rugged author = { email: "email@example.com", time: Time.now, name: "Example Git User" } diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb index d2d334e6413..984236b74ce 100644 --- a/spec/lib/gitlab/template/issue_template_spec.rb +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -4,16 +4,15 @@ describe Gitlab::Template::IssueTemplate do subject { described_class } let(:user) { create(:user) } - let(:project) { create(:project) } - let(:file_path_1) { '.gitlab/issue_templates/bug.md' } - let(:file_path_2) { '.gitlab/issue_templates/template_test.md' } - let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' } - - before do - project.add_user(user, Gitlab::Access::MASTER) - project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) - project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false) - project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false) + + let(:project) do + create(:project, + create_template: { + user: user, + access: Gitlab::Access::MASTER, + path: 'issue_templates' + } + ) end describe '.all' do diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb index ddf68c4cf78..e98a8beccdd 100644 --- a/spec/lib/gitlab/template/merge_request_template_spec.rb +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -4,16 +4,15 @@ describe Gitlab::Template::MergeRequestTemplate do subject { described_class } let(:user) { create(:user) } - let(:project) { create(:project) } - let(:file_path_1) { '.gitlab/merge_request_templates/bug.md' } - let(:file_path_2) { '.gitlab/merge_request_templates/template_test.md' } - let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' } - - before do - project.add_user(user, Gitlab::Access::MASTER) - project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false) - project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false) - project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false) + + let(:project) do + create(:project, + create_template: { + user: user, + access: Gitlab::Access::MASTER, + path: 'merge_request_templates' + } + ) end describe '.all' do diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index 21b9c6e7150..97568c47903 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -21,7 +21,13 @@ describe 'CycleAnalytics#production', feature: true do ["production deploy happens after merge request is merged (along with other changes)", lambda do |context, data| # Make other changes on master - sha = context.project.repository.commit_file(context.user, context.random_git_name, "content", "commit message", 'master', false) + sha = context.project.repository.commit_file( + context.user, + context.random_git_name, + 'content', + message: 'commit message', + branch_name: 'master', + update: false) context.project.repository.commit(sha) context.deploy_master diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index dad653964b7..27c826110fb 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -28,10 +28,10 @@ describe 'CycleAnalytics#staging', feature: true do sha = context.project.repository.commit_file( context.user, context.random_git_name, - "content", - "commit message", - 'master', - false) + 'content', + message: 'commit message', + branch_name: 'master', + update: false) context.project.repository.commit(sha) context.deploy_master diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb index 388abb6a0df..a0e51681725 100644 --- a/spec/services/merge_requests/resolve_service_spec.rb +++ b/spec/services/merge_requests/resolve_service_spec.rb @@ -66,7 +66,13 @@ describe MergeRequests::ResolveService do context 'when the source project is a fork and does not contain the HEAD of the target branch' do let!(:target_head) do - project.repository.commit_file(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false) + project.repository.commit_file( + user, + 'new-file-in-target', + '', + message: 'Add new file in target', + branch_name: 'conflict-start', + update: false) end before do diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb index 75c95d70951..6ed55289ed9 100644 --- a/spec/support/cycle_analytics_helpers.rb +++ b/spec/support/cycle_analytics_helpers.rb @@ -35,7 +35,13 @@ module CycleAnalyticsHelpers project.repository.add_branch(user, source_branch, 'master') end - sha = project.repository.commit_file(user, random_git_name, "content", "commit message", source_branch, false) + sha = project.repository.commit_file( + user, + random_git_name, + 'content', + message: 'commit message', + branch_name: source_branch, + update: false) project.repository.commit(sha) opts = { -- cgit v1.2.1 From 9c6563f64a3a770cccb9fcf3eb609416c2466080 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 19:46:51 +0800 Subject: rubocop prefers lisp style --- spec/lib/gitlab/template/issue_template_spec.rb | 4 +--- spec/lib/gitlab/template/merge_request_template_spec.rb | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb index 984236b74ce..4275fda5c74 100644 --- a/spec/lib/gitlab/template/issue_template_spec.rb +++ b/spec/lib/gitlab/template/issue_template_spec.rb @@ -10,9 +10,7 @@ describe Gitlab::Template::IssueTemplate do create_template: { user: user, access: Gitlab::Access::MASTER, - path: 'issue_templates' - } - ) + path: 'issue_templates' }) end describe '.all' do diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb index e98a8beccdd..708bb084eef 100644 --- a/spec/lib/gitlab/template/merge_request_template_spec.rb +++ b/spec/lib/gitlab/template/merge_request_template_spec.rb @@ -10,9 +10,7 @@ describe Gitlab::Template::MergeRequestTemplate do create_template: { user: user, access: Gitlab::Access::MASTER, - path: 'merge_request_templates' - } - ) + path: 'merge_request_templates' }) end describe '.all' do -- cgit v1.2.1 From e7599eb0b76f7ef199e8719377a212f53aaa17f8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 8 Dec 2016 19:49:20 +0800 Subject: Should pass repository rather than project --- app/services/compare_service.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 199a015622e..fcbfe68f1a3 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -19,7 +19,8 @@ class CompareService if target_project == source_project compare(source_sha, target_project, target_branch, straight) else - target_project.repository.with_tmp_ref(source_project, source_branch) do + target_project.repository.with_tmp_ref( + source_project.repository, source_branch) do compare(source_sha, target_project, target_branch, straight) end end -- cgit v1.2.1 From 56f697466d288c3bff16cc270e5cb2c50c9c14c8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 9 Dec 2016 20:05:35 +0800 Subject: Introduce git_action and normalize previous_path Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_19747456 --- app/models/repository.rb | 72 +++++++++++++++++++++++++++++++----------------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 1684aa97567..9d554991ab4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -862,32 +862,8 @@ class Repository [] end - actions.each do |action| - path = Gitlab::Git::PathHelper.normalize_path(action[:file_path]).to_s - - raise Gitlab::Git::Repository::InvalidBlobName.new("Invalid path") if - path.split('/').include?('..') - - case action[:action] - when :create, :update, :move - mode = - case action[:action] - when :update - index.get(path)[:mode] - when :move - index.get(action[:previous_path])[:mode] - end - mode ||= 0o100644 - - index.remove(action[:previous_path]) if action[:action] == :move - - content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content] - oid = rugged.write(content, :blob) - - index.add(path: path, oid: oid, mode: mode) - when :delete - index.remove(path) - end + actions.each do |act| + git_action(index, act) end options = { @@ -1181,6 +1157,50 @@ class Repository private + def git_action(index, action) + path = normalize_path(action[:file_path]) + + if action[:action] == :move + previous_path = normalize_path(action[:previous_path]) + end + + case action[:action] + when :create, :update, :move + mode = + case action[:action] + when :update + index.get(path)[:mode] + when :move + index.get(previous_path)[:mode] + end + mode ||= 0o100644 + + index.remove(previous_path) if action[:action] == :move + + content = if action[:encoding] == 'base64' + Base64.decode64(action[:content]) + else + action[:content] + end + + oid = rugged.write(content, :blob) + + index.add(path: path, oid: oid, mode: mode) + when :delete + index.remove(path) + end + end + + def normalize_path(path) + pathname = Gitlab::Git::PathHelper.normalize_path(path) + + if pathname.each_filename.include?('..') + raise Gitlab::Git::Repository::InvalidBlobName.new('Invalid path') + end + + pathname.to_s + end + def refs_directory_exists? return false unless path_with_namespace -- cgit v1.2.1 From 5a115671b9d7c22daf8160d7284786d0f8b216cb Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 9 Dec 2016 20:06:19 +0800 Subject: Use rugged.references.delete to delete reference Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_19746468 --- app/models/repository.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 9d554991ab4..389d52f8c0f 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1064,18 +1064,18 @@ class Repository end def with_tmp_ref(source_repository, source_branch_name) - random_string = SecureRandom.hex + tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" fetch_ref( source_repository.path_to_repo, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch_name}", - "refs/tmp/#{random_string}/head" + tmp_ref ) yield ensure - FileUtils.rm_rf("#{path_to_repo}/refs/tmp/#{random_string}") + rugged.references.delete(tmp_ref) end def fetch_ref(source_path, source_ref, target_ref) -- cgit v1.2.1 From bb9d30590d4ca5b25d5020234916ce961acf15b6 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Sat, 10 Dec 2016 00:40:23 +0800 Subject: Pass source_commit so that we save a few lookups --- app/models/repository.rb | 51 +++++++++++++---------------------- app/services/git_operation_service.rb | 25 ++++++++--------- spec/models/repository_spec.rb | 28 +++++++++++++++---- 3 files changed, 54 insertions(+), 50 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 389d52f8c0f..4034a49ae63 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -849,15 +849,12 @@ class Repository GitOperationService.new(user, self).with_branch( branch_name, source_branch_name: source_branch_name, - source_project: source_project) do + source_project: source_project) do |source_commit| index = rugged.index - branch_commit = source_project.repository.find_branch( - source_branch_name || branch_name) - parents = if branch_commit - last_commit = branch_commit.dereferenced_target - index.read_tree(last_commit.raw_commit.tree) - [last_commit.sha] + parents = if source_commit + index.read_tree(source_commit.raw_commit.tree) + [source_commit.sha] else [] end @@ -904,17 +901,17 @@ class Repository end def merge(user, merge_request, options = {}) - our_commit = rugged.branches[merge_request.target_branch].target - their_commit = rugged.lookup(merge_request.diff_head_sha) + GitOperationService.new(user, self).with_branch( + merge_request.target_branch) do |source_commit| + our_commit = source_commit.raw_commit + their_commit = rugged.lookup(merge_request.diff_head_sha) - raise "Invalid merge target" if our_commit.nil? - raise "Invalid merge source" if their_commit.nil? + raise 'Invalid merge target' unless our_commit + raise 'Invalid merge source' unless their_commit - merge_index = rugged.merge_commits(our_commit, their_commit) - return false if merge_index.conflicts? + merge_index = rugged.merge_commits(our_commit, their_commit) + break if merge_index.conflicts? - GitOperationService.new(user, self).with_branch( - merge_request.target_branch) do actual_options = options.merge( parents: [our_commit, their_commit], tree: merge_index.write_tree(rugged), @@ -924,6 +921,8 @@ class Repository merge_request.update(in_progress_merge_commit_sha: commit_id) commit_id end + rescue Repository::CommitError # when merge_index.conflicts? + false end def revert( @@ -936,10 +935,8 @@ class Repository GitOperationService.new(user, self).with_branch( branch_name, source_branch_name: source_branch_name, - source_project: source_project) do + source_project: source_project) do |source_commit| - source_sha = source_project.repository.find_source_sha( - source_branch_name || branch_name) committer = user_to_committer(user) Rugged::Commit.create(rugged, @@ -947,7 +944,7 @@ class Repository author: committer, committer: committer, tree: revert_tree_id, - parents: [rugged.lookup(source_sha)]) + parents: [source_commit.sha]) end end @@ -961,10 +958,8 @@ class Repository GitOperationService.new(user, self).with_branch( branch_name, source_branch_name: source_branch_name, - source_project: source_project) do + source_project: source_project) do |source_commit| - source_sha = source_project.repository.find_source_sha( - source_branch_name || branch_name) committer = user_to_committer(user) Rugged::Commit.create(rugged, @@ -976,7 +971,7 @@ class Repository }, committer: committer, tree: cherry_pick_tree_id, - parents: [rugged.lookup(source_sha)]) + parents: [source_commit.sha]) end end @@ -1145,16 +1140,6 @@ class Repository end end - protected - - def find_source_sha(branch_name) - if branch_exists?(branch_name) - find_branch(branch_name).dereferenced_target.sha - else - Gitlab::Git::BLANK_SHA - end - end - private def git_action(index, action) diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index a7d267cd6b4..62a9eda3eba 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -37,13 +37,16 @@ GitOperationService = Struct.new(:user, :repository) do check_with_branch_arguments!( branch_name, source_branch_name, source_project) - update_branch_with_hooks(branch_name) do |ref| + source_commit = source_project.repository.find_branch( + source_branch_name || branch_name).try(:dereferenced_target) + + update_branch_with_hooks(branch_name) do if repository.project == source_project - yield(ref) + yield(source_commit) else repository.with_tmp_ref( source_project.repository, source_branch_name) do - yield(ref) + yield(source_commit) end end end @@ -54,31 +57,29 @@ GitOperationService = Struct.new(:user, :repository) do def update_branch_with_hooks(branch_name) update_autocrlf_option - ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - oldrev = Gitlab::Git::BLANK_SHA was_empty = repository.empty? # Make commit - newrev = yield(ref) + newrev = yield unless newrev raise Repository::CommitError.new('Failed to create commit') end branch = repository.find_branch(branch_name) - oldrev = if repository.rugged.lookup(newrev).parent_ids.empty? || - branch.nil? - Gitlab::Git::BLANK_SHA + oldrev = if branch + # This could verify we're not losing commits + repository.rugged.merge_base(newrev, branch.target) else - repository.rugged.merge_base( - newrev, branch.dereferenced_target.sha) + Gitlab::Git::BLANK_SHA end + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name with_hooks_and_update_ref(ref, oldrev, newrev) do # If repo was empty expire cache repository.after_create if was_empty repository.after_create_branch if was_empty || - oldrev == Gitlab::Git::BLANK_SHA + Gitlab::Git.blank_ref?(oldrev) end newrev diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 9cc13a9a25b..a61d7f0c76d 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -257,6 +257,24 @@ describe Repository, models: true do expect(newdir.path).to eq('newdir') end + context "when committing to another project" do + let(:forked_project) { create(:project) } + + it "creates a fork and commit to the forked project" do + expect do + repository.commit_dir(user, 'newdir', + message: 'Create newdir', branch_name: 'patch', + source_branch_name: 'master', source_project: forked_project) + end.to change { repository.commits('master').count }.by(0) + + expect(repository.branch_exists?('patch')).to be_truthy + expect(forked_project.repository.branch_exists?('patch')).to be_falsy + + newdir = repository.tree('patch', 'newdir') + expect(newdir.path).to eq('newdir') + end + end + context "when an author is specified" do it "uses the given email/name to set the commit's author" do expect do @@ -758,9 +776,9 @@ describe Repository, models: true do end context 'when the update adds more than one commit' do - it 'runs without errors' do - old_rev = '33f3729a45c02fc67d00adb1b8bca394b0e761d9' + let(:old_rev) { '33f3729a45c02fc67d00adb1b8bca394b0e761d9' } + it 'runs without errors' do # old_rev is an ancestor of new_rev expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev) @@ -779,10 +797,10 @@ describe Repository, models: true do end context 'when the update would remove commits from the target branch' do - it 'raises an exception' do - branch = 'master' - old_rev = repository.find_branch(branch).dereferenced_target.sha + let(:branch) { 'master' } + let(:old_rev) { repository.find_branch(branch).dereferenced_target.sha } + it 'raises an exception' do # The 'master' branch is NOT an ancestor of new_rev. expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev) -- cgit v1.2.1 From 3e01385bca92dc8c0df3aa4032cc58d708dc0ff5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Sat, 10 Dec 2016 01:23:49 +0800 Subject: Should pass branch name, not commit object! --- app/services/compare_service.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index fcbfe68f1a3..31c371c4b34 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,15 +3,16 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::Git::Compare object that responds to commits and diffs class CompareService - attr_reader :source_project, :source_branch + attr_reader :source_project, :source_branch_name - def initialize(new_source_project, source_branch_name) + def initialize(new_source_project, new_source_branch_name) @source_project = new_source_project - @source_branch = new_source_project.commit(source_branch_name) + @source_branch_name = new_source_branch_name end def execute(target_project, target_branch, straight: false) - source_sha = source_branch.try(:sha) + source_sha = source_project.repository. + commit(source_branch_name).try(:sha) return unless source_sha @@ -20,7 +21,7 @@ class CompareService compare(source_sha, target_project, target_branch, straight) else target_project.repository.with_tmp_ref( - source_project.repository, source_branch) do + source_project.repository, source_branch_name) do compare(source_sha, target_project, target_branch, straight) end end -- cgit v1.2.1 From c0dfa0c609805558b30f11046eedadfb8a14886d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Mon, 12 Dec 2016 23:07:52 +0800 Subject: Not sure why, but apparently SHA works better It's very weird that source_commit.raw_commit and rugged.branches[merge_request.target_branch].target should be completely the same. I checked with == and other values which proved that both should be the same, but still tests cannot pass for: spec/services/merge_requests/refresh_service_spec.rb I decided to give it up. We could just use SHA and that works fine anyway. --- app/models/repository.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 44f66b89600..7a7236993a8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -903,8 +903,8 @@ class Repository def merge(user, merge_request, options = {}) GitOperationService.new(user, self).with_branch( merge_request.target_branch) do |source_commit| - our_commit = source_commit.raw_commit - their_commit = rugged.lookup(merge_request.diff_head_sha) + our_commit = source_commit.sha + their_commit = merge_request.diff_head_sha raise 'Invalid merge target' unless our_commit raise 'Invalid merge source' unless their_commit -- cgit v1.2.1 From 26af4b5a61d5cdaffa7769336f40cd0861f6b1d4 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 14 Dec 2016 01:21:48 +0800 Subject: Also check blob path from source branch Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_19747244 --- app/models/repository.rb | 57 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 7a7236993a8..2e706b770b2 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -738,19 +738,11 @@ class Repository message:, branch_name:, author_email: nil, author_name: nil, source_branch_name: nil, source_project: project) - if branch_exists?(branch_name) - # tree_entry is private - entry = raw_repository.send(:tree_entry, commit(branch_name), path) - - if entry - if entry[:type] == :blob - raise Gitlab::Git::Repository::InvalidBlobName.new( - "Directory already exists as a file") - else - raise Gitlab::Git::Repository::InvalidBlobName.new( - "Directory already exists") - end - end + check_tree_entry_for_dir(branch_name, path) + + if source_branch_name + source_project.repository. + check_tree_entry_for_dir(source_branch_name, path) end commit_file( @@ -773,11 +765,16 @@ class Repository message:, branch_name:, update: true, author_email: nil, author_name: nil, source_branch_name: nil, source_project: project) - if branch_exists?(branch_name) && update == false - # tree_entry is private - if raw_repository.send(:tree_entry, commit(branch_name), path) - raise Gitlab::Git::Repository::InvalidBlobName.new( - "Filename already exists; update not allowed") + unless update + error_message = "Filename already exists; update not allowed" + + if tree_entry_at(branch_name, path) + raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) + end + + if source_branch_name && + source_project.repository.tree_entry_at(source_branch_name, path) + raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) end end @@ -1140,6 +1137,30 @@ class Repository end end + protected + + def tree_entry_at(branch_name, path) + branch_exists?(branch_name) && + # tree_entry is private + raw_repository.send(:tree_entry, commit(branch_name), path) + end + + def check_tree_entry_for_dir(branch_name, path) + return unless branch_exists?(branch_name) + + entry = tree_entry_at(branch_name, path) + + return unless entry + + if entry[:type] == :blob + raise Gitlab::Git::Repository::InvalidBlobName.new( + "Directory already exists as a file") + else + raise Gitlab::Git::Repository::InvalidBlobName.new( + "Directory already exists") + end + end + private def git_action(index, action) -- cgit v1.2.1 From dc4b3dd0ae5d6e0b55ba6723e5deff6eee127409 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 14 Dec 2016 01:47:17 +0800 Subject: Fix source_project and also pass source_project Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_19747556 --- app/controllers/concerns/creates_commit.rb | 4 ++-- app/services/commits/change_service.rb | 1 + app/services/files/create_dir_service.rb | 1 + app/services/files/create_service.rb | 1 + app/services/files/delete_service.rb | 1 + app/services/files/multi_service.rb | 1 + 6 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index d2fcb2efd6a..a94077c2bd4 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -6,12 +6,12 @@ module CreatesCommit source_branch = @ref if @ref && @repository.find_branch(@ref) commit_params = @commit_params.merge( - source_project: @project, + source_project: @tree_edit_project, source_branch: source_branch, target_branch: @target_branch ) - result = service.new(@tree_edit_project, current_user, commit_params).execute + result = service.new(@project, current_user, commit_params).execute if result[:status] == :success update_flash_notice(success_notice) diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 99d459908e7..9c630f5bbf1 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -37,6 +37,7 @@ module Commits @commit, into, tree_id, + source_project: @source_project, source_branch_name: @target_branch) success diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index 4a2b2e8fcaf..ee4e130a38f 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -8,6 +8,7 @@ module Files branch_name: @target_branch, author_email: @author_email, author_name: @author_name, + source_project: @source_project, source_branch_name: @source_branch) end diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index c95cb75f7cb..853c471666d 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -10,6 +10,7 @@ module Files update: false, author_email: @author_email, author_name: @author_name, + source_project: @source_project, source_branch_name: @source_branch) end diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index 45a9a559469..cfe532d49b3 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -8,6 +8,7 @@ module Files branch_name: @target_branch, author_email: @author_email, author_name: @author_name, + source_project: @source_project, source_branch_name: @source_branch) end end diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index 42ed97ca3c0..f77e5d91103 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -10,6 +10,7 @@ module Files actions: params[:actions], author_email: @author_email, author_name: @author_name, + source_project: @source_project, source_branch_name: @source_branch ) end -- cgit v1.2.1 From 46d752ce218d833ff947bd4503de56300471a8cb Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 14 Dec 2016 02:03:04 +0800 Subject: Use a regular class for GitOperationService Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_19747793 --- app/services/git_operation_service.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 62a9eda3eba..9a052f952cf 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -1,5 +1,12 @@ -GitOperationService = Struct.new(:user, :repository) do +class GitOperationService + attr_reader :user, :repository + + def initialize(new_user, new_repository) + @user = new_user + @repository = new_repository + end + def add_branch(branch_name, newrev) ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name oldrev = Gitlab::Git::BLANK_SHA -- cgit v1.2.1 From d03c605bd4a128d45179dd05f117a78aab7af6be Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 14 Dec 2016 02:29:35 +0800 Subject: Unify parameters and callback after hooks Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_19747856 --- app/services/git_operation_service.rb | 34 ++++++++++++------------- lib/gitlab/git.rb | 2 +- spec/models/repository_spec.rb | 7 +++-- spec/workers/git_garbage_collect_worker_spec.rb | 2 +- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 9a052f952cf..68b28231595 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -11,7 +11,7 @@ class GitOperationService ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name oldrev = Gitlab::Git::BLANK_SHA - with_hooks_and_update_ref(ref, oldrev, newrev) + update_ref_in_hooks(ref, newrev, oldrev) end def rm_branch(branch) @@ -19,14 +19,14 @@ class GitOperationService oldrev = branch.dereferenced_target.id newrev = Gitlab::Git::BLANK_SHA - with_hooks_and_update_ref(ref, oldrev, newrev) + update_ref_in_hooks(ref, newrev, oldrev) end def add_tag(tag_name, newrev, options = {}) ref = Gitlab::Git::TAG_REF_PREFIX + tag_name oldrev = Gitlab::Git::BLANK_SHA - with_hooks(ref, oldrev, newrev) do |service| + with_hooks(ref, newrev, oldrev) do |service| raw_tag = repository.rugged.tags.create(tag_name, newrev, options) service.newrev = raw_tag.target_id end @@ -82,25 +82,23 @@ class GitOperationService end ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name - with_hooks_and_update_ref(ref, oldrev, newrev) do - # If repo was empty expire cache - repository.after_create if was_empty - repository.after_create_branch if was_empty || - Gitlab::Git.blank_ref?(oldrev) - end + update_ref_in_hooks(ref, newrev, oldrev) + + # If repo was empty expire cache + repository.after_create if was_empty + repository.after_create_branch if was_empty || + Gitlab::Git.blank_ref?(oldrev) newrev end - def with_hooks_and_update_ref(ref, oldrev, newrev) - with_hooks(ref, oldrev, newrev) do |service| - update_ref!(ref, newrev, oldrev) - - yield(service) if block_given? + def update_ref_in_hooks(ref, newrev, oldrev) + with_hooks(ref, newrev, oldrev) do + update_ref(ref, newrev, oldrev) end end - def with_hooks(ref, oldrev, newrev) + def with_hooks(ref, newrev, oldrev) result = nil GitHooksService.new.execute( @@ -116,7 +114,7 @@ class GitOperationService result end - def update_ref!(name, newrev, oldrev) + def update_ref(ref, newrev, oldrev) # We use 'git update-ref' because libgit2/rugged currently does not # offer 'compare and swap' ref updates. Without compare-and-swap we can # (and have!) accidentally reset the ref to an earlier state, clobbering @@ -125,12 +123,12 @@ class GitOperationService _, status = Gitlab::Popen.popen( command, repository.path_to_repo) do |stdin| - stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00") + stdin.write("update #{ref}\x00#{newrev}\x00#{oldrev}\x00") end unless status.zero? raise Repository::CommitError.new( - "Could not update branch #{name.sub('refs/heads/', '')}." \ + "Could not update branch #{Gitlab::Git.branch_name(ref)}." \ " Please refresh and try again.") end end diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb index 3cd515e4a3a..d3df3f1bca1 100644 --- a/lib/gitlab/git.rb +++ b/lib/gitlab/git.rb @@ -6,7 +6,7 @@ module Gitlab class << self def ref_name(ref) - ref.gsub(/\Arefs\/(tags|heads)\//, '') + ref.sub(/\Arefs\/(tags|heads)\//, '') end def branch_name(ref) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index a61d7f0c76d..65e96351033 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -829,7 +829,6 @@ describe Repository, models: true do context 'when target branch is different from source branch' do before do allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) - allow(repository).to receive(:update_ref!) end it 'expires branch cache' do @@ -1474,16 +1473,16 @@ describe Repository, models: true do end end - describe '#update_ref!' do + describe '#update_ref' do it 'can create a ref' do - GitOperationService.new(nil, repository).send(:update_ref!, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) expect(repository.find_branch('foobar')).not_to be_nil end it 'raises CommitError when the ref update fails' do expect do - GitOperationService.new(nil, repository).send(:update_ref!, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + GitOperationService.new(nil, repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) end.to raise_error(Repository::CommitError) end end diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index 2b31efbf631..5ef8cf1105b 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -108,7 +108,7 @@ describe GitGarbageCollectWorker do parents: [old_commit], ) GitOperationService.new(nil, project.repository).send( - :update_ref!, + :update_ref, "refs/heads/#{SecureRandom.hex(6)}", new_commit_sha, Gitlab::Git::BLANK_SHA -- cgit v1.2.1 From 944a8fa4d204ce7e9967f372a61657e75b4e88a0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 14 Dec 2016 03:00:16 +0800 Subject: Use branch_exists? to check branches Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_19747922 --- app/services/git_operation_service.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 68b28231595..b00fbcf9a79 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -150,14 +150,14 @@ class GitOperationService ' :source_project is different from current project' end - unless source_project.repository.commit(source_branch_name).try(:sha) + unless source_project.repository.branch_exists?(source_branch_name) raise Repository::CommitError.new( "Cannot find branch #{branch_name} nor" \ " #{source_branch_name} from" \ " #{source_project.path_with_namespace}") end elsif source_branch_name - unless repository.commit(source_branch_name).try(:sha) + unless repository.branch_exists?(source_branch_name) raise Repository::CommitError.new( "Cannot find branch #{branch_name} nor" \ " #{source_branch_name} from" \ -- cgit v1.2.1 From 56d131dcd52cba98e0eee253cab8bbf0b3b706df Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 14 Dec 2016 03:03:20 +0800 Subject: Use ArgumentError error instead because it's a bug if it happens. Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_19747933 --- app/services/git_operation_service.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index b00fbcf9a79..0d1bd05e552 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -151,17 +151,17 @@ class GitOperationService end unless source_project.repository.branch_exists?(source_branch_name) - raise Repository::CommitError.new( + raise ArgumentError, "Cannot find branch #{branch_name} nor" \ " #{source_branch_name} from" \ - " #{source_project.path_with_namespace}") + " #{source_project.path_with_namespace}" end elsif source_branch_name unless repository.branch_exists?(source_branch_name) - raise Repository::CommitError.new( + raise ArgumentError, "Cannot find branch #{branch_name} nor" \ " #{source_branch_name} from" \ - " #{repository.project.path_with_namespace}") + " #{repository.project.path_with_namespace}" end end end -- cgit v1.2.1 From 99b556976370bfe0c052d15b6a8f0642256173fd Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 14 Dec 2016 03:33:43 +0800 Subject: Try to use those @mr variables for full correctness --- app/controllers/concerns/creates_commit.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index a94077c2bd4..f0e6fc4b3e8 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,14 +4,16 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables - source_branch = @ref if @ref && @repository.find_branch(@ref) + source_branch = @ref if @ref && + @mr_source_project.repository.branch_exists?(@ref) commit_params = @commit_params.merge( - source_project: @tree_edit_project, + source_project: @mr_source_project, source_branch: source_branch, - target_branch: @target_branch + target_branch: @mr_target_branch ) - result = service.new(@project, current_user, commit_params).execute + result = service.new( + @mr_target_project, current_user, commit_params).execute if result[:status] == :success update_flash_notice(success_notice) -- cgit v1.2.1 From 14c4db2ae4efa1187476f11569df1b77c9c055fa Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 4 Jan 2017 22:31:06 +0800 Subject: Add a comment to explain why newrev should be updated Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_20301332 --- app/services/git_operation_service.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 0d1bd05e552..5acfdb0f9e2 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -1,4 +1,3 @@ - class GitOperationService attr_reader :user, :repository @@ -28,6 +27,9 @@ class GitOperationService with_hooks(ref, newrev, oldrev) do |service| raw_tag = repository.rugged.tags.create(tag_name, newrev, options) + + # If raw_tag is an annotated tag, we'll need to update newrev to point + # to the new revision. service.newrev = raw_tag.target_id end end -- cgit v1.2.1 From c1a75c3c0b59af7a9e6af3ff834adf56256aa2ce Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 4 Jan 2017 22:32:36 +0800 Subject: Prefer leading dots over trailing dots Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_20601323 --- app/controllers/projects/compare_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 325987199fa..d359920c91e 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -37,8 +37,8 @@ class Projects::CompareController < Projects::ApplicationController end def define_diff_vars - @compare = CompareService.new(@project, @head_ref). - execute(@project, @start_ref) + @compare = CompareService.new(@project, @head_ref) + .execute(@project, @start_ref) if @compare @commits = @compare.commits -- cgit v1.2.1 From ecac2f1122f093455e7b9e5d054157241dcd5cff Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Wed, 4 Jan 2017 22:50:01 +0800 Subject: Update the comment: Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_20876648 --- app/services/git_operation_service.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 5acfdb0f9e2..f36c0b082e4 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -26,10 +26,12 @@ class GitOperationService oldrev = Gitlab::Git::BLANK_SHA with_hooks(ref, newrev, oldrev) do |service| + # We want to pass the OID of the tag object to the hooks. For an + # annotated tag we don't know that OID until after the tag object + # (raw_tag) is created in the repository. That is why we have to + # update the value after creating the tag object. Only the + # "post-receive" hook will receive the correct value in this case. raw_tag = repository.rugged.tags.create(tag_name, newrev, options) - - # If raw_tag is an annotated tag, we'll need to update newrev to point - # to the new revision. service.newrev = raw_tag.target_id end end -- cgit v1.2.1 From 05d742a047cca3ded10e6e3a545e211a3592c89c Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 5 Jan 2017 01:10:35 +0800 Subject: Indent the way rubocop likes --- app/controllers/concerns/creates_commit.rb | 4 ++-- app/models/repository.rb | 2 +- app/services/git_operation_service.rb | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 58de0917049..9d41c559b0b 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,8 +4,8 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables - source_branch = @ref if @ref && - @mr_source_project.repository.branch_exists?(@ref) + source_branch = @ref if + @ref && @mr_source_project.repository.branch_exists?(@ref) commit_params = @commit_params.merge( source_project: @mr_source_project, source_branch: source_branch, diff --git a/app/models/repository.rb b/app/models/repository.rb index b01437acd8e..46995bdcb33 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -781,7 +781,7 @@ class Repository end if source_branch_name && - source_project.repository.tree_entry_at(source_branch_name, path) + source_project.repository.tree_entry_at(source_branch_name, path) raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) end end diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index f36c0b082e4..00c85112873 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -90,8 +90,8 @@ class GitOperationService # If repo was empty expire cache repository.after_create if was_empty - repository.after_create_branch if was_empty || - Gitlab::Git.blank_ref?(oldrev) + repository.after_create_branch if + was_empty || Gitlab::Git.blank_ref?(oldrev) newrev end -- cgit v1.2.1 From 99ac0935271b1e99f4512e496104219045f1018e Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 5 Jan 2017 01:52:21 +0800 Subject: Introduce Repository#with_repo_branch_commit We merge repository checks inside it so we don't have to check it on the call site, and we could also load the commit for the caller. This greatly reduce code duplication. Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_20572919 --- app/models/repository.rb | 25 ++++++++++++++++--------- app/services/compare_service.rb | 18 ++++++------------ app/services/git_operation_service.rb | 18 ++++++------------ 3 files changed, 28 insertions(+), 33 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 46995bdcb33..b1a789492d3 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1063,19 +1063,26 @@ class Repository Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip) end - def with_tmp_ref(source_repository, source_branch_name) - tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" + def with_repo_branch_commit(source_repository, source_branch_name) + branch_name_or_sha = + if source_repository == self + source_branch_name + else + tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" - fetch_ref( - source_repository.path_to_repo, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch_name}", - tmp_ref - ) + fetch_ref( + source_repository.path_to_repo, + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch_name}", + tmp_ref + ) + + source_repository.commit(source_branch_name).sha + end - yield + yield(commit(branch_name_or_sha)) ensure - rugged.references.delete(tmp_ref) + rugged.references.delete(tmp_ref) if tmp_ref end def fetch_ref(source_path, source_ref, target_ref) diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 31c371c4b34..d3d613661a6 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -11,19 +11,13 @@ class CompareService end def execute(target_project, target_branch, straight: false) - source_sha = source_project.repository. - commit(source_branch_name).try(:sha) - - return unless source_sha - # If compare with other project we need to fetch ref first - if target_project == source_project - compare(source_sha, target_project, target_branch, straight) - else - target_project.repository.with_tmp_ref( - source_project.repository, source_branch_name) do - compare(source_sha, target_project, target_branch, straight) - end + target_project.repository.with_repo_branch_commit( + source_project.repository, + source_branch_name) do |commit| + break unless commit + + compare(commit.sha, target_project, target_branch, straight) end end diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 00c85112873..ed9822cfee6 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -43,23 +43,17 @@ class GitOperationService def with_branch( branch_name, source_branch_name: nil, - source_project: repository.project) + source_project: repository.project, + &block) check_with_branch_arguments!( branch_name, source_branch_name, source_project) - source_commit = source_project.repository.find_branch( - source_branch_name || branch_name).try(:dereferenced_target) - update_branch_with_hooks(branch_name) do - if repository.project == source_project - yield(source_commit) - else - repository.with_tmp_ref( - source_project.repository, source_branch_name) do - yield(source_commit) - end - end + repository.with_repo_branch_commit( + source_project.repository, + source_branch_name || branch_name, + &block) end end -- cgit v1.2.1 From e01c692a35d817c09416356b549f473f63d78dc8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 5 Jan 2017 02:53:45 +0800 Subject: Remove tag with git hooks --- app/models/repository.rb | 19 +++++++++++-------- app/services/delete_tag_service.rb | 2 +- app/services/git_operation_service.rb | 12 +++++++++++- spec/models/repository_spec.rb | 5 +++-- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index b1a789492d3..e834936aa93 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -196,16 +196,14 @@ class Repository true end - # TODO: why we don't pass user here? - def rm_tag(tag_name) + def rm_tag(user, tag_name) before_remove_tag + tag = find_tag(tag_name) - begin - rugged.tags.delete(tag_name) - true - rescue Rugged::ReferenceError - false - end + GitOperationService.new(user, self).rm_tag(tag) + + after_remove_tag + true end def ref_names @@ -401,6 +399,11 @@ class Repository repository_event(:remove_tag) end + # Runs code after removing a tag. + def after_remove_tag + expire_tags_cache + end + def before_import expire_content_cache end diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb index a44dee14a0f..9d4bffb93e9 100644 --- a/app/services/delete_tag_service.rb +++ b/app/services/delete_tag_service.rb @@ -7,7 +7,7 @@ class DeleteTagService < BaseService return error('No such tag', 404) end - if repository.rm_tag(tag_name) + if repository.rm_tag(current_user, tag_name) release = project.releases.find_by(tag: tag_name) release.destroy if release diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index ed9822cfee6..3b7f702e3ab 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -15,7 +15,7 @@ class GitOperationService def rm_branch(branch) ref = Gitlab::Git::BRANCH_REF_PREFIX + branch.name - oldrev = branch.dereferenced_target.id + oldrev = branch.target newrev = Gitlab::Git::BLANK_SHA update_ref_in_hooks(ref, newrev, oldrev) @@ -36,6 +36,16 @@ class GitOperationService end end + def rm_tag(tag) + ref = Gitlab::Git::TAG_REF_PREFIX + tag.name + oldrev = tag.target + newrev = Gitlab::Git::BLANK_SHA + + update_ref_in_hooks(ref, newrev, oldrev) do + repository.rugged.tags.delete(tag_name) + end + end + # Whenever `source_branch_name` is passed, if `branch_name` doesn't exist, # it would be created from `source_branch_name`. # If `source_project` is passed, and the branch doesn't exist, diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index b1fee342b57..0f43c5c019a 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1419,9 +1419,10 @@ describe Repository, models: true do describe '#rm_tag' do it 'removes a tag' do expect(repository).to receive(:before_remove_tag) - expect(repository.rugged.tags).to receive(:delete).with('v1.1.0') - repository.rm_tag('v1.1.0') + repository.rm_tag(create(:user), 'v1.1.0') + + expect(repository.find_tag('v1.1.0')).to be_nil end end -- cgit v1.2.1 From 9244c81bc5f1c74966cb3ecb7b099fe7fd33689f Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 5 Jan 2017 03:01:16 +0800 Subject: I think I am really confused, should be @tree_edit_project Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_20571990 --- app/controllers/concerns/creates_commit.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 9d41c559b0b..a2e1d7c4653 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -13,7 +13,7 @@ module CreatesCommit ) result = service.new( - @mr_target_project, current_user, commit_params).execute + @tree_edit_project, current_user, commit_params).execute if result[:status] == :success update_flash_notice(success_notice) -- cgit v1.2.1 From 0b3b56b34d959322cced8a317138945c685015b8 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 5 Jan 2017 22:58:04 +0800 Subject: Merge request terms are reversed for GitOperationService This is seriously confusing but a target branch in merge request, is actually the source branch for GitOperationService, which is the base branch. The source branch in a merge request, is the target branch for GitOperationService, which is where we want to make commits. Perhaps we should rename source branch in GitOperationService to base branch, and target branch to committing branch. --- app/controllers/concerns/creates_commit.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index a2e1d7c4653..025fca088bf 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -5,15 +5,15 @@ module CreatesCommit set_commit_variables source_branch = @ref if - @ref && @mr_source_project.repository.branch_exists?(@ref) + @ref && @mr_target_project.repository.branch_exists?(@ref) commit_params = @commit_params.merge( - source_project: @mr_source_project, + source_project: @mr_target_project, source_branch: source_branch, - target_branch: @mr_target_branch + target_branch: @mr_source_branch ) result = service.new( - @tree_edit_project, current_user, commit_params).execute + @mr_source_project, current_user, commit_params).execute if result[:status] == :success update_flash_notice(success_notice) -- cgit v1.2.1 From 5e12b3d841b0da1a2c6047de53a033107bbb5c32 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Thu, 5 Jan 2017 23:49:11 +0800 Subject: Prefer leading dots over trailing dots --- app/services/create_branch_service.rb | 4 ++-- app/workers/emails_on_push_worker.rb | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb index 1b5e504573a..77459d8779d 100644 --- a/app/services/create_branch_service.rb +++ b/app/services/create_branch_service.rb @@ -1,7 +1,7 @@ class CreateBranchService < BaseService def execute(branch_name, ref) - result = ValidateNewBranchService.new(project, current_user). - execute(branch_name) + result = ValidateNewBranchService.new(project, current_user) + .execute(branch_name) return result if result[:status] == :error diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index d4c3f14ec06..f5ccc84c160 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -33,15 +33,15 @@ class EmailsOnPushWorker reverse_compare = false if action == :push - compare = CompareService.new(project, after_sha). - execute(project, before_sha) + compare = CompareService.new(project, after_sha) + .execute(project, before_sha) diff_refs = compare.diff_refs return false if compare.same if compare.commits.empty? - compare = CompareService.new(project, before_sha). - execute(project, after_sha) + compare = CompareService.new(project, before_sha) + .execute(project, after_sha) diff_refs = compare.diff_refs reverse_compare = true -- cgit v1.2.1 From ae86a1b9d3c9ca4ce592fa89085acd059ffc09a0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 6 Jan 2017 02:11:27 +0800 Subject: Just trust set_commit_variables to set everything! Removing those weird setup in assign_change_commit_vars fixed all the failures in the tests. I still cannot say why but clearly we need to have better names. It's so confusing right now. We should seriously stop fiddling those instance variables. --- app/controllers/concerns/creates_commit.rb | 4 +--- app/controllers/projects/commit_controller.rb | 8 +++----- app/services/commits/change_service.rb | 3 ++- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 025fca088bf..eafaed8a3d0 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,11 +4,9 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables - source_branch = @ref if - @ref && @mr_target_project.repository.branch_exists?(@ref) commit_params = @commit_params.merge( source_project: @mr_target_project, - source_branch: source_branch, + source_branch: @mr_target_branch, target_branch: @mr_source_branch ) diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 791ed88db30..44f34006049 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -40,7 +40,7 @@ class Projects::CommitController < Projects::ApplicationController end def revert - assign_change_commit_vars(@commit.revert_branch_name) + assign_change_commit_vars return render_404 if @target_branch.blank? @@ -49,7 +49,7 @@ class Projects::CommitController < Projects::ApplicationController end def cherry_pick - assign_change_commit_vars(@commit.cherry_pick_branch_name) + assign_change_commit_vars return render_404 if @target_branch.blank? @@ -110,11 +110,9 @@ class Projects::CommitController < Projects::ApplicationController @ci_pipelines = project.pipelines.where(sha: commit.sha) end - def assign_change_commit_vars(mr_source_branch) + def assign_change_commit_vars @commit = project.commit(params[:id]) @target_branch = params[:target_branch] - @mr_source_branch = mr_source_branch - @mr_target_branch = @target_branch @commit_params = { commit: @commit, create_merge_request: params[:create_merge_request].present? || different_project? diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 9b241aa8b04..60bd59a5d9f 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -5,6 +5,7 @@ module Commits def execute @source_project = params[:source_project] || @project + @source_branch = params[:source_branch] @target_branch = params[:target_branch] @commit = params[:commit] @create_merge_request = params[:create_merge_request].present? @@ -38,7 +39,7 @@ module Commits into, tree_id, source_project: @source_project, - source_branch_name: @target_branch) + source_branch_name: @source_branch) success else -- cgit v1.2.1 From a30f278bdee399346f199ada0e33f5c2d233d861 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 6 Jan 2017 04:18:51 +0800 Subject: Fix for initial commit and remove unneeded args --- app/controllers/concerns/creates_commit.rb | 12 +++++++++--- app/services/commits/change_service.rb | 11 +++++++++-- lib/api/commits.rb | 2 -- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index eafaed8a3d0..f5f9cdeaec5 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,9 +4,10 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables + source_branch = @mr_target_branch unless initial_commit? commit_params = @commit_params.merge( source_project: @mr_target_project, - source_branch: @mr_target_branch, + source_branch: source_branch, target_branch: @mr_source_branch ) @@ -113,7 +114,7 @@ module CreatesCommit else # Merge request to this project @mr_target_project = @project - @mr_target_branch ||= @ref + @mr_target_branch = @ref || @target_branch end else # Edit file in fork @@ -121,7 +122,12 @@ module CreatesCommit # Merge request from fork to this project @mr_source_project = @tree_edit_project @mr_target_project = @project - @mr_target_branch ||= @ref + @mr_target_branch = @ref || @target_branch end end + + def initial_commit? + @mr_target_branch.nil? || + !@mr_target_project.repository.branch_exists?(@mr_target_branch) + end end diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 60bd59a5d9f..4a5d8029413 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -26,8 +26,15 @@ module Commits def commit_change(action) raise NotImplementedError unless repository.respond_to?(action) - into = @create_merge_request ? @commit.public_send("#{action}_branch_name") : @target_branch - tree_id = repository.public_send("check_#{action}_content", @commit, @target_branch) + if @create_merge_request + into = @commit.public_send("#{action}_branch_name") + tree_branch = @source_branch + else + into = tree_branch = @target_branch + end + + tree_id = repository.public_send( + "check_#{action}_content", @commit, tree_branch) if tree_id validate_target_branch(into) if @create_merge_request diff --git a/lib/api/commits.rb b/lib/api/commits.rb index cf2489dbb67..2c1da0902c9 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -140,8 +140,6 @@ module API commit_params = { commit: commit, create_merge_request: false, - source_project: user_project, - source_branch: commit.cherry_pick_branch_name, target_branch: params[:branch] } -- cgit v1.2.1 From dea589d635d4c41fbf0db721b132ea466c34cb4a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 6 Jan 2017 04:41:17 +0800 Subject: Prefer leading dots over trailing dots --- app/models/merge_request_diff.rb | 4 ++-- app/services/commits/change_service.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index 7946d8e123e..00c2a3695af 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -169,8 +169,8 @@ class MergeRequestDiff < ActiveRecord::Base # When compare merge request versions we want diff A..B instead of A...B # so we handle cases when user does squash and rebase of the commits between versions. # For this reason we set straight to true by default. - CompareService.new(project, head_commit_sha). - execute(project, sha, straight: straight) + CompareService.new(project, head_commit_sha) + .execute(project, sha, straight: straight) end def commits_count diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 4a5d8029413..8d1dfbcea7d 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -70,8 +70,8 @@ module Commits # Temporary branch exists and contains the change commit return if repository.find_branch(new_branch) - result = ValidateNewBranchService.new(@project, current_user). - execute(new_branch) + result = ValidateNewBranchService.new(@project, current_user) + .execute(new_branch) if result[:status] == :error raise ChangeError, "There was an error creating the source branch: #{result[:message]}" -- cgit v1.2.1 From 593228ffe3b2e4ff82c4d63e5d5c59b835f70085 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 6 Jan 2017 20:59:38 +0800 Subject: Don't set invalid @mr_source_branch when create_merge_request? --- app/controllers/concerns/creates_commit.rb | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index f5f9cdeaec5..258791bb5cd 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -91,16 +91,13 @@ module CreatesCommit @mr_source_project != @mr_target_project end - def different_branch? - @mr_source_branch != @mr_target_branch || different_project? - end - def create_merge_request? - params[:create_merge_request].present? && different_branch? + params[:create_merge_request].present? end + # TODO: We should really clean this up def set_commit_variables - @mr_source_branch ||= @target_branch + @mr_source_branch = @target_branch unless create_merge_request? if can?(current_user, :push_code, @project) # Edit file in this project -- cgit v1.2.1 From a4b97b2cb61c03d08e25cf2cd7fcbb3f21611350 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 6 Jan 2017 22:05:30 +0800 Subject: Rename source to base to avoid confusion from MR --- app/controllers/concerns/creates_commit.rb | 6 +-- app/models/repository.rb | 78 +++++++++++++++--------------- app/services/commits/change_service.rb | 10 ++-- app/services/compare_service.rb | 12 ++--- app/services/files/base_service.rb | 10 ++-- app/services/files/create_dir_service.rb | 4 +- app/services/files/create_service.rb | 6 +-- app/services/files/delete_service.rb | 4 +- app/services/files/multi_service.rb | 6 +-- app/services/files/update_service.rb | 6 +-- app/services/git_operation_service.rb | 40 +++++++-------- 11 files changed, 91 insertions(+), 91 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 258791bb5cd..c503f8bf696 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,10 +4,10 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables - source_branch = @mr_target_branch unless initial_commit? + base_branch = @mr_target_branch unless initial_commit? commit_params = @commit_params.merge( - source_project: @mr_target_project, - source_branch: source_branch, + base_project: @mr_target_project, + base_branch: base_branch, target_branch: @mr_source_branch ) diff --git a/app/models/repository.rb b/app/models/repository.rb index e834936aa93..a335c629a78 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -748,12 +748,12 @@ class Repository user, path, message:, branch_name:, author_email: nil, author_name: nil, - source_branch_name: nil, source_project: project) + base_branch_name: nil, base_project: project) check_tree_entry_for_dir(branch_name, path) - if source_branch_name - source_project.repository. - check_tree_entry_for_dir(source_branch_name, path) + if base_branch_name + base_project.repository. + check_tree_entry_for_dir(base_branch_name, path) end commit_file( @@ -765,8 +765,8 @@ class Repository update: false, author_email: author_email, author_name: author_name, - source_branch_name: source_branch_name, - source_project: source_project) + base_branch_name: base_branch_name, + base_project: base_project) end # rubocop:enable Metrics/ParameterLists @@ -775,7 +775,7 @@ class Repository user, path, content, message:, branch_name:, update: true, author_email: nil, author_name: nil, - source_branch_name: nil, source_project: project) + base_branch_name: nil, base_project: project) unless update error_message = "Filename already exists; update not allowed" @@ -783,8 +783,8 @@ class Repository raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) end - if source_branch_name && - source_project.repository.tree_entry_at(source_branch_name, path) + if base_branch_name && + base_project.repository.tree_entry_at(base_branch_name, path) raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) end end @@ -795,8 +795,8 @@ class Repository branch_name: branch_name, author_email: author_email, author_name: author_name, - source_branch_name: source_branch_name, - source_project: source_project, + base_branch_name: base_branch_name, + base_project: base_project, actions: [{ action: :create, file_path: path, content: content }]) @@ -808,7 +808,7 @@ class Repository user, path, content, message:, branch_name:, previous_path:, author_email: nil, author_name: nil, - source_branch_name: nil, source_project: project) + base_branch_name: nil, base_project: project) action = if previous_path && previous_path != path :move else @@ -821,8 +821,8 @@ class Repository branch_name: branch_name, author_email: author_email, author_name: author_name, - source_branch_name: source_branch_name, - source_project: source_project, + base_branch_name: base_branch_name, + base_project: base_project, actions: [{ action: action, file_path: path, content: content, @@ -835,15 +835,15 @@ class Repository user, path, message:, branch_name:, author_email: nil, author_name: nil, - source_branch_name: nil, source_project: project) + base_branch_name: nil, base_project: project) multi_action( user: user, message: message, branch_name: branch_name, author_email: author_email, author_name: author_name, - source_branch_name: source_branch_name, - source_project: source_project, + base_branch_name: base_branch_name, + base_project: base_project, actions: [{ action: :delete, file_path: path }]) end @@ -853,16 +853,16 @@ class Repository def multi_action( user:, branch_name:, message:, actions:, author_email: nil, author_name: nil, - source_branch_name: nil, source_project: project) + base_branch_name: nil, base_project: project) GitOperationService.new(user, self).with_branch( branch_name, - source_branch_name: source_branch_name, - source_project: source_project) do |source_commit| + base_branch_name: base_branch_name, + base_project: base_project) do |base_commit| index = rugged.index - parents = if source_commit - index.read_tree(source_commit.raw_commit.tree) - [source_commit.sha] + parents = if base_commit + index.read_tree(base_commit.raw_commit.tree) + [base_commit.sha] else [] end @@ -910,8 +910,8 @@ class Repository def merge(user, merge_request, options = {}) GitOperationService.new(user, self).with_branch( - merge_request.target_branch) do |source_commit| - our_commit = source_commit.sha + merge_request.target_branch) do |base_commit| + our_commit = base_commit.sha their_commit = merge_request.diff_head_sha raise 'Invalid merge target' unless our_commit @@ -935,15 +935,15 @@ class Repository def revert( user, commit, branch_name, revert_tree_id = nil, - source_branch_name: nil, source_project: project) + base_branch_name: nil, base_project: project) revert_tree_id ||= check_revert_content(commit, branch_name) return false unless revert_tree_id GitOperationService.new(user, self).with_branch( branch_name, - source_branch_name: source_branch_name, - source_project: source_project) do |source_commit| + base_branch_name: base_branch_name, + base_project: base_project) do |base_commit| committer = user_to_committer(user) @@ -952,21 +952,21 @@ class Repository author: committer, committer: committer, tree: revert_tree_id, - parents: [source_commit.sha]) + parents: [base_commit.sha]) end end def cherry_pick( user, commit, branch_name, cherry_pick_tree_id = nil, - source_branch_name: nil, source_project: project) + base_branch_name: nil, base_project: project) cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name) return false unless cherry_pick_tree_id GitOperationService.new(user, self).with_branch( branch_name, - source_branch_name: source_branch_name, - source_project: source_project) do |source_commit| + base_branch_name: base_branch_name, + base_project: base_project) do |base_commit| committer = user_to_committer(user) @@ -979,7 +979,7 @@ class Repository }, committer: committer, tree: cherry_pick_tree_id, - parents: [source_commit.sha]) + parents: [base_commit.sha]) end end @@ -1066,20 +1066,20 @@ class Repository Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip) end - def with_repo_branch_commit(source_repository, source_branch_name) + def with_repo_branch_commit(base_repository, base_branch_name) branch_name_or_sha = - if source_repository == self - source_branch_name + if base_repository == self + base_branch_name else tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" fetch_ref( - source_repository.path_to_repo, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{source_branch_name}", + base_repository.path_to_repo, + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{base_branch_name}", tmp_ref ) - source_repository.commit(source_branch_name).sha + base_repository.commit(base_branch_name).sha end yield(commit(branch_name_or_sha)) diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 8d1dfbcea7d..1faa052e0ca 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -4,8 +4,8 @@ module Commits class ChangeError < StandardError; end def execute - @source_project = params[:source_project] || @project - @source_branch = params[:source_branch] + @base_project = params[:base_project] || @project + @base_branch = params[:base_branch] @target_branch = params[:target_branch] @commit = params[:commit] @create_merge_request = params[:create_merge_request].present? @@ -28,7 +28,7 @@ module Commits if @create_merge_request into = @commit.public_send("#{action}_branch_name") - tree_branch = @source_branch + tree_branch = @base_branch else into = tree_branch = @target_branch end @@ -45,8 +45,8 @@ module Commits @commit, into, tree_id, - source_project: @source_project, - source_branch_name: @source_branch) + base_project: @base_project, + base_branch_name: @base_branch) success else diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index d3d613661a6..2e4f8ee9dc8 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,18 +3,18 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::Git::Compare object that responds to commits and diffs class CompareService - attr_reader :source_project, :source_branch_name + attr_reader :base_project, :base_branch_name - def initialize(new_source_project, new_source_branch_name) - @source_project = new_source_project - @source_branch_name = new_source_branch_name + def initialize(new_base_project, new_base_branch_name) + @base_project = new_base_project + @base_branch_name = new_base_branch_name end def execute(target_project, target_branch, straight: false) # If compare with other project we need to fetch ref first target_project.repository.with_repo_branch_commit( - source_project.repository, - source_branch_name) do |commit| + base_project.repository, + base_branch_name) do |commit| break unless commit compare(commit.sha, target_project, target_branch, straight) diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 80e1d1d60f2..89f7dcbaa87 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -3,8 +3,8 @@ module Files class ValidationError < StandardError; end def execute - @source_project = params[:source_project] || @project - @source_branch = params[:source_branch] + @base_project = params[:base_project] || @project + @base_branch = params[:base_branch] @target_branch = params[:target_branch] @commit_message = params[:commit_message] @@ -22,7 +22,7 @@ module Files # Validate parameters validate - # Create new branch if it different from source_branch + # Create new branch if it different from base_branch validate_target_branch if different_branch? result = commit @@ -38,7 +38,7 @@ module Files private def different_branch? - @source_branch != @target_branch || @source_project != @project + @base_branch != @target_branch || @base_project != @project end def file_has_changed? @@ -59,7 +59,7 @@ module Files end unless project.empty_repo? - unless @source_project.repository.branch_exists?(@source_branch) + unless @base_project.repository.branch_exists?(@base_branch) raise_error('You can only create or edit files when you are on a branch') end diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index ee4e130a38f..53b6d456e0d 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -8,8 +8,8 @@ module Files branch_name: @target_branch, author_email: @author_email, author_name: @author_name, - source_project: @source_project, - source_branch_name: @source_branch) + base_project: @base_project, + base_branch_name: @base_branch) end def validate diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 853c471666d..270dc6471aa 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -10,8 +10,8 @@ module Files update: false, author_email: @author_email, author_name: @author_name, - source_project: @source_project, - source_branch_name: @source_branch) + base_project: @base_project, + base_branch_name: @base_branch) end def validate @@ -34,7 +34,7 @@ module Files unless project.empty_repo? @file_path.slice!(0) if @file_path.start_with?('/') - blob = repository.blob_at_branch(@source_branch, @file_path) + blob = repository.blob_at_branch(@base_branch, @file_path) if blob raise_error('Your changes could not be committed because a file with the same name already exists') diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index cfe532d49b3..d5341b9e197 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -8,8 +8,8 @@ module Files branch_name: @target_branch, author_email: @author_email, author_name: @author_name, - source_project: @source_project, - source_branch_name: @source_branch) + base_project: @base_project, + base_branch_name: @base_branch) end end end diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index f77e5d91103..ca13b887e06 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -10,8 +10,8 @@ module Files actions: params[:actions], author_email: @author_email, author_name: @author_name, - source_project: @source_project, - source_branch_name: @source_branch + base_project: @base_project, + base_branch_name: @base_branch ) end @@ -63,7 +63,7 @@ module Files end def last_commit - Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path) + Gitlab::Git::Commit.last_for_path(repository, @base_branch, @file_path) end def regex_check(file) diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index 5f671817cdb..f546b169550 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -9,8 +9,8 @@ module Files previous_path: @previous_path, author_email: @author_email, author_name: @author_name, - source_project: @source_project, - source_branch_name: @source_branch) + base_project: @base_project, + base_branch_name: @base_branch) end private @@ -25,7 +25,7 @@ module Files def last_commit @last_commit ||= Gitlab::Git::Commit. - last_for_path(@source_project.repository, @source_branch, @file_path) + last_for_path(@base_project.repository, @base_branch, @file_path) end end end diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 3b7f702e3ab..ec23407544c 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -46,23 +46,23 @@ class GitOperationService end end - # Whenever `source_branch_name` is passed, if `branch_name` doesn't exist, - # it would be created from `source_branch_name`. - # If `source_project` is passed, and the branch doesn't exist, - # it would try to find the source from it instead of current repository. + # Whenever `base_branch_name` is passed, if `branch_name` doesn't exist, + # it would be created from `base_branch_name`. + # If `base_project` is passed, and the branch doesn't exist, + # it would try to find the base from it instead of current repository. def with_branch( branch_name, - source_branch_name: nil, - source_project: repository.project, + base_branch_name: nil, + base_project: repository.project, &block) check_with_branch_arguments!( - branch_name, source_branch_name, source_project) + branch_name, base_branch_name, base_project) update_branch_with_hooks(branch_name) do repository.with_repo_branch_commit( - source_project.repository, - source_branch_name || branch_name, + base_project.repository, + base_branch_name || branch_name, &block) end end @@ -148,27 +148,27 @@ class GitOperationService end def check_with_branch_arguments!( - branch_name, source_branch_name, source_project) + branch_name, base_branch_name, base_project) return if repository.branch_exists?(branch_name) - if repository.project != source_project - unless source_branch_name + if repository.project != base_project + unless base_branch_name raise ArgumentError, - 'Should also pass :source_branch_name if' + - ' :source_project is different from current project' + 'Should also pass :base_branch_name if' + + ' :base_project is different from current project' end - unless source_project.repository.branch_exists?(source_branch_name) + unless base_project.repository.branch_exists?(base_branch_name) raise ArgumentError, "Cannot find branch #{branch_name} nor" \ - " #{source_branch_name} from" \ - " #{source_project.path_with_namespace}" + " #{base_branch_name} from" \ + " #{base_project.path_with_namespace}" end - elsif source_branch_name - unless repository.branch_exists?(source_branch_name) + elsif base_branch_name + unless repository.branch_exists?(base_branch_name) raise ArgumentError, "Cannot find branch #{branch_name} nor" \ - " #{source_branch_name} from" \ + " #{base_branch_name} from" \ " #{repository.project.path_with_namespace}" end end -- cgit v1.2.1 From 358501df2d3229f68be700d2fc57cd3c3e7e5042 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 6 Jan 2017 22:20:02 +0800 Subject: Properly fix the edge case! --- app/controllers/concerns/creates_commit.rb | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index c503f8bf696..516b1cac6ef 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -97,8 +97,6 @@ module CreatesCommit # TODO: We should really clean this up def set_commit_variables - @mr_source_branch = @target_branch unless create_merge_request? - if can?(current_user, :push_code, @project) # Edit file in this project @tree_edit_project = @project @@ -121,10 +119,25 @@ module CreatesCommit @mr_target_project = @project @mr_target_branch = @ref || @target_branch end + + @mr_source_branch = guess_mr_source_branch end def initial_commit? @mr_target_branch.nil? || !@mr_target_project.repository.branch_exists?(@mr_target_branch) end + + def guess_mr_source_branch + # XXX: Happens when viewing a commit without a branch. In this case, + # @target_branch would be the default branch for @mr_source_project, + # however we want a generated new branch here. Thus we can't use + # @target_branch, but should pass nil to indicate that we want a new + # branch instead of @target_branch. + return if + create_merge_request? && + @mr_source_project.repository.branch_exists?(@target_branch) + + @target_branch + end end -- cgit v1.2.1 From e3c36850a618ee2f7f9087b681e62d8a50e7b1b1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 6 Jan 2017 22:49:23 +0800 Subject: Detect if we really want a new merge request properly --- app/controllers/concerns/creates_commit.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 516b1cac6ef..646d922cb24 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -92,7 +92,9 @@ module CreatesCommit end def create_merge_request? - params[:create_merge_request].present? + # XXX: Even if the field is set, if we're checking the same branch + # as the target branch, we don't want to create a merge request. + params[:create_merge_request].present? && @ref != @target_branch end # TODO: We should really clean this up @@ -136,7 +138,8 @@ module CreatesCommit # branch instead of @target_branch. return if create_merge_request? && - @mr_source_project.repository.branch_exists?(@target_branch) + # XXX: Don't understand why rubocop prefers this indention + @mr_source_project.repository.branch_exists?(@target_branch) @target_branch end -- cgit v1.2.1 From ccc73c455ba0b95b531c69414a6a1f47667f16b5 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 6 Jan 2017 23:29:13 +0800 Subject: Rename from base to start because base could mean merge base --- app/controllers/concerns/creates_commit.rb | 6 +-- app/models/repository.rb | 78 +++++++++++++++--------------- app/services/commits/change_service.rb | 10 ++-- app/services/compare_service.rb | 12 ++--- app/services/files/base_service.rb | 10 ++-- app/services/files/create_dir_service.rb | 4 +- app/services/files/create_service.rb | 6 +-- app/services/files/delete_service.rb | 4 +- app/services/files/multi_service.rb | 6 +-- app/services/files/update_service.rb | 6 +-- app/services/git_operation_service.rb | 40 +++++++-------- 11 files changed, 91 insertions(+), 91 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 646d922cb24..2ece99aebc0 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -4,10 +4,10 @@ module CreatesCommit def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil) set_commit_variables - base_branch = @mr_target_branch unless initial_commit? + start_branch = @mr_target_branch unless initial_commit? commit_params = @commit_params.merge( - base_project: @mr_target_project, - base_branch: base_branch, + start_project: @mr_target_project, + start_branch: start_branch, target_branch: @mr_source_branch ) diff --git a/app/models/repository.rb b/app/models/repository.rb index a335c629a78..f8bdfb602a9 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -748,12 +748,12 @@ class Repository user, path, message:, branch_name:, author_email: nil, author_name: nil, - base_branch_name: nil, base_project: project) + start_branch_name: nil, start_project: project) check_tree_entry_for_dir(branch_name, path) - if base_branch_name - base_project.repository. - check_tree_entry_for_dir(base_branch_name, path) + if start_branch_name + start_project.repository. + check_tree_entry_for_dir(start_branch_name, path) end commit_file( @@ -765,8 +765,8 @@ class Repository update: false, author_email: author_email, author_name: author_name, - base_branch_name: base_branch_name, - base_project: base_project) + start_branch_name: start_branch_name, + start_project: start_project) end # rubocop:enable Metrics/ParameterLists @@ -775,7 +775,7 @@ class Repository user, path, content, message:, branch_name:, update: true, author_email: nil, author_name: nil, - base_branch_name: nil, base_project: project) + start_branch_name: nil, start_project: project) unless update error_message = "Filename already exists; update not allowed" @@ -783,8 +783,8 @@ class Repository raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) end - if base_branch_name && - base_project.repository.tree_entry_at(base_branch_name, path) + if start_branch_name && + start_project.repository.tree_entry_at(start_branch_name, path) raise Gitlab::Git::Repository::InvalidBlobName.new(error_message) end end @@ -795,8 +795,8 @@ class Repository branch_name: branch_name, author_email: author_email, author_name: author_name, - base_branch_name: base_branch_name, - base_project: base_project, + start_branch_name: start_branch_name, + start_project: start_project, actions: [{ action: :create, file_path: path, content: content }]) @@ -808,7 +808,7 @@ class Repository user, path, content, message:, branch_name:, previous_path:, author_email: nil, author_name: nil, - base_branch_name: nil, base_project: project) + start_branch_name: nil, start_project: project) action = if previous_path && previous_path != path :move else @@ -821,8 +821,8 @@ class Repository branch_name: branch_name, author_email: author_email, author_name: author_name, - base_branch_name: base_branch_name, - base_project: base_project, + start_branch_name: start_branch_name, + start_project: start_project, actions: [{ action: action, file_path: path, content: content, @@ -835,15 +835,15 @@ class Repository user, path, message:, branch_name:, author_email: nil, author_name: nil, - base_branch_name: nil, base_project: project) + start_branch_name: nil, start_project: project) multi_action( user: user, message: message, branch_name: branch_name, author_email: author_email, author_name: author_name, - base_branch_name: base_branch_name, - base_project: base_project, + start_branch_name: start_branch_name, + start_project: start_project, actions: [{ action: :delete, file_path: path }]) end @@ -853,16 +853,16 @@ class Repository def multi_action( user:, branch_name:, message:, actions:, author_email: nil, author_name: nil, - base_branch_name: nil, base_project: project) + start_branch_name: nil, start_project: project) GitOperationService.new(user, self).with_branch( branch_name, - base_branch_name: base_branch_name, - base_project: base_project) do |base_commit| + start_branch_name: start_branch_name, + start_project: start_project) do |start_commit| index = rugged.index - parents = if base_commit - index.read_tree(base_commit.raw_commit.tree) - [base_commit.sha] + parents = if start_commit + index.read_tree(start_commit.raw_commit.tree) + [start_commit.sha] else [] end @@ -910,8 +910,8 @@ class Repository def merge(user, merge_request, options = {}) GitOperationService.new(user, self).with_branch( - merge_request.target_branch) do |base_commit| - our_commit = base_commit.sha + merge_request.target_branch) do |start_commit| + our_commit = start_commit.sha their_commit = merge_request.diff_head_sha raise 'Invalid merge target' unless our_commit @@ -935,15 +935,15 @@ class Repository def revert( user, commit, branch_name, revert_tree_id = nil, - base_branch_name: nil, base_project: project) + start_branch_name: nil, start_project: project) revert_tree_id ||= check_revert_content(commit, branch_name) return false unless revert_tree_id GitOperationService.new(user, self).with_branch( branch_name, - base_branch_name: base_branch_name, - base_project: base_project) do |base_commit| + start_branch_name: start_branch_name, + start_project: start_project) do |start_commit| committer = user_to_committer(user) @@ -952,21 +952,21 @@ class Repository author: committer, committer: committer, tree: revert_tree_id, - parents: [base_commit.sha]) + parents: [start_commit.sha]) end end def cherry_pick( user, commit, branch_name, cherry_pick_tree_id = nil, - base_branch_name: nil, base_project: project) + start_branch_name: nil, start_project: project) cherry_pick_tree_id ||= check_cherry_pick_content(commit, branch_name) return false unless cherry_pick_tree_id GitOperationService.new(user, self).with_branch( branch_name, - base_branch_name: base_branch_name, - base_project: base_project) do |base_commit| + start_branch_name: start_branch_name, + start_project: start_project) do |start_commit| committer = user_to_committer(user) @@ -979,7 +979,7 @@ class Repository }, committer: committer, tree: cherry_pick_tree_id, - parents: [base_commit.sha]) + parents: [start_commit.sha]) end end @@ -1066,20 +1066,20 @@ class Repository Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:strip) end - def with_repo_branch_commit(base_repository, base_branch_name) + def with_repo_branch_commit(start_repository, start_branch_name) branch_name_or_sha = - if base_repository == self - base_branch_name + if start_repository == self + start_branch_name else tmp_ref = "refs/tmp/#{SecureRandom.hex}/head" fetch_ref( - base_repository.path_to_repo, - "#{Gitlab::Git::BRANCH_REF_PREFIX}#{base_branch_name}", + start_repository.path_to_repo, + "#{Gitlab::Git::BRANCH_REF_PREFIX}#{start_branch_name}", tmp_ref ) - base_repository.commit(base_branch_name).sha + start_repository.commit(start_branch_name).sha end yield(commit(branch_name_or_sha)) diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb index 1faa052e0ca..25e22f14e60 100644 --- a/app/services/commits/change_service.rb +++ b/app/services/commits/change_service.rb @@ -4,8 +4,8 @@ module Commits class ChangeError < StandardError; end def execute - @base_project = params[:base_project] || @project - @base_branch = params[:base_branch] + @start_project = params[:start_project] || @project + @start_branch = params[:start_branch] @target_branch = params[:target_branch] @commit = params[:commit] @create_merge_request = params[:create_merge_request].present? @@ -28,7 +28,7 @@ module Commits if @create_merge_request into = @commit.public_send("#{action}_branch_name") - tree_branch = @base_branch + tree_branch = @start_branch else into = tree_branch = @target_branch end @@ -45,8 +45,8 @@ module Commits @commit, into, tree_id, - base_project: @base_project, - base_branch_name: @base_branch) + start_project: @start_project, + start_branch_name: @start_branch) success else diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb index 2e4f8ee9dc8..ab4c02a97a0 100644 --- a/app/services/compare_service.rb +++ b/app/services/compare_service.rb @@ -3,18 +3,18 @@ require 'securerandom' # Compare 2 branches for one repo or between repositories # and return Gitlab::Git::Compare object that responds to commits and diffs class CompareService - attr_reader :base_project, :base_branch_name + attr_reader :start_project, :start_branch_name - def initialize(new_base_project, new_base_branch_name) - @base_project = new_base_project - @base_branch_name = new_base_branch_name + def initialize(new_start_project, new_start_branch_name) + @start_project = new_start_project + @start_branch_name = new_start_branch_name end def execute(target_project, target_branch, straight: false) # If compare with other project we need to fetch ref first target_project.repository.with_repo_branch_commit( - base_project.repository, - base_branch_name) do |commit| + start_project.repository, + start_branch_name) do |commit| break unless commit compare(commit.sha, target_project, target_branch, straight) diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index 89f7dcbaa87..0a25f56d24c 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -3,8 +3,8 @@ module Files class ValidationError < StandardError; end def execute - @base_project = params[:base_project] || @project - @base_branch = params[:base_branch] + @start_project = params[:start_project] || @project + @start_branch = params[:start_branch] @target_branch = params[:target_branch] @commit_message = params[:commit_message] @@ -22,7 +22,7 @@ module Files # Validate parameters validate - # Create new branch if it different from base_branch + # Create new branch if it different from start_branch validate_target_branch if different_branch? result = commit @@ -38,7 +38,7 @@ module Files private def different_branch? - @base_branch != @target_branch || @base_project != @project + @start_branch != @target_branch || @start_project != @project end def file_has_changed? @@ -59,7 +59,7 @@ module Files end unless project.empty_repo? - unless @base_project.repository.branch_exists?(@base_branch) + unless @start_project.repository.branch_exists?(@start_branch) raise_error('You can only create or edit files when you are on a branch') end diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb index 53b6d456e0d..858de5f0538 100644 --- a/app/services/files/create_dir_service.rb +++ b/app/services/files/create_dir_service.rb @@ -8,8 +8,8 @@ module Files branch_name: @target_branch, author_email: @author_email, author_name: @author_name, - base_project: @base_project, - base_branch_name: @base_branch) + start_project: @start_project, + start_branch_name: @start_branch) end def validate diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb index 270dc6471aa..88dd7bbaedb 100644 --- a/app/services/files/create_service.rb +++ b/app/services/files/create_service.rb @@ -10,8 +10,8 @@ module Files update: false, author_email: @author_email, author_name: @author_name, - base_project: @base_project, - base_branch_name: @base_branch) + start_project: @start_project, + start_branch_name: @start_branch) end def validate @@ -34,7 +34,7 @@ module Files unless project.empty_repo? @file_path.slice!(0) if @file_path.start_with?('/') - blob = repository.blob_at_branch(@base_branch, @file_path) + blob = repository.blob_at_branch(@start_branch, @file_path) if blob raise_error('Your changes could not be committed because a file with the same name already exists') diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb index d5341b9e197..50f0ffcac9f 100644 --- a/app/services/files/delete_service.rb +++ b/app/services/files/delete_service.rb @@ -8,8 +8,8 @@ module Files branch_name: @target_branch, author_email: @author_email, author_name: @author_name, - base_project: @base_project, - base_branch_name: @base_branch) + start_project: @start_project, + start_branch_name: @start_branch) end end end diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb index ca13b887e06..6ba868df04d 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -10,8 +10,8 @@ module Files actions: params[:actions], author_email: @author_email, author_name: @author_name, - base_project: @base_project, - base_branch_name: @base_branch + start_project: @start_project, + start_branch_name: @start_branch ) end @@ -63,7 +63,7 @@ module Files end def last_commit - Gitlab::Git::Commit.last_for_path(repository, @base_branch, @file_path) + Gitlab::Git::Commit.last_for_path(repository, @start_branch, @file_path) end def regex_check(file) diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb index f546b169550..a71fe61a4b6 100644 --- a/app/services/files/update_service.rb +++ b/app/services/files/update_service.rb @@ -9,8 +9,8 @@ module Files previous_path: @previous_path, author_email: @author_email, author_name: @author_name, - base_project: @base_project, - base_branch_name: @base_branch) + start_project: @start_project, + start_branch_name: @start_branch) end private @@ -25,7 +25,7 @@ module Files def last_commit @last_commit ||= Gitlab::Git::Commit. - last_for_path(@base_project.repository, @base_branch, @file_path) + last_for_path(@start_project.repository, @start_branch, @file_path) end end end diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index ec23407544c..2b2ba0870a4 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -46,23 +46,23 @@ class GitOperationService end end - # Whenever `base_branch_name` is passed, if `branch_name` doesn't exist, - # it would be created from `base_branch_name`. - # If `base_project` is passed, and the branch doesn't exist, - # it would try to find the base from it instead of current repository. + # Whenever `start_branch_name` is passed, if `branch_name` doesn't exist, + # it would be created from `start_branch_name`. + # If `start_project` is passed, and the branch doesn't exist, + # it would try to find the commits from it instead of current repository. def with_branch( branch_name, - base_branch_name: nil, - base_project: repository.project, + start_branch_name: nil, + start_project: repository.project, &block) check_with_branch_arguments!( - branch_name, base_branch_name, base_project) + branch_name, start_branch_name, start_project) update_branch_with_hooks(branch_name) do repository.with_repo_branch_commit( - base_project.repository, - base_branch_name || branch_name, + start_project.repository, + start_branch_name || branch_name, &block) end end @@ -148,27 +148,27 @@ class GitOperationService end def check_with_branch_arguments!( - branch_name, base_branch_name, base_project) + branch_name, start_branch_name, start_project) return if repository.branch_exists?(branch_name) - if repository.project != base_project - unless base_branch_name + if repository.project != start_project + unless start_branch_name raise ArgumentError, - 'Should also pass :base_branch_name if' + - ' :base_project is different from current project' + 'Should also pass :start_branch_name if' + + ' :start_project is different from current project' end - unless base_project.repository.branch_exists?(base_branch_name) + unless start_project.repository.branch_exists?(start_branch_name) raise ArgumentError, "Cannot find branch #{branch_name} nor" \ - " #{base_branch_name} from" \ - " #{base_project.path_with_namespace}" + " #{start_branch_name} from" \ + " #{start_project.path_with_namespace}" end - elsif base_branch_name - unless repository.branch_exists?(base_branch_name) + elsif start_branch_name + unless repository.branch_exists?(start_branch_name) raise ArgumentError, "Cannot find branch #{branch_name} nor" \ - " #{base_branch_name} from" \ + " #{start_branch_name} from" \ " #{repository.project.path_with_namespace}" end end -- cgit v1.2.1 From 4c5ff1d08ef5ddb7db432b864e18dd7674fbc116 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Tue, 18 Oct 2016 17:46:48 -0500 Subject: add webpack, webpack-rails, and webpack-dev-server along with a simple hello world test Add the following line to GDK Procfile to play with it: webpack: exec support/exec-cd gitlab npm run dev-server --- .eslintignore | 3 +- Gemfile | 2 ++ Gemfile.lock | 3 ++ Procfile | 1 + app/assets/javascripts/webpack/bundle.js | 1 + app/assets/javascripts/webpack/hello_world.js | 3 ++ app/views/layouts/_head.html.haml | 1 + config/application.rb | 5 +++ config/webpack.config.js | 46 +++++++++++++++++++++++++++ package.json | 6 ++++ 10 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/webpack/bundle.js create mode 100644 app/assets/javascripts/webpack/hello_world.js create mode 100644 config/webpack.config.js diff --git a/.eslintignore b/.eslintignore index b4bfa5a1f7a..a9d27a6765e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,7 +1,8 @@ +/builds/ /coverage/ /coverage-javascript/ /node_modules/ /public/ /tmp/ /vendor/ -/builds/ +webpack.config.js diff --git a/Gemfile b/Gemfile index 2e8ad75fd71..27e415966df 100644 --- a/Gemfile +++ b/Gemfile @@ -214,6 +214,8 @@ gem 'oj', '~> 2.17.4' gem 'chronic', '~> 0.10.2' gem 'chronic_duration', '~> 0.10.6' +gem 'webpack-rails', '~> 0.9.9' + gem 'sass-rails', '~> 5.0.6' gem 'coffee-rails', '~> 4.1.0' gem 'uglifier', '~> 2.7.2' diff --git a/Gemfile.lock b/Gemfile.lock index c99313163a4..b88f51a7a43 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -779,6 +779,8 @@ GEM webmock (1.21.0) addressable (>= 2.3.6) crack (>= 0.3.2) + webpack-rails (0.9.9) + rails (>= 3.2.0) websocket-driver (0.6.3) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) @@ -980,6 +982,7 @@ DEPENDENCIES vmstat (~> 2.3.0) web-console (~> 2.0) webmock (~> 1.21.0) + webpack-rails (~> 0.9.9) wikicloth (= 0.8.1) BUNDLED WITH diff --git a/Procfile b/Procfile index cad738d4292..5e8f4a962ab 100644 --- a/Procfile +++ b/Procfile @@ -4,4 +4,5 @@ # web: RAILS_ENV=development bin/web start_foreground worker: RAILS_ENV=development bin/background_jobs start_foreground +webpack: npm run dev-server # mail_room: bundle exec mail_room -q -c config/mail_room.yml diff --git a/app/assets/javascripts/webpack/bundle.js b/app/assets/javascripts/webpack/bundle.js new file mode 100644 index 00000000000..6c841b25771 --- /dev/null +++ b/app/assets/javascripts/webpack/bundle.js @@ -0,0 +1 @@ +require('./hello_world'); diff --git a/app/assets/javascripts/webpack/hello_world.js b/app/assets/javascripts/webpack/hello_world.js new file mode 100644 index 00000000000..5be69b187fd --- /dev/null +++ b/app/assets/javascripts/webpack/hello_world.js @@ -0,0 +1,3 @@ +/* eslint-disable no-console */ + +console.log('hello world!'); diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 3096f0ee19e..87aadfb1bf5 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -29,6 +29,7 @@ = stylesheet_link_tag "print", media: "print" = javascript_include_tag "application" + = javascript_include_tag *webpack_asset_paths("bundle") - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts diff --git a/config/application.rb b/config/application.rb index d36c6d5c92e..02839dba1ed 100644 --- a/config/application.rb +++ b/config/application.rb @@ -80,6 +80,11 @@ module Gitlab # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql + # Configure webpack + config.webpack.config_file = "config/webpack.config.js" + config.webpack.output_dir = "public/assets/webpack" + config.webpack.public_path = "assets/webpack" + # Enable the asset pipeline config.assets.enabled = true config.assets.paths << Gemojione.images_path diff --git a/config/webpack.config.js b/config/webpack.config.js new file mode 100644 index 00000000000..ea51c9d1af7 --- /dev/null +++ b/config/webpack.config.js @@ -0,0 +1,46 @@ +'use strict'; + +var path = require('path'); +var webpack = require('webpack'); +var StatsPlugin = require('stats-webpack-plugin'); + +var IS_PRODUCTION = process.env.NODE_ENV === 'production'; +var ROOT_PATH = path.resolve(__dirname, '..'); + +// must match config.webpack.dev_server.port +var DEV_SERVER_PORT = 3808; + +var config = { + context: ROOT_PATH, + entry: { + bundle: './app/assets/javascripts/webpack/bundle.js' + }, + + output: { + path: path.join(ROOT_PATH, 'public/assets/webpack'), + publicPath: '/assets/webpack/', + filename: IS_PRODUCTION ? '[name]-[chunkhash].js' : '[name].js' + }, + + plugins: [ + // manifest filename must match config.webpack.manifest_filename + // webpack-rails only needs assetsByChunkName to function properly + new StatsPlugin('manifest.json', { + chunkModules: false, + source: false, + chunks: false, + modules: false, + assets: true + }) + ] +} + +if (!IS_PRODUCTION) { + config.devServer = { + port: DEV_SERVER_PORT, + headers: { 'Access-Control-Allow-Origin': '*' } + }; + config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath; +} + +module.exports = config; diff --git a/package.json b/package.json index 49b8210e427..de199c269db 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,16 @@ { "private": true, "scripts": { + "dev-server": "node_modules/.bin/webpack-dev-server --config config/webpack.config.js", "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .", "eslint-fix": "npm run eslint -- --fix", "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html" }, + "dependencies": { + "stats-webpack-plugin": "^0.4.2", + "webpack": "^1.13.2", + "webpack-dev-server": "^1.16.2" + }, "devDependencies": { "eslint": "^3.10.1", "eslint-config-airbnb-base": "^10.0.1", -- cgit v1.2.1 From df72d65a025fe2cd6778e560541b7784d5bed6ef Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 19 Oct 2016 14:29:43 -0500 Subject: compile es6/es2015 with babel --- app/assets/javascripts/webpack/bundle.js | 5 ++++- app/assets/javascripts/webpack/hello_world.js | 3 --- app/assets/javascripts/webpack/hello_world.js.es6 | 11 +++++++++++ config/webpack.config.js | 19 ++++++++++++++++++- package.json | 3 +++ 5 files changed, 36 insertions(+), 5 deletions(-) delete mode 100644 app/assets/javascripts/webpack/hello_world.js create mode 100644 app/assets/javascripts/webpack/hello_world.js.es6 diff --git a/app/assets/javascripts/webpack/bundle.js b/app/assets/javascripts/webpack/bundle.js index 6c841b25771..b8e38151e5f 100644 --- a/app/assets/javascripts/webpack/bundle.js +++ b/app/assets/javascripts/webpack/bundle.js @@ -1 +1,4 @@ -require('./hello_world'); +var HelloWorld = require('./hello_world').default; + +var message = new HelloWorld('webpack'); +message.sayHello(); diff --git a/app/assets/javascripts/webpack/hello_world.js b/app/assets/javascripts/webpack/hello_world.js deleted file mode 100644 index 5be69b187fd..00000000000 --- a/app/assets/javascripts/webpack/hello_world.js +++ /dev/null @@ -1,3 +0,0 @@ -/* eslint-disable no-console */ - -console.log('hello world!'); diff --git a/app/assets/javascripts/webpack/hello_world.js.es6 b/app/assets/javascripts/webpack/hello_world.js.es6 new file mode 100644 index 00000000000..b3d7c9dd4d4 --- /dev/null +++ b/app/assets/javascripts/webpack/hello_world.js.es6 @@ -0,0 +1,11 @@ +/* eslint-disable no-undef, no-alert */ + +export default class HelloWorld { + constructor(name) { + this.message = `Hello ${name}!`; + } + + sayHello() { + alert(this.message); + } +} diff --git a/config/webpack.config.js b/config/webpack.config.js index ea51c9d1af7..b4892a11ad0 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -22,6 +22,19 @@ var config = { filename: IS_PRODUCTION ? '[name]-[chunkhash].js' : '[name].js' }, + module: { + loaders: [ + { + test: /\.es6$/, + exclude: /node_modules/, + loader: 'babel-loader', + query: { + presets: ['es2015'] + } + } + ] + }, + plugins: [ // manifest filename must match config.webpack.manifest_filename // webpack-rails only needs assetsByChunkName to function properly @@ -32,7 +45,11 @@ var config = { modules: false, assets: true }) - ] + ], + + resolve: { + extensions: ['', '.js', '.es6', '.js.es6'] + } } if (!IS_PRODUCTION) { diff --git a/package.json b/package.json index de199c269db..526a3c34e01 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,9 @@ "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html" }, "dependencies": { + "babel-core": "^6.17.0", + "babel-loader": "^6.2.5", + "babel-preset-es2015": "^6.16.0", "stats-webpack-plugin": "^0.4.2", "webpack": "^1.13.2", "webpack-dev-server": "^1.16.2" -- cgit v1.2.1 From 61ca496b3227b977e5bbf4001e1a88e398c87737 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 19 Oct 2016 15:47:49 -0500 Subject: optimize production output and generate sourcemaps --- config/webpack.config.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index b4892a11ad0..5d285e5fc40 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -22,6 +22,8 @@ var config = { filename: IS_PRODUCTION ? '[name]-[chunkhash].js' : '[name].js' }, + devtool: 'source-map', + module: { loaders: [ { @@ -52,7 +54,19 @@ var config = { } } -if (!IS_PRODUCTION) { +if (IS_PRODUCTION) { + config.plugins.push( + new webpack.NoErrorsPlugin(), + new webpack.optimize.UglifyJsPlugin({ + compress: { warnings: false } + }), + new webpack.DefinePlugin({ + 'process.env': { NODE_ENV: JSON.stringify('production') } + }), + new webpack.optimize.DedupePlugin(), + new webpack.optimize.OccurrenceOrderPlugin() + ); +} else { config.devServer = { port: DEV_SERVER_PORT, headers: { 'Access-Control-Allow-Origin': '*' } -- cgit v1.2.1 From 000180e5e469c39f626faccf91cdb2b1d25a97f8 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 19 Oct 2016 15:56:42 -0500 Subject: conditionally apply webpack-dev-server config --- config/webpack.config.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 5d285e5fc40..2aa1e8d7dd7 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -5,6 +5,7 @@ var webpack = require('webpack'); var StatsPlugin = require('stats-webpack-plugin'); var IS_PRODUCTION = process.env.NODE_ENV === 'production'; +var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1; var ROOT_PATH = path.resolve(__dirname, '..'); // must match config.webpack.dev_server.port @@ -66,7 +67,9 @@ if (IS_PRODUCTION) { new webpack.optimize.DedupePlugin(), new webpack.optimize.OccurrenceOrderPlugin() ); -} else { +} + +if (IS_DEV_SERVER) { config.devServer = { port: DEV_SERVER_PORT, headers: { 'Access-Control-Allow-Origin': '*' } -- cgit v1.2.1 From 31bd36845ba1c18404f029f85b6a0f841c1c390c Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 20 Oct 2016 00:32:56 -0500 Subject: temporarily regress to babel 5 for parity with sprockets-es6 gem --- config/webpack.config.js | 5 +---- package.json | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 2aa1e8d7dd7..acd5597da3a 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -30,10 +30,7 @@ var config = { { test: /\.es6$/, exclude: /node_modules/, - loader: 'babel-loader', - query: { - presets: ['es2015'] - } + loader: 'babel-loader' } ] }, diff --git a/package.json b/package.json index 526a3c34e01..b089eb805c7 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html" }, "dependencies": { - "babel-core": "^6.17.0", - "babel-loader": "^6.2.5", - "babel-preset-es2015": "^6.16.0", + "babel": "^5.8.38", + "babel-core": "^5.8.38", + "babel-loader": "^5.4.2", "stats-webpack-plugin": "^0.4.2", "webpack": "^1.13.2", "webpack-dev-server": "^1.16.2" -- cgit v1.2.1 From 55f291e8ceae6d3d432039f72f6935c62fb2a872 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 28 Oct 2016 03:22:02 -0500 Subject: replace application.js sprockets output with webpack-generated equivalent --- app/assets/javascripts/application.js | 1 + .../javascripts/lib/utils/datetime_utility.js | 2 +- app/assets/javascripts/users/calendar.js | 4 +- app/assets/javascripts/webpack/application.js | 293 +++++ app/assets/javascripts/webpack/bundle.js | 4 - app/assets/javascripts/webpack/hello_world.js.es6 | 11 - app/views/layouts/_head.html.haml | 3 +- config/webpack.config.js | 20 +- package.json | 13 + vendor/assets/javascripts/jquery.atwho.js | 1202 ++++++++++++++++++++ vendor/assets/javascripts/jquery.caret.js | 436 +++++++ vendor/assets/javascripts/jquery.turbolinks.js | 49 + vendor/assets/javascripts/turbolinks.js | 781 +++++++++++++ 13 files changed, 2797 insertions(+), 22 deletions(-) create mode 100644 app/assets/javascripts/webpack/application.js delete mode 100644 app/assets/javascripts/webpack/bundle.js delete mode 100644 app/assets/javascripts/webpack/hello_world.js.es6 create mode 100644 vendor/assets/javascripts/jquery.atwho.js create mode 100644 vendor/assets/javascripts/jquery.caret.js create mode 100644 vendor/assets/javascripts/jquery.turbolinks.js create mode 100644 vendor/assets/javascripts/turbolinks.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index e43afbb4cc9..c21f0572fa7 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -22,6 +22,7 @@ /*= require jquery.endless-scroll */ /*= require jquery.highlight */ /*= require jquery.waitforimages */ +/*= require jquery.caret */ /*= require jquery.atwho */ /*= require jquery.scrollTo */ /*= require jquery.turbolinks */ diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index e8e502694d6..30e4e490543 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -17,7 +17,7 @@ w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; w.gl.utils.formatDate = function(datetime) { - return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); + return (new Date(datetime)).format('mmm d, yyyy h:MMtt Z'); }; w.gl.utils.getDayName = function(date) { diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index 578be7c3590..43ee6a9d9fd 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -33,7 +33,7 @@ date.setDate(date.getDate() + i); var day = date.getDay(); - var count = timestamps[dateFormat(date, 'yyyy-mm-dd')]; + var count = timestamps[date.format('yyyy-mm-dd')]; // Create a new group array if this is the first day of the week // or if is first object @@ -122,7 +122,7 @@ if (stamp.count > 0) { contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : ''); } - dateText = dateFormat(date, 'mmm d, yyyy'); + dateText = date.format('mmm d, yyyy'); return contribText + "
    " + (gl.utils.getDayName(date)) + " " + dateText; }; })(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) { diff --git a/app/assets/javascripts/webpack/application.js b/app/assets/javascripts/webpack/application.js new file mode 100644 index 00000000000..660e00d4c5b --- /dev/null +++ b/app/assets/javascripts/webpack/application.js @@ -0,0 +1,293 @@ +/* eslint-disable */ + +/** + * Simulate sprockets compile order of application.js through CommonJS require statements + * + * Currently exports everything appropriate to window until the scripts that rely on this behavior + * can be refactored. + * + * Test the output from this against sprockets output and it should be almost identical apart from + * webpack's CommonJS wrapper. You can add the following line to webpack.config.js to fix the + * script indentation: + * config.output.sourcePrefix = ''; + */ + +/*= require jquery2 */ +window.jQuery = window.$ = require('jquery'); + +/*= require jquery-ui/autocomplete */ +// depends on jquery-ui/core, jquery-ui/widget, jquery-ui/menu, jquery-ui/position +require('jquery-ui/ui/core'); +require('jquery-ui/ui/widget'); +require('jquery-ui/ui/position'); +require('jquery-ui/ui/menu'); +require('jquery-ui/ui/autocomplete'); + +/*= require jquery-ui/datepicker */ +// depends on jquery-ui/core +require('jquery-ui/ui/datepicker'); + +/*= require jquery-ui/draggable */ +// depends on jquery-ui/core, jquery-ui/widget, jquery-ui/mouse +require('jquery-ui/ui/mouse'); +require('jquery-ui/ui/draggable'); + +/*= require jquery-ui/effect-highlight */ +// depends on jquery-ui/effect +require('jquery-ui/ui/effect'); +require('jquery-ui/ui/effect-highlight'); + +/*= require jquery-ui/sortable */ +// depends on jquery-ui/core, jquery-ui/widget, jquery-ui/mouse +require('jquery-ui/ui/sortable'); + +/*= require jquery_ujs */ +require('jquery-ujs'); + +/*= require jquery.endless-scroll */ +require('vendor/jquery.endless-scroll'); + +/*= require jquery.highlight */ +require('vendor/jquery.highlight'); + +/*= require jquery.waitforimages */ +require('vendor/jquery.waitforimages'); + +/*= require jquery.atwho */ +require('vendor/jquery.caret'); // required by jquery.atwho +require('vendor/jquery.atwho'); + +/*= require jquery.scrollTo */ +require('vendor/jquery.scrollTo'); + +/*= require jquery.turbolinks */ +require('vendor/jquery.turbolinks'); + +/*= require js.cookie */ +window.Cookies = require('vendor/js.cookie'); + +/*= require turbolinks */ +require('vendor/turbolinks'); + +/*= require autosave */ +require('../autosave'); + +/*= require bootstrap/affix */ +require('bootstrap/js/affix'); + +/*= require bootstrap/alert */ +require('bootstrap/js/alert'); + +/*= require bootstrap/button */ +require('bootstrap/js/button'); + +/*= require bootstrap/collapse */ +require('bootstrap/js/collapse'); + +/*= require bootstrap/dropdown */ +require('bootstrap/js/dropdown'); + +/*= require bootstrap/modal */ +require('bootstrap/js/modal'); + +/*= require bootstrap/scrollspy */ +require('bootstrap/js/scrollspy'); + +/*= require bootstrap/tab */ +require('bootstrap/js/tab'); + +/*= require bootstrap/transition */ +require('bootstrap/js/transition'); + +/*= require bootstrap/tooltip */ +require('bootstrap/js/tooltip'); + +/*= require bootstrap/popover */ +require('bootstrap/js/popover'); + +/*= require select2 */ +require('select2/select2.js'); + +/*= require underscore */ +window._ = require('underscore'); + +/*= require dropzone */ +window.Dropzone = require('dropzone'); + +/*= require mousetrap */ +require('mousetrap'); + +/*= require mousetrap/pause */ +require('mousetrap/plugins/pause/mousetrap-pause'); + +/*= require shortcuts */ +require('../shortcuts'); + +/*= require shortcuts_navigation */ +require('../shortcuts_navigation'); + +/*= require shortcuts_dashboard_navigation */ +require('../shortcuts_dashboard_navigation'); + +/*= require shortcuts_issuable */ +require('../shortcuts_issuable'); + +/*= require shortcuts_network */ +require('../shortcuts_network'); + +/*= require jquery.nicescroll */ +require('vendor/jquery.nicescroll'); + +/*= require date.format */ +require('vendor/date.format'); + +/*= require_directory ./behaviors */ +require('vendor/jquery.ba-resize'); +window.autosize = require('vendor/autosize'); +require('../behaviors/autosize'); // requires vendor/jquery.ba-resize and vendor/autosize +require('../behaviors/details_behavior'); +require('../extensions/jquery'); +require('../behaviors/quick_submit'); // requires extensions/jquery +require('../behaviors/requires_input'); +require('../behaviors/toggler_behavior'); + +/*= require_directory ./blob */ +require('../blob/template_selector'); +require('../blob/blob_ci_yaml'); // requires template_selector +require('../blob/blob_file_dropzone'); +require('../blob/blob_gitignore_selector'); +require('../blob/blob_gitignore_selectors'); +require('../blob/blob_license_selector'); +require('../blob/blob_license_selectors'); + +/*= require_directory ./templates */ +require('../templates/issuable_template_selector'); +require('../templates/issuable_template_selectors'); + +/*= require_directory ./commit */ +require('../commit/file'); +require('../commit/image_file'); + +/*= require_directory ./extensions */ +require('../extensions/array'); +require('../extensions/element'); + +/*= require_directory ./lib/utils */ +require('../lib/utils/animate'); +require('../lib/utils/common_utils'); +require('../lib/utils/datetime_utility'); +// require('../lib/utils/emoji_aliases.js.erb'); +window.gl.emojiAliases = function() { return require('emoji-aliases'); }; +require('../lib/utils/jquery.timeago'); +require('../lib/utils/notify'); +require('../lib/utils/text_utility'); +require('../lib/utils/type_utility'); +require('../lib/utils/url_utility'); + +/*= require_directory ./u2f */ +require('../u2f/authenticate'); +require('../u2f/error'); +require('../u2f/register'); +require('../u2f/util'); + +/*= require_directory . */ +require('../abuse_reports'); +require('../activities'); +require('../admin'); +require('../api'); +require('../aside'); +require('../awards_handler'); +require('../breakpoints'); +require('../broadcast_message'); +require('../build'); +require('../build_artifacts'); +require('../build_variables'); +require('../commit'); +require('../commits'); +require('../compare'); +require('../compare_autocomplete'); +require('../confirm_danger_modal'); +window.Clipboard = require('vendor/clipboard'); // required by copy_to_clipboard +require('../copy_to_clipboard'); +require('../create_label'); +require('vue'); // required by cycle_analytics +require('../cycle_analytics'); +require('../diff'); +require('../dispatcher'); +require('../preview_markdown'); +require('../dropzone_input'); +require('../due_date_select'); +require('../files_comment_button'); +require('../flash'); +require('../gfm_auto_complete'); +require('../gl_dropdown'); +require('../gl_field_errors'); +require('../gl_form'); +require('../group_avatar'); +require('../groups_select'); +require('../header'); +require('../importer_status'); +require('../issuable'); +require('../issuable_context'); +require('../issuable_form'); +require('vendor/task_list'); // required by issue +require('../issue'); +require('../issue_status_select'); +require('../issues_bulk_assignment'); +require('../label_manager'); +require('../labels'); +require('../labels_select'); +require('../layout_nav'); +require('../line_highlighter'); +require('../logo'); +require('../member_expiration_date'); +require('../members'); +require('../merge_request_tabs'); +require('../merge_request'); +require('../merge_request_widget'); +require('../merged_buttons'); +require('../milestone'); +require('../milestone_select'); +require('../namespace_select'); +require('../new_branch_form'); +require('../new_commit_form'); +require('../notes'); +require('../notifications_dropdown'); +require('../notifications_form'); +require('../pager'); +require('../pipelines'); +require('../project'); +require('../project_avatar'); +require('../project_find_file'); +require('../project_fork'); +require('../project_import'); +require('../project_new'); +require('../project_select'); +require('../project_show'); +require('../projects_list'); +require('../right_sidebar'); +require('../search'); +require('../search_autocomplete'); +require('../shortcuts_blob'); +require('../shortcuts_find_file'); +require('../sidebar'); +require('../single_file_diff'); +require('../snippets_list'); +require('../star'); +require('../subscription'); +require('../subscription_select'); +require('../syntax_highlight'); +require('../todos'); +require('../tree'); +require('../user'); +require('../user_tabs'); +require('../username_validator'); +require('../users_select'); +require('vendor/latinise'); // required by wikis +require('../wikis'); +require('../zen_mode'); + +/*= require fuzzaldrin-plus */ +require('vendor/fuzzaldrin-plus'); + +require('../application'); diff --git a/app/assets/javascripts/webpack/bundle.js b/app/assets/javascripts/webpack/bundle.js deleted file mode 100644 index b8e38151e5f..00000000000 --- a/app/assets/javascripts/webpack/bundle.js +++ /dev/null @@ -1,4 +0,0 @@ -var HelloWorld = require('./hello_world').default; - -var message = new HelloWorld('webpack'); -message.sayHello(); diff --git a/app/assets/javascripts/webpack/hello_world.js.es6 b/app/assets/javascripts/webpack/hello_world.js.es6 deleted file mode 100644 index b3d7c9dd4d4..00000000000 --- a/app/assets/javascripts/webpack/hello_world.js.es6 +++ /dev/null @@ -1,11 +0,0 @@ -/* eslint-disable no-undef, no-alert */ - -export default class HelloWorld { - constructor(name) { - this.message = `Hello ${name}!`; - } - - sayHello() { - alert(this.message); - } -} diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 87aadfb1bf5..d260e2133f2 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -28,8 +28,7 @@ = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" - = javascript_include_tag "application" - = javascript_include_tag *webpack_asset_paths("bundle") + = javascript_include_tag *webpack_asset_paths("application") - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts diff --git a/config/webpack.config.js b/config/webpack.config.js index acd5597da3a..d45638fbcbd 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -14,7 +14,7 @@ var DEV_SERVER_PORT = 3808; var config = { context: ROOT_PATH, entry: { - bundle: './app/assets/javascripts/webpack/bundle.js' + application: './app/assets/javascripts/webpack/application.js' }, output: { @@ -31,6 +31,15 @@ var config = { test: /\.es6$/, exclude: /node_modules/, loader: 'babel-loader' + }, + { + test: /\.(js|es6)$/, + loader: 'imports-loader', + query: 'this=>window' + }, + { + test: /\.json$/, + loader: 'json-loader' } ] }, @@ -48,7 +57,14 @@ var config = { ], resolve: { - extensions: ['', '.js', '.es6', '.js.es6'] + extensions: ['', '.js', '.es6', '.js.es6'], + alias: { + 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap', + 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), + 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), + 'vue$': 'vue/dist/vue.js', + 'vue-resource$': 'vue-resource/dist/vue-resource.js' + } } } diff --git a/package.json b/package.json index b089eb805c7..7ef811ae478 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,20 @@ "babel": "^5.8.38", "babel-core": "^5.8.38", "babel-loader": "^5.4.2", + "bootstrap-sass": "3.3.6", + "dropzone": "4.2.0", + "exports-loader": "^0.6.3", + "imports-loader": "^0.6.5", + "jquery": "2.2.1", + "jquery-ui": "github:jquery/jquery-ui#1.11.4", + "jquery-ujs": "1.2.1", + "json-loader": "^0.5.4", + "mousetrap": "1.4.6", + "select2": "3.5.2-browserify", "stats-webpack-plugin": "^0.4.2", + "underscore": "1.8.3", + "vue": "1.0.26", + "vue-resource": "0.9.3", "webpack": "^1.13.2", "webpack-dev-server": "^1.16.2" }, diff --git a/vendor/assets/javascripts/jquery.atwho.js b/vendor/assets/javascripts/jquery.atwho.js new file mode 100644 index 00000000000..0d295ebe5af --- /dev/null +++ b/vendor/assets/javascripts/jquery.atwho.js @@ -0,0 +1,1202 @@ +/** + * at.js - 1.5.1 + * Copyright (c) 2016 chord.luo ; + * Homepage: http://ichord.github.com/At.js + * License: MIT + */ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define(["jquery"], function (a0) { + return (factory(a0)); + }); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else { + factory(jQuery); + } +}(this, function ($) { +var DEFAULT_CALLBACKS, KEY_CODE; + +KEY_CODE = { + DOWN: 40, + UP: 38, + ESC: 27, + TAB: 9, + ENTER: 13, + CTRL: 17, + A: 65, + P: 80, + N: 78, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + BACKSPACE: 8, + SPACE: 32 +}; + +DEFAULT_CALLBACKS = { + beforeSave: function(data) { + return Controller.arrayToDefaultHash(data); + }, + matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { + var _a, _y, match, regexp, space; + flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + if (should_startWithSpace) { + flag = '(?:^|\\s)' + flag; + } + _a = decodeURI("%C3%80"); + _y = decodeURI("%C3%BF"); + space = acceptSpaceBar ? "\ " : ""; + regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi'); + match = regexp.exec(subtext); + if (match) { + return match[2] || match[1]; + } else { + return null; + } + }, + filter: function(query, data, searchKey) { + var _results, i, item, len; + _results = []; + for (i = 0, len = data.length; i < len; i++) { + item = data[i]; + if (~new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase())) { + _results.push(item); + } + } + return _results; + }, + remoteFilter: null, + sorter: function(query, items, searchKey) { + var _results, i, item, len; + if (!query) { + return items; + } + _results = []; + for (i = 0, len = items.length; i < len; i++) { + item = items[i]; + item.atwho_order = new String(item[searchKey]).toLowerCase().indexOf(query.toLowerCase()); + if (item.atwho_order > -1) { + _results.push(item); + } + } + return _results.sort(function(a, b) { + return a.atwho_order - b.atwho_order; + }); + }, + tplEval: function(tpl, map) { + var error, error1, template; + template = tpl; + try { + if (typeof tpl !== 'string') { + template = tpl(map); + } + return template.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) { + return map[key]; + }); + } catch (error1) { + error = error1; + return ""; + } + }, + highlighter: function(li, query) { + var regexp; + if (!query) { + return li; + } + regexp = new RegExp(">\\s*(\\w*?)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig'); + return li.replace(regexp, function(str, $1, $2, $3) { + return '> ' + $1 + '' + $2 + '' + $3 + ' <'; + }); + }, + beforeInsert: function(value, $li, e) { + return value; + }, + beforeReposition: function(offset) { + return offset; + }, + afterMatchFailed: function(at, el) {} +}; + +var App; + +App = (function() { + function App(inputor) { + this.currentFlag = null; + this.controllers = {}; + this.aliasMaps = {}; + this.$inputor = $(inputor); + this.setupRootElement(); + this.listen(); + } + + App.prototype.createContainer = function(doc) { + var ref; + if ((ref = this.$el) != null) { + ref.remove(); + } + return $(doc.body).append(this.$el = $("
    ")); + }; + + App.prototype.setupRootElement = function(iframe, asRoot) { + var error, error1; + if (asRoot == null) { + asRoot = false; + } + if (iframe) { + this.window = iframe.contentWindow; + this.document = iframe.contentDocument || this.window.document; + this.iframe = iframe; + } else { + this.document = this.$inputor[0].ownerDocument; + this.window = this.document.defaultView || this.document.parentWindow; + try { + this.iframe = this.window.frameElement; + } catch (error1) { + error = error1; + this.iframe = null; + if ($.fn.atwho.debug) { + throw new Error("iframe auto-discovery is failed.\nPlease use `setIframe` to set the target iframe manually.\n" + error); + } + } + } + return this.createContainer((this.iframeAsRoot = asRoot) ? this.document : document); + }; + + App.prototype.controller = function(at) { + var c, current, currentFlag, ref; + if (this.aliasMaps[at]) { + current = this.controllers[this.aliasMaps[at]]; + } else { + ref = this.controllers; + for (currentFlag in ref) { + c = ref[currentFlag]; + if (currentFlag === at) { + current = c; + break; + } + } + } + if (current) { + return current; + } else { + return this.controllers[this.currentFlag]; + } + }; + + App.prototype.setContextFor = function(at) { + this.currentFlag = at; + return this; + }; + + App.prototype.reg = function(flag, setting) { + var base, controller; + controller = (base = this.controllers)[flag] || (base[flag] = this.$inputor.is('[contentEditable]') ? new EditableController(this, flag) : new TextareaController(this, flag)); + if (setting.alias) { + this.aliasMaps[setting.alias] = flag; + } + controller.init(setting); + return this; + }; + + App.prototype.listen = function() { + return this.$inputor.on('compositionstart', (function(_this) { + return function(e) { + var ref; + if ((ref = _this.controller()) != null) { + ref.view.hide(); + } + _this.isComposing = true; + return null; + }; + })(this)).on('compositionend', (function(_this) { + return function(e) { + _this.isComposing = false; + setTimeout(function(e) { + return _this.dispatch(e); + }); + return null; + }; + })(this)).on('keyup.atwhoInner', (function(_this) { + return function(e) { + return _this.onKeyup(e); + }; + })(this)).on('keydown.atwhoInner', (function(_this) { + return function(e) { + return _this.onKeydown(e); + }; + })(this)).on('blur.atwhoInner', (function(_this) { + return function(e) { + var c; + if (c = _this.controller()) { + c.expectedQueryCBId = null; + return c.view.hide(e, c.getOpt("displayTimeout")); + } + }; + })(this)).on('click.atwhoInner', (function(_this) { + return function(e) { + return _this.dispatch(e); + }; + })(this)).on('scroll.atwhoInner', (function(_this) { + return function() { + var lastScrollTop; + lastScrollTop = _this.$inputor.scrollTop(); + return function(e) { + var currentScrollTop, ref; + currentScrollTop = e.target.scrollTop; + if (lastScrollTop !== currentScrollTop) { + if ((ref = _this.controller()) != null) { + ref.view.hide(e); + } + } + lastScrollTop = currentScrollTop; + return true; + }; + }; + })(this)()); + }; + + App.prototype.shutdown = function() { + var _, c, ref; + ref = this.controllers; + for (_ in ref) { + c = ref[_]; + c.destroy(); + delete this.controllers[_]; + } + this.$inputor.off('.atwhoInner'); + return this.$el.remove(); + }; + + App.prototype.dispatch = function(e) { + var _, c, ref, results; + ref = this.controllers; + results = []; + for (_ in ref) { + c = ref[_]; + results.push(c.lookUp(e)); + } + return results; + }; + + App.prototype.onKeyup = function(e) { + var ref; + switch (e.keyCode) { + case KEY_CODE.ESC: + e.preventDefault(); + if ((ref = this.controller()) != null) { + ref.view.hide(); + } + break; + case KEY_CODE.DOWN: + case KEY_CODE.UP: + case KEY_CODE.CTRL: + case KEY_CODE.ENTER: + $.noop(); + break; + case KEY_CODE.P: + case KEY_CODE.N: + if (!e.ctrlKey) { + this.dispatch(e); + } + break; + default: + this.dispatch(e); + } + }; + + App.prototype.onKeydown = function(e) { + var ref, view; + view = (ref = this.controller()) != null ? ref.view : void 0; + if (!(view && view.visible())) { + return; + } + switch (e.keyCode) { + case KEY_CODE.ESC: + e.preventDefault(); + view.hide(e); + break; + case KEY_CODE.UP: + e.preventDefault(); + view.prev(); + break; + case KEY_CODE.DOWN: + e.preventDefault(); + view.next(); + break; + case KEY_CODE.P: + if (!e.ctrlKey) { + return; + } + e.preventDefault(); + view.prev(); + break; + case KEY_CODE.N: + if (!e.ctrlKey) { + return; + } + e.preventDefault(); + view.next(); + break; + case KEY_CODE.TAB: + case KEY_CODE.ENTER: + case KEY_CODE.SPACE: + if (!view.visible()) { + return; + } + if (!this.controller().getOpt('spaceSelectsMatch') && e.keyCode === KEY_CODE.SPACE) { + return; + } + if (!this.controller().getOpt('tabSelectsMatch') && e.keyCode === KEY_CODE.TAB) { + return; + } + if (view.highlighted()) { + e.preventDefault(); + view.choose(e); + } else { + view.hide(e); + } + break; + default: + $.noop(); + } + }; + + return App; + +})(); + +var Controller, + slice = [].slice; + +Controller = (function() { + Controller.prototype.uid = function() { + return (Math.random().toString(16) + "000000000").substr(2, 8) + (new Date().getTime()); + }; + + function Controller(app, at1) { + this.app = app; + this.at = at1; + this.$inputor = this.app.$inputor; + this.id = this.$inputor[0].id || this.uid(); + this.expectedQueryCBId = null; + this.setting = null; + this.query = null; + this.pos = 0; + this.range = null; + if ((this.$el = $("#atwho-ground-" + this.id, this.app.$el)).length === 0) { + this.app.$el.append(this.$el = $("
    ")); + } + this.model = new Model(this); + this.view = new View(this); + } + + Controller.prototype.init = function(setting) { + this.setting = $.extend({}, this.setting || $.fn.atwho["default"], setting); + this.view.init(); + return this.model.reload(this.setting.data); + }; + + Controller.prototype.destroy = function() { + this.trigger('beforeDestroy'); + this.model.destroy(); + this.view.destroy(); + return this.$el.remove(); + }; + + Controller.prototype.callDefault = function() { + var args, error, error1, funcName; + funcName = arguments[0], args = 2 <= arguments.length ? slice.call(arguments, 1) : []; + try { + return DEFAULT_CALLBACKS[funcName].apply(this, args); + } catch (error1) { + error = error1; + return $.error(error + " Or maybe At.js doesn't have function " + funcName); + } + }; + + Controller.prototype.trigger = function(name, data) { + var alias, eventName; + if (data == null) { + data = []; + } + data.push(this); + alias = this.getOpt('alias'); + eventName = alias ? name + "-" + alias + ".atwho" : name + ".atwho"; + return this.$inputor.trigger(eventName, data); + }; + + Controller.prototype.callbacks = function(funcName) { + return this.getOpt("callbacks")[funcName] || DEFAULT_CALLBACKS[funcName]; + }; + + Controller.prototype.getOpt = function(at, default_value) { + var e, error1; + try { + return this.setting[at]; + } catch (error1) { + e = error1; + return null; + } + }; + + Controller.prototype.insertContentFor = function($li) { + var data, tpl; + tpl = this.getOpt('insertTpl'); + data = $.extend({}, $li.data('item-data'), { + 'atwho-at': this.at + }); + return this.callbacks("tplEval").call(this, tpl, data, "onInsert"); + }; + + Controller.prototype.renderView = function(data) { + var searchKey; + searchKey = this.getOpt("searchKey"); + data = this.callbacks("sorter").call(this, this.query.text, data.slice(0, 1001), searchKey); + return this.view.render(data.slice(0, this.getOpt('limit'))); + }; + + Controller.arrayToDefaultHash = function(data) { + var i, item, len, results; + if (!$.isArray(data)) { + return data; + } + results = []; + for (i = 0, len = data.length; i < len; i++) { + item = data[i]; + if ($.isPlainObject(item)) { + results.push(item); + } else { + results.push({ + name: item + }); + } + } + return results; + }; + + Controller.prototype.lookUp = function(e) { + var query, wait; + if (e && e.type === 'click' && !this.getOpt('lookUpOnClick')) { + return; + } + if (this.getOpt('suspendOnComposing') && this.app.isComposing) { + return; + } + query = this.catchQuery(e); + if (!query) { + this.expectedQueryCBId = null; + return query; + } + this.app.setContextFor(this.at); + if (wait = this.getOpt('delay')) { + this._delayLookUp(query, wait); + } else { + this._lookUp(query); + } + return query; + }; + + Controller.prototype._delayLookUp = function(query, wait) { + var now, remaining; + now = Date.now ? Date.now() : new Date().getTime(); + this.previousCallTime || (this.previousCallTime = now); + remaining = wait - (now - this.previousCallTime); + if ((0 < remaining && remaining < wait)) { + this.previousCallTime = now; + this._stopDelayedCall(); + return this.delayedCallTimeout = setTimeout((function(_this) { + return function() { + _this.previousCallTime = 0; + _this.delayedCallTimeout = null; + return _this._lookUp(query); + }; + })(this), wait); + } else { + this._stopDelayedCall(); + if (this.previousCallTime !== now) { + this.previousCallTime = 0; + } + return this._lookUp(query); + } + }; + + Controller.prototype._stopDelayedCall = function() { + if (this.delayedCallTimeout) { + clearTimeout(this.delayedCallTimeout); + return this.delayedCallTimeout = null; + } + }; + + Controller.prototype._generateQueryCBId = function() { + return {}; + }; + + Controller.prototype._lookUp = function(query) { + var _callback; + _callback = function(queryCBId, data) { + if (queryCBId !== this.expectedQueryCBId) { + return; + } + if (data && data.length > 0) { + return this.renderView(this.constructor.arrayToDefaultHash(data)); + } else { + return this.view.hide(); + } + }; + this.expectedQueryCBId = this._generateQueryCBId(); + return this.model.query(query.text, $.proxy(_callback, this, this.expectedQueryCBId)); + }; + + return Controller; + +})(); + +var TextareaController, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +TextareaController = (function(superClass) { + extend(TextareaController, superClass); + + function TextareaController() { + return TextareaController.__super__.constructor.apply(this, arguments); + } + + TextareaController.prototype.catchQuery = function() { + var caretPos, content, end, isString, query, start, subtext; + content = this.$inputor.val(); + caretPos = this.$inputor.caret('pos', { + iframe: this.app.iframe + }); + subtext = content.slice(0, caretPos); + query = this.callbacks("matcher").call(this, this.at, subtext, this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar")); + isString = typeof query === 'string'; + if (isString && query.length < this.getOpt('minLen', 0)) { + return; + } + if (isString && query.length <= this.getOpt('maxLen', 20)) { + start = caretPos - query.length; + end = start + query.length; + this.pos = start; + query = { + 'text': query, + 'headPos': start, + 'endPos': end + }; + this.trigger("matched", [this.at, query.text]); + } else { + query = null; + this.view.hide(); + } + return this.query = query; + }; + + TextareaController.prototype.rect = function() { + var c, iframeOffset, scaleBottom; + if (!(c = this.$inputor.caret('offset', this.pos - 1, { + iframe: this.app.iframe + }))) { + return; + } + if (this.app.iframe && !this.app.iframeAsRoot) { + iframeOffset = $(this.app.iframe).offset(); + c.left += iframeOffset.left; + c.top += iframeOffset.top; + } + scaleBottom = this.app.document.selection ? 0 : 2; + return { + left: c.left, + top: c.top, + bottom: c.top + c.height + scaleBottom + }; + }; + + TextareaController.prototype.insert = function(content, $li) { + var $inputor, source, startStr, suffix, text; + $inputor = this.$inputor; + source = $inputor.val(); + startStr = source.slice(0, Math.max(this.query.headPos - this.at.length, 0)); + suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || " "; + content += suffix; + text = "" + startStr + content + (source.slice(this.query['endPos'] || 0)); + $inputor.val(text); + $inputor.caret('pos', startStr.length + content.length, { + iframe: this.app.iframe + }); + if (!$inputor.is(':focus')) { + $inputor.focus(); + } + return $inputor.change(); + }; + + return TextareaController; + +})(Controller); + +var EditableController, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty; + +EditableController = (function(superClass) { + extend(EditableController, superClass); + + function EditableController() { + return EditableController.__super__.constructor.apply(this, arguments); + } + + EditableController.prototype._getRange = function() { + var sel; + sel = this.app.window.getSelection(); + if (sel.rangeCount > 0) { + return sel.getRangeAt(0); + } + }; + + EditableController.prototype._setRange = function(position, node, range) { + if (range == null) { + range = this._getRange(); + } + if (!range) { + return; + } + node = $(node)[0]; + if (position === 'after') { + range.setEndAfter(node); + range.setStartAfter(node); + } else { + range.setEndBefore(node); + range.setStartBefore(node); + } + range.collapse(false); + return this._clearRange(range); + }; + + EditableController.prototype._clearRange = function(range) { + var sel; + if (range == null) { + range = this._getRange(); + } + sel = this.app.window.getSelection(); + if (this.ctrl_a_pressed == null) { + sel.removeAllRanges(); + return sel.addRange(range); + } + }; + + EditableController.prototype._movingEvent = function(e) { + var ref; + return e.type === 'click' || ((ref = e.which) === KEY_CODE.RIGHT || ref === KEY_CODE.LEFT || ref === KEY_CODE.UP || ref === KEY_CODE.DOWN); + }; + + EditableController.prototype._unwrap = function(node) { + var next; + node = $(node).unwrap().get(0); + if ((next = node.nextSibling) && next.nodeValue) { + node.nodeValue += next.nodeValue; + $(next).remove(); + } + return node; + }; + + EditableController.prototype.catchQuery = function(e) { + var $inserted, $query, _range, index, inserted, isString, lastNode, matched, offset, query, query_content, range; + if (!(range = this._getRange())) { + return; + } + if (!range.collapsed) { + return; + } + if (e.which === KEY_CODE.ENTER) { + ($query = $(range.startContainer).closest('.atwho-query')).contents().unwrap(); + if ($query.is(':empty')) { + $query.remove(); + } + ($query = $(".atwho-query", this.app.document)).text($query.text()).contents().last().unwrap(); + this._clearRange(); + return; + } + if (/firefox/i.test(navigator.userAgent)) { + if ($(range.startContainer).is(this.$inputor)) { + this._clearRange(); + return; + } + if (e.which === KEY_CODE.BACKSPACE && range.startContainer.nodeType === document.ELEMENT_NODE && (offset = range.startOffset - 1) >= 0) { + _range = range.cloneRange(); + _range.setStart(range.startContainer, offset); + if ($(_range.cloneContents()).contents().last().is('.atwho-inserted')) { + inserted = $(range.startContainer).contents().get(offset); + this._setRange('after', $(inserted).contents().last()); + } + } else if (e.which === KEY_CODE.LEFT && range.startContainer.nodeType === document.TEXT_NODE) { + $inserted = $(range.startContainer.previousSibling); + if ($inserted.is('.atwho-inserted') && range.startOffset === 0) { + this._setRange('after', $inserted.contents().last()); + } + } + } + $(range.startContainer).closest('.atwho-inserted').addClass('atwho-query').siblings().removeClass('atwho-query'); + if (($query = $(".atwho-query", this.app.document)).length > 0 && $query.is(':empty') && $query.text().length === 0) { + $query.remove(); + } + if (!this._movingEvent(e)) { + $query.removeClass('atwho-inserted'); + } + if ($query.length > 0) { + switch (e.which) { + case KEY_CODE.LEFT: + this._setRange('before', $query.get(0), range); + $query.removeClass('atwho-query'); + return; + case KEY_CODE.RIGHT: + this._setRange('after', $query.get(0).nextSibling, range); + $query.removeClass('atwho-query'); + return; + } + } + if ($query.length > 0 && (query_content = $query.attr('data-atwho-at-query'))) { + $query.empty().html(query_content).attr('data-atwho-at-query', null); + this._setRange('after', $query.get(0), range); + } + _range = range.cloneRange(); + _range.setStart(range.startContainer, 0); + matched = this.callbacks("matcher").call(this, this.at, _range.toString(), this.getOpt('startWithSpace'), this.getOpt("acceptSpaceBar")); + isString = typeof matched === 'string'; + if ($query.length === 0 && isString && (index = range.startOffset - this.at.length - matched.length) >= 0) { + range.setStart(range.startContainer, index); + $query = $('', this.app.document).attr(this.getOpt("editableAtwhoQueryAttrs")).addClass('atwho-query'); + range.surroundContents($query.get(0)); + lastNode = $query.contents().last().get(0); + if (/firefox/i.test(navigator.userAgent)) { + range.setStart(lastNode, lastNode.length); + range.setEnd(lastNode, lastNode.length); + this._clearRange(range); + } else { + this._setRange('after', lastNode, range); + } + } + if (isString && matched.length < this.getOpt('minLen', 0)) { + return; + } + if (isString && matched.length <= this.getOpt('maxLen', 20)) { + query = { + text: matched, + el: $query + }; + this.trigger("matched", [this.at, query.text]); + return this.query = query; + } else { + this.view.hide(); + this.query = { + el: $query + }; + if ($query.text().indexOf(this.at) >= 0) { + if (this._movingEvent(e) && $query.hasClass('atwho-inserted')) { + $query.removeClass('atwho-query'); + } else if (false !== this.callbacks('afterMatchFailed').call(this, this.at, $query)) { + this._setRange("after", this._unwrap($query.text($query.text()).contents().first())); + } + } + return null; + } + }; + + EditableController.prototype.rect = function() { + var $iframe, iframeOffset, rect; + rect = this.query.el.offset(); + if (this.app.iframe && !this.app.iframeAsRoot) { + iframeOffset = ($iframe = $(this.app.iframe)).offset(); + rect.left += iframeOffset.left - this.$inputor.scrollLeft(); + rect.top += iframeOffset.top - this.$inputor.scrollTop(); + } + rect.bottom = rect.top + this.query.el.height(); + return rect; + }; + + EditableController.prototype.insert = function(content, $li) { + var data, range, suffix, suffixNode; + if (!this.$inputor.is(':focus')) { + this.$inputor.focus(); + } + suffix = (suffix = this.getOpt('suffix')) === "" ? suffix : suffix || "\u00A0"; + data = $li.data('item-data'); + this.query.el.removeClass('atwho-query').addClass('atwho-inserted').html(content).attr('data-atwho-at-query', "" + data['atwho-at'] + this.query.text); + if (range = this._getRange()) { + range.setEndAfter(this.query.el[0]); + range.collapse(false); + range.insertNode(suffixNode = this.app.document.createTextNode("\u200D" + suffix)); + this._setRange('after', suffixNode, range); + } + if (!this.$inputor.is(':focus')) { + this.$inputor.focus(); + } + return this.$inputor.change(); + }; + + return EditableController; + +})(Controller); + +var Model; + +Model = (function() { + function Model(context) { + this.context = context; + this.at = this.context.at; + this.storage = this.context.$inputor; + } + + Model.prototype.destroy = function() { + return this.storage.data(this.at, null); + }; + + Model.prototype.saved = function() { + return this.fetch() > 0; + }; + + Model.prototype.query = function(query, callback) { + var _remoteFilter, data, searchKey; + data = this.fetch(); + searchKey = this.context.getOpt("searchKey"); + data = this.context.callbacks('filter').call(this.context, query, data, searchKey) || []; + _remoteFilter = this.context.callbacks('remoteFilter'); + if (data.length > 0 || (!_remoteFilter && data.length === 0)) { + return callback(data); + } else { + return _remoteFilter.call(this.context, query, callback); + } + }; + + Model.prototype.fetch = function() { + return this.storage.data(this.at) || []; + }; + + Model.prototype.save = function(data) { + return this.storage.data(this.at, this.context.callbacks("beforeSave").call(this.context, data || [])); + }; + + Model.prototype.load = function(data) { + if (!(this.saved() || !data)) { + return this._load(data); + } + }; + + Model.prototype.reload = function(data) { + return this._load(data); + }; + + Model.prototype._load = function(data) { + if (typeof data === "string") { + return $.ajax(data, { + dataType: "json" + }).done((function(_this) { + return function(data) { + return _this.save(data); + }; + })(this)); + } else { + return this.save(data); + } + }; + + return Model; + +})(); + +var View; + +View = (function() { + function View(context) { + this.context = context; + this.$el = $("
      "); + this.$elUl = this.$el.children(); + this.timeoutID = null; + this.context.$el.append(this.$el); + this.bindEvent(); + } + + View.prototype.init = function() { + var header_tpl, id; + id = this.context.getOpt("alias") || this.context.at.charCodeAt(0); + header_tpl = this.context.getOpt("headerTpl"); + if (header_tpl && this.$el.children().length === 1) { + this.$el.prepend(header_tpl); + } + return this.$el.attr({ + 'id': "at-view-" + id + }); + }; + + View.prototype.destroy = function() { + return this.$el.remove(); + }; + + View.prototype.bindEvent = function() { + var $menu, lastCoordX, lastCoordY; + $menu = this.$el.find('ul'); + lastCoordX = 0; + lastCoordY = 0; + return $menu.on('mousemove.atwho-view', 'li', (function(_this) { + return function(e) { + var $cur; + if (lastCoordX === e.clientX && lastCoordY === e.clientY) { + return; + } + lastCoordX = e.clientX; + lastCoordY = e.clientY; + $cur = $(e.currentTarget); + if ($cur.hasClass('cur')) { + return; + } + $menu.find('.cur').removeClass('cur'); + return $cur.addClass('cur'); + }; + })(this)).on('click.atwho-view', 'li', (function(_this) { + return function(e) { + $menu.find('.cur').removeClass('cur'); + $(e.currentTarget).addClass('cur'); + _this.choose(e); + return e.preventDefault(); + }; + })(this)); + }; + + View.prototype.visible = function() { + return this.$el.is(":visible"); + }; + + View.prototype.highlighted = function() { + return this.$el.find(".cur").length > 0; + }; + + View.prototype.choose = function(e) { + var $li, content; + if (($li = this.$el.find(".cur")).length) { + content = this.context.insertContentFor($li); + this.context._stopDelayedCall(); + this.context.insert(this.context.callbacks("beforeInsert").call(this.context, content, $li, e), $li); + this.context.trigger("inserted", [$li, e]); + this.hide(e); + } + if (this.context.getOpt("hideWithoutSuffix")) { + return this.stopShowing = true; + } + }; + + View.prototype.reposition = function(rect) { + var _window, offset, overflowOffset, ref; + _window = this.context.app.iframeAsRoot ? this.context.app.window : window; + if (rect.bottom + this.$el.height() - $(_window).scrollTop() > $(_window).height()) { + rect.bottom = rect.top - this.$el.height(); + } + if (rect.left > (overflowOffset = $(_window).width() - this.$el.width() - 5)) { + rect.left = overflowOffset; + } + offset = { + left: rect.left, + top: rect.bottom + }; + if ((ref = this.context.callbacks("beforeReposition")) != null) { + ref.call(this.context, offset); + } + this.$el.offset(offset); + return this.context.trigger("reposition", [offset]); + }; + + View.prototype.next = function() { + var cur, next, nextEl, offset; + cur = this.$el.find('.cur').removeClass('cur'); + next = cur.next(); + if (!next.length) { + next = this.$el.find('li:first'); + } + next.addClass('cur'); + nextEl = next[0]; + offset = nextEl.offsetTop + nextEl.offsetHeight + (nextEl.nextSibling ? nextEl.nextSibling.offsetHeight : 0); + return this.scrollTop(Math.max(0, offset - this.$el.height())); + }; + + View.prototype.prev = function() { + var cur, offset, prev, prevEl; + cur = this.$el.find('.cur').removeClass('cur'); + prev = cur.prev(); + if (!prev.length) { + prev = this.$el.find('li:last'); + } + prev.addClass('cur'); + prevEl = prev[0]; + offset = prevEl.offsetTop + prevEl.offsetHeight + (prevEl.nextSibling ? prevEl.nextSibling.offsetHeight : 0); + return this.scrollTop(Math.max(0, offset - this.$el.height())); + }; + + View.prototype.scrollTop = function(scrollTop) { + var scrollDuration; + scrollDuration = this.context.getOpt('scrollDuration'); + if (scrollDuration) { + return this.$elUl.animate({ + scrollTop: scrollTop + }, scrollDuration); + } else { + return this.$elUl.scrollTop(scrollTop); + } + }; + + View.prototype.show = function() { + var rect; + if (this.stopShowing) { + this.stopShowing = false; + return; + } + if (!this.visible()) { + this.$el.show(); + this.$el.scrollTop(0); + this.context.trigger('shown'); + } + if (rect = this.context.rect()) { + return this.reposition(rect); + } + }; + + View.prototype.hide = function(e, time) { + var callback; + if (!this.visible()) { + return; + } + if (isNaN(time)) { + this.$el.hide(); + return this.context.trigger('hidden', [e]); + } else { + callback = (function(_this) { + return function() { + return _this.hide(); + }; + })(this); + clearTimeout(this.timeoutID); + return this.timeoutID = setTimeout(callback, time); + } + }; + + View.prototype.render = function(list) { + var $li, $ul, i, item, len, li, tpl; + if (!($.isArray(list) && list.length > 0)) { + this.hide(); + return; + } + this.$el.find('ul').empty(); + $ul = this.$el.find('ul'); + tpl = this.context.getOpt('displayTpl'); + for (i = 0, len = list.length; i < len; i++) { + item = list[i]; + item = $.extend({}, item, { + 'atwho-at': this.context.at + }); + li = this.context.callbacks("tplEval").call(this.context, tpl, item, "onDisplay"); + $li = $(this.context.callbacks("highlighter").call(this.context, li, this.context.query.text)); + $li.data("item-data", item); + $ul.append($li); + } + this.show(); + if (this.context.getOpt('highlightFirst')) { + return $ul.find("li:first").addClass("cur"); + } + }; + + return View; + +})(); + +var Api; + +Api = { + load: function(at, data) { + var c; + if (c = this.controller(at)) { + return c.model.load(data); + } + }, + isSelecting: function() { + var ref; + return !!((ref = this.controller()) != null ? ref.view.visible() : void 0); + }, + hide: function() { + var ref; + return (ref = this.controller()) != null ? ref.view.hide() : void 0; + }, + reposition: function() { + var c; + if (c = this.controller()) { + return c.view.reposition(c.rect()); + } + }, + setIframe: function(iframe, asRoot) { + this.setupRootElement(iframe, asRoot); + return null; + }, + run: function() { + return this.dispatch(); + }, + destroy: function() { + this.shutdown(); + return this.$inputor.data('atwho', null); + } +}; + +$.fn.atwho = function(method) { + var _args, result; + _args = arguments; + result = null; + this.filter('textarea, input, [contenteditable=""], [contenteditable=true]').each(function() { + var $this, app; + if (!(app = ($this = $(this)).data("atwho"))) { + $this.data('atwho', (app = new App(this))); + } + if (typeof method === 'object' || !method) { + return app.reg(method.at, method); + } else if (Api[method] && app) { + return result = Api[method].apply(app, Array.prototype.slice.call(_args, 1)); + } else { + return $.error("Method " + method + " does not exist on jQuery.atwho"); + } + }); + if (result != null) { + return result; + } else { + return this; + } +}; + +$.fn.atwho["default"] = { + at: void 0, + alias: void 0, + data: null, + displayTpl: "
    • ${name}
    • ", + insertTpl: "${atwho-at}${name}", + headerTpl: null, + callbacks: DEFAULT_CALLBACKS, + searchKey: "name", + suffix: void 0, + hideWithoutSuffix: false, + startWithSpace: true, + acceptSpaceBar: false, + highlightFirst: true, + limit: 5, + maxLen: 20, + minLen: 0, + displayTimeout: 300, + delay: null, + spaceSelectsMatch: false, + tabSelectsMatch: true, + editableAtwhoQueryAttrs: {}, + scrollDuration: 150, + suspendOnComposing: true, + lookUpOnClick: true +}; + +$.fn.atwho.debug = false; + +})); diff --git a/vendor/assets/javascripts/jquery.caret.js b/vendor/assets/javascripts/jquery.caret.js new file mode 100644 index 00000000000..811ec63ee47 --- /dev/null +++ b/vendor/assets/javascripts/jquery.caret.js @@ -0,0 +1,436 @@ +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(["jquery"], function ($) { + return (root.returnExportsGlobal = factory($)); + }); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like enviroments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else { + factory(jQuery); + } +}(this, function ($) { + +/* + Implement Github like autocomplete mentions + http://ichord.github.com/At.js + + Copyright (c) 2013 chord.luo@gmail.com + Licensed under the MIT license. +*/ + +/* +本插件操作 textarea 或者 input 内的插入符 +只实现了获得插入符在文本框中的位置,我设置 +插入符的位置. +*/ + +"use strict"; +var EditableCaret, InputCaret, Mirror, Utils, discoveryIframeOf, methods, oDocument, oFrame, oWindow, pluginName, setContextBy; + +pluginName = 'caret'; + +EditableCaret = (function() { + function EditableCaret($inputor) { + this.$inputor = $inputor; + this.domInputor = this.$inputor[0]; + } + + EditableCaret.prototype.setPos = function(pos) { + var fn, found, offset, sel; + if (sel = oWindow.getSelection()) { + offset = 0; + found = false; + (fn = function(pos, parent) { + var node, range, _i, _len, _ref, _results; + _ref = parent.childNodes; + _results = []; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + node = _ref[_i]; + if (found) { + break; + } + if (node.nodeType === 3) { + if (offset + node.length >= pos) { + found = true; + range = oDocument.createRange(); + range.setStart(node, pos - offset); + sel.removeAllRanges(); + sel.addRange(range); + break; + } else { + _results.push(offset += node.length); + } + } else { + _results.push(fn(pos, node)); + } + } + return _results; + })(pos, this.domInputor); + } + return this.domInputor; + }; + + EditableCaret.prototype.getIEPosition = function() { + return this.getPosition(); + }; + + EditableCaret.prototype.getPosition = function() { + var inputor_offset, offset; + offset = this.getOffset(); + inputor_offset = this.$inputor.offset(); + offset.left -= inputor_offset.left; + offset.top -= inputor_offset.top; + return offset; + }; + + EditableCaret.prototype.getOldIEPos = function() { + var preCaretTextRange, textRange; + textRange = oDocument.selection.createRange(); + preCaretTextRange = oDocument.body.createTextRange(); + preCaretTextRange.moveToElementText(this.domInputor); + preCaretTextRange.setEndPoint("EndToEnd", textRange); + return preCaretTextRange.text.length; + }; + + EditableCaret.prototype.getPos = function() { + var clonedRange, pos, range; + if (range = this.range()) { + clonedRange = range.cloneRange(); + clonedRange.selectNodeContents(this.domInputor); + clonedRange.setEnd(range.endContainer, range.endOffset); + pos = clonedRange.toString().length; + clonedRange.detach(); + return pos; + } else if (oDocument.selection) { + return this.getOldIEPos(); + } + }; + + EditableCaret.prototype.getOldIEOffset = function() { + var range, rect; + range = oDocument.selection.createRange().duplicate(); + range.moveStart("character", -1); + rect = range.getBoundingClientRect(); + return { + height: rect.bottom - rect.top, + left: rect.left, + top: rect.top + }; + }; + + EditableCaret.prototype.getOffset = function(pos) { + var clonedRange, offset, range, rect, shadowCaret; + if (oWindow.getSelection && (range = this.range())) { + if (range.endOffset - 1 > 0 && range.endContainer !== this.domInputor) { + clonedRange = range.cloneRange(); + clonedRange.setStart(range.endContainer, range.endOffset - 1); + clonedRange.setEnd(range.endContainer, range.endOffset); + rect = clonedRange.getBoundingClientRect(); + offset = { + height: rect.height, + left: rect.left + rect.width, + top: rect.top + }; + clonedRange.detach(); + } + if (!offset || (offset != null ? offset.height : void 0) === 0) { + clonedRange = range.cloneRange(); + shadowCaret = $(oDocument.createTextNode("|")); + clonedRange.insertNode(shadowCaret[0]); + clonedRange.selectNode(shadowCaret[0]); + rect = clonedRange.getBoundingClientRect(); + offset = { + height: rect.height, + left: rect.left, + top: rect.top + }; + shadowCaret.remove(); + clonedRange.detach(); + } + } else if (oDocument.selection) { + offset = this.getOldIEOffset(); + } + if (offset) { + offset.top += $(oWindow).scrollTop(); + offset.left += $(oWindow).scrollLeft(); + } + return offset; + }; + + EditableCaret.prototype.range = function() { + var sel; + if (!oWindow.getSelection) { + return; + } + sel = oWindow.getSelection(); + if (sel.rangeCount > 0) { + return sel.getRangeAt(0); + } else { + return null; + } + }; + + return EditableCaret; + +})(); + +InputCaret = (function() { + function InputCaret($inputor) { + this.$inputor = $inputor; + this.domInputor = this.$inputor[0]; + } + + InputCaret.prototype.getIEPos = function() { + var endRange, inputor, len, normalizedValue, pos, range, textInputRange; + inputor = this.domInputor; + range = oDocument.selection.createRange(); + pos = 0; + if (range && range.parentElement() === inputor) { + normalizedValue = inputor.value.replace(/\r\n/g, "\n"); + len = normalizedValue.length; + textInputRange = inputor.createTextRange(); + textInputRange.moveToBookmark(range.getBookmark()); + endRange = inputor.createTextRange(); + endRange.collapse(false); + if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { + pos = len; + } else { + pos = -textInputRange.moveStart("character", -len); + } + } + return pos; + }; + + InputCaret.prototype.getPos = function() { + if (oDocument.selection) { + return this.getIEPos(); + } else { + return this.domInputor.selectionStart; + } + }; + + InputCaret.prototype.setPos = function(pos) { + var inputor, range; + inputor = this.domInputor; + if (oDocument.selection) { + range = inputor.createTextRange(); + range.move("character", pos); + range.select(); + } else if (inputor.setSelectionRange) { + inputor.setSelectionRange(pos, pos); + } + return inputor; + }; + + InputCaret.prototype.getIEOffset = function(pos) { + var h, textRange, x, y; + textRange = this.domInputor.createTextRange(); + pos || (pos = this.getPos()); + textRange.move('character', pos); + x = textRange.boundingLeft; + y = textRange.boundingTop; + h = textRange.boundingHeight; + return { + left: x, + top: y, + height: h + }; + }; + + InputCaret.prototype.getOffset = function(pos) { + var $inputor, offset, position; + $inputor = this.$inputor; + if (oDocument.selection) { + offset = this.getIEOffset(pos); + offset.top += $(oWindow).scrollTop() + $inputor.scrollTop(); + offset.left += $(oWindow).scrollLeft() + $inputor.scrollLeft(); + return offset; + } else { + offset = $inputor.offset(); + position = this.getPosition(pos); + return offset = { + left: offset.left + position.left - $inputor.scrollLeft(), + top: offset.top + position.top - $inputor.scrollTop(), + height: position.height + }; + } + }; + + InputCaret.prototype.getPosition = function(pos) { + var $inputor, at_rect, end_range, format, html, mirror, start_range; + $inputor = this.$inputor; + format = function(value) { + value = value.replace(/<|>|`|"|&/g, '?').replace(/\r\n|\r|\n/g, "
      "); + if (/firefox/i.test(navigator.userAgent)) { + value = value.replace(/\s/g, ' '); + } + return value; + }; + if (pos === void 0) { + pos = this.getPos(); + } + start_range = $inputor.val().slice(0, pos); + end_range = $inputor.val().slice(pos); + html = "" + format(start_range) + ""; + html += "|"; + html += "" + format(end_range) + ""; + mirror = new Mirror($inputor); + return at_rect = mirror.create(html).rect(); + }; + + InputCaret.prototype.getIEPosition = function(pos) { + var h, inputorOffset, offset, x, y; + offset = this.getIEOffset(pos); + inputorOffset = this.$inputor.offset(); + x = offset.left - inputorOffset.left; + y = offset.top - inputorOffset.top; + h = offset.height; + return { + left: x, + top: y, + height: h + }; + }; + + return InputCaret; + +})(); + +Mirror = (function() { + Mirror.prototype.css_attr = ["borderBottomWidth", "borderLeftWidth", "borderRightWidth", "borderTopStyle", "borderRightStyle", "borderBottomStyle", "borderLeftStyle", "borderTopWidth", "boxSizing", "fontFamily", "fontSize", "fontWeight", "height", "letterSpacing", "lineHeight", "marginBottom", "marginLeft", "marginRight", "marginTop", "outlineWidth", "overflow", "overflowX", "overflowY", "paddingBottom", "paddingLeft", "paddingRight", "paddingTop", "textAlign", "textOverflow", "textTransform", "whiteSpace", "wordBreak", "wordWrap"]; + + function Mirror($inputor) { + this.$inputor = $inputor; + } + + Mirror.prototype.mirrorCss = function() { + var css, + _this = this; + css = { + position: 'absolute', + left: -9999, + top: 0, + zIndex: -20000 + }; + if (this.$inputor.prop('tagName') === 'TEXTAREA') { + this.css_attr.push('width'); + } + $.each(this.css_attr, function(i, p) { + return css[p] = _this.$inputor.css(p); + }); + return css; + }; + + Mirror.prototype.create = function(html) { + this.$mirror = $('
      '); + this.$mirror.css(this.mirrorCss()); + this.$mirror.html(html); + this.$inputor.after(this.$mirror); + return this; + }; + + Mirror.prototype.rect = function() { + var $flag, pos, rect; + $flag = this.$mirror.find("#caret"); + pos = $flag.position(); + rect = { + left: pos.left, + top: pos.top, + height: $flag.height() + }; + this.$mirror.remove(); + return rect; + }; + + return Mirror; + +})(); + +Utils = { + contentEditable: function($inputor) { + return !!($inputor[0].contentEditable && $inputor[0].contentEditable === 'true'); + } +}; + +methods = { + pos: function(pos) { + if (pos || pos === 0) { + return this.setPos(pos); + } else { + return this.getPos(); + } + }, + position: function(pos) { + if (oDocument.selection) { + return this.getIEPosition(pos); + } else { + return this.getPosition(pos); + } + }, + offset: function(pos) { + var offset; + offset = this.getOffset(pos); + return offset; + } +}; + +oDocument = null; + +oWindow = null; + +oFrame = null; + +setContextBy = function(settings) { + var iframe; + if (iframe = settings != null ? settings.iframe : void 0) { + oFrame = iframe; + oWindow = iframe.contentWindow; + return oDocument = iframe.contentDocument || oWindow.document; + } else { + oFrame = void 0; + oWindow = window; + return oDocument = document; + } +}; + +discoveryIframeOf = function($dom) { + var error; + oDocument = $dom[0].ownerDocument; + oWindow = oDocument.defaultView || oDocument.parentWindow; + try { + return oFrame = oWindow.frameElement; + } catch (_error) { + error = _error; + } +}; + +$.fn.caret = function(method, value, settings) { + var caret; + if (methods[method]) { + if ($.isPlainObject(value)) { + setContextBy(value); + value = void 0; + } else { + setContextBy(settings); + } + caret = Utils.contentEditable(this) ? new EditableCaret(this) : new InputCaret(this); + return methods[method].apply(caret, [value]); + } else { + return $.error("Method " + method + " does not exist on jQuery.caret"); + } +}; + +$.fn.caret.EditableCaret = EditableCaret; + +$.fn.caret.InputCaret = InputCaret; + +$.fn.caret.Utils = Utils; + +$.fn.caret.apis = methods; + + +})); diff --git a/vendor/assets/javascripts/jquery.turbolinks.js b/vendor/assets/javascripts/jquery.turbolinks.js new file mode 100644 index 00000000000..fd6e95e75d5 --- /dev/null +++ b/vendor/assets/javascripts/jquery.turbolinks.js @@ -0,0 +1,49 @@ +// Generated by CoffeeScript 1.7.1 + +/* +jQuery.Turbolinks ~ https://github.com/kossnocorp/jquery.turbolinks +jQuery plugin for drop-in fix binded events problem caused by Turbolinks + +The MIT License +Copyright (c) 2012-2013 Sasha Koss & Rico Sta. Cruz + */ + +(function() { + var $, $document; + + $ = window.jQuery || (typeof require === "function" ? require('jquery') : void 0); + + $document = $(document); + + $.turbo = { + version: '2.1.0', + isReady: false, + use: function(load, fetch) { + return $document.off('.turbo').on("" + load + ".turbo", this.onLoad).on("" + fetch + ".turbo", this.onFetch); + }, + addCallback: function(callback) { + if ($.turbo.isReady) { + callback($); + } + return $document.on('turbo:ready', function() { + return callback($); + }); + }, + onLoad: function() { + $.turbo.isReady = true; + return $document.trigger('turbo:ready'); + }, + onFetch: function() { + return $.turbo.isReady = false; + }, + register: function() { + $(this.onLoad); + return $.fn.ready = this.addCallback; + } + }; + + $.turbo.register(); + + $.turbo.use('page:load', 'page:fetch'); + +}).call(this); diff --git a/vendor/assets/javascripts/turbolinks.js b/vendor/assets/javascripts/turbolinks.js new file mode 100644 index 00000000000..17a2635bf2a --- /dev/null +++ b/vendor/assets/javascripts/turbolinks.js @@ -0,0 +1,781 @@ +// Turbolinks Classic v2.5.3 compiled from CoffeeScript +(function() { + var CSRFToken, Click, ComponentUrl, EVENTS, Link, ProgressBar, browserIsntBuggy, browserSupportsCustomEvents, browserSupportsPushState, browserSupportsTurbolinks, bypassOnLoadPopstate, cacheCurrentPage, cacheSize, changePage, clone, constrainPageCacheTo, createDocument, crossOriginRedirect, currentState, enableProgressBar, enableTransitionCache, executeScriptTags, extractTitleAndBody, fetch, fetchHistory, fetchReplacement, historyStateIsDefined, initializeTurbolinks, installDocumentReadyPageEventTriggers, installHistoryChangeHandler, installJqueryAjaxSuccessPageUpdateTrigger, loadedAssets, manuallyTriggerHashChangeForFirefox, pageCache, pageChangePrevented, pagesCached, popCookie, processResponse, progressBar, recallScrollPosition, ref, referer, reflectNewUrl, reflectRedirectedUrl, rememberCurrentState, rememberCurrentUrl, rememberReferer, removeNoscriptTags, requestMethodIsSafe, resetScrollPosition, setAutofocusElement, transitionCacheEnabled, transitionCacheFor, triggerEvent, visit, xhr, + indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, + extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, + hasProp = {}.hasOwnProperty, + slice = [].slice, + bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + pageCache = {}; + + cacheSize = 10; + + transitionCacheEnabled = false; + + progressBar = null; + + currentState = null; + + loadedAssets = null; + + referer = null; + + xhr = null; + + EVENTS = { + BEFORE_CHANGE: 'page:before-change', + FETCH: 'page:fetch', + RECEIVE: 'page:receive', + CHANGE: 'page:change', + UPDATE: 'page:update', + LOAD: 'page:load', + RESTORE: 'page:restore', + BEFORE_UNLOAD: 'page:before-unload', + EXPIRE: 'page:expire' + }; + + fetch = function(url) { + var cachedPage; + url = new ComponentUrl(url); + rememberReferer(); + cacheCurrentPage(); + if (progressBar != null) { + progressBar.start(); + } + if (transitionCacheEnabled && (cachedPage = transitionCacheFor(url.absolute))) { + fetchHistory(cachedPage); + return fetchReplacement(url, null, false); + } else { + return fetchReplacement(url, resetScrollPosition); + } + }; + + transitionCacheFor = function(url) { + var cachedPage; + cachedPage = pageCache[url]; + if (cachedPage && !cachedPage.transitionCacheDisabled) { + return cachedPage; + } + }; + + enableTransitionCache = function(enable) { + if (enable == null) { + enable = true; + } + return transitionCacheEnabled = enable; + }; + + enableProgressBar = function(enable) { + if (enable == null) { + enable = true; + } + if (!browserSupportsTurbolinks) { + return; + } + if (enable) { + return progressBar != null ? progressBar : progressBar = new ProgressBar('html'); + } else { + if (progressBar != null) { + progressBar.uninstall(); + } + return progressBar = null; + } + }; + + fetchReplacement = function(url, onLoadFunction, showProgressBar) { + if (showProgressBar == null) { + showProgressBar = true; + } + triggerEvent(EVENTS.FETCH, { + url: url.absolute + }); + if (xhr != null) { + xhr.abort(); + } + xhr = new XMLHttpRequest; + xhr.open('GET', url.withoutHashForIE10compatibility(), true); + xhr.setRequestHeader('Accept', 'text/html, application/xhtml+xml, application/xml'); + xhr.setRequestHeader('X-XHR-Referer', referer); + xhr.onload = function() { + var doc; + triggerEvent(EVENTS.RECEIVE, { + url: url.absolute + }); + if (doc = processResponse()) { + reflectNewUrl(url); + reflectRedirectedUrl(); + changePage.apply(null, extractTitleAndBody(doc)); + manuallyTriggerHashChangeForFirefox(); + if (typeof onLoadFunction === "function") { + onLoadFunction(); + } + return triggerEvent(EVENTS.LOAD); + } else { + return document.location.href = crossOriginRedirect() || url.absolute; + } + }; + if (progressBar && showProgressBar) { + xhr.onprogress = (function(_this) { + return function(event) { + var percent; + percent = event.lengthComputable ? event.loaded / event.total * 100 : progressBar.value + (100 - progressBar.value) / 10; + return progressBar.advanceTo(percent); + }; + })(this); + } + xhr.onloadend = function() { + return xhr = null; + }; + xhr.onerror = function() { + return document.location.href = url.absolute; + }; + return xhr.send(); + }; + + fetchHistory = function(cachedPage) { + if (xhr != null) { + xhr.abort(); + } + changePage(cachedPage.title, cachedPage.body); + recallScrollPosition(cachedPage); + return triggerEvent(EVENTS.RESTORE); + }; + + cacheCurrentPage = function() { + var currentStateUrl; + currentStateUrl = new ComponentUrl(currentState.url); + pageCache[currentStateUrl.absolute] = { + url: currentStateUrl.relative, + body: document.body, + title: document.title, + positionY: window.pageYOffset, + positionX: window.pageXOffset, + cachedAt: new Date().getTime(), + transitionCacheDisabled: document.querySelector('[data-no-transition-cache]') != null + }; + return constrainPageCacheTo(cacheSize); + }; + + pagesCached = function(size) { + if (size == null) { + size = cacheSize; + } + if (/^[\d]+$/.test(size)) { + return cacheSize = parseInt(size); + } + }; + + constrainPageCacheTo = function(limit) { + var cacheTimesRecentFirst, i, key, len, pageCacheKeys, results; + pageCacheKeys = Object.keys(pageCache); + cacheTimesRecentFirst = pageCacheKeys.map(function(url) { + return pageCache[url].cachedAt; + }).sort(function(a, b) { + return b - a; + }); + results = []; + for (i = 0, len = pageCacheKeys.length; i < len; i++) { + key = pageCacheKeys[i]; + if (!(pageCache[key].cachedAt <= cacheTimesRecentFirst[limit])) { + continue; + } + triggerEvent(EVENTS.EXPIRE, pageCache[key]); + results.push(delete pageCache[key]); + } + return results; + }; + + changePage = function(title, body, csrfToken, runScripts) { + triggerEvent(EVENTS.BEFORE_UNLOAD); + document.title = title; + document.documentElement.replaceChild(body, document.body); + if (csrfToken != null) { + CSRFToken.update(csrfToken); + } + setAutofocusElement(); + if (runScripts) { + executeScriptTags(); + } + currentState = window.history.state; + if (progressBar != null) { + progressBar.done(); + } + triggerEvent(EVENTS.CHANGE); + return triggerEvent(EVENTS.UPDATE); + }; + + executeScriptTags = function() { + var attr, copy, i, j, len, len1, nextSibling, parentNode, ref, ref1, script, scripts; + scripts = Array.prototype.slice.call(document.body.querySelectorAll('script:not([data-turbolinks-eval="false"])')); + for (i = 0, len = scripts.length; i < len; i++) { + script = scripts[i]; + if (!((ref = script.type) === '' || ref === 'text/javascript')) { + continue; + } + copy = document.createElement('script'); + ref1 = script.attributes; + for (j = 0, len1 = ref1.length; j < len1; j++) { + attr = ref1[j]; + copy.setAttribute(attr.name, attr.value); + } + if (!script.hasAttribute('async')) { + copy.async = false; + } + copy.appendChild(document.createTextNode(script.innerHTML)); + parentNode = script.parentNode, nextSibling = script.nextSibling; + parentNode.removeChild(script); + parentNode.insertBefore(copy, nextSibling); + } + }; + + removeNoscriptTags = function(node) { + node.innerHTML = node.innerHTML.replace(//ig, ''); + return node; + }; + + setAutofocusElement = function() { + var autofocusElement, list; + autofocusElement = (list = document.querySelectorAll('input[autofocus], textarea[autofocus]'))[list.length - 1]; + if (autofocusElement && document.activeElement !== autofocusElement) { + return autofocusElement.focus(); + } + }; + + reflectNewUrl = function(url) { + if ((url = new ComponentUrl(url)).absolute !== referer) { + return window.history.pushState({ + turbolinks: true, + url: url.absolute + }, '', url.absolute); + } + }; + + reflectRedirectedUrl = function() { + var location, preservedHash; + if (location = xhr.getResponseHeader('X-XHR-Redirected-To')) { + location = new ComponentUrl(location); + preservedHash = location.hasNoHash() ? document.location.hash : ''; + return window.history.replaceState(window.history.state, '', location.href + preservedHash); + } + }; + + crossOriginRedirect = function() { + var redirect; + if (((redirect = xhr.getResponseHeader('Location')) != null) && (new ComponentUrl(redirect)).crossOrigin()) { + return redirect; + } + }; + + rememberReferer = function() { + return referer = document.location.href; + }; + + rememberCurrentUrl = function() { + return window.history.replaceState({ + turbolinks: true, + url: document.location.href + }, '', document.location.href); + }; + + rememberCurrentState = function() { + return currentState = window.history.state; + }; + + manuallyTriggerHashChangeForFirefox = function() { + var url; + if (navigator.userAgent.match(/Firefox/) && !(url = new ComponentUrl).hasNoHash()) { + window.history.replaceState(currentState, '', url.withoutHash()); + return document.location.hash = url.hash; + } + }; + + recallScrollPosition = function(page) { + return window.scrollTo(page.positionX, page.positionY); + }; + + resetScrollPosition = function() { + if (document.location.hash) { + return document.location.href = document.location.href; + } else { + return window.scrollTo(0, 0); + } + }; + + clone = function(original) { + var copy, key, value; + if ((original == null) || typeof original !== 'object') { + return original; + } + copy = new original.constructor(); + for (key in original) { + value = original[key]; + copy[key] = clone(value); + } + return copy; + }; + + popCookie = function(name) { + var ref, value; + value = ((ref = document.cookie.match(new RegExp(name + "=(\\w+)"))) != null ? ref[1].toUpperCase() : void 0) || ''; + document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'; + return value; + }; + + triggerEvent = function(name, data) { + var event; + if (typeof Prototype !== 'undefined') { + Event.fire(document, name, data, true); + } + event = document.createEvent('Events'); + if (data) { + event.data = data; + } + event.initEvent(name, true, true); + return document.dispatchEvent(event); + }; + + pageChangePrevented = function(url) { + return !triggerEvent(EVENTS.BEFORE_CHANGE, { + url: url + }); + }; + + processResponse = function() { + var assetsChanged, clientOrServerError, doc, extractTrackAssets, intersection, validContent; + clientOrServerError = function() { + var ref; + return (400 <= (ref = xhr.status) && ref < 600); + }; + validContent = function() { + var contentType; + return ((contentType = xhr.getResponseHeader('Content-Type')) != null) && contentType.match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/); + }; + extractTrackAssets = function(doc) { + var i, len, node, ref, results; + ref = doc.querySelector('head').childNodes; + results = []; + for (i = 0, len = ref.length; i < len; i++) { + node = ref[i]; + if ((typeof node.getAttribute === "function" ? node.getAttribute('data-turbolinks-track') : void 0) != null) { + results.push(node.getAttribute('src') || node.getAttribute('href')); + } + } + return results; + }; + assetsChanged = function(doc) { + var fetchedAssets; + loadedAssets || (loadedAssets = extractTrackAssets(document)); + fetchedAssets = extractTrackAssets(doc); + return fetchedAssets.length !== loadedAssets.length || intersection(fetchedAssets, loadedAssets).length !== loadedAssets.length; + }; + intersection = function(a, b) { + var i, len, ref, results, value; + if (a.length > b.length) { + ref = [b, a], a = ref[0], b = ref[1]; + } + results = []; + for (i = 0, len = a.length; i < len; i++) { + value = a[i]; + if (indexOf.call(b, value) >= 0) { + results.push(value); + } + } + return results; + }; + if (!clientOrServerError() && validContent()) { + doc = createDocument(xhr.responseText); + if (doc && !assetsChanged(doc)) { + return doc; + } + } + }; + + extractTitleAndBody = function(doc) { + var title; + title = doc.querySelector('title'); + return [title != null ? title.textContent : void 0, removeNoscriptTags(doc.querySelector('body')), CSRFToken.get(doc).token, 'runScripts']; + }; + + CSRFToken = { + get: function(doc) { + var tag; + if (doc == null) { + doc = document; + } + return { + node: tag = doc.querySelector('meta[name="csrf-token"]'), + token: tag != null ? typeof tag.getAttribute === "function" ? tag.getAttribute('content') : void 0 : void 0 + }; + }, + update: function(latest) { + var current; + current = this.get(); + if ((current.token != null) && (latest != null) && current.token !== latest) { + return current.node.setAttribute('content', latest); + } + } + }; + + createDocument = function(html) { + var doc; + doc = document.documentElement.cloneNode(); + doc.innerHTML = html; + doc.head = doc.querySelector('head'); + doc.body = doc.querySelector('body'); + return doc; + }; + + ComponentUrl = (function() { + function ComponentUrl(original1) { + this.original = original1 != null ? original1 : document.location.href; + if (this.original.constructor === ComponentUrl) { + return this.original; + } + this._parse(); + } + + ComponentUrl.prototype.withoutHash = function() { + return this.href.replace(this.hash, '').replace('#', ''); + }; + + ComponentUrl.prototype.withoutHashForIE10compatibility = function() { + return this.withoutHash(); + }; + + ComponentUrl.prototype.hasNoHash = function() { + return this.hash.length === 0; + }; + + ComponentUrl.prototype.crossOrigin = function() { + return this.origin !== (new ComponentUrl).origin; + }; + + ComponentUrl.prototype._parse = function() { + var ref; + (this.link != null ? this.link : this.link = document.createElement('a')).href = this.original; + ref = this.link, this.href = ref.href, this.protocol = ref.protocol, this.host = ref.host, this.hostname = ref.hostname, this.port = ref.port, this.pathname = ref.pathname, this.search = ref.search, this.hash = ref.hash; + this.origin = [this.protocol, '//', this.hostname].join(''); + if (this.port.length !== 0) { + this.origin += ":" + this.port; + } + this.relative = [this.pathname, this.search, this.hash].join(''); + return this.absolute = this.href; + }; + + return ComponentUrl; + + })(); + + Link = (function(superClass) { + extend(Link, superClass); + + Link.HTML_EXTENSIONS = ['html']; + + Link.allowExtensions = function() { + var extension, extensions, i, len; + extensions = 1 <= arguments.length ? slice.call(arguments, 0) : []; + for (i = 0, len = extensions.length; i < len; i++) { + extension = extensions[i]; + Link.HTML_EXTENSIONS.push(extension); + } + return Link.HTML_EXTENSIONS; + }; + + function Link(link1) { + this.link = link1; + if (this.link.constructor === Link) { + return this.link; + } + this.original = this.link.href; + this.originalElement = this.link; + this.link = this.link.cloneNode(false); + Link.__super__.constructor.apply(this, arguments); + } + + Link.prototype.shouldIgnore = function() { + return this.crossOrigin() || this._anchored() || this._nonHtml() || this._optOut() || this._target(); + }; + + Link.prototype._anchored = function() { + return (this.hash.length > 0 || this.href.charAt(this.href.length - 1) === '#') && (this.withoutHash() === (new ComponentUrl).withoutHash()); + }; + + Link.prototype._nonHtml = function() { + return this.pathname.match(/\.[a-z]+$/g) && !this.pathname.match(new RegExp("\\.(?:" + (Link.HTML_EXTENSIONS.join('|')) + ")?$", 'g')); + }; + + Link.prototype._optOut = function() { + var ignore, link; + link = this.originalElement; + while (!(ignore || link === document)) { + ignore = link.getAttribute('data-no-turbolink') != null; + link = link.parentNode; + } + return ignore; + }; + + Link.prototype._target = function() { + return this.link.target.length !== 0; + }; + + return Link; + + })(ComponentUrl); + + Click = (function() { + Click.installHandlerLast = function(event) { + if (!event.defaultPrevented) { + document.removeEventListener('click', Click.handle, false); + return document.addEventListener('click', Click.handle, false); + } + }; + + Click.handle = function(event) { + return new Click(event); + }; + + function Click(event1) { + this.event = event1; + if (this.event.defaultPrevented) { + return; + } + this._extractLink(); + if (this._validForTurbolinks()) { + if (!pageChangePrevented(this.link.absolute)) { + visit(this.link.href); + } + this.event.preventDefault(); + } + } + + Click.prototype._extractLink = function() { + var link; + link = this.event.target; + while (!(!link.parentNode || link.nodeName === 'A')) { + link = link.parentNode; + } + if (link.nodeName === 'A' && link.href.length !== 0) { + return this.link = new Link(link); + } + }; + + Click.prototype._validForTurbolinks = function() { + return (this.link != null) && !(this.link.shouldIgnore() || this._nonStandardClick()); + }; + + Click.prototype._nonStandardClick = function() { + return this.event.which > 1 || this.event.metaKey || this.event.ctrlKey || this.event.shiftKey || this.event.altKey; + }; + + return Click; + + })(); + + ProgressBar = (function() { + var className; + + className = 'turbolinks-progress-bar'; + + function ProgressBar(elementSelector) { + this.elementSelector = elementSelector; + this._trickle = bind(this._trickle, this); + this.value = 0; + this.content = ''; + this.speed = 300; + this.opacity = 0.99; + this.install(); + } + + ProgressBar.prototype.install = function() { + this.element = document.querySelector(this.elementSelector); + this.element.classList.add(className); + this.styleElement = document.createElement('style'); + document.head.appendChild(this.styleElement); + return this._updateStyle(); + }; + + ProgressBar.prototype.uninstall = function() { + this.element.classList.remove(className); + return document.head.removeChild(this.styleElement); + }; + + ProgressBar.prototype.start = function() { + return this.advanceTo(5); + }; + + ProgressBar.prototype.advanceTo = function(value) { + var ref; + if ((value > (ref = this.value) && ref <= 100)) { + this.value = value; + this._updateStyle(); + if (this.value === 100) { + return this._stopTrickle(); + } else if (this.value > 0) { + return this._startTrickle(); + } + } + }; + + ProgressBar.prototype.done = function() { + if (this.value > 0) { + this.advanceTo(100); + return this._reset(); + } + }; + + ProgressBar.prototype._reset = function() { + var originalOpacity; + originalOpacity = this.opacity; + setTimeout((function(_this) { + return function() { + _this.opacity = 0; + return _this._updateStyle(); + }; + })(this), this.speed / 2); + return setTimeout((function(_this) { + return function() { + _this.value = 0; + _this.opacity = originalOpacity; + return _this._withSpeed(0, function() { + return _this._updateStyle(true); + }); + }; + })(this), this.speed); + }; + + ProgressBar.prototype._startTrickle = function() { + if (this.trickling) { + return; + } + this.trickling = true; + return setTimeout(this._trickle, this.speed); + }; + + ProgressBar.prototype._stopTrickle = function() { + return delete this.trickling; + }; + + ProgressBar.prototype._trickle = function() { + if (!this.trickling) { + return; + } + this.advanceTo(this.value + Math.random() / 2); + return setTimeout(this._trickle, this.speed); + }; + + ProgressBar.prototype._withSpeed = function(speed, fn) { + var originalSpeed, result; + originalSpeed = this.speed; + this.speed = speed; + result = fn(); + this.speed = originalSpeed; + return result; + }; + + ProgressBar.prototype._updateStyle = function(forceRepaint) { + if (forceRepaint == null) { + forceRepaint = false; + } + if (forceRepaint) { + this._changeContentToForceRepaint(); + } + return this.styleElement.textContent = this._createCSSRule(); + }; + + ProgressBar.prototype._changeContentToForceRepaint = function() { + return this.content = this.content === '' ? ' ' : ''; + }; + + ProgressBar.prototype._createCSSRule = function() { + return this.elementSelector + "." + className + "::before {\n content: '" + this.content + "';\n position: fixed;\n top: 0;\n left: 0;\n z-index: 2000;\n background-color: #0076ff;\n height: 3px;\n opacity: " + this.opacity + ";\n width: " + this.value + "%;\n transition: width " + this.speed + "ms ease-out, opacity " + (this.speed / 2) + "ms ease-in;\n transform: translate3d(0,0,0);\n}"; + }; + + return ProgressBar; + + })(); + + bypassOnLoadPopstate = function(fn) { + return setTimeout(fn, 500); + }; + + installDocumentReadyPageEventTriggers = function() { + return document.addEventListener('DOMContentLoaded', (function() { + triggerEvent(EVENTS.CHANGE); + return triggerEvent(EVENTS.UPDATE); + }), true); + }; + + installJqueryAjaxSuccessPageUpdateTrigger = function() { + if (typeof jQuery !== 'undefined') { + return jQuery(document).on('ajaxSuccess', function(event, xhr, settings) { + if (!jQuery.trim(xhr.responseText)) { + return; + } + return triggerEvent(EVENTS.UPDATE); + }); + } + }; + + installHistoryChangeHandler = function(event) { + var cachedPage, ref; + if ((ref = event.state) != null ? ref.turbolinks : void 0) { + if (cachedPage = pageCache[(new ComponentUrl(event.state.url)).absolute]) { + cacheCurrentPage(); + return fetchHistory(cachedPage); + } else { + return visit(event.target.location.href); + } + } + }; + + initializeTurbolinks = function() { + rememberCurrentUrl(); + rememberCurrentState(); + document.addEventListener('click', Click.installHandlerLast, true); + window.addEventListener('hashchange', function(event) { + rememberCurrentUrl(); + return rememberCurrentState(); + }, false); + return bypassOnLoadPopstate(function() { + return window.addEventListener('popstate', installHistoryChangeHandler, false); + }); + }; + + historyStateIsDefined = window.history.state !== void 0 || navigator.userAgent.match(/Firefox\/2[6|7]/); + + browserSupportsPushState = window.history && window.history.pushState && window.history.replaceState && historyStateIsDefined; + + browserIsntBuggy = !navigator.userAgent.match(/CriOS\//); + + requestMethodIsSafe = (ref = popCookie('request_method')) === 'GET' || ref === ''; + + browserSupportsTurbolinks = browserSupportsPushState && browserIsntBuggy && requestMethodIsSafe; + + browserSupportsCustomEvents = document.addEventListener && document.createEvent; + + if (browserSupportsCustomEvents) { + installDocumentReadyPageEventTriggers(); + installJqueryAjaxSuccessPageUpdateTrigger(); + } + + if (browserSupportsTurbolinks) { + visit = fetch; + initializeTurbolinks(); + } else { + visit = function(url) { + return document.location.href = url; + }; + } + + this.Turbolinks = { + visit: visit, + pagesCached: pagesCached, + enableTransitionCache: enableTransitionCache, + enableProgressBar: enableProgressBar, + allowLinkExtensions: Link.allowExtensions, + supported: browserSupportsTurbolinks, + EVENTS: clone(EVENTS) + }; + +}).call(this); -- cgit v1.2.1 From a6394540327cd3919e5189a35a21b57800a104fc Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 6 Jan 2017 23:50:08 +0800 Subject: Fix renaming --- lib/api/commits.rb | 2 +- lib/api/files.rb | 2 +- spec/features/projects/files/editing_a_file_spec.rb | 2 +- spec/lib/gitlab/diff/position_tracer_spec.rb | 6 +++--- spec/models/repository_spec.rb | 2 +- spec/services/files/update_service_spec.rb | 4 ++-- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/api/commits.rb b/lib/api/commits.rb index 2c1da0902c9..031759cdcdf 100644 --- a/lib/api/commits.rb +++ b/lib/api/commits.rb @@ -55,7 +55,7 @@ module API authorize! :push_code, user_project attrs = declared_params - attrs[:source_branch] = attrs[:branch_name] + attrs[:start_branch] = attrs[:branch_name] attrs[:target_branch] = attrs[:branch_name] attrs[:actions].map! do |action| action[:action] = action[:action].to_sym diff --git a/lib/api/files.rb b/lib/api/files.rb index 2e79e22e649..c58472de578 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -5,7 +5,7 @@ module API def commit_params(attrs) { file_path: attrs[:file_path], - source_branch: attrs[:branch_name], + start_branch: attrs[:branch_name], target_branch: attrs[:branch_name], commit_message: attrs[:commit_message], file_content: attrs[:content], diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb index fe047e00409..36a80d7575d 100644 --- a/spec/features/projects/files/editing_a_file_spec.rb +++ b/spec/features/projects/files/editing_a_file_spec.rb @@ -7,7 +7,7 @@ feature 'User wants to edit a file', feature: true do let(:user) { create(:user) } let(:commit_params) do { - source_branch: project.default_branch, + start_branch: project.default_branch, target_branch: project.default_branch, commit_message: "Committing First Update", file_path: ".gitignore", diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index c268f84c759..f77ab016e9b 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -99,7 +99,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do Files::CreateService.new( project, current_user, - source_branch: branch_name, + start_branch: branch_name, target_branch: branch_name, commit_message: "Create file", file_path: file_name, @@ -112,7 +112,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do Files::UpdateService.new( project, current_user, - source_branch: branch_name, + start_branch: branch_name, target_branch: branch_name, commit_message: "Update file", file_path: file_name, @@ -125,7 +125,7 @@ describe Gitlab::Diff::PositionTracer, lib: true do Files::DeleteService.new( project, current_user, - source_branch: branch_name, + start_branch: branch_name, target_branch: branch_name, commit_message: "Delete file", file_path: file_name diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 0f43c5c019a..36564a3f9e0 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -280,7 +280,7 @@ describe Repository, models: true do expect do repository.commit_dir(user, 'newdir', message: 'Create newdir', branch_name: 'patch', - source_branch_name: 'master', source_project: forked_project) + start_branch_name: 'master', start_project: forked_project) end.to change { repository.commits('master').count }.by(0) expect(repository.branch_exists?('patch')).to be_truthy diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb index 6fadee9304b..35e6e139238 100644 --- a/spec/services/files/update_service_spec.rb +++ b/spec/services/files/update_service_spec.rb @@ -17,8 +17,8 @@ describe Files::UpdateService do file_content: new_contents, file_content_encoding: "text", last_commit_sha: last_commit_sha, - source_project: project, - source_branch: project.default_branch, + start_project: project, + start_branch: project.default_branch, target_branch: target_branch } end -- cgit v1.2.1 From fe964cc235ed22a5d013d5874284763b698aba7c Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 3 Nov 2016 16:38:54 -0500 Subject: migrate all javascript asset bundles and require syntax --- .eslintrc | 4 +- app/assets/javascripts/application.js | 111 ++++---- app/assets/javascripts/awards_handler.js | 4 +- app/assets/javascripts/behaviors/autosize.js | 4 +- app/assets/javascripts/behaviors/quick_submit.js | 2 +- app/assets/javascripts/behaviors/requires_input.js | 2 +- app/assets/javascripts/blob/blob_ci_yaml.js.es6 | 3 +- .../blob/blob_dockerfile_selector.js.es6 | 3 +- .../javascripts/blob/blob_gitignore_selector.js | 2 +- .../javascripts/blob/blob_license_selector.js | 2 +- .../javascripts/blob_edit/blob_edit_bundle.js | 2 +- app/assets/javascripts/boards/boards_bundle.js.es6 | 28 +- .../javascripts/boards/components/board.js.es6 | 6 +- .../boards/components/board_list.js.es6 | 4 +- app/assets/javascripts/copy_to_clipboard.js | 2 +- .../cycle_analytics/cycle_analytics_bundle.js.es6 | 9 +- .../diff_notes/diff_notes_bundle.js.es6 | 18 +- app/assets/javascripts/dropzone_input.js | 2 +- .../environments/components/environment.js.es6 | 8 +- .../components/environment_actions.js.es6 | 3 +- .../components/environment_external_url.js.es6 | 3 +- .../components/environment_item.js.es6 | 17 +- .../components/environment_rollback.js.es6 | 3 +- .../components/environment_stop.js.es6 | 3 +- .../components/environment_terminal_button.js.es6 | 3 +- .../environments/environments_bundle.js.es6 | 8 +- .../services/environments_service.js.es6 | 1 + app/assets/javascripts/gl_field_errors.js.es6 | 2 +- app/assets/javascripts/gl_form.js | 2 + app/assets/javascripts/graphs/graphs_bundle.js | 15 +- .../javascripts/graphs/stat_graph_contributors.js | 2 +- .../graphs/stat_graph_contributors_graph.js | 2 +- app/assets/javascripts/header.js | 1 - app/assets/javascripts/issue.js | 6 +- app/assets/javascripts/lib/chart.js | 6 +- app/assets/javascripts/lib/d3.js | 6 +- .../javascripts/lib/utils/datetime_utility.js | 4 +- .../javascripts/lib/utils/emoji_aliases.js.erb | 6 - app/assets/javascripts/line_highlighter.js | 2 +- .../merge_conflicts/merge_conflicts_bundle.js.es6 | 16 +- app/assets/javascripts/merge_request.js | 6 +- app/assets/javascripts/merge_request_tabs.js.es6 | 6 +- app/assets/javascripts/network/network_bundle.js | 10 +- app/assets/javascripts/notes.js | 15 +- app/assets/javascripts/pipelines.js.es6 | 2 +- app/assets/javascripts/profile/profile_bundle.js | 10 +- .../protected_branches_bundle.js | 4 +- app/assets/javascripts/shortcuts_blob.js | 2 +- .../javascripts/shortcuts_dashboard_navigation.js | 2 +- app/assets/javascripts/shortcuts_find_file.js | 2 +- app/assets/javascripts/shortcuts_issuable.js | 4 +- app/assets/javascripts/shortcuts_navigation.js | 2 +- app/assets/javascripts/shortcuts_network.js | 2 +- app/assets/javascripts/snippet/snippet_bundle.js | 4 +- app/assets/javascripts/subbable_resource.js.es6 | 3 - .../templates/issuable_template_selector.js.es6 | 2 +- .../javascripts/terminal/terminal_bundle.js.es6 | 10 +- app/assets/javascripts/users/users_bundle.js | 10 +- .../javascripts/vue_common_component/commit.js.es6 | 4 +- app/assets/javascripts/webpack/application.js | 293 --------------------- app/assets/javascripts/wikis.js.es6 | 6 +- app/assets/javascripts/zen_mode.js | 10 +- app/helpers/javascript_helper.rb | 3 + app/views/profiles/_head.html.haml | 2 +- app/views/projects/blob/edit.html.haml | 2 +- app/views/projects/blob/new.html.haml | 2 +- app/views/projects/boards/_show.html.haml | 4 +- app/views/projects/cycle_analytics/show.html.haml | 2 +- app/views/projects/environments/index.html.haml | 2 +- app/views/projects/graphs/_head.html.haml | 4 +- app/views/projects/merge_requests/_show.html.haml | 2 +- .../projects/merge_requests/conflicts.html.haml | 2 +- .../merge_requests/widget/open/_accept.html.haml | 2 +- .../merge_requests/widget/open/_check.html.haml | 2 +- app/views/projects/network/show.html.haml | 2 +- .../projects/protected_branches/index.html.haml | 2 +- app/views/shared/snippets/_form.html.haml | 3 +- app/views/users/show.html.haml | 4 +- config/application.rb | 20 +- config/webpack.config.js | 21 +- package.json | 1 + vendor/assets/javascripts/es6-promise.auto.js | 3 - vendor/assets/javascripts/xterm/fit.js | 4 +- 83 files changed, 261 insertions(+), 564 deletions(-) delete mode 100644 app/assets/javascripts/lib/utils/emoji_aliases.js.erb delete mode 100644 app/assets/javascripts/webpack/application.js diff --git a/.eslintrc b/.eslintrc index e13f76b213c..8bab73bbe16 100644 --- a/.eslintrc +++ b/.eslintrc @@ -15,6 +15,8 @@ "filenames" ], "rules": { - "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"] + "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"], + "import/no-extraneous-dependencies": "off", + "import/no-unresolved": "off" } } diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index c21f0572fa7..830d706f02d 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,4 +1,4 @@ -/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */ +/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len, no-multi-spaces, import/newline-after-import */ /* global bp */ /* global Cookies */ /* global Flash */ @@ -6,62 +6,59 @@ /* global AwardsHandler */ /* global Aside */ -// This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript 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 jquery2 */ -/*= require jquery-ui/autocomplete */ -/*= require jquery-ui/datepicker */ -/*= require jquery-ui/draggable */ -/*= require jquery-ui/effect-highlight */ -/*= require jquery-ui/sortable */ -/*= require jquery_ujs */ -/*= require jquery.endless-scroll */ -/*= require jquery.highlight */ -/*= require jquery.waitforimages */ -/*= require jquery.caret */ -/*= require jquery.atwho */ -/*= require jquery.scrollTo */ -/*= require jquery.turbolinks */ -/*= require js.cookie */ -/*= require turbolinks */ -/*= require autosave */ -/*= require bootstrap/affix */ -/*= require bootstrap/alert */ -/*= require bootstrap/button */ -/*= require bootstrap/collapse */ -/*= require bootstrap/dropdown */ -/*= require bootstrap/modal */ -/*= require bootstrap/scrollspy */ -/*= require bootstrap/tab */ -/*= require bootstrap/transition */ -/*= require bootstrap/tooltip */ -/*= require bootstrap/popover */ -/*= require select2 */ -/*= require underscore */ -/*= require dropzone */ -/*= require mousetrap */ -/*= require mousetrap/pause */ -/*= require shortcuts */ -/*= require shortcuts_navigation */ -/*= require shortcuts_dashboard_navigation */ -/*= require shortcuts_issuable */ -/*= require shortcuts_network */ -/*= require jquery.nicescroll */ -/*= require date.format */ -/*= require_directory ./behaviors */ -/*= require_directory ./blob */ -/*= require_directory ./templates */ -/*= require_directory ./commit */ -/*= require_directory ./extensions */ -/*= require_directory ./lib/utils */ -/*= require_directory ./u2f */ -/*= require_directory . */ -/*= require fuzzaldrin-plus */ -/*= require es6-promise.auto */ +function requireAll(context) { return context.keys().map(context); } + +window.$ = window.jQuery = require('jquery'); +require('jquery-ui/ui/autocomplete'); +require('jquery-ui/ui/datepicker'); +require('jquery-ui/ui/draggable'); +require('jquery-ui/ui/effect-highlight'); +require('jquery-ui/ui/sortable'); +require('jquery-ujs'); +require('vendor/jquery.endless-scroll'); +require('vendor/jquery.highlight'); +require('vendor/jquery.waitforimages'); +require('vendor/jquery.caret'); +require('vendor/jquery.atwho'); +require('vendor/jquery.scrollTo'); +require('vendor/jquery.turbolinks'); +window.Cookies = require('vendor/js.cookie'); +require('vendor/turbolinks'); +require('./autosave'); +require('bootstrap/js/affix'); +require('bootstrap/js/alert'); +require('bootstrap/js/button'); +require('bootstrap/js/collapse'); +require('bootstrap/js/dropdown'); +require('bootstrap/js/modal'); +require('bootstrap/js/scrollspy'); +require('bootstrap/js/tab'); +require('bootstrap/js/transition'); +require('bootstrap/js/tooltip'); +require('bootstrap/js/popover'); +require('select2/select2.js'); +window._ = require('underscore'); +window.Dropzone = require('dropzone'); +require('mousetrap'); +require('mousetrap/plugins/pause/mousetrap-pause'); +require('./shortcuts'); +require('./shortcuts_navigation'); +require('./shortcuts_dashboard_navigation'); +require('./shortcuts_issuable'); +require('./shortcuts_network'); +require('vendor/jquery.nicescroll'); +require('vendor/date.format'); +requireAll(require.context('./behaviors', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./blob', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./templates', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./commit', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./extensions', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./lib/utils', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('.', false, /^\.\/(?!application).*\.(js|es6)$/)); +require('vendor/fuzzaldrin-plus'); +window.ES6Promise = require('vendor/es6-promise.auto'); +window.ES6Promise.polyfill(); (function () { document.addEventListener('page:fetch', function () { diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 107a7978a87..db9df8cdd3c 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -1,11 +1,13 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-plusplus, no-return-assign, camelcase, padded-blocks */ /* global Cookies */ +var emojiAliases = require('emoji-aliases'); + (function() { this.AwardsHandler = (function() { var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence function AwardsHandler() { - this.aliases = gl.emojiAliases(); + this.aliases = emojiAliases; $(document).off('click', '.js-add-award').on('click', '.js-add-award', (function(_this) { return function(e) { e.stopPropagation(); diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js index c62a4c5a456..9b5cd25989d 100644 --- a/app/assets/javascripts/behaviors/autosize.js +++ b/app/assets/javascripts/behaviors/autosize.js @@ -1,8 +1,8 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, padded-blocks, max-len */ /* global autosize */ -/*= require jquery.ba-resize */ -/*= require autosize */ +var autosize = require('vendor/autosize'); +require('vendor/jquery.ba-resize'); (function() { $(function() { diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js index 586f941a6e3..4f43e8baf42 100644 --- a/app/assets/javascripts/behaviors/quick_submit.js +++ b/app/assets/javascripts/behaviors/quick_submit.js @@ -6,7 +6,7 @@ // "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form // is submitted. // -/*= require extensions/jquery */ +require('../extensions/jquery'); // // ### Example Markup diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js index 72362988b2e..28bc06c5d76 100644 --- a/app/assets/javascripts/behaviors/requires_input.js +++ b/app/assets/javascripts/behaviors/requires_input.js @@ -4,7 +4,7 @@ // When called on a form with input fields with the `required` attribute, the // form's submit button will be disabled until all required fields have values. // -/*= require extensions/jquery */ +require('../extensions/jquery'); // // ### Example Markup diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 index 57bd13eecf8..a548509ee0d 100644 --- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 +++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6 @@ -1,7 +1,8 @@ /* eslint-disable padded-blocks, no-param-reassign, comma-dangle */ /* global Api */ -/*= require blob/template_selector */ +require('./template_selector'); + ((global) => { class BlobCiYamlSelector extends gl.TemplateSelector { diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 index bdf95017613..d4f60cc6ecd 100644 --- a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 +++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 @@ -1,5 +1,6 @@ /* global Api */ -/*= require blob/template_selector */ + +require('./template_selector'); (() => { const global = window.gl || (window.gl = {}); diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js index 15563e429a0..82a198ad825 100644 --- a/app/assets/javascripts/blob/blob_gitignore_selector.js +++ b/app/assets/javascripts/blob/blob_gitignore_selector.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, padded-blocks */ /* global Api */ -/*= require blob/template_selector */ +require('./template_selector'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js index d9c6f65a083..4c3ca20e25d 100644 --- a/app/assets/javascripts/blob/blob_license_selector.js +++ b/app/assets/javascripts/blob/blob_license_selector.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, padded-blocks */ /* global Api */ -/*= require blob/template_selector */ +require('./template_selector'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js index 8c40e36a80a..9c523d3b22e 100644 --- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js @@ -2,7 +2,7 @@ /* global EditBlob */ /* global NewCommitForm */ -/*= require_tree . */ +require('./edit_blob'); (function() { $(function() { diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 2c9ab61c94d..9f454486efc 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -1,19 +1,21 @@ -/* eslint-disable one-var, indent, quote-props, comma-dangle, space-before-function-paren */ +/* eslint-disable one-var, indent, quote-props, comma-dangle, space-before-function-paren, import/newline-after-import, no-multi-spaces, max-len */ /* global Vue */ /* global BoardService */ -//= require vue -//= require vue-resource -//= require Sortable -//= require_tree ./models -//= require_tree ./stores -//= require_tree ./services -//= require_tree ./mixins -//= require_tree ./filters -//= require ./components/board -//= require ./components/board_sidebar -//= require ./components/new_list_dropdown -//= require ./vue_resource_interceptor +function requireAll(context) { return context.keys().map(context); } + +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +window.Sortable = require('vendor/Sortable'); +requireAll(require.context('./models', true, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./stores', true, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./services', true, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./mixins', true, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./filters', true, /^\.\/.*\.(js|es6)$/)); +require('./components/board'); +require('./components/board_sidebar'); +require('./components/new_list_dropdown'); +require('./vue_resource_interceptor'); $(() => { const $boardApp = document.getElementById('board-app'), diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index d1fb0ec48e0..07960fbbd80 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -2,9 +2,9 @@ /* global Vue */ /* global Sortable */ -//= require ./board_blank_state -//= require ./board_delete -//= require ./board_list +require('./board_blank_state'); +require('./board_delete'); +require('./board_list'); (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 6711930622b..d896076d4af 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -2,8 +2,8 @@ /* global Vue */ /* global Sortable */ -//= require ./board_card -//= require ./board_new_issue +require('./board_card'); +require('./board_new_issue'); (() => { const Store = gl.issueBoards.BoardsStore; diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js index 6a13f38588d..0a6f95e96ec 100644 --- a/app/assets/javascripts/copy_to_clipboard.js +++ b/app/assets/javascripts/copy_to_clipboard.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */ /* global Clipboard */ -/*= require clipboard */ +window.Clipboard = require('vendor/clipboard'); (function() { var genericError, genericSuccess, showTooltip; diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 index 2f810a69758..3b2f5efa27d 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -2,9 +2,12 @@ /* global Cookies */ /* global Flash */ -//= require vue -//= require_tree ./svg -//= require_tree . +window.Vue = require('vue'); +window.Cookies = require('vendor/js.cookie'); + +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('.', false, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/)); $(() => { const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 index 840b5aa922e..0a25b8f1ccd 100644 --- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 +++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 @@ -1,14 +1,16 @@ -/* eslint-disable func-names, comma-dangle, new-cap, no-new */ +/* eslint-disable func-names, comma-dangle, new-cap, no-new, import/newline-after-import, no-multi-spaces, max-len */ /* global Vue */ /* global ResolveCount */ -//= require vue -//= require vue-resource -//= require_directory ./models -//= require_directory ./stores -//= require_directory ./services -//= require_directory ./mixins -//= require_directory ./components +function requireAll(context) { return context.keys().map(context); } + +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +requireAll(require.context('./models', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./stores', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./services', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./mixins', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./components', false, /^\.\/.*\.(js|es6)$/)); $(() => { const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn'; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 56cb39be642..b280ab88c8d 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,7 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks */ /* global Dropzone */ -/*= require preview_markdown */ +require('./preview_markdown'); (function() { this.DropzoneInput = (function() { diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 6b7fb9215d1..cc533620ead 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -2,10 +2,10 @@ /* global Vue */ /* global EnvironmentsService */ -//= require vue -//= require vue-resource -//= require_tree ../services/ -//= require ./environment_item +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('../services/environments_service'); +require('./environment_item'); (() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/components/environment_actions.js.es6 b/app/assets/javascripts/environments/components/environment_actions.js.es6 index 81468f4d3bc..ed1c78945db 100644 --- a/app/assets/javascripts/environments/components/environment_actions.js.es6 +++ b/app/assets/javascripts/environments/components/environment_actions.js.es6 @@ -1,6 +1,7 @@ -/*= require vue */ /* global Vue */ +window.Vue = require('vue'); + (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; diff --git a/app/assets/javascripts/environments/components/environment_external_url.js.es6 b/app/assets/javascripts/environments/components/environment_external_url.js.es6 index 6592c1b5f0f..28cc0022d17 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.js.es6 +++ b/app/assets/javascripts/environments/components/environment_external_url.js.es6 @@ -1,6 +1,7 @@ -/*= require vue */ /* global Vue */ +window.Vue = require('vue'); + (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 0e6bc3fdb2c..521873b14b4 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -1,14 +1,15 @@ /* global Vue */ /* global timeago */ -/*= require timeago */ -/*= require lib/utils/text_utility */ -/*= require vue_common_component/commit */ -/*= require ./environment_actions */ -/*= require ./environment_external_url */ -/*= require ./environment_stop */ -/*= require ./environment_rollback */ -/*= require ./environment_terminal_button */ +window.Vue = require('vue'); +window.timeago = require('vendor/timeago'); +require('../../lib/utils/text_utility'); +require('../../vue_common_component/commit'); +require('./environment_actions'); +require('./environment_external_url'); +require('./environment_stop'); +require('./environment_rollback'); +require('./environment_terminal_button'); (() => { /** diff --git a/app/assets/javascripts/environments/components/environment_rollback.js.es6 b/app/assets/javascripts/environments/components/environment_rollback.js.es6 index b52298b4a88..5938340a128 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.js.es6 +++ b/app/assets/javascripts/environments/components/environment_rollback.js.es6 @@ -1,6 +1,7 @@ -/*= require vue */ /* global Vue */ +window.Vue = require('vue'); + (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; diff --git a/app/assets/javascripts/environments/components/environment_stop.js.es6 b/app/assets/javascripts/environments/components/environment_stop.js.es6 index 0a29f2f36e9..be9526989a0 100644 --- a/app/assets/javascripts/environments/components/environment_stop.js.es6 +++ b/app/assets/javascripts/environments/components/environment_stop.js.es6 @@ -1,6 +1,7 @@ -/*= require vue */ /* global Vue */ +window.Vue = require('vue'); + (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 index 050184ba497..a3ad063f7cb 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 +++ b/app/assets/javascripts/environments/components/environment_terminal_button.js.es6 @@ -1,6 +1,7 @@ -/*= require vue */ /* global Vue */ +window.Vue = require('vue'); + (() => { window.gl = window.gl || {}; window.gl.environmentsList = window.gl.environmentsList || {}; diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 9f24a6a4f88..58f4c6eadb2 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -1,8 +1,8 @@ -//= require vue -//= require_tree ./stores/ -//= require ./components/environment -//= require ./vue_resource_interceptor +window.Vue = require('vue'); +require('./stores/environments_store'); +require('./components/environment'); +require('./vue_resource_interceptor'); $(() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/services/environments_service.js.es6 b/app/assets/javascripts/environments/services/environments_service.js.es6 index 575a45d9802..fab8d977f58 100644 --- a/app/assets/javascripts/environments/services/environments_service.js.es6 +++ b/app/assets/javascripts/environments/services/environments_service.js.es6 @@ -1,5 +1,6 @@ /* globals Vue */ /* eslint-disable no-unused-vars, no-param-reassign */ + class EnvironmentsService { constructor(root) { diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6 index 63f9cafa8d0..8b46c4e378f 100644 --- a/app/assets/javascripts/gl_field_errors.js.es6 +++ b/app/assets/javascripts/gl_field_errors.js.es6 @@ -1,6 +1,6 @@ /* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign, padded-blocks */ -//= require gl_field_error +require('./gl_field_error'); ((global) => { const customValidationFlag = 'gl-field-error-ignore'; diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index 04814fa0843..34244813b4b 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -3,6 +3,8 @@ /* global DropzoneInput */ /* global autosize */ +var autosize = require('vendor/autosize'); + (function() { this.GLForm = (function() { function GLForm(form) { diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js index 32c26349da0..4f7777aa5bc 100644 --- a/app/assets/javascripts/graphs/graphs_bundle.js +++ b/app/assets/javascripts/graphs/graphs_bundle.js @@ -1,12 +1,3 @@ -/* eslint-disable func-names, space-before-function-paren */ -// This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript 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_tree . */ - -(function() { - -}).call(this); +// require everything else in this directory +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('.', false, /^\.\/(?!graphs_bundle).*\.(js|es6)$/)); diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js index 2d08a7c6ac3..c702ce2743d 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors.js @@ -5,7 +5,7 @@ /* global ContributorsStatGraphUtil */ /* global d3 */ -/*= require d3 */ +window.d3 = require('d3'); (function() { this.ContributorsStatGraph = (function() { diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js index 9c5e9381e52..3b7370bd8f6 100644 --- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js +++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js @@ -2,7 +2,7 @@ /* global d3 */ /* global ContributorsGraph */ -/*= require d3 */ +window.d3 = require('d3'); (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }, diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index c7cbf9ca44b..9dd14b4c2ed 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,6 +1,5 @@ /* eslint-disable wrap-iife, func-names, space-before-function-paren, padded-blocks, prefer-arrow-callback, no-var, max-len */ (function() { - $(document).on('todo:toggle', function(e, count) { var $todoPendingCount = $('.todos-pending-count'); $todoPendingCount.text(gl.text.addDelimiter(count)); diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js index 61e8531153b..97e03ede0e5 100644 --- a/app/assets/javascripts/issue.js +++ b/app/assets/javascripts/issue.js @@ -1,9 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */ /* global Flash */ -/*= require flash */ -/*= require jquery.waitforimages */ -/*= require task_list */ +require('./flash'); +require('vendor/jquery.waitforimages'); +require('vendor/task_list'); (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js index d8ad5aaeffe..9b011d89e93 100644 --- a/app/assets/javascripts/lib/chart.js +++ b/app/assets/javascripts/lib/chart.js @@ -1,7 +1,3 @@ /* eslint-disable func-names, space-before-function-paren */ -/*= require Chart */ - -(function() { - -}).call(this); +window.Chart = require('vendor/Chart'); diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js index 57e7986576c..a9dd32edbed 100644 --- a/app/assets/javascripts/lib/d3.js +++ b/app/assets/javascripts/lib/d3.js @@ -1,7 +1,3 @@ /* eslint-disable func-names, space-before-function-paren */ -/*= require d3 */ - -(function() { - -}).call(this); +window.d3 = require('d3'); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 30e4e490543..b6c1fd4c4f8 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -2,8 +2,8 @@ /* global timeago */ /* global dateFormat */ -/*= require timeago */ -/*= require date.format */ +window.timeago = require('vendor/timeago'); +require('vendor/date.format'); (function() { (function(w) { diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb deleted file mode 100644 index aeb86c9fa5b..00000000000 --- a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb +++ /dev/null @@ -1,6 +0,0 @@ -(function() { - gl.emojiAliases = function() { - return JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>'); - }; - -}).call(this); diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index 9af89b79f84..e7351173610 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -4,7 +4,7 @@ // // Handles single- and multi-line selection and highlight for blob views. // -/*= require jquery.scrollTo */ +require('vendor/jquery.scrollTo'); // // ### Example Markup diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 index 83520702f9b..92fad17cf00 100644 --- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 +++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 @@ -2,14 +2,14 @@ /* global Vue */ /* global Flash */ -//= require vue -//= require ./merge_conflict_store -//= require ./merge_conflict_service -//= require ./mixins/line_conflict_utils -//= require ./mixins/line_conflict_actions -//= require ./components/diff_file_editor -//= require ./components/inline_conflict_lines -//= require ./components/parallel_conflict_lines +window.Vue = require('vue'); +require('./merge_conflict_store'); +require('./merge_conflict_service'); +require('./mixins/line_conflict_utils'); +require('./mixins/line_conflict_actions'); +require('./components/diff_file_editor'); +require('./components/inline_conflict_lines'); +require('./components/parallel_conflict_lines'); $(() => { const INTERACTIVE_RESOLVE_MODE = 'interactive'; diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 244c2f6746c..19526157410 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -1,9 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len, prefer-arrow-callback */ /* global MergeRequestTabs */ -/*= require jquery.waitforimages */ -/*= require task_list */ -/*= require merge_request_tabs */ +require('vendor/jquery.waitforimages'); +require('vendor/task_list'); +require('./merge_request_tabs'); (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 860e7e066a0..b8ab00458de 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -1,11 +1,11 @@ /* eslint-disable no-new, class-methods-use-this */ /* global Breakpoints */ /* global Cookies */ -/* global DiffNotesApp */ /* global Flash */ -/*= require js.cookie */ -/*= require breakpoints */ +require('./breakpoints'); +window.Cookies = require('vendor/js.cookie'); +require('./flash'); /* eslint-disable max-len */ // MergeRequestTabs diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js index 17833d3419a..1e91911c02c 100644 --- a/app/assets/javascripts/network/network_bundle.js +++ b/app/assets/javascripts/network/network_bundle.js @@ -2,13 +2,9 @@ /* global Network */ /* global ShortcutsNetwork */ -// This is a manifest file that'll be compiled into including all the files listed below. -// Add new JavaScript 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_tree . */ +// require everything else in this directory +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('.', false, /^\.\/(?!network_bundle).*\.(js|es6)$/)); (function() { $(function() { diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 8de5a6191b6..872c0d0caaa 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -4,13 +4,14 @@ /* global Autosave */ /* global ResolveService */ -/*= require autosave */ -/*= require autosize */ -/*= require dropzone */ -/*= require dropzone_input */ -/*= require gfm_auto_complete */ -/*= require jquery.atwho */ -/*= require task_list */ +require('./autosave'); +window.autosize = require('vendor/autosize'); +window.Dropzone = require('dropzone'); +require('./dropzone_input'); +require('./gfm_auto_complete'); +require('vendor/jquery.caret'); // required by jquery.atwho +require('vendor/jquery.atwho'); +require('vendor/task_list'); (function() { var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6 index 0b09ad113a3..f704551a548 100644 --- a/app/assets/javascripts/pipelines.js.es6 +++ b/app/assets/javascripts/pipelines.js.es6 @@ -1,6 +1,6 @@ /* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, padded-blocks, no-param-reassign, max-len */ -//= require lib/utils/bootstrap_linked_tabs +require('./lib/utils/bootstrap_linked_tabs'); ((global) => { diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js index f50802bdf2e..d7f3c9fd37e 100644 --- a/app/assets/javascripts/profile/profile_bundle.js +++ b/app/assets/javascripts/profile/profile_bundle.js @@ -1,7 +1,3 @@ -/* eslint-disable func-names, space-before-function-paren */ - -/*= require_tree . */ - -(function() { - -}).call(this); +// require everything else in this directory +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('.', false, /^\.\/(?!profile_bundle).*\.(js|es6)$/)); diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js index 15b3affd469..ffb66caf5f4 100644 --- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js +++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js @@ -1 +1,3 @@ -/*= require_tree . */ +// require everything else in this directory +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('.', false, /^\.\/(?!protected_branches_bundle).*\.(js|es6)$/)); diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js index c26903038b4..41c00e2048b 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/shortcuts_blob.js @@ -2,7 +2,7 @@ /* global Shortcuts */ /* global Mousetrap */ -/*= require shortcuts */ +require('./shortcuts'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js index 4549742bbcb..bc33d52a906 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js @@ -2,7 +2,7 @@ /* global Mousetrap */ /* global Shortcuts */ -/*= require shortcuts */ +require('./shortcuts'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js index 3a81380eef0..775c3a5e9b7 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/shortcuts_find_file.js @@ -2,7 +2,7 @@ /* global Mousetrap */ /* global ShortcutsNavigation */ -/*= require shortcuts_navigation */ +require('./shortcuts_navigation'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index b892fbc3393..2823e43585c 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -4,8 +4,8 @@ /* global ShortcutsNavigation */ /* global sidebar */ -/*= require mousetrap */ -/*= require shortcuts_navigation */ +require('mousetrap'); +require('./shortcuts_navigation'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js index 0776d0a9b67..d35d1dd6bf2 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/shortcuts_navigation.js @@ -2,7 +2,7 @@ /* global Mousetrap */ /* global Shortcuts */ -/*= require shortcuts */ +require('./shortcuts'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js index ecc3fab84c3..6a73b48899b 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/shortcuts_network.js @@ -2,7 +2,7 @@ /* global Mousetrap */ /* global ShortcutsNavigation */ -/*= require shortcuts_navigation */ +require('./shortcuts_navigation'); (function() { var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js index 18512d179b3..a3f128f9315 100644 --- a/app/assets/javascripts/snippet/snippet_bundle.js +++ b/app/assets/javascripts/snippet/snippet_bundle.js @@ -1,7 +1,9 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, semi, padded-blocks, max-len */ /* global ace */ -/*= require_tree . */ +// require everything else in this directory +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('.', false, /^\.\/(?!snippet_bundle).*\.(js|es6)$/)); (function() { $(function() { diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 index 932120157a3..d8191605128 100644 --- a/app/assets/javascripts/subbable_resource.js.es6 +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -1,6 +1,3 @@ -//= require vue -//= require vue-resource - (() => { /* * SubbableResource can be extended to provide a pubsub-style service for one-off REST diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6 index bdec948fb63..8dce6ed9fbf 100644 --- a/app/assets/javascripts/templates/issuable_template_selector.js.es6 +++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable prefer-const, comma-dangle, max-len, no-useless-return, object-curly-spacing, no-param-reassign, max-len */ /* global Api */ -/*= require ../blob/template_selector */ +require('../blob/template_selector'); ((global) => { class IssuableTemplateSelector extends gl.TemplateSelector { diff --git a/app/assets/javascripts/terminal/terminal_bundle.js.es6 b/app/assets/javascripts/terminal/terminal_bundle.js.es6 index 33d2c1e1a17..13cf3a10a38 100644 --- a/app/assets/javascripts/terminal/terminal_bundle.js.es6 +++ b/app/assets/javascripts/terminal/terminal_bundle.js.es6 @@ -1,7 +1,7 @@ -//= require xterm/encoding-indexes -//= require xterm/encoding -//= require xterm/xterm.js -//= require xterm/fit.js -//= require ./terminal.js +require('vendor/xterm/encoding-indexes.js'); +require('vendor/xterm/encoding.js'); +window.Terminal = require('vendor/xterm/xterm.js'); +require('vendor/xterm/fit.js'); +require('./terminal.js'); $(() => new gl.Terminal({ selector: '#terminal' })); diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js index f50802bdf2e..4cad60a59b1 100644 --- a/app/assets/javascripts/users/users_bundle.js +++ b/app/assets/javascripts/users/users_bundle.js @@ -1,7 +1,3 @@ -/* eslint-disable func-names, space-before-function-paren */ - -/*= require_tree . */ - -(function() { - -}).call(this); +// require everything else in this directory +function requireAll(context) { return context.keys().map(context); } +requireAll(require.context('.', false, /^\.\/(?!users_bundle).*\.(js|es6)$/)); diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 index 62a22e39a3b..4adad7bea31 100644 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ b/app/assets/javascripts/vue_common_component/commit.js.es6 @@ -1,5 +1,7 @@ -/*= require vue */ /* global Vue */ + +window.Vue = require('vue'); + (() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/webpack/application.js b/app/assets/javascripts/webpack/application.js deleted file mode 100644 index 660e00d4c5b..00000000000 --- a/app/assets/javascripts/webpack/application.js +++ /dev/null @@ -1,293 +0,0 @@ -/* eslint-disable */ - -/** - * Simulate sprockets compile order of application.js through CommonJS require statements - * - * Currently exports everything appropriate to window until the scripts that rely on this behavior - * can be refactored. - * - * Test the output from this against sprockets output and it should be almost identical apart from - * webpack's CommonJS wrapper. You can add the following line to webpack.config.js to fix the - * script indentation: - * config.output.sourcePrefix = ''; - */ - -/*= require jquery2 */ -window.jQuery = window.$ = require('jquery'); - -/*= require jquery-ui/autocomplete */ -// depends on jquery-ui/core, jquery-ui/widget, jquery-ui/menu, jquery-ui/position -require('jquery-ui/ui/core'); -require('jquery-ui/ui/widget'); -require('jquery-ui/ui/position'); -require('jquery-ui/ui/menu'); -require('jquery-ui/ui/autocomplete'); - -/*= require jquery-ui/datepicker */ -// depends on jquery-ui/core -require('jquery-ui/ui/datepicker'); - -/*= require jquery-ui/draggable */ -// depends on jquery-ui/core, jquery-ui/widget, jquery-ui/mouse -require('jquery-ui/ui/mouse'); -require('jquery-ui/ui/draggable'); - -/*= require jquery-ui/effect-highlight */ -// depends on jquery-ui/effect -require('jquery-ui/ui/effect'); -require('jquery-ui/ui/effect-highlight'); - -/*= require jquery-ui/sortable */ -// depends on jquery-ui/core, jquery-ui/widget, jquery-ui/mouse -require('jquery-ui/ui/sortable'); - -/*= require jquery_ujs */ -require('jquery-ujs'); - -/*= require jquery.endless-scroll */ -require('vendor/jquery.endless-scroll'); - -/*= require jquery.highlight */ -require('vendor/jquery.highlight'); - -/*= require jquery.waitforimages */ -require('vendor/jquery.waitforimages'); - -/*= require jquery.atwho */ -require('vendor/jquery.caret'); // required by jquery.atwho -require('vendor/jquery.atwho'); - -/*= require jquery.scrollTo */ -require('vendor/jquery.scrollTo'); - -/*= require jquery.turbolinks */ -require('vendor/jquery.turbolinks'); - -/*= require js.cookie */ -window.Cookies = require('vendor/js.cookie'); - -/*= require turbolinks */ -require('vendor/turbolinks'); - -/*= require autosave */ -require('../autosave'); - -/*= require bootstrap/affix */ -require('bootstrap/js/affix'); - -/*= require bootstrap/alert */ -require('bootstrap/js/alert'); - -/*= require bootstrap/button */ -require('bootstrap/js/button'); - -/*= require bootstrap/collapse */ -require('bootstrap/js/collapse'); - -/*= require bootstrap/dropdown */ -require('bootstrap/js/dropdown'); - -/*= require bootstrap/modal */ -require('bootstrap/js/modal'); - -/*= require bootstrap/scrollspy */ -require('bootstrap/js/scrollspy'); - -/*= require bootstrap/tab */ -require('bootstrap/js/tab'); - -/*= require bootstrap/transition */ -require('bootstrap/js/transition'); - -/*= require bootstrap/tooltip */ -require('bootstrap/js/tooltip'); - -/*= require bootstrap/popover */ -require('bootstrap/js/popover'); - -/*= require select2 */ -require('select2/select2.js'); - -/*= require underscore */ -window._ = require('underscore'); - -/*= require dropzone */ -window.Dropzone = require('dropzone'); - -/*= require mousetrap */ -require('mousetrap'); - -/*= require mousetrap/pause */ -require('mousetrap/plugins/pause/mousetrap-pause'); - -/*= require shortcuts */ -require('../shortcuts'); - -/*= require shortcuts_navigation */ -require('../shortcuts_navigation'); - -/*= require shortcuts_dashboard_navigation */ -require('../shortcuts_dashboard_navigation'); - -/*= require shortcuts_issuable */ -require('../shortcuts_issuable'); - -/*= require shortcuts_network */ -require('../shortcuts_network'); - -/*= require jquery.nicescroll */ -require('vendor/jquery.nicescroll'); - -/*= require date.format */ -require('vendor/date.format'); - -/*= require_directory ./behaviors */ -require('vendor/jquery.ba-resize'); -window.autosize = require('vendor/autosize'); -require('../behaviors/autosize'); // requires vendor/jquery.ba-resize and vendor/autosize -require('../behaviors/details_behavior'); -require('../extensions/jquery'); -require('../behaviors/quick_submit'); // requires extensions/jquery -require('../behaviors/requires_input'); -require('../behaviors/toggler_behavior'); - -/*= require_directory ./blob */ -require('../blob/template_selector'); -require('../blob/blob_ci_yaml'); // requires template_selector -require('../blob/blob_file_dropzone'); -require('../blob/blob_gitignore_selector'); -require('../blob/blob_gitignore_selectors'); -require('../blob/blob_license_selector'); -require('../blob/blob_license_selectors'); - -/*= require_directory ./templates */ -require('../templates/issuable_template_selector'); -require('../templates/issuable_template_selectors'); - -/*= require_directory ./commit */ -require('../commit/file'); -require('../commit/image_file'); - -/*= require_directory ./extensions */ -require('../extensions/array'); -require('../extensions/element'); - -/*= require_directory ./lib/utils */ -require('../lib/utils/animate'); -require('../lib/utils/common_utils'); -require('../lib/utils/datetime_utility'); -// require('../lib/utils/emoji_aliases.js.erb'); -window.gl.emojiAliases = function() { return require('emoji-aliases'); }; -require('../lib/utils/jquery.timeago'); -require('../lib/utils/notify'); -require('../lib/utils/text_utility'); -require('../lib/utils/type_utility'); -require('../lib/utils/url_utility'); - -/*= require_directory ./u2f */ -require('../u2f/authenticate'); -require('../u2f/error'); -require('../u2f/register'); -require('../u2f/util'); - -/*= require_directory . */ -require('../abuse_reports'); -require('../activities'); -require('../admin'); -require('../api'); -require('../aside'); -require('../awards_handler'); -require('../breakpoints'); -require('../broadcast_message'); -require('../build'); -require('../build_artifacts'); -require('../build_variables'); -require('../commit'); -require('../commits'); -require('../compare'); -require('../compare_autocomplete'); -require('../confirm_danger_modal'); -window.Clipboard = require('vendor/clipboard'); // required by copy_to_clipboard -require('../copy_to_clipboard'); -require('../create_label'); -require('vue'); // required by cycle_analytics -require('../cycle_analytics'); -require('../diff'); -require('../dispatcher'); -require('../preview_markdown'); -require('../dropzone_input'); -require('../due_date_select'); -require('../files_comment_button'); -require('../flash'); -require('../gfm_auto_complete'); -require('../gl_dropdown'); -require('../gl_field_errors'); -require('../gl_form'); -require('../group_avatar'); -require('../groups_select'); -require('../header'); -require('../importer_status'); -require('../issuable'); -require('../issuable_context'); -require('../issuable_form'); -require('vendor/task_list'); // required by issue -require('../issue'); -require('../issue_status_select'); -require('../issues_bulk_assignment'); -require('../label_manager'); -require('../labels'); -require('../labels_select'); -require('../layout_nav'); -require('../line_highlighter'); -require('../logo'); -require('../member_expiration_date'); -require('../members'); -require('../merge_request_tabs'); -require('../merge_request'); -require('../merge_request_widget'); -require('../merged_buttons'); -require('../milestone'); -require('../milestone_select'); -require('../namespace_select'); -require('../new_branch_form'); -require('../new_commit_form'); -require('../notes'); -require('../notifications_dropdown'); -require('../notifications_form'); -require('../pager'); -require('../pipelines'); -require('../project'); -require('../project_avatar'); -require('../project_find_file'); -require('../project_fork'); -require('../project_import'); -require('../project_new'); -require('../project_select'); -require('../project_show'); -require('../projects_list'); -require('../right_sidebar'); -require('../search'); -require('../search_autocomplete'); -require('../shortcuts_blob'); -require('../shortcuts_find_file'); -require('../sidebar'); -require('../single_file_diff'); -require('../snippets_list'); -require('../star'); -require('../subscription'); -require('../subscription_select'); -require('../syntax_highlight'); -require('../todos'); -require('../tree'); -require('../user'); -require('../user_tabs'); -require('../username_validator'); -require('../users_select'); -require('vendor/latinise'); // required by wikis -require('../wikis'); -require('../zen_mode'); - -/*= require fuzzaldrin-plus */ -require('vendor/fuzzaldrin-plus'); - -require('../application'); diff --git a/app/assets/javascripts/wikis.js.es6 b/app/assets/javascripts/wikis.js.es6 index ecff5fd5bf4..ef99b2e92f0 100644 --- a/app/assets/javascripts/wikis.js.es6 +++ b/app/assets/javascripts/wikis.js.es6 @@ -1,9 +1,9 @@ /* eslint-disable no-param-reassign */ /* global Breakpoints */ -/*= require latinise */ -/*= require breakpoints */ -/*= require jquery.nicescroll */ +require('vendor/latinise'); +require('./breakpoints'); +require('vendor/jquery.nicescroll'); ((global) => { const dasherize = str => str.replace(/[_\s]+/g, '-'); diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js index e09b59dd5aa..a1c3a19a3e9 100644 --- a/app/assets/javascripts/zen_mode.js +++ b/app/assets/javascripts/zen_mode.js @@ -6,11 +6,11 @@ // /*= provides zen_mode:enter */ /*= provides zen_mode:leave */ -// -/*= require jquery.scrollTo */ -/*= require dropzone */ -/*= require mousetrap */ -/*= require mousetrap/pause */ + +require('vendor/jquery.scrollTo'); +window.Dropzone = require('dropzone'); +require('mousetrap'); +require('mousetrap/plugins/pause/mousetrap-pause'); // // ### Events diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb index 0e456214d37..d9c8c70076b 100644 --- a/app/helpers/javascript_helper.rb +++ b/app/helpers/javascript_helper.rb @@ -2,4 +2,7 @@ module JavascriptHelper def page_specific_javascript_tag(js) javascript_include_tag asset_path(js), { "data-turbolinks-track" => true } end + def page_specific_javascript_bundle_tag(js) + javascript_include_tag *webpack_asset_paths(js), { "data-turbolinks-track" => true } + end end diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml index 943ebdaeffe..1df04ea614e 100644 --- a/app/views/profiles/_head.html.haml +++ b/app/views/profiles/_head.html.haml @@ -1,3 +1,3 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/cropper.js') - = page_specific_javascript_tag('profile/profile_bundle.js') + = page_specific_javascript_bundle_tag('profile') diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index a5dcd93f42e..8853801016b 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -2,7 +2,7 @@ - page_title "Edit", @blob.path, @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js') + = page_specific_javascript_bundle_tag('blob_edit') = render "projects/commits/head" %div{ class: container_class } diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml index b6ed9518c48..e0ce8cc9601 100644 --- a/app/views/projects/blob/new.html.haml +++ b/app/views/projects/blob/new.html.haml @@ -1,7 +1,7 @@ - page_title "New File", @path.presence, @ref - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js') + = page_specific_javascript_bundle_tag('blob_edit') %h3.page-title New File diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 356bd50f7f3..2d1f377a6dd 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -3,8 +3,8 @@ - page_title "Boards" - content_for :page_specific_javascripts do - = page_specific_javascript_tag('boards/boards_bundle.js') - = page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test? + = page_specific_javascript_bundle_tag('boards') + = page_specific_javascript_bundle_tag('boards_test') if Rails.env.test? %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" %script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list" diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index 479ce44f378..5405ff16bea 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -1,7 +1,7 @@ - @no_container = true - page_title "Cycle Analytics" - content_for :page_specific_javascripts do - = page_specific_javascript_tag("cycle_analytics/cycle_analytics_bundle.js") + = page_specific_javascript_bundle_tag('cycle_analytics') = render "projects/pipelines/head" diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml index 8c728eb0f6a..1f27d41ddd9 100644 --- a/app/views/projects/environments/index.html.haml +++ b/app/views/projects/environments/index.html.haml @@ -3,7 +3,7 @@ = render "projects/pipelines/head" - content_for :page_specific_javascripts do - = page_specific_javascript_tag("environments/environments_bundle.js") + = page_specific_javascript_bundle_tag("environments") #environments-list-view{ data: { environments_data: environments_list_data, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml index 1a62a6a809c..67018aaa2ac 100644 --- a/app/views/projects/graphs/_head.html.haml +++ b/app/views/projects/graphs/_head.html.haml @@ -5,8 +5,8 @@ %ul{ class: (container_class) } - content_for :page_specific_javascripts do - = page_specific_javascript_tag('lib/chart.js') - = page_specific_javascript_tag('graphs/graphs_bundle.js') + = page_specific_javascript_bundle_tag('lib_chart') + = page_specific_javascript_bundle_tag('graphs') = nav_link(action: :show) do = link_to 'Contributors', namespace_project_graph_path = nav_link(action: :commits) do diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 1f63803c24e..97618984bb4 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -3,7 +3,7 @@ - page_description @merge_request.description - page_card_attributes @merge_request.card_attributes - content_for :page_specific_javascripts do - = page_specific_javascript_tag('diff_notes/diff_notes_bundle.js') + = page_specific_javascript_bundle_tag('diff_notes') .merge-request{ 'data-url' => merge_request_path(@merge_request) } = render "projects/merge_requests/show/mr_title" diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml index b8b87dcdcaf..9242a84f150 100644 --- a/app/views/projects/merge_requests/conflicts.html.haml +++ b/app/views/projects/merge_requests/conflicts.html.haml @@ -1,6 +1,6 @@ - page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests" - content_for :page_specific_javascripts do - = page_specific_javascript_tag('merge_conflicts/merge_conflicts_bundle.js') + = page_specific_javascript_bundle_tag('merge_conflicts') = page_specific_javascript_tag('lib/ace.js') = render "projects/merge_requests/show/mr_title" diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 7809e9c8c72..487943f1167 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -1,5 +1,5 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + = page_specific_javascript_bundle_tag('merge_request_widget') - status_class = @pipeline ? " ci-#{@pipeline.status}" : nil diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml index 50086767446..909dc52fc06 100644 --- a/app/views/projects/merge_requests/widget/open/_check.html.haml +++ b/app/views/projects/merge_requests/widget/open/_check.html.haml @@ -1,5 +1,5 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + = page_specific_javascript_bundle_tag('merge_request_widget') %strong = icon("spinner spin") diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index d8951e69242..b88eef65cef 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 = page_specific_javascript_tag('lib/raphael.js') - = page_specific_javascript_tag('network/network_bundle.js') + = page_specific_javascript_bundle_tag('network') = render "projects/commits/head" = render "head" %div{ class: container_class } diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml index 42e9bdbd30e..b3b419bd92d 100644 --- a/app/views/projects/protected_branches/index.html.haml +++ b/app/views/projects/protected_branches/index.html.haml @@ -1,6 +1,6 @@ - page_title "Protected branches" - content_for :page_specific_javascripts do - = page_specific_javascript_tag('protected_branches/protected_branches_bundle.js') + = page_specific_javascript_bundle_tag('protected_branches') .row.prepend-top-default.append-bottom-default .col-lg-3 diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 0c788032020..b5362a7d5d5 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -1,6 +1,6 @@ - content_for :page_specific_javascripts do = page_specific_javascript_tag('lib/ace.js') - = page_specific_javascript_tag('snippet/snippet_bundle.js') + = page_specific_javascript_bundle_tag('snippet') .snippet-form-holder = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f| @@ -34,4 +34,3 @@ = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" - else = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" - diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index fb25eed4f37..6f43d083e4a 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 - = page_specific_javascript_tag('lib/d3.js') - = page_specific_javascript_tag('users/users_bundle.js') + = page_specific_javascript_bundle_tag('lib_d3') + = page_specific_javascript_bundle_tag('users') - header_title @user.name, user_path(@user) - @no_container = true diff --git a/config/application.rb b/config/application.rb index 02839dba1ed..7607c7a61b2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -96,23 +96,9 @@ module Gitlab config.assets.precompile << "katex.css" config.assets.precompile << "katex.js" config.assets.precompile << "xterm/xterm.css" - config.assets.precompile << "graphs/graphs_bundle.js" - config.assets.precompile << "users/users_bundle.js" - config.assets.precompile << "network/network_bundle.js" - config.assets.precompile << "profile/profile_bundle.js" - config.assets.precompile << "protected_branches/protected_branches_bundle.js" - config.assets.precompile << "diff_notes/diff_notes_bundle.js" - config.assets.precompile << "merge_request_widget/ci_bundle.js" - config.assets.precompile << "boards/boards_bundle.js" - config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js" - config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" - config.assets.precompile << "boards/test_utils/simulate_drag.js" - config.assets.precompile << "environments/environments_bundle.js" - config.assets.precompile << "blob_edit/blob_edit_bundle.js" - config.assets.precompile << "snippet/snippet_bundle.js" - config.assets.precompile << "terminal/terminal_bundle.js" - config.assets.precompile << "lib/utils/*.js" - config.assets.precompile << "lib/*.js" + config.assets.precompile << "lib/ace.js" + config.assets.precompile << "lib/cropper.js" + config.assets.precompile << "lib/raphael.js" config.assets.precompile << "u2f.js" config.assets.precompile << "vendor/assets/fonts/*" diff --git a/config/webpack.config.js b/config/webpack.config.js index d45638fbcbd..2fcf2e11450 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -12,9 +12,26 @@ var ROOT_PATH = path.resolve(__dirname, '..'); var DEV_SERVER_PORT = 3808; var config = { - context: ROOT_PATH, + context: path.join(ROOT_PATH, 'app/assets/javascripts'), entry: { - application: './app/assets/javascripts/webpack/application.js' + application: './application.js', + blob_edit: './blob_edit/blob_edit_bundle.js', + boards: './boards/boards_bundle.js', + boards_test: './boards/test_utils/simulate_drag.js', + cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', + diff_notes: './diff_notes/diff_notes_bundle.js', + environments: './environments/environments_bundle.js', + graphs: './graphs/graphs_bundle.js', + merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js', + merge_request_widget: './merge_request_widget/ci_bundle.js', + network: './network/network_bundle.js', + profile: './profile/profile_bundle.js', + protected_branches: './protected_branches/protected_branches_bundle.js', + snippet: './snippet/snippet_bundle.js', + terminal: './terminal/terminal_bundle.js', + users: './users/users_bundle.js', + lib_chart: './lib/chart.js', + lib_d3: './lib/d3.js' }, output: { diff --git a/package.json b/package.json index 7ef811ae478..966c3daab40 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "babel-core": "^5.8.38", "babel-loader": "^5.4.2", "bootstrap-sass": "3.3.6", + "d3": "3.5.11", "dropzone": "4.2.0", "exports-loader": "^0.6.3", "imports-loader": "^0.6.5", diff --git a/vendor/assets/javascripts/es6-promise.auto.js b/vendor/assets/javascripts/es6-promise.auto.js index 19e6c13a655..b8887115a37 100644 --- a/vendor/assets/javascripts/es6-promise.auto.js +++ b/vendor/assets/javascripts/es6-promise.auto.js @@ -1154,6 +1154,3 @@ Promise.Promise = Promise; return Promise; }))); - -ES6Promise.polyfill(); -//# sourceMappingURL=es6-promise.auto.map diff --git a/vendor/assets/javascripts/xterm/fit.js b/vendor/assets/javascripts/xterm/fit.js index 7e24fd9b36e..55438452cad 100644 --- a/vendor/assets/javascripts/xterm/fit.js +++ b/vendor/assets/javascripts/xterm/fit.js @@ -16,12 +16,12 @@ /* * CommonJS environment */ - module.exports = fit(require('../../xterm')); + module.exports = fit(require('./xterm')); } else if (typeof define == 'function') { /* * Require.js is available */ - define(['../../xterm'], fit); + define(['./xterm'], fit); } else { /* * Plain browser environment -- cgit v1.2.1 From 24b48a37131546f5927805e3be7d88e07dbfa9af Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 4 Nov 2016 18:12:00 -0500 Subject: disable "use strict" in babel config as it was broken in sprockets --- config/webpack.config.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 2fcf2e11450..da2a19838e4 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -47,7 +47,13 @@ var config = { { test: /\.es6$/, exclude: /node_modules/, - loader: 'babel-loader' + loader: 'babel-loader', + query: { + // "use strict" was broken in sprockets-es6 due to sprockets concatination method. + // many es5 strict errors which were never caught ended up in our es6 assets as a result. + // this hack is necessary until they can be fixed. + blacklist: ["useStrict"] + } }, { test: /\.(js|es6)$/, -- cgit v1.2.1 From 720650d2b0a978700edfd759c0928598f2b0ae0b Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 3 Nov 2016 19:11:38 -0500 Subject: precompile webpack assets when testing --- .gitlab-ci.yml | 5 +++++ config/application.rb | 1 + config/environments/development.rb | 3 +++ 3 files changed, 9 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e0e780e1e6b..2ec9dfd2165 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,6 +24,9 @@ before_script: - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) $FLAGS' - retry gem install knapsack - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql' + - curl --silent --location https://deb.nodesource.com/setup_6.x | bash - + - apt-get install --assume-yes nodejs + - npm install stages: - prepare @@ -61,6 +64,7 @@ stages: <<: *dedicated-runner <<: *use-db script: + - bundle exec rake webpack:compile - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} @@ -79,6 +83,7 @@ stages: <<: *dedicated-runner <<: *use-db script: + - bundle exec rake webpack:compile - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} diff --git a/config/application.rb b/config/application.rb index 7607c7a61b2..4efe73c7798 100644 --- a/config/application.rb +++ b/config/application.rb @@ -84,6 +84,7 @@ module Gitlab config.webpack.config_file = "config/webpack.config.js" config.webpack.output_dir = "public/assets/webpack" config.webpack.public_path = "assets/webpack" + config.webpack.dev_server.enabled = false # Enable the asset pipeline config.assets.enabled = true diff --git a/config/environments/development.rb b/config/environments/development.rb index 45a8c1add3e..168c434f261 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -22,6 +22,9 @@ Rails.application.configure do # Only use best-standards-support built into browsers config.action_dispatch.best_standards_support = :builtin + # Enable webpack dev server + config.webpack.dev_server.enabled = true + # Do not compress assets config.assets.compress = false -- cgit v1.2.1 From a2c1f2e0ea5ce1030ecfe4fa821b9268773baac5 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 21 Nov 2016 10:56:25 -0600 Subject: exempt node_modules from rubocop linting --- .rubocop.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.rubocop.yml b/.rubocop.yml index 80eb4a5c19e..3735a718c37 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,6 +17,7 @@ AllCops: # Exclude some GitLab files Exclude: - 'vendor/**/*' + - 'node_modules/**/*' - 'db/*' - 'db/fixtures/**/*' - 'tmp/**/*' -- cgit v1.2.1 From 11e9f32d1be94d9216416c730c0b72effed1aa73 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 21 Nov 2016 15:40:58 -0600 Subject: fix rubocop complaint about ambiguous splat operator --- app/helpers/javascript_helper.rb | 2 +- app/views/layouts/_head.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb index d9c8c70076b..64284910d4d 100644 --- a/app/helpers/javascript_helper.rb +++ b/app/helpers/javascript_helper.rb @@ -3,6 +3,6 @@ module JavascriptHelper javascript_include_tag asset_path(js), { "data-turbolinks-track" => true } end def page_specific_javascript_bundle_tag(js) - javascript_include_tag *webpack_asset_paths(js), { "data-turbolinks-track" => true } + javascript_include_tag(*webpack_asset_paths(js), { "data-turbolinks-track" => true }) end end diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index d260e2133f2..0d9f838e804 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -28,7 +28,7 @@ = stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "print", media: "print" - = javascript_include_tag *webpack_asset_paths("application") + = javascript_include_tag(*webpack_asset_paths("application")) - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts -- cgit v1.2.1 From a569aa295a9e733c1aa975d69a95a71a677a43bb Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 21 Nov 2016 16:32:03 -0600 Subject: use karma for javascript testing --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 966c3daab40..97a98ad7cfc 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "eslint-plugin-filenames": "^1.1.0", "eslint-plugin-import": "^2.2.0", "eslint-plugin-jasmine": "^2.1.0", - "istanbul": "^0.4.5" + "istanbul": "^0.4.5", + "karma": "^1.3.0" } } -- cgit v1.2.1 From 3eb8569778ce2559eedab706f6f901238e39b355 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Sat, 10 Dec 2016 16:06:41 +0000 Subject: Correct merge conflicts Fixed eslint failures --- app/assets/javascripts/application.js | 1 - .../javascripts/lib/utils/datetime_utility.js | 4 +- vendor/assets/javascripts/date.format.js | 207 +++++++++++---------- 3 files changed, 109 insertions(+), 103 deletions(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 830d706f02d..6a6f827e580 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -47,7 +47,6 @@ require('./shortcuts_dashboard_navigation'); require('./shortcuts_issuable'); require('./shortcuts_network'); require('vendor/jquery.nicescroll'); -require('vendor/date.format'); requireAll(require.context('./behaviors', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./blob', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./templates', false, /^\.\/.*\.(js|es6)$/)); diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index b6c1fd4c4f8..f859fc9c0da 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -3,7 +3,7 @@ /* global dateFormat */ window.timeago = require('vendor/timeago'); -require('vendor/date.format'); +window.dateFormat = require('vendor/date.format'); (function() { (function(w) { @@ -17,7 +17,7 @@ require('vendor/date.format'); w.gl.utils.days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; w.gl.utils.formatDate = function(datetime) { - return (new Date(datetime)).format('mmm d, yyyy h:MMtt Z'); + return dateFormat(datetime, 'mmm d, yyyy h:MMtt Z'); }; w.gl.utils.getDayName = function(date) { diff --git a/vendor/assets/javascripts/date.format.js b/vendor/assets/javascripts/date.format.js index f5dc4abcd80..2c9b4825443 100644 --- a/vendor/assets/javascripts/date.format.js +++ b/vendor/assets/javascripts/date.format.js @@ -11,115 +11,122 @@ * The date defaults to the current date/time. * The mask defaults to dateFormat.masks.default. */ + (function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.dateFormat = factory()); + }(this, (function () { 'use strict'; + var dateFormat = function () { + var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, + timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, + timezoneClip = /[^-+\dA-Z]/g, + pad = function (val, len) { + val = String(val); + len = len || 2; + while (val.length < len) val = "0" + val; + return val; + }; -var dateFormat = function () { - var token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g, - timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g, - timezoneClip = /[^-+\dA-Z]/g, - pad = function (val, len) { - val = String(val); - len = len || 2; - while (val.length < len) val = "0" + val; - return val; - }; + // Regexes and supporting functions are cached through closure + return function (date, mask, utc) { + var dF = dateFormat; - // Regexes and supporting functions are cached through closure - return function (date, mask, utc) { - var dF = dateFormat; + // You can't provide utc if you skip other args (use the "UTC:" mask prefix) + if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { + mask = date; + date = undefined; + } - // You can't provide utc if you skip other args (use the "UTC:" mask prefix) - if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) { - mask = date; - date = undefined; - } + // Passing date through Date applies Date.parse, if necessary + date = date ? new Date(date) : new Date; + if (isNaN(date)) throw SyntaxError("invalid date"); - // Passing date through Date applies Date.parse, if necessary - date = date ? new Date(date) : new Date; - if (isNaN(date)) throw SyntaxError("invalid date"); + mask = String(dF.masks[mask] || mask || dF.masks["default"]); - mask = String(dF.masks[mask] || mask || dF.masks["default"]); + // Allow setting the utc argument via the mask + if (mask.slice(0, 4) == "UTC:") { + mask = mask.slice(4); + utc = true; + } - // Allow setting the utc argument via the mask - if (mask.slice(0, 4) == "UTC:") { - mask = mask.slice(4); - utc = true; - } + var _ = utc ? "getUTC" : "get", + d = date[_ + "Date"](), + D = date[_ + "Day"](), + m = date[_ + "Month"](), + y = date[_ + "FullYear"](), + H = date[_ + "Hours"](), + M = date[_ + "Minutes"](), + s = date[_ + "Seconds"](), + L = date[_ + "Milliseconds"](), + o = utc ? 0 : date.getTimezoneOffset(), + flags = { + d: d, + dd: pad(d), + ddd: dF.i18n.dayNames[D], + dddd: dF.i18n.dayNames[D + 7], + m: m + 1, + mm: pad(m + 1), + mmm: dF.i18n.monthNames[m], + mmmm: dF.i18n.monthNames[m + 12], + yy: String(y).slice(2), + yyyy: y, + h: H % 12 || 12, + hh: pad(H % 12 || 12), + H: H, + HH: pad(H), + M: M, + MM: pad(M), + s: s, + ss: pad(s), + l: pad(L, 3), + L: pad(L > 99 ? Math.round(L / 10) : L), + t: H < 12 ? "a" : "p", + tt: H < 12 ? "am" : "pm", + T: H < 12 ? "A" : "P", + TT: H < 12 ? "AM" : "PM", + Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), + o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), + S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] + }; - var _ = utc ? "getUTC" : "get", - d = date[_ + "Date"](), - D = date[_ + "Day"](), - m = date[_ + "Month"](), - y = date[_ + "FullYear"](), - H = date[_ + "Hours"](), - M = date[_ + "Minutes"](), - s = date[_ + "Seconds"](), - L = date[_ + "Milliseconds"](), - o = utc ? 0 : date.getTimezoneOffset(), - flags = { - d: d, - dd: pad(d), - ddd: dF.i18n.dayNames[D], - dddd: dF.i18n.dayNames[D + 7], - m: m + 1, - mm: pad(m + 1), - mmm: dF.i18n.monthNames[m], - mmmm: dF.i18n.monthNames[m + 12], - yy: String(y).slice(2), - yyyy: y, - h: H % 12 || 12, - hh: pad(H % 12 || 12), - H: H, - HH: pad(H), - M: M, - MM: pad(M), - s: s, - ss: pad(s), - l: pad(L, 3), - L: pad(L > 99 ? Math.round(L / 10) : L), - t: H < 12 ? "a" : "p", - tt: H < 12 ? "am" : "pm", - T: H < 12 ? "A" : "P", - TT: H < 12 ? "AM" : "PM", - Z: utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""), - o: (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4), - S: ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10] - }; + return mask.replace(token, function ($0) { + return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); + }); + }; + }(); - return mask.replace(token, function ($0) { - return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1); - }); + // Some common format strings + dateFormat.masks = { + "default": "ddd mmm dd yyyy HH:MM:ss", + shortDate: "m/d/yy", + mediumDate: "mmm d, yyyy", + longDate: "mmmm d, yyyy", + fullDate: "dddd, mmmm d, yyyy", + shortTime: "h:MM TT", + mediumTime: "h:MM:ss TT", + longTime: "h:MM:ss TT Z", + isoDate: "yyyy-mm-dd", + isoTime: "HH:MM:ss", + isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", + isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" }; -}(); -// Some common format strings -dateFormat.masks = { - "default": "ddd mmm dd yyyy HH:MM:ss", - shortDate: "m/d/yy", - mediumDate: "mmm d, yyyy", - longDate: "mmmm d, yyyy", - fullDate: "dddd, mmmm d, yyyy", - shortTime: "h:MM TT", - mediumTime: "h:MM:ss TT", - longTime: "h:MM:ss TT Z", - isoDate: "yyyy-mm-dd", - isoTime: "HH:MM:ss", - isoDateTime: "yyyy-mm-dd'T'HH:MM:ss", - isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'" -}; + // Internationalization strings + dateFormat.i18n = { + dayNames: [ + "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", + "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" + ], + monthNames: [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", + "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" + ] + }; -// Internationalization strings -dateFormat.i18n = { - dayNames: [ - "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", - "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday" - ], - monthNames: [ - "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", - "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" - ] -}; + // For convenience... + Date.prototype.format = function (mask, utc) { + return dateFormat(this, mask, utc); + }; -// For convenience... -Date.prototype.format = function (mask, utc) { - return dateFormat(this, mask, utc); -}; + return dateFormat; +}))); -- cgit v1.2.1 From 7c47cc94c5d7425583db3610c85cb150df601a91 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Thu, 29 Dec 2016 15:42:48 -0600 Subject: Swapped out teaspoon for karma --- .gitlab-ci.yml | 12 ++---- config/karma.config.js | 27 +++++++++++++ config/webpack.config.js | 7 +++- lib/tasks/gitlab/test.rake | 2 +- lib/tasks/test.rake | 2 +- package.json | 10 ++++- spec/javascripts/abuse_reports_spec.js.es6 | 5 ++- spec/javascripts/activities_spec.js.es6 | 9 +++-- spec/javascripts/awards_handler_spec.js | 8 ++-- spec/javascripts/behaviors/autosize_spec.js | 2 +- spec/javascripts/behaviors/quick_submit_spec.js | 3 +- spec/javascripts/behaviors/requires_input_spec.js | 3 +- spec/javascripts/boards/boards_store_spec.js.es6 | 25 ++++++------ spec/javascripts/boards/issue_spec.js.es6 | 25 ++++++------ spec/javascripts/boards/list_spec.js.es6 | 25 ++++++------ spec/javascripts/bootstrap_linked_tabs_spec.js.es6 | 3 +- spec/javascripts/build_spec.js.es6 | 11 +++--- spec/javascripts/dashboard_spec.js.es6 | 8 ++-- spec/javascripts/datetime_utility_spec.js.es6 | 2 +- spec/javascripts/diff_comments_store_spec.js.es6 | 7 ++-- .../environments/environment_actions_spec.js.es6 | 5 ++- .../environment_external_url_spec.js.es6 | 5 ++- .../environments/environment_item_spec.js.es6 | 7 ++-- .../environments/environment_rollback_spec.js.es6 | 6 ++- .../environments/environment_stop_spec.js.es6 | 6 ++- .../environments/environments_store_spec.js.es6 | 6 +-- spec/javascripts/environments/mock_data.js.es6 | 1 + spec/javascripts/extensions/array_spec.js.es6 | 2 +- spec/javascripts/extensions/element_spec.js.es6 | 2 +- spec/javascripts/extensions/jquery_spec.js | 2 +- spec/javascripts/extensions/object_spec.js.es6 | 2 +- spec/javascripts/gl_dropdown_spec.js.es6 | 10 ++--- spec/javascripts/gl_field_errors_spec.js.es6 | 4 +- .../graphs/stat_graph_contributors_graph_spec.js | 2 +- .../graphs/stat_graph_contributors_util_spec.js | 2 +- spec/javascripts/graphs/stat_graph_spec.js | 2 +- spec/javascripts/header_spec.js | 6 +-- spec/javascripts/issuable_spec.js.es6 | 4 +- spec/javascripts/issue_spec.js | 5 ++- spec/javascripts/labels_issue_sidebar_spec.js.es6 | 21 +++++----- .../javascripts/lib/utils/common_utils_spec.js.es6 | 2 +- spec/javascripts/line_highlighter_spec.js | 3 +- spec/javascripts/merge_request_spec.js | 3 +- spec/javascripts/merge_request_tabs_spec.js | 9 +++-- spec/javascripts/merge_request_widget_spec.js | 4 +- .../mini_pipeline_graph_dropdown_spec.js.es6 | 4 +- spec/javascripts/new_branch_spec.js | 5 ++- spec/javascripts/notes_spec.js | 9 +++-- spec/javascripts/pipelines_spec.js.es6 | 2 +- spec/javascripts/pretty_time_spec.js.es6 | 2 +- spec/javascripts/project_title_spec.js | 16 ++++---- spec/javascripts/right_sidebar_spec.js | 9 ++--- spec/javascripts/search_autocomplete_spec.js | 17 ++++---- spec/javascripts/shortcuts_issuable_spec.js | 3 +- spec/javascripts/signin_tabs_memoizer_spec.js.es6 | 3 +- spec/javascripts/smart_interval_spec.js.es6 | 4 +- spec/javascripts/spec_helper.js | 46 ++-------------------- spec/javascripts/subbable_resource_spec.js.es6 | 7 ++-- spec/javascripts/syntax_highlight_spec.js | 3 +- spec/javascripts/u2f/authenticate_spec.js | 11 +++--- spec/javascripts/u2f/register_spec.js | 11 +++--- .../vue_common_components/commit_spec.js.es6 | 3 +- spec/javascripts/zen_mode_spec.js | 3 +- vendor/assets/javascripts/jquery.turbolinks.js | 21 ++++++++-- 64 files changed, 260 insertions(+), 236 deletions(-) create mode 100644 config/karma.config.js diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 2ec9dfd2165..6e727333929 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -291,7 +291,7 @@ rake db:seed_fu: paths: - log/development.log -teaspoon: +karma: cache: paths: - vendor/ruby @@ -300,9 +300,9 @@ teaspoon: <<: *use-db <<: *dedicated-runner script: - - npm install - npm link istanbul - - rake teaspoon + - rake webpack:compile + - npm run karma-start artifacts: name: coverage-javascript expire_in: 31d @@ -381,8 +381,6 @@ lint:javascript: - node_modules/ stage: test image: "node:7.1" - before_script: - - npm install script: - npm --silent run eslint @@ -393,8 +391,6 @@ lint:javascript:report: - node_modules/ stage: post-test image: "node:7.1" - before_script: - - npm install script: - find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files - npm --silent run eslint-report || true # ignore exit code @@ -444,7 +440,7 @@ pages: <<: *dedicated-runner dependencies: - coverage - - teaspoon + - karma - lint:javascript:report script: - mv public/ .public/ diff --git a/config/karma.config.js b/config/karma.config.js new file mode 100644 index 00000000000..478ef082547 --- /dev/null +++ b/config/karma.config.js @@ -0,0 +1,27 @@ +var path = require('path'); +var webpackConfig = require('./webpack.config.js'); +var ROOT_PATH = path.resolve(__dirname, '..'); + +// Karma configuration +module.exports = function(config) { + config.set({ + basePath: ROOT_PATH, + frameworks: ['jquery-2.1.0', 'jasmine'], + files: [ + 'spec/javascripts/*_spec.js', + 'spec/javascripts/*_spec.js.es6', + { pattern: 'spec/javascripts/fixtures/**/*.html', included: false, served: true }, + { pattern: 'spec/javascripts/fixtures/**/*.json', included: false, served: true }, + ], + preprocessors: { + 'spec/javascripts/*_spec.js': ['webpack'], + 'spec/javascripts/*_spec.js.es6': ['webpack'], + 'app/assets/javascripts/**/*.js': ['webpack'], + 'app/assets/javascripts/**/*.js.es6': ['webpack'], + }, + + webpack: webpackConfig, + + webpackMiddleware: { stats: 'errors-only' }, + }); +}; diff --git a/config/webpack.config.js b/config/webpack.config.js index da2a19838e4..5cba995888a 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -58,7 +58,7 @@ var config = { { test: /\.(js|es6)$/, loader: 'imports-loader', - query: 'this=>window' + query: '$=jquery,jQuery=jquery,this=>window' }, { test: /\.json$/, @@ -87,7 +87,10 @@ var config = { 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vue$': 'vue/dist/vue.js', 'vue-resource$': 'vue-resource/dist/vue-resource.js' - } + }, + root: [ + path.join(ROOT_PATH, 'app/assets/javascripts'), + ], } } diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index 4d4e746503a..ec7aec1621c 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -6,7 +6,7 @@ namespace :gitlab do %W(rake rubocop), %W(rake spinach), %W(rake spec), - %W(rake teaspoon) + %W(npm run karma-start) ] cmds.each do |cmd| diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake index d3dcbd2c29b..83f53e5454b 100644 --- a/lib/tasks/test.rake +++ b/lib/tasks/test.rake @@ -7,5 +7,5 @@ end unless Rails.env.production? desc "GitLab | Run all tests on CI with simplecov" - task test_ci: [:rubocop, :brakeman, :teaspoon, :spinach, :spec] + task test_ci: [:rubocop, :brakeman, :'karma-start', :spinach, :spec] end diff --git a/package.json b/package.json index 97a98ad7cfc..ec012e9c3bd 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "dev-server": "node_modules/.bin/webpack-dev-server --config config/webpack.config.js", "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .", "eslint-fix": "npm run eslint -- --fix", - "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html" + "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html", + "karma-start": "karma start config/karma.config.js" }, "dependencies": { "babel": "^5.8.38", @@ -35,6 +36,11 @@ "eslint-plugin-import": "^2.2.0", "eslint-plugin-jasmine": "^2.1.0", "istanbul": "^0.4.5", - "karma": "^1.3.0" + "jasmine-core": "^2.5.2", + "jasmine-jquery": "^2.1.1", + "karma": "^1.3.0", + "karma-jasmine": "^1.1.0", + "karma-jquery": "^0.1.0", + "karma-webpack": "^1.8.0" } } diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index cf19aa05031..dadee40f9b9 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -1,5 +1,6 @@ -/*= require lib/utils/text_utility */ -/*= require abuse_reports */ +require('./spec_helper'); +require('lib/utils/text_utility'); +require('abuse_reports'); ((global) => { describe('Abuse Reports', () => { diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index b3617a45bd4..61fabd37170 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,9 +1,10 @@ /* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */ -/*= require js.cookie.js */ -/*= require jquery.endless-scroll.js */ -/*= require pager */ -/*= require activities */ +require('./spec_helper'); +require('vendor/js.cookie.js'); +require('vendor/jquery.endless-scroll.js'); +require('pager'); +require('activities'); (() => { window.gon || (window.gon = {}); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index faba2837d41..88757e5c236 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,10 +1,10 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */ /* global AwardsHandler */ -/*= require awards_handler */ -/*= require jquery */ -/*= require js.cookie */ -/*= require ./fixtures/emoji_menu */ +require('./spec_helper'); +require('awards_handler'); +require('vendor/js.cookie'); +require('./fixtures/emoji_menu'); (function() { var awardsHandler, lazyAssert, urlRoot; diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index e77d732a32a..e05793cf2e3 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, padded-blocks, max-len */ -/*= require behaviors/autosize */ +require('behaviors/autosize'); (function() { describe('Autosize behavior', function() { diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 1a1f34cfdc0..4a00b1b2d38 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,6 +1,7 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ -/*= require behaviors/quick_submit */ +require('./spec_helper'); +require('behaviors/quick_submit'); (function() { describe('Quick Submit behavior', function() { diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 1f62591c06d..44d91d41abf 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -1,6 +1,7 @@ /* eslint-disable space-before-function-paren, no-var, padded-blocks */ -/*= require behaviors/requires_input */ +require('./spec_helper'); +require('behaviors/requires_input'); (function() { describe('requiresInput', function() { diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index b3a1afa28a5..d995380c620 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -6,19 +6,18 @@ /* global listObj */ /* global listObjDuplicate */ -//= require jquery -//= require jquery_ujs -//= require js.cookie -//= require vue -//= require vue-resource -//= require lib/utils/url_utility -//= require boards/models/issue -//= require boards/models/label -//= require boards/models/list -//= require boards/models/user -//= require boards/services/board_service -//= require boards/stores/boards_store -//= require ./mock_data +require('jquery_ujs'); +require('js.cookie'); +require('vue'); +require('vue-resource'); +require('lib/utils/url_utility'); +require('boards/models/issue'); +require('boards/models/label'); +require('boards/models/list'); +require('boards/models/user'); +require('boards/services/board_service'); +require('boards/stores/boards_store'); +require('./mock_data'); describe('Store', () => { beforeEach(() => { diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index c8a61a0a9b5..2cdbdd725b1 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -2,19 +2,18 @@ /* global BoardService */ /* global ListIssue */ -//= require jquery -//= require jquery_ujs -//= require js.cookie -//= require vue -//= require vue-resource -//= require lib/utils/url_utility -//= require boards/models/issue -//= require boards/models/label -//= require boards/models/list -//= require boards/models/user -//= require boards/services/board_service -//= require boards/stores/boards_store -//= require ./mock_data +require('jquery_ujs'); +require('js.cookie'); +require('vue'); +require('vue-resource'); +require('lib/utils/url_utility'); +require('boards/models/issue'); +require('boards/models/label'); +require('boards/models/list'); +require('boards/models/user'); +require('boards/services/board_service'); +require('boards/stores/boards_store'); +require('./mock_data'); describe('Issue model', () => { let issue; diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 7d942ec3d65..dd82f482207 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -5,19 +5,18 @@ /* global List */ /* global listObj */ -//= require jquery -//= require jquery_ujs -//= require js.cookie -//= require vue -//= require vue-resource -//= require lib/utils/url_utility -//= require boards/models/issue -//= require boards/models/label -//= require boards/models/list -//= require boards/models/user -//= require boards/services/board_service -//= require boards/stores/boards_store -//= require ./mock_data +require('jquery_ujs'); +require('js.cookie'); +require('vue'); +require('vue-resource'); +require('lib/utils/url_utility'); +require('boards/models/issue'); +require('boards/models/label'); +require('boards/models/list'); +require('boards/models/user'); +require('boards/services/board_service'); +require('boards/stores/boards_store'); +require('./mock_data'); describe('List model', () => { let list; diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 index ea953d0f5a5..b6d223dcb80 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 @@ -1,4 +1,5 @@ -//= require lib/utils/bootstrap_linked_tabs +require('./spec_helper'); +require('lib/utils/bootstrap_linked_tabs'); (() => { describe('Linked Tabs', () => { diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 0c556382980..8b0c797647b 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -2,11 +2,12 @@ /* global Build */ /* global Turbolinks */ -//= require lib/utils/datetime_utility -//= require build -//= require breakpoints -//= require jquery.nicescroll -//= require turbolinks +require('./spec_helper'); +require('lib/utils/datetime_utility'); +require('build'); +require('breakpoints'); +require('vendor/jquery.nicescroll'); +require('vendor/turbolinks'); describe('Build', () => { const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index 3f6b328348d..4223215c096 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -1,9 +1,9 @@ /* eslint-disable no-new, padded-blocks */ -/*= require sidebar */ -/*= require jquery */ -/*= require js.cookie */ -/*= require lib/utils/text_utility */ +require('./spec_helper'); +require('sidebar'); +require('vendor/js.cookie'); +require('lib/utils/text_utility'); ((global) => { describe('Dashboard', () => { diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 index 8ece24555c5..713e7742988 100644 --- a/spec/javascripts/datetime_utility_spec.js.es6 +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -1,4 +1,4 @@ -//= require lib/utils/datetime_utility +require('lib/utils/datetime_utility'); (() => { describe('Date time utils', () => { diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index 18805d26ac0..f27ba0f93f7 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -1,10 +1,9 @@ /* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ /* global CommentsStore */ -//= require vue -//= require diff_notes/models/discussion -//= require diff_notes/models/note -//= require diff_notes/stores/comments +require('diff_notes/models/discussion'); +require('diff_notes/models/note'); +require('diff_notes/stores/comments'); (() => { function createDiscussion(noteId = 1, resolved = true) { diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index 056e4d41e93..34330f7bfd6 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -1,5 +1,6 @@ -//= require vue -//= require environments/components/environment_actions +require('./spec_helper'); +require('vue'); +require('environments/components/environment_actions'); describe('Actions Component', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index 950a5d53fad..71785e7ecf1 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -1,5 +1,6 @@ -//= require vue -//= require environments/components/environment_external_url +require('./spec_helper'); +require('vue'); +require('environments/components/environment_external_url'); describe('External URL Component', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index c178b9cc1ec..be753498269 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,6 +1,7 @@ -//= require vue -//= require timeago -//= require environments/components/environment_item +require('./spec_helper'); +require('vue'); +require('timeago'); +require('environments/components/environment_item'); describe('Environment item', () => { preloadFixtures('static/environments/table.html.raw'); diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 21241116e29..72f44014258 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,5 +1,7 @@ -//= require vue -//= require environments/components/environment_rollback +require('./spec_helper'); +require('vue'); +require('environments/components/environment_rollback'); + describe('Rollback Component', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index bb998a32f32..37ae970859c 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,5 +1,7 @@ -//= require vue -//= require environments/components/environment_stop +require('./spec_helper'); +require('vue'); +require('environments/components/environment_stop'); + describe('Stop Component', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 17c00acf63e..eefe87be5aa 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,8 +1,8 @@ /* global environmentsList */ -//= require vue -//= require environments/stores/environments_store -//= require ./mock_data +require('vue'); +require('environments/stores/environments_store'); +require('./mock_data'); (() => { describe('Store', () => { diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 9e16bc3e6a5..bc5f6246cba 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -1,4 +1,5 @@ /* eslint-disable no-unused-vars */ + const environmentsList = [ { id: 31, diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6 index 2ec759c8e80..5396e0eb639 100644 --- a/spec/javascripts/extensions/array_spec.js.es6 +++ b/spec/javascripts/extensions/array_spec.js.es6 @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, padded-blocks */ -/*= require extensions/array */ +require('extensions/array'); (function() { describe('Array extensions', function() { diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6 index c5b86d35204..49544ae8b5c 100644 --- a/spec/javascripts/extensions/element_spec.js.es6 +++ b/spec/javascripts/extensions/element_spec.js.es6 @@ -1,4 +1,4 @@ -/*= require extensions/element */ +require('extensions/element'); (() => { describe('Element extensions', function () { diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index 91846bb9143..3163414b134 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, padded-blocks */ -/*= require extensions/jquery */ +require('extensions/jquery'); (function() { describe('jQuery extensions', function() { diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6 index 3b71c255b30..77ffa1a35ae 100644 --- a/spec/javascripts/extensions/object_spec.js.es6 +++ b/spec/javascripts/extensions/object_spec.js.es6 @@ -1,4 +1,4 @@ -/*= require extensions/object */ +require('extensions/object'); describe('Object extensions', () => { describe('assign', () => { diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index ce96571bd52..fac5b20ea7e 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -1,11 +1,11 @@ /* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */ /* global Turbolinks */ -/*= require jquery */ -/*= require gl_dropdown */ -/*= require turbolinks */ -/*= require lib/utils/common_utils */ -/*= require lib/utils/type_utility */ +require('./spec_helper'); +require('gl_dropdown'); +require('vendor/turbolinks'); +require('lib/utils/common_utils'); +require('lib/utils/type_utility'); (() => { const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index e5d934540af..763e1bb5685 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */ -//= require jquery -//= require gl_field_errors +require('./spec_helper'); +require('gl_field_errors'); ((global) => { preloadFixtures('static/gl_field_errors.html.raw'); diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index bc5cbeb6a40..a914eda90bb 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -3,7 +3,7 @@ /* global ContributorsGraph */ /* global ContributorsMasterGraph */ -//= require graphs/stat_graph_contributors_graph +require('graphs/stat_graph_contributors_graph'); describe("ContributorsGraph", function () { describe("#set_x_domain", function () { diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 751f3d175e2..4f82e1c46db 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,7 +1,7 @@ /* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */ /* global ContributorsStatGraphUtil */ -//= require graphs/stat_graph_contributors_util +require('graphs/stat_graph_contributors_util'); describe("ContributorsStatGraphUtil", function () { diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index 0da124632ae..a017f35831d 100644 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,7 +1,7 @@ /* eslint-disable quotes, padded-blocks, semi */ /* global StatGraph */ -//= require graphs/stat_graph +require('graphs/stat_graph'); describe("StatGraph", function () { diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index b5262afa1cf..cb12fa327d4 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, padded-blocks, no-var */ -/*= require header */ -/*= require lib/utils/text_utility */ -/*= require jquery */ +require('./spec_helper'); +require('header'); +require('lib/utils/text_utility'); (function() { diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 index 917a6267b92..2daadfa0c77 100644 --- a/spec/javascripts/issuable_spec.js.es6 +++ b/spec/javascripts/issuable_spec.js.es6 @@ -1,8 +1,8 @@ /* global Issuable */ /* global Turbolinks */ -//= require issuable -//= require turbolinks +require('issuable'); +require('turbolinks'); (() => { const BASE_URL = '/user/project/issues?scope=all&state=closed'; diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index eb07421826c..126b3682d5d 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,8 +1,9 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */ /* global Issue */ -/*= require lib/utils/text_utility */ -/*= require issue */ +require('./spec_helper'); +require('lib/utils/text_utility'); +require('issue'); (function() { var INVALID_URL = 'http://goesnowhere.nothing/whereami'; diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index e3146559a4a..885e975c32b 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -2,17 +2,16 @@ /* global IssuableContext */ /* global LabelsSelect */ -//= require lib/utils/type_utility -//= require jquery -//= require bootstrap -//= require gl_dropdown -//= require select2 -//= require jquery.nicescroll -//= require api -//= require create_label -//= require issuable_context -//= require users_select -//= require labels_select +require('./spec_helper'); +require('lib/utils/type_utility'); +require('gl_dropdown'); +require('select2'); +require('vendor/jquery.nicescroll'); +require('api'); +require('create_label'); +require('issuable_context'); +require('users_select'); +require('labels_select'); (() => { let saveLabelCount = 0; diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index ef75f600898..46aa0702bda 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -1,4 +1,4 @@ -//= require lib/utils/common_utils +require('lib/utils/common_utils'); (() => { describe('common_utils', () => { diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index 31f516b41bf..fb549b846e0 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,7 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */ /* global LineHighlighter */ -/*= require line_highlighter */ +require('./spec_helper'); +require('line_highlighter'); (function() { describe('LineHighlighter', function() { diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 9b232617fe5..bbfa6aa67a5 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,7 +1,8 @@ /* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */ /* global MergeRequest */ -/*= require merge_request */ +require('./spec_helper'); +require('merge_request'); (function() { describe('MergeRequest', function() { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 98201fb98ed..a8fa47cdd57 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,9 +1,10 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ -/*= require merge_request_tabs */ -//= require breakpoints -//= require lib/utils/common_utils -//= require jquery.scrollTo +require('./spec_helper'); +require('merge_request_tabs'); +require('breakpoints'); +require('lib/utils/common_utils'); +require('vendor/jquery.scrollTo'); (function () { describe('MergeRequestTabs', function () { diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index 6f91529db00..b29f5bad234 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */ -/*= require merge_request_widget */ -/*= require lib/utils/datetime_utility */ +require('merge_request_widget'); +require('lib/utils/datetime_utility'); (function() { describe('MergeRequestWidget', function() { diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 index a1c2fe3df37..32b80a4f4bd 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable no-new */ -//= require flash -//= require mini_pipeline_graph_dropdown +require('flash'); +require('mini_pipeline_graph_dropdown'); (() => { describe('Mini Pipeline Graph Dropdown', () => { diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index e0dc549a9f4..8d8c8ec9b0d 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,8 +1,9 @@ /* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, padded-blocks, max-len */ /* global NewBranchForm */ -/*= require jquery-ui/autocomplete */ -/*= require new_branch_form */ +require('./spec_helper'); +require('jquery-ui/ui/autocomplete'); +require('new_branch_form'); (function() { describe('Branch', function() { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 9cdb0a5d5aa..d3cf2f6a0bb 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,10 +1,11 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ /* global Notes */ -/*= require notes */ -/*= require autosize */ -/*= require gl_form */ -/*= require lib/utils/text_utility */ +window._ = require('underscore'); +require('notes'); +require('vendor/autosize'); +require('gl_form'); +require('lib/utils/text_utility'); (function() { window.gon || (window.gon = {}); diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6 index f0f9ad7430d..1bee64b814f 100644 --- a/spec/javascripts/pipelines_spec.js.es6 +++ b/spec/javascripts/pipelines_spec.js.es6 @@ -1,4 +1,4 @@ -//= require pipelines +require('pipelines'); (() => { describe('Pipelines', () => { diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6 index 2e12d45f7a7..207d40983b4 100644 --- a/spec/javascripts/pretty_time_spec.js.es6 +++ b/spec/javascripts/pretty_time_spec.js.es6 @@ -1,4 +1,4 @@ -//= require lib/utils/pretty_time +require('lib/utils/pretty_time'); (() => { const PrettyTime = gl.PrettyTime; diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 27b071f266d..bc09bbbe512 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,14 +1,14 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, padded-blocks, max-len */ - /* global Project */ -/*= require bootstrap */ -/*= require select2 */ -/*= require lib/utils/type_utility */ -/*= require gl_dropdown */ -/*= require api */ -/*= require project_select */ -/*= require project */ +require('./spec_helper'); +require('bootstrap/js/dropdown'); +require('select2/select2.js'); +require('lib/utils/type_utility'); +require('gl_dropdown'); +require('api'); +require('project_select'); +require('project'); (function() { window.gon || (window.gon = {}); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 0177d8e4e79..e0343c19fbe 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,11 +1,10 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */ /* global Sidebar */ -/*= require right_sidebar */ -/*= require jquery */ -/*= require js.cookie */ - -/*= require extensions/jquery.js */ +require('./spec_helper'); +require('right_sidebar'); +require('vendor/js.cookie'); +require('extensions/jquery.js'); (function() { var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index e13c4ad772c..2609ca4f53b 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,13 +1,12 @@ /* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, padded-blocks, max-len */ - -/*= require gl_dropdown */ -/*= require search_autocomplete */ -/*= require jquery */ -/*= require lib/utils/common_utils */ -/*= require lib/utils/type_utility */ -/*= require fuzzaldrin-plus */ -/*= require turbolinks */ -/*= require jquery.turbolinks */ +require('./spec_helper'); +require('gl_dropdown'); +require('search_autocomplete'); +require('lib/utils/common_utils'); +require('lib/utils/type_utility'); +require('vendor/fuzzaldrin-plus'); +require('vendor/turbolinks'); +require('vendor/jquery.turbolinks'); (function() { var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index ae5d639ad9c..e6605c46bfc 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,7 +1,8 @@ /* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */ /* global ShortcutsIssuable */ -/*= require shortcuts_issuable */ +require('./spec_helper'); +require('shortcuts_issuable'); (function() { describe('ShortcutsIssuable', function() { diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 index c274b9c45f4..f7aa3e663f9 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 +++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 @@ -1,4 +1,5 @@ -/*= require signin_tabs_memoizer */ +require('./spec_helper'); +require('signin_tabs_memoizer'); ((global) => { describe('SigninTabsMemoizer', () => { diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index 39d236986b9..23cf8689585 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -1,5 +1,5 @@ -//= require jquery -//= require smart_interval +require('./spec_helper'); +require('smart_interval'); (() => { const DEFAULT_MAX_INTERVAL = 100; diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index f8e3aca29fa..b6dcdba927b 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -1,48 +1,8 @@ -/* eslint-disable space-before-function-paren */ -// PhantomJS (Teaspoons default driver) doesn't have support for -// Function.prototype.bind, which has caused confusion. Use this polyfill to -// avoid the confusion. -/*= require support/bind-poly */ +require('jasmine-jquery'); -// You can require your own javascript files here. By default this will include -// everything in application, however you may get better load performance if you -// require the specific files that are being used in the spec that tests them. -/*= require jquery */ -/*= require jquery.turbolinks */ -/*= require bootstrap */ -/*= require underscore */ +jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; +jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; -// Teaspoon includes some support files, but you can use anything from your own -// support path too. -// require support/jasmine-jquery-1.7.0 -// require support/jasmine-jquery-2.0.0 -/*= require support/jasmine-jquery-2.1.0 */ - -// require support/sinon -// require support/your-support-file -// Deferring execution -// If you're using CommonJS, RequireJS or some other asynchronous library you can -// defer execution. Call Teaspoon.execute() after everything has been loaded. -// Simple example of a timeout: -// Teaspoon.defer = true -// setTimeout(Teaspoon.execute, 1000) -// Matching files -// By default Teaspoon will look for files that match -// _spec.{js,js.es6}. Add a filename_spec.js file in your spec path -// and it'll be included in the default suite automatically. If you want to -// customize suites, check out the configuration in teaspoon_env.rb -// Manifest -// If you'd rather require your spec files manually (to control order for -// instance) you can disable the suite matcher in the configuration and use this -// file as a manifest. -// For more information: http://github.com/modeset/teaspoon - -// set our fixtures path -jasmine.getFixtures().fixturesPath = '/teaspoon/fixtures'; -jasmine.getJSONFixtures().fixturesPath = '/teaspoon/fixtures'; - -// defined in ActionDispatch::TestRequest -// see https://github.com/rails/rails/blob/v4.2.7.1/actionpack/lib/action_dispatch/testing/test_request.rb#L7 window.gl = window.gl || {}; window.gl.TEST_HOST = 'http://test.host'; window.gon = window.gon || {}; diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6 index 6a70dd856a7..c24e860afd1 100644 --- a/spec/javascripts/subbable_resource_spec.js.es6 +++ b/spec/javascripts/subbable_resource_spec.js.es6 @@ -1,9 +1,8 @@ /* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */ -//= vue -//= vue-resource -//= require jquery -//= require subbable_resource +require('./spec_helper'); +window._ = require('underscore'); +require('subbable_resource'); /* * Test that each rest verb calls the publish and subscribe function and passes the correct value back diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index 5984ce8ffd4..c06339fa709 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -1,6 +1,7 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes, padded-blocks */ -/*= require syntax_highlight */ +require('./spec_helper'); +require('syntax_highlight'); (function() { describe('Syntax Highlighter', function() { diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index dc2f4967985..9cd59fb019e 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -2,11 +2,12 @@ /* global MockU2FDevice */ /* global U2FAuthenticate */ -/*= require u2f/authenticate */ -/*= require u2f/util */ -/*= require u2f/error */ -/*= require u2f */ -/*= require ./mock_u2f_device */ +require('./spec_helper'); +require('u2f/authenticate'); +require('u2f/util'); +require('u2f/error'); +require('vendor/u2f'); +require('./mock_u2f_device'); (function() { describe('U2FAuthenticate', function() { diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index ab4c5edd044..36989bee2d2 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -2,11 +2,12 @@ /* global MockU2FDevice */ /* global U2FRegister */ -/*= require u2f/register */ -/*= require u2f/util */ -/*= require u2f/error */ -/*= require u2f */ -/*= require ./mock_u2f_device */ +require('./spec_helper'); +require('u2f/register'); +require('u2f/util'); +require('u2f/error'); +require('vendor/u2f'); +require('./mock_u2f_device'); (function() { describe('U2FRegister', function() { diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index d6c6f786fb1..23ae7c4ba19 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -1,4 +1,5 @@ -//= require vue_common_component/commit +require('./spec_helper'); +require('vue_common_component/commit'); describe('Commit component', () => { let props; diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index f1c2edcc55c..2c790b193b0 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -3,7 +3,8 @@ /* global Mousetrap */ /* global ZenMode */ -/*= require zen_mode */ +require('./spec_helper'); +require('zen_mode'); (function() { var enterZen, escapeKeydown, exitZen; diff --git a/vendor/assets/javascripts/jquery.turbolinks.js b/vendor/assets/javascripts/jquery.turbolinks.js index fd6e95e75d5..0cf3fc7cf7a 100644 --- a/vendor/assets/javascripts/jquery.turbolinks.js +++ b/vendor/assets/javascripts/jquery.turbolinks.js @@ -8,10 +8,23 @@ The MIT License Copyright (c) 2012-2013 Sasha Koss & Rico Sta. Cruz */ -(function() { +(function (root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module unless amdModuleId is set + define(["jquery"], function (a0) { + return (factory(a0)); + }); + } else if (typeof exports === 'object') { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(require("jquery")); + } else { + factory(jQuery); + } +}(this, function($) { var $, $document; - - $ = window.jQuery || (typeof require === "function" ? require('jquery') : void 0); + $ = $ || window.jQuery || (typeof require === "function" ? require('jquery') : void 0); $document = $(document); @@ -46,4 +59,4 @@ Copyright (c) 2012-2013 Sasha Koss & Rico Sta. Cruz $.turbo.use('page:load', 'page:fetch'); -}).call(this); +})); -- cgit v1.2.1 From 1e8a4189dc4cd97702999367775c54c10463ffc9 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sun, 11 Dec 2016 01:31:19 -0600 Subject: move webpack asset compilation to the setup-test-env stage --- .gitlab-ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6e727333929..ebf165abf14 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,8 +24,6 @@ before_script: - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) $FLAGS' - retry gem install knapsack - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql' - - curl --silent --location https://deb.nodesource.com/setup_6.x | bash - - - apt-get install --assume-yes nodejs - npm install stages: @@ -64,7 +62,6 @@ stages: <<: *dedicated-runner <<: *use-db script: - - bundle exec rake webpack:compile - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} @@ -83,7 +80,6 @@ stages: <<: *dedicated-runner <<: *use-db script: - - bundle exec rake webpack:compile - JOB_NAME=( $CI_BUILD_NAME ) - export CI_NODE_INDEX=${JOB_NAME[1]} - export CI_NODE_TOTAL=${JOB_NAME[2]} @@ -113,6 +109,7 @@ setup-test-env: <<: *dedicated-runner stage: prepare script: + - bundle exec rake webpack:compile - bundle exec rake assets:precompile 2>/dev/null - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' artifacts: @@ -381,6 +378,8 @@ lint:javascript: - node_modules/ stage: test image: "node:7.1" + before_script: + - npm install script: - npm --silent run eslint @@ -391,6 +390,8 @@ lint:javascript:report: - node_modules/ stage: post-test image: "node:7.1" + before_script: + - npm install script: - find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files - npm --silent run eslint-report || true # ignore exit code -- cgit v1.2.1 From ee6de1d34f27fa4e60ab8a493d1b2d6ca997e91e Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Thu, 29 Dec 2016 15:30:58 -0600 Subject: ignore karma config in eslint --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index a9d27a6765e..c742b08c005 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,4 +5,5 @@ /public/ /tmp/ /vendor/ +karma.config.js webpack.config.js -- cgit v1.2.1 From 0e4aaa06d3a83f66a90a6f084efc8742a4221d5f Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 9 Jan 2017 18:04:04 -0600 Subject: remove remaining vestiges of teaspoon test runner --- .gitlab-ci.yml | 3 +- Gemfile | 4 - Gemfile.lock | 9 --- bin/teaspoon | 8 -- lib/tasks/gitlab/test.rake | 2 +- lib/tasks/karma.rake | 25 +++++++ lib/tasks/teaspoon.rake | 25 ------- lib/tasks/test.rake | 2 +- package.json | 1 + spec/teaspoon_env.rb | 178 --------------------------------------------- 10 files changed, 29 insertions(+), 228 deletions(-) delete mode 100755 bin/teaspoon create mode 100644 lib/tasks/karma.rake delete mode 100644 lib/tasks/teaspoon.rake delete mode 100644 spec/teaspoon_env.rb diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ebf165abf14..b8d2499f984 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -298,8 +298,7 @@ karma: <<: *dedicated-runner script: - npm link istanbul - - rake webpack:compile - - npm run karma-start + - rake karma artifacts: name: coverage-javascript expire_in: 31d diff --git a/Gemfile b/Gemfile index 27e415966df..0a5b4f5753c 100644 --- a/Gemfile +++ b/Gemfile @@ -290,13 +290,9 @@ group :development, :test do gem 'capybara-screenshot', '~> 1.0.0' gem 'poltergeist', '~> 1.9.0' - gem 'teaspoon', '~> 1.1.0' - gem 'teaspoon-jasmine', '~> 2.2.0' - gem 'spring', '~> 1.7.0' gem 'spring-commands-rspec', '~> 1.0.4' gem 'spring-commands-spinach', '~> 1.1.0' - gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'rubocop', '~> 0.43.0', require: false gem 'rubocop-rspec', '~> 1.5.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index b88f51a7a43..d02d35d638c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -697,8 +697,6 @@ GEM spring (>= 0.9.1) spring-commands-spinach (1.1.0) spring (>= 0.9.1) - spring-commands-teaspoon (0.0.2) - spring (>= 0.9.1) sprockets (3.7.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -722,10 +720,6 @@ GEM sys-filesystem (1.1.6) ffi sysexits (1.2.0) - teaspoon (1.1.5) - railties (>= 3.2.5, < 6) - teaspoon-jasmine (2.2.0) - teaspoon (>= 1.0.0) temple (0.7.7) test_after_commit (0.4.2) activerecord (>= 3.2) @@ -958,14 +952,11 @@ DEPENDENCIES spring (~> 1.7.0) spring-commands-rspec (~> 1.0.4) spring-commands-spinach (~> 1.1.0) - spring-commands-teaspoon (~> 0.0.2) sprockets (~> 3.7.0) sprockets-es6 (~> 0.9.2) stackprof (~> 0.2.10) state_machines-activerecord (~> 0.4.0) sys-filesystem (~> 1.1.6) - teaspoon (~> 1.1.0) - teaspoon-jasmine (~> 2.2.0) test_after_commit (~> 0.4.2) thin (~> 1.7.0) timecop (~> 0.8.0) diff --git a/bin/teaspoon b/bin/teaspoon deleted file mode 100755 index 7c3b8dfc4ed..00000000000 --- a/bin/teaspoon +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env ruby -begin - load File.expand_path('../spring', __FILE__) -rescue LoadError => e - raise unless e.message.include?('spring') -end -require 'bundler/setup' -load Gem.bin_path('teaspoon', 'teaspoon') diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake index ec7aec1621c..84810b489ce 100644 --- a/lib/tasks/gitlab/test.rake +++ b/lib/tasks/gitlab/test.rake @@ -6,7 +6,7 @@ namespace :gitlab do %W(rake rubocop), %W(rake spinach), %W(rake spec), - %W(npm run karma-start) + %W(rake karma) ] cmds.each do |cmd| diff --git a/lib/tasks/karma.rake b/lib/tasks/karma.rake new file mode 100644 index 00000000000..89812a179ec --- /dev/null +++ b/lib/tasks/karma.rake @@ -0,0 +1,25 @@ +unless Rails.env.production? + Rake::Task['karma'].clear if Rake::Task.task_defined?('karma') + + namespace :karma do + desc 'GitLab | Karma | Generate fixtures for JavaScript tests' + RSpec::Core::RakeTask.new(:fixtures) do |t| + ENV['NO_KNAPSACK'] = 'true' + t.pattern = 'spec/javascripts/fixtures/*.rb' + t.rspec_opts = '--format documentation' + end + + desc 'GitLab | Karma | Run JavaScript tests' + task :tests do + sh "npm run karma" do |ok, res| + abort('rake karma:tests failed') unless ok + end + end + end + + desc 'GitLab | Karma | Shortcut for karma:fixtures and karma:tests' + task :karma do + Rake::Task['karma:fixtures'].invoke + Rake::Task['karma:tests'].invoke + end +end diff --git a/lib/tasks/teaspoon.rake b/lib/tasks/teaspoon.rake deleted file mode 100644 index 08caedd7ff3..00000000000 --- a/lib/tasks/teaspoon.rake +++ /dev/null @@ -1,25 +0,0 @@ -unless Rails.env.production? - Rake::Task['teaspoon'].clear if Rake::Task.task_defined?('teaspoon') - - namespace :teaspoon do - desc 'GitLab | Teaspoon | Generate fixtures for JavaScript tests' - RSpec::Core::RakeTask.new(:fixtures) do |t| - ENV['NO_KNAPSACK'] = 'true' - t.pattern = 'spec/javascripts/fixtures/*.rb' - t.rspec_opts = '--format documentation' - end - - desc 'GitLab | Teaspoon | Run JavaScript tests' - task :tests do - require "teaspoon/console" - options = {} - abort('rake teaspoon:tests failed') if Teaspoon::Console.new(options).failures? - end - end - - desc 'GitLab | Teaspoon | Shortcut for teaspoon:fixtures and teaspoon:tests' - task :teaspoon do - Rake::Task['teaspoon:fixtures'].invoke - Rake::Task['teaspoon:tests'].invoke - end -end diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake index 83f53e5454b..3e01f91d32c 100644 --- a/lib/tasks/test.rake +++ b/lib/tasks/test.rake @@ -7,5 +7,5 @@ end unless Rails.env.production? desc "GitLab | Run all tests on CI with simplecov" - task test_ci: [:rubocop, :brakeman, :'karma-start', :spinach, :spec] + task test_ci: [:rubocop, :brakeman, :karma, :spinach, :spec] end diff --git a/package.json b/package.json index ec012e9c3bd..ca767ebec9c 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .", "eslint-fix": "npm run eslint -- --fix", "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html", + "karma": "karma start config/karma.config.js --single-run", "karma-start": "karma start config/karma.config.js" }, "dependencies": { diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb deleted file mode 100644 index 5ea020f313c..00000000000 --- a/spec/teaspoon_env.rb +++ /dev/null @@ -1,178 +0,0 @@ -Teaspoon.configure do |config| - # Determines where the Teaspoon routes will be mounted. Changing this to "/jasmine" would allow you to browse to - # `http://localhost:3000/jasmine` to run your tests. - config.mount_at = "/teaspoon" - - # Specifies the root where Teaspoon will look for files. If you're testing an engine using a dummy application it can - # be useful to set this to your engines root (e.g. `Teaspoon::Engine.root`). - # Note: Defaults to `Rails.root` if nil. - config.root = nil - - # Paths that will be appended to the Rails assets paths - # Note: Relative to `config.root`. - config.asset_paths = ["spec/javascripts", "spec/javascripts/stylesheets"] - - # Fixtures are rendered through a controller, which allows using HAML, RABL/JBuilder, etc. Files in these paths will - # be rendered as fixtures. - config.fixture_paths = ["spec/javascripts/fixtures"] - - # SUITES - # - # You can modify the default suite configuration and create new suites here. Suites are isolated from one another. - # - # When defining a suite you can provide a name and a block. If the name is left blank, :default is assumed. You can - # omit various directives and the ones defined in the default suite will be used. - # - # To run a specific suite - # - in the browser: http://localhost/teaspoon/[suite_name] - # - with the rake task: rake teaspoon suite=[suite_name] - # - with the cli: teaspoon --suite=[suite_name] - config.suite do |suite| - # Specify the framework you would like to use. This allows you to select versions, and will do some basic setup for - # you -- which you can override with the directives below. This should be specified first, as it can override other - # directives. - # Note: If no version is specified, the latest is assumed. - # - # Versions: 1.3.1, 2.0.3, 2.1.3, 2.2.0 - suite.use_framework :jasmine, "2.2.0" - - # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These - # files need to be within an asset path. You can add asset paths using the `config.asset_paths`. - suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.es6,es6}" - - # Load additional JS files, but requiring them in your spec helper is the preferred way to do this. - # suite.javascripts = [] - - # You can include your own stylesheets if you want to change how Teaspoon looks. - # Note: Spec related CSS can and should be loaded using fixtures. - # suite.stylesheets = ["teaspoon"] - - # This suites spec helper, which can require additional support files. This file is loaded before any of your test - # files are loaded. - suite.helper = "spec_helper" - - # Partial to be rendered in the head tag of the runner. You can use the provided ones or define your own by creating - # a `_boot.html.erb` in your fixtures path, and adjust the config to `"/boot"` for instance. - # - # Available: boot, boot_require_js - suite.boot_partial = "boot" - - # Partial to be rendered in the body tag of the runner. You can define your own to create a custom body structure. - suite.body_partial = "body" - - # Hooks allow you to use `Teaspoon.hook("fixtures")` before, after, or during your spec run. This will make a - # synchronous Ajax request to the server that will call all of the blocks you've defined for that hook name. - # suite.hook :fixtures, &proc{} - - # Determine whether specs loaded into the test harness should be embedded as individual script tags or concatenated - # into a single file. Similar to Rails' asset `debug: true` and `config.assets.debug = true` options. By default, - # Teaspoon expands all assets to provide more valuable stack traces that reference individual source files. - # suite.expand_assets = true - end - - # Example suite. Since we're just filtering to files already within the root test/javascripts, these files will also - # be run in the default suite -- but can be focused into a more specific suite. - # config.suite :targeted do |suite| - # suite.matcher = "spec/javascripts/targeted/*_spec.{js,js.coffee,coffee}" - # end - - # CONSOLE RUNNER SPECIFIC - # - # These configuration directives are applicable only when running via the rake task or command line interface. These - # directives can be overridden using the command line interface arguments or with ENV variables when using the rake - # task. - # - # Command Line Interface: - # teaspoon --driver=phantomjs --server-port=31337 --fail-fast=true --format=junit --suite=my_suite /spec/file_spec.js - # - # Rake: - # teaspoon DRIVER=phantomjs SERVER_PORT=31337 FAIL_FAST=true FORMATTERS=junit suite=my_suite - - # Specify which headless driver to use. Supports PhantomJS and Selenium Webdriver. - # - # Available: :phantomjs, :selenium, :capybara_webkit - # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS - # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver - # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit - # config.driver = :phantomjs - - # Specify additional options for the driver. - # - # PhantomJS: https://github.com/modeset/teaspoon/wiki/Using-PhantomJS - # Selenium Webdriver: https://github.com/modeset/teaspoon/wiki/Using-Selenium-WebDriver - # Capybara Webkit: https://github.com/modeset/teaspoon/wiki/Using-Capybara-Webkit - # config.driver_options = nil - - # Specify the timeout for the driver. Specs are expected to complete within this time frame or the run will be - # considered a failure. This is to avoid issues that can arise where tests stall. - # config.driver_timeout = 180 - - # Specify a server to use with Rack (e.g. thin, mongrel). If nil is provided Rack::Server is used. - # config.server = nil - - # Specify a port to run on a specific port, otherwise Teaspoon will use a random available port. - # config.server_port = nil - - # Timeout for starting the server in seconds. If your server is slow to start you may have to bump this, or you may - # want to lower this if you know it shouldn't take long to start. - # config.server_timeout = 20 - - # Force Teaspoon to fail immediately after a failing suite. Can be useful to make Teaspoon fail early if you have - # several suites, but in environments like CI this may not be desirable. - # config.fail_fast = true - - # Specify the formatters to use when outputting the results. - # Note: Output files can be specified by using `"junit>/path/to/output.xml"`. - # - # Available: :dot, :clean, :documentation, :json, :junit, :pride, :rspec_html, :snowday, :swayze_or_oprah, :tap, :tap_y, :teamcity - # config.formatters = [:dot] - - # Specify if you want color output from the formatters. - # config.color = true - - # Teaspoon pipes all console[log/debug/error] to $stdout. This is useful to catch places where you've forgotten to - # remove them, but in verbose applications this may not be desirable. - # config.suppress_log = false - - # COVERAGE REPORTS / THRESHOLD ASSERTIONS - # - # Coverage reports requires Istanbul (https://github.com/gotwarlost/istanbul) to add instrumentation to your code and - # display coverage statistics. - # - # Coverage configurations are similar to suites. You can define several, and use different ones under different - # conditions. - # - # To run with a specific coverage configuration - # - with the rake task: rake teaspoon USE_COVERAGE=[coverage_name] - # - with the cli: teaspoon --coverage=[coverage_name] - - # Specify that you always want a coverage configuration to be used. Otherwise, specify that you want coverage - # on the CLI. - # Set this to "true" or the name of your coverage config. - config.use_coverage = true - - # You can have multiple coverage configs by passing a name to config.coverage. - # e.g. config.coverage :ci do |coverage| - # The default coverage config name is :default. - config.coverage do |coverage| - # Which coverage reports Istanbul should generate. Correlates directly to what Istanbul supports. - # - # Available: text-summary, text, html, lcov, lcovonly, cobertura, teamcity - coverage.reports = ["text-summary", "html"] - - # The path that the coverage should be written to - when there's an artifact to write to disk. - # Note: Relative to `config.root`. - coverage.output_path = "coverage-javascript" - - # Assets to be ignored when generating coverage reports. Accepts an array of filenames or regular expressions. The - # default excludes assets from vendor, gems and support libraries. - coverage.ignore = [%r{vendor/}, %r{spec/}] - - # Various thresholds requirements can be defined, and those thresholds will be checked at the end of a run. If any - # aren't met the run will fail with a message. Thresholds can be defined as a percentage (0-100), or nil. - # coverage.statements = nil - # coverage.functions = nil - # coverage.branches = nil - # coverage.lines = nil - end -end -- cgit v1.2.1 From eb1bbe73d06dbb9419607c0882f2d6da7fe4428c Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 6 Jan 2017 12:46:56 -0600 Subject: fix spec_helper missing/broken references --- spec/javascripts/behaviors/autosize_spec.js | 1 + spec/javascripts/behaviors/quick_submit_spec.js | 2 +- spec/javascripts/behaviors/requires_input_spec.js | 2 +- spec/javascripts/boards/boards_store_spec.js.es6 | 1 + spec/javascripts/boards/issue_spec.js.es6 | 1 + spec/javascripts/boards/list_spec.js.es6 | 1 + spec/javascripts/datetime_utility_spec.js.es6 | 1 + spec/javascripts/diff_comments_store_spec.js.es6 | 1 + spec/javascripts/environments/environment_actions_spec.js.es6 | 2 +- spec/javascripts/environments/environment_external_url_spec.js.es6 | 2 +- spec/javascripts/environments/environment_item_spec.js.es6 | 2 +- spec/javascripts/environments/environment_rollback_spec.js.es6 | 2 +- spec/javascripts/environments/environment_stop_spec.js.es6 | 2 +- spec/javascripts/environments/environments_store_spec.js.es6 | 1 + spec/javascripts/extensions/array_spec.js.es6 | 1 + spec/javascripts/extensions/element_spec.js.es6 | 1 + spec/javascripts/extensions/jquery_spec.js | 1 + spec/javascripts/extensions/object_spec.js.es6 | 1 + spec/javascripts/graphs/stat_graph_contributors_graph_spec.js | 1 + spec/javascripts/graphs/stat_graph_contributors_util_spec.js | 1 + spec/javascripts/graphs/stat_graph_spec.js | 1 + spec/javascripts/header_spec.js | 1 + spec/javascripts/issuable_spec.js.es6 | 1 + spec/javascripts/lib/utils/common_utils_spec.js.es6 | 1 + spec/javascripts/merge_request_widget_spec.js | 1 + spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 | 1 + spec/javascripts/notes_spec.js | 1 + spec/javascripts/pipelines_spec.js.es6 | 1 + spec/javascripts/pretty_time_spec.js.es6 | 1 + spec/javascripts/search_autocomplete_spec.js | 1 + spec/javascripts/u2f/authenticate_spec.js | 2 +- spec/javascripts/u2f/register_spec.js | 2 +- spec/javascripts/vue_common_components/commit_spec.js.es6 | 2 +- 33 files changed, 33 insertions(+), 10 deletions(-) diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index e05793cf2e3..603f1ae5f3e 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -1,5 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, padded-blocks, max-len */ +require('../spec_helper'); require('behaviors/autosize'); (function() { diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index 4a00b1b2d38..f7bf95daa6c 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ -require('./spec_helper'); +require('../spec_helper'); require('behaviors/quick_submit'); (function() { diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 44d91d41abf..40a113cded3 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, padded-blocks */ -require('./spec_helper'); +require('../spec_helper'); require('behaviors/requires_input'); (function() { diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index d995380c620..1df3e65fba5 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -6,6 +6,7 @@ /* global listObj */ /* global listObjDuplicate */ +require('../spec_helper'); require('jquery_ujs'); require('js.cookie'); require('vue'); diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index 2cdbdd725b1..98f09e77609 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -2,6 +2,7 @@ /* global BoardService */ /* global ListIssue */ +require('../spec_helper'); require('jquery_ujs'); require('js.cookie'); require('vue'); diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index dd82f482207..9f92854eff0 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -5,6 +5,7 @@ /* global List */ /* global listObj */ +require('../spec_helper'); require('jquery_ujs'); require('js.cookie'); require('vue'); diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 index 713e7742988..d4f27d2691a 100644 --- a/spec/javascripts/datetime_utility_spec.js.es6 +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -1,3 +1,4 @@ +require('./spec_helper'); require('lib/utils/datetime_utility'); (() => { diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index f27ba0f93f7..487f1ab8b5d 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -1,6 +1,7 @@ /* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ /* global CommentsStore */ +require('./spec_helper'); require('diff_notes/models/discussion'); require('diff_notes/models/note'); require('diff_notes/stores/comments'); diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index 34330f7bfd6..d2d644fc325 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -1,4 +1,4 @@ -require('./spec_helper'); +require('../spec_helper'); require('vue'); require('environments/components/environment_actions'); diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index 71785e7ecf1..42886302183 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -1,4 +1,4 @@ -require('./spec_helper'); +require('../spec_helper'); require('vue'); require('environments/components/environment_external_url'); diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index be753498269..2e0f676065c 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,4 +1,4 @@ -require('./spec_helper'); +require('../spec_helper'); require('vue'); require('timeago'); require('environments/components/environment_item'); diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 72f44014258..46002ceef8b 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,4 +1,4 @@ -require('./spec_helper'); +require('../spec_helper'); require('vue'); require('environments/components/environment_rollback'); diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index 37ae970859c..7ab6cbbda6a 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,4 +1,4 @@ -require('./spec_helper'); +require('../spec_helper'); require('vue'); require('environments/components/environment_stop'); diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index eefe87be5aa..dbc03bd8c4e 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,5 +1,6 @@ /* global environmentsList */ +require('../spec_helper'); require('vue'); require('environments/stores/environments_store'); require('./mock_data'); diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6 index 5396e0eb639..f35cda4cac7 100644 --- a/spec/javascripts/extensions/array_spec.js.es6 +++ b/spec/javascripts/extensions/array_spec.js.es6 @@ -1,5 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, padded-blocks */ +require('../spec_helper'); require('extensions/array'); (function() { diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6 index 49544ae8b5c..fddd7600cb9 100644 --- a/spec/javascripts/extensions/element_spec.js.es6 +++ b/spec/javascripts/extensions/element_spec.js.es6 @@ -1,3 +1,4 @@ +require('../spec_helper'); require('extensions/element'); (() => { diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index 3163414b134..5f5bca4bc06 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -1,5 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, padded-blocks */ +require('../spec_helper'); require('extensions/jquery'); (function() { diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6 index 77ffa1a35ae..25707be7bb4 100644 --- a/spec/javascripts/extensions/object_spec.js.es6 +++ b/spec/javascripts/extensions/object_spec.js.es6 @@ -1,3 +1,4 @@ +require('../spec_helper'); require('extensions/object'); describe('Object extensions', () => { diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index a914eda90bb..b4d278dfe88 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -3,6 +3,7 @@ /* global ContributorsGraph */ /* global ContributorsMasterGraph */ +require('../spec_helper'); require('graphs/stat_graph_contributors_graph'); describe("ContributorsGraph", function () { diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 4f82e1c46db..8323fcff02d 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,6 +1,7 @@ /* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */ /* global ContributorsStatGraphUtil */ +require('../spec_helper'); require('graphs/stat_graph_contributors_util'); describe("ContributorsStatGraphUtil", function () { diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index a017f35831d..66c8c16a40f 100644 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,6 +1,7 @@ /* eslint-disable quotes, padded-blocks, semi */ /* global StatGraph */ +require('../spec_helper'); require('graphs/stat_graph'); describe("StatGraph", function () { diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index cb12fa327d4..4e93e33d432 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -1,4 +1,5 @@ /* eslint-disable space-before-function-paren, padded-blocks, no-var */ + require('./spec_helper'); require('header'); require('lib/utils/text_utility'); diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 index 2daadfa0c77..b5f4b3d6751 100644 --- a/spec/javascripts/issuable_spec.js.es6 +++ b/spec/javascripts/issuable_spec.js.es6 @@ -1,6 +1,7 @@ /* global Issuable */ /* global Turbolinks */ +require('./spec_helper'); require('issuable'); require('turbolinks'); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 46aa0702bda..02aabf202d0 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -1,3 +1,4 @@ +require('../../spec_helper'); require('lib/utils/common_utils'); (() => { diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index b29f5bad234..0ec119c94b3 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,5 +1,6 @@ /* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */ +require('./spec_helper'); require('merge_request_widget'); require('lib/utils/datetime_utility'); diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 index 32b80a4f4bd..1faa1f88d70 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 @@ -1,5 +1,6 @@ /* eslint-disable no-new */ +require('./spec_helper'); require('flash'); require('mini_pipeline_graph_dropdown'); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index d3cf2f6a0bb..e257deac201 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,6 +1,7 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ /* global Notes */ +require('./spec_helper'); window._ = require('underscore'); require('notes'); require('vendor/autosize'); diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6 index 1bee64b814f..e51977ab720 100644 --- a/spec/javascripts/pipelines_spec.js.es6 +++ b/spec/javascripts/pipelines_spec.js.es6 @@ -1,3 +1,4 @@ +require('./spec_helper'); require('pipelines'); (() => { diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6 index 207d40983b4..249aa4817e9 100644 --- a/spec/javascripts/pretty_time_spec.js.es6 +++ b/spec/javascripts/pretty_time_spec.js.es6 @@ -1,3 +1,4 @@ +require('./spec_helper'); require('lib/utils/pretty_time'); (() => { diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 2609ca4f53b..d919a754a75 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,4 +1,5 @@ /* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, padded-blocks, max-len */ + require('./spec_helper'); require('gl_dropdown'); require('search_autocomplete'); diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 9cd59fb019e..a14f0e3c448 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -2,7 +2,7 @@ /* global MockU2FDevice */ /* global U2FAuthenticate */ -require('./spec_helper'); +require('../spec_helper'); require('u2f/authenticate'); require('u2f/util'); require('u2f/error'); diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 36989bee2d2..157c8796fd5 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -2,7 +2,7 @@ /* global MockU2FDevice */ /* global U2FRegister */ -require('./spec_helper'); +require('../spec_helper'); require('u2f/register'); require('u2f/util'); require('u2f/error'); diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index 23ae7c4ba19..28ffc005001 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -1,4 +1,4 @@ -require('./spec_helper'); +require('../spec_helper'); require('vue_common_component/commit'); describe('Commit component', () => { -- cgit v1.2.1 From 7837fe46f0e73119aaaacebd14f794f3e0fbc865 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 6 Jan 2017 12:50:01 -0600 Subject: fix broken js.cookie imports --- spec/javascripts/activities_spec.js.es6 | 2 +- spec/javascripts/awards_handler_spec.js | 2 +- spec/javascripts/boards/boards_store_spec.js.es6 | 2 +- spec/javascripts/boards/issue_spec.js.es6 | 2 +- spec/javascripts/boards/list_spec.js.es6 | 2 +- spec/javascripts/dashboard_spec.js.es6 | 2 +- spec/javascripts/right_sidebar_spec.js | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index 61fabd37170..6d625eeb973 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */ require('./spec_helper'); -require('vendor/js.cookie.js'); +window.Cookies = require('vendor/js.cookie'); require('vendor/jquery.endless-scroll.js'); require('pager'); require('activities'); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 88757e5c236..c256da072c0 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -3,7 +3,7 @@ require('./spec_helper'); require('awards_handler'); -require('vendor/js.cookie'); +window.Cookies = require('vendor/js.cookie'); require('./fixtures/emoji_menu'); (function() { diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index 1df3e65fba5..0a6842a0af2 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -8,7 +8,7 @@ require('../spec_helper'); require('jquery_ujs'); -require('js.cookie'); +window.Cookies = require('vendor/js.cookie'); require('vue'); require('vue-resource'); require('lib/utils/url_utility'); diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index 98f09e77609..6569f0f82f5 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -4,7 +4,7 @@ require('../spec_helper'); require('jquery_ujs'); -require('js.cookie'); +window.Cookies = require('vendor/js.cookie'); require('vue'); require('vue-resource'); require('lib/utils/url_utility'); diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 9f92854eff0..189da6ea9fe 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -7,7 +7,7 @@ require('../spec_helper'); require('jquery_ujs'); -require('js.cookie'); +window.Cookies = require('vendor/js.cookie'); require('vue'); require('vue-resource'); require('lib/utils/url_utility'); diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index 4223215c096..7e9e622970d 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -2,7 +2,7 @@ require('./spec_helper'); require('sidebar'); -require('vendor/js.cookie'); +window.Cookies = require('vendor/js.cookie'); require('lib/utils/text_utility'); ((global) => { diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index e0343c19fbe..65730b42ea3 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -3,7 +3,7 @@ require('./spec_helper'); require('right_sidebar'); -require('vendor/js.cookie'); +window.Cookies = require('vendor/js.cookie'); require('extensions/jquery.js'); (function() { -- cgit v1.2.1 From 8b26f8edea98d3971e81ec4fa05a875a05e32d58 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 6 Jan 2017 15:48:23 -0600 Subject: correct misnamed require statements --- spec/javascripts/boards/boards_store_spec.js.es6 | 2 +- spec/javascripts/boards/issue_spec.js.es6 | 2 +- spec/javascripts/boards/list_spec.js.es6 | 2 +- spec/javascripts/environments/environment_item_spec.js.es6 | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index 0a6842a0af2..99020fa213f 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -7,7 +7,7 @@ /* global listObjDuplicate */ require('../spec_helper'); -require('jquery_ujs'); +require('jquery-ujs'); window.Cookies = require('vendor/js.cookie'); require('vue'); require('vue-resource'); diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index 6569f0f82f5..23bc4eb276f 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -3,7 +3,7 @@ /* global ListIssue */ require('../spec_helper'); -require('jquery_ujs'); +require('jquery-ujs'); window.Cookies = require('vendor/js.cookie'); require('vue'); require('vue-resource'); diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 189da6ea9fe..11e7f25b83c 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -6,7 +6,7 @@ /* global listObj */ require('../spec_helper'); -require('jquery_ujs'); +require('jquery-ujs'); window.Cookies = require('vendor/js.cookie'); require('vue'); require('vue-resource'); diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index 2e0f676065c..6a697b45d67 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,6 +1,6 @@ require('../spec_helper'); require('vue'); -require('timeago'); +window.timeago = require('vendor/timeago'); require('environments/components/environment_item'); describe('Environment item', () => { -- cgit v1.2.1 From c2bd29ec7ba06d5aeeee9c054f5e90d3a740a5aa Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 6 Jan 2017 13:27:33 -0600 Subject: simplify and combine karma file patterns --- config/karma.config.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/config/karma.config.js b/config/karma.config.js index 478ef082547..0a3530b65ac 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -8,20 +8,13 @@ module.exports = function(config) { basePath: ROOT_PATH, frameworks: ['jquery-2.1.0', 'jasmine'], files: [ - 'spec/javascripts/*_spec.js', - 'spec/javascripts/*_spec.js.es6', - { pattern: 'spec/javascripts/fixtures/**/*.html', included: false, served: true }, - { pattern: 'spec/javascripts/fixtures/**/*.json', included: false, served: true }, + 'spec/javascripts/**/*_spec.js?(.es6)', + { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false }, ], preprocessors: { - 'spec/javascripts/*_spec.js': ['webpack'], - 'spec/javascripts/*_spec.js.es6': ['webpack'], - 'app/assets/javascripts/**/*.js': ['webpack'], - 'app/assets/javascripts/**/*.js.es6': ['webpack'], + 'spec/javascripts/**/*_spec.js?(.es6)': ['webpack'], }, - webpack: webpackConfig, - webpackMiddleware: { stats: 'errors-only' }, }); }; -- cgit v1.2.1 From 663936878f398710ed0fdd0f33b3c1b9efb4e1d0 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 6 Jan 2017 15:44:03 -0600 Subject: include common libraries within spec_helper --- config/webpack.config.js | 2 +- package.json | 1 - spec/javascripts/build_spec.js.es6 | 1 - spec/javascripts/gl_dropdown_spec.js.es6 | 1 - spec/javascripts/issuable_spec.js.es6 | 1 - spec/javascripts/notes_spec.js | 1 - spec/javascripts/project_title_spec.js | 1 - spec/javascripts/search_autocomplete_spec.js | 2 -- spec/javascripts/spec_helper.js | 19 +++++++++++++++++++ spec/javascripts/subbable_resource_spec.js.es6 | 1 - 10 files changed, 20 insertions(+), 10 deletions(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 5cba995888a..8dd9cf5b960 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -58,7 +58,7 @@ var config = { { test: /\.(js|es6)$/, loader: 'imports-loader', - query: '$=jquery,jQuery=jquery,this=>window' + query: 'this=>window' }, { test: /\.json$/, diff --git a/package.json b/package.json index ca767ebec9c..cea0ea4884c 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "jasmine-jquery": "^2.1.1", "karma": "^1.3.0", "karma-jasmine": "^1.1.0", - "karma-jquery": "^0.1.0", "karma-webpack": "^1.8.0" } } diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 8b0c797647b..1821c44fdfb 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -7,7 +7,6 @@ require('lib/utils/datetime_utility'); require('build'); require('breakpoints'); require('vendor/jquery.nicescroll'); -require('vendor/turbolinks'); describe('Build', () => { const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index fac5b20ea7e..7b2b4771756 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -3,7 +3,6 @@ require('./spec_helper'); require('gl_dropdown'); -require('vendor/turbolinks'); require('lib/utils/common_utils'); require('lib/utils/type_utility'); diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 index b5f4b3d6751..5d3269626af 100644 --- a/spec/javascripts/issuable_spec.js.es6 +++ b/spec/javascripts/issuable_spec.js.es6 @@ -3,7 +3,6 @@ require('./spec_helper'); require('issuable'); -require('turbolinks'); (() => { const BASE_URL = '/user/project/issues?scope=all&state=closed'; diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index e257deac201..75f5192a54e 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -2,7 +2,6 @@ /* global Notes */ require('./spec_helper'); -window._ = require('underscore'); require('notes'); require('vendor/autosize'); require('gl_form'); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index bc09bbbe512..a438b01fbce 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -2,7 +2,6 @@ /* global Project */ require('./spec_helper'); -require('bootstrap/js/dropdown'); require('select2/select2.js'); require('lib/utils/type_utility'); require('gl_dropdown'); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index d919a754a75..4ac9117fd8a 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -6,8 +6,6 @@ require('search_autocomplete'); require('lib/utils/common_utils'); require('lib/utils/type_utility'); require('vendor/fuzzaldrin-plus'); -require('vendor/turbolinks'); -require('vendor/jquery.turbolinks'); (function() { var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index b6dcdba927b..64f1ca4b80d 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -1,8 +1,27 @@ require('jasmine-jquery'); +// include common libraries +window.$ = window.jQuery = require('jquery'); +window._ = require('underscore'); +require('vendor/turbolinks'); +require('vendor/jquery.turbolinks'); +require('bootstrap/js/affix'); +require('bootstrap/js/alert'); +require('bootstrap/js/button'); +require('bootstrap/js/collapse'); +require('bootstrap/js/dropdown'); +require('bootstrap/js/modal'); +require('bootstrap/js/scrollspy'); +require('bootstrap/js/tab'); +require('bootstrap/js/transition'); +require('bootstrap/js/tooltip'); +require('bootstrap/js/popover'); + +// configure jasmine jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; +// stub expected globals window.gl = window.gl || {}; window.gl.TEST_HOST = 'http://test.host'; window.gon = window.gon || {}; diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6 index c24e860afd1..a70a1419792 100644 --- a/spec/javascripts/subbable_resource_spec.js.es6 +++ b/spec/javascripts/subbable_resource_spec.js.es6 @@ -1,7 +1,6 @@ /* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */ require('./spec_helper'); -window._ = require('underscore'); require('subbable_resource'); /* -- cgit v1.2.1 From fc3f4f88e094a192dd825d5ce4ca9bb742c8b4d3 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 6 Jan 2017 16:06:14 -0600 Subject: correct vue require statements --- spec/javascripts/boards/boards_store_spec.js.es6 | 4 ++-- spec/javascripts/boards/issue_spec.js.es6 | 4 ++-- spec/javascripts/boards/list_spec.js.es6 | 4 ++-- spec/javascripts/environments/environment_actions_spec.js.es6 | 2 +- spec/javascripts/environments/environment_external_url_spec.js.es6 | 2 +- spec/javascripts/environments/environment_item_spec.js.es6 | 2 +- spec/javascripts/environments/environment_rollback_spec.js.es6 | 2 +- spec/javascripts/environments/environment_stop_spec.js.es6 | 2 +- spec/javascripts/environments/environments_store_spec.js.es6 | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index 99020fa213f..c8de9ad79c3 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -9,8 +9,8 @@ require('../spec_helper'); require('jquery-ujs'); window.Cookies = require('vendor/js.cookie'); -require('vue'); -require('vue-resource'); +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); require('lib/utils/url_utility'); require('boards/models/issue'); require('boards/models/label'); diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index 23bc4eb276f..143315e4ac1 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -5,8 +5,8 @@ require('../spec_helper'); require('jquery-ujs'); window.Cookies = require('vendor/js.cookie'); -require('vue'); -require('vue-resource'); +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); require('lib/utils/url_utility'); require('boards/models/issue'); require('boards/models/label'); diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 11e7f25b83c..ab04e7b5896 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -8,8 +8,8 @@ require('../spec_helper'); require('jquery-ujs'); window.Cookies = require('vendor/js.cookie'); -require('vue'); -require('vue-resource'); +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); require('lib/utils/url_utility'); require('boards/models/issue'); require('boards/models/label'); diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index d2d644fc325..8d3539b29a5 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -1,5 +1,5 @@ require('../spec_helper'); -require('vue'); +window.Vue = require('vue'); require('environments/components/environment_actions'); describe('Actions Component', () => { diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index 42886302183..01264781a68 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -1,5 +1,5 @@ require('../spec_helper'); -require('vue'); +window.Vue = require('vue'); require('environments/components/environment_external_url'); describe('External URL Component', () => { diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index 6a697b45d67..56c718eb501 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,5 +1,5 @@ require('../spec_helper'); -require('vue'); +window.Vue = require('vue'); window.timeago = require('vendor/timeago'); require('environments/components/environment_item'); diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 46002ceef8b..5c378901608 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,5 +1,5 @@ require('../spec_helper'); -require('vue'); +window.Vue = require('vue'); require('environments/components/environment_rollback'); describe('Rollback Component', () => { diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index 7ab6cbbda6a..96c738f42d6 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,5 +1,5 @@ require('../spec_helper'); -require('vue'); +window.Vue = require('vue'); require('environments/components/environment_stop'); describe('Stop Component', () => { diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index dbc03bd8c4e..8c113b5679e 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,7 +1,7 @@ /* global environmentsList */ require('../spec_helper'); -require('vue'); +window.Vue = require('vue'); require('environments/stores/environments_store'); require('./mock_data'); -- cgit v1.2.1 From 05b95e712d1ea70d8658de42f7c87661e6937976 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 7 Jan 2017 01:48:34 -0600 Subject: update vue library to match vendors directory --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cea0ea4884c..ef16a371f91 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "select2": "3.5.2-browserify", "stats-webpack-plugin": "^0.4.2", "underscore": "1.8.3", - "vue": "1.0.26", + "vue": "2.0.3", "vue-resource": "0.9.3", "webpack": "^1.13.2", "webpack-dev-server": "^1.16.2" -- cgit v1.2.1 From 52c6702ec708deedef189784cdcb39564b2dcf52 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 6 Jan 2017 17:21:54 -0600 Subject: include spec_helper within karma config rather than each individual test --- config/karma.config.js | 5 +++-- spec/javascripts/abuse_reports_spec.js.es6 | 1 - spec/javascripts/activities_spec.js.es6 | 1 - spec/javascripts/awards_handler_spec.js | 1 - spec/javascripts/behaviors/autosize_spec.js | 1 - spec/javascripts/behaviors/quick_submit_spec.js | 1 - spec/javascripts/behaviors/requires_input_spec.js | 1 - spec/javascripts/boards/boards_store_spec.js.es6 | 1 - spec/javascripts/boards/issue_spec.js.es6 | 1 - spec/javascripts/boards/list_spec.js.es6 | 1 - spec/javascripts/bootstrap_linked_tabs_spec.js.es6 | 1 - spec/javascripts/build_spec.js.es6 | 1 - spec/javascripts/dashboard_spec.js.es6 | 1 - spec/javascripts/datetime_utility_spec.js.es6 | 1 - spec/javascripts/diff_comments_store_spec.js.es6 | 1 - spec/javascripts/environments/environment_actions_spec.js.es6 | 1 - spec/javascripts/environments/environment_external_url_spec.js.es6 | 1 - spec/javascripts/environments/environment_item_spec.js.es6 | 1 - spec/javascripts/environments/environment_rollback_spec.js.es6 | 1 - spec/javascripts/environments/environment_stop_spec.js.es6 | 1 - spec/javascripts/environments/environments_store_spec.js.es6 | 1 - spec/javascripts/extensions/array_spec.js.es6 | 1 - spec/javascripts/extensions/element_spec.js.es6 | 1 - spec/javascripts/extensions/jquery_spec.js | 1 - spec/javascripts/extensions/object_spec.js.es6 | 1 - spec/javascripts/gl_dropdown_spec.js.es6 | 1 - spec/javascripts/gl_field_errors_spec.js.es6 | 1 - spec/javascripts/graphs/stat_graph_contributors_graph_spec.js | 1 - spec/javascripts/graphs/stat_graph_contributors_util_spec.js | 1 - spec/javascripts/graphs/stat_graph_spec.js | 1 - spec/javascripts/header_spec.js | 1 - spec/javascripts/issuable_spec.js.es6 | 1 - spec/javascripts/issue_spec.js | 1 - spec/javascripts/labels_issue_sidebar_spec.js.es6 | 1 - spec/javascripts/lib/utils/common_utils_spec.js.es6 | 1 - spec/javascripts/line_highlighter_spec.js | 1 - spec/javascripts/merge_request_spec.js | 1 - spec/javascripts/merge_request_tabs_spec.js | 1 - spec/javascripts/merge_request_widget_spec.js | 1 - spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 | 1 - spec/javascripts/new_branch_spec.js | 1 - spec/javascripts/notes_spec.js | 1 - spec/javascripts/pipelines_spec.js.es6 | 1 - spec/javascripts/pretty_time_spec.js.es6 | 1 - spec/javascripts/project_title_spec.js | 1 - spec/javascripts/right_sidebar_spec.js | 1 - spec/javascripts/search_autocomplete_spec.js | 1 - spec/javascripts/shortcuts_issuable_spec.js | 1 - spec/javascripts/signin_tabs_memoizer_spec.js.es6 | 1 - spec/javascripts/smart_interval_spec.js.es6 | 1 - spec/javascripts/subbable_resource_spec.js.es6 | 1 - spec/javascripts/syntax_highlight_spec.js | 1 - spec/javascripts/u2f/authenticate_spec.js | 1 - spec/javascripts/u2f/register_spec.js | 1 - spec/javascripts/vue_common_components/commit_spec.js.es6 | 1 - spec/javascripts/zen_mode_spec.js | 1 - 56 files changed, 3 insertions(+), 57 deletions(-) diff --git a/config/karma.config.js b/config/karma.config.js index 0a3530b65ac..96d33490b37 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -6,13 +6,14 @@ var ROOT_PATH = path.resolve(__dirname, '..'); module.exports = function(config) { config.set({ basePath: ROOT_PATH, - frameworks: ['jquery-2.1.0', 'jasmine'], + frameworks: ['jasmine'], files: [ + 'spec/javascripts/spec_helper.js', 'spec/javascripts/**/*_spec.js?(.es6)', { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false }, ], preprocessors: { - 'spec/javascripts/**/*_spec.js?(.es6)': ['webpack'], + 'spec/javascripts/**/*.js?(.es6)': ['webpack'], }, webpack: webpackConfig, webpackMiddleware: { stats: 'errors-only' }, diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index dadee40f9b9..f4b6c9deae5 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -1,4 +1,3 @@ -require('./spec_helper'); require('lib/utils/text_utility'); require('abuse_reports'); diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index 6d625eeb973..feba184dbb0 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */ -require('./spec_helper'); window.Cookies = require('vendor/js.cookie'); require('vendor/jquery.endless-scroll.js'); require('pager'); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index c256da072c0..65efa3df90b 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */ /* global AwardsHandler */ -require('./spec_helper'); require('awards_handler'); window.Cookies = require('vendor/js.cookie'); require('./fixtures/emoji_menu'); diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index 603f1ae5f3e..e05793cf2e3 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -1,6 +1,5 @@ /* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, padded-blocks, max-len */ -require('../spec_helper'); require('behaviors/autosize'); (function() { diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index f7bf95daa6c..c7c6f6393a6 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,6 +1,5 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ -require('../spec_helper'); require('behaviors/quick_submit'); (function() { diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 40a113cded3..793405cd197 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -1,6 +1,5 @@ /* eslint-disable space-before-function-paren, no-var, padded-blocks */ -require('../spec_helper'); require('behaviors/requires_input'); (function() { diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index c8de9ad79c3..7817e7c1b92 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -6,7 +6,6 @@ /* global listObj */ /* global listObjDuplicate */ -require('../spec_helper'); require('jquery-ujs'); window.Cookies = require('vendor/js.cookie'); window.Vue = require('vue'); diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index 143315e4ac1..cf2485bff8d 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -2,7 +2,6 @@ /* global BoardService */ /* global ListIssue */ -require('../spec_helper'); require('jquery-ujs'); window.Cookies = require('vendor/js.cookie'); window.Vue = require('vue'); diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index ab04e7b5896..65094642b64 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -5,7 +5,6 @@ /* global List */ /* global listObj */ -require('../spec_helper'); require('jquery-ujs'); window.Cookies = require('vendor/js.cookie'); window.Vue = require('vue'); diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 index b6d223dcb80..f73bb5c6fed 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 @@ -1,4 +1,3 @@ -require('./spec_helper'); require('lib/utils/bootstrap_linked_tabs'); (() => { diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 1821c44fdfb..41a3e614cd1 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -2,7 +2,6 @@ /* global Build */ /* global Turbolinks */ -require('./spec_helper'); require('lib/utils/datetime_utility'); require('build'); require('breakpoints'); diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index 7e9e622970d..5fb101fd584 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable no-new, padded-blocks */ -require('./spec_helper'); require('sidebar'); window.Cookies = require('vendor/js.cookie'); require('lib/utils/text_utility'); diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 index d4f27d2691a..713e7742988 100644 --- a/spec/javascripts/datetime_utility_spec.js.es6 +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -1,4 +1,3 @@ -require('./spec_helper'); require('lib/utils/datetime_utility'); (() => { diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index 487f1ab8b5d..f27ba0f93f7 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -1,7 +1,6 @@ /* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ /* global CommentsStore */ -require('./spec_helper'); require('diff_notes/models/discussion'); require('diff_notes/models/note'); require('diff_notes/stores/comments'); diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index 8d3539b29a5..304c3dccd1e 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -1,4 +1,3 @@ -require('../spec_helper'); window.Vue = require('vue'); require('environments/components/environment_actions'); diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index 01264781a68..d50971583a1 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -1,4 +1,3 @@ -require('../spec_helper'); window.Vue = require('vue'); require('environments/components/environment_external_url'); diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index 56c718eb501..f6079afd617 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,4 +1,3 @@ -require('../spec_helper'); window.Vue = require('vue'); window.timeago = require('vendor/timeago'); require('environments/components/environment_item'); diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 5c378901608..87ffd76c152 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,4 +1,3 @@ -require('../spec_helper'); window.Vue = require('vue'); require('environments/components/environment_rollback'); diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index 96c738f42d6..b0b8f355fde 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,4 +1,3 @@ -require('../spec_helper'); window.Vue = require('vue'); require('environments/components/environment_stop'); diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 8c113b5679e..bcbe214eb05 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,6 +1,5 @@ /* global environmentsList */ -require('../spec_helper'); window.Vue = require('vue'); require('environments/stores/environments_store'); require('./mock_data'); diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6 index f35cda4cac7..5396e0eb639 100644 --- a/spec/javascripts/extensions/array_spec.js.es6 +++ b/spec/javascripts/extensions/array_spec.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable space-before-function-paren, no-var, padded-blocks */ -require('../spec_helper'); require('extensions/array'); (function() { diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6 index fddd7600cb9..49544ae8b5c 100644 --- a/spec/javascripts/extensions/element_spec.js.es6 +++ b/spec/javascripts/extensions/element_spec.js.es6 @@ -1,4 +1,3 @@ -require('../spec_helper'); require('extensions/element'); (() => { diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index 5f5bca4bc06..3163414b134 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -1,6 +1,5 @@ /* eslint-disable space-before-function-paren, no-var, padded-blocks */ -require('../spec_helper'); require('extensions/jquery'); (function() { diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6 index 25707be7bb4..77ffa1a35ae 100644 --- a/spec/javascripts/extensions/object_spec.js.es6 +++ b/spec/javascripts/extensions/object_spec.js.es6 @@ -1,4 +1,3 @@ -require('../spec_helper'); require('extensions/object'); describe('Object extensions', () => { diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 7b2b4771756..1f4d7a4eb07 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -1,7 +1,6 @@ /* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */ /* global Turbolinks */ -require('./spec_helper'); require('gl_dropdown'); require('lib/utils/common_utils'); require('lib/utils/type_utility'); diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index 763e1bb5685..53f7e576394 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */ -require('./spec_helper'); require('gl_field_errors'); ((global) => { diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index b4d278dfe88..a914eda90bb 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -3,7 +3,6 @@ /* global ContributorsGraph */ /* global ContributorsMasterGraph */ -require('../spec_helper'); require('graphs/stat_graph_contributors_graph'); describe("ContributorsGraph", function () { diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 8323fcff02d..4f82e1c46db 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,7 +1,6 @@ /* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */ /* global ContributorsStatGraphUtil */ -require('../spec_helper'); require('graphs/stat_graph_contributors_util'); describe("ContributorsStatGraphUtil", function () { diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index 66c8c16a40f..a017f35831d 100644 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,7 +1,6 @@ /* eslint-disable quotes, padded-blocks, semi */ /* global StatGraph */ -require('../spec_helper'); require('graphs/stat_graph'); describe("StatGraph", function () { diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index 4e93e33d432..570d0ab78cb 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -1,6 +1,5 @@ /* eslint-disable space-before-function-paren, padded-blocks, no-var */ -require('./spec_helper'); require('header'); require('lib/utils/text_utility'); diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 index 5d3269626af..eaf155f1606 100644 --- a/spec/javascripts/issuable_spec.js.es6 +++ b/spec/javascripts/issuable_spec.js.es6 @@ -1,7 +1,6 @@ /* global Issuable */ /* global Turbolinks */ -require('./spec_helper'); require('issuable'); (() => { diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 126b3682d5d..2938d69e94c 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */ /* global Issue */ -require('./spec_helper'); require('lib/utils/text_utility'); require('issue'); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index 885e975c32b..ac3b4e8e0f6 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -2,7 +2,6 @@ /* global IssuableContext */ /* global LabelsSelect */ -require('./spec_helper'); require('lib/utils/type_utility'); require('gl_dropdown'); require('select2'); diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 02aabf202d0..46aa0702bda 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -1,4 +1,3 @@ -require('../../spec_helper'); require('lib/utils/common_utils'); (() => { diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index fb549b846e0..afc51e6682a 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */ /* global LineHighlighter */ -require('./spec_helper'); require('line_highlighter'); (function() { diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index bbfa6aa67a5..5f98edc3bb0 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */ /* global MergeRequest */ -require('./spec_helper'); require('merge_request'); (function() { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index a8fa47cdd57..5f53334f44c 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,6 +1,5 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ -require('./spec_helper'); require('merge_request_tabs'); require('breakpoints'); require('lib/utils/common_utils'); diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index 0ec119c94b3..b29f5bad234 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,6 +1,5 @@ /* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */ -require('./spec_helper'); require('merge_request_widget'); require('lib/utils/datetime_utility'); diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 index 1faa1f88d70..32b80a4f4bd 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable no-new */ -require('./spec_helper'); require('flash'); require('mini_pipeline_graph_dropdown'); diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 8d8c8ec9b0d..40c6b6d3999 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, padded-blocks, max-len */ /* global NewBranchForm */ -require('./spec_helper'); require('jquery-ui/ui/autocomplete'); require('new_branch_form'); diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 75f5192a54e..0b2ac007495 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ /* global Notes */ -require('./spec_helper'); require('notes'); require('vendor/autosize'); require('gl_form'); diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6 index e51977ab720..1bee64b814f 100644 --- a/spec/javascripts/pipelines_spec.js.es6 +++ b/spec/javascripts/pipelines_spec.js.es6 @@ -1,4 +1,3 @@ -require('./spec_helper'); require('pipelines'); (() => { diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6 index 249aa4817e9..207d40983b4 100644 --- a/spec/javascripts/pretty_time_spec.js.es6 +++ b/spec/javascripts/pretty_time_spec.js.es6 @@ -1,4 +1,3 @@ -require('./spec_helper'); require('lib/utils/pretty_time'); (() => { diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index a438b01fbce..a774b978458 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, padded-blocks, max-len */ /* global Project */ -require('./spec_helper'); require('select2/select2.js'); require('lib/utils/type_utility'); require('gl_dropdown'); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 65730b42ea3..026ae04eb21 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */ /* global Sidebar */ -require('./spec_helper'); require('right_sidebar'); window.Cookies = require('vendor/js.cookie'); require('extensions/jquery.js'); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 4ac9117fd8a..8d7f48eabc5 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,6 +1,5 @@ /* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, padded-blocks, max-len */ -require('./spec_helper'); require('gl_dropdown'); require('search_autocomplete'); require('lib/utils/common_utils'); diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index e6605c46bfc..65c4f42e3b8 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,7 +1,6 @@ /* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */ /* global ShortcutsIssuable */ -require('./spec_helper'); require('shortcuts_issuable'); (function() { diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 index f7aa3e663f9..b34942d78d1 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 +++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 @@ -1,4 +1,3 @@ -require('./spec_helper'); require('signin_tabs_memoizer'); ((global) => { diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index 23cf8689585..3075965d7f9 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -1,4 +1,3 @@ -require('./spec_helper'); require('smart_interval'); (() => { diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6 index a70a1419792..1434ae8364d 100644 --- a/spec/javascripts/subbable_resource_spec.js.es6 +++ b/spec/javascripts/subbable_resource_spec.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */ -require('./spec_helper'); require('subbable_resource'); /* diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index c06339fa709..b5e869e2169 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -1,6 +1,5 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes, padded-blocks */ -require('./spec_helper'); require('syntax_highlight'); (function() { diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index a14f0e3c448..22cea407943 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -2,7 +2,6 @@ /* global MockU2FDevice */ /* global U2FAuthenticate */ -require('../spec_helper'); require('u2f/authenticate'); require('u2f/util'); require('u2f/error'); diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 157c8796fd5..17b3732975b 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -2,7 +2,6 @@ /* global MockU2FDevice */ /* global U2FRegister */ -require('../spec_helper'); require('u2f/register'); require('u2f/util'); require('u2f/error'); diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index 28ffc005001..12d8c411847 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -1,4 +1,3 @@ -require('../spec_helper'); require('vue_common_component/commit'); describe('Commit component', () => { diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 2c790b193b0..7fe2ab68c75 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -3,7 +3,6 @@ /* global Mousetrap */ /* global ZenMode */ -require('./spec_helper'); require('zen_mode'); (function() { -- cgit v1.2.1 From b0341c14d0657b41203b7e5d6d6cbeb64d67b387 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 6 Jan 2017 17:30:54 -0600 Subject: move additional libraries into spec_helper --- spec/javascripts/activities_spec.js.es6 | 1 - spec/javascripts/awards_handler_spec.js | 1 - spec/javascripts/boards/boards_store_spec.js.es6 | 4 ---- spec/javascripts/boards/issue_spec.js.es6 | 4 ---- spec/javascripts/boards/list_spec.js.es6 | 4 ---- spec/javascripts/dashboard_spec.js.es6 | 1 - spec/javascripts/environments/environment_actions_spec.js.es6 | 1 - spec/javascripts/environments/environment_external_url_spec.js.es6 | 1 - spec/javascripts/environments/environment_item_spec.js.es6 | 1 - spec/javascripts/environments/environment_rollback_spec.js.es6 | 1 - spec/javascripts/environments/environment_stop_spec.js.es6 | 1 - spec/javascripts/environments/environments_store_spec.js.es6 | 1 - spec/javascripts/right_sidebar_spec.js | 1 - spec/javascripts/spec_helper.js | 4 ++++ 14 files changed, 4 insertions(+), 22 deletions(-) diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index feba184dbb0..cf57ca34a79 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */ -window.Cookies = require('vendor/js.cookie'); require('vendor/jquery.endless-scroll.js'); require('pager'); require('activities'); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 65efa3df90b..6950e7a2ce4 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -2,7 +2,6 @@ /* global AwardsHandler */ require('awards_handler'); -window.Cookies = require('vendor/js.cookie'); require('./fixtures/emoji_menu'); (function() { diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index 7817e7c1b92..a94f107f67c 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -6,10 +6,6 @@ /* global listObj */ /* global listObjDuplicate */ -require('jquery-ujs'); -window.Cookies = require('vendor/js.cookie'); -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); require('lib/utils/url_utility'); require('boards/models/issue'); require('boards/models/label'); diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index cf2485bff8d..2f38bea7d48 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -2,10 +2,6 @@ /* global BoardService */ /* global ListIssue */ -require('jquery-ujs'); -window.Cookies = require('vendor/js.cookie'); -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); require('lib/utils/url_utility'); require('boards/models/issue'); require('boards/models/label'); diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 65094642b64..edd472573de 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -5,10 +5,6 @@ /* global List */ /* global listObj */ -require('jquery-ujs'); -window.Cookies = require('vendor/js.cookie'); -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); require('lib/utils/url_utility'); require('boards/models/issue'); require('boards/models/label'); diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index 5fb101fd584..b9e819aa218 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -1,7 +1,6 @@ /* eslint-disable no-new, padded-blocks */ require('sidebar'); -window.Cookies = require('vendor/js.cookie'); require('lib/utils/text_utility'); ((global) => { diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index 304c3dccd1e..c02c2e10b9a 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -1,4 +1,3 @@ -window.Vue = require('vue'); require('environments/components/environment_actions'); describe('Actions Component', () => { diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index d50971583a1..5270ebef0ae 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -1,4 +1,3 @@ -window.Vue = require('vue'); require('environments/components/environment_external_url'); describe('External URL Component', () => { diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index f6079afd617..400850db028 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,4 +1,3 @@ -window.Vue = require('vue'); window.timeago = require('vendor/timeago'); require('environments/components/environment_item'); diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 87ffd76c152..96d80d59b9a 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,4 +1,3 @@ -window.Vue = require('vue'); require('environments/components/environment_rollback'); describe('Rollback Component', () => { diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index b0b8f355fde..a243b967425 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,4 +1,3 @@ -window.Vue = require('vue'); require('environments/components/environment_stop'); describe('Stop Component', () => { diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index bcbe214eb05..090179ce873 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,6 +1,5 @@ /* global environmentsList */ -window.Vue = require('vue'); require('environments/stores/environments_store'); require('./mock_data'); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 026ae04eb21..db6b7244135 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -2,7 +2,6 @@ /* global Sidebar */ require('right_sidebar'); -window.Cookies = require('vendor/js.cookie'); require('extensions/jquery.js'); (function() { diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index 64f1ca4b80d..b55f08e3311 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -3,6 +3,10 @@ require('jasmine-jquery'); // include common libraries window.$ = window.jQuery = require('jquery'); window._ = require('underscore'); +window.Cookies = require('vendor/js.cookie'); +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('jquery-ujs'); require('vendor/turbolinks'); require('vendor/jquery.turbolinks'); require('bootstrap/js/affix'); -- cgit v1.2.1 From a8078d629e50ec1ea0e562e24e7bd7ea8931d896 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 6 Jan 2017 18:19:42 -0600 Subject: migrate all tests into a single webpack bundle --- config/karma.config.js | 3 +-- spec/javascripts/.eslintrc | 5 +++-- spec/javascripts/spec_helper.js | 31 ------------------------------ spec/javascripts/test_bundle.js | 42 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 46 insertions(+), 35 deletions(-) delete mode 100644 spec/javascripts/spec_helper.js create mode 100644 spec/javascripts/test_bundle.js diff --git a/config/karma.config.js b/config/karma.config.js index 96d33490b37..b317c3f00b2 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -8,8 +8,7 @@ module.exports = function(config) { basePath: ROOT_PATH, frameworks: ['jasmine'], files: [ - 'spec/javascripts/spec_helper.js', - 'spec/javascripts/**/*_spec.js?(.es6)', + 'spec/javascripts/test_bundle.js', { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false }, ], preprocessors: { diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc index dcbcd014dc3..b3d191e15ab 100644 --- a/spec/javascripts/.eslintrc +++ b/spec/javascripts/.eslintrc @@ -22,7 +22,8 @@ }, "plugins": ["jasmine"], "rules": { - "prefer-arrow-callback": 0, - "func-names": 0 + "func-names": 0, + "no-console": 0, + "prefer-arrow-callback": 0 } } diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js deleted file mode 100644 index b55f08e3311..00000000000 --- a/spec/javascripts/spec_helper.js +++ /dev/null @@ -1,31 +0,0 @@ -require('jasmine-jquery'); - -// include common libraries -window.$ = window.jQuery = require('jquery'); -window._ = require('underscore'); -window.Cookies = require('vendor/js.cookie'); -window.Vue = require('vue'); -window.Vue.use(require('vue-resource')); -require('jquery-ujs'); -require('vendor/turbolinks'); -require('vendor/jquery.turbolinks'); -require('bootstrap/js/affix'); -require('bootstrap/js/alert'); -require('bootstrap/js/button'); -require('bootstrap/js/collapse'); -require('bootstrap/js/dropdown'); -require('bootstrap/js/modal'); -require('bootstrap/js/scrollspy'); -require('bootstrap/js/tab'); -require('bootstrap/js/transition'); -require('bootstrap/js/tooltip'); -require('bootstrap/js/popover'); - -// configure jasmine -jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; -jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; - -// stub expected globals -window.gl = window.gl || {}; -window.gl.TEST_HOST = 'http://test.host'; -window.gon = window.gon || {}; diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js new file mode 100644 index 00000000000..cbe8abbbc08 --- /dev/null +++ b/spec/javascripts/test_bundle.js @@ -0,0 +1,42 @@ +// enable test fixtures +require('jasmine-jquery'); + +jasmine.getFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; +jasmine.getJSONFixtures().fixturesPath = 'base/spec/javascripts/fixtures'; + +// include common libraries +window.$ = window.jQuery = require('jquery'); +window._ = require('underscore'); +window.Cookies = require('vendor/js.cookie'); +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('jquery-ujs'); +require('vendor/turbolinks'); +require('vendor/jquery.turbolinks'); +require('bootstrap/js/affix'); +require('bootstrap/js/alert'); +require('bootstrap/js/button'); +require('bootstrap/js/collapse'); +require('bootstrap/js/dropdown'); +require('bootstrap/js/modal'); +require('bootstrap/js/scrollspy'); +require('bootstrap/js/tab'); +require('bootstrap/js/transition'); +require('bootstrap/js/tooltip'); +require('bootstrap/js/popover'); + +// stub expected globals +window.gl = window.gl || {}; +window.gl.TEST_HOST = 'http://test.host'; +window.gon = window.gon || {}; + +// render all of our tests +const testsContext = require.context('.', true, /_spec$/); +testsContext.keys().forEach(function (path) { + try { + testsContext(path); + } catch (err) { + console.error('[ERROR] WITH SPEC FILE: ', path); + console.error(err); + } +}); -- cgit v1.2.1 From 8d2099cb53e933e94ebc17e9c186c0f4ece107cb Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 9 Jan 2017 17:23:54 -0600 Subject: use ~ to reference non-local scripts instead of resolve.root --- config/webpack.config.js | 6 ++---- spec/javascripts/abuse_reports_spec.js.es6 | 4 ++-- spec/javascripts/activities_spec.js.es6 | 4 ++-- spec/javascripts/awards_handler_spec.js | 2 +- spec/javascripts/behaviors/autosize_spec.js | 2 +- spec/javascripts/behaviors/quick_submit_spec.js | 2 +- spec/javascripts/behaviors/requires_input_spec.js | 2 +- spec/javascripts/boards/boards_store_spec.js.es6 | 14 +++++++------- spec/javascripts/boards/issue_spec.js.es6 | 14 +++++++------- spec/javascripts/boards/list_spec.js.es6 | 14 +++++++------- spec/javascripts/bootstrap_linked_tabs_spec.js.es6 | 2 +- spec/javascripts/build_spec.js.es6 | 6 +++--- spec/javascripts/dashboard_spec.js.es6 | 4 ++-- spec/javascripts/datetime_utility_spec.js.es6 | 2 +- spec/javascripts/diff_comments_store_spec.js.es6 | 6 +++--- .../environments/environment_actions_spec.js.es6 | 2 +- .../environments/environment_external_url_spec.js.es6 | 2 +- spec/javascripts/environments/environment_item_spec.js.es6 | 2 +- .../environments/environment_rollback_spec.js.es6 | 2 +- spec/javascripts/environments/environment_stop_spec.js.es6 | 2 +- .../environments/environments_store_spec.js.es6 | 2 +- spec/javascripts/extensions/array_spec.js.es6 | 2 +- spec/javascripts/extensions/element_spec.js.es6 | 2 +- spec/javascripts/extensions/jquery_spec.js | 2 +- spec/javascripts/extensions/object_spec.js.es6 | 2 +- spec/javascripts/gl_dropdown_spec.js.es6 | 6 +++--- spec/javascripts/gl_field_errors_spec.js.es6 | 2 +- .../graphs/stat_graph_contributors_graph_spec.js | 2 +- .../graphs/stat_graph_contributors_util_spec.js | 2 +- spec/javascripts/graphs/stat_graph_spec.js | 2 +- spec/javascripts/header_spec.js | 4 ++-- spec/javascripts/issuable_spec.js.es6 | 2 +- spec/javascripts/issue_spec.js | 4 ++-- spec/javascripts/labels_issue_sidebar_spec.js.es6 | 14 +++++++------- spec/javascripts/lib/utils/common_utils_spec.js.es6 | 2 +- spec/javascripts/line_highlighter_spec.js | 2 +- spec/javascripts/merge_request_spec.js | 2 +- spec/javascripts/merge_request_tabs_spec.js | 6 +++--- spec/javascripts/merge_request_widget_spec.js | 4 ++-- spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 | 4 ++-- spec/javascripts/new_branch_spec.js | 2 +- spec/javascripts/notes_spec.js | 6 +++--- spec/javascripts/pipelines_spec.js.es6 | 2 +- spec/javascripts/pretty_time_spec.js.es6 | 2 +- spec/javascripts/project_title_spec.js | 10 +++++----- spec/javascripts/right_sidebar_spec.js | 4 ++-- spec/javascripts/search_autocomplete_spec.js | 8 ++++---- spec/javascripts/shortcuts_issuable_spec.js | 2 +- spec/javascripts/signin_tabs_memoizer_spec.js.es6 | 2 +- spec/javascripts/smart_interval_spec.js.es6 | 2 +- spec/javascripts/subbable_resource_spec.js.es6 | 2 +- spec/javascripts/syntax_highlight_spec.js | 2 +- spec/javascripts/u2f/authenticate_spec.js | 6 +++--- spec/javascripts/u2f/register_spec.js | 6 +++--- spec/javascripts/vue_common_components/commit_spec.js.es6 | 2 +- spec/javascripts/zen_mode_spec.js | 2 +- 56 files changed, 110 insertions(+), 112 deletions(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 8dd9cf5b960..4060228c94b 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -82,15 +82,13 @@ var config = { resolve: { extensions: ['', '.js', '.es6', '.js.es6'], alias: { + '~': path.join(ROOT_PATH, 'app/assets/javascripts'), 'bootstrap/js': 'bootstrap-sass/assets/javascripts/bootstrap', 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vue$': 'vue/dist/vue.js', 'vue-resource$': 'vue-resource/dist/vue-resource.js' - }, - root: [ - path.join(ROOT_PATH, 'app/assets/javascripts'), - ], + } } } diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index f4b6c9deae5..6e23a7a0b56 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -1,5 +1,5 @@ -require('lib/utils/text_utility'); -require('abuse_reports'); +require('~/lib/utils/text_utility'); +require('~/abuse_reports'); ((global) => { describe('Abuse Reports', () => { diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index cf57ca34a79..aba16a03ce2 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,8 +1,8 @@ /* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */ require('vendor/jquery.endless-scroll.js'); -require('pager'); -require('activities'); +require('~/pager'); +require('~/activities'); (() => { window.gon || (window.gon = {}); diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 6950e7a2ce4..672f6f33ad3 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */ /* global AwardsHandler */ -require('awards_handler'); +require('~/awards_handler'); require('./fixtures/emoji_menu'); (function() { diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index e05793cf2e3..3b29579e70e 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, comma-dangle, no-return-assign, padded-blocks, max-len */ -require('behaviors/autosize'); +require('~/behaviors/autosize'); (function() { describe('Autosize behavior', function() { diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index c7c6f6393a6..247eb5f70ea 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ -require('behaviors/quick_submit'); +require('~/behaviors/quick_submit'); (function() { describe('Quick Submit behavior', function() { diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 793405cd197..fd098196e7d 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, padded-blocks */ -require('behaviors/requires_input'); +require('~/behaviors/requires_input'); (function() { describe('requiresInput', function() { diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index a94f107f67c..8f8f6d22066 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -6,13 +6,13 @@ /* global listObj */ /* global listObjDuplicate */ -require('lib/utils/url_utility'); -require('boards/models/issue'); -require('boards/models/label'); -require('boards/models/list'); -require('boards/models/user'); -require('boards/services/board_service'); -require('boards/stores/boards_store'); +require('~/lib/utils/url_utility'); +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/services/board_service'); +require('~/boards/stores/boards_store'); require('./mock_data'); describe('Store', () => { diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index 2f38bea7d48..5514f34c828 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -2,13 +2,13 @@ /* global BoardService */ /* global ListIssue */ -require('lib/utils/url_utility'); -require('boards/models/issue'); -require('boards/models/label'); -require('boards/models/list'); -require('boards/models/user'); -require('boards/services/board_service'); -require('boards/stores/boards_store'); +require('~/lib/utils/url_utility'); +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/services/board_service'); +require('~/boards/stores/boards_store'); require('./mock_data'); describe('Issue model', () => { diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index edd472573de..31b49e3e27a 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -5,13 +5,13 @@ /* global List */ /* global listObj */ -require('lib/utils/url_utility'); -require('boards/models/issue'); -require('boards/models/label'); -require('boards/models/list'); -require('boards/models/user'); -require('boards/services/board_service'); -require('boards/stores/boards_store'); +require('~/lib/utils/url_utility'); +require('~/boards/models/issue'); +require('~/boards/models/label'); +require('~/boards/models/list'); +require('~/boards/models/user'); +require('~/boards/services/board_service'); +require('~/boards/stores/boards_store'); require('./mock_data'); describe('List model', () => { diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 index f73bb5c6fed..bb2545cddf9 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 @@ -1,4 +1,4 @@ -require('lib/utils/bootstrap_linked_tabs'); +require('~/lib/utils/bootstrap_linked_tabs'); (() => { describe('Linked Tabs', () => { diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 41a3e614cd1..d2a093df146 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -2,9 +2,9 @@ /* global Build */ /* global Turbolinks */ -require('lib/utils/datetime_utility'); -require('build'); -require('breakpoints'); +require('~/lib/utils/datetime_utility'); +require('~/build'); +require('~/breakpoints'); require('vendor/jquery.nicescroll'); describe('Build', () => { diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index b9e819aa218..501380693d4 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable no-new, padded-blocks */ -require('sidebar'); -require('lib/utils/text_utility'); +require('~/sidebar'); +require('~/lib/utils/text_utility'); ((global) => { describe('Dashboard', () => { diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 index 713e7742988..d5eec10be42 100644 --- a/spec/javascripts/datetime_utility_spec.js.es6 +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -1,4 +1,4 @@ -require('lib/utils/datetime_utility'); +require('~/lib/utils/datetime_utility'); (() => { describe('Date time utils', () => { diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index f27ba0f93f7..cf2f17de5ee 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -1,9 +1,9 @@ /* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ /* global CommentsStore */ -require('diff_notes/models/discussion'); -require('diff_notes/models/note'); -require('diff_notes/stores/comments'); +require('~/diff_notes/models/discussion'); +require('~/diff_notes/models/note'); +require('~/diff_notes/stores/comments'); (() => { function createDiscussion(noteId = 1, resolved = true) { diff --git a/spec/javascripts/environments/environment_actions_spec.js.es6 b/spec/javascripts/environments/environment_actions_spec.js.es6 index c02c2e10b9a..b1838045a06 100644 --- a/spec/javascripts/environments/environment_actions_spec.js.es6 +++ b/spec/javascripts/environments/environment_actions_spec.js.es6 @@ -1,4 +1,4 @@ -require('environments/components/environment_actions'); +require('~/environments/components/environment_actions'); describe('Actions Component', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_external_url_spec.js.es6 b/spec/javascripts/environments/environment_external_url_spec.js.es6 index 5270ebef0ae..a6a587e69f5 100644 --- a/spec/javascripts/environments/environment_external_url_spec.js.es6 +++ b/spec/javascripts/environments/environment_external_url_spec.js.es6 @@ -1,4 +1,4 @@ -require('environments/components/environment_external_url'); +require('~/environments/components/environment_external_url'); describe('External URL Component', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index 400850db028..9858f346c83 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -1,5 +1,5 @@ window.timeago = require('vendor/timeago'); -require('environments/components/environment_item'); +require('~/environments/components/environment_item'); describe('Environment item', () => { preloadFixtures('static/environments/table.html.raw'); diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6 index 96d80d59b9a..8c7e1e912b4 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js.es6 +++ b/spec/javascripts/environments/environment_rollback_spec.js.es6 @@ -1,4 +1,4 @@ -require('environments/components/environment_rollback'); +require('~/environments/components/environment_rollback'); describe('Rollback Component', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environment_stop_spec.js.es6 b/spec/javascripts/environments/environment_stop_spec.js.es6 index a243b967425..2dfce5ba824 100644 --- a/spec/javascripts/environments/environment_stop_spec.js.es6 +++ b/spec/javascripts/environments/environment_stop_spec.js.es6 @@ -1,4 +1,4 @@ -require('environments/components/environment_stop'); +require('~/environments/components/environment_stop'); describe('Stop Component', () => { preloadFixtures('static/environments/element.html.raw'); diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 090179ce873..9a8300d3832 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,6 +1,6 @@ /* global environmentsList */ -require('environments/stores/environments_store'); +require('~/environments/stores/environments_store'); require('./mock_data'); (() => { diff --git a/spec/javascripts/extensions/array_spec.js.es6 b/spec/javascripts/extensions/array_spec.js.es6 index 5396e0eb639..75372266808 100644 --- a/spec/javascripts/extensions/array_spec.js.es6 +++ b/spec/javascripts/extensions/array_spec.js.es6 @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, padded-blocks */ -require('extensions/array'); +require('~/extensions/array'); (function() { describe('Array extensions', function() { diff --git a/spec/javascripts/extensions/element_spec.js.es6 b/spec/javascripts/extensions/element_spec.js.es6 index 49544ae8b5c..2d8a128ed33 100644 --- a/spec/javascripts/extensions/element_spec.js.es6 +++ b/spec/javascripts/extensions/element_spec.js.es6 @@ -1,4 +1,4 @@ -require('extensions/element'); +require('~/extensions/element'); (() => { describe('Element extensions', function () { diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js index 3163414b134..298832f6985 100644 --- a/spec/javascripts/extensions/jquery_spec.js +++ b/spec/javascripts/extensions/jquery_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, padded-blocks */ -require('extensions/jquery'); +require('~/extensions/jquery'); (function() { describe('jQuery extensions', function() { diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6 index 77ffa1a35ae..2467ed78459 100644 --- a/spec/javascripts/extensions/object_spec.js.es6 +++ b/spec/javascripts/extensions/object_spec.js.es6 @@ -1,4 +1,4 @@ -require('extensions/object'); +require('~/extensions/object'); describe('Object extensions', () => { describe('assign', () => { diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 1f4d7a4eb07..b079aae13c3 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -1,9 +1,9 @@ /* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */ /* global Turbolinks */ -require('gl_dropdown'); -require('lib/utils/common_utils'); -require('lib/utils/type_utility'); +require('~/gl_dropdown'); +require('~/lib/utils/common_utils'); +require('~/lib/utils/type_utility'); (() => { const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index 53f7e576394..51ba59df671 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */ -require('gl_field_errors'); +require('~/gl_field_errors'); ((global) => { preloadFixtures('static/gl_field_errors.html.raw'); diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index a914eda90bb..88aaaa0471b 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -3,7 +3,7 @@ /* global ContributorsGraph */ /* global ContributorsMasterGraph */ -require('graphs/stat_graph_contributors_graph'); +require('~/graphs/stat_graph_contributors_graph'); describe("ContributorsGraph", function () { describe("#set_x_domain", function () { diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 4f82e1c46db..671b0ae391c 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,7 +1,7 @@ /* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */ /* global ContributorsStatGraphUtil */ -require('graphs/stat_graph_contributors_util'); +require('~/graphs/stat_graph_contributors_util'); describe("ContributorsStatGraphUtil", function () { diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index a017f35831d..5b3b7c9222a 100644 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,7 +1,7 @@ /* eslint-disable quotes, padded-blocks, semi */ /* global StatGraph */ -require('graphs/stat_graph'); +require('~/graphs/stat_graph'); describe("StatGraph", function () { diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js index 570d0ab78cb..a281502b6ba 100644 --- a/spec/javascripts/header_spec.js +++ b/spec/javascripts/header_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, padded-blocks, no-var */ -require('header'); -require('lib/utils/text_utility'); +require('~/header'); +require('~/lib/utils/text_utility'); (function() { diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 index eaf155f1606..d846c242b1e 100644 --- a/spec/javascripts/issuable_spec.js.es6 +++ b/spec/javascripts/issuable_spec.js.es6 @@ -1,7 +1,7 @@ /* global Issuable */ /* global Turbolinks */ -require('issuable'); +require('~/issuable'); (() => { const BASE_URL = '/user/project/issues?scope=all&state=closed'; diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 2938d69e94c..d1d6d5e22cb 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,8 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */ /* global Issue */ -require('lib/utils/text_utility'); -require('issue'); +require('~/lib/utils/text_utility'); +require('~/issue'); (function() { var INVALID_URL = 'http://goesnowhere.nothing/whereami'; diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index ac3b4e8e0f6..61ccef42cd8 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -2,15 +2,15 @@ /* global IssuableContext */ /* global LabelsSelect */ -require('lib/utils/type_utility'); -require('gl_dropdown'); +require('~/lib/utils/type_utility'); +require('~/gl_dropdown'); require('select2'); require('vendor/jquery.nicescroll'); -require('api'); -require('create_label'); -require('issuable_context'); -require('users_select'); -require('labels_select'); +require('~/api'); +require('~/create_label'); +require('~/issuable_context'); +require('~/users_select'); +require('~/labels_select'); (() => { let saveLabelCount = 0; diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 46aa0702bda..58fb54077f5 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -1,4 +1,4 @@ -require('lib/utils/common_utils'); +require('~/lib/utils/common_utils'); (() => { describe('common_utils', () => { diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index afc51e6682a..be80e06af53 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */ /* global LineHighlighter */ -require('line_highlighter'); +require('~/line_highlighter'); (function() { describe('LineHighlighter', function() { diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index 5f98edc3bb0..f87e87f4204 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */ /* global MergeRequest */ -require('merge_request'); +require('~/merge_request'); (function() { describe('MergeRequest', function() { diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 5f53334f44c..114b7cca61e 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -1,8 +1,8 @@ /* eslint-disable no-var, comma-dangle, object-shorthand */ -require('merge_request_tabs'); -require('breakpoints'); -require('lib/utils/common_utils'); +require('~/merge_request_tabs'); +require('~/breakpoints'); +require('~/lib/utils/common_utils'); require('vendor/jquery.scrollTo'); (function () { diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index b29f5bad234..c09cea28696 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, indent, quote-props, no-var, padded-blocks, max-len */ -require('merge_request_widget'); -require('lib/utils/datetime_utility'); +require('~/merge_request_widget'); +require('~/lib/utils/datetime_utility'); (function() { describe('MergeRequestWidget', function() { diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 index 32b80a4f4bd..a6994f6edf4 100644 --- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 +++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable no-new */ -require('flash'); -require('mini_pipeline_graph_dropdown'); +require('~/flash'); +require('~/mini_pipeline_graph_dropdown'); (() => { describe('Mini Pipeline Graph Dropdown', () => { diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 40c6b6d3999..0fdfa4d037b 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -2,7 +2,7 @@ /* global NewBranchForm */ require('jquery-ui/ui/autocomplete'); -require('new_branch_form'); +require('~/new_branch_form'); (function() { describe('Branch', function() { diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 0b2ac007495..295b44a3e74 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,10 +1,10 @@ /* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ /* global Notes */ -require('notes'); +require('~/notes'); require('vendor/autosize'); -require('gl_form'); -require('lib/utils/text_utility'); +require('~/gl_form'); +require('~/lib/utils/text_utility'); (function() { window.gon || (window.gon = {}); diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6 index 1bee64b814f..6120ca5543d 100644 --- a/spec/javascripts/pipelines_spec.js.es6 +++ b/spec/javascripts/pipelines_spec.js.es6 @@ -1,4 +1,4 @@ -require('pipelines'); +require('~/pipelines'); (() => { describe('Pipelines', () => { diff --git a/spec/javascripts/pretty_time_spec.js.es6 b/spec/javascripts/pretty_time_spec.js.es6 index 207d40983b4..fe5317e05b1 100644 --- a/spec/javascripts/pretty_time_spec.js.es6 +++ b/spec/javascripts/pretty_time_spec.js.es6 @@ -1,4 +1,4 @@ -require('lib/utils/pretty_time'); +require('~/lib/utils/pretty_time'); (() => { const PrettyTime = gl.PrettyTime; diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index a774b978458..ab0808bab18 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -2,11 +2,11 @@ /* global Project */ require('select2/select2.js'); -require('lib/utils/type_utility'); -require('gl_dropdown'); -require('api'); -require('project_select'); -require('project'); +require('~/lib/utils/type_utility'); +require('~/gl_dropdown'); +require('~/api'); +require('~/project_select'); +require('~/project'); (function() { window.gon || (window.gon = {}); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index db6b7244135..2a711b15133 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,8 +1,8 @@ /* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */ /* global Sidebar */ -require('right_sidebar'); -require('extensions/jquery.js'); +require('~/right_sidebar'); +require('~/extensions/jquery.js'); (function() { var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 8d7f48eabc5..08d9b775940 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -1,9 +1,9 @@ /* eslint-disable space-before-function-paren, max-len, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, consistent-return, no-param-reassign, default-case, no-return-assign, comma-dangle, object-shorthand, prefer-template, quotes, new-parens, vars-on-top, new-cap, padded-blocks, max-len */ -require('gl_dropdown'); -require('search_autocomplete'); -require('lib/utils/common_utils'); -require('lib/utils/type_utility'); +require('~/gl_dropdown'); +require('~/search_autocomplete'); +require('~/lib/utils/common_utils'); +require('~/lib/utils/type_utility'); require('vendor/fuzzaldrin-plus'); (function() { diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 65c4f42e3b8..7c577cc1b38 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,7 +1,7 @@ /* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */ /* global ShortcutsIssuable */ -require('shortcuts_issuable'); +require('~/shortcuts_issuable'); (function() { describe('ShortcutsIssuable', function() { diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 index b34942d78d1..d83d9a57b42 100644 --- a/spec/javascripts/signin_tabs_memoizer_spec.js.es6 +++ b/spec/javascripts/signin_tabs_memoizer_spec.js.es6 @@ -1,4 +1,4 @@ -require('signin_tabs_memoizer'); +require('~/signin_tabs_memoizer'); ((global) => { describe('SigninTabsMemoizer', () => { diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index 3075965d7f9..0c8051810cc 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -1,4 +1,4 @@ -require('smart_interval'); +require('~/smart_interval'); (() => { const DEFAULT_MAX_INTERVAL = 100; diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6 index 1434ae8364d..ef1b32c2d19 100644 --- a/spec/javascripts/subbable_resource_spec.js.es6 +++ b/spec/javascripts/subbable_resource_spec.js.es6 @@ -1,6 +1,6 @@ /* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */ -require('subbable_resource'); +require('~/subbable_resource'); /* * Test that each rest verb calls the publish and subscribe function and passes the correct value back diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js index b5e869e2169..6c953f1b71c 100644 --- a/spec/javascripts/syntax_highlight_spec.js +++ b/spec/javascripts/syntax_highlight_spec.js @@ -1,6 +1,6 @@ /* eslint-disable space-before-function-paren, no-var, no-return-assign, quotes, padded-blocks */ -require('syntax_highlight'); +require('~/syntax_highlight'); (function() { describe('Syntax Highlighter', function() { diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 22cea407943..e5948131744 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -2,9 +2,9 @@ /* global MockU2FDevice */ /* global U2FAuthenticate */ -require('u2f/authenticate'); -require('u2f/util'); -require('u2f/error'); +require('~/u2f/authenticate'); +require('~/u2f/util'); +require('~/u2f/error'); require('vendor/u2f'); require('./mock_u2f_device'); diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 17b3732975b..50522ff2391 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -2,9 +2,9 @@ /* global MockU2FDevice */ /* global U2FRegister */ -require('u2f/register'); -require('u2f/util'); -require('u2f/error'); +require('~/u2f/register'); +require('~/u2f/util'); +require('~/u2f/error'); require('vendor/u2f'); require('./mock_u2f_device'); diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index 12d8c411847..bbd914de4ea 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -1,4 +1,4 @@ -require('vue_common_component/commit'); +require('~/vue_common_component/commit'); describe('Commit component', () => { let props; diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 7fe2ab68c75..7a68356376f 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -3,7 +3,7 @@ /* global Mousetrap */ /* global ZenMode */ -require('zen_mode'); +require('~/zen_mode'); (function() { var enterZen, escapeKeydown, exitZen; -- cgit v1.2.1 From 970155a3f47ee90d6bfab8d0b5b63f103b832cbd Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 7 Jan 2017 01:27:37 -0600 Subject: fix globals within boards_bundle mock data --- spec/javascripts/boards/mock_data.js.es6 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index 8d3e2237fda..7a399b307ad 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -56,3 +56,8 @@ const boardsMockInterceptor = (request, next) => { status: 200 })); }; + +window.listObj = listObj; +window.listObjDuplicate = listObjDuplicate; +window.BoardsMockData = BoardsMockData; +window.boardsMockInterceptor = boardsMockInterceptor; -- cgit v1.2.1 From 31af0fbaf83c5a4e4dbcaabf0819084b13d07233 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 7 Jan 2017 01:36:19 -0600 Subject: improve sourcemap generation --- config/karma.config.js | 2 +- config/webpack.config.js | 3 ++- package.json | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/config/karma.config.js b/config/karma.config.js index b317c3f00b2..545390a5a1e 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -12,7 +12,7 @@ module.exports = function(config) { { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false }, ], preprocessors: { - 'spec/javascripts/**/*.js?(.es6)': ['webpack'], + 'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap'], }, webpack: webpackConfig, webpackMiddleware: { stats: 'errors-only' }, diff --git a/config/webpack.config.js b/config/webpack.config.js index 4060228c94b..f363a9fc4a9 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -40,7 +40,7 @@ var config = { filename: IS_PRODUCTION ? '[name]-[chunkhash].js' : '[name].js' }, - devtool: 'source-map', + devtool: 'inline-source-map', module: { loaders: [ @@ -93,6 +93,7 @@ var config = { } if (IS_PRODUCTION) { + config.devtool = 'source-map'; config.plugins.push( new webpack.NoErrorsPlugin(), new webpack.optimize.UglifyJsPlugin({ diff --git a/package.json b/package.json index ef16a371f91..4ce08487c3d 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "underscore": "1.8.3", "vue": "2.0.3", "vue-resource": "0.9.3", - "webpack": "^1.13.2", + "webpack": "^1.14.0", "webpack-dev-server": "^1.16.2" }, "devDependencies": { @@ -41,6 +41,7 @@ "jasmine-jquery": "^2.1.1", "karma": "^1.3.0", "karma-jasmine": "^1.1.0", + "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.8.0" } } -- cgit v1.2.1 From 80917acd7ef8ce43fce0e657f9538ae79c537729 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 7 Jan 2017 01:38:36 -0600 Subject: prevent karma from reloading on save until webpack is ready --- config/karma.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/karma.config.js b/config/karma.config.js index 545390a5a1e..5c3d454addf 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -8,7 +8,7 @@ module.exports = function(config) { basePath: ROOT_PATH, frameworks: ['jasmine'], files: [ - 'spec/javascripts/test_bundle.js', + { pattern: 'spec/javascripts/test_bundle.js', watched: false }, { pattern: 'spec/javascripts/fixtures/**/*@(.json|.html|.html.raw)', included: false }, ], preprocessors: { -- cgit v1.2.1 From 8dc13eca9a4865bbd2afb5ef2ca9697a2324a88e Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 7 Jan 2017 01:46:55 -0600 Subject: fix failing tests --- app/assets/javascripts/gl_dropdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index bb516b3d2df..a669d8bd4e2 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -251,7 +251,7 @@ _this.fullData = data; _this.parseData(_this.fullData); _this.focusTextInput(); - if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val().trim() !== '') { + if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { return _this.filter.input.trigger('input'); } }; -- cgit v1.2.1 From 19b94e715d7a0409d2f39b2efae306d0c202ee66 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 7 Jan 2017 01:47:45 -0600 Subject: fix globals within environments_bundle mock data --- spec/javascripts/environments/mock_data.js.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index bc5f6246cba..563414d0b1a 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -134,3 +134,5 @@ const environmentsList = [ updated_at: '2016-11-07T11:11:16.525Z', }, ]; + +window.environmentsList = environmentsList; -- cgit v1.2.1 From 707bcbba4baf62b77bde16086828681902669d38 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 7 Jan 2017 01:51:35 -0600 Subject: fix failing tests for merge_request_widget_spec.js --- app/assets/javascripts/merge_request_widget.js.es6 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 0305aeb07d9..c3a4306316b 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -4,6 +4,8 @@ /* global merge_request_widget */ /* global Turbolinks */ +require('./smart_interval'); + ((global) => { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; -- cgit v1.2.1 From e3ff5ff42698cecf17eb9cd59231c16132bb3e79 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 9 Jan 2017 13:17:48 -0600 Subject: ensire u2f object is accessible in a commonJS environment --- vendor/assets/javascripts/u2f.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/vendor/assets/javascripts/u2f.js b/vendor/assets/javascripts/u2f.js index e666b136051..a33e5e0ade9 100644 --- a/vendor/assets/javascripts/u2f.js +++ b/vendor/assets/javascripts/u2f.js @@ -745,4 +745,6 @@ u2f.getApiVersion = function(callback, opt_timeoutSeconds) { }; port.postMessage(req); }); -}; \ No newline at end of file +}; + +window.u2f || (window.u2f = u2f); -- cgit v1.2.1 From 5f2d1712180b5b56f486b1b2e0888e7acf094451 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 9 Jan 2017 13:18:21 -0600 Subject: prevent u2f authenticate test from auto-submitting a form --- spec/javascripts/u2f/authenticate_spec.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index e5948131744..ddbc1455057 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -25,19 +25,20 @@ require('./mock_u2f_device'); document.querySelector('#js-login-2fa-device'), document.querySelector('.js-2fa-form') ); + + // bypass automatic form submission within renderAuthenticated + spyOn(this.component, 'renderAuthenticated').and.returnValue(true); + return this.component.start(); }); it('allows authenticating via a U2F device', function() { - var authenticatedMessage, deviceResponse, inProgressMessage; + var inProgressMessage; inProgressMessage = this.container.find("p"); expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); this.u2fDevice.respondToAuthenticateRequest({ deviceData: "this is data from the device" }); - authenticatedMessage = this.container.find("p"); - deviceResponse = this.container.find('#js-device-response'); - expect(authenticatedMessage.text()).toContain('We heard back from your U2F device. You have been authenticated.'); - return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); + expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); }); return describe("errors", function() { it("displays an error message", function() { @@ -51,7 +52,7 @@ require('./mock_u2f_device'); return expect(errorMessage.text()).toContain("There was a problem communicating with your device"); }); return it("allows retrying authentication after an error", function() { - var authenticatedMessage, retryButton, setupButton; + var retryButton, setupButton; setupButton = this.container.find("#js-login-u2f-device"); setupButton.trigger('click'); this.u2fDevice.respondToAuthenticateRequest({ @@ -64,8 +65,7 @@ require('./mock_u2f_device'); this.u2fDevice.respondToAuthenticateRequest({ deviceData: "this is data from the device" }); - authenticatedMessage = this.container.find("p"); - return expect(authenticatedMessage.text()).toContain("We heard back from your U2F device. You have been authenticated."); + expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); }); }); }); -- cgit v1.2.1 From 5ffbb5dee58dcc5541a06a17700c87b62c4c31d8 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 9 Jan 2017 13:50:13 -0600 Subject: bypass buggy "focus" event in Chrome --- spec/javascripts/shortcuts_issuable_spec.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 7c577cc1b38..3f7f6cf0113 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -50,13 +50,8 @@ require('~/shortcuts_issuable'); return expect(triggered).toBe(true); }); return it('triggers `focus`', function() { - var focused; - focused = false; - $(this.selector).on('focus', function() { - return focused = true; - }); this.shortcut.replyWithSelectedText(); - return expect(focused).toBe(true); + expect(document.activeElement).toBe(document.querySelector(this.selector)); }); }); describe('with a one-line selection', function() { -- cgit v1.2.1 From d2649bf8e3d55e28e453f891faa92745267affe5 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 9 Jan 2017 16:33:56 -0600 Subject: bypass Chrome "focus" event bugs and prevent Turbolinks from triggering --- spec/javascripts/search_autocomplete_spec.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 08d9b775940..d564a49517b 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -112,13 +112,15 @@ require('vendor/fuzzaldrin-plus'); preloadFixtures('static/search_autocomplete.html.raw'); beforeEach(function() { loadFixtures('static/search_autocomplete.html.raw'); - return widget = new gl.SearchAutocomplete; + widget = new gl.SearchAutocomplete; + // Prevent turbolinks from triggering within gl_dropdown + spyOn(window.Turbolinks, 'visit').and.returnValue(true); }); it('should show Dashboard specific dropdown menu', function() { var list; addBodyAttributes(); mockDashboardOptions(); - widget.searchInput.focus(); + widget.searchInput.triggerHandler('focus'); list = widget.wrap.find('.dropdown-menu').find('ul'); return assertLinks(list, dashboardIssuesPath, dashboardMRsPath); }); @@ -126,7 +128,7 @@ require('vendor/fuzzaldrin-plus'); var list; addBodyAttributes('group'); mockGroupOptions(); - widget.searchInput.focus(); + widget.searchInput.triggerHandler('focus'); list = widget.wrap.find('.dropdown-menu').find('ul'); return assertLinks(list, groupIssuesPath, groupMRsPath); }); @@ -134,7 +136,7 @@ require('vendor/fuzzaldrin-plus'); var list; addBodyAttributes('project'); mockProjectOptions(); - widget.searchInput.focus(); + widget.searchInput.triggerHandler('focus'); list = widget.wrap.find('.dropdown-menu').find('ul'); return assertLinks(list, projectIssuesPath, projectMRsPath); }); @@ -143,7 +145,7 @@ require('vendor/fuzzaldrin-plus'); addBodyAttributes('project'); mockProjectOptions(); widget.searchInput.val('help'); - widget.searchInput.focus(); + widget.searchInput.triggerHandler('focus'); list = widget.wrap.find('.dropdown-menu').find('ul'); link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']"; return expect(list.find(link).length).toBe(0); @@ -154,7 +156,7 @@ require('vendor/fuzzaldrin-plus'); addBodyAttributes(); mockDashboardOptions(true); var submitSpy = spyOnEvent('form', 'submit'); - widget.searchInput.focus(); + widget.searchInput.triggerHandler('focus'); widget.wrap.trigger($.Event('keydown', { which: DOWN })); var enterKeyEvent = $.Event('keydown', { which: ENTER }); widget.searchInput.trigger(enterKeyEvent); -- cgit v1.2.1 From bdcb81be95b3287e14cf23b2f1d0849b24077360 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Mon, 9 Jan 2017 16:35:58 -0600 Subject: remove teaspoon-specific test --- spec/javascripts/lib/utils/common_utils_spec.js.es6 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 58fb54077f5..e33360a1875 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -10,9 +10,9 @@ require('~/lib/utils/common_utils'); // IE11 will return a relative pathname while other browsers will return a full pathname. // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor // element will create an absolute url relative to the current execution context. - // The JavaScript test suite is executed at '/teaspoon' which will lead to an absolute - // url starting with '/teaspoon'. - expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22'); + // The JavaScript test suite is executed at '/' which will lead to an absolute url + // starting with '/'. + expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/%22%20test=%22asf%22'); }); }); describe('gl.utils.parseUrlPathname', () => { -- cgit v1.2.1 From 5bb258cd8fbc051aa65fca133578eb30761b5ca1 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 13 Jan 2017 11:04:41 -0500 Subject: phantomJS doesn't allow us to spyOn history.replaceState --- spec/javascripts/bootstrap_linked_tabs_spec.js.es6 | 20 ++++++++++++++------ spec/javascripts/gl_dropdown_spec.js.es6 | 1 + .../javascripts/lib/utils/common_utils_spec.js.es6 | 2 +- spec/javascripts/merge_request_tabs_spec.js | 22 +++++++++++++++------- spec/javascripts/pipelines_spec.js.es6 | 5 +++++ spec/javascripts/project_title_spec.js | 2 ++ spec/javascripts/right_sidebar_spec.js | 2 ++ 7 files changed, 40 insertions(+), 14 deletions(-) diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 index bb2545cddf9..54055b04f62 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 @@ -1,6 +1,10 @@ require('~/lib/utils/bootstrap_linked_tabs'); (() => { + // TODO: remove this hack! + // PhantomJS causes spyOn to panic because replaceState isn't "writable" + const phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; + describe('Linked Tabs', () => { preloadFixtures('static/linked_tabs.html.raw'); @@ -10,7 +14,9 @@ require('~/lib/utils/bootstrap_linked_tabs'); describe('when is initialized', () => { beforeEach(() => { - spyOn(window.history, 'replaceState').and.callFake(function () {}); + if (!phantomjs) { + spyOn(window.history, 'replaceState').and.callFake(function () {}); + } }); it('should activate the tab correspondent to the given action', () => { @@ -36,7 +42,7 @@ require('~/lib/utils/bootstrap_linked_tabs'); describe('on click', () => { it('should change the url according to the clicked tab', () => { - const historySpy = spyOn(history, 'replaceState').and.callFake(() => {}); + const historySpy = !phantomjs && spyOn(history, 'replaceState').and.callFake(() => {}); const linkedTabs = new window.gl.LinkedTabs({ // eslint-disable-line action: 'show', @@ -49,10 +55,12 @@ require('~/lib/utils/bootstrap_linked_tabs'); secondTab.click(); - expect(historySpy).toHaveBeenCalledWith({ - turbolinks: true, - url: newState, - }, document.title, newState); + if (historySpy) { + expect(historySpy).toHaveBeenCalledWith({ + turbolinks: true, + url: newState, + }, document.title, newState); + } }); }); }); diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index b079aae13c3..c60fbd73c63 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -42,6 +42,7 @@ require('~/lib/utils/type_utility'); describe('Dropdown', function describeDropdown() { preloadFixtures('static/gl_dropdown.html.raw'); + loadJSONFixtures('projects.json'); function initDropDown(hasRemote, isFilterable) { this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({ diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index a54bb9bb444..bd9c82121a6 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -12,7 +12,7 @@ require('~/lib/utils/common_utils'); // element will create an absolute url relative to the current execution context. // The JavaScript test suite is executed at '/' which will lead to an absolute url // starting with '/'. - expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/%22%20test=%22asf%22'); + expect(gl.utils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22'); }); }); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 114b7cca61e..377acd5a3aa 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -6,6 +6,10 @@ require('~/lib/utils/common_utils'); require('vendor/jquery.scrollTo'); (function () { + // TODO: remove this hack! + // PhantomJS causes spyOn to panic because replaceState isn't "writable" + const phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; + describe('MergeRequestTabs', function () { var stubLocation = {}; var setLocation = function (stubs) { @@ -22,9 +26,11 @@ require('vendor/jquery.scrollTo'); this.class = new gl.MergeRequestTabs({ stubLocation: stubLocation }); setLocation(); - this.spies = { - history: spyOn(window.history, 'replaceState').and.callFake(function () {}) - }; + if (!phantomjs) { + this.spies = { + history: spyOn(window.history, 'replaceState').and.callFake(function () {}) + }; + } }); describe('#activateTab', function () { @@ -98,10 +104,12 @@ require('vendor/jquery.scrollTo'); pathname: '/foo/bar/merge_requests/1' }); newState = this.subject('commits'); - expect(this.spies.history).toHaveBeenCalledWith({ - turbolinks: true, - url: newState - }, document.title, newState); + if (!phantomjs) { + expect(this.spies.history).toHaveBeenCalledWith({ + turbolinks: true, + url: newState + }, document.title, newState); + } }); it('treats "show" like "notes"', function () { setLocation({ diff --git a/spec/javascripts/pipelines_spec.js.es6 b/spec/javascripts/pipelines_spec.js.es6 index 6120ca5543d..72770a702d3 100644 --- a/spec/javascripts/pipelines_spec.js.es6 +++ b/spec/javascripts/pipelines_spec.js.es6 @@ -1,5 +1,10 @@ require('~/pipelines'); +// Fix for phantomJS +if (!Element.prototype.matches && Element.prototype.webkitMatchesSelector) { + Element.prototype.matches = Element.prototype.webkitMatchesSelector; +} + (() => { describe('Pipelines', () => { preloadFixtures('static/pipeline_graph.html.raw'); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index ab0808bab18..fa59a937c8e 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -15,6 +15,8 @@ require('~/project'); describe('Project Title', function() { preloadFixtures('static/project_title.html.raw'); + loadJSONFixtures('projects.json'); + beforeEach(function() { loadFixtures('static/project_title.html.raw'); return this.project = new Project(); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 2a711b15133..3b00f15795c 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -34,6 +34,8 @@ require('~/extensions/jquery.js'); describe('RightSidebar', function() { var fixtureName = 'issues/open-issue.html.raw'; preloadFixtures(fixtureName); + loadJSONFixtures('todos.json'); + beforeEach(function() { loadFixtures(fixtureName); this.sidebar = new Sidebar; -- cgit v1.2.1 From 60339e044b0909e31e385bcc687f6bdf60b049b2 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 13 Jan 2017 11:05:09 -0500 Subject: phantomJS requires "use strict" for polyfill to properly work --- app/assets/javascripts/extensions/array.js.es6 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/extensions/array.js.es6 b/app/assets/javascripts/extensions/array.js.es6 index 717566a4715..8956e303488 100644 --- a/app/assets/javascripts/extensions/array.js.es6 +++ b/app/assets/javascripts/extensions/array.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable no-extend-native, func-names, space-before-function-paren, semi, space-infix-ops, max-len */ +/* eslint-disable no-extend-native, func-names, space-before-function-paren, semi, space-infix-ops, strict, max-len */ + +'use strict'; + Array.prototype.first = function() { return this[0]; } -- cgit v1.2.1 From 8a643c8fcd15e23a3b97ddc03a161ce3c8451099 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 13 Jan 2017 11:05:26 -0500 Subject: add phantomJS to karma config --- config/karma.config.js | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/config/karma.config.js b/config/karma.config.js index 5c3d454addf..44229e2ee88 100644 --- a/config/karma.config.js +++ b/config/karma.config.js @@ -6,6 +6,7 @@ var ROOT_PATH = path.resolve(__dirname, '..'); module.exports = function(config) { config.set({ basePath: ROOT_PATH, + browsers: ['PhantomJS'], frameworks: ['jasmine'], files: [ { pattern: 'spec/javascripts/test_bundle.js', watched: false }, diff --git a/package.json b/package.json index 4ce08487c3d..fe48fa152c1 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "jasmine-jquery": "^2.1.1", "karma": "^1.3.0", "karma-jasmine": "^1.1.0", + "karma-phantomjs-launcher": "^1.0.2", "karma-sourcemap-loader": "^0.3.7", "karma-webpack": "^1.8.0" } -- cgit v1.2.1 From fc4fcf33bfa3279bb0a81aac8498ada67b5e309b Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Fri, 13 Jan 2017 11:11:35 -0500 Subject: Added gzip compression --- config/webpack.config.js | 6 +++++- package.json | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index bd5863689fd..e9b63eb3bca 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -3,6 +3,7 @@ var path = require('path'); var webpack = require('webpack'); var StatsPlugin = require('stats-webpack-plugin'); +var CompressionPlugin = require("compression-webpack-plugin"); var IS_PRODUCTION = process.env.NODE_ENV === 'production'; var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1; @@ -79,7 +80,10 @@ var config = { chunks: false, modules: false, assets: true - }) + }), + new CompressionPlugin({ + asset: '[path].gz[query]', + }), ], resolve: { diff --git a/package.json b/package.json index fe48fa152c1..73fb487b973 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "babel-core": "^5.8.38", "babel-loader": "^5.4.2", "bootstrap-sass": "3.3.6", + "compression-webpack-plugin": "^0.3.2", "d3": "3.5.11", "dropzone": "4.2.0", "exports-loader": "^0.6.3", -- cgit v1.2.1 From 2ee2daa4b91f255218d6f68befa8395d2c62bb3a Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 13 Jan 2017 11:27:19 -0500 Subject: more post-merge fixes --- app/assets/javascripts/application.js | 2 +- .../javascripts/vue_pipelines_index/index.js.es6 | 2 +- config/webpack.config.js | 2 +- .../filtered_search/dropdown_utils_spec.js.es6 | 8 ++++---- .../filtered_search_dropdown_manager_spec.js.es6 | 6 +++--- .../filtered_search_token_keys_spec.js.es6 | 4 ++-- .../filtered_search_tokenizer_spec.js.es6 | 6 +++--- spec/javascripts/lib/utils/text_utility_spec.js.es6 | 2 +- spec/javascripts/vue_pagination/pagination_spec.js.es6 | 18 ++++++++---------- 9 files changed, 24 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 952b295566e..98042106111 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -54,8 +54,8 @@ requireAll(require.context('./commit', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./extensions', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./lib/utils', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('.', false, /^\.\/(?!application).*\.(js|es6)$/)); requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('.', false, /^\.\/(?!application).*\.(js|es6)$/)); require('vendor/fuzzaldrin-plus'); window.ES6Promise = require('vendor/es6-promise.auto'); window.ES6Promise.polyfill(); diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index 46626cb1445..2015c0e581c 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -1,7 +1,7 @@ /* global Vue, VueResource, gl */ window.Vue = require('vue'); window.Vue.use(require('vue-resource')); -require('../vue_common_component/commit') +require('../vue_common_component/commit'); require('../boards/vue_resource_interceptor'); require('./status'); require('./store'); diff --git a/config/webpack.config.js b/config/webpack.config.js index e9b63eb3bca..0b40e2c6190 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -35,7 +35,7 @@ var config = { lib_chart: './lib/chart.js', lib_d3: './lib/d3.js', vue_pagination: './vue_pagination/index.js', - vue_pipelines: './vue_pipelines_index/index.js', + vue_pipelines: './vue_pipelines_index/index.js', }, output: { diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 index ce61b73aa8a..96f33427213 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js.es6 @@ -1,7 +1,7 @@ -//= require extensions/array -//= require filtered_search/dropdown_utils -//= require filtered_search/filtered_search_tokenizer -//= require filtered_search/filtered_search_dropdown_manager +require('~/extensions/array'); +require('~/filtered_search/dropdown_utils'); +require('~/filtered_search/filtered_search_tokenizer'); +require('~/filtered_search/filtered_search_dropdown_manager'); (() => { describe('Dropdown Utils', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 index d0d27ceb4a6..0bc6689eba5 100644 --- a/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_dropdown_manager_spec.js.es6 @@ -1,6 +1,6 @@ -//= require extensions/array -//= require filtered_search/filtered_search_tokenizer -//= require filtered_search/filtered_search_dropdown_manager +require('~/extensions/array'); +require('~/filtered_search/filtered_search_tokenizer'); +require('~/filtered_search/filtered_search_dropdown_manager'); (() => { describe('Filtered Search Dropdown Manager', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 index 6df7c0e44ef..7df9d9ec1cb 100644 --- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js.es6 @@ -1,5 +1,5 @@ -//= require extensions/array -//= require filtered_search/filtered_search_token_keys +require('~/extensions/array'); +require('~/filtered_search/filtered_search_token_keys'); (() => { describe('Filtered Search Token Keys', () => { diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 index ac7f8e9cbcd..84c0e9cbfe2 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js.es6 @@ -1,6 +1,6 @@ -//= require extensions/array -//= require filtered_search/filtered_search_token_keys -//= require filtered_search/filtered_search_tokenizer +require('~/extensions/array'); +require('~/filtered_search/filtered_search_token_keys'); +require('~/filtered_search/filtered_search_tokenizer'); (() => { describe('Filtered Search Tokenizer', () => { diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6 index e97356b65d5..976e24c4ea5 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js.es6 +++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6 @@ -1,4 +1,4 @@ -//= require lib/utils/text_utility +require('~/lib/utils/text_utility'); (() => { describe('text_utility', () => { diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_pagination/pagination_spec.js.es6 index 1a7f2bb5fb8..8935c474ee5 100644 --- a/spec/javascripts/vue_pagination/pagination_spec.js.es6 +++ b/spec/javascripts/vue_pagination/pagination_spec.js.es6 @@ -1,7 +1,5 @@ -//= require vue -//= require lib/utils/common_utils -//= require vue_pagination/index -/* global fixture, gl */ +require('~/lib/utils/common_utils'); +require('~/vue_pagination/index'); describe('Pagination component', () => { let component; @@ -17,7 +15,7 @@ describe('Pagination component', () => { }; it('should render and start at page 1', () => { - fixture.set('
      '); + setFixtures('
      '); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -40,7 +38,7 @@ describe('Pagination component', () => { }); it('should go to the previous page', () => { - fixture.set('
      '); + setFixtures('
      '); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -61,7 +59,7 @@ describe('Pagination component', () => { }); it('should go to the next page', () => { - fixture.set('
      '); + setFixtures('
      '); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -82,7 +80,7 @@ describe('Pagination component', () => { }); it('should go to the last page', () => { - fixture.set('
      '); + setFixtures('
      '); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -103,7 +101,7 @@ describe('Pagination component', () => { }); it('should go to the first page', () => { - fixture.set('
      '); + setFixtures('
      '); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -124,7 +122,7 @@ describe('Pagination component', () => { }); it('should do nothing', () => { - fixture.set('
      '); + setFixtures('
      '); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), -- cgit v1.2.1 From 6ad01a49d478460905da4596ad22061fa85d6873 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 13 Jan 2017 12:51:19 -0500 Subject: rewrite test which relied on teaspoon quirk --- spec/javascripts/lib/utils/common_utils_spec.js.es6 | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index bd9c82121a6..9e8456c03aa 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -42,9 +42,13 @@ require('~/lib/utils/common_utils'); }); describe('gl.utils.getParameterByName', () => { + beforeEach(() => { + window.history.pushState({}, null, '?scope=all&p=2'); + }); + it('should return valid parameter', () => { - const value = gl.utils.getParameterByName('reporter'); - expect(value).toBe('Console'); + const value = gl.utils.getParameterByName('scope'); + expect(value).toBe('all'); }); it('should return invalid parameter', () => { -- cgit v1.2.1 From 55fc4e8fd88e3ac813ba87ef6ec8f2d73354520d Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Fri, 13 Jan 2017 19:49:14 -0500 Subject: fix requireAll within filtered search bundle --- .../javascripts/filtered_search/filtered_search_bundle.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js index b4186f8376a..392f1835966 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js +++ b/app/assets/javascripts/filtered_search/filtered_search_bundle.js @@ -1,9 +1,3 @@ - // This is a manifest file that'll be compiled into including all the files listed below. - // Add new JavaScript 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. - // - function requireAll(context) { return context.keys().map(context); } +function requireAll(context) { return context.keys().map(context); } - requireAll(require.context('./', true, /^\.\/.*\.(js|es6)$/)); +requireAll(require.context('./', true, /^\.\/(?!filtered_search_bundle).*\.(js|es6)$/)); -- cgit v1.2.1 From 0ef0ead559300dca22ae0b6a792a1660424aa564 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 14 Jan 2017 14:50:22 -0500 Subject: fix missing components in cycle_analytics_bundle --- app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 index 3b2f5efa27d..c41c57c1dcd 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 @@ -7,7 +7,7 @@ window.Cookies = require('vendor/js.cookie'); function requireAll(context) { return context.keys().map(context); } requireAll(require.context('./svg', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('.', false, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/)); +requireAll(require.context('.', true, /^\.\/(?!cycle_analytics_bundle).*\.(js|es6)$/)); $(() => { const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed'; -- cgit v1.2.1 From 8425e647fbd34c4c28d1d9df8fdf6495085a1f89 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sat, 14 Jan 2017 15:17:04 -0500 Subject: add dependency license approvals see: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7288#note_21415733 --- config/dependency_decisions.yml | 121 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 10 deletions(-) diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml index c11296975b7..aabe859730a 100644 --- a/config/dependency_decisions.yml +++ b/config/dependency_decisions.yml @@ -1,9 +1,9 @@ --- -# IGNORED GROUPS AND GEMS - - :ignore_group - development - :who: Connor Shea - :why: Development gems are not distributed with the final product and are therefore exempt. + :why: Development gems are not distributed with the final product and are therefore + exempt. :versions: [] :when: 2016-04-17 21:27:01.054140000 Z - - :ignore_group @@ -18,8 +18,6 @@ :why: Bundler is MIT licensed but will sometimes fail in CI. :versions: [] :when: 2016-05-02 06:42:08.045090000 Z - -# LICENSE WHITELIST - - :whitelist - MIT - :who: Connor Shea @@ -86,9 +84,6 @@ :why: https://opensource.org/licenses/BSD-2-Clause :versions: [] :when: 2016-07-26 21:24:07.248480000 Z - - -# LICENSE BLACKLIST - - :blacklist - GPLv2 - :who: Connor Shea @@ -107,9 +102,6 @@ :why: The OSL license is a copyleft license :versions: [] :when: 2016-10-28 11:02:15.540105000 Z - - -# GEM LICENSES - - :license - raphael-rails - MIT @@ -201,3 +193,112 @@ :why: https://github.com/jmcnevin/rubypants/blob/master/LICENSE.rdoc :versions: [] :when: 2016-05-02 05:56:50.696858000 Z +- - :approve + - after + - :who: Matt Lee + :why: https://github.com/Raynos/after/blob/master/LICENCE + :versions: [] + :when: 2017-01-14 20:00:32.473125000 Z +- - :approve + - amdefine + - :who: Matt Lee + :why: MIT License + :versions: [] + :when: 2017-01-14 20:08:31.810633000 Z +- - :approve + - base64id + - :who: Matt Lee + :why: https://github.com/faeldt/base64id/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:08:33.174760000 Z +- - :approve + - blob + - :who: Matt Lee + :why: https://github.com/webmodules/blob/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:08:34.564048000 Z +- - :approve + - callsite + - :who: Matt Lee + :why: https://github.com/tj/callsite/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:08:35.976025000 Z +- - :approve + - component-bind + - :who: Matt Lee + :why: https://github.com/component/bind/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:08:37.291219000 Z +- - :approve + - component-inherit + - :who: Matt Lee + :why: https://github.com/component/inherit/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:10:41.804804000 Z +- - :approve + - fsevents + - :who: Matt Lee + :why: https://github.com/strongloop/fsevents/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:50:20.037775000 Z +- - :approve + - indexof + - :who: Matt Lee + :why: https://github.com/component/indexof/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:10:43.209900000 Z +- - :approve + - is-integer + - :who: Matt Lee + :why: https://github.com/parshap/js-is-integer/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:10:44.540916000 Z +- - :approve + - jsonify + - :who: Matt Lee + :why: Public Domain - no formal license on this one. probably okay as its been + the same for along time. would prefer to see CC0 + :versions: [] + :when: 2017-01-14 20:10:45.857261000 Z +- - :approve + - object-component + - :who: Matt Lee + :why: https://github.com/component/object/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:10:47.190148000 Z +- - :approve + - optimist + - :who: Matt Lee + :why: https://github.com/substack/node-optimist/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:10:48.563077000 Z +- - :approve + - path-is-inside + - :who: Matt Lee + :why: https://github.com/domenic/path-is-inside/blob/master/LICENSE.txt + :versions: [] + :when: 2017-01-14 20:10:49.910497000 Z +- - :approve + - rc + - :who: Matt Lee + :why: https://github.com/dominictarr/rc/blob/master/LICENSE.MIT + :versions: [] + :when: 2017-01-14 20:10:51.244695000 Z +- - :approve + - ripemd160 + - :who: Matt Lee + :why: https://github.com/crypto-browserify/ripemd160/blob/master/LICENSE.md + :versions: [] + :when: 2017-01-14 20:10:52.560282000 Z +- - :approve + - select2 + - :who: Matt Lee + :why: https://github.com/select2/select2/blob/master/LICENSE.md + :versions: [] + :when: 2017-01-14 20:10:53.909618000 Z +- - :approve + - tweetnacl + - :who: Matt Lee + :why: https://github.com/dchest/tweetnacl-js/blob/master/LICENSE + :versions: [] + :when: 2017-01-14 20:10:57.812077000 Z -- cgit v1.2.1 From c9dbd5f5130ea45ba83b0047bec47d222629c782 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Sun, 15 Jan 2017 01:44:47 -0500 Subject: optimize gitlab ci to only run npm install once --- .gitlab-ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a038af69665..6d77d94637a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,7 +24,6 @@ before_script: - '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) $FLAGS' - retry gem install knapsack - '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql' - - npm install stages: - prepare @@ -109,12 +108,14 @@ setup-test-env: <<: *dedicated-runner stage: prepare script: + - npm install - bundle exec rake webpack:compile - bundle exec rake assets:precompile 2>/dev/null - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' artifacts: expire_in: 7d paths: + - node_modules - public/assets - tmp/tests @@ -235,7 +236,7 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21 script: - bundle exec $CI_BUILD_NAME -rubocop: +rubocop: <<: *ruby-static-analysis <<: *dedicated-runner stage: test @@ -298,7 +299,7 @@ karma: cache: paths: - vendor/ruby - - node_modules/ + - node_modules stage: test <<: *use-db <<: *dedicated-runner -- cgit v1.2.1 From 7c29f03e23c0a87b1dc9dacda173f40de17378eb Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 18 Jan 2017 13:25:10 -0600 Subject: ensure click event is bound only once --- app/assets/javascripts/diff.js.es6 | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/diff.js.es6 b/app/assets/javascripts/diff.js.es6 index 5e1a4c948aa..328d383a060 100644 --- a/app/assets/javascripts/diff.js.es6 +++ b/app/assets/javascripts/diff.js.es6 @@ -2,6 +2,7 @@ (() => { const UNFOLD_COUNT = 20; + let isBound = false; class Diff { constructor() { @@ -15,10 +16,12 @@ $('.content-wrapper .container-fluid').removeClass('container-limited'); } - $(document) - .off('click', '.js-unfold, .diff-line-num a') - .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) - .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); + if (!isBound) { + $(document) + .on('click', '.js-unfold', this.handleClickUnfold.bind(this)) + .on('click', '.diff-line-num a', this.handleClickLineNum.bind(this)); + isBound = true; + } this.openAnchoredDiff(); } -- cgit v1.2.1 From bc0a82334af7ef3a49067277cc5b7a7f3889658e Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 18 Jan 2017 13:55:46 -0600 Subject: ensure last line in diff block is contained within a table row element --- app/views/projects/diffs/_parallel_view.html.haml | 3 ++- app/views/projects/diffs/_text_file.html.haml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index b087485aa17..a25d7834c29 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -38,4 +38,5 @@ - if discussion_left || discussion_right = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right - if !diff_file.new_file && last_line > 0 - = diff_match_line last_line, last_line, bottom: true, view: :parallel + %tr.line_holder.parallel + = diff_match_line last_line, last_line, bottom: true, view: :parallel diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index f1d2d4bf268..fa1b5d5f8ac 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -13,4 +13,5 @@ - last_line = diff_file.highlighted_diff_lines.last.new_pos - if !diff_file.new_file && last_line > 0 - = diff_match_line last_line, last_line, bottom: true + %tr.line_holder + = diff_match_line last_line, last_line, bottom: true -- cgit v1.2.1 From ece2e80bf88c594b454d8dce9d040f99725ef535 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 18 Jan 2017 15:03:12 -0600 Subject: ensure linenumber data attribute is correct for the last line in a diff chunk --- app/views/projects/blob/diff.html.haml | 2 +- app/views/projects/diffs/_parallel_view.html.haml | 7 +++---- app/views/projects/diffs/_text_file.html.haml | 7 +++---- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index 538f8591f13..3b1a2e54ec2 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -27,4 +27,4 @@ - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to, class: line_class } - = diff_match_line @form.to, @form.to, text: @match_line, view: diff_view, bottom: true + = diff_match_line @form.to - @form.offset, @form.to, text: @match_line, view: diff_view, bottom: true diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index a25d7834c29..8145d66f192 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -1,11 +1,9 @@ / Side-by-side diff view .text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data } %table - - last_line = 0 - diff_file.parallel_diff_lines.each do |line| - left = line[:left] - right = line[:right] - - last_line = right.new_pos if right %tr.line_holder.parallel - if left - if left.meta? @@ -37,6 +35,7 @@ - discussion_left, discussion_right = parallel_diff_discussions(left, right, diff_file) - if discussion_left || discussion_right = render "discussions/parallel_diff_discussion", discussion_left: discussion_left, discussion_right: discussion_right - - if !diff_file.new_file && last_line > 0 + - if !diff_file.new_file && diff_file.diff_lines.any? + - last_line = diff_file.diff_lines.last %tr.line_holder.parallel - = diff_match_line last_line, last_line, bottom: true, view: :parallel + = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true, view: :parallel diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index fa1b5d5f8ac..2eea1db169a 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -4,14 +4,13 @@ %a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show. %table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' } - - last_line = 0 - discussions = @grouped_diff_discussions unless @diff_notes_disabled = render partial: "projects/diffs/line", collection: diff_file.highlighted_diff_lines, as: :line, locals: { diff_file: diff_file, discussions: discussions } - - last_line = diff_file.highlighted_diff_lines.last.new_pos - - if !diff_file.new_file && last_line > 0 + - if !diff_file.new_file && diff_file.highlighted_diff_lines.any? + - last_line = diff_file.highlighted_diff_lines.last %tr.line_holder - = diff_match_line last_line, last_line, bottom: true + = diff_match_line last_line.old_pos, last_line.new_pos, bottom: true -- cgit v1.2.1 From 7c583293cda3be8def5c665000a540124c838122 Mon Sep 17 00:00:00 2001 From: Mike Greiling Date: Wed, 18 Jan 2017 16:40:34 -0600 Subject: prevent nonewline type diff lines from containing unfolding link --- app/views/projects/diffs/_parallel_view.html.haml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index b087485aa17..cbd021487ef 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -8,8 +8,12 @@ - last_line = right.new_pos if right %tr.line_holder.parallel - if left - - if left.meta? + - case left.type + - when 'match' = diff_match_line left.old_pos, nil, text: left.text, view: :parallel + - when 'nonewline' + %td.old_line.diff-line-num + %td.line_content.match= left.text - else - left_line_code = diff_file.line_code(left) - left_position = diff_file.position(left) @@ -21,8 +25,12 @@ %td.line_content.parallel - if right - - if right.meta? + - case right.type + - when 'match' = diff_match_line nil, right.new_pos, text: left.text, view: :parallel + - when 'nonewline' + %td.new_line.diff-line-num + %td.line_content.match= right.text - else - right_line_code = diff_file.line_code(right) - right_position = diff_file.position(right) -- cgit v1.2.1 From c5b7cc54e9bfceda7d48b1f15bcf064a0d96c07d Mon Sep 17 00:00:00 2001 From: Phil Hughes Date: Thu, 19 Jan 2017 09:41:38 +0000 Subject: Use webpack_port file if it exists --- config/environments/development.rb | 4 ++++ config/webpack.config.js | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index 168c434f261..f8cf196bc7c 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,4 +1,6 @@ Rails.application.configure do + WEBPACK_DEV_PORT = `cat ../webpack_port 2>/dev/null || echo '3808'`.to_i + # Settings specified here will take precedence over those in config/application.rb # In the development environment your application's code is reloaded on @@ -24,6 +26,8 @@ Rails.application.configure do # Enable webpack dev server config.webpack.dev_server.enabled = true + config.webpack.dev_server.port = WEBPACK_DEV_PORT + config.webpack.dev_server.manifest_port = WEBPACK_DEV_PORT # Do not compress assets config.assets.compress = false diff --git a/config/webpack.config.js b/config/webpack.config.js index 762147e8b06..bddd181b452 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -1,5 +1,6 @@ 'use strict'; +var fs = require('fs'); var path = require('path'); var webpack = require('webpack'); var StatsPlugin = require('stats-webpack-plugin'); @@ -10,7 +11,13 @@ var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1; var ROOT_PATH = path.resolve(__dirname, '..'); // must match config.webpack.dev_server.port -var DEV_SERVER_PORT = 3808; +var DEV_SERVER_PORT; + +try { + DEV_SERVER_PORT = parseInt(fs.readFileSync('../webpack_port'), 10); +} catch (e) { + DEV_SERVER_PORT = 3808; +} var config = { context: path.join(ROOT_PATH, 'app/assets/javascripts'), -- cgit v1.2.1 From d15b7db1216f220b9f5af7e777cf04712483cbdf Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Tue, 17 Jan 2017 14:50:49 -0500 Subject: Fix References header parser for Microsoft Exchange Microsoft Exchange would append a comma and another message id into the References header, therefore we'll need to fallback and parse the header by ourselves. Closes #26567 --- lib/gitlab/email/receiver.rb | 17 ++++++++- lib/gitlab/incoming_email.rb | 9 +++-- ...sing_and_key_inside_references_with_a_comma.eml | 42 ++++++++++++++++++++++ .../email/handler/create_note_handler_spec.rb | 6 ++++ spec/lib/gitlab/incoming_email_spec.rb | 15 ++++++++ 5 files changed, 83 insertions(+), 6 deletions(-) create mode 100644 spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index a40c44eb1bc..df9d1cae8da 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -35,6 +35,8 @@ module Gitlab handler.execute end + private + def build_mail Mail::Message.new(@raw) rescue Encoding::UndefinedConversionError, @@ -54,7 +56,20 @@ module Gitlab end def key_from_additional_headers(mail) - Array(mail.references).find do |mail_id| + find_key_from_references(ensure_references_array(mail.references)) + end + + def ensure_references_array(references) + case references + when Array + references + when String # Handle emails from Microsoft exchange which uses commas + Gitlab::IncomingEmail.scan_fallback_references(references) + end + end + + def find_key_from_references(references) + references.find do |mail_id| key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id) break key if key end diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index 801dfde9a36..9ae3a2c1214 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -3,8 +3,6 @@ module Gitlab WILDCARD_PLACEHOLDER = '%{key}'.freeze class << self - FALLBACK_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze - def enabled? config.enabled && config.address end @@ -32,10 +30,11 @@ module Gitlab end def key_from_fallback_message_id(mail_id) - match = mail_id.match(FALLBACK_MESSAGE_ID_REGEX) - return unless match + mail_id[/\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/, 1] + end - match[1] + def scan_fallback_references(references) + references.scan(/(?!<)[^<>]+(?=>)/.freeze) end def config diff --git a/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml new file mode 100644 index 00000000000..6823db0cfc8 --- /dev/null +++ b/spec/fixtures/emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml @@ -0,0 +1,42 @@ +Return-Path: +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for ; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for ; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog +To: reply@appmail.adventuretime.ooo +Message-ID: +In-Reply-To: +References: , +Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux' +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 + +I could not disagree more. I am obviously biased but adventure time is the +greatest show ever created. Everyone should watch it. + +- Jake out + + +On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta + wrote: +> +> +> +> eviltrout posted in 'Adventure Time Sux' on Discourse Meta: +> +> --- +> hey guys everyone knows adventure time sucks! +> +> --- +> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3 +> +> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences). +> diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb index 48660d1dd1b..0f2bd009148 100644 --- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -174,6 +174,12 @@ describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do it_behaves_like 'an email that contains a mail key', 'References' end + + context 'mail key is in the References header with a comma' do + let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references_with_a_comma.eml') } + + it_behaves_like 'an email that contains a mail key', 'References' + end end end end diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index 1dcf2c0668b..01d0cb6cbd6 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -48,4 +48,19 @@ describe Gitlab::IncomingEmail, lib: true do expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key') end end + + context 'self.scan_fallback_references' do + let(:references) do + '' + + ' ' + + ',' + end + + it 'returns reply key' do + expect(described_class.scan_fallback_references(references)) + .to eq(%w[issue_1@localhost + reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost + exchange@microsoft.com]) + end + end end -- cgit v1.2.1 From 7fcbe37df37cb9f04eae4e690305a26ea88410d2 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 20 Jan 2017 20:20:40 +0800 Subject: Specify that iOS app would also do this --- lib/gitlab/email/receiver.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index df9d1cae8da..fa08b5c668f 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -63,7 +63,9 @@ module Gitlab case references when Array references - when String # Handle emails from Microsoft exchange which uses commas + when String + # Handle emails from clients which append with commas, + # example clients are Microsoft exchange and iOS app Gitlab::IncomingEmail.scan_fallback_references(references) end end -- cgit v1.2.1 From 15f8642994bc74bea1a39d079c70b1f4e4730bf1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 20 Jan 2017 22:36:12 +0800 Subject: Add changelog entry --- changelogs/unreleased/fix-references-header-parsing.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/fix-references-header-parsing.yml diff --git a/changelogs/unreleased/fix-references-header-parsing.yml b/changelogs/unreleased/fix-references-header-parsing.yml new file mode 100644 index 00000000000..b927279cdf4 --- /dev/null +++ b/changelogs/unreleased/fix-references-header-parsing.yml @@ -0,0 +1,5 @@ +--- +title: Fix reply by email without sub-addressing for some clients from + Microsoft and Apple +merge_request: 8620 +author: -- cgit v1.2.1 From 09f1a9e320306677d0a94c8f8d7b58c3024c3ed7 Mon Sep 17 00:00:00 2001 From: Allison Whilden Date: Sat, 21 Jan 2017 17:00:55 -0800 Subject: [ci skip] UX guide: Update animation guidance to 100ms --- doc/development/ux_guide/animation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/ux_guide/animation.md b/doc/development/ux_guide/animation.md index 903e54bf9dc..5dae4bcc905 100644 --- a/doc/development/ux_guide/animation.md +++ b/doc/development/ux_guide/animation.md @@ -19,7 +19,7 @@ Easing specifies the rate of change of a parameter over time (see [easings.net]( ### Hover -Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `200ms linear` transition for a color hover effect. +Interactive elements (links, buttons, etc.) should have a hover state. A subtle animation for this transition adds a polished feel. We should target a `100ms - 150ms linear` transition for a color hover effect. View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here. -- cgit v1.2.1 From 863efcc60437ccec67be6bd68af9ff3678141b89 Mon Sep 17 00:00:00 2001 From: dimitrieh Date: Mon, 23 Jan 2017 00:44:07 +0100 Subject: Improve pipeline status icon linking in widgets --- app/views/projects/commit/_commit_box.html.haml | 3 ++- app/views/projects/merge_requests/widget/_heading.html.haml | 3 ++- app/views/projects/pipelines/_info.html.haml | 4 ++-- .../26982-improve-pipeline-status-icon-linking-in-widgets.yml | 4 ++++ 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 08eb0c57f66..2b1c4e28ce2 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -64,7 +64,8 @@ - if @commit.status .well-segment.pipeline-info %div{ class: "icon-container ci-status-icon-#{@commit.status}" } - = ci_icon_for_status(@commit.status) + = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do + = ci_icon_for_status(@commit.status) Pipeline = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace" for diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index c80dc33058d..cc939ab9441 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -2,7 +2,8 @@ .mr-widget-heading - %w[success success_with_warnings skipped canceled failed running pending].each do |status| .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } - = ci_icon_for_status(status) + = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id) do + = ci_icon_for_status(status) %span Pipeline = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline' diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index ca76f13ef5e..6caa5f16dc6 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -23,8 +23,8 @@ .info-well - if @commit.status .well-segment.pipeline-info - %div{ class: "icon-container ci-status-icon-#{@commit.status}" } - = ci_icon_for_status(@commit.status) + .icon-container + = icon('clock-o') = pluralize @pipeline.statuses.count(:id), "build" - if @pipeline.ref from diff --git a/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml new file mode 100644 index 00000000000..c5c57af5aaf --- /dev/null +++ b/changelogs/unreleased/26982-improve-pipeline-status-icon-linking-in-widgets.yml @@ -0,0 +1,4 @@ +--- +title: Improve pipeline status icon linking in widgets +merge_request: +author: -- cgit v1.2.1 From bc9c245b87375abafd9050648bf020b879172a79 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" Date: Tue, 10 Jan 2017 19:43:58 +0100 Subject: Chat Commands have presenters This improves the styling and readability of the code. This is supported by both Mattermost and Slack. --- .../chat_slash_commands_service.rb | 20 +-- lib/gitlab/chat_commands/base_command.rb | 4 - lib/gitlab/chat_commands/command.rb | 22 +-- lib/gitlab/chat_commands/deploy.rb | 24 ++-- lib/gitlab/chat_commands/issue_create.rb | 18 ++- lib/gitlab/chat_commands/issue_search.rb | 10 +- lib/gitlab/chat_commands/issue_show.rb | 8 +- lib/gitlab/chat_commands/presenter.rb | 131 ----------------- lib/gitlab/chat_commands/presenters/access.rb | 22 +++ lib/gitlab/chat_commands/presenters/base.rb | 73 ++++++++++ lib/gitlab/chat_commands/presenters/deploy.rb | 24 ++++ lib/gitlab/chat_commands/presenters/issuable.rb | 33 +++++ lib/gitlab/chat_commands/presenters/list_issues.rb | 32 +++++ lib/gitlab/chat_commands/presenters/show_issue.rb | 38 +++++ lib/mattermost/client.rb | 41 ------ lib/mattermost/command.rb | 10 -- lib/mattermost/error.rb | 3 - lib/mattermost/session.rb | 160 --------------------- lib/mattermost/team.rb | 7 - spec/lib/gitlab/chat_commands/command_spec.rb | 60 +------- spec/lib/gitlab/chat_commands/deploy_spec.rb | 24 ++-- spec/lib/gitlab/chat_commands/issue_create_spec.rb | 12 +- spec/lib/gitlab/chat_commands/issue_search_spec.rb | 12 +- spec/lib/gitlab/chat_commands/issue_show_spec.rb | 25 +++- .../gitlab/chat_commands/presenters/access_spec.rb | 49 +++++++ .../gitlab/chat_commands/presenters/deploy_spec.rb | 47 ++++++ .../chat_commands/presenters/list_issues_spec.rb | 24 ++++ .../chat_commands/presenters/show_issue_spec.rb | 27 ++++ spec/lib/mattermost/client_spec.rb | 24 ---- spec/lib/mattermost/command_spec.rb | 61 -------- spec/lib/mattermost/session_spec.rb | 123 ---------------- spec/lib/mattermost/team_spec.rb | 66 --------- 32 files changed, 476 insertions(+), 758 deletions(-) delete mode 100644 lib/gitlab/chat_commands/presenter.rb create mode 100644 lib/gitlab/chat_commands/presenters/access.rb create mode 100644 lib/gitlab/chat_commands/presenters/base.rb create mode 100644 lib/gitlab/chat_commands/presenters/deploy.rb create mode 100644 lib/gitlab/chat_commands/presenters/issuable.rb create mode 100644 lib/gitlab/chat_commands/presenters/list_issues.rb create mode 100644 lib/gitlab/chat_commands/presenters/show_issue.rb delete mode 100644 lib/mattermost/client.rb delete mode 100644 lib/mattermost/command.rb delete mode 100644 lib/mattermost/error.rb delete mode 100644 lib/mattermost/session.rb delete mode 100644 lib/mattermost/team.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/access_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb delete mode 100644 spec/lib/mattermost/client_spec.rb delete mode 100644 spec/lib/mattermost/command_spec.rb delete mode 100644 spec/lib/mattermost/session_spec.rb delete mode 100644 spec/lib/mattermost/team_spec.rb diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 2bcff541cc0..608754f3035 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -28,20 +28,24 @@ class ChatSlashCommandsService < Service end def trigger(params) - return unless valid_token?(params[:token]) + return access_presenter unless valid_token?(params[:token]) user = find_chat_user(params) - unless user + + if user + Gitlab::ChatCommands::Command.new(project, user, params).execute + else url = authorize_chat_name_url(params) - return presenter.authorize_chat_name(url) + access_presenter(url).authorize end - - Gitlab::ChatCommands::Command.new(project, user, - params).execute end private + def access_presenter(url = nil) + Gitlab::ChatCommands::Presenters::Access.new(url) + end + def find_chat_user(params) ChatNames::FindUserService.new(self, params).execute end @@ -49,8 +53,4 @@ class ChatSlashCommandsService < Service def authorize_chat_name_url(params) ChatNames::AuthorizeUserService.new(self, params).execute end - - def presenter - Gitlab::ChatCommands::Presenter.new - end end diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index 4fe53ce93a9..25da8474e95 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -42,10 +42,6 @@ module Gitlab def find_by_iid(iid) collection.find_by(iid: iid) end - - def presenter - Gitlab::ChatCommands::Presenter.new - end end end end diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 145086755e4..ac7ee868402 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -13,9 +13,9 @@ module Gitlab if command if command.allowed?(project, current_user) - present command.new(project, current_user, params).execute(match) + command.new(project, current_user, params).execute(match) else - access_denied + Gitlab::ChatCommands::Presenters::Access.new.access_denied end else help(help_messages) @@ -25,7 +25,7 @@ module Gitlab def match_command match = nil service = available_commands.find do |klass| - match = klass.match(command) + match = klass.match(params[:text]) end [service, match] @@ -42,22 +42,6 @@ module Gitlab klass.available?(project) end end - - def command - params[:text] - end - - def help(messages) - presenter.help(messages, params[:command]) - end - - def access_denied - presenter.access_denied - end - - def present(resource) - presenter.present(resource) - end end end end diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb index 7127d2f6d04..458d90f84e8 100644 --- a/lib/gitlab/chat_commands/deploy.rb +++ b/lib/gitlab/chat_commands/deploy.rb @@ -1,8 +1,6 @@ module Gitlab module ChatCommands class Deploy < BaseCommand - include Gitlab::Routing.url_helpers - def self.match(text) /\Adeploy\s+(?\S+.*)\s+to+\s+(?\S+.*)\z/.match(text) end @@ -24,35 +22,29 @@ module Gitlab to = match[:to] actions = find_actions(from, to) - return unless actions.present? - if actions.one? - play!(from, to, actions.first) + if actions.none? + Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions + elsif actions.one? + action = play!(from, to, actions.first) + Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to) else - Result.new(:error, 'Too many actions defined') + Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions end end private def play!(from, to, action) - new_action = action.play(current_user) - - Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.") + action.play(current_user) end def find_actions(from, to) environment = project.environments.find_by(name: from) - return unless environment + return [] unless environment environment.actions_for(to).select(&:starts_environment?) end - - def url(subject) - polymorphic_url( - [subject.project.namespace.becomes(Namespace), subject.project, subject] - ) - end end end end diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index cefb6775db8..a06f13b0f72 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -2,7 +2,7 @@ module Gitlab module ChatCommands class IssueCreate < IssueCommand def self.match(text) - # we can not match \n with the dot by passing the m modifier as than + # we can not match \n with the dot by passing the m modifier as than # the title and description are not seperated /\Aissue\s+(new|create)\s+(?[^\n]*)\n*(?<description>(.|\n)*)/.match(text) end @@ -19,8 +19,24 @@ module Gitlab title = match[:title] description = match[:description].to_s.rstrip + issue = create_issue(title: title, description: description) + + if issue.errors.any? + presenter(issue).display_errors + else + presenter(issue).present + end + end + + private + + def create_issue(title:, description:) Issues::CreateService.new(project, current_user, title: title, description: description).execute end + + def presenter(issue) + Gitlab::ChatCommands::Presenters::ShowIssue.new(issue) + end end end end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb index 51bf80c800b..e2d3a0f466a 100644 --- a/lib/gitlab/chat_commands/issue_search.rb +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -10,7 +10,15 @@ module Gitlab end def execute(match) - collection.search(match[:query]).limit(QUERY_LIMIT) + issues = collection.search(match[:query]).limit(QUERY_LIMIT) + + if issues.none? + Presenters::Access.new(issues).not_found + elsif issues.one? + Presenters::ShowIssue.new(issues.first).present + else + Presenters::ListIssues.new(issues).present + end end end end diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb index 2a45d49cf6b..9f3e1b9a64b 100644 --- a/lib/gitlab/chat_commands/issue_show.rb +++ b/lib/gitlab/chat_commands/issue_show.rb @@ -10,7 +10,13 @@ module Gitlab end def execute(match) - find_by_iid(match[:iid]) + issue = find_by_iid(match[:iid]) + + if issue + Gitlab::ChatCommands::Presenters::ShowIssue.new(issue).present + else + Gitlab::ChatCommands::Presenters::Access.new.not_found + end end end end diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb deleted file mode 100644 index 8930a21f406..00000000000 --- a/lib/gitlab/chat_commands/presenter.rb +++ /dev/null @@ -1,131 +0,0 @@ -module Gitlab - module ChatCommands - class Presenter - include Gitlab::Routing - - def authorize_chat_name(url) - message = if url - ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})." - else - ":sweat_smile: Couldn't identify you, nor can I autorize you!" - end - - ephemeral_response(message) - end - - def help(commands, trigger) - if commands.none? - ephemeral_response("No commands configured") - else - commands.map! { |command| "#{trigger} #{command}" } - message = header_with_list("Available commands", commands) - - ephemeral_response(message) - end - end - - def present(subject) - return not_found unless subject - - if subject.is_a?(Gitlab::ChatCommands::Result) - show_result(subject) - elsif subject.respond_to?(:count) - if subject.none? - not_found - elsif subject.one? - single_resource(subject.first) - else - multiple_resources(subject) - end - else - single_resource(subject) - end - end - - def access_denied - ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).") - end - - private - - def show_result(result) - case result.type - when :success - in_channel_response(result.message) - else - ephemeral_response(result.message) - end - end - - def not_found - ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") - end - - def single_resource(resource) - return error(resource) if resource.errors.any? || !resource.persisted? - - message = "#{title(resource)}:" - message << "\n\n#{resource.description}" if resource.try(:description) - - in_channel_response(message) - end - - def multiple_resources(resources) - titles = resources.map { |resource| title(resource) } - - message = header_with_list("Multiple results were found:", titles) - - ephemeral_response(message) - end - - def error(resource) - message = header_with_list("The action was not successful, because:", resource.errors.messages) - - ephemeral_response(message) - end - - def title(resource) - reference = resource.try(:to_reference) || resource.try(:id) - title = resource.try(:title) || resource.try(:name) - - "[#{reference} #{title}](#{url(resource)})" - end - - def header_with_list(header, items) - message = [header] - - items.each do |item| - message << "- #{item}" - end - - message.join("\n") - end - - def url(resource) - url_for( - [ - resource.project.namespace.becomes(Namespace), - resource.project, - resource - ] - ) - end - - def ephemeral_response(message) - { - response_type: :ephemeral, - text: message, - status: 200 - } - end - - def in_channel_response(message) - { - response_type: :in_channel, - text: message, - status: 200 - } - end - end - end -end diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb new file mode 100644 index 00000000000..6d18d745608 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/access.rb @@ -0,0 +1,22 @@ +module Gitlab::ChatCommands::Presenters + class Access < Gitlab::ChatCommands::Presenters::Base + def access_denied + ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).") + end + + def not_found + ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:") + end + + def authorize + message = + if @resource + ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})." + else + ":sweat_smile: Couldn't identify you, nor can I autorize you!" + end + + ephemeral_response(text: message) + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb new file mode 100644 index 00000000000..0897025d85f --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/base.rb @@ -0,0 +1,73 @@ +module Gitlab::ChatCommands::Presenters + class Base + include Gitlab::Routing.url_helpers + + def initialize(resource = nil) + @resource = resource + end + + def display_errors + message = header_with_list("The action was not successful, because:", @resource.errors.full_messages) + + ephemeral_response(text: message) + end + + private + + def header_with_list(header, items) + message = [header] + + items.each do |item| + message << "- #{item}" + end + + message.join("\n") + end + + def ephemeral_response(message) + response = { + response_type: :ephemeral, + status: 200 + }.merge(message) + + format_response(response) + end + + def in_channel_response(message) + response = { + response_type: :in_channel, + status: 200 + }.merge(message) + + format_response(response) + end + + def format_response(response) + response[:text] = format(response[:text]) if response.has_key?(:text) + + if response.has_key?(:attachments) + response[:attachments].each do |attachment| + attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext] + attachment[:text] = format(attachment[:text]) if attachment[:text] + end + end + + response + end + + # Convert Markdown to slacks format + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def resource_url + url_for( + [ + @resource.project.namespace.becomes(Namespace), + @resource.project, + @resource + ] + ) + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb new file mode 100644 index 00000000000..4f6333812ff --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -0,0 +1,24 @@ +module Gitlab::ChatCommands::Presenters + class Deploy < Gitlab::ChatCommands::Presenters::Base + def present(from, to) + message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})." + in_channel_response(text: message) + end + + def no_actions + ephemeral_response(text: "No action found to be executed") + end + + def too_many_actions + ephemeral_response(text: "Too many actions defined") + end + + private + + def resource_url + polymorphic_url( + [ @resource.project.namespace.becomes(Namespace), @resource.project, @resource] + ) + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb new file mode 100644 index 00000000000..9623387f188 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -0,0 +1,33 @@ +module Gitlab::ChatCommands::Presenters + class Issuable < Gitlab::ChatCommands::Presenters::Base + private + + def project + @resource.project + end + + def author + @resource.author + end + + def fields + [ + { + title: "Assignee", + value: @resource.assignee ? @resource.assignee.name : "_None_", + short: true + }, + { + title: "Milestone", + value: @resource.milestone ? @resource.milestone.title : "_None_", + short: true + }, + { + title: "Labels", + value: @resource.labels.any? ? @resource.label_names : "_None_", + short: true + } + ] + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/list_issues.rb b/lib/gitlab/chat_commands/presenters/list_issues.rb new file mode 100644 index 00000000000..5a7b3fca5c2 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/list_issues.rb @@ -0,0 +1,32 @@ +module Gitlab::ChatCommands::Presenters + class ListIssues < Gitlab::ChatCommands::Presenters::Base + def present + ephemeral_response(text: "Here are the issues I found:", attachments: attachments) + end + + private + + def attachments + @resource.map do |issue| + state = issue.open? ? "Open" : "Closed" + + { + fallback: "Issue #{issue.to_reference}: #{issue.title}", + color: "#d22852", + text: "[#{issue.to_reference}](#{url_for([namespace, project, issue])}) · #{issue.title} (#{state})", + mrkdwn_in: [ + "text" + ] + } + end + end + + def project + @project ||= @resource.first.project + end + + def namespace + @namespace ||= project.namespace.becomes(Namespace) + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/show_issue.rb b/lib/gitlab/chat_commands/presenters/show_issue.rb new file mode 100644 index 00000000000..2a89c30b972 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/show_issue.rb @@ -0,0 +1,38 @@ +module Gitlab::ChatCommands::Presenters + class ShowIssue < Gitlab::ChatCommands::Presenters::Issuable + def present + in_channel_response(show_issue) + end + + private + + def show_issue + { + attachments: [ + { + title: @resource.title, + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "#{@resource.to_reference}: #{@resource.title}", + text: text, + fields: fields, + mrkdwn_in: [ + :title, + :text + ] + } + ] + } + end + + def text + message = "" + message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero? + message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero? + message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero? + + message + end + end +end diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb deleted file mode 100644 index ec2903b7ec6..00000000000 --- a/lib/mattermost/client.rb +++ /dev/null @@ -1,41 +0,0 @@ -module Mattermost - class ClientError < Mattermost::Error; end - - class Client - attr_reader :user - - def initialize(user) - @user = user - end - - private - - def with_session(&blk) - Mattermost::Session.new(user).with_session(&blk) - end - - def json_get(path, options = {}) - with_session do |session| - json_response session.get(path, options) - end - end - - def json_post(path, options = {}) - with_session do |session| - json_response session.post(path, options) - end - end - - def json_response(response) - json_response = JSON.parse(response.body) - - unless response.success? - raise Mattermost::ClientError.new(json_response['message'] || 'Undefined error') - end - - json_response - rescue JSON::JSONError - raise Mattermost::ClientError.new('Cannot parse response') - end - end -end diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb deleted file mode 100644 index d1e4bb0eccf..00000000000 --- a/lib/mattermost/command.rb +++ /dev/null @@ -1,10 +0,0 @@ -module Mattermost - class Command < Client - def create(params) - response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create", - body: params.to_json) - - response['token'] - end - end -end diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb deleted file mode 100644 index 014df175be0..00000000000 --- a/lib/mattermost/error.rb +++ /dev/null @@ -1,3 +0,0 @@ -module Mattermost - class Error < StandardError; end -end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb deleted file mode 100644 index 377cb7b1021..00000000000 --- a/lib/mattermost/session.rb +++ /dev/null @@ -1,160 +0,0 @@ -module Mattermost - class NoSessionError < Mattermost::Error - def message - 'No session could be set up, is Mattermost configured with Single Sign On?' - end - end - - class ConnectionError < Mattermost::Error; end - - # This class' prime objective is to obtain a session token on a Mattermost - # instance with SSO configured where this GitLab instance is the provider. - # - # The process depends on OAuth, but skips a step in the authentication cycle. - # For example, usually a user would click the 'login in GitLab' button on - # Mattermost, which would yield a 302 status code and redirects you to GitLab - # to approve the use of your account on Mattermost. Which would trigger a - # callback so Mattermost knows this request is approved and gets the required - # data to create the user account etc. - # - # This class however skips the button click, and also the approval phase to - # speed up the process and keep it without manual action and get a session - # going. - class Session - include Doorkeeper::Helpers::Controller - include HTTParty - - LEASE_TIMEOUT = 60 - - base_uri Settings.mattermost.host - - attr_accessor :current_resource_owner, :token - - def initialize(current_user) - @current_resource_owner = current_user - end - - def with_session - with_lease do - raise Mattermost::NoSessionError unless create - - begin - yield self - rescue Errno::ECONNREFUSED - raise Mattermost::NoSessionError - ensure - destroy - end - end - end - - # Next methods are needed for Doorkeeper - def pre_auth - @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new( - Doorkeeper.configuration, server.client_via_uid, params) - end - - def authorization - @authorization ||= strategy.request - end - - def strategy - @strategy ||= server.authorization_request(pre_auth.response_type) - end - - def request - @request ||= OpenStruct.new(parameters: params) - end - - def params - Rack::Utils.parse_query(oauth_uri.query).symbolize_keys - end - - def get(path, options = {}) - handle_exceptions do - self.class.get(path, options.merge(headers: @headers)) - end - end - - def post(path, options = {}) - handle_exceptions do - self.class.post(path, options.merge(headers: @headers)) - end - end - - private - - def create - return unless oauth_uri - return unless token_uri - - @token = request_token - @headers = { - Authorization: "Bearer #{@token}" - } - - @token - end - - def destroy - post('/api/v3/users/logout') - end - - def oauth_uri - return @oauth_uri if defined?(@oauth_uri) - - @oauth_uri = nil - - response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) - return unless 300 <= response.code && response.code < 400 - - redirect_uri = response.headers['location'] - return unless redirect_uri - - @oauth_uri = URI.parse(redirect_uri) - end - - def token_uri - @token_uri ||= - if oauth_uri - authorization.authorize.redirect_uri if pre_auth.authorizable? - end - end - - def request_token - response = get(token_uri, follow_redirects: false) - - if 200 <= response.code && response.code < 400 - response.headers['token'] - end - end - - def with_lease - lease_uuid = lease_try_obtain - raise NoSessionError unless lease_uuid - - begin - yield - ensure - Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) - end - end - - def lease_key - "mattermost:session" - end - - def lease_try_obtain - lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) - lease.try_obtain - end - - def handle_exceptions - yield - rescue HTTParty::Error => e - raise Mattermost::ConnectionError.new(e.message) - rescue Errno::ECONNREFUSED - raise Mattermost::ConnectionError.new(e.message) - end - end -end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb deleted file mode 100644 index 784eca6ab5a..00000000000 --- a/lib/mattermost/team.rb +++ /dev/null @@ -1,7 +0,0 @@ -module Mattermost - class Team < Client - def all - json_get('/api/v3/teams/all') - end - end -end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index a2d84977f58..b634df52b68 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -5,19 +5,7 @@ describe Gitlab::ChatCommands::Command, service: true do let(:user) { create(:user) } describe '#execute' do - subject do - described_class.new(project, user, params).execute - end - - context 'when no command is available' do - let(:params) { { text: 'issue show 1' } } - let(:project) { create(:project, has_external_issue_tracker: true) } - - it 'displays 404 messages' do - expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('404 not found') - end - end + subject { described_class.new(project, user, params).execute } context 'when an unknown command is triggered' do let(:params) { { command: '/gitlab', text: "unknown command 123" } } @@ -34,47 +22,7 @@ describe Gitlab::ChatCommands::Command, service: true do it 'rejects the actions' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Whoops! That action is not allowed') - end - end - - context 'issue is successfully created' do - let(:params) { { text: "issue create my new issue" } } - - before do - project.team << [user, :master] - end - - it 'presents the issue' do - expect(subject[:text]).to match("my new issue") - end - - it 'shows a link to the new issue' do - expect(subject[:text]).to match(/\/issues\/\d+/) - end - end - - context 'searching for an issue' do - let(:params) { { text: 'issue search find me' } } - let!(:issue) { create(:issue, project: project, title: 'find me') } - - before do - project.team << [user, :master] - end - - context 'a single issue is found' do - it 'presents the issue' do - expect(subject[:text]).to match(issue.title) - end - end - - context 'multiple issues found' do - let!(:issue2) { create(:issue, project: project, title: "someone find me") } - - it 'shows a link to the new issue' do - expect(subject[:text]).to match(issue.title) - expect(subject[:text]).to match(issue2.title) - end + expect(subject[:text]).to start_with('Whoops! This action is not allowed') end end @@ -90,7 +38,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'and user can not create deployment' do it 'returns action' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Whoops! That action is not allowed') + expect(subject[:text]).to start_with('Whoops! This action is not allowed') end end @@ -100,7 +48,7 @@ describe Gitlab::ChatCommands::Command, service: true do end it 'returns action' do - expect(subject[:text]).to include('Deployment from staging to production started.') + expect(subject[:text]).to include('Deployment started from staging to production') expect(subject[:response_type]).to be(:in_channel) end diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb index bd8099c92da..b3358a32161 100644 --- a/spec/lib/gitlab/chat_commands/deploy_spec.rb +++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb @@ -15,8 +15,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do end context 'if no environment is defined' do - it 'returns nil' do - expect(subject).to be_nil + it 'does not execute an action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") end end @@ -26,8 +27,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do let!(:deployment) { create(:deployment, environment: staging, deployable: build) } context 'without actions' do - it 'returns nil' do - expect(subject).to be_nil + it 'does not execute an action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") end end @@ -37,8 +39,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do end it 'returns success result' do - expect(subject.type).to eq(:success) - expect(subject.message).to include('Deployment from staging to production started') + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with('Deployment started from staging to production') end context 'when duplicate action exists' do @@ -47,8 +49,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do end it 'returns error' do - expect(subject.type).to eq(:error) - expect(subject.message).to include('Too many actions defined') + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq('Too many actions defined') end end @@ -59,9 +61,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do name: 'teardown', environment: 'production') end - it 'returns success result' do - expect(subject.type).to eq(:success) - expect(subject.message).to include('Deployment from staging to production started') + it 'returns the success message' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with('Deployment started from staging to production') end end end diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb index 6c71e79ff6d..0f84b19a5a4 100644 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do it 'creates the issue' do expect { subject }.to change { project.issues.count }.by(1) - expect(subject.title).to eq('bird is the word') + expect(subject[:response_type]).to be(:in_channel) end end @@ -41,6 +41,16 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do expect { subject }.to change { project.issues.count }.by(1) end end + + context 'issue cannot be created' do + let!(:issue) { create(:issue, project: project, title: 'bird is the word') } + let(:regex_match) { described_class.match("issue create #{'a' * 512}}") } + + it 'displays the errors' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("- Title is too long") + end + end end describe '.match' do diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb index 24c06a967fa..04d10ad52a1 100644 --- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Gitlab::ChatCommands::IssueSearch, service: true do describe '#execute' do - let!(:issue) { create(:issue, title: 'find me') } + let!(:issue) { create(:issue, project: project, title: 'find me') } let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') } - let(:project) { issue.project } + let(:project) { create(:empty_project) } let(:user) { issue.author } let(:regex_match) { described_class.match("issue search find") } @@ -14,7 +14,8 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do context 'when the user has no access' do it 'only returns the open issues' do - expect(subject).not_to include(confidential) + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("not found") end end @@ -24,13 +25,14 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do end it 'returns all results' do - expect(subject).to include(confidential, issue) + expect(subject).to have_key(:attachments) + expect(subject[:text]).to match("Here are the issues I found:") end end context 'without hits on the query' do it 'returns an empty collection' do - expect(subject).to be_empty + expect(subject[:text]).to match("not found") end end end diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb index 2eab73e49e5..89932c395c6 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe Gitlab::ChatCommands::IssueShow, service: true do describe '#execute' do - let(:issue) { create(:issue) } - let(:project) { issue.project } + let(:issue) { create(:issue, project: project) } + let(:project) { create(:empty_project) } let(:user) { issue.author } let(:regex_match) { described_class.match("issue show #{issue.iid}") } @@ -16,15 +16,19 @@ describe Gitlab::ChatCommands::IssueShow, service: true do end context 'the issue exists' do + let(:title) { subject[:attachments].first[:title] } + it 'returns the issue' do - expect(subject.iid).to be issue.iid + expect(subject[:response_type]).to be(:in_channel) + expect(title).to eq(issue.title) end context 'when its reference is given' do let(:regex_match) { described_class.match("issue show #{issue.to_reference}") } it 'shows the issue' do - expect(subject.iid).to be issue.iid + expect(subject[:response_type]).to be(:in_channel) + expect(title).to eq(issue.title) end end end @@ -32,17 +36,24 @@ describe Gitlab::ChatCommands::IssueShow, service: true do context 'the issue does not exist' do let(:regex_match) { described_class.match("issue show 2343242") } - it "returns nil" do - expect(subject).to be_nil + it "returns not found" do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("not found") end end end - describe 'self.match' do + describe '.match' do it 'matches the iid' do match = described_class.match("issue show 123") expect(match[:iid]).to eq("123") end + + it 'accepts a reference' do + match = described_class.match("issue show #{Issue.reference_prefix}123") + + expect(match[:iid]).to eq("123") + end end end diff --git a/spec/lib/gitlab/chat_commands/presenters/access_spec.rb b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb new file mode 100644 index 00000000000..ae41d75ab0c --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::Access do + describe '#access_denied' do + subject { described_class.new.access_denied } + + it { is_expected.to be_a(Hash) } + + it 'displays an error message' do + expect(subject[:text]).to match("is not allowed") + expect(subject[:response_type]).to be(:ephemeral) + end + end + + describe '#not_found' do + subject { described_class.new.not_found } + + it { is_expected.to be_a(Hash) } + + it 'tells the user the resource was not found' do + expect(subject[:text]).to match("not found!") + expect(subject[:response_type]).to be(:ephemeral) + end + end + + describe '#authorize' do + context 'with an authorization URL' do + subject { described_class.new('http://authorize.me').authorize } + + it { is_expected.to be_a(Hash) } + + it 'tells the user to authorize' do + expect(subject[:text]).to match("connect your GitLab account") + expect(subject[:response_type]).to be(:ephemeral) + end + end + + context 'without authorization url' do + subject { described_class.new.authorize } + + it { is_expected.to be_a(Hash) } + + it 'tells the user to authorize' do + expect(subject[:text]).to match("Couldn't identify you") + expect(subject[:response_type]).to be(:ephemeral) + end + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb new file mode 100644 index 00000000000..1c48c727e30 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::Deploy do + let(:build) { create(:ci_build) } + + describe '#present' do + subject { described_class.new(build).present('staging', 'prod') } + + it { is_expected.to have_key(:text) } + it { is_expected.to have_key(:response_type) } + it { is_expected.to have_key(:status) } + it { is_expected.not_to have_key(:attachments) } + + it 'messages the channel of the deploy' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with("Deployment started from staging to prod") + end + end + + describe '#no_actions' do + subject { described_class.new(nil).no_actions } + + it { is_expected.to have_key(:text) } + it { is_expected.to have_key(:response_type) } + it { is_expected.to have_key(:status) } + it { is_expected.not_to have_key(:attachments) } + + it 'tells the user there is no action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") + end + end + + describe '#too_many_actions' do + subject { described_class.new(nil).too_many_actions } + + it { is_expected.to have_key(:text) } + it { is_expected.to have_key(:response_type) } + it { is_expected.to have_key(:status) } + it { is_expected.not_to have_key(:attachments) } + + it 'tells the user there is no action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("Too many actions defined") + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb new file mode 100644 index 00000000000..1852395fc97 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::ListIssues do + let(:project) { create(:empty_project) } + let(:message) { subject[:text] } + let(:issue) { project.issues.first } + + before { create_list(:issue, 2, project: project) } + + subject { described_class.new(project.issues).present } + + it do + is_expected.to have_key(:text) + is_expected.to have_key(:status) + is_expected.to have_key(:response_type) + is_expected.to have_key(:attachments) + end + + it 'shows a list of results' do + expect(subject[:response_type]).to be(:ephemeral) + + expect(message).to start_with("Here are the issues I found") + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb new file mode 100644 index 00000000000..13a318fe680 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::ShowIssue do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:attachment) { subject[:attachments].first } + + subject { described_class.new(issue).present } + + it { is_expected.to be_a(Hash) } + + it 'shows the issue' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject).to have_key(:attachments) + expect(attachment[:title]).to eq(issue.title) + end + + context 'with upvotes' do + before do + create(:award_emoji, :upvote, awardable: issue) + end + + it 'shows the upvote count' do + expect(attachment[:text]).to start_with(":+1: 1") + end + end +end diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb deleted file mode 100644 index dc11a414717..00000000000 --- a/spec/lib/mattermost/client_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Client do - let(:user) { build(:user) } - - subject { described_class.new(user) } - - context 'JSON parse error' do - before do - Struct.new("Request", :body, :success?) - end - - it 'yields an error on malformed JSON' do - bad_json = Struct::Request.new("I'm not json", true) - expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError) - end - - it 'shows a client error if the request was unsuccessful' do - bad_request = Struct::Request.new("true", false) - - expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError) - end - end -end diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb deleted file mode 100644 index 5ccf1100898..00000000000 --- a/spec/lib/mattermost/command_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Command do - let(:params) { { 'token' => 'token', team_id: 'abc' } } - - before do - Mattermost::Session.base_uri('http://mattermost.example.com') - - allow_any_instance_of(Mattermost::Client).to receive(:with_session). - and_yield(Mattermost::Session.new(nil)) - end - - describe '#create' do - let(:params) do - { team_id: 'abc', - trigger: 'gitlab' - } - end - - subject { described_class.new(nil).create(params) } - - context 'for valid trigger word' do - before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). - with(body: { - team_id: 'abc', - trigger: 'gitlab' }.to_json). - to_return( - status: 200, - headers: { 'Content-Type' => 'application/json' }, - body: { token: 'token' }.to_json - ) - end - - it 'returns a token' do - is_expected.to eq('token') - end - end - - context 'for error message' do - before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). - to_return( - status: 500, - headers: { 'Content-Type' => 'application/json' }, - body: { - id: 'api.command.duplicate_trigger.app_error', - message: 'This trigger word is already in use. Please choose another word.', - detailed_error: '', - request_id: 'obc374man7bx5r3dbc1q5qhf3r', - status_code: 500 - }.to_json - ) - end - - it 'raises an error with message' do - expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.') - end - end - end -end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb deleted file mode 100644 index 74d12e37181..00000000000 --- a/spec/lib/mattermost/session_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Session, type: :request do - let(:user) { create(:user) } - - let(:gitlab_url) { "http://gitlab.com" } - let(:mattermost_url) { "http://mattermost.com" } - - subject { described_class.new(user) } - - # Needed for doorkeeper to function - it { is_expected.to respond_to(:current_resource_owner) } - it { is_expected.to respond_to(:request) } - it { is_expected.to respond_to(:authorization) } - it { is_expected.to respond_to(:strategy) } - - before do - described_class.base_uri(mattermost_url) - end - - describe '#with session' do - let(:location) { 'http://location.tld' } - let!(:stub) do - WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login"). - to_return(headers: { 'location' => location }, status: 307) - end - - context 'without oauth uri' do - it 'makes a request to the oauth uri' do - expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) - end - end - - context 'with oauth_uri' do - let!(:doorkeeper) do - Doorkeeper::Application.create( - name: "GitLab Mattermost", - redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete", - scopes: "") - end - - context 'without token_uri' do - it 'can not create a session' do - expect do - subject.with_session - end.to raise_error(Mattermost::NoSessionError) - end - end - - context 'with token_uri' do - let(:state) { "state" } - let(:params) do - { response_type: "code", - client_id: doorkeeper.uid, - redirect_uri: "#{mattermost_url}/signup/gitlab/complete", - state: state } - end - let(:location) do - "#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}" - end - - before do - WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete"). - with(query: hash_including({ 'state' => state })). - to_return do |request| - post "/oauth/token", - client_id: doorkeeper.uid, - client_secret: doorkeeper.secret, - redirect_uri: params[:redirect_uri], - grant_type: 'authorization_code', - code: request.uri.query_values['code'] - - if response.status == 200 - { headers: { 'token' => 'thisworksnow' }, status: 202 } - end - end - - WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout"). - to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) - end - - it 'can setup a session' do - subject.with_session do |session| - end - - expect(subject.token).not_to be_nil - end - - it 'returns the value of the block' do - result = subject.with_session do |session| - "value" - end - - expect(result).to eq("value") - end - end - end - - context 'with lease' do - before do - allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk') - end - - it 'tries to obtain a lease' do - expect(subject).to receive(:lease_try_obtain) - expect(Gitlab::ExclusiveLease).to receive(:cancel) - - # Cannot setup a session, but we should still cancel the lease - expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) - end - end - - context 'without lease' do - before do - allow(subject).to receive(:lease_try_obtain).and_return(nil) - end - - it 'returns a NoSessionError error' do - expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) - end - end - end -end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb deleted file mode 100644 index 2d14be6bcc2..00000000000 --- a/spec/lib/mattermost/team_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Team do - before do - Mattermost::Session.base_uri('http://mattermost.example.com') - - allow_any_instance_of(Mattermost::Client).to receive(:with_session). - and_yield(Mattermost::Session.new(nil)) - end - - describe '#all' do - subject { described_class.new(nil).all } - - context 'for valid request' do - let(:response) do - [{ - "id" => "xiyro8huptfhdndadpz8r3wnbo", - "create_at" => 1482174222155, - "update_at" => 1482174222155, - "delete_at" => 0, - "display_name" => "chatops", - "name" => "chatops", - "email" => "admin@example.com", - "type" => "O", - "company_name" => "", - "allowed_domains" => "", - "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", - "allow_open_invite" => false }] - end - - before do - stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). - to_return( - status: 200, - headers: { 'Content-Type' => 'application/json' }, - body: response.to_json - ) - end - - it 'returns a token' do - is_expected.to eq(response) - end - end - - context 'for error message' do - before do - stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). - to_return( - status: 500, - headers: { 'Content-Type' => 'application/json' }, - body: { - id: 'api.team.list.app_error', - message: 'Cannot list teams.', - detailed_error: '', - request_id: 'obc374man7bx5r3dbc1q5qhf3r', - status_code: 500 - }.to_json - ) - end - - it 'raises an error with message' do - expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.') - end - end - end -end -- cgit v1.2.1 From 19c55a47b77f6c63db39a45946dc47f3c95fc744 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Wed, 11 Jan 2017 08:54:44 -0500 Subject: Revert removing of some files --- lib/mattermost/client.rb | 41 ++++++++++++ lib/mattermost/command.rb | 10 +++ lib/mattermost/error.rb | 3 + lib/mattermost/session.rb | 160 ++++++++++++++++++++++++++++++++++++++++++++++ lib/mattermost/team.rb | 7 ++ 5 files changed, 221 insertions(+) create mode 100644 lib/mattermost/client.rb create mode 100644 lib/mattermost/command.rb create mode 100644 lib/mattermost/error.rb create mode 100644 lib/mattermost/session.rb create mode 100644 lib/mattermost/team.rb diff --git a/lib/mattermost/client.rb b/lib/mattermost/client.rb new file mode 100644 index 00000000000..ec2903b7ec6 --- /dev/null +++ b/lib/mattermost/client.rb @@ -0,0 +1,41 @@ +module Mattermost + class ClientError < Mattermost::Error; end + + class Client + attr_reader :user + + def initialize(user) + @user = user + end + + private + + def with_session(&blk) + Mattermost::Session.new(user).with_session(&blk) + end + + def json_get(path, options = {}) + with_session do |session| + json_response session.get(path, options) + end + end + + def json_post(path, options = {}) + with_session do |session| + json_response session.post(path, options) + end + end + + def json_response(response) + json_response = JSON.parse(response.body) + + unless response.success? + raise Mattermost::ClientError.new(json_response['message'] || 'Undefined error') + end + + json_response + rescue JSON::JSONError + raise Mattermost::ClientError.new('Cannot parse response') + end + end +end diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb new file mode 100644 index 00000000000..d1e4bb0eccf --- /dev/null +++ b/lib/mattermost/command.rb @@ -0,0 +1,10 @@ +module Mattermost + class Command < Client + def create(params) + response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create", + body: params.to_json) + + response['token'] + end + end +end diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb new file mode 100644 index 00000000000..014df175be0 --- /dev/null +++ b/lib/mattermost/error.rb @@ -0,0 +1,3 @@ +module Mattermost + class Error < StandardError; end +end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb new file mode 100644 index 00000000000..377cb7b1021 --- /dev/null +++ b/lib/mattermost/session.rb @@ -0,0 +1,160 @@ +module Mattermost + class NoSessionError < Mattermost::Error + def message + 'No session could be set up, is Mattermost configured with Single Sign On?' + end + end + + class ConnectionError < Mattermost::Error; end + + # This class' prime objective is to obtain a session token on a Mattermost + # instance with SSO configured where this GitLab instance is the provider. + # + # The process depends on OAuth, but skips a step in the authentication cycle. + # For example, usually a user would click the 'login in GitLab' button on + # Mattermost, which would yield a 302 status code and redirects you to GitLab + # to approve the use of your account on Mattermost. Which would trigger a + # callback so Mattermost knows this request is approved and gets the required + # data to create the user account etc. + # + # This class however skips the button click, and also the approval phase to + # speed up the process and keep it without manual action and get a session + # going. + class Session + include Doorkeeper::Helpers::Controller + include HTTParty + + LEASE_TIMEOUT = 60 + + base_uri Settings.mattermost.host + + attr_accessor :current_resource_owner, :token + + def initialize(current_user) + @current_resource_owner = current_user + end + + def with_session + with_lease do + raise Mattermost::NoSessionError unless create + + begin + yield self + rescue Errno::ECONNREFUSED + raise Mattermost::NoSessionError + ensure + destroy + end + end + end + + # Next methods are needed for Doorkeeper + def pre_auth + @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new( + Doorkeeper.configuration, server.client_via_uid, params) + end + + def authorization + @authorization ||= strategy.request + end + + def strategy + @strategy ||= server.authorization_request(pre_auth.response_type) + end + + def request + @request ||= OpenStruct.new(parameters: params) + end + + def params + Rack::Utils.parse_query(oauth_uri.query).symbolize_keys + end + + def get(path, options = {}) + handle_exceptions do + self.class.get(path, options.merge(headers: @headers)) + end + end + + def post(path, options = {}) + handle_exceptions do + self.class.post(path, options.merge(headers: @headers)) + end + end + + private + + def create + return unless oauth_uri + return unless token_uri + + @token = request_token + @headers = { + Authorization: "Bearer #{@token}" + } + + @token + end + + def destroy + post('/api/v3/users/logout') + end + + def oauth_uri + return @oauth_uri if defined?(@oauth_uri) + + @oauth_uri = nil + + response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) + return unless 300 <= response.code && response.code < 400 + + redirect_uri = response.headers['location'] + return unless redirect_uri + + @oauth_uri = URI.parse(redirect_uri) + end + + def token_uri + @token_uri ||= + if oauth_uri + authorization.authorize.redirect_uri if pre_auth.authorizable? + end + end + + def request_token + response = get(token_uri, follow_redirects: false) + + if 200 <= response.code && response.code < 400 + response.headers['token'] + end + end + + def with_lease + lease_uuid = lease_try_obtain + raise NoSessionError unless lease_uuid + + begin + yield + ensure + Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) + end + end + + def lease_key + "mattermost:session" + end + + def lease_try_obtain + lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) + lease.try_obtain + end + + def handle_exceptions + yield + rescue HTTParty::Error => e + raise Mattermost::ConnectionError.new(e.message) + rescue Errno::ECONNREFUSED + raise Mattermost::ConnectionError.new(e.message) + end + end +end diff --git a/lib/mattermost/team.rb b/lib/mattermost/team.rb new file mode 100644 index 00000000000..784eca6ab5a --- /dev/null +++ b/lib/mattermost/team.rb @@ -0,0 +1,7 @@ +module Mattermost + class Team < Client + def all + json_get('/api/v3/teams/all') + end + end +end -- cgit v1.2.1 From e96ddef3deaac499bcdd67800839e59f46ae67b5 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Wed, 11 Jan 2017 09:04:49 -0500 Subject: Add help command --- lib/gitlab/chat_commands/command.rb | 13 +++++-------- lib/gitlab/chat_commands/help.rb | 28 ++++++++++++++++++++++++++++ lib/gitlab/chat_commands/presenters/help.rb | 20 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 lib/gitlab/chat_commands/help.rb create mode 100644 lib/gitlab/chat_commands/presenters/help.rb diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index ac7ee868402..4e5031a8a26 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -18,25 +18,22 @@ module Gitlab Gitlab::ChatCommands::Presenters::Access.new.access_denied end else - help(help_messages) + Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands) end end def match_command match = nil - service = available_commands.find do |klass| - match = klass.match(params[:text]) - end + service = + available_commands.find do |klass| + match = klass.match(params[:text]) + end [service, match] end private - def help_messages - available_commands.map(&:help_message) - end - def available_commands COMMANDS.select do |klass| klass.available?(project) diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb new file mode 100644 index 00000000000..e76733f5445 --- /dev/null +++ b/lib/gitlab/chat_commands/help.rb @@ -0,0 +1,28 @@ +module Gitlab + module ChatCommands + class Help < BaseCommand + # This class has to be used last, as it always matches. It has to match + # because other commands were not triggered and we want to show the help + # command + def self.match(_text) + true + end + + def self.help_message + 'help' + end + + def self.allowed?(_project, _user) + true + end + + def execute(commands) + Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger) + end + + def trigger + params[:command] + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb new file mode 100644 index 00000000000..133b707231f --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -0,0 +1,20 @@ +module Gitlab::ChatCommands::Presenters + class Help < Gitlab::ChatCommands::Presenters::Base + def present(trigger) + message = + if @resource.none? + "No commands available :thinking_face:" + else + header_with_list("Available commands", full_commands(trigger)) + end + + ephemeral_response(text: message) + end + + private + + def full_commands(trigger) + @resource.map { |command| "#{trigger} #{command.help_message}" } + end + end +end -- cgit v1.2.1 From e9d163865bc6f987e7fa277536c6bc3950fbf1e0 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 12 Jan 2017 09:04:21 -0500 Subject: Fix tests --- app/models/project_services/chat_slash_commands_service.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 608754f3035..5eb1bd86e9d 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -28,7 +28,7 @@ class ChatSlashCommandsService < Service end def trigger(params) - return access_presenter unless valid_token?(params[:token]) + return unless valid_token?(params[:token]) user = find_chat_user(params) @@ -36,16 +36,12 @@ class ChatSlashCommandsService < Service Gitlab::ChatCommands::Command.new(project, user, params).execute else url = authorize_chat_name_url(params) - access_presenter(url).authorize + Gitlab::ChatCommands::Presenters::Access.new(url).authorize end end private - def access_presenter(url = nil) - Gitlab::ChatCommands::Presenters::Access.new(url) - end - def find_chat_user(params) ChatNames::FindUserService.new(self, params).execute end -- cgit v1.2.1 From 52ca0d2c9ecb7e7d4d0130f19bf0fb7b772a357d Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 19 Jan 2017 09:22:09 +0100 Subject: Incorporate feedback --- changelogs/unreleased/zj-format-chat-messages.yml | 4 + lib/gitlab/chat_commands/issue_create.rb | 2 +- lib/gitlab/chat_commands/presenters/access.rb | 36 +++--- lib/gitlab/chat_commands/presenters/base.rb | 112 ++++++++++--------- lib/gitlab/chat_commands/presenters/deploy.rb | 39 ++++--- lib/gitlab/chat_commands/presenters/help.rb | 31 +++--- lib/gitlab/chat_commands/presenters/issuable.rb | 66 ++++++----- lib/gitlab/chat_commands/presenters/list_issues.rb | 59 ++++++---- lib/gitlab/chat_commands/presenters/new_issue.rb | 42 +++++++ lib/gitlab/chat_commands/presenters/show_issue.rb | 72 +++++++----- spec/lib/gitlab/chat_commands/issue_search_spec.rb | 2 +- spec/lib/gitlab/chat_commands/issue_show_spec.rb | 4 +- .../gitlab/chat_commands/presenters/deploy_spec.rb | 2 +- .../chat_commands/presenters/list_issues_spec.rb | 5 +- .../chat_commands/presenters/show_issue_spec.rb | 4 +- spec/lib/mattermost/client_spec.rb | 24 ++++ spec/lib/mattermost/command_spec.rb | 61 ++++++++++ spec/lib/mattermost/session_spec.rb | 123 +++++++++++++++++++++ spec/lib/mattermost/team_spec.rb | 66 +++++++++++ 19 files changed, 565 insertions(+), 189 deletions(-) create mode 100644 changelogs/unreleased/zj-format-chat-messages.yml create mode 100644 lib/gitlab/chat_commands/presenters/new_issue.rb create mode 100644 spec/lib/mattermost/client_spec.rb create mode 100644 spec/lib/mattermost/command_spec.rb create mode 100644 spec/lib/mattermost/session_spec.rb create mode 100644 spec/lib/mattermost/team_spec.rb diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml new file mode 100644 index 00000000000..2494884f5c9 --- /dev/null +++ b/changelogs/unreleased/zj-format-chat-messages.yml @@ -0,0 +1,4 @@ +--- +title: Reformat messages ChatOps +merge_request: 8528 +author: diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index a06f13b0f72..3f3d7de8b2e 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -35,7 +35,7 @@ module Gitlab end def presenter(issue) - Gitlab::ChatCommands::Presenters::ShowIssue.new(issue) + Gitlab::ChatCommands::Presenters::NewIssue.new(issue) end end end diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb index 6d18d745608..b66ef48d6a8 100644 --- a/lib/gitlab/chat_commands/presenters/access.rb +++ b/lib/gitlab/chat_commands/presenters/access.rb @@ -1,22 +1,26 @@ -module Gitlab::ChatCommands::Presenters - class Access < Gitlab::ChatCommands::Presenters::Base - def access_denied - ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).") - end - - def not_found - ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:") - end +module Gitlab + module ChatCommands + module Presenters + class Access < Presenters::Base + def access_denied + ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).") + end - def authorize - message = - if @resource - ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})." - else - ":sweat_smile: Couldn't identify you, nor can I autorize you!" + def not_found + ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:") end - ephemeral_response(text: message) + def authorize + message = + if @resource + ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})." + else + ":sweat_smile: Couldn't identify you, nor can I autorize you!" + end + + ephemeral_response(text: message) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb index 0897025d85f..2700a5a2ad5 100644 --- a/lib/gitlab/chat_commands/presenters/base.rb +++ b/lib/gitlab/chat_commands/presenters/base.rb @@ -1,73 +1,77 @@ -module Gitlab::ChatCommands::Presenters - class Base - include Gitlab::Routing.url_helpers +module Gitlab + module ChatCommands + module Presenters + class Base + include Gitlab::Routing.url_helpers + + def initialize(resource = nil) + @resource = resource + end - def initialize(resource = nil) - @resource = resource - end + def display_errors + message = header_with_list("The action was not successful, because:", @resource.errors.full_messages) - def display_errors - message = header_with_list("The action was not successful, because:", @resource.errors.full_messages) + ephemeral_response(text: message) + end - ephemeral_response(text: message) - end + private - private + def header_with_list(header, items) + message = [header] - def header_with_list(header, items) - message = [header] + items.each do |item| + message << "- #{item}" + end - items.each do |item| - message << "- #{item}" - end + message.join("\n") + end - message.join("\n") - end + def ephemeral_response(message) + response = { + response_type: :ephemeral, + status: 200 + }.merge(message) - def ephemeral_response(message) - response = { - response_type: :ephemeral, - status: 200 - }.merge(message) + format_response(response) + end - format_response(response) - end + def in_channel_response(message) + response = { + response_type: :in_channel, + status: 200 + }.merge(message) - def in_channel_response(message) - response = { - response_type: :in_channel, - status: 200 - }.merge(message) + format_response(response) + end - format_response(response) - end + def format_response(response) + response[:text] = format(response[:text]) if response.has_key?(:text) - def format_response(response) - response[:text] = format(response[:text]) if response.has_key?(:text) + if response.has_key?(:attachments) + response[:attachments].each do |attachment| + attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext] + attachment[:text] = format(attachment[:text]) if attachment[:text] + end + end - if response.has_key?(:attachments) - response[:attachments].each do |attachment| - attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext] - attachment[:text] = format(attachment[:text]) if attachment[:text] + response end - end - - response - end - # Convert Markdown to slacks format - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end + # Convert Markdown to slacks format + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end - def resource_url - url_for( - [ - @resource.project.namespace.becomes(Namespace), - @resource.project, - @resource - ] - ) + def resource_url + url_for( + [ + @resource.project.namespace.becomes(Namespace), + @resource.project, + @resource + ] + ) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb index 4f6333812ff..b1cfaac15af 100644 --- a/lib/gitlab/chat_commands/presenters/deploy.rb +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -1,24 +1,29 @@ -module Gitlab::ChatCommands::Presenters - class Deploy < Gitlab::ChatCommands::Presenters::Base - def present(from, to) - message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})." - in_channel_response(text: message) - end +module Gitlab + module ChatCommands + module Presenters + class Deploy < Presenters::Base + def present(from, to) + message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})." - def no_actions - ephemeral_response(text: "No action found to be executed") - end + in_channel_response(text: message) + end - def too_many_actions - ephemeral_response(text: "Too many actions defined") - end + def no_actions + ephemeral_response(text: "No action found to be executed") + end + + def too_many_actions + ephemeral_response(text: "Too many actions defined") + end - private + private - def resource_url - polymorphic_url( - [ @resource.project.namespace.becomes(Namespace), @resource.project, @resource] - ) + def resource_url + polymorphic_url( + [ @resource.project.namespace.becomes(Namespace), @resource.project, @resource] + ) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb index 133b707231f..c7a67467b7e 100644 --- a/lib/gitlab/chat_commands/presenters/help.rb +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -1,20 +1,25 @@ -module Gitlab::ChatCommands::Presenters - class Help < Gitlab::ChatCommands::Presenters::Base - def present(trigger) - message = - if @resource.none? - "No commands available :thinking_face:" - else - header_with_list("Available commands", full_commands(trigger)) +module Gitlab + module ChatCommands + module Presenters + class Help < Presenters::Base + def present(trigger) + ephemeral_response(text: help_message(trigger)) end - ephemeral_response(text: message) - end + private - private + def help_message(trigger) + if @resource.none? + "No commands available :thinking_face:" + else + header_with_list("Available commands", full_commands(trigger)) + end + end - def full_commands(trigger) - @resource.map { |command| "#{trigger} #{command.help_message}" } + def full_commands(trigger) + @resource.map { |command| "#{trigger} #{command.help_message}" } + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb index 9623387f188..2cb6b1525fc 100644 --- a/lib/gitlab/chat_commands/presenters/issuable.rb +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -1,33 +1,45 @@ -module Gitlab::ChatCommands::Presenters - class Issuable < Gitlab::ChatCommands::Presenters::Base - private +module Gitlab + module ChatCommands + module Presenters + class Issuable < Presenters::Base + private - def project - @resource.project - end + def color(issuable) + issuable.open? ? '#38ae67' : '#d22852' + end - def author - @resource.author - end + def status_text(issuable) + issuable.open? ? 'Open' : 'Closed' + end + + def project + @resource.project + end + + def author + @resource.author + end - def fields - [ - { - title: "Assignee", - value: @resource.assignee ? @resource.assignee.name : "_None_", - short: true - }, - { - title: "Milestone", - value: @resource.milestone ? @resource.milestone.title : "_None_", - short: true - }, - { - title: "Labels", - value: @resource.labels.any? ? @resource.label_names : "_None_", - short: true - } - ] + def fields + [ + { + title: "Assignee", + value: @resource.assignee ? @resource.assignee.name : "_None_", + short: true + }, + { + title: "Milestone", + value: @resource.milestone ? @resource.milestone.title : "_None_", + short: true + }, + { + title: "Labels", + value: @resource.labels.any? ? @resource.label_names : "_None_", + short: true + } + ] + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/list_issues.rb b/lib/gitlab/chat_commands/presenters/list_issues.rb index 5a7b3fca5c2..2458b9356b7 100644 --- a/lib/gitlab/chat_commands/presenters/list_issues.rb +++ b/lib/gitlab/chat_commands/presenters/list_issues.rb @@ -1,32 +1,43 @@ -module Gitlab::ChatCommands::Presenters - class ListIssues < Gitlab::ChatCommands::Presenters::Base - def present - ephemeral_response(text: "Here are the issues I found:", attachments: attachments) - end +module Gitlab + module ChatCommands + module Presenters + class ListIssues < Presenters::Issuable + def present + text = if @resource.count >= 5 + "Here are the first 5 issues I found:" + else + "Here are the #{@resource.count} issues I found:" + end - private + ephemeral_response(text: text, attachments: attachments) + end - def attachments - @resource.map do |issue| - state = issue.open? ? "Open" : "Closed" + private - { - fallback: "Issue #{issue.to_reference}: #{issue.title}", - color: "#d22852", - text: "[#{issue.to_reference}](#{url_for([namespace, project, issue])}) · #{issue.title} (#{state})", - mrkdwn_in: [ - "text" - ] - } - end - end + def attachments + @resource.map do |issue| + url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" - def project - @project ||= @resource.first.project - end + { + color: color(issue), + fallback: "#{issue.to_reference} #{issue.title}", + text: "#{url} · #{issue.title} (#{status_text(issue)})", - def namespace - @namespace ||= project.namespace.becomes(Namespace) + mrkdwn_in: [ + "text" + ] + } + end + end + + def project + @project ||= @resource.first.project + end + + def namespace + @namespace ||= project.namespace.becomes(Namespace) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/new_issue.rb b/lib/gitlab/chat_commands/presenters/new_issue.rb new file mode 100644 index 00000000000..c7c6febb56e --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/new_issue.rb @@ -0,0 +1,42 @@ +module Gitlab + module ChatCommands + module Presenters + class NewIssue < Presenters::Issuable + def present + in_channel_response(show_issue) + end + + private + + def show_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :title, + :text + ] + } + ] + } + end + + def pretext + "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" + end + + def author_profile_link + "[#{author.to_reference}](#{url_for(author)})" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/show_issue.rb b/lib/gitlab/chat_commands/presenters/show_issue.rb index 2a89c30b972..e5644a4ad7e 100644 --- a/lib/gitlab/chat_commands/presenters/show_issue.rb +++ b/lib/gitlab/chat_commands/presenters/show_issue.rb @@ -1,38 +1,54 @@ -module Gitlab::ChatCommands::Presenters - class ShowIssue < Gitlab::ChatCommands::Presenters::Issuable - def present - in_channel_response(show_issue) - end +module Gitlab + module ChatCommands + module Presenters + class ShowIssue < Presenters::Issuable + def present + in_channel_response(show_issue) + end - private + private - def show_issue - { - attachments: [ + def show_issue { - title: @resource.title, - title_link: resource_url, - author_name: author.name, - author_icon: author.avatar_url, - fallback: "#{@resource.to_reference}: #{@resource.title}", - text: text, - fields: fields, - mrkdwn_in: [ - :title, - :text + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + text: text, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :pretext, + :text + ] + } ] } - ] - } - end + end + + def text + message = "**#{status_text(@resource)}**" + + if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? + return message + end + + message << " · " + message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero? + message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero? + message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero? - def text - message = "" - message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero? - message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero? - message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero? + message + end - message + def pretext + "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" + end + end end end end diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb index 04d10ad52a1..551ccb79a58 100644 --- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -26,7 +26,7 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do it 'returns all results' do expect(subject).to have_key(:attachments) - expect(subject[:text]).to match("Here are the issues I found:") + expect(subject[:text]).to eq("Here are the 2 issues I found:") end end diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb index 89932c395c6..1f20d0a44ce 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::ChatCommands::IssueShow, service: true do it 'returns the issue' do expect(subject[:response_type]).to be(:in_channel) - expect(title).to eq(issue.title) + expect(title).to start_with(issue.title) end context 'when its reference is given' do @@ -28,7 +28,7 @@ describe Gitlab::ChatCommands::IssueShow, service: true do it 'shows the issue' do expect(subject[:response_type]).to be(:in_channel) - expect(title).to eq(issue.title) + expect(title).to start_with(issue.title) end end end diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb index 1c48c727e30..dc2dd300072 100644 --- a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb @@ -32,7 +32,7 @@ describe Gitlab::ChatCommands::Presenters::Deploy do end describe '#too_many_actions' do - subject { described_class.new(nil).too_many_actions } + subject { described_class.new([]).too_many_actions } it { is_expected.to have_key(:text) } it { is_expected.to have_key(:response_type) } diff --git a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb index 1852395fc97..13a1f70fe78 100644 --- a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb @@ -3,13 +3,12 @@ require 'spec_helper' describe Gitlab::ChatCommands::Presenters::ListIssues do let(:project) { create(:empty_project) } let(:message) { subject[:text] } - let(:issue) { project.issues.first } before { create_list(:issue, 2, project: project) } subject { described_class.new(project.issues).present } - it do + it 'formats the message correct' do is_expected.to have_key(:text) is_expected.to have_key(:status) is_expected.to have_key(:response_type) @@ -19,6 +18,6 @@ describe Gitlab::ChatCommands::Presenters::ListIssues do it 'shows a list of results' do expect(subject[:response_type]).to be(:ephemeral) - expect(message).to start_with("Here are the issues I found") + expect(message).to start_with("Here are the 2 issues I found") end end diff --git a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb index 13a318fe680..ca4062e692a 100644 --- a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::ChatCommands::Presenters::ShowIssue do it 'shows the issue' do expect(subject[:response_type]).to be(:in_channel) expect(subject).to have_key(:attachments) - expect(attachment[:title]).to eq(issue.title) + expect(attachment[:title]).to start_with(issue.title) end context 'with upvotes' do @@ -21,7 +21,7 @@ describe Gitlab::ChatCommands::Presenters::ShowIssue do end it 'shows the upvote count' do - expect(attachment[:text]).to start_with(":+1: 1") + expect(attachment[:text]).to start_with("**Open** · :+1: 1") end end end diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb new file mode 100644 index 00000000000..dc11a414717 --- /dev/null +++ b/spec/lib/mattermost/client_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Mattermost::Client do + let(:user) { build(:user) } + + subject { described_class.new(user) } + + context 'JSON parse error' do + before do + Struct.new("Request", :body, :success?) + end + + it 'yields an error on malformed JSON' do + bad_json = Struct::Request.new("I'm not json", true) + expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError) + end + + it 'shows a client error if the request was unsuccessful' do + bad_request = Struct::Request.new("true", false) + + expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError) + end + end +end diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb new file mode 100644 index 00000000000..5ccf1100898 --- /dev/null +++ b/spec/lib/mattermost/command_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Mattermost::Command do + let(:params) { { 'token' => 'token', team_id: 'abc' } } + + before do + Mattermost::Session.base_uri('http://mattermost.example.com') + + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) + end + + describe '#create' do + let(:params) do + { team_id: 'abc', + trigger: 'gitlab' + } + end + + subject { described_class.new(nil).create(params) } + + context 'for valid trigger word' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + with(body: { + team_id: 'abc', + trigger: 'gitlab' }.to_json). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { token: 'token' }.to_json + ) + end + + it 'returns a token' do + is_expected.to eq('token') + end + end + + context 'for error message' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + id: 'api.command.duplicate_trigger.app_error', + message: 'This trigger word is already in use. Please choose another word.', + detailed_error: '', + request_id: 'obc374man7bx5r3dbc1q5qhf3r', + status_code: 500 + }.to_json + ) + end + + it 'raises an error with message' do + expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.') + end + end + end +end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb new file mode 100644 index 00000000000..74d12e37181 --- /dev/null +++ b/spec/lib/mattermost/session_spec.rb @@ -0,0 +1,123 @@ +require 'spec_helper' + +describe Mattermost::Session, type: :request do + let(:user) { create(:user) } + + let(:gitlab_url) { "http://gitlab.com" } + let(:mattermost_url) { "http://mattermost.com" } + + subject { described_class.new(user) } + + # Needed for doorkeeper to function + it { is_expected.to respond_to(:current_resource_owner) } + it { is_expected.to respond_to(:request) } + it { is_expected.to respond_to(:authorization) } + it { is_expected.to respond_to(:strategy) } + + before do + described_class.base_uri(mattermost_url) + end + + describe '#with session' do + let(:location) { 'http://location.tld' } + let!(:stub) do + WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login"). + to_return(headers: { 'location' => location }, status: 307) + end + + context 'without oauth uri' do + it 'makes a request to the oauth uri' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with oauth_uri' do + let!(:doorkeeper) do + Doorkeeper::Application.create( + name: "GitLab Mattermost", + redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete", + scopes: "") + end + + context 'without token_uri' do + it 'can not create a session' do + expect do + subject.with_session + end.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with token_uri' do + let(:state) { "state" } + let(:params) do + { response_type: "code", + client_id: doorkeeper.uid, + redirect_uri: "#{mattermost_url}/signup/gitlab/complete", + state: state } + end + let(:location) do + "#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}" + end + + before do + WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete"). + with(query: hash_including({ 'state' => state })). + to_return do |request| + post "/oauth/token", + client_id: doorkeeper.uid, + client_secret: doorkeeper.secret, + redirect_uri: params[:redirect_uri], + grant_type: 'authorization_code', + code: request.uri.query_values['code'] + + if response.status == 200 + { headers: { 'token' => 'thisworksnow' }, status: 202 } + end + end + + WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout"). + to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) + end + + it 'can setup a session' do + subject.with_session do |session| + end + + expect(subject.token).not_to be_nil + end + + it 'returns the value of the block' do + result = subject.with_session do |session| + "value" + end + + expect(result).to eq("value") + end + end + end + + context 'with lease' do + before do + allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk') + end + + it 'tries to obtain a lease' do + expect(subject).to receive(:lease_try_obtain) + expect(Gitlab::ExclusiveLease).to receive(:cancel) + + # Cannot setup a session, but we should still cancel the lease + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'without lease' do + before do + allow(subject).to receive(:lease_try_obtain).and_return(nil) + end + + it 'returns a NoSessionError error' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + end +end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb new file mode 100644 index 00000000000..2d14be6bcc2 --- /dev/null +++ b/spec/lib/mattermost/team_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Mattermost::Team do + before do + Mattermost::Session.base_uri('http://mattermost.example.com') + + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) + end + + describe '#all' do + subject { described_class.new(nil).all } + + context 'for valid request' do + let(:response) do + [{ + "id" => "xiyro8huptfhdndadpz8r3wnbo", + "create_at" => 1482174222155, + "update_at" => 1482174222155, + "delete_at" => 0, + "display_name" => "chatops", + "name" => "chatops", + "email" => "admin@example.com", + "type" => "O", + "company_name" => "", + "allowed_domains" => "", + "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", + "allow_open_invite" => false }] + end + + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: response.to_json + ) + end + + it 'returns a token' do + is_expected.to eq(response) + end + end + + context 'for error message' do + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + id: 'api.team.list.app_error', + message: 'Cannot list teams.', + detailed_error: '', + request_id: 'obc374man7bx5r3dbc1q5qhf3r', + status_code: 500 + }.to_json + ) + end + + it 'raises an error with message' do + expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.') + end + end + end +end -- cgit v1.2.1 From 6a3d29c73d2578c7b2a40f2dfcb823b681a70f7e Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Mon, 14 Nov 2016 01:56:39 -0200 Subject: Add ability to define a coverage regex in the .gitlab-ci.yml * Instead of using the proposed `coverage` key, this expects `coverage_regex` --- app/models/ci/build.rb | 7 ++++-- .../20161114024742_add_coverage_regex_to_builds.rb | 13 +++++++++++ db/schema.rb | 1 + lib/ci/gitlab_ci_yaml_processor.rb | 1 + lib/gitlab/ci/config/entry/job.rb | 8 +++++-- .../ci/config/entry/legacy_validation_helpers.rb | 10 ++++++--- lib/gitlab/ci/config/entry/validators.rb | 10 +++++++++ lib/gitlab/ci/config/node/regexp.rb | 26 ++++++++++++++++++++++ 8 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 db/migrate/20161114024742_add_coverage_regex_to_builds.rb create mode 100644 lib/gitlab/ci/config/node/regexp.rb diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5fe8ddf69d7..4691b33ee9b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -276,8 +276,7 @@ module Ci def update_coverage return unless project - coverage_regex = project.build_coverage_regex - return unless coverage_regex + return unless coverage_regex = self.coverage_regex coverage = extract_coverage(trace, coverage_regex) if coverage.is_a? Numeric @@ -522,6 +521,10 @@ module Ci self.update(artifacts_expire_at: nil) end + def coverage_regex + read_attribute(:coverage_regex) || build_attributes_from_config[:coverage] || project.build_coverage_regex + end + def when read_attribute(:when) || build_attributes_from_config[:when] || 'on_success' end diff --git a/db/migrate/20161114024742_add_coverage_regex_to_builds.rb b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb new file mode 100644 index 00000000000..88aa5d52b39 --- /dev/null +++ b/db/migrate/20161114024742_add_coverage_regex_to_builds.rb @@ -0,0 +1,13 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddCoverageRegexToBuilds < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + add_column :ci_builds, :coverage_regex, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 3c836db27fc..1cc9e7eec5e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -215,6 +215,7 @@ ActiveRecord::Schema.define(version: 20170121130655) do t.datetime "queued_at" t.string "token" t.integer "lock_version" + t.string "coverage_regex" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 7463bd719d5..2f5ef4d36bd 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -61,6 +61,7 @@ module Ci allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment_name], + coverage_regex: job[:coverage_regex], yaml_variables: yaml_variables(name), options: { image: job[:image], diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index a55362f0b6b..3c7ef99cefc 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -11,7 +11,7 @@ module Gitlab ALLOWED_KEYS = %i[tags script only except type image services allow_failure type stage when artifacts cache dependencies before_script - after_script variables environment] + after_script variables environment coverage_regex] validations do validates :config, allowed_keys: ALLOWED_KEYS @@ -71,9 +71,12 @@ module Gitlab entry :environment, Entry::Environment, description: 'Environment configuration for this job.' + node :coverage_regex, Node::Regexp, + description: 'Coverage scanning regex configuration for this job.' + helpers :before_script, :script, :stage, :type, :after_script, :cache, :image, :services, :only, :except, :variables, - :artifacts, :commands, :environment + :artifacts, :commands, :environment, :coverage_regex attributes :script, :tags, :allow_failure, :when, :dependencies @@ -130,6 +133,7 @@ module Gitlab variables: variables_defined? ? variables_value : nil, environment: environment_defined? ? environment_value : nil, environment_name: environment_defined? ? environment_value[:name] : nil, + coverage_regex: coverage_regex_defined? ? coverage_regex_value : nil, artifacts: artifacts_value, after_script: after_script_value } end diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb index f01975aab5c..34e7052befc 100644 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb @@ -28,17 +28,21 @@ module Gitlab value.is_a?(String) || value.is_a?(Symbol) end + def validate_regexp(value) + !!::Regexp.new(value) + rescue RegexpError + false + end + def validate_string_or_regexp(value) return true if value.is_a?(Symbol) return false unless value.is_a?(String) if value.first == '/' && value.last == '/' - Regexp.new(value[1...-1]) + validate_regexp(value[1...-1]) else true end - rescue RegexpError - false end def validate_boolean(value) diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 8632dd0e233..03a8205b081 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -54,6 +54,16 @@ module Gitlab end end + class RegexpValidator < ActiveModel::EachValidator + include LegacyValidationHelpers + + def validate_each(record, attribute, value) + unless validate_regexp(value) + record.errors.add(attribute, 'must be a regular expression') + end + end + end + class TypeValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) type = options[:with] diff --git a/lib/gitlab/ci/config/node/regexp.rb b/lib/gitlab/ci/config/node/regexp.rb new file mode 100644 index 00000000000..7c5843eb8b6 --- /dev/null +++ b/lib/gitlab/ci/config/node/regexp.rb @@ -0,0 +1,26 @@ +module Gitlab + module Ci + class Config + module Node + ## + # Entry that represents a Regular Expression. + # + class Regexp < Entry + include Validatable + + validations do + validates :config, regexp: true + end + + def value + if @config.first == '/' && @config.last == '/' + @config[1...-1] + else + @config + end + end + end + end + end + end +end -- cgit v1.2.1 From 646b9c54d043edf17924e82d8e80a56e18d14ce4 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Fri, 18 Nov 2016 01:42:35 -0200 Subject: Comply to requests made in the review and adjust to the Entry/Node changes This commit: * Turns `coverage_regex` into `coverage` entry in yml file * Fixes smaller requests from code reviewers for the previous commit * This commit is temporary (will be squashed afterwards) This commit does not (further commits will do though): * Add global `coverage` entry handling in yml file as suggested by Grzegorz * Add specs * Create changelog * Create docs --- app/models/ci/build.rb | 6 ++--- lib/ci/gitlab_ci_yaml_processor.rb | 2 +- lib/gitlab/ci/config/entry/coverage.rb | 26 ++++++++++++++++++++++ lib/gitlab/ci/config/entry/job.rb | 8 +++---- .../ci/config/entry/legacy_validation_helpers.rb | 3 ++- lib/gitlab/ci/config/node/regexp.rb | 26 ---------------------- 6 files changed, 36 insertions(+), 35 deletions(-) create mode 100644 lib/gitlab/ci/config/entry/coverage.rb delete mode 100644 lib/gitlab/ci/config/node/regexp.rb diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 4691b33ee9b..46a6b4c724a 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -276,8 +276,8 @@ module Ci def update_coverage return unless project - return unless coverage_regex = self.coverage_regex - coverage = extract_coverage(trace, coverage_regex) + return unless regex = self.coverage_regex + coverage = extract_coverage(trace, regex) if coverage.is_a? Numeric update_attributes(coverage: coverage) @@ -522,7 +522,7 @@ module Ci end def coverage_regex - read_attribute(:coverage_regex) || build_attributes_from_config[:coverage] || project.build_coverage_regex + read_attribute(:coverage_regex) || project.build_coverage_regex end def when diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 2f5ef4d36bd..649ee4d018b 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -61,7 +61,7 @@ module Ci allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment_name], - coverage_regex: job[:coverage_regex], + coverage_regex: job[:coverage], yaml_variables: yaml_variables(name), options: { image: job[:image], diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb new file mode 100644 index 00000000000..88fc03db2d9 --- /dev/null +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -0,0 +1,26 @@ +module Gitlab + module Ci + class Config + module Entry + ## + # Entry that represents a Regular Expression. + # + class Coverage < Node + include Validatable + + validations do + validates :config, regexp: true + end + + def value + if @config.first == '/' && @config.last == '/' + @config[1...-1] + else + @config + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 3c7ef99cefc..bde6663344a 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -11,7 +11,7 @@ module Gitlab ALLOWED_KEYS = %i[tags script only except type image services allow_failure type stage when artifacts cache dependencies before_script - after_script variables environment coverage_regex] + after_script variables environment coverage] validations do validates :config, allowed_keys: ALLOWED_KEYS @@ -71,12 +71,12 @@ module Gitlab entry :environment, Entry::Environment, description: 'Environment configuration for this job.' - node :coverage_regex, Node::Regexp, + entry :coverage, Entry::Coverage, description: 'Coverage scanning regex configuration for this job.' helpers :before_script, :script, :stage, :type, :after_script, :cache, :image, :services, :only, :except, :variables, - :artifacts, :commands, :environment, :coverage_regex + :artifacts, :commands, :environment, :coverage attributes :script, :tags, :allow_failure, :when, :dependencies @@ -133,7 +133,7 @@ module Gitlab variables: variables_defined? ? variables_value : nil, environment: environment_defined? ? environment_value : nil, environment_name: environment_defined? ? environment_value[:name] : nil, - coverage_regex: coverage_regex_defined? ? coverage_regex_value : nil, + coverage: coverage_defined? ? coverage_value : nil, artifacts: artifacts_value, after_script: after_script_value } end diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb index 34e7052befc..98db4632dad 100644 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb @@ -29,7 +29,8 @@ module Gitlab end def validate_regexp(value) - !!::Regexp.new(value) + Regexp.new(value) + true rescue RegexpError false end diff --git a/lib/gitlab/ci/config/node/regexp.rb b/lib/gitlab/ci/config/node/regexp.rb deleted file mode 100644 index 7c5843eb8b6..00000000000 --- a/lib/gitlab/ci/config/node/regexp.rb +++ /dev/null @@ -1,26 +0,0 @@ -module Gitlab - module Ci - class Config - module Node - ## - # Entry that represents a Regular Expression. - # - class Regexp < Entry - include Validatable - - validations do - validates :config, regexp: true - end - - def value - if @config.first == '/' && @config.last == '/' - @config[1...-1] - else - @config - end - end - end - end - end - end -end -- cgit v1.2.1 From d0afc500e30ad0fe334d6dc16dd1766d8f6c523a Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Sat, 19 Nov 2016 22:48:02 -0200 Subject: Change expected `coverage` structure for CI configuration YAML file Instead of using: `coverage: /\(\d+.\d+%\) covered/` This structure must be used now: ``` coverage: output_filter: /\(\d+.\d+%\) covered/` ``` The surrounding '/' is optional. --- lib/ci/gitlab_ci_yaml_processor.rb | 2 +- lib/gitlab/ci/config/entry/coverage.rb | 20 +++++++++++++++----- lib/gitlab/ci/config/entry/job.rb | 2 +- .../ci/config/entry/legacy_validation_helpers.rb | 4 ++-- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 649ee4d018b..02944e0385a 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -61,7 +61,7 @@ module Ci allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment_name], - coverage_regex: job[:coverage], + coverage_regex: job[:coverage][:output_filter], yaml_variables: yaml_variables(name), options: { image: job[:image], diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index 88fc03db2d9..e5da3cf23fd 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -8,17 +8,27 @@ module Gitlab class Coverage < Node include Validatable + ALLOWED_KEYS = %i[output_filter] + validations do - validates :config, regexp: true + validates :config, type: Hash + validates :config, allowed_keys: ALLOWED_KEYS + validates :output_filter, regexp: true end - def value - if @config.first == '/' && @config.last == '/' - @config[1...-1] + def output_filter + output_filter_value = @config[:output_filter].to_s + + if output_filter_value.start_with?('/') && output_filter_value.end_with?('/') + output_filter_value[1...-1] else - @config + value[:output_filter] end end + + def value + @config.merge(output_filter: output_filter) + end end end end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index bde6663344a..69a5e6f433d 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -72,7 +72,7 @@ module Gitlab description: 'Environment configuration for this job.' entry :coverage, Entry::Coverage, - description: 'Coverage scanning regex configuration for this job.' + description: 'Coverage configuration for this job.' helpers :before_script, :script, :stage, :type, :after_script, :cache, :image, :services, :only, :except, :variables, diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb index 98db4632dad..d8e74b15712 100644 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb @@ -31,7 +31,7 @@ module Gitlab def validate_regexp(value) Regexp.new(value) true - rescue RegexpError + rescue RegexpError, TypeError false end @@ -39,7 +39,7 @@ module Gitlab return true if value.is_a?(Symbol) return false unless value.is_a?(String) - if value.first == '/' && value.last == '/' + if value.start_with?('/') && value.end_with?('/') validate_regexp(value[1...-1]) else true -- cgit v1.2.1 From 9f97cc6515ac1254c443673c84700942690bbc15 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Sun, 20 Nov 2016 00:05:49 -0200 Subject: Add `coverage` to the Global config entry as well --- lib/gitlab/ci/config/entry/global.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb index a4ec8f0ff2f..ede97cc0504 100644 --- a/lib/gitlab/ci/config/entry/global.rb +++ b/lib/gitlab/ci/config/entry/global.rb @@ -33,8 +33,11 @@ module Gitlab entry :cache, Entry::Cache, description: 'Configure caching between build jobs.' + entry :coverage, Entry::Coverage, + description: 'Coverage configuration for this pipeline.' + helpers :before_script, :image, :services, :after_script, - :variables, :stages, :types, :cache, :jobs + :variables, :stages, :types, :cache, :coverage, :jobs def compose!(_deps = nil) super(self) do -- cgit v1.2.1 From 94eb2f47c732dc9485aba4ebe52238e882a43473 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Sun, 20 Nov 2016 02:18:58 -0200 Subject: Add changelog entry and extend the documentation accordingly --- changelogs/unreleased/issue-20428.yml | 4 +++ doc/ci/yaml/README.md | 51 +++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 changelogs/unreleased/issue-20428.yml diff --git a/changelogs/unreleased/issue-20428.yml b/changelogs/unreleased/issue-20428.yml new file mode 100644 index 00000000000..60da1c14702 --- /dev/null +++ b/changelogs/unreleased/issue-20428.yml @@ -0,0 +1,4 @@ +--- +title: Add ability to define a coverage regex in the .gitlab-ci.yml +merge_request: 7447 +author: Leandro Camargo diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 75a0897eb15..a8c0721bbcc 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -76,6 +76,7 @@ There are a few reserved `keywords` that **cannot** be used as job names: | after_script | no | Define commands that run after each job's script | | variables | no | Define build variables | | cache | no | Define list of files that should be cached between subsequent runs | +| coverage | no | Define coverage settings for all jobs | ### image and services @@ -278,6 +279,33 @@ cache: untracked: true ``` +### coverage + +`coverage` allows you to configure how coverage will be filtered out from the +build outputs. Setting this up globally will make all the jobs to use this +setting for output filtering and extracting the coverage information from your +builds. + +#### coverage:output_filter + +For now, there is only the `output_filter` directive expected to be inside the +`coverage` entry. And it is expected to be a regular expression. + +So, in the end, you're going to have something like the following: + +```yaml +coverage: + output_filter: /\(\d+\.\d+\) covered\./ +``` + +It's worth to keep in mind that the surrounding `/` is optional. So, the above +example is the same as the following: + +```yaml +coverage: + output_filter: \(\d+\.\d+\) covered\. +``` + ## Jobs `.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job @@ -319,6 +347,8 @@ job_name: | before_script | no | Override a set of commands that are executed before build | | after_script | no | Override a set of commands that are executed after build | | environment | no | Defines a name of environment to which deployment is done by this build | +| environment | no | Defines a name of environment to which deployment is done by this build | +| coverage | no | Define coverage settings for a given job | ### script @@ -993,6 +1023,27 @@ job: - execute this after my script ``` +### job coverage + +This entry is pretty much the same as described in the global context in +[`coverage`](#coverage). The only difference is that, by setting it inside +the job level, whatever is set in there will take precedence over what has +been defined in the global level. A quick example of one overwritting the +other would be: + +```yaml +coverage: + output_filter: /\(\d+\.\d+\) covered\./ + +job1: + coverage: + output_filter: /Code coverage: \d+\.\d+/ +``` + +In the example above, considering the context of the job `job1`, the coverage +regex that would be used is `/Code coverage: \d+\.\d+/` instead of +`/\(\d+\.\d+\) covered\./`. + ## Git Strategy > Introduced in GitLab 8.9 as an experimental feature. May change or be removed -- cgit v1.2.1 From 0713a7c3a9eb1bcfdf6adde0c3365549c19a3ee1 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Mon, 21 Nov 2016 02:38:03 -0200 Subject: Add specs to cover the implemented feature and fix a small bug --- lib/gitlab/ci/config/entry/coverage.rb | 2 +- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 31 ++++++++++++++++++ spec/lib/gitlab/ci/config/entry/coverage_spec.rb | 40 ++++++++++++++++++++++++ spec/lib/gitlab/ci/config/entry/global_spec.rb | 17 +++++++--- spec/lib/gitlab/ci/config/entry/job_spec.rb | 14 +++++++++ spec/models/ci/build_spec.rb | 33 +++++++++++++++++++ 6 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 spec/lib/gitlab/ci/config/entry/coverage_spec.rb diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index e5da3cf23fd..af12837130c 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -22,7 +22,7 @@ module Gitlab if output_filter_value.start_with?('/') && output_filter_value.end_with?('/') output_filter_value[1...-1] else - value[:output_filter] + @config[:output_filter] 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 f824e2e1efe..eb2d9c6e0e3 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -4,6 +4,37 @@ module Ci describe GitlabCiYamlProcessor, lib: true do let(:path) { 'path' } + describe '#build_attributes' do + context 'Coverage entry' do + subject { described_class.new(config, path).build_attributes(:rspec) } + + let(:config_base) { { rspec: { script: "rspec" } } } + let(:config) { YAML.dump(config_base) } + + context 'when config has coverage set at the global scope' do + before do + config_base.update( + coverage: { output_filter: '\(\d+\.\d+\) covered' } + ) + end + + context 'and \'rspec\' job doesn\'t have coverage set' do + it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } + end + + context 'but \'rspec\' job also has coverage set' do + before do + config_base[:rspec].update( + coverage: { output_filter: '/Code coverage: \d+\.\d+/' } + ) + end + + it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } + end + end + end + end + describe "#builds_for_ref" do let(:type) { 'test' } diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb new file mode 100644 index 00000000000..9e59755d9f8 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Entry::Coverage do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context 'when entry config value is correct' do + let(:config) { { output_filter: 'Code coverage: \d+\.\d+' } } + + describe '#value' do + subject { entry.value } + it { is_expected.to eq config } + end + + describe '#errors' do + subject { entry.errors } + it { is_expected.to be_empty } + end + + describe '#valid?' do + subject { entry } + it { is_expected.to be_valid } + end + end + + context 'when entry value is not correct' do + let(:config) { { output_filter: '(malformed regexp' } } + + describe '#errors' do + subject { entry.errors } + it { is_expected.to include /coverage output filter must be a regular expression/ } + end + + describe '#valid?' do + subject { entry } + it { is_expected.not_to be_valid } + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index e64c8d46bd8..66a1380bc61 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -4,12 +4,19 @@ describe Gitlab::Ci::Config::Entry::Global do let(:global) { described_class.new(hash) } describe '.nodes' do - it 'can contain global config keys' do - expect(described_class.nodes).to include :before_script - end + subject { described_class.nodes } + + it { is_expected.to be_a Hash } + + context 'when filtering all the entry/node names' do + subject { described_class.nodes.keys } + + let(:result) do + %i[before_script image services after_script variables stages types + cache coverage] + end - it 'returns a hash' do - expect(described_class.nodes).to be_a Hash + it { is_expected.to match_array result } end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index fc9b8b86dc4..d20f4ec207d 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -3,6 +3,20 @@ require 'spec_helper' describe Gitlab::Ci::Config::Entry::Job do let(:entry) { described_class.new(config, name: :rspec) } + describe '.nodes' do + context 'when filtering all the entry/node names' do + subject { described_class.nodes.keys } + + let(:result) do + %i[before_script script stage type after_script cache + image services only except variables artifacts + environment coverage] + end + + it { is_expected.to match_array result } + end + end + describe 'validations' do before { entry.compose! } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index f031876e812..9e5481017e2 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -221,6 +221,39 @@ describe Ci::Build, :models do end end + describe '#coverage_regex' do + subject { build.coverage_regex } + let(:project_regex) { '\(\d+\.\d+\) covered' } + let(:build_regex) { 'Code coverage: \d+\.\d+' } + + context 'when project has build_coverage_regex set' do + before { project.build_coverage_regex = project_regex } + + context 'and coverage_regex attribute is not set' do + it { is_expected.to eq(project_regex) } + end + + context 'but coverage_regex attribute is also set' do + before { build.coverage_regex = build_regex } + it { is_expected.to eq(build_regex) } + end + end + + context 'when neither project nor build has coverage regex set' do + it { is_expected.to be_nil } + end + end + + describe '#update_coverage' do + it 'grants coverage_regex method is called inside of it' do + build.coverage_regex = '\(\d+.\d+\%\) covered' + allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } + allow(build).to receive(:coverage_regex).and_call_original + allow(build).to receive(:update_attributes).with(coverage: 98.29) { true } + expect(build.update_coverage).to be true + end + end + describe 'deployment' do describe '#last_deployment' do subject { build.last_deployment } -- cgit v1.2.1 From bb12ee051f95ee747c0e2b98a85675de53dca8ea Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Mon, 21 Nov 2016 02:51:29 -0200 Subject: Fix wrong description for Coverage entry (in ruby comments) --- lib/gitlab/ci/config/entry/coverage.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index af12837130c..41e1d6e0c86 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -3,7 +3,7 @@ module Gitlab class Config module Entry ## - # Entry that represents a Regular Expression. + # Entry that represents Coverage settings. # class Coverage < Node include Validatable -- cgit v1.2.1 From f1e920ed86133bfea0abfc66ca44282813822073 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Sat, 26 Nov 2016 01:02:08 -0200 Subject: Simplify coverage setting and comply to some requests in code review --- doc/ci/yaml/README.md | 30 ++++++------------------ lib/ci/gitlab_ci_yaml_processor.rb | 2 +- lib/gitlab/ci/config/entry/coverage.rb | 20 ++++------------ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 17 ++++---------- spec/lib/gitlab/ci/config/entry/coverage_spec.rb | 14 +++++------ spec/models/ci/build_spec.rb | 7 +++--- 6 files changed, 28 insertions(+), 62 deletions(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index a8c0721bbcc..0a264c0e228 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -286,24 +286,11 @@ build outputs. Setting this up globally will make all the jobs to use this setting for output filtering and extracting the coverage information from your builds. -#### coverage:output_filter - -For now, there is only the `output_filter` directive expected to be inside the -`coverage` entry. And it is expected to be a regular expression. - -So, in the end, you're going to have something like the following: +Regular expressions are used by default. So using surrounding `/` is optional, given it'll always be read as a regular expression. Don't forget to escape special characters whenever you want to match them in the regular expression. +A simple example: ```yaml -coverage: - output_filter: /\(\d+\.\d+\) covered\./ -``` - -It's worth to keep in mind that the surrounding `/` is optional. So, the above -example is the same as the following: - -```yaml -coverage: - output_filter: \(\d+\.\d+\) covered\. +coverage: \(\d+\.\d+\) covered\. ``` ## Jobs @@ -347,7 +334,6 @@ job_name: | before_script | no | Override a set of commands that are executed before build | | after_script | no | Override a set of commands that are executed after build | | environment | no | Defines a name of environment to which deployment is done by this build | -| environment | no | Defines a name of environment to which deployment is done by this build | | coverage | no | Define coverage settings for a given job | ### script @@ -1032,17 +1018,15 @@ been defined in the global level. A quick example of one overwritting the other would be: ```yaml -coverage: - output_filter: /\(\d+\.\d+\) covered\./ +coverage: \(\d+\.\d+\) covered\. job1: - coverage: - output_filter: /Code coverage: \d+\.\d+/ + coverage: Code coverage: \d+\.\d+ ``` In the example above, considering the context of the job `job1`, the coverage -regex that would be used is `/Code coverage: \d+\.\d+/` instead of -`/\(\d+\.\d+\) covered\./`. +regex that would be used is `Code coverage: \d+\.\d+` instead of +`\(\d+\.\d+\) covered\.`. ## Git Strategy diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb index 02944e0385a..649ee4d018b 100644 --- a/lib/ci/gitlab_ci_yaml_processor.rb +++ b/lib/ci/gitlab_ci_yaml_processor.rb @@ -61,7 +61,7 @@ module Ci allow_failure: job[:allow_failure] || false, when: job[:when] || 'on_success', environment: job[:environment_name], - coverage_regex: job[:coverage][:output_filter], + coverage_regex: job[:coverage], yaml_variables: yaml_variables(name), options: { image: job[:image], diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index 41e1d6e0c86..aa738fcfd11 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -8,27 +8,17 @@ module Gitlab class Coverage < Node include Validatable - ALLOWED_KEYS = %i[output_filter] - validations do - validates :config, type: Hash - validates :config, allowed_keys: ALLOWED_KEYS - validates :output_filter, regexp: true + validates :config, regexp: true end - def output_filter - output_filter_value = @config[:output_filter].to_s - - if output_filter_value.start_with?('/') && output_filter_value.end_with?('/') - output_filter_value[1...-1] + def value + if @config.start_with?('/') && @config.end_with?('/') + @config[1...-1] else - @config[:output_filter] + @config end end - - def value - @config.merge(output_filter: output_filter) - 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 eb2d9c6e0e3..ac706216d5a 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -9,26 +9,17 @@ module Ci subject { described_class.new(config, path).build_attributes(:rspec) } let(:config_base) { { rspec: { script: "rspec" } } } - let(:config) { YAML.dump(config_base) } + let(:config) { YAML.dump(config_base) } context 'when config has coverage set at the global scope' do - before do - config_base.update( - coverage: { output_filter: '\(\d+\.\d+\) covered' } - ) - end + before { config_base.update(coverage: '\(\d+\.\d+\) covered') } - context 'and \'rspec\' job doesn\'t have coverage set' do + context "and 'rspec' job doesn't have coverage set" do it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } end context 'but \'rspec\' job also has coverage set' do - before do - config_base[:rspec].update( - coverage: { output_filter: '/Code coverage: \d+\.\d+/' } - ) - end - + before { config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' } it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } end end diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb index 9e59755d9f8..0549dbc732b 100644 --- a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb @@ -5,35 +5,35 @@ describe Gitlab::Ci::Config::Entry::Coverage do describe 'validations' do context 'when entry config value is correct' do - let(:config) { { output_filter: 'Code coverage: \d+\.\d+' } } + let(:config) { 'Code coverage: \d+\.\d+' } describe '#value' do subject { entry.value } - it { is_expected.to eq config } + it { is_expected.to eq config } end describe '#errors' do subject { entry.errors } - it { is_expected.to be_empty } + it { is_expected.to be_empty } end describe '#valid?' do subject { entry } - it { is_expected.to be_valid } + it { is_expected.to be_valid } end end context 'when entry value is not correct' do - let(:config) { { output_filter: '(malformed regexp' } } + let(:config) { '(malformed regexp' } describe '#errors' do subject { entry.errors } - it { is_expected.to include /coverage output filter must be a regular expression/ } + it { is_expected.to include /coverage config must be a regular expression/ } end describe '#valid?' do subject { entry } - it { is_expected.not_to be_valid } + it { is_expected.not_to be_valid } end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 9e5481017e2..7c054dd95f5 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -222,9 +222,10 @@ describe Ci::Build, :models do end describe '#coverage_regex' do - subject { build.coverage_regex } + subject { build.coverage_regex } + let(:project_regex) { '\(\d+\.\d+\) covered' } - let(:build_regex) { 'Code coverage: \d+\.\d+' } + let(:build_regex) { 'Code coverage: \d+\.\d+' } context 'when project has build_coverage_regex set' do before { project.build_coverage_regex = project_regex } @@ -235,7 +236,7 @@ describe Ci::Build, :models do context 'but coverage_regex attribute is also set' do before { build.coverage_regex = build_regex } - it { is_expected.to eq(build_regex) } + it { is_expected.to eq(build_regex) } end end -- cgit v1.2.1 From 6323cd7203dbf1850e7939e81db4b1a9c6cf6d76 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Mon, 5 Dec 2016 02:00:47 -0200 Subject: Comply to more requirements and requests made in the code review --- app/models/ci/build.rb | 2 +- doc/ci/yaml/README.md | 16 +++++++++------- lib/gitlab/ci/config/entry/coverage.rb | 2 +- lib/gitlab/ci/config/entry/legacy_validation_helpers.rb | 5 ++--- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 9 +++++++-- spec/models/ci/build_spec.rb | 11 ++++++++--- 6 files changed, 28 insertions(+), 17 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 46a6b4c724a..951818ad561 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -522,7 +522,7 @@ module Ci end def coverage_regex - read_attribute(:coverage_regex) || project.build_coverage_regex + super || project.build_coverage_regex end def when diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 0a264c0e228..5e2d9788f33 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -286,11 +286,13 @@ build outputs. Setting this up globally will make all the jobs to use this setting for output filtering and extracting the coverage information from your builds. -Regular expressions are used by default. So using surrounding `/` is optional, given it'll always be read as a regular expression. Don't forget to escape special characters whenever you want to match them in the regular expression. +Regular expressions are used by default. So using surrounding `/` is optional, +given it'll always be read as a regular expression. Don't forget to escape +special characters whenever you want to match them literally. A simple example: ```yaml -coverage: \(\d+\.\d+\) covered\. +coverage: /\(\d+\.\d+\) covered\./ ``` ## Jobs @@ -1014,19 +1016,19 @@ job: This entry is pretty much the same as described in the global context in [`coverage`](#coverage). The only difference is that, by setting it inside the job level, whatever is set in there will take precedence over what has -been defined in the global level. A quick example of one overwritting the +been defined in the global level. A quick example of one overriding the other would be: ```yaml -coverage: \(\d+\.\d+\) covered\. +coverage: /\(\d+\.\d+\) covered\./ job1: - coverage: Code coverage: \d+\.\d+ + coverage: /Code coverage: \d+\.\d+/ ``` In the example above, considering the context of the job `job1`, the coverage -regex that would be used is `Code coverage: \d+\.\d+` instead of -`\(\d+\.\d+\) covered\.`. +regex that would be used is `/Code coverage: \d+\.\d+/` instead of +`/\(\d+\.\d+\) covered\./`. ## Git Strategy diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index aa738fcfd11..706bfc882de 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -13,7 +13,7 @@ module Gitlab end def value - if @config.start_with?('/') && @config.end_with?('/') + if @config.first == '/' && @config.last == '/' @config[1...-1] else @config diff --git a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb index d8e74b15712..9b9a0a8125a 100644 --- a/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb +++ b/lib/gitlab/ci/config/entry/legacy_validation_helpers.rb @@ -29,8 +29,7 @@ module Gitlab end def validate_regexp(value) - Regexp.new(value) - true + !value.nil? && Regexp.new(value.to_s) && true rescue RegexpError, TypeError false end @@ -39,7 +38,7 @@ module Gitlab return true if value.is_a?(Symbol) return false unless value.is_a?(String) - if value.start_with?('/') && value.end_with?('/') + if value.first == '/' && value.last == '/' validate_regexp(value[1...-1]) else true diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index ac706216d5a..3ffcfaa1f29 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -12,14 +12,19 @@ module Ci let(:config) { YAML.dump(config_base) } context 'when config has coverage set at the global scope' do - before { config_base.update(coverage: '\(\d+\.\d+\) covered') } + before do + config_base.update(coverage: '\(\d+\.\d+\) covered') + end context "and 'rspec' job doesn't have coverage set" do it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } end context 'but \'rspec\' job also has coverage set' do - before { config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' } + before do + config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' + end + it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 7c054dd95f5..7baaef9c85e 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -228,14 +228,19 @@ describe Ci::Build, :models do let(:build_regex) { 'Code coverage: \d+\.\d+' } context 'when project has build_coverage_regex set' do - before { project.build_coverage_regex = project_regex } + before do + project.build_coverage_regex = project_regex + end context 'and coverage_regex attribute is not set' do it { is_expected.to eq(project_regex) } end context 'but coverage_regex attribute is also set' do - before { build.coverage_regex = build_regex } + before do + build.coverage_regex = build_regex + end + it { is_expected.to eq(build_regex) } end end @@ -250,7 +255,7 @@ describe Ci::Build, :models do build.coverage_regex = '\(\d+.\d+\%\) covered' allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } allow(build).to receive(:coverage_regex).and_call_original - allow(build).to receive(:update_attributes).with(coverage: 98.29) { true } + expect(build).to receive(:update_attributes).with(coverage: 98.29) { true } expect(build.update_coverage).to be true end end -- cgit v1.2.1 From f8bec0d1fb05d2c3e87a0470579ee7a650ade23c Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Tue, 6 Dec 2016 02:39:59 -0200 Subject: Improve specs styles/organization and add some more specs --- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- spec/lib/gitlab/ci/config/entry/coverage_spec.rb | 25 +++++++++++++++++++++--- spec/lib/gitlab/ci/config/entry/global_spec.rb | 18 ++++++++--------- spec/models/ci/build_spec.rb | 13 ++++++------ 4 files changed, 38 insertions(+), 20 deletions(-) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 3ffcfaa1f29..b1e09350847 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -20,7 +20,7 @@ module Ci it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } end - context 'but \'rspec\' job also has coverage set' do + context "but 'rspec' job also has coverage set" do before do config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' end diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb index 0549dbc732b..8f989ebd732 100644 --- a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb @@ -4,12 +4,31 @@ describe Gitlab::Ci::Config::Entry::Coverage do let(:entry) { described_class.new(config) } describe 'validations' do - context 'when entry config value is correct' do + context "when entry config value is correct without surrounding '/'" do let(:config) { 'Code coverage: \d+\.\d+' } describe '#value' do subject { entry.value } - it { is_expected.to eq config } + it { is_expected.to eq(config) } + end + + describe '#errors' do + subject { entry.errors } + it { is_expected.to be_empty } + end + + describe '#valid?' do + subject { entry } + it { is_expected.to be_valid } + end + end + + context "when entry config value is correct with surrounding '/'" do + let(:config) { '/Code coverage: \d+\.\d+/' } + + describe '#value' do + subject { entry.value } + it { is_expected.to eq(config[1...-1]) } end describe '#errors' do @@ -28,7 +47,7 @@ describe Gitlab::Ci::Config::Entry::Coverage do describe '#errors' do subject { entry.errors } - it { is_expected.to include /coverage config must be a regular expression/ } + it { is_expected.to include(/coverage config must be a regular expression/) } end describe '#valid?' do diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 66a1380bc61..7b7f5761ebd 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -4,19 +4,17 @@ describe Gitlab::Ci::Config::Entry::Global do let(:global) { described_class.new(hash) } describe '.nodes' do - subject { described_class.nodes } - - it { is_expected.to be_a Hash } + it 'returns a hash' do + expect(described_class.nodes).to be_a(Hash) + end context 'when filtering all the entry/node names' do - subject { described_class.nodes.keys } - - let(:result) do - %i[before_script image services after_script variables stages types - cache coverage] + it 'contains the expected node names' do + node_names = described_class.nodes.keys + expect(node_names).to match_array(%i[before_script image services + after_script variables stages + types cache coverage]) end - - it { is_expected.to match_array result } end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 7baaef9c85e..52cc45f07b2 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -251,12 +251,13 @@ describe Ci::Build, :models do end describe '#update_coverage' do - it 'grants coverage_regex method is called inside of it' do - build.coverage_regex = '\(\d+.\d+\%\) covered' - allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } - allow(build).to receive(:coverage_regex).and_call_original - expect(build).to receive(:update_attributes).with(coverage: 98.29) { true } - expect(build.update_coverage).to be true + context "regarding coverage_regex's value," do + it "saves the correct extracted coverage value" do + build.coverage_regex = '\(\d+.\d+\%\) covered' + allow(build).to receive(:trace) { 'Coverage 1033 / 1051 LOC (98.29%) covered' } + expect(build).to receive(:update_attributes).with(coverage: 98.29) { true } + expect(build.update_coverage).to be true + end end end -- cgit v1.2.1 From be7106a145b1e3d4c6e06503e0f7f3032ace3764 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Wed, 7 Dec 2016 03:01:34 -0200 Subject: Force coverage value to always be surrounded by '/' --- app/models/ci/build.rb | 10 ++++-- doc/ci/yaml/README.md | 7 +++-- lib/gitlab/ci/config/entry/coverage.rb | 8 ----- lib/gitlab/ci/config/entry/trigger.rb | 10 +----- lib/gitlab/ci/config/entry/validators.rb | 39 ++++++++++++++++++++++++ spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 6 ++-- spec/lib/gitlab/ci/config/entry/coverage_spec.rb | 17 ++++------- spec/models/ci/build_spec.rb | 9 +++--- 8 files changed, 65 insertions(+), 41 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 951818ad561..e3753869b67 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -275,8 +275,10 @@ module Ci end def update_coverage - return unless project - return unless regex = self.coverage_regex + regex = coverage_regex.to_s[1...-1] + + return if regex.blank? + coverage = extract_coverage(trace, regex) if coverage.is_a? Numeric @@ -522,7 +524,9 @@ module Ci end def coverage_regex - super || project.build_coverage_regex + super || + project.try(:build_coverage_regex).presence && + "/#{project.try(:build_coverage_regex)}/" end def when diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 5e2d9788f33..85b2c75cee8 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -286,9 +286,10 @@ build outputs. Setting this up globally will make all the jobs to use this setting for output filtering and extracting the coverage information from your builds. -Regular expressions are used by default. So using surrounding `/` is optional, -given it'll always be read as a regular expression. Don't forget to escape -special characters whenever you want to match them literally. +Regular expressions are the only valid kind of value expected here. So, using +surrounding `/` is mandatory in order to consistently and explicitly represent +a regular expression string. You must escape special characters if you want to +match them literally. A simple example: ```yaml diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index 706bfc882de..25546f363fb 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -11,14 +11,6 @@ module Gitlab validations do validates :config, regexp: true end - - def value - if @config.first == '/' && @config.last == '/' - @config[1...-1] - else - @config - end - end end end end diff --git a/lib/gitlab/ci/config/entry/trigger.rb b/lib/gitlab/ci/config/entry/trigger.rb index 28b0a9ffe01..16b234e6c59 100644 --- a/lib/gitlab/ci/config/entry/trigger.rb +++ b/lib/gitlab/ci/config/entry/trigger.rb @@ -9,15 +9,7 @@ module Gitlab include Validatable validations do - include LegacyValidationHelpers - - validate :array_of_strings_or_regexps - - def array_of_strings_or_regexps - unless validate_array_of_strings_or_regexps(config) - errors.add(:config, 'should be an array of strings or regexps') - end - end + validates :config, array_of_strings_or_regexps: true end end end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 03a8205b081..5f50b80af6c 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -62,6 +62,45 @@ module Gitlab record.errors.add(attribute, 'must be a regular expression') end end + + private + + def look_like_regexp?(value) + value =~ %r{\A/.*/\z} + end + + def validate_regexp(value) + look_like_regexp?(value) && + Regexp.new(value.to_s[1...-1]) && + true + rescue RegexpError + false + end + end + + class ArrayOfStringsOrRegexps < RegexpValidator + def validate_each(record, attribute, value) + unless validate_array_of_strings_or_regexps(value) + record.errors.add(attribute, 'should be an array of strings or regexps') + end + end + + private + + def validate_array_of_strings_or_regexps(values) + values.is_a?(Array) && values.all?(&method(:validate_string_or_regexp)) + end + + def validate_string_or_regexp(value) + return true if value.is_a?(Symbol) + return false unless value.is_a?(String) + + if look_like_regexp?(value) + validate_regexp(value) + else + true + end + end end class TypeValidator < ActiveModel::EachValidator diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index b1e09350847..e2302f5968a 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -13,11 +13,11 @@ module Ci context 'when config has coverage set at the global scope' do before do - config_base.update(coverage: '\(\d+\.\d+\) covered') + config_base.update(coverage: '/\(\d+\.\d+\) covered/') end context "and 'rspec' job doesn't have coverage set" do - it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } + it { is_expected.to include(coverage_regex: '/\(\d+\.\d+\) covered/') } end context "but 'rspec' job also has coverage set" do @@ -25,7 +25,7 @@ module Ci config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' end - it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } + it { is_expected.to include(coverage_regex: '/Code coverage: \d+\.\d+/') } end end end diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb index 8f989ebd732..eb04075f1be 100644 --- a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb @@ -4,31 +4,26 @@ describe Gitlab::Ci::Config::Entry::Coverage do let(:entry) { described_class.new(config) } describe 'validations' do - context "when entry config value is correct without surrounding '/'" do + context "when entry config value doesn't have the surrounding '/'" do let(:config) { 'Code coverage: \d+\.\d+' } - describe '#value' do - subject { entry.value } - it { is_expected.to eq(config) } - end - describe '#errors' do subject { entry.errors } - it { is_expected.to be_empty } + it { is_expected.to include(/coverage config must be a regular expression/) } end describe '#valid?' do subject { entry } - it { is_expected.to be_valid } + it { is_expected.not_to be_valid } end end - context "when entry config value is correct with surrounding '/'" do + context "when entry config value has the surrounding '/'" do let(:config) { '/Code coverage: \d+\.\d+/' } describe '#value' do subject { entry.value } - it { is_expected.to eq(config[1...-1]) } + it { is_expected.to eq(config) } end describe '#errors' do @@ -42,7 +37,7 @@ describe Gitlab::Ci::Config::Entry::Coverage do end end - context 'when entry value is not correct' do + context 'when entry value is not valid' do let(:config) { '(malformed regexp' } describe '#errors' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 52cc45f07b2..f23155a5d13 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -224,19 +224,20 @@ describe Ci::Build, :models do describe '#coverage_regex' do subject { build.coverage_regex } - let(:project_regex) { '\(\d+\.\d+\) covered' } - let(:build_regex) { 'Code coverage: \d+\.\d+' } - context 'when project has build_coverage_regex set' do + let(:project_regex) { '\(\d+\.\d+\) covered' } + before do project.build_coverage_regex = project_regex end context 'and coverage_regex attribute is not set' do - it { is_expected.to eq(project_regex) } + it { is_expected.to eq("/#{project_regex}/") } end context 'but coverage_regex attribute is also set' do + let(:build_regex) { '/Code coverage: \d+\.\d+/' } + before do build.coverage_regex = build_regex end -- cgit v1.2.1 From 518fd2eb93711e1e9c3d597a6bdf13366d9abdb5 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Fri, 9 Dec 2016 00:20:39 -0200 Subject: Improve/polish code logic for some Ci::Build methods --- app/models/ci/build.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index e3753869b67..2a613d12913 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -275,11 +275,11 @@ module Ci end def update_coverage - regex = coverage_regex.to_s[1...-1] + regex = coverage_regex - return if regex.blank? + return unless regex - coverage = extract_coverage(trace, regex) + coverage = extract_coverage(trace, regex[1...-1]) if coverage.is_a? Numeric update_attributes(coverage: coverage) @@ -526,7 +526,7 @@ module Ci def coverage_regex super || project.try(:build_coverage_regex).presence && - "/#{project.try(:build_coverage_regex)}/" + "/#{project.build_coverage_regex}/" end def when -- cgit v1.2.1 From 8fe708f4a2850d71c11234b234e039b2a9422299 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Tue, 13 Dec 2016 02:53:12 -0200 Subject: Make more code improvements around the '/' stripping logic --- app/models/ci/build.rb | 35 +++++++++--------------- lib/gitlab/ci/config/entry/coverage.rb | 4 +++ lib/gitlab/ci/config/entry/validators.rb | 12 +++----- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 11 ++++++-- spec/lib/gitlab/ci/config/entry/coverage_spec.rb | 2 +- spec/lib/gitlab/ci/config/entry/global_spec.rb | 4 +-- spec/models/ci/build_spec.rb | 4 +-- 7 files changed, 35 insertions(+), 37 deletions(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 2a613d12913..62fec28d2d5 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -275,30 +275,23 @@ module Ci end def update_coverage - regex = coverage_regex - - return unless regex - - coverage = extract_coverage(trace, regex[1...-1]) - - if coverage.is_a? Numeric - update_attributes(coverage: coverage) - end + coverage = extract_coverage(trace, coverage_regex) + update_attributes(coverage: coverage) if coverage.is_a?(Numeric) end def extract_coverage(text, regex) - begin - matches = text.scan(Regexp.new(regex)).last - matches = matches.last if matches.kind_of?(Array) - coverage = matches.gsub(/\d+(\.\d+)?/).first + return unless regex - if coverage.present? - coverage.to_f - end - rescue - # if bad regex or something goes wrong we dont want to interrupt transition - # so we just silentrly ignore error for now + matches = text.scan(Regexp.new(regex)).last + matches = matches.last if matches.kind_of?(Array) + coverage = matches.gsub(/\d+(\.\d+)?/).first + + if coverage.present? + coverage.to_f end + rescue + # if bad regex or something goes wrong we dont want to interrupt transition + # so we just silentrly ignore error for now end def has_trace_file? @@ -524,9 +517,7 @@ module Ci end def coverage_regex - super || - project.try(:build_coverage_regex).presence && - "/#{project.build_coverage_regex}/" + super || project.try(:build_coverage_regex) end def when diff --git a/lib/gitlab/ci/config/entry/coverage.rb b/lib/gitlab/ci/config/entry/coverage.rb index 25546f363fb..12a063059cb 100644 --- a/lib/gitlab/ci/config/entry/coverage.rb +++ b/lib/gitlab/ci/config/entry/coverage.rb @@ -11,6 +11,10 @@ module Gitlab validations do validates :config, regexp: true end + + def value + @config[1...-1] + end end end end diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 5f50b80af6c..30c52dd65e8 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -66,7 +66,7 @@ module Gitlab private def look_like_regexp?(value) - value =~ %r{\A/.*/\z} + value.start_with?('/') && value.end_with?('/') end def validate_regexp(value) @@ -78,7 +78,7 @@ module Gitlab end end - class ArrayOfStringsOrRegexps < RegexpValidator + class ArrayOfStringsOrRegexpsValidator < RegexpValidator def validate_each(record, attribute, value) unless validate_array_of_strings_or_regexps(value) record.errors.add(attribute, 'should be an array of strings or regexps') @@ -94,12 +94,8 @@ module Gitlab def validate_string_or_regexp(value) return true if value.is_a?(Symbol) return false unless value.is_a?(String) - - if look_like_regexp?(value) - validate_regexp(value) - else - true - end + return validate_regexp(value) if look_like_regexp?(value) + true 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 e2302f5968a..49349035b3b 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -17,7 +17,7 @@ module Ci end context "and 'rspec' job doesn't have coverage set" do - it { is_expected.to include(coverage_regex: '/\(\d+\.\d+\) covered/') } + it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } end context "but 'rspec' job also has coverage set" do @@ -25,7 +25,7 @@ module Ci config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' end - it { is_expected.to include(coverage_regex: '/Code coverage: \d+\.\d+/') } + it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } end end end @@ -48,6 +48,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: {}, allow_failure: false, @@ -462,6 +463,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: { image: "ruby:2.1", @@ -490,6 +492,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: { image: "ruby:2.5", @@ -729,6 +732,7 @@ module Ci stage_idx: 1, name: "rspec", commands: "pwd\nrspec", + coverage_regex: nil, tag_list: [], options: { image: "ruby:2.1", @@ -940,6 +944,7 @@ module Ci stage_idx: 1, name: "normal_job", commands: "test", + coverage_regex: nil, tag_list: [], options: {}, when: "on_success", @@ -985,6 +990,7 @@ module Ci stage_idx: 0, name: "job1", commands: "execute-script-for-job", + coverage_regex: nil, tag_list: [], options: {}, when: "on_success", @@ -997,6 +1003,7 @@ module Ci stage_idx: 0, name: "job2", commands: "execute-script-for-job", + coverage_regex: nil, tag_list: [], options: {}, when: "on_success", diff --git a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb index eb04075f1be..4c6bd859552 100644 --- a/spec/lib/gitlab/ci/config/entry/coverage_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/coverage_spec.rb @@ -23,7 +23,7 @@ describe Gitlab::Ci::Config::Entry::Coverage do describe '#value' do subject { entry.value } - it { is_expected.to eq(config) } + it { is_expected.to eq(config[1...-1]) } end describe '#errors' do diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index 7b7f5761ebd..d4f1780b174 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -40,7 +40,7 @@ describe Gitlab::Ci::Config::Entry::Global do end it 'creates node object for each entry' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'creates node object using valid class' do @@ -181,7 +181,7 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#nodes' do it 'instantizes all nodes' do - expect(global.descendants.count).to eq 8 + expect(global.descendants.count).to eq 9 end it 'contains unspecified nodes' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index f23155a5d13..fe0a9707b2a 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -232,11 +232,11 @@ describe Ci::Build, :models do end context 'and coverage_regex attribute is not set' do - it { is_expected.to eq("/#{project_regex}/") } + it { is_expected.to eq(project_regex) } end context 'but coverage_regex attribute is also set' do - let(:build_regex) { '/Code coverage: \d+\.\d+/' } + let(:build_regex) { 'Code coverage: \d+\.\d+' } before do build.coverage_regex = build_regex -- cgit v1.2.1 From 441a9beec3e6834d3fe5e047e65c4d8b32ff86d5 Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Wed, 18 Jan 2017 01:42:38 -0200 Subject: Make some other refinements to validation logic --- lib/gitlab/ci/config/entry/validators.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/ci/config/entry/validators.rb b/lib/gitlab/ci/config/entry/validators.rb index 30c52dd65e8..bd7428b1272 100644 --- a/lib/gitlab/ci/config/entry/validators.rb +++ b/lib/gitlab/ci/config/entry/validators.rb @@ -66,7 +66,8 @@ module Gitlab private def look_like_regexp?(value) - value.start_with?('/') && value.end_with?('/') + value.is_a?(String) && value.start_with?('/') && + value.end_with?('/') end def validate_regexp(value) @@ -92,7 +93,6 @@ module Gitlab end def validate_string_or_regexp(value) - return true if value.is_a?(Symbol) return false unless value.is_a?(String) return validate_regexp(value) if look_like_regexp?(value) true -- cgit v1.2.1 From 1c24c79a83bff0d1535d813eb8146fc799d5d8ac Mon Sep 17 00:00:00 2001 From: Leandro Camargo <leandroico@gmail.com> Date: Wed, 25 Jan 2017 01:48:03 -0200 Subject: Be more lenient on build coverage value updates and fix specs --- app/models/ci/build.rb | 2 +- spec/lib/gitlab/import_export/safe_model_attributes.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 62fec28d2d5..b1f77bf242c 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -276,7 +276,7 @@ module Ci def update_coverage coverage = extract_coverage(trace, coverage_regex) - update_attributes(coverage: coverage) if coverage.is_a?(Numeric) + update_attributes(coverage: coverage) if coverage.present? end def extract_coverage(text, regex) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 493bc2db21a..95b230e4f5c 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -222,6 +222,7 @@ CommitStatus: - queued_at - token - lock_version +- coverage_regex Ci::Variable: - id - project_id -- cgit v1.2.1 From acda0cd48d69dbd98ec9df8339f15139cd098726 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 26 Jan 2017 19:13:02 +0800 Subject: @tree_edit_project is no longer used Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_21626434 --- app/controllers/concerns/creates_commit.rb | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index 2ece99aebc0..fa7c22b5388 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -101,7 +101,6 @@ module CreatesCommit def set_commit_variables if can?(current_user, :push_code, @project) # Edit file in this project - @tree_edit_project = @project @mr_source_project = @project if @project.forked? @@ -114,10 +113,8 @@ module CreatesCommit @mr_target_branch = @ref || @target_branch end else - # Edit file in fork - @tree_edit_project = current_user.fork_of(@project) # Merge request from fork to this project - @mr_source_project = @tree_edit_project + @mr_source_project = current_user.fork_of(@project) @mr_target_project = @project @mr_target_branch = @ref || @target_branch end -- cgit v1.2.1 From 9bb4cd75ad51f61a53a2bef205be2b4b24acc513 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 26 Jan 2017 19:22:44 +0800 Subject: Use commit rather than branch, and rename to avoid confusion Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_21626953 https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_21626952 --- app/models/repository.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index f3a0148c423..6c847e07c00 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -996,10 +996,10 @@ class Repository end end - def check_revert_content(commit, branch_name) - source_sha = find_branch(branch_name).dereferenced_target.sha - args = [commit.id, source_sha] - args << { mainline: 1 } if commit.merge_commit? + def check_revert_content(target_commit, branch_name) + source_sha = commit(branch_name).sha + args = [target_commit.sha, source_sha] + args << { mainline: 1 } if target_commit.merge_commit? revert_index = rugged.revert_commit(*args) return false if revert_index.conflicts? @@ -1010,10 +1010,10 @@ class Repository tree_id end - def check_cherry_pick_content(commit, branch_name) - source_sha = find_branch(branch_name).dereferenced_target.sha - args = [commit.id, source_sha] - args << 1 if commit.merge_commit? + def check_cherry_pick_content(target_commit, branch_name) + source_sha = commit(branch_name).sha + args = [target_commit.sha, source_sha] + args << 1 if target_commit.merge_commit? cherry_pick_index = rugged.cherrypick_commit(*args) return false if cherry_pick_index.conflicts? -- cgit v1.2.1 From 05f4e48a4c761e13faf080e2d33fb8cc9886f723 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 26 Jan 2017 19:35:19 +0800 Subject: Make GitHooksService#execute return block value Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_21627207 --- app/services/git_hooks_service.rb | 6 +++--- app/services/git_operation_service.rb | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb index 6cd3908d43a..d222d1e63aa 100644 --- a/app/services/git_hooks_service.rb +++ b/app/services/git_hooks_service.rb @@ -18,9 +18,9 @@ class GitHooksService end end - yield self - - run_hook('post-receive') + yield(self).tap do + run_hook('post-receive') + end end private diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index 2b2ba0870a4..df9c393844d 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -107,8 +107,6 @@ class GitOperationService end def with_hooks(ref, newrev, oldrev) - result = nil - GitHooksService.new.execute( user, repository.path_to_repo, @@ -116,10 +114,8 @@ class GitOperationService newrev, ref) do |service| - result = yield(service) if block_given? + yield(service) end - - result end def update_ref(ref, newrev, oldrev) -- cgit v1.2.1 From d475fa094689a6319fa60f2532898234979e30d3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 26 Jan 2017 21:18:04 +0800 Subject: We don't care about the return value now --- spec/services/git_hooks_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index 41b0968b8b4..3318dfb22b6 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -21,7 +21,7 @@ describe GitHooksService, services: true do hook = double(trigger: [true, nil]) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - expect(service.execute(user, @repo_path, @blankrev, @newrev, @ref) { }).to eq([true, nil]) + service.execute(user, @repo_path, @blankrev, @newrev, @ref) { } end end -- cgit v1.2.1 From 8f3aa6ac338ee3595909fea9938611fb03187e6a Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 26 Jan 2017 21:59:12 +0800 Subject: Try to check if branch diverged explicitly Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_21627134 --- app/services/git_operation_service.rb | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/app/services/git_operation_service.rb b/app/services/git_operation_service.rb index df9c393844d..27bcc047601 100644 --- a/app/services/git_operation_service.rb +++ b/app/services/git_operation_service.rb @@ -82,12 +82,7 @@ class GitOperationService end branch = repository.find_branch(branch_name) - oldrev = if branch - # This could verify we're not losing commits - repository.rugged.merge_base(newrev, branch.target) - else - Gitlab::Git::BLANK_SHA - end + oldrev = find_oldrev_from_branch(newrev, branch) ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name update_ref_in_hooks(ref, newrev, oldrev) @@ -100,6 +95,18 @@ class GitOperationService newrev end + def find_oldrev_from_branch(newrev, branch) + return Gitlab::Git::BLANK_SHA unless branch + + oldrev = branch.target + + if oldrev == repository.rugged.merge_base(newrev, branch.target) + oldrev + else + raise Repository::CommitError.new('Branch diverged') + end + end + def update_ref_in_hooks(ref, newrev, oldrev) with_hooks(ref, newrev, oldrev) do update_ref(ref, newrev, oldrev) -- cgit v1.2.1 From dbda72a79999998bfd1d77b3102bc16053a2685e Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 26 Jan 2017 15:30:34 +0100 Subject: Rename presenters for consitency --- lib/gitlab/chat_commands/command.rb | 2 +- lib/gitlab/chat_commands/issue_create.rb | 42 ------------ lib/gitlab/chat_commands/issue_new.rb | 42 ++++++++++++ lib/gitlab/chat_commands/issue_search.rb | 8 +-- lib/gitlab/chat_commands/issue_show.rb | 2 +- lib/gitlab/chat_commands/presenters/deploy.rb | 8 --- lib/gitlab/chat_commands/presenters/help.rb | 6 +- lib/gitlab/chat_commands/presenters/issuable.rb | 4 +- lib/gitlab/chat_commands/presenters/issue_new.rb | 48 +++++++++++++ .../chat_commands/presenters/issue_search.rb | 45 +++++++++++++ lib/gitlab/chat_commands/presenters/issue_show.rb | 56 ++++++++++++++++ lib/gitlab/chat_commands/presenters/list_issues.rb | 43 ------------ lib/gitlab/chat_commands/presenters/new_issue.rb | 42 ------------ lib/gitlab/chat_commands/presenters/show_issue.rb | 54 --------------- spec/lib/gitlab/chat_commands/command_spec.rb | 2 +- spec/lib/gitlab/chat_commands/issue_create_spec.rb | 78 ---------------------- spec/lib/gitlab/chat_commands/issue_new_spec.rb | 78 ++++++++++++++++++++++ .../chat_commands/presenters/issue_new_spec.rb | 17 +++++ .../chat_commands/presenters/issue_search_spec.rb | 23 +++++++ .../chat_commands/presenters/issue_show_spec.rb | 27 ++++++++ .../chat_commands/presenters/list_issues_spec.rb | 23 ------- .../chat_commands/presenters/show_issue_spec.rb | 27 -------- 22 files changed, 346 insertions(+), 331 deletions(-) delete mode 100644 lib/gitlab/chat_commands/issue_create.rb create mode 100644 lib/gitlab/chat_commands/issue_new.rb create mode 100644 lib/gitlab/chat_commands/presenters/issue_new.rb create mode 100644 lib/gitlab/chat_commands/presenters/issue_search.rb create mode 100644 lib/gitlab/chat_commands/presenters/issue_show.rb delete mode 100644 lib/gitlab/chat_commands/presenters/list_issues.rb delete mode 100644 lib/gitlab/chat_commands/presenters/new_issue.rb delete mode 100644 lib/gitlab/chat_commands/presenters/show_issue.rb delete mode 100644 spec/lib/gitlab/chat_commands/issue_create_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/issue_new_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb delete mode 100644 spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb delete mode 100644 spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 4e5031a8a26..e7baa20356c 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -3,7 +3,7 @@ module Gitlab class Command < BaseCommand COMMANDS = [ Gitlab::ChatCommands::IssueShow, - Gitlab::ChatCommands::IssueCreate, + Gitlab::ChatCommands::IssueNew, Gitlab::ChatCommands::IssueSearch, Gitlab::ChatCommands::Deploy, ].freeze diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb deleted file mode 100644 index 3f3d7de8b2e..00000000000 --- a/lib/gitlab/chat_commands/issue_create.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Gitlab - module ChatCommands - class IssueCreate < IssueCommand - def self.match(text) - # we can not match \n with the dot by passing the m modifier as than - # the title and description are not seperated - /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) - end - - def self.help_message - 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>' - end - - def self.allowed?(project, user) - can?(user, :create_issue, project) - end - - def execute(match) - title = match[:title] - description = match[:description].to_s.rstrip - - issue = create_issue(title: title, description: description) - - if issue.errors.any? - presenter(issue).display_errors - else - presenter(issue).present - end - end - - private - - def create_issue(title:, description:) - Issues::CreateService.new(project, current_user, title: title, description: description).execute - end - - def presenter(issue) - Gitlab::ChatCommands::Presenters::NewIssue.new(issue) - end - end - end -end diff --git a/lib/gitlab/chat_commands/issue_new.rb b/lib/gitlab/chat_commands/issue_new.rb new file mode 100644 index 00000000000..016054ecd46 --- /dev/null +++ b/lib/gitlab/chat_commands/issue_new.rb @@ -0,0 +1,42 @@ +module Gitlab + module ChatCommands + class IssueNew < IssueCommand + def self.match(text) + # we can not match \n with the dot by passing the m modifier as than + # the title and description are not seperated + /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) + end + + def self.help_message + 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>' + end + + def self.allowed?(project, user) + can?(user, :create_issue, project) + end + + def execute(match) + title = match[:title] + description = match[:description].to_s.rstrip + + issue = create_issue(title: title, description: description) + + if issue.persisted? + presenter(issue).present + else + presenter(issue).display_errors + end + end + + private + + def create_issue(title:, description:) + Issues::CreateService.new(project, current_user, title: title, description: description).execute + end + + def presenter(issue) + Gitlab::ChatCommands::Presenters::IssueNew.new(issue) + end + end + end +end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb index e2d3a0f466a..3491b53093e 100644 --- a/lib/gitlab/chat_commands/issue_search.rb +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -12,12 +12,10 @@ module Gitlab def execute(match) issues = collection.search(match[:query]).limit(QUERY_LIMIT) - if issues.none? - Presenters::Access.new(issues).not_found - elsif issues.one? - Presenters::ShowIssue.new(issues.first).present + if issues.present? + Presenters::IssueSearch.new(issues).present else - Presenters::ListIssues.new(issues).present + Presenters::Access.new(issues).not_found end end end diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb index 9f3e1b9a64b..d6013f4d10c 100644 --- a/lib/gitlab/chat_commands/issue_show.rb +++ b/lib/gitlab/chat_commands/issue_show.rb @@ -13,7 +13,7 @@ module Gitlab issue = find_by_iid(match[:iid]) if issue - Gitlab::ChatCommands::Presenters::ShowIssue.new(issue).present + Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present else Gitlab::ChatCommands::Presenters::Access.new.not_found end diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb index b1cfaac15af..863d0bf99ca 100644 --- a/lib/gitlab/chat_commands/presenters/deploy.rb +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -15,14 +15,6 @@ module Gitlab def too_many_actions ephemeral_response(text: "Too many actions defined") end - - private - - def resource_url - polymorphic_url( - [ @resource.project.namespace.becomes(Namespace), @resource.project, @resource] - ) - end end end end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb index c7a67467b7e..39ad3249f5b 100644 --- a/lib/gitlab/chat_commands/presenters/help.rb +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -9,10 +9,10 @@ module Gitlab private def help_message(trigger) - if @resource.none? - "No commands available :thinking_face:" - else + if @resource.present? header_with_list("Available commands", full_commands(trigger)) + else + "No commands available :thinking_face:" end end diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb index 2cb6b1525fc..dfb1c8f6616 100644 --- a/lib/gitlab/chat_commands/presenters/issuable.rb +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -1,9 +1,7 @@ module Gitlab module ChatCommands module Presenters - class Issuable < Presenters::Base - private - + module Issuable def color(issuable) issuable.open? ? '#38ae67' : '#d22852' end diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb new file mode 100644 index 00000000000..d26dd22b2a0 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_new.rb @@ -0,0 +1,48 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueNew < Presenters::Base + include Presenters::Issuable + + def present + in_channel_response(new_issue) + end + + private + + def new_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :title, + :text + ] + } + ] + } + end + + def pretext + "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" + end + + def project_link + "[#{project.name_with_namespace}](#{url_for(project)})" + end + + def author_profile_link + "[#{author.to_reference}](#{url_for(author)})" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb new file mode 100644 index 00000000000..d58a6d6114a --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_search.rb @@ -0,0 +1,45 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueSearch < Presenters::Base + include Presenters::Issuable + + def present + text = if @resource.count >= 5 + "Here are the first 5 issues I found:" + else + "Here are the #{@resource.count} issues I found:" + end + + ephemeral_response(text: text, attachments: attachments) + end + + private + + def attachments + @resource.map do |issue| + url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" + + { + color: color(issue), + fallback: "#{issue.to_reference} #{issue.title}", + text: "#{url} · #{issue.title} (#{status_text(issue)})", + + mrkdwn_in: [ + "text" + ] + } + end + end + + def project + @project ||= @resource.first.project + end + + def namespace + @namespace ||= project.namespace.becomes(Namespace) + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb new file mode 100644 index 00000000000..2fc671f13a6 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_show.rb @@ -0,0 +1,56 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueShow < Presenters::Base + include Presenters::Issuable + + def present + in_channel_response(show_issue) + end + + private + + def show_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "Issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + text: text, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :pretext, + :text + ] + } + ] + } + end + + def text + message = "**#{status_text(@resource)}**" + + if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? + return message + end + + message << " · " + message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero? + message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero? + message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero? + + message + end + + def pretext + "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/list_issues.rb b/lib/gitlab/chat_commands/presenters/list_issues.rb deleted file mode 100644 index 2458b9356b7..00000000000 --- a/lib/gitlab/chat_commands/presenters/list_issues.rb +++ /dev/null @@ -1,43 +0,0 @@ -module Gitlab - module ChatCommands - module Presenters - class ListIssues < Presenters::Issuable - def present - text = if @resource.count >= 5 - "Here are the first 5 issues I found:" - else - "Here are the #{@resource.count} issues I found:" - end - - ephemeral_response(text: text, attachments: attachments) - end - - private - - def attachments - @resource.map do |issue| - url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" - - { - color: color(issue), - fallback: "#{issue.to_reference} #{issue.title}", - text: "#{url} · #{issue.title} (#{status_text(issue)})", - - mrkdwn_in: [ - "text" - ] - } - end - end - - def project - @project ||= @resource.first.project - end - - def namespace - @namespace ||= project.namespace.becomes(Namespace) - end - end - end - end -end diff --git a/lib/gitlab/chat_commands/presenters/new_issue.rb b/lib/gitlab/chat_commands/presenters/new_issue.rb deleted file mode 100644 index c7c6febb56e..00000000000 --- a/lib/gitlab/chat_commands/presenters/new_issue.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Gitlab - module ChatCommands - module Presenters - class NewIssue < Presenters::Issuable - def present - in_channel_response(show_issue) - end - - private - - def show_issue - { - attachments: [ - { - title: "#{@resource.title} · #{@resource.to_reference}", - title_link: resource_url, - author_name: author.name, - author_icon: author.avatar_url, - fallback: "New issue #{@resource.to_reference}: #{@resource.title}", - pretext: pretext, - color: color(@resource), - fields: fields, - mrkdwn_in: [ - :title, - :text - ] - } - ] - } - end - - def pretext - "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" - end - - def author_profile_link - "[#{author.to_reference}](#{url_for(author)})" - end - end - end - end -end diff --git a/lib/gitlab/chat_commands/presenters/show_issue.rb b/lib/gitlab/chat_commands/presenters/show_issue.rb deleted file mode 100644 index e5644a4ad7e..00000000000 --- a/lib/gitlab/chat_commands/presenters/show_issue.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Gitlab - module ChatCommands - module Presenters - class ShowIssue < Presenters::Issuable - def present - in_channel_response(show_issue) - end - - private - - def show_issue - { - attachments: [ - { - title: "#{@resource.title} · #{@resource.to_reference}", - title_link: resource_url, - author_name: author.name, - author_icon: author.avatar_url, - fallback: "New issue #{@resource.to_reference}: #{@resource.title}", - pretext: pretext, - text: text, - color: color(@resource), - fields: fields, - mrkdwn_in: [ - :pretext, - :text - ] - } - ] - } - end - - def text - message = "**#{status_text(@resource)}**" - - if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? - return message - end - - message << " · " - message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero? - message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero? - message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero? - - message - end - - def pretext - "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" - end - end - end - end -end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index b634df52b68..f4441f9f93c 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -78,7 +78,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'IssueCreate is triggered' do let(:params) { { text: 'issue create my title' } } - it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) } + it { is_expected.to eq(Gitlab::ChatCommands::IssueNew) } end context 'IssueSearch is triggered' do diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb deleted file mode 100644 index 0f84b19a5a4..00000000000 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ChatCommands::IssueCreate, service: true do - describe '#execute' do - let(:project) { create(:empty_project) } - let(:user) { create(:user) } - let(:regex_match) { described_class.match("issue create bird is the word") } - - before do - project.team << [user, :master] - end - - subject do - described_class.new(project, user).execute(regex_match) - end - - context 'without description' do - it 'creates the issue' do - expect { subject }.to change { project.issues.count }.by(1) - - expect(subject[:response_type]).to be(:in_channel) - end - end - - context 'with description' do - let(:description) { "Surfin bird" } - let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") } - - it 'creates the issue with description' do - subject - - expect(Issue.last.description).to eq(description) - end - end - - context "with more newlines between the title and the description" do - let(:description) { "Surfin bird" } - let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") } - - it 'creates the issue' do - expect { subject }.to change { project.issues.count }.by(1) - end - end - - context 'issue cannot be created' do - let!(:issue) { create(:issue, project: project, title: 'bird is the word') } - let(:regex_match) { described_class.match("issue create #{'a' * 512}}") } - - it 'displays the errors' do - expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to match("- Title is too long") - end - end - end - - describe '.match' do - it 'matches the title without description' do - match = described_class.match("issue create my title") - - expect(match[:title]).to eq('my title') - expect(match[:description]).to eq("") - end - - it 'matches the title with description' do - match = described_class.match("issue create my title\n\ndescription") - - expect(match[:title]).to eq('my title') - expect(match[:description]).to eq('description') - end - - it 'matches the alias new' do - match = described_class.match("issue new my title") - - expect(match).not_to be_nil - expect(match[:title]).to eq('my title') - end - end -end diff --git a/spec/lib/gitlab/chat_commands/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/issue_new_spec.rb new file mode 100644 index 00000000000..84c22328064 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/issue_new_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::IssueNew, service: true do + describe '#execute' do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:regex_match) { described_class.match("issue create bird is the word") } + + before do + project.team << [user, :master] + end + + subject do + described_class.new(project, user).execute(regex_match) + end + + context 'without description' do + it 'creates the issue' do + expect { subject }.to change { project.issues.count }.by(1) + + expect(subject[:response_type]).to be(:in_channel) + end + end + + context 'with description' do + let(:description) { "Surfin bird" } + let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") } + + it 'creates the issue with description' do + subject + + expect(Issue.last.description).to eq(description) + end + end + + context "with more newlines between the title and the description" do + let(:description) { "Surfin bird" } + let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") } + + it 'creates the issue' do + expect { subject }.to change { project.issues.count }.by(1) + end + end + + context 'issue cannot be created' do + let!(:issue) { create(:issue, project: project, title: 'bird is the word') } + let(:regex_match) { described_class.match("issue create #{'a' * 512}}") } + + it 'displays the errors' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("- Title is too long") + end + end + end + + describe '.match' do + it 'matches the title without description' do + match = described_class.match("issue create my title") + + expect(match[:title]).to eq('my title') + expect(match[:description]).to eq("") + end + + it 'matches the title with description' do + match = described_class.match("issue create my title\n\ndescription") + + expect(match[:title]).to eq('my title') + expect(match[:description]).to eq('description') + end + + it 'matches the alias new' do + match = described_class.match("issue new my title") + + expect(match).not_to be_nil + expect(match[:title]).to eq('my title') + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb new file mode 100644 index 00000000000..17fcdbc2452 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueNew do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:attachment) { subject[:attachments].first } + + subject { described_class.new(issue).present } + + it { is_expected.to be_a(Hash) } + + it 'shows the issue' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject).to have_key(:attachments) + expect(attachment[:title]).to start_with(issue.title) + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb new file mode 100644 index 00000000000..ec6d3e34a96 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueSearch do + let(:project) { create(:empty_project) } + let(:message) { subject[:text] } + + before { create_list(:issue, 2, project: project) } + + subject { described_class.new(project.issues).present } + + it 'formats the message correct' do + is_expected.to have_key(:text) + is_expected.to have_key(:status) + is_expected.to have_key(:response_type) + is_expected.to have_key(:attachments) + end + + it 'shows a list of results' do + expect(subject[:response_type]).to be(:ephemeral) + + expect(message).to start_with("Here are the 2 issues I found") + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb new file mode 100644 index 00000000000..89d154e26e4 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueShow do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:attachment) { subject[:attachments].first } + + subject { described_class.new(issue).present } + + it { is_expected.to be_a(Hash) } + + it 'shows the issue' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject).to have_key(:attachments) + expect(attachment[:title]).to start_with(issue.title) + end + + context 'with upvotes' do + before do + create(:award_emoji, :upvote, awardable: issue) + end + + it 'shows the upvote count' do + expect(attachment[:text]).to start_with("**Open** · :+1: 1") + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb deleted file mode 100644 index 13a1f70fe78..00000000000 --- a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ChatCommands::Presenters::ListIssues do - let(:project) { create(:empty_project) } - let(:message) { subject[:text] } - - before { create_list(:issue, 2, project: project) } - - subject { described_class.new(project.issues).present } - - it 'formats the message correct' do - is_expected.to have_key(:text) - is_expected.to have_key(:status) - is_expected.to have_key(:response_type) - is_expected.to have_key(:attachments) - end - - it 'shows a list of results' do - expect(subject[:response_type]).to be(:ephemeral) - - expect(message).to start_with("Here are the 2 issues I found") - end -end diff --git a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb deleted file mode 100644 index ca4062e692a..00000000000 --- a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ChatCommands::Presenters::ShowIssue do - let(:project) { create(:empty_project) } - let(:issue) { create(:issue, project: project) } - let(:attachment) { subject[:attachments].first } - - subject { described_class.new(issue).present } - - it { is_expected.to be_a(Hash) } - - it 'shows the issue' do - expect(subject[:response_type]).to be(:in_channel) - expect(subject).to have_key(:attachments) - expect(attachment[:title]).to start_with(issue.title) - end - - context 'with upvotes' do - before do - create(:award_emoji, :upvote, awardable: issue) - end - - it 'shows the upvote count' do - expect(attachment[:text]).to start_with("**Open** · :+1: 1") - end - end -end -- cgit v1.2.1 From eb242fc865c032f6408f3b68700da9b840b416dd Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 26 Jan 2017 22:37:22 +0800 Subject: Make sure different project gets a merge request Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7237#note_21626479 --- app/controllers/concerns/creates_commit.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index fa7c22b5388..6286d67d30c 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -93,8 +93,10 @@ module CreatesCommit def create_merge_request? # XXX: Even if the field is set, if we're checking the same branch - # as the target branch, we don't want to create a merge request. - params[:create_merge_request].present? && @ref != @target_branch + # as the target branch in the same project, + # we don't want to create a merge request. + params[:create_merge_request].present? && + (different_project? || @ref != @target_branch) end # TODO: We should really clean this up -- cgit v1.2.1 From 34a1e3dcdbb7fdfcc1bafdc9dbaeee3c79b94c1c Mon Sep 17 00:00:00 2001 From: Eric Eastwood <contact@ericeastwood.com> Date: Tue, 24 Jan 2017 23:12:06 -0600 Subject: Fix permalink discussion note being collapsed --- .../javascripts/behaviors/toggler_behavior.js | 26 +++++++++++-------- ...151-fix-discussion-note-permalink-collapsed.yml | 4 +++ .../merge_requests/toggler_behavior_spec.rb | 29 ++++++++++++++++++++++ 3 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml create mode 100644 spec/features/merge_requests/toggler_behavior_spec.rb diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js index 6a49715590c..a7181904ac9 100644 --- a/app/assets/javascripts/behaviors/toggler_behavior.js +++ b/app/assets/javascripts/behaviors/toggler_behavior.js @@ -1,6 +1,19 @@ /* eslint-disable wrap-iife, func-names, space-before-function-paren, prefer-arrow-callback, vars-on-top, no-var, max-len */ (function(w) { $(function() { + var toggleContainer = function(container, /* optional */toggleState) { + var $container = $(container); + + $container + .find('.js-toggle-button .fa') + .toggleClass('fa-chevron-up', toggleState) + .toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined); + + $container + .find('.js-toggle-content') + .toggle(toggleState); + }; + // Toggle button. Show/hide content inside parent container. // Button does not change visibility. If button has icon - it changes chevron style. // @@ -10,14 +23,7 @@ // $('body').on('click', '.js-toggle-button', function(e) { e.preventDefault(); - $(this) - .find('.fa') - .toggleClass('fa-chevron-down fa-chevron-up') - .end() - .closest('.js-toggle-container') - .find('.js-toggle-content') - .toggle() - ; + toggleContainer($(this).closest('.js-toggle-container')); }); // If we're accessing a permalink, ensure it is not inside a @@ -26,8 +32,8 @@ var anchor = hash && document.getElementById(hash); var container = anchor && $(anchor).closest('.js-toggle-container'); - if (container && container.find('.js-toggle-content').is(':hidden')) { - container.find('.js-toggle-button').trigger('click'); + if (container) { + toggleContainer(container, true); anchor.scrollIntoView(); } }); diff --git a/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml b/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml new file mode 100644 index 00000000000..ddd454da376 --- /dev/null +++ b/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml @@ -0,0 +1,4 @@ +--- +title: Fix permalink discussion note being collapsed +merge_request: +author: diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb new file mode 100644 index 00000000000..6958f6a2c9f --- /dev/null +++ b/spec/features/merge_requests/toggler_behavior_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +feature 'toggler_behavior', js: true, feature: true do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request, source_project: project, author: user) } + let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) } + let(:fragment_id) { "#note_#{note.id}" } + + before do + login_as :admin + project = merge_request.source_project + visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}" + page.current_window.resize_to(1000, 300) + end + + describe 'scroll position' do + it 'should be scrolled down to fragment' do + page_height = page.current_window.size[1] + page_scroll_y = page.evaluate_script("window.scrollY") + fragment_position_top = page.evaluate_script("document.querySelector('#{fragment_id}').getBoundingClientRect().top") + + expect(find('.js-toggle-content').visible?).to eq true + expect(find(fragment_id).visible?).to eq true + expect(fragment_position_top).to be > page_scroll_y + expect(fragment_position_top).to be < (page_scroll_y + page_height) + end + end +end -- cgit v1.2.1 From f258f4f742ec9e172ecf4f9482e05fd2deee2b04 Mon Sep 17 00:00:00 2001 From: dimitrieh <dimitriehoekstra@gmail.com> Date: Fri, 27 Jan 2017 16:45:43 +0100 Subject: Remove hover animation from row elements --- app/assets/stylesheets/framework/animations.scss | 12 ++++++++---- .../26863-Remove-hover-animation-from-row-elements.yml | 4 ++++ 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 8d38fc78a19..b1003ecf420 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -116,11 +116,15 @@ a { @include transition(background-color, color, border); } -.tree-table td, -.well-list > li { - @include transition(background-color, border-color); -} +// .tree-table td, +// .well-list > li { +// @include transition(background-color, border-color); +// } .stage-nav-item { @include transition(background-color, box-shadow); } + +.nav-sidebar a { + transition: none; +} diff --git a/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml b/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml new file mode 100644 index 00000000000..8dfabf87c2a --- /dev/null +++ b/changelogs/unreleased/26863-Remove-hover-animation-from-row-elements.yml @@ -0,0 +1,4 @@ +--- +title: Remove hover animation from row elements +merge_request: +author: -- cgit v1.2.1 From f8efb3ba7d913562c6917b32c5523e9877a2f4b5 Mon Sep 17 00:00:00 2001 From: dimitrieh <dimitriehoekstra@gmail.com> Date: Fri, 27 Jan 2017 16:46:28 +0100 Subject: removed commented out scss --- app/assets/stylesheets/framework/animations.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index b1003ecf420..a5f15f4836d 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -116,11 +116,6 @@ a { @include transition(background-color, color, border); } -// .tree-table td, -// .well-list > li { -// @include transition(background-color, border-color); -// } - .stage-nav-item { @include transition(background-color, box-shadow); } -- cgit v1.2.1 From 444ac6aa02e5b4b7025a9058a98dc6ae8db8e806 Mon Sep 17 00:00:00 2001 From: Clement Ho <ClemMakesApps@gmail.com> Date: Fri, 27 Jan 2017 13:22:12 -0600 Subject: Fix filtering usernames with multiple words --- .../filtered_search/dropdown_user.js.es6 | 9 ++++- .../fix-filtering-username-with-multiple-words.yml | 4 +++ .../filtered_search/dropdown_user_spec.js.es6 | 40 ++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/fix-filtering-username-with-multiple-words.yml create mode 100644 spec/javascripts/filtered_search/dropdown_user_spec.js.es6 diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index 7bf199d9274..162fd6044e5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -39,8 +39,15 @@ getSearchInput() { const query = gl.DropdownUtils.getSearchInput(this.input); const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query); + let value = lastToken.value || ''; - return lastToken.value || ''; + // Removes the first character if it is a quotation so that we can search + // with multiple words + if (value[0] === '"' || value[0] === '\'') { + value = value.slice(1); + } + + return value; } init() { diff --git a/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml b/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml new file mode 100644 index 00000000000..3513f5afdfb --- /dev/null +++ b/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml @@ -0,0 +1,4 @@ +--- +title: Fix filtering usernames with multiple words +merge_request: 8851 +author: diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 new file mode 100644 index 00000000000..5eba4343a1d --- /dev/null +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 @@ -0,0 +1,40 @@ +//= require filtered_search/dropdown_utils +//= require filtered_search/filtered_search_tokenizer +//= require filtered_search/filtered_search_dropdown +//= require filtered_search/dropdown_user + +(() => { + describe('Dropdown User', () => { + describe('getSearchInput', () => { + let dropdownUser; + + beforeEach(() => { + spyOn(gl.FilteredSearchDropdown.prototype, 'constructor').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + spyOn(gl.DropdownUtils, 'getSearchInput').and.callFake(() => {}); + + dropdownUser = new gl.DropdownUser(); + }); + + it('should not return the double quote found in value', () => { + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + lastToken: { + value: '"johnny appleseed', + }, + }); + + expect(dropdownUser.getSearchInput()).toBe('johnny appleseed'); + }); + + it('should not return the single quote found in value', () => { + spyOn(gl.FilteredSearchTokenizer, 'processTokens').and.returnValue({ + lastToken: { + value: '\'larry boy', + }, + }); + + expect(dropdownUser.getSearchInput()).toBe('larry boy'); + }); + }); + }); +})(); -- cgit v1.2.1 From a5f7934420e1d26682700e02aa8fc9333e808f47 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Sat, 28 Jan 2017 10:20:17 -0600 Subject: remove sprockets-es6 and execjs dependencies --- Gemfile | 1 - Gemfile.lock | 9 --------- 2 files changed, 10 deletions(-) diff --git a/Gemfile b/Gemfile index f7886b1ff6a..e38dcae70ce 100644 --- a/Gemfile +++ b/Gemfile @@ -7,7 +7,6 @@ gem 'rails-deprecated_sanitizer', '~> 1.0.3' gem 'responders', '~> 2.0' gem 'sprockets', '~> 3.7.0' -gem 'sprockets-es6', '~> 0.9.2' # Default values for AR models gem 'default_value_for', '~> 3.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 25c722165cf..b1ef956c32c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -72,10 +72,6 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) - babel-source (5.8.35) - babel-transpiler (0.7.0) - babel-source (>= 4.0, < 6) - execjs (~> 2.0) babosa (1.0.2) base32 (0.3.2) bcrypt (3.1.11) @@ -733,10 +729,6 @@ GEM sprockets (3.7.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-es6 (0.9.2) - babel-source (>= 5.8.11) - babel-transpiler - sprockets (>= 3.0.0) sprockets-rails (3.1.1) actionpack (>= 4.0) activesupport (>= 4.0) @@ -986,7 +978,6 @@ DEPENDENCIES spring-commands-rspec (~> 1.0.4) spring-commands-spinach (~> 1.1.0) sprockets (~> 3.7.0) - sprockets-es6 (~> 0.9.2) stackprof (~> 0.2.10) state_machines-activerecord (~> 0.4.0) sys-filesystem (~> 1.1.6) -- cgit v1.2.1 From 7c5d4942742cf8d5c942fba0888eaab5e1133fb4 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Sat, 28 Jan 2017 11:38:30 -0600 Subject: fix frontend tests --- spec/javascripts/commits_spec.js.es6 | 20 ++++++++++++++++---- .../filtered_search_manager_spec.js.es6 | 11 +++++------ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/spec/javascripts/commits_spec.js.es6 b/spec/javascripts/commits_spec.js.es6 index bb9a9072f3a..05260760c43 100644 --- a/spec/javascripts/commits_spec.js.es6 +++ b/spec/javascripts/commits_spec.js.es6 @@ -1,10 +1,19 @@ /* global CommitsList */ -//= require jquery.endless-scroll -//= require pager -//= require commits +require('vendor/jquery.endless-scroll'); +require('~/pager'); +require('~/commits'); (() => { + // TODO: remove this hack! + // PhantomJS causes spyOn to panic because replaceState isn't "writable" + let phantomjs; + try { + phantomjs = !Object.getOwnPropertyDescriptor(window.history, 'replaceState').writable; + } catch (err) { + phantomjs = false; + } + describe('Commits List', () => { beforeEach(() => { setFixtures(` @@ -25,7 +34,10 @@ beforeEach(() => { CommitsList.init(25); CommitsList.searchField.val(''); - spyOn(history, 'replaceState').and.stub(); + + if (!phantomjs) { + spyOn(history, 'replaceState').and.stub(); + } ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => { req.success({ data: '<li>Result</li>', diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 index c8b5c2b36ad..1a06b60b2c2 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 @@ -1,11 +1,10 @@ /* global Turbolinks */ -//= require turbolinks -//= require lib/utils/common_utils -//= require filtered_search/filtered_search_token_keys -//= require filtered_search/filtered_search_tokenizer -//= require filtered_search/filtered_search_dropdown_manager -//= require filtered_search/filtered_search_manager +require('~/lib/utils/common_utils'); +require('~/filtered_search/filtered_search_token_keys'); +require('~/filtered_search/filtered_search_tokenizer'); +require('~/filtered_search/filtered_search_dropdown_manager'); +require('~/filtered_search/filtered_search_manager'); (() => { describe('Filtered Search Manager', () => { -- cgit v1.2.1 From abb122a44f750f8f4f5a784acb6e53db0743b789 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Sat, 28 Jan 2017 11:42:48 -0600 Subject: update rake tasks --- .gitlab-ci.yml | 3 +-- lib/tasks/gitlab/assets.rake | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 588f5b59e4c..1772fda9225 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -108,8 +108,7 @@ setup-test-env: stage: prepare script: - npm install - - bundle exec rake webpack:compile - - bundle exec rake gitlab:assets:compile 2>/dev/null + - bundle exec rake gitlab:assets:compile - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' artifacts: expire_in: 7d diff --git a/lib/tasks/gitlab/assets.rake b/lib/tasks/gitlab/assets.rake index 5d884bf9f66..b6ef8260191 100644 --- a/lib/tasks/gitlab/assets.rake +++ b/lib/tasks/gitlab/assets.rake @@ -3,6 +3,7 @@ namespace :gitlab do desc 'GitLab | Assets | Compile all frontend assets' task :compile do Rake::Task['assets:precompile'].invoke + Rake::Task['webpack:compile'].invoke Rake::Task['gitlab:assets:fix_urls'].invoke end -- cgit v1.2.1 From 3ee67bcf25d87dff35b3f8fcfb16345b36b3d58f Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Sat, 28 Jan 2017 12:13:34 -0600 Subject: fix duplicate data-toggle attribute --- app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index b195b0ef3ba..a7176e27ea1 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -28,7 +28,6 @@ data-toggle="dropdown" title="Manual build" data-placement="top" - data-toggle="dropdown" aria-label="Manual build" > <span v-html='svgs.iconPlay' aria-hidden="true"></span> @@ -54,7 +53,6 @@ data-toggle="dropdown" title="Artifacts" data-placement="top" - data-toggle="dropdown" aria-label="Artifacts" > <i class="fa fa-download" aria-hidden="true"></i> -- cgit v1.2.1 From 5e1243b0d6d53abc70733d9cf15cbd3ca2b4bf29 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Sat, 28 Jan 2017 15:41:40 -0600 Subject: fix test failure for merge request widget --- .../merge_requests/widget/open/_merge_when_build_succeeds.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index f70cd09c5f4..648a1e4cf33 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -1,5 +1,5 @@ - content_for :page_specific_javascripts do - = page_specific_javascript_tag('merge_request_widget/ci_bundle.js') + = page_specific_javascript_bundle_tag('merge_request_widget') %h4 Set by #{link_to_member(@project, @merge_request.merge_user, avatar: true)} -- cgit v1.2.1 From 356368d959dedce2ff83ae6caff4114444465389 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Sat, 28 Jan 2017 16:30:43 -0600 Subject: fix failing rspec build --- app/assets/javascripts/merge_request.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index d4e7fe235ad..8762ec35b80 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -110,9 +110,8 @@ require('./merge_request_tabs'); }; MergeRequest.prototype.initCommitMessageListeners = function() { - var textarea = $('textarea.js-commit-message'); - - $('a.js-with-description-link').on('click', function(e) { + $(document).on('click', 'a.js-with-description-link', function(e) { + var textarea = $('textarea.js-commit-message'); e.preventDefault(); textarea.val(textarea.data('messageWithDescription')); @@ -120,7 +119,8 @@ require('./merge_request_tabs'); $('p.js-without-description-hint').show(); }); - $('a.js-without-description-link').on('click', function(e) { + $(document).on('click', 'a.js-without-description-link', function(e) { + var textarea = $('textarea.js-commit-message'); e.preventDefault(); textarea.val(textarea.data('messageWithoutDescription')); -- cgit v1.2.1 From ecdcd1be87e140c144bb2afc861a1dc56b297cdd Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Sun, 29 Jan 2017 20:05:27 -0600 Subject: add CHAGELOG.md entry for webpack branch --- changelogs/unreleased/go-go-gadget-webpack.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/go-go-gadget-webpack.yml diff --git a/changelogs/unreleased/go-go-gadget-webpack.yml b/changelogs/unreleased/go-go-gadget-webpack.yml new file mode 100644 index 00000000000..7f372ccb428 --- /dev/null +++ b/changelogs/unreleased/go-go-gadget-webpack.yml @@ -0,0 +1,4 @@ +--- +title: use webpack to bundle frontend assets and use karma for frontend testing +merge_request: 7288 +author: -- cgit v1.2.1 From 6dbd60f695faf4126bd9fa2a5cd8a36f672563a4 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Mon, 30 Jan 2017 13:41:57 +0600 Subject: replace words user(s) with member(s) --- app/views/groups/group_members/_new_group_member.html.haml | 4 ++-- app/views/groups/group_members/index.html.haml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index b185b81db7f..5b1a4630c56 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -3,7 +3,7 @@ .col-md-4.col-lg-6 = users_select_tag(:user_ids, multiple: true, class: 'input-clamp', scope: :all, email_user: true) .help-block.append-bottom-10 - Search for users by name, username, or email, or invite new ones using their email address. + Search for members by name, username, or email, or invite new ones using their email address. .col-md-3.col-lg-2 = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "form-control project-access-select" @@ -16,7 +16,7 @@ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input .help-block.append-bottom-10 - On this date, the user(s) will automatically lose access to this group and all of its projects. + On this date, the member(s) will automatically lose access to this group and all of its projects. .col-md-2 = f.submit 'Add to group', class: "btn btn-create btn-block" diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index f4c432a095a..2e4e4511bb6 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -7,7 +7,7 @@ - if can?(current_user, :admin_group_member, @group) .project-members-new.append-bottom-default %p.clearfix - Add new user to + Add new member to %strong= @group.name = render "new_group_member" @@ -15,7 +15,7 @@ .append-bottom-default.clearfix %h5.member.existing-title - Existing users + Existing members = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do .form-group = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } @@ -24,7 +24,7 @@ = render 'shared/members/sort_dropdown' .panel.panel-default .panel-heading - Users with access to + Members with access to %strong= @group.name %span.badge= @members.total_count %ul.content-list -- cgit v1.2.1 From c95d88f923b14a3387c7d148318bcadf50767def Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Mon, 30 Jan 2017 13:50:01 +0600 Subject: adds changelog --- changelogs/unreleased/25460-replace-word-users-with-members.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/25460-replace-word-users-with-members.yml diff --git a/changelogs/unreleased/25460-replace-word-users-with-members.yml b/changelogs/unreleased/25460-replace-word-users-with-members.yml new file mode 100644 index 00000000000..dac90eaa34d --- /dev/null +++ b/changelogs/unreleased/25460-replace-word-users-with-members.yml @@ -0,0 +1,4 @@ +--- +title: Replace word user with member +merge_request: 8872 +author: -- cgit v1.2.1 From dc6921bdbbabd08be4426345140cb507b286eac7 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Tue, 10 Jan 2017 19:43:58 +0100 Subject: Chat Commands have presenters This improves the styling and readability of the code. This is supported by both Mattermost and Slack. --- .../chat_slash_commands_service.rb | 20 +-- lib/gitlab/chat_commands/base_command.rb | 4 - lib/gitlab/chat_commands/command.rb | 22 +-- lib/gitlab/chat_commands/deploy.rb | 24 ++-- lib/gitlab/chat_commands/issue_create.rb | 18 ++- lib/gitlab/chat_commands/issue_search.rb | 10 +- lib/gitlab/chat_commands/issue_show.rb | 8 +- lib/gitlab/chat_commands/presenter.rb | 131 ----------------- lib/gitlab/chat_commands/presenters/access.rb | 22 +++ lib/gitlab/chat_commands/presenters/base.rb | 73 ++++++++++ lib/gitlab/chat_commands/presenters/deploy.rb | 24 ++++ lib/gitlab/chat_commands/presenters/issuable.rb | 33 +++++ lib/gitlab/chat_commands/presenters/list_issues.rb | 32 +++++ lib/gitlab/chat_commands/presenters/show_issue.rb | 38 +++++ lib/mattermost/error.rb | 3 - lib/mattermost/session.rb | 160 --------------------- spec/lib/gitlab/chat_commands/command_spec.rb | 50 +------ spec/lib/gitlab/chat_commands/deploy_spec.rb | 24 ++-- spec/lib/gitlab/chat_commands/issue_create_spec.rb | 12 +- spec/lib/gitlab/chat_commands/issue_search_spec.rb | 12 +- spec/lib/gitlab/chat_commands/issue_show_spec.rb | 25 +++- .../gitlab/chat_commands/presenters/access_spec.rb | 49 +++++++ .../gitlab/chat_commands/presenters/deploy_spec.rb | 47 ++++++ .../chat_commands/presenters/list_issues_spec.rb | 24 ++++ .../chat_commands/presenters/show_issue_spec.rb | 27 ++++ spec/lib/mattermost/client_spec.rb | 24 ---- spec/lib/mattermost/command_spec.rb | 61 -------- spec/lib/mattermost/session_spec.rb | 123 ---------------- spec/lib/mattermost/team_spec.rb | 66 --------- 29 files changed, 479 insertions(+), 687 deletions(-) delete mode 100644 lib/gitlab/chat_commands/presenter.rb create mode 100644 lib/gitlab/chat_commands/presenters/access.rb create mode 100644 lib/gitlab/chat_commands/presenters/base.rb create mode 100644 lib/gitlab/chat_commands/presenters/deploy.rb create mode 100644 lib/gitlab/chat_commands/presenters/issuable.rb create mode 100644 lib/gitlab/chat_commands/presenters/list_issues.rb create mode 100644 lib/gitlab/chat_commands/presenters/show_issue.rb delete mode 100644 lib/mattermost/error.rb delete mode 100644 lib/mattermost/session.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/access_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb delete mode 100644 spec/lib/mattermost/client_spec.rb delete mode 100644 spec/lib/mattermost/command_spec.rb delete mode 100644 spec/lib/mattermost/session_spec.rb delete mode 100644 spec/lib/mattermost/team_spec.rb diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 2bcff541cc0..608754f3035 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -28,20 +28,24 @@ class ChatSlashCommandsService < Service end def trigger(params) - return unless valid_token?(params[:token]) + return access_presenter unless valid_token?(params[:token]) user = find_chat_user(params) - unless user + + if user + Gitlab::ChatCommands::Command.new(project, user, params).execute + else url = authorize_chat_name_url(params) - return presenter.authorize_chat_name(url) + access_presenter(url).authorize end - - Gitlab::ChatCommands::Command.new(project, user, - params).execute end private + def access_presenter(url = nil) + Gitlab::ChatCommands::Presenters::Access.new(url) + end + def find_chat_user(params) ChatNames::FindUserService.new(self, params).execute end @@ -49,8 +53,4 @@ class ChatSlashCommandsService < Service def authorize_chat_name_url(params) ChatNames::AuthorizeUserService.new(self, params).execute end - - def presenter - Gitlab::ChatCommands::Presenter.new - end end diff --git a/lib/gitlab/chat_commands/base_command.rb b/lib/gitlab/chat_commands/base_command.rb index 4fe53ce93a9..25da8474e95 100644 --- a/lib/gitlab/chat_commands/base_command.rb +++ b/lib/gitlab/chat_commands/base_command.rb @@ -42,10 +42,6 @@ module Gitlab def find_by_iid(iid) collection.find_by(iid: iid) end - - def presenter - Gitlab::ChatCommands::Presenter.new - end end end end diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 145086755e4..ac7ee868402 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -13,9 +13,9 @@ module Gitlab if command if command.allowed?(project, current_user) - present command.new(project, current_user, params).execute(match) + command.new(project, current_user, params).execute(match) else - access_denied + Gitlab::ChatCommands::Presenters::Access.new.access_denied end else help(help_messages) @@ -25,7 +25,7 @@ module Gitlab def match_command match = nil service = available_commands.find do |klass| - match = klass.match(command) + match = klass.match(params[:text]) end [service, match] @@ -42,22 +42,6 @@ module Gitlab klass.available?(project) end end - - def command - params[:text] - end - - def help(messages) - presenter.help(messages, params[:command]) - end - - def access_denied - presenter.access_denied - end - - def present(resource) - presenter.present(resource) - end end end end diff --git a/lib/gitlab/chat_commands/deploy.rb b/lib/gitlab/chat_commands/deploy.rb index 7127d2f6d04..458d90f84e8 100644 --- a/lib/gitlab/chat_commands/deploy.rb +++ b/lib/gitlab/chat_commands/deploy.rb @@ -1,8 +1,6 @@ module Gitlab module ChatCommands class Deploy < BaseCommand - include Gitlab::Routing.url_helpers - def self.match(text) /\Adeploy\s+(?<from>\S+.*)\s+to+\s+(?<to>\S+.*)\z/.match(text) end @@ -24,35 +22,29 @@ module Gitlab to = match[:to] actions = find_actions(from, to) - return unless actions.present? - if actions.one? - play!(from, to, actions.first) + if actions.none? + Gitlab::ChatCommands::Presenters::Deploy.new(nil).no_actions + elsif actions.one? + action = play!(from, to, actions.first) + Gitlab::ChatCommands::Presenters::Deploy.new(action).present(from, to) else - Result.new(:error, 'Too many actions defined') + Gitlab::ChatCommands::Presenters::Deploy.new(actions).too_many_actions end end private def play!(from, to, action) - new_action = action.play(current_user) - - Result.new(:success, "Deployment from #{from} to #{to} started. Follow the progress: #{url(new_action)}.") + action.play(current_user) end def find_actions(from, to) environment = project.environments.find_by(name: from) - return unless environment + return [] unless environment environment.actions_for(to).select(&:starts_environment?) end - - def url(subject) - polymorphic_url( - [subject.project.namespace.becomes(Namespace), subject.project, subject] - ) - end end end end diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index cefb6775db8..a06f13b0f72 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -2,7 +2,7 @@ module Gitlab module ChatCommands class IssueCreate < IssueCommand def self.match(text) - # we can not match \n with the dot by passing the m modifier as than + # we can not match \n with the dot by passing the m modifier as than # the title and description are not seperated /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) end @@ -19,8 +19,24 @@ module Gitlab title = match[:title] description = match[:description].to_s.rstrip + issue = create_issue(title: title, description: description) + + if issue.errors.any? + presenter(issue).display_errors + else + presenter(issue).present + end + end + + private + + def create_issue(title:, description:) Issues::CreateService.new(project, current_user, title: title, description: description).execute end + + def presenter(issue) + Gitlab::ChatCommands::Presenters::ShowIssue.new(issue) + end end end end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb index 51bf80c800b..e2d3a0f466a 100644 --- a/lib/gitlab/chat_commands/issue_search.rb +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -10,7 +10,15 @@ module Gitlab end def execute(match) - collection.search(match[:query]).limit(QUERY_LIMIT) + issues = collection.search(match[:query]).limit(QUERY_LIMIT) + + if issues.none? + Presenters::Access.new(issues).not_found + elsif issues.one? + Presenters::ShowIssue.new(issues.first).present + else + Presenters::ListIssues.new(issues).present + end end end end diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb index 2a45d49cf6b..9f3e1b9a64b 100644 --- a/lib/gitlab/chat_commands/issue_show.rb +++ b/lib/gitlab/chat_commands/issue_show.rb @@ -10,7 +10,13 @@ module Gitlab end def execute(match) - find_by_iid(match[:iid]) + issue = find_by_iid(match[:iid]) + + if issue + Gitlab::ChatCommands::Presenters::ShowIssue.new(issue).present + else + Gitlab::ChatCommands::Presenters::Access.new.not_found + end end end end diff --git a/lib/gitlab/chat_commands/presenter.rb b/lib/gitlab/chat_commands/presenter.rb deleted file mode 100644 index 8930a21f406..00000000000 --- a/lib/gitlab/chat_commands/presenter.rb +++ /dev/null @@ -1,131 +0,0 @@ -module Gitlab - module ChatCommands - class Presenter - include Gitlab::Routing - - def authorize_chat_name(url) - message = if url - ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{url})." - else - ":sweat_smile: Couldn't identify you, nor can I autorize you!" - end - - ephemeral_response(message) - end - - def help(commands, trigger) - if commands.none? - ephemeral_response("No commands configured") - else - commands.map! { |command| "#{trigger} #{command}" } - message = header_with_list("Available commands", commands) - - ephemeral_response(message) - end - end - - def present(subject) - return not_found unless subject - - if subject.is_a?(Gitlab::ChatCommands::Result) - show_result(subject) - elsif subject.respond_to?(:count) - if subject.none? - not_found - elsif subject.one? - single_resource(subject.first) - else - multiple_resources(subject) - end - else - single_resource(subject) - end - end - - def access_denied - ephemeral_response("Whoops! That action is not allowed. This incident will be [reported](https://xkcd.com/838/).") - end - - private - - def show_result(result) - case result.type - when :success - in_channel_response(result.message) - else - ephemeral_response(result.message) - end - end - - def not_found - ephemeral_response("404 not found! GitLab couldn't find what you were looking for! :boom:") - end - - def single_resource(resource) - return error(resource) if resource.errors.any? || !resource.persisted? - - message = "#{title(resource)}:" - message << "\n\n#{resource.description}" if resource.try(:description) - - in_channel_response(message) - end - - def multiple_resources(resources) - titles = resources.map { |resource| title(resource) } - - message = header_with_list("Multiple results were found:", titles) - - ephemeral_response(message) - end - - def error(resource) - message = header_with_list("The action was not successful, because:", resource.errors.messages) - - ephemeral_response(message) - end - - def title(resource) - reference = resource.try(:to_reference) || resource.try(:id) - title = resource.try(:title) || resource.try(:name) - - "[#{reference} #{title}](#{url(resource)})" - end - - def header_with_list(header, items) - message = [header] - - items.each do |item| - message << "- #{item}" - end - - message.join("\n") - end - - def url(resource) - url_for( - [ - resource.project.namespace.becomes(Namespace), - resource.project, - resource - ] - ) - end - - def ephemeral_response(message) - { - response_type: :ephemeral, - text: message, - status: 200 - } - end - - def in_channel_response(message) - { - response_type: :in_channel, - text: message, - status: 200 - } - end - end - end -end diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb new file mode 100644 index 00000000000..6d18d745608 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/access.rb @@ -0,0 +1,22 @@ +module Gitlab::ChatCommands::Presenters + class Access < Gitlab::ChatCommands::Presenters::Base + def access_denied + ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).") + end + + def not_found + ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:") + end + + def authorize + message = + if @resource + ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})." + else + ":sweat_smile: Couldn't identify you, nor can I autorize you!" + end + + ephemeral_response(text: message) + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb new file mode 100644 index 00000000000..0897025d85f --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/base.rb @@ -0,0 +1,73 @@ +module Gitlab::ChatCommands::Presenters + class Base + include Gitlab::Routing.url_helpers + + def initialize(resource = nil) + @resource = resource + end + + def display_errors + message = header_with_list("The action was not successful, because:", @resource.errors.full_messages) + + ephemeral_response(text: message) + end + + private + + def header_with_list(header, items) + message = [header] + + items.each do |item| + message << "- #{item}" + end + + message.join("\n") + end + + def ephemeral_response(message) + response = { + response_type: :ephemeral, + status: 200 + }.merge(message) + + format_response(response) + end + + def in_channel_response(message) + response = { + response_type: :in_channel, + status: 200 + }.merge(message) + + format_response(response) + end + + def format_response(response) + response[:text] = format(response[:text]) if response.has_key?(:text) + + if response.has_key?(:attachments) + response[:attachments].each do |attachment| + attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext] + attachment[:text] = format(attachment[:text]) if attachment[:text] + end + end + + response + end + + # Convert Markdown to slacks format + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end + + def resource_url + url_for( + [ + @resource.project.namespace.becomes(Namespace), + @resource.project, + @resource + ] + ) + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb new file mode 100644 index 00000000000..4f6333812ff --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -0,0 +1,24 @@ +module Gitlab::ChatCommands::Presenters + class Deploy < Gitlab::ChatCommands::Presenters::Base + def present(from, to) + message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})." + in_channel_response(text: message) + end + + def no_actions + ephemeral_response(text: "No action found to be executed") + end + + def too_many_actions + ephemeral_response(text: "Too many actions defined") + end + + private + + def resource_url + polymorphic_url( + [ @resource.project.namespace.becomes(Namespace), @resource.project, @resource] + ) + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb new file mode 100644 index 00000000000..9623387f188 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -0,0 +1,33 @@ +module Gitlab::ChatCommands::Presenters + class Issuable < Gitlab::ChatCommands::Presenters::Base + private + + def project + @resource.project + end + + def author + @resource.author + end + + def fields + [ + { + title: "Assignee", + value: @resource.assignee ? @resource.assignee.name : "_None_", + short: true + }, + { + title: "Milestone", + value: @resource.milestone ? @resource.milestone.title : "_None_", + short: true + }, + { + title: "Labels", + value: @resource.labels.any? ? @resource.label_names : "_None_", + short: true + } + ] + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/list_issues.rb b/lib/gitlab/chat_commands/presenters/list_issues.rb new file mode 100644 index 00000000000..5a7b3fca5c2 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/list_issues.rb @@ -0,0 +1,32 @@ +module Gitlab::ChatCommands::Presenters + class ListIssues < Gitlab::ChatCommands::Presenters::Base + def present + ephemeral_response(text: "Here are the issues I found:", attachments: attachments) + end + + private + + def attachments + @resource.map do |issue| + state = issue.open? ? "Open" : "Closed" + + { + fallback: "Issue #{issue.to_reference}: #{issue.title}", + color: "#d22852", + text: "[#{issue.to_reference}](#{url_for([namespace, project, issue])}) · #{issue.title} (#{state})", + mrkdwn_in: [ + "text" + ] + } + end + end + + def project + @project ||= @resource.first.project + end + + def namespace + @namespace ||= project.namespace.becomes(Namespace) + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/show_issue.rb b/lib/gitlab/chat_commands/presenters/show_issue.rb new file mode 100644 index 00000000000..2a89c30b972 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/show_issue.rb @@ -0,0 +1,38 @@ +module Gitlab::ChatCommands::Presenters + class ShowIssue < Gitlab::ChatCommands::Presenters::Issuable + def present + in_channel_response(show_issue) + end + + private + + def show_issue + { + attachments: [ + { + title: @resource.title, + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "#{@resource.to_reference}: #{@resource.title}", + text: text, + fields: fields, + mrkdwn_in: [ + :title, + :text + ] + } + ] + } + end + + def text + message = "" + message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero? + message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero? + message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero? + + message + end + end +end diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb deleted file mode 100644 index 014df175be0..00000000000 --- a/lib/mattermost/error.rb +++ /dev/null @@ -1,3 +0,0 @@ -module Mattermost - class Error < StandardError; end -end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb deleted file mode 100644 index 377cb7b1021..00000000000 --- a/lib/mattermost/session.rb +++ /dev/null @@ -1,160 +0,0 @@ -module Mattermost - class NoSessionError < Mattermost::Error - def message - 'No session could be set up, is Mattermost configured with Single Sign On?' - end - end - - class ConnectionError < Mattermost::Error; end - - # This class' prime objective is to obtain a session token on a Mattermost - # instance with SSO configured where this GitLab instance is the provider. - # - # The process depends on OAuth, but skips a step in the authentication cycle. - # For example, usually a user would click the 'login in GitLab' button on - # Mattermost, which would yield a 302 status code and redirects you to GitLab - # to approve the use of your account on Mattermost. Which would trigger a - # callback so Mattermost knows this request is approved and gets the required - # data to create the user account etc. - # - # This class however skips the button click, and also the approval phase to - # speed up the process and keep it without manual action and get a session - # going. - class Session - include Doorkeeper::Helpers::Controller - include HTTParty - - LEASE_TIMEOUT = 60 - - base_uri Settings.mattermost.host - - attr_accessor :current_resource_owner, :token - - def initialize(current_user) - @current_resource_owner = current_user - end - - def with_session - with_lease do - raise Mattermost::NoSessionError unless create - - begin - yield self - rescue Errno::ECONNREFUSED - raise Mattermost::NoSessionError - ensure - destroy - end - end - end - - # Next methods are needed for Doorkeeper - def pre_auth - @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new( - Doorkeeper.configuration, server.client_via_uid, params) - end - - def authorization - @authorization ||= strategy.request - end - - def strategy - @strategy ||= server.authorization_request(pre_auth.response_type) - end - - def request - @request ||= OpenStruct.new(parameters: params) - end - - def params - Rack::Utils.parse_query(oauth_uri.query).symbolize_keys - end - - def get(path, options = {}) - handle_exceptions do - self.class.get(path, options.merge(headers: @headers)) - end - end - - def post(path, options = {}) - handle_exceptions do - self.class.post(path, options.merge(headers: @headers)) - end - end - - private - - def create - return unless oauth_uri - return unless token_uri - - @token = request_token - @headers = { - Authorization: "Bearer #{@token}" - } - - @token - end - - def destroy - post('/api/v3/users/logout') - end - - def oauth_uri - return @oauth_uri if defined?(@oauth_uri) - - @oauth_uri = nil - - response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) - return unless 300 <= response.code && response.code < 400 - - redirect_uri = response.headers['location'] - return unless redirect_uri - - @oauth_uri = URI.parse(redirect_uri) - end - - def token_uri - @token_uri ||= - if oauth_uri - authorization.authorize.redirect_uri if pre_auth.authorizable? - end - end - - def request_token - response = get(token_uri, follow_redirects: false) - - if 200 <= response.code && response.code < 400 - response.headers['token'] - end - end - - def with_lease - lease_uuid = lease_try_obtain - raise NoSessionError unless lease_uuid - - begin - yield - ensure - Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) - end - end - - def lease_key - "mattermost:session" - end - - def lease_try_obtain - lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) - lease.try_obtain - end - - def handle_exceptions - yield - rescue HTTParty::Error => e - raise Mattermost::ConnectionError.new(e.message) - rescue Errno::ECONNREFUSED - raise Mattermost::ConnectionError.new(e.message) - end - end -end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index 1e81eaef18c..d8b2303555c 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -5,6 +5,7 @@ describe Gitlab::ChatCommands::Command, service: true do let(:user) { create(:user) } describe '#execute' do +<<<<<<< HEAD subject do described_class.new(project, user, params).execute end @@ -18,6 +19,9 @@ describe Gitlab::ChatCommands::Command, service: true do expect(subject[:text]).to start_with('404 not found') end end +======= + subject { described_class.new(project, user, params).execute } +>>>>>>> Chat Commands have presenters context 'when an unknown command is triggered' do let(:params) { { command: '/gitlab', text: "unknown command 123" } } @@ -34,47 +38,7 @@ describe Gitlab::ChatCommands::Command, service: true do it 'rejects the actions' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Whoops! That action is not allowed') - end - end - - context 'issue is successfully created' do - let(:params) { { text: "issue create my new issue" } } - - before do - project.team << [user, :master] - end - - it 'presents the issue' do - expect(subject[:text]).to match("my new issue") - end - - it 'shows a link to the new issue' do - expect(subject[:text]).to match(/\/issues\/\d+/) - end - end - - context 'searching for an issue' do - let(:params) { { text: 'issue search find me' } } - let!(:issue) { create(:issue, project: project, title: 'find me') } - - before do - project.team << [user, :master] - end - - context 'a single issue is found' do - it 'presents the issue' do - expect(subject[:text]).to match(issue.title) - end - end - - context 'multiple issues found' do - let!(:issue2) { create(:issue, project: project, title: "someone find me") } - - it 'shows a link to the new issue' do - expect(subject[:text]).to match(issue.title) - expect(subject[:text]).to match(issue2.title) - end + expect(subject[:text]).to start_with('Whoops! This action is not allowed') end end @@ -90,7 +54,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'and user can not create deployment' do it 'returns action' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Whoops! That action is not allowed') + expect(subject[:text]).to start_with('Whoops! This action is not allowed') end end @@ -100,7 +64,7 @@ describe Gitlab::ChatCommands::Command, service: true do end it 'returns action' do - expect(subject[:text]).to include('Deployment from staging to production started.') + expect(subject[:text]).to include('Deployment started from staging to production') expect(subject[:response_type]).to be(:in_channel) end diff --git a/spec/lib/gitlab/chat_commands/deploy_spec.rb b/spec/lib/gitlab/chat_commands/deploy_spec.rb index bd8099c92da..b3358a32161 100644 --- a/spec/lib/gitlab/chat_commands/deploy_spec.rb +++ b/spec/lib/gitlab/chat_commands/deploy_spec.rb @@ -15,8 +15,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do end context 'if no environment is defined' do - it 'returns nil' do - expect(subject).to be_nil + it 'does not execute an action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") end end @@ -26,8 +27,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do let!(:deployment) { create(:deployment, environment: staging, deployable: build) } context 'without actions' do - it 'returns nil' do - expect(subject).to be_nil + it 'does not execute an action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") end end @@ -37,8 +39,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do end it 'returns success result' do - expect(subject.type).to eq(:success) - expect(subject.message).to include('Deployment from staging to production started') + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with('Deployment started from staging to production') end context 'when duplicate action exists' do @@ -47,8 +49,8 @@ describe Gitlab::ChatCommands::Deploy, service: true do end it 'returns error' do - expect(subject.type).to eq(:error) - expect(subject.message).to include('Too many actions defined') + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq('Too many actions defined') end end @@ -59,9 +61,9 @@ describe Gitlab::ChatCommands::Deploy, service: true do name: 'teardown', environment: 'production') end - it 'returns success result' do - expect(subject.type).to eq(:success) - expect(subject.message).to include('Deployment from staging to production started') + it 'returns the success message' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with('Deployment started from staging to production') end end end diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb index 6c71e79ff6d..0f84b19a5a4 100644 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_create_spec.rb @@ -18,7 +18,7 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do it 'creates the issue' do expect { subject }.to change { project.issues.count }.by(1) - expect(subject.title).to eq('bird is the word') + expect(subject[:response_type]).to be(:in_channel) end end @@ -41,6 +41,16 @@ describe Gitlab::ChatCommands::IssueCreate, service: true do expect { subject }.to change { project.issues.count }.by(1) end end + + context 'issue cannot be created' do + let!(:issue) { create(:issue, project: project, title: 'bird is the word') } + let(:regex_match) { described_class.match("issue create #{'a' * 512}}") } + + it 'displays the errors' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("- Title is too long") + end + end end describe '.match' do diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb index 24c06a967fa..04d10ad52a1 100644 --- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' describe Gitlab::ChatCommands::IssueSearch, service: true do describe '#execute' do - let!(:issue) { create(:issue, title: 'find me') } + let!(:issue) { create(:issue, project: project, title: 'find me') } let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') } - let(:project) { issue.project } + let(:project) { create(:empty_project) } let(:user) { issue.author } let(:regex_match) { described_class.match("issue search find") } @@ -14,7 +14,8 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do context 'when the user has no access' do it 'only returns the open issues' do - expect(subject).not_to include(confidential) + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("not found") end end @@ -24,13 +25,14 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do end it 'returns all results' do - expect(subject).to include(confidential, issue) + expect(subject).to have_key(:attachments) + expect(subject[:text]).to match("Here are the issues I found:") end end context 'without hits on the query' do it 'returns an empty collection' do - expect(subject).to be_empty + expect(subject[:text]).to match("not found") end end end diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb index 2eab73e49e5..89932c395c6 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe Gitlab::ChatCommands::IssueShow, service: true do describe '#execute' do - let(:issue) { create(:issue) } - let(:project) { issue.project } + let(:issue) { create(:issue, project: project) } + let(:project) { create(:empty_project) } let(:user) { issue.author } let(:regex_match) { described_class.match("issue show #{issue.iid}") } @@ -16,15 +16,19 @@ describe Gitlab::ChatCommands::IssueShow, service: true do end context 'the issue exists' do + let(:title) { subject[:attachments].first[:title] } + it 'returns the issue' do - expect(subject.iid).to be issue.iid + expect(subject[:response_type]).to be(:in_channel) + expect(title).to eq(issue.title) end context 'when its reference is given' do let(:regex_match) { described_class.match("issue show #{issue.to_reference}") } it 'shows the issue' do - expect(subject.iid).to be issue.iid + expect(subject[:response_type]).to be(:in_channel) + expect(title).to eq(issue.title) end end end @@ -32,17 +36,24 @@ describe Gitlab::ChatCommands::IssueShow, service: true do context 'the issue does not exist' do let(:regex_match) { described_class.match("issue show 2343242") } - it "returns nil" do - expect(subject).to be_nil + it "returns not found" do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("not found") end end end - describe 'self.match' do + describe '.match' do it 'matches the iid' do match = described_class.match("issue show 123") expect(match[:iid]).to eq("123") end + + it 'accepts a reference' do + match = described_class.match("issue show #{Issue.reference_prefix}123") + + expect(match[:iid]).to eq("123") + end end end diff --git a/spec/lib/gitlab/chat_commands/presenters/access_spec.rb b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb new file mode 100644 index 00000000000..ae41d75ab0c --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/access_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::Access do + describe '#access_denied' do + subject { described_class.new.access_denied } + + it { is_expected.to be_a(Hash) } + + it 'displays an error message' do + expect(subject[:text]).to match("is not allowed") + expect(subject[:response_type]).to be(:ephemeral) + end + end + + describe '#not_found' do + subject { described_class.new.not_found } + + it { is_expected.to be_a(Hash) } + + it 'tells the user the resource was not found' do + expect(subject[:text]).to match("not found!") + expect(subject[:response_type]).to be(:ephemeral) + end + end + + describe '#authorize' do + context 'with an authorization URL' do + subject { described_class.new('http://authorize.me').authorize } + + it { is_expected.to be_a(Hash) } + + it 'tells the user to authorize' do + expect(subject[:text]).to match("connect your GitLab account") + expect(subject[:response_type]).to be(:ephemeral) + end + end + + context 'without authorization url' do + subject { described_class.new.authorize } + + it { is_expected.to be_a(Hash) } + + it 'tells the user to authorize' do + expect(subject[:text]).to match("Couldn't identify you") + expect(subject[:response_type]).to be(:ephemeral) + end + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb new file mode 100644 index 00000000000..1c48c727e30 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::Deploy do + let(:build) { create(:ci_build) } + + describe '#present' do + subject { described_class.new(build).present('staging', 'prod') } + + it { is_expected.to have_key(:text) } + it { is_expected.to have_key(:response_type) } + it { is_expected.to have_key(:status) } + it { is_expected.not_to have_key(:attachments) } + + it 'messages the channel of the deploy' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject[:text]).to start_with("Deployment started from staging to prod") + end + end + + describe '#no_actions' do + subject { described_class.new(nil).no_actions } + + it { is_expected.to have_key(:text) } + it { is_expected.to have_key(:response_type) } + it { is_expected.to have_key(:status) } + it { is_expected.not_to have_key(:attachments) } + + it 'tells the user there is no action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("No action found to be executed") + end + end + + describe '#too_many_actions' do + subject { described_class.new(nil).too_many_actions } + + it { is_expected.to have_key(:text) } + it { is_expected.to have_key(:response_type) } + it { is_expected.to have_key(:status) } + it { is_expected.not_to have_key(:attachments) } + + it 'tells the user there is no action' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to eq("Too many actions defined") + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb new file mode 100644 index 00000000000..1852395fc97 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::ListIssues do + let(:project) { create(:empty_project) } + let(:message) { subject[:text] } + let(:issue) { project.issues.first } + + before { create_list(:issue, 2, project: project) } + + subject { described_class.new(project.issues).present } + + it do + is_expected.to have_key(:text) + is_expected.to have_key(:status) + is_expected.to have_key(:response_type) + is_expected.to have_key(:attachments) + end + + it 'shows a list of results' do + expect(subject[:response_type]).to be(:ephemeral) + + expect(message).to start_with("Here are the issues I found") + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb new file mode 100644 index 00000000000..13a318fe680 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::ShowIssue do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:attachment) { subject[:attachments].first } + + subject { described_class.new(issue).present } + + it { is_expected.to be_a(Hash) } + + it 'shows the issue' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject).to have_key(:attachments) + expect(attachment[:title]).to eq(issue.title) + end + + context 'with upvotes' do + before do + create(:award_emoji, :upvote, awardable: issue) + end + + it 'shows the upvote count' do + expect(attachment[:text]).to start_with(":+1: 1") + end + end +end diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb deleted file mode 100644 index dc11a414717..00000000000 --- a/spec/lib/mattermost/client_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Client do - let(:user) { build(:user) } - - subject { described_class.new(user) } - - context 'JSON parse error' do - before do - Struct.new("Request", :body, :success?) - end - - it 'yields an error on malformed JSON' do - bad_json = Struct::Request.new("I'm not json", true) - expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError) - end - - it 'shows a client error if the request was unsuccessful' do - bad_request = Struct::Request.new("true", false) - - expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError) - end - end -end diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb deleted file mode 100644 index 5ccf1100898..00000000000 --- a/spec/lib/mattermost/command_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Command do - let(:params) { { 'token' => 'token', team_id: 'abc' } } - - before do - Mattermost::Session.base_uri('http://mattermost.example.com') - - allow_any_instance_of(Mattermost::Client).to receive(:with_session). - and_yield(Mattermost::Session.new(nil)) - end - - describe '#create' do - let(:params) do - { team_id: 'abc', - trigger: 'gitlab' - } - end - - subject { described_class.new(nil).create(params) } - - context 'for valid trigger word' do - before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). - with(body: { - team_id: 'abc', - trigger: 'gitlab' }.to_json). - to_return( - status: 200, - headers: { 'Content-Type' => 'application/json' }, - body: { token: 'token' }.to_json - ) - end - - it 'returns a token' do - is_expected.to eq('token') - end - end - - context 'for error message' do - before do - stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). - to_return( - status: 500, - headers: { 'Content-Type' => 'application/json' }, - body: { - id: 'api.command.duplicate_trigger.app_error', - message: 'This trigger word is already in use. Please choose another word.', - detailed_error: '', - request_id: 'obc374man7bx5r3dbc1q5qhf3r', - status_code: 500 - }.to_json - ) - end - - it 'raises an error with message' do - expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.') - end - end - end -end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb deleted file mode 100644 index 74d12e37181..00000000000 --- a/spec/lib/mattermost/session_spec.rb +++ /dev/null @@ -1,123 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Session, type: :request do - let(:user) { create(:user) } - - let(:gitlab_url) { "http://gitlab.com" } - let(:mattermost_url) { "http://mattermost.com" } - - subject { described_class.new(user) } - - # Needed for doorkeeper to function - it { is_expected.to respond_to(:current_resource_owner) } - it { is_expected.to respond_to(:request) } - it { is_expected.to respond_to(:authorization) } - it { is_expected.to respond_to(:strategy) } - - before do - described_class.base_uri(mattermost_url) - end - - describe '#with session' do - let(:location) { 'http://location.tld' } - let!(:stub) do - WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login"). - to_return(headers: { 'location' => location }, status: 307) - end - - context 'without oauth uri' do - it 'makes a request to the oauth uri' do - expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) - end - end - - context 'with oauth_uri' do - let!(:doorkeeper) do - Doorkeeper::Application.create( - name: "GitLab Mattermost", - redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete", - scopes: "") - end - - context 'without token_uri' do - it 'can not create a session' do - expect do - subject.with_session - end.to raise_error(Mattermost::NoSessionError) - end - end - - context 'with token_uri' do - let(:state) { "state" } - let(:params) do - { response_type: "code", - client_id: doorkeeper.uid, - redirect_uri: "#{mattermost_url}/signup/gitlab/complete", - state: state } - end - let(:location) do - "#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}" - end - - before do - WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete"). - with(query: hash_including({ 'state' => state })). - to_return do |request| - post "/oauth/token", - client_id: doorkeeper.uid, - client_secret: doorkeeper.secret, - redirect_uri: params[:redirect_uri], - grant_type: 'authorization_code', - code: request.uri.query_values['code'] - - if response.status == 200 - { headers: { 'token' => 'thisworksnow' }, status: 202 } - end - end - - WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout"). - to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) - end - - it 'can setup a session' do - subject.with_session do |session| - end - - expect(subject.token).not_to be_nil - end - - it 'returns the value of the block' do - result = subject.with_session do |session| - "value" - end - - expect(result).to eq("value") - end - end - end - - context 'with lease' do - before do - allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk') - end - - it 'tries to obtain a lease' do - expect(subject).to receive(:lease_try_obtain) - expect(Gitlab::ExclusiveLease).to receive(:cancel) - - # Cannot setup a session, but we should still cancel the lease - expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) - end - end - - context 'without lease' do - before do - allow(subject).to receive(:lease_try_obtain).and_return(nil) - end - - it 'returns a NoSessionError error' do - expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) - end - end - end -end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb deleted file mode 100644 index 2d14be6bcc2..00000000000 --- a/spec/lib/mattermost/team_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -require 'spec_helper' - -describe Mattermost::Team do - before do - Mattermost::Session.base_uri('http://mattermost.example.com') - - allow_any_instance_of(Mattermost::Client).to receive(:with_session). - and_yield(Mattermost::Session.new(nil)) - end - - describe '#all' do - subject { described_class.new(nil).all } - - context 'for valid request' do - let(:response) do - [{ - "id" => "xiyro8huptfhdndadpz8r3wnbo", - "create_at" => 1482174222155, - "update_at" => 1482174222155, - "delete_at" => 0, - "display_name" => "chatops", - "name" => "chatops", - "email" => "admin@example.com", - "type" => "O", - "company_name" => "", - "allowed_domains" => "", - "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", - "allow_open_invite" => false }] - end - - before do - stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). - to_return( - status: 200, - headers: { 'Content-Type' => 'application/json' }, - body: response.to_json - ) - end - - it 'returns a token' do - is_expected.to eq(response) - end - end - - context 'for error message' do - before do - stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). - to_return( - status: 500, - headers: { 'Content-Type' => 'application/json' }, - body: { - id: 'api.team.list.app_error', - message: 'Cannot list teams.', - detailed_error: '', - request_id: 'obc374man7bx5r3dbc1q5qhf3r', - status_code: 500 - }.to_json - ) - end - - it 'raises an error with message' do - expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.') - end - end - end -end -- cgit v1.2.1 From 746f47208dc52cd6ca68c0893de5513c250f524b Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Wed, 11 Jan 2017 08:54:44 -0500 Subject: Revert removing of some files --- lib/mattermost/command.rb | 4 ++ lib/mattermost/error.rb | 3 + lib/mattermost/session.rb | 160 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 lib/mattermost/error.rb create mode 100644 lib/mattermost/session.rb diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index 33e450d7f0a..2e4f7705f86 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,7 +1,11 @@ module Mattermost class Command < Client def create(params) +<<<<<<< HEAD response = session_post("/api/v3/teams/#{params[:team_id]}/commands/create", +======= + response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create", +>>>>>>> Revert removing of some files body: params.to_json) response['token'] diff --git a/lib/mattermost/error.rb b/lib/mattermost/error.rb new file mode 100644 index 00000000000..014df175be0 --- /dev/null +++ b/lib/mattermost/error.rb @@ -0,0 +1,3 @@ +module Mattermost + class Error < StandardError; end +end diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb new file mode 100644 index 00000000000..377cb7b1021 --- /dev/null +++ b/lib/mattermost/session.rb @@ -0,0 +1,160 @@ +module Mattermost + class NoSessionError < Mattermost::Error + def message + 'No session could be set up, is Mattermost configured with Single Sign On?' + end + end + + class ConnectionError < Mattermost::Error; end + + # This class' prime objective is to obtain a session token on a Mattermost + # instance with SSO configured where this GitLab instance is the provider. + # + # The process depends on OAuth, but skips a step in the authentication cycle. + # For example, usually a user would click the 'login in GitLab' button on + # Mattermost, which would yield a 302 status code and redirects you to GitLab + # to approve the use of your account on Mattermost. Which would trigger a + # callback so Mattermost knows this request is approved and gets the required + # data to create the user account etc. + # + # This class however skips the button click, and also the approval phase to + # speed up the process and keep it without manual action and get a session + # going. + class Session + include Doorkeeper::Helpers::Controller + include HTTParty + + LEASE_TIMEOUT = 60 + + base_uri Settings.mattermost.host + + attr_accessor :current_resource_owner, :token + + def initialize(current_user) + @current_resource_owner = current_user + end + + def with_session + with_lease do + raise Mattermost::NoSessionError unless create + + begin + yield self + rescue Errno::ECONNREFUSED + raise Mattermost::NoSessionError + ensure + destroy + end + end + end + + # Next methods are needed for Doorkeeper + def pre_auth + @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new( + Doorkeeper.configuration, server.client_via_uid, params) + end + + def authorization + @authorization ||= strategy.request + end + + def strategy + @strategy ||= server.authorization_request(pre_auth.response_type) + end + + def request + @request ||= OpenStruct.new(parameters: params) + end + + def params + Rack::Utils.parse_query(oauth_uri.query).symbolize_keys + end + + def get(path, options = {}) + handle_exceptions do + self.class.get(path, options.merge(headers: @headers)) + end + end + + def post(path, options = {}) + handle_exceptions do + self.class.post(path, options.merge(headers: @headers)) + end + end + + private + + def create + return unless oauth_uri + return unless token_uri + + @token = request_token + @headers = { + Authorization: "Bearer #{@token}" + } + + @token + end + + def destroy + post('/api/v3/users/logout') + end + + def oauth_uri + return @oauth_uri if defined?(@oauth_uri) + + @oauth_uri = nil + + response = get("/api/v3/oauth/gitlab/login", follow_redirects: false) + return unless 300 <= response.code && response.code < 400 + + redirect_uri = response.headers['location'] + return unless redirect_uri + + @oauth_uri = URI.parse(redirect_uri) + end + + def token_uri + @token_uri ||= + if oauth_uri + authorization.authorize.redirect_uri if pre_auth.authorizable? + end + end + + def request_token + response = get(token_uri, follow_redirects: false) + + if 200 <= response.code && response.code < 400 + response.headers['token'] + end + end + + def with_lease + lease_uuid = lease_try_obtain + raise NoSessionError unless lease_uuid + + begin + yield + ensure + Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) + end + end + + def lease_key + "mattermost:session" + end + + def lease_try_obtain + lease = ::Gitlab::ExclusiveLease.new(lease_key, timeout: LEASE_TIMEOUT) + lease.try_obtain + end + + def handle_exceptions + yield + rescue HTTParty::Error => e + raise Mattermost::ConnectionError.new(e.message) + rescue Errno::ECONNREFUSED + raise Mattermost::ConnectionError.new(e.message) + end + end +end -- cgit v1.2.1 From 53846da2c7fe3879b4f26383d6367b0bb69e5dc8 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Wed, 11 Jan 2017 09:04:49 -0500 Subject: Add help command --- lib/gitlab/chat_commands/command.rb | 13 +++++-------- lib/gitlab/chat_commands/help.rb | 28 ++++++++++++++++++++++++++++ lib/gitlab/chat_commands/presenters/help.rb | 20 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 lib/gitlab/chat_commands/help.rb create mode 100644 lib/gitlab/chat_commands/presenters/help.rb diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index ac7ee868402..4e5031a8a26 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -18,25 +18,22 @@ module Gitlab Gitlab::ChatCommands::Presenters::Access.new.access_denied end else - help(help_messages) + Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands) end end def match_command match = nil - service = available_commands.find do |klass| - match = klass.match(params[:text]) - end + service = + available_commands.find do |klass| + match = klass.match(params[:text]) + end [service, match] end private - def help_messages - available_commands.map(&:help_message) - end - def available_commands COMMANDS.select do |klass| klass.available?(project) diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb new file mode 100644 index 00000000000..e76733f5445 --- /dev/null +++ b/lib/gitlab/chat_commands/help.rb @@ -0,0 +1,28 @@ +module Gitlab + module ChatCommands + class Help < BaseCommand + # This class has to be used last, as it always matches. It has to match + # because other commands were not triggered and we want to show the help + # command + def self.match(_text) + true + end + + def self.help_message + 'help' + end + + def self.allowed?(_project, _user) + true + end + + def execute(commands) + Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger) + end + + def trigger + params[:command] + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb new file mode 100644 index 00000000000..133b707231f --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -0,0 +1,20 @@ +module Gitlab::ChatCommands::Presenters + class Help < Gitlab::ChatCommands::Presenters::Base + def present(trigger) + message = + if @resource.none? + "No commands available :thinking_face:" + else + header_with_list("Available commands", full_commands(trigger)) + end + + ephemeral_response(text: message) + end + + private + + def full_commands(trigger) + @resource.map { |command| "#{trigger} #{command.help_message}" } + end + end +end -- cgit v1.2.1 From 72843e021dba0022b75f3fd3988115691c19a4fb Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 12 Jan 2017 09:04:21 -0500 Subject: Fix tests --- app/models/project_services/chat_slash_commands_service.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 608754f3035..5eb1bd86e9d 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -28,7 +28,7 @@ class ChatSlashCommandsService < Service end def trigger(params) - return access_presenter unless valid_token?(params[:token]) + return unless valid_token?(params[:token]) user = find_chat_user(params) @@ -36,16 +36,12 @@ class ChatSlashCommandsService < Service Gitlab::ChatCommands::Command.new(project, user, params).execute else url = authorize_chat_name_url(params) - access_presenter(url).authorize + Gitlab::ChatCommands::Presenters::Access.new(url).authorize end end private - def access_presenter(url = nil) - Gitlab::ChatCommands::Presenters::Access.new(url) - end - def find_chat_user(params) ChatNames::FindUserService.new(self, params).execute end -- cgit v1.2.1 From 4ce1a17c9767a80dfae0b47cee236d2a5d88918b Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 19 Jan 2017 09:22:09 +0100 Subject: Incorporate feedback --- changelogs/unreleased/zj-format-chat-messages.yml | 4 + lib/gitlab/chat_commands/issue_create.rb | 2 +- lib/gitlab/chat_commands/presenters/access.rb | 36 +++--- lib/gitlab/chat_commands/presenters/base.rb | 112 ++++++++++--------- lib/gitlab/chat_commands/presenters/deploy.rb | 39 ++++--- lib/gitlab/chat_commands/presenters/help.rb | 31 +++--- lib/gitlab/chat_commands/presenters/issuable.rb | 66 ++++++----- lib/gitlab/chat_commands/presenters/list_issues.rb | 59 ++++++---- lib/gitlab/chat_commands/presenters/new_issue.rb | 42 +++++++ lib/gitlab/chat_commands/presenters/show_issue.rb | 72 +++++++----- spec/lib/gitlab/chat_commands/issue_search_spec.rb | 2 +- spec/lib/gitlab/chat_commands/issue_show_spec.rb | 4 +- .../gitlab/chat_commands/presenters/deploy_spec.rb | 2 +- .../chat_commands/presenters/list_issues_spec.rb | 5 +- .../chat_commands/presenters/show_issue_spec.rb | 4 +- spec/lib/mattermost/client_spec.rb | 24 ++++ spec/lib/mattermost/command_spec.rb | 61 ++++++++++ spec/lib/mattermost/session_spec.rb | 123 +++++++++++++++++++++ spec/lib/mattermost/team_spec.rb | 66 +++++++++++ 19 files changed, 565 insertions(+), 189 deletions(-) create mode 100644 changelogs/unreleased/zj-format-chat-messages.yml create mode 100644 lib/gitlab/chat_commands/presenters/new_issue.rb create mode 100644 spec/lib/mattermost/client_spec.rb create mode 100644 spec/lib/mattermost/command_spec.rb create mode 100644 spec/lib/mattermost/session_spec.rb create mode 100644 spec/lib/mattermost/team_spec.rb diff --git a/changelogs/unreleased/zj-format-chat-messages.yml b/changelogs/unreleased/zj-format-chat-messages.yml new file mode 100644 index 00000000000..2494884f5c9 --- /dev/null +++ b/changelogs/unreleased/zj-format-chat-messages.yml @@ -0,0 +1,4 @@ +--- +title: Reformat messages ChatOps +merge_request: 8528 +author: diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb index a06f13b0f72..3f3d7de8b2e 100644 --- a/lib/gitlab/chat_commands/issue_create.rb +++ b/lib/gitlab/chat_commands/issue_create.rb @@ -35,7 +35,7 @@ module Gitlab end def presenter(issue) - Gitlab::ChatCommands::Presenters::ShowIssue.new(issue) + Gitlab::ChatCommands::Presenters::NewIssue.new(issue) end end end diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb index 6d18d745608..b66ef48d6a8 100644 --- a/lib/gitlab/chat_commands/presenters/access.rb +++ b/lib/gitlab/chat_commands/presenters/access.rb @@ -1,22 +1,26 @@ -module Gitlab::ChatCommands::Presenters - class Access < Gitlab::ChatCommands::Presenters::Base - def access_denied - ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).") - end - - def not_found - ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:") - end +module Gitlab + module ChatCommands + module Presenters + class Access < Presenters::Base + def access_denied + ephemeral_response(text: "Whoops! This action is not allowed. This incident will be [reported](https://xkcd.com/838/).") + end - def authorize - message = - if @resource - ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})." - else - ":sweat_smile: Couldn't identify you, nor can I autorize you!" + def not_found + ephemeral_response(text: "404 not found! GitLab couldn't find what you were looking for! :boom:") end - ephemeral_response(text: message) + def authorize + message = + if @resource + ":wave: Hi there! Before I do anything for you, please [connect your GitLab account](#{@resource})." + else + ":sweat_smile: Couldn't identify you, nor can I autorize you!" + end + + ephemeral_response(text: message) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/base.rb b/lib/gitlab/chat_commands/presenters/base.rb index 0897025d85f..2700a5a2ad5 100644 --- a/lib/gitlab/chat_commands/presenters/base.rb +++ b/lib/gitlab/chat_commands/presenters/base.rb @@ -1,73 +1,77 @@ -module Gitlab::ChatCommands::Presenters - class Base - include Gitlab::Routing.url_helpers +module Gitlab + module ChatCommands + module Presenters + class Base + include Gitlab::Routing.url_helpers + + def initialize(resource = nil) + @resource = resource + end - def initialize(resource = nil) - @resource = resource - end + def display_errors + message = header_with_list("The action was not successful, because:", @resource.errors.full_messages) - def display_errors - message = header_with_list("The action was not successful, because:", @resource.errors.full_messages) + ephemeral_response(text: message) + end - ephemeral_response(text: message) - end + private - private + def header_with_list(header, items) + message = [header] - def header_with_list(header, items) - message = [header] + items.each do |item| + message << "- #{item}" + end - items.each do |item| - message << "- #{item}" - end + message.join("\n") + end - message.join("\n") - end + def ephemeral_response(message) + response = { + response_type: :ephemeral, + status: 200 + }.merge(message) - def ephemeral_response(message) - response = { - response_type: :ephemeral, - status: 200 - }.merge(message) + format_response(response) + end - format_response(response) - end + def in_channel_response(message) + response = { + response_type: :in_channel, + status: 200 + }.merge(message) - def in_channel_response(message) - response = { - response_type: :in_channel, - status: 200 - }.merge(message) + format_response(response) + end - format_response(response) - end + def format_response(response) + response[:text] = format(response[:text]) if response.has_key?(:text) - def format_response(response) - response[:text] = format(response[:text]) if response.has_key?(:text) + if response.has_key?(:attachments) + response[:attachments].each do |attachment| + attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext] + attachment[:text] = format(attachment[:text]) if attachment[:text] + end + end - if response.has_key?(:attachments) - response[:attachments].each do |attachment| - attachment[:pretext] = format(attachment[:pretext]) if attachment[:pretext] - attachment[:text] = format(attachment[:text]) if attachment[:text] + response end - end - - response - end - # Convert Markdown to slacks format - def format(string) - Slack::Notifier::LinkFormatter.format(string) - end + # Convert Markdown to slacks format + def format(string) + Slack::Notifier::LinkFormatter.format(string) + end - def resource_url - url_for( - [ - @resource.project.namespace.becomes(Namespace), - @resource.project, - @resource - ] - ) + def resource_url + url_for( + [ + @resource.project.namespace.becomes(Namespace), + @resource.project, + @resource + ] + ) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb index 4f6333812ff..b1cfaac15af 100644 --- a/lib/gitlab/chat_commands/presenters/deploy.rb +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -1,24 +1,29 @@ -module Gitlab::ChatCommands::Presenters - class Deploy < Gitlab::ChatCommands::Presenters::Base - def present(from, to) - message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})." - in_channel_response(text: message) - end +module Gitlab + module ChatCommands + module Presenters + class Deploy < Presenters::Base + def present(from, to) + message = "Deployment started from #{from} to #{to}. [Follow its progress](#{resource_url})." - def no_actions - ephemeral_response(text: "No action found to be executed") - end + in_channel_response(text: message) + end - def too_many_actions - ephemeral_response(text: "Too many actions defined") - end + def no_actions + ephemeral_response(text: "No action found to be executed") + end + + def too_many_actions + ephemeral_response(text: "Too many actions defined") + end - private + private - def resource_url - polymorphic_url( - [ @resource.project.namespace.becomes(Namespace), @resource.project, @resource] - ) + def resource_url + polymorphic_url( + [ @resource.project.namespace.becomes(Namespace), @resource.project, @resource] + ) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb index 133b707231f..c7a67467b7e 100644 --- a/lib/gitlab/chat_commands/presenters/help.rb +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -1,20 +1,25 @@ -module Gitlab::ChatCommands::Presenters - class Help < Gitlab::ChatCommands::Presenters::Base - def present(trigger) - message = - if @resource.none? - "No commands available :thinking_face:" - else - header_with_list("Available commands", full_commands(trigger)) +module Gitlab + module ChatCommands + module Presenters + class Help < Presenters::Base + def present(trigger) + ephemeral_response(text: help_message(trigger)) end - ephemeral_response(text: message) - end + private - private + def help_message(trigger) + if @resource.none? + "No commands available :thinking_face:" + else + header_with_list("Available commands", full_commands(trigger)) + end + end - def full_commands(trigger) - @resource.map { |command| "#{trigger} #{command.help_message}" } + def full_commands(trigger) + @resource.map { |command| "#{trigger} #{command.help_message}" } + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb index 9623387f188..2cb6b1525fc 100644 --- a/lib/gitlab/chat_commands/presenters/issuable.rb +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -1,33 +1,45 @@ -module Gitlab::ChatCommands::Presenters - class Issuable < Gitlab::ChatCommands::Presenters::Base - private +module Gitlab + module ChatCommands + module Presenters + class Issuable < Presenters::Base + private - def project - @resource.project - end + def color(issuable) + issuable.open? ? '#38ae67' : '#d22852' + end - def author - @resource.author - end + def status_text(issuable) + issuable.open? ? 'Open' : 'Closed' + end + + def project + @resource.project + end + + def author + @resource.author + end - def fields - [ - { - title: "Assignee", - value: @resource.assignee ? @resource.assignee.name : "_None_", - short: true - }, - { - title: "Milestone", - value: @resource.milestone ? @resource.milestone.title : "_None_", - short: true - }, - { - title: "Labels", - value: @resource.labels.any? ? @resource.label_names : "_None_", - short: true - } - ] + def fields + [ + { + title: "Assignee", + value: @resource.assignee ? @resource.assignee.name : "_None_", + short: true + }, + { + title: "Milestone", + value: @resource.milestone ? @resource.milestone.title : "_None_", + short: true + }, + { + title: "Labels", + value: @resource.labels.any? ? @resource.label_names : "_None_", + short: true + } + ] + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/list_issues.rb b/lib/gitlab/chat_commands/presenters/list_issues.rb index 5a7b3fca5c2..2458b9356b7 100644 --- a/lib/gitlab/chat_commands/presenters/list_issues.rb +++ b/lib/gitlab/chat_commands/presenters/list_issues.rb @@ -1,32 +1,43 @@ -module Gitlab::ChatCommands::Presenters - class ListIssues < Gitlab::ChatCommands::Presenters::Base - def present - ephemeral_response(text: "Here are the issues I found:", attachments: attachments) - end +module Gitlab + module ChatCommands + module Presenters + class ListIssues < Presenters::Issuable + def present + text = if @resource.count >= 5 + "Here are the first 5 issues I found:" + else + "Here are the #{@resource.count} issues I found:" + end - private + ephemeral_response(text: text, attachments: attachments) + end - def attachments - @resource.map do |issue| - state = issue.open? ? "Open" : "Closed" + private - { - fallback: "Issue #{issue.to_reference}: #{issue.title}", - color: "#d22852", - text: "[#{issue.to_reference}](#{url_for([namespace, project, issue])}) · #{issue.title} (#{state})", - mrkdwn_in: [ - "text" - ] - } - end - end + def attachments + @resource.map do |issue| + url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" - def project - @project ||= @resource.first.project - end + { + color: color(issue), + fallback: "#{issue.to_reference} #{issue.title}", + text: "#{url} · #{issue.title} (#{status_text(issue)})", - def namespace - @namespace ||= project.namespace.becomes(Namespace) + mrkdwn_in: [ + "text" + ] + } + end + end + + def project + @project ||= @resource.first.project + end + + def namespace + @namespace ||= project.namespace.becomes(Namespace) + end + end end end end diff --git a/lib/gitlab/chat_commands/presenters/new_issue.rb b/lib/gitlab/chat_commands/presenters/new_issue.rb new file mode 100644 index 00000000000..c7c6febb56e --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/new_issue.rb @@ -0,0 +1,42 @@ +module Gitlab + module ChatCommands + module Presenters + class NewIssue < Presenters::Issuable + def present + in_channel_response(show_issue) + end + + private + + def show_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :title, + :text + ] + } + ] + } + end + + def pretext + "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" + end + + def author_profile_link + "[#{author.to_reference}](#{url_for(author)})" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/show_issue.rb b/lib/gitlab/chat_commands/presenters/show_issue.rb index 2a89c30b972..e5644a4ad7e 100644 --- a/lib/gitlab/chat_commands/presenters/show_issue.rb +++ b/lib/gitlab/chat_commands/presenters/show_issue.rb @@ -1,38 +1,54 @@ -module Gitlab::ChatCommands::Presenters - class ShowIssue < Gitlab::ChatCommands::Presenters::Issuable - def present - in_channel_response(show_issue) - end +module Gitlab + module ChatCommands + module Presenters + class ShowIssue < Presenters::Issuable + def present + in_channel_response(show_issue) + end - private + private - def show_issue - { - attachments: [ + def show_issue { - title: @resource.title, - title_link: resource_url, - author_name: author.name, - author_icon: author.avatar_url, - fallback: "#{@resource.to_reference}: #{@resource.title}", - text: text, - fields: fields, - mrkdwn_in: [ - :title, - :text + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + text: text, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :pretext, + :text + ] + } ] } - ] - } - end + end + + def text + message = "**#{status_text(@resource)}**" + + if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? + return message + end + + message << " · " + message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero? + message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero? + message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero? - def text - message = "" - message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero? - message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero? - message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero? + message + end - message + def pretext + "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" + end + end end end end diff --git a/spec/lib/gitlab/chat_commands/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/issue_search_spec.rb index 04d10ad52a1..551ccb79a58 100644 --- a/spec/lib/gitlab/chat_commands/issue_search_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_search_spec.rb @@ -26,7 +26,7 @@ describe Gitlab::ChatCommands::IssueSearch, service: true do it 'returns all results' do expect(subject).to have_key(:attachments) - expect(subject[:text]).to match("Here are the issues I found:") + expect(subject[:text]).to eq("Here are the 2 issues I found:") end end diff --git a/spec/lib/gitlab/chat_commands/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/issue_show_spec.rb index 89932c395c6..1f20d0a44ce 100644 --- a/spec/lib/gitlab/chat_commands/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/issue_show_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::ChatCommands::IssueShow, service: true do it 'returns the issue' do expect(subject[:response_type]).to be(:in_channel) - expect(title).to eq(issue.title) + expect(title).to start_with(issue.title) end context 'when its reference is given' do @@ -28,7 +28,7 @@ describe Gitlab::ChatCommands::IssueShow, service: true do it 'shows the issue' do expect(subject[:response_type]).to be(:in_channel) - expect(title).to eq(issue.title) + expect(title).to start_with(issue.title) end end end diff --git a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb index 1c48c727e30..dc2dd300072 100644 --- a/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/deploy_spec.rb @@ -32,7 +32,7 @@ describe Gitlab::ChatCommands::Presenters::Deploy do end describe '#too_many_actions' do - subject { described_class.new(nil).too_many_actions } + subject { described_class.new([]).too_many_actions } it { is_expected.to have_key(:text) } it { is_expected.to have_key(:response_type) } diff --git a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb index 1852395fc97..13a1f70fe78 100644 --- a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb @@ -3,13 +3,12 @@ require 'spec_helper' describe Gitlab::ChatCommands::Presenters::ListIssues do let(:project) { create(:empty_project) } let(:message) { subject[:text] } - let(:issue) { project.issues.first } before { create_list(:issue, 2, project: project) } subject { described_class.new(project.issues).present } - it do + it 'formats the message correct' do is_expected.to have_key(:text) is_expected.to have_key(:status) is_expected.to have_key(:response_type) @@ -19,6 +18,6 @@ describe Gitlab::ChatCommands::Presenters::ListIssues do it 'shows a list of results' do expect(subject[:response_type]).to be(:ephemeral) - expect(message).to start_with("Here are the issues I found") + expect(message).to start_with("Here are the 2 issues I found") end end diff --git a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb index 13a318fe680..ca4062e692a 100644 --- a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::ChatCommands::Presenters::ShowIssue do it 'shows the issue' do expect(subject[:response_type]).to be(:in_channel) expect(subject).to have_key(:attachments) - expect(attachment[:title]).to eq(issue.title) + expect(attachment[:title]).to start_with(issue.title) end context 'with upvotes' do @@ -21,7 +21,7 @@ describe Gitlab::ChatCommands::Presenters::ShowIssue do end it 'shows the upvote count' do - expect(attachment[:text]).to start_with(":+1: 1") + expect(attachment[:text]).to start_with("**Open** · :+1: 1") end end end diff --git a/spec/lib/mattermost/client_spec.rb b/spec/lib/mattermost/client_spec.rb new file mode 100644 index 00000000000..dc11a414717 --- /dev/null +++ b/spec/lib/mattermost/client_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Mattermost::Client do + let(:user) { build(:user) } + + subject { described_class.new(user) } + + context 'JSON parse error' do + before do + Struct.new("Request", :body, :success?) + end + + it 'yields an error on malformed JSON' do + bad_json = Struct::Request.new("I'm not json", true) + expect { subject.send(:json_response, bad_json) }.to raise_error(Mattermost::ClientError) + end + + it 'shows a client error if the request was unsuccessful' do + bad_request = Struct::Request.new("true", false) + + expect { subject.send(:json_response, bad_request) }.to raise_error(Mattermost::ClientError) + end + end +end diff --git a/spec/lib/mattermost/command_spec.rb b/spec/lib/mattermost/command_spec.rb new file mode 100644 index 00000000000..5ccf1100898 --- /dev/null +++ b/spec/lib/mattermost/command_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe Mattermost::Command do + let(:params) { { 'token' => 'token', team_id: 'abc' } } + + before do + Mattermost::Session.base_uri('http://mattermost.example.com') + + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) + end + + describe '#create' do + let(:params) do + { team_id: 'abc', + trigger: 'gitlab' + } + end + + subject { described_class.new(nil).create(params) } + + context 'for valid trigger word' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + with(body: { + team_id: 'abc', + trigger: 'gitlab' }.to_json). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: { token: 'token' }.to_json + ) + end + + it 'returns a token' do + is_expected.to eq('token') + end + end + + context 'for error message' do + before do + stub_request(:post, 'http://mattermost.example.com/api/v3/teams/abc/commands/create'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + id: 'api.command.duplicate_trigger.app_error', + message: 'This trigger word is already in use. Please choose another word.', + detailed_error: '', + request_id: 'obc374man7bx5r3dbc1q5qhf3r', + status_code: 500 + }.to_json + ) + end + + it 'raises an error with message' do + expect { subject }.to raise_error(Mattermost::Error, 'This trigger word is already in use. Please choose another word.') + end + end + end +end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb new file mode 100644 index 00000000000..74d12e37181 --- /dev/null +++ b/spec/lib/mattermost/session_spec.rb @@ -0,0 +1,123 @@ +require 'spec_helper' + +describe Mattermost::Session, type: :request do + let(:user) { create(:user) } + + let(:gitlab_url) { "http://gitlab.com" } + let(:mattermost_url) { "http://mattermost.com" } + + subject { described_class.new(user) } + + # Needed for doorkeeper to function + it { is_expected.to respond_to(:current_resource_owner) } + it { is_expected.to respond_to(:request) } + it { is_expected.to respond_to(:authorization) } + it { is_expected.to respond_to(:strategy) } + + before do + described_class.base_uri(mattermost_url) + end + + describe '#with session' do + let(:location) { 'http://location.tld' } + let!(:stub) do + WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login"). + to_return(headers: { 'location' => location }, status: 307) + end + + context 'without oauth uri' do + it 'makes a request to the oauth uri' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with oauth_uri' do + let!(:doorkeeper) do + Doorkeeper::Application.create( + name: "GitLab Mattermost", + redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete", + scopes: "") + end + + context 'without token_uri' do + it 'can not create a session' do + expect do + subject.with_session + end.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with token_uri' do + let(:state) { "state" } + let(:params) do + { response_type: "code", + client_id: doorkeeper.uid, + redirect_uri: "#{mattermost_url}/signup/gitlab/complete", + state: state } + end + let(:location) do + "#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}" + end + + before do + WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete"). + with(query: hash_including({ 'state' => state })). + to_return do |request| + post "/oauth/token", + client_id: doorkeeper.uid, + client_secret: doorkeeper.secret, + redirect_uri: params[:redirect_uri], + grant_type: 'authorization_code', + code: request.uri.query_values['code'] + + if response.status == 200 + { headers: { 'token' => 'thisworksnow' }, status: 202 } + end + end + + WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout"). + to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) + end + + it 'can setup a session' do + subject.with_session do |session| + end + + expect(subject.token).not_to be_nil + end + + it 'returns the value of the block' do + result = subject.with_session do |session| + "value" + end + + expect(result).to eq("value") + end + end + end + + context 'with lease' do + before do + allow(subject).to receive(:lease_try_obtain).and_return('aldkfjsldfk') + end + + it 'tries to obtain a lease' do + expect(subject).to receive(:lease_try_obtain) + expect(Gitlab::ExclusiveLease).to receive(:cancel) + + # Cannot setup a session, but we should still cancel the lease + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'without lease' do + before do + allow(subject).to receive(:lease_try_obtain).and_return(nil) + end + + it 'returns a NoSessionError error' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + end +end diff --git a/spec/lib/mattermost/team_spec.rb b/spec/lib/mattermost/team_spec.rb new file mode 100644 index 00000000000..2d14be6bcc2 --- /dev/null +++ b/spec/lib/mattermost/team_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Mattermost::Team do + before do + Mattermost::Session.base_uri('http://mattermost.example.com') + + allow_any_instance_of(Mattermost::Client).to receive(:with_session). + and_yield(Mattermost::Session.new(nil)) + end + + describe '#all' do + subject { described_class.new(nil).all } + + context 'for valid request' do + let(:response) do + [{ + "id" => "xiyro8huptfhdndadpz8r3wnbo", + "create_at" => 1482174222155, + "update_at" => 1482174222155, + "delete_at" => 0, + "display_name" => "chatops", + "name" => "chatops", + "email" => "admin@example.com", + "type" => "O", + "company_name" => "", + "allowed_domains" => "", + "invite_id" => "o4utakb9jtb7imctdfzbf9r5ro", + "allow_open_invite" => false }] + end + + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 200, + headers: { 'Content-Type' => 'application/json' }, + body: response.to_json + ) + end + + it 'returns a token' do + is_expected.to eq(response) + end + end + + context 'for error message' do + before do + stub_request(:get, 'http://mattermost.example.com/api/v3/teams/all'). + to_return( + status: 500, + headers: { 'Content-Type' => 'application/json' }, + body: { + id: 'api.team.list.app_error', + message: 'Cannot list teams.', + detailed_error: '', + request_id: 'obc374man7bx5r3dbc1q5qhf3r', + status_code: 500 + }.to_json + ) + end + + it 'raises an error with message' do + expect { subject }.to raise_error(Mattermost::Error, 'Cannot list teams.') + end + end + end +end -- cgit v1.2.1 From 5ec214b0a5d9d3f0f0418a0e14ebf30b60a14a12 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Thu, 26 Jan 2017 15:30:34 +0100 Subject: Rename presenters for consitency --- lib/gitlab/chat_commands/command.rb | 2 +- lib/gitlab/chat_commands/issue_create.rb | 42 ------------ lib/gitlab/chat_commands/issue_new.rb | 42 ++++++++++++ lib/gitlab/chat_commands/issue_search.rb | 8 +-- lib/gitlab/chat_commands/issue_show.rb | 2 +- lib/gitlab/chat_commands/presenters/deploy.rb | 8 --- lib/gitlab/chat_commands/presenters/help.rb | 6 +- lib/gitlab/chat_commands/presenters/issuable.rb | 4 +- lib/gitlab/chat_commands/presenters/issue_new.rb | 48 +++++++++++++ .../chat_commands/presenters/issue_search.rb | 45 +++++++++++++ lib/gitlab/chat_commands/presenters/issue_show.rb | 56 ++++++++++++++++ lib/gitlab/chat_commands/presenters/list_issues.rb | 43 ------------ lib/gitlab/chat_commands/presenters/new_issue.rb | 42 ------------ lib/gitlab/chat_commands/presenters/show_issue.rb | 54 --------------- lib/mattermost/command.rb | 4 -- spec/lib/gitlab/chat_commands/command_spec.rb | 2 +- spec/lib/gitlab/chat_commands/issue_create_spec.rb | 78 ---------------------- spec/lib/gitlab/chat_commands/issue_new_spec.rb | 78 ++++++++++++++++++++++ .../chat_commands/presenters/issue_new_spec.rb | 17 +++++ .../chat_commands/presenters/issue_search_spec.rb | 23 +++++++ .../chat_commands/presenters/issue_show_spec.rb | 27 ++++++++ .../chat_commands/presenters/list_issues_spec.rb | 23 ------- .../chat_commands/presenters/show_issue_spec.rb | 27 -------- 23 files changed, 346 insertions(+), 335 deletions(-) delete mode 100644 lib/gitlab/chat_commands/issue_create.rb create mode 100644 lib/gitlab/chat_commands/issue_new.rb create mode 100644 lib/gitlab/chat_commands/presenters/issue_new.rb create mode 100644 lib/gitlab/chat_commands/presenters/issue_search.rb create mode 100644 lib/gitlab/chat_commands/presenters/issue_show.rb delete mode 100644 lib/gitlab/chat_commands/presenters/list_issues.rb delete mode 100644 lib/gitlab/chat_commands/presenters/new_issue.rb delete mode 100644 lib/gitlab/chat_commands/presenters/show_issue.rb delete mode 100644 spec/lib/gitlab/chat_commands/issue_create_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/issue_new_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb create mode 100644 spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb delete mode 100644 spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb delete mode 100644 spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index 4e5031a8a26..e7baa20356c 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -3,7 +3,7 @@ module Gitlab class Command < BaseCommand COMMANDS = [ Gitlab::ChatCommands::IssueShow, - Gitlab::ChatCommands::IssueCreate, + Gitlab::ChatCommands::IssueNew, Gitlab::ChatCommands::IssueSearch, Gitlab::ChatCommands::Deploy, ].freeze diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb deleted file mode 100644 index 3f3d7de8b2e..00000000000 --- a/lib/gitlab/chat_commands/issue_create.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Gitlab - module ChatCommands - class IssueCreate < IssueCommand - def self.match(text) - # we can not match \n with the dot by passing the m modifier as than - # the title and description are not seperated - /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) - end - - def self.help_message - 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>' - end - - def self.allowed?(project, user) - can?(user, :create_issue, project) - end - - def execute(match) - title = match[:title] - description = match[:description].to_s.rstrip - - issue = create_issue(title: title, description: description) - - if issue.errors.any? - presenter(issue).display_errors - else - presenter(issue).present - end - end - - private - - def create_issue(title:, description:) - Issues::CreateService.new(project, current_user, title: title, description: description).execute - end - - def presenter(issue) - Gitlab::ChatCommands::Presenters::NewIssue.new(issue) - end - end - end -end diff --git a/lib/gitlab/chat_commands/issue_new.rb b/lib/gitlab/chat_commands/issue_new.rb new file mode 100644 index 00000000000..016054ecd46 --- /dev/null +++ b/lib/gitlab/chat_commands/issue_new.rb @@ -0,0 +1,42 @@ +module Gitlab + module ChatCommands + class IssueNew < IssueCommand + def self.match(text) + # we can not match \n with the dot by passing the m modifier as than + # the title and description are not seperated + /\Aissue\s+(new|create)\s+(?<title>[^\n]*)\n*(?<description>(.|\n)*)/.match(text) + end + + def self.help_message + 'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>' + end + + def self.allowed?(project, user) + can?(user, :create_issue, project) + end + + def execute(match) + title = match[:title] + description = match[:description].to_s.rstrip + + issue = create_issue(title: title, description: description) + + if issue.persisted? + presenter(issue).present + else + presenter(issue).display_errors + end + end + + private + + def create_issue(title:, description:) + Issues::CreateService.new(project, current_user, title: title, description: description).execute + end + + def presenter(issue) + Gitlab::ChatCommands::Presenters::IssueNew.new(issue) + end + end + end +end diff --git a/lib/gitlab/chat_commands/issue_search.rb b/lib/gitlab/chat_commands/issue_search.rb index e2d3a0f466a..3491b53093e 100644 --- a/lib/gitlab/chat_commands/issue_search.rb +++ b/lib/gitlab/chat_commands/issue_search.rb @@ -12,12 +12,10 @@ module Gitlab def execute(match) issues = collection.search(match[:query]).limit(QUERY_LIMIT) - if issues.none? - Presenters::Access.new(issues).not_found - elsif issues.one? - Presenters::ShowIssue.new(issues.first).present + if issues.present? + Presenters::IssueSearch.new(issues).present else - Presenters::ListIssues.new(issues).present + Presenters::Access.new(issues).not_found end end end diff --git a/lib/gitlab/chat_commands/issue_show.rb b/lib/gitlab/chat_commands/issue_show.rb index 9f3e1b9a64b..d6013f4d10c 100644 --- a/lib/gitlab/chat_commands/issue_show.rb +++ b/lib/gitlab/chat_commands/issue_show.rb @@ -13,7 +13,7 @@ module Gitlab issue = find_by_iid(match[:iid]) if issue - Gitlab::ChatCommands::Presenters::ShowIssue.new(issue).present + Gitlab::ChatCommands::Presenters::IssueShow.new(issue).present else Gitlab::ChatCommands::Presenters::Access.new.not_found end diff --git a/lib/gitlab/chat_commands/presenters/deploy.rb b/lib/gitlab/chat_commands/presenters/deploy.rb index b1cfaac15af..863d0bf99ca 100644 --- a/lib/gitlab/chat_commands/presenters/deploy.rb +++ b/lib/gitlab/chat_commands/presenters/deploy.rb @@ -15,14 +15,6 @@ module Gitlab def too_many_actions ephemeral_response(text: "Too many actions defined") end - - private - - def resource_url - polymorphic_url( - [ @resource.project.namespace.becomes(Namespace), @resource.project, @resource] - ) - end end end end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb index c7a67467b7e..39ad3249f5b 100644 --- a/lib/gitlab/chat_commands/presenters/help.rb +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -9,10 +9,10 @@ module Gitlab private def help_message(trigger) - if @resource.none? - "No commands available :thinking_face:" - else + if @resource.present? header_with_list("Available commands", full_commands(trigger)) + else + "No commands available :thinking_face:" end end diff --git a/lib/gitlab/chat_commands/presenters/issuable.rb b/lib/gitlab/chat_commands/presenters/issuable.rb index 2cb6b1525fc..dfb1c8f6616 100644 --- a/lib/gitlab/chat_commands/presenters/issuable.rb +++ b/lib/gitlab/chat_commands/presenters/issuable.rb @@ -1,9 +1,7 @@ module Gitlab module ChatCommands module Presenters - class Issuable < Presenters::Base - private - + module Issuable def color(issuable) issuable.open? ? '#38ae67' : '#d22852' end diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb new file mode 100644 index 00000000000..d26dd22b2a0 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_new.rb @@ -0,0 +1,48 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueNew < Presenters::Base + include Presenters::Issuable + + def present + in_channel_response(new_issue) + end + + private + + def new_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "New issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :title, + :text + ] + } + ] + } + end + + def pretext + "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" + end + + def project_link + "[#{project.name_with_namespace}](#{url_for(project)})" + end + + def author_profile_link + "[#{author.to_reference}](#{url_for(author)})" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb new file mode 100644 index 00000000000..d58a6d6114a --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_search.rb @@ -0,0 +1,45 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueSearch < Presenters::Base + include Presenters::Issuable + + def present + text = if @resource.count >= 5 + "Here are the first 5 issues I found:" + else + "Here are the #{@resource.count} issues I found:" + end + + ephemeral_response(text: text, attachments: attachments) + end + + private + + def attachments + @resource.map do |issue| + url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" + + { + color: color(issue), + fallback: "#{issue.to_reference} #{issue.title}", + text: "#{url} · #{issue.title} (#{status_text(issue)})", + + mrkdwn_in: [ + "text" + ] + } + end + end + + def project + @project ||= @resource.first.project + end + + def namespace + @namespace ||= project.namespace.becomes(Namespace) + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb new file mode 100644 index 00000000000..2fc671f13a6 --- /dev/null +++ b/lib/gitlab/chat_commands/presenters/issue_show.rb @@ -0,0 +1,56 @@ +module Gitlab + module ChatCommands + module Presenters + class IssueShow < Presenters::Base + include Presenters::Issuable + + def present + in_channel_response(show_issue) + end + + private + + def show_issue + { + attachments: [ + { + title: "#{@resource.title} · #{@resource.to_reference}", + title_link: resource_url, + author_name: author.name, + author_icon: author.avatar_url, + fallback: "Issue #{@resource.to_reference}: #{@resource.title}", + pretext: pretext, + text: text, + color: color(@resource), + fields: fields, + mrkdwn_in: [ + :pretext, + :text + ] + } + ] + } + end + + def text + message = "**#{status_text(@resource)}**" + + if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? + return message + end + + message << " · " + message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero? + message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero? + message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero? + + message + end + + def pretext + "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" + end + end + end + end +end diff --git a/lib/gitlab/chat_commands/presenters/list_issues.rb b/lib/gitlab/chat_commands/presenters/list_issues.rb deleted file mode 100644 index 2458b9356b7..00000000000 --- a/lib/gitlab/chat_commands/presenters/list_issues.rb +++ /dev/null @@ -1,43 +0,0 @@ -module Gitlab - module ChatCommands - module Presenters - class ListIssues < Presenters::Issuable - def present - text = if @resource.count >= 5 - "Here are the first 5 issues I found:" - else - "Here are the #{@resource.count} issues I found:" - end - - ephemeral_response(text: text, attachments: attachments) - end - - private - - def attachments - @resource.map do |issue| - url = "[#{issue.to_reference}](#{url_for([namespace, project, issue])})" - - { - color: color(issue), - fallback: "#{issue.to_reference} #{issue.title}", - text: "#{url} · #{issue.title} (#{status_text(issue)})", - - mrkdwn_in: [ - "text" - ] - } - end - end - - def project - @project ||= @resource.first.project - end - - def namespace - @namespace ||= project.namespace.becomes(Namespace) - end - end - end - end -end diff --git a/lib/gitlab/chat_commands/presenters/new_issue.rb b/lib/gitlab/chat_commands/presenters/new_issue.rb deleted file mode 100644 index c7c6febb56e..00000000000 --- a/lib/gitlab/chat_commands/presenters/new_issue.rb +++ /dev/null @@ -1,42 +0,0 @@ -module Gitlab - module ChatCommands - module Presenters - class NewIssue < Presenters::Issuable - def present - in_channel_response(show_issue) - end - - private - - def show_issue - { - attachments: [ - { - title: "#{@resource.title} · #{@resource.to_reference}", - title_link: resource_url, - author_name: author.name, - author_icon: author.avatar_url, - fallback: "New issue #{@resource.to_reference}: #{@resource.title}", - pretext: pretext, - color: color(@resource), - fields: fields, - mrkdwn_in: [ - :title, - :text - ] - } - ] - } - end - - def pretext - "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" - end - - def author_profile_link - "[#{author.to_reference}](#{url_for(author)})" - end - end - end - end -end diff --git a/lib/gitlab/chat_commands/presenters/show_issue.rb b/lib/gitlab/chat_commands/presenters/show_issue.rb deleted file mode 100644 index e5644a4ad7e..00000000000 --- a/lib/gitlab/chat_commands/presenters/show_issue.rb +++ /dev/null @@ -1,54 +0,0 @@ -module Gitlab - module ChatCommands - module Presenters - class ShowIssue < Presenters::Issuable - def present - in_channel_response(show_issue) - end - - private - - def show_issue - { - attachments: [ - { - title: "#{@resource.title} · #{@resource.to_reference}", - title_link: resource_url, - author_name: author.name, - author_icon: author.avatar_url, - fallback: "New issue #{@resource.to_reference}: #{@resource.title}", - pretext: pretext, - text: text, - color: color(@resource), - fields: fields, - mrkdwn_in: [ - :pretext, - :text - ] - } - ] - } - end - - def text - message = "**#{status_text(@resource)}**" - - if @resource.upvotes.zero? && @resource.downvotes.zero? && @resource.user_notes_count.zero? - return message - end - - message << " · " - message << ":+1: #{@resource.upvotes} " unless @resource.upvotes.zero? - message << ":-1: #{@resource.downvotes} " unless @resource.downvotes.zero? - message << ":speech_balloon: #{@resource.user_notes_count}" unless @resource.user_notes_count.zero? - - message - end - - def pretext - "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" - end - end - end - end -end diff --git a/lib/mattermost/command.rb b/lib/mattermost/command.rb index 2e4f7705f86..33e450d7f0a 100644 --- a/lib/mattermost/command.rb +++ b/lib/mattermost/command.rb @@ -1,11 +1,7 @@ module Mattermost class Command < Client def create(params) -<<<<<<< HEAD response = session_post("/api/v3/teams/#{params[:team_id]}/commands/create", -======= - response = json_post("/api/v3/teams/#{params[:team_id]}/commands/create", ->>>>>>> Revert removing of some files body: params.to_json) response['token'] diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index d8b2303555c..0acf40de1d3 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -94,7 +94,7 @@ describe Gitlab::ChatCommands::Command, service: true do context 'IssueCreate is triggered' do let(:params) { { text: 'issue create my title' } } - it { is_expected.to eq(Gitlab::ChatCommands::IssueCreate) } + it { is_expected.to eq(Gitlab::ChatCommands::IssueNew) } end context 'IssueSearch is triggered' do diff --git a/spec/lib/gitlab/chat_commands/issue_create_spec.rb b/spec/lib/gitlab/chat_commands/issue_create_spec.rb deleted file mode 100644 index 0f84b19a5a4..00000000000 --- a/spec/lib/gitlab/chat_commands/issue_create_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ChatCommands::IssueCreate, service: true do - describe '#execute' do - let(:project) { create(:empty_project) } - let(:user) { create(:user) } - let(:regex_match) { described_class.match("issue create bird is the word") } - - before do - project.team << [user, :master] - end - - subject do - described_class.new(project, user).execute(regex_match) - end - - context 'without description' do - it 'creates the issue' do - expect { subject }.to change { project.issues.count }.by(1) - - expect(subject[:response_type]).to be(:in_channel) - end - end - - context 'with description' do - let(:description) { "Surfin bird" } - let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") } - - it 'creates the issue with description' do - subject - - expect(Issue.last.description).to eq(description) - end - end - - context "with more newlines between the title and the description" do - let(:description) { "Surfin bird" } - let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") } - - it 'creates the issue' do - expect { subject }.to change { project.issues.count }.by(1) - end - end - - context 'issue cannot be created' do - let!(:issue) { create(:issue, project: project, title: 'bird is the word') } - let(:regex_match) { described_class.match("issue create #{'a' * 512}}") } - - it 'displays the errors' do - expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to match("- Title is too long") - end - end - end - - describe '.match' do - it 'matches the title without description' do - match = described_class.match("issue create my title") - - expect(match[:title]).to eq('my title') - expect(match[:description]).to eq("") - end - - it 'matches the title with description' do - match = described_class.match("issue create my title\n\ndescription") - - expect(match[:title]).to eq('my title') - expect(match[:description]).to eq('description') - end - - it 'matches the alias new' do - match = described_class.match("issue new my title") - - expect(match).not_to be_nil - expect(match[:title]).to eq('my title') - end - end -end diff --git a/spec/lib/gitlab/chat_commands/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/issue_new_spec.rb new file mode 100644 index 00000000000..84c22328064 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/issue_new_spec.rb @@ -0,0 +1,78 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::IssueNew, service: true do + describe '#execute' do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:regex_match) { described_class.match("issue create bird is the word") } + + before do + project.team << [user, :master] + end + + subject do + described_class.new(project, user).execute(regex_match) + end + + context 'without description' do + it 'creates the issue' do + expect { subject }.to change { project.issues.count }.by(1) + + expect(subject[:response_type]).to be(:in_channel) + end + end + + context 'with description' do + let(:description) { "Surfin bird" } + let(:regex_match) { described_class.match("issue create bird is the word\n#{description}") } + + it 'creates the issue with description' do + subject + + expect(Issue.last.description).to eq(description) + end + end + + context "with more newlines between the title and the description" do + let(:description) { "Surfin bird" } + let(:regex_match) { described_class.match("issue create bird is the word\n\n#{description}\n") } + + it 'creates the issue' do + expect { subject }.to change { project.issues.count }.by(1) + end + end + + context 'issue cannot be created' do + let!(:issue) { create(:issue, project: project, title: 'bird is the word') } + let(:regex_match) { described_class.match("issue create #{'a' * 512}}") } + + it 'displays the errors' do + expect(subject[:response_type]).to be(:ephemeral) + expect(subject[:text]).to match("- Title is too long") + end + end + end + + describe '.match' do + it 'matches the title without description' do + match = described_class.match("issue create my title") + + expect(match[:title]).to eq('my title') + expect(match[:description]).to eq("") + end + + it 'matches the title with description' do + match = described_class.match("issue create my title\n\ndescription") + + expect(match[:title]).to eq('my title') + expect(match[:description]).to eq('description') + end + + it 'matches the alias new' do + match = described_class.match("issue new my title") + + expect(match).not_to be_nil + expect(match[:title]).to eq('my title') + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb new file mode 100644 index 00000000000..17fcdbc2452 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_new_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueNew do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:attachment) { subject[:attachments].first } + + subject { described_class.new(issue).present } + + it { is_expected.to be_a(Hash) } + + it 'shows the issue' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject).to have_key(:attachments) + expect(attachment[:title]).to start_with(issue.title) + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb new file mode 100644 index 00000000000..ec6d3e34a96 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_search_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueSearch do + let(:project) { create(:empty_project) } + let(:message) { subject[:text] } + + before { create_list(:issue, 2, project: project) } + + subject { described_class.new(project.issues).present } + + it 'formats the message correct' do + is_expected.to have_key(:text) + is_expected.to have_key(:status) + is_expected.to have_key(:response_type) + is_expected.to have_key(:attachments) + end + + it 'shows a list of results' do + expect(subject[:response_type]).to be(:ephemeral) + + expect(message).to start_with("Here are the 2 issues I found") + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb new file mode 100644 index 00000000000..89d154e26e4 --- /dev/null +++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::ChatCommands::Presenters::IssueShow do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:attachment) { subject[:attachments].first } + + subject { described_class.new(issue).present } + + it { is_expected.to be_a(Hash) } + + it 'shows the issue' do + expect(subject[:response_type]).to be(:in_channel) + expect(subject).to have_key(:attachments) + expect(attachment[:title]).to start_with(issue.title) + end + + context 'with upvotes' do + before do + create(:award_emoji, :upvote, awardable: issue) + end + + it 'shows the upvote count' do + expect(attachment[:text]).to start_with("**Open** · :+1: 1") + end + end +end diff --git a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb b/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb deleted file mode 100644 index 13a1f70fe78..00000000000 --- a/spec/lib/gitlab/chat_commands/presenters/list_issues_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ChatCommands::Presenters::ListIssues do - let(:project) { create(:empty_project) } - let(:message) { subject[:text] } - - before { create_list(:issue, 2, project: project) } - - subject { described_class.new(project.issues).present } - - it 'formats the message correct' do - is_expected.to have_key(:text) - is_expected.to have_key(:status) - is_expected.to have_key(:response_type) - is_expected.to have_key(:attachments) - end - - it 'shows a list of results' do - expect(subject[:response_type]).to be(:ephemeral) - - expect(message).to start_with("Here are the 2 issues I found") - end -end diff --git a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb b/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb deleted file mode 100644 index ca4062e692a..00000000000 --- a/spec/lib/gitlab/chat_commands/presenters/show_issue_spec.rb +++ /dev/null @@ -1,27 +0,0 @@ -require 'spec_helper' - -describe Gitlab::ChatCommands::Presenters::ShowIssue do - let(:project) { create(:empty_project) } - let(:issue) { create(:issue, project: project) } - let(:attachment) { subject[:attachments].first } - - subject { described_class.new(issue).present } - - it { is_expected.to be_a(Hash) } - - it 'shows the issue' do - expect(subject[:response_type]).to be(:in_channel) - expect(subject).to have_key(:attachments) - expect(attachment[:title]).to start_with(issue.title) - end - - context 'with upvotes' do - before do - create(:award_emoji, :upvote, awardable: issue) - end - - it 'shows the upvote count' do - expect(attachment[:text]).to start_with("**Open** · :+1: 1") - end - end -end -- cgit v1.2.1 From 500e1a56e0a2225a61ec4bea40a474e7e3e3d1cc Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Mon, 30 Jan 2017 16:58:30 +0600 Subject: unifies mr diff file button style --- app/helpers/commits_helper.rb | 2 +- app/views/projects/diffs/_file.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index e9461b9f859..6dcb624c4da 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -198,7 +198,7 @@ module CommitsHelper link_to( namespace_project_blob_path(project.namespace, project, tree_join(commit_sha, diff_new_path)), - class: 'btn view-file js-view-file btn-file-option' + class: 'btn view-file js-view-file' ) do raw('View file @') + content_tag(:span, commit_sha[0..6], class: 'commit-short-id') diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index c37a33bbcd5..fc478ccc995 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -5,7 +5,7 @@ - unless diff_file.submodule? .file-actions.hidden-xs - if blob_text_viewable?(blob) - = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do + = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip', title: "Toggle comments for this file", disabled: @diff_notes_disabled do = icon('comment') \ - if editable_diff?(diff_file) -- cgit v1.2.1 From d4dd1fcf93d10b65b9e1b5ca392daacaf7c5138c Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Mon, 30 Jan 2017 17:19:32 +0600 Subject: adds changelog --- changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml diff --git a/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml new file mode 100644 index 00000000000..293aab67d39 --- /dev/null +++ b/changelogs/unreleased/27291-unify-mr-diff-file-buttons.yml @@ -0,0 +1,4 @@ +--- +title: Unify MR diff file button style +merge_request: 8874 +author: -- cgit v1.2.1 From cc24682b58a66a217e6ffa1d56f8d45900c10d03 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira <oswaldo@gitlab.com> Date: Fri, 27 Jan 2017 15:56:40 -0200 Subject: Unify projects search by removing /projects/:search endpoint --- .../unreleased/22007-unify-projects-search.yml | 4 ++ lib/api/projects.rb | 16 -------- spec/requests/api/projects_spec.rb | 46 ---------------------- 3 files changed, 4 insertions(+), 62 deletions(-) create mode 100644 changelogs/unreleased/22007-unify-projects-search.yml diff --git a/changelogs/unreleased/22007-unify-projects-search.yml b/changelogs/unreleased/22007-unify-projects-search.yml new file mode 100644 index 00000000000..f43c1925ad0 --- /dev/null +++ b/changelogs/unreleased/22007-unify-projects-search.yml @@ -0,0 +1,4 @@ +--- +title: Unify projects search by removing /projects/:search endpoint +merge_request: 8877 +author: diff --git a/lib/api/projects.rb b/lib/api/projects.rb index 941f47114a4..92a70faf1c2 100644 --- a/lib/api/projects.rb +++ b/lib/api/projects.rb @@ -151,22 +151,6 @@ module API present_projects Project.all, with: Entities::ProjectWithAccess, statistics: params[:statistics] end - desc 'Search for projects the current user has access to' do - success Entities::Project - end - params do - requires :query, type: String, desc: 'The project name to be searched' - use :sort_params - use :pagination - end - get "/search/:query", requirements: { query: /[^\/]+/ } do - search_service = Search::GlobalService.new(current_user, search: params[:query]).execute - projects = search_service.objects('projects', params[:page]) - projects = projects.reorder(params[:order_by] => params[:sort]) - - present paginate(projects), with: Entities::Project - end - desc 'Create new project' do success Entities::Project end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index a1db81ce18c..8b04748b3b0 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -1085,52 +1085,6 @@ describe API::Projects, api: true do end end - describe 'GET /projects/search/:query' do - let!(:query) { 'query'} - let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } - let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } - let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) } - let!(:pre_post) { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) } - let!(:unfound) { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) } - let!(:internal) { create(:empty_project, :internal, name: "internal #{query}") } - let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') } - let!(:public) { create(:empty_project, :public, name: "public #{query}") } - let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') } - let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") } - - shared_examples_for 'project search response' do |args = {}| - it 'returns project search responses' do - get api("/projects/search/#{args[:query]}", current_user) - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.size).to eq(args[:results]) - json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) } - end - end - - context 'when unauthenticated' do - it_behaves_like 'project search response', query: 'query', results: 1 do - let(:current_user) { nil } - end - end - - context 'when authenticated' do - it_behaves_like 'project search response', query: 'query', results: 6 do - let(:current_user) { user } - end - it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do - let(:current_user) { user } - end - end - - context 'when authenticated as a different user' do - it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do - let(:current_user) { user2 } - end - end - end - describe 'PUT /projects/:id' do before { project } before { user } -- cgit v1.2.1 From e8f4f30627c07cbca26e7ce292cad7942ca76d0d Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Wed, 25 Jan 2017 16:36:40 +0600 Subject: layout rearranged --- app/views/admin/projects/index.html.haml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 2e6f03fcde0..5936312801b 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -27,7 +27,7 @@ = icon("search", class: "search-icon") .dropdown - - toggle_text = 'Search for Namespace' + - toggle_text = 'Namespace' - if params[:namespace_id].present? - namespace = Namespace.find(params[:namespace_id]) - toggle_text = "#{namespace.kind}: #{namespace.path}" @@ -37,8 +37,9 @@ = dropdown_filter("Search for Namespace") = dropdown_content = dropdown_loading - - = button_tag "Search", class: "btn btn-primary btn-search" + = render 'shared/projects/dropdown' + = link_to new_project_path, class: 'btn btn-new' do + New Project %ul.nav-links - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path } @@ -56,11 +57,6 @@ = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do Public - .nav-controls - = render 'shared/projects/dropdown' - = link_to new_project_path, class: 'btn btn-new' do - New Project - .projects-list-holder - if @projects.any? %ul.projects-list.content-list -- cgit v1.2.1 From d8b8168623ae780b72723546524cce0aae979556 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 30 Jan 2017 12:18:52 -0600 Subject: Remove underline style for icon hover --- app/assets/stylesheets/framework/icons.scss | 6 ++++++ app/views/projects/merge_requests/widget/_heading.html.haml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index dccf5177e35..33de9022da7 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -57,3 +57,9 @@ fill: $gl-text-color; } } + +.icon-link { + &:hover { + text-decoration: none; + } +} diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index cc939ab9441..ecd626872a5 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -2,7 +2,7 @@ .mr-widget-heading - %w[success success_with_warnings skipped canceled failed running pending].each do |status| .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) } - = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id) do + = link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do = ci_icon_for_status(status) %span Pipeline -- cgit v1.2.1 From cadef802758ce4cf0b59849ca4613545967c2da6 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira <oswaldo@gitlab.com> Date: Mon, 30 Jan 2017 15:41:56 -0200 Subject: Remain V3 endpoint unchanged --- lib/api/api.rb | 7 +- lib/api/v3/projects.rb | 458 +++++++++++ spec/requests/api/v3/projects_spec.rb | 1424 +++++++++++++++++++++++++++++++++ spec/support/api_helpers.rb | 9 +- 4 files changed, 1895 insertions(+), 3 deletions(-) create mode 100644 lib/api/v3/projects.rb create mode 100644 spec/requests/api/v3/projects_spec.rb diff --git a/lib/api/api.rb b/lib/api/api.rb index 6cf6b501021..090109d5e6f 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -1,7 +1,12 @@ module API class API < Grape::API include APIGuard - version 'v3', using: :path + + version %w(v3 v4), using: :path + + version 'v3', using: :path do + mount ::API::V3::Projects + end before { allow_access_with_scope :api } diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb new file mode 100644 index 00000000000..bac7d485a22 --- /dev/null +++ b/lib/api/v3/projects.rb @@ -0,0 +1,458 @@ +module API + module V3 + class Projects < Grape::API + include PaginationParams + + before { authenticate_non_get! } + + helpers do + params :optional_params do + optional :description, type: String, desc: 'The description of the project' + optional :issues_enabled, type: Boolean, desc: 'Flag indication if the issue tracker is enabled' + optional :merge_requests_enabled, type: Boolean, desc: 'Flag indication if merge requests are enabled' + optional :wiki_enabled, type: Boolean, desc: 'Flag indication if the wiki is enabled' + optional :builds_enabled, type: Boolean, desc: 'Flag indication if builds are enabled' + optional :snippets_enabled, type: Boolean, desc: 'Flag indication if snippets are enabled' + optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project' + optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project' + optional :lfs_enabled, type: Boolean, desc: 'Flag indication if Git LFS is enabled for that project' + optional :public, type: Boolean, desc: 'Create a public project. The same as visibility_level = 20.' + optional :visibility_level, type: Integer, values: [ + Gitlab::VisibilityLevel::PRIVATE, + Gitlab::VisibilityLevel::INTERNAL, + Gitlab::VisibilityLevel::PUBLIC ], desc: 'Create a public project. The same as visibility_level = 20.' + optional :public_builds, type: Boolean, desc: 'Perform public builds' + optional :request_access_enabled, type: Boolean, desc: 'Allow users to request member access' + optional :only_allow_merge_if_build_succeeds, type: Boolean, desc: 'Only allow to merge if builds succeed' + optional :only_allow_merge_if_all_discussions_are_resolved, type: Boolean, desc: 'Only allow to merge if all discussions are resolved' + end + + def map_public_to_visibility_level(attrs) + publik = attrs.delete(:public) + if !publik.nil? && !attrs[:visibility_level].present? + # Since setting the public attribute to private could mean either + # private or internal, use the more conservative option, private. + attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE + end + attrs + end + end + + resource :projects do + helpers do + params :collection_params do + use :sort_params + use :filter_params + use :pagination + + optional :simple, type: Boolean, default: false, + desc: 'Return only the ID, URL, name, and path of each project' + end + + params :sort_params do + optional :order_by, type: String, values: %w[id name path created_at updated_at last_activity_at], + default: 'created_at', desc: 'Return projects ordered by field' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return projects sorted in ascending and descending order' + end + + params :filter_params do + optional :archived, type: Boolean, default: false, desc: 'Limit by archived status' + optional :visibility, type: String, values: %w[public internal private], + desc: 'Limit by visibility' + optional :search, type: String, desc: 'Return list of authorized projects matching the search criteria' + end + + params :statistics_params do + optional :statistics, type: Boolean, default: false, desc: 'Include project statistics' + end + + params :create_params do + optional :namespace_id, type: Integer, desc: 'Namespace ID for the new project. Default to the user namespace.' + optional :import_url, type: String, desc: 'URL from which the project is imported' + end + + def present_projects(projects, options = {}) + options = options.reverse_merge( + with: Entities::Project, + current_user: current_user, + simple: params[:simple], + ) + + projects = filter_projects(projects) + projects = projects.with_statistics if options[:statistics] + options[:with] = Entities::BasicProjectDetails if options[:simple] + + present paginate(projects), options + end + end + + desc 'Get a list of visible projects for authenticated user' do + success Entities::BasicProjectDetails + end + params do + use :collection_params + end + get '/visible' do + entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails + present_projects ProjectsFinder.new.execute(current_user), with: entity + end + + desc 'Get a projects list for authenticated user' do + success Entities::BasicProjectDetails + end + params do + use :collection_params + end + get do + authenticate! + + present_projects current_user.authorized_projects, + with: Entities::ProjectWithAccess + end + + desc 'Get an owned projects list for authenticated user' do + success Entities::BasicProjectDetails + end + params do + use :collection_params + use :statistics_params + end + get '/owned' do + authenticate! + + present_projects current_user.owned_projects, + with: Entities::ProjectWithAccess, + statistics: params[:statistics] + end + + desc 'Gets starred project for the authenticated user' do + success Entities::BasicProjectDetails + end + params do + use :collection_params + end + get '/starred' do + authenticate! + + present_projects current_user.viewable_starred_projects + end + + desc 'Get all projects for admin user' do + success Entities::BasicProjectDetails + end + params do + use :collection_params + use :statistics_params + end + get '/all' do + authenticated_as_admin! + + present_projects Project.all, with: Entities::ProjectWithAccess, statistics: params[:statistics] + end + + desc 'Search for projects the current user has access to' do + success Entities::Project + end + params do + requires :query, type: String, desc: 'The project name to be searched' + use :sort_params + use :pagination + end + get "/search/:query", requirements: { query: /[^\/]+/ } do + search_service = Search::GlobalService.new(current_user, search: params[:query]).execute + projects = search_service.objects('projects', params[:page]) + projects = projects.reorder(params[:order_by] => params[:sort]) + + present paginate(projects), with: Entities::Project + end + + desc 'Create new project' do + success Entities::Project + end + params do + requires :name, type: String, desc: 'The name of the project' + optional :path, type: String, desc: 'The path of the repository' + use :optional_params + use :create_params + end + post do + attrs = map_public_to_visibility_level(declared_params(include_missing: false)) + project = ::Projects::CreateService.new(current_user, attrs).execute + + if project.saved? + present project, with: Entities::Project, + user_can_admin_project: can?(current_user, :admin_project, project) + else + if project.errors[:limit_reached].present? + error!(project.errors[:limit_reached], 403) + end + render_validation_error!(project) + end + end + + desc 'Create new project for a specified user. Only available to admin users.' do + success Entities::Project + end + params do + requires :name, type: String, desc: 'The name of the project' + requires :user_id, type: Integer, desc: 'The ID of a user' + optional :default_branch, type: String, desc: 'The default branch of the project' + use :optional_params + use :create_params + end + post "user/:user_id" do + authenticated_as_admin! + user = User.find_by(id: params.delete(:user_id)) + not_found!('User') unless user + + attrs = map_public_to_visibility_level(declared_params(include_missing: false)) + project = ::Projects::CreateService.new(user, attrs).execute + + if project.saved? + present project, with: Entities::Project, + user_can_admin_project: can?(current_user, :admin_project, project) + else + render_validation_error!(project) + end + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects, requirements: { id: /[^\/]+/ } do + desc 'Get a single project' do + success Entities::ProjectWithAccess + end + get ":id" do + entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails + present user_project, with: entity, current_user: current_user, + user_can_admin_project: can?(current_user, :admin_project, user_project) + end + + desc 'Get events for a single project' do + success Entities::Event + end + params do + use :pagination + end + get ":id/events" do + present paginate(user_project.events.recent), with: Entities::Event + end + + desc 'Fork new project for the current user or provided namespace.' do + success Entities::Project + end + params do + optional :namespace, type: String, desc: 'The ID or name of the namespace that the project will be forked into' + end + post 'fork/:id' do + fork_params = declared_params(include_missing: false) + namespace_id = fork_params[:namespace] + + if namespace_id.present? + fork_params[:namespace] = if namespace_id =~ /^\d+$/ + Namespace.find_by(id: namespace_id) + else + Namespace.find_by_path_or_name(namespace_id) + end + + unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace]) + not_found!('Target Namespace') + end + end + + forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute + + if forked_project.errors.any? + conflict!(forked_project.errors.messages) + else + present forked_project, with: Entities::Project, + user_can_admin_project: can?(current_user, :admin_project, forked_project) + end + end + + desc 'Update an existing project' do + success Entities::Project + end + params do + optional :name, type: String, desc: 'The name of the project' + optional :default_branch, type: String, desc: 'The default branch of the project' + optional :path, type: String, desc: 'The path of the repository' + use :optional_params + at_least_one_of :name, :description, :issues_enabled, :merge_requests_enabled, + :wiki_enabled, :builds_enabled, :snippets_enabled, + :shared_runners_enabled, :container_registry_enabled, + :lfs_enabled, :public, :visibility_level, :public_builds, + :request_access_enabled, :only_allow_merge_if_build_succeeds, + :only_allow_merge_if_all_discussions_are_resolved, :path, + :default_branch + end + put ':id' do + authorize_admin_project + attrs = map_public_to_visibility_level(declared_params(include_missing: false)) + authorize! :rename_project, user_project if attrs[:name].present? + authorize! :change_visibility_level, user_project if attrs[:visibility_level].present? + + result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute + + if result[:status] == :success + present user_project, with: Entities::Project, + user_can_admin_project: can?(current_user, :admin_project, user_project) + else + render_validation_error!(user_project) + end + end + + desc 'Archive a project' do + success Entities::Project + end + post ':id/archive' do + authorize!(:archive_project, user_project) + + user_project.archive! + + present user_project, with: Entities::Project + end + + desc 'Unarchive a project' do + success Entities::Project + end + post ':id/unarchive' do + authorize!(:archive_project, user_project) + + user_project.unarchive! + + present user_project, with: Entities::Project + end + + desc 'Star a project' do + success Entities::Project + end + post ':id/star' do + if current_user.starred?(user_project) + not_modified! + else + current_user.toggle_star(user_project) + user_project.reload + + present user_project, with: Entities::Project + end + end + + desc 'Unstar a project' do + success Entities::Project + end + delete ':id/star' do + if current_user.starred?(user_project) + current_user.toggle_star(user_project) + user_project.reload + + present user_project, with: Entities::Project + else + not_modified! + end + end + + desc 'Remove a project' + delete ":id" do + authorize! :remove_project, user_project + ::Projects::DestroyService.new(user_project, current_user, {}).async_execute + end + + desc 'Mark this project as forked from another' + params do + requires :forked_from_id, type: String, desc: 'The ID of the project it was forked from' + end + post ":id/fork/:forked_from_id" do + authenticated_as_admin! + + forked_from_project = find_project!(params[:forked_from_id]) + not_found!("Source Project") unless forked_from_project + + if user_project.forked_from_project.nil? + user_project.create_forked_project_link(forked_to_project_id: user_project.id, forked_from_project_id: forked_from_project.id) + else + render_api_error!("Project already forked", 409) + end + end + + desc 'Remove a forked_from relationship' + delete ":id/fork" do + authorize! :remove_fork_project, user_project + + if user_project.forked? + user_project.forked_project_link.destroy + else + not_modified! + end + end + + desc 'Share the project with a group' do + success Entities::ProjectGroupLink + end + params do + requires :group_id, type: Integer, desc: 'The ID of a group' + requires :group_access, type: Integer, values: Gitlab::Access.values, desc: 'The group access level' + optional :expires_at, type: Date, desc: 'Share expiration date' + end + post ":id/share" do + authorize! :admin_project, user_project + group = Group.find_by_id(params[:group_id]) + + unless group && can?(current_user, :read_group, group) + not_found!('Group') + end + + unless user_project.allowed_to_share_with_group? + return render_api_error!("The project sharing with group is disabled", 400) + end + + link = user_project.project_group_links.new(declared_params(include_missing: false)) + + if link.save + present link, with: Entities::ProjectGroupLink + else + render_api_error!(link.errors.full_messages.first, 409) + end + end + + params do + requires :group_id, type: Integer, desc: 'The ID of the group' + end + delete ":id/share/:group_id" do + authorize! :admin_project, user_project + + link = user_project.project_group_links.find_by(group_id: params[:group_id]) + not_found!('Group Link') unless link + + link.destroy + no_content! + end + + desc 'Upload a file' + params do + requires :file, type: File, desc: 'The file to be uploaded' + end + post ":id/uploads" do + ::Projects::UploadService.new(user_project, params[:file]).execute + end + + desc 'Get the users list of a project' do + success Entities::UserBasic + end + params do + optional :search, type: String, desc: 'Return list of users matching the search criteria' + use :pagination + end + get ':id/users' do + users = user_project.team.users + users = users.search(params[:search]) if params[:search].present? + + present paginate(users), with: Entities::UserBasic + end + end + end + end +end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb new file mode 100644 index 00000000000..c3f53d0da37 --- /dev/null +++ b/spec/requests/api/v3/projects_spec.rb @@ -0,0 +1,1424 @@ +require 'spec_helper' + +describe API::V3::Projects, v3_api: true do + include ApiHelpers + include Gitlab::CurrentSettings + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:user3) { create(:user) } + let(:admin) { create(:admin) } + let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } + let(:project2) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace) } + let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') } + let(:project_member) { create(:project_member, :master, user: user, project: project) } + let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } + let(:user4) { create(:user) } + let(:project3) do + create(:project, + :private, + :repository, + name: 'second_project', + path: 'second_project', + creator_id: user.id, + namespace: user.namespace, + merge_requests_enabled: false, + issues_enabled: false, wiki_enabled: false, + snippets_enabled: false) + end + let(:project_member3) do + create(:project_member, + user: user4, + project: project3, + access_level: ProjectMember::MASTER) + end + let(:project4) do + create(:empty_project, + name: 'third_project', + path: 'third_project', + creator_id: user4.id, + namespace: user4.namespace) + end + + describe 'GET /projects' do + before { project } + + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api('/projects') + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as regular user' do + it 'returns an array of projects' do + get v3_api('/projects', user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project.name) + expect(json_response.first['owner']['username']).to eq(user.username) + end + + it 'includes the project labels as the tag_list' do + get v3_api('/projects', user) + expect(response.status).to eq 200 + expect(json_response).to be_an Array + expect(json_response.first.keys).to include('tag_list') + end + + it 'includes open_issues_count' do + get v3_api('/projects', user) + expect(response.status).to eq 200 + expect(json_response).to be_an Array + expect(json_response.first.keys).to include('open_issues_count') + end + + it 'does not include open_issues_count' do + project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) + + get v3_api('/projects', user) + expect(response.status).to eq 200 + expect(json_response).to be_an Array + expect(json_response.first.keys).not_to include('open_issues_count') + end + + context 'GET /projects?simple=true' do + it 'returns a simplified version of all the projects' do + expected_keys = ["id", "http_url_to_repo", "web_url", "name", "name_with_namespace", "path", "path_with_namespace"] + + get v3_api('/projects?simple=true', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first.keys).to match_array expected_keys + end + end + + context 'and using search' do + it 'returns searched project' do + get v3_api('/projects', user), { search: project.name } + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + end + end + + context 'and using the visibility filter' do + it 'filters based on private visibility param' do + get v3_api('/projects', user), { visibility: 'private' } + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count) + end + + it 'filters based on internal visibility param' do + get v3_api('/projects', user), { visibility: 'internal' } + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count) + end + + it 'filters based on public visibility param' do + get v3_api('/projects', user), { visibility: 'public' } + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count) + end + end + + context 'and using sorting' do + before do + project2 + project3 + end + + it 'returns the correct order when sorted by id' do + get v3_api('/projects', user), { order_by: 'id', sort: 'desc' } + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(project3.id) + end + end + end + end + + describe 'GET /projects/all' do + before { project } + + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api('/projects/all') + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as regular user' do + it 'returns authentication error' do + get v3_api('/projects/all', user) + expect(response).to have_http_status(403) + end + end + + context 'when authenticated as admin' do + it 'returns an array of all projects' do + get v3_api('/projects/all', admin) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + + expect(json_response).to satisfy do |response| + response.one? do |entry| + entry.has_key?('permissions') && + entry['name'] == project.name && + entry['owner']['username'] == user.username + end + end + end + + it "does not include statistics by default" do + get v3_api('/projects/all', admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).not_to include('statistics') + end + + it "includes statistics if requested" do + get v3_api('/projects/all', admin), statistics: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).to include 'statistics' + end + end + end + + describe 'GET /projects/owned' do + before do + project3 + project4 + end + + context 'when unauthenticated' do + it 'returns authentication error' do + get v3_api('/projects/owned') + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as project owner' do + it 'returns an array of projects the user owns' do + get v3_api('/projects/owned', user4) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['name']).to eq(project4.name) + expect(json_response.first['owner']['username']).to eq(user4.username) + end + + it "does not include statistics by default" do + get v3_api('/projects/owned', user4) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first).not_to include('statistics') + end + + it "includes statistics if requested" do + attributes = { + commit_count: 23, + storage_size: 702, + repository_size: 123, + lfs_objects_size: 234, + build_artifacts_size: 345, + } + + project4.statistics.update!(attributes) + + get v3_api('/projects/owned', user4), statistics: true + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['statistics']).to eq attributes.stringify_keys + end + end + end + + describe 'GET /projects/visible' do + shared_examples_for 'visible projects response' do + it 'returns the visible projects' do + get v3_api('/projects/visible', current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).to contain_exactly(*projects.map(&:id)) + end + end + + let!(:public_project) { create(:empty_project, :public) } + before do + project + project2 + project3 + project4 + end + + context 'when unauthenticated' do + it_behaves_like 'visible projects response' do + let(:current_user) { nil } + let(:projects) { [public_project] } + end + end + + context 'when authenticated' do + it_behaves_like 'visible projects response' do + let(:current_user) { user } + let(:projects) { [public_project, project, project2, project3] } + end + end + + context 'when authenticated as a different user' do + it_behaves_like 'visible projects response' do + let(:current_user) { user2 } + let(:projects) { [public_project] } + end + end + end + + describe 'GET /projects/starred' do + let(:public_project) { create(:empty_project, :public) } + + before do + project_member2 + user3.update_attributes(starred_projects: [project, project2, project3, public_project]) + end + + it 'returns the starred projects viewable by the user' do + get v3_api('/projects/starred', user3) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.map { |project| project['id'] }).to contain_exactly(project.id, public_project.id) + end + end + + describe 'POST /projects' do + context 'maximum number of projects reached' do + it 'does not create new project and respond with 403' do + allow_any_instance_of(User).to receive(:projects_limit_left).and_return(0) + expect { post v3_api('/projects', user2), name: 'foo' }. + to change {Project.count}.by(0) + expect(response).to have_http_status(403) + end + end + + it 'creates new project without path and return 201' do + expect { post v3_api('/projects', user), name: 'foo' }. + to change { Project.count }.by(1) + expect(response).to have_http_status(201) + end + + it 'creates last project before reaching project limit' do + allow_any_instance_of(User).to receive(:projects_limit_left).and_return(1) + post v3_api('/projects', user2), name: 'foo' + expect(response).to have_http_status(201) + end + + it 'does not create new project without name and return 400' do + expect { post v3_api('/projects', user) }.not_to change { Project.count } + expect(response).to have_http_status(400) + end + + it "assigns attributes to project" do + project = attributes_for(:project, { + path: 'camelCasePath', + description: FFaker::Lorem.sentence, + issues_enabled: false, + merge_requests_enabled: false, + wiki_enabled: false, + only_allow_merge_if_build_succeeds: false, + request_access_enabled: true, + only_allow_merge_if_all_discussions_are_resolved: false + }) + + post v3_api('/projects', user), project + + project.each_pair do |k, v| + next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k) + expect(json_response[k.to_s]).to eq(v) + end + + # Check feature permissions attributes + project = Project.find_by_path(project[:path]) + expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.merge_requests_access_level).to eq(ProjectFeature::DISABLED) + expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED) + end + + it 'sets a project as public' do + project = attributes_for(:project, :public) + post v3_api('/projects', user), project + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + it 'sets a project as public using :public' do + project = attributes_for(:project, { public: true }) + post v3_api('/projects', user), project + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + it 'sets a project as internal' do + project = attributes_for(:project, :internal) + post v3_api('/projects', user), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets a project as internal overriding :public' do + project = attributes_for(:project, :internal, { public: true }) + post v3_api('/projects', user), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets a project as private' do + project = attributes_for(:project, :private) + post v3_api('/projects', user), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + it 'sets a project as private using :public' do + project = attributes_for(:project, { public: false }) + post v3_api('/projects', user), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + it 'sets a project as allowing merge even if build fails' do + project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) + post v3_api('/projects', user), project + expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey + end + + it 'sets a project as allowing merge only if build succeeds' do + project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) + post v3_api('/projects', user), project + expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy + end + + it 'sets a project as allowing merge even if discussions are unresolved' do + project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) + + post v3_api('/projects', user), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey + end + + it 'sets a project as allowing merge if only_allow_merge_if_all_discussions_are_resolved is nil' do + project = attributes_for(:project, only_allow_merge_if_all_discussions_are_resolved: nil) + + post v3_api('/projects', user), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey + end + + it 'sets a project as allowing merge only if all discussions are resolved' do + project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) + + post v3_api('/projects', user), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy + end + + context 'when a visibility level is restricted' do + before do + @project = attributes_for(:project, { public: true }) + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) + end + + it 'does not allow a non-admin to use a restricted visibility level' do + post v3_api('/projects', user), @project + + expect(response).to have_http_status(400) + expect(json_response['message']['visibility_level'].first).to( + match('restricted by your GitLab administrator') + ) + end + + it 'allows an admin to override restricted visibility settings' do + post v3_api('/projects', admin), @project + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to( + eq(Gitlab::VisibilityLevel::PUBLIC) + ) + end + end + end + + describe 'POST /projects/user/:id' do + before { project } + before { admin } + + it 'should create new project without path and return 201' do + expect { post v3_api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) + expect(response).to have_http_status(201) + end + + it 'responds with 400 on failure and not project' do + expect { post v3_api("/projects/user/#{user.id}", admin) }. + not_to change { Project.count } + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('name is missing') + end + + it 'assigns attributes to project' do + project = attributes_for(:project, { + description: FFaker::Lorem.sentence, + issues_enabled: false, + merge_requests_enabled: false, + wiki_enabled: false, + request_access_enabled: true + }) + + post v3_api("/projects/user/#{user.id}", admin), project + + expect(response).to have_http_status(201) + project.each_pair do |k, v| + next if %i[has_external_issue_tracker path].include?(k) + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'sets a project as public' do + project = attributes_for(:project, :public) + post v3_api("/projects/user/#{user.id}", admin), project + + expect(response).to have_http_status(201) + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + it 'sets a project as public using :public' do + project = attributes_for(:project, { public: true }) + post v3_api("/projects/user/#{user.id}", admin), project + + expect(response).to have_http_status(201) + expect(json_response['public']).to be_truthy + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PUBLIC) + end + + it 'sets a project as internal' do + project = attributes_for(:project, :internal) + post v3_api("/projects/user/#{user.id}", admin), project + + expect(response).to have_http_status(201) + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets a project as internal overriding :public' do + project = attributes_for(:project, :internal, { public: true }) + post v3_api("/projects/user/#{user.id}", admin), project + expect(response).to have_http_status(201) + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::INTERNAL) + end + + it 'sets a project as private' do + project = attributes_for(:project, :private) + post v3_api("/projects/user/#{user.id}", admin), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + it 'sets a project as private using :public' do + project = attributes_for(:project, { public: false }) + post v3_api("/projects/user/#{user.id}", admin), project + expect(json_response['public']).to be_falsey + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + it 'sets a project as allowing merge even if build fails' do + project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false }) + post v3_api("/projects/user/#{user.id}", admin), project + expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey + end + + it 'sets a project as allowing merge only if build succeeds' do + project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true }) + post v3_api("/projects/user/#{user.id}", admin), project + expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy + end + + it 'sets a project as allowing merge even if discussions are unresolved' do + project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: false }) + + post v3_api("/projects/user/#{user.id}", admin), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_falsey + end + + it 'sets a project as allowing merge only if all discussions are resolved' do + project = attributes_for(:project, { only_allow_merge_if_all_discussions_are_resolved: true }) + + post v3_api("/projects/user/#{user.id}", admin), project + + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy + end + end + + describe "POST /projects/:id/uploads" do + before { project } + + it "uploads the file and returns its info" do + post v3_api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") + + expect(response).to have_http_status(201) + expect(json_response['alt']).to eq("dk") + expect(json_response['url']).to start_with("/uploads/") + expect(json_response['url']).to end_with("/dk.png") + end + end + + describe 'GET /projects/:id' do + context 'when unauthenticated' do + it 'returns the public projects' do + public_project = create(:empty_project, :public) + + get v3_api("/projects/#{public_project.id}") + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(public_project.id) + expect(json_response['description']).to eq(public_project.description) + expect(json_response.keys).not_to include('permissions') + end + end + + context 'when authenticated' do + before do + project + project_member + end + + it 'returns a project by id' do + group = create(:group) + link = create(:project_group_link, project: project, group: group) + + get v3_api("/projects/#{project.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(project.id) + expect(json_response['description']).to eq(project.description) + expect(json_response['default_branch']).to eq(project.default_branch) + expect(json_response['tag_list']).to be_an Array + expect(json_response['public']).to be_falsey + expect(json_response['archived']).to be_falsey + expect(json_response['visibility_level']).to be_present + expect(json_response['ssh_url_to_repo']).to be_present + expect(json_response['http_url_to_repo']).to be_present + expect(json_response['web_url']).to be_present + expect(json_response['owner']).to be_a Hash + expect(json_response['owner']).to be_a Hash + expect(json_response['name']).to eq(project.name) + expect(json_response['path']).to be_present + expect(json_response['issues_enabled']).to be_present + expect(json_response['merge_requests_enabled']).to be_present + expect(json_response['wiki_enabled']).to be_present + expect(json_response['builds_enabled']).to be_present + expect(json_response['snippets_enabled']).to be_present + expect(json_response['container_registry_enabled']).to be_present + expect(json_response['created_at']).to be_present + expect(json_response['last_activity_at']).to be_present + expect(json_response['shared_runners_enabled']).to be_present + expect(json_response['creator_id']).to be_present + expect(json_response['namespace']).to be_present + expect(json_response['avatar_url']).to be_nil + expect(json_response['star_count']).to be_present + expect(json_response['forks_count']).to be_present + expect(json_response['public_builds']).to be_present + expect(json_response['shared_with_groups']).to be_an Array + expect(json_response['shared_with_groups'].length).to eq(1) + expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id) + expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name) + expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access) + expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds) + expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved) + end + + it 'returns a project by path name' do + get v3_api("/projects/#{project.id}", user) + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(project.name) + end + + it 'returns a 404 error if not found' do + get v3_api('/projects/42', user) + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'returns a 404 error if user is not a member' do + other_user = create(:user) + get v3_api("/projects/#{project.id}", other_user) + expect(response).to have_http_status(404) + end + + it 'handles users with dots' do + dot_user = create(:user, username: 'dot.user') + project = create(:empty_project, creator_id: dot_user.id, namespace: dot_user.namespace) + + get v3_api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(project.name) + end + + it 'exposes namespace fields' do + get v3_api("/projects/#{project.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['namespace']).to eq({ + 'id' => user.namespace.id, + 'name' => user.namespace.name, + 'path' => user.namespace.path, + 'kind' => user.namespace.kind, + }) + end + + describe 'permissions' do + context 'all projects' do + before { project.team << [user, :master] } + + it 'contains permission information' do + get v3_api("/projects", user) + + expect(response).to have_http_status(200) + expect(json_response.first['permissions']['project_access']['access_level']). + to eq(Gitlab::Access::MASTER) + expect(json_response.first['permissions']['group_access']).to be_nil + end + end + + context 'personal project' do + it 'sets project access and returns 200' do + project.team << [user, :master] + get v3_api("/projects/#{project.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['permissions']['project_access']['access_level']). + to eq(Gitlab::Access::MASTER) + expect(json_response['permissions']['group_access']).to be_nil + end + end + + context 'group project' do + let(:project2) { create(:empty_project, group: create(:group)) } + + before { project2.group.add_owner(user) } + + it 'sets the owner and return 200' do + get v3_api("/projects/#{project2.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['permissions']['project_access']).to be_nil + expect(json_response['permissions']['group_access']['access_level']). + to eq(Gitlab::Access::OWNER) + end + end + end + end + end + + describe 'GET /projects/:id/events' do + shared_examples_for 'project events response' do + it 'returns the project events' do + member = create(:user) + create(:project_member, :developer, user: member, project: project) + note = create(:note_on_issue, note: 'What an awesome day!', project: project) + EventCreateService.new.leave_note(note, note.author) + + get v3_api("/projects/#{project.id}/events", current_user) + + expect(response).to have_http_status(200) + + first_event = json_response.first + + expect(first_event['action_name']).to eq('commented on') + expect(first_event['note']['body']).to eq('What an awesome day!') + + last_event = json_response.last + + expect(last_event['action_name']).to eq('joined') + expect(last_event['project_id'].to_i).to eq(project.id) + expect(last_event['author_username']).to eq(member.username) + expect(last_event['author']['name']).to eq(member.name) + end + end + + context 'when unauthenticated' do + it_behaves_like 'project events response' do + let(:project) { create(:empty_project, :public) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + context 'valid request' do + it_behaves_like 'project events response' do + let(:current_user) { user } + end + end + + it 'returns a 404 error if not found' do + get v3_api('/projects/42/events', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'returns a 404 error if user is not a member' do + other_user = create(:user) + + get v3_api("/projects/#{project.id}/events", other_user) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET /projects/:id/users' do + shared_examples_for 'project users response' do + it 'returns the project users' do + member = create(:user) + create(:project_member, :developer, user: member, project: project) + + get v3_api("/projects/#{project.id}/users", current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + + first_user = json_response.first + + expect(first_user['username']).to eq(member.username) + expect(first_user['name']).to eq(member.name) + expect(first_user.keys).to contain_exactly(*%w[name username id state avatar_url web_url]) + end + end + + context 'when unauthenticated' do + it_behaves_like 'project users response' do + let(:project) { create(:empty_project, :public) } + let(:current_user) { nil } + end + end + + context 'when authenticated' do + context 'valid request' do + it_behaves_like 'project users response' do + let(:current_user) { user } + end + end + + it 'returns a 404 error if not found' do + get v3_api('/projects/42/users', user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + + it 'returns a 404 error if user is not a member' do + other_user = create(:user) + + get v3_api("/projects/#{project.id}/users", other_user) + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET /projects/:id/snippets' do + before { snippet } + + it 'returns an array of project snippets' do + get v3_api("/projects/#{project.id}/snippets", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(snippet.title) + end + end + + describe 'GET /projects/:id/snippets/:snippet_id' do + it 'returns a project snippet' do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user) + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(snippet.title) + end + + it 'returns a 404 error if snippet id not found' do + get v3_api("/projects/#{project.id}/snippets/1234", user) + expect(response).to have_http_status(404) + end + end + + describe 'POST /projects/:id/snippets' do + it 'creates a new project snippet' do + post v3_api("/projects/#{project.id}/snippets", user), + title: 'v3_api test', file_name: 'sample.rb', code: 'test', + visibility_level: '0' + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('v3_api test') + end + + it 'returns a 400 error if invalid snippet is given' do + post v3_api("/projects/#{project.id}/snippets", user) + expect(status).to eq(400) + end + end + + describe 'PUT /projects/:id/snippets/:snippet_id' do + it 'updates an existing project snippet' do + put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user), + code: 'updated code' + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('example') + expect(snippet.reload.content).to eq('updated code') + end + + it 'updates an existing project snippet with new title' do + put v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user), + title: 'other v3_api test' + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('other v3_api test') + end + end + + describe 'DELETE /projects/:id/snippets/:snippet_id' do + before { snippet } + + it 'deletes existing project snippet' do + expect do + delete v3_api("/projects/#{project.id}/snippets/#{snippet.id}", user) + end.to change { Snippet.count }.by(-1) + expect(response).to have_http_status(200) + end + + it 'returns 404 when deleting unknown snippet id' do + delete v3_api("/projects/#{project.id}/snippets/1234", user) + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/:id/snippets/:snippet_id/raw' do + it 'gets a raw project snippet' do + get v3_api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user) + expect(response).to have_http_status(200) + end + + it 'returns a 404 error if raw project snippet not found' do + get v3_api("/projects/#{project.id}/snippets/5555/raw", user) + expect(response).to have_http_status(404) + end + end + + describe :fork_admin do + let(:project_fork_target) { create(:empty_project) } + let(:project_fork_source) { create(:empty_project, :public) } + + describe 'POST /projects/:id/fork/:forked_from_id' do + let(:new_project_fork_source) { create(:empty_project, :public) } + + it "is not available for non admin users" do + post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", user) + expect(response).to have_http_status(403) + end + + it 'allows project to be forked from an existing project' do + expect(project_fork_target.forked?).not_to be_truthy + post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + expect(response).to have_http_status(201) + project_fork_target.reload + expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) + expect(project_fork_target.forked_project_link).not_to be_nil + expect(project_fork_target.forked?).to be_truthy + end + + it 'fails if forked_from project which does not exist' do + post v3_api("/projects/#{project_fork_target.id}/fork/9999", admin) + expect(response).to have_http_status(404) + end + + it 'fails with 409 if already forked' do + post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + project_fork_target.reload + expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) + post v3_api("/projects/#{project_fork_target.id}/fork/#{new_project_fork_source.id}", admin) + expect(response).to have_http_status(409) + project_fork_target.reload + expect(project_fork_target.forked_from_project.id).to eq(project_fork_source.id) + expect(project_fork_target.forked?).to be_truthy + end + end + + describe 'DELETE /projects/:id/fork' do + it "is not visible to users outside group" do + delete v3_api("/projects/#{project_fork_target.id}/fork", user) + expect(response).to have_http_status(404) + end + + context 'when users belong to project group' do + let(:project_fork_target) { create(:empty_project, group: create(:group)) } + + before do + project_fork_target.group.add_owner user + project_fork_target.group.add_developer user2 + end + + it 'is forbidden to non-owner users' do + delete v3_api("/projects/#{project_fork_target.id}/fork", user2) + expect(response).to have_http_status(403) + end + + it 'makes forked project unforked' do + post v3_api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin) + project_fork_target.reload + expect(project_fork_target.forked_from_project).not_to be_nil + expect(project_fork_target.forked?).to be_truthy + delete v3_api("/projects/#{project_fork_target.id}/fork", admin) + expect(response).to have_http_status(200) + project_fork_target.reload + expect(project_fork_target.forked_from_project).to be_nil + expect(project_fork_target.forked?).not_to be_truthy + end + + it 'is idempotent if not forked' do + expect(project_fork_target.forked_from_project).to be_nil + delete v3_api("/projects/#{project_fork_target.id}/fork", admin) + expect(response).to have_http_status(304) + expect(project_fork_target.reload.forked_from_project).to be_nil + end + end + end + end + + describe "POST /projects/:id/share" do + let(:group) { create(:group) } + + it "shares project with group" do + expires_at = 10.days.from_now.to_date + + expect do + post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at + end.to change { ProjectGroupLink.count }.by(1) + + expect(response).to have_http_status(201) + expect(json_response['group_id']).to eq(group.id) + expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER) + expect(json_response['expires_at']).to eq(expires_at.to_s) + end + + it "returns a 400 error when group id is not given" do + post v3_api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER + expect(response).to have_http_status(400) + end + + it "returns a 400 error when access level is not given" do + post v3_api("/projects/#{project.id}/share", user), group_id: group.id + expect(response).to have_http_status(400) + end + + it "returns a 400 error when sharing is disabled" do + project.namespace.update(share_with_group_lock: true) + post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER + expect(response).to have_http_status(400) + end + + it 'returns a 404 error when user cannot read group' do + private_group = create(:group, :private) + + post v3_api("/projects/#{project.id}/share", user), group_id: private_group.id, group_access: Gitlab::Access::DEVELOPER + + expect(response).to have_http_status(404) + end + + it 'returns a 404 error when group does not exist' do + post v3_api("/projects/#{project.id}/share", user), group_id: 1234, group_access: Gitlab::Access::DEVELOPER + + expect(response).to have_http_status(404) + end + + it "returns a 400 error when wrong params passed" do + post v3_api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234 + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq 'group_access does not have a valid value' + end + end + + describe 'DELETE /projects/:id/share/:group_id' do + it 'returns 204 when deleting a group share' do + group = create(:group, :public) + create(:project_group_link, group: group, project: project) + + delete v3_api("/projects/#{project.id}/share/#{group.id}", user) + + expect(response).to have_http_status(204) + expect(project.project_group_links).to be_empty + end + + it 'returns a 400 when group id is not an integer' do + delete v3_api("/projects/#{project.id}/share/foo", user) + + expect(response).to have_http_status(400) + end + + it 'returns a 404 error when group link does not exist' do + delete v3_api("/projects/#{project.id}/share/1234", user) + + expect(response).to have_http_status(404) + end + + it 'returns a 404 error when project does not exist' do + delete v3_api("/projects/123/share/1234", user) + + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/search/:query' do + let!(:query) { 'query'} + let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } + let!(:pre) { create(:empty_project, name: "pre_#{query}", creator_id: user.id, namespace: user.namespace) } + let!(:post) { create(:empty_project, name: "#{query}_post", creator_id: user.id, namespace: user.namespace) } + let!(:pre_post) { create(:empty_project, name: "pre_#{query}_post", creator_id: user.id, namespace: user.namespace) } + let!(:unfound) { create(:empty_project, name: 'unfound', creator_id: user.id, namespace: user.namespace) } + let!(:internal) { create(:empty_project, :internal, name: "internal #{query}") } + let!(:unfound_internal) { create(:empty_project, :internal, name: 'unfound internal') } + let!(:public) { create(:empty_project, :public, name: "public #{query}") } + let!(:unfound_public) { create(:empty_project, :public, name: 'unfound public') } + let!(:one_dot_two) { create(:empty_project, :public, name: "one.dot.two") } + + shared_examples_for 'project search response' do |args = {}| + it 'returns project search responses' do + get v3_api("/projects/search/#{args[:query]}", current_user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(args[:results]) + json_response.each { |project| expect(project['name']).to match(args[:match_regex] || /.*#{args[:query]}.*/) } + end + end + + context 'when unauthenticated' do + it_behaves_like 'project search response', query: 'query', results: 1 do + let(:current_user) { nil } + end + end + + context 'when authenticated' do + it_behaves_like 'project search response', query: 'query', results: 6 do + let(:current_user) { user } + end + it_behaves_like 'project search response', query: 'one.dot.two', results: 1 do + let(:current_user) { user } + end + end + + context 'when authenticated as a different user' do + it_behaves_like 'project search response', query: 'query', results: 2, match_regex: /(internal|public) query/ do + let(:current_user) { user2 } + end + end + end + + describe 'PUT /projects/:id' do + before { project } + before { user } + before { user3 } + before { user4 } + before { project3 } + before { project4 } + before { project_member3 } + before { project_member2 } + + context 'when unauthenticated' do + it 'returns authentication error' do + project_param = { name: 'bar' } + put v3_api("/projects/#{project.id}"), project_param + expect(response).to have_http_status(401) + end + end + + context 'when authenticated as project owner' do + it 'updates name' do + project_param = { name: 'bar' } + put v3_api("/projects/#{project.id}", user), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'updates visibility_level' do + project_param = { visibility_level: 20 } + put v3_api("/projects/#{project3.id}", user), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'updates visibility_level from public to private' do + project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC }) + project_param = { public: false } + put v3_api("/projects/#{project3.id}", user), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE) + end + + it 'does not update name to existing name' do + project_param = { name: project3.name } + put v3_api("/projects/#{project.id}", user), project_param + expect(response).to have_http_status(400) + expect(json_response['message']['name']).to eq(['has already been taken']) + end + + it 'updates request_access_enabled' do + project_param = { request_access_enabled: false } + + put v3_api("/projects/#{project.id}", user), project_param + + expect(response).to have_http_status(200) + expect(json_response['request_access_enabled']).to eq(false) + end + + it 'updates path & name to existing path & name in different namespace' do + project_param = { path: project4.path, name: project4.name } + put v3_api("/projects/#{project3.id}", user), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + end + + context 'when authenticated as project master' do + it 'updates path' do + project_param = { path: 'bar' } + put v3_api("/projects/#{project3.id}", user4), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'updates other attributes' do + project_param = { issues_enabled: true, + wiki_enabled: true, + snippets_enabled: true, + merge_requests_enabled: true, + description: 'new description' } + + put v3_api("/projects/#{project3.id}", user4), project_param + expect(response).to have_http_status(200) + project_param.each_pair do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'does not update path to existing path' do + project_param = { path: project.path } + put v3_api("/projects/#{project3.id}", user4), project_param + expect(response).to have_http_status(400) + expect(json_response['message']['path']).to eq(['has already been taken']) + end + + it 'does not update name' do + project_param = { name: 'bar' } + put v3_api("/projects/#{project3.id}", user4), project_param + expect(response).to have_http_status(403) + end + + it 'does not update visibility_level' do + project_param = { visibility_level: 20 } + put v3_api("/projects/#{project3.id}", user4), project_param + expect(response).to have_http_status(403) + end + end + + context 'when authenticated as project developer' do + it 'does not update other attributes' do + project_param = { path: 'bar', + issues_enabled: true, + wiki_enabled: true, + snippets_enabled: true, + merge_requests_enabled: true, + description: 'new description', + request_access_enabled: true } + put v3_api("/projects/#{project.id}", user3), project_param + expect(response).to have_http_status(403) + end + end + end + + describe 'POST /projects/:id/archive' do + context 'on an unarchived project' do + it 'archives the project' do + post v3_api("/projects/#{project.id}/archive", user) + + expect(response).to have_http_status(201) + expect(json_response['archived']).to be_truthy + end + end + + context 'on an archived project' do + before do + project.archive! + end + + it 'remains archived' do + post v3_api("/projects/#{project.id}/archive", user) + + expect(response).to have_http_status(201) + expect(json_response['archived']).to be_truthy + end + end + + context 'user without archiving rights to the project' do + before do + project.team << [user3, :developer] + end + + it 'rejects the action' do + post v3_api("/projects/#{project.id}/archive", user3) + + expect(response).to have_http_status(403) + end + end + end + + describe 'POST /projects/:id/unarchive' do + context 'on an unarchived project' do + it 'remains unarchived' do + post v3_api("/projects/#{project.id}/unarchive", user) + + expect(response).to have_http_status(201) + expect(json_response['archived']).to be_falsey + end + end + + context 'on an archived project' do + before do + project.archive! + end + + it 'unarchives the project' do + post v3_api("/projects/#{project.id}/unarchive", user) + + expect(response).to have_http_status(201) + expect(json_response['archived']).to be_falsey + end + end + + context 'user without archiving rights to the project' do + before do + project.team << [user3, :developer] + end + + it 'rejects the action' do + post v3_api("/projects/#{project.id}/unarchive", user3) + + expect(response).to have_http_status(403) + end + end + end + + describe 'POST /projects/:id/star' do + context 'on an unstarred project' do + it 'stars the project' do + expect { post v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(1) + + expect(response).to have_http_status(201) + expect(json_response['star_count']).to eq(1) + end + end + + context 'on a starred project' do + before do + user.toggle_star(project) + project.reload + end + + it 'does not modify the star count' do + expect { post v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + + expect(response).to have_http_status(304) + end + end + end + + describe 'DELETE /projects/:id/star' do + context 'on a starred project' do + before do + user.toggle_star(project) + project.reload + end + + it 'unstars the project' do + expect { delete v3_api("/projects/#{project.id}/star", user) }.to change { project.reload.star_count }.by(-1) + + expect(response).to have_http_status(200) + expect(json_response['star_count']).to eq(0) + end + end + + context 'on an unstarred project' do + it 'does not modify the star count' do + expect { delete v3_api("/projects/#{project.id}/star", user) }.not_to change { project.reload.star_count } + + expect(response).to have_http_status(304) + end + end + end + + describe 'DELETE /projects/:id' do + context 'when authenticated as user' do + it 'removes project' do + delete v3_api("/projects/#{project.id}", user) + expect(response).to have_http_status(200) + end + + it 'does not remove a project if not an owner' do + user3 = create(:user) + project.team << [user3, :developer] + delete v3_api("/projects/#{project.id}", user3) + expect(response).to have_http_status(403) + end + + it 'does not remove a non existing project' do + delete v3_api('/projects/1328', user) + expect(response).to have_http_status(404) + end + + it 'does not remove a project not attached to user' do + delete v3_api("/projects/#{project.id}", user2) + expect(response).to have_http_status(404) + end + end + + context 'when authenticated as admin' do + it 'removes any existing project' do + delete v3_api("/projects/#{project.id}", admin) + expect(response).to have_http_status(200) + end + + it 'does not remove a non existing project' do + delete v3_api('/projects/1328', admin) + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index 68b196d9033..ae6e708cf87 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -17,8 +17,8 @@ module ApiHelpers # => "/api/v2/issues?foo=bar&private_token=..." # # Returns the relative path to the requested API resource - def api(path, user = nil) - "/api/#{API::API.version}#{path}" + + def api(path, user = nil, version: API::API.version) + "/api/#{version}#{path}" + # Normalize query string (path.index('?') ? '' : '?') + @@ -31,6 +31,11 @@ module ApiHelpers end end + # Temporary helper method for simplifying V3 exclusive API specs + def v3_api(path, user = nil) + api(path, user, version: 'v3') + end + def ci_api(path, user = nil) "/ci/api/v1/#{path}" + -- cgit v1.2.1 From 29414ab0438583c7401e94a74a613497874b5e4e Mon Sep 17 00:00:00 2001 From: Drew Blessing <drew@gitlab.com> Date: Tue, 24 Jan 2017 11:12:49 -0600 Subject: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms We accept half a dozen different authentication mechanisms for Git over HTTP. Fairly high in the list we were checking user password, which would also query LDAP. In the case of LFS, OAuth tokens or personal access tokens, we were unnecessarily hitting LDAP when the authentication will not succeed. This was causing some LDAP/AD systems to lock the account. Now, user password authentication is the last mechanism tried since it's the most expensive. --- .../24462-reduce_ldap_queries_for_lfs.yml | 4 + lib/gitlab/auth.rb | 11 ++- spec/lib/gitlab/auth_spec.rb | 96 ++++++++++++++++------ 3 files changed, 82 insertions(+), 29 deletions(-) create mode 100644 changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml diff --git a/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml new file mode 100644 index 00000000000..05fbd8f0bf2 --- /dev/null +++ b/changelogs/unreleased/24462-reduce_ldap_queries_for_lfs.yml @@ -0,0 +1,4 @@ +--- +title: Reduce hits to LDAP on Git HTTP auth by reordering auth mechanisms +merge_request: 8752 +author: diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb index 8dda65c71ef..f638905a1e0 100644 --- a/lib/gitlab/auth.rb +++ b/lib/gitlab/auth.rb @@ -10,13 +10,16 @@ module Gitlab def find_for_git_client(login, password, project:, ip:) raise "Must provide an IP for rate limiting" if ip.nil? + # `user_with_password_for_git` should be the last check + # because it's the most expensive, especially when LDAP + # is enabled. result = service_request_check(login, password, project) || build_access_token_check(login, password) || - user_with_password_for_git(login, password) || - oauth_access_token_check(login, password) || lfs_token_check(login, password) || + oauth_access_token_check(login, password) || personal_access_token_check(login, password) || + user_with_password_for_git(login, password) || Gitlab::Auth::Result.new rate_limit!(ip, success: result.success?, login: login) @@ -143,7 +146,9 @@ module Gitlab read_authentication_abilities end - Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password) + if Devise.secure_compare(token_handler.token, password) + Gitlab::Auth::Result.new(actor, nil, token_handler.type, authentication_abilities) + end end def build_access_token_check(login, password) diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index f251c0dd25a..b234de4c772 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -58,58 +58,102 @@ describe Gitlab::Auth, lib: true do expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) end - it 'recognizes user lfs tokens' do - user = create(:user) - token = Gitlab::LfsToken.new(user).token + context 'while using LFS authenticate' do + it 'recognizes user lfs tokens' do + user = create(:user) + token = Gitlab::LfsToken.new(user).token - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) - expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) - end + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) + expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) + end - it 'recognizes deploy key lfs tokens' do - key = create(:deploy_key) - token = Gitlab::LfsToken.new(key).token + it 'recognizes deploy key lfs tokens' do + key = create(:deploy_key) + token = Gitlab::LfsToken.new(key).token - expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") - expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) - end + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") + expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) + end - context "while using OAuth tokens as passwords" do - it 'succeeds for OAuth tokens with the `api` scope' do + it 'does not try password auth before oauth' do user = create(:user) - application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") + token = Gitlab::LfsToken.new(user).token + + expect(gl_auth).not_to receive(:find_with_user_password) + gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip') + end + end + + context 'while using OAuth tokens as passwords' do + let(:user) { create(:user) } + let(:token_w_api_scope) { Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'api') } + let(:application) { Doorkeeper::Application.create!(name: 'MyApp', redirect_uri: 'https://app.com', owner: user) } + + it 'succeeds for OAuth tokens with the `api` scope' do expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2') - expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + expect(gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) end it 'fails for OAuth tokens with other scopes' do - user = create(:user) - application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user") + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: 'read_user') expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2') expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) end + + it 'does not try password auth before oauth' do + expect(gl_auth).not_to receive(:find_with_user_password) + + gl_auth.find_for_git_client("oauth2", token_w_api_scope.token, project: nil, ip: 'ip') + end end - context "while using personal access tokens as passwords" do - it 'succeeds for personal access tokens with the `api` scope' do - user = create(:user) - personal_access_token = create(:personal_access_token, user: user, scopes: ['api']) + context 'while using personal access tokens as passwords' do + let(:user) { create(:user) } + let(:token_w_api_scope) { create(:personal_access_token, user: user, scopes: ['api']) } + it 'succeeds for personal access tokens with the `api` scope' do expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email) - expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) + expect(gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) end it 'fails for personal access tokens with other scopes' do - user = create(:user) personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email) expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) end + + it 'does not try password auth before personal access tokens' do + expect(gl_auth).not_to receive(:find_with_user_password) + + gl_auth.find_for_git_client(user.email, token_w_api_scope.token, project: nil, ip: 'ip') + end + end + + context 'while using regular user and password' do + it 'falls through lfs authentication' do + user = create( + :user, + username: 'normal_user', + password: 'my-secret', + ) + + expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')) + .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + end + + it 'falls through oauth authentication when the username is oauth2' do + user = create( + :user, + username: 'oauth2', + password: 'my-secret', + ) + + expect(gl_auth.find_for_git_client(user.username, user.password, project: nil, ip: 'ip')) + .to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + end end it 'returns double nil for invalid credentials' do -- cgit v1.2.1 From d7f298c177555a09ac06acc9ad037611f664cc9e Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Mon, 30 Jan 2017 12:12:57 +0100 Subject: Incorporate feedback --- lib/gitlab/chat_commands/command.rb | 2 +- lib/gitlab/chat_commands/help.rb | 4 ++-- lib/gitlab/chat_commands/presenters/access.rb | 14 ++++++++++++++ lib/gitlab/chat_commands/presenters/help.rb | 12 +++++++----- lib/gitlab/chat_commands/presenters/issue_new.rb | 4 +++- lib/gitlab/chat_commands/presenters/issue_search.rb | 4 +++- lib/gitlab/chat_commands/presenters/issue_show.rb | 11 ++++++++--- spec/lib/gitlab/chat_commands/command_spec.rb | 6 +----- .../lib/gitlab/chat_commands/presenters/issue_show_spec.rb | 10 ++++++++++ 9 files changed, 49 insertions(+), 18 deletions(-) diff --git a/lib/gitlab/chat_commands/command.rb b/lib/gitlab/chat_commands/command.rb index e7baa20356c..f34ed0f4cf2 100644 --- a/lib/gitlab/chat_commands/command.rb +++ b/lib/gitlab/chat_commands/command.rb @@ -18,7 +18,7 @@ module Gitlab Gitlab::ChatCommands::Presenters::Access.new.access_denied end else - Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands) + Gitlab::ChatCommands::Help.new(project, current_user, params).execute(available_commands, params[:text]) end end diff --git a/lib/gitlab/chat_commands/help.rb b/lib/gitlab/chat_commands/help.rb index e76733f5445..6c0e4d304a4 100644 --- a/lib/gitlab/chat_commands/help.rb +++ b/lib/gitlab/chat_commands/help.rb @@ -16,8 +16,8 @@ module Gitlab true end - def execute(commands) - Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger) + def execute(commands, text) + Gitlab::ChatCommands::Presenters::Help.new(commands).present(trigger, text) end def trigger diff --git a/lib/gitlab/chat_commands/presenters/access.rb b/lib/gitlab/chat_commands/presenters/access.rb index b66ef48d6a8..92f4fa17f78 100644 --- a/lib/gitlab/chat_commands/presenters/access.rb +++ b/lib/gitlab/chat_commands/presenters/access.rb @@ -20,6 +20,20 @@ module Gitlab ephemeral_response(text: message) end + + def unknown_command(commands) + ephemeral_response(text: help_message(trigger)) + end + + private + + def help_message(trigger) + header_with_list("Command not found, these are the commands you can use", full_commands(trigger)) + end + + def full_commands(trigger) + @resource.map { |command| "#{trigger} #{command.help_message}" } + end end end end diff --git a/lib/gitlab/chat_commands/presenters/help.rb b/lib/gitlab/chat_commands/presenters/help.rb index 39ad3249f5b..cd47b7f4c6a 100644 --- a/lib/gitlab/chat_commands/presenters/help.rb +++ b/lib/gitlab/chat_commands/presenters/help.rb @@ -2,17 +2,19 @@ module Gitlab module ChatCommands module Presenters class Help < Presenters::Base - def present(trigger) - ephemeral_response(text: help_message(trigger)) + def present(trigger, text) + ephemeral_response(text: help_message(trigger, text)) end private - def help_message(trigger) - if @resource.present? + def help_message(trigger, text) + return "No commands available :thinking_face:" unless @resource.present? + + if text.start_with?('help') header_with_list("Available commands", full_commands(trigger)) else - "No commands available :thinking_face:" + header_with_list("Unknown command, these commands are available", full_commands(trigger)) end end diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb index d26dd22b2a0..6e88e0574a3 100644 --- a/lib/gitlab/chat_commands/presenters/issue_new.rb +++ b/lib/gitlab/chat_commands/presenters/issue_new.rb @@ -24,7 +24,9 @@ module Gitlab fields: fields, mrkdwn_in: [ :title, - :text + :pretext, + :text, + :fields ] } ] diff --git a/lib/gitlab/chat_commands/presenters/issue_search.rb b/lib/gitlab/chat_commands/presenters/issue_search.rb index d58a6d6114a..3478359b91d 100644 --- a/lib/gitlab/chat_commands/presenters/issue_search.rb +++ b/lib/gitlab/chat_commands/presenters/issue_search.rb @@ -7,6 +7,8 @@ module Gitlab def present text = if @resource.count >= 5 "Here are the first 5 issues I found:" + elsif @resource.one? + "Here is the only issue I found:" else "Here are the #{@resource.count} issues I found:" end @@ -26,7 +28,7 @@ module Gitlab text: "#{url} · #{issue.title} (#{status_text(issue)})", mrkdwn_in: [ - "text" + :text ] } end diff --git a/lib/gitlab/chat_commands/presenters/issue_show.rb b/lib/gitlab/chat_commands/presenters/issue_show.rb index 2fc671f13a6..fe5847ccd15 100644 --- a/lib/gitlab/chat_commands/presenters/issue_show.rb +++ b/lib/gitlab/chat_commands/presenters/issue_show.rb @@ -5,7 +5,11 @@ module Gitlab include Presenters::Issuable def present - in_channel_response(show_issue) + if @resource.confidential? + ephemeral_response(show_issue) + else + in_channel_response(show_issue) + end end private @@ -25,7 +29,8 @@ module Gitlab fields: fields, mrkdwn_in: [ :pretext, - :text + :text, + :fields ] } ] @@ -48,7 +53,7 @@ module Gitlab end def pretext - "Issue *#{@resource.to_reference} from #{project.name_with_namespace}" + "Issue *#{@resource.to_reference}* from #{project.name_with_namespace}" end end end diff --git a/spec/lib/gitlab/chat_commands/command_spec.rb b/spec/lib/gitlab/chat_commands/command_spec.rb index 0acf40de1d3..b6e924d67be 100644 --- a/spec/lib/gitlab/chat_commands/command_spec.rb +++ b/spec/lib/gitlab/chat_commands/command_spec.rb @@ -5,7 +5,6 @@ describe Gitlab::ChatCommands::Command, service: true do let(:user) { create(:user) } describe '#execute' do -<<<<<<< HEAD subject do described_class.new(project, user, params).execute end @@ -19,16 +18,13 @@ describe Gitlab::ChatCommands::Command, service: true do expect(subject[:text]).to start_with('404 not found') end end -======= - subject { described_class.new(project, user, params).execute } ->>>>>>> Chat Commands have presenters context 'when an unknown command is triggered' do let(:params) { { command: '/gitlab', text: "unknown command 123" } } it 'displays the help message' do expect(subject[:response_type]).to be(:ephemeral) - expect(subject[:text]).to start_with('Available commands') + expect(subject[:text]).to start_with('Unknown command') expect(subject[:text]).to match('/gitlab issue show') end end diff --git a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb index 89d154e26e4..5b678d31fce 100644 --- a/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb +++ b/spec/lib/gitlab/chat_commands/presenters/issue_show_spec.rb @@ -21,7 +21,17 @@ describe Gitlab::ChatCommands::Presenters::IssueShow do end it 'shows the upvote count' do + expect(subject[:response_type]).to be(:in_channel) expect(attachment[:text]).to start_with("**Open** · :+1: 1") end end + + context 'confidential issue' do + let(:issue) { create(:issue, project: project) } + + it 'shows an ephemeral response' do + expect(subject[:response_type]).to be(:in_channel) + expect(attachment[:text]).to start_with("**Open**") + end + end end -- cgit v1.2.1 From b61b45a7596b3f20aa1a0d264e6ba3c7af844bac Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 30 Jan 2017 14:01:02 -0600 Subject: Link to pipeline page from commit widget --- app/views/projects/commit/_commit_box.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 2b1c4e28ce2..56f2c1529fe 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -64,10 +64,10 @@ - if @commit.status .well-segment.pipeline-info %div{ class: "icon-container ci-status-icon-#{@commit.status}" } - = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do + = link_to namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id) do = ci_icon_for_status(@commit.status) Pipeline - = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace" + = link_to "##{@commit.pipelines.last.id}", namespace_project_pipeline_path(@project.namespace, @project, @commit.pipelines.last.id), class: "monospace" for = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace" %span.ci-status-label -- cgit v1.2.1 From 538d1bffec52ecdaae44eaf9fdbcb6102c1cbefd Mon Sep 17 00:00:00 2001 From: Adam Pahlevi <adam.pahlevi@gmail.com> Date: Sat, 28 Jan 2017 09:50:25 +0700 Subject: resolve deprecation warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit don’t pass AR object, use the ID to avoid depr warning pass in the id instead of AR object to specs for `ProjectDestroyWorker` --- app/controllers/groups_controller.rb | 2 +- spec/models/members/project_member_spec.rb | 2 +- spec/workers/project_destroy_worker_spec.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index f81237db991..264b14713fb 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -84,7 +84,7 @@ class GroupsController < Groups::ApplicationController if Groups::UpdateService.new(@group, current_user, group_params).execute redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else - @group.reset_path! + @group.restore_path! render action: "edit" end diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb index 90d14c2c0b9..e4be0aba7a6 100644 --- a/spec/models/members/project_member_spec.rb +++ b/spec/models/members/project_member_spec.rb @@ -117,7 +117,7 @@ describe ProjectMember, models: true do users = create_list(:user, 2) described_class.add_users_to_projects( - [projects.first.id, projects.second], + [projects.first.id, projects.second.id], [users.first.id, users.second], described_class::MASTER) diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb index 1b910d9b91e..1f4c39eb64a 100644 --- a/spec/workers/project_destroy_worker_spec.rb +++ b/spec/workers/project_destroy_worker_spec.rb @@ -8,14 +8,14 @@ describe ProjectDestroyWorker do describe "#perform" do it "deletes the project" do - subject.perform(project.id, project.owner, {}) + subject.perform(project.id, project.owner.id, {}) expect(Project.all).not_to include(project) expect(Dir.exist?(path)).to be_falsey end it "deletes the project but skips repo deletion" do - subject.perform(project.id, project.owner, { "skip_repo" => true }) + subject.perform(project.id, project.owner.id, { "skip_repo" => true }) expect(Project.all).not_to include(project) expect(Dir.exist?(path)).to be_truthy -- cgit v1.2.1 From 124c99032292c11a2f69197233683db9bee4d463 Mon Sep 17 00:00:00 2001 From: Adam Pahlevi <adam.pahlevi@gmail.com> Date: Sat, 28 Jan 2017 10:18:39 +0700 Subject: add complete changelog --- changelogs/unreleased/fix-depr-warn.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-depr-warn.yml diff --git a/changelogs/unreleased/fix-depr-warn.yml b/changelogs/unreleased/fix-depr-warn.yml new file mode 100644 index 00000000000..61817027720 --- /dev/null +++ b/changelogs/unreleased/fix-depr-warn.yml @@ -0,0 +1,4 @@ +--- +title: resolve deprecation warnings +merge_request: 8855 +author: Adam Pahlevi -- cgit v1.2.1 From ad69a89f044a8493d14f9ef4ce2c7e09b830003f Mon Sep 17 00:00:00 2001 From: tauriedavis <taurie@gitlab.com> Date: Mon, 30 Jan 2017 19:29:35 -0800 Subject: 19164 Add settings dropdown to mobile screens --- app/assets/stylesheets/framework/nav.scss | 12 ++++++++---- changelogs/unreleased/19164-mobile-settings.yml | 4 ++++ 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/19164-mobile-settings.yml diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index 401c2d0f6ee..adbc141262e 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -294,16 +294,19 @@ .container-fluid { position: relative; + + .nav-control { + @media (max-width: $screen-sm-max) { + text-align: left; + margin-right: 75px; + } + } } .controls { float: right; padding: 7px 0 0; - @media (max-width: $screen-sm-max) { - display: none; - } - i { color: $layout-link-gray; } @@ -361,6 +364,7 @@ .fade-left { @include fade(right, $gray-light); left: -5px; + text-align: center; .fa { left: -7px; diff --git a/changelogs/unreleased/19164-mobile-settings.yml b/changelogs/unreleased/19164-mobile-settings.yml new file mode 100644 index 00000000000..c26a20f87e2 --- /dev/null +++ b/changelogs/unreleased/19164-mobile-settings.yml @@ -0,0 +1,4 @@ +--- +title: 19164 Add settings dropdown to mobile screens +merge_request: +author: -- cgit v1.2.1 From 8615138706337e6aca75d635e8fe3a867f9e69bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Tue, 31 Jan 2017 11:05:50 +0100 Subject: Move Dashboard shortcuts specs from Spinah to RSpec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- features/dashboard/shortcuts.feature | 21 --------------------- features/steps/dashboard/shortcuts.rb | 7 ------- spec/features/dashboard/shortcuts_spec.rb | 29 +++++++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 28 deletions(-) delete mode 100644 features/dashboard/shortcuts.feature delete mode 100644 features/steps/dashboard/shortcuts.rb create mode 100644 spec/features/dashboard/shortcuts_spec.rb diff --git a/features/dashboard/shortcuts.feature b/features/dashboard/shortcuts.feature deleted file mode 100644 index 41d79aa6ec8..00000000000 --- a/features/dashboard/shortcuts.feature +++ /dev/null @@ -1,21 +0,0 @@ -@dashboard -Feature: Dashboard Shortcuts - Background: - Given I sign in as a user - And I visit dashboard page - - @javascript - Scenario: Navigate to projects tab - Given I press "g" and "p" - Then the active main tab should be Projects - - @javascript - Scenario: Navigate to issue tab - Given I press "g" and "i" - Then the active main tab should be Issues - - @javascript - Scenario: Navigate to merge requests tab - Given I press "g" and "m" - Then the active main tab should be Merge Requests - diff --git a/features/steps/dashboard/shortcuts.rb b/features/steps/dashboard/shortcuts.rb deleted file mode 100644 index 118d27888df..00000000000 --- a/features/steps/dashboard/shortcuts.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Spinach::Features::DashboardShortcuts < Spinach::FeatureSteps - include SharedAuthentication - include SharedPaths - include SharedProject - include SharedSidebarActiveTab - include SharedShortcuts -end diff --git a/spec/features/dashboard/shortcuts_spec.rb b/spec/features/dashboard/shortcuts_spec.rb new file mode 100644 index 00000000000..d9be4e5dbdd --- /dev/null +++ b/spec/features/dashboard/shortcuts_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +feature 'Dashboard shortcuts', feature: true, js: true do + before do + login_as :user + visit dashboard_projects_path + end + + scenario 'Navigate to tabs' do + find('body').native.send_key('g') + find('body').native.send_key('p') + + ensure_active_main_tab('Projects') + + find('body').native.send_key('g') + find('body').native.send_key('i') + + ensure_active_main_tab('Issues') + + find('body').native.send_key('g') + find('body').native.send_key('m') + + ensure_active_main_tab('Merge Requests') + end + + def ensure_active_main_tab(content) + expect(find('.nav-sidebar li.active')).to have_content(content) + end +end -- cgit v1.2.1 From 7d0cdf62673303e9164c2a98ca9387d8f26c2233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Tue, 31 Jan 2017 11:28:56 +0100 Subject: Simplify the SSH protocol introduction and link to a DO tutorial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- doc/ssh/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 9803937fcf9..9e391d647a8 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -4,10 +4,12 @@ Git is a distributed version control system, which means you can work locally but you can also share or "push" your changes to other servers. Before you can push your changes to a GitLab server you need a secure communication channel for sharing information. -GitLab uses Public-key or asymmetric cryptography -which encrypts a communication channel by locking it with your "private key" -and allows trusted parties to unlock it with your "public key". -If someone does not have your public key they cannot access the unencrypted message. + +The SSH protocol provides this security and allows you to authenticate to the +GitLab remote server without supplying your username or password each time. + +For a more detailed explanation of how the SSH protocol works, we advise you to +read [this nice tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/understanding-the-ssh-encryption-and-connection-process). ## Locating an existing SSH key pair -- cgit v1.2.1 From 90114fea5f4ec410fd3a01892706ad6a066efa43 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Tue, 31 Jan 2017 17:00:53 +0600 Subject: hides search button --- app/views/admin/projects/index.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 5936312801b..cf8d438670b 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -40,6 +40,7 @@ = render 'shared/projects/dropdown' = link_to new_project_path, class: 'btn btn-new' do New Project + = button_tag "Search", class: "btn btn-primary btn-search hide" %ul.nav-links - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path } -- cgit v1.2.1 From dcac161472d163d1c64555f62c15722c2ee01ec9 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Tue, 31 Jan 2017 17:04:23 +0600 Subject: adds changelog --- changelogs/unreleased/redesign-searchbar-admin-project-26794.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/redesign-searchbar-admin-project-26794.yml diff --git a/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml b/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml new file mode 100644 index 00000000000..547a7c6755c --- /dev/null +++ b/changelogs/unreleased/redesign-searchbar-admin-project-26794.yml @@ -0,0 +1,4 @@ +--- +title: Redesign searchbar in admin project list +merge_request: 8776 +author: -- cgit v1.2.1 From ef20bb2edd932a8e144aded11c83046e77ea79d9 Mon Sep 17 00:00:00 2001 From: dimitrieh <dimitriehoekstra@gmail.com> Date: Tue, 31 Jan 2017 12:09:37 +0100 Subject: Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles --- app/assets/javascripts/environments/components/environment.js.es6 | 2 +- app/views/projects/environments/show.html.haml | 2 +- changelogs/unreleased/27494-environment-list-column-headers.yml | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/27494-environment-list-column-headers.yml diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index fea642467fa..971be04e2d2 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -182,7 +182,7 @@ <th class="environments-deploy">Last deployment</th> <th class="environments-build">Build</th> <th class="environments-commit">Commit</th> - <th class="environments-date">Created</th> + <th class="environments-date">Updated</th> <th class="hidden-xs environments-actions"></th> </tr> </thead> diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 6e0d9456900..51c31a09378 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -33,7 +33,7 @@ %th ID %th Commit %th Build - %th + %th Created %th.hidden-xs = render @deployments diff --git a/changelogs/unreleased/27494-environment-list-column-headers.yml b/changelogs/unreleased/27494-environment-list-column-headers.yml new file mode 100644 index 00000000000..798c01f3238 --- /dev/null +++ b/changelogs/unreleased/27494-environment-list-column-headers.yml @@ -0,0 +1,4 @@ +--- +title: Edited the column header for the environments list from created to updated and added created to environments detail page colum header titles +merge_request: +author: -- cgit v1.2.1 From a54a734f70273b99b2f34236ebf4abd83dad43dc Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Tue, 31 Jan 2017 17:40:37 +0600 Subject: fixes mobile view --- app/views/shared/projects/_dropdown.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml index b7f8551153b..ac028f18e50 100644 --- a/app/views/shared/projects/_dropdown.html.haml +++ b/app/views/shared/projects/_dropdown.html.haml @@ -2,7 +2,7 @@ - personal = params[:personal] - archived = params[:archived] - namespace_id = params[:namespace_id] -.dropdown.inline +.dropdown - toggle_text = projects_sort_options_hash[@sort] = dropdown_toggle(toggle_text, { toggle: 'dropdown' }, { id: 'sort-projects-dropdown' }) %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable -- cgit v1.2.1 From 4840c682414aa2e7d57fa6f792405a49709b7dd8 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Tue, 31 Jan 2017 10:40:24 +0000 Subject: Don't capitalize environment name in show page Upate test to match the new behavior --- app/views/projects/environments/show.html.haml | 2 +- changelogs/unreleased/27484-environment-show-name.yml | 4 ++++ spec/features/environment_spec.rb | 4 ++++ spec/features/environments_spec.rb | 2 +- 4 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/27484-environment-show-name.yml diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index 6e0d9456900..b23ca109746 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -5,7 +5,7 @@ %div{ class: container_class } .top-area.adjust .col-md-9 - %h3.page-title= @environment.name.capitalize + %h3.page-title= @environment.name .col-md-3 .nav-controls = render 'projects/environments/terminal_button', environment: @environment diff --git a/changelogs/unreleased/27484-environment-show-name.yml b/changelogs/unreleased/27484-environment-show-name.yml new file mode 100644 index 00000000000..dc400d65006 --- /dev/null +++ b/changelogs/unreleased/27484-environment-show-name.yml @@ -0,0 +1,4 @@ +--- +title: Don't capitalize environment name in show page +merge_request: +author: diff --git a/spec/features/environment_spec.rb b/spec/features/environment_spec.rb index 56f6cd2e095..511c95b758f 100644 --- a/spec/features/environment_spec.rb +++ b/spec/features/environment_spec.rb @@ -19,6 +19,10 @@ feature 'Environment', :feature do visit_environment(environment) end + scenario 'shows environment name' do + expect(page).to have_content(environment.name) + end + context 'without deployments' do scenario 'does show no deployments' do expect(page).to have_content('You don\'t have any deployments right now.') diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 72b984cfab8..c033b693213 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -194,7 +194,7 @@ feature 'Environments page', :feature, :js do end scenario 'does create a new pipeline' do - expect(page).to have_content('Production') + expect(page).to have_content('production') end end -- cgit v1.2.1 From 164eb3aa37cbcff2c5fbf582c3acdbaa3e6fee77 Mon Sep 17 00:00:00 2001 From: "Z.J. van de Weg" <git@zjvandeweg.nl> Date: Tue, 31 Jan 2017 12:00:43 +0100 Subject: Improve styling of the new issue message --- lib/gitlab/chat_commands/presenters/issue_new.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/chat_commands/presenters/issue_new.rb b/lib/gitlab/chat_commands/presenters/issue_new.rb index 6e88e0574a3..a1a3add56c9 100644 --- a/lib/gitlab/chat_commands/presenters/issue_new.rb +++ b/lib/gitlab/chat_commands/presenters/issue_new.rb @@ -34,11 +34,11 @@ module Gitlab end def pretext - "I opened an issue on behalf on #{author_profile_link}: *#{@resource.to_reference}* from #{project.name_with_namespace}" + "I created an issue on #{author_profile_link}'s behalf: **#{@resource.to_reference}** in #{project_link}" end def project_link - "[#{project.name_with_namespace}](#{url_for(project)})" + "[#{project.name_with_namespace}](#{projects_url(project)})" end def author_profile_link -- cgit v1.2.1 From f728589e6714be2367391bbf398d228a73d674d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Wed, 25 Jan 2017 18:21:37 +0100 Subject: Document that the retro and kickoff notes are public MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [ci skip] Signed-off-by: Rémy Coutable <remy@rymai.me> --- CONTRIBUTING.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d404f1b91df..8d1f3d3f926 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,6 +88,27 @@ contributing to GitLab. Please see the [UX Guide for GitLab]. +## Release retrospective and kickoff + +### Retrospective + +After each release (usually on the 22nd of each month), we have a retrospective +call where we discuss what went well, what went wrong, and what we can improve +for the next release. The [retrospective notes] are public and you are invited +to comment them. +If you're interested, you can even join the [retrospective call][retro-kickoff-call]. + +### Kickoff + +Before working on the next release (usually on the 8th of each month), we have a +kickoff call to explain what we expect to ship in the next release. The +[kickoff notes] are public and you are invited to comment them. +If you're interested, you can even join the [kickoff call][retro-kickoff-call]. + +[retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing +[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing +[retro-kickoff-call]: https://gitlab.zoom.us/j/918821206 + ## Issue tracker To get support for your particular problem please use the -- cgit v1.2.1 From a1a5dd4b5c9abac53bb0af6f2bd74dc1159cef8d Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 31 Jan 2017 16:10:16 +0100 Subject: add spec replicating validation error --- .../gitlab/import_export/members_mapper_spec.rb | 28 ++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 0b7984d6ca9..495ca4b4955 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -92,5 +92,33 @@ describe Gitlab::ImportExport::MembersMapper, services: true do expect(members_mapper.map[exported_user_id]).to eq(user2.id) end end + + context 'importer same as group member' do + let(:user2) { create(:admin, authorized_projects_populated: true) } + let(:group) { create(:group) } + let(:project) { create(:empty_project, :public, name: 'searchable_project', namespace: group) } + let(:members_mapper) do + described_class.new( + exported_members: exported_members, user: user2, project: project) + end + + before do + GroupMember.add_users_to_group( + group, + [user, user2], + GroupMember::DEVELOPER + ) + end + + it 'maps the project member' do + expect(members_mapper.map[exported_user_id]).to eq(user2.id) + end + + it 'maps the project member if it already exists' do + ProjectMember.create!(user: user2, access_level: ProjectMember::MASTER, source_id: project.id) + + expect(members_mapper.map[exported_user_id]).to eq(user2.id) + end + end end end -- cgit v1.2.1 From 918eaba0c3898b5b81d2db1cd598fd3fecac9ef8 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 31 Jan 2017 16:32:26 +0100 Subject: remove old project members from project --- lib/gitlab/import_export/members_mapper.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index 2405b94db50..6189867dc9c 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -41,6 +41,10 @@ module Gitlab end def ensure_default_member! + @project.project_members.each do |member| + member.destroy + end + ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true) end -- cgit v1.2.1 From dc58df4160726b989aff0d4d80443b9f62c6ed56 Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 31 Jan 2017 16:33:02 +0100 Subject: add changelog --- changelogs/unreleased/fix-import-user-validation-error.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-import-user-validation-error.yml diff --git a/changelogs/unreleased/fix-import-user-validation-error.yml b/changelogs/unreleased/fix-import-user-validation-error.yml new file mode 100644 index 00000000000..985a3b0b26f --- /dev/null +++ b/changelogs/unreleased/fix-import-user-validation-error.yml @@ -0,0 +1,4 @@ +--- +title: Remove old project members when retrying an export +merge_request: +author: -- cgit v1.2.1 From 87346bfe6de362b832e27c75f3e9b1def2ef1f11 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Tue, 31 Jan 2017 10:04:29 -0600 Subject: Remove settings cog from within admin scroll tabs; keep links centered --- app/assets/stylesheets/framework/nav.scss | 1 - app/views/layouts/nav/_admin.html.haml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss index adbc141262e..fd081c2d7e1 100644 --- a/app/assets/stylesheets/framework/nav.scss +++ b/app/assets/stylesheets/framework/nav.scss @@ -297,7 +297,6 @@ .nav-control { @media (max-width: $screen-sm-max) { - text-align: left; margin-right: 75px; } } diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml index ac04f57e217..19a947af4ca 100644 --- a/app/views/layouts/nav/_admin.html.haml +++ b/app/views/layouts/nav/_admin.html.haml @@ -1,5 +1,5 @@ += render 'layouts/nav/admin_settings' .scrolling-tabs-container{ class: nav_control_class } - = render 'layouts/nav/admin_settings' .fade-left = icon('angle-left') .fade-right -- cgit v1.2.1 From 89a2438ab44862b34ba1030761c27b37059389ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Tue, 31 Jan 2017 17:37:31 +0100 Subject: Fix wrong call to ProjectCacheWorker.perform MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It's either ProjectCacheWorker#perform or ProjectCacheWorker.perform_async! Signed-off-by: Rémy Coutable <remy@rymai.me> --- .../27516-fix-wrong-call-to-project_cache_worker-method.yml | 4 ++++ lib/tasks/gitlab/import.rake | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml diff --git a/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml new file mode 100644 index 00000000000..bc990c66866 --- /dev/null +++ b/changelogs/unreleased/27516-fix-wrong-call-to-project_cache_worker-method.yml @@ -0,0 +1,4 @@ +--- +title: Fix wrong call to ProjectCacheWorker.perform +merge_request: 8910 +author: diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index a2eca74a3c8..036a9307ab5 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -63,7 +63,7 @@ namespace :gitlab do if project.persisted? puts " * Created #{project.name} (#{repo_path})".color(:green) - ProjectCacheWorker.perform(project.id) + ProjectCacheWorker.perform_async(project.id) else puts " * Failed trying to create #{project.name} (#{repo_path})".color(:red) puts " Errors: #{project.errors.messages}".color(:red) -- cgit v1.2.1 From 8ea1dafe83da0b018dcc413242194749afd0e05a Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 31 Jan 2017 19:20:35 +0100 Subject: use destroy_all --- lib/gitlab/import_export/members_mapper.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb index 6189867dc9c..a09577ae48d 100644 --- a/lib/gitlab/import_export/members_mapper.rb +++ b/lib/gitlab/import_export/members_mapper.rb @@ -41,9 +41,7 @@ module Gitlab end def ensure_default_member! - @project.project_members.each do |member| - member.destroy - end + @project.project_members.destroy_all ProjectMember.create!(user: @user, access_level: ProjectMember::MASTER, source_id: @project.id, importing: true) end -- cgit v1.2.1 From 7bf6df8463c4f8871682f385e9368d169b4ffecf Mon Sep 17 00:00:00 2001 From: Brian Hall <brian@hack.design> Date: Mon, 30 Jan 2017 22:12:31 -0600 Subject: Change the reply shortcut to focus the field even without a selection. --- app/assets/javascripts/lib/utils/common_utils.js.es6 | 1 + app/assets/javascripts/shortcuts_issuable.js | 9 ++++++--- changelogs/unreleased/empty-selection-reply-shortcut.yml | 4 ++++ spec/javascripts/shortcuts_issuable_spec.js | 12 ++++++++++-- 4 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/empty-selection-reply-shortcut.yml diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 51993bb3420..e3bff2559fd 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -162,6 +162,7 @@ w.gl.utils.getSelectedFragment = () => { const selection = window.getSelection(); + if (selection.rangeCount === 0) return null; const documentFragment = selection.getRangeAt(0).cloneContents(); if (documentFragment.textContent.length === 0) return null; diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 4ef516af8c8..4dcc5ebe28f 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -39,17 +39,20 @@ } ShortcutsIssuable.prototype.replyWithSelectedText = function() { - var quote, replyField, documentFragment, selected, separator; + var quote, documentFragment, selected, separator; + var replyField = $('.js-main-target-form #note_note'); documentFragment = window.gl.utils.getSelectedFragment(); - if (!documentFragment) return; + if (!documentFragment) { + replyField.focus(); + return; + } // If the documentFragment contains more than just Markdown, don't copy as GFM. if (documentFragment.querySelector('.md, .wiki')) return; selected = window.gl.CopyAsGFM.nodeToGFM(documentFragment); - replyField = $('.js-main-target-form #note_note'); if (selected.trim() === "") { return; } diff --git a/changelogs/unreleased/empty-selection-reply-shortcut.yml b/changelogs/unreleased/empty-selection-reply-shortcut.yml new file mode 100644 index 00000000000..5a42c98a800 --- /dev/null +++ b/changelogs/unreleased/empty-selection-reply-shortcut.yml @@ -0,0 +1,4 @@ +--- +title: Change the reply shortcut to focus the field even without a selection. +merge_request: 8873 +author: Brian Hall diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index 386fc8f514e..db2302c4fb0 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -27,11 +27,19 @@ return this.selector = 'form.js-main-target-form textarea#note_note'; }); describe('with empty selection', function() { - return it('does nothing', function() { - stubSelection(''); + it('does not return an error', function() { this.shortcut.replyWithSelectedText(); return expect($(this.selector).val()).toBe(''); }); + return it('triggers `input`', function() { + var focused; + focused = false; + $(this.selector).on('focus', function() { + return focused = true; + }); + this.shortcut.replyWithSelectedText(); + return expect(focused).toBe(true); + }); }); describe('with any selection', function() { beforeEach(function() { -- cgit v1.2.1 From c6aed2dfc8f879a9edf611235cae3760f3af3d4e Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 31 Jan 2017 20:06:05 +0100 Subject: update spec --- spec/lib/gitlab/import_export/members_mapper_spec.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb index 495ca4b4955..f2cb028206f 100644 --- a/spec/lib/gitlab/import_export/members_mapper_spec.rb +++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb @@ -103,11 +103,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do end before do - GroupMember.add_users_to_group( - group, - [user, user2], - GroupMember::DEVELOPER - ) + group.add_users([user, user2], GroupMember::DEVELOPER) end it 'maps the project member' do @@ -115,7 +111,7 @@ describe Gitlab::ImportExport::MembersMapper, services: true do end it 'maps the project member if it already exists' do - ProjectMember.create!(user: user2, access_level: ProjectMember::MASTER, source_id: project.id) + project.add_master(user2) expect(members_mapper.map[exported_user_id]).to eq(user2.id) end -- cgit v1.2.1 From 6fffdf7f2e72128eac53adfd734deb4fad8421fa Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Tue, 31 Jan 2017 15:28:14 -0600 Subject: remove dev-server config from development environment --- config/environments/development.rb | 7 ------- 1 file changed, 7 deletions(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index f8cf196bc7c..45a8c1add3e 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,6 +1,4 @@ Rails.application.configure do - WEBPACK_DEV_PORT = `cat ../webpack_port 2>/dev/null || echo '3808'`.to_i - # Settings specified here will take precedence over those in config/application.rb # In the development environment your application's code is reloaded on @@ -24,11 +22,6 @@ Rails.application.configure do # Only use best-standards-support built into browsers config.action_dispatch.best_standards_support = :builtin - # Enable webpack dev server - config.webpack.dev_server.enabled = true - config.webpack.dev_server.port = WEBPACK_DEV_PORT - config.webpack.dev_server.manifest_port = WEBPACK_DEV_PORT - # Do not compress assets config.assets.compress = false -- cgit v1.2.1 From 120f9abaa15ce0feec1dc457ad3dc3787e4fbfc6 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 3 Nov 2015 21:28:07 +0100 Subject: Add GitLab Pages - The pages are created when build artifacts for `pages` job are uploaded - Pages serve the content under: http://group.pages.domain.com/project - Pages can be used to serve the group page, special project named as host: group.pages.domain.com - User can provide own 403 and 404 error pages by creating 403.html and 404.html in group page project - Pages can be explicitly removed from the project by clicking Remove Pages in Project Settings - The size of pages is limited by Application Setting: max pages size, which limits the maximum size of unpacked archive (default: 100MB) - The public/ is extracted from artifacts and content is served as static pages - Pages asynchronous worker use `dd` to limit the unpacked tar size - Pages needs to be explicitly enabled and domain needs to be specified in gitlab.yml - Pages are part of backups - Pages notify the deployment status using Commit Status API - Pages use a new sidekiq queue: pages - Pages use a separate nginx config which needs to be explicitly added --- .../admin/application_settings_controller.rb | 1 + app/controllers/projects_controller.rb | 10 ++ app/models/ci/build.rb | 3 +- app/models/project.rb | 25 +++++ app/policies/project_policy.rb | 1 + app/services/update_pages_service.rb | 15 +++ .../admin/application_settings/_form.html.haml | 8 ++ app/views/projects/edit.html.haml | 35 ++++++ app/workers/pages_worker.rb | 123 +++++++++++++++++++++ config/gitlab.yml.example | 11 ++ config/initializers/1_settings.rb | 6 + config/routes/project.rb | 1 + ...32013_add_pages_size_to_application_settings.rb | 5 + db/schema.rb | 1 + doc/README.md | 1 + doc/install/installation.md | 13 +++ doc/pages/README.md | 12 ++ lib/backup/pages.rb | 13 +++ lib/support/nginx/gitlab-pages | 27 +++++ lib/tasks/gitlab/backup.rake | 21 ++++ shared/pages/.gitkeep | 0 spec/fixtures/pages.tar.gz | Bin 0 -> 1795 bytes spec/fixtures/pages_empty.tar.gz | Bin 0 -> 128 bytes spec/services/update_pages_service_spec.rb | 43 +++++++ spec/tasks/gitlab/backup_rake_spec.rb | 14 ++- spec/workers/pages_worker_spec.rb | 58 ++++++++++ 26 files changed, 441 insertions(+), 6 deletions(-) create mode 100644 app/services/update_pages_service.rb create mode 100644 app/workers/pages_worker.rb create mode 100644 db/migrate/20151215132013_add_pages_size_to_application_settings.rb create mode 100644 doc/pages/README.md create mode 100644 lib/backup/pages.rb create mode 100644 lib/support/nginx/gitlab-pages create mode 100644 shared/pages/.gitkeep create mode 100644 spec/fixtures/pages.tar.gz create mode 100644 spec/fixtures/pages_empty.tar.gz create mode 100644 spec/services/update_pages_service_spec.rb create mode 100644 spec/workers/pages_worker_spec.rb diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 543d5eac504..df8682e246e 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -109,6 +109,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :plantuml_url, :max_artifacts_size, :max_attachment_size, + :max_pages_size, :metrics_enabled, :metrics_host, :metrics_method_call_threshold, diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 444ff837bb3..123dc179e73 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -151,6 +151,16 @@ class ProjectsController < Projects::ApplicationController end end + def remove_pages + return access_denied! unless can?(current_user, :remove_pages, @project) + + @project.remove_pages + + respond_to do |format| + format.html { redirect_to project_path(@project) } + end + end + def housekeeping ::Projects::HousekeepingService.new(@project).execute diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 5fe8ddf69d7..095a346f337 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -256,7 +256,7 @@ module Ci end def project_id - pipeline.project_id + gl_project_id end def project_name @@ -457,6 +457,7 @@ module Ci build_data = Gitlab::DataBuilder::Build.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) + UpdatePagesService.new(build_data).execute project.running_or_pending_build_count(force: true) end diff --git a/app/models/project.rb b/app/models/project.rb index 37f4705adbd..48ff5ec7fc7 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -53,6 +53,8 @@ class Project < ActiveRecord::Base update_column(:last_activity_at, self.created_at) end + after_destroy :remove_pages + # update visibility_level of forks after_update :update_forks_visibility_level def update_forks_visibility_level @@ -1160,6 +1162,29 @@ class Project < ActiveRecord::Base ensure_runners_token! end + def pages_url + if Dir.exist?(public_pages_path) + host = "#{namespace.path}.#{Settings.pages.domain}" + + # If the project path is the same as host, leave the short version + return "http://#{host}" if host == path + + "http://#{host}/#{path}" + end + end + + def pages_path + File.join(Settings.pages.path, path_with_namespace) + end + + def public_pages_path + File.join(pages_path, 'public') + end + + def remove_pages + FileUtils.rm_r(pages_path, force: true) + end + def wiki @wiki ||= ProjectWiki.new(self, self.owner) end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 71ef8901932..63bc639688d 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -136,6 +136,7 @@ class ProjectPolicy < BasePolicy can! :remove_fork_project can! :destroy_merge_request can! :destroy_issue + can! :remove_pages end def team_member_owner_access! diff --git a/app/services/update_pages_service.rb b/app/services/update_pages_service.rb new file mode 100644 index 00000000000..818bb94a293 --- /dev/null +++ b/app/services/update_pages_service.rb @@ -0,0 +1,15 @@ +class UpdatePagesService + attr_reader :data + + def initialize(data) + @data = data + end + + def execute + return unless Settings.pages.enabled + return unless data[:build_name] == 'pages' + return unless data[:build_status] == 'success' + + PagesWorker.perform_async(data[:build_id]) + end +end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 558bbe07b16..125a805a897 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -186,6 +186,14 @@ = f.text_area :help_page_text, class: 'form-control', rows: 4 .help-block Markdown enabled + %fieldset + %legend Pages + .form-group + = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :max_pages_size, class: 'form-control' + .help-block Zero for unlimited + %fieldset %legend Continuous Integration .form-group diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index ec944d4ffb7..89e2d4046b8 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -133,6 +133,41 @@ %hr = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" = f.submit 'Save changes', class: "btn btn-save" + + - if Settings.pages.enabled + .pages-settings + .panel.panel-default + .panel-heading Pages + .errors-holder + .panel-body + - if @project.pages_url + %strong + Congratulations. Your pages are served at: + %p= link_to @project.pages_url, @project.pages_url + - else + %p + To publish pages create .gitlab-ci.yml with + %strong pages job + and send public/ folder to GitLab. + %p + Use existing tools: + %ul + %li + %pre + :plain + pages: + image: jekyll + script: jekyll build + artifacts: + paths: + - public + + - if @project.pages_url && can?(current_user, :remove_pages, @project) + .form-actions + = link_to 'Remove pages', remove_pages_namespace_project_path(@project.namespace, @project), + data: { confirm: "Are you sure that you want to remove pages for this project?" }, + method: :post, class: "btn btn-warning" + .row.prepend-top-default %hr .row.prepend-top-default diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb new file mode 100644 index 00000000000..9aa3030264b --- /dev/null +++ b/app/workers/pages_worker.rb @@ -0,0 +1,123 @@ +class PagesWorker + include Sidekiq::Worker + include Gitlab::CurrentSettings + + BLOCK_SIZE = 32.kilobytes + MAX_SIZE = 1.terabyte + + sidekiq_options queue: :pages + + def perform(build_id) + @build_id = build_id + return unless valid? + + # Create status notifying the deployment of pages + @status = GenericCommitStatus.new( + project: project, + commit: build.commit, + user: build.user, + ref: build.ref, + stage: 'deploy', + name: 'pages:deploy' + ) + @status.run! + + FileUtils.mkdir_p(tmp_path) + + # Calculate dd parameters: we limit the size of pages + max_size = current_application_settings.max_pages_size.megabytes + max_size ||= MAX_SIZE + blocks = 1 + max_size / BLOCK_SIZE + + # Create temporary directory in which we will extract the artifacts + Dir.mktmpdir(nil, tmp_path) do |temp_path| + # We manually extract the archive and limit the archive size with dd + results = Open3.pipeline(%W(gunzip -c #{artifacts}), + %W(dd bs=#{BLOCK_SIZE} count=#{blocks}), + %W(tar -x -C #{temp_path} public/)) + return unless results.compact.all?(&:success?) + + # Check if we did extract public directory + temp_public_path = File.join(temp_path, 'public') + return unless Dir.exists?(temp_public_path) + + FileUtils.mkdir_p(pages_path) + + # Lock file for time of deployment to prevent the two processes from doing the concurrent deployment + File.open(lock_path, File::RDWR|File::CREAT, 0644) do |f| + f.flock(File::LOCK_EX) + return unless valid? + + # Do atomic move of pages + # Move and removal may not be atomic, but they are significantly faster then extracting and removal + # 1. We move deployed public to previous public path (file removal is slow) + # 2. We move temporary public to be deployed public + # 3. We remove previous public path + if File.exists?(public_path) + FileUtils.move(public_path, previous_public_path) + end + FileUtils.move(temp_public_path, public_path) + end + + if File.exists?(previous_public_path) + FileUtils.rm_r(previous_public_path, force: true) + end + + @status.success + end + ensure + @status.drop if @status && @status.active? + end + + private + + def valid? + # check if sha for the ref is still the most recent one + # this helps in case when multiple deployments happens + build && build.artifacts_file? && sha == latest_sha + end + + def build + @build ||= Ci::Build.find_by(id: @build_id) + end + + def project + @project ||= build.project + end + + def tmp_path + @tmp_path ||= File.join(Settings.pages.path, 'tmp') + end + + def pages_path + @pages_path ||= project.pages_path + end + + def public_path + @public_path ||= File.join(pages_path, 'public') + end + + def previous_public_path + @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}") + end + + def lock_path + @lock_path ||= File.join(pages_path, 'deploy.lock') + end + + def ref + build.ref + end + + def artifacts + build.artifacts_file.path + end + + def latest_sha + project.commit(build.ref).try(:sha).to_s + end + + def sha + build.sha + end +end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 42e5f105d46..d41280624ae 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -153,6 +153,17 @@ production: &base # The location where LFS objects are stored (default: shared/lfs-objects). # storage_path: shared/lfs-objects + ## GitLab Pages + pages: + enabled: false + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + # The domain under which the pages are served: + # http://group.example.com/project + # or project path can be a group page: group.example.com + domain: example.com + ## Mattermost ## For enabling Add to Mattermost button mattermost: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 4f33aad8693..0c06b73ba36 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -254,6 +254,12 @@ Settings.registry['issuer'] ||= nil Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].compact.join(':') Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root) +# Pages +Settings['pages'] ||= Settingslogic.new({}) +Settings.pages['enabled'] = false if Settings.pages['enabled'].nil? +Settings.pages['path'] = File.expand_path('shared/pages/', Rails.root) +Settings.pages['domain'] ||= "example.com" + # # Git LFS # diff --git a/config/routes/project.rb b/config/routes/project.rb index f36febc6e04..cd56f6281f5 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -329,6 +329,7 @@ constraints(ProjectUrlConstrainer.new) do post :archive post :unarchive post :housekeeping + post :remove_pages post :toggle_star post :preview_markdown post :export diff --git a/db/migrate/20151215132013_add_pages_size_to_application_settings.rb b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb new file mode 100644 index 00000000000..e7fb73190e8 --- /dev/null +++ b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb @@ -0,0 +1,5 @@ +class AddPagesSizeToApplicationSettings < ActiveRecord::Migration + def up + add_column :application_settings, :max_pages_size, :integer, default: 100, null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index 5efb4f6595c..15f378b28ff 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -61,6 +61,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.boolean "shared_runners_enabled", default: true, null: false t.integer "max_artifacts_size", default: 100, null: false t.string "runners_registration_token" + t.integer "max_pages_size", default: 100, null: false t.boolean "require_two_factor_authentication", default: false t.integer "two_factor_grace_period", default: 48 t.boolean "metrics_enabled", default: false diff --git a/doc/README.md b/doc/README.md index 909740211a6..f036febb7b9 100644 --- a/doc/README.md +++ b/doc/README.md @@ -12,6 +12,7 @@ - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry. - [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. +- [GitLab Pages](pages/README.md) Using GitLab Pages. - [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab. - [Importing and exporting projects between instances](user/project/settings/import_export.md). - [Markdown](user/markdown.md) GitLab's advanced formatting system. diff --git a/doc/install/installation.md b/doc/install/installation.md index 425c5d93efb..c78e469055d 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -313,6 +313,9 @@ sudo usermod -aG redis git # Change the permissions of the directory where CI artifacts are stored sudo chmod -R u+rwX shared/artifacts/ + # Change the permissions of the directory where CI artifacts are stored + sudo chmod -R ug+rwX shared/pages/ + # Copy the example Unicorn config sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb @@ -484,6 +487,16 @@ Make sure to edit the config file to match your setup. Also, ensure that you mat # or else sudo rm -f /etc/nginx/sites-enabled/default sudo editor /etc/nginx/sites-available/gitlab +Copy the GitLab pages site config: + + sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages + sudo ln -s /etc/nginx/sites-available/gitlab-pages /etc/nginx/sites-enabled/gitlab-pages + + # Change YOUR_GITLAB_PAGES\.DOMAIN to the fully-qualified + # domain name under which the pages will be served. + # The . (dot) replace with \. (backslash+dot) + sudo editor /etc/nginx/sites-available/gitlab-pages + **Note:** If you want to use HTTPS, replace the `gitlab` Nginx config with `gitlab-ssl`. See [Using HTTPS](#using-https) for HTTPS configuration details. ### Test Configuration diff --git a/doc/pages/README.md b/doc/pages/README.md new file mode 100644 index 00000000000..d08c34c3ebc --- /dev/null +++ b/doc/pages/README.md @@ -0,0 +1,12 @@ +# GitLab Pages + +To start using GitLab Pages add to your project .gitlab-ci.yml with special pages job. + + pages: + image: jekyll + script: jekyll build + artifacts: + paths: + - public + +TODO diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb new file mode 100644 index 00000000000..215ded93bfe --- /dev/null +++ b/lib/backup/pages.rb @@ -0,0 +1,13 @@ +require 'backup/files' + +module Backup + class Pages < Files + def initialize + super('pages', Gitlab.config.pages.path) + end + + def create_files_dir + Dir.mkdir(app_files_dir, 0700) + end + end +end diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages new file mode 100644 index 00000000000..0eeb0cd1917 --- /dev/null +++ b/lib/support/nginx/gitlab-pages @@ -0,0 +1,27 @@ +## Pages serving host +server { + listen 0.0.0.0:80; + listen [::]:80 ipv6only=on; + + ## Replace this with something like pages.gitlab.com + server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; + root /home/git/gitlab/shared/pages/${group}; + + ## Individual nginx logs for GitLab pages + access_log /var/log/nginx/gitlab_pages_access.log; + error_log /var/log/nginx/gitlab_pages_error.log; + + # 1. Try to get /project/ to => shared/pages/${group}/public/ or index.html + # 2. Try to get / to => shared/pages/${group}/${host}/public/ or index.html + location ~ ^/([^/]*)(/.*)?$ { + try_files "/$1/public$2" + "/$1/public$2/index.html" + "/${host}/public/${uri}" + "/${host}/public/${uri}/index.html" + =404; + } + + # Define custom error pages + error_page 403 /403.html; + error_page 404 /404.html; +} diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake index a9f1255e8cf..ffab6f492fb 100644 --- a/lib/tasks/gitlab/backup.rake +++ b/lib/tasks/gitlab/backup.rake @@ -13,6 +13,7 @@ namespace :gitlab do Rake::Task["gitlab:backup:uploads:create"].invoke Rake::Task["gitlab:backup:builds:create"].invoke Rake::Task["gitlab:backup:artifacts:create"].invoke + Rake::Task["gitlab:backup:pages:create"].invoke Rake::Task["gitlab:backup:lfs:create"].invoke Rake::Task["gitlab:backup:registry:create"].invoke @@ -56,6 +57,7 @@ namespace :gitlab do Rake::Task['gitlab:backup:uploads:restore'].invoke unless backup.skipped?('uploads') Rake::Task['gitlab:backup:builds:restore'].invoke unless backup.skipped?('builds') Rake::Task['gitlab:backup:artifacts:restore'].invoke unless backup.skipped?('artifacts') + Rake::Task["gitlab:backup:pages:restore"].invoke unless backup.skipped?("pages") Rake::Task['gitlab:backup:lfs:restore'].invoke unless backup.skipped?('lfs') Rake::Task['gitlab:backup:registry:restore'].invoke unless backup.skipped?('registry') Rake::Task['gitlab:shell:setup'].invoke @@ -159,6 +161,25 @@ namespace :gitlab do end end + namespace :pages do + task create: :environment do + $progress.puts "Dumping pages ... ".blue + + if ENV["SKIP"] && ENV["SKIP"].include?("pages") + $progress.puts "[SKIPPED]".cyan + else + Backup::Pages.new.dump + $progress.puts "done".green + end + end + + task restore: :environment do + $progress.puts "Restoring pages ... ".blue + Backup::Pages.new.restore + $progress.puts "done".green + end + end + namespace :lfs do task create: :environment do $progress.puts "Dumping lfs objects ... ".color(:blue) diff --git a/shared/pages/.gitkeep b/shared/pages/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/spec/fixtures/pages.tar.gz b/spec/fixtures/pages.tar.gz new file mode 100644 index 00000000000..d0e89378b3e Binary files /dev/null and b/spec/fixtures/pages.tar.gz differ diff --git a/spec/fixtures/pages_empty.tar.gz b/spec/fixtures/pages_empty.tar.gz new file mode 100644 index 00000000000..5c2afa1a8f6 Binary files /dev/null and b/spec/fixtures/pages_empty.tar.gz differ diff --git a/spec/services/update_pages_service_spec.rb b/spec/services/update_pages_service_spec.rb new file mode 100644 index 00000000000..ed392cd94ee --- /dev/null +++ b/spec/services/update_pages_service_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe UpdatePagesService, services: true do + let(:build) { create(:ci_build) } + let(:data) { Gitlab::BuildDataBuilder.build(build) } + let(:service) { UpdatePagesService.new(data) } + + context 'execute asynchronously for pages job' do + before { build.name = 'pages' } + + context 'on success' do + before { build.success } + + it 'should execute worker' do + expect(PagesWorker).to receive(:perform_async) + service.execute + end + end + + %w(pending running failed canceled).each do |status| + context "on #{status}" do + before { build.status = status } + + it 'should not execute worker' do + expect(PagesWorker).to_not receive(:perform_async) + service.execute + end + end + end + end + + context 'for other jobs' do + before do + build.name = 'other job' + build.success + end + + it 'should not execute worker' do + expect(PagesWorker).to_not receive(:perform_async) + service.execute + end + end +end diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index bc751d20ce1..df8a47893f9 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -28,7 +28,7 @@ describe 'gitlab:app namespace rake task' do end def reenable_backup_sub_tasks - %w{db repo uploads builds artifacts lfs registry}.each do |subtask| + %w{db repo uploads builds artifacts pages lfs registry}.each do |subtask| Rake::Task["gitlab:backup:#{subtask}:create"].reenable end end @@ -71,6 +71,7 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task['gitlab:backup:builds:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:uploads:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive(:invoke) + expect(Rake::Task['gitlab:backup:pages:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:lfs:restore']).to receive(:invoke) expect(Rake::Task['gitlab:backup:registry:restore']).to receive(:invoke) expect(Rake::Task['gitlab:shell:setup']).to receive(:invoke) @@ -202,7 +203,7 @@ describe 'gitlab:app namespace rake task' do it 'sets correct permissions on the tar contents' do tar_contents, exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz} ) expect(exit_status).to eq(0) expect(tar_contents).to match('db/') @@ -210,14 +211,15 @@ describe 'gitlab:app namespace rake task' do expect(tar_contents).to match('repositories/') expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('artifacts.tar.gz') + expect(tar_contents).to match('pages.tar.gz') expect(tar_contents).to match('lfs.tar.gz') expect(tar_contents).to match('registry.tar.gz') - expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/) + expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/) end it 'deletes temp directories' do temp_dirs = Dir.glob( - File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs,registry}') + File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,pages,lfs,registry}') ) expect(temp_dirs).to be_empty @@ -304,7 +306,7 @@ describe 'gitlab:app namespace rake task' do it "does not contain skipped item" do tar_contents, _exit_status = Gitlab::Popen.popen( - %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz registry.tar.gz} + %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz pages.tar.gz lfs.tar.gz registry.tar.gz} ) expect(tar_contents).to match('db/') @@ -312,6 +314,7 @@ describe 'gitlab:app namespace rake task' do expect(tar_contents).to match('builds.tar.gz') expect(tar_contents).to match('artifacts.tar.gz') expect(tar_contents).to match('lfs.tar.gz') + expect(tar_contents).to match('pages.tar.gz') expect(tar_contents).to match('registry.tar.gz') expect(tar_contents).not_to match('repositories/') end @@ -327,6 +330,7 @@ describe 'gitlab:app namespace rake task' do expect(Rake::Task['gitlab:backup:uploads:restore']).not_to receive :invoke expect(Rake::Task['gitlab:backup:builds:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:artifacts:restore']).to receive :invoke + expect(Rake::Task['gitlab:backup:pages:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:lfs:restore']).to receive :invoke expect(Rake::Task['gitlab:backup:registry:restore']).to receive :invoke expect(Rake::Task['gitlab:shell:setup']).to receive :invoke diff --git a/spec/workers/pages_worker_spec.rb b/spec/workers/pages_worker_spec.rb new file mode 100644 index 00000000000..158a4b3ba8d --- /dev/null +++ b/spec/workers/pages_worker_spec.rb @@ -0,0 +1,58 @@ +require "spec_helper" + +describe PagesWorker do + let(:project) { create :project } + let(:commit) { create :ci_commit, project: project, sha: project.commit('HEAD').sha } + let(:build) { create :ci_build, commit: commit, ref: 'HEAD' } + let(:worker) { PagesWorker.new } + let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/pages.tar.gz', 'application/octet-stream') } + let(:empty_file) { fixture_file_upload(Rails.root + 'spec/fixtures/pages_empty.tar.gz', 'application/octet-stream') } + let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'application/octet-stream') } + + before do + project.remove_pages + end + + context 'for valid file' do + before { build.update_attributes(artifacts_file: file) } + + it 'succeeds' do + expect(project.pages_url).to be_nil + expect(worker.perform(build.id)).to be_truthy + expect(project.pages_url).to_not be_nil + end + + it 'limits pages size' do + stub_application_setting(max_pages_size: 1) + expect(worker.perform(build.id)).to_not be_truthy + end + + it 'removes pages after destroy' do + expect(project.pages_url).to be_nil + expect(worker.perform(build.id)).to be_truthy + expect(project.pages_url).to_not be_nil + project.destroy + expect(Dir.exist?(project.public_pages_path)).to be_falsey + end + end + + it 'fails if no artifacts' do + expect(worker.perform(build.id)).to_not be_truthy + end + + it 'fails for empty file fails' do + build.update_attributes(artifacts_file: empty_file) + expect(worker.perform(build.id)).to_not be_truthy + end + + it 'fails for invalid archive' do + build.update_attributes(artifacts_file: invalid_file) + expect(worker.perform(build.id)).to_not be_truthy + end + + it 'fails if sha on branch is not latest' do + commit.update_attributes(sha: 'old_sha') + build.update_attributes(artifacts_file: file) + expect(worker.perform(build.id)).to_not be_truthy + end +end -- cgit v1.2.1 From 732a821d4f00f9812d014b6c58eae2f9a18f7ddd Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 15 Dec 2015 22:48:23 +0100 Subject: Fix specs --- app/workers/pages_worker.rb | 3 ++- config/sidekiq_queues.yml | 1 + lib/backup/manager.rb | 2 +- spec/services/update_pages_service_spec.rb | 4 ++++ 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index 9aa3030264b..c51ec81c9da 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -34,7 +34,8 @@ class PagesWorker # We manually extract the archive and limit the archive size with dd results = Open3.pipeline(%W(gunzip -c #{artifacts}), %W(dd bs=#{BLOCK_SIZE} count=#{blocks}), - %W(tar -x -C #{temp_path} public/)) + %W(tar -x -C #{temp_path} public/), + err: '/dev/null') return unless results.compact.all?(&:success?) # Check if we did extract public directory diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 022b0e80917..56bf4e6b1de 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -50,3 +50,4 @@ - [reactive_caching, 1] - [cronjob, 1] - [default, 1] + - [pages, 1] diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index cefbfdce3bb..f099c0651ac 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -1,6 +1,6 @@ module Backup class Manager - ARCHIVES_TO_BACKUP = %w[uploads builds artifacts lfs registry] + ARCHIVES_TO_BACKUP = %w[uploads builds artifacts pages lfs registry] FOLDERS_TO_BACKUP = %w[repositories db] FILE_NAME_SUFFIX = '_gitlab_backup.tar' diff --git a/spec/services/update_pages_service_spec.rb b/spec/services/update_pages_service_spec.rb index ed392cd94ee..cf1ca15da44 100644 --- a/spec/services/update_pages_service_spec.rb +++ b/spec/services/update_pages_service_spec.rb @@ -5,6 +5,10 @@ describe UpdatePagesService, services: true do let(:data) { Gitlab::BuildDataBuilder.build(build) } let(:service) { UpdatePagesService.new(data) } + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + end + context 'execute asynchronously for pages job' do before { build.name = 'pages' } -- cgit v1.2.1 From ac09f857cd9edd4a18280f617b48fe436109ceaa Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 16 Dec 2015 12:53:35 +0100 Subject: Remove locking and add force to FileUtils methods --- app/workers/pages_worker.rb | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index c51ec81c9da..59f4b4f16f4 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -5,7 +5,7 @@ class PagesWorker BLOCK_SIZE = 32.kilobytes MAX_SIZE = 1.terabyte - sidekiq_options queue: :pages + sidekiq_options queue: :pages, retry: false def perform(build_id) @build_id = build_id @@ -44,25 +44,17 @@ class PagesWorker FileUtils.mkdir_p(pages_path) - # Lock file for time of deployment to prevent the two processes from doing the concurrent deployment - File.open(lock_path, File::RDWR|File::CREAT, 0644) do |f| - f.flock(File::LOCK_EX) - return unless valid? - - # Do atomic move of pages - # Move and removal may not be atomic, but they are significantly faster then extracting and removal - # 1. We move deployed public to previous public path (file removal is slow) - # 2. We move temporary public to be deployed public - # 3. We remove previous public path - if File.exists?(public_path) - FileUtils.move(public_path, previous_public_path) - end - FileUtils.move(temp_public_path, public_path) - end - - if File.exists?(previous_public_path) - FileUtils.rm_r(previous_public_path, force: true) - end + # Ignore deployment if the HEAD changed when we were extracting the archive + return unless valid? + + # Do atomic move of pages + # Move and removal may not be atomic, but they are significantly faster then extracting and removal + # 1. We move deployed public to previous public path (file removal is slow) + # 2. We move temporary public to be deployed public + # 3. We remove previous public path + FileUtils.move(public_path, previous_public_path, force: true) + FileUtils.move(temp_public_path, public_path) + FileUtils.rm_r(previous_public_path, force: true) @status.success end -- cgit v1.2.1 From fa68e403e0d5539b19ceb5396394d634babdc2b9 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 16 Dec 2015 12:53:54 +0100 Subject: Support https and custom port for pages --- app/models/project.rb | 7 +++++-- config/gitlab.yml.example | 2 ++ config/initializers/1_settings.rb | 28 +++++++++++++++++++--------- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 48ff5ec7fc7..aa16b055e81 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1165,11 +1165,14 @@ class Project < ActiveRecord::Base def pages_url if Dir.exist?(public_pages_path) host = "#{namespace.path}.#{Settings.pages.domain}" + url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix| + "#{prefix}#{namespace.path}." + end # If the project path is the same as host, leave the short version - return "http://#{host}" if host == path + return url if host == path - "http://#{host}/#{path}" + "#{url}/#{path}" end end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index d41280624ae..fa764844cb9 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -163,6 +163,8 @@ production: &base # http://group.example.com/project # or project path can be a group page: group.example.com domain: example.com + port: 80 # Set to 443 if you serve the pages with HTTPS + https: false # Set to true if you serve the pages with HTTPS ## Mattermost ## For enabling Add to Mattermost button diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 0c06b73ba36..3932745e20c 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -5,8 +5,8 @@ class Settings < Settingslogic namespace Rails.env class << self - def gitlab_on_standard_port? - gitlab.port.to_i == (gitlab.https ? 443 : 80) + def on_standard_port?(config) + config.port.to_i == (config.https ? 443 : 80) end def host_without_www(url) @@ -14,7 +14,7 @@ class Settings < Settingslogic end def build_gitlab_ci_url - if gitlab_on_standard_port? + if on_standard_port?(gitlab) custom_port = nil else custom_port = ":#{gitlab.port}" @@ -27,6 +27,10 @@ class Settings < Settingslogic ].join('') end + def build_pages_url + base_url(pages).join('') + end + def build_gitlab_shell_ssh_path_prefix user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}" @@ -42,11 +46,11 @@ class Settings < Settingslogic end def build_base_gitlab_url - base_gitlab_url.join('') + base_url(gitlab).join('') end def build_gitlab_url - (base_gitlab_url + [gitlab.relative_url_root]).join('') + (base_url(gitlab) + [gitlab.relative_url_root]).join('') end # check that values in `current` (string or integer) is a contant in `modul`. @@ -74,11 +78,11 @@ class Settings < Settingslogic private - def base_gitlab_url - custom_port = gitlab_on_standard_port? ? nil : ":#{gitlab.port}" - [ gitlab.protocol, + def base_url(config) + custom_port = on_standard_port?(config) ? nil : ":#{config.port}" + [ config.protocol, "://", - gitlab.host, + config.host, custom_port ] end @@ -254,11 +258,17 @@ Settings.registry['issuer'] ||= nil Settings.registry['host_port'] ||= [Settings.registry['host'], Settings.registry['port']].compact.join(':') Settings.registry['path'] = File.expand_path(Settings.registry['path'] || File.join(Settings.shared['path'], 'registry'), Rails.root) +# # Pages +# Settings['pages'] ||= Settingslogic.new({}) Settings.pages['enabled'] = false if Settings.pages['enabled'].nil? Settings.pages['path'] = File.expand_path('shared/pages/', Rails.root) Settings.pages['domain'] ||= "example.com" +Settings.pages['https'] = false if Settings.pages['https'].nil? +Settings.pages['port'] ||= Settings.pages.https ? 443 : 80 +Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http" +Settings.pages['url'] ||= Settings.send(:build_pages_url) # # Git LFS -- cgit v1.2.1 From b27371d89848b35a1be06e253d0958f723fc242f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 16 Dec 2015 12:59:01 +0100 Subject: Change pages domain to host --- app/models/project.rb | 2 +- config/gitlab.yml.example | 2 +- config/initializers/1_settings.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index aa16b055e81..a1888c089ce 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1164,7 +1164,7 @@ class Project < ActiveRecord::Base def pages_url if Dir.exist?(public_pages_path) - host = "#{namespace.path}.#{Settings.pages.domain}" + host = "#{namespace.path}.#{Settings.pages.host}" url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix| "#{prefix}#{namespace.path}." end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index fa764844cb9..c6f06d43d07 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -162,7 +162,7 @@ production: &base # The domain under which the pages are served: # http://group.example.com/project # or project path can be a group page: group.example.com - domain: example.com + host: example.com port: 80 # Set to 443 if you serve the pages with HTTPS https: false # Set to true if you serve the pages with HTTPS diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 3932745e20c..b938a9cdb2a 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -264,7 +264,7 @@ Settings.registry['path'] = File.expand_path(Settings.registry['path' Settings['pages'] ||= Settingslogic.new({}) Settings.pages['enabled'] = false if Settings.pages['enabled'].nil? Settings.pages['path'] = File.expand_path('shared/pages/', Rails.root) -Settings.pages['domain'] ||= "example.com" +Settings.pages['host'] ||= "example.com" Settings.pages['https'] = false if Settings.pages['https'].nil? Settings.pages['port'] ||= Settings.pages.https ? 443 : 80 Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http" -- cgit v1.2.1 From adc1a9abb5adbf746b492938cb11576753edcc7e Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 16 Dec 2015 13:04:05 +0100 Subject: Re-add missing gitlab_on_standard_port --- config/initializers/1_settings.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index b938a9cdb2a..842ea94c5a4 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -5,8 +5,8 @@ class Settings < Settingslogic namespace Rails.env class << self - def on_standard_port?(config) - config.port.to_i == (config.https ? 443 : 80) + def gitlab_on_standard_port? + on_standard_port?(gitlab) end def host_without_www(url) @@ -87,6 +87,10 @@ class Settings < Settingslogic ] end + def on_standard_port?(config) + config.port.to_i == (config.https ? 443 : 80) + end + # Extract the host part of the given +url+. def host(url) url = url.downcase -- cgit v1.2.1 From d28f1a7f4aa4bdf664e04a43022667e4e7637e73 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 16 Dec 2015 16:29:53 +0100 Subject: Split PagesWorker --- app/workers/pages_worker.rb | 101 ++++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index 59f4b4f16f4..c34259c15f1 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -12,62 +12,83 @@ class PagesWorker return unless valid? # Create status notifying the deployment of pages - @status = GenericCommitStatus.new( - project: project, - commit: build.commit, - user: build.user, - ref: build.ref, - stage: 'deploy', - name: 'pages:deploy' - ) + @status = create_status @status.run! - - FileUtils.mkdir_p(tmp_path) - - # Calculate dd parameters: we limit the size of pages - max_size = current_application_settings.max_pages_size.megabytes - max_size ||= MAX_SIZE - blocks = 1 + max_size / BLOCK_SIZE + raise 'pages are outdated' unless latest? # Create temporary directory in which we will extract the artifacts - Dir.mktmpdir(nil, tmp_path) do |temp_path| - # We manually extract the archive and limit the archive size with dd - results = Open3.pipeline(%W(gunzip -c #{artifacts}), - %W(dd bs=#{BLOCK_SIZE} count=#{blocks}), - %W(tar -x -C #{temp_path} public/), - err: '/dev/null') - return unless results.compact.all?(&:success?) + Dir.mktmpdir(nil, tmp_path) do |archive_path| + results = extract_archive(archive_path) + raise 'pages failed to extract' unless results.all?(&:success?) # Check if we did extract public directory - temp_public_path = File.join(temp_path, 'public') - return unless Dir.exists?(temp_public_path) + archive_public_path = File.join(archive_path, 'public') + raise 'pages miss the public folder' unless Dir.exists?(archive_public_path) + raise 'pages are outdated' unless latest? + deploy_page!(archive_public_path) - FileUtils.mkdir_p(pages_path) + @status.success + end + rescue => e + fail(e.message, !latest?) + end - # Ignore deployment if the HEAD changed when we were extracting the archive - return unless valid? + private - # Do atomic move of pages - # Move and removal may not be atomic, but they are significantly faster then extracting and removal - # 1. We move deployed public to previous public path (file removal is slow) - # 2. We move temporary public to be deployed public - # 3. We remove previous public path - FileUtils.move(public_path, previous_public_path, force: true) - FileUtils.move(temp_public_path, public_path) - FileUtils.rm_r(previous_public_path, force: true) + def create_status + GenericCommitStatus.new( + project: project, + commit: build.commit, + user: build.user, + ref: build.ref, + stage: 'deploy', + name: 'pages:deploy' + ) + end - @status.success - end + def extract_archive(temp_path) + results = Open3.pipeline(%W(gunzip -c #{artifacts}), + %W(dd bs=#{BLOCK_SIZE} count=#{blocks}), + %W(tar -x -C #{temp_path} public/), + err: '/dev/null') + results.compact + end + + def deploy_page!(archive_public_path) + # Do atomic move of pages + # Move and removal may not be atomic, but they are significantly faster then extracting and removal + # 1. We move deployed public to previous public path (file removal is slow) + # 2. We move temporary public to be deployed public + # 3. We remove previous public path + FileUtils.mkdir_p(pages_path) + FileUtils.move(public_path, previous_public_path, force: true) + FileUtils.move(archive_public_path, public_path) ensure - @status.drop if @status && @status.active? + FileUtils.rm_r(previous_public_path, force: true) end - private + def fail(message, allow_failure = true) + @status.allow_failure = allow_failure + @status.description = message + @status.drop + end def valid? + build && build.artifacts_file? + end + + def latest? # check if sha for the ref is still the most recent one # this helps in case when multiple deployments happens - build && build.artifacts_file? && sha == latest_sha + sha == latest_sha + end + + def blocks + # Calculate dd parameters: we limit the size of pages + max_size = current_application_settings.max_pages_size.megabytes + max_size ||= MAX_SIZE + blocks = 1 + max_size / BLOCK_SIZE + blocks end def build -- cgit v1.2.1 From 35dd2e12837ea507a02aca239fc7f78f949e9e99 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 16 Dec 2015 17:09:11 +0100 Subject: Fix tests --- app/workers/pages_worker.rb | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index c34259c15f1..6c6bb7ed13f 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -14,23 +14,26 @@ class PagesWorker # Create status notifying the deployment of pages @status = create_status @status.run! + raise 'pages are outdated' unless latest? # Create temporary directory in which we will extract the artifacts + FileUtils.mkdir_p(tmp_path) Dir.mktmpdir(nil, tmp_path) do |archive_path| - results = extract_archive(archive_path) - raise 'pages failed to extract' unless results.all?(&:success?) + extract_archive!(archive_path) # Check if we did extract public directory archive_public_path = File.join(archive_path, 'public') raise 'pages miss the public folder' unless Dir.exists?(archive_public_path) raise 'pages are outdated' unless latest? + deploy_page!(archive_public_path) @status.success end rescue => e fail(e.message, !latest?) + return false end private @@ -46,12 +49,12 @@ class PagesWorker ) end - def extract_archive(temp_path) + def extract_archive!(temp_path) results = Open3.pipeline(%W(gunzip -c #{artifacts}), %W(dd bs=#{BLOCK_SIZE} count=#{blocks}), %W(tar -x -C #{temp_path} public/), err: '/dev/null') - results.compact + raise 'pages failed to extract' unless results.compact.all?(&:success?) end def deploy_page!(archive_public_path) @@ -61,7 +64,10 @@ class PagesWorker # 2. We move temporary public to be deployed public # 3. We remove previous public path FileUtils.mkdir_p(pages_path) - FileUtils.move(public_path, previous_public_path, force: true) + begin + FileUtils.move(public_path, previous_public_path) + rescue + end FileUtils.move(archive_public_path, public_path) ensure FileUtils.rm_r(previous_public_path, force: true) -- cgit v1.2.1 From 0f2274cc14f1a4071d5b4ab258209e8c1b14d698 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 17 Dec 2015 11:03:58 +0200 Subject: First draft of pages documentation --- doc/pages/README.md | 63 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index d08c34c3ebc..548a24c4cc4 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -1,12 +1,59 @@ # GitLab Pages -To start using GitLab Pages add to your project .gitlab-ci.yml with special pages job. +_**Note:** This feature was introduced in GitLab EE 8.3_ - pages: - image: jekyll - script: jekyll build - artifacts: - paths: - - public +To start using GitLab Pages add to your project `.gitlab-ci.yml` the special +`pages` job. The example below is using [jekyll][] and assumes the created +HTML files are generated under the `public/` directory which resides under the +root directory of your Git repository. -TODO +```yaml +pages: + image: jekyll + script: jekyll build + artifacts: + paths: + - public +``` + +- The pages are created when build artifacts for `pages` job are uploaded +- Pages serve the content under: http://group.pages.domain.com/project +- Pages can be used to serve the group page, special project named as host: group.pages.domain.com +- User can provide own 403 and 404 error pages by creating 403.html and 404.html in group page project +- Pages can be explicitly removed from the project by clicking Remove Pages in Project Settings +- The size of pages is limited by Application Setting: max pages size, which limits the maximum size of unpacked archive (default: 100MB) +- The public/ is extracted from artifacts and content is served as static pages +- Pages asynchronous worker use `dd` to limit the unpacked tar size +- Pages needs to be explicitly enabled and domain needs to be specified in gitlab.yml +- Pages are part of backups +- Pages notify the deployment status using Commit Status API +- Pages use a new sidekiq queue: pages +- Pages use a separate nginx config which needs to be explicitly added + +## Examples + +- Add example with stages. `test` using a linter tool, `deploy` in `pages` +- Add examples of more static tool generators + +```yaml +image: jekyll + +stages: + - test + - deploy + +lint: + script: jekyll build + stage: test + +pages: + script: jekyll build + stage: deploy + artifacts: + paths: + - public +``` + +## Current limitations + +- We currently support only http and port 80. It will be extended in the future. -- cgit v1.2.1 From c57881395bd7ba92b8b985545b239b00f3100284 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 17 Dec 2015 11:07:35 +0200 Subject: Add missing jekyll website link --- doc/pages/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/pages/README.md b/doc/pages/README.md index 548a24c4cc4..3353561b9a9 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -57,3 +57,5 @@ pages: ## Current limitations - We currently support only http and port 80. It will be extended in the future. + +[jekyll]: http://jekyllrb.com/ -- cgit v1.2.1 From f934460e81d954afb10049d4d596744e1e7b0cab Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 17 Dec 2015 11:09:52 +0200 Subject: Fix wrong assumption that the public dir must be present in your git repo --- doc/pages/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 3353561b9a9..ceda2a38915 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -4,8 +4,7 @@ _**Note:** This feature was introduced in GitLab EE 8.3_ To start using GitLab Pages add to your project `.gitlab-ci.yml` the special `pages` job. The example below is using [jekyll][] and assumes the created -HTML files are generated under the `public/` directory which resides under the -root directory of your Git repository. +HTML files are generated under the `public/` directory. ```yaml pages: -- cgit v1.2.1 From f8b0d06b3d341b822c6a2b7f54c979f7c0e86b94 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 17 Dec 2015 15:33:43 +0100 Subject: Fix pages storage path --- config/initializers/1_settings.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 842ea94c5a4..e52171f0d64 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -267,7 +267,7 @@ Settings.registry['path'] = File.expand_path(Settings.registry['path' # Settings['pages'] ||= Settingslogic.new({}) Settings.pages['enabled'] = false if Settings.pages['enabled'].nil? -Settings.pages['path'] = File.expand_path('shared/pages/', Rails.root) +Settings.pages['path'] ||= File.expand_path('shared/pages/', Rails.root) Settings.pages['host'] ||= "example.com" Settings.pages['https'] = false if Settings.pages['https'].nil? Settings.pages['port'] ||= Settings.pages.https ? 443 : 80 -- cgit v1.2.1 From ab220022f40b526ce5b937caed2a13b4b1ca239b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 17 Dec 2015 18:03:38 +0200 Subject: Add GitLab Pages administration guide --- doc/README.md | 1 + doc/pages/administration.md | 146 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 doc/pages/administration.md diff --git a/doc/README.md b/doc/README.md index f036febb7b9..951a302f8ba 100644 --- a/doc/README.md +++ b/doc/README.md @@ -54,6 +54,7 @@ - [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. - [Git LFS configuration](workflow/lfs/lfs_administration.md) - [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast. +- [GitLab Pages configuration](pages/administration.md) - [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics. - [GitLab performance monitoring with Prometheus](administration/monitoring/performance/prometheus.md) Configure GitLab and Prometheus for measuring performance metrics. - [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests. diff --git a/doc/pages/administration.md b/doc/pages/administration.md new file mode 100644 index 00000000000..e754d4cbd96 --- /dev/null +++ b/doc/pages/administration.md @@ -0,0 +1,146 @@ +# GitLab Pages Administration + +_**Note:** This feature was [introduced][ee-80] in GitLab EE 8.3_ + +If you are looking for ways to upload your static content in GitLab Pages, you +probably want to read the [user documentation](README.md). + +## Configuration + +There are a couple of things to consider before enabling GitLab pages in your +GitLab EE instance. + +1. You need to properly configure your DNS to point to the domain that pages + will be served +1. Pages use a separate nginx configuration file which needs to be explicitly + added in the server under which GitLab EE runs + +Both of these settings are described in detail in the sections below. + +### DNS configuration + +GitLab Pages expect to run on their own virtual host. In your DNS you need to +add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the host that +GitLab runs. For example, an entry would look like this: + +``` +*.gitlabpages.com. 60 IN A 1.2.3.4 +``` + +where `gitlabpages.com` is the domain under which GitLab Pages will be served +and `1.2.3.4` is the IP address of your GitLab instance. + +It is strongly advised to **not** use the GitLab domain to serve user pages. +See [security](#security). + +### Omnibus package installations + +See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages.html>. + +### Installations from source + +1. Go to the GitLab installation directory: + + ```bash + cd /home/git/gitlab + ``` + +1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` in + order to enable the pages feature: + + ```bash + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + # The domain under which the pages are served: + # http://group.example.com/project + # or project path can be a group page: group.example.com + host: example.com + port: 80 # Set to 443 if you serve the pages with HTTPS + https: false # Set to true if you serve the pages with HTTPS + ``` + +1. Make sure you have copied the new `gitlab-pages` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf + ``` + + Don't forget to add your domain name in the Nginx config. For example if your + GitLab pages domain is `gitlabpages.com`, replace + + ```bash + server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; + ``` + + with + + ``` + server_name ~^(?<group>.*)\.gitlabpages\.com$; + ``` + + You must be extra careful to not remove the backslashes. + +1. Restart Nginx and GitLab: + + ```bash + sudo service nginx restart + sudo service gitlab restart + ``` + +### Running GitLab pages with HTTPS + +If you want the pages to be served under HTTPS, a wildcard SSL certificate is +required. + +1. In `gitlab.yml`, set the port to `443` and https to `true`: + + ```bash + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + # The domain under which the pages are served: + # http://group.example.com/project + # or project path can be a group page: group.example.com + host: gitlabpages.com + port: 443 # Set to 443 if you serve the pages with HTTPS + https: true # Set to true if you serve the pages with HTTPS + ``` +1. Use the `gitlab-pages-ssl` Nginx configuration file + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf + ``` + + Make sure to edit the config and add your domain as well as correctly point + to the right location where the SSL certificates reside. + +## Set maximum pages size + +The maximum size of the unpacked archive can be configured in the Admin area +under the Application settings in the **Maximum size of pages (MB)**. +The default is 100MB. + +## Security + +You should strongly consider running GitLab pages under a different hostname +than GitLab to prevent XSS. + +## How it works + +- The public/ is extracted from artifacts and content is served as static pages +- Pages asynchronous worker use `dd` to limit the unpacked tar size +- Pages are part of backups +- Pages notify the deployment status using Commit Status API +- Pages use a new sidekiq queue: pages + +[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 +[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record -- cgit v1.2.1 From a60f5f6cb429ff232647665ab288b3df251669e5 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 17 Dec 2015 19:38:21 +0200 Subject: GitLab Pages admin guide clean up [ci skip] - Fix markdown - Remove how it works section, maybe add it at a later point --- doc/pages/administration.md | 129 +++++++++++++++++++++----------------------- 1 file changed, 62 insertions(+), 67 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index e754d4cbd96..02e575c851a 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -41,56 +41,55 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. 1. Go to the GitLab installation directory: - ```bash - cd /home/git/gitlab - ``` - -1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` in - order to enable the pages feature: - - ```bash - ## GitLab Pages - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - # path: shared/pages - - # The domain under which the pages are served: - # http://group.example.com/project - # or project path can be a group page: group.example.com - host: example.com - port: 80 # Set to 443 if you serve the pages with HTTPS - https: false # Set to true if you serve the pages with HTTPS + ```bash + cd /home/git/gitlab + ``` + +1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true`: + + ```bash + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + # The domain under which the pages are served: + # http://group.example.com/project + # or project path can be a group page: group.example.com + host: example.com + port: 80 # Set to 443 if you serve the pages with HTTPS + https: false # Set to true if you serve the pages with HTTPS ``` 1. Make sure you have copied the new `gitlab-pages` Nginx configuration file: - ```bash - sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf - ``` + ```bash + sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf + ``` - Don't forget to add your domain name in the Nginx config. For example if your - GitLab pages domain is `gitlabpages.com`, replace + Don't forget to add your domain name in the Nginx config. For example if + your GitLab pages domain is `gitlabpages.com`, replace - ```bash - server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; - ``` + ```bash + server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; + ``` - with + with - ``` - server_name ~^(?<group>.*)\.gitlabpages\.com$; - ``` + ``` + server_name ~^(?<group>.*)\.gitlabpages\.com$; + ``` - You must be extra careful to not remove the backslashes. + You must be extra careful to not remove the backslashes. 1. Restart Nginx and GitLab: - ```bash - sudo service nginx restart - sudo service gitlab restart - ``` + ```bash + sudo service nginx restart + sudo service gitlab restart + ``` ### Running GitLab pages with HTTPS @@ -99,29 +98,29 @@ required. 1. In `gitlab.yml`, set the port to `443` and https to `true`: - ```bash - ## GitLab Pages - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - # path: shared/pages - - # The domain under which the pages are served: - # http://group.example.com/project - # or project path can be a group page: group.example.com - host: gitlabpages.com - port: 443 # Set to 443 if you serve the pages with HTTPS - https: true # Set to true if you serve the pages with HTTPS + ```bash + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + # The domain under which the pages are served: + # http://group.example.com/project + # or project path can be a group page: group.example.com + host: gitlabpages.com + port: 443 # Set to 443 if you serve the pages with HTTPS + https: true # Set to true if you serve the pages with HTTPS ``` -1. Use the `gitlab-pages-ssl` Nginx configuration file +1. Copy the `gitlab-pages-ssl` Nginx configuration file: - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf - ``` + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf + ``` - Make sure to edit the config and add your domain as well as correctly point - to the right location where the SSL certificates reside. + Make sure to edit the config to add your domain as well as correctly point + to the right location where the SSL certificates reside. ## Set maximum pages size @@ -129,18 +128,14 @@ The maximum size of the unpacked archive can be configured in the Admin area under the Application settings in the **Maximum size of pages (MB)**. The default is 100MB. -## Security +## Backup -You should strongly consider running GitLab pages under a different hostname -than GitLab to prevent XSS. +Pages are part of the regular backup so there is nothing to configure. -## How it works +## Security -- The public/ is extracted from artifacts and content is served as static pages -- Pages asynchronous worker use `dd` to limit the unpacked tar size -- Pages are part of backups -- Pages notify the deployment status using Commit Status API -- Pages use a new sidekiq queue: pages +You should strongly consider running GitLab pages under a different hostname +than GitLab to prevent XSS attacks. [ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 [wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record -- cgit v1.2.1 From 38028d92539afb70ab6c1d5ec0d03299a9d2c1db Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 17 Dec 2015 19:56:07 +0200 Subject: Add example when using a subdomain [ci skip] --- doc/pages/administration.md | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 02e575c851a..6e242efae9f 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -45,7 +45,8 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. cd /home/git/gitlab ``` -1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true`: +1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and + the `host` to the FQDN under which GitLab Pages will be served: ```bash ## GitLab Pages @@ -82,7 +83,13 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. server_name ~^(?<group>.*)\.gitlabpages\.com$; ``` - You must be extra careful to not remove the backslashes. + You must be extra careful to not remove the backslashes. If you are using + a subdomain, make sure to escape all dots (`.`) with a backslash (\). + For example `pages.gitlab.io` would be: + + ``` + server_name ~^(?<group>.*)\.pages\.gitlab\.io$; + ``` 1. Restart Nginx and GitLab: @@ -108,7 +115,7 @@ required. # The domain under which the pages are served: # http://group.example.com/project # or project path can be a group page: group.example.com - host: gitlabpages.com + host: example.com port: 443 # Set to 443 if you serve the pages with HTTPS https: true # Set to true if you serve the pages with HTTPS ``` @@ -120,7 +127,8 @@ required. ``` Make sure to edit the config to add your domain as well as correctly point - to the right location where the SSL certificates reside. + to the right location where the SSL certificates reside. After all changes + restart Nginx. ## Set maximum pages size -- cgit v1.2.1 From 94fdf58a87f8f2cc54c0482a2fce9e3fa425d4b9 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 17 Dec 2015 19:25:28 +0100 Subject: Store pages in shared/pages/fqdn/fqdn/public or shared/pages/fqdn/subpath/public - makes it simpler to implement CNAMEs in future --- app/models/project.rb | 14 ++++--- app/workers/pages_worker.rb | 4 -- doc/pages/administration.md | 12 ++---- lib/support/nginx/gitlab-pages | 11 ++++-- lib/support/nginx/gitlab-pages-ssl | 80 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 98 insertions(+), 23 deletions(-) create mode 100644 lib/support/nginx/gitlab-pages-ssl diff --git a/app/models/project.rb b/app/models/project.rb index a1888c089ce..9cdd01e433d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1137,6 +1137,7 @@ class Project < ActiveRecord::Base issues.opened.count end +<<<<<<< HEAD def visibility_level_allowed_as_fork?(level = self.visibility_level) return true unless forked? @@ -1162,22 +1163,23 @@ class Project < ActiveRecord::Base ensure_runners_token! end + def pages_host + "#{namespace.path}.#{Settings.pages.host}" + end + def pages_url if Dir.exist?(public_pages_path) - host = "#{namespace.path}.#{Settings.pages.host}" - url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix| - "#{prefix}#{namespace.path}." - end + url = Gitlab.config.pages.url.sub(Settings.pages.host, pages_host) # If the project path is the same as host, leave the short version - return url if host == path + return url if pages_host == path "#{url}/#{path}" end end def pages_path - File.join(Settings.pages.path, path_with_namespace) + File.join(Settings.pages.path, pages_host, path) end def public_pages_path diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index 6c6bb7ed13f..836e8d8ad9d 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -121,10 +121,6 @@ class PagesWorker @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}") end - def lock_path - @lock_path ||= File.join(pages_path, 'deploy.lock') - end - def ref build.ref end diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 6e242efae9f..28e975e5621 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -74,22 +74,16 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. your GitLab pages domain is `gitlabpages.com`, replace ```bash - server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; + server_name *.YOUR_GITLAB_PAGES.DOMAIN; ``` with ``` - server_name ~^(?<group>.*)\.gitlabpages\.com$; + server_name *.gitlabpages.com; ``` - You must be extra careful to not remove the backslashes. If you are using - a subdomain, make sure to escape all dots (`.`) with a backslash (\). - For example `pages.gitlab.io` would be: - - ``` - server_name ~^(?<group>.*)\.pages\.gitlab\.io$; - ``` + You must be add `*` in front of your domain, this is required to catch all subdomains of gitlabpages.com. 1. Restart Nginx and GitLab: diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages index 0eeb0cd1917..6300c268521 100644 --- a/lib/support/nginx/gitlab-pages +++ b/lib/support/nginx/gitlab-pages @@ -1,18 +1,21 @@ +## GitLab +## + ## Pages serving host server { listen 0.0.0.0:80; listen [::]:80 ipv6only=on; ## Replace this with something like pages.gitlab.com - server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; - root /home/git/gitlab/shared/pages/${group}; + server_name *.YOUR_GITLAB_PAGES.DOMAIN; + root /home/git/gitlab/shared/pages/${host}; ## Individual nginx logs for GitLab pages access_log /var/log/nginx/gitlab_pages_access.log; error_log /var/log/nginx/gitlab_pages_error.log; - # 1. Try to get /project/ to => shared/pages/${group}/public/ or index.html - # 2. Try to get / to => shared/pages/${group}/${host}/public/ or index.html + # 1. Try to get /project/ from => shared/pages/${host}/${project}/public/ + # 2. Try to get / from => shared/pages/${host}/${host}/public/ location ~ ^/([^/]*)(/.*)?$ { try_files "/$1/public$2" "/$1/public$2/index.html" diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl new file mode 100644 index 00000000000..d3e8379ed29 --- /dev/null +++ b/lib/support/nginx/gitlab-pages-ssl @@ -0,0 +1,80 @@ +## GitLab +## + +## Redirects all HTTP traffic to the HTTPS host +server { + ## Either remove "default_server" from the listen line below, + ## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab + ## to be served if you visit any address that your server responds to, eg. + ## the ip address of the server (http://x.x.x.x/) + listen 0.0.0.0:80; + listen [::]:80 ipv6only=on; + + server_name *.YOUR_GITLAB_PAGES.DOMAIN; + server_tokens off; ## Don't show the nginx version number, a security best practice + + return 301 https://$http_host$request_uri; + + access_log /var/log/nginx/gitlab_pages_access.log; + error_log /var/log/nginx/gitlab_pages_access.log; +} + +## Pages serving host +server { + listen 0.0.0.0:443 ssl; + listen [::]:443 ipv6only=on ssl; + + ## Replace this with something like pages.gitlab.com + server_name *.YOUR_GITLAB_PAGES.DOMAIN; + server_tokens off; ## Don't show the nginx version number, a security best practice + root /home/git/gitlab/shared/pages/${host}; + + ## Strong SSL Security + ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ + ssl on; + ssl_certificate /etc/nginx/ssl/gitlab-pages.crt; + ssl_certificate_key /etc/nginx/ssl/gitlab-pages.key; + + # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs + ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 5m; + + ## See app/controllers/application_controller.rb for headers set + + ## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL. + ## Replace with your ssl_trusted_certificate. For more info see: + ## - https://medium.com/devops-programming/4445f4862461 + ## - https://www.ruby-forum.com/topic/4419319 + ## - https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx + # ssl_stapling on; + # ssl_stapling_verify on; + # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt; + # resolver 208.67.222.222 208.67.222.220 valid=300s; # Can change to your DNS resolver if desired + # resolver_timeout 5s; + + ## [Optional] Generate a stronger DHE parameter: + ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096 + ## + # ssl_dhparam /etc/ssl/certs/dhparam.pem; + + ## Individual nginx logs for GitLab pages + access_log /var/log/nginx/gitlab_pages_access.log; + error_log /var/log/nginx/gitlab_pages_error.log; + + # 1. Try to get /project/ from => shared/pages/${host}/${project}/public/ + # 2. Try to get / from => shared/pages/${host}/${host}/public/ + location ~ ^/([^/]*)(/.*)?$ { + try_files "/$1/public$2" + "/$1/public$2/index.html" + "/${host}/public/${uri}" + "/${host}/public/${uri}/index.html" + =404; + } + + # Define custom error pages + error_page 403 /403.html; + error_page 404 /404.html; +} -- cgit v1.2.1 From 6fd06943f9135fcaa406f8df0c016981067638a5 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 17 Dec 2015 21:03:14 +0200 Subject: Point to GP administration guide, no need to duplicate things [ci skip] --- doc/install/installation.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index c78e469055d..4496243da25 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -313,7 +313,7 @@ sudo usermod -aG redis git # Change the permissions of the directory where CI artifacts are stored sudo chmod -R u+rwX shared/artifacts/ - # Change the permissions of the directory where CI artifacts are stored + # Change the permissions of the directory where GitLab Pages are stored sudo chmod -R ug+rwX shared/pages/ # Copy the example Unicorn config @@ -487,16 +487,10 @@ Make sure to edit the config file to match your setup. Also, ensure that you mat # or else sudo rm -f /etc/nginx/sites-enabled/default sudo editor /etc/nginx/sites-available/gitlab -Copy the GitLab pages site config: +If you intend to enable GitLab pages, there is a separate Nginx config you need +to use. Read all about the needed configuration at the +[GitLab Pages administration guide](../pages/administration.md). - sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages - sudo ln -s /etc/nginx/sites-available/gitlab-pages /etc/nginx/sites-enabled/gitlab-pages - - # Change YOUR_GITLAB_PAGES\.DOMAIN to the fully-qualified - # domain name under which the pages will be served. - # The . (dot) replace with \. (backslash+dot) - sudo editor /etc/nginx/sites-available/gitlab-pages - **Note:** If you want to use HTTPS, replace the `gitlab` Nginx config with `gitlab-ssl`. See [Using HTTPS](#using-https) for HTTPS configuration details. ### Test Configuration -- cgit v1.2.1 From 4e03436befb2c912549440236456c063aa628e51 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 17 Dec 2015 21:29:24 +0200 Subject: Small lines improvement [ci skip] --- doc/pages/administration.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 28e975e5621..7feeddd496b 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -83,7 +83,8 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. server_name *.gitlabpages.com; ``` - You must be add `*` in front of your domain, this is required to catch all subdomains of gitlabpages.com. + You must be add `*` in front of your domain, this is required to catch all + subdomains of `gitlabpages.com`. 1. Restart Nginx and GitLab: @@ -113,6 +114,7 @@ required. port: 443 # Set to 443 if you serve the pages with HTTPS https: true # Set to true if you serve the pages with HTTPS ``` + 1. Copy the `gitlab-pages-ssl` Nginx configuration file: ```bash -- cgit v1.2.1 From 6f999703c1b9cc3dc8ecf50429a4bf896dda3b2f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 18 Dec 2015 00:44:11 +0200 Subject: Finish GitLab Pages user documentation --- doc/pages/README.md | 141 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 41 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index ceda2a38915..1d6ea1991f8 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -1,60 +1,119 @@ # GitLab Pages -_**Note:** This feature was introduced in GitLab EE 8.3_ +_**Note:** This feature was [introduced][ee-80] in GitLab EE 8.3_ -To start using GitLab Pages add to your project `.gitlab-ci.yml` the special -`pages` job. The example below is using [jekyll][] and assumes the created -HTML files are generated under the `public/` directory. +GitLab Pages allow you to host static content + +## Enable the pages feature in your GitLab EE instance + +The administrator guide is located at [administration](administration.md). + +## Understanding how GitLab Pages work + +GitLab Pages rely heavily on GitLab CI and its ability to upload artifacts. +The steps that are performed from the initialization of a project to the +creation of the static content, can be summed up to: + +1. Create project (its name could be specific according to the case) +1. Enable the GitLab Pages feature under the project's settings +1. Provide a specific job in `.gitlab-ci.yml` +1. GitLab Runner builds the project +1. GitLab CI uploads the artifacts +1. Nginx serves the content + +As a user, you should normally be concerned only with the first three items. + +In general there are four kinds of pages one might create. This is better +explained with an example so let's make some assumptions. + +The domain under which the pages are hosted is named `gitlab.io`. There is a +user with the username `walter` and they are the owner of an organization named +`therug`. The personal project of `walter` is named `area51` and don't forget +that the organization has also a project under its namespace, called +`welovecats`. + +The following table depicts what the project's name should be and under which +URL it will be accessible. + +| Pages type | Repository name | URL schema | +| ---------- | --------------- | ---------- | +| User page | `walter/walter.gitlab.io` | `https://walter.gitlab.io` | +| Group page | `therug/therug.gitlab.io` | `https://therug.gitlab.io` | +| Specific project under a user's page | `walter/area51` | `https://walter.gitlab.io/area51` | +| Specific project under a group's page | `therug/welovecats` | `https://therug.gitlab.io/welovecats` | + +## Enable the pages feature in your project + +The GitLab Pages feature needs to be explicitly enabled for each project +under its **Settings**. + +## Remove the contents of your pages + +Pages can be explicitly removed from a project by clicking **Remove Pages** +in a project's **Settings**. + +## Explore the contents of .gitlab-ci.yml + +Before reading this section, make sure you familiarize yourself with GitLab CI +and the specific syntax of[`.gitlab-ci.yml`](../ci/yaml/README.md) by +following our [quick start guide](../ci/quick_start/README.md). + +--- + +To make use of GitLab Pages your `.gitlab-ci.yml` must follow the rules below: + +1. A special `pages` job must be defined +1. Any static content must be placed under a `public/` directory +1. `artifacts` with a path to the `public/` directory must be defined + +The pages are created after the build completes successfully and the artifacts +for the `pages` job are uploaded to GitLab. + +The example below is using [Jekyll][] and assumes that the created HTML files +are generated under the `public/` directory. ```yaml +image: ruby:2.1 + pages: - image: jekyll - script: jekyll build + script: + - gem install jekyll + - jekyll build -d public/ artifacts: paths: - public ``` -- The pages are created when build artifacts for `pages` job are uploaded -- Pages serve the content under: http://group.pages.domain.com/project -- Pages can be used to serve the group page, special project named as host: group.pages.domain.com -- User can provide own 403 and 404 error pages by creating 403.html and 404.html in group page project -- Pages can be explicitly removed from the project by clicking Remove Pages in Project Settings -- The size of pages is limited by Application Setting: max pages size, which limits the maximum size of unpacked archive (default: 100MB) -- The public/ is extracted from artifacts and content is served as static pages -- Pages asynchronous worker use `dd` to limit the unpacked tar size -- Pages needs to be explicitly enabled and domain needs to be specified in gitlab.yml -- Pages are part of backups -- Pages notify the deployment status using Commit Status API -- Pages use a new sidekiq queue: pages -- Pages use a separate nginx config which needs to be explicitly added - -## Examples - -- Add example with stages. `test` using a linter tool, `deploy` in `pages` -- Add examples of more static tool generators +## Example projects -```yaml -image: jekyll +Below is a list of example projects that make use of static generators. +Contributions are very welcome. -stages: - - test - - deploy +* [Jekyll](https://gitlab.com/gitlab-examples/pages-jekyll) -lint: - script: jekyll build - stage: test +## Custom error codes pages -pages: - script: jekyll build - stage: deploy - artifacts: - paths: - - public -``` +You can provide your own 403 and 404 error pages by creating the `403.html` and +`404.html` files respectively in the `public/` directory that will be included +in the artifacts. + +## Frequently Asked Questions + +**Q:** Where are my generated pages stored? + +**A:** All content is located by default under `shared/pages/` in the root +directory of the GitLab installation. To be exact, all specific projects under +a namespace are stored ind `shared/pages/${namespace}/${project}/public/` and +all user/group pages in `shared/pages/${namespace}/${namespace}/public/`. + +--- + +**Q:** Can I download my generated pages? -## Current limitations +**A:** Sure. All you need is to download the artifacts archive from the build + page. -- We currently support only http and port 80. It will be extended in the future. +--- [jekyll]: http://jekyllrb.com/ +[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 -- cgit v1.2.1 From 139ebce691fd9575ebde46111accb86d03c12b21 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 18 Dec 2015 01:30:16 +0200 Subject: Clean up the text in pages view --- app/views/projects/edit.html.haml | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 89e2d4046b8..f9c6809b903 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -142,25 +142,32 @@ .panel-body - if @project.pages_url %strong - Congratulations. Your pages are served at: + Congratulations! Your pages are served at: %p= link_to @project.pages_url, @project.pages_url - else %p - To publish pages create .gitlab-ci.yml with - %strong pages job - and send public/ folder to GitLab. + Learn how to upload your static site and have it served by + GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/pages/README.html", target: :blank}. %p - Use existing tools: + In the example below we define a special job named + %code pages + which is using Jekyll to build a static site. The generated + HTML will be stored in the + %code public/ + directory which will then be archived and uploaded to GitLab. + The name of the directory should not be different than + %code public/ + in order for the pages to work. %ul %li %pre :plain pages: - image: jekyll - script: jekyll build + image: jekyll/jekyll + script: jekyll build -d public/ artifacts: paths: - - public + - public/ - if @project.pages_url && can?(current_user, :remove_pages, @project) .form-actions -- cgit v1.2.1 From cab3ea0044c4b072fa0fd41eb21ea664830cac1f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 18 Dec 2015 01:57:09 +0200 Subject: Add missing intro [ci skip] --- doc/pages/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 1d6ea1991f8..1afe2a97036 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -2,7 +2,9 @@ _**Note:** This feature was [introduced][ee-80] in GitLab EE 8.3_ -GitLab Pages allow you to host static content +With GitLab Pages you can host for free your static websites on GitLab. +Combined with the power of GitLab CI and the help of GitLab Runner you can +deploy static pages for your individual projects your user or your group. ## Enable the pages feature in your GitLab EE instance -- cgit v1.2.1 From 31454eb2f0f6bf960be5a51f0d8073672ea621a9 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 18 Dec 2015 01:57:32 +0200 Subject: Capitalize Pages [ci skip] --- doc/pages/administration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 7feeddd496b..02d737aedbb 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -93,7 +93,7 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. sudo service gitlab restart ``` -### Running GitLab pages with HTTPS +### Running GitLab Pages with HTTPS If you want the pages to be served under HTTPS, a wildcard SSL certificate is required. -- cgit v1.2.1 From 5e8675c15bf5a56764d943918b2260209c002776 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 18 Dec 2015 02:20:29 +0200 Subject: Add section on storage path [ci skip] --- doc/pages/administration.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 02d737aedbb..8df861f70ec 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -48,7 +48,7 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. 1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and the `host` to the FQDN under which GitLab Pages will be served: - ```bash + ```yaml ## GitLab Pages pages: enabled: true @@ -132,6 +132,25 @@ The maximum size of the unpacked archive can be configured in the Admin area under the Application settings in the **Maximum size of pages (MB)**. The default is 100MB. +## Change storage path + +Pages are stored by default in `/home/git/gitlab/shared/pages`. +If you wish to store them in another location you must set it up in +`gitlab.yml` under the `pages` section: + +```yaml +pages: + enabled: true + # The location where pages are stored (default: shared/pages). + path: /mnt/storage/pages +``` + +Restart GitLab for the changes to take effect: + +```bash +sudo service gitlab restart +``` + ## Backup Pages are part of the regular backup so there is nothing to configure. -- cgit v1.2.1 From 6bb7a19c2b4b130438cc4f24512feac9941a9b02 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 18 Dec 2015 12:18:00 +0100 Subject: Revert "Small lines improvement [ci skip]" This reverts commit 0d73bd93bab349d84910cf14773633b8a66df466. --- doc/pages/administration.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 8df861f70ec..29762829819 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -83,8 +83,7 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. server_name *.gitlabpages.com; ``` - You must be add `*` in front of your domain, this is required to catch all - subdomains of `gitlabpages.com`. + You must be add `*` in front of your domain, this is required to catch all subdomains of gitlabpages.com. 1. Restart Nginx and GitLab: @@ -114,7 +113,6 @@ required. port: 443 # Set to 443 if you serve the pages with HTTPS https: true # Set to true if you serve the pages with HTTPS ``` - 1. Copy the `gitlab-pages-ssl` Nginx configuration file: ```bash -- cgit v1.2.1 From 4afab3d4b64bf4aac228306636bb1b477debe8ce Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 18 Dec 2015 12:18:11 +0100 Subject: Revert "Store pages in shared/pages/fqdn/fqdn/public or shared/pages/fqdn/subpath/public - makes it simpler to implement CNAMEs in future" This reverts commit 86a2a78f0d13a678899460638add6b862059433e. --- app/models/project.rb | 14 +++---- app/workers/pages_worker.rb | 4 ++ doc/pages/administration.md | 12 ++++-- lib/support/nginx/gitlab-pages | 11 ++---- lib/support/nginx/gitlab-pages-ssl | 80 -------------------------------------- 5 files changed, 23 insertions(+), 98 deletions(-) delete mode 100644 lib/support/nginx/gitlab-pages-ssl diff --git a/app/models/project.rb b/app/models/project.rb index 9cdd01e433d..a1888c089ce 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1137,7 +1137,6 @@ class Project < ActiveRecord::Base issues.opened.count end -<<<<<<< HEAD def visibility_level_allowed_as_fork?(level = self.visibility_level) return true unless forked? @@ -1163,23 +1162,22 @@ class Project < ActiveRecord::Base ensure_runners_token! end - def pages_host - "#{namespace.path}.#{Settings.pages.host}" - end - def pages_url if Dir.exist?(public_pages_path) - url = Gitlab.config.pages.url.sub(Settings.pages.host, pages_host) + host = "#{namespace.path}.#{Settings.pages.host}" + url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix| + "#{prefix}#{namespace.path}." + end # If the project path is the same as host, leave the short version - return url if pages_host == path + return url if host == path "#{url}/#{path}" end end def pages_path - File.join(Settings.pages.path, pages_host, path) + File.join(Settings.pages.path, path_with_namespace) end def public_pages_path diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index 836e8d8ad9d..6c6bb7ed13f 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -121,6 +121,10 @@ class PagesWorker @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}") end + def lock_path + @lock_path ||= File.join(pages_path, 'deploy.lock') + end + def ref build.ref end diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 29762829819..98a26ec7be9 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -74,16 +74,22 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. your GitLab pages domain is `gitlabpages.com`, replace ```bash - server_name *.YOUR_GITLAB_PAGES.DOMAIN; + server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; ``` with ``` - server_name *.gitlabpages.com; + server_name ~^(?<group>.*)\.gitlabpages\.com$; ``` - You must be add `*` in front of your domain, this is required to catch all subdomains of gitlabpages.com. + You must be extra careful to not remove the backslashes. If you are using + a subdomain, make sure to escape all dots (`.`) with a backslash (\). + For example `pages.gitlab.io` would be: + + ``` + server_name ~^(?<group>.*)\.pages\.gitlab\.io$; + ``` 1. Restart Nginx and GitLab: diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages index 6300c268521..0eeb0cd1917 100644 --- a/lib/support/nginx/gitlab-pages +++ b/lib/support/nginx/gitlab-pages @@ -1,21 +1,18 @@ -## GitLab -## - ## Pages serving host server { listen 0.0.0.0:80; listen [::]:80 ipv6only=on; ## Replace this with something like pages.gitlab.com - server_name *.YOUR_GITLAB_PAGES.DOMAIN; - root /home/git/gitlab/shared/pages/${host}; + server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; + root /home/git/gitlab/shared/pages/${group}; ## Individual nginx logs for GitLab pages access_log /var/log/nginx/gitlab_pages_access.log; error_log /var/log/nginx/gitlab_pages_error.log; - # 1. Try to get /project/ from => shared/pages/${host}/${project}/public/ - # 2. Try to get / from => shared/pages/${host}/${host}/public/ + # 1. Try to get /project/ to => shared/pages/${group}/public/ or index.html + # 2. Try to get / to => shared/pages/${group}/${host}/public/ or index.html location ~ ^/([^/]*)(/.*)?$ { try_files "/$1/public$2" "/$1/public$2/index.html" diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl deleted file mode 100644 index d3e8379ed29..00000000000 --- a/lib/support/nginx/gitlab-pages-ssl +++ /dev/null @@ -1,80 +0,0 @@ -## GitLab -## - -## Redirects all HTTP traffic to the HTTPS host -server { - ## Either remove "default_server" from the listen line below, - ## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab - ## to be served if you visit any address that your server responds to, eg. - ## the ip address of the server (http://x.x.x.x/) - listen 0.0.0.0:80; - listen [::]:80 ipv6only=on; - - server_name *.YOUR_GITLAB_PAGES.DOMAIN; - server_tokens off; ## Don't show the nginx version number, a security best practice - - return 301 https://$http_host$request_uri; - - access_log /var/log/nginx/gitlab_pages_access.log; - error_log /var/log/nginx/gitlab_pages_access.log; -} - -## Pages serving host -server { - listen 0.0.0.0:443 ssl; - listen [::]:443 ipv6only=on ssl; - - ## Replace this with something like pages.gitlab.com - server_name *.YOUR_GITLAB_PAGES.DOMAIN; - server_tokens off; ## Don't show the nginx version number, a security best practice - root /home/git/gitlab/shared/pages/${host}; - - ## Strong SSL Security - ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ - ssl on; - ssl_certificate /etc/nginx/ssl/gitlab-pages.crt; - ssl_certificate_key /etc/nginx/ssl/gitlab-pages.key; - - # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs - ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; - ssl_protocols TLSv1 TLSv1.1 TLSv1.2; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - ssl_session_timeout 5m; - - ## See app/controllers/application_controller.rb for headers set - - ## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL. - ## Replace with your ssl_trusted_certificate. For more info see: - ## - https://medium.com/devops-programming/4445f4862461 - ## - https://www.ruby-forum.com/topic/4419319 - ## - https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx - # ssl_stapling on; - # ssl_stapling_verify on; - # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt; - # resolver 208.67.222.222 208.67.222.220 valid=300s; # Can change to your DNS resolver if desired - # resolver_timeout 5s; - - ## [Optional] Generate a stronger DHE parameter: - ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096 - ## - # ssl_dhparam /etc/ssl/certs/dhparam.pem; - - ## Individual nginx logs for GitLab pages - access_log /var/log/nginx/gitlab_pages_access.log; - error_log /var/log/nginx/gitlab_pages_error.log; - - # 1. Try to get /project/ from => shared/pages/${host}/${project}/public/ - # 2. Try to get / from => shared/pages/${host}/${host}/public/ - location ~ ^/([^/]*)(/.*)?$ { - try_files "/$1/public$2" - "/$1/public$2/index.html" - "/${host}/public/${uri}" - "/${host}/public/${uri}/index.html" - =404; - } - - # Define custom error pages - error_page 403 /403.html; - error_page 404 /404.html; -} -- cgit v1.2.1 From 2c2447771f098f7c8d692e7318d8f822df468b48 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 18 Dec 2015 12:40:34 +0100 Subject: Rename pages namespace or project path when changed - Move UploadsTransfer to ProjectTransfer and inherit from this to UploadsTransfer and PagesTransfer --- app/models/namespace.rb | 1 + app/models/project.rb | 1 + app/services/projects/transfer_service.rb | 3 ++ app/workers/pages_worker.rb | 4 -- doc/pages/administration.md | 1 + lib/gitlab/pages_transfer.rb | 7 ++++ lib/gitlab/project_transfer.rb | 35 +++++++++++++++++ lib/gitlab/uploads_transfer.rb | 30 +-------------- spec/lib/gitlab/project_transfer_spec.rb | 51 +++++++++++++++++++++++++ spec/lib/gitlab/uploads_transfer_spec.rb | 50 ------------------------ spec/services/projects/transfer_service_spec.rb | 2 + 11 files changed, 102 insertions(+), 83 deletions(-) create mode 100644 lib/gitlab/pages_transfer.rb create mode 100644 lib/gitlab/project_transfer.rb create mode 100644 spec/lib/gitlab/project_transfer_spec.rb delete mode 100644 spec/lib/gitlab/uploads_transfer_spec.rb diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 67d8c1c2e4c..2fb2eb44aaa 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -130,6 +130,7 @@ class Namespace < ActiveRecord::Base end Gitlab::UploadsTransfer.new.rename_namespace(path_was, path) + Gitlab::PagesTransfer.new.rename_namespace(path_was, path) remove_exports! diff --git a/app/models/project.rb b/app/models/project.rb index a1888c089ce..e9c7108e805 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -961,6 +961,7 @@ class Project < ActiveRecord::Base Gitlab::AppLogger.info "Project was renamed: #{old_path_with_namespace} -> #{new_path_with_namespace}" Gitlab::UploadsTransfer.new.rename_project(path_was, path, namespace.path) + Gitlab::PagesTransfer.new.rename_project(path_was, path, namespace.path) end # Expires various caches before a project is renamed. diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 34ec575e808..20b049b5973 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -64,6 +64,9 @@ module Projects # Move uploads Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) + # Move pages + Gitlab::PagesTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path) + project.old_path_with_namespace = old_path SystemHooksService.new.execute_hooks_for(project, :transfer) diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index 6c6bb7ed13f..836e8d8ad9d 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -121,10 +121,6 @@ class PagesWorker @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}") end - def lock_path - @lock_path ||= File.join(pages_path, 'deploy.lock') - end - def ref build.ref end diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 98a26ec7be9..2356a123fa3 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -119,6 +119,7 @@ required. port: 443 # Set to 443 if you serve the pages with HTTPS https: true # Set to true if you serve the pages with HTTPS ``` + 1. Copy the `gitlab-pages-ssl` Nginx configuration file: ```bash diff --git a/lib/gitlab/pages_transfer.rb b/lib/gitlab/pages_transfer.rb new file mode 100644 index 00000000000..fb215f27cbd --- /dev/null +++ b/lib/gitlab/pages_transfer.rb @@ -0,0 +1,7 @@ +module Gitlab + class PagesTransfer < ProjectTransfer + def root_dir + Gitlab.config.pages.path + end + end +end diff --git a/lib/gitlab/project_transfer.rb b/lib/gitlab/project_transfer.rb new file mode 100644 index 00000000000..1bba0b78e2f --- /dev/null +++ b/lib/gitlab/project_transfer.rb @@ -0,0 +1,35 @@ +module Gitlab + class ProjectTransfer + def move_project(project_path, namespace_path_was, namespace_path) + new_namespace_folder = File.join(root_dir, namespace_path) + FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder) + from = File.join(root_dir, namespace_path_was, project_path) + to = File.join(root_dir, namespace_path, project_path) + move(from, to, "") + end + + def rename_project(path_was, path, namespace_path) + base_dir = File.join(root_dir, namespace_path) + move(path_was, path, base_dir) + end + + def rename_namespace(path_was, path) + move(path_was, path) + end + + def root_dir + raise NotImplementedError + end + + private + + def move(path_was, path, base_dir = nil) + base_dir = root_dir unless base_dir + from = File.join(base_dir, path_was) + to = File.join(base_dir, path) + FileUtils.mv(from, to) + rescue Errno::ENOENT + false + end + end +end diff --git a/lib/gitlab/uploads_transfer.rb b/lib/gitlab/uploads_transfer.rb index be8fcc7b2d2..81701831a6a 100644 --- a/lib/gitlab/uploads_transfer.rb +++ b/lib/gitlab/uploads_transfer.rb @@ -1,33 +1,5 @@ module Gitlab - class UploadsTransfer - def move_project(project_path, namespace_path_was, namespace_path) - new_namespace_folder = File.join(root_dir, namespace_path) - FileUtils.mkdir_p(new_namespace_folder) unless Dir.exist?(new_namespace_folder) - from = File.join(root_dir, namespace_path_was, project_path) - to = File.join(root_dir, namespace_path, project_path) - move(from, to, "") - end - - def rename_project(path_was, path, namespace_path) - base_dir = File.join(root_dir, namespace_path) - move(path_was, path, base_dir) - end - - def rename_namespace(path_was, path) - move(path_was, path) - end - - private - - def move(path_was, path, base_dir = nil) - base_dir = root_dir unless base_dir - from = File.join(base_dir, path_was) - to = File.join(base_dir, path) - FileUtils.mv(from, to) - rescue Errno::ENOENT - false - end - + class UploadsTransfer < ProjectTransfer def root_dir File.join(Rails.root, "public", "uploads") end diff --git a/spec/lib/gitlab/project_transfer_spec.rb b/spec/lib/gitlab/project_transfer_spec.rb new file mode 100644 index 00000000000..e2d6b1b9ab7 --- /dev/null +++ b/spec/lib/gitlab/project_transfer_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Gitlab::ProjectTransfer, lib: true do + before do + @root_dir = File.join(Rails.root, "public", "uploads") + @project_transfer = Gitlab::ProjectTransfer.new + allow(@project_transfer).to receive(:root_dir).and_return(@root_dir) + + @project_path_was = "test_project_was" + @project_path = "test_project" + @namespace_path_was = "test_namespace_was" + @namespace_path = "test_namespace" + end + + after do + FileUtils.rm_rf([ + File.join(@root_dir, @namespace_path), + File.join(@root_dir, @namespace_path_was) + ]) + end + + describe '#move_project' do + it "moves project upload to another namespace" do + FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path)) + @project_transfer.move_project(@project_path, @namespace_path_was, @namespace_path) + + expected_path = File.join(@root_dir, @namespace_path, @project_path) + expect(Dir.exist?(expected_path)).to be_truthy + end + end + + describe '#rename_project' do + it "renames project" do + FileUtils.mkdir_p(File.join(@root_dir, @namespace_path, @project_path_was)) + @project_transfer.rename_project(@project_path_was, @project_path, @namespace_path) + + expected_path = File.join(@root_dir, @namespace_path, @project_path) + expect(Dir.exist?(expected_path)).to be_truthy + end + end + + describe '#rename_namespace' do + it "renames namespace" do + FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path)) + @project_transfer.rename_namespace(@namespace_path_was, @namespace_path) + + expected_path = File.join(@root_dir, @namespace_path, @project_path) + expect(Dir.exist?(expected_path)).to be_truthy + end + end +end diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/uploads_transfer_spec.rb deleted file mode 100644 index 4092f7fb638..00000000000 --- a/spec/lib/gitlab/uploads_transfer_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' - -describe Gitlab::UploadsTransfer, lib: true do - before do - @root_dir = File.join(Rails.root, "public", "uploads") - @upload_transfer = Gitlab::UploadsTransfer.new - - @project_path_was = "test_project_was" - @project_path = "test_project" - @namespace_path_was = "test_namespace_was" - @namespace_path = "test_namespace" - end - - after do - FileUtils.rm_rf([ - File.join(@root_dir, @namespace_path), - File.join(@root_dir, @namespace_path_was) - ]) - end - - describe '#move_project' do - it "moves project upload to another namespace" do - FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path)) - @upload_transfer.move_project(@project_path, @namespace_path_was, @namespace_path) - - expected_path = File.join(@root_dir, @namespace_path, @project_path) - expect(Dir.exist?(expected_path)).to be_truthy - end - end - - describe '#rename_project' do - it "renames project" do - FileUtils.mkdir_p(File.join(@root_dir, @namespace_path, @project_path_was)) - @upload_transfer.rename_project(@project_path_was, @project_path, @namespace_path) - - expected_path = File.join(@root_dir, @namespace_path, @project_path) - expect(Dir.exist?(expected_path)).to be_truthy - end - end - - describe '#rename_namespace' do - it "renames namespace" do - FileUtils.mkdir_p(File.join(@root_dir, @namespace_path_was, @project_path)) - @upload_transfer.rename_namespace(@namespace_path_was, @namespace_path) - - expected_path = File.join(@root_dir, @namespace_path, @project_path) - expect(Dir.exist?(expected_path)).to be_truthy - end - end -end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 1540b90163a..5d5812c2c15 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -9,6 +9,8 @@ describe Projects::TransferService, services: true do before do allow_any_instance_of(Gitlab::UploadsTransfer). to receive(:move_project).and_return(true) + allow_any_instance_of(Gitlab::PagesTransfer). + to receive(:move_project).and_return(true) group.add_owner(user) @result = transfer_project(project, user, group) end -- cgit v1.2.1 From d26eadf305ecf40d320c3bca0baf9a58288de7a6 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 18 Dec 2015 13:40:13 +0200 Subject: Fix small typos in GP user guide --- doc/pages/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 1afe2a97036..552b3436d77 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -62,7 +62,7 @@ following our [quick start guide](../ci/quick_start/README.md). --- -To make use of GitLab Pages your `.gitlab-ci.yml` must follow the rules below: +To make use of GitLab Pages, your `.gitlab-ci.yml` must follow the rules below: 1. A special `pages` job must be defined 1. Any static content must be placed under a `public/` directory @@ -105,7 +105,7 @@ in the artifacts. **A:** All content is located by default under `shared/pages/` in the root directory of the GitLab installation. To be exact, all specific projects under -a namespace are stored ind `shared/pages/${namespace}/${project}/public/` and +a namespace are stored in `shared/pages/${namespace}/${project}/public/` and all user/group pages in `shared/pages/${namespace}/${namespace}/public/`. --- -- cgit v1.2.1 From a691e9f494c7a661f9fa8ba199dbe6180d8d0329 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 18 Dec 2015 13:40:55 +0200 Subject: Minor cleanup, use gitlab.io as an example domain for GP --- doc/pages/administration.md | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 2356a123fa3..b1ef2cebb14 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -12,26 +12,26 @@ GitLab EE instance. 1. You need to properly configure your DNS to point to the domain that pages will be served -1. Pages use a separate nginx configuration file which needs to be explicitly +1. Pages use a separate Nginx configuration file which needs to be explicitly added in the server under which GitLab EE runs Both of these settings are described in detail in the sections below. ### DNS configuration -GitLab Pages expect to run on their own virtual host. In your DNS you need to -add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the host that -GitLab runs. For example, an entry would look like this: +GitLab Pages expect to run on their own virtual host. In your DNS server/provider +you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the +host that GitLab runs. For example, an entry would look like this: ``` -*.gitlabpages.com. 60 IN A 1.2.3.4 +*.gitlab.io. 60 IN A 1.2.3.4 ``` -where `gitlabpages.com` is the domain under which GitLab Pages will be served +where `gitlab.io` is the domain under which GitLab Pages will be served and `1.2.3.4` is the IP address of your GitLab instance. It is strongly advised to **not** use the GitLab domain to serve user pages. -See [security](#security). +For more information see the [security section](#security). ### Omnibus package installations @@ -58,7 +58,7 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. # The domain under which the pages are served: # http://group.example.com/project # or project path can be a group page: group.example.com - host: example.com + host: gitlab.io port: 80 # Set to 443 if you serve the pages with HTTPS https: false # Set to true if you serve the pages with HTTPS ``` @@ -71,7 +71,7 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. ``` Don't forget to add your domain name in the Nginx config. For example if - your GitLab pages domain is `gitlabpages.com`, replace + your GitLab pages domain is `gitlab.io`, replace ```bash server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; @@ -80,16 +80,11 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. with ``` - server_name ~^(?<group>.*)\.gitlabpages\.com$; + server_name *.gitlab.io; ``` - You must be extra careful to not remove the backslashes. If you are using - a subdomain, make sure to escape all dots (`.`) with a backslash (\). - For example `pages.gitlab.io` would be: - - ``` - server_name ~^(?<group>.*)\.pages\.gitlab\.io$; - ``` + You must be add `*` in front of your domain, this is required to catch all + subdomains of `gitlab.io`. 1. Restart Nginx and GitLab: @@ -115,7 +110,7 @@ required. # The domain under which the pages are served: # http://group.example.com/project # or project path can be a group page: group.example.com - host: example.com + host: gitlab.io port: 443 # Set to 443 if you serve the pages with HTTPS https: true # Set to true if you serve the pages with HTTPS ``` @@ -128,13 +123,13 @@ required. ``` Make sure to edit the config to add your domain as well as correctly point - to the right location where the SSL certificates reside. After all changes - restart Nginx. + to the right location of the SSL certificate files. Restart Nginx for the + changes to take effect. ## Set maximum pages size -The maximum size of the unpacked archive can be configured in the Admin area -under the Application settings in the **Maximum size of pages (MB)**. +The maximum size of the unpacked archive per project can be configured in the +Admin area under the Application settings in the **Maximum size of pages (MB)**. The default is 100MB. ## Change storage path -- cgit v1.2.1 From 9ff381c492695bf9b76b27047bd0b38a70a4daac Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 18 Dec 2015 13:41:33 +0200 Subject: Add pages to excluded directories in backup task [ci skip] --- doc/raketasks/backup_restore.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index f6b4db71b44..0fb69d63dbe 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -84,6 +84,29 @@ Deleting tmp directories...[DONE] Deleting old backups... [SKIPPING] ``` +## Exclude specific directories from the backup + +You can choose what should be backed up by adding the environment variable `SKIP`. +The available options are: + +* `db` +* `uploads` (attachments) +* `repositories` +* `builds` (CI build output logs) +* `artifacts` (CI build artifacts) +* `lfs` (LFS objects) +* `pages` (pages content) + +Use a comma to specify several options at the same time: + +``` +# use this command if you've installed GitLab with the Omnibus package +sudo gitlab-rake gitlab:backup:create SKIP=db,uploads + +# if you've installed GitLab from source +sudo -u git -H bundle exec rake gitlab:backup:create SKIP=db,uploads RAILS_ENV=production +``` + ## Upload backups to remote (cloud) storage Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates. -- cgit v1.2.1 From e9e8a2f60811c460d0bb850da2bb35ea43e35698 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 18 Dec 2015 13:07:53 +0100 Subject: Asynchronously remove pages --- app/models/project.rb | 6 +++++- app/services/update_pages_service.rb | 2 +- app/workers/pages_worker.rb | 11 ++++++++++- spec/workers/pages_worker_spec.rb | 21 ++++++++++++++------- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index e9c7108e805..7a5bf77c5a9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1186,7 +1186,11 @@ class Project < ActiveRecord::Base end def remove_pages - FileUtils.rm_r(pages_path, force: true) + temp_path = "#{path}.#{SecureRandom.hex}" + + if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.path) + PagesWorker.perform_in(5.minutes, :remove, namespace.path, temp_path) + end end def wiki diff --git a/app/services/update_pages_service.rb b/app/services/update_pages_service.rb index 818bb94a293..39f08b2a03d 100644 --- a/app/services/update_pages_service.rb +++ b/app/services/update_pages_service.rb @@ -10,6 +10,6 @@ class UpdatePagesService return unless data[:build_name] == 'pages' return unless data[:build_status] == 'success' - PagesWorker.perform_async(data[:build_id]) + PagesWorker.perform_async(:deploy, data[:build_id]) end end diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index 836e8d8ad9d..ff765a6c13c 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -7,7 +7,11 @@ class PagesWorker sidekiq_options queue: :pages, retry: false - def perform(build_id) + def perform(action, *arg) + send(action, *arg) + end + + def deploy(build_id) @build_id = build_id return unless valid? @@ -36,6 +40,11 @@ class PagesWorker return false end + def remove(namespace_path, project_path) + full_path = File.join(Settings.pages.path, namespace_path, project_path) + FileUtils.rm_r(full_path, force: true) + end + private def create_status diff --git a/spec/workers/pages_worker_spec.rb b/spec/workers/pages_worker_spec.rb index 158a4b3ba8d..85592154598 100644 --- a/spec/workers/pages_worker_spec.rb +++ b/spec/workers/pages_worker_spec.rb @@ -18,41 +18,48 @@ describe PagesWorker do it 'succeeds' do expect(project.pages_url).to be_nil - expect(worker.perform(build.id)).to be_truthy + expect(worker.deploy(build.id)).to be_truthy expect(project.pages_url).to_not be_nil end it 'limits pages size' do stub_application_setting(max_pages_size: 1) - expect(worker.perform(build.id)).to_not be_truthy + expect(worker.deploy(build.id)).to_not be_truthy end it 'removes pages after destroy' do + expect(PagesWorker).to receive(:perform_in) expect(project.pages_url).to be_nil - expect(worker.perform(build.id)).to be_truthy + expect(worker.deploy(build.id)).to be_truthy expect(project.pages_url).to_not be_nil project.destroy expect(Dir.exist?(project.public_pages_path)).to be_falsey end end + it 'fails to remove project pages when no pages is deployed' do + expect(PagesWorker).to_not receive(:perform_in) + expect(project.pages_url).to be_nil + project.destroy + end + it 'fails if no artifacts' do - expect(worker.perform(build.id)).to_not be_truthy + expect(worker.deploy(build.id)).to_not be_truthy end it 'fails for empty file fails' do build.update_attributes(artifacts_file: empty_file) - expect(worker.perform(build.id)).to_not be_truthy + expect(worker.deploy(build.id)).to_not be_truthy end it 'fails for invalid archive' do build.update_attributes(artifacts_file: invalid_file) - expect(worker.perform(build.id)).to_not be_truthy + expect(worker.deploy(build.id)).to_not be_truthy end it 'fails if sha on branch is not latest' do commit.update_attributes(sha: 'old_sha') build.update_attributes(artifacts_file: file) - expect(worker.perform(build.id)).to_not be_truthy + expect(worker.deploy(build.id)).to_not be_truthy end end -- cgit v1.2.1 From 2c6a852529cc42507e45d1266489df491ef1b893 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 18 Dec 2015 13:09:14 +0100 Subject: Refer to server_name with regex --- doc/pages/administration.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index b1ef2cebb14..9c24fee4f1a 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -80,11 +80,16 @@ See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages. with ``` - server_name *.gitlab.io; + server_name ~^(?<group>.*)\.gitlabpages\.com$; ``` - You must be add `*` in front of your domain, this is required to catch all - subdomains of `gitlab.io`. + You must be extra careful to not remove the backslashes. If you are using + a subdomain, make sure to escape all dots (`.`) with a backslash (\). + For example `pages.gitlab.io` would be: + + ``` + server_name ~^(?<group>.*)\.pages\.gitlab\.io$; + ``` 1. Restart Nginx and GitLab: -- cgit v1.2.1 From 3fbe9b3b49d871071e16dad215e9af0e4dd68e42 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 18 Dec 2015 13:15:12 +0100 Subject: Add comment about the sequence when removing the pages --- app/models/project.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/models/project.rb b/app/models/project.rb index 7a5bf77c5a9..8a8aca44945 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1186,6 +1186,9 @@ class Project < ActiveRecord::Base end def remove_pages + # 1. We rename pages to temporary directory + # 2. We wait 5 minutes, due to NFS caching + # 3. We asynchronously remove pages with force temp_path = "#{path}.#{SecureRandom.hex}" if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.path) -- cgit v1.2.1 From 6c9ba469d9538c58434db492c0a955c20aba2ba1 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 18 Dec 2015 16:18:57 +0100 Subject: Bring back GitLab Pages SSL config --- lib/support/nginx/gitlab-pages | 3 ++ lib/support/nginx/gitlab-pages-ssl | 81 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 lib/support/nginx/gitlab-pages-ssl diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages index 0eeb0cd1917..33f573d0b5b 100644 --- a/lib/support/nginx/gitlab-pages +++ b/lib/support/nginx/gitlab-pages @@ -1,3 +1,6 @@ +## GitLab +## + ## Pages serving host server { listen 0.0.0.0:80; diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl new file mode 100644 index 00000000000..006610262f9 --- /dev/null +++ b/lib/support/nginx/gitlab-pages-ssl @@ -0,0 +1,81 @@ +## GitLab +## + +## Redirects all HTTP traffic to the HTTPS host +server { + ## Either remove "default_server" from the listen line below, + ## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab + ## to be served if you visit any address that your server responds to, eg. + ## the ip address of the server (http://x.x.x.x/) + listen 0.0.0.0:80; + listen [::]:80 ipv6only=on; + + ## Replace this with something like pages.gitlab.com + server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; + server_tokens off; ## Don't show the nginx version number, a security best practice + + return 301 https://$http_host$request_uri; + + access_log /var/log/nginx/gitlab_pages_access.log; + error_log /var/log/nginx/gitlab_pages_access.log; +} + +## Pages serving host +server { + listen 0.0.0.0:443 ssl; + listen [::]:443 ipv6only=on ssl; + + ## Replace this with something like pages.gitlab.com + server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; + server_tokens off; ## Don't show the nginx version number, a security best practice + root /home/git/gitlab/shared/pages/${group}; + + ## Strong SSL Security + ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ + ssl on; + ssl_certificate /etc/nginx/ssl/gitlab-pages.crt; + ssl_certificate_key /etc/nginx/ssl/gitlab-pages.key; + + # GitLab needs backwards compatible ciphers to retain compatibility with Java IDEs + ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4"; + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 5m; + + ## See app/controllers/application_controller.rb for headers set + + ## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL. + ## Replace with your ssl_trusted_certificate. For more info see: + ## - https://medium.com/devops-programming/4445f4862461 + ## - https://www.ruby-forum.com/topic/4419319 + ## - https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx + # ssl_stapling on; + # ssl_stapling_verify on; + # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt; + # resolver 208.67.222.222 208.67.222.220 valid=300s; # Can change to your DNS resolver if desired + # resolver_timeout 5s; + + ## [Optional] Generate a stronger DHE parameter: + ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096 + ## + # ssl_dhparam /etc/ssl/certs/dhparam.pem; + + ## Individual nginx logs for GitLab pages + access_log /var/log/nginx/gitlab_pages_access.log; + error_log /var/log/nginx/gitlab_pages_error.log; + + # 1. Try to get /project/ to => shared/pages/${group}/public/ or index.html + # 2. Try to get / to => shared/pages/${group}/${host}/public/ or index.html + location ~ ^/([^/]*)(/.*)?$ { + try_files "/$1/public$2" + "/$1/public$2/index.html" + "/${host}/public/${uri}" + "/${host}/public/${uri}/index.html" + =404; + } + + # Define custom error pages + error_page 403 /403.html; + error_page 404 /404.html; +} -- cgit v1.2.1 From 324fe12a125b1a766978556d7d8f2b8bb6c22e43 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 18 Dec 2015 16:40:59 +0100 Subject: Fix pages path settings option. --- config/initializers/1_settings.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index e52171f0d64..860cafad325 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -267,7 +267,7 @@ Settings.registry['path'] = File.expand_path(Settings.registry['path' # Settings['pages'] ||= Settingslogic.new({}) Settings.pages['enabled'] = false if Settings.pages['enabled'].nil? -Settings.pages['path'] ||= File.expand_path('shared/pages/', Rails.root) +Settings.pages['path'] = File.expand_path(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"), Rails.root) Settings.pages['host'] ||= "example.com" Settings.pages['https'] = false if Settings.pages['https'].nil? Settings.pages['port'] ||= Settings.pages.https ? 443 : 80 -- cgit v1.2.1 From 9c78a206ce2039cdba095c2631538f9e50c28f95 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 18 Dec 2015 18:08:41 +0200 Subject: Typo fixes, remove unnecessary information about pages path [ci skip] --- doc/pages/README.md | 15 +++------------ doc/pages/administration.md | 4 ++-- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 552b3436d77..c83fdb63e53 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -4,7 +4,7 @@ _**Note:** This feature was [introduced][ee-80] in GitLab EE 8.3_ With GitLab Pages you can host for free your static websites on GitLab. Combined with the power of GitLab CI and the help of GitLab Runner you can -deploy static pages for your individual projects your user or your group. +deploy static pages for your individual projects, your user or your group. ## Enable the pages feature in your GitLab EE instance @@ -101,19 +101,10 @@ in the artifacts. ## Frequently Asked Questions -**Q:** Where are my generated pages stored? - -**A:** All content is located by default under `shared/pages/` in the root -directory of the GitLab installation. To be exact, all specific projects under -a namespace are stored in `shared/pages/${namespace}/${project}/public/` and -all user/group pages in `shared/pages/${namespace}/${namespace}/public/`. - ---- - **Q:** Can I download my generated pages? -**A:** Sure. All you need is to download the artifacts archive from the build - page. +**A:** Sure. All you need to do is download the artifacts archive from the + build page. --- diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 9c24fee4f1a..6c5436842fe 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -30,8 +30,8 @@ host that GitLab runs. For example, an entry would look like this: where `gitlab.io` is the domain under which GitLab Pages will be served and `1.2.3.4` is the IP address of your GitLab instance. -It is strongly advised to **not** use the GitLab domain to serve user pages. -For more information see the [security section](#security). +You should not use the GitLab domain to serve user pages. For more information +see the [security section](#security). ### Omnibus package installations -- cgit v1.2.1 From c66b15803a5674a5b97968ae9479b2bd293ca34f Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 18 Dec 2015 17:08:02 +0100 Subject: Fix confusing implementation detail in nginx config about how gitlab-pages work [ci skip] --- lib/support/nginx/gitlab-pages | 4 ++-- lib/support/nginx/gitlab-pages-ssl | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages index 33f573d0b5b..ed4f7e4316a 100644 --- a/lib/support/nginx/gitlab-pages +++ b/lib/support/nginx/gitlab-pages @@ -14,8 +14,8 @@ server { access_log /var/log/nginx/gitlab_pages_access.log; error_log /var/log/nginx/gitlab_pages_error.log; - # 1. Try to get /project/ to => shared/pages/${group}/public/ or index.html - # 2. Try to get / to => shared/pages/${group}/${host}/public/ or index.html + # 1. Try to get /path/ from shared/pages/${group}/${path}/public/ + # 2. Try to get / from shared/pages/${group}/${host}/public/ location ~ ^/([^/]*)(/.*)?$ { try_files "/$1/public$2" "/$1/public$2/index.html" diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl index 006610262f9..dcbbee4042a 100644 --- a/lib/support/nginx/gitlab-pages-ssl +++ b/lib/support/nginx/gitlab-pages-ssl @@ -53,8 +53,6 @@ server { # ssl_stapling on; # ssl_stapling_verify on; # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt; - # resolver 208.67.222.222 208.67.222.220 valid=300s; # Can change to your DNS resolver if desired - # resolver_timeout 5s; ## [Optional] Generate a stronger DHE parameter: ## sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096 @@ -65,8 +63,8 @@ server { access_log /var/log/nginx/gitlab_pages_access.log; error_log /var/log/nginx/gitlab_pages_error.log; - # 1. Try to get /project/ to => shared/pages/${group}/public/ or index.html - # 2. Try to get / to => shared/pages/${group}/${host}/public/ or index.html + # 1. Try to get /path/ from shared/pages/${group}/${path}/public/ + # 2. Try to get / from shared/pages/${group}/${host}/public/ location ~ ^/([^/]*)(/.*)?$ { try_files "/$1/public$2" "/$1/public$2/index.html" -- cgit v1.2.1 From 0bb480dcc738889e56397a7c05950ce8d73caedf Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 22 Dec 2015 14:24:54 +0200 Subject: Add note about shared runners [ci skip] --- doc/pages/administration.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 6c5436842fe..529a1450fd3 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -14,6 +14,9 @@ GitLab EE instance. will be served 1. Pages use a separate Nginx configuration file which needs to be explicitly added in the server under which GitLab EE runs +1. Optionally but recommended, you can add some + [shared runners](../ci/runners/README.md) so that your users don't have to + bring their own. Both of these settings are described in detail in the sections below. -- cgit v1.2.1 From 8aba28e14dd7c66b17e0bc4a4230d0f5586155d0 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 22 Dec 2015 14:26:06 +0200 Subject: Clarify some things in Pages [ci skip] * Pages are enabled by default on each project * Add note about using the `only` parameter --- doc/pages/README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index c83fdb63e53..ea975ec8149 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -17,13 +17,14 @@ The steps that are performed from the initialization of a project to the creation of the static content, can be summed up to: 1. Create project (its name could be specific according to the case) -1. Enable the GitLab Pages feature under the project's settings 1. Provide a specific job in `.gitlab-ci.yml` 1. GitLab Runner builds the project 1. GitLab CI uploads the artifacts 1. Nginx serves the content As a user, you should normally be concerned only with the first three items. +If [shared runners](../ci/runners/README.md) are enabled by your GitLab +administrator, you should be able to use them instead of bringing your own. In general there are four kinds of pages one might create. This is better explained with an example so let's make some assumptions. @@ -68,6 +69,13 @@ To make use of GitLab Pages, your `.gitlab-ci.yml` must follow the rules below: 1. Any static content must be placed under a `public/` directory 1. `artifacts` with a path to the `public/` directory must be defined +Be aware that Pages are by default branch/tag agnostic and their deployment +relies solely on what you specify in `gitlab-ci.yml`. If you don't limit the +`pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except), +whenever a new commit is pushed to whatever branch or tag, the Pages will be +overwritten. In the examples below, we limit the Pages to be deployed whenever +a commit is pushed only on the `master` branch, which is advisable to do so. + The pages are created after the build completes successfully and the artifacts for the `pages` job are uploaded to GitLab. @@ -84,6 +92,8 @@ pages: artifacts: paths: - public + only: + - master ``` ## Example projects @@ -101,10 +111,9 @@ in the artifacts. ## Frequently Asked Questions -**Q:** Can I download my generated pages? +**Q: Can I download my generated pages?** -**A:** Sure. All you need to do is download the artifacts archive from the - build page. +Sure. All you need to do is download the artifacts archive from the build page. --- -- cgit v1.2.1 From a7fa7f269a3a6b7562880d2a780013cbab0a5110 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 22 Dec 2015 16:57:22 +0200 Subject: Add missing dot in .gitlab-ci.yml [ci skip] --- doc/pages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index ea975ec8149..f6eb8ccb7a7 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -70,7 +70,7 @@ To make use of GitLab Pages, your `.gitlab-ci.yml` must follow the rules below: 1. `artifacts` with a path to the `public/` directory must be defined Be aware that Pages are by default branch/tag agnostic and their deployment -relies solely on what you specify in `gitlab-ci.yml`. If you don't limit the +relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the `pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except), whenever a new commit is pushed to whatever branch or tag, the Pages will be overwritten. In the examples below, we limit the Pages to be deployed whenever -- cgit v1.2.1 From 1d159ffbf807e3853f45faa1e7075b9a6546953f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Tue, 22 Dec 2015 18:07:14 +0000 Subject: Fix URL to GitLab pages documentation --- app/views/projects/edit.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index f9c6809b903..f4c1db1b93d 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -147,7 +147,7 @@ - else %p Learn how to upload your static site and have it served by - GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/pages/README.html", target: :blank}. + GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}. %p In the example below we define a special job named %code pages -- cgit v1.2.1 From 6e70870a2e80ea092f8528f727753184eb3265fb Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 15 Jan 2016 12:21:52 +0100 Subject: Move most of PagesWorker logic UpdatePagesService --- app/models/ci/build.rb | 2 +- app/services/pages_service.rb | 15 +++ app/services/projects/update_pages_service.rb | 132 ++++++++++++++++++++ app/services/update_pages_service.rb | 15 --- app/workers/pages_worker.rb | 133 +-------------------- spec/services/pages_service_spec.rb | 47 ++++++++ spec/services/projects/update_pages_worker_spec.rb | 70 +++++++++++ spec/services/update_pages_service_spec.rb | 47 -------- spec/workers/pages_worker_spec.rb | 65 ---------- 9 files changed, 267 insertions(+), 259 deletions(-) create mode 100644 app/services/pages_service.rb create mode 100644 app/services/projects/update_pages_service.rb delete mode 100644 app/services/update_pages_service.rb create mode 100644 spec/services/pages_service_spec.rb create mode 100644 spec/services/projects/update_pages_worker_spec.rb delete mode 100644 spec/services/update_pages_service_spec.rb delete mode 100644 spec/workers/pages_worker_spec.rb diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 095a346f337..da8e66e5f6e 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -457,7 +457,7 @@ module Ci build_data = Gitlab::DataBuilder::Build.build(self) project.execute_hooks(build_data.dup, :build_hooks) project.execute_services(build_data.dup, :build_hooks) - UpdatePagesService.new(build_data).execute + PagesService.new(build_data).execute project.running_or_pending_build_count(force: true) end diff --git a/app/services/pages_service.rb b/app/services/pages_service.rb new file mode 100644 index 00000000000..446eeb34d3b --- /dev/null +++ b/app/services/pages_service.rb @@ -0,0 +1,15 @@ +class PagesService + attr_reader :data + + def initialize(data) + @data = data + end + + def execute + return unless Settings.pages.enabled + return unless data[:build_name] == 'pages' + return unless data[:build_status] == 'success' + + PagesWorker.perform_async(:deploy, data[:build_id]) + end +end diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb new file mode 100644 index 00000000000..e1bb4c92e40 --- /dev/null +++ b/app/services/projects/update_pages_service.rb @@ -0,0 +1,132 @@ +module Projects + class UpdatePagesService < BaseService + BLOCK_SIZE = 32.kilobytes + MAX_SIZE = 1.terabyte + + attr_reader :build + + def initialize(project, build) + @project, @build = project, build + end + + def execute + # Create status notifying the deployment of pages + @status = create_status + @status.run! + + raise 'missing pages artifacts' unless build.artifacts_file? + raise 'pages are outdated' unless latest? + + # Create temporary directory in which we will extract the artifacts + FileUtils.mkdir_p(tmp_path) + Dir.mktmpdir(nil, tmp_path) do |archive_path| + extract_archive!(archive_path) + + # Check if we did extract public directory + archive_public_path = File.join(archive_path, 'public') + raise 'pages miss the public folder' unless Dir.exists?(archive_public_path) + raise 'pages are outdated' unless latest? + + deploy_page!(archive_public_path) + success + end + rescue => e + error(e.message) + end + + private + + def success + @status.success + super + end + + def error(message, http_status = nil) + @status.allow_failure = !latest? + @status.description = message + @status.drop + super + end + + def create_status + GenericCommitStatus.new( + project: project, + commit: build.commit, + user: build.user, + ref: build.ref, + stage: 'deploy', + name: 'pages:deploy' + ) + end + + def extract_archive!(temp_path) + results = Open3.pipeline(%W(gunzip -c #{artifacts}), + %W(dd bs=#{BLOCK_SIZE} count=#{blocks}), + %W(tar -x -C #{temp_path} public/), + err: '/dev/null') + raise 'pages failed to extract' unless results.compact.all?(&:success?) + end + + def deploy_page!(archive_public_path) + # Do atomic move of pages + # Move and removal may not be atomic, but they are significantly faster then extracting and removal + # 1. We move deployed public to previous public path (file removal is slow) + # 2. We move temporary public to be deployed public + # 3. We remove previous public path + FileUtils.mkdir_p(pages_path) + begin + FileUtils.move(public_path, previous_public_path) + rescue + end + FileUtils.move(archive_public_path, public_path) + ensure + FileUtils.rm_r(previous_public_path, force: true) + end + + def latest? + # check if sha for the ref is still the most recent one + # this helps in case when multiple deployments happens + sha == latest_sha + end + + def blocks + # Calculate dd parameters: we limit the size of pages + max_size = current_application_settings.max_pages_size.megabytes + max_size ||= MAX_SIZE + blocks = 1 + max_size / BLOCK_SIZE + blocks + end + + def tmp_path + @tmp_path ||= File.join(Settings.pages.path, 'tmp') + end + + def pages_path + @pages_path ||= project.pages_path + end + + def public_path + @public_path ||= File.join(pages_path, 'public') + end + + def previous_public_path + @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}") + end + + def ref + build.ref + end + + def artifacts + build.artifacts_file.path + end + + def latest_sha + project.commit(build.ref).try(:sha).to_s + end + + def sha + build.sha + end + end +end diff --git a/app/services/update_pages_service.rb b/app/services/update_pages_service.rb deleted file mode 100644 index 39f08b2a03d..00000000000 --- a/app/services/update_pages_service.rb +++ /dev/null @@ -1,15 +0,0 @@ -class UpdatePagesService - attr_reader :data - - def initialize(data) - @data = data - end - - def execute - return unless Settings.pages.enabled - return unless data[:build_name] == 'pages' - return unless data[:build_status] == 'success' - - PagesWorker.perform_async(:deploy, data[:build_id]) - end -end diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index ff765a6c13c..8c99e8dbe76 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -1,9 +1,5 @@ class PagesWorker include Sidekiq::Worker - include Gitlab::CurrentSettings - - BLOCK_SIZE = 32.kilobytes - MAX_SIZE = 1.terabyte sidekiq_options queue: :pages, retry: false @@ -12,137 +8,12 @@ class PagesWorker end def deploy(build_id) - @build_id = build_id - return unless valid? - - # Create status notifying the deployment of pages - @status = create_status - @status.run! - - raise 'pages are outdated' unless latest? - - # Create temporary directory in which we will extract the artifacts - FileUtils.mkdir_p(tmp_path) - Dir.mktmpdir(nil, tmp_path) do |archive_path| - extract_archive!(archive_path) - - # Check if we did extract public directory - archive_public_path = File.join(archive_path, 'public') - raise 'pages miss the public folder' unless Dir.exists?(archive_public_path) - raise 'pages are outdated' unless latest? - - deploy_page!(archive_public_path) - - @status.success - end - rescue => e - fail(e.message, !latest?) - return false + build = Ci::Build.find_by(id: build_id) + Projects::UpdatePagesService.new(build.project, build).execute end def remove(namespace_path, project_path) full_path = File.join(Settings.pages.path, namespace_path, project_path) FileUtils.rm_r(full_path, force: true) end - - private - - def create_status - GenericCommitStatus.new( - project: project, - commit: build.commit, - user: build.user, - ref: build.ref, - stage: 'deploy', - name: 'pages:deploy' - ) - end - - def extract_archive!(temp_path) - results = Open3.pipeline(%W(gunzip -c #{artifacts}), - %W(dd bs=#{BLOCK_SIZE} count=#{blocks}), - %W(tar -x -C #{temp_path} public/), - err: '/dev/null') - raise 'pages failed to extract' unless results.compact.all?(&:success?) - end - - def deploy_page!(archive_public_path) - # Do atomic move of pages - # Move and removal may not be atomic, but they are significantly faster then extracting and removal - # 1. We move deployed public to previous public path (file removal is slow) - # 2. We move temporary public to be deployed public - # 3. We remove previous public path - FileUtils.mkdir_p(pages_path) - begin - FileUtils.move(public_path, previous_public_path) - rescue - end - FileUtils.move(archive_public_path, public_path) - ensure - FileUtils.rm_r(previous_public_path, force: true) - end - - def fail(message, allow_failure = true) - @status.allow_failure = allow_failure - @status.description = message - @status.drop - end - - def valid? - build && build.artifacts_file? - end - - def latest? - # check if sha for the ref is still the most recent one - # this helps in case when multiple deployments happens - sha == latest_sha - end - - def blocks - # Calculate dd parameters: we limit the size of pages - max_size = current_application_settings.max_pages_size.megabytes - max_size ||= MAX_SIZE - blocks = 1 + max_size / BLOCK_SIZE - blocks - end - - def build - @build ||= Ci::Build.find_by(id: @build_id) - end - - def project - @project ||= build.project - end - - def tmp_path - @tmp_path ||= File.join(Settings.pages.path, 'tmp') - end - - def pages_path - @pages_path ||= project.pages_path - end - - def public_path - @public_path ||= File.join(pages_path, 'public') - end - - def previous_public_path - @previous_public_path ||= File.join(pages_path, "public.#{SecureRandom.hex}") - end - - def ref - build.ref - end - - def artifacts - build.artifacts_file.path - end - - def latest_sha - project.commit(build.ref).try(:sha).to_s - end - - def sha - build.sha - end end diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb new file mode 100644 index 00000000000..e6ad93358a0 --- /dev/null +++ b/spec/services/pages_service_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe PagesService, services: true do + let(:build) { create(:ci_build) } + let(:data) { Gitlab::BuildDataBuilder.build(build) } + let(:service) { PagesService.new(data) } + + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + end + + context 'execute asynchronously for pages job' do + before { build.name = 'pages' } + + context 'on success' do + before { build.success } + + it 'should execute worker' do + expect(PagesWorker).to receive(:perform_async) + service.execute + end + end + + %w(pending running failed canceled).each do |status| + context "on #{status}" do + before { build.status = status } + + it 'should not execute worker' do + expect(PagesWorker).to_not receive(:perform_async) + service.execute + end + end + end + end + + context 'for other jobs' do + before do + build.name = 'other job' + build.success + end + + it 'should not execute worker' do + expect(PagesWorker).to_not receive(:perform_async) + service.execute + end + end +end diff --git a/spec/services/projects/update_pages_worker_spec.rb b/spec/services/projects/update_pages_worker_spec.rb new file mode 100644 index 00000000000..0607c025b9e --- /dev/null +++ b/spec/services/projects/update_pages_worker_spec.rb @@ -0,0 +1,70 @@ +require "spec_helper" + +describe Projects::UpdatePagesService do + let(:project) { create :project } + let(:commit) { create :ci_commit, project: project, sha: project.commit('HEAD').sha } + let(:build) { create :ci_build, commit: commit, ref: 'HEAD' } + let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/pages.tar.gz', 'application/octet-stream') } + let(:empty_file) { fixture_file_upload(Rails.root + 'spec/fixtures/pages_empty.tar.gz', 'application/octet-stream') } + let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'application/octet-stream') } + + subject { described_class.new(project, build) } + + before do + project.remove_pages + end + + context 'for valid file' do + before { build.update_attributes(artifacts_file: file) } + + it 'succeeds' do + expect(project.pages_url).to be_nil + expect(execute).to eq(:success) + expect(project.pages_url).to_not be_nil + end + + it 'limits pages size' do + stub_application_setting(max_pages_size: 1) + expect(execute).to_not eq(:success) + end + + it 'removes pages after destroy' do + expect(PagesWorker).to receive(:perform_in) + expect(project.pages_url).to be_nil + expect(execute).to eq(:success) + expect(project.pages_url).to_not be_nil + project.destroy + expect(Dir.exist?(project.public_pages_path)).to be_falsey + end + end + + it 'fails to remove project pages when no pages is deployed' do + expect(PagesWorker).to_not receive(:perform_in) + expect(project.pages_url).to be_nil + project.destroy + end + + it 'fails if no artifacts' do + expect(execute).to_not eq(:success) + end + + it 'fails for empty file fails' do + build.update_attributes(artifacts_file: empty_file) + expect(execute).to_not eq(:success) + end + + it 'fails for invalid archive' do + build.update_attributes(artifacts_file: invalid_file) + expect(execute).to_not eq(:success) + end + + it 'fails if sha on branch is not latest' do + commit.update_attributes(sha: 'old_sha') + build.update_attributes(artifacts_file: file) + expect(execute).to_not eq(:success) + end + + def execute + subject.execute[:status] + end +end diff --git a/spec/services/update_pages_service_spec.rb b/spec/services/update_pages_service_spec.rb deleted file mode 100644 index cf1ca15da44..00000000000 --- a/spec/services/update_pages_service_spec.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'spec_helper' - -describe UpdatePagesService, services: true do - let(:build) { create(:ci_build) } - let(:data) { Gitlab::BuildDataBuilder.build(build) } - let(:service) { UpdatePagesService.new(data) } - - before do - allow(Gitlab.config.pages).to receive(:enabled).and_return(true) - end - - context 'execute asynchronously for pages job' do - before { build.name = 'pages' } - - context 'on success' do - before { build.success } - - it 'should execute worker' do - expect(PagesWorker).to receive(:perform_async) - service.execute - end - end - - %w(pending running failed canceled).each do |status| - context "on #{status}" do - before { build.status = status } - - it 'should not execute worker' do - expect(PagesWorker).to_not receive(:perform_async) - service.execute - end - end - end - end - - context 'for other jobs' do - before do - build.name = 'other job' - build.success - end - - it 'should not execute worker' do - expect(PagesWorker).to_not receive(:perform_async) - service.execute - end - end -end diff --git a/spec/workers/pages_worker_spec.rb b/spec/workers/pages_worker_spec.rb deleted file mode 100644 index 85592154598..00000000000 --- a/spec/workers/pages_worker_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -require "spec_helper" - -describe PagesWorker do - let(:project) { create :project } - let(:commit) { create :ci_commit, project: project, sha: project.commit('HEAD').sha } - let(:build) { create :ci_build, commit: commit, ref: 'HEAD' } - let(:worker) { PagesWorker.new } - let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/pages.tar.gz', 'application/octet-stream') } - let(:empty_file) { fixture_file_upload(Rails.root + 'spec/fixtures/pages_empty.tar.gz', 'application/octet-stream') } - let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'application/octet-stream') } - - before do - project.remove_pages - end - - context 'for valid file' do - before { build.update_attributes(artifacts_file: file) } - - it 'succeeds' do - expect(project.pages_url).to be_nil - expect(worker.deploy(build.id)).to be_truthy - expect(project.pages_url).to_not be_nil - end - - it 'limits pages size' do - stub_application_setting(max_pages_size: 1) - expect(worker.deploy(build.id)).to_not be_truthy - end - - it 'removes pages after destroy' do - expect(PagesWorker).to receive(:perform_in) - expect(project.pages_url).to be_nil - expect(worker.deploy(build.id)).to be_truthy - expect(project.pages_url).to_not be_nil - project.destroy - expect(Dir.exist?(project.public_pages_path)).to be_falsey - end - end - - it 'fails to remove project pages when no pages is deployed' do - expect(PagesWorker).to_not receive(:perform_in) - expect(project.pages_url).to be_nil - project.destroy - end - - it 'fails if no artifacts' do - expect(worker.deploy(build.id)).to_not be_truthy - end - - it 'fails for empty file fails' do - build.update_attributes(artifacts_file: empty_file) - expect(worker.deploy(build.id)).to_not be_truthy - end - - it 'fails for invalid archive' do - build.update_attributes(artifacts_file: invalid_file) - expect(worker.deploy(build.id)).to_not be_truthy - end - - it 'fails if sha on branch is not latest' do - commit.update_attributes(sha: 'old_sha') - build.update_attributes(artifacts_file: file) - expect(worker.deploy(build.id)).to_not be_truthy - end -end -- cgit v1.2.1 From c4c8ca04052aaf7d37c2335066381b536df68427 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 20 Jan 2016 21:49:26 +0100 Subject: Added support for zip archives in pages The ZIP archive size is calculated from artifacts metadata that should get uploaded for new artifacts --- app/services/projects/update_pages_service.rb | 41 ++++++++++-- spec/fixtures/pages.zip | Bin 0 -> 1851 bytes spec/fixtures/pages.zip.meta | Bin 0 -> 225 bytes spec/fixtures/pages_empty.zip | Bin 0 -> 160 bytes spec/fixtures/pages_empty.zip.meta | Bin 0 -> 116 bytes spec/services/projects/update_pages_worker_spec.rb | 74 ++++++++++++--------- 6 files changed, 78 insertions(+), 37 deletions(-) create mode 100644 spec/fixtures/pages.zip create mode 100644 spec/fixtures/pages.zip.meta create mode 100644 spec/fixtures/pages_empty.zip create mode 100644 spec/fixtures/pages_empty.zip.meta diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index e1bb4c92e40..ceabd29fd52 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -2,6 +2,7 @@ module Projects class UpdatePagesService < BaseService BLOCK_SIZE = 32.kilobytes MAX_SIZE = 1.terabyte + SITE_PATH = 'public/' attr_reader :build @@ -60,13 +61,42 @@ module Projects end def extract_archive!(temp_path) + if artifacts.ends_with?('.tar.gz') || artifacts.ends_with?('.tgz') + extract_tar_archive!(temp_path) + elsif artifacts.ends_with?('.zip') + extract_zip_archive!(temp_path) + else + raise 'unsupported artifacts format' + end + end + + def extract_tar_archive!(temp_path) results = Open3.pipeline(%W(gunzip -c #{artifacts}), %W(dd bs=#{BLOCK_SIZE} count=#{blocks}), - %W(tar -x -C #{temp_path} public/), + %W(tar -x -C #{temp_path} #{SITE_PATH}), err: '/dev/null') raise 'pages failed to extract' unless results.compact.all?(&:success?) end + def extract_zip_archive!(temp_path) + raise 'missing artifacts metadata' unless build.artifacts_metadata? + + # Calculate page size after extract + public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true) + + if public_entry.total_size > max_size + raise "artifacts for pages are too large: #{total_size}" + end + + # Requires UnZip at least 6.00 Info-ZIP. + # -n never overwrite existing files + # We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories + site_path = File.join(SITE_PATH, '*') + unless system(*%W(unzip -n #{artifacts} #{site_path} -d #{temp_path})) + raise 'pages failed to extract' + end + end + def deploy_page!(archive_public_path) # Do atomic move of pages # Move and removal may not be atomic, but they are significantly faster then extracting and removal @@ -91,10 +121,11 @@ module Projects def blocks # Calculate dd parameters: we limit the size of pages - max_size = current_application_settings.max_pages_size.megabytes - max_size ||= MAX_SIZE - blocks = 1 + max_size / BLOCK_SIZE - blocks + 1 + max_size / BLOCK_SIZE + end + + def max_size + current_application_settings.max_pages_size.megabytes || MAX_SIZE end def tmp_path diff --git a/spec/fixtures/pages.zip b/spec/fixtures/pages.zip new file mode 100644 index 00000000000..9558fcd4b94 Binary files /dev/null and b/spec/fixtures/pages.zip differ diff --git a/spec/fixtures/pages.zip.meta b/spec/fixtures/pages.zip.meta new file mode 100644 index 00000000000..1e6198a15f0 Binary files /dev/null and b/spec/fixtures/pages.zip.meta differ diff --git a/spec/fixtures/pages_empty.zip b/spec/fixtures/pages_empty.zip new file mode 100644 index 00000000000..db3f0334c12 Binary files /dev/null and b/spec/fixtures/pages_empty.zip differ diff --git a/spec/fixtures/pages_empty.zip.meta b/spec/fixtures/pages_empty.zip.meta new file mode 100644 index 00000000000..d0b93b3b9c0 Binary files /dev/null and b/spec/fixtures/pages_empty.zip.meta differ diff --git a/spec/services/projects/update_pages_worker_spec.rb b/spec/services/projects/update_pages_worker_spec.rb index 0607c025b9e..68e66866340 100644 --- a/spec/services/projects/update_pages_worker_spec.rb +++ b/spec/services/projects/update_pages_worker_spec.rb @@ -4,9 +4,7 @@ describe Projects::UpdatePagesService do let(:project) { create :project } let(:commit) { create :ci_commit, project: project, sha: project.commit('HEAD').sha } let(:build) { create :ci_build, commit: commit, ref: 'HEAD' } - let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/pages.tar.gz', 'application/octet-stream') } - let(:empty_file) { fixture_file_upload(Rails.root + 'spec/fixtures/pages_empty.tar.gz', 'application/octet-stream') } - let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'application/octet-stream') } + let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') } subject { described_class.new(project, build) } @@ -14,27 +12,50 @@ describe Projects::UpdatePagesService do project.remove_pages end - context 'for valid file' do - before { build.update_attributes(artifacts_file: file) } + %w(tar.gz zip).each do |format| + context "for valid #{format}" do + let(:file) { fixture_file_upload(Rails.root + "spec/fixtures/pages.#{format}") } + let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{format}") } + let(:metadata) do + filename = Rails.root + "spec/fixtures/pages.#{format}.meta" + fixture_file_upload(filename) if File.exists?(filename) + end - it 'succeeds' do - expect(project.pages_url).to be_nil - expect(execute).to eq(:success) - expect(project.pages_url).to_not be_nil - end + before do + build.update_attributes(artifacts_file: file) + build.update_attributes(artifacts_metadata: metadata) + end - it 'limits pages size' do - stub_application_setting(max_pages_size: 1) - expect(execute).to_not eq(:success) - end + it 'succeeds' do + expect(project.pages_url).to be_nil + expect(execute).to eq(:success) + expect(project.pages_url).to_not be_nil + end + + it 'limits pages size' do + stub_application_setting(max_pages_size: 1) + expect(execute).to_not eq(:success) + end - it 'removes pages after destroy' do - expect(PagesWorker).to receive(:perform_in) - expect(project.pages_url).to be_nil - expect(execute).to eq(:success) - expect(project.pages_url).to_not be_nil - project.destroy - expect(Dir.exist?(project.public_pages_path)).to be_falsey + it 'removes pages after destroy' do + expect(PagesWorker).to receive(:perform_in) + expect(project.pages_url).to be_nil + expect(execute).to eq(:success) + expect(project.pages_url).to_not be_nil + project.destroy + expect(Dir.exist?(project.public_pages_path)).to be_falsey + end + + it 'fails if sha on branch is not latest' do + commit.update_attributes(sha: 'old_sha') + build.update_attributes(artifacts_file: file) + expect(execute).to_not eq(:success) + end + + it 'fails for empty file fails' do + build.update_attributes(artifacts_file: empty_file) + expect(execute).to_not eq(:success) + end end end @@ -48,21 +69,10 @@ describe Projects::UpdatePagesService do expect(execute).to_not eq(:success) end - it 'fails for empty file fails' do - build.update_attributes(artifacts_file: empty_file) - expect(execute).to_not eq(:success) - end - it 'fails for invalid archive' do build.update_attributes(artifacts_file: invalid_file) expect(execute).to_not eq(:success) end - - it 'fails if sha on branch is not latest' do - commit.update_attributes(sha: 'old_sha') - build.update_attributes(artifacts_file: file) - expect(execute).to_not eq(:success) - end def execute subject.execute[:status] -- cgit v1.2.1 From 5f7257c27dace1dcb9d3eb4732caf68f061a8d68 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 9 Feb 2016 18:06:55 +0100 Subject: Initial work on GitLab Pages update --- Gemfile | 3 + Gemfile.lock | 4 + app/controllers/projects/pages_controller.rb | 94 ++++++++++++++++++++++ app/controllers/projects_controller.rb | 10 --- app/helpers/projects_helper.rb | 8 ++ app/models/project.rb | 51 ++++++++++-- app/policies/project_policy.rb | 1 + .../projects/update_pages_configuration_service.rb | 53 ++++++++++++ app/validators/certificate_key_validator.rb | 24 ++++++ app/validators/certificate_validator.rb | 30 +++++++ app/views/layouts/nav/_project_settings.html.haml | 4 + app/views/projects/pages/_access.html.haml | 34 ++++++++ app/views/projects/pages/_destroy.haml | 10 +++ app/views/projects/pages/_disabled.html.haml | 4 + app/views/projects/pages/_form.html.haml | 35 ++++++++ .../projects/pages/_remove_certificate.html.haml | 16 ++++ .../projects/pages/_upload_certificate.html.haml | 32 ++++++++ app/views/projects/pages/_use.html.haml | 18 +++++ app/views/projects/pages/show.html.haml | 18 +++++ app/workers/pages_worker.rb | 6 +- config/initializers/1_settings.rb | 1 + config/routes/project.rb | 5 +- ...09125808_add_pages_custom_domain_to_projects.rb | 10 +++ 23 files changed, 451 insertions(+), 20 deletions(-) create mode 100644 app/controllers/projects/pages_controller.rb create mode 100644 app/services/projects/update_pages_configuration_service.rb create mode 100644 app/validators/certificate_key_validator.rb create mode 100644 app/validators/certificate_validator.rb create mode 100644 app/views/projects/pages/_access.html.haml create mode 100644 app/views/projects/pages/_destroy.haml create mode 100644 app/views/projects/pages/_disabled.html.haml create mode 100644 app/views/projects/pages/_form.html.haml create mode 100644 app/views/projects/pages/_remove_certificate.html.haml create mode 100644 app/views/projects/pages/_upload_certificate.html.haml create mode 100644 app/views/projects/pages/_use.html.haml create mode 100644 app/views/projects/pages/show.html.haml create mode 100644 db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb diff --git a/Gemfile b/Gemfile index dd7c93c5a75..bc1b13c7331 100644 --- a/Gemfile +++ b/Gemfile @@ -48,6 +48,9 @@ gem 'rqrcode-rails3', '~> 0.1.7' gem 'attr_encrypted', '~> 3.0.0' gem 'u2f', '~> 0.2.1' +# GitLab Pages +gem 'validates_hostname', '~> 1.0.0' + # Browser detection gem 'browser', '~> 2.2' diff --git a/Gemfile.lock b/Gemfile.lock index 3b207d19d1f..6263b02b041 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -799,6 +799,9 @@ GEM get_process_mem (~> 0) unicorn (>= 4, < 6) uniform_notifier (1.10.0) + validates_hostname (1.0.5) + activerecord (>= 3.0) + activesupport (>= 3.0) version_sorter (2.1.0) virtus (1.0.5) axiom-types (~> 0.1) @@ -1014,6 +1017,7 @@ DEPENDENCIES unf (~> 0.1.4) unicorn (~> 5.1.0) unicorn-worker-killer (~> 0.4.4) + validates_hostname (~> 1.0.0) version_sorter (~> 2.1.0) virtus (~> 1.0.1) vmstat (~> 2.3.0) diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb new file mode 100644 index 00000000000..ef0ed505142 --- /dev/null +++ b/app/controllers/projects/pages_controller.rb @@ -0,0 +1,94 @@ +class Projects::PagesController < Projects::ApplicationController + layout 'project_settings' + + before_action :authorize_update_pages!, except: [:show] + before_action :authorize_remove_pages!, only: :destroy + + helper_method :valid_certificate?, :valid_certificate_key? + helper_method :valid_key_for_certificiate?, :valid_certificate_intermediates? + helper_method :certificate, :certificate_key + + def show + end + + def update + if @project.update_attributes(pages_params) + redirect_to namespace_project_pages_path(@project.namespace, @project) + else + render 'show' + end + end + + def certificate + @project.remove_pages_certificate + end + + def destroy + @project.remove_pages + + respond_to do |format| + format.html { redirect_to project_path(@project) } + end + end + + private + + def pages_params + params.require(:project).permit( + :pages_custom_certificate, + :pages_custom_certificate_key, + :pages_custom_domain, + :pages_redirect_http, + ) + end + + def valid_certificate? + certificate.present? + end + + def valid_certificate_key? + certificate_key.present? + end + + def valid_key_for_certificiate? + return false unless certificate + return false unless certificate_key + + certificate.verify(certificate_key) + rescue OpenSSL::X509::CertificateError + false + end + + def valid_certificate_intermediates? + return false unless certificate + + store = OpenSSL::X509::Store.new + store.set_default_paths + + # This forces to load all intermediate certificates stored in `pages_custom_certificate` + Tempfile.open('project_certificate') do |f| + f.write(@project.pages_custom_certificate) + f.flush + store.add_file(f.path) + end + + store.verify(certificate) + rescue OpenSSL::X509::StoreError + false + end + + def certificate + return unless @project.pages_custom_certificate + + @certificate ||= OpenSSL::X509::Certificate.new(@project.pages_custom_certificate) + rescue OpenSSL::X509::CertificateError + nil + end + + def certificate_key + return unless @project.pages_custom_certificate_key + @certificate_key ||= OpenSSL::PKey::RSA.new(@project.pages_custom_certificate_key) + rescue OpenSSL::PKey::PKeyError + nil + end +end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 123dc179e73..444ff837bb3 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -151,16 +151,6 @@ class ProjectsController < Projects::ApplicationController end end - def remove_pages - return access_denied! unless can?(current_user, :remove_pages, @project) - - @project.remove_pages - - respond_to do |format| - format.html { redirect_to project_path(@project) } - end - end - def housekeeping ::Projects::HousekeepingService.new(@project).execute diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index eb98204285d..63aa182502d 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -81,6 +81,14 @@ module ProjectsHelper "You are going to remove the fork relationship to source project #{@project.forked_from_project.name_with_namespace}. Are you ABSOLUTELY sure?" end + def remove_pages_message(project) + "You are going to remove the pages for #{project.name_with_namespace}.\n Are you ABSOLUTELY sure?" + end + + def remove_pages_certificate_message(project) + "You are going to remove a certificates for #{project.name_with_namespace}.\n Are you ABSOLUTELY sure?" + end + def project_nav_tabs @nav_tabs ||= get_project_nav_tabs(@project, current_user) end diff --git a/app/models/project.rb b/app/models/project.rb index 8a8aca44945..34618817fb6 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -76,6 +76,8 @@ class Project < ActiveRecord::Base attr_accessor :new_default_branch attr_accessor :old_path_with_namespace + attr_encrypted :pages_custom_certificate_key, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base + alias_attribute :title, :name # Relations @@ -205,6 +207,11 @@ class Project < ActiveRecord::Base presence: true, inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } + validates :pages_custom_domain, hostname: true, allow_blank: true, allow_nil: true + validates_uniqueness_of :pages_custom_domain, allow_nil: true, allow_blank: true + validates :pages_custom_certificate, certificate: { intermediate: true } + validates :pages_custom_certificate_key, certificate_key: true + add_authentication_token_field :runners_token before_save :ensure_runners_token @@ -1164,16 +1171,27 @@ class Project < ActiveRecord::Base end def pages_url - if Dir.exist?(public_pages_path) - host = "#{namespace.path}.#{Settings.pages.host}" - url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix| - "#{prefix}#{namespace.path}." - end + return unless Dir.exist?(public_pages_path) + + host = "#{namespace.path}.#{Settings.pages.host}" + url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix| + "#{prefix}#{namespace.path}." + end + + # If the project path is the same as host, leave the short version + return url if host == path + + "#{url}/#{path}" + end - # If the project path is the same as host, leave the short version - return url if host == path + def pages_custom_url + return unless pages_custom_domain + return unless Dir.exist?(public_pages_path) - "#{url}/#{path}" + if Gitlab.config.pages.https + return "https://#{pages_custom_domain}" + else + return "http://#{pages_custom_domain}" end end @@ -1185,6 +1203,15 @@ class Project < ActiveRecord::Base File.join(pages_path, 'public') end + def remove_pages_certificate + update( + pages_custom_certificate: nil, + pages_custom_certificate_key: nil + ) + + UpdatePagesConfigurationService.new(self).execute + end + def remove_pages # 1. We rename pages to temporary directory # 2. We wait 5 minutes, due to NFS caching @@ -1194,6 +1221,14 @@ class Project < ActiveRecord::Base if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.path) PagesWorker.perform_in(5.minutes, :remove, namespace.path, temp_path) end + + update( + pages_custom_certificate: nil, + pages_custom_certificate_key: nil, + pages_custom_domain: nil + ) + + UpdatePagesConfigurationService.new(self).execute end def wiki diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 63bc639688d..ca5b39a001f 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -110,6 +110,7 @@ class ProjectPolicy < BasePolicy can! :admin_pipeline can! :admin_environment can! :admin_deployment + can! :update_pages end def public_access! diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb new file mode 100644 index 00000000000..be4c2fbef8c --- /dev/null +++ b/app/services/projects/update_pages_configuration_service.rb @@ -0,0 +1,53 @@ +module Projects + class UpdatePagesConfigurationService < BaseService + attr_reader :project + + def initialize(project) + @project = project + end + + def execute + update_file(pages_cname_file, project.pages_custom_domain) + update_file(pages_certificate_file, project.pages_custom_certificate) + update_file(pages_certificate_file_key, project.pages_custom_certificate_key) + reload_daemon + success + rescue => e + error(e.message) + end + + private + + def reload_daemon + # GitLab Pages daemon constantly watches for modification time of `pages.path` + # It reloads configuration when `pages.path` is modified + File.touch(Settings.pages.path) + end + + def pages_path + @pages_path ||= project.pages_path + end + + def pages_cname_file + File.join(pages_path, 'CNAME') + end + + def pages_certificate_file + File.join(pages_path, 'domain.crt') + end + + def pages_certificate_key_file + File.join(pages_path, 'domain.key') + end + + def update_file(file, data) + if data + File.open(file, 'w') do |file| + file.write(data) + end + else + File.rm_r(file) + end + end + end +end diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb new file mode 100644 index 00000000000..3b5bd30db1a --- /dev/null +++ b/app/validators/certificate_key_validator.rb @@ -0,0 +1,24 @@ +# UrlValidator +# +# Custom validator for private keys. +# +# class Project < ActiveRecord::Base +# validates :certificate_key, certificate_key: true +# end +# +class CertificateKeyValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless valid_private_key_pem?(value) + record.errors.add(attribute, "must be a valid PEM private key") + end + end + + private + + def valid_private_key_pem?(value) + pkey = OpenSSL::PKey::RSA.new(value) + pkey.private? + rescue OpenSSL::PKey::PKeyError + false + end +end diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb new file mode 100644 index 00000000000..2cba5a435b7 --- /dev/null +++ b/app/validators/certificate_validator.rb @@ -0,0 +1,30 @@ +# UrlValidator +# +# Custom validator for private keys. +# +# class Project < ActiveRecord::Base +# validates :certificate_key, certificate_key: true +# end +# +class CertificateValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + certificate = parse_certificate(value) + unless certificate + record.errors.add(attribute, "must be a valid PEM certificate") + end + + if options[:intermediates] + unless certificate + record.errors.add(attribute, "certificate verification failed: missing intermediate certificates") + end + end + end + + private + + def parse_certificate(value) + OpenSSL::X509::Certificate.new(value) + rescue OpenSSL::X509::CertificateError + nil + end +end diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml index c6df66d2c3c..d6c158b6de3 100644 --- a/app/views/layouts/nav/_project_settings.html.haml +++ b/app/views/layouts/nav/_project_settings.html.haml @@ -34,3 +34,7 @@ = link_to namespace_project_pipelines_settings_path(@project.namespace, @project), title: 'CI/CD Pipelines' do %span CI/CD Pipelines + = nav_link(controller: :pages) do + = link_to namespace_project_pages_path(@project.namespace, @project), title: 'Pages', data: {placement: 'right'} do + %span + Pages diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml new file mode 100644 index 00000000000..d64f99fd22b --- /dev/null +++ b/app/views/projects/pages/_access.html.haml @@ -0,0 +1,34 @@ +- if @project.pages_url + .panel.panel-default + .panel-heading + Access pages + .panel-body + %p + %strong + Congratulations! Your pages are served at: + %p= link_to @project.pages_url, @project.pages_url + + - if Settings.pages.custom_domain && @project.pages_custom_url + %p= link_to @project.pages_custom_url, @project.pages_custom_url + + - if @project.pages_custom_certificate + - unless valid_certificate? + #error_explanation + .alert.alert-warning + Your certificate is invalid. + + - unless valid_certificate_key? + #error_explanation + .alert.alert-warning + Your private key is invalid. + + - unless valid_key_for_certificiate? + #error_explanation + .alert.alert-warning + Your private key can't be used with your certificate. + + - unless valid_certificate_intermediates? + #error_explanation + .alert.alert-warning + Your certificate doesn't have intermediates. + Your page may not work properly. diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml new file mode 100644 index 00000000000..61b995a5934 --- /dev/null +++ b/app/views/projects/pages/_destroy.haml @@ -0,0 +1,10 @@ +- if can?(current_user, :remove_pages, @project) && @project.pages_url + .panel.panel-default.panel.panel-danger + .panel-heading Remove pages + .errors-holder + .panel-body + = form_tag(namespace_project_pages_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do + %p + Removing the pages will prevent from exposing them to outside world. + .form-actions + = button_to 'Remove pages', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_pages_message(@project) } diff --git a/app/views/projects/pages/_disabled.html.haml b/app/views/projects/pages/_disabled.html.haml new file mode 100644 index 00000000000..cf9ef5b4d6f --- /dev/null +++ b/app/views/projects/pages/_disabled.html.haml @@ -0,0 +1,4 @@ +.panel.panel-default + .nothing-here-block + GitLab Pages is disabled. + Ask your system's administrator to enable it. diff --git a/app/views/projects/pages/_form.html.haml b/app/views/projects/pages/_form.html.haml new file mode 100644 index 00000000000..a7b03d552db --- /dev/null +++ b/app/views/projects/pages/_form.html.haml @@ -0,0 +1,35 @@ +- if can?(current_user, :update_pages, @project) + .panel.panel-default + .panel-heading + Settings + .panel-body + = form_for [@project], url: namespace_project_pages_path(@project.namespace, @project), html: { class: 'form-horizontal fieldset-form' } do |f| + - if @project.errors.any? + #error_explanation + .alert.alert-danger + - @project.errors.full_messages.each do |msg| + %p= msg + + .form-group + = f.label :pages_domain, class: 'control-label' do + Custom domain + .col-sm-10 + - if Settings.pages.custom_domain + = f.text_field :pages_custom_domain, required: false, autocomplete: 'off', class: 'form-control' + %span.help-inline Allows you to serve the pages under your domain + - else + .nothing-here-block + Support for custom domains and certificates is disabled. + Ask your system's administrator to enable it. + + - if Settings.pages.https + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :pages_redirect_http do + = f.check_box :pages_redirect_http + %span.descr Force HTTPS + .help-block Redirect the HTTP to HTTPS forcing to always use the secure connection + + .form-actions + = f.submit 'Save changes', class: "btn btn-save" diff --git a/app/views/projects/pages/_remove_certificate.html.haml b/app/views/projects/pages/_remove_certificate.html.haml new file mode 100644 index 00000000000..e8c0d03adfa --- /dev/null +++ b/app/views/projects/pages/_remove_certificate.html.haml @@ -0,0 +1,16 @@ +- if can?(current_user, :update_pages, @project) && @project.pages_custom_certificate + .panel.panel-default.panel.panel-danger + .panel-heading + Remove certificate + .errors-holder + .panel-body + = form_tag(certificates_namespace_project_pages_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do + %p + Removing the certificate will stop serving the page under HTTPS. + - if certificate + %p + %pre + = certificate.to_text + + .form-actions + = button_to 'Remove certificate', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_pages_certificate_message(@project) } diff --git a/app/views/projects/pages/_upload_certificate.html.haml b/app/views/projects/pages/_upload_certificate.html.haml new file mode 100644 index 00000000000..30873fcf395 --- /dev/null +++ b/app/views/projects/pages/_upload_certificate.html.haml @@ -0,0 +1,32 @@ +- if can?(current_user, :update_pages, @project) && Settings.pages.https && Settings.pages.custom_domain + .panel.panel-default + .panel-heading + Certificate + .panel-body + %p + Allows you to upload your certificate which will be used to serve pages under your domain. + %br + + = form_for [@project], url: namespace_project_pages_path(@project.namespace, @project), html: { class: 'form-horizontal fieldset-form' } do |f| + - if @project.errors.any? + #error_explanation + .alert.alert-danger + - @project.errors.full_messages.each do |msg| + %p= msg + + .form-group + = f.label :pages_custom_certificate, class: 'control-label' do + Certificate (PEM) + .col-sm-10 + = f.text_area :pages_custom_certificate, required: true, rows: 5, class: 'form-control', value: '' + %span.help-inline Upload a certificate for your domain with all intermediates + + .form-group + = f.label :pages_custom_certificate_key, class: 'control-label' do + Key (PEM) + .col-sm-10 + = f.text_area :pages_custom_certificate_key, required: true, rows: 5, class: 'form-control', value: '' + %span.help-inline Upload a certificate for your domain with all intermediates + + .form-actions + = f.submit 'Update certificate', class: "btn btn-save" diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml new file mode 100644 index 00000000000..5542bbe670b --- /dev/null +++ b/app/views/projects/pages/_use.html.haml @@ -0,0 +1,18 @@ +- unless @project.pages_url + .panel.panel-info + .panel-heading + Configure pages + .panel-body + %p + Learn how to upload your static site and have it served by + GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}. + %p + In the example below we define a special job named + %code pages + which is using Jekyll to build a static site. The generated + HTML will be stored in the + %code public/ + directory which will then be archived and uploaded to GitLab. + The name of the directory should not be different than + %code public/ + in order for the pages to work. diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml new file mode 100644 index 00000000000..5f689800da8 --- /dev/null +++ b/app/views/projects/pages/show.html.haml @@ -0,0 +1,18 @@ +- page_title "Pages" +%h3.page_title Pages +%p.light + With GitLab Pages you can host for free your static websites on GitLab. + Combined with the power of GitLab CI and the help of GitLab Runner + you can deploy static pages for your individual projects, your user or your group. +%hr + +- if Settings.pages.enabled + = render 'access' + = render 'use' + - if @project.pages_url + = render 'form' + = render 'upload_certificate' + = render 'remove_certificate' + = render 'destroy' +- else + = render 'disabled' diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index 8c99e8dbe76..4eeb9666bb0 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -9,7 +9,11 @@ class PagesWorker def deploy(build_id) build = Ci::Build.find_by(id: build_id) - Projects::UpdatePagesService.new(build.project, build).execute + result = Projects::UpdatePagesService.new(build.project, build).execute + if result[:status] == :success + result = Projects::UpdatePagesConfigurationService.new(build.project).execute + end + result end def remove(namespace_path, project_path) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 860cafad325..239aa662d9f 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -273,6 +273,7 @@ Settings.pages['https'] = false if Settings.pages['https'].nil? Settings.pages['port'] ||= Settings.pages.https ? 443 : 80 Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http" Settings.pages['url'] ||= Settings.send(:build_pages_url) +Settings.pages['custom_domain'] ||= false if Settings.pages['custom_domain'].nil? # # Git LFS diff --git a/config/routes/project.rb b/config/routes/project.rb index cd56f6281f5..956a2d3186f 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -39,6 +39,10 @@ constraints(ProjectUrlConstrainer.new) do end end + resource :pages, only: [:show, :update, :destroy] do + delete :certificates + end + resources :compare, only: [:index, :create] do collection do get :diff_for_path @@ -329,7 +333,6 @@ constraints(ProjectUrlConstrainer.new) do post :archive post :unarchive post :housekeeping - post :remove_pages post :toggle_star post :preview_markdown post :export diff --git a/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb b/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb new file mode 100644 index 00000000000..6472199fc4a --- /dev/null +++ b/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb @@ -0,0 +1,10 @@ +class AddPagesCustomDomainToProjects < ActiveRecord::Migration + def change + add_column :projects, :pages_custom_certificate, :text + add_column :projects, :pages_custom_certificate_key, :text + add_column :projects, :pages_custom_certificate_key_iv, :string + add_column :projects, :pages_custom_certificate_key_salt, :string + add_column :projects, :pages_custom_domain, :string, unique: true + add_column :projects, :pages_redirect_http, :boolean, default: false, null: false + end +end -- cgit v1.2.1 From 930a7030b5a0080128b2fe3e2b9506717c54a6a5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 9 Feb 2016 19:04:39 +0100 Subject: Implement proper verification of certificate's public_key against the private_key --- app/controllers/projects/pages_controller.rb | 5 +-- app/models/project.rb | 8 ++--- app/validators/certificate_key_validator.rb | 1 + app/validators/certificate_validator.rb | 14 +++----- app/views/projects/edit.html.haml | 41 ---------------------- app/views/projects/pages/_use.html.haml | 10 ------ ...09125808_add_pages_custom_domain_to_projects.rb | 6 ++-- 7 files changed, 15 insertions(+), 70 deletions(-) diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index ef0ed505142..359544472e9 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -54,8 +54,9 @@ class Projects::PagesController < Projects::ApplicationController return false unless certificate return false unless certificate_key - certificate.verify(certificate_key) - rescue OpenSSL::X509::CertificateError + # We compare the public key stored in certificate with public key from certificate key + certificate.public_key.to_pem == certificate_key.public_key.to_pem + rescue OpenSSL::X509::CertificateError, OpenSSL::PKey::PKeyError false end diff --git a/app/models/project.rb b/app/models/project.rb index 34618817fb6..f447c2bf293 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -76,8 +76,6 @@ class Project < ActiveRecord::Base attr_accessor :new_default_branch attr_accessor :old_path_with_namespace - attr_encrypted :pages_custom_certificate_key, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base - alias_attribute :title, :name # Relations @@ -209,14 +207,16 @@ class Project < ActiveRecord::Base validates :pages_custom_domain, hostname: true, allow_blank: true, allow_nil: true validates_uniqueness_of :pages_custom_domain, allow_nil: true, allow_blank: true - validates :pages_custom_certificate, certificate: { intermediate: true } - validates :pages_custom_certificate_key, certificate_key: true + validates :pages_custom_certificate, certificate: true, allow_nil: true, allow_blank: true + validates :pages_custom_certificate_key, certificate_key: true, allow_nil: true, allow_blank: true add_authentication_token_field :runners_token before_save :ensure_runners_token mount_uploader :avatar, AvatarUploader + attr_encrypted :pages_custom_certificate_key, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base + # Scopes default_scope { where(pending_delete: false) } diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb index 3b5bd30db1a..7039bd5a621 100644 --- a/app/validators/certificate_key_validator.rb +++ b/app/validators/certificate_key_validator.rb @@ -16,6 +16,7 @@ class CertificateKeyValidator < ActiveModel::EachValidator private def valid_private_key_pem?(value) + return unless value pkey = OpenSSL::PKey::RSA.new(value) pkey.private? rescue OpenSSL::PKey::PKeyError diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb index 2cba5a435b7..2a04c76d4b9 100644 --- a/app/validators/certificate_validator.rb +++ b/app/validators/certificate_validator.rb @@ -3,26 +3,20 @@ # Custom validator for private keys. # # class Project < ActiveRecord::Base -# validates :certificate_key, certificate_key: true +# validates :certificate_key, certificate: true # end # class CertificateValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) - certificate = parse_certificate(value) - unless certificate + unless valid_certificate_pem?(value) record.errors.add(attribute, "must be a valid PEM certificate") end - - if options[:intermediates] - unless certificate - record.errors.add(attribute, "certificate verification failed: missing intermediate certificates") - end - end end private - def parse_certificate(value) + def valid_certificate_pem?(value) + return unless value OpenSSL::X509::Certificate.new(value) rescue OpenSSL::X509::CertificateError nil diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index f4c1db1b93d..dab40d37ead 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -134,47 +134,6 @@ = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar" = f.submit 'Save changes', class: "btn btn-save" - - if Settings.pages.enabled - .pages-settings - .panel.panel-default - .panel-heading Pages - .errors-holder - .panel-body - - if @project.pages_url - %strong - Congratulations! Your pages are served at: - %p= link_to @project.pages_url, @project.pages_url - - else - %p - Learn how to upload your static site and have it served by - GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}. - %p - In the example below we define a special job named - %code pages - which is using Jekyll to build a static site. The generated - HTML will be stored in the - %code public/ - directory which will then be archived and uploaded to GitLab. - The name of the directory should not be different than - %code public/ - in order for the pages to work. - %ul - %li - %pre - :plain - pages: - image: jekyll/jekyll - script: jekyll build -d public/ - artifacts: - paths: - - public/ - - - if @project.pages_url && can?(current_user, :remove_pages, @project) - .form-actions - = link_to 'Remove pages', remove_pages_namespace_project_path(@project.namespace, @project), - data: { confirm: "Are you sure that you want to remove pages for this project?" }, - method: :post, class: "btn btn-warning" - .row.prepend-top-default %hr .row.prepend-top-default diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index 5542bbe670b..ee38f45d44d 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -6,13 +6,3 @@ %p Learn how to upload your static site and have it served by GitLab by following the #{link_to "documentation on GitLab Pages", "http://doc.gitlab.com/ee/pages/README.html", target: :blank}. - %p - In the example below we define a special job named - %code pages - which is using Jekyll to build a static site. The generated - HTML will be stored in the - %code public/ - directory which will then be archived and uploaded to GitLab. - The name of the directory should not be different than - %code public/ - in order for the pages to work. diff --git a/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb b/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb index 6472199fc4a..13b42d18a7a 100644 --- a/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb +++ b/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb @@ -1,9 +1,9 @@ class AddPagesCustomDomainToProjects < ActiveRecord::Migration def change add_column :projects, :pages_custom_certificate, :text - add_column :projects, :pages_custom_certificate_key, :text - add_column :projects, :pages_custom_certificate_key_iv, :string - add_column :projects, :pages_custom_certificate_key_salt, :string + add_column :projects, :encrypted_pages_custom_certificate_key, :text + add_column :projects, :encrypted_pages_custom_certificate_key_iv, :string + add_column :projects, :encrypted_pages_custom_certificate_key_salt, :string add_column :projects, :pages_custom_domain, :string, unique: true add_column :projects, :pages_redirect_http, :boolean, default: false, null: false end -- cgit v1.2.1 From f034f6b3ec5dc8b72f43c954ddb34bae037be254 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 10 Feb 2016 11:37:27 +0100 Subject: WIP --- app/controllers/projects/pages_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index 359544472e9..055f182ae00 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -89,7 +89,7 @@ class Projects::PagesController < Projects::ApplicationController def certificate_key return unless @project.pages_custom_certificate_key @certificate_key ||= OpenSSL::PKey::RSA.new(@project.pages_custom_certificate_key) - rescue OpenSSL::PKey::PKeyError + rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError nil end end -- cgit v1.2.1 From 6e99226cca41f36d92c4ccb2cd398d2256091adc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 10 Feb 2016 12:07:46 +0100 Subject: Added PagesDomain --- app/models/pages_domain.rb | 29 +++++++++++++++++ app/models/project.rb | 38 ++-------------------- ...09125808_add_pages_custom_domain_to_projects.rb | 10 ------ db/migrate/20160210105555_create_pages_domain.rb | 14 ++++++++ 4 files changed, 45 insertions(+), 46 deletions(-) create mode 100644 app/models/pages_domain.rb delete mode 100644 db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb create mode 100644 db/migrate/20160210105555_create_pages_domain.rb diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb new file mode 100644 index 00000000000..eebdf7501de --- /dev/null +++ b/app/models/pages_domain.rb @@ -0,0 +1,29 @@ +class PagesDomain < ActiveRecord::Base + belongs_to :project + + validates :domain, hostname: true + validates_uniqueness_of :domain, allow_nil: true, allow_blank: true + validates :certificate, certificate: true, allow_nil: true, allow_blank: true + validates :key, certificate_key: true, allow_nil: true, allow_blank: true + + attr_encrypted :pages_custom_certificate_key, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base + + after_create :update + after_save :update + after_destroy :update + + def url + return unless domain + return unless Dir.exist?(project.public_pages_path) + + if certificate + return "https://#{domain}" + else + return "http://#{domain}" + end + end + + def update + UpdatePagesConfigurationService.new(project).execute + end +end diff --git a/app/models/project.rb b/app/models/project.rb index f447c2bf293..dac52a0fc5e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -150,6 +150,7 @@ class Project < ActiveRecord::Base has_many :lfs_objects, through: :lfs_objects_projects has_many :project_group_links, dependent: :destroy has_many :invited_groups, through: :project_group_links, source: :group + has_many :pages_domains, dependent: :destroy has_many :todos, dependent: :destroy has_many :notification_settings, dependent: :destroy, as: :source @@ -205,18 +206,11 @@ class Project < ActiveRecord::Base presence: true, inclusion: { in: ->(_object) { Gitlab.config.repositories.storages.keys } } - validates :pages_custom_domain, hostname: true, allow_blank: true, allow_nil: true - validates_uniqueness_of :pages_custom_domain, allow_nil: true, allow_blank: true - validates :pages_custom_certificate, certificate: true, allow_nil: true, allow_blank: true - validates :pages_custom_certificate_key, certificate_key: true, allow_nil: true, allow_blank: true - add_authentication_token_field :runners_token before_save :ensure_runners_token mount_uploader :avatar, AvatarUploader - attr_encrypted :pages_custom_certificate_key, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base - # Scopes default_scope { where(pending_delete: false) } @@ -1184,17 +1178,6 @@ class Project < ActiveRecord::Base "#{url}/#{path}" end - def pages_custom_url - return unless pages_custom_domain - return unless Dir.exist?(public_pages_path) - - if Gitlab.config.pages.https - return "https://#{pages_custom_domain}" - else - return "http://#{pages_custom_domain}" - end - end - def pages_path File.join(Settings.pages.path, path_with_namespace) end @@ -1203,32 +1186,15 @@ class Project < ActiveRecord::Base File.join(pages_path, 'public') end - def remove_pages_certificate - update( - pages_custom_certificate: nil, - pages_custom_certificate_key: nil - ) - - UpdatePagesConfigurationService.new(self).execute - end - def remove_pages # 1. We rename pages to temporary directory # 2. We wait 5 minutes, due to NFS caching # 3. We asynchronously remove pages with force - temp_path = "#{path}.#{SecureRandom.hex}" + temp_path = "#{path}.#{SecureRandom.hex}.deleted" if Gitlab::PagesTransfer.new.rename_project(path, temp_path, namespace.path) PagesWorker.perform_in(5.minutes, :remove, namespace.path, temp_path) end - - update( - pages_custom_certificate: nil, - pages_custom_certificate_key: nil, - pages_custom_domain: nil - ) - - UpdatePagesConfigurationService.new(self).execute end def wiki diff --git a/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb b/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb deleted file mode 100644 index 13b42d18a7a..00000000000 --- a/db/migrate/20160209125808_add_pages_custom_domain_to_projects.rb +++ /dev/null @@ -1,10 +0,0 @@ -class AddPagesCustomDomainToProjects < ActiveRecord::Migration - def change - add_column :projects, :pages_custom_certificate, :text - add_column :projects, :encrypted_pages_custom_certificate_key, :text - add_column :projects, :encrypted_pages_custom_certificate_key_iv, :string - add_column :projects, :encrypted_pages_custom_certificate_key_salt, :string - add_column :projects, :pages_custom_domain, :string, unique: true - add_column :projects, :pages_redirect_http, :boolean, default: false, null: false - end -end diff --git a/db/migrate/20160210105555_create_pages_domain.rb b/db/migrate/20160210105555_create_pages_domain.rb new file mode 100644 index 00000000000..9af206143bd --- /dev/null +++ b/db/migrate/20160210105555_create_pages_domain.rb @@ -0,0 +1,14 @@ +class CreatePagesDomain < ActiveRecord::Migration + def change + create_table :pages_domains do |t| + t.integer :project_id + t.text :certificate + t.text :encrypted_key + t.string :encrypted_key_iv + t.string :encrypted_key_salt + t.string :domain + end + + add_index :pages_domains, :domain, unique: true + end +end -- cgit v1.2.1 From 13b6bad17ec46eb78878f6972da1e7e34be86bb5 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 10 Feb 2016 15:06:31 +0100 Subject: Implement extra domains and save pages configuration --- app/controllers/projects/pages_controller.rb | 94 ++++++++-------------- app/helpers/projects_helper.rb | 4 - app/models/pages_domain.rb | 84 ++++++++++++++++++- .../projects/update_pages_configuration_service.rb | 32 +++++--- app/views/projects/pages/_access.html.haml | 29 +------ app/views/projects/pages/_destroy.haml | 2 +- app/views/projects/pages/_form.html.haml | 64 +++++++-------- app/views/projects/pages/_list.html.haml | 16 ++++ app/views/projects/pages/_no_domains.html.haml | 6 ++ .../projects/pages/_remove_certificate.html.haml | 16 ---- .../projects/pages/_upload_certificate.html.haml | 32 -------- app/views/projects/pages/index.html.haml | 25 ++++++ app/views/projects/pages/new.html.haml | 6 ++ app/views/projects/pages/show.html.haml | 38 +++++---- config/gitlab.yml.example | 2 + config/initializers/1_settings.rb | 3 +- config/routes/project.rb | 4 +- 17 files changed, 249 insertions(+), 208 deletions(-) create mode 100644 app/views/projects/pages/_list.html.haml create mode 100644 app/views/projects/pages/_no_domains.html.haml delete mode 100644 app/views/projects/pages/_remove_certificate.html.haml delete mode 100644 app/views/projects/pages/_upload_certificate.html.haml create mode 100644 app/views/projects/pages/index.html.haml create mode 100644 app/views/projects/pages/new.html.haml diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index 055f182ae00..82814afe196 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -2,25 +2,45 @@ class Projects::PagesController < Projects::ApplicationController layout 'project_settings' before_action :authorize_update_pages!, except: [:show] - before_action :authorize_remove_pages!, only: :destroy + before_action :authorize_remove_pages!, only: [:remove_pages] + before_action :label, only: [:destroy] + before_action :domain, only: [:show] helper_method :valid_certificate?, :valid_certificate_key? helper_method :valid_key_for_certificiate?, :valid_certificate_intermediates? helper_method :certificate, :certificate_key + def index + @domains = @project.pages_domains.order(:domain) + end + def show end - def update - if @project.update_attributes(pages_params) + def new + @domain = @project.pages_domains.new + end + + def create + @domain = @project.pages_domains.create(pages_domain_params) + + if @domain.valid? redirect_to namespace_project_pages_path(@project.namespace, @project) else - render 'show' + render 'new' end end - def certificate - @project.remove_pages_certificate + def destroy + @domain.destroy + + respond_to do |format| + format.html do + redirect_to(namespace_project_pages_path(@project.namespace, @project), + notice: 'Domain was removed') + end + format.js + end end def destroy @@ -33,63 +53,15 @@ class Projects::PagesController < Projects::ApplicationController private - def pages_params - params.require(:project).permit( - :pages_custom_certificate, - :pages_custom_certificate_key, - :pages_custom_domain, - :pages_redirect_http, + def pages_domain_params + params.require(:pages_domain).permit( + :certificate, + :key, + :domain ) end - def valid_certificate? - certificate.present? - end - - def valid_certificate_key? - certificate_key.present? - end - - def valid_key_for_certificiate? - return false unless certificate - return false unless certificate_key - - # We compare the public key stored in certificate with public key from certificate key - certificate.public_key.to_pem == certificate_key.public_key.to_pem - rescue OpenSSL::X509::CertificateError, OpenSSL::PKey::PKeyError - false - end - - def valid_certificate_intermediates? - return false unless certificate - - store = OpenSSL::X509::Store.new - store.set_default_paths - - # This forces to load all intermediate certificates stored in `pages_custom_certificate` - Tempfile.open('project_certificate') do |f| - f.write(@project.pages_custom_certificate) - f.flush - store.add_file(f.path) - end - - store.verify(certificate) - rescue OpenSSL::X509::StoreError - false - end - - def certificate - return unless @project.pages_custom_certificate - - @certificate ||= OpenSSL::X509::Certificate.new(@project.pages_custom_certificate) - rescue OpenSSL::X509::CertificateError - nil - end - - def certificate_key - return unless @project.pages_custom_certificate_key - @certificate_key ||= OpenSSL::PKey::RSA.new(@project.pages_custom_certificate_key) - rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError - nil + def domain + @domain ||= @project.pages_domains.find_by(domain: params[:id].to_s) end end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 63aa182502d..054cc849839 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -85,10 +85,6 @@ module ProjectsHelper "You are going to remove the pages for #{project.name_with_namespace}.\n Are you ABSOLUTELY sure?" end - def remove_pages_certificate_message(project) - "You are going to remove a certificates for #{project.name_with_namespace}.\n Are you ABSOLUTELY sure?" - end - def project_nav_tabs @nav_tabs ||= get_project_nav_tabs(@project, current_user) end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index eebdf7501de..810af4e832a 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -2,19 +2,25 @@ class PagesDomain < ActiveRecord::Base belongs_to :project validates :domain, hostname: true - validates_uniqueness_of :domain, allow_nil: true, allow_blank: true + validates_uniqueness_of :domain, case_sensitive: false validates :certificate, certificate: true, allow_nil: true, allow_blank: true validates :key, certificate_key: true, allow_nil: true, allow_blank: true - attr_encrypted :pages_custom_certificate_key, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base + validate :validate_matching_key, if: ->(domain) { domain.certificate.present? && domain.key.present? } + validate :validate_intermediates, if: ->(domain) { domain.certificate.present? } + + attr_encrypted :key, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base after_create :update after_save :update after_destroy :update + def to_param + domain + end + def url return unless domain - return unless Dir.exist?(project.public_pages_path) if certificate return "https://#{domain}" @@ -23,7 +29,77 @@ class PagesDomain < ActiveRecord::Base end end + def has_matching_key? + return unless x509 + return unless pkey + + # We compare the public key stored in certificate with public key from certificate key + x509.check_private_key(pkey) + end + + def has_intermediates? + return false unless x509 + + store = OpenSSL::X509::Store.new + store.set_default_paths + + # This forces to load all intermediate certificates stored in `certificate` + Tempfile.open('certificate_chain') do |f| + f.write(certificate) + f.flush + store.add_file(f.path) + end + + store.verify(x509) + rescue OpenSSL::X509::StoreError + false + end + + def expired? + return false unless x509 + current = Time.new + return current < x509.not_before || x509.not_after < current + end + + def subject + return unless x509 + return x509.subject.to_s + end + + def fingerprint + return unless x509 + @fingeprint ||= OpenSSL::Digest::SHA256.new(x509.to_der).to_s + end + + private + + def x509 + return unless certificate + @x509 ||= OpenSSL::X509::Certificate.new(certificate) + rescue OpenSSL::X509::CertificateError + nil + end + + def pkey + return unless key + @pkey ||= OpenSSL::PKey::RSA.new(key) + rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError + nil + end + def update - UpdatePagesConfigurationService.new(project).execute + ::Projects::UpdatePagesConfigurationService.new(project).execute + end + + def validate_matching_key + unless has_matching_key? + self.errors.add(:key, "doesn't match the certificate") + end + end + + def validate_intermediates + unless has_intermediates? + self.errors.add(:certificate, 'misses intermediates') + end end end diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb index be4c2fbef8c..5afb0582ca6 100644 --- a/app/services/projects/update_pages_configuration_service.rb +++ b/app/services/projects/update_pages_configuration_service.rb @@ -7,9 +7,7 @@ module Projects end def execute - update_file(pages_cname_file, project.pages_custom_domain) - update_file(pages_certificate_file, project.pages_custom_certificate) - update_file(pages_certificate_file_key, project.pages_custom_certificate_key) + update_file(pages_config_file, pages_config) reload_daemon success rescue => e @@ -18,6 +16,22 @@ module Projects private + def pages_config + { + domains: pages_domains_config + } + end + + def pages_domains_config + project.pages_domains.map do |domain| + { + domain: domain.domain, + certificate: domain.certificate, + key: domain.key, + } + end + end + def reload_daemon # GitLab Pages daemon constantly watches for modification time of `pages.path` # It reloads configuration when `pages.path` is modified @@ -28,16 +42,8 @@ module Projects @pages_path ||= project.pages_path end - def pages_cname_file - File.join(pages_path, 'CNAME') - end - - def pages_certificate_file - File.join(pages_path, 'domain.crt') - end - - def pages_certificate_key_file - File.join(pages_path, 'domain.key') + def pages_config_file + File.join(pages_path, 'config.jso') end def update_file(file, data) diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml index d64f99fd22b..9740877b214 100644 --- a/app/views/projects/pages/_access.html.haml +++ b/app/views/projects/pages/_access.html.haml @@ -5,30 +5,9 @@ .panel-body %p %strong - Congratulations! Your pages are served at: - %p= link_to @project.pages_url, @project.pages_url - - - if Settings.pages.custom_domain && @project.pages_custom_url - %p= link_to @project.pages_custom_url, @project.pages_custom_url - - - if @project.pages_custom_certificate - - unless valid_certificate? - #error_explanation - .alert.alert-warning - Your certificate is invalid. + Congratulations! Your pages are served under: - - unless valid_certificate_key? - #error_explanation - .alert.alert-warning - Your private key is invalid. - - - unless valid_key_for_certificiate? - #error_explanation - .alert.alert-warning - Your private key can't be used with your certificate. + %p= link_to @project.pages_url, @project.pages_url - - unless valid_certificate_intermediates? - #error_explanation - .alert.alert-warning - Your certificate doesn't have intermediates. - Your page may not work properly. + - @project.pages_domains.each do |domain| + %p= link_to domain.url, domain.url diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index 61b995a5934..dd493a6d312 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -3,7 +3,7 @@ .panel-heading Remove pages .errors-holder .panel-body - = form_tag(namespace_project_pages_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do + = form_tag(remove_pages_namespace_project_pages_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do %p Removing the pages will prevent from exposing them to outside world. .form-actions diff --git a/app/views/projects/pages/_form.html.haml b/app/views/projects/pages/_form.html.haml index a7b03d552db..c69b76c6697 100644 --- a/app/views/projects/pages/_form.html.haml +++ b/app/views/projects/pages/_form.html.haml @@ -1,35 +1,35 @@ -- if can?(current_user, :update_pages, @project) - .panel.panel-default - .panel-heading - Settings - .panel-body - = form_for [@project], url: namespace_project_pages_path(@project.namespace, @project), html: { class: 'form-horizontal fieldset-form' } do |f| - - if @project.errors.any? - #error_explanation - .alert.alert-danger - - @project.errors.full_messages.each do |msg| - %p= msg += form_for [@domain], url: namespace_project_pages_path(@project.namespace, @project), html: { class: 'form-horizontal fieldset-form' } do |f| + - if @domain.errors.any? + #error_explanation + .alert.alert-danger + - @domain.errors.full_messages.each do |msg| + %p= msg - .form-group - = f.label :pages_domain, class: 'control-label' do - Custom domain - .col-sm-10 - - if Settings.pages.custom_domain - = f.text_field :pages_custom_domain, required: false, autocomplete: 'off', class: 'form-control' - %span.help-inline Allows you to serve the pages under your domain - - else - .nothing-here-block - Support for custom domains and certificates is disabled. - Ask your system's administrator to enable it. + .form-group + = f.label :domain, class: 'control-label' do + Domain + .col-sm-10 + = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control' + %span.help-inline * required - - if Settings.pages.https - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :pages_redirect_http do - = f.check_box :pages_redirect_http - %span.descr Force HTTPS - .help-block Redirect the HTTP to HTTPS forcing to always use the secure connection + - if Settings.pages.external_https + .form-group + = f.label :certificate, class: 'control-label' do + Certificate (PEM) + .col-sm-10 + = f.text_area :certificate, rows: 5, class: 'form-control', value: '' + %span.help-inline Upload a certificate for your domain with all intermediates - .form-actions - = f.submit 'Save changes', class: "btn btn-save" + .form-group + = f.label :key, class: 'control-label' do + Key (PEM) + .col-sm-10 + = f.text_area :key, rows: 5, class: 'form-control', value: '' + %span.help-inline Upload a certificate for your domain with all intermediates + - else + .nothing-here-block + Support for custom certificates is disabled. + Ask your system's administrator to enable it. + + .form-actions + = f.submit 'Create New Domain', class: "btn btn-save" diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml new file mode 100644 index 00000000000..7dfeb0e6e12 --- /dev/null +++ b/app/views/projects/pages/_list.html.haml @@ -0,0 +1,16 @@ +.panel.panel-default + .panel-heading + Domains (#{@domains.count}) + %ul.well-list + - @domains.each do |domain| + %li + .pull-right + = link_to 'Details', namespace_project_page_path(@project.namespace, @project, domain), class: "btn btn-sm btn-grouped" + = link_to 'Remove', namespace_project_page_path(@project.namespace, @project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" + .clearfix + %span= link_to domain.domain, domain.url + %p + - if domain.subject + %span.label.label-gray Certificate: #{domain.subject} + - if domain.expired? + %span.label.label-danger Expired diff --git a/app/views/projects/pages/_no_domains.html.haml b/app/views/projects/pages/_no_domains.html.haml new file mode 100644 index 00000000000..5a18740346a --- /dev/null +++ b/app/views/projects/pages/_no_domains.html.haml @@ -0,0 +1,6 @@ +.panel.panel-default + .panel-heading + Domains + .nothing-here-block + Support for domains and certificates is disabled. + Ask your system's administrator to enable it. diff --git a/app/views/projects/pages/_remove_certificate.html.haml b/app/views/projects/pages/_remove_certificate.html.haml deleted file mode 100644 index e8c0d03adfa..00000000000 --- a/app/views/projects/pages/_remove_certificate.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -- if can?(current_user, :update_pages, @project) && @project.pages_custom_certificate - .panel.panel-default.panel.panel-danger - .panel-heading - Remove certificate - .errors-holder - .panel-body - = form_tag(certificates_namespace_project_pages_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do - %p - Removing the certificate will stop serving the page under HTTPS. - - if certificate - %p - %pre - = certificate.to_text - - .form-actions - = button_to 'Remove certificate', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_pages_certificate_message(@project) } diff --git a/app/views/projects/pages/_upload_certificate.html.haml b/app/views/projects/pages/_upload_certificate.html.haml deleted file mode 100644 index 30873fcf395..00000000000 --- a/app/views/projects/pages/_upload_certificate.html.haml +++ /dev/null @@ -1,32 +0,0 @@ -- if can?(current_user, :update_pages, @project) && Settings.pages.https && Settings.pages.custom_domain - .panel.panel-default - .panel-heading - Certificate - .panel-body - %p - Allows you to upload your certificate which will be used to serve pages under your domain. - %br - - = form_for [@project], url: namespace_project_pages_path(@project.namespace, @project), html: { class: 'form-horizontal fieldset-form' } do |f| - - if @project.errors.any? - #error_explanation - .alert.alert-danger - - @project.errors.full_messages.each do |msg| - %p= msg - - .form-group - = f.label :pages_custom_certificate, class: 'control-label' do - Certificate (PEM) - .col-sm-10 - = f.text_area :pages_custom_certificate, required: true, rows: 5, class: 'form-control', value: '' - %span.help-inline Upload a certificate for your domain with all intermediates - - .form-group - = f.label :pages_custom_certificate_key, class: 'control-label' do - Key (PEM) - .col-sm-10 - = f.text_area :pages_custom_certificate_key, required: true, rows: 5, class: 'form-control', value: '' - %span.help-inline Upload a certificate for your domain with all intermediates - - .form-actions - = f.submit 'Update certificate', class: "btn btn-save" diff --git a/app/views/projects/pages/index.html.haml b/app/views/projects/pages/index.html.haml new file mode 100644 index 00000000000..fea34c113ba --- /dev/null +++ b/app/views/projects/pages/index.html.haml @@ -0,0 +1,25 @@ +- page_title "Pages" +%h3.page_title + Pages + + = link_to new_namespace_project_page_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Domain" do + %i.fa.fa-plus + New Domain + +%p.light + With GitLab Pages you can host for free your static websites on GitLab. + Combined with the power of GitLab CI and the help of GitLab Runner + you can deploy static pages for your individual projects, your user or your group. + +%hr.clearfix + +- if Settings.pages.enabled + = render 'access' + = render 'use' + - if Settings.pages.external_http || Settings.pages.external_https + = render 'list' + - else + = render 'no_domains' + = render 'destroy' +- else + = render 'disabled' diff --git a/app/views/projects/pages/new.html.haml b/app/views/projects/pages/new.html.haml new file mode 100644 index 00000000000..2609df62aac --- /dev/null +++ b/app/views/projects/pages/new.html.haml @@ -0,0 +1,6 @@ +- page_title 'Pages' +%h3.page_title + New Pages Domain +%hr.clearfix +%div + = render 'form' diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index 5f689800da8..98c4e890968 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -1,18 +1,22 @@ -- page_title "Pages" -%h3.page_title Pages -%p.light - With GitLab Pages you can host for free your static websites on GitLab. - Combined with the power of GitLab CI and the help of GitLab Runner - you can deploy static pages for your individual projects, your user or your group. -%hr +- page_title "#{@domain.domain}", "Pages Domain" -- if Settings.pages.enabled - = render 'access' - = render 'use' - - if @project.pages_url - = render 'form' - = render 'upload_certificate' - = render 'remove_certificate' - = render 'destroy' -- else - = render 'disabled' +%h3.page-title + #{@domain.domain} + +.table-holder + %table.table + %tr + %td + Domain + %td + = link_to @domain.domain, @domain.url + %tr + %td + Certificate + %td + - if @domain.certificate + %pre + = @domain.certificate.to_text + - else + .light + missing diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index c6f06d43d07..f2bde602795 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -165,6 +165,8 @@ production: &base host: example.com port: 80 # Set to 443 if you serve the pages with HTTPS https: false # Set to true if you serve the pages with HTTPS + # external_http: "1.1.1.1:80" # if defined notifies the GitLab pages do support Custom Domains + # external_https: "1.1.1.1:443" # if defined notifies the GitLab pages do support Custom Domains with Certificates ## Mattermost ## For enabling Add to Mattermost button diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 239aa662d9f..0015ddf902d 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -273,7 +273,8 @@ Settings.pages['https'] = false if Settings.pages['https'].nil? Settings.pages['port'] ||= Settings.pages.https ? 443 : 80 Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http" Settings.pages['url'] ||= Settings.send(:build_pages_url) -Settings.pages['custom_domain'] ||= false if Settings.pages['custom_domain'].nil? +Settings.pages['external_http'] ||= false if Settings.pages['external_http'].nil? +Settings.pages['external_https'] ||= false if Settings.pages['external_https'].nil? # # Git LFS diff --git a/config/routes/project.rb b/config/routes/project.rb index 956a2d3186f..ac1e3fce16a 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -39,8 +39,8 @@ constraints(ProjectUrlConstrainer.new) do end end - resource :pages, only: [:show, :update, :destroy] do - delete :certificates + resources :pages, except: [:edit, :update] do + delete :remove_pages end resources :compare, only: [:index, :create] do -- cgit v1.2.1 From e5e2e7b70315cbcee12db66ebc73dbd0ef4a14ae Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 10 Feb 2016 16:21:39 +0100 Subject: Fix the remove_pages --- app/controllers/projects/pages_controller.rb | 5 +++++ app/services/projects/update_pages_configuration_service.rb | 4 ++-- app/views/projects/pages/index.html.haml | 7 ++++--- config/routes/project.rb | 4 +++- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index 82814afe196..0c7f2bd5784 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -51,6 +51,11 @@ class Projects::PagesController < Projects::ApplicationController end end + def remove_pages + project.remove_pages + project.pages_domains.destroy_all + end + private def pages_domain_params diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb index 5afb0582ca6..53e9d9e2757 100644 --- a/app/services/projects/update_pages_configuration_service.rb +++ b/app/services/projects/update_pages_configuration_service.rb @@ -43,7 +43,7 @@ module Projects end def pages_config_file - File.join(pages_path, 'config.jso') + File.join(pages_path, 'config.json') end def update_file(file, data) @@ -52,7 +52,7 @@ module Projects file.write(data) end else - File.rm_r(file) + File.rm(file, force: true) end end end diff --git a/app/views/projects/pages/index.html.haml b/app/views/projects/pages/index.html.haml index fea34c113ba..284f362d535 100644 --- a/app/views/projects/pages/index.html.haml +++ b/app/views/projects/pages/index.html.haml @@ -2,9 +2,10 @@ %h3.page_title Pages - = link_to new_namespace_project_page_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Domain" do - %i.fa.fa-plus - New Domain + - if Settings.pages.external_http || Settings.pages.external_https + = link_to new_namespace_project_page_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Domain" do + %i.fa.fa-plus + New Domain %p.light With GitLab Pages you can host for free your static websites on GitLab. diff --git a/config/routes/project.rb b/config/routes/project.rb index ac1e3fce16a..7a41cb81bfa 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -40,7 +40,9 @@ constraints(ProjectUrlConstrainer.new) do end resources :pages, except: [:edit, :update] do - delete :remove_pages + collection do + delete :remove_pages + end end resources :compare, only: [:index, :create] do -- cgit v1.2.1 From 0552c0b6f185433ad0a7caac321f0a6d445a0b63 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 10 Feb 2016 16:45:59 +0100 Subject: Fix views --- app/models/pages_domain.rb | 4 ++-- app/views/projects/pages/_list.html.haml | 33 ++++++++++++++++---------------- app/views/projects/pages/show.html.haml | 6 +++--- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 810af4e832a..985329bb856 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -71,8 +71,6 @@ class PagesDomain < ActiveRecord::Base @fingeprint ||= OpenSSL::Digest::SHA256.new(x509.to_der).to_s end - private - def x509 return unless certificate @x509 ||= OpenSSL::X509::Certificate.new(certificate) @@ -87,6 +85,8 @@ class PagesDomain < ActiveRecord::Base nil end + private + def update ::Projects::UpdatePagesConfigurationService.new(project).execute end diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml index 7dfeb0e6e12..e88a001d636 100644 --- a/app/views/projects/pages/_list.html.haml +++ b/app/views/projects/pages/_list.html.haml @@ -1,16 +1,17 @@ -.panel.panel-default - .panel-heading - Domains (#{@domains.count}) - %ul.well-list - - @domains.each do |domain| - %li - .pull-right - = link_to 'Details', namespace_project_page_path(@project.namespace, @project, domain), class: "btn btn-sm btn-grouped" - = link_to 'Remove', namespace_project_page_path(@project.namespace, @project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" - .clearfix - %span= link_to domain.domain, domain.url - %p - - if domain.subject - %span.label.label-gray Certificate: #{domain.subject} - - if domain.expired? - %span.label.label-danger Expired +- if @domains.any? + .panel.panel-default + .panel-heading + Domains (#{@domains.count}) + %ul.well-list + - @domains.each do |domain| + %li + .pull-right + = link_to 'Details', namespace_project_page_path(@project.namespace, @project, domain), class: "btn btn-sm btn-grouped" + = link_to 'Remove', namespace_project_page_path(@project.namespace, @project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" + .clearfix + %span= link_to domain.domain, domain.url + %p + - if domain.subject + %span.label.label-gray Certificate: #{domain.subject} + - if domain.expired? + %span.label.label-danger Expired diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index 98c4e890968..52493b1959b 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -1,7 +1,7 @@ - page_title "#{@domain.domain}", "Pages Domain" %h3.page-title - #{@domain.domain} + Pages Domain .table-holder %table.table @@ -14,9 +14,9 @@ %td Certificate %td - - if @domain.certificate + - if @domain.x509 %pre - = @domain.certificate.to_text + = @domain.x509.to_text - else .light missing -- cgit v1.2.1 From d3b828487647f106a8947864e18ac1ad7bd9d6f4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 12 Feb 2016 16:05:17 +0100 Subject: Pages domain model specs --- app/models/pages_domain.rb | 50 ++++--- .../projects/update_pages_configuration_service.rb | 22 ++- app/services/projects/update_pages_service.rb | 3 +- app/views/projects/pages/show.html.haml | 4 +- db/schema.rb | 11 ++ spec/factories/pages_domains.rb | 79 +++++++++++ spec/models/pages_domain_spec.rb | 152 +++++++++++++++++++++ spec/models/project_spec.rb | 1 + .../services/projects/update_pages_service_spec.rb | 80 +++++++++++ spec/services/projects/update_pages_worker_spec.rb | 80 ----------- 10 files changed, 373 insertions(+), 109 deletions(-) create mode 100644 spec/factories/pages_domains.rb create mode 100644 spec/models/pages_domain_spec.rb create mode 100644 spec/services/projects/update_pages_service_spec.rb delete mode 100644 spec/services/projects/update_pages_worker_spec.rb diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 985329bb856..b594957493a 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -6,7 +6,8 @@ class PagesDomain < ActiveRecord::Base validates :certificate, certificate: true, allow_nil: true, allow_blank: true validates :key, certificate_key: true, allow_nil: true, allow_blank: true - validate :validate_matching_key, if: ->(domain) { domain.certificate.present? && domain.key.present? } + validate :validate_pages_domain + validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? } validate :validate_intermediates, if: ->(domain) { domain.certificate.present? } attr_encrypted :key, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base @@ -30,8 +31,8 @@ class PagesDomain < ActiveRecord::Base end def has_matching_key? - return unless x509 - return unless pkey + return false unless x509 + return false unless pkey # We compare the public key stored in certificate with public key from certificate key x509.check_private_key(pkey) @@ -40,6 +41,9 @@ class PagesDomain < ActiveRecord::Base def has_intermediates? return false unless x509 + # self-signed certificates doesn't have the certificate chain + return true if x509.verify(x509.public_key) + store = OpenSSL::X509::Store.new store.set_default_paths @@ -66,23 +70,8 @@ class PagesDomain < ActiveRecord::Base return x509.subject.to_s end - def fingerprint - return unless x509 - @fingeprint ||= OpenSSL::Digest::SHA256.new(x509.to_der).to_s - end - - def x509 - return unless certificate - @x509 ||= OpenSSL::X509::Certificate.new(certificate) - rescue OpenSSL::X509::CertificateError - nil - end - - def pkey - return unless key - @pkey ||= OpenSSL::PKey::RSA.new(key) - rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError - nil + def certificate_text + @certificate_text ||= x509.try(:to_text) end private @@ -102,4 +91,25 @@ class PagesDomain < ActiveRecord::Base self.errors.add(:certificate, 'misses intermediates') end end + + def validate_pages_domain + return unless domain + if domain.downcase.ends_with?(".#{Settings.pages.host}".downcase) + self.errors.add(:domain, "*.#{Settings.pages.host} is restricted") + end + end + + def x509 + return unless certificate + @x509 ||= OpenSSL::X509::Certificate.new(certificate) + rescue OpenSSL::X509::CertificateError + nil + end + + def pkey + return unless key + @pkey ||= OpenSSL::PKey::RSA.new(key) + rescue OpenSSL::PKey::PKeyError, OpenSSL::Cipher::CipherError + nil + end end diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb index 53e9d9e2757..b5324587d0e 100644 --- a/app/services/projects/update_pages_configuration_service.rb +++ b/app/services/projects/update_pages_configuration_service.rb @@ -35,7 +35,7 @@ module Projects def reload_daemon # GitLab Pages daemon constantly watches for modification time of `pages.path` # It reloads configuration when `pages.path` is modified - File.touch(Settings.pages.path) + update_file(pages_update_file, SecureRandom.hex(64)) end def pages_path @@ -46,14 +46,24 @@ module Projects File.join(pages_path, 'config.json') end + def pages_update_file + File.join(Settings.pages.path, '.update') + end + def update_file(file, data) - if data - File.open(file, 'w') do |file| - file.write(data) - end - else + unless data File.rm(file, force: true) + return + end + + temp_file = "#{file}.#{SecureRandom.hex(16)}" + File.open(temp_file, 'w') do |file| + file.write(data) end + File.mv(temp_file, file, force: true) + ensure + # In case if the updating fails + File.rm(temp_file, force: true) end end end diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index ceabd29fd52..a9979bf1e96 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -1,5 +1,6 @@ module Projects - class UpdatePagesService < BaseService + class + UpdatePagesService < BaseService BLOCK_SIZE = 32.kilobytes MAX_SIZE = 1.terabyte SITE_PATH = 'public/' diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index 52493b1959b..8b7010b75b2 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -14,9 +14,9 @@ %td Certificate %td - - if @domain.x509 + - if @domain.certificate_text %pre - = @domain.x509.to_text + = @domain.certificate_text - else .light missing diff --git a/db/schema.rb b/db/schema.rb index 15f378b28ff..dc3d8c22e8d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -855,6 +855,17 @@ ActiveRecord::Schema.define(version: 20170130204620) do add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree + create_table "pages_domains", force: :cascade do |t| + t.integer "project_id" + t.text "certificate" + t.text "encrypted_key" + t.string "encrypted_key_iv" + t.string "encrypted_key_salt" + t.string "domain" + end + + add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree + create_table "personal_access_tokens", force: :cascade do |t| t.integer "user_id", null: false t.string "token", null: false diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb new file mode 100644 index 00000000000..4608867087c --- /dev/null +++ b/spec/factories/pages_domains.rb @@ -0,0 +1,79 @@ +FactoryGirl.define do + factory :pages_domain, class: 'PagesDomain' do + domain 'my.domain.com' + + trait :with_certificate do + certificate '-----BEGIN CERTIFICATE----- +MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0 +LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ +MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa +SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT +nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w +DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD +VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh +IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ +joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese +5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg +YHi2yesCrOvVXt+lgPTd +-----END CERTIFICATE-----' + end + + trait :with_key do + key '-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN +SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t +PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB +kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd +j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/ +uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR +5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O +AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K +EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh +Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C +m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH +EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx +63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi +nNp/xedE1YxutQ== +-----END PRIVATE KEY-----' + end + + trait :with_certificate_chain do + # This certificate is signed with different key + certificate '-----BEGIN CERTIFICATE----- +MIIDGTCCAgGgAwIBAgIBAjANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDEwdUZXN0 +IENBMB4XDTE2MDIxMjE0MjMwMFoXDTE3MDIxMTE0MjMwMFowHTEbMBkGA1UEAxMS +dGVzdC1jZXJ0aWZpY2F0ZS0yMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAw8RWetIUT0YymSuKvBpClzDv/jQdX0Ch+2iF7f4Lm3lcmoUuXgyhl/WRe5K9 +ONuMHPQlZbeavEbvWb0BsU7geInhsjd/zAu3EP17jfSIXToUdSD20wcSG/yclLdZ +qhb6NCtHTJKFUI8BktoS7kafkdvmeem/UJFzlvcA6VMyGDkS8ZN39a45R1jGmPEl +Yk0g1jW7lSKcBLjU1O/Csv59LyWXqBP6jR1vB8ijlUf1IyK8gOk7NHF13GHl7Z3A +/8zwuEt/pB3yK92o71P+FnSEcJ23zcAalz6H9ajVTzRr/AXttineBNVYnEuPXW+V +Rsboe+bBO/e4pVKXnQ1F3aMT7QIDAQABo28wbTAMBgNVHRMBAf8EAjAAMB0GA1Ud +DgQWBBSFwo3rhc26lD8ZVaBVcUY1NyCOLDALBgNVHQ8EBAMCBeAwEQYJYIZIAYb4 +QgEBBAQDAgZAMB4GCWCGSAGG+EIBDQQRFg94Y2EgY2VydGlmaWNhdGUwDQYJKoZI +hvcNAQEFBQADggEBABppUhunuT7qArM9gZ2gLgcOK8qyZWU8AJulvloaCZDvqGVs +Qom0iEMBrrt5+8bBevNiB49Tz7ok8NFgLzrlEnOw6y6QGjiI/g8sRKEiXl+ZNX8h +s8VN6arqT348OU8h2BixaXDmBF/IqZVApGhR8+B4fkCt0VQmdzVuHGbOQXMWJCpl +WlU8raZoPIqf6H/8JA97pM/nk/3CqCoHsouSQv+jGY4pSL22RqsO0ylIM0LDBbmF +m4AEaojTljX1tMJAF9Rbiw/omam5bDPq2JWtosrz/zB69y5FaQjc6FnCk0M4oN/+ +VM+d42lQAgoq318A84Xu5vRh1KCAJuztkhNbM+w= +-----END CERTIFICATE-----' + end + + trait :with_expired_certificate do + certificate '-----BEGIN CERTIFICATE----- +MIIBsDCCARmgAwIBAgIBATANBgkqhkiG9w0BAQUFADAeMRwwGgYDVQQDExNleHBp +cmVkLWNlcnRpZmljYXRlMB4XDTE1MDIxMjE0MzMwMFoXDTE2MDIwMTE0MzMwMFow +HjEcMBoGA1UEAxMTZXhwaXJlZC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEF +AAOBjQAwgYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2ge +NR1qlNFaSvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLyS +NT438kdTnY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEA +ATANBgkqhkiG9w0BAQUFAAOBgQBNj+vWvneyW1KkbVK+b/cVmnYPSfbkHrYK6m8X +Hq9LkWn6WP4EHsesHyslgTQZF8C7kVLTbLn2noLnOE+Mp3vcWlZxl3Yk6aZMhKS+ +Iy6oRpHaCF/2obZdIdgf9rlyz0fkqyHJc9GkioSoOhJZxEV2SgAkap8yS0sX2tJ9 +ZDXgrA== +-----END CERTIFICATE-----' + end + end +end diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb new file mode 100644 index 00000000000..929b2a26549 --- /dev/null +++ b/spec/models/pages_domain_spec.rb @@ -0,0 +1,152 @@ +require 'spec_helper' + +describe PagesDomain, models: true do + describe 'associations' do + it { is_expected.to belong_to(:project) } + end + + describe :validate_domain do + subject { build(:pages_domain, domain: domain) } + + context 'is unique' do + let(:domain) { 'my.domain.com' } + + it { is_expected.to validate_uniqueness_of(:domain) } + end + + context 'valid domain' do + let(:domain) { 'my.domain.com' } + + it { is_expected.to be_valid } + end + + context 'no domain' do + let(:domain) { nil } + + it { is_expected.to_not be_valid } + end + + context 'invalid domain' do + let(:domain) { '0123123' } + + it { is_expected.to_not be_valid } + end + + context 'domain from .example.com' do + let(:domain) { 'my.domain.com' } + + before { allow(Settings.pages).to receive(:host).and_return('domain.com') } + + it { is_expected.to_not be_valid } + end + end + + describe 'validate certificate' do + subject { domain } + + context 'when only certificate is specified' do + let(:domain) { build(:pages_domain, :with_certificate) } + + it { is_expected.to_not be_valid } + end + + context 'when only key is specified' do + let(:domain) { build(:pages_domain, :with_key) } + + it { is_expected.to_not be_valid } + end + + context 'with matching key' do + let(:domain) { build(:pages_domain, :with_certificate, :with_key) } + + it { is_expected.to be_valid } + end + + context 'for not matching key' do + let(:domain) { build(:pages_domain, :with_certificate_chain, :with_key) } + + it { is_expected.to_not be_valid } + end + end + + describe :url do + subject { domain.url } + + context 'without the certificate' do + let(:domain) { build(:pages_domain) } + + it { is_expected.to eq('http://my.domain.com') } + end + + context 'with a certificate' do + let(:domain) { build(:pages_domain, :with_certificate) } + + it { is_expected.to eq('https://my.domain.com') } + end + end + + describe :has_matching_key? do + subject { domain.has_matching_key? } + + context 'for matching key' do + let(:domain) { build(:pages_domain, :with_certificate, :with_key) } + + it { is_expected.to be_truthy } + end + + context 'for invalid key' do + let(:domain) { build(:pages_domain, :with_certificate_chain, :with_key) } + + it { is_expected.to be_falsey } + end + end + + describe :has_intermediates? do + subject { domain.has_intermediates? } + + context 'for self signed' do + let(:domain) { build(:pages_domain, :with_certificate) } + + it { is_expected.to be_truthy } + end + + context 'for certificate chain without the root' do + let(:domain) { build(:pages_domain, :with_certificate_chain) } + + it { is_expected.to be_falsey } + end + end + + describe :expired? do + subject { domain.expired? } + + context 'for valid' do + let(:domain) { build(:pages_domain, :with_certificate) } + + it { is_expected.to be_falsey } + end + + context 'for expired' do + let(:domain) { build(:pages_domain, :with_expired_certificate) } + + it { is_expected.to be_truthy } + end + end + + describe :subject do + let(:domain) { build(:pages_domain, :with_certificate) } + + subject { domain.subject } + + it { is_expected.to eq('/CN=test-certificate') } + end + + describe :certificate_text do + let(:domain) { build(:pages_domain, :with_certificate) } + + subject { domain.certificate_text } + + # We test only existence of output, since the output is long + it { is_expected.to_not be_empty } + end +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 48b085781e7..5fde9194e93 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -60,6 +60,7 @@ describe Project, models: true do it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:triggers) } + it { is_expected.to have_many(:pages_domains) } it { is_expected.to have_many(:labels).class_name('ProjectLabel').dependent(:destroy) } it { is_expected.to have_many(:users_star_projects).dependent(:destroy) } it { is_expected.to have_many(:environments).dependent(:destroy) } diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb new file mode 100644 index 00000000000..68e66866340 --- /dev/null +++ b/spec/services/projects/update_pages_service_spec.rb @@ -0,0 +1,80 @@ +require "spec_helper" + +describe Projects::UpdatePagesService do + let(:project) { create :project } + let(:commit) { create :ci_commit, project: project, sha: project.commit('HEAD').sha } + let(:build) { create :ci_build, commit: commit, ref: 'HEAD' } + let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') } + + subject { described_class.new(project, build) } + + before do + project.remove_pages + end + + %w(tar.gz zip).each do |format| + context "for valid #{format}" do + let(:file) { fixture_file_upload(Rails.root + "spec/fixtures/pages.#{format}") } + let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{format}") } + let(:metadata) do + filename = Rails.root + "spec/fixtures/pages.#{format}.meta" + fixture_file_upload(filename) if File.exists?(filename) + end + + before do + build.update_attributes(artifacts_file: file) + build.update_attributes(artifacts_metadata: metadata) + end + + it 'succeeds' do + expect(project.pages_url).to be_nil + expect(execute).to eq(:success) + expect(project.pages_url).to_not be_nil + end + + it 'limits pages size' do + stub_application_setting(max_pages_size: 1) + expect(execute).to_not eq(:success) + end + + it 'removes pages after destroy' do + expect(PagesWorker).to receive(:perform_in) + expect(project.pages_url).to be_nil + expect(execute).to eq(:success) + expect(project.pages_url).to_not be_nil + project.destroy + expect(Dir.exist?(project.public_pages_path)).to be_falsey + end + + it 'fails if sha on branch is not latest' do + commit.update_attributes(sha: 'old_sha') + build.update_attributes(artifacts_file: file) + expect(execute).to_not eq(:success) + end + + it 'fails for empty file fails' do + build.update_attributes(artifacts_file: empty_file) + expect(execute).to_not eq(:success) + end + end + end + + it 'fails to remove project pages when no pages is deployed' do + expect(PagesWorker).to_not receive(:perform_in) + expect(project.pages_url).to be_nil + project.destroy + end + + it 'fails if no artifacts' do + expect(execute).to_not eq(:success) + end + + it 'fails for invalid archive' do + build.update_attributes(artifacts_file: invalid_file) + expect(execute).to_not eq(:success) + end + + def execute + subject.execute[:status] + end +end diff --git a/spec/services/projects/update_pages_worker_spec.rb b/spec/services/projects/update_pages_worker_spec.rb deleted file mode 100644 index 68e66866340..00000000000 --- a/spec/services/projects/update_pages_worker_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -require "spec_helper" - -describe Projects::UpdatePagesService do - let(:project) { create :project } - let(:commit) { create :ci_commit, project: project, sha: project.commit('HEAD').sha } - let(:build) { create :ci_build, commit: commit, ref: 'HEAD' } - let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') } - - subject { described_class.new(project, build) } - - before do - project.remove_pages - end - - %w(tar.gz zip).each do |format| - context "for valid #{format}" do - let(:file) { fixture_file_upload(Rails.root + "spec/fixtures/pages.#{format}") } - let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{format}") } - let(:metadata) do - filename = Rails.root + "spec/fixtures/pages.#{format}.meta" - fixture_file_upload(filename) if File.exists?(filename) - end - - before do - build.update_attributes(artifacts_file: file) - build.update_attributes(artifacts_metadata: metadata) - end - - it 'succeeds' do - expect(project.pages_url).to be_nil - expect(execute).to eq(:success) - expect(project.pages_url).to_not be_nil - end - - it 'limits pages size' do - stub_application_setting(max_pages_size: 1) - expect(execute).to_not eq(:success) - end - - it 'removes pages after destroy' do - expect(PagesWorker).to receive(:perform_in) - expect(project.pages_url).to be_nil - expect(execute).to eq(:success) - expect(project.pages_url).to_not be_nil - project.destroy - expect(Dir.exist?(project.public_pages_path)).to be_falsey - end - - it 'fails if sha on branch is not latest' do - commit.update_attributes(sha: 'old_sha') - build.update_attributes(artifacts_file: file) - expect(execute).to_not eq(:success) - end - - it 'fails for empty file fails' do - build.update_attributes(artifacts_file: empty_file) - expect(execute).to_not eq(:success) - end - end - end - - it 'fails to remove project pages when no pages is deployed' do - expect(PagesWorker).to_not receive(:perform_in) - expect(project.pages_url).to be_nil - project.destroy - end - - it 'fails if no artifacts' do - expect(execute).to_not eq(:success) - end - - it 'fails for invalid archive' do - build.update_attributes(artifacts_file: invalid_file) - expect(execute).to_not eq(:success) - end - - def execute - subject.execute[:status] - end -end -- cgit v1.2.1 From b7fd7daee4610674a2301c618fd60b8997f2cf8a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Sun, 14 Feb 2016 19:58:45 +0100 Subject: Fix rubocop complains --- app/controllers/projects/pages_controller.rb | 21 ++++++++++----------- app/models/pages_domain.rb | 4 ++-- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index 0c7f2bd5784..2268d2d8aa2 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -43,26 +43,25 @@ class Projects::PagesController < Projects::ApplicationController end end - def destroy - @project.remove_pages - - respond_to do |format| - format.html { redirect_to project_path(@project) } - end - end - def remove_pages project.remove_pages project.pages_domains.destroy_all + + respond_to do |format| + format.html do + redirect_to(namespace_project_pages_path(@project.namespace, @project), + notice: 'Pages were removed') + end + end end private def pages_domain_params params.require(:pages_domain).permit( - :certificate, - :key, - :domain + :certificate, + :key, + :domain ) end diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index b594957493a..83fdc1c630d 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -62,12 +62,12 @@ class PagesDomain < ActiveRecord::Base def expired? return false unless x509 current = Time.new - return current < x509.not_before || x509.not_after < current + current < x509.not_before || x509.not_after < current end def subject return unless x509 - return x509.subject.to_s + x509.subject.to_s end def certificate_text -- cgit v1.2.1 From db35f3dc573fe5d07eb03de7690d98eef98784d3 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Sun, 14 Feb 2016 20:28:02 +0100 Subject: Add tests for Active Tab --- app/views/projects/pages/_disabled.html.haml | 2 +- features/project/active_tab.feature | 7 +++++++ features/steps/project/active_tab.rb | 8 ++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/views/projects/pages/_disabled.html.haml b/app/views/projects/pages/_disabled.html.haml index cf9ef5b4d6f..ad51fbc6cab 100644 --- a/app/views/projects/pages/_disabled.html.haml +++ b/app/views/projects/pages/_disabled.html.haml @@ -1,4 +1,4 @@ .panel.panel-default .nothing-here-block - GitLab Pages is disabled. + GitLab Pages are disabled. Ask your system's administrator to enable it. diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature index d033e6b167b..5c14c5db665 100644 --- a/features/project/active_tab.feature +++ b/features/project/active_tab.feature @@ -53,6 +53,13 @@ Feature: Project Active Tab And no other sub navs should be active And the active main tab should be Settings + Scenario: On Project Settings/Pages + Given I visit my project's settings page + And I click the "Pages" tab + Then the active sub nav should be Pages + And no other sub navs should be active + And the active main tab should be Settings + Scenario: On Project Members Given I visit my project's members page Then the active sub nav should be Members diff --git a/features/steps/project/active_tab.rb b/features/steps/project/active_tab.rb index 9f701840f1d..e842d7bec2b 100644 --- a/features/steps/project/active_tab.rb +++ b/features/steps/project/active_tab.rb @@ -35,6 +35,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps click_link('Deploy Keys') end + step 'I click the "Pages" tab' do + click_link('Pages') + end + step 'the active sub nav should be Members' do ensure_active_sub_nav('Members') end @@ -47,6 +51,10 @@ class Spinach::Features::ProjectActiveTab < Spinach::FeatureSteps ensure_active_sub_nav('Deploy Keys') end + step 'the active sub nav should be Pages' do + ensure_active_sub_nav('Pages') + end + # Sub Tabs: Commits step 'I click the "Compare" tab' do -- cgit v1.2.1 From 84edc9a22f5d858cb02f32d22b66c92fb939378a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Sun, 14 Feb 2016 21:06:24 +0100 Subject: Added spinach tests --- .../projects/pages_domains_controller.rb | 0 app/helpers/projects_helper.rb | 4 - app/views/projects/pages/_destroy.haml | 9 +- app/views/projects/pages/_form.html.haml | 2 +- app/views/projects/pages/index.html.haml | 6 +- features/project/pages.feature | 73 +++++++++++ features/steps/project/pages.rb | 139 +++++++++++++++++++++ 7 files changed, 220 insertions(+), 13 deletions(-) create mode 100644 app/controllers/projects/pages_domains_controller.rb create mode 100644 features/project/pages.feature create mode 100644 features/steps/project/pages.rb diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 054cc849839..eb98204285d 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -81,10 +81,6 @@ module ProjectsHelper "You are going to remove the fork relationship to source project #{@project.forked_from_project.name_with_namespace}. Are you ABSOLUTELY sure?" end - def remove_pages_message(project) - "You are going to remove the pages for #{project.name_with_namespace}.\n Are you ABSOLUTELY sure?" - end - def project_nav_tabs @nav_tabs ||= get_project_nav_tabs(@project, current_user) end diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index dd493a6d312..c560aca5725 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -3,8 +3,7 @@ .panel-heading Remove pages .errors-holder .panel-body - = form_tag(remove_pages_namespace_project_pages_path(@project.namespace, @project), method: :delete, class: 'form-horizontal') do - %p - Removing the pages will prevent from exposing them to outside world. - .form-actions - = button_to 'Remove pages', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_pages_message(@project) } + %p + Removing the pages will prevent from exposing them to outside world. + .form-actions + = link_to 'Remove', remove_pages_namespace_project_pages_path(@project.namespace, @project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove" diff --git a/app/views/projects/pages/_form.html.haml b/app/views/projects/pages/_form.html.haml index c69b76c6697..fd411462330 100644 --- a/app/views/projects/pages/_form.html.haml +++ b/app/views/projects/pages/_form.html.haml @@ -12,7 +12,7 @@ = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control' %span.help-inline * required - - if Settings.pages.external_https + - if Gitlab.config.pages.external_https .form-group = f.label :certificate, class: 'control-label' do Certificate (PEM) diff --git a/app/views/projects/pages/index.html.haml b/app/views/projects/pages/index.html.haml index 284f362d535..1a5dbb79830 100644 --- a/app/views/projects/pages/index.html.haml +++ b/app/views/projects/pages/index.html.haml @@ -2,7 +2,7 @@ %h3.page_title Pages - - if Settings.pages.external_http || Settings.pages.external_https + - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https = link_to new_namespace_project_page_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Domain" do %i.fa.fa-plus New Domain @@ -14,10 +14,10 @@ %hr.clearfix -- if Settings.pages.enabled +- if Gitlab.config.pages.enabled = render 'access' = render 'use' - - if Settings.pages.external_http || Settings.pages.external_https + - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https = render 'list' - else = render 'no_domains' diff --git a/features/project/pages.feature b/features/project/pages.feature new file mode 100644 index 00000000000..392f2d29c3c --- /dev/null +++ b/features/project/pages.feature @@ -0,0 +1,73 @@ +Feature: Project Pages + Background: + Given I sign in as a user + And I own a project + + Scenario: Pages are disabled + Given pages are disabled + When I visit the Project Pages + Then I should see that GitLab Pages are disabled + + Scenario: I can see the pages usage if not deployed + Given pages are enabled + When I visit the Project Pages + Then I should see the usage of GitLab Pages + + Scenario: I can access the pages if deployed + Given pages are enabled + And pages are deployed + When I visit the Project Pages + Then I should be able to access the Pages + + Scenario: I should message that domains support is disabled + Given pages are enabled + And pages are deployed + And support for external domains is disabled + When I visit the Project Pages + Then I should see that support for domains is disabled + + Scenario: I should see a new domain button + Given pages are enabled + And pages are exposed on external HTTP address + When I visit the Project Pages + And I should be able to add a New Domain + + Scenario: I should be able to add a new domain + Given pages are enabled + And pages are exposed on external HTTP address + When I visit add a new Pages Domain + And I fill the domain + And I click on "Create New Domain" + Then I should see a new domain added + + Scenario: I should be denied to add the same domain twice + Given pages are enabled + And pages are exposed on external HTTP address + And pages domain is added + When I visit add a new Pages Domain + And I fill the domain + And I click on "Create New Domain" + Then I should see error message that domain already exists + + Scenario: I should message that certificates support is disabled when trying to add a new domain + Given pages are enabled + And pages are exposed on external HTTP address + And pages domain is added + When I visit add a new Pages Domain + Then I should see that support for certificates is disabled + + Scenario: I should be able to add a new domain with certificate + Given pages are enabled + And pages are exposed on external HTTPS address + When I visit add a new Pages Domain + And I fill the domain + And I fill the certificate and key + And I click on "Create New Domain" + Then I should see a new domain added + + Scenario: I can remove the pages if deployed + Given pages are enabled + And pages are deployed + When I visit the Project Pages + And I click Remove Pages + Then The Pages should get removed diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb new file mode 100644 index 00000000000..d484ae90bdc --- /dev/null +++ b/features/steps/project/pages.rb @@ -0,0 +1,139 @@ +class Spinach::Features::ProjectPages < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedProject + + step 'pages are enabled' do + Gitlab.config.pages.stub(:enabled).and_return(true) + Gitlab.config.pages.stub(:host).and_return('example.com') + Gitlab.config.pages.stub(:port).and_return(80) + Gitlab.config.pages.stub(:https).and_return(false) + end + + step 'pages are disabled' do + Gitlab.config.pages.stub(:enabled).and_return(false) + end + + step 'I visit the Project Pages' do + visit namespace_project_pages_path(@project.namespace, @project) + end + + step 'I should see that GitLab Pages are disabled' do + expect(page).to have_content('GitLab Pages are disabled') + end + + step 'I should see the usage of GitLab Pages' do + expect(page).to have_content('Configure pages') + end + + step 'pages are deployed' do + commit = @project.ensure_ci_commit(@project.commit('HEAD').sha) + build = build(:ci_build, + project: @project, + commit: commit, + ref: 'HEAD', + artifacts_file: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip'), + artifacts_metadata: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta') + ) + result = ::Projects::UpdatePagesService.new(@project, build).execute + expect(result[:status]).to eq(:success) + end + + step 'I should be able to access the Pages' do + expect(page).to have_content('Access pages') + end + + step 'I should see that support for domains is disabled' do + expect(page).to have_content('Support for domains and certificates is disabled') + end + + step 'support for external domains is disabled' do + Gitlab.config.pages.stub(:external_http).and_return(nil) + Gitlab.config.pages.stub(:external_https).and_return(nil) + end + + step 'pages are exposed on external HTTP address' do + Gitlab.config.pages.stub(:external_http).and_return('1.1.1.1:80') + Gitlab.config.pages.stub(:external_https).and_return(nil) + end + + step 'pages are exposed on external HTTPS address' do + Gitlab.config.pages.stub(:external_http).and_return('1.1.1.1:80') + Gitlab.config.pages.stub(:external_https).and_return('1.1.1.1:443') + end + + step 'I should be able to add a New Domain' do + expect(page).to have_content('New Domain') + end + + step 'I visit add a new Pages Domain' do + visit new_namespace_project_page_path(@project.namespace, @project) + end + + step 'I fill the domain' do + fill_in 'Domain', with: 'my.test.domain.com' + end + + step 'I click on "Create New Domain"' do + click_button 'Create New Domain' + end + + step 'I should see a new domain added' do + expect(page).to have_content('Domains (1)') + expect(page).to have_content('my.test.domain.com') + end + + step 'pages domain is added' do + @project.pages_domains.create!(domain: 'my.test.domain.com') + end + + step 'I should see error message that domain already exists' do + expect(page).to have_content('Domain has already been taken') + end + + step 'I should see that support for certificates is disabled' do + expect(page).to have_content('Support for custom certificates is disabled') + end + + step 'I fill the certificate and key' do + fill_in 'Certificate (PEM)', with: '-----BEGIN CERTIFICATE----- +MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0 +LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ +MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa +SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT +nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w +DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD +VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh +IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ +joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese +5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg +YHi2yesCrOvVXt+lgPTd +-----END CERTIFICATE-----' + + fill_in 'Key (PEM)', with: '-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN +SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t +PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB +kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd +j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/ +uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR +5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O +AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K +EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh +Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C +m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH +EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx +63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi +nNp/xedE1YxutQ== +-----END PRIVATE KEY-----' + end + + step 'I click Remove Pages' do + click_link 'Remove pages' + end + + step 'The Pages should get removed' do + expect(@project.pages_url).to be_nil + end +end -- cgit v1.2.1 From 7f12cb0eed06ad3f83126a3a8038e7fa658f4eac Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Sun, 14 Feb 2016 21:22:44 +0100 Subject: Split PagesController into PagesController and PagesDomainsController 1. PagesController is used to show all domains and general overview of Pages 2. PagesDomainsController is used to manage pages domains --- app/controllers/projects/pages_controller.rb | 54 +--------------------- .../projects/pages_domains_controller.rb | 49 ++++++++++++++++++++ app/views/projects/pages/_destroy.haml | 2 +- app/views/projects/pages/_form.html.haml | 35 -------------- app/views/projects/pages/_list.html.haml | 4 +- app/views/projects/pages/index.html.haml | 26 ----------- app/views/projects/pages/new.html.haml | 6 --- app/views/projects/pages/show.html.haml | 44 ++++++++++-------- app/views/projects/pages_domains/_form.html.haml | 35 ++++++++++++++ app/views/projects/pages_domains/new.html.haml | 6 +++ app/views/projects/pages_domains/show.html.haml | 22 +++++++++ config/routes/project.rb | 6 +-- features/steps/project/pages.rb | 2 +- 13 files changed, 144 insertions(+), 147 deletions(-) delete mode 100644 app/views/projects/pages/_form.html.haml delete mode 100644 app/views/projects/pages/index.html.haml delete mode 100644 app/views/projects/pages/new.html.haml create mode 100644 app/views/projects/pages_domains/_form.html.haml create mode 100644 app/views/projects/pages_domains/new.html.haml create mode 100644 app/views/projects/pages_domains/show.html.haml diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index 2268d2d8aa2..b73f998392d 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -1,49 +1,13 @@ class Projects::PagesController < Projects::ApplicationController layout 'project_settings' - before_action :authorize_update_pages!, except: [:show] - before_action :authorize_remove_pages!, only: [:remove_pages] - before_action :label, only: [:destroy] - before_action :domain, only: [:show] - - helper_method :valid_certificate?, :valid_certificate_key? - helper_method :valid_key_for_certificiate?, :valid_certificate_intermediates? - helper_method :certificate, :certificate_key - - def index - @domains = @project.pages_domains.order(:domain) - end + before_action :authorize_update_pages! def show - end - - def new - @domain = @project.pages_domains.new - end - - def create - @domain = @project.pages_domains.create(pages_domain_params) - - if @domain.valid? - redirect_to namespace_project_pages_path(@project.namespace, @project) - else - render 'new' - end + @domains = @project.pages_domains.order(:domain) end def destroy - @domain.destroy - - respond_to do |format| - format.html do - redirect_to(namespace_project_pages_path(@project.namespace, @project), - notice: 'Domain was removed') - end - format.js - end - end - - def remove_pages project.remove_pages project.pages_domains.destroy_all @@ -54,18 +18,4 @@ class Projects::PagesController < Projects::ApplicationController end end end - - private - - def pages_domain_params - params.require(:pages_domain).permit( - :certificate, - :key, - :domain - ) - end - - def domain - @domain ||= @project.pages_domains.find_by(domain: params[:id].to_s) - end end diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb index e69de29bb2d..b8c253f6ae3 100644 --- a/app/controllers/projects/pages_domains_controller.rb +++ b/app/controllers/projects/pages_domains_controller.rb @@ -0,0 +1,49 @@ +class Projects::PagesDomainsController < Projects::ApplicationController + layout 'project_settings' + + before_action :authorize_update_pages!, except: [:show] + before_action :domain, only: [:show, :destroy] + + def show + end + + def new + @domain = @project.pages_domains.new + end + + def create + @domain = @project.pages_domains.create(pages_domain_params) + + if @domain.valid? + redirect_to namespace_project_pages_path(@project.namespace, @project) + else + render 'new' + end + end + + def destroy + @domain.destroy + + respond_to do |format| + format.html do + redirect_to(namespace_project_pages_path(@project.namespace, @project), + notice: 'Domain was removed') + end + format.js + end + end + + private + + def pages_domain_params + params.require(:pages_domain).permit( + :certificate, + :key, + :domain + ) + end + + def domain + @domain ||= @project.pages_domains.find_by(domain: params[:id].to_s) + end +end diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index c560aca5725..0cd25f82cd4 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -6,4 +6,4 @@ %p Removing the pages will prevent from exposing them to outside world. .form-actions - = link_to 'Remove', remove_pages_namespace_project_pages_path(@project.namespace, @project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove" + = link_to 'Remove pages', namespace_project_pages_path(@project.namespace, @project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove" diff --git a/app/views/projects/pages/_form.html.haml b/app/views/projects/pages/_form.html.haml deleted file mode 100644 index fd411462330..00000000000 --- a/app/views/projects/pages/_form.html.haml +++ /dev/null @@ -1,35 +0,0 @@ -= form_for [@domain], url: namespace_project_pages_path(@project.namespace, @project), html: { class: 'form-horizontal fieldset-form' } do |f| - - if @domain.errors.any? - #error_explanation - .alert.alert-danger - - @domain.errors.full_messages.each do |msg| - %p= msg - - .form-group - = f.label :domain, class: 'control-label' do - Domain - .col-sm-10 - = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control' - %span.help-inline * required - - - if Gitlab.config.pages.external_https - .form-group - = f.label :certificate, class: 'control-label' do - Certificate (PEM) - .col-sm-10 - = f.text_area :certificate, rows: 5, class: 'form-control', value: '' - %span.help-inline Upload a certificate for your domain with all intermediates - - .form-group - = f.label :key, class: 'control-label' do - Key (PEM) - .col-sm-10 - = f.text_area :key, rows: 5, class: 'form-control', value: '' - %span.help-inline Upload a certificate for your domain with all intermediates - - else - .nothing-here-block - Support for custom certificates is disabled. - Ask your system's administrator to enable it. - - .form-actions - = f.submit 'Create New Domain', class: "btn btn-save" diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml index e88a001d636..c1a6948a574 100644 --- a/app/views/projects/pages/_list.html.haml +++ b/app/views/projects/pages/_list.html.haml @@ -6,8 +6,8 @@ - @domains.each do |domain| %li .pull-right - = link_to 'Details', namespace_project_page_path(@project.namespace, @project, domain), class: "btn btn-sm btn-grouped" - = link_to 'Remove', namespace_project_page_path(@project.namespace, @project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" + = link_to 'Details', namespace_project_pages_domain_path(@project.namespace, @project, domain), class: "btn btn-sm btn-grouped" + = link_to 'Remove', namespace_project_pages_domain_path(@project.namespace, @project, domain), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped" .clearfix %span= link_to domain.domain, domain.url %p diff --git a/app/views/projects/pages/index.html.haml b/app/views/projects/pages/index.html.haml deleted file mode 100644 index 1a5dbb79830..00000000000 --- a/app/views/projects/pages/index.html.haml +++ /dev/null @@ -1,26 +0,0 @@ -- page_title "Pages" -%h3.page_title - Pages - - - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https - = link_to new_namespace_project_page_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Domain" do - %i.fa.fa-plus - New Domain - -%p.light - With GitLab Pages you can host for free your static websites on GitLab. - Combined with the power of GitLab CI and the help of GitLab Runner - you can deploy static pages for your individual projects, your user or your group. - -%hr.clearfix - -- if Gitlab.config.pages.enabled - = render 'access' - = render 'use' - - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https - = render 'list' - - else - = render 'no_domains' - = render 'destroy' -- else - = render 'disabled' diff --git a/app/views/projects/pages/new.html.haml b/app/views/projects/pages/new.html.haml deleted file mode 100644 index 2609df62aac..00000000000 --- a/app/views/projects/pages/new.html.haml +++ /dev/null @@ -1,6 +0,0 @@ -- page_title 'Pages' -%h3.page_title - New Pages Domain -%hr.clearfix -%div - = render 'form' diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index 8b7010b75b2..9be6f8678cf 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -1,22 +1,26 @@ -- page_title "#{@domain.domain}", "Pages Domain" +- page_title "Pages" +%h3.page_title + Pages -%h3.page-title - Pages Domain + - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https + = link_to new_namespace_project_pages_domain_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Domain" do + %i.fa.fa-plus + New Domain -.table-holder - %table.table - %tr - %td - Domain - %td - = link_to @domain.domain, @domain.url - %tr - %td - Certificate - %td - - if @domain.certificate_text - %pre - = @domain.certificate_text - - else - .light - missing +%p.light + With GitLab Pages you can host for free your static websites on GitLab. + Combined with the power of GitLab CI and the help of GitLab Runner + you can deploy static pages for your individual projects, your user or your group. + +%hr.clearfix + +- if Gitlab.config.pages.enabled + = render 'access' + = render 'use' + - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https + = render 'list' + - else + = render 'no_domains' + = render 'destroy' +- else + = render 'disabled' diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml new file mode 100644 index 00000000000..5458f9e7734 --- /dev/null +++ b/app/views/projects/pages_domains/_form.html.haml @@ -0,0 +1,35 @@ += form_for [@project.namespace, @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f| + - if @domain.errors.any? + #error_explanation + .alert.alert-danger + - @domain.errors.full_messages.each do |msg| + %p= msg + + .form-group + = f.label :domain, class: 'control-label' do + Domain + .col-sm-10 + = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control' + %span.help-inline * required + + - if Gitlab.config.pages.external_https + .form-group + = f.label :certificate, class: 'control-label' do + Certificate (PEM) + .col-sm-10 + = f.text_area :certificate, rows: 5, class: 'form-control', value: '' + %span.help-inline Upload a certificate for your domain with all intermediates + + .form-group + = f.label :key, class: 'control-label' do + Key (PEM) + .col-sm-10 + = f.text_area :key, rows: 5, class: 'form-control', value: '' + %span.help-inline Upload a certificate for your domain with all intermediates + - else + .nothing-here-block + Support for custom certificates is disabled. + Ask your system's administrator to enable it. + + .form-actions + = f.submit 'Create New Domain', class: "btn btn-save" diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml new file mode 100644 index 00000000000..2609df62aac --- /dev/null +++ b/app/views/projects/pages_domains/new.html.haml @@ -0,0 +1,6 @@ +- page_title 'Pages' +%h3.page_title + New Pages Domain +%hr.clearfix +%div + = render 'form' diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml new file mode 100644 index 00000000000..8b7010b75b2 --- /dev/null +++ b/app/views/projects/pages_domains/show.html.haml @@ -0,0 +1,22 @@ +- page_title "#{@domain.domain}", "Pages Domain" + +%h3.page-title + Pages Domain + +.table-holder + %table.table + %tr + %td + Domain + %td + = link_to @domain.domain, @domain.url + %tr + %td + Certificate + %td + - if @domain.certificate_text + %pre + = @domain.certificate_text + - else + .light + missing diff --git a/config/routes/project.rb b/config/routes/project.rb index 7a41cb81bfa..ea3bfdd45e6 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -39,10 +39,8 @@ constraints(ProjectUrlConstrainer.new) do end end - resources :pages, except: [:edit, :update] do - collection do - delete :remove_pages - end + resource :pages, only: [:show, :destroy] do + resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains' end resources :compare, only: [:index, :create] do diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb index d484ae90bdc..a5cb81b0ef3 100644 --- a/features/steps/project/pages.rb +++ b/features/steps/project/pages.rb @@ -67,7 +67,7 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps end step 'I visit add a new Pages Domain' do - visit new_namespace_project_page_path(@project.namespace, @project) + visit new_namespace_project_pages_domain_path(@project.namespace, @project) end step 'I fill the domain' do -- cgit v1.2.1 From c6723ed3e018d6249ca9409ebd08c04bd76dea97 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Mon, 15 Feb 2016 14:44:54 +0100 Subject: Updated configuration saving --- app/services/projects/update_pages_configuration_service.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb index b5324587d0e..188847b5ad6 100644 --- a/app/services/projects/update_pages_configuration_service.rb +++ b/app/services/projects/update_pages_configuration_service.rb @@ -7,7 +7,7 @@ module Projects end def execute - update_file(pages_config_file, pages_config) + update_file(pages_config_file, pages_config.to_json) reload_daemon success rescue => e @@ -52,18 +52,18 @@ module Projects def update_file(file, data) unless data - File.rm(file, force: true) + FileUtils.remove(file, force: true) return end temp_file = "#{file}.#{SecureRandom.hex(16)}" - File.open(temp_file, 'w') do |file| - file.write(data) + File.open(temp_file, 'w') do |f| + f.write(data) end - File.mv(temp_file, file, force: true) + FileUtils.move(temp_file, file, force: true) ensure # In case if the updating fails - File.rm(temp_file, force: true) + FileUtils.remove(temp_file, force: true) end end end -- cgit v1.2.1 From fd7756ec3741e503197d451927849a09526423f6 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Mon, 15 Feb 2016 14:45:11 +0100 Subject: Added information about the CNAME record --- app/views/projects/pages_domains/show.html.haml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml index 8b7010b75b2..e4c5922d863 100644 --- a/app/views/projects/pages_domains/show.html.haml +++ b/app/views/projects/pages_domains/show.html.haml @@ -10,6 +10,14 @@ Domain %td = link_to @domain.domain, @domain.url + %tr + %td + DNS + %td + %p + To access the domain create a new DNS record: + %pre + #{@domain.domain} CNAME #{@domain.project.namespace.path}.#{Settings.pages.host}. %tr %td Certificate -- cgit v1.2.1 From 361047a7911dbf5da3c33aefa5c77a43621e5514 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Mon, 15 Feb 2016 15:01:42 +0100 Subject: Updated according to comments --- app/models/pages_domain.rb | 4 ++-- app/services/projects/update_pages_service.rb | 3 +-- app/views/projects/pages/show.html.haml | 6 +++--- app/views/projects/pages_domains/_form.html.haml | 7 +++---- app/views/projects/pages_domains/new.html.haml | 2 +- app/views/projects/pages_domains/show.html.haml | 2 +- 6 files changed, 11 insertions(+), 13 deletions(-) diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 83fdc1c630d..9155e57331d 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -24,9 +24,9 @@ class PagesDomain < ActiveRecord::Base return unless domain if certificate - return "https://#{domain}" + "https://#{domain}" else - return "http://#{domain}" + "http://#{domain}" end end diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index a9979bf1e96..ceabd29fd52 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -1,6 +1,5 @@ module Projects - class - UpdatePagesService < BaseService + class UpdatePagesService < BaseService BLOCK_SIZE = 32.kilobytes MAX_SIZE = 1.terabyte SITE_PATH = 'public/' diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index 9be6f8678cf..f4ca33f418b 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -1,14 +1,14 @@ -- page_title "Pages" +- page_title 'Pages' %h3.page_title Pages - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https - = link_to new_namespace_project_pages_domain_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Domain" do + = link_to new_namespace_project_pages_domain_path(@project.namespace, @project), class: 'btn btn-new pull-right', title: 'New Domain' do %i.fa.fa-plus New Domain %p.light - With GitLab Pages you can host for free your static websites on GitLab. + With GitLab Pages you can host your static websites on GitLab. Combined with the power of GitLab CI and the help of GitLab Runner you can deploy static pages for your individual projects, your user or your group. diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml index 5458f9e7734..e97d19653d5 100644 --- a/app/views/projects/pages_domains/_form.html.haml +++ b/app/views/projects/pages_domains/_form.html.haml @@ -10,22 +10,21 @@ Domain .col-sm-10 = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control' - %span.help-inline * required - if Gitlab.config.pages.external_https .form-group = f.label :certificate, class: 'control-label' do Certificate (PEM) .col-sm-10 - = f.text_area :certificate, rows: 5, class: 'form-control', value: '' + = f.text_area :certificate, rows: 5, class: 'form-control' %span.help-inline Upload a certificate for your domain with all intermediates .form-group = f.label :key, class: 'control-label' do Key (PEM) .col-sm-10 - = f.text_area :key, rows: 5, class: 'form-control', value: '' - %span.help-inline Upload a certificate for your domain with all intermediates + = f.text_area :key, rows: 5, class: 'form-control' + %span.help-inline Upload a private key for your certificate - else .nothing-here-block Support for custom certificates is disabled. diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml index 2609df62aac..e1477c71d06 100644 --- a/app/views/projects/pages_domains/new.html.haml +++ b/app/views/projects/pages_domains/new.html.haml @@ -1,4 +1,4 @@ -- page_title 'Pages' +- page_title 'New Pages Domain' %h3.page_title New Pages Domain %hr.clearfix diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml index e4c5922d863..52dddb052a7 100644 --- a/app/views/projects/pages_domains/show.html.haml +++ b/app/views/projects/pages_domains/show.html.haml @@ -1,4 +1,4 @@ -- page_title "#{@domain.domain}", "Pages Domain" +- page_title "#{@domain.domain}", 'Pages Domains' %h3.page-title Pages Domain -- cgit v1.2.1 From 4d2337175872217eb22f035c3fcd981a38e8a374 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 16 Feb 2016 10:48:51 +0100 Subject: Final fixes --- config/gitlab.yml.example | 4 ++-- features/steps/project/pages.rb | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index f2bde602795..1cf24e3f3db 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -165,8 +165,8 @@ production: &base host: example.com port: 80 # Set to 443 if you serve the pages with HTTPS https: false # Set to true if you serve the pages with HTTPS - # external_http: "1.1.1.1:80" # if defined notifies the GitLab pages do support Custom Domains - # external_https: "1.1.1.1:443" # if defined notifies the GitLab pages do support Custom Domains with Certificates + # external_http: "1.1.1.1:80" # If defined, enables custom domain support in GitLab Pages + # external_https: "1.1.1.1:443" # If defined, enables custom domain and certificate support in GitLab Pages ## Mattermost ## For enabling Add to Mattermost button diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb index a5cb81b0ef3..ac44aac9e38 100644 --- a/features/steps/project/pages.rb +++ b/features/steps/project/pages.rb @@ -4,14 +4,14 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps include SharedProject step 'pages are enabled' do - Gitlab.config.pages.stub(:enabled).and_return(true) - Gitlab.config.pages.stub(:host).and_return('example.com') - Gitlab.config.pages.stub(:port).and_return(80) - Gitlab.config.pages.stub(:https).and_return(false) + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + allow(Gitlab.config.pages).to receive(:host).and_return('example.com') + allow(Gitlab.config.pages).to receive(:port).and_return(80) + allow(Gitlab.config.pages).to receive(:https).and_return(false) end step 'pages are disabled' do - Gitlab.config.pages.stub(:enabled).and_return(false) + allow(Gitlab.config.pages).to receive(:enabled).and_return(false) end step 'I visit the Project Pages' do @@ -48,18 +48,18 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps end step 'support for external domains is disabled' do - Gitlab.config.pages.stub(:external_http).and_return(nil) - Gitlab.config.pages.stub(:external_https).and_return(nil) + allow(Gitlab.config.pages).to receive(:external_http).and_return(nil) + allow(Gitlab.config.pages).to receive(:external_https).and_return(nil) end step 'pages are exposed on external HTTP address' do - Gitlab.config.pages.stub(:external_http).and_return('1.1.1.1:80') - Gitlab.config.pages.stub(:external_https).and_return(nil) + allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80') + allow(Gitlab.config.pages).to receive(:external_https).and_return(nil) end step 'pages are exposed on external HTTPS address' do - Gitlab.config.pages.stub(:external_http).and_return('1.1.1.1:80') - Gitlab.config.pages.stub(:external_https).and_return('1.1.1.1:443') + allow(Gitlab.config.pages).to receive(:external_http).and_return('1.1.1.1:80') + allow(Gitlab.config.pages).to receive(:external_https).and_return('1.1.1.1:443') end step 'I should be able to add a New Domain' do -- cgit v1.2.1 From 3bcb65c9f2ceaa4213c6d3eb1641ecb0f07d35ad Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 16 Feb 2016 13:32:38 +0100 Subject: Added pages version [ci skip] --- GITLAB_PAGES_VERSION | 1 + 1 file changed, 1 insertion(+) create mode 100644 GITLAB_PAGES_VERSION diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION new file mode 100644 index 00000000000..6e8bf73aa55 --- /dev/null +++ b/GITLAB_PAGES_VERSION @@ -0,0 +1 @@ +0.1.0 -- cgit v1.2.1 From 8f09ec28379da331fb5bd4a4da950def7b83dd94 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 16 Feb 2016 14:39:58 +0100 Subject: Verify trusted certificate chain --- spec/factories/pages_domains.rb | 78 +++++++++++++++++++++++++++++++++++++++- spec/models/pages_domain_spec.rb | 14 +++++--- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb index 4608867087c..ff72df8dc02 100644 --- a/spec/factories/pages_domains.rb +++ b/spec/factories/pages_domains.rb @@ -38,8 +38,9 @@ nNp/xedE1YxutQ== -----END PRIVATE KEY-----' end - trait :with_certificate_chain do + trait :with_missing_chain do # This certificate is signed with different key + # And misses the CA to build trust chain certificate '-----BEGIN CERTIFICATE----- MIIDGTCCAgGgAwIBAgIBAjANBgkqhkiG9w0BAQUFADASMRAwDgYDVQQDEwdUZXN0 IENBMB4XDTE2MDIxMjE0MjMwMFoXDTE3MDIxMTE0MjMwMFowHTEbMBkGA1UEAxMS @@ -61,6 +62,81 @@ VM+d42lQAgoq318A84Xu5vRh1KCAJuztkhNbM+w= -----END CERTIFICATE-----' end + trait :with_trusted_chain do + # This is + # [Intermediate #2 (SHA-2)] 'Comodo RSA Domain Validation Secure Server CA' + # [Intermediate #1 (SHA-2)] COMODO RSA Certification Authority + # We only validate that we want to rebuild the trust chain, + # we don't need end-to-end certificate to do that + certificate '-----BEGIN CERTIFICATE----- +MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQwMjEy +MDAwMDAwWhcNMjkwMjExMjM1OTU5WjCBkDELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxNjA0BgNVBAMTLUNPTU9ETyBSU0EgRG9tYWluIFZh +bGlkYXRpb24gU2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEP +ADCCAQoCggEBAI7CAhnhoFmk6zg1jSz9AdDTScBkxwtiBUUWOqigwAwCfx3M28Sh +bXcDow+G+eMGnD4LgYqbSRutA776S9uMIO3Vzl5ljj4Nr0zCsLdFXlIvNN5IJGS0 +Qa4Al/e+Z96e0HqnU4A7fK31llVvl0cKfIWLIpeNs4TgllfQcBhglo/uLQeTnaG6 +ytHNe+nEKpooIZFNb5JPJaXyejXdJtxGpdCsWTWM/06RQ1A/WZMebFEh7lgUq/51 +UHg+TLAchhP6a5i84DuUHoVS3AOTJBhuyydRReZw3iVDpA3hSqXttn7IzW3uLh0n +c13cRTCAquOyQQuvvUSH2rnlG51/ruWFgqUCAwEAAaOCAWUwggFhMB8GA1UdIwQY +MBaAFLuvfgI9+qbxPISOre44mOzZMjLUMB0GA1UdDgQWBBSQr2o6lFoL2JDqElZz +30O0Oija5zAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNV +HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwGwYDVR0gBBQwEjAGBgRVHSAAMAgG +BmeBDAECATBMBgNVHR8ERTBDMEGgP6A9hjtodHRwOi8vY3JsLmNvbW9kb2NhLmNv +bS9DT01PRE9SU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBxBggrBgEFBQcB +AQRlMGMwOwYIKwYBBQUHMAKGL2h0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NPTU9E +T1JTQUFkZFRydXN0Q0EuY3J0MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21v +ZG9jYS5jb20wDQYJKoZIhvcNAQEMBQADggIBAE4rdk+SHGI2ibp3wScF9BzWRJ2p +mj6q1WZmAT7qSeaiNbz69t2Vjpk1mA42GHWx3d1Qcnyu3HeIzg/3kCDKo2cuH1Z/ +e+FE6kKVxF0NAVBGFfKBiVlsit2M8RKhjTpCipj4SzR7JzsItG8kO3KdY3RYPBps +P0/HEZrIqPW1N+8QRcZs2eBelSaz662jue5/DJpmNXMyYE7l3YphLG5SEXdoltMY +dVEVABt0iN3hxzgEQyjpFv3ZBdRdRydg1vs4O2xyopT4Qhrf7W8GjEXCBgCq5Ojc +2bXhc3js9iPc0d1sjhqPpepUfJa3w/5Vjo1JXvxku88+vZbrac2/4EjxYoIQ5QxG +V/Iz2tDIY+3GH5QFlkoakdH368+PUq4NCNk+qKBR6cGHdNXJ93SrLlP7u3r7l+L4 +HyaPs9Kg4DdbKDsx5Q5XLVq4rXmsXiBmGqW5prU5wfWYQ//u+aen/e7KJD2AFsQX +j4rBYKEMrltDR5FL1ZoXX/nUh8HCjLfn4g8wGTeGrODcQgPmlKidrv0PJFGUzpII +0fxQ8ANAe4hZ7Q7drNJ3gjTcBpUC2JD5Leo31Rpg0Gcg19hCC0Wvgmje3WYkN5Ap +lBlGGSW4gNfL1IYoakRwJiNiqZ+Gb7+6kHDSVneFeO/qJakXzlByjAA6quPbYzSf ++AZxAeKCINT+b72x +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFdDCCBFygAwIBAgIQJ2buVutJ846r13Ci/ITeIjANBgkqhkiG9w0BAQwFADBv +MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk +ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF +eHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFow +gYUxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO +BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMSswKQYD +VQQDEyJDT01PRE8gUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkq +hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkehUktIKVrGsDSTdxc9EZ3SZKzejfSNw +AHG8U9/E+ioSj0t/EFa9n3Byt2F/yUsPF6c947AEYe7/EZfH9IY+Cvo+XPmT5jR6 +2RRr55yzhaCCenavcZDX7P0N+pxs+t+wgvQUfvm+xKYvT3+Zf7X8Z0NyvQwA1onr +ayzT7Y+YHBSrfuXjbvzYqOSSJNpDa2K4Vf3qwbxstovzDo2a5JtsaZn4eEgwRdWt +4Q08RWD8MpZRJ7xnw8outmvqRsfHIKCxH2XeSAi6pE6p8oNGN4Tr6MyBSENnTnIq +m1y9TBsoilwie7SrmNnu4FGDwwlGTm0+mfqVF9p8M1dBPI1R7Qu2XK8sYxrfV8g/ +vOldxJuvRZnio1oktLqpVj3Pb6r/SVi+8Kj/9Lit6Tf7urj0Czr56ENCHonYhMsT +8dm74YlguIwoVqwUHZwK53Hrzw7dPamWoUi9PPevtQ0iTMARgexWO/bTouJbt7IE +IlKVgJNp6I5MZfGRAy1wdALqi2cVKWlSArvX31BqVUa/oKMoYX9w0MOiqiwhqkfO +KJwGRXa/ghgntNWutMtQ5mv0TIZxMOmm3xaG4Nj/QN370EKIf6MzOi5cHkERgWPO +GHFrK+ymircxXDpqR+DDeVnWIBqv8mqYqnK8V0rSS527EPywTEHl7R09XiidnMy/ +s1Hap0flhFMCAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTvA73g +JMtUGjAdBgNVHQ4EFgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQD +AgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQGA1UdHwQ9 +MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVzdEV4dGVy +bmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0dHA6 +Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggEBAGS/g/FfmoXQ +zbihKVcN6Fr30ek+8nYEbvFScLsePP9NDXRqzIGCJdPDoCpdTPW6i6FtxFQJdcfj +Jw5dhHk3QBN39bSsHNA7qxcS1u80GH4r6XnTq1dFDK8o+tDb5VCViLvfhVdpfZLY +Uspzgb8c8+a4bmYRBbMelC1/kZWSWfFMzqORcUx8Rww7Cxn2obFshj5cqsQugsv5 +B5a6SE2Q8pTIqXOi6wZ7I53eovNNVZ96YUWYGGjHXkBrI/V5eu+MtWuLt29G9Hvx +PUsE2JOAWVrgQSQdso8VYFhH2+9uRv0V9dlfmrPb2LjkQLPNlzmuhbsdjrzch5vR +pu/xO28QOG8= +-----END CERTIFICATE-----' + end + trait :with_expired_certificate do certificate '-----BEGIN CERTIFICATE----- MIIBsDCCARmgAwIBAgIBATANBgkqhkiG9w0BAQUFADAeMRwwGgYDVQQDExNleHBp diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index 929b2a26549..3e083ba9001 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -63,7 +63,7 @@ describe PagesDomain, models: true do end context 'for not matching key' do - let(:domain) { build(:pages_domain, :with_certificate_chain, :with_key) } + let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) } it { is_expected.to_not be_valid } end @@ -95,7 +95,7 @@ describe PagesDomain, models: true do end context 'for invalid key' do - let(:domain) { build(:pages_domain, :with_certificate_chain, :with_key) } + let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) } it { is_expected.to be_falsey } end @@ -110,11 +110,17 @@ describe PagesDomain, models: true do it { is_expected.to be_truthy } end - context 'for certificate chain without the root' do - let(:domain) { build(:pages_domain, :with_certificate_chain) } + context 'for missing certificate chain' do + let(:domain) { build(:pages_domain, :with_missing_chain) } it { is_expected.to be_falsey } end + + context 'for trusted certificate chain' do + let(:domain) { build(:pages_domain, :with_trusted_chain) } + + it { is_expected.to be_truthy } + end end describe :expired? do -- cgit v1.2.1 From 63eb415610b151495ac54e98804ce37ba5500be4 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 16 Feb 2016 14:40:54 +0100 Subject: Fix certificate validators --- app/validators/certificate_key_validator.rb | 2 +- app/validators/certificate_validator.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/validators/certificate_key_validator.rb b/app/validators/certificate_key_validator.rb index 7039bd5a621..098b16017d2 100644 --- a/app/validators/certificate_key_validator.rb +++ b/app/validators/certificate_key_validator.rb @@ -16,7 +16,7 @@ class CertificateKeyValidator < ActiveModel::EachValidator private def valid_private_key_pem?(value) - return unless value + return false unless value pkey = OpenSSL::PKey::RSA.new(value) pkey.private? rescue OpenSSL::PKey::PKeyError diff --git a/app/validators/certificate_validator.rb b/app/validators/certificate_validator.rb index 2a04c76d4b9..e3d18097f71 100644 --- a/app/validators/certificate_validator.rb +++ b/app/validators/certificate_validator.rb @@ -16,9 +16,9 @@ class CertificateValidator < ActiveModel::EachValidator private def valid_certificate_pem?(value) - return unless value - OpenSSL::X509::Certificate.new(value) + return false unless value + OpenSSL::X509::Certificate.new(value).present? rescue OpenSSL::X509::CertificateError - nil + false end end -- cgit v1.2.1 From 92a1efe69b8d3a491e14b5d583033ec7ebd4c623 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 16 Feb 2016 19:13:28 +0100 Subject: Use GitLab Pages 0.2.0 --- GITLAB_PAGES_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 6e8bf73aa55..0ea3a944b39 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.1.0 +0.2.0 -- cgit v1.2.1 From c089f103342ae8f60c7fa9055ef79e3245d6a5fb Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Wed, 17 Feb 2016 10:05:26 +0100 Subject: Update comments --- features/steps/project/pages.rb | 2 +- spec/factories/pages_domains.rb | 6 ++---- spec/models/pages_domain_spec.rb | 4 ++++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb index ac44aac9e38..b3a6b93c5d0 100644 --- a/features/steps/project/pages.rb +++ b/features/steps/project/pages.rb @@ -34,7 +34,7 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps ref: 'HEAD', artifacts_file: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip'), artifacts_metadata: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta') - ) + ) result = ::Projects::UpdatePagesService.new(@project, build).execute expect(result[:status]).to eq(:success) end diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb index ff72df8dc02..6d2e45f41ba 100644 --- a/spec/factories/pages_domains.rb +++ b/spec/factories/pages_domains.rb @@ -63,11 +63,9 @@ VM+d42lQAgoq318A84Xu5vRh1KCAJuztkhNbM+w= end trait :with_trusted_chain do - # This is + # This contains # [Intermediate #2 (SHA-2)] 'Comodo RSA Domain Validation Secure Server CA' - # [Intermediate #1 (SHA-2)] COMODO RSA Certification Authority - # We only validate that we want to rebuild the trust chain, - # we don't need end-to-end certificate to do that + # [Intermediate #1 (SHA-2)] 'COMODO RSA Certification Authority' certificate '-----BEGIN CERTIFICATE----- MIIGCDCCA/CgAwIBAgIQKy5u6tl1NmwUim7bo3yMBzANBgkqhkiG9w0BAQwFADCB hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index 3e083ba9001..0b95bf594c5 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -117,6 +117,10 @@ describe PagesDomain, models: true do end context 'for trusted certificate chain' do + # We only validate that we can to rebuild the trust chain, for certificates + # We assume that 'AddTrustExternalCARoot' needed to validate the chain is in trusted store. + # It will be if ca-certificates is installed on Debian/Ubuntu/Alpine + let(:domain) { build(:pages_domain, :with_trusted_chain) } it { is_expected.to be_truthy } -- cgit v1.2.1 From 492627c987fd167c956df49843e741cbe29fd77a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 19 Feb 2016 15:11:03 +0100 Subject: Fix the URL of group pages --- app/models/project.rb | 10 +++++++--- doc/pages/README.md | 8 ++++++++ spec/models/project_spec.rb | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index dac52a0fc5e..73a642e1580 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1167,12 +1167,16 @@ class Project < ActiveRecord::Base def pages_url return unless Dir.exist?(public_pages_path) - host = "#{namespace.path}.#{Settings.pages.host}" + # The hostname always needs to be in downcased + # All web servers convert hostname to lowercase + host = "#{namespace.path}.#{Settings.pages.host}".downcase + + # The host in URL always needs to be downcased url = Gitlab.config.pages.url.sub(/^https?:\/\//) do |prefix| "#{prefix}#{namespace.path}." - end + end.downcase - # If the project path is the same as host, leave the short version + # If the project path is the same as host, we serve it as group page return url if host == path "#{url}/#{path}" diff --git a/doc/pages/README.md b/doc/pages/README.md index f6eb8ccb7a7..eb4217e0d1a 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -45,6 +45,14 @@ URL it will be accessible. | Specific project under a user's page | `walter/area51` | `https://walter.gitlab.io/area51` | | Specific project under a group's page | `therug/welovecats` | `https://therug.gitlab.io/welovecats` | +## Group pages + +You can create a group page in context of your group. +The project for group page must be written in lower. + +If you have a group `TheRug` and pages are hosted under `Example.com` in order to create a group page +create a new project named `therug.example.com`. + ## Enable the pages feature in your project The GitLab Pages feature needs to be explicitly enabled for each project diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 5fde9194e93..cf45ee54fa4 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1845,4 +1845,37 @@ describe Project, models: true do def enable_lfs allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end + + describe :pages_url do + let(:group) { create :group, name: group_name } + let(:project) { create :empty_project, namespace: group, name: project_name } + let(:domain) { 'Example.com' } + + subject { project.pages_url } + + before do + FileUtils.mkdir_p(project.public_pages_path) + + allow(Settings.pages).to receive(:host).and_return(domain) + allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com') + end + + after do + FileUtils.rmdir(project.public_pages_path) + end + + context 'group page' do + let(:group_name) { 'Group' } + let(:project_name) { 'group.example.com' } + + it { is_expected.to eq("http://group.example.com") } + end + + context 'project page' do + let(:group_name) { 'Group' } + let(:project_name) { 'Project' } + + it { is_expected.to eq("http://group.example.com/project") } + end + end end -- cgit v1.2.1 From 3e6cbcdd00017acae132daafa5af35f16bf48e3c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 19 Feb 2016 15:11:26 +0100 Subject: Fix pages abilities --- app/controllers/projects/pages_controller.rb | 3 ++- app/policies/project_policy.rb | 2 ++ app/views/projects/pages/_destroy.haml | 2 ++ app/views/projects/pages/_list.html.haml | 2 +- app/views/projects/pages/_no_domains.html.haml | 13 +++++++------ app/views/projects/pages/show.html.haml | 2 +- doc/user/permissions.md | 3 +++ 7 files changed, 18 insertions(+), 9 deletions(-) diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index b73f998392d..fbd18b68141 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -1,7 +1,8 @@ class Projects::PagesController < Projects::ApplicationController layout 'project_settings' - before_action :authorize_update_pages! + before_action :authorize_read_pages!, only: [:show] + before_action :authorize_update_pages!, except: [:show] def show @domains = @project.pages_domains.order(:domain) diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index ca5b39a001f..f5fd50745aa 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -110,6 +110,8 @@ class ProjectPolicy < BasePolicy can! :admin_pipeline can! :admin_environment can! :admin_deployment + can! :admin_pages + can! :read_pages can! :update_pages end diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index 0cd25f82cd4..896a86712a1 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -7,3 +7,5 @@ Removing the pages will prevent from exposing them to outside world. .form-actions = link_to 'Remove pages', namespace_project_pages_path(@project.namespace, @project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove" +- else + .nothing-here-block Only the project owner can remove pages diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml index c1a6948a574..4f2dd1a1398 100644 --- a/app/views/projects/pages/_list.html.haml +++ b/app/views/projects/pages/_list.html.haml @@ -1,4 +1,4 @@ -- if @domains.any? +- if can?(current_user, :update_pages, @project) && @domains.any? .panel.panel-default .panel-heading Domains (#{@domains.count}) diff --git a/app/views/projects/pages/_no_domains.html.haml b/app/views/projects/pages/_no_domains.html.haml index 5a18740346a..7cea5f3e70b 100644 --- a/app/views/projects/pages/_no_domains.html.haml +++ b/app/views/projects/pages/_no_domains.html.haml @@ -1,6 +1,7 @@ -.panel.panel-default - .panel-heading - Domains - .nothing-here-block - Support for domains and certificates is disabled. - Ask your system's administrator to enable it. +- if can?(current_user, :update_pages, @project) + .panel.panel-default + .panel-heading + Domains + .nothing-here-block + Support for domains and certificates is disabled. + Ask your system's administrator to enable it. diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index f4ca33f418b..b6595269b06 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -2,7 +2,7 @@ %h3.page_title Pages - - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https + - if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https) = link_to new_namespace_project_pages_domain_path(@project.namespace, @project), class: 'btn btn-new pull-right', title: 'New Domain' do %i.fa.fa-plus New Domain diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 678fc3ffd1f..e87cae092a5 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -62,11 +62,14 @@ The following table depicts the various user permission levels in a project. | Manage runners | | | | ✓ | ✓ | | Manage build triggers | | | | ✓ | ✓ | | Manage variables | | | | ✓ | ✓ | +| Manage pages | | | | ✓ | ✓ | +| Manage pages domains and certificates | | | | ✓ | ✓ | | Switch visibility level | | | | | ✓ | | Transfer project to another namespace | | | | | ✓ | | Remove project | | | | | ✓ | | Force push to protected branches [^3] | | | | | | | Remove protected branches [^3] | | | | | | +| Remove pages | | | | | ✓ | ## Group -- cgit v1.2.1 From 8a861c87bf8ba71d5c1a479c8118d9ed6aaf8e88 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 19 Feb 2016 20:07:27 +0100 Subject: Describe #pages_url instead of :pages_url --- spec/models/project_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index cf45ee54fa4..bb4d82a4df1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1846,7 +1846,7 @@ describe Project, models: true do allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end - describe :pages_url do + describe '#pages_url' do let(:group) { create :group, name: group_name } let(:project) { create :empty_project, namespace: group, name: project_name } let(:domain) { 'Example.com' } -- cgit v1.2.1 From 06d96a9a624d31294bdf16a4662aaa7121274061 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 19 Feb 2016 20:12:56 +0100 Subject: Introduce pages_deployed? to Project model --- app/models/project.rb | 6 ++++-- app/views/projects/pages/_access.html.haml | 2 +- app/views/projects/pages/_destroy.haml | 2 +- app/views/projects/pages/_use.html.haml | 2 +- spec/models/project_spec.rb | 23 ++++++++++++++++------ .../services/projects/update_pages_service_spec.rb | 12 +++++------ 6 files changed, 30 insertions(+), 17 deletions(-) diff --git a/app/models/project.rb b/app/models/project.rb index 73a642e1580..a1034e80b6c 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1164,9 +1164,11 @@ class Project < ActiveRecord::Base ensure_runners_token! end - def pages_url - return unless Dir.exist?(public_pages_path) + def pages_deployed? + Dir.exist?(public_pages_path) + end + def pages_url # The hostname always needs to be in downcased # All web servers convert hostname to lowercase host = "#{namespace.path}.#{Settings.pages.host}".downcase diff --git a/app/views/projects/pages/_access.html.haml b/app/views/projects/pages/_access.html.haml index 9740877b214..82e20eeebb3 100644 --- a/app/views/projects/pages/_access.html.haml +++ b/app/views/projects/pages/_access.html.haml @@ -1,4 +1,4 @@ -- if @project.pages_url +- if @project.pages_deployed? .panel.panel-default .panel-heading Access pages diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index 896a86712a1..6a7b6baf767 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -1,4 +1,4 @@ -- if can?(current_user, :remove_pages, @project) && @project.pages_url +- if can?(current_user, :remove_pages, @project) && @project.pages_deployed? .panel.panel-default.panel.panel-danger .panel-heading Remove pages .errors-holder diff --git a/app/views/projects/pages/_use.html.haml b/app/views/projects/pages/_use.html.haml index ee38f45d44d..9db46f0b1fc 100644 --- a/app/views/projects/pages/_use.html.haml +++ b/app/views/projects/pages/_use.html.haml @@ -1,4 +1,4 @@ -- unless @project.pages_url +- unless @project.pages_deployed? .panel.panel-info .panel-heading Configure pages diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index bb4d82a4df1..558674b5b39 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1068,6 +1068,23 @@ describe Project, models: true do end end + describe '#pages_deployed?' do + let(:project) { create :empty_project } + + subject { project.pages_deployed? } + + context 'if public folder does exist' do + before { FileUtils.mkdir_p(project.public_pages_path) } + after { FileUtils.rmdir(project.public_pages_path) } + + it { is_expected.to be_truthy } + end + + context "if public folder doesn't exist" do + it { is_expected.to be_falsey } + end + end + describe '.search' do let(:project) { create(:empty_project, description: 'kitten mittens') } @@ -1854,16 +1871,10 @@ describe Project, models: true do subject { project.pages_url } before do - FileUtils.mkdir_p(project.public_pages_path) - allow(Settings.pages).to receive(:host).and_return(domain) allow(Gitlab.config.pages).to receive(:url).and_return('http://example.com') end - after do - FileUtils.rmdir(project.public_pages_path) - end - context 'group page' do let(:group_name) { 'Group' } let(:project_name) { 'group.example.com' } diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index 68e66866340..51da582c497 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -27,9 +27,9 @@ describe Projects::UpdatePagesService do end it 'succeeds' do - expect(project.pages_url).to be_nil + expect(project.pages_deployed?).to be_falsey expect(execute).to eq(:success) - expect(project.pages_url).to_not be_nil + expect(project.pages_deployed?).to be_truthy end it 'limits pages size' do @@ -39,11 +39,11 @@ describe Projects::UpdatePagesService do it 'removes pages after destroy' do expect(PagesWorker).to receive(:perform_in) - expect(project.pages_url).to be_nil + expect(project.pages_deployed?).to be_falsey expect(execute).to eq(:success) - expect(project.pages_url).to_not be_nil + expect(project.pages_deployed?).to be_truthy project.destroy - expect(Dir.exist?(project.public_pages_path)).to be_falsey + expect(project.pages_deployed?).to be_falsey end it 'fails if sha on branch is not latest' do @@ -61,7 +61,7 @@ describe Projects::UpdatePagesService do it 'fails to remove project pages when no pages is deployed' do expect(PagesWorker).to_not receive(:perform_in) - expect(project.pages_url).to be_nil + expect(project.pages_deployed?).to be_falsey project.destroy end -- cgit v1.2.1 From 861129c33ae1a1c4c3122832033c838d4af5d88d Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 19 Feb 2016 20:13:58 +0100 Subject: Mock Dir::exist? in project_spec.rb --- spec/models/project_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 558674b5b39..591ea314142 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1074,8 +1074,7 @@ describe Project, models: true do subject { project.pages_deployed? } context 'if public folder does exist' do - before { FileUtils.mkdir_p(project.public_pages_path) } - after { FileUtils.rmdir(project.public_pages_path) } + before { allow(Dir).to receive(:exist?).with(project.public_pages_path).and_return(true) } it { is_expected.to be_truthy } end -- cgit v1.2.1 From a621b9d5dff7dfb2418a473473df6e6011dfc63a Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 19 Feb 2016 20:17:10 +0100 Subject: Update docs --- doc/pages/README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index eb4217e0d1a..c9d0d7e2b49 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -47,11 +47,10 @@ URL it will be accessible. ## Group pages -You can create a group page in context of your group. -The project for group page must be written in lower. +To create a page for a group, add a new project to it. The project name must be lowercased. -If you have a group `TheRug` and pages are hosted under `Example.com` in order to create a group page -create a new project named `therug.example.com`. +For example, if you have a group called `TheRug` and pages are hosted under `Example.com`, +create a project named `therug.example.com`. ## Enable the pages feature in your project -- cgit v1.2.1 From c634ff42ae26ed8e33a70a4c5cb75b38f68644fc Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Fri, 19 Feb 2016 22:17:15 +0100 Subject: Fix broken feature tests --- features/steps/project/pages.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb index b3a6b93c5d0..34f97f1ea8b 100644 --- a/features/steps/project/pages.rb +++ b/features/steps/project/pages.rb @@ -134,6 +134,6 @@ nNp/xedE1YxutQ== end step 'The Pages should get removed' do - expect(@project.pages_url).to be_nil + expect(@project.pages_deployed?).to be_falsey end end -- cgit v1.2.1 From d5ccea0286b229ba64a50e4576a68674d83ef30b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sat, 20 Feb 2016 23:29:40 +0200 Subject: Add init scripts for GitLab Pages daemon --- lib/support/init.d/gitlab | 61 +++++++++++++++++++++++++++---- lib/support/init.d/gitlab.default.example | 24 ++++++++++++ 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index 31b00ff128a..38a9ab194d1 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -89,6 +89,13 @@ check_pids(){ mpid=0 fi fi + if [ "$gitlab_pages_enabled" = true ]; then + if [ -f "$gitlab_pages_pid_path" ]; then + gppid=$(cat "$gitlab_pages_pid_path") + else + gppid=0 + fi + fi } ## Called when we have started the two processes and are waiting for their pid files. @@ -144,7 +151,15 @@ check_status(){ mail_room_status="-1" fi fi - if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then + if [ "$gitlab_pages_enabled" = true ]; then + if [ $gppid -ne 0 ]; then + kill -0 "$gppid" 2>/dev/null + gitlab_pages_status="$?" + else + gitlab_pages_status="-1" + fi + fi + if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; } && { [ "$gitlab_pages_enabled" != true ] || [ $gitlab_pages_status = 0 ]; }; then gitlab_status=0 else # http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html @@ -186,12 +201,19 @@ check_stale_pids(){ exit 1 fi fi + if [ "$gitlab_pages_enabled" = true ] && [ "$gppid" != "0" ] && [ "$gitlab_pages_status" != "0" ]; then + echo "Removing stale GitLab Pages job dispatcher pid. This is most likely caused by GitLab Pages crashing the last time it ran." + if ! rm "$gitlab_pages_pid_path"; then + echo "Unable to remove stale pid, exiting" + exit 1 + fi + fi } ## If no parts of the service is running, bail out. exit_if_not_running(){ check_stale_pids - if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then + if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then echo "GitLab is not running." exit fi @@ -213,6 +235,9 @@ start_gitlab() { if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then echo "Starting GitLab MailRoom" fi + if [ "gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" != "0" ]; then + echo "Starting GitLab Pages" + fi # Then check if the service is running. If it is: don't start again. if [ "$web_status" = "0" ]; then @@ -252,6 +277,16 @@ start_gitlab() { fi fi + if [ "$gitlab_pages_enabled" = true ]; then + if [ "$gitlab_pages_status" = "0" ]; then + echo "The GitLab Pages is already running with pid $spid, not restarting" + else + $app_root/bin/daemon_with_pidfile $gitlab_pages_pid_path \ + $gitlab_pages_dir/gitlab-pages $gitlab_pages_options \ + >> $gitlab_pages_log 2>&1 & + fi + fi + # Wait for the pids to be planted wait_for_pids # Finally check the status to tell wether or not GitLab is running @@ -278,13 +313,17 @@ stop_gitlab() { echo "Shutting down GitLab MailRoom" RAILS_ENV=$RAILS_ENV bin/mail_room stop fi + if [ "$gitlab_pages_status" = "0" ]; then + echo "Shutting down gitlab-pages" + kill -- $(cat $gitlab_pages_pid_path) + fi # If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script. - while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do + while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; }; do sleep 1 check_status printf "." - if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then + if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then printf "\n" break fi @@ -298,6 +337,7 @@ stop_gitlab() { if [ "$mail_room_enabled" = true ]; then rm "$mail_room_pid_path" 2>/dev/null fi + rm -f "$gitlab_pages_pid_path" print_status } @@ -305,7 +345,7 @@ stop_gitlab() { ## Prints the status of GitLab and its components. print_status() { check_status - if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then + if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" != "0" ]; }; then echo "GitLab is not running." return fi @@ -331,7 +371,14 @@ print_status() { printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n" fi fi - if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then + if [ "$gitlab_pages_enabled" = true ]; then + if [ "$gitlab_pages_status" = "0" ]; then + echo "The GitLab Pages with pid $mpid is running." + else + printf "The GitLab Pages is \033[31mnot running\033[0m.\n" + fi + fi + if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; } && { [ "$gitlab_pages_enabled" != true ] || [ "$gitlab_pages_status" = "0" ]; }; then printf "GitLab and all its components are \033[32mup and running\033[0m.\n" fi } @@ -362,7 +409,7 @@ reload_gitlab(){ ## Restarts Sidekiq and Unicorn. restart_gitlab(){ check_status - if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then + if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; } || { [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" = "0" ]; }; then stop_gitlab fi start_gitlab diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index cc8617b72ca..6a4f6b090c9 100755 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -47,6 +47,30 @@ gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public" gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" +# The GitLab Pages Daemon needs to use a separate IP address on which it will +# listen. You can also use different ports than 80 or 443 that will be +# forwarded to GitLab Pages Daemon. +# +# To enable HTTP support for custom domains add the `-listen-http` directive +# in `gitlab_pages_options` below. +# The value of -listen-http must be set to `gitlab.yml > pages > external_http` +# as well. For example: +# +# -listen-http 1.1.1.1:80 +# +# To enable HTTPS support for custom domains add the `-listen-https`, +# `-root-cert` and `-root-key` directives in `gitlab_pages_options` below. +# The value of -listen-https must be set to `gitlab.yml > pages > external_https` +# as well. For example: +# +# -listen-https 1.1.1.1:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key +# +# The -pages-domain must be specified the same as in `gitlab.yml > pages > host`. +# Set `gitlab_pages_enabled=false` if you want to disable the Pages feature. +gitlab_pages_enabled=true +gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8282" +gitlab_pages_log="$app_root/log/gitlab-pages.log" + # mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled. # This is required for the Reply by email feature. # The default is "false" -- cgit v1.2.1 From 50bbc326a475f0cca8e63c7a8de96b3f5538cee0 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sat, 20 Feb 2016 23:32:46 +0200 Subject: Change NGINX pages configs to account for the Pages daemon --- lib/support/nginx/gitlab-pages | 16 +++++++--------- lib/support/nginx/gitlab-pages-ssl | 18 ++++++++---------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages index ed4f7e4316a..2e0eb2af4b1 100644 --- a/lib/support/nginx/gitlab-pages +++ b/lib/support/nginx/gitlab-pages @@ -8,20 +8,18 @@ server { ## Replace this with something like pages.gitlab.com server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; - root /home/git/gitlab/shared/pages/${group}; ## Individual nginx logs for GitLab pages access_log /var/log/nginx/gitlab_pages_access.log; error_log /var/log/nginx/gitlab_pages_error.log; - # 1. Try to get /path/ from shared/pages/${group}/${path}/public/ - # 2. Try to get / from shared/pages/${group}/${host}/public/ - location ~ ^/([^/]*)(/.*)?$ { - try_files "/$1/public$2" - "/$1/public$2/index.html" - "/${host}/public/${uri}" - "/${host}/public/${uri}/index.html" - =404; + location / { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # The same address as passed to GitLab Pages: `-listen-proxy` + proxy_pass http://localhost:8282/; } # Define custom error pages diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl index dcbbee4042a..1045cd42d2f 100644 --- a/lib/support/nginx/gitlab-pages-ssl +++ b/lib/support/nginx/gitlab-pages-ssl @@ -23,12 +23,11 @@ server { ## Pages serving host server { listen 0.0.0.0:443 ssl; - listen [::]:443 ipv6only=on ssl; + listen [::]:443 ipv6only=on ssl http2; ## Replace this with something like pages.gitlab.com server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; server_tokens off; ## Don't show the nginx version number, a security best practice - root /home/git/gitlab/shared/pages/${group}; ## Strong SSL Security ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/ @@ -63,14 +62,13 @@ server { access_log /var/log/nginx/gitlab_pages_access.log; error_log /var/log/nginx/gitlab_pages_error.log; - # 1. Try to get /path/ from shared/pages/${group}/${path}/public/ - # 2. Try to get / from shared/pages/${group}/${host}/public/ - location ~ ^/([^/]*)(/.*)?$ { - try_files "/$1/public$2" - "/$1/public$2/index.html" - "/${host}/public/${uri}" - "/${host}/public/${uri}/index.html" - =404; + location / { + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + # The same address as passed to GitLab Pages: `-listen-proxy` + proxy_pass http://localhost:8282/; } # Define custom error pages -- cgit v1.2.1 From 4b45f284c9d060de06f4f54d9e5b1c2815b743dd Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 00:31:46 +0200 Subject: Change the pages daemon proxy listen port to 8090 So as to be consistent with what is set in Omnibus --- lib/support/init.d/gitlab.default.example | 2 +- lib/support/nginx/gitlab-pages | 2 +- lib/support/nginx/gitlab-pages-ssl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index 6a4f6b090c9..f096298afbb 100755 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -68,7 +68,7 @@ gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" # The -pages-domain must be specified the same as in `gitlab.yml > pages > host`. # Set `gitlab_pages_enabled=false` if you want to disable the Pages feature. gitlab_pages_enabled=true -gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8282" +gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090" gitlab_pages_log="$app_root/log/gitlab-pages.log" # mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled. diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages index 2e0eb2af4b1..169d7d11ca7 100644 --- a/lib/support/nginx/gitlab-pages +++ b/lib/support/nginx/gitlab-pages @@ -19,7 +19,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # The same address as passed to GitLab Pages: `-listen-proxy` - proxy_pass http://localhost:8282/; + proxy_pass http://localhost:8090/; } # Define custom error pages diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl index 1045cd42d2f..16edd337e10 100644 --- a/lib/support/nginx/gitlab-pages-ssl +++ b/lib/support/nginx/gitlab-pages-ssl @@ -68,7 +68,7 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # The same address as passed to GitLab Pages: `-listen-proxy` - proxy_pass http://localhost:8282/; + proxy_pass http://localhost:8090/; } # Define custom error pages -- cgit v1.2.1 From deb9481efde12e6198b0330bb8eb4c802d1d4b4c Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 00:32:58 +0200 Subject: Add missing variables for gitlab-pages [ci skip] --- lib/support/init.d/gitlab | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index 38a9ab194d1..9f2ce01d931 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -42,6 +42,11 @@ gitlab_workhorse_dir=$(cd $app_root/../gitlab-workhorse 2> /dev/null && pwd) gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $rails_socket -documentRoot $app_root/public" gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" +gitlab_pages_enabled=false +gitlab_pages_dir=$(cd $app_root/../gitlab-pages 2> /dev/null && pwd) +gitlab_pages_pid_path="$pid_path/gitlab-pages.pid" +gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090" +gitlab_pages_log="$app_root/log/gitlab-pages.log" shell_path="/bin/bash" # Read configuration variable file if it is present -- cgit v1.2.1 From fd9916c8d2976f724589d581943cc6aa4b1237f7 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 00:41:08 +0200 Subject: Add section about changes from 8.4 to 8.5 [ci skip] --- doc/pages/administration.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 529a1450fd3..5e0e4f8efed 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -5,6 +5,15 @@ _**Note:** This feature was [introduced][ee-80] in GitLab EE 8.3_ If you are looking for ways to upload your static content in GitLab Pages, you probably want to read the [user documentation](README.md). +## Changes to GitLab Pages from GitLab 8.4 to 8.5 + +In GitLab 8.5 we introduced the [gitlab-pages daemon] which is now the +recommended way to set up GitLab Pages. + +The NGINX configs have changed to reflect this change. + +[gitlab-pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages + ## Configuration There are a couple of things to consider before enabling GitLab pages in your -- cgit v1.2.1 From 055b8230cc84252e143798938d179024b083f152 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 12:17:36 +0200 Subject: Add Changelog of GitLab Pages --- doc/pages/administration.md | 51 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 5e0e4f8efed..f45b013b8f6 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -1,14 +1,17 @@ # GitLab Pages Administration -_**Note:** This feature was [introduced][ee-80] in GitLab EE 8.3_ +> **Note:** +This feature was [introduced][ee-80] in GitLab EE 8.3. + +This document describes how to set up the _latest_ GitLab Pages feature. Make +sure to read the [changelog](#changelog) if you are upgrading to a new GitLab +version as it may include new features and changes needed to be made in your +configuration. If you are looking for ways to upload your static content in GitLab Pages, you probably want to read the [user documentation](README.md). -## Changes to GitLab Pages from GitLab 8.4 to 8.5 - -In GitLab 8.5 we introduced the [gitlab-pages daemon] which is now the -recommended way to set up GitLab Pages. +[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 The NGINX configs have changed to reflect this change. @@ -177,5 +180,39 @@ Pages are part of the regular backup so there is nothing to configure. You should strongly consider running GitLab pages under a different hostname than GitLab to prevent XSS attacks. -[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 -[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record +## Changelog + +GitLab Pages were first introduced in GitLab EE 8.3. Since then, many features +where added, like custom CNAME and TLS support, and many more are likely to +come. Below is a brief changelog. If no changes were introduced, assume that +the documentation is the same as the previous version(s). + +--- + +**GitLab 8.5 ([documentation][8-5-docs])** + +- In GitLab 8.5 we introduced the [gitlab-pages][] daemon which is now the + recommended way to set up GitLab Pages. +- The [NGINX configs][] have changed to reflect this change. So make sure to + update them. +- Custom CNAME and TLS certificates support + +[8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md +[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.0 +[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx + +--- + +**GitLab 8.4** + +No new changes. + +--- + +**GitLab 8.3 ([documentation][8-3-docs])** + +- GitLab Pages feature was introduced. + +[8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md + +--- -- cgit v1.2.1 From 96fed04452ff9b637c2298c05a89f1f14fa091f2 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 12:27:50 +0200 Subject: Add first draft of architecture --- doc/pages/administration.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index f45b013b8f6..f52703b7e43 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -13,9 +13,13 @@ probably want to read the [user documentation](README.md). [ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 -The NGINX configs have changed to reflect this change. +## Architecture -[gitlab-pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages +GitLab uses a separate tool ([gitlab-pages]), a simple HTTP server written in +Go that serves GitLab Pages with CNAMEs and SNI using HTTP/HTTP2. You are +encouraged to read its [README][pages-readme] to fully understand how it works. + +[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md ## Configuration -- cgit v1.2.1 From 0a4585bee4a5f7edde61d7358fb3a30b8f8224a1 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 17:34:18 +0200 Subject: Add MR that custom CNAMEs were introduced --- doc/pages/administration.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index f52703b7e43..74c960afa4f 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -1,7 +1,10 @@ # GitLab Pages Administration > **Note:** -This feature was [introduced][ee-80] in GitLab EE 8.3. +> This feature was first [introduced][ee-80] in GitLab EE 8.3. +> Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. + +--- This document describes how to set up the _latest_ GitLab Pages feature. Make sure to read the [changelog](#changelog) if you are upgrading to a new GitLab -- cgit v1.2.1 From 516a95ddb71c524e10d19fb3cf4ab4893772fa03 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 17:35:01 +0200 Subject: Add TOC --- doc/pages/administration.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 74c960afa4f..282736cdfd8 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -15,6 +15,27 @@ If you are looking for ways to upload your static content in GitLab Pages, you probably want to read the [user documentation](README.md). [ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 +[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 + +--- + +<!-- START doctoc generated TOC please keep comment here to allow auto update --> +<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Architecture](#architecture) +- [Configuration](#configuration) + - [DNS configuration](#dns-configuration) + - [Omnibus package installations](#omnibus-package-installations) + - [Installations from source](#installations-from-source) + - [Running GitLab Pages with HTTPS](#running-gitlab-pages-with-https) +- [Set maximum pages size](#set-maximum-pages-size) +- [Change storage path](#change-storage-path) +- [Backup](#backup) +- [Security](#security) +- [Changelog](#changelog) + +<!-- END doctoc generated TOC please keep comment here to allow auto update --> ## Architecture -- cgit v1.2.1 From f8927dad8c1f2940953d6694a8b7f507dbe0c3e4 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 17:36:34 +0200 Subject: More changelog clarification --- doc/pages/administration.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 282736cdfd8..fc858cd20e7 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -212,8 +212,9 @@ than GitLab to prevent XSS attacks. GitLab Pages were first introduced in GitLab EE 8.3. Since then, many features where added, like custom CNAME and TLS support, and many more are likely to -come. Below is a brief changelog. If no changes were introduced, assume that -the documentation is the same as the previous version(s). +come. Below is a brief changelog. If no changes were introduced or a version is +missing from the changelog, assume that the documentation is the same as the +latest previous version. --- -- cgit v1.2.1 From acf7ae5ed80ec39d26dbd37c09bc0f3eb78e1628 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 20:52:15 +0200 Subject: Add info about the pages daemon --- doc/pages/administration.md | 49 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index fc858cd20e7..3f3d6cac9b2 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -37,11 +37,52 @@ probably want to read the [user documentation](README.md). <!-- END doctoc generated TOC please keep comment here to allow auto update --> -## Architecture +## The GitLab Pages daemon -GitLab uses a separate tool ([gitlab-pages]), a simple HTTP server written in -Go that serves GitLab Pages with CNAMEs and SNI using HTTP/HTTP2. You are -encouraged to read its [README][pages-readme] to fully understand how it works. +Starting from GitLab EE 8.5, Pages make use of a separate tool ([gitlab-pages]), +a simple HTTP server written in Go that serves GitLab Pages with CNAMEs and SNI +using HTTP/HTTP2. You are encouraged to read its [README][pages-readme] to fully +understand how it works. + +What is supported when using the pages daemon: + +- Multiple domains per-project +- One TLS certificate per-domain + - Validation of certificate + - Validation of certificate chain + - Validation of private key against certificate + +--- + +In the case of custom domains, the Pages daemon needs to listen on ports `80` +and/or `443`. For that reason, there is some flexibility in the way which you +can set it up, so you basically have three choices: + +1. Run the pages daemon in the same server as GitLab, listening on a secondary IP +1. Run the pages daemon in the same server as GitLab, listening on the same IP + but on different ports. In that case, you will have to proxy the traffic with + a loadbalancer. +1. Run the pages daemon in a separate server. In that case, the Pages [`path`] + must also be present in the server that the pages daemon is installed, so + you will have to share it via network. + +[`path`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/config/gitlab.yml.example#L155 + +### Install the Pages daemon + +**Install the Pages daemon on a source installation** + +``` +cd /home/git +sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git +cd gitlab-pages +sudo -u git -H git checkout 0.2.0 +sudo -u git -H make +``` + +**Install the Pages daemon on Omnibus** + +The `gitlab-pages` daemon is included in the Omnibus package. [pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md -- cgit v1.2.1 From dfc3e58a5d763d0250e76625ada08b88a7cb7e63 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 20:52:53 +0200 Subject: Add configuration scenarios --- doc/pages/administration.md | 46 +++++++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 3f3d6cac9b2..db0eb83b9f4 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -88,18 +88,40 @@ The `gitlab-pages` daemon is included in the Omnibus package. ## Configuration -There are a couple of things to consider before enabling GitLab pages in your -GitLab EE instance. - -1. You need to properly configure your DNS to point to the domain that pages - will be served -1. Pages use a separate Nginx configuration file which needs to be explicitly - added in the server under which GitLab EE runs -1. Optionally but recommended, you can add some - [shared runners](../ci/runners/README.md) so that your users don't have to - bring their own. - -Both of these settings are described in detail in the sections below. +There are multiple ways to set up GitLab Pages according to what URL scheme you +are willing to support. Below you will find all possible scenarios to choose +from. + +### Configuration scenarios + +Before proceeding you have to decide what Pages scenario you want to use. +Remember that in either scenario, you need: + +1. A separate domain +1. A separate Nginx configuration file which needs to be explicitly added in + the server under which GitLab EE runs (Omnibus does that automatically) +1. (Optional) A wildcard certificate for that domain if you decide to serve + pages under HTTPS +1. (Optional but recommended) [Shared runners](../ci/runners/README.md) so that + your users don't have to bring their own. + +The possible scenarios are depicted in the table below. + +| URL scheme | Option | Wildcard certificate | Pages daemon | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `http://page.gitlab.io` | 1 | no | no | no | no | no | +| `https://page.gitlab.io` | 1 | yes | no | no | no | no | +| `http://page.gitlab.io` and `http://page.com` | 2 | no | yes | yes | no | yes | +| `https://page.gitlab.io` and `https://page.com` | 2 | yes | yes | yes/no | yes | yes | + +As you see from the table above, each URL scheme comes with an option: + +1. Pages enabled, daemon is enabled and NGINX will proxy all requests to the + daemon. Pages daemon doesn't listen to the outside world. +1. Pages enabled, daemon is enabled AND pages has external IP support enabled. + In that case, the pages daemon is running, NGINX still proxies requests to + the daemon but the daemon is also able to receive requests from the outside + world. Custom domains and TLS are supported. ### DNS configuration -- cgit v1.2.1 From 39dff1e1066d2e33267602f11f611af145ba169a Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 20:53:20 +0200 Subject: Update TOC --- doc/pages/administration.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index db0eb83b9f4..4e4bea57096 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -23,12 +23,16 @@ probably want to read the [user documentation](README.md). <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -- [Architecture](#architecture) +- [The GitLab Pages daemon](#the-gitlab-pages-daemon) + - [Install the Pages daemon](#install-the-pages-daemon) - [Configuration](#configuration) + - [Configuration scenarios](#configuration-scenarios) - [DNS configuration](#dns-configuration) - - [Omnibus package installations](#omnibus-package-installations) - - [Installations from source](#installations-from-source) +- [Custom domains without TLS](#custom-domains-without-tls) +- [Custom domains with TLS](#custom-domains-with-tls) +- [Installations from source](#installations-from-source) - [Running GitLab Pages with HTTPS](#running-gitlab-pages-with-https) +- [Omnibus package installations](#omnibus-package-installations) - [Set maximum pages size](#set-maximum-pages-size) - [Change storage path](#change-storage-path) - [Backup](#backup) -- cgit v1.2.1 From cfc54df4a8386ec5d58ba55fff98264fe746e3ba Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 23:06:44 +0200 Subject: Set pages daemon to false --- lib/support/init.d/gitlab.default.example | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index f096298afbb..e5797d8fe3c 100755 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -47,9 +47,9 @@ gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid" gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public" gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" -# The GitLab Pages Daemon needs to use a separate IP address on which it will -# listen. You can also use different ports than 80 or 443 that will be -# forwarded to GitLab Pages Daemon. +# The GitLab Pages Daemon needs either a separate IP address on which it will +# listen or use different ports than 80 or 443 that will be forwarded to GitLab +# Pages Daemon. # # To enable HTTP support for custom domains add the `-listen-http` directive # in `gitlab_pages_options` below. @@ -66,8 +66,8 @@ gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log" # -listen-https 1.1.1.1:443 -root-cert /path/to/example.com.crt -root-key /path/to/example.com.key # # The -pages-domain must be specified the same as in `gitlab.yml > pages > host`. -# Set `gitlab_pages_enabled=false` if you want to disable the Pages feature. -gitlab_pages_enabled=true +# Set `gitlab_pages_enabled=true` if you want to enable the Pages feature. +gitlab_pages_enabled=false gitlab_pages_options="-pages-domain example.com -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090" gitlab_pages_log="$app_root/log/gitlab-pages.log" -- cgit v1.2.1 From b39947864df75fc52781489fbe59185d788cb208 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sun, 21 Feb 2016 23:07:14 +0200 Subject: chmod 644 gitlab.default.example No need to be executable since it is sourced in /etc/init.d/gitlab --- lib/support/init.d/gitlab.default.example | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 lib/support/init.d/gitlab.default.example diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example old mode 100755 new mode 100644 -- cgit v1.2.1 From 2a484c6a291172c9c69cdcfe068ea4e440771393 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 00:28:14 +0200 Subject: Introduce custom domains setup for source installations [ci skip] --- doc/pages/administration.md | 98 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 94 insertions(+), 4 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 4e4bea57096..1d7ee65a0d6 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -30,8 +30,8 @@ probably want to read the [user documentation](README.md). - [DNS configuration](#dns-configuration) - [Custom domains without TLS](#custom-domains-without-tls) - [Custom domains with TLS](#custom-domains-with-tls) -- [Installations from source](#installations-from-source) - - [Running GitLab Pages with HTTPS](#running-gitlab-pages-with-https) +- [Wildcard HTTPS domain without custom domains](#wildcard-https-domain-without-custom-domains) +- [Wildcard HTTP domain without custom domains](#wildcard-http-domain-without-custom-domains) - [Omnibus package installations](#omnibus-package-installations) - [Set maximum pages size](#set-maximum-pages-size) - [Change storage path](#change-storage-path) @@ -145,9 +145,99 @@ see the [security section](#security). ### Omnibus package installations -See the relevant documentation at <http://doc.gitlab.com/omnibus/settings/pages.html>. +## Custom domains without TLS -### Installations from source +1. [Install the pages daemon](#install-the-pages-daemon) +1. Edit `gitlab.yml` to look like the example below. You need to change the + `host` to the FQDN under which GitLab Pages will be served. Set + `external_http` to the secondary IP on which the pages daemon will listen + for connections: + + ```yaml + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 80 + https: false + + external_http: 1.1.1.1:80 + ``` + +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain` and `-listen-http` must match the `host` and `external_http` + settings that you set above respectively: + + ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80" + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Make sure to edit the config to add your domain as well as correctly point + to the right location of the SSL certificate files. Restart Nginx for the + changes to take effect. + +1. [Restart GitLab](../../administration/restart_gitlab.md) + +## Custom domains with TLS + +1. [Install the pages daemon](#install-the-pages-daemon) +1. Edit `gitlab.yml` to look like the example below. You need to change the + `host` to the FQDN under which GitLab Pages will be served. Set + `external_http` and `external_https` to the secondary IP on which the pages + daemon will listen for connections: + + ```yaml + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 443 + https: true + + external_http: 1.1.1.1:80 + external_https: 1.1.1.1:443 + ``` + +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`, + `external_http` and `external_https` settings that you set above respectively. + The `-root-cert` and `-root-key` settings are the wildcard TLS certificates + of the `example.io` domain: + + ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80 -listen-https 1.1.1.1:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Make sure to edit the config to add your domain as well as correctly point + to the right location of the SSL certificate files. Restart Nginx for the + changes to take effect. + +1. [Restart GitLab](../../administration/restart_gitlab.md) + +## Wildcard HTTPS domain without custom domains 1. Go to the GitLab installation directory: -- cgit v1.2.1 From 54c943a59732cb0e0ce90a3d5db7f98ae807f22b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 01:10:49 +0200 Subject: Reword pages daemon intro --- doc/pages/administration.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 1d7ee65a0d6..8477bfa8b21 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -44,11 +44,12 @@ probably want to read the [user documentation](README.md). ## The GitLab Pages daemon Starting from GitLab EE 8.5, Pages make use of a separate tool ([gitlab-pages]), -a simple HTTP server written in Go that serves GitLab Pages with CNAMEs and SNI -using HTTP/HTTP2. You are encouraged to read its [README][pages-readme] to fully -understand how it works. +a simple HTTP server written in Go that can listen on an external IP address +and provide support for custom domains and custom certificates. The GitLab +Pages Daemon supports dynamic certificates through SNI and exposes pages using +HTTP2 by default. -What is supported when using the pages daemon: +Here is a brief list with what it is supported when using the pages daemon: - Multiple domains per-project - One TLS certificate per-domain @@ -56,6 +57,9 @@ What is supported when using the pages daemon: - Validation of certificate chain - Validation of private key against certificate +You are encouraged to read its [README][pages-readme] to fully understand how +it works. + --- In the case of custom domains, the Pages daemon needs to listen on ports `80` -- cgit v1.2.1 From ca26884c1538322150c8d3e08a6f2f5295b1c725 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 01:11:36 +0200 Subject: Add info about the loadbalancer --- doc/pages/administration.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 8477bfa8b21..c009263f648 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -67,12 +67,18 @@ and/or `443`. For that reason, there is some flexibility in the way which you can set it up, so you basically have three choices: 1. Run the pages daemon in the same server as GitLab, listening on a secondary IP -1. Run the pages daemon in the same server as GitLab, listening on the same IP - but on different ports. In that case, you will have to proxy the traffic with - a loadbalancer. 1. Run the pages daemon in a separate server. In that case, the Pages [`path`] must also be present in the server that the pages daemon is installed, so you will have to share it via network. +1. Run the pages daemon in the same server as GitLab, listening on the same IP + but on different ports. In that case, you will have to proxy the traffic with + a loadbalancer. If you choose that route note that you should use TCP load + balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the + pages will not be able to be served with user provided certificates. For + HTTP it's OK to use HTTP or TCP load balancing. + +In this document, we will proceed assuming the first option. First let's +install the pages daemon. [`path`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/config/gitlab.yml.example#L155 -- cgit v1.2.1 From fed8b62c3d4c57dae05ed4796c19bccdaf540886 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 01:12:31 +0200 Subject: Remove pages daemon from table and add it as prerequisite --- doc/pages/administration.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index c009263f648..eeda4af18fe 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -108,25 +108,26 @@ from. ### Configuration scenarios -Before proceeding you have to decide what Pages scenario you want to use. -Remember that in either scenario, you need: +Before proceeding with setting up GitLab Pages, you have to decide which route +you want to take. Note that in either scenario, you need: +1. To use the [GitLab Pages daemon](#the-gitlab-pages-daemon) 1. A separate domain 1. A separate Nginx configuration file which needs to be explicitly added in the server under which GitLab EE runs (Omnibus does that automatically) 1. (Optional) A wildcard certificate for that domain if you decide to serve pages under HTTPS 1. (Optional but recommended) [Shared runners](../ci/runners/README.md) so that - your users don't have to bring their own. + your users don't have to bring their own The possible scenarios are depicted in the table below. -| URL scheme | Option | Wildcard certificate | Pages daemon | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| URL scheme | Option | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | | --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `http://page.gitlab.io` | 1 | no | no | no | no | no | -| `https://page.gitlab.io` | 1 | yes | no | no | no | no | -| `http://page.gitlab.io` and `http://page.com` | 2 | no | yes | yes | no | yes | -| `https://page.gitlab.io` and `https://page.com` | 2 | yes | yes | yes/no | yes | yes | +| `http://page.example.io` | 1 | no | no | no | no | +| `https://page.example.io` | 1 | yes | no | no | no | +| `http://page.example.io` and `http://page.com` | 2 | no | yes | no | yes | +| `https://page.example.io` and `https://page.com` | 2 | yes | redirects to HTTPS | yes | yes | As you see from the table above, each URL scheme comes with an option: -- cgit v1.2.1 From d24763fa16874cf8f99a32635787134f4f76d699 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 01:22:28 +0200 Subject: Add the four scenarios and NGINX caveats --- doc/pages/administration.md | 203 +++++++++++++++++++++++++++----------------- 1 file changed, 126 insertions(+), 77 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index eeda4af18fe..5d7c0d76620 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -28,11 +28,12 @@ probably want to read the [user documentation](README.md). - [Configuration](#configuration) - [Configuration scenarios](#configuration-scenarios) - [DNS configuration](#dns-configuration) -- [Custom domains without TLS](#custom-domains-without-tls) -- [Custom domains with TLS](#custom-domains-with-tls) -- [Wildcard HTTPS domain without custom domains](#wildcard-https-domain-without-custom-domains) -- [Wildcard HTTP domain without custom domains](#wildcard-http-domain-without-custom-domains) -- [Omnibus package installations](#omnibus-package-installations) +- [Setting up GitLab Pages](#setting-up-gitlab-pages) + - [Custom domains with HTTPS support](#custom-domains-with-https-support) + - [Custom domains without HTTPS support](#custom-domains-without-https-support) + - [Wildcard HTTP domain without custom domains](#wildcard-http-domain-without-custom-domains) + - [Wildcard HTTPS domain without custom domains](#wildcard-https-domain-without-custom-domains) +- [NGINX caveats](#nginx-caveats) - [Set maximum pages size](#set-maximum-pages-size) - [Change storage path](#change-storage-path) - [Backup](#backup) @@ -145,46 +146,57 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the host that GitLab runs. For example, an entry would look like this: ``` -*.gitlab.io. 60 IN A 1.2.3.4 +*.example.io. 1800 IN A 1.2.3.4 ``` -where `gitlab.io` is the domain under which GitLab Pages will be served +where `example.io` is the domain under which GitLab Pages will be served and `1.2.3.4` is the IP address of your GitLab instance. You should not use the GitLab domain to serve user pages. For more information see the [security section](#security). -### Omnibus package installations +[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record -## Custom domains without TLS +## Setting up GitLab Pages + +Below are the four scenarios that are described in +[#configuration-scenarios](#configuration-scenarios). + +### Custom domains with HTTPS support + +**Source installations:** 1. [Install the pages daemon](#install-the-pages-daemon) 1. Edit `gitlab.yml` to look like the example below. You need to change the `host` to the FQDN under which GitLab Pages will be served. Set - `external_http` to the secondary IP on which the pages daemon will listen - for connections: + `external_http` and `external_https` to the secondary IP on which the pages + daemon will listen for connections: ```yaml + ## GitLab Pages pages: enabled: true # The location where pages are stored (default: shared/pages). # path: shared/pages host: example.io - port: 80 - https: false + port: 443 + https: true external_http: 1.1.1.1:80 + external_https: 1.1.1.1:443 ``` 1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in order to enable the pages daemon. In `gitlab_pages_options` the - `-pages-domain` and `-listen-http` must match the `host` and `external_http` - settings that you set above respectively: + `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`, + `external_http` and `external_https` settings that you set above respectively. + The `-root-cert` and `-root-key` settings are the wildcard TLS certificates + of the `example.io` domain: ``` gitlab_pages_enabled=true - gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80" + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80 -listen-https 1.1.1.1:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key ``` 1. Copy the `gitlab-pages-ssl` Nginx configuration file: @@ -194,45 +206,47 @@ see the [security section](#security). sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf ``` - Make sure to edit the config to add your domain as well as correctly point - to the right location of the SSL certificate files. Restart Nginx for the - changes to take effect. + Make sure to [properly edit the config](#nginx-caveats) to add your domain + as well as correctly point to the right location of the SSL certificate + files. Restart Nginx for the changes to take effect. + +1. [Restart GitLab][restart] + +--- + +**Omnibus installations:** -1. [Restart GitLab](../../administration/restart_gitlab.md) +### Custom domains without HTTPS support -## Custom domains with TLS +**Source installations:** 1. [Install the pages daemon](#install-the-pages-daemon) 1. Edit `gitlab.yml` to look like the example below. You need to change the `host` to the FQDN under which GitLab Pages will be served. Set - `external_http` and `external_https` to the secondary IP on which the pages - daemon will listen for connections: + `external_http` to the secondary IP on which the pages daemon will listen + for connections: ```yaml - ## GitLab Pages pages: enabled: true # The location where pages are stored (default: shared/pages). # path: shared/pages host: example.io - port: 443 - https: true + port: 80 + https: false external_http: 1.1.1.1:80 - external_https: 1.1.1.1:443 ``` 1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in order to enable the pages daemon. In `gitlab_pages_options` the - `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`, - `external_http` and `external_https` settings that you set above respectively. - The `-root-cert` and `-root-key` settings are the wildcard TLS certificates - of the `example.io` domain: + `-pages-domain` and `-listen-http` must match the `host` and `external_http` + settings that you set above respectively: ``` gitlab_pages_enabled=true - gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80 -listen-https 1.1.1.1:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80" ``` 1. Copy the `gitlab-pages-ssl` Nginx configuration file: @@ -242,14 +256,20 @@ see the [security section](#security). sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf ``` - Make sure to edit the config to add your domain as well as correctly point - to the right location of the SSL certificate files. Restart Nginx for the - changes to take effect. + Make sure to [properly edit the config](#nginx-caveats) to add your domain. + Restart Nginx for the changes to take effect. + +1. [Restart GitLab][restart] + +--- -1. [Restart GitLab](../../administration/restart_gitlab.md) +**Omnibus installations:** -## Wildcard HTTPS domain without custom domains +### Wildcard HTTP domain without custom domains +**Source installations:** + +1. [Install the pages daemon](#install-the-pages-daemon) 1. Go to the GitLab installation directory: ```bash @@ -266,12 +286,9 @@ see the [security section](#security). # The location where pages are stored (default: shared/pages). # path: shared/pages - # The domain under which the pages are served: - # http://group.example.com/project - # or project path can be a group page: group.example.com - host: gitlab.io - port: 80 # Set to 443 if you serve the pages with HTTPS - https: false # Set to true if you serve the pages with HTTPS + host: example.io + port: 80 + https: false ``` 1. Make sure you have copied the new `gitlab-pages` Nginx configuration file: @@ -281,39 +298,27 @@ see the [security section](#security). sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf ``` - Don't forget to add your domain name in the Nginx config. For example if - your GitLab pages domain is `gitlab.io`, replace + Make sure to [properly edit the config](#nginx-caveats) to add your domain. + Restart Nginx for the changes to take effect. - ```bash - server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; - ``` +1. [Restart GitLab][restart] - with +--- - ``` - server_name ~^(?<group>.*)\.gitlabpages\.com$; - ``` +**Omnibus installations:** - You must be extra careful to not remove the backslashes. If you are using - a subdomain, make sure to escape all dots (`.`) with a backslash (\). - For example `pages.gitlab.io` would be: +1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: + ```ruby + pages_external_url 'http://example.io' ``` - server_name ~^(?<group>.*)\.pages\.gitlab\.io$; - ``` +1. [Reconfigure GitLab][reconfigure] -1. Restart Nginx and GitLab: +### Wildcard HTTPS domain without custom domains - ```bash - sudo service nginx restart - sudo service gitlab restart - ``` - -### Running GitLab Pages with HTTPS - -If you want the pages to be served under HTTPS, a wildcard SSL certificate is -required. +**Source installations:** +1. [Install the pages daemon](#install-the-pages-daemon) 1. In `gitlab.yml`, set the port to `443` and https to `true`: ```bash @@ -323,24 +328,65 @@ required. # The location where pages are stored (default: shared/pages). # path: shared/pages - # The domain under which the pages are served: - # http://group.example.com/project - # or project path can be a group page: group.example.com - host: gitlab.io - port: 443 # Set to 443 if you serve the pages with HTTPS - https: true # Set to true if you serve the pages with HTTPS + host: example.io + port: 443 + https: true ``` 1. Copy the `gitlab-pages-ssl` Nginx configuration file: ```bash sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf ``` - Make sure to edit the config to add your domain as well as correctly point - to the right location of the SSL certificate files. Restart Nginx for the - changes to take effect. + Make sure to [properly edit the config](#nginx-caveats) to add your domain + as well as correctly point to the right location of the SSL certificate + files. Restart Nginx for the changes to take effect. + +--- + +**Omnibus installations:** + +1. Place the certificate and key inside `/etc/gitlab/ssl` +1. In `/etc/gitlab/gitlab.rb` specify the following configuration: + + ```ruby + pages_external_url 'https://example.io' + + pages_nginx['redirect_http_to_https'] = true + pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt" + pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key" + ``` + + where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key, + respectively. + +1. [Reconfigure GitLab][reconfigure] + +## NGINX caveats + +Be extra careful when setting up the domain name in the NGINX config. You must +not remove the backslashes. + +If your GitLab pages domain is `example.io`, replace: + +```bash +server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; +``` + +with: + +``` +server_name ~^(?<group>.*)\.example\.io$; +``` + +If you are using a subdomain, make sure to escape all dots (`.`) with a +backslash (\). For example `pages.example.io` would be: + +``` +server_name ~^(?<group>.*)\.pages\.example\.io$; +``` ## Set maximum pages size @@ -413,3 +459,6 @@ No new changes. [8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md --- + +[reconfigure]: ../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: ../../administration/restart_gitlab.md#installations-from-source -- cgit v1.2.1 From 228455af6baefff6bea679c30216d6ede703b2de Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 01:28:28 +0200 Subject: Rename NGINX section --- doc/pages/administration.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 5d7c0d76620..675a41d8023 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -33,7 +33,7 @@ probably want to read the [user documentation](README.md). - [Custom domains without HTTPS support](#custom-domains-without-https-support) - [Wildcard HTTP domain without custom domains](#wildcard-http-domain-without-custom-domains) - [Wildcard HTTPS domain without custom domains](#wildcard-https-domain-without-custom-domains) -- [NGINX caveats](#nginx-caveats) +- [NGINX configuration](#nginx-configuration) - [Set maximum pages size](#set-maximum-pages-size) - [Change storage path](#change-storage-path) - [Backup](#backup) @@ -206,7 +206,7 @@ Below are the four scenarios that are described in sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf ``` - Make sure to [properly edit the config](#nginx-caveats) to add your domain + Make sure to [properly edit the config](#nginx-configuration) to add your domain as well as correctly point to the right location of the SSL certificate files. Restart Nginx for the changes to take effect. @@ -249,14 +249,14 @@ Below are the four scenarios that are described in gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80" ``` -1. Copy the `gitlab-pages-ssl` Nginx configuration file: +1. Copy the `gitlab-pages` Nginx configuration file: ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf ``` - Make sure to [properly edit the config](#nginx-caveats) to add your domain. + Make sure to [properly edit the config](#nginx-configuration) to add your domain. Restart Nginx for the changes to take effect. 1. [Restart GitLab][restart] @@ -298,7 +298,7 @@ Below are the four scenarios that are described in sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf ``` - Make sure to [properly edit the config](#nginx-caveats) to add your domain. + Make sure to [properly edit the config](#nginx-configuration) to add your domain. Restart Nginx for the changes to take effect. 1. [Restart GitLab][restart] @@ -340,7 +340,7 @@ Below are the four scenarios that are described in sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf ``` - Make sure to [properly edit the config](#nginx-caveats) to add your domain + Make sure to [properly edit the config](#nginx-configuration) to add your domain as well as correctly point to the right location of the SSL certificate files. Restart Nginx for the changes to take effect. @@ -364,7 +364,7 @@ Below are the four scenarios that are described in 1. [Reconfigure GitLab][reconfigure] -## NGINX caveats +## NGINX configuration Be extra careful when setting up the domain name in the NGINX config. You must not remove the backslashes. -- cgit v1.2.1 From 3858443fdf96435d21dc3331169f9066793cf5e7 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 02:37:54 +0200 Subject: Add heading to the GitLab Pages setup scenarios --- doc/pages/administration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 675a41d8023..2b861cc55cf 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -24,6 +24,7 @@ probably want to read the [user documentation](README.md). **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [The GitLab Pages daemon](#the-gitlab-pages-daemon) + - [The GitLab Pages daemon and the case of custom domains](#the-gitlab-pages-daemon-and-the-case-of-custom-domains) - [Install the Pages daemon](#install-the-pages-daemon) - [Configuration](#configuration) - [Configuration scenarios](#configuration-scenarios) @@ -61,7 +62,7 @@ Here is a brief list with what it is supported when using the pages daemon: You are encouraged to read its [README][pages-readme] to fully understand how it works. ---- +### The GitLab Pages daemon and the case of custom domains In the case of custom domains, the Pages daemon needs to listen on ports `80` and/or `443`. For that reason, there is some flexibility in the way which you -- cgit v1.2.1 From f8c8dc03d007efeb52a6f81add6b49697001cb09 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 02:39:27 +0200 Subject: Add remaining Omnibus configs --- doc/pages/administration.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 2b861cc55cf..2b50ed1a126 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -217,6 +217,23 @@ Below are the four scenarios that are described in **Omnibus installations:** +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + nginx['listen_addresses'] = ['1.1.1.1'] + pages_nginx['enable'] = false + gitlab_pages['external_http'] = '1.1.1.2:80' + gitlab_pages['external_https'] = '1.1.1.2:443' + ``` + + where `1.1.1.1` is the primary IP address that GitLab is listening to and + `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + Read more at the + [NGINX configuration for custom domains](#nginx-configuration-for-custom-domains) + section. + +1. [Reconfigure GitLab][reconfigure] + ### Custom domains without HTTPS support **Source installations:** @@ -266,6 +283,22 @@ Below are the four scenarios that are described in **Omnibus installations:** +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + nginx['listen_addresses'] = ['1.1.1.1'] + pages_nginx['enable'] = false + gitlab_pages['external_http'] = '1.1.1.2:80' + ``` + + where `1.1.1.1` is the primary IP address that GitLab is listening to and + `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + Read more at the + [NGINX configuration for custom domains](#nginx-configuration-for-custom-domains) + section. + +1. [Reconfigure GitLab][reconfigure] + ### Wildcard HTTP domain without custom domains **Source installations:** @@ -313,6 +346,7 @@ Below are the four scenarios that are described in ```ruby pages_external_url 'http://example.io' ``` + 1. [Reconfigure GitLab][reconfigure] ### Wildcard HTTPS domain without custom domains -- cgit v1.2.1 From d9e3bb0e7def068c5b24937bf887b20784d4bd8e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 02:40:04 +0200 Subject: Add a separate NGINX section --- doc/pages/administration.md | 89 ++++++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 41 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 2b50ed1a126..f67bb63ff07 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -35,6 +35,9 @@ probably want to read the [user documentation](README.md). - [Wildcard HTTP domain without custom domains](#wildcard-http-domain-without-custom-domains) - [Wildcard HTTPS domain without custom domains](#wildcard-https-domain-without-custom-domains) - [NGINX configuration](#nginx-configuration) + - [NGINX configuration files](#nginx-configuration-files) + - [NGINX configuration for custom domains](#nginx-configuration-for-custom-domains) + - [NGINX caveats](#nginx-caveats) - [Set maximum pages size](#set-maximum-pages-size) - [Change storage path](#change-storage-path) - [Backup](#backup) @@ -200,17 +203,7 @@ Below are the four scenarios that are described in gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80 -listen-https 1.1.1.1:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key ``` -1. Copy the `gitlab-pages-ssl` Nginx configuration file: - - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf - ``` - - Make sure to [properly edit the config](#nginx-configuration) to add your domain - as well as correctly point to the right location of the SSL certificate - files. Restart Nginx for the changes to take effect. - +1. Make sure to [configure NGINX](#nginx-configuration) properly. 1. [Restart GitLab][restart] --- @@ -267,16 +260,7 @@ Below are the four scenarios that are described in gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80" ``` -1. Copy the `gitlab-pages` Nginx configuration file: - - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf - ``` - - Make sure to [properly edit the config](#nginx-configuration) to add your domain. - Restart Nginx for the changes to take effect. - +1. Make sure to [configure NGINX](#nginx-configuration) properly. 1. [Restart GitLab][restart] --- @@ -325,16 +309,7 @@ Below are the four scenarios that are described in https: false ``` -1. Make sure you have copied the new `gitlab-pages` Nginx configuration file: - - ```bash - sudo cp lib/support/nginx/gitlab-pages /etc/nginx/sites-available/gitlab-pages.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages.conf - ``` - - Make sure to [properly edit the config](#nginx-configuration) to add your domain. - Restart Nginx for the changes to take effect. - +1. Make sure to [configure NGINX](#nginx-configuration) properly. 1. [Restart GitLab][restart] --- @@ -368,16 +343,7 @@ Below are the four scenarios that are described in https: true ``` -1. Copy the `gitlab-pages-ssl` Nginx configuration file: - - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf - ``` - - Make sure to [properly edit the config](#nginx-configuration) to add your domain - as well as correctly point to the right location of the SSL certificate - files. Restart Nginx for the changes to take effect. +1. Make sure to [configure NGINX](#nginx-configuration) properly. --- @@ -401,6 +367,47 @@ Below are the four scenarios that are described in ## NGINX configuration +Depending on your setup, you will need to make some changes to NGINX. +Specifically you must change the domain name and the IP address where NGINX +listens to. Read the following sections for more details. + +### NGINX configuration files + +Copy the `gitlab-pages-ssl` Nginx configuration file: + +```bash +sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf +sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf +``` + +Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +### NGINX configuration for custom domains + +> If you are not using custom domains ignore this section. + +[In the case of custom domains](#the-gitlab-pages-daemon-and-the-case-of-custom-domains), +if you have the secondary IP address configured on the same server as GitLab, +you need to change **all** NGINX configs to listen on the first IP address. + +**Source installations:** + +1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace + `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + listens to. +1. Restart NGINX + +**Omnibus installations:** + +1. Edit `/etc/gitlab/gilab.rb`: + + ``` + nginx['listen_addresses'] = ['1.1.1.1'] + ``` +1. [Reconfigure GitLab][reconfigure] + +### NGINX caveats + Be extra careful when setting up the domain name in the NGINX config. You must not remove the backslashes. -- cgit v1.2.1 From 84ff07cdcc846cec7a58aca641363531cda86d92 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 02:49:02 +0200 Subject: Simplify NGINX server_name regex --- doc/pages/administration.md | 10 +++++----- lib/support/nginx/gitlab-pages | 2 +- lib/support/nginx/gitlab-pages-ssl | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index f67bb63ff07..30d2b46c36a 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -414,20 +414,20 @@ not remove the backslashes. If your GitLab pages domain is `example.io`, replace: ```bash -server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; +server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$; ``` with: ``` -server_name ~^(?<group>.*)\.example\.io$; +server_name ~^.*\.example\.io$; ``` -If you are using a subdomain, make sure to escape all dots (`.`) with a -backslash (\). For example `pages.example.io` would be: +If you are using a subdomain, make sure to escape all dots (`.`) except from +the first one with a backslash (\). For example `pages.example.io` would be: ``` -server_name ~^(?<group>.*)\.pages\.example\.io$; +server_name ~^.*\.pages\.example\.io$; ``` ## Set maximum pages size diff --git a/lib/support/nginx/gitlab-pages b/lib/support/nginx/gitlab-pages index 169d7d11ca7..d9746c5c1aa 100644 --- a/lib/support/nginx/gitlab-pages +++ b/lib/support/nginx/gitlab-pages @@ -7,7 +7,7 @@ server { listen [::]:80 ipv6only=on; ## Replace this with something like pages.gitlab.com - server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; + server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$; ## Individual nginx logs for GitLab pages access_log /var/log/nginx/gitlab_pages_access.log; diff --git a/lib/support/nginx/gitlab-pages-ssl b/lib/support/nginx/gitlab-pages-ssl index 16edd337e10..a1ccf266835 100644 --- a/lib/support/nginx/gitlab-pages-ssl +++ b/lib/support/nginx/gitlab-pages-ssl @@ -11,7 +11,7 @@ server { listen [::]:80 ipv6only=on; ## Replace this with something like pages.gitlab.com - server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; + server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$; server_tokens off; ## Don't show the nginx version number, a security best practice return 301 https://$http_host$request_uri; @@ -26,7 +26,7 @@ server { listen [::]:443 ipv6only=on ssl http2; ## Replace this with something like pages.gitlab.com - server_name ~^(?<group>.*)\.YOUR_GITLAB_PAGES\.DOMAIN$; + server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$; server_tokens off; ## Don't show the nginx version number, a security best practice ## Strong SSL Security -- cgit v1.2.1 From aadbb6065b58d3d308b1fe7a15f5dfad290e0771 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 03:03:05 +0200 Subject: Move Omnibus storage path --- doc/pages/administration.md | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 30d2b46c36a..c99ff87e87d 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -72,9 +72,9 @@ and/or `443`. For that reason, there is some flexibility in the way which you can set it up, so you basically have three choices: 1. Run the pages daemon in the same server as GitLab, listening on a secondary IP -1. Run the pages daemon in a separate server. In that case, the Pages [`path`] - must also be present in the server that the pages daemon is installed, so - you will have to share it via network. +1. Run the pages daemon in a separate server. In that case, the + [Pages path](#change-storage-path) must also be present in the server that + the pages daemon is installed, so you will have to share it via network. 1. Run the pages daemon in the same server as GitLab, listening on the same IP but on different ports. In that case, you will have to proxy the traffic with a loadbalancer. If you choose that route note that you should use TCP load @@ -85,8 +85,6 @@ can set it up, so you basically have three choices: In this document, we will proceed assuming the first option. First let's install the pages daemon. -[`path`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/config/gitlab.yml.example#L155 - ### Install the Pages daemon **Install the Pages daemon on a source installation** @@ -404,6 +402,7 @@ you need to change **all** NGINX configs to listen on the first IP address. ``` nginx['listen_addresses'] = ['1.1.1.1'] ``` + 1. [Reconfigure GitLab][reconfigure] ### NGINX caveats @@ -438,22 +437,32 @@ The default is 100MB. ## Change storage path -Pages are stored by default in `/home/git/gitlab/shared/pages`. -If you wish to store them in another location you must set it up in -`gitlab.yml` under the `pages` section: +**Source installations:** -```yaml -pages: - enabled: true - # The location where pages are stored (default: shared/pages). - path: /mnt/storage/pages -``` +1. Pages are stored by default in `/home/git/gitlab/shared/pages`. + If you wish to store them in another location you must set it up in + `gitlab.yml` under the `pages` section: -Restart GitLab for the changes to take effect: + ```yaml + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + path: /mnt/storage/pages + ``` -```bash -sudo service gitlab restart -``` +1. [Restart GitLab][restart] + +**Omnibus installations:** + +1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`. + If you wish to store them in another location you must set it up in + `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['pages_path'] = "/mnt/storage/pages" + ``` + +1. [Reconfigure GitLab][reconfigure] ## Backup -- cgit v1.2.1 From 37fc486c73d91d25ddeb35eb6d528e16dcb1e9a9 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 03:03:42 +0200 Subject: Link to backup task --- doc/pages/administration.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index c99ff87e87d..62665a94f1c 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -466,7 +466,7 @@ The default is 100MB. ## Backup -Pages are part of the regular backup so there is nothing to configure. +Pages are part of the [regular backup][backup] so there is nothing to configure. ## Security @@ -509,7 +509,6 @@ No new changes. [8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md ---- - [reconfigure]: ../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../../administration/restart_gitlab.md#installations-from-source +[backup]: ../../raketasks/backup_restore.md -- cgit v1.2.1 From 9ca234615e44e2b365b8d6c1cc5a91c5bdd7159d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 03:16:27 +0200 Subject: Add link to Omnibus 8-3 docs --- doc/pages/administration.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 62665a94f1c..4aaabdb3442 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -490,6 +490,7 @@ latest previous version. - The [NGINX configs][] have changed to reflect this change. So make sure to update them. - Custom CNAME and TLS certificates support +- Documentation was moved to one place [8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md [gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.0 @@ -503,12 +504,12 @@ No new changes. --- -**GitLab 8.3 ([documentation][8-3-docs])** +**GitLab 8.3 ([source docs][8-3-docs], [Omnibus docs][8-3-omnidocs])** - GitLab Pages feature was introduced. [8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md - +[8-3-omnidocs]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/8-3-stable-ee/doc/settings/pages.md [reconfigure]: ../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../../administration/restart_gitlab.md#installations-from-source [backup]: ../../raketasks/backup_restore.md -- cgit v1.2.1 From e5c7c8ca5b56563e1c133d736eb13c87aa749fa9 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 03:27:44 +0200 Subject: Small reword fixes --- doc/pages/administration.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 4aaabdb3442..ad9c04a64bb 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -48,7 +48,7 @@ probably want to read the [user documentation](README.md). ## The GitLab Pages daemon -Starting from GitLab EE 8.5, Pages make use of a separate tool ([gitlab-pages]), +Starting from GitLab EE 8.5, GitLab Pages make use of the [GitLab Pages daemon], a simple HTTP server written in Go that can listen on an external IP address and provide support for custom domains and custom certificates. The GitLab Pages Daemon supports dynamic certificates through SNI and exposes pages using @@ -65,6 +65,9 @@ Here is a brief list with what it is supported when using the pages daemon: You are encouraged to read its [README][pages-readme] to fully understand how it works. +[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages +[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md + ### The GitLab Pages daemon and the case of custom domains In the case of custom domains, the Pages daemon needs to listen on ports `80` @@ -87,7 +90,7 @@ install the pages daemon. ### Install the Pages daemon -**Install the Pages daemon on a source installation** +**Source installations** ``` cd /home/git @@ -97,11 +100,10 @@ sudo -u git -H git checkout 0.2.0 sudo -u git -H make ``` -**Install the Pages daemon on Omnibus** +**Omnibus installations** The `gitlab-pages` daemon is included in the Omnibus package. -[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md ## Configuration -- cgit v1.2.1 From bdc7301aa448b79766fa342459aa1749deeb7b85 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 03:33:20 +0200 Subject: Add configuration prerequisites section [ci skip] --- doc/pages/administration.md | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index ad9c04a64bb..7bdd331df65 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -27,6 +27,7 @@ probably want to read the [user documentation](README.md). - [The GitLab Pages daemon and the case of custom domains](#the-gitlab-pages-daemon-and-the-case-of-custom-domains) - [Install the Pages daemon](#install-the-pages-daemon) - [Configuration](#configuration) + - [Configuration prerequisites](#configuration-prerequisites) - [Configuration scenarios](#configuration-scenarios) - [DNS configuration](#dns-configuration) - [Setting up GitLab Pages](#setting-up-gitlab-pages) @@ -108,13 +109,13 @@ The `gitlab-pages` daemon is included in the Omnibus package. ## Configuration There are multiple ways to set up GitLab Pages according to what URL scheme you -are willing to support. Below you will find all possible scenarios to choose -from. +are willing to support. -### Configuration scenarios +### Configuration prerequisites -Before proceeding with setting up GitLab Pages, you have to decide which route -you want to take. Note that in either scenario, you need: +In the next section you will find all possible scenarios to choose from. + +In either scenario, you will need: 1. To use the [GitLab Pages daemon](#the-gitlab-pages-daemon) 1. A separate domain @@ -125,6 +126,11 @@ you want to take. Note that in either scenario, you need: 1. (Optional but recommended) [Shared runners](../ci/runners/README.md) so that your users don't have to bring their own +### Configuration scenarios + +Before proceeding with setting up GitLab Pages, you have to decide which route +you want to take. + The possible scenarios are depicted in the table below. | URL scheme | Option | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -- cgit v1.2.1 From e40693047aeb293afa00800a5eea743559337308 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 09:57:27 +0200 Subject: Checkout the tag of pages daemon --- doc/pages/administration.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 7bdd331df65..ddbc8a7765f 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -86,8 +86,8 @@ can set it up, so you basically have three choices: pages will not be able to be served with user provided certificates. For HTTP it's OK to use HTTP or TCP load balancing. -In this document, we will proceed assuming the first option. First let's -install the pages daemon. +In this document, we will proceed assuming the first option. Let's begin by +installing the pages daemon. ### Install the Pages daemon @@ -97,7 +97,7 @@ install the pages daemon. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages -sudo -u git -H git checkout 0.2.0 +sudo -u git -H git checkout v0.2.0 sudo -u git -H make ``` -- cgit v1.2.1 From 8094a9d1115bfbe2899fd63862f0d3f9fcce438b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 12:19:02 +0200 Subject: Add missing Omnibus settings --- doc/pages/administration.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index ddbc8a7765f..d0fdbeafa5b 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -219,8 +219,11 @@ Below are the four scenarios that are described in 1. Edit `/etc/gitlab/gitlab.rb`: ```ruby + pages_external_url "https://example.io" nginx['listen_addresses'] = ['1.1.1.1'] pages_nginx['enable'] = false + gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt" + gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key" gitlab_pages['external_http'] = '1.1.1.2:80' gitlab_pages['external_https'] = '1.1.1.2:443' ``` @@ -276,6 +279,7 @@ Below are the four scenarios that are described in 1. Edit `/etc/gitlab/gitlab.rb`: ```ruby + pages_external_url "https://example.io" nginx['listen_addresses'] = ['1.1.1.1'] pages_nginx['enable'] = false gitlab_pages['external_http'] = '1.1.1.2:80' -- cgit v1.2.1 From 5556db04040c8c97834728dcf0fb26d2ea2c9a16 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 22 Feb 2016 12:26:22 +0200 Subject: Add missing gitlab-pages related vars in init.d/gitlab --- lib/support/init.d/gitlab | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index 9f2ce01d931..97414ead3dd 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -107,7 +107,7 @@ check_pids(){ wait_for_pids(){ # We are sleeping a bit here mostly because sidekiq is slow at writing its pid i=0; - while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do + while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ] || { [ "$gitlab_pages_enabled" = true ] && [ ! -f $gitlab_pages_pid_path ]; }; do sleep 0.1; i=$((i+1)) if [ $((i%10)) = 0 ]; then @@ -240,7 +240,7 @@ start_gitlab() { if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then echo "Starting GitLab MailRoom" fi - if [ "gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" != "0" ]; then + if [ "$gitlab_pages_enabled" = true ] && [ "$gitlab_pages_status" != "0" ]; then echo "Starting GitLab Pages" fi -- cgit v1.2.1 From 639cf728f8c14560e85e0f54d5f4f27329d98c3c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 23 Feb 2016 12:03:03 +0100 Subject: Fix adding pages domain to projects in groups --- app/views/projects/pages_domains/_form.html.haml | 2 +- features/project/pages.feature | 9 +++++++++ features/steps/shared/project.rb | 6 ++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml index e97d19653d5..ca1b41b140a 100644 --- a/app/views/projects/pages_domains/_form.html.haml +++ b/app/views/projects/pages_domains/_form.html.haml @@ -1,4 +1,4 @@ -= form_for [@project.namespace, @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f| += form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f| - if @domain.errors.any? #error_explanation .alert.alert-danger diff --git a/features/project/pages.feature b/features/project/pages.feature index 392f2d29c3c..87d88348d09 100644 --- a/features/project/pages.feature +++ b/features/project/pages.feature @@ -40,6 +40,15 @@ Feature: Project Pages And I click on "Create New Domain" Then I should see a new domain added + Scenario: I should be able to add a new domain for project in group namespace + Given I own a project in some group namespace + And pages are enabled + And pages are exposed on external HTTP address + When I visit add a new Pages Domain + And I fill the domain + And I click on "Create New Domain" + Then I should see a new domain added + Scenario: I should be denied to add the same domain twice Given pages are enabled And pages are exposed on external HTTP address diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 7a6707a7dfb..dae248b8b7e 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -7,6 +7,12 @@ module SharedProject @project.team << [@user, :master] end + step "I own a project in some group namespace" do + @group = create(:group, name: 'some group') + @project = create(:project, namespace: @group) + @project.team << [@user, :master] + end + step "project exists in some group namespace" do @group = create(:group, name: 'some group') @project = create(:project, :repository, namespace: @group, public_builds: false) -- cgit v1.2.1 From 55214fe1ebed923b23df43afc6da34aede2a00d2 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 23 Feb 2016 15:45:03 +0200 Subject: First iteration on simplifying the pages user docs [ci skip] --- doc/pages/README.md | 156 ++++++++++++++++++++++++++++++------- doc/pages/img/create_user_page.png | Bin 0 -> 66593 bytes 2 files changed, 126 insertions(+), 30 deletions(-) create mode 100644 doc/pages/img/create_user_page.png diff --git a/doc/pages/README.md b/doc/pages/README.md index c9d0d7e2b49..73e18d1b9a7 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -1,11 +1,38 @@ # GitLab Pages -_**Note:** This feature was [introduced][ee-80] in GitLab EE 8.3_ +> **Note:** +> This feature was [introduced][ee-80] in GitLab EE 8.3. +> Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. With GitLab Pages you can host for free your static websites on GitLab. Combined with the power of GitLab CI and the help of GitLab Runner you can deploy static pages for your individual projects, your user or your group. +--- + +<!-- START doctoc generated TOC please keep comment here to allow auto update --> +<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> +**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* + +- [Enable the pages feature in your GitLab EE instance](#enable-the-pages-feature-in-your-gitlab-ee-instance) +- [Understanding how GitLab Pages work](#understanding-how-gitlab-pages-work) +- [Two kinds of GitLab Pages](#two-kinds-of-gitlab-pages) + - [GitLab pages per user or group](#gitlab-pages-per-user-or-group) + - [GitLab pages per project](#gitlab-pages-per-project) +- [Enable the pages feature in your project](#enable-the-pages-feature-in-your-project) +- [Remove the contents of your pages](#remove-the-contents-of-your-pages) +- [Explore the contents of .gitlab-ci.yml](#explore-the-contents-of-gitlab-ci-yml) +- [Example projects](#example-projects) +- [Custom error codes pages](#custom-error-codes-pages) + - [Adding a custom domain to your Pages](#adding-a-custom-domain-to-your-pages) + - [Securing your Pages with TLS](#securing-your-pages-with-tls) +- [Enable the pages feature in your project](#enable-the-pages-feature-in-your-project) +- [Remove the contents of your pages](#remove-the-contents-of-your-pages) +- [Limitations](#limitations) +- [Frequently Asked Questions](#frequently-asked-questions) + +<!-- END doctoc generated TOC please keep comment here to allow auto update --> + ## Enable the pages feature in your GitLab EE instance The administrator guide is located at [administration](administration.md). @@ -16,41 +43,60 @@ GitLab Pages rely heavily on GitLab CI and its ability to upload artifacts. The steps that are performed from the initialization of a project to the creation of the static content, can be summed up to: -1. Create project (its name could be specific according to the case) -1. Provide a specific job in `.gitlab-ci.yml` +1. Find out the general domain name that is used for GitLab Pages + (ask your administrator). This is very important, so you should first make + sure you get that right. +1. Create a project (its name should be specific according to the case) +1. Provide a specific job named `pages` in `.gitlab-ci.yml` 1. GitLab Runner builds the project 1. GitLab CI uploads the artifacts -1. Nginx serves the content +1. The [GitLab Pages daemon][pages-daemon] serves the content -As a user, you should normally be concerned only with the first three items. -If [shared runners](../ci/runners/README.md) are enabled by your GitLab +As a user, you should normally be concerned only with the first three or four +items. If [shared runners](../ci/runners/README.md) are enabled by your GitLab administrator, you should be able to use them instead of bringing your own. -In general there are four kinds of pages one might create. This is better -explained with an example so let's make some assumptions. +> **Note:** +> In the rest of this document we will assume that the general domain name that +> is used for GitLab Pages is `example.io`. + +## Two kinds of GitLab Pages + +In general there are two kinds of pages one might create: + +- Pages per user/group +- Pages per project -The domain under which the pages are hosted is named `gitlab.io`. There is a -user with the username `walter` and they are the owner of an organization named -`therug`. The personal project of `walter` is named `area51` and don't forget -that the organization has also a project under its namespace, called -`welovecats`. +In GitLab, usernames and groupnames are unique and often people refer to them +as namespaces. There can be only one namespace in a GitLab instance. -The following table depicts what the project's name should be and under which -URL it will be accessible. +> **Warning:** +> There are some known [limitations](#limitations) regarding namespaces served +> under the general domain name and HTTPS. Make sure to read that section. + +### GitLab pages per user or group + +Head over your GitLab instance that supports GitLab Pages and create a +repository named `username.example.io`, where `username` is your username on +GitLab. If the first part of the project name doesn’t match exactly your +username, it won’t work, so make sure to get it right. + +![Create a user-based pages repository](img/create_user_page.png) + +--- -| Pages type | Repository name | URL schema | -| ---------- | --------------- | ---------- | -| User page | `walter/walter.gitlab.io` | `https://walter.gitlab.io` | -| Group page | `therug/therug.gitlab.io` | `https://therug.gitlab.io` | -| Specific project under a user's page | `walter/area51` | `https://walter.gitlab.io/area51` | -| Specific project under a group's page | `therug/welovecats` | `https://therug.gitlab.io/welovecats` | +To create a group page the steps are exactly the same. Just make sure that +you are creating the project within the group's namespace. -## Group pages +After you upload some static content to your repository, you will be able to +access it under `http(s)://username.example.io`. Keep reading to find out how. -To create a page for a group, add a new project to it. The project name must be lowercased. +### GitLab pages per project + +> **Note:** +> You do _not_ have to create a project named `username.example.io` in order to +> serve a project's page. -For example, if you have a group called `TheRug` and pages are hosted under `Example.com`, -create a project named `therug.example.com`. ## Enable the pages feature in your project @@ -60,7 +106,7 @@ under its **Settings**. ## Remove the contents of your pages Pages can be explicitly removed from a project by clicking **Remove Pages** -in a project's **Settings**. +Go to your project's **Settings > Pages**. ## Explore the contents of .gitlab-ci.yml @@ -86,8 +132,8 @@ a commit is pushed only on the `master` branch, which is advisable to do so. The pages are created after the build completes successfully and the artifacts for the `pages` job are uploaded to GitLab. -The example below is using [Jekyll][] and assumes that the created HTML files -are generated under the `public/` directory. +The example below uses [Jekyll][] and generates the created HTML files +under the `public/` directory. ```yaml image: ruby:2.1 @@ -103,11 +149,31 @@ pages: - master ``` +The example below doesn't use any static site generator, but simply moves all +files from the root of the project to the `public/` directory. The `.public` +workaround is so `cp` doesn’t also copy `public/` to itself in an infinite +loop. + +```yaml +pages: + stage: deploy + script: + - mkdir .public + - cp -r * .public + - mv .public public + artifacts: + paths: + - public + only: + - master +``` + ## Example projects -Below is a list of example projects that make use of static generators. -Contributions are very welcome. +Below is a list of example projects for GitLab Pages with a plain HTML website +or various static site generators. Contributions are very welcome. +* [Plain HTML](https://gitlab.com/gitlab-examples/pages-plain-html) * [Jekyll](https://gitlab.com/gitlab-examples/pages-jekyll) ## Custom error codes pages @@ -116,6 +182,34 @@ You can provide your own 403 and 404 error pages by creating the `403.html` and `404.html` files respectively in the `public/` directory that will be included in the artifacts. +### Adding a custom domain to your Pages + + + +### Securing your Pages with TLS + + +## Enable the pages feature in your project + +The GitLab Pages feature needs to be explicitly enabled for each project +under **Settings > Pages**. + +## Remove the contents of your pages + +Pages can be explicitly removed from a project by clicking **Remove Pages** +in a project's **Settings**. + +## Limitations + +When using Pages under the general domain of a GitLab instance (`*.example.io`), +you _cannot_ use HTTPS with sub-subdomains. That means that if your +username/groupname contains a dot, for example `foo.bar`, the domain +`https://foo.bar.example.io` will _not_ work. This is a limitation of the +[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you +don't redirect HTTP to HTTPS. + +[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC" + ## Frequently Asked Questions **Q: Can I download my generated pages?** @@ -126,3 +220,5 @@ Sure. All you need to do is download the artifacts archive from the build page. [jekyll]: http://jekyllrb.com/ [ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 +[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 +[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages diff --git a/doc/pages/img/create_user_page.png b/doc/pages/img/create_user_page.png new file mode 100644 index 00000000000..8c2b67e233a Binary files /dev/null and b/doc/pages/img/create_user_page.png differ -- cgit v1.2.1 From 47bff50ffd68fccd6db71bc800194d49fad6eaa1 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 23 Feb 2016 19:29:24 +0200 Subject: Reorganize sections [ci skip] --- doc/pages/README.md | 72 ++++++++++++++++++++++++----------------------------- 1 file changed, 33 insertions(+), 39 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 73e18d1b9a7..50f45a4cae1 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -15,19 +15,17 @@ deploy static pages for your individual projects, your user or your group. **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [Enable the pages feature in your GitLab EE instance](#enable-the-pages-feature-in-your-gitlab-ee-instance) -- [Understanding how GitLab Pages work](#understanding-how-gitlab-pages-work) -- [Two kinds of GitLab Pages](#two-kinds-of-gitlab-pages) +- [Getting started with GitLab Pages](#getting-started-with-gitlab-pages) - [GitLab pages per user or group](#gitlab-pages-per-user-or-group) - [GitLab pages per project](#gitlab-pages-per-project) -- [Enable the pages feature in your project](#enable-the-pages-feature-in-your-project) -- [Remove the contents of your pages](#remove-the-contents-of-your-pages) -- [Explore the contents of .gitlab-ci.yml](#explore-the-contents-of-gitlab-ci-yml) -- [Example projects](#example-projects) -- [Custom error codes pages](#custom-error-codes-pages) - - [Adding a custom domain to your Pages](#adding-a-custom-domain-to-your-pages) - - [Securing your Pages with TLS](#securing-your-pages-with-tls) -- [Enable the pages feature in your project](#enable-the-pages-feature-in-your-project) -- [Remove the contents of your pages](#remove-the-contents-of-your-pages) + - [Enable the pages feature in your project](#enable-the-pages-feature-in-your-project) + - [Remove the contents of your pages](#remove-the-contents-of-your-pages) + - [Explore the contents of .gitlab-ci.yml](#explore-the-contents-of-gitlab-ci-yml) +- [Next steps](#next-steps) + - [Adding a custom domain to your Pages website](#adding-a-custom-domain-to-your-pages-website) + - [Securing your custom domain website with TLS](#securing-your-custom-domain-website-with-tls) + - [Example projects](#example-projects) + - [Custom error codes pages](#custom-error-codes-pages) - [Limitations](#limitations) - [Frequently Asked Questions](#frequently-asked-questions) @@ -37,7 +35,7 @@ deploy static pages for your individual projects, your user or your group. The administrator guide is located at [administration](administration.md). -## Understanding how GitLab Pages work +## Getting started with GitLab Pages GitLab Pages rely heavily on GitLab CI and its ability to upload artifacts. The steps that are performed from the initialization of a project to the @@ -60,8 +58,6 @@ administrator, you should be able to use them instead of bringing your own. > In the rest of this document we will assume that the general domain name that > is used for GitLab Pages is `example.io`. -## Two kinds of GitLab Pages - In general there are two kinds of pages one might create: - Pages per user/group @@ -78,7 +74,7 @@ as namespaces. There can be only one namespace in a GitLab instance. Head over your GitLab instance that supports GitLab Pages and create a repository named `username.example.io`, where `username` is your username on -GitLab. If the first part of the project name doesn’t match exactly your +GitLab. If the first part of the project name doesn't match exactly your username, it won’t work, so make sure to get it right. ![Create a user-based pages repository](img/create_user_page.png) @@ -97,18 +93,19 @@ access it under `http(s)://username.example.io`. Keep reading to find out how. > You do _not_ have to create a project named `username.example.io` in order to > serve a project's page. +GitLab Pages for projects -## Enable the pages feature in your project +### Enable the pages feature in your project The GitLab Pages feature needs to be explicitly enabled for each project under its **Settings**. -## Remove the contents of your pages +### Remove the contents of your pages Pages can be explicitly removed from a project by clicking **Remove Pages** Go to your project's **Settings > Pages**. -## Explore the contents of .gitlab-ci.yml +### Explore the contents of .gitlab-ci.yml Before reading this section, make sure you familiarize yourself with GitLab CI and the specific syntax of[`.gitlab-ci.yml`](../ci/yaml/README.md) by @@ -151,7 +148,7 @@ pages: The example below doesn't use any static site generator, but simply moves all files from the root of the project to the `public/` directory. The `.public` -workaround is so `cp` doesn’t also copy `public/` to itself in an infinite +workaround is so `cp` doesn't also copy `public/` to itself in an infinite loop. ```yaml @@ -168,36 +165,27 @@ pages: - master ``` -## Example projects +## Next steps + +### Adding a custom domain to your Pages website + + +### Securing your custom domain website with TLS + +### Example projects Below is a list of example projects for GitLab Pages with a plain HTML website or various static site generators. Contributions are very welcome. -* [Plain HTML](https://gitlab.com/gitlab-examples/pages-plain-html) -* [Jekyll](https://gitlab.com/gitlab-examples/pages-jekyll) +- [Plain HTML](https://gitlab.com/gitlab-examples/pages-plain-html) +- [Jekyll](https://gitlab.com/gitlab-examples/pages-jekyll) -## Custom error codes pages +### Custom error codes pages You can provide your own 403 and 404 error pages by creating the `403.html` and `404.html` files respectively in the `public/` directory that will be included in the artifacts. -### Adding a custom domain to your Pages - - - -### Securing your Pages with TLS - - -## Enable the pages feature in your project - -The GitLab Pages feature needs to be explicitly enabled for each project -under **Settings > Pages**. - -## Remove the contents of your pages - -Pages can be explicitly removed from a project by clicking **Remove Pages** -in a project's **Settings**. ## Limitations @@ -216,6 +204,12 @@ don't redirect HTTP to HTTPS. Sure. All you need to do is download the artifacts archive from the build page. + +**Q: Can I use GitLab Pages if my project is private?** + +Yes. GitLab Pages doesn't care whether you set your project's visibility level +to private, internal or public. + --- [jekyll]: http://jekyllrb.com/ -- cgit v1.2.1 From 5b9d886963dd9be2487226f34012bf0ed0de406d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 24 Feb 2016 09:49:48 +0200 Subject: Add links to CI and runner --- doc/pages/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 50f45a4cae1..0d24d92a23c 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -5,7 +5,7 @@ > Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. With GitLab Pages you can host for free your static websites on GitLab. -Combined with the power of GitLab CI and the help of GitLab Runner you can +Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can deploy static pages for your individual projects, your user or your group. --- @@ -216,3 +216,5 @@ to private, internal or public. [ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 [ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 [pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages +[gitlab ci]: https://about.gitlab.com/gitlab-ci +[gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner -- cgit v1.2.1 From 81533b8f0c3efb9c1e11933f13e768550afa9d42 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 24 Feb 2016 12:36:31 +0200 Subject: More section cleanup --- doc/pages/README.md | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 0d24d92a23c..17063ef3ba0 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -4,6 +4,10 @@ > This feature was [introduced][ee-80] in GitLab EE 8.3. > Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. +> **Note:** +> This document is about the user guide. To learn how to enable GitLab Pages +> across your GitLab instance, visit the [administrator documentation](administration.md). + With GitLab Pages you can host for free your static websites on GitLab. Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can deploy static pages for your individual projects, your user or your group. @@ -14,13 +18,11 @@ deploy static pages for your individual projects, your user or your group. <!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* -- [Enable the pages feature in your GitLab EE instance](#enable-the-pages-feature-in-your-gitlab-ee-instance) - [Getting started with GitLab Pages](#getting-started-with-gitlab-pages) - [GitLab pages per user or group](#gitlab-pages-per-user-or-group) - [GitLab pages per project](#gitlab-pages-per-project) - - [Enable the pages feature in your project](#enable-the-pages-feature-in-your-project) - - [Remove the contents of your pages](#remove-the-contents-of-your-pages) - [Explore the contents of .gitlab-ci.yml](#explore-the-contents-of-gitlab-ci-yml) + - [Remove the contents of your pages](#remove-the-contents-of-your-pages) - [Next steps](#next-steps) - [Adding a custom domain to your Pages website](#adding-a-custom-domain-to-your-pages-website) - [Securing your custom domain website with TLS](#securing-your-custom-domain-website-with-tls) @@ -31,10 +33,6 @@ deploy static pages for your individual projects, your user or your group. <!-- END doctoc generated TOC please keep comment here to allow auto update --> -## Enable the pages feature in your GitLab EE instance - -The administrator guide is located at [administration](administration.md). - ## Getting started with GitLab Pages GitLab Pages rely heavily on GitLab CI and its ability to upload artifacts. @@ -95,16 +93,6 @@ access it under `http(s)://username.example.io`. Keep reading to find out how. GitLab Pages for projects -### Enable the pages feature in your project - -The GitLab Pages feature needs to be explicitly enabled for each project -under its **Settings**. - -### Remove the contents of your pages - -Pages can be explicitly removed from a project by clicking **Remove Pages** -Go to your project's **Settings > Pages**. - ### Explore the contents of .gitlab-ci.yml Before reading this section, make sure you familiarize yourself with GitLab CI @@ -165,6 +153,11 @@ pages: - master ``` +### Remove the contents of your pages + +Pages can be explicitly removed from a project by clicking **Remove Pages** +Go to your project's **Settings > Pages**. + ## Next steps ### Adding a custom domain to your Pages website -- cgit v1.2.1 From edc8782e85bfde761b4f2af52d62889176295703 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 24 Feb 2016 14:10:51 +0200 Subject: Split user and group sections --- doc/pages/README.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 17063ef3ba0..7337120298f 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -19,7 +19,8 @@ deploy static pages for your individual projects, your user or your group. **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [Getting started with GitLab Pages](#getting-started-with-gitlab-pages) - - [GitLab pages per user or group](#gitlab-pages-per-user-or-group) + - [GitLab pages per user](#gitlab-pages-per-user) + - [GitLab pages per group](#gitlab-pages-per-group) - [GitLab pages per project](#gitlab-pages-per-project) - [Explore the contents of .gitlab-ci.yml](#explore-the-contents-of-gitlab-ci-yml) - [Remove the contents of your pages](#remove-the-contents-of-your-pages) @@ -68,7 +69,7 @@ as namespaces. There can be only one namespace in a GitLab instance. > There are some known [limitations](#limitations) regarding namespaces served > under the general domain name and HTTPS. Make sure to read that section. -### GitLab pages per user or group +### GitLab pages per user Head over your GitLab instance that supports GitLab Pages and create a repository named `username.example.io`, where `username` is your username on @@ -79,19 +80,25 @@ username, it won’t work, so make sure to get it right. --- -To create a group page the steps are exactly the same. Just make sure that -you are creating the project within the group's namespace. - After you upload some static content to your repository, you will be able to access it under `http(s)://username.example.io`. Keep reading to find out how. +### GitLab pages per group + +To create a group page the steps are the same like when creating a website for +users. Just make sure that you are creating the project within the group's +namespace. + +After you upload some static content to your repository, you will be able to +access it under `http(s)://groupname.example.io`. + ### GitLab pages per project > **Note:** > You do _not_ have to create a project named `username.example.io` in order to > serve a project's page. -GitLab Pages for projects + ### Explore the contents of .gitlab-ci.yml -- cgit v1.2.1 From bcf891719f89620544ab8f4ea7d91748fa277251 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 24 Feb 2016 14:13:44 +0200 Subject: Convert CI quick start guide into a note [ci skip] --- doc/pages/README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 7337120298f..f63cfb3cda2 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -102,11 +102,10 @@ access it under `http(s)://groupname.example.io`. ### Explore the contents of .gitlab-ci.yml -Before reading this section, make sure you familiarize yourself with GitLab CI -and the specific syntax of[`.gitlab-ci.yml`](../ci/yaml/README.md) by -following our [quick start guide](../ci/quick_start/README.md). - ---- +> **Note:** +> Before reading this section, make sure you familiarize yourself with GitLab CI +> and the specific syntax of[`.gitlab-ci.yml`](../ci/yaml/README.md) by +> following our [quick start guide](../ci/quick_start/README.md). To make use of GitLab Pages, your `.gitlab-ci.yml` must follow the rules below: -- cgit v1.2.1 From dbfde1d06f984ff37c5c953f914acf8d2ffe1c63 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 24 Feb 2016 16:12:12 +0200 Subject: Add requirements section --- doc/pages/README.md | 46 ++++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index f63cfb3cda2..42df9c79f22 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -19,6 +19,7 @@ deploy static pages for your individual projects, your user or your group. **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - [Getting started with GitLab Pages](#getting-started-with-gitlab-pages) + - [GitLab Pages requirements](#gitlab-pages-requirements) - [GitLab pages per user](#gitlab-pages-per-user) - [GitLab pages per group](#gitlab-pages-per-group) - [GitLab pages per project](#gitlab-pages-per-project) @@ -36,39 +37,40 @@ deploy static pages for your individual projects, your user or your group. ## Getting started with GitLab Pages -GitLab Pages rely heavily on GitLab CI and its ability to upload artifacts. -The steps that are performed from the initialization of a project to the -creation of the static content, can be summed up to: - -1. Find out the general domain name that is used for GitLab Pages - (ask your administrator). This is very important, so you should first make - sure you get that right. -1. Create a project (its name should be specific according to the case) -1. Provide a specific job named `pages` in `.gitlab-ci.yml` -1. GitLab Runner builds the project -1. GitLab CI uploads the artifacts -1. The [GitLab Pages daemon][pages-daemon] serves the content - -As a user, you should normally be concerned only with the first three or four -items. If [shared runners](../ci/runners/README.md) are enabled by your GitLab -administrator, you should be able to use them instead of bringing your own. - -> **Note:** -> In the rest of this document we will assume that the general domain name that -> is used for GitLab Pages is `example.io`. +GitLab Pages rely heavily on GitLab CI and its ability to upload +[artifacts](../ci/yaml/README.md#artifacts). In general there are two kinds of pages one might create: -- Pages per user/group -- Pages per project +- Pages per user/group (`username.example.io`) +- Pages per project (`username.example.io/projectname`) In GitLab, usernames and groupnames are unique and often people refer to them as namespaces. There can be only one namespace in a GitLab instance. +> **Note:** +> In the rest of this document we will assume that the general domain name that +> is used for GitLab Pages is `example.io`. + > **Warning:** > There are some known [limitations](#limitations) regarding namespaces served > under the general domain name and HTTPS. Make sure to read that section. +### GitLab Pages requirements + +In brief, this is what you need to upload your website in GitLab Pages: + +1. Find out the general domain name that is used for GitLab Pages + (ask your administrator). This is very important, so you should first make + sure you get that right. +1. Create a project +1. Provide a specific job named [`pages`](../ci/yaml/README.md#pages) in + [`.gitlab-ci.yml`](../ci/yaml/README.md) +1. A GitLab Runner to build GitLab Pages + +If [shared runners](../ci/runners/README.md) are enabled by your GitLab +administrator, you should be able to use them instead of bringing your own. + ### GitLab pages per user Head over your GitLab instance that supports GitLab Pages and create a -- cgit v1.2.1 From 34d75cff99d32f7f3a5e53fc47292328aa7ac1de Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 24 Feb 2016 16:12:38 +0200 Subject: Rephrase --- doc/pages/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 42df9c79f22..4413a327246 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -82,8 +82,9 @@ username, it won’t work, so make sure to get it right. --- -After you upload some static content to your repository, you will be able to -access it under `http(s)://username.example.io`. Keep reading to find out how. +After you push some static content to your repository and GitLab Runner uploads +the artifacts to GitLab CI, you will be able to access your website under +`http(s)://username.example.io`. Keep reading to find out how. ### GitLab pages per group -- cgit v1.2.1 From 8f7eb4e3d04edc415af7a1dc58e96de366823c19 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 24 Feb 2016 16:12:47 +0200 Subject: Add image for pages removal --- doc/pages/README.md | 4 +++- doc/pages/img/pages_remove.png | Bin 0 -> 19232 bytes 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 doc/pages/img/pages_remove.png diff --git a/doc/pages/README.md b/doc/pages/README.md index 4413a327246..6f1d2d27554 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -165,7 +165,9 @@ pages: ### Remove the contents of your pages Pages can be explicitly removed from a project by clicking **Remove Pages** -Go to your project's **Settings > Pages**. +in your project's **Settings > Pages**. + +![Remove pages](img/pages_remove.png) ## Next steps diff --git a/doc/pages/img/pages_remove.png b/doc/pages/img/pages_remove.png new file mode 100644 index 00000000000..36141bd4d12 Binary files /dev/null and b/doc/pages/img/pages_remove.png differ -- cgit v1.2.1 From c9b63ee44ccedbbd1fd8b1e7da57442a25dd2e2c Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 24 Feb 2016 16:26:34 +0200 Subject: Add pages job in yaml document --- doc/ci/yaml/README.md | 29 +++++++++++++++++++++++++++++ doc/pages/README.md | 5 +++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index f11257be5c3..ca293d54cb6 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1281,6 +1281,35 @@ with an API call. [Read more in the triggers documentation.](../triggers/README.md) +### pages + +`pages` is a special job that is used to upload static content to GitLab that +can be used to serve your website. It has a special syntax, so the two +requirements below must be met: + +1. Any static content must be placed under a `public/` directory +1. `artifacts` with a path to the `public/` directory must be defined + +The example below simply moves all files from the root of the project to the +`public/` directory. The `.public` workaround is so `cp` doesn't also copy +`public/` to itself in an infinite loop: + +``` +pages: + stage: deploy + script: + - mkdir .public + - cp -r * .public + - mv .public public + artifacts: + paths: + - public + only: + - master +``` + +Read more on [GitLab Pages user documentation](../../pages/README.md). + ## Validate the .gitlab-ci.yml Each instance of GitLab CI has an embedded debug tool called Lint. diff --git a/doc/pages/README.md b/doc/pages/README.md index 6f1d2d27554..c44d48e9cbf 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -64,7 +64,7 @@ In brief, this is what you need to upload your website in GitLab Pages: (ask your administrator). This is very important, so you should first make sure you get that right. 1. Create a project -1. Provide a specific job named [`pages`](../ci/yaml/README.md#pages) in +1. Provide a specific job named [`pages`][pages] in [`.gitlab-ci.yml`](../ci/yaml/README.md) 1. A GitLab Runner to build GitLab Pages @@ -112,7 +112,7 @@ access it under `http(s)://groupname.example.io`. To make use of GitLab Pages, your `.gitlab-ci.yml` must follow the rules below: -1. A special `pages` job must be defined +1. A special [`pages`][pages] job must be defined 1. Any static content must be placed under a `public/` directory 1. `artifacts` with a path to the `public/` directory must be defined @@ -222,3 +222,4 @@ to private, internal or public. [pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages [gitlab ci]: https://about.gitlab.com/gitlab-ci [gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner +[pages]: ../ci/yaml/README.md#pages -- cgit v1.2.1 From 5009acc04de0daf78472df5ec7061aac53bd6576 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 24 Feb 2016 16:28:08 +0200 Subject: Convert shared runners into a note --- doc/pages/README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index c44d48e9cbf..3cb560f0150 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -68,8 +68,9 @@ In brief, this is what you need to upload your website in GitLab Pages: [`.gitlab-ci.yml`](../ci/yaml/README.md) 1. A GitLab Runner to build GitLab Pages -If [shared runners](../ci/runners/README.md) are enabled by your GitLab -administrator, you should be able to use them instead of bringing your own. +> **Note:** +> If [shared runners](../ci/runners/README.md) are enabled by your GitLab +> administrator, you should be able to use them instead of bringing your own. ### GitLab pages per user -- cgit v1.2.1 From 69854d9786c5080c32a982f0dda5d29fe9f92f46 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 24 Feb 2016 18:14:28 +0200 Subject: Merge user and group pages --- doc/pages/README.md | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 3cb560f0150..78100fa452b 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -20,8 +20,7 @@ deploy static pages for your individual projects, your user or your group. - [Getting started with GitLab Pages](#getting-started-with-gitlab-pages) - [GitLab Pages requirements](#gitlab-pages-requirements) - - [GitLab pages per user](#gitlab-pages-per-user) - - [GitLab pages per group](#gitlab-pages-per-group) + - [GitLab pages per user or group](#gitlab-pages-per-user-or-group) - [GitLab pages per project](#gitlab-pages-per-project) - [Explore the contents of .gitlab-ci.yml](#explore-the-contents-of-gitlab-ci-yml) - [Remove the contents of your pages](#remove-the-contents-of-your-pages) @@ -72,7 +71,7 @@ In brief, this is what you need to upload your website in GitLab Pages: > If [shared runners](../ci/runners/README.md) are enabled by your GitLab > administrator, you should be able to use them instead of bringing your own. -### GitLab pages per user +### GitLab pages per user or group Head over your GitLab instance that supports GitLab Pages and create a repository named `username.example.io`, where `username` is your username on @@ -83,18 +82,13 @@ username, it won’t work, so make sure to get it right. --- -After you push some static content to your repository and GitLab Runner uploads -the artifacts to GitLab CI, you will be able to access your website under -`http(s)://username.example.io`. Keep reading to find out how. - -### GitLab pages per group - -To create a group page the steps are the same like when creating a website for +To create a group page, the steps are the same like when creating a website for users. Just make sure that you are creating the project within the group's namespace. -After you upload some static content to your repository, you will be able to -access it under `http(s)://groupname.example.io`. +After you push some static content to your repository and GitLab Runner uploads +the artifacts to GitLab CI, you will be able to access your website under +`http(s)://username.example.io`. Keep reading to find out how. ### GitLab pages per project -- cgit v1.2.1 From 7e7da5b2cbce6f4edab40f92d664fc4030314121 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 24 Feb 2016 18:14:50 +0200 Subject: Add table explaining the types of pages [ci skip] --- doc/pages/README.md | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 78100fa452b..e36b651b9b3 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -36,10 +36,14 @@ deploy static pages for your individual projects, your user or your group. ## Getting started with GitLab Pages +> **Note:** +> In the rest of this document we will assume that the general domain name that +> is used for GitLab Pages is `example.io`. + GitLab Pages rely heavily on GitLab CI and its ability to upload [artifacts](../ci/yaml/README.md#artifacts). -In general there are two kinds of pages one might create: +In general there are two types of pages one might create: - Pages per user/group (`username.example.io`) - Pages per project (`username.example.io/projectname`) @@ -47,9 +51,12 @@ In general there are two kinds of pages one might create: In GitLab, usernames and groupnames are unique and often people refer to them as namespaces. There can be only one namespace in a GitLab instance. -> **Note:** -> In the rest of this document we will assume that the general domain name that -> is used for GitLab Pages is `example.io`. +| Type of GitLab Pages | Project name | Website served under | +| -------------------- | --------------- | -------------------- | +| User pages | `username.example.io` | `http(s)://username.example.io` | +| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | +| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` | +| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`| > **Warning:** > There are some known [limitations](#limitations) regarding namespaces served -- cgit v1.2.1 From ea8ff3922cec940024b03d7b3f87d07e8a02695f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 24 Feb 2016 18:19:17 +0200 Subject: Rename user/project pages headings --- doc/pages/README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index e36b651b9b3..ea46003cb3b 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -20,8 +20,8 @@ deploy static pages for your individual projects, your user or your group. - [Getting started with GitLab Pages](#getting-started-with-gitlab-pages) - [GitLab Pages requirements](#gitlab-pages-requirements) - - [GitLab pages per user or group](#gitlab-pages-per-user-or-group) - - [GitLab pages per project](#gitlab-pages-per-project) + - [User or group Pages](#user-or-group-pages) + - [Project Pages](#project-pages) - [Explore the contents of .gitlab-ci.yml](#explore-the-contents-of-gitlab-ci-yml) - [Remove the contents of your pages](#remove-the-contents-of-your-pages) - [Next steps](#next-steps) @@ -78,7 +78,7 @@ In brief, this is what you need to upload your website in GitLab Pages: > If [shared runners](../ci/runners/README.md) are enabled by your GitLab > administrator, you should be able to use them instead of bringing your own. -### GitLab pages per user or group +### User or group Pages Head over your GitLab instance that supports GitLab Pages and create a repository named `username.example.io`, where `username` is your username on @@ -97,14 +97,13 @@ After you push some static content to your repository and GitLab Runner uploads the artifacts to GitLab CI, you will be able to access your website under `http(s)://username.example.io`. Keep reading to find out how. -### GitLab pages per project +### Project Pages > **Note:** > You do _not_ have to create a project named `username.example.io` in order to > serve a project's page. - ### Explore the contents of .gitlab-ci.yml > **Note:** -- cgit v1.2.1 From d1fe6a6f840ea07304d0da0b8a8477c7946dd1d3 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 25 Feb 2016 10:12:10 +0200 Subject: Mention the power of CI and that Pages support all static generators --- doc/pages/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/pages/README.md b/doc/pages/README.md index ea46003cb3b..dadce43f724 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -12,6 +12,12 @@ With GitLab Pages you can host for free your static websites on GitLab. Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can deploy static pages for your individual projects, your user or your group. +The key thing about GitLab Pages is the [`.gitlab-ci.yml`](../ci/yaml/README.md) +file, something that gives you absolute control over the build process. You can +actually watch your website being built live by following the CI build traces. + +GitLab Pages support any kind of [static site generator][staticgen]. + --- <!-- START doctoc generated TOC please keep comment here to allow auto update --> @@ -224,3 +230,4 @@ to private, internal or public. [gitlab ci]: https://about.gitlab.com/gitlab-ci [gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner [pages]: ../ci/yaml/README.md#pages +[staticgen]: https://www.staticgen.com/ -- cgit v1.2.1 From 758f5599bd0b4f4a624d1da247063d1384cd395f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 25 Feb 2016 10:13:26 +0200 Subject: Move Pages removal to Next steps section --- doc/pages/README.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index dadce43f724..f0d2ecb995d 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -29,12 +29,12 @@ GitLab Pages support any kind of [static site generator][staticgen]. - [User or group Pages](#user-or-group-pages) - [Project Pages](#project-pages) - [Explore the contents of .gitlab-ci.yml](#explore-the-contents-of-gitlab-ci-yml) - - [Remove the contents of your pages](#remove-the-contents-of-your-pages) - [Next steps](#next-steps) - [Adding a custom domain to your Pages website](#adding-a-custom-domain-to-your-pages-website) - [Securing your custom domain website with TLS](#securing-your-custom-domain-website-with-tls) - [Example projects](#example-projects) - [Custom error codes pages](#custom-error-codes-pages) + - [Remove the contents of your pages](#remove-the-contents-of-your-pages) - [Limitations](#limitations) - [Frequently Asked Questions](#frequently-asked-questions) @@ -197,6 +197,13 @@ You can provide your own 403 and 404 error pages by creating the `403.html` and `404.html` files respectively in the `public/` directory that will be included in the artifacts. +### Remove the contents of your pages + +If you ever feel the need to purge your Pages content, you can do so by going +to your project's **Settings > Pages** and hit **Remove pages**. Simple as that. + +![Remove pages](img/pages_remove.png) + ## Limitations -- cgit v1.2.1 From fa374abd9830fc2f21ee740c21f49d72179e9602 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 25 Feb 2016 10:14:13 +0200 Subject: Add note to use gitlab.io when using GitLab.com --- doc/pages/README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index f0d2ecb995d..ccad79fb026 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -44,10 +44,8 @@ GitLab Pages support any kind of [static site generator][staticgen]. > **Note:** > In the rest of this document we will assume that the general domain name that -> is used for GitLab Pages is `example.io`. - -GitLab Pages rely heavily on GitLab CI and its ability to upload -[artifacts](../ci/yaml/README.md#artifacts). +> is used for GitLab Pages is `example.io`. If you are using GitLab.com to +> host your website, replace `example.io` with `gitlab.io`. In general there are two types of pages one might create: -- cgit v1.2.1 From 3d91145cd0db52000eab67df7db0afec705b7f40 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 25 Feb 2016 10:16:16 +0200 Subject: Be more precise who people are --- doc/pages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index ccad79fb026..db445f10088 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -52,7 +52,7 @@ In general there are two types of pages one might create: - Pages per user/group (`username.example.io`) - Pages per project (`username.example.io/projectname`) -In GitLab, usernames and groupnames are unique and often people refer to them +In GitLab, usernames and groupnames are unique and we often refer to them as namespaces. There can be only one namespace in a GitLab instance. | Type of GitLab Pages | Project name | Website served under | -- cgit v1.2.1 From a3aef3551a7d35241da51ec59060e83434da982b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 25 Feb 2016 10:16:44 +0200 Subject: Be more clear what project name is referred to --- doc/pages/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index db445f10088..a5c9b9c23c3 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -55,8 +55,8 @@ In general there are two types of pages one might create: In GitLab, usernames and groupnames are unique and we often refer to them as namespaces. There can be only one namespace in a GitLab instance. -| Type of GitLab Pages | Project name | Website served under | -| -------------------- | --------------- | -------------------- | +| Type of GitLab Pages | Project name created in GitLab | Website URL | +| -------------------- | ------------ | ----------- | | User pages | `username.example.io` | `http(s)://username.example.io` | | Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | | Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` | -- cgit v1.2.1 From 18478e5d4fedf80ce8b62acf3c1a765b3eaa127d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Thu, 25 Feb 2016 10:19:09 +0200 Subject: Reword --- doc/pages/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index a5c9b9c23c3..d6f8d28fa7c 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -74,8 +74,8 @@ In brief, this is what you need to upload your website in GitLab Pages: (ask your administrator). This is very important, so you should first make sure you get that right. 1. Create a project -1. Provide a specific job named [`pages`][pages] in - [`.gitlab-ci.yml`](../ci/yaml/README.md) +1. Push a [`.gitlab-ci.yml`](../ci/yaml/README.md) file in your repository with + a specific job named [`pages`][pages] 1. A GitLab Runner to build GitLab Pages > **Note:** -- cgit v1.2.1 From e7b2784c956a7b6698ec23d6c5a62f4a83bc2fbd Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 4 Mar 2016 16:09:25 +0200 Subject: Add new sections and clean-up - Finish user/group/project sections - Give more .gitlab-ci.yml examples [ci skip] --- doc/pages/README.md | 221 ++++++++++++++++++++++-------- doc/pages/img/create_user_page.png | Bin 66593 -> 0 bytes doc/pages/img/pages_create_project.png | Bin 0 -> 33597 bytes doc/pages/img/pages_create_user_page.png | Bin 0 -> 87071 bytes doc/pages/img/pages_new_domain_button.png | Bin 0 -> 51136 bytes doc/pages/img/pages_remove.png | Bin 19232 -> 27259 bytes 6 files changed, 165 insertions(+), 56 deletions(-) delete mode 100644 doc/pages/img/create_user_page.png create mode 100644 doc/pages/img/pages_create_project.png create mode 100644 doc/pages/img/pages_create_user_page.png create mode 100644 doc/pages/img/pages_new_domain_button.png diff --git a/doc/pages/README.md b/doc/pages/README.md index d6f8d28fa7c..8ca80b2f0bc 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -12,12 +12,6 @@ With GitLab Pages you can host for free your static websites on GitLab. Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can deploy static pages for your individual projects, your user or your group. -The key thing about GitLab Pages is the [`.gitlab-ci.yml`](../ci/yaml/README.md) -file, something that gives you absolute control over the build process. You can -actually watch your website being built live by following the CI build traces. - -GitLab Pages support any kind of [static site generator][staticgen]. - --- <!-- START doctoc generated TOC please keep comment here to allow auto update --> @@ -28,15 +22,22 @@ GitLab Pages support any kind of [static site generator][staticgen]. - [GitLab Pages requirements](#gitlab-pages-requirements) - [User or group Pages](#user-or-group-pages) - [Project Pages](#project-pages) - - [Explore the contents of .gitlab-ci.yml](#explore-the-contents-of-gitlab-ci-yml) + - [Explore the contents of `.gitlab-ci.yml`](#explore-the-contents-of-gitlab-ciyml) + - [How `.gitlab-ci.yml` looks like when using plain HTML files](#how-gitlab-ciyml-looks-like-when-using-plain-html-files) + - [How `.gitlab-ci.yml` looks like when using a static generator](#how-gitlab-ciyml-looks-like-when-using-a-static-generator) + - [How to set up GitLab Pages in a repository where there's also actual code](#how-to-set-up-gitlab-pages-in-a-repository-where-there-s-also-actual-code) - [Next steps](#next-steps) - - [Adding a custom domain to your Pages website](#adding-a-custom-domain-to-your-pages-website) - - [Securing your custom domain website with TLS](#securing-your-custom-domain-website-with-tls) - - [Example projects](#example-projects) + - [Add a custom domain to your Pages website](#add-a-custom-domain-to-your-pages-website) + - [Secure your custom domain website with TLS](#secure-your-custom-domain-website-with-tls) + - [Use a static generator to develop your website](#use-a-static-generator-to-develop-your-website) + - [Example projects](#example-projects) - [Custom error codes pages](#custom-error-codes-pages) - [Remove the contents of your pages](#remove-the-contents-of-your-pages) - [Limitations](#limitations) - [Frequently Asked Questions](#frequently-asked-questions) + - [Can I download my generated pages?](#can-i-download-my-generated-pages) + - [Can I use GitLab Pages if my project is private?](#can-i-use-gitlab-pages-if-my-project-is-private) + - [Q: Do I have to create a project named `username.example.io` in order to host a project website?](#q-do-i-have-to-create-a-project-named-username-example-io-in-order-to-host-a-project-website) <!-- END doctoc generated TOC please keep comment here to allow auto update --> @@ -49,13 +50,16 @@ GitLab Pages support any kind of [static site generator][staticgen]. In general there are two types of pages one might create: -- Pages per user/group (`username.example.io`) -- Pages per project (`username.example.io/projectname`) +- Pages per user (`username.example.io`) or per group (`groupname.example.io`) +- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`) In GitLab, usernames and groupnames are unique and we often refer to them -as namespaces. There can be only one namespace in a GitLab instance. +as namespaces. There can be only one namespace in a GitLab instance. Below you +can see the connection between the type of GitLab Pages, what the project name +that is created on GitLab looks like and the website URL it will be ultimately +be served on. -| Type of GitLab Pages | Project name created in GitLab | Website URL | +| Type of GitLab Pages | The name of the project created in GitLab | Website URL | | -------------------- | ------------ | ----------- | | User pages | `username.example.io` | `http(s)://username.example.io` | | Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | @@ -74,9 +78,9 @@ In brief, this is what you need to upload your website in GitLab Pages: (ask your administrator). This is very important, so you should first make sure you get that right. 1. Create a project -1. Push a [`.gitlab-ci.yml`](../ci/yaml/README.md) file in your repository with - a specific job named [`pages`][pages] -1. A GitLab Runner to build GitLab Pages +1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory + of your repository with a specific job named [`pages`][pages]. +1. Set up a GitLab Runner to build your website > **Note:** > If [shared runners](../ci/runners/README.md) are enabled by your GitLab @@ -84,63 +88,91 @@ In brief, this is what you need to upload your website in GitLab Pages: ### User or group Pages +For user and group pages, the name of the project should be specific to the +username or groupname and the general domain name that is used for GitLab Pages. Head over your GitLab instance that supports GitLab Pages and create a repository named `username.example.io`, where `username` is your username on GitLab. If the first part of the project name doesn't match exactly your username, it won’t work, so make sure to get it right. -![Create a user-based pages repository](img/create_user_page.png) - ---- - To create a group page, the steps are the same like when creating a website for users. Just make sure that you are creating the project within the group's namespace. +![Create a user-based pages project](img/pages_create_user_page.png) + +--- + After you push some static content to your repository and GitLab Runner uploads the artifacts to GitLab CI, you will be able to access your website under `http(s)://username.example.io`. Keep reading to find out how. +>**Note:** +If your username/groupname contains a dot, for example `foo.bar`, you will not +be able to use the wildcard domain HTTPS, read more at [limitations](#limitations). + ### Project Pages -> **Note:** -> You do _not_ have to create a project named `username.example.io` in order to -> serve a project's page. +GitLab Pages for projects can be created by both user and group accounts. +The steps to create a project page for a user or a group are identical: + +1. Create a new project +1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory + of your repository with a specific job named [`pages`][pages]. +1. Set up a GitLab Runner to build your website + +A user's project will be served under `http(s)://username.example.io/projectname` +whereas a group's project under `http(s)://groupname.example.io/projectname`. +### Explore the contents of `.gitlab-ci.yml` -### Explore the contents of .gitlab-ci.yml +The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that +gives you absolute control over the build process. You can actually watch your +website being built live by following the CI build traces. > **Note:** > Before reading this section, make sure you familiarize yourself with GitLab CI > and the specific syntax of[`.gitlab-ci.yml`](../ci/yaml/README.md) by > following our [quick start guide](../ci/quick_start/README.md). -To make use of GitLab Pages, your `.gitlab-ci.yml` must follow the rules below: +To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the +rules below: -1. A special [`pages`][pages] job must be defined -1. Any static content must be placed under a `public/` directory +1. A special job named [`pages`][pages] must be defined +1. Any static content which will be served by GitLab Pages must be placed under + a `public/` directory 1. `artifacts` with a path to the `public/` directory must be defined +In its simplest form, `.gitlab-ci.yml` looks like: + +```yaml +pages: + script: + - my_commands + artifacts: + paths: + - public +``` + +When the Runner reaches to build the `pages` job, it executes whatever is +defined in the `script` parameter and if the build completes with a non-zero +exit status, it then uploads the `public/` directory to GitLab Pages. + +The `public/` directory should contain all the static content of your website. +Depending on how you plan to publish your website, the steps defined in the +[`script` parameter](../ci/yaml/README.md#script) may differ. + Be aware that Pages are by default branch/tag agnostic and their deployment relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the `pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except), whenever a new commit is pushed to whatever branch or tag, the Pages will be -overwritten. In the examples below, we limit the Pages to be deployed whenever -a commit is pushed only on the `master` branch, which is advisable to do so. - -The pages are created after the build completes successfully and the artifacts -for the `pages` job are uploaded to GitLab. - -The example below uses [Jekyll][] and generates the created HTML files -under the `public/` directory. +overwritten. In the example below, we limit the Pages to be deployed whenever +a commit is pushed only on the `master` branch: ```yaml -image: ruby:2.1 - pages: script: - - gem install jekyll - - jekyll build -d public/ + - my_commands artifacts: paths: - public @@ -148,14 +180,28 @@ pages: - master ``` -The example below doesn't use any static site generator, but simply moves all -files from the root of the project to the `public/` directory. The `.public` -workaround is so `cp` doesn't also copy `public/` to itself in an infinite -loop. +We then tell the Runner to treat the `public/` directory as `artifacts` and +upload it to GitLab. And since all these parameters were all under a `pages` +job, the contents of the `public` directory will be served by GitLab Pages. + +#### How `.gitlab-ci.yml` looks like when the static content is in your repository + +Supposedly your repository contained the following files: + +``` +├── index.html +├── css +│   └── main.css +└── js + └── main.js +``` + +Then the `.gitlab-ci.yml` example below simply moves all files from the root +directory of the project to the `public/` directory. The `.public` workaround +is so `cp` doesn't also copy `public/` to itself in an infinite loop: ```yaml pages: - stage: deploy script: - mkdir .public - cp -r * .public @@ -167,27 +213,84 @@ pages: - master ``` -### Remove the contents of your pages +### How `.gitlab-ci.yml` looks like when using a static generator -Pages can be explicitly removed from a project by clicking **Remove Pages** -in your project's **Settings > Pages**. +In general, GitLab Pages support any kind of [static site generator][staticgen], +since the Runner can be configured to run any possible command. -![Remove pages](img/pages_remove.png) +In the root directory of your Git repository, place the source files of your +favorite static generator. Then provide a `.gitlab-ci.yml` file which is +specific to your static generator. + +The example below, uses [Jekyll] to build the static site: + +```yaml +pages: + images: jekyll/jekyll:latest + script: + - jekyll build -d public/ + artifacts: + paths: + - public + only: + - master +``` + +Here, we used the Docker executor and in the first line we specified the base +image against which our builds will run. + +You have to make sure that the generated static files are ultimately placed +under the `public` directory, that's why in the `script` section we run the +`jekyll` command that builds the website and puts all content in the `public/` +directory. + +We then tell the Runner to treat the `public/` directory as `artifacts` and +upload it to GitLab. + +--- + +See the [jekyll example project][pages-jekyll] to better understand how this +works. + +For a list of Pages projects, see [example projects](#example-projects) to get +you started. + +#### How to set up GitLab Pages in a repository where there's also actual code + +You can have your project's code in the `master` branch and use an orphan +`pages` branch that will host your static generator site. ## Next steps -### Adding a custom domain to your Pages website +### Add a custom domain to your Pages website +If this setting is enabled by your GitLab administrator, you should be able to +see the **New Domain** button when visiting your project's **Settings > Pages**. -### Securing your custom domain website with TLS +![New domain button](img/pages_new_domain_button.png) -### Example projects +--- + +You are not limited to one domain per can add multiple domains pointing to your +website hosted under GitLab. + +### Secure your custom domain website with TLS + +### Use a static generator to develop your website + +#### Example projects Below is a list of example projects for GitLab Pages with a plain HTML website or various static site generators. Contributions are very welcome. - [Plain HTML](https://gitlab.com/gitlab-examples/pages-plain-html) - [Jekyll](https://gitlab.com/gitlab-examples/pages-jekyll) +- [Hugo](https://gitlab.com/gitlab-examples/pages-hugo) +- [Middleman](https://gitlab.com/gitlab-examples/pages-middleman) +- [Hexo](https://gitlab.com/gitlab-examples/pages-hexo) +- [Brunch](https://gitlab.com/gitlab-examples/pages-brunch) +- [Metalsmith](https://gitlab.com/gitlab-examples/pages-metalsmith) +- [Harp](https://gitlab.com/gitlab-examples/pages-harp) ### Custom error codes pages @@ -216,16 +319,21 @@ don't redirect HTTP to HTTPS. ## Frequently Asked Questions -**Q: Can I download my generated pages?** +### Can I download my generated pages? Sure. All you need to do is download the artifacts archive from the build page. +### Can I use GitLab Pages if my project is private? -**Q: Can I use GitLab Pages if my project is private?** - -Yes. GitLab Pages doesn't care whether you set your project's visibility level +Yes. GitLab Pages don't care whether you set your project's visibility level to private, internal or public. +### Q: Do I have to create a project named `username.example.io` in order to host a project website? + +No. You can create a new project named `foo` and have it served under +`http(s)://username.example.io/foo` without having previously created a +user page. + --- [jekyll]: http://jekyllrb.com/ @@ -236,3 +344,4 @@ to private, internal or public. [gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner [pages]: ../ci/yaml/README.md#pages [staticgen]: https://www.staticgen.com/ +[pages-jekyll]: https://gitlab.com/gitlab-examples/pages-jekyll diff --git a/doc/pages/img/create_user_page.png b/doc/pages/img/create_user_page.png deleted file mode 100644 index 8c2b67e233a..00000000000 Binary files a/doc/pages/img/create_user_page.png and /dev/null differ diff --git a/doc/pages/img/pages_create_project.png b/doc/pages/img/pages_create_project.png new file mode 100644 index 00000000000..a936d8e5dbd Binary files /dev/null and b/doc/pages/img/pages_create_project.png differ diff --git a/doc/pages/img/pages_create_user_page.png b/doc/pages/img/pages_create_user_page.png new file mode 100644 index 00000000000..3f615d3757d Binary files /dev/null and b/doc/pages/img/pages_create_user_page.png differ diff --git a/doc/pages/img/pages_new_domain_button.png b/doc/pages/img/pages_new_domain_button.png new file mode 100644 index 00000000000..c3640133bb2 Binary files /dev/null and b/doc/pages/img/pages_new_domain_button.png differ diff --git a/doc/pages/img/pages_remove.png b/doc/pages/img/pages_remove.png index 36141bd4d12..adbfb654877 100644 Binary files a/doc/pages/img/pages_remove.png and b/doc/pages/img/pages_remove.png differ -- cgit v1.2.1 From 52dcde272a7766dd22a045fe7ca94e3690a9590e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 4 Mar 2016 16:56:05 +0200 Subject: Move examples to own section --- doc/pages/README.md | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 8ca80b2f0bc..f614781e2ba 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -23,14 +23,13 @@ deploy static pages for your individual projects, your user or your group. - [User or group Pages](#user-or-group-pages) - [Project Pages](#project-pages) - [Explore the contents of `.gitlab-ci.yml`](#explore-the-contents-of-gitlab-ciyml) - - [How `.gitlab-ci.yml` looks like when using plain HTML files](#how-gitlab-ciyml-looks-like-when-using-plain-html-files) - - [How `.gitlab-ci.yml` looks like when using a static generator](#how-gitlab-ciyml-looks-like-when-using-a-static-generator) + - [How `.gitlab-ci.yml` looks like when the static content is in your repository](#how-gitlab-ciyml-looks-like-when-the-static-content-is-in-your-repository) + - [How `.gitlab-ci.yml` looks like when using a static generator](#how-gitlab-ciyml-looks-like-when-using-a-static-generator) - [How to set up GitLab Pages in a repository where there's also actual code](#how-to-set-up-gitlab-pages-in-a-repository-where-there-s-also-actual-code) - [Next steps](#next-steps) + - [Example projects](#example-projects) - [Add a custom domain to your Pages website](#add-a-custom-domain-to-your-pages-website) - [Secure your custom domain website with TLS](#secure-your-custom-domain-website-with-tls) - - [Use a static generator to develop your website](#use-a-static-generator-to-develop-your-website) - - [Example projects](#example-projects) - [Custom error codes pages](#custom-error-codes-pages) - [Remove the contents of your pages](#remove-the-contents-of-your-pages) - [Limitations](#limitations) @@ -262,6 +261,20 @@ You can have your project's code in the `master` branch and use an orphan ## Next steps +### Example projects + +Below is a list of example projects for GitLab Pages with a plain HTML website +or various static site generators. Contributions are very welcome. + +- [Plain HTML](https://gitlab.com/gitlab-examples/pages-plain-html) +- [Jekyll](https://gitlab.com/gitlab-examples/pages-jekyll) +- [Hugo](https://gitlab.com/gitlab-examples/pages-hugo) +- [Middleman](https://gitlab.com/gitlab-examples/pages-middleman) +- [Hexo](https://gitlab.com/gitlab-examples/pages-hexo) +- [Brunch](https://gitlab.com/gitlab-examples/pages-brunch) +- [Metalsmith](https://gitlab.com/gitlab-examples/pages-metalsmith) +- [Harp](https://gitlab.com/gitlab-examples/pages-harp) + ### Add a custom domain to your Pages website If this setting is enabled by your GitLab administrator, you should be able to @@ -305,7 +318,6 @@ to your project's **Settings > Pages** and hit **Remove pages**. Simple as that. ![Remove pages](img/pages_remove.png) - ## Limitations When using Pages under the general domain of a GitLab instance (`*.example.io`), -- cgit v1.2.1 From 50d32d6cc6cf8643c1676f6241a125623a5af06e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 4 Mar 2016 16:56:23 +0200 Subject: Add examples to 404 pages [ci skip] --- doc/pages/README.md | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index f614781e2ba..dc425f6a564 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -289,27 +289,22 @@ website hosted under GitLab. ### Secure your custom domain website with TLS -### Use a static generator to develop your website - -#### Example projects - -Below is a list of example projects for GitLab Pages with a plain HTML website -or various static site generators. Contributions are very welcome. - -- [Plain HTML](https://gitlab.com/gitlab-examples/pages-plain-html) -- [Jekyll](https://gitlab.com/gitlab-examples/pages-jekyll) -- [Hugo](https://gitlab.com/gitlab-examples/pages-hugo) -- [Middleman](https://gitlab.com/gitlab-examples/pages-middleman) -- [Hexo](https://gitlab.com/gitlab-examples/pages-hexo) -- [Brunch](https://gitlab.com/gitlab-examples/pages-brunch) -- [Metalsmith](https://gitlab.com/gitlab-examples/pages-metalsmith) -- [Harp](https://gitlab.com/gitlab-examples/pages-harp) ### Custom error codes pages You can provide your own 403 and 404 error pages by creating the `403.html` and -`404.html` files respectively in the `public/` directory that will be included -in the artifacts. +`404.html` files respectively in the root directory of the `public/` directory +that will be included in the artifacts. + +If the case of `404.html`, there are different scenarios. For example: + +- If you use project Pages (served under `/projectname/`) and try to access + `/projectname/non/exsiting_file`, GitLab Pages will try to serve first + `/projectname/404.html`, and then `/404.html`. +- If you use user/group Pages (served under `/`) and try to access + `/non/existing_file` GitLab Pages will try to serve `/404.html`. +- If you use a custom domain and try to access `/non/existing_file`, GitLab + Pages will try to server only `/404.html`. ### Remove the contents of your pages -- cgit v1.2.1 From 231e3a2b13ef4b4cae59c1c3828a8a4c0dd28239 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 4 Mar 2016 18:07:00 +0200 Subject: Add example of hosting Pages in a specific branch [ci skip] --- doc/pages/README.md | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index dc425f6a564..effa2a35938 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -256,8 +256,46 @@ you started. #### How to set up GitLab Pages in a repository where there's also actual code -You can have your project's code in the `master` branch and use an orphan -`pages` branch that will host your static generator site. +Remember that GitLab Pages are by default branch/tag agnostic and their +deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit +the `pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except), +whenever a new commit is pushed to a branch that will be used specifically for +your pages. + +That way, you can have your project's code in the `master` branch and use an +orphan branch (let's name it `pages`) that will host your static generator site. + +You can create a new empty branch like this: + +```bash +git checkout --orphan pages +``` + +The first commit made on this new branch will have no parents and it will be +the root of a new history totally disconnected from all the other branches and +commits. Push the source files of your static generator in the `pages` branch. + +Below is a copy of `.gitlab-ci.yml` where the most significant line is the last +one, specifying to execute everything in the `pages` branch: + +``` +pages: + images: jekyll/jekyll:latest + script: + - jekyll build -d public/ + artifacts: + paths: + - public + only: + - pages +``` + +See an example that has different files in the [`master` branch][jekyll-master] +and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which +also includes `.gitlab-ci.yml`. + +[jekyll-master]: https://gitlab.com/gitlab-examples/pages-jekyll-branched/tree/master +[jekyll-pages]: https://gitlab.com/gitlab-examples/pages-jekyll-branched/tree/pages ## Next steps -- cgit v1.2.1 From 8442e3f05dfd822630934f6d3f7b4c478b78e0f5 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 4 Mar 2016 18:43:21 +0200 Subject: Add sections on custom domains [ci skip] --- doc/pages/README.md | 30 ++++++++++++++++++++++++++---- doc/pages/img/pages_dns_details.png | Bin 0 -> 34686 bytes doc/pages/img/pages_multiple_domains.png | Bin 0 -> 63716 bytes doc/pages/img/pages_upload_cert.png | Bin 0 -> 103730 bytes 4 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 doc/pages/img/pages_dns_details.png create mode 100644 doc/pages/img/pages_multiple_domains.png create mode 100644 doc/pages/img/pages_upload_cert.png diff --git a/doc/pages/README.md b/doc/pages/README.md index effa2a35938..080570cdaff 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -24,7 +24,7 @@ deploy static pages for your individual projects, your user or your group. - [Project Pages](#project-pages) - [Explore the contents of `.gitlab-ci.yml`](#explore-the-contents-of-gitlab-ciyml) - [How `.gitlab-ci.yml` looks like when the static content is in your repository](#how-gitlab-ciyml-looks-like-when-the-static-content-is-in-your-repository) - - [How `.gitlab-ci.yml` looks like when using a static generator](#how-gitlab-ciyml-looks-like-when-using-a-static-generator) + - [How `.gitlab-ci.yml` looks like when using a static generator](#how-gitlab-ciyml-looks-like-when-using-a-static-generator) - [How to set up GitLab Pages in a repository where there's also actual code](#how-to-set-up-gitlab-pages-in-a-repository-where-there-s-also-actual-code) - [Next steps](#next-steps) - [Example projects](#example-projects) @@ -212,7 +212,7 @@ pages: - master ``` -### How `.gitlab-ci.yml` looks like when using a static generator +#### How `.gitlab-ci.yml` looks like when using a static generator In general, GitLab Pages support any kind of [static site generator][staticgen], since the Runner can be configured to run any possible command. @@ -299,6 +299,9 @@ also includes `.gitlab-ci.yml`. ## Next steps +So you have successfully deployed your website, congratulations! Let's check +what more you can do with GitLab Pages. + ### Example projects Below is a list of example projects for GitLab Pages with a plain HTML website @@ -313,6 +316,9 @@ or various static site generators. Contributions are very welcome. - [Metalsmith](https://gitlab.com/gitlab-examples/pages-metalsmith) - [Harp](https://gitlab.com/gitlab-examples/pages-harp) +Visit the gitlab-examples group for a full list of projects: +<https://gitlab.com/groups/gitlab-examples>. + ### Add a custom domain to your Pages website If this setting is enabled by your GitLab administrator, you should be able to @@ -322,11 +328,27 @@ see the **New Domain** button when visiting your project's **Settings > Pages**. --- -You are not limited to one domain per can add multiple domains pointing to your -website hosted under GitLab. +You can add multiple domains pointing to your website hosted under GitLab. +Once the domain is added, you can see it listed under the **Domains** section. + +![Pages multiple domains](img/pages_multiple_domains.png) + +--- + +As a last step, you need to configure your DNS and add a CNAME pointing to your +user/group page. Click on the **Details** button of a domain for further +instructions. + +![Pages DNS details](img/pages_dns_details.png) ### Secure your custom domain website with TLS +When you add a new custom domain, you also have the chance to add a TLS +certificate. If this setting is enabled by your GitLab administrator, you +should be able to see the option to upload the public certificate and the +private key when adding a new domain. + +![Pages upload cert](img/pages_upload_cert.png) ### Custom error codes pages diff --git a/doc/pages/img/pages_dns_details.png b/doc/pages/img/pages_dns_details.png new file mode 100644 index 00000000000..8d34f3b7f38 Binary files /dev/null and b/doc/pages/img/pages_dns_details.png differ diff --git a/doc/pages/img/pages_multiple_domains.png b/doc/pages/img/pages_multiple_domains.png new file mode 100644 index 00000000000..2bc7cee07a6 Binary files /dev/null and b/doc/pages/img/pages_multiple_domains.png differ diff --git a/doc/pages/img/pages_upload_cert.png b/doc/pages/img/pages_upload_cert.png new file mode 100644 index 00000000000..06d85ab1971 Binary files /dev/null and b/doc/pages/img/pages_upload_cert.png differ -- cgit v1.2.1 From aaebb21625575675dc80c185598b5d86776e79ef Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 7 Mar 2016 11:47:01 +0200 Subject: Clarify where 403 and 404 pages should exist --- doc/pages/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 080570cdaff..d9d38c1aa92 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -354,7 +354,9 @@ private key when adding a new domain. You can provide your own 403 and 404 error pages by creating the `403.html` and `404.html` files respectively in the root directory of the `public/` directory -that will be included in the artifacts. +that will be included in the artifacts. Usually this is the root directory of +your project, but that may differ depending on your static generator +configuration. If the case of `404.html`, there are different scenarios. For example: -- cgit v1.2.1 From 0255a85810f09fe6290c8e44a375d89eddd60ac7 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 7 Mar 2016 12:00:40 +0200 Subject: Use the default ruby:2.1 image --- doc/pages/README.md | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index d9d38c1aa92..cf4e45e17dd 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -224,15 +224,17 @@ specific to your static generator. The example below, uses [Jekyll] to build the static site: ```yaml -pages: - images: jekyll/jekyll:latest +image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1 + +pages: # the build job must be named pages script: - - jekyll build -d public/ + - gem install jekyll # we install jekyll + - jekyll build -d public/ # we tell jekyll to build the site for us artifacts: paths: - - public + - public # this is where the site will live and the Runner uploads it in GitLab only: - - master + - master # this script is only affecting the master branch ``` Here, we used the Docker executor and in the first line we specified the base @@ -241,7 +243,11 @@ image against which our builds will run. You have to make sure that the generated static files are ultimately placed under the `public` directory, that's why in the `script` section we run the `jekyll` command that builds the website and puts all content in the `public/` -directory. +directory. Depending on the static generator of your choice, this command will +differ. Search in the documentation of the static generator you will use if +there is an option to explicitly set the output directory. If there is not +such an option, you can always add one more line under `script` to rename the +resulting directory in `public/`. We then tell the Runner to treat the `public/` directory as `artifacts` and upload it to GitLab. @@ -251,8 +257,8 @@ upload it to GitLab. See the [jekyll example project][pages-jekyll] to better understand how this works. -For a list of Pages projects, see [example projects](#example-projects) to get -you started. +For a list of Pages projects, see the [example projects](#example-projects) to +get you started. #### How to set up GitLab Pages in a repository where there's also actual code @@ -279,9 +285,11 @@ Below is a copy of `.gitlab-ci.yml` where the most significant line is the last one, specifying to execute everything in the `pages` branch: ``` +image: ruby:2.1 + pages: - images: jekyll/jekyll:latest script: + - gem install jekyll - jekyll build -d public/ artifacts: paths: -- cgit v1.2.1 From a77a101d312910175dff3d8ebe8ecd1d8da357ee Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 7 Mar 2016 12:35:10 +0200 Subject: Add separate section for GitLab.com --- doc/pages/README.md | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index cf4e45e17dd..8f4c0bc8081 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -12,6 +12,9 @@ With GitLab Pages you can host for free your static websites on GitLab. Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can deploy static pages for your individual projects, your user or your group. +Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) if you are +using GitLab.com to host your website. + --- <!-- START doctoc generated TOC please keep comment here to allow auto update --> @@ -25,18 +28,19 @@ deploy static pages for your individual projects, your user or your group. - [Explore the contents of `.gitlab-ci.yml`](#explore-the-contents-of-gitlab-ciyml) - [How `.gitlab-ci.yml` looks like when the static content is in your repository](#how-gitlab-ciyml-looks-like-when-the-static-content-is-in-your-repository) - [How `.gitlab-ci.yml` looks like when using a static generator](#how-gitlab-ciyml-looks-like-when-using-a-static-generator) - - [How to set up GitLab Pages in a repository where there's also actual code](#how-to-set-up-gitlab-pages-in-a-repository-where-there-s-also-actual-code) + - [How to set up GitLab Pages in a repository where there's also actual code](#how-to-set-up-gitlab-pages-in-a-repository-where-theres-also-actual-code) - [Next steps](#next-steps) - [Example projects](#example-projects) - [Add a custom domain to your Pages website](#add-a-custom-domain-to-your-pages-website) - [Secure your custom domain website with TLS](#secure-your-custom-domain-website-with-tls) - [Custom error codes pages](#custom-error-codes-pages) - [Remove the contents of your pages](#remove-the-contents-of-your-pages) +- [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlabcom) - [Limitations](#limitations) - [Frequently Asked Questions](#frequently-asked-questions) - [Can I download my generated pages?](#can-i-download-my-generated-pages) - [Can I use GitLab Pages if my project is private?](#can-i-use-gitlab-pages-if-my-project-is-private) - - [Q: Do I have to create a project named `username.example.io` in order to host a project website?](#q-do-i-have-to-create-a-project-named-username-example-io-in-order-to-host-a-project-website) + - [Do I have to create a project named `username.example.io` in order to host a project website?](#do-i-have-to-create-a-project-named-usernameexampleio-in-order-to-host-a-project-website) <!-- END doctoc generated TOC please keep comment here to allow auto update --> @@ -44,8 +48,7 @@ deploy static pages for your individual projects, your user or your group. > **Note:** > In the rest of this document we will assume that the general domain name that -> is used for GitLab Pages is `example.io`. If you are using GitLab.com to -> host your website, replace `example.io` with `gitlab.io`. +> is used for GitLab Pages is `example.io`. In general there are two types of pages one might create: @@ -78,7 +81,7 @@ In brief, this is what you need to upload your website in GitLab Pages: sure you get that right. 1. Create a project 1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory - of your repository with a specific job named [`pages`][pages]. + of your repository with a specific job named [`pages`][pages] 1. Set up a GitLab Runner to build your website > **Note:** @@ -383,6 +386,17 @@ to your project's **Settings > Pages** and hit **Remove pages**. Simple as that. ![Remove pages](img/pages_remove.png) +## GitLab Pages on GitLab.com + +If you are using GitLab.com to host your website, then: + +- The general domain name for GitLab Pages on GitLab.com is `gitlab.io` +- Shared runners are provided for free and can be used to build your website + if you cannot or don't want to set up your own Runner +- Custom domains and TLS support are enabled + +The rest of the guide still applies. + ## Limitations When using Pages under the general domain of a GitLab instance (`*.example.io`), @@ -405,7 +419,7 @@ Sure. All you need to do is download the artifacts archive from the build page. Yes. GitLab Pages don't care whether you set your project's visibility level to private, internal or public. -### Q: Do I have to create a project named `username.example.io` in order to host a project website? +### Do I have to create a project named `username.example.io` in order to host a project website? No. You can create a new project named `foo` and have it served under `http(s)://username.example.io/foo` without having previously created a -- cgit v1.2.1 From 552e8993496fc57d15c5b712806aa87ed974fc6e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 7 Mar 2016 12:36:08 +0200 Subject: Clarification --- doc/pages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 8f4c0bc8081..682359ad266 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -218,7 +218,7 @@ pages: #### How `.gitlab-ci.yml` looks like when using a static generator In general, GitLab Pages support any kind of [static site generator][staticgen], -since the Runner can be configured to run any possible command. +since `.gitlab-ci.yml` can be configured to run any possible command. In the root directory of your Git repository, place the source files of your favorite static generator. Then provide a `.gitlab-ci.yml` file which is -- cgit v1.2.1 From 1fe13d63cded87f06b7ab3edf77945b15d191b91 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 7 Mar 2016 12:46:35 +0200 Subject: Remove confusing FAQ question [ci skip] --- doc/pages/README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 682359ad266..e9234e14a17 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -40,7 +40,6 @@ using GitLab.com to host your website. - [Frequently Asked Questions](#frequently-asked-questions) - [Can I download my generated pages?](#can-i-download-my-generated-pages) - [Can I use GitLab Pages if my project is private?](#can-i-use-gitlab-pages-if-my-project-is-private) - - [Do I have to create a project named `username.example.io` in order to host a project website?](#do-i-have-to-create-a-project-named-usernameexampleio-in-order-to-host-a-project-website) <!-- END doctoc generated TOC please keep comment here to allow auto update --> @@ -419,12 +418,6 @@ Sure. All you need to do is download the artifacts archive from the build page. Yes. GitLab Pages don't care whether you set your project's visibility level to private, internal or public. -### Do I have to create a project named `username.example.io` in order to host a project website? - -No. You can create a new project named `foo` and have it served under -`http(s)://username.example.io/foo` without having previously created a -user page. - --- [jekyll]: http://jekyllrb.com/ -- cgit v1.2.1 From d557b3fec51eaa560d4ccbafe344a4ab4b7bb5dd Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 7 Mar 2016 13:00:40 +0200 Subject: Fix markdown anchor link [ci skip] --- doc/pages/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index e9234e14a17..fa31a3ec560 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -12,8 +12,8 @@ With GitLab Pages you can host for free your static websites on GitLab. Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can deploy static pages for your individual projects, your user or your group. -Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) if you are -using GitLab.com to host your website. +Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlabcom) for specific +information, if you are using GitLab.com to host your website. --- -- cgit v1.2.1 From 9a8818f87f8bfc34e900b8fa2903a313980cd0fc Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 7 Mar 2016 19:09:42 +0200 Subject: Add FAQ on Pages website type precedence --- doc/pages/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/pages/README.md b/doc/pages/README.md index fa31a3ec560..0f37cce2c8f 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -40,6 +40,7 @@ information, if you are using GitLab.com to host your website. - [Frequently Asked Questions](#frequently-asked-questions) - [Can I download my generated pages?](#can-i-download-my-generated-pages) - [Can I use GitLab Pages if my project is private?](#can-i-use-gitlab-pages-if-my-project-is-private) + - [Do I need to create a user/group website before creating a project website?](#do-i-need-to-create-a-usergroup-website-before-creating-a-project-website) <!-- END doctoc generated TOC please keep comment here to allow auto update --> @@ -418,6 +419,11 @@ Sure. All you need to do is download the artifacts archive from the build page. Yes. GitLab Pages don't care whether you set your project's visibility level to private, internal or public. +### Do I need to create a user/group website before creating a project website? + +No, you don't. You can create your project first and it will be accessed under +`http(s)://namespace.example.io/projectname`. + --- [jekyll]: http://jekyllrb.com/ -- cgit v1.2.1 From 873eacaf8c7e4612a3800a65038e24508558d06c Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 8 Mar 2016 16:20:11 +0200 Subject: Mention that shared runners on GitLab.com are enabled by default [ci skip] --- doc/pages/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 0f37cce2c8f..9377135be7d 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -390,10 +390,10 @@ to your project's **Settings > Pages** and hit **Remove pages**. Simple as that. If you are using GitLab.com to host your website, then: -- The general domain name for GitLab Pages on GitLab.com is `gitlab.io` -- Shared runners are provided for free and can be used to build your website - if you cannot or don't want to set up your own Runner -- Custom domains and TLS support are enabled +- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`. +- Custom domains and TLS support are enabled. +- Shared runners are enabled by default, provided for free and can be used to + build your website. If you want you can still bring your own Runner. The rest of the guide still applies. -- cgit v1.2.1 From 2b18f02023496bec4c661a5134f6c6aa5753cae6 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 9 Mar 2016 10:51:41 +0200 Subject: Add section on redirects [ci skip] --- doc/pages/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/pages/README.md b/doc/pages/README.md index 9377135be7d..7e9184b54d3 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -37,6 +37,7 @@ information, if you are using GitLab.com to host your website. - [Remove the contents of your pages](#remove-the-contents-of-your-pages) - [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlabcom) - [Limitations](#limitations) +- [Redirects in GitLab Pages](#redirects-in-gitlab-pages) - [Frequently Asked Questions](#frequently-asked-questions) - [Can I download my generated pages?](#can-i-download-my-generated-pages) - [Can I use GitLab Pages if my project is private?](#can-i-use-gitlab-pages-if-my-project-is-private) @@ -408,6 +409,16 @@ don't redirect HTTP to HTTPS. [rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC" +## Redirects in GitLab Pages + +Since you cannot use any custom server configuration files, like `.htaccess` or +any `.conf` file for that matter, if you want to redirect a web page to another +location, you can use the [HTTP meta refresh tag][metarefresh]. + +Some static site generators provide plugins for that functionality so that you +don't have to create and edit HTML files manually. For example, Jekyll has the +[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from). + ## Frequently Asked Questions ### Can I download my generated pages? @@ -435,3 +446,4 @@ No, you don't. You can create your project first and it will be accessed under [pages]: ../ci/yaml/README.md#pages [staticgen]: https://www.staticgen.com/ [pages-jekyll]: https://gitlab.com/gitlab-examples/pages-jekyll +[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh -- cgit v1.2.1 From 1bdfdd87d81c9415def30ac17518a991b900d095 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 9 Mar 2016 11:00:56 +0200 Subject: Add known issues section --- doc/pages/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/pages/README.md b/doc/pages/README.md index 7e9184b54d3..53462f81edb 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -42,6 +42,7 @@ information, if you are using GitLab.com to host your website. - [Can I download my generated pages?](#can-i-download-my-generated-pages) - [Can I use GitLab Pages if my project is private?](#can-i-use-gitlab-pages-if-my-project-is-private) - [Do I need to create a user/group website before creating a project website?](#do-i-need-to-create-a-usergroup-website-before-creating-a-project-website) +- [Known issues](#known-issues) <!-- END doctoc generated TOC please keep comment here to allow auto update --> @@ -435,6 +436,10 @@ to private, internal or public. No, you don't. You can create your project first and it will be accessed under `http(s)://namespace.example.io/projectname`. +## Known issues + +For a list of known issues, visit GitLab's [public issue tracker]. + --- [jekyll]: http://jekyllrb.com/ @@ -447,3 +452,4 @@ No, you don't. You can create your project first and it will be accessed under [staticgen]: https://www.staticgen.com/ [pages-jekyll]: https://gitlab.com/gitlab-examples/pages-jekyll [metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh +[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages -- cgit v1.2.1 From 8fc3030ab64ca41d506fcc40eefcde64fa59f83e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 9 Mar 2016 11:07:52 +0200 Subject: Add note on custom domains limitation [ci skip] --- doc/pages/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/pages/README.md b/doc/pages/README.md index 53462f81edb..335c37b4814 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -354,6 +354,14 @@ instructions. ![Pages DNS details](img/pages_dns_details.png) +--- + +>**Note:** +Currently there is support only for custom domains on per-project basis. That +means that if you add a custom domain (`example.com`) for your user website +(`username.example.io`), a project that is served under `username.example.io/foo`, +will not be accessible under `example.com/foo`. + ### Secure your custom domain website with TLS When you add a new custom domain, you also have the chance to add a TLS -- cgit v1.2.1 From 8f5e7c36e4e8f60ab34a29da18e499705216c261 Mon Sep 17 00:00:00 2001 From: Heather McNamee <heather@gitlab.com> Date: Wed, 23 Mar 2016 10:58:28 +0000 Subject: Update links to new pages group. --- doc/pages/README.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 335c37b4814..938311ebf95 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -307,8 +307,8 @@ See an example that has different files in the [`master` branch][jekyll-master] and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which also includes `.gitlab-ci.yml`. -[jekyll-master]: https://gitlab.com/gitlab-examples/pages-jekyll-branched/tree/master -[jekyll-pages]: https://gitlab.com/gitlab-examples/pages-jekyll-branched/tree/pages +[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master +[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages ## Next steps @@ -320,17 +320,17 @@ what more you can do with GitLab Pages. Below is a list of example projects for GitLab Pages with a plain HTML website or various static site generators. Contributions are very welcome. -- [Plain HTML](https://gitlab.com/gitlab-examples/pages-plain-html) -- [Jekyll](https://gitlab.com/gitlab-examples/pages-jekyll) -- [Hugo](https://gitlab.com/gitlab-examples/pages-hugo) -- [Middleman](https://gitlab.com/gitlab-examples/pages-middleman) -- [Hexo](https://gitlab.com/gitlab-examples/pages-hexo) -- [Brunch](https://gitlab.com/gitlab-examples/pages-brunch) -- [Metalsmith](https://gitlab.com/gitlab-examples/pages-metalsmith) -- [Harp](https://gitlab.com/gitlab-examples/pages-harp) +- [Plain HTML](https://gitlab.com/pages/plain-html) +- [Jekyll](https://gitlab.com/pages/jekyll) +- [Hugo](https://gitlab.com/pages/hugo) +- [Middleman](https://gitlab.com/pages/middleman) +- [Hexo](https://gitlab.com/pages/hexo) +- [Brunch](https://gitlab.com/pages/brunch) +- [Metalsmith](https://gitlab.com/pages/metalsmith) +- [Harp](https://gitlab.com/pages/harp) -Visit the gitlab-examples group for a full list of projects: -<https://gitlab.com/groups/gitlab-examples>. +Visit the GitLab Pages group for a full list of example projects: +<https://gitlab.com/groups/pages>. ### Add a custom domain to your Pages website @@ -458,6 +458,6 @@ For a list of known issues, visit GitLab's [public issue tracker]. [gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner [pages]: ../ci/yaml/README.md#pages [staticgen]: https://www.staticgen.com/ -[pages-jekyll]: https://gitlab.com/gitlab-examples/pages-jekyll +[pages-jekyll]: https://gitlab.com/pages/jekyll [metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh [public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages -- cgit v1.2.1 From 39f9056dbb45f8aec3b97ecf85ca4b0c00b49534 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 31 Mar 2016 11:54:33 +0200 Subject: Update GitLab Pages to 0.2.1 --- GITLAB_PAGES_VERSION | 2 +- doc/pages/administration.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 0ea3a944b39..0c62199f16a 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.2.0 +0.2.1 diff --git a/doc/pages/administration.md b/doc/pages/administration.md index d0fdbeafa5b..68b9003a420 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -97,7 +97,7 @@ installing the pages daemon. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages -sudo -u git -H git checkout v0.2.0 +sudo -u git -H git checkout v0.2.1 sudo -u git -H make ``` @@ -505,7 +505,7 @@ latest previous version. - Documentation was moved to one place [8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md -[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.0 +[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.1 [NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx --- -- cgit v1.2.1 From 2c1eeb5c73d625b0f4481e8a7985d95071e7640c Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 5 Apr 2016 18:06:11 +0200 Subject: Update GitLab Pages to 0.2.2 --- GITLAB_PAGES_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 0c62199f16a..ee1372d33a2 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.2.1 +0.2.2 -- cgit v1.2.1 From fd61cd08ceb6d05b6e1bf103c60add02bdd3edbe Mon Sep 17 00:00:00 2001 From: Nick Thomas <gitlab@ur.gs> Date: Thu, 21 Apr 2016 17:33:08 +0000 Subject: pages: Fix "undefined local variable or method `total_size'" when maximum page size is exceeded --- app/services/projects/update_pages_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index ceabd29fd52..45a28b46d06 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -85,7 +85,7 @@ module Projects public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true) if public_entry.total_size > max_size - raise "artifacts for pages are too large: #{total_size}" + raise "artifacts for pages are too large: #{public_entry.total_size}" end # Requires UnZip at least 6.00 Info-ZIP. -- cgit v1.2.1 From b22c06d311525cbb9edc95ab99da26fa9b921395 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Thu, 12 May 2016 09:38:27 -0500 Subject: Bump GitLab Pages to 0.2.4 --- GITLAB_PAGES_VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index ee1372d33a2..abd410582de 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -0.2.2 +0.2.4 -- cgit v1.2.1 From caedc996bdb37718b8397988662835b3c8163e46 Mon Sep 17 00:00:00 2001 From: Connor Shea <connor.james.shea@gmail.com> Date: Fri, 3 Jun 2016 13:01:54 -0600 Subject: Adds algorithm to the pages domain key and remote mirror credentials encrypted attributes for forward compatibility with attr_encrypted 3.0.0. aes-256-cbc is the default algorithm for attr_encrypted 1.x, but the default is changed in 3.0 and thus must be declared explicitly. See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4216/ for more information. This will prevent OpenSSL errors once the code from that MR is merged into EE. --- app/models/pages_domain.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 9155e57331d..2f4cded15c8 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -10,7 +10,10 @@ class PagesDomain < ActiveRecord::Base validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? } validate :validate_intermediates, if: ->(domain) { domain.certificate.present? } - attr_encrypted :key, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base + attr_encrypted :key, + mode: :per_attribute_iv_and_salt, + key: Gitlab::Application.secrets.db_key_base, + algorithm: 'aes-256-cbc' after_create :update after_save :update -- cgit v1.2.1 From 0168a24064a5748ab0de902a12c0fe9851668fb1 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Mon, 8 Aug 2016 20:53:31 +0800 Subject: Only show the message if user is not the owner Closes #323 --- app/views/projects/pages/_destroy.haml | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index 6a7b6baf767..df5add89545 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -1,11 +1,12 @@ -- if can?(current_user, :remove_pages, @project) && @project.pages_deployed? - .panel.panel-default.panel.panel-danger - .panel-heading Remove pages - .errors-holder - .panel-body - %p - Removing the pages will prevent from exposing them to outside world. - .form-actions - = link_to 'Remove pages', namespace_project_pages_path(@project.namespace, @project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove" +- if can?(current_user, :remove_pages, @project) + - if @project.pages_deployed? + .panel.panel-default.panel.panel-danger + .panel-heading Remove pages + .errors-holder + .panel-body + %p + Removing the pages will prevent from exposing them to outside world. + .form-actions + = link_to 'Remove pages', namespace_project_pages_path(@project.namespace, @project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove" - else .nothing-here-block Only the project owner can remove pages -- cgit v1.2.1 From e6a7eb43b2dda4d47d25b4d7ecc068ba01f5b54d Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Thu, 18 Aug 2016 23:36:31 +0800 Subject: If no pages were deployed, show nothing. Feedback: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/628#note_14000640 --- app/views/projects/pages/_destroy.haml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/projects/pages/_destroy.haml b/app/views/projects/pages/_destroy.haml index df5add89545..42d9ef5ccba 100644 --- a/app/views/projects/pages/_destroy.haml +++ b/app/views/projects/pages/_destroy.haml @@ -1,5 +1,5 @@ -- if can?(current_user, :remove_pages, @project) - - if @project.pages_deployed? +- if @project.pages_deployed? + - if can?(current_user, :remove_pages, @project) .panel.panel-default.panel.panel-danger .panel-heading Remove pages .errors-holder @@ -8,5 +8,5 @@ Removing the pages will prevent from exposing them to outside world. .form-actions = link_to 'Remove pages', namespace_project_pages_path(@project.namespace, @project), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove" -- else - .nothing-here-block Only the project owner can remove pages + - else + .nothing-here-block Only the project owner can remove pages -- cgit v1.2.1 From 0763b5ea4a5e463b6cc0e94ae05ba2e58262cf74 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Fri, 19 Aug 2016 00:24:27 +0800 Subject: Add a test for checking pages setting --- spec/features/projects/pages_spec.rb | 58 ++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 spec/features/projects/pages_spec.rb diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb new file mode 100644 index 00000000000..acd7a1abb9a --- /dev/null +++ b/spec/features/projects/pages_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +feature 'Pages', feature: true do + given(:project) { create(:empty_project) } + given(:user) { create(:user) } + given(:role) { :master } + + background do + project.team << [user, role] + + login_as(user) + end + + shared_examples 'no pages deployed' do + scenario 'does not see anything to destroy' do + visit namespace_project_pages_path(project.namespace, project) + + expect(page).not_to have_link('Remove pages') + expect(page).not_to have_text('Only the project owner can remove pages') + end + end + + context 'when user is the owner' do + background do + project.namespace.update(owner: user) + end + + context 'when pages deployed' do + background do + allow_any_instance_of(Project).to receive(:pages_deployed?) { true } + end + + scenario 'sees "Remove pages" link' do + visit namespace_project_pages_path(project.namespace, project) + + expect(page).to have_link('Remove pages') + end + end + + it_behaves_like 'no pages deployed' + end + + context 'when the user is not the owner' do + context 'when pages deployed' do + background do + allow_any_instance_of(Project).to receive(:pages_deployed?) { true } + end + + scenario 'sees "Only the project owner can remove pages" text' do + visit namespace_project_pages_path(project.namespace, project) + + expect(page).to have_text('Only the project owner can remove pages') + end + end + + it_behaves_like 'no pages deployed' + end +end -- cgit v1.2.1 From c3e483b05b11e862dfe14104c8ba63cbdb11d953 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Tue, 23 Aug 2016 21:47:22 +0800 Subject: Stub to enable it so that we could test this --- spec/features/projects/pages_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb index acd7a1abb9a..11793c0f303 100644 --- a/spec/features/projects/pages_spec.rb +++ b/spec/features/projects/pages_spec.rb @@ -6,6 +6,8 @@ feature 'Pages', feature: true do given(:role) { :master } background do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + project.team << [user, role] login_as(user) -- cgit v1.2.1 From 109553afd0ba80d47a282eb5e68ce3b5eda1d818 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Tue, 7 Jun 2016 16:40:15 +0200 Subject: Fix EE specs after ci_commit rename to pipeline --- app/services/projects/update_pages_service.rb | 2 +- features/steps/project/pages.rb | 12 ++++++------ spec/services/projects/update_pages_service_spec.rb | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 45a28b46d06..f588f6feb8c 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -52,7 +52,7 @@ module Projects def create_status GenericCommitStatus.new( project: project, - commit: build.commit, + pipeline: build.pipeline, user: build.user, ref: build.ref, stage: 'deploy', diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb index 34f97f1ea8b..a76672168fb 100644 --- a/features/steps/project/pages.rb +++ b/features/steps/project/pages.rb @@ -27,13 +27,13 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps end step 'pages are deployed' do - commit = @project.ensure_ci_commit(@project.commit('HEAD').sha) + pipeline = @project.ensure_pipeline(@project.commit('HEAD').sha, 'HEAD') build = build(:ci_build, - project: @project, - commit: commit, - ref: 'HEAD', - artifacts_file: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip'), - artifacts_metadata: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta') + project: @project, + pipeline: pipeline, + ref: 'HEAD', + artifacts_file: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip'), + artifacts_metadata: fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta') ) result = ::Projects::UpdatePagesService.new(@project, build).execute expect(result[:status]).to eq(:success) diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index 51da582c497..4eac7875864 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -2,8 +2,8 @@ require "spec_helper" describe Projects::UpdatePagesService do let(:project) { create :project } - let(:commit) { create :ci_commit, project: project, sha: project.commit('HEAD').sha } - let(:build) { create :ci_build, commit: commit, ref: 'HEAD' } + let(:pipeline) { create :ci_pipeline, project: project, sha: project.commit('HEAD').sha } + let(:build) { create :ci_build, pipeline: pipeline, ref: 'HEAD' } let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') } subject { described_class.new(project, build) } @@ -47,7 +47,7 @@ describe Projects::UpdatePagesService do end it 'fails if sha on branch is not latest' do - commit.update_attributes(sha: 'old_sha') + pipeline.update_attributes(sha: 'old_sha') build.update_attributes(artifacts_file: file) expect(execute).to_not eq(:success) end -- cgit v1.2.1 From 8fd926fe862fdaa5f44c11c2184c7e2734e5e173 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Fri, 2 Sep 2016 22:33:39 +0800 Subject: Project#ensure_pipeline changed the args order --- features/steps/project/pages.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/steps/project/pages.rb b/features/steps/project/pages.rb index a76672168fb..c80c6273807 100644 --- a/features/steps/project/pages.rb +++ b/features/steps/project/pages.rb @@ -27,7 +27,7 @@ class Spinach::Features::ProjectPages < Spinach::FeatureSteps end step 'pages are deployed' do - pipeline = @project.ensure_pipeline(@project.commit('HEAD').sha, 'HEAD') + pipeline = @project.ensure_pipeline('HEAD', @project.commit('HEAD').sha) build = build(:ci_build, project: @project, pipeline: pipeline, -- cgit v1.2.1 From d8ae09229a7798450263534a486c9d11b087d816 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Sat, 19 Nov 2016 19:20:05 +0100 Subject: Remove Pages ToC from docs [ci skip] --- doc/pages/administration.md | 37 +++---------------------------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 68b9003a420..2ef46932c13 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -14,39 +14,6 @@ configuration. If you are looking for ways to upload your static content in GitLab Pages, you probably want to read the [user documentation](README.md). -[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 -[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 - ---- - -<!-- START doctoc generated TOC please keep comment here to allow auto update --> -<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [The GitLab Pages daemon](#the-gitlab-pages-daemon) - - [The GitLab Pages daemon and the case of custom domains](#the-gitlab-pages-daemon-and-the-case-of-custom-domains) - - [Install the Pages daemon](#install-the-pages-daemon) -- [Configuration](#configuration) - - [Configuration prerequisites](#configuration-prerequisites) - - [Configuration scenarios](#configuration-scenarios) - - [DNS configuration](#dns-configuration) -- [Setting up GitLab Pages](#setting-up-gitlab-pages) - - [Custom domains with HTTPS support](#custom-domains-with-https-support) - - [Custom domains without HTTPS support](#custom-domains-without-https-support) - - [Wildcard HTTP domain without custom domains](#wildcard-http-domain-without-custom-domains) - - [Wildcard HTTPS domain without custom domains](#wildcard-https-domain-without-custom-domains) -- [NGINX configuration](#nginx-configuration) - - [NGINX configuration files](#nginx-configuration-files) - - [NGINX configuration for custom domains](#nginx-configuration-for-custom-domains) - - [NGINX caveats](#nginx-caveats) -- [Set maximum pages size](#set-maximum-pages-size) -- [Change storage path](#change-storage-path) -- [Backup](#backup) -- [Security](#security) -- [Changelog](#changelog) - -<!-- END doctoc generated TOC please keep comment here to allow auto update --> - ## The GitLab Pages daemon Starting from GitLab EE 8.5, GitLab Pages make use of the [GitLab Pages daemon], @@ -522,6 +489,8 @@ No new changes. [8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md [8-3-omnidocs]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/8-3-stable-ee/doc/settings/pages.md +[backup]: ../../raketasks/backup_restore.md +[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 +[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 [reconfigure]: ../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../../administration/restart_gitlab.md#installations-from-source -[backup]: ../../raketasks/backup_restore.md -- cgit v1.2.1 From baac53652c258241b45dbe77ba99b02f22ef020f Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 29 Nov 2016 10:59:40 +0100 Subject: Clarify where the settings are in Pages docs Fixes https://gitlab.com/gitlab-org/gitlab-ee/issues/1333 [ci skip] --- doc/pages/README.md | 40 ++++++---------------------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 938311ebf95..e70ca7b48bd 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -12,40 +12,9 @@ With GitLab Pages you can host for free your static websites on GitLab. Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can deploy static pages for your individual projects, your user or your group. -Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlabcom) for specific +Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific information, if you are using GitLab.com to host your website. ---- - -<!-- START doctoc generated TOC please keep comment here to allow auto update --> -<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --> -**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* - -- [Getting started with GitLab Pages](#getting-started-with-gitlab-pages) - - [GitLab Pages requirements](#gitlab-pages-requirements) - - [User or group Pages](#user-or-group-pages) - - [Project Pages](#project-pages) - - [Explore the contents of `.gitlab-ci.yml`](#explore-the-contents-of-gitlab-ciyml) - - [How `.gitlab-ci.yml` looks like when the static content is in your repository](#how-gitlab-ciyml-looks-like-when-the-static-content-is-in-your-repository) - - [How `.gitlab-ci.yml` looks like when using a static generator](#how-gitlab-ciyml-looks-like-when-using-a-static-generator) - - [How to set up GitLab Pages in a repository where there's also actual code](#how-to-set-up-gitlab-pages-in-a-repository-where-theres-also-actual-code) -- [Next steps](#next-steps) - - [Example projects](#example-projects) - - [Add a custom domain to your Pages website](#add-a-custom-domain-to-your-pages-website) - - [Secure your custom domain website with TLS](#secure-your-custom-domain-website-with-tls) - - [Custom error codes pages](#custom-error-codes-pages) - - [Remove the contents of your pages](#remove-the-contents-of-your-pages) -- [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlabcom) -- [Limitations](#limitations) -- [Redirects in GitLab Pages](#redirects-in-gitlab-pages) -- [Frequently Asked Questions](#frequently-asked-questions) - - [Can I download my generated pages?](#can-i-download-my-generated-pages) - - [Can I use GitLab Pages if my project is private?](#can-i-use-gitlab-pages-if-my-project-is-private) - - [Do I need to create a user/group website before creating a project website?](#do-i-need-to-create-a-usergroup-website-before-creating-a-project-website) -- [Known issues](#known-issues) - -<!-- END doctoc generated TOC please keep comment here to allow auto update --> - ## Getting started with GitLab Pages > **Note:** @@ -335,7 +304,8 @@ Visit the GitLab Pages group for a full list of example projects: ### Add a custom domain to your Pages website If this setting is enabled by your GitLab administrator, you should be able to -see the **New Domain** button when visiting your project's **Settings > Pages**. +see the **New Domain** button when visiting your project's settings through the +gear icon in the top right and then navigating to **Pages**. ![New domain button](img/pages_new_domain_button.png) @@ -392,7 +362,9 @@ If the case of `404.html`, there are different scenarios. For example: ### Remove the contents of your pages If you ever feel the need to purge your Pages content, you can do so by going -to your project's **Settings > Pages** and hit **Remove pages**. Simple as that. +to your project's settings through the gear icon in the top right, and then +navigating to **Pages**. Hit the **Remove pages** button and your Pages website +will be deleted. Simple as that. ![Remove pages](img/pages_remove.png) -- cgit v1.2.1 From f6c66f4567f6818ec4678dcfb3e543d478f9dc36 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones <jamedjo@gmail.com> Date: Thu, 15 Dec 2016 20:01:55 +0000 Subject: Fix reconfigure link on doc/pages/administration.md --- doc/pages/administration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 2ef46932c13..00100466ce2 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -492,5 +492,5 @@ No new changes. [backup]: ../../raketasks/backup_restore.md [ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 [ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 -[reconfigure]: ../../administration/restart_gitlab.md#omnibus-gitlab-reconfigure +[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../../administration/restart_gitlab.md#installations-from-source -- cgit v1.2.1 From 80f9794c3e05362c2b06978d8a664ebf4e0ae0a6 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones <jamedjo@gmail.com> Date: Sat, 17 Dec 2016 00:52:06 +0000 Subject: Fix restart link on doc/pages/administration.md --- doc/pages/administration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 00100466ce2..07b603f78c7 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -489,8 +489,8 @@ No new changes. [8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md [8-3-omnidocs]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/8-3-stable-ee/doc/settings/pages.md -[backup]: ../../raketasks/backup_restore.md +[backup]: ../raketasks/backup_restore.md [ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 [ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 [reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure -[restart]: ../../administration/restart_gitlab.md#installations-from-source +[restart]: ../administration/restart_gitlab.md#installations-from-source -- cgit v1.2.1 From 86f4767dc1afea9f0744e4fb0c5ce663bf7e3de8 Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Fri, 23 Dec 2016 00:26:33 +0530 Subject: Fix 500 error while navigating to the `pages_domains` 'show' page. ================== = Implementation = ================== 1. The path of the page is of the form 'group/project/pages/domains/<domain_name>' 2. Rails looks at `params[:id]` (which should be the domain name), and finds the relevant model record. 3. Given a domain like `foo.bar`, Rails sets `params[:id]` to `foo` (should be `foo.bar`), and sets `params[:format]` to `bar` 4. This commit fixes the issue by adding a route constraint, so that `params[:id]` is set to the entire `foo.bar` domain name. ========= = Tests = ========= 1. Add controller specs for the `PagesDomainController`. These are slightly orthogonal to this bug fix (they don't fail when this bug is present), but should be present nonetheless. 2. Add routing specs that catch this bug (by asserting that the `id` param is passed as expected when it contains a domain name). 3. Modify the 'RESTful project resources' routing spec shared example to accomodate controllers where the controller path (such as `pages/domains`) is different from the controller name (such as `pages_domains`). --- config/routes/project.rb | 2 +- .../projects/pages_domains_controller_spec.rb | 64 ++++++++++++++++++++++ spec/routing/project_routing_spec.rb | 37 ++++++++++--- 3 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 spec/controllers/projects/pages_domains_controller_spec.rb diff --git a/config/routes/project.rb b/config/routes/project.rb index ea3bfdd45e6..b6b432256df 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -40,7 +40,7 @@ constraints(ProjectUrlConstrainer.new) do end resource :pages, only: [:show, :destroy] do - resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains' + resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: /[^\/]+/ } end resources :compare, only: [:index, :create] do diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb new file mode 100644 index 00000000000..2362df895a8 --- /dev/null +++ b/spec/controllers/projects/pages_domains_controller_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Projects::PagesDomainsController do + let(:user) { create(:user) } + let(:project) { create(:project) } + + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project + } + end + + before do + sign_in(user) + project.team << [user, :master] + end + + describe 'GET show' do + let!(:pages_domain) { create(:pages_domain, project: project) } + + it "displays the 'show' page" do + get(:show, request_params.merge(id: pages_domain.domain)) + + expect(response).to have_http_status(200) + expect(response).to render_template('show') + end + end + + describe 'GET new' do + it "displays the 'new' page" do + get(:new, request_params) + + expect(response).to have_http_status(200) + expect(response).to render_template('new') + end + end + + describe 'POST create' do + let(:pages_domain_params) do + build(:pages_domain, :with_certificate, :with_key).slice(:key, :certificate, :domain) + end + + it "creates a new pages domain" do + expect do + post(:create, request_params.merge(pages_domain: pages_domain_params)) + end.to change { PagesDomain.count }.by(1) + + expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project)) + end + end + + describe 'DELETE destroy' do + let!(:pages_domain) { create(:pages_domain, project: project) } + + it "deletes the pages domain" do + expect do + delete(:destroy, request_params.merge(id: pages_domain.domain)) + end.to change { PagesDomain.count }.by(-1) + + expect(response).to redirect_to(namespace_project_pages_path(project.namespace, project)) + end + end +end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 77549db2927..43be785ad9d 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -27,35 +27,42 @@ describe 'project routing' do # let(:actions) { [:index] } # let(:controller) { 'issues' } # end + # + # # Different controller name and path + # it_behaves_like 'RESTful project resources' do + # let(:controller) { 'pages_domains' } + # let(:controller_path) { 'pages/domains' } + # end shared_examples 'RESTful project resources' do let(:actions) { [:index, :create, :new, :edit, :show, :update, :destroy] } + let(:controller_path) { controller } it 'to #index' do - expect(get("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index) + expect(get("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#index", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:index) end it 'to #create' do - expect(post("/gitlab/gitlabhq/#{controller}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create) + expect(post("/gitlab/gitlabhq/#{controller_path}")).to route_to("projects/#{controller}#create", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:create) end it 'to #new' do - expect(get("/gitlab/gitlabhq/#{controller}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new) + expect(get("/gitlab/gitlabhq/#{controller_path}/new")).to route_to("projects/#{controller}#new", namespace_id: 'gitlab', project_id: 'gitlabhq') if actions.include?(:new) end it 'to #edit' do - expect(get("/gitlab/gitlabhq/#{controller}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit) + expect(get("/gitlab/gitlabhq/#{controller_path}/1/edit")).to route_to("projects/#{controller}#edit", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:edit) end it 'to #show' do - expect(get("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show) + expect(get("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#show", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:show) end it 'to #update' do - expect(put("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update) + expect(put("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#update", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:update) end it 'to #destroy' do - expect(delete("/gitlab/gitlabhq/#{controller}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy) + expect(delete("/gitlab/gitlabhq/#{controller_path}/1")).to route_to("projects/#{controller}#destroy", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') if actions.include?(:destroy) end end @@ -539,4 +546,20 @@ describe 'project routing' do 'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq') end end + + describe Projects::PagesDomainsController, 'routing' do + it_behaves_like 'RESTful project resources' do + let(:actions) { [:show, :new, :create, :destroy] } + let(:controller) { 'pages_domains' } + let(:controller_path) { 'pages/domains' } + end + + it 'to #destroy with a valid domain name' do + expect(delete('/gitlab/gitlabhq/pages/domains/my.domain.com')).to route_to('projects/pages_domains#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'my.domain.com') + end + + it 'to #show with a valid domain' do + expect(get('/gitlab/gitlabhq/pages/domains/my.domain.com')).to route_to('projects/pages_domains#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'my.domain.com') + end + end end -- cgit v1.2.1 From 66bfc9e9e78257d9e6e232b004f6152440ebe27b Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Wed, 17 Aug 2016 13:27:19 +0300 Subject: [CE->EE] Fix specs --- spec/services/pages_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb index e6ad93358a0..254b4f688cf 100644 --- a/spec/services/pages_service_spec.rb +++ b/spec/services/pages_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe PagesService, services: true do let(:build) { create(:ci_build) } - let(:data) { Gitlab::BuildDataBuilder.build(build) } + let(:data) { Gitlab::DataBuilder::Build.build(build) } let(:service) { PagesService.new(data) } before do -- cgit v1.2.1 From 12d44272ec68e38760d5886b27546e8c13f7942a Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 30 May 2016 10:11:46 +0200 Subject: Fix Rubocop offenses --- spec/models/pages_domain_spec.rb | 14 +++++++------- spec/services/pages_service_spec.rb | 4 ++-- spec/services/projects/update_pages_service_spec.rb | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index 0b95bf594c5..0cbea5be106 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -23,13 +23,13 @@ describe PagesDomain, models: true do context 'no domain' do let(:domain) { nil } - it { is_expected.to_not be_valid } + it { is_expected.not_to be_valid } end context 'invalid domain' do let(:domain) { '0123123' } - it { is_expected.to_not be_valid } + it { is_expected.not_to be_valid } end context 'domain from .example.com' do @@ -37,7 +37,7 @@ describe PagesDomain, models: true do before { allow(Settings.pages).to receive(:host).and_return('domain.com') } - it { is_expected.to_not be_valid } + it { is_expected.not_to be_valid } end end @@ -47,13 +47,13 @@ describe PagesDomain, models: true do context 'when only certificate is specified' do let(:domain) { build(:pages_domain, :with_certificate) } - it { is_expected.to_not be_valid } + it { is_expected.not_to be_valid } end context 'when only key is specified' do let(:domain) { build(:pages_domain, :with_key) } - it { is_expected.to_not be_valid } + it { is_expected.not_to be_valid } end context 'with matching key' do @@ -65,7 +65,7 @@ describe PagesDomain, models: true do context 'for not matching key' do let(:domain) { build(:pages_domain, :with_missing_chain, :with_key) } - it { is_expected.to_not be_valid } + it { is_expected.not_to be_valid } end end @@ -157,6 +157,6 @@ describe PagesDomain, models: true do subject { domain.certificate_text } # We test only existence of output, since the output is long - it { is_expected.to_not be_empty } + it { is_expected.not_to be_empty } end end diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb index 254b4f688cf..b4215b2cd02 100644 --- a/spec/services/pages_service_spec.rb +++ b/spec/services/pages_service_spec.rb @@ -26,7 +26,7 @@ describe PagesService, services: true do before { build.status = status } it 'should not execute worker' do - expect(PagesWorker).to_not receive(:perform_async) + expect(PagesWorker).not_to receive(:perform_async) service.execute end end @@ -40,7 +40,7 @@ describe PagesService, services: true do end it 'should not execute worker' do - expect(PagesWorker).to_not receive(:perform_async) + expect(PagesWorker).not_to receive(:perform_async) service.execute end end diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index 4eac7875864..af1c6a5e7b5 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -34,7 +34,7 @@ describe Projects::UpdatePagesService do it 'limits pages size' do stub_application_setting(max_pages_size: 1) - expect(execute).to_not eq(:success) + expect(execute).not_to eq(:success) end it 'removes pages after destroy' do @@ -49,29 +49,29 @@ describe Projects::UpdatePagesService do it 'fails if sha on branch is not latest' do pipeline.update_attributes(sha: 'old_sha') build.update_attributes(artifacts_file: file) - expect(execute).to_not eq(:success) + expect(execute).not_to eq(:success) end it 'fails for empty file fails' do build.update_attributes(artifacts_file: empty_file) - expect(execute).to_not eq(:success) + expect(execute).not_to eq(:success) end end end it 'fails to remove project pages when no pages is deployed' do - expect(PagesWorker).to_not receive(:perform_in) + expect(PagesWorker).not_to receive(:perform_in) expect(project.pages_deployed?).to be_falsey project.destroy end it 'fails if no artifacts' do - expect(execute).to_not eq(:success) + expect(execute).not_to eq(:success) end it 'fails for invalid archive' do build.update_attributes(artifacts_file: invalid_file) - expect(execute).to_not eq(:success) + expect(execute).not_to eq(:success) end def execute -- cgit v1.2.1 From 91c07d16cd371a545a9ad089ccc0313c7a13bb5b Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Fri, 13 May 2016 12:51:52 +0200 Subject: Fixed Rubocop deprecation warnings --- app/services/projects/update_pages_service.rb | 2 +- spec/services/projects/update_pages_service_spec.rb | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index f588f6feb8c..90fff91dd9c 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -25,7 +25,7 @@ module Projects # Check if we did extract public directory archive_public_path = File.join(archive_path, 'public') - raise 'pages miss the public folder' unless Dir.exists?(archive_public_path) + raise 'pages miss the public folder' unless Dir.exist?(archive_public_path) raise 'pages are outdated' unless latest? deploy_page!(archive_public_path) diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index af1c6a5e7b5..411b22a0fb8 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -5,7 +5,7 @@ describe Projects::UpdatePagesService do let(:pipeline) { create :ci_pipeline, project: project, sha: project.commit('HEAD').sha } let(:build) { create :ci_build, pipeline: pipeline, ref: 'HEAD' } let(:invalid_file) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png') } - + subject { described_class.new(project, build) } before do @@ -18,7 +18,7 @@ describe Projects::UpdatePagesService do let(:empty_file) { fixture_file_upload(Rails.root + "spec/fixtures/pages_empty.#{format}") } let(:metadata) do filename = Rails.root + "spec/fixtures/pages.#{format}.meta" - fixture_file_upload(filename) if File.exists?(filename) + fixture_file_upload(filename) if File.exist?(filename) end before do @@ -73,7 +73,7 @@ describe Projects::UpdatePagesService do build.update_attributes(artifacts_file: invalid_file) expect(execute).not_to eq(:success) end - + def execute subject.execute[:status] end -- cgit v1.2.1 From 7163da6046c2b57f9e9cf3b83959a57763e2f460 Mon Sep 17 00:00:00 2001 From: Kamil Trzcinski <ayufan@ayufan.eu> Date: Sat, 13 Aug 2016 11:15:31 +0200 Subject: Fix GitLab Pages test failures --- app/services/projects/update_pages_service.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 90fff91dd9c..c52f3d3e230 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -13,6 +13,7 @@ module Projects def execute # Create status notifying the deployment of pages @status = create_status + @status.enqueue! @status.run! raise 'missing pages artifacts' unless build.artifacts_file? -- cgit v1.2.1 From 6ba149279445bd376e145dab2d7fa58808031692 Mon Sep 17 00:00:00 2001 From: Nick Thomas <nick@gitlab.com> Date: Tue, 20 Dec 2016 11:24:44 +0000 Subject: Update validates_hostname to 1.0.6 to fix a bug in parsing hexadecimal-looking domain names --- Gemfile | 2 +- Gemfile.lock | 4 ++-- spec/models/pages_domain_spec.rb | 8 +++++++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index bc1b13c7331..49851aabe19 100644 --- a/Gemfile +++ b/Gemfile @@ -49,7 +49,7 @@ gem 'attr_encrypted', '~> 3.0.0' gem 'u2f', '~> 0.2.1' # GitLab Pages -gem 'validates_hostname', '~> 1.0.0' +gem 'validates_hostname', '~> 1.0.6' # Browser detection gem 'browser', '~> 2.2' diff --git a/Gemfile.lock b/Gemfile.lock index 6263b02b041..5736862e5ab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -799,7 +799,7 @@ GEM get_process_mem (~> 0) unicorn (>= 4, < 6) uniform_notifier (1.10.0) - validates_hostname (1.0.5) + validates_hostname (1.0.6) activerecord (>= 3.0) activesupport (>= 3.0) version_sorter (2.1.0) @@ -1017,7 +1017,7 @@ DEPENDENCIES unf (~> 0.1.4) unicorn (~> 5.1.0) unicorn-worker-killer (~> 0.4.4) - validates_hostname (~> 1.0.0) + validates_hostname (~> 1.0.6) version_sorter (~> 2.1.0) virtus (~> 1.0.1) vmstat (~> 2.3.0) diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index 0cbea5be106..e6a4583a8fb 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -4,7 +4,7 @@ describe PagesDomain, models: true do describe 'associations' do it { is_expected.to belong_to(:project) } end - + describe :validate_domain do subject { build(:pages_domain, domain: domain) } @@ -20,6 +20,12 @@ describe PagesDomain, models: true do it { is_expected.to be_valid } end + context 'valid hexadecimal-looking domain' do + let(:domain) { '0x12345.com'} + + it { is_expected.to be_valid } + end + context 'no domain' do let(:domain) { nil } -- cgit v1.2.1 From 8a09a25185bb05a9360370f40a8f7ff50279e60b Mon Sep 17 00:00:00 2001 From: Michael <gitlab.michael@oiu.ch> Date: Mon, 28 Nov 2016 11:51:00 +0000 Subject: small but mighty confusing typo --- doc/pages/administration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 07b603f78c7..0e1665fa832 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -246,7 +246,7 @@ Below are the four scenarios that are described in 1. Edit `/etc/gitlab/gitlab.rb`: ```ruby - pages_external_url "https://example.io" + pages_external_url "http://example.io" nginx['listen_addresses'] = ['1.1.1.1'] pages_nginx['enable'] = false gitlab_pages['external_http'] = '1.1.1.2:80' -- cgit v1.2.1 From 877c121cfcca1f9bb116ff0a8831c5a9096e2853 Mon Sep 17 00:00:00 2001 From: Rebecca Skinner <traybaby@gmail.com> Date: Mon, 28 Mar 2016 14:15:48 +0000 Subject: Fixed typo ("server" -> "serve") --- doc/pages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index e70ca7b48bd..e427d7f283d 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -357,7 +357,7 @@ If the case of `404.html`, there are different scenarios. For example: - If you use user/group Pages (served under `/`) and try to access `/non/existing_file` GitLab Pages will try to serve `/404.html`. - If you use a custom domain and try to access `/non/existing_file`, GitLab - Pages will try to server only `/404.html`. + Pages will try to serve only `/404.html`. ### Remove the contents of your pages -- cgit v1.2.1 From 5075fb3bb7d0d91cec697095dc7c7803333a7ffb Mon Sep 17 00:00:00 2001 From: James Lopez <james@jameslopez.es> Date: Tue, 28 Jun 2016 10:14:24 +0200 Subject: fix attr_encrypted in EE --- app/models/pages_domain.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 2f4cded15c8..0b9ebf1ffe2 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -12,6 +12,7 @@ class PagesDomain < ActiveRecord::Base attr_encrypted :key, mode: :per_attribute_iv_and_salt, + insecure_mode: true, key: Gitlab::Application.secrets.db_key_base, algorithm: 'aes-256-cbc' -- cgit v1.2.1 From cd582d3c19843e776cf4aaf151753ec61a9e56ef Mon Sep 17 00:00:00 2001 From: Brian Hall <brian@hack.design> Date: Tue, 31 Jan 2017 13:29:34 -0600 Subject: Remove unnecessary returns / unset variables from the CoffeeScript -> JS conversion. --- spec/javascripts/shortcuts_issuable_spec.js | 47 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index db2302c4fb0..db11c2516a6 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -11,9 +11,9 @@ beforeEach(function() { loadFixtures(fixtureName); document.querySelector('.js-new-note-form').classList.add('js-main-target-form'); - return this.shortcut = new ShortcutsIssuable(); + this.shortcut = new ShortcutsIssuable(); }); - return describe('#replyWithSelectedText', function() { + describe('#replyWithSelectedText', function() { var stubSelection; // Stub window.gl.utils.getSelectedFragment to return a node with the provided HTML. stubSelection = function(html) { @@ -24,64 +24,61 @@ }; }; beforeEach(function() { - return this.selector = 'form.js-main-target-form textarea#note_note'; + this.selector = 'form.js-main-target-form textarea#note_note'; }); describe('with empty selection', function() { it('does not return an error', function() { this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe(''); + expect($(this.selector).val()).toBe(''); }); - return it('triggers `input`', function() { - var focused; - focused = false; + it('triggers `input`', function() { + var focused = false; $(this.selector).on('focus', function() { - return focused = true; + focused = true; }); this.shortcut.replyWithSelectedText(); - return expect(focused).toBe(true); + expect(focused).toBe(true); }); }); describe('with any selection', function() { beforeEach(function() { - return stubSelection('<p>Selected text.</p>'); + stubSelection('<p>Selected text.</p>'); }); it('leaves existing input intact', function() { $(this.selector).val('This text was already here.'); expect($(this.selector).val()).toBe('This text was already here.'); this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n"); + expect($(this.selector).val()).toBe("This text was already here.\n\n> Selected text.\n\n"); }); it('triggers `input`', function() { - var triggered; - triggered = false; + var triggered = false; $(this.selector).on('input', function() { - return triggered = true; + triggered = true; }); this.shortcut.replyWithSelectedText(); - return expect(triggered).toBe(true); + expect(triggered).toBe(true); }); - return it('triggers `focus`', function() { - var focused; - focused = false; + it('triggers `focus`', function() { + var focused = false; $(this.selector).on('focus', function() { - return focused = true; + focused = true; }); this.shortcut.replyWithSelectedText(); - return expect(focused).toBe(true); + expect(focused).toBe(true); }); }); describe('with a one-line selection', function() { - return it('quotes the selection', function() { + it('quotes the selection', function() { stubSelection('<p>This text has been selected.</p>'); this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe("> This text has been selected.\n\n"); + expect($(this.selector).val()).toBe("> This text has been selected.\n\n"); }); }); - return describe('with a multi-line selection', function() { - return it('quotes the selected lines as a group', function() { + describe('with a multi-line selection', function() { + it('quotes the selected lines as a group', function() { stubSelection("<p>Selected line one.</p>\n\n<p>Selected line two.</p>\n\n<p>Selected line three.</p>"); this.shortcut.replyWithSelectedText(); - return expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n"); + expect($(this.selector).val()).toBe("> Selected line one.\n>\n> Selected line two.\n>\n> Selected line three.\n\n"); }); }); }); -- cgit v1.2.1 From e48a1755f42eabb2f459028d10d2d6c1160af4af Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Sun, 29 Jan 2017 17:37:24 -0500 Subject: Add traits for the different Event types to the Event factory --- spec/factories/events.rb | 12 +++++++++++ spec/finders/contributed_projects_finder_spec.rb | 13 +++++------- spec/lib/event_filter_spec.rb | 18 ++++++++-------- .../import_export/project_tree_saver_spec.rb | 2 +- spec/models/event_spec.rb | 24 +++++++++++----------- spec/models/user_spec.rb | 8 ++++---- spec/support/import_export/export_file_helper.rb | 2 +- 7 files changed, 44 insertions(+), 35 deletions(-) diff --git a/spec/factories/events.rb b/spec/factories/events.rb index bfe41f71b57..55727d6b62c 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -3,6 +3,18 @@ FactoryGirl.define do project factory: :empty_project author factory: :user + trait(:created) { action Event::CREATED } + trait(:updated) { action Event::UPDATED } + trait(:closed) { action Event::CLOSED } + trait(:reopened) { action Event::REOPENED } + trait(:pushed) { action Event::PUSHED } + trait(:commented) { action Event::COMMENTED } + trait(:merged) { action Event::MERGED } + trait(:joined) { action Event::JOINED } + trait(:left) { action Event::LEFT } + trait(:destroyed) { action Event::DESTROYED } + trait(:expired) { action Event::EXPIRED } + factory :closed_issue_event do action { Event::CLOSED } target factory: :closed_issue diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb index ad2d456529a..34f665826b6 100644 --- a/spec/finders/contributed_projects_finder_spec.rb +++ b/spec/finders/contributed_projects_finder_spec.rb @@ -10,15 +10,12 @@ describe ContributedProjectsFinder do let!(:private_project) { create(:empty_project, :private) } before do - private_project.team << [source_user, Gitlab::Access::MASTER] - private_project.team << [current_user, Gitlab::Access::DEVELOPER] - public_project.team << [source_user, Gitlab::Access::MASTER] + private_project.add_master(source_user) + private_project.add_developer(current_user) + public_project.add_master(source_user) - create(:event, action: Event::PUSHED, project: public_project, - target: public_project, author: source_user) - - create(:event, action: Event::PUSHED, project: private_project, - target: private_project, author: source_user) + create(:event, :pushed, project: public_project, target: public_project, author: source_user) + create(:event, :pushed, project: private_project, target: private_project, author: source_user) end describe 'without a current user' do diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb index e3066311b7d..d70690f589d 100644 --- a/spec/lib/event_filter_spec.rb +++ b/spec/lib/event_filter_spec.rb @@ -5,15 +5,15 @@ describe EventFilter, lib: true do let(:source_user) { create(:user) } let!(:public_project) { create(:empty_project, :public) } - let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) } - let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) } - let!(:created_event) { create(:event, action: Event::CREATED, project: public_project, target: public_project, author: source_user) } - let!(:updated_event) { create(:event, action: Event::UPDATED, project: public_project, target: public_project, author: source_user) } - let!(:closed_event) { create(:event, action: Event::CLOSED, project: public_project, target: public_project, author: source_user) } - let!(:reopened_event) { create(:event, action: Event::REOPENED, project: public_project, target: public_project, author: source_user) } - let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) } - let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) } - let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) } + let!(:push_event) { create(:event, :pushed, project: public_project, target: public_project, author: source_user) } + let!(:merged_event) { create(:event, :merged, project: public_project, target: public_project, author: source_user) } + let!(:created_event) { create(:event, :created, project: public_project, target: public_project, author: source_user) } + let!(:updated_event) { create(:event, :updated, project: public_project, target: public_project, author: source_user) } + let!(:closed_event) { create(:event, :closed, project: public_project, target: public_project, author: source_user) } + let!(:reopened_event) { create(:event, :reopened, project: public_project, target: public_project, author: source_user) } + let!(:comments_event) { create(:event, :commented, project: public_project, target: public_project, author: source_user) } + let!(:joined_event) { create(:event, :joined, project: public_project, target: public_project, author: source_user) } + let!(:left_event) { create(:event, :left, project: public_project, target: public_project, author: source_user) } it 'applies push filter' do events = EventFilter.new(EventFilter.push).apply_filter(Event.all) diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index d480c3821ec..1d65b24c2c9 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -182,7 +182,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do project: project, commit_id: ci_pipeline.sha) - create(:event, target: milestone, project: project, action: Event::CREATED, author: user) + create(:event, :created, target: milestone, project: project, author: user) create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker') project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 349474bb656..8c90a538f57 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -19,7 +19,7 @@ describe Event, models: true do let(:project) { create(:empty_project) } it 'calls the reset_project_activity method' do - expect_any_instance_of(Event).to receive(:reset_project_activity) + expect_any_instance_of(described_class).to receive(:reset_project_activity) create_event(project, project.owner) end @@ -43,33 +43,33 @@ describe Event, models: true do describe '#membership_changed?' do context "created" do - subject { build(:event, action: Event::CREATED).membership_changed? } + subject { build(:event, :created).membership_changed? } it { is_expected.to be_falsey } end context "updated" do - subject { build(:event, action: Event::UPDATED).membership_changed? } + subject { build(:event, :updated).membership_changed? } it { is_expected.to be_falsey } end context "expired" do - subject { build(:event, action: Event::EXPIRED).membership_changed? } + subject { build(:event, :expired).membership_changed? } it { is_expected.to be_truthy } end context "left" do - subject { build(:event, action: Event::LEFT).membership_changed? } + subject { build(:event, :left).membership_changed? } it { is_expected.to be_truthy } end context "joined" do - subject { build(:event, action: Event::JOINED).membership_changed? } + subject { build(:event, :joined).membership_changed? } it { is_expected.to be_truthy } end end describe '#note?' do - subject { Event.new(project: target.project, target: target) } + subject { described_class.new(project: target.project, target: target) } context 'issue note event' do let(:target) { create(:note_on_issue) } @@ -97,7 +97,7 @@ describe Event, models: true do let(:note_on_commit) { create(:note_on_commit, project: project) } let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) } let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) } - let(:event) { Event.new(project: project, target: target, author_id: author.id) } + let(:event) { described_class.new(project: project, target: target, author_id: author.id) } before do project.team << [member, :developer] @@ -221,13 +221,13 @@ describe Event, models: true do let!(:event2) { create(:closed_issue_event) } describe 'without an explicit limit' do - subject { Event.limit_recent } + subject { described_class.limit_recent } it { is_expected.to eq([event2, event1]) } end describe 'with an explicit limit' do - subject { Event.limit_recent(1) } + subject { described_class.limit_recent(1) } it { is_expected.to eq([event2]) } end @@ -294,9 +294,9 @@ describe Event, models: true do } } - Event.create({ + described_class.create({ project: project, - action: Event::PUSHED, + action: described_class::PUSHED, data: data, author_id: user.id }.merge!(attrs)) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6ca5ad747d1..6d58b1455c4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1013,8 +1013,8 @@ describe User, models: true do let!(:project2) { create(:empty_project, forked_from_project: project3) } let!(:project3) { create(:empty_project) } let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) } - let!(:push_event) { create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject) } - let!(:merge_event) { create(:event, action: Event::CREATED, project: project3, target: merge_request, author: subject) } + let!(:push_event) { create(:event, :pushed, project: project1, target: project1, author: subject) } + let!(:merge_event) { create(:event, :created, project: project3, target: merge_request, author: subject) } before do project1.team << [subject, :master] @@ -1058,7 +1058,7 @@ describe User, models: true do let!(:push_data) do Gitlab::DataBuilder::Push.build_sample(project2, subject) end - let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) } + let!(:push_event) { create(:event, :pushed, project: project2, target: project1, author: subject, data: push_data) } before do project1.team << [subject, :master] @@ -1086,7 +1086,7 @@ describe User, models: true do expect(subject.recent_push(project2)).to eq(push_event) push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject) - push_event1 = create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject, data: push_data1) + push_event1 = create(:event, :pushed, project: project1, target: project1, author: subject, data: push_data1) expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest end diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb index 1b0a4583f5c..944ea30656f 100644 --- a/spec/support/import_export/export_file_helper.rb +++ b/spec/support/import_export/export_file_helper.rb @@ -35,7 +35,7 @@ module ExportFileHelper project: project, commit_id: ci_pipeline.sha) - create(:event, target: milestone, project: project, action: Event::CREATED, author: user) + create(:event, :created, target: milestone, project: project, author: user) create(:project_member, :master, user: user, project: project) create(:ci_variable, project: project) create(:ci_trigger, project: project) -- cgit v1.2.1 From dd1c410ea4478400e2650f4102726f2ab1a906bd Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Sun, 29 Jan 2017 17:53:48 -0500 Subject: Reduce the number of loops that Cycle Analytics specs use See https://gitlab.com/gitlab-org/gitlab-ce/issues/27402 --- spec/models/cycle_analytics/code_spec.rb | 22 ++++------ spec/models/cycle_analytics/issue_spec.rb | 12 +++--- spec/models/cycle_analytics/production_spec.rb | 18 +++----- spec/models/cycle_analytics/review_spec.rb | 4 +- spec/models/cycle_analytics/staging_spec.rb | 18 +++----- spec/models/cycle_analytics/test_spec.rb | 50 +++++++++------------- .../cycle_analytics_helpers/test_generation.rb | 50 ++++++++++------------ 7 files changed, 72 insertions(+), 102 deletions(-) diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb index 3b7cc7d9e2e..9053485939e 100644 --- a/spec/models/cycle_analytics/code_spec.rb +++ b/spec/models/cycle_analytics/code_spec.rb @@ -27,15 +27,13 @@ describe 'CycleAnalytics#code', feature: true do context "when a regular merge request (that doesn't close the issue) is created" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) + issue = create(:issue, project: project) - create_commit_referencing_issue(issue) - create_merge_request_closing_issue(issue, message: "Closes nothing") + create_commit_referencing_issue(issue) + create_merge_request_closing_issue(issue, message: "Closes nothing") - merge_merge_requests_closing_issue(issue) - deploy_master - end + merge_merge_requests_closing_issue(issue) + deploy_master expect(subject[:code].median).to be_nil end @@ -60,14 +58,12 @@ describe 'CycleAnalytics#code', feature: true do context "when a regular merge request (that doesn't close the issue) is created" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) + issue = create(:issue, project: project) - create_commit_referencing_issue(issue) - create_merge_request_closing_issue(issue, message: "Closes nothing") + create_commit_referencing_issue(issue) + create_merge_request_closing_issue(issue, message: "Closes nothing") - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:code].median).to be_nil end diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb index 5c73edbbc53..fc7d18bd40e 100644 --- a/spec/models/cycle_analytics/issue_spec.rb +++ b/spec/models/cycle_analytics/issue_spec.rb @@ -33,14 +33,12 @@ describe 'CycleAnalytics#issue', models: true do context "when a regular label (instead of a list label) is added to the issue" do it "returns nil" do - 5.times do - regular_label = create(:label) - issue = create(:issue, project: project) - issue.update(label_ids: [regular_label.id]) + regular_label = create(:label) + issue = create(:issue, project: project) + issue.update(label_ids: [regular_label.id]) - create_merge_request_closing_issue(issue) - merge_merge_requests_closing_issue(issue) - end + create_merge_request_closing_issue(issue) + merge_merge_requests_closing_issue(issue) expect(subject[:issue].median).to be_nil end diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb index 591bbdddf55..2cbee741fb0 100644 --- a/spec/models/cycle_analytics/production_spec.rb +++ b/spec/models/cycle_analytics/production_spec.rb @@ -29,11 +29,9 @@ describe 'CycleAnalytics#production', feature: true do context "when a regular merge request (that doesn't close the issue) is merged and deployed" do it "returns nil" do - 5.times do - merge_request = create(:merge_request) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master - end + merge_request = create(:merge_request) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master expect(subject[:production].median).to be_nil end @@ -41,12 +39,10 @@ describe 'CycleAnalytics#production', feature: true do context "when the deployment happens to a non-production environment" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(environment: 'staging') - end + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master(environment: 'staging') expect(subject[:production].median).to be_nil end diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb index 33d2c0a7416..febb18c9884 100644 --- a/spec/models/cycle_analytics/review_spec.rb +++ b/spec/models/cycle_analytics/review_spec.rb @@ -23,9 +23,7 @@ describe 'CycleAnalytics#review', feature: true do context "when a regular merge request (that doesn't close the issue) is created and merged" do it "returns nil" do - 5.times do - MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) - end + MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) expect(subject[:review].median).to be_nil end diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb index 00693d67475..104e65335dd 100644 --- a/spec/models/cycle_analytics/staging_spec.rb +++ b/spec/models/cycle_analytics/staging_spec.rb @@ -40,11 +40,9 @@ describe 'CycleAnalytics#staging', feature: true do context "when a regular merge request (that doesn't close the issue) is merged and deployed" do it "returns nil" do - 5.times do - merge_request = create(:merge_request) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master - end + merge_request = create(:merge_request) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master expect(subject[:staging].median).to be_nil end @@ -52,12 +50,10 @@ describe 'CycleAnalytics#staging', feature: true do context "when the deployment happens to a non-production environment" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(environment: 'staging') - end + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + MergeRequests::MergeService.new(project, user).execute(merge_request) + deploy_master(environment: 'staging') expect(subject[:staging].median).to be_nil end diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb index f857ea6cbec..c2ba012a0e6 100644 --- a/spec/models/cycle_analytics/test_spec.rb +++ b/spec/models/cycle_analytics/test_spec.rb @@ -24,16 +24,14 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is for a regular merge request (that doesn't close an issue)" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - pipeline.run! - pipeline.succeed! + pipeline.run! + pipeline.succeed! - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:test].median).to be_nil end @@ -41,12 +39,10 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is not for a merge request" do it "returns nil" do - 5.times do - pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha) + pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha) - pipeline.run! - pipeline.succeed! - end + pipeline.run! + pipeline.succeed! expect(subject[:test].median).to be_nil end @@ -54,16 +50,14 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is dropped (failed)" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - pipeline.run! - pipeline.drop! + pipeline.run! + pipeline.drop! - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:test].median).to be_nil end @@ -71,16 +65,14 @@ describe 'CycleAnalytics#test', feature: true do context "when the pipeline is cancelled" do it "returns nil" do - 5.times do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(issue) - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) + issue = create(:issue, project: project) + merge_request = create_merge_request_closing_issue(issue) + pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - pipeline.run! - pipeline.cancel! + pipeline.run! + pipeline.cancel! - merge_merge_requests_closing_issue(issue) - end + merge_merge_requests_closing_issue(issue) expect(subject[:test].median).to be_nil end diff --git a/spec/support/cycle_analytics_helpers/test_generation.rb b/spec/support/cycle_analytics_helpers/test_generation.rb index 10b90b40ba7..19b32c84d81 100644 --- a/spec/support/cycle_analytics_helpers/test_generation.rb +++ b/spec/support/cycle_analytics_helpers/test_generation.rb @@ -63,22 +63,20 @@ module CycleAnalyticsHelpers # test case. allow(self).to receive(:project) { other_project } - 5.times do - data = data_fn[self] - start_time = Time.now - end_time = rand(1..10).days.from_now - - start_time_conditions.each do |condition_name, condition_fn| - Timecop.freeze(start_time) { condition_fn[self, data] } - end + data = data_fn[self] + start_time = Time.now + end_time = rand(1..10).days.from_now - end_time_conditions.each do |condition_name, condition_fn| - Timecop.freeze(end_time) { condition_fn[self, data] } - end + start_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(start_time) { condition_fn[self, data] } + end - Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + end_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(end_time) { condition_fn[self, data] } end + Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + # Turn off the stub before checking assertions allow(self).to receive(:project).and_call_original @@ -114,17 +112,15 @@ module CycleAnalyticsHelpers context "start condition NOT PRESENT: #{start_time_conditions.map(&:first).to_sentence}" do context "end condition: #{end_time_conditions.map(&:first).to_sentence}" do it "returns nil" do - 5.times do - data = data_fn[self] - end_time = rand(1..10).days.from_now - - end_time_conditions.each_with_index do |(condition_name, condition_fn), index| - Timecop.freeze(end_time + index.days) { condition_fn[self, data] } - end + data = data_fn[self] + end_time = rand(1..10).days.from_now - Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + end_time_conditions.each_with_index do |(condition_name, condition_fn), index| + Timecop.freeze(end_time + index.days) { condition_fn[self, data] } end + Timecop.freeze(end_time + 1.day) { post_fn[self, data] } if post_fn + expect(subject[phase].median).to be_nil end end @@ -133,17 +129,15 @@ module CycleAnalyticsHelpers context "start condition: #{start_time_conditions.map(&:first).to_sentence}" do context "end condition NOT PRESENT: #{end_time_conditions.map(&:first).to_sentence}" do it "returns nil" do - 5.times do - data = data_fn[self] - start_time = Time.now - - start_time_conditions.each do |condition_name, condition_fn| - Timecop.freeze(start_time) { condition_fn[self, data] } - end + data = data_fn[self] + start_time = Time.now - post_fn[self, data] if post_fn + start_time_conditions.each do |condition_name, condition_fn| + Timecop.freeze(start_time) { condition_fn[self, data] } end + post_fn[self, data] if post_fn + expect(subject[phase].median).to be_nil end end -- cgit v1.2.1 From d751712dba0642d6f86b9c23224b17a34dba516b Mon Sep 17 00:00:00 2001 From: Bryce Johnson <bryce@gitlab.com> Date: Wed, 1 Feb 2017 16:39:27 -0500 Subject: Vertically center add-diff-note button. --- app/assets/stylesheets/pages/notes.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index da0caa30c26..f310cc72da0 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -467,7 +467,7 @@ ul.notes { } .add-diff-note { - margin-top: -4px; + margin-top: -8px; border-radius: 40px; background: $white-light; padding: 4px; -- cgit v1.2.1 From 21cf64b43140a6e5e9addd514da4304c70bd0815 Mon Sep 17 00:00:00 2001 From: Clement Ho <ClemMakesApps@gmail.com> Date: Wed, 1 Feb 2017 15:40:07 -0600 Subject: Fix filtered search manager spec teaspoon error --- spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 index c8b5c2b36ad..a508dacf7f0 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 @@ -23,6 +23,7 @@ `); spyOn(gl.FilteredSearchManager.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(gl.FilteredSearchManager.prototype, 'cleanup').and.callFake(() => {}); spyOn(gl.FilteredSearchManager.prototype, 'loadSearchParamsFromURL').and.callFake(() => {}); spyOn(gl.FilteredSearchDropdownManager.prototype, 'setDropdown').and.callFake(() => {}); spyOn(gl.utils, 'getParameterByName').and.returnValue(null); -- cgit v1.2.1 From a2d837a3719187a906b60b9212b0dbf02396cb59 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 1 Feb 2017 13:05:33 -0600 Subject: add rack middleware to proxy webpack dev server --- Gemfile | 2 ++ Gemfile.lock | 3 +++ config/application.rb | 2 ++ config/gitlab.yml.example | 10 ++++++++++ config/initializers/1_settings.rb | 9 +++++++++ config/initializers/static_files.rb | 21 +++++++++++++++++++++ lib/gitlab/middleware/webpack_proxy.rb | 24 ++++++++++++++++++++++++ 7 files changed, 71 insertions(+) create mode 100644 lib/gitlab/middleware/webpack_proxy.rb diff --git a/Gemfile b/Gemfile index f9aecca70ff..6af69e8a37a 100644 --- a/Gemfile +++ b/Gemfile @@ -312,6 +312,8 @@ group :development, :test do gem 'activerecord_sane_schema_dumper', '0.2' gem 'stackprof', '~> 0.2.10' + + gem 'rack-proxy', '~> 0.6.0' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 0434fdefcd5..bcf500b16f9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -544,6 +544,8 @@ GEM rack (>= 1.1) rack-protection (1.5.3) rack + rack-proxy (0.6.0) + rack rack-test (0.6.3) rack (>= 1.0) rails (4.2.7.1) @@ -943,6 +945,7 @@ DEPENDENCIES rack-attack (~> 4.4.1) rack-cors (~> 0.4.0) rack-oauth2 (~> 1.2.1) + rack-proxy (~> 0.6.0) rails (= 4.2.7.1) rails-deprecated_sanitizer (~> 1.0.3) rainbow (~> 2.1.0) diff --git a/config/application.rb b/config/application.rb index 4efe73c7798..9088d3c432b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -84,6 +84,8 @@ module Gitlab config.webpack.config_file = "config/webpack.config.js" config.webpack.output_dir = "public/assets/webpack" config.webpack.public_path = "assets/webpack" + + # Webpack dev server configuration is handled in initializers/static_files.rb config.webpack.dev_server.enabled = false # Enable the asset pipeline diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 42e5f105d46..2906633fcbc 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -505,6 +505,16 @@ production: &base # Git timeout to read a commit, in seconds timeout: 10 + ## Webpack settings + # If enabled, this will tell rails to serve frontend assets from the webpack-dev-server running + # on a given port instead of serving directly from /assets/webpack. This is only indended for use + # in development. + webpack: + # dev_server: + # enabled: true + # host: localhost + # port: 3808 + # # 5. Extra customization # ========================== diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 4f33aad8693..ea61aa9e047 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -409,6 +409,15 @@ Settings.rack_attack.git_basic_auth['bantime'] ||= 1.hour Settings['gitaly'] ||= Settingslogic.new({}) Settings.gitaly['socket_path'] ||= ENV['GITALY_SOCKET_PATH'] +# +# Webpack settings +# +Settings['webpack'] ||= Settingslogic.new({}) +Settings.webpack['dev_server'] ||= Settingslogic.new({}) +Settings.webpack.dev_server['enabled'] ||= false +Settings.webpack.dev_server['host'] ||= 'localhost' +Settings.webpack.dev_server['port'] ||= 3808 + # # Testing settings # diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index d6dbf8b9fbf..718cdd51782 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -12,4 +12,25 @@ if app.config.serve_static_files app.paths["public"].first, app.config.static_cache_control ) + + # If webpack-dev-server is configured, proxy webpack's public directory + # instead of looking for static assets + if Gitlab.config.webpack.dev_server.enabled + app.config.webpack.dev_server.merge!( + enabled: true, + host: Gitlab.config.gitlab.host, + port: Gitlab.config.gitlab.port, + https: Gitlab.config.gitlab.https, + manifest_host: Gitlab.config.webpack.dev_server.host, + manifest_port: Gitlab.config.webpack.dev_server.port, + ) + + app.config.middleware.insert_before( + Gitlab::Middleware::Static, + Gitlab::Middleware::WebpackProxy, + proxy_path: app.config.webpack.public_path, + proxy_host: Gitlab.config.webpack.dev_server.host, + proxy_port: Gitlab.config.webpack.dev_server.port, + ) + end end diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb new file mode 100644 index 00000000000..3fe32adeade --- /dev/null +++ b/lib/gitlab/middleware/webpack_proxy.rb @@ -0,0 +1,24 @@ +# This Rack middleware is intended to proxy the webpack assets directory to the +# webpack-dev-server. It is only intended for use in development. + +module Gitlab + module Middleware + class WebpackProxy < Rack::Proxy + def initialize(app = nil, opts = {}) + @proxy_host = opts.fetch(:proxy_host, 'localhost') + @proxy_port = opts.fetch(:proxy_port, 3808) + @proxy_path = opts[:proxy_path] if opts[:proxy_path] + super(app, opts) + end + + def perform_request(env) + unless @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}") + return @app.call(env) + end + + env['HTTP_HOST'] = "#{@proxy_host}:#{@proxy_port}" + super(env) + end + end + end +end -- cgit v1.2.1 From 6b2e5e344cb81070ec69efd0f7029abe1db8163d Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 1 Feb 2017 13:05:52 -0600 Subject: configure webpack dev server port via environment variable --- config/webpack.config.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index bddd181b452..db4ce84f376 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -6,18 +6,10 @@ var webpack = require('webpack'); var StatsPlugin = require('stats-webpack-plugin'); var CompressionPlugin = require("compression-webpack-plugin"); +var ROOT_PATH = path.resolve(__dirname, '..'); var IS_PRODUCTION = process.env.NODE_ENV === 'production'; var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1; -var ROOT_PATH = path.resolve(__dirname, '..'); - -// must match config.webpack.dev_server.port -var DEV_SERVER_PORT; - -try { - DEV_SERVER_PORT = parseInt(fs.readFileSync('../webpack_port'), 10); -} catch (e) { - DEV_SERVER_PORT = 3808; -} +var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808; var config = { context: path.join(ROOT_PATH, 'app/assets/javascripts'), -- cgit v1.2.1 From deb2fa20a9ba97bba9105942c81e7b8ce34d566e Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Wed, 1 Feb 2017 13:51:16 -0600 Subject: remove changes to Procfile --- Procfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Procfile b/Procfile index 5e8f4a962ab..cad738d4292 100644 --- a/Procfile +++ b/Procfile @@ -4,5 +4,4 @@ # web: RAILS_ENV=development bin/web start_foreground worker: RAILS_ENV=development bin/background_jobs start_foreground -webpack: npm run dev-server # mail_room: bundle exec mail_room -q -c config/mail_room.yml -- cgit v1.2.1 From ceb1ebd9590aaddc96cc059735bcf571464a8460 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Thu, 11 Aug 2016 18:30:18 +0300 Subject: Active tense test coverage Ports changes from https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/642 back into CE --- spec/features/notes_on_merge_requests_spec.rb | 6 +++--- spec/helpers/diff_helper_spec.rb | 10 +++++----- spec/javascripts/awards_handler_spec.js | 2 +- spec/lib/gitlab/diff/highlight_spec.rb | 8 ++++---- spec/lib/gitlab/diff/parallel_diff_spec.rb | 2 +- spec/lib/gitlab/highlight_spec.rb | 2 +- spec/lib/gitlab/ldap/access_spec.rb | 2 +- spec/requests/api/builds_spec.rb | 2 +- spec/requests/api/groups_spec.rb | 8 ++++---- spec/requests/api/projects_spec.rb | 2 +- spec/requests/ci/api/builds_spec.rb | 4 ++-- spec/services/event_create_service_spec.rb | 6 +++--- spec/services/merge_requests/close_service_spec.rb | 2 +- spec/tasks/gitlab/mail_google_schema_whitelisting.rb | 2 +- 14 files changed, 29 insertions(+), 29 deletions(-) diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index b785b2f7704..fab2d532e06 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -89,7 +89,7 @@ describe 'Comments', feature: true do end end - it 'should reset the edit note form textarea with the original content of the note if cancelled' do + it 'resets the edit note form textarea with the original content of the note if cancelled' do within('.current-note-edit-form') do fill_in 'note[note]', with: 'Some new content' find('.btn-cancel').click @@ -198,7 +198,7 @@ describe 'Comments', feature: true do end describe 'the note form' do - it "shouldn't add a second form for same row" do + it "does not add a second form for same row" do click_diff_line is_expected. @@ -206,7 +206,7 @@ describe 'Comments', feature: true do count: 1) end - it 'should be removed when canceled' do + it 'is removed when canceled' do is_expected.to have_css('.js-temp-notes-holder') page.within("form[data-line-code='#{line_code}']") do diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 468bcc7badc..eae097126ce 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -134,7 +134,7 @@ describe DiffHelper do let(:new_pos) { 50 } let(:text) { 'some_text' } - it "should generate foldable top match line for inline view with empty text by default" do + it "generates foldable top match line for inline view with empty text by default" do output = diff_match_line old_pos, new_pos expect(output).to be_html_safe @@ -143,7 +143,7 @@ describe DiffHelper do expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: '' end - it "should allow to define text and bottom option" do + it "allows to define text and bottom option" do output = diff_match_line old_pos, new_pos, text: text, bottom: true expect(output).to be_html_safe @@ -152,7 +152,7 @@ describe DiffHelper do expect(output).to have_css 'td:nth-child(3):not(.parallel).line_content.match', text: text end - it "should generate match line for parallel view" do + it "generates match line for parallel view" do output = diff_match_line old_pos, new_pos, text: text, view: :parallel expect(output).to be_html_safe @@ -162,7 +162,7 @@ describe DiffHelper do expect(output).to have_css 'td:nth-child(4).line_content.match.parallel', text: text end - it "should allow to generate only left match line for parallel view" do + it "allows to generate only left match line for parallel view" do output = diff_match_line old_pos, nil, text: text, view: :parallel expect(output).to be_html_safe @@ -171,7 +171,7 @@ describe DiffHelper do expect(output).not_to have_css 'td:nth-child(3)' end - it "should allow to generate only right match line for parallel view" do + it "allows to generate only right match line for parallel view" do output = diff_match_line nil, new_pos, text: text, view: :parallel expect(output).to be_html_safe diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 71446b9df61..f1bfd529983 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -113,7 +113,7 @@ }); }); describe('::getAwardUrl', function() { - return it('should return the url for request', function() { + return it('returns the url for request', function() { return expect(awardsHandler.getAwardUrl()).toBe('http://test.host/frontend-fixtures/issues-project/issues/1/toggle_award_emoji'); }); }); diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index 1e21270d928..5893485634d 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -12,11 +12,11 @@ describe Gitlab::Diff::Highlight, lib: true do context "with a diff file" do let(:subject) { Gitlab::Diff::Highlight.new(diff_file, repository: project.repository).highlight } - it 'should return Gitlab::Diff::Line elements' do + it 'returns Gitlab::Diff::Line elements' do expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) end - it 'should not modify "match" lines' do + it 'does not modify "match" lines' do expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') end @@ -43,11 +43,11 @@ describe Gitlab::Diff::Highlight, lib: true do context "with diff lines" do let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines, repository: project.repository).highlight } - it 'should return Gitlab::Diff::Line elements' do + it 'returns Gitlab::Diff::Line elements' do expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) end - it 'should not modify "match" lines' do + it 'does not modify "match" lines' do expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') end diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb index fe5fa048413..0f779339c54 100644 --- a/spec/lib/gitlab/diff/parallel_diff_spec.rb +++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::Diff::ParallelDiff, lib: true do subject { described_class.new(diff_file) } describe '#parallelize' do - it 'should return an array of arrays containing the parsed diff' do + it 'returns an array of arrays containing the parsed diff' do diff_lines = diff_file.highlighted_diff_lines expected = [ # Unchanged lines diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index fadfe4d378e..e177d883158 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -12,7 +12,7 @@ describe Gitlab::Highlight, lib: true do Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') end - it 'should properly highlight all the lines' do + it 'highlights all the lines properly' do expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n}) expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n}) expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n}) diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index b9d12c3c24c..9dd997aa7dc 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -14,7 +14,7 @@ describe Gitlab::LDAP::Access, lib: true do it { is_expected.to be_falsey } - it 'should block user in GitLab' do + it 'blocks user in GitLab' do expect(access).to receive(:block_user).with(user, 'does not exist anymore') access.allowed? diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 645e36683bc..bd6e23ee769 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -67,7 +67,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'should not return project builds' do + it 'does not return project builds' do expect(response).to have_http_status(401) end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 1187d2e609d..a027c23bb88 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -326,7 +326,7 @@ describe API::Groups, api: true do expect(response).to have_http_status(404) end - it "should only return projects to which user has access" do + it "only returns projects to which user has access" do project3.team << [user3, :developer] get api("/groups/#{group1.id}/projects", user3) @@ -338,7 +338,7 @@ describe API::Groups, api: true do end context "when authenticated as admin" do - it "should return any existing group" do + it "returns any existing group" do get api("/groups/#{group2.id}/projects", admin) expect(response).to have_http_status(200) @@ -346,7 +346,7 @@ describe API::Groups, api: true do expect(json_response.first['name']).to eq(project2.name) end - it "should not return a non existing group" do + it "does not return a non existing group" do get api("/groups/1328/projects", admin) expect(response).to have_http_status(404) @@ -354,7 +354,7 @@ describe API::Groups, api: true do end context 'when using group path in URL' do - it 'should return any existing group' do + it 'returns any existing group' do get api("/groups/#{group1.path}/projects", admin) expect(response).to have_http_status(200) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index a1db81ce18c..753dde0ca3a 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -459,7 +459,7 @@ describe API::Projects, api: true do before { project } before { admin } - it 'should create new project without path and return 201' do + it 'creates new project without path and return 201' do expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1) expect(response).to have_http_status(201) end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 8dbe5f0b025..1cedaa4ba63 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -458,7 +458,7 @@ describe Ci::API::Builds do before { build.run! } describe "POST /builds/:id/artifacts/authorize" do - context "should authorize posting artifact to running build" do + context "authorizes posting artifact to running build" do it "using token as parameter" do post authorize_url, { token: build.token }, headers @@ -492,7 +492,7 @@ describe Ci::API::Builds do end end - context "should fail to post too large artifact" do + context "fails to post too large artifact" do it "using token as parameter" do stub_application_setting(max_artifacts_size: 0) diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index b7dc99ed887..f2c2009bcbf 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -9,7 +9,7 @@ describe EventCreateService, services: true do it { expect(service.open_issue(issue, issue.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.open_issue(issue, issue.author) }.to change { Event.count } end end @@ -19,7 +19,7 @@ describe EventCreateService, services: true do it { expect(service.close_issue(issue, issue.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.close_issue(issue, issue.author) }.to change { Event.count } end end @@ -29,7 +29,7 @@ describe EventCreateService, services: true do it { expect(service.reopen_issue(issue, issue.author)).to be_truthy } - it "should create new event" do + it "creates new event" do expect { service.reopen_issue(issue, issue.author) }.to change { Event.count } end end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 5f6a7716beb..d55a7657c0e 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -29,7 +29,7 @@ describe MergeRequests::CloseService, services: true do it { expect(@merge_request).to be_valid } it { expect(@merge_request).to be_closed } - it 'should execute hooks with close action' do + it 'executes hooks with close action' do expect(service).to have_received(:execute_hooks). with(@merge_request, 'close') end diff --git a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb index 80fc8c48fed..8d1cff7a261 100644 --- a/spec/tasks/gitlab/mail_google_schema_whitelisting.rb +++ b/spec/tasks/gitlab/mail_google_schema_whitelisting.rb @@ -20,7 +20,7 @@ describe 'gitlab:mail_google_schema_whitelisting rake task' do Rake.application.invoke_task "gitlab:mail_google_schema_whitelisting" end - it 'should run the task without errors' do + it 'runs the task without errors' do expect { run_rake_task }.not_to raise_error end end -- cgit v1.2.1 From 94545e58f3be29f40fd4e20bb58324246a1f6a60 Mon Sep 17 00:00:00 2001 From: Valery Sizov <valery@gitlab.com> Date: Thu, 11 Aug 2016 18:30:18 +0300 Subject: Active tense test coverage in pages spec Ports change from https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/642 --- spec/services/pages_service_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb index b4215b2cd02..aa63fe3a5c1 100644 --- a/spec/services/pages_service_spec.rb +++ b/spec/services/pages_service_spec.rb @@ -15,7 +15,7 @@ describe PagesService, services: true do context 'on success' do before { build.success } - it 'should execute worker' do + it 'executes worker' do expect(PagesWorker).to receive(:perform_async) service.execute end @@ -25,7 +25,7 @@ describe PagesService, services: true do context "on #{status}" do before { build.status = status } - it 'should not execute worker' do + it 'does not execute worker' do expect(PagesWorker).not_to receive(:perform_async) service.execute end @@ -39,7 +39,7 @@ describe PagesService, services: true do build.success end - it 'should not execute worker' do + it 'does not execute worker' do expect(PagesWorker).not_to receive(:perform_async) service.execute end -- cgit v1.2.1 From 9677b5387201b2530b5cd18fe2b0bc897eae581e Mon Sep 17 00:00:00 2001 From: James Edwards-Jones <jedwardsjones@gitlab.com> Date: Wed, 1 Feb 2017 22:43:36 +0000 Subject: Excluded pages_domains from import/export spec --- spec/lib/gitlab/import_export/all_models.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 7fb6829f582..6aa10a6509a 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -191,6 +191,7 @@ project: - environments - deployments - project_feature +- pages_domains - authorized_users - project_authorizations - route -- cgit v1.2.1 From 80ad1c62991fd5e3fcdc0e9d803d4df19bef9bf2 Mon Sep 17 00:00:00 2001 From: samrose3 <sam@gitlab.com> Date: Mon, 23 Jan 2017 18:30:03 -0500 Subject: Support non-ASCII characters in GFM autocomplete --- app/assets/javascripts/gfm_auto_complete.js.es6 | 4 ++-- ...n-does-not-suggest-by-non-ascii-characters-in-name.yml | 4 ++++ spec/features/issues/gfm_autocomplete_spec.rb | 15 ++++++++++++++- 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6 index 3f23095dad9..7f1f2a5d278 100644 --- a/app/assets/javascripts/gfm_auto_complete.js.es6 +++ b/app/assets/javascripts/gfm_auto_complete.js.es6 @@ -83,12 +83,12 @@ _a = decodeURI("%C3%80"); _y = decodeURI("%C3%BF"); - regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi'); + regexp = new RegExp("^(?:\\B|[^a-zA-Z0-9_" + atSymbolsWithoutBar + "]|\\s)" + flag + "(?![" + atSymbolsWithBar + "])(([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]|[^\\x00-\\x7a])*)$", 'gi'); match = regexp.exec(subtext); if (match) { - return match[2] || match[1]; + return (match[1] || match[1] === "") ? match[1] : match[2]; } else { return null; } diff --git a/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml new file mode 100644 index 00000000000..1758ed9e9ea --- /dev/null +++ b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml @@ -0,0 +1,4 @@ +--- +title: Support non-ASCII characters in GFM autocomplete +merge_request: 8729 +author: diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 31156fcf994..93139dc9e94 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' feature 'GFM autocomplete', feature: true, js: true do include WaitForAjax - let(:user) { create(:user, username: 'someone.special') } + let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') } let(:project) { create(:project) } let(:label) { create(:label, project: project, title: 'special+') } let(:issue) { create(:issue, project: project) } @@ -59,6 +59,19 @@ feature 'GFM autocomplete', feature: true, js: true do expect(find('#at-view-64')).to have_selector('.cur:first-of-type') end + it 'includes items for assignee dropdowns with non-ASCII characters in name' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys('') + find('#note_note').native.send_keys("@#{user.name[0...8]}") + end + + expect(page).to have_selector('.atwho-container') + + wait_for_ajax + + expect(find('#at-view-64')).to have_content(user.name) + end + it 'selects the first item for non-assignee dropdowns if a query is entered' do page.within '.timeline-content-form' do find('#note_note').native.send_keys('') -- cgit v1.2.1 From 239743345a23c53b2c41509cefb288450d3bb563 Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Thu, 26 Jan 2017 17:58:42 -0800 Subject: Fix GitLab Pages not refreshing upon new content Due to autoloading and Ruby scoping, the .update file was never being updated due to this error: ``` NoMethodError: undefined method `pages' for Projects::Settings:Module from /opt/gitlab/embedded/service/gitlab-rails/app/services/projects/update_pages_configuration_service.rb:50:in `pages_update_file' from /opt/gitlab/embedded/service/gitlab-rails/lib/gitlab/metrics/instrumentation.rb:157:in `pages_update_file' from (irb):6 from /opt/gitlab/embedded/lib/ruby/gems/2.3.0/gems/railties-4.2.7.1/lib/rails/commands/console.rb:110:in `start' from /opt/gitlab/embedded/lib/ruby/gems/2.3.0/gems/railties-4.2.7.1/lib/rails/commands/console.rb:9:in `start' from /opt/gitlab/embedded/lib/ruby/gems/2.3.0/gems/railties-4.2.7.1/lib/rails/commands/commands_tasks.rb:68:in `console' from /opt/gitlab/embedded/lib/ruby/gems/2.3.0/gems/railties-4.2.7.1/lib/rails/commands/commands_tasks.rb:39:in `run_command!' from /opt/gitlab/embedded/lib/ruby/gems/2.3.0/gems/railties-4.2.7.1/lib/rails/commands.rb:17:in `<top (required)>' from bin/rails:9:in `require' ``` This error was caught and discarded quietly. This fix exercises this code and fixes the scope problem. Closes gitlab-com/infrastructure#1058 --- .../projects/update_pages_configuration_service.rb | 2 +- .../update_pages_configuration_service_spec.rb | 24 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 spec/services/projects/update_pages_configuration_service_spec.rb diff --git a/app/services/projects/update_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb index 188847b5ad6..eb4809afa85 100644 --- a/app/services/projects/update_pages_configuration_service.rb +++ b/app/services/projects/update_pages_configuration_service.rb @@ -47,7 +47,7 @@ module Projects end def pages_update_file - File.join(Settings.pages.path, '.update') + File.join(::Settings.pages.path, '.update') end def update_file(file, data) diff --git a/spec/services/projects/update_pages_configuration_service_spec.rb b/spec/services/projects/update_pages_configuration_service_spec.rb new file mode 100644 index 00000000000..8b329bc21c3 --- /dev/null +++ b/spec/services/projects/update_pages_configuration_service_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Projects::UpdatePagesConfigurationService, services: true do + let(:project) { create(:empty_project) } + subject { described_class.new(project) } + + describe "#update" do + let(:file) { Tempfile.new('pages-test') } + + after do + file.close + file.unlink + end + + it 'updates the .update file' do + # Access this reference to ensure scoping works + Projects::Settings # rubocop:disable Lint/Void + expect(subject).to receive(:pages_config_file).and_return(file.path) + expect(subject).to receive(:reload_daemon).and_call_original + + expect(subject.execute).to eq({ status: :success }) + end + end +end -- cgit v1.2.1 From 7cacaf18de37987f15a3c41d1d022621c810d699 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Tue, 10 Jan 2017 15:46:47 +0000 Subject: Fix constant resolution in UpdatePagesService There is now a `Projects::Settings` module, for the members controller. Ensure that we get the actual settings, not that module. --- app/services/projects/update_pages_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index c52f3d3e230..f5f9ee88912 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -130,7 +130,7 @@ module Projects end def tmp_path - @tmp_path ||= File.join(Settings.pages.path, 'tmp') + @tmp_path ||= File.join(::Settings.pages.path, 'tmp') end def pages_path -- cgit v1.2.1 From e7d4b8a03068419794162ffcfa13703c09dbcd02 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Tue, 17 Jan 2017 19:47:42 -0500 Subject: First iteration on Pages refactoring --- doc/pages/administration.md | 352 ++++++++++++++++++++++---------------------- 1 file changed, 176 insertions(+), 176 deletions(-) diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 0e1665fa832..9a94282a229 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -1,8 +1,9 @@ # GitLab Pages Administration -> **Note:** -> This feature was first [introduced][ee-80] in GitLab EE 8.3. -> Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. +> **Notes:** +> - [Introduced][ee-80] in GitLab EE 8.3. +> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. +> - GitLab Pages were ported to Community Edition in GitLab 8.16. --- @@ -14,33 +15,20 @@ configuration. If you are looking for ways to upload your static content in GitLab Pages, you probably want to read the [user documentation](README.md). -## The GitLab Pages daemon - -Starting from GitLab EE 8.5, GitLab Pages make use of the [GitLab Pages daemon], -a simple HTTP server written in Go that can listen on an external IP address -and provide support for custom domains and custom certificates. The GitLab -Pages Daemon supports dynamic certificates through SNI and exposes pages using -HTTP2 by default. - -Here is a brief list with what it is supported when using the pages daemon: - -- Multiple domains per-project -- One TLS certificate per-domain - - Validation of certificate - - Validation of certificate chain - - Validation of private key against certificate +## Overview +GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server +written in Go that can listen on an external IP address and provide support for +custom domains and custom certificates. It supports dynamic certificates through +SNI and exposes pages using HTTP2 by default. You are encouraged to read its [README][pages-readme] to fully understand how it works. -[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages -[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md - -### The GitLab Pages daemon and the case of custom domains +--- In the case of custom domains, the Pages daemon needs to listen on ports `80` and/or `443`. For that reason, there is some flexibility in the way which you -can set it up, so you basically have three choices: +can set it up: 1. Run the pages daemon in the same server as GitLab, listening on a secondary IP 1. Run the pages daemon in a separate server. In that case, the @@ -53,68 +41,18 @@ can set it up, so you basically have three choices: pages will not be able to be served with user provided certificates. For HTTP it's OK to use HTTP or TCP load balancing. -In this document, we will proceed assuming the first option. Let's begin by -installing the pages daemon. - -### Install the Pages daemon - -**Source installations** - -``` -cd /home/git -sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git -cd gitlab-pages -sudo -u git -H git checkout v0.2.1 -sudo -u git -H make -``` - -**Omnibus installations** - -The `gitlab-pages` daemon is included in the Omnibus package. - - -## Configuration - -There are multiple ways to set up GitLab Pages according to what URL scheme you -are willing to support. - -### Configuration prerequisites +In this document, we will proceed assuming the first option. -In the next section you will find all possible scenarios to choose from. +## Prerequisites -In either scenario, you will need: +Before proceeding with the Pages configuration, you will need to: -1. To use the [GitLab Pages daemon](#the-gitlab-pages-daemon) -1. A separate domain -1. A separate Nginx configuration file which needs to be explicitly added in - the server under which GitLab EE runs (Omnibus does that automatically) -1. (Optional) A wildcard certificate for that domain if you decide to serve - pages under HTTPS -1. (Optional but recommended) [Shared runners](../ci/runners/README.md) so that - your users don't have to bring their own - -### Configuration scenarios - -Before proceeding with setting up GitLab Pages, you have to decide which route -you want to take. - -The possible scenarios are depicted in the table below. - -| URL scheme | Option | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `http://page.example.io` | 1 | no | no | no | no | -| `https://page.example.io` | 1 | yes | no | no | no | -| `http://page.example.io` and `http://page.com` | 2 | no | yes | no | yes | -| `https://page.example.io` and `https://page.com` | 2 | yes | redirects to HTTPS | yes | yes | - -As you see from the table above, each URL scheme comes with an option: - -1. Pages enabled, daemon is enabled and NGINX will proxy all requests to the - daemon. Pages daemon doesn't listen to the outside world. -1. Pages enabled, daemon is enabled AND pages has external IP support enabled. - In that case, the pages daemon is running, NGINX still proxies requests to - the daemon but the daemon is also able to receive requests from the outside - world. Custom domains and TLS are supported. +1. Have a separate domain under which the GitLab Pages will be served +1. (Optional) Have a wildcard certificate for that domain if you decide to serve + Pages under HTTPS +1. Configure a wildcard DNS record +1. (Optional but recommended) Enable [Shared runners](../ci/runners/README.md) + so that your users don't have to bring their own ### DNS configuration @@ -129,21 +67,39 @@ host that GitLab runs. For example, an entry would look like this: where `example.io` is the domain under which GitLab Pages will be served and `1.2.3.4` is the IP address of your GitLab instance. +> **Note:** You should not use the GitLab domain to serve user pages. For more information see the [security section](#security). [wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record -## Setting up GitLab Pages +## Configuration -Below are the four scenarios that are described in -[#configuration-scenarios](#configuration-scenarios). +Depending on your needs, you can install GitLab Pages in four different ways. -### Custom domains with HTTPS support +### Option 1. Custom domains with HTTPS support + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes | + +Pages enabled, daemon is enabled AND pages has external IP support enabled. +In that case, the pages daemon is running, NGINX still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. **Source installations:** -1. [Install the pages daemon](#install-the-pages-daemon) +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.1 + sudo -u git -H make + ``` + 1. Edit `gitlab.yml` to look like the example below. You need to change the `host` to the FQDN under which GitLab Pages will be served. Set `external_http` and `external_https` to the secondary IP on which the pages @@ -176,7 +132,19 @@ Below are the four scenarios that are described in gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80 -listen-https 1.1.1.1:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key ``` -1. Make sure to [configure NGINX](#nginx-configuration) properly. +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace + `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + listens to. +1. Restart NGINX 1. [Restart GitLab][restart] --- @@ -197,17 +165,32 @@ Below are the four scenarios that are described in where `1.1.1.1` is the primary IP address that GitLab is listening to and `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. - Read more at the - [NGINX configuration for custom domains](#nginx-configuration-for-custom-domains) - section. 1. [Reconfigure GitLab][reconfigure] -### Custom domains without HTTPS support +### Option 2. Custom domains without HTTPS support + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `http://page.example.io` and `http://page.com` | no | yes | no | yes | + +Pages enabled, daemon is enabled AND pages has external IP support enabled. +In that case, the pages daemon is running, NGINX still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. **Source installations:** -1. [Install the pages daemon](#install-the-pages-daemon) +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.1 + sudo -u git -H make + ``` + 1. Edit `gitlab.yml` to look like the example below. You need to change the `host` to the FQDN under which GitLab Pages will be served. Set `external_http` to the secondary IP on which the pages daemon will listen @@ -236,7 +219,19 @@ Below are the four scenarios that are described in gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80" ``` -1. Make sure to [configure NGINX](#nginx-configuration) properly. +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace + `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + listens to. +1. Restart NGINX 1. [Restart GitLab][restart] --- @@ -254,58 +249,29 @@ Below are the four scenarios that are described in where `1.1.1.1` is the primary IP address that GitLab is listening to and `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. - Read more at the - [NGINX configuration for custom domains](#nginx-configuration-for-custom-domains) - section. 1. [Reconfigure GitLab][reconfigure] -### Wildcard HTTP domain without custom domains +### Option 3. Wildcard HTTPS domain without custom domains -**Source installations:** - -1. [Install the pages daemon](#install-the-pages-daemon) -1. Go to the GitLab installation directory: - - ```bash - cd /home/git/gitlab - ``` - -1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and - the `host` to the FQDN under which GitLab Pages will be served: - - ```yaml - ## GitLab Pages - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - # path: shared/pages - - host: example.io - port: 80 - https: false - ``` - -1. Make sure to [configure NGINX](#nginx-configuration) properly. -1. [Restart GitLab][restart] +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `https://page.example.io` | yes | no | no | no | ---- +Pages enabled, daemon is enabled and NGINX will proxy all requests to the +daemon. Pages daemon doesn't listen to the outside world. -**Omnibus installations:** +**Source installations:** -1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: +1. Install the Pages daemon: - ```ruby - pages_external_url 'http://example.io' ``` - -1. [Reconfigure GitLab][reconfigure] - -### Wildcard HTTPS domain without custom domains - -**Source installations:** - -1. [Install the pages daemon](#install-the-pages-daemon) + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.1 + sudo -u git -H make + ``` 1. In `gitlab.yml`, set the port to `443` and https to `true`: ```bash @@ -320,7 +286,14 @@ Below are the four scenarios that are described in https: true ``` -1. Make sure to [configure NGINX](#nginx-configuration) properly. +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. --- @@ -342,49 +315,76 @@ Below are the four scenarios that are described in 1. [Reconfigure GitLab][reconfigure] -## NGINX configuration +### Option 4. Wildcard HTTP domain without custom domains -Depending on your setup, you will need to make some changes to NGINX. -Specifically you must change the domain name and the IP address where NGINX -listens to. Read the following sections for more details. +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `http://page.example.io` | no | no | no | no | -### NGINX configuration files +Pages enabled, daemon is enabled and NGINX will proxy all requests to the +daemon. Pages daemon doesn't listen to the outside world. -Copy the `gitlab-pages-ssl` Nginx configuration file: +**Source installations:** -```bash -sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf -sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf -``` +1. Install the Pages daemon: -Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.1 + sudo -u git -H make + ``` -### NGINX configuration for custom domains +1. Go to the GitLab installation directory: -> If you are not using custom domains ignore this section. + ```bash + cd /home/git/gitlab + ``` -[In the case of custom domains](#the-gitlab-pages-daemon-and-the-case-of-custom-domains), -if you have the secondary IP address configured on the same server as GitLab, -you need to change **all** NGINX configs to listen on the first IP address. +1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and + the `host` to the FQDN under which GitLab Pages will be served: -**Source installations:** + ```yaml + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 80 + https: false + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. -1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace - `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab - listens to. 1. Restart NGINX +1. [Restart GitLab][restart] + +--- **Omnibus installations:** -1. Edit `/etc/gitlab/gilab.rb`: +1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: - ``` - nginx['listen_addresses'] = ['1.1.1.1'] + ```ruby + pages_external_url 'http://example.io' ``` 1. [Reconfigure GitLab][reconfigure] -### NGINX caveats +## NGINX caveats + +>**Note:** +The following information applies only for installations from source. Be extra careful when setting up the domain name in the NGINX config. You must not remove the backslashes. @@ -462,35 +462,35 @@ latest previous version. --- +**GitLab 8.16 ([documentation][8-16-docs])** + +- GitLab Pages were ported to Community Edition in GitLab 8.16. +- Documentation was refactored to be more modular and easy to follow. + **GitLab 8.5 ([documentation][8-5-docs])** - In GitLab 8.5 we introduced the [gitlab-pages][] daemon which is now the recommended way to set up GitLab Pages. - The [NGINX configs][] have changed to reflect this change. So make sure to update them. -- Custom CNAME and TLS certificates support -- Documentation was moved to one place - -[8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md -[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.1 -[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx +- Custom CNAME and TLS certificates support. +- Documentation was moved to one place. --- -**GitLab 8.4** - -No new changes. - ---- - -**GitLab 8.3 ([source docs][8-3-docs], [Omnibus docs][8-3-omnidocs])** +**GitLab 8.3 ([documentation][8-3-docs])** - GitLab Pages feature was introduced. [8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md -[8-3-omnidocs]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/8-3-stable-ee/doc/settings/pages.md +[8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md +[8-16-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable-ce/doc/pages/administration.md [backup]: ../raketasks/backup_restore.md [ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 [ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 +[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages +[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx +[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md [reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../administration/restart_gitlab.md#installations-from-source +[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.1 -- cgit v1.2.1 From b14ee42ffad4b5f47a9c440d8467677b1f41ce06 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Wed, 18 Jan 2017 12:46:52 -0500 Subject: Move Pages docs to new location --- doc/README.md | 4 +- .../high_availability/load_balancer.md | 2 +- doc/administration/pages/index.md | 497 +++++++++++++++++++++ doc/install/installation.md | 2 +- doc/pages/README.md | 436 +----------------- doc/pages/administration.md | 497 +-------------------- doc/pages/img/pages_create_project.png | Bin 33597 -> 0 bytes doc/pages/img/pages_create_user_page.png | Bin 87071 -> 0 bytes doc/pages/img/pages_dns_details.png | Bin 34686 -> 0 bytes doc/pages/img/pages_multiple_domains.png | Bin 63716 -> 0 bytes doc/pages/img/pages_new_domain_button.png | Bin 51136 -> 0 bytes doc/pages/img/pages_remove.png | Bin 27259 -> 0 bytes doc/pages/img/pages_upload_cert.png | Bin 103730 -> 0 bytes doc/university/README.md | 2 +- doc/university/support/README.md | 2 +- .../project/pages/img/pages_create_project.png | Bin 0 -> 33597 bytes .../project/pages/img/pages_create_user_page.png | Bin 0 -> 87071 bytes doc/user/project/pages/img/pages_dns_details.png | Bin 0 -> 34686 bytes .../project/pages/img/pages_multiple_domains.png | Bin 0 -> 63716 bytes .../project/pages/img/pages_new_domain_button.png | Bin 0 -> 51136 bytes doc/user/project/pages/img/pages_remove.png | Bin 0 -> 27259 bytes doc/user/project/pages/img/pages_upload_cert.png | Bin 0 -> 103730 bytes doc/user/project/pages/index.md | 435 ++++++++++++++++++ 23 files changed, 940 insertions(+), 937 deletions(-) create mode 100644 doc/administration/pages/index.md delete mode 100644 doc/pages/img/pages_create_project.png delete mode 100644 doc/pages/img/pages_create_user_page.png delete mode 100644 doc/pages/img/pages_dns_details.png delete mode 100644 doc/pages/img/pages_multiple_domains.png delete mode 100644 doc/pages/img/pages_new_domain_button.png delete mode 100644 doc/pages/img/pages_remove.png delete mode 100644 doc/pages/img/pages_upload_cert.png create mode 100644 doc/user/project/pages/img/pages_create_project.png create mode 100644 doc/user/project/pages/img/pages_create_user_page.png create mode 100644 doc/user/project/pages/img/pages_dns_details.png create mode 100644 doc/user/project/pages/img/pages_multiple_domains.png create mode 100644 doc/user/project/pages/img/pages_new_domain_button.png create mode 100644 doc/user/project/pages/img/pages_remove.png create mode 100644 doc/user/project/pages/img/pages_upload_cert.png create mode 100644 doc/user/project/pages/index.md diff --git a/doc/README.md b/doc/README.md index 951a302f8ba..6e94f1e8e87 100644 --- a/doc/README.md +++ b/doc/README.md @@ -12,7 +12,7 @@ - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab. - [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry. - [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab. -- [GitLab Pages](pages/README.md) Using GitLab Pages. +- [GitLab Pages](user/project/pages/index.md) Using GitLab Pages. - [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab. - [Importing and exporting projects between instances](user/project/settings/import_export.md). - [Markdown](user/markdown.md) GitLab's advanced formatting system. @@ -54,7 +54,7 @@ - [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE. - [Git LFS configuration](workflow/lfs/lfs_administration.md) - [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast. -- [GitLab Pages configuration](pages/administration.md) +- [GitLab Pages configuration](administration/pages/index.md) Configure GitLab Pages. - [GitLab performance monitoring with InfluxDB](administration/monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics. - [GitLab performance monitoring with Prometheus](administration/monitoring/performance/prometheus.md) Configure GitLab and Prometheus for measuring performance metrics. - [Request Profiling](administration/monitoring/performance/request_profiling.md) Get a detailed profile on slow requests. diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index 1824829903c..dad8e956c0e 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -66,4 +66,4 @@ Read more on high-availability configuration: configure custom domains with custom SSL, which would not be possible if SSL was terminated at the load balancer. -[gitlab-pages]: http://docs.gitlab.com/ee/pages/administration.html +[gitlab-pages]: ../pages/index.md diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md new file mode 100644 index 00000000000..da148a0f2bb --- /dev/null +++ b/doc/administration/pages/index.md @@ -0,0 +1,497 @@ +# GitLab Pages Administration + +> **Notes:** +> - [Introduced][ee-80] in GitLab EE 8.3. +> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. +> - GitLab Pages were ported to Community Edition in GitLab 8.16. + +--- + +This document describes how to set up the _latest_ GitLab Pages feature. Make +sure to read the [changelog](#changelog) if you are upgrading to a new GitLab +version as it may include new features and changes needed to be made in your +configuration. + +If you are looking for ways to upload your static content in GitLab Pages, you +probably want to read the [user documentation][pages-userguide]. + +## Overview + +GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server +written in Go that can listen on an external IP address and provide support for +custom domains and custom certificates. It supports dynamic certificates through +SNI and exposes pages using HTTP2 by default. +You are encouraged to read its [README][pages-readme] to fully understand how +it works. + +--- + +In the case of custom domains, the Pages daemon needs to listen on ports `80` +and/or `443`. For that reason, there is some flexibility in the way which you +can set it up: + +1. Run the pages daemon in the same server as GitLab, listening on a secondary IP +1. Run the pages daemon in a separate server. In that case, the + [Pages path](#change-storage-path) must also be present in the server that + the pages daemon is installed, so you will have to share it via network. +1. Run the pages daemon in the same server as GitLab, listening on the same IP + but on different ports. In that case, you will have to proxy the traffic with + a loadbalancer. If you choose that route note that you should use TCP load + balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the + pages will not be able to be served with user provided certificates. For + HTTP it's OK to use HTTP or TCP load balancing. + +In this document, we will proceed assuming the first option. + +## Prerequisites + +Before proceeding with the Pages configuration, you will need to: + +1. Have a separate domain under which the GitLab Pages will be served +1. (Optional) Have a wildcard certificate for that domain if you decide to serve + Pages under HTTPS +1. Configure a wildcard DNS record +1. (Optional but recommended) Enable [Shared runners](../ci/runners/README.md) + so that your users don't have to bring their own + +### DNS configuration + +GitLab Pages expect to run on their own virtual host. In your DNS server/provider +you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the +host that GitLab runs. For example, an entry would look like this: + +``` +*.example.io. 1800 IN A 1.2.3.4 +``` + +where `example.io` is the domain under which GitLab Pages will be served +and `1.2.3.4` is the IP address of your GitLab instance. + +> **Note:** +You should not use the GitLab domain to serve user pages. For more information +see the [security section](#security). + +[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record + +## Configuration + +Depending on your needs, you can install GitLab Pages in four different ways. + +### Option 1. Custom domains with HTTPS support + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes | + +Pages enabled, daemon is enabled AND pages has external IP support enabled. +In that case, the pages daemon is running, NGINX still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. + +**Source installations:** + +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.1 + sudo -u git -H make + ``` + +1. Edit `gitlab.yml` to look like the example below. You need to change the + `host` to the FQDN under which GitLab Pages will be served. Set + `external_http` and `external_https` to the secondary IP on which the pages + daemon will listen for connections: + + ```yaml + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 443 + https: true + + external_http: 1.1.1.1:80 + external_https: 1.1.1.1:443 + ``` + +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`, + `external_http` and `external_https` settings that you set above respectively. + The `-root-cert` and `-root-key` settings are the wildcard TLS certificates + of the `example.io` domain: + + ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80 -listen-https 1.1.1.1:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace + `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + listens to. +1. Restart NGINX +1. [Restart GitLab][restart] + +--- + +**Omnibus installations:** + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + pages_external_url "https://example.io" + nginx['listen_addresses'] = ['1.1.1.1'] + pages_nginx['enable'] = false + gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt" + gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key" + gitlab_pages['external_http'] = '1.1.1.2:80' + gitlab_pages['external_https'] = '1.1.1.2:443' + ``` + + where `1.1.1.1` is the primary IP address that GitLab is listening to and + `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + +1. [Reconfigure GitLab][reconfigure] + +### Option 2. Custom domains without HTTPS support + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `http://page.example.io` and `http://page.com` | no | yes | no | yes | + +Pages enabled, daemon is enabled AND pages has external IP support enabled. +In that case, the pages daemon is running, NGINX still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. + +**Source installations:** + +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.1 + sudo -u git -H make + ``` + +1. Edit `gitlab.yml` to look like the example below. You need to change the + `host` to the FQDN under which GitLab Pages will be served. Set + `external_http` to the secondary IP on which the pages daemon will listen + for connections: + + ```yaml + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 80 + https: false + + external_http: 1.1.1.1:80 + ``` + +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain` and `-listen-http` must match the `host` and `external_http` + settings that you set above respectively: + + ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80" + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace + `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + listens to. +1. Restart NGINX +1. [Restart GitLab][restart] + +--- + +**Omnibus installations:** + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + pages_external_url "http://example.io" + nginx['listen_addresses'] = ['1.1.1.1'] + pages_nginx['enable'] = false + gitlab_pages['external_http'] = '1.1.1.2:80' + ``` + + where `1.1.1.1` is the primary IP address that GitLab is listening to and + `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + +1. [Reconfigure GitLab][reconfigure] + +### Option 3. Wildcard HTTPS domain without custom domains + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `https://page.example.io` | yes | no | no | no | + +Pages enabled, daemon is enabled and NGINX will proxy all requests to the +daemon. Pages daemon doesn't listen to the outside world. + +**Source installations:** + +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.1 + sudo -u git -H make + ``` +1. In `gitlab.yml`, set the port to `443` and https to `true`: + + ```bash + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 443 + https: true + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +--- + +**Omnibus installations:** + +1. Place the certificate and key inside `/etc/gitlab/ssl` +1. In `/etc/gitlab/gitlab.rb` specify the following configuration: + + ```ruby + pages_external_url 'https://example.io' + + pages_nginx['redirect_http_to_https'] = true + pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt" + pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key" + ``` + + where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key, + respectively. + +1. [Reconfigure GitLab][reconfigure] + +### Option 4. Wildcard HTTP domain without custom domains + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `http://page.example.io` | no | no | no | no | + +Pages enabled, daemon is enabled and NGINX will proxy all requests to the +daemon. Pages daemon doesn't listen to the outside world. + +**Source installations:** + +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.1 + sudo -u git -H make + ``` + +1. Go to the GitLab installation directory: + + ```bash + cd /home/git/gitlab + ``` + +1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and + the `host` to the FQDN under which GitLab Pages will be served: + + ```yaml + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 80 + https: false + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Restart NGINX +1. [Restart GitLab][restart] + +--- + +**Omnibus installations:** + +1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: + + ```ruby + pages_external_url 'http://example.io' + ``` + +1. [Reconfigure GitLab][reconfigure] + +## NGINX caveats + +>**Note:** +The following information applies only for installations from source. + +Be extra careful when setting up the domain name in the NGINX config. You must +not remove the backslashes. + +If your GitLab pages domain is `example.io`, replace: + +```bash +server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$; +``` + +with: + +``` +server_name ~^.*\.example\.io$; +``` + +If you are using a subdomain, make sure to escape all dots (`.`) except from +the first one with a backslash (\). For example `pages.example.io` would be: + +``` +server_name ~^.*\.pages\.example\.io$; +``` + +## Set maximum pages size + +The maximum size of the unpacked archive per project can be configured in the +Admin area under the Application settings in the **Maximum size of pages (MB)**. +The default is 100MB. + +## Change storage path + +**Source installations:** + +1. Pages are stored by default in `/home/git/gitlab/shared/pages`. + If you wish to store them in another location you must set it up in + `gitlab.yml` under the `pages` section: + + ```yaml + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + path: /mnt/storage/pages + ``` + +1. [Restart GitLab][restart] + +**Omnibus installations:** + +1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`. + If you wish to store them in another location you must set it up in + `/etc/gitlab/gitlab.rb`: + + ```ruby + gitlab_rails['pages_path'] = "/mnt/storage/pages" + ``` + +1. [Reconfigure GitLab][reconfigure] + +## Backup + +Pages are part of the [regular backup][backup] so there is nothing to configure. + +## Security + +You should strongly consider running GitLab pages under a different hostname +than GitLab to prevent XSS attacks. + +## Changelog + +GitLab Pages were first introduced in GitLab EE 8.3. Since then, many features +where added, like custom CNAME and TLS support, and many more are likely to +come. Below is a brief changelog. If no changes were introduced or a version is +missing from the changelog, assume that the documentation is the same as the +latest previous version. + +--- + +**GitLab 8.16 ([documentation][8-16-docs])** + +- GitLab Pages were ported to Community Edition in GitLab 8.16. +- Documentation was refactored to be more modular and easy to follow. + +**GitLab 8.5 ([documentation][8-5-docs])** + +- In GitLab 8.5 we introduced the [gitlab-pages][] daemon which is now the + recommended way to set up GitLab Pages. +- The [NGINX configs][] have changed to reflect this change. So make sure to + update them. +- Custom CNAME and TLS certificates support. +- Documentation was moved to one place. + +--- + +**GitLab 8.3 ([documentation][8-3-docs])** + +- GitLab Pages feature was introduced. + +[8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md +[8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md +[8-16-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable-ce/doc/administration/pages/index.md +[backup]: ../raketasks/backup_restore.md +[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 +[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 +[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages +[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx +[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md +[pages-userguide]: ../../user/project/pages/index.md +[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: ../administration/restart_gitlab.md#installations-from-source +[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.1 diff --git a/doc/install/installation.md b/doc/install/installation.md index 4496243da25..276e7f6916e 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -489,7 +489,7 @@ Make sure to edit the config file to match your setup. Also, ensure that you mat If you intend to enable GitLab pages, there is a separate Nginx config you need to use. Read all about the needed configuration at the -[GitLab Pages administration guide](../pages/administration.md). +[GitLab Pages administration guide](../administration/pages/index.md). **Note:** If you want to use HTTPS, replace the `gitlab` Nginx config with `gitlab-ssl`. See [Using HTTPS](#using-https) for HTTPS configuration details. diff --git a/doc/pages/README.md b/doc/pages/README.md index e427d7f283d..44b74513fd9 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -1,435 +1 @@ -# GitLab Pages - -> **Note:** -> This feature was [introduced][ee-80] in GitLab EE 8.3. -> Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. - -> **Note:** -> This document is about the user guide. To learn how to enable GitLab Pages -> across your GitLab instance, visit the [administrator documentation](administration.md). - -With GitLab Pages you can host for free your static websites on GitLab. -Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can -deploy static pages for your individual projects, your user or your group. - -Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific -information, if you are using GitLab.com to host your website. - -## Getting started with GitLab Pages - -> **Note:** -> In the rest of this document we will assume that the general domain name that -> is used for GitLab Pages is `example.io`. - -In general there are two types of pages one might create: - -- Pages per user (`username.example.io`) or per group (`groupname.example.io`) -- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`) - -In GitLab, usernames and groupnames are unique and we often refer to them -as namespaces. There can be only one namespace in a GitLab instance. Below you -can see the connection between the type of GitLab Pages, what the project name -that is created on GitLab looks like and the website URL it will be ultimately -be served on. - -| Type of GitLab Pages | The name of the project created in GitLab | Website URL | -| -------------------- | ------------ | ----------- | -| User pages | `username.example.io` | `http(s)://username.example.io` | -| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | -| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` | -| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`| - -> **Warning:** -> There are some known [limitations](#limitations) regarding namespaces served -> under the general domain name and HTTPS. Make sure to read that section. - -### GitLab Pages requirements - -In brief, this is what you need to upload your website in GitLab Pages: - -1. Find out the general domain name that is used for GitLab Pages - (ask your administrator). This is very important, so you should first make - sure you get that right. -1. Create a project -1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory - of your repository with a specific job named [`pages`][pages] -1. Set up a GitLab Runner to build your website - -> **Note:** -> If [shared runners](../ci/runners/README.md) are enabled by your GitLab -> administrator, you should be able to use them instead of bringing your own. - -### User or group Pages - -For user and group pages, the name of the project should be specific to the -username or groupname and the general domain name that is used for GitLab Pages. -Head over your GitLab instance that supports GitLab Pages and create a -repository named `username.example.io`, where `username` is your username on -GitLab. If the first part of the project name doesn't match exactly your -username, it won’t work, so make sure to get it right. - -To create a group page, the steps are the same like when creating a website for -users. Just make sure that you are creating the project within the group's -namespace. - -![Create a user-based pages project](img/pages_create_user_page.png) - ---- - -After you push some static content to your repository and GitLab Runner uploads -the artifacts to GitLab CI, you will be able to access your website under -`http(s)://username.example.io`. Keep reading to find out how. - ->**Note:** -If your username/groupname contains a dot, for example `foo.bar`, you will not -be able to use the wildcard domain HTTPS, read more at [limitations](#limitations). - -### Project Pages - -GitLab Pages for projects can be created by both user and group accounts. -The steps to create a project page for a user or a group are identical: - -1. Create a new project -1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory - of your repository with a specific job named [`pages`][pages]. -1. Set up a GitLab Runner to build your website - -A user's project will be served under `http(s)://username.example.io/projectname` -whereas a group's project under `http(s)://groupname.example.io/projectname`. - -### Explore the contents of `.gitlab-ci.yml` - -The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that -gives you absolute control over the build process. You can actually watch your -website being built live by following the CI build traces. - -> **Note:** -> Before reading this section, make sure you familiarize yourself with GitLab CI -> and the specific syntax of[`.gitlab-ci.yml`](../ci/yaml/README.md) by -> following our [quick start guide](../ci/quick_start/README.md). - -To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the -rules below: - -1. A special job named [`pages`][pages] must be defined -1. Any static content which will be served by GitLab Pages must be placed under - a `public/` directory -1. `artifacts` with a path to the `public/` directory must be defined - -In its simplest form, `.gitlab-ci.yml` looks like: - -```yaml -pages: - script: - - my_commands - artifacts: - paths: - - public -``` - -When the Runner reaches to build the `pages` job, it executes whatever is -defined in the `script` parameter and if the build completes with a non-zero -exit status, it then uploads the `public/` directory to GitLab Pages. - -The `public/` directory should contain all the static content of your website. -Depending on how you plan to publish your website, the steps defined in the -[`script` parameter](../ci/yaml/README.md#script) may differ. - -Be aware that Pages are by default branch/tag agnostic and their deployment -relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the -`pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except), -whenever a new commit is pushed to whatever branch or tag, the Pages will be -overwritten. In the example below, we limit the Pages to be deployed whenever -a commit is pushed only on the `master` branch: - -```yaml -pages: - script: - - my_commands - artifacts: - paths: - - public - only: - - master -``` - -We then tell the Runner to treat the `public/` directory as `artifacts` and -upload it to GitLab. And since all these parameters were all under a `pages` -job, the contents of the `public` directory will be served by GitLab Pages. - -#### How `.gitlab-ci.yml` looks like when the static content is in your repository - -Supposedly your repository contained the following files: - -``` -├── index.html -├── css -│   └── main.css -└── js - └── main.js -``` - -Then the `.gitlab-ci.yml` example below simply moves all files from the root -directory of the project to the `public/` directory. The `.public` workaround -is so `cp` doesn't also copy `public/` to itself in an infinite loop: - -```yaml -pages: - script: - - mkdir .public - - cp -r * .public - - mv .public public - artifacts: - paths: - - public - only: - - master -``` - -#### How `.gitlab-ci.yml` looks like when using a static generator - -In general, GitLab Pages support any kind of [static site generator][staticgen], -since `.gitlab-ci.yml` can be configured to run any possible command. - -In the root directory of your Git repository, place the source files of your -favorite static generator. Then provide a `.gitlab-ci.yml` file which is -specific to your static generator. - -The example below, uses [Jekyll] to build the static site: - -```yaml -image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1 - -pages: # the build job must be named pages - script: - - gem install jekyll # we install jekyll - - jekyll build -d public/ # we tell jekyll to build the site for us - artifacts: - paths: - - public # this is where the site will live and the Runner uploads it in GitLab - only: - - master # this script is only affecting the master branch -``` - -Here, we used the Docker executor and in the first line we specified the base -image against which our builds will run. - -You have to make sure that the generated static files are ultimately placed -under the `public` directory, that's why in the `script` section we run the -`jekyll` command that builds the website and puts all content in the `public/` -directory. Depending on the static generator of your choice, this command will -differ. Search in the documentation of the static generator you will use if -there is an option to explicitly set the output directory. If there is not -such an option, you can always add one more line under `script` to rename the -resulting directory in `public/`. - -We then tell the Runner to treat the `public/` directory as `artifacts` and -upload it to GitLab. - ---- - -See the [jekyll example project][pages-jekyll] to better understand how this -works. - -For a list of Pages projects, see the [example projects](#example-projects) to -get you started. - -#### How to set up GitLab Pages in a repository where there's also actual code - -Remember that GitLab Pages are by default branch/tag agnostic and their -deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit -the `pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except), -whenever a new commit is pushed to a branch that will be used specifically for -your pages. - -That way, you can have your project's code in the `master` branch and use an -orphan branch (let's name it `pages`) that will host your static generator site. - -You can create a new empty branch like this: - -```bash -git checkout --orphan pages -``` - -The first commit made on this new branch will have no parents and it will be -the root of a new history totally disconnected from all the other branches and -commits. Push the source files of your static generator in the `pages` branch. - -Below is a copy of `.gitlab-ci.yml` where the most significant line is the last -one, specifying to execute everything in the `pages` branch: - -``` -image: ruby:2.1 - -pages: - script: - - gem install jekyll - - jekyll build -d public/ - artifacts: - paths: - - public - only: - - pages -``` - -See an example that has different files in the [`master` branch][jekyll-master] -and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which -also includes `.gitlab-ci.yml`. - -[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master -[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages - -## Next steps - -So you have successfully deployed your website, congratulations! Let's check -what more you can do with GitLab Pages. - -### Example projects - -Below is a list of example projects for GitLab Pages with a plain HTML website -or various static site generators. Contributions are very welcome. - -- [Plain HTML](https://gitlab.com/pages/plain-html) -- [Jekyll](https://gitlab.com/pages/jekyll) -- [Hugo](https://gitlab.com/pages/hugo) -- [Middleman](https://gitlab.com/pages/middleman) -- [Hexo](https://gitlab.com/pages/hexo) -- [Brunch](https://gitlab.com/pages/brunch) -- [Metalsmith](https://gitlab.com/pages/metalsmith) -- [Harp](https://gitlab.com/pages/harp) - -Visit the GitLab Pages group for a full list of example projects: -<https://gitlab.com/groups/pages>. - -### Add a custom domain to your Pages website - -If this setting is enabled by your GitLab administrator, you should be able to -see the **New Domain** button when visiting your project's settings through the -gear icon in the top right and then navigating to **Pages**. - -![New domain button](img/pages_new_domain_button.png) - ---- - -You can add multiple domains pointing to your website hosted under GitLab. -Once the domain is added, you can see it listed under the **Domains** section. - -![Pages multiple domains](img/pages_multiple_domains.png) - ---- - -As a last step, you need to configure your DNS and add a CNAME pointing to your -user/group page. Click on the **Details** button of a domain for further -instructions. - -![Pages DNS details](img/pages_dns_details.png) - ---- - ->**Note:** -Currently there is support only for custom domains on per-project basis. That -means that if you add a custom domain (`example.com`) for your user website -(`username.example.io`), a project that is served under `username.example.io/foo`, -will not be accessible under `example.com/foo`. - -### Secure your custom domain website with TLS - -When you add a new custom domain, you also have the chance to add a TLS -certificate. If this setting is enabled by your GitLab administrator, you -should be able to see the option to upload the public certificate and the -private key when adding a new domain. - -![Pages upload cert](img/pages_upload_cert.png) - -### Custom error codes pages - -You can provide your own 403 and 404 error pages by creating the `403.html` and -`404.html` files respectively in the root directory of the `public/` directory -that will be included in the artifacts. Usually this is the root directory of -your project, but that may differ depending on your static generator -configuration. - -If the case of `404.html`, there are different scenarios. For example: - -- If you use project Pages (served under `/projectname/`) and try to access - `/projectname/non/exsiting_file`, GitLab Pages will try to serve first - `/projectname/404.html`, and then `/404.html`. -- If you use user/group Pages (served under `/`) and try to access - `/non/existing_file` GitLab Pages will try to serve `/404.html`. -- If you use a custom domain and try to access `/non/existing_file`, GitLab - Pages will try to serve only `/404.html`. - -### Remove the contents of your pages - -If you ever feel the need to purge your Pages content, you can do so by going -to your project's settings through the gear icon in the top right, and then -navigating to **Pages**. Hit the **Remove pages** button and your Pages website -will be deleted. Simple as that. - -![Remove pages](img/pages_remove.png) - -## GitLab Pages on GitLab.com - -If you are using GitLab.com to host your website, then: - -- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`. -- Custom domains and TLS support are enabled. -- Shared runners are enabled by default, provided for free and can be used to - build your website. If you want you can still bring your own Runner. - -The rest of the guide still applies. - -## Limitations - -When using Pages under the general domain of a GitLab instance (`*.example.io`), -you _cannot_ use HTTPS with sub-subdomains. That means that if your -username/groupname contains a dot, for example `foo.bar`, the domain -`https://foo.bar.example.io` will _not_ work. This is a limitation of the -[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you -don't redirect HTTP to HTTPS. - -[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC" - -## Redirects in GitLab Pages - -Since you cannot use any custom server configuration files, like `.htaccess` or -any `.conf` file for that matter, if you want to redirect a web page to another -location, you can use the [HTTP meta refresh tag][metarefresh]. - -Some static site generators provide plugins for that functionality so that you -don't have to create and edit HTML files manually. For example, Jekyll has the -[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from). - -## Frequently Asked Questions - -### Can I download my generated pages? - -Sure. All you need to do is download the artifacts archive from the build page. - -### Can I use GitLab Pages if my project is private? - -Yes. GitLab Pages don't care whether you set your project's visibility level -to private, internal or public. - -### Do I need to create a user/group website before creating a project website? - -No, you don't. You can create your project first and it will be accessed under -`http(s)://namespace.example.io/projectname`. - -## Known issues - -For a list of known issues, visit GitLab's [public issue tracker]. - ---- - -[jekyll]: http://jekyllrb.com/ -[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 -[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 -[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages -[gitlab ci]: https://about.gitlab.com/gitlab-ci -[gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner -[pages]: ../ci/yaml/README.md#pages -[staticgen]: https://www.staticgen.com/ -[pages-jekyll]: https://gitlab.com/pages/jekyll -[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh -[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages +This document was moved to [user/project/pages](../user/project/pages). diff --git a/doc/pages/administration.md b/doc/pages/administration.md index 9a94282a229..4eb3bb32c77 100644 --- a/doc/pages/administration.md +++ b/doc/pages/administration.md @@ -1,496 +1 @@ -# GitLab Pages Administration - -> **Notes:** -> - [Introduced][ee-80] in GitLab EE 8.3. -> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. -> - GitLab Pages were ported to Community Edition in GitLab 8.16. - ---- - -This document describes how to set up the _latest_ GitLab Pages feature. Make -sure to read the [changelog](#changelog) if you are upgrading to a new GitLab -version as it may include new features and changes needed to be made in your -configuration. - -If you are looking for ways to upload your static content in GitLab Pages, you -probably want to read the [user documentation](README.md). - -## Overview - -GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server -written in Go that can listen on an external IP address and provide support for -custom domains and custom certificates. It supports dynamic certificates through -SNI and exposes pages using HTTP2 by default. -You are encouraged to read its [README][pages-readme] to fully understand how -it works. - ---- - -In the case of custom domains, the Pages daemon needs to listen on ports `80` -and/or `443`. For that reason, there is some flexibility in the way which you -can set it up: - -1. Run the pages daemon in the same server as GitLab, listening on a secondary IP -1. Run the pages daemon in a separate server. In that case, the - [Pages path](#change-storage-path) must also be present in the server that - the pages daemon is installed, so you will have to share it via network. -1. Run the pages daemon in the same server as GitLab, listening on the same IP - but on different ports. In that case, you will have to proxy the traffic with - a loadbalancer. If you choose that route note that you should use TCP load - balancing for HTTPS. If you use TLS-termination (HTTPS-load balancing) the - pages will not be able to be served with user provided certificates. For - HTTP it's OK to use HTTP or TCP load balancing. - -In this document, we will proceed assuming the first option. - -## Prerequisites - -Before proceeding with the Pages configuration, you will need to: - -1. Have a separate domain under which the GitLab Pages will be served -1. (Optional) Have a wildcard certificate for that domain if you decide to serve - Pages under HTTPS -1. Configure a wildcard DNS record -1. (Optional but recommended) Enable [Shared runners](../ci/runners/README.md) - so that your users don't have to bring their own - -### DNS configuration - -GitLab Pages expect to run on their own virtual host. In your DNS server/provider -you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the -host that GitLab runs. For example, an entry would look like this: - -``` -*.example.io. 1800 IN A 1.2.3.4 -``` - -where `example.io` is the domain under which GitLab Pages will be served -and `1.2.3.4` is the IP address of your GitLab instance. - -> **Note:** -You should not use the GitLab domain to serve user pages. For more information -see the [security section](#security). - -[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record - -## Configuration - -Depending on your needs, you can install GitLab Pages in four different ways. - -### Option 1. Custom domains with HTTPS support - -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes | - -Pages enabled, daemon is enabled AND pages has external IP support enabled. -In that case, the pages daemon is running, NGINX still proxies requests to -the daemon but the daemon is also able to receive requests from the outside -world. Custom domains and TLS are supported. - -**Source installations:** - -1. Install the Pages daemon: - - ``` - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git - cd gitlab-pages - sudo -u git -H git checkout v0.2.1 - sudo -u git -H make - ``` - -1. Edit `gitlab.yml` to look like the example below. You need to change the - `host` to the FQDN under which GitLab Pages will be served. Set - `external_http` and `external_https` to the secondary IP on which the pages - daemon will listen for connections: - - ```yaml - ## GitLab Pages - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - # path: shared/pages - - host: example.io - port: 443 - https: true - - external_http: 1.1.1.1:80 - external_https: 1.1.1.1:443 - ``` - -1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in - order to enable the pages daemon. In `gitlab_pages_options` the - `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`, - `external_http` and `external_https` settings that you set above respectively. - The `-root-cert` and `-root-key` settings are the wildcard TLS certificates - of the `example.io` domain: - - ``` - gitlab_pages_enabled=true - gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80 -listen-https 1.1.1.1:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key - ``` - -1. Copy the `gitlab-pages-ssl` Nginx configuration file: - - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf - ``` - - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - -1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace - `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab - listens to. -1. Restart NGINX -1. [Restart GitLab][restart] - ---- - -**Omnibus installations:** - -1. Edit `/etc/gitlab/gitlab.rb`: - - ```ruby - pages_external_url "https://example.io" - nginx['listen_addresses'] = ['1.1.1.1'] - pages_nginx['enable'] = false - gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt" - gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key" - gitlab_pages['external_http'] = '1.1.1.2:80' - gitlab_pages['external_https'] = '1.1.1.2:443' - ``` - - where `1.1.1.1` is the primary IP address that GitLab is listening to and - `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. - -1. [Reconfigure GitLab][reconfigure] - -### Option 2. Custom domains without HTTPS support - -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `http://page.example.io` and `http://page.com` | no | yes | no | yes | - -Pages enabled, daemon is enabled AND pages has external IP support enabled. -In that case, the pages daemon is running, NGINX still proxies requests to -the daemon but the daemon is also able to receive requests from the outside -world. Custom domains and TLS are supported. - -**Source installations:** - -1. Install the Pages daemon: - - ``` - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git - cd gitlab-pages - sudo -u git -H git checkout v0.2.1 - sudo -u git -H make - ``` - -1. Edit `gitlab.yml` to look like the example below. You need to change the - `host` to the FQDN under which GitLab Pages will be served. Set - `external_http` to the secondary IP on which the pages daemon will listen - for connections: - - ```yaml - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - # path: shared/pages - - host: example.io - port: 80 - https: false - - external_http: 1.1.1.1:80 - ``` - -1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in - order to enable the pages daemon. In `gitlab_pages_options` the - `-pages-domain` and `-listen-http` must match the `host` and `external_http` - settings that you set above respectively: - - ``` - gitlab_pages_enabled=true - gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80" - ``` - -1. Copy the `gitlab-pages-ssl` Nginx configuration file: - - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf - ``` - - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - -1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace - `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab - listens to. -1. Restart NGINX -1. [Restart GitLab][restart] - ---- - -**Omnibus installations:** - -1. Edit `/etc/gitlab/gitlab.rb`: - - ```ruby - pages_external_url "http://example.io" - nginx['listen_addresses'] = ['1.1.1.1'] - pages_nginx['enable'] = false - gitlab_pages['external_http'] = '1.1.1.2:80' - ``` - - where `1.1.1.1` is the primary IP address that GitLab is listening to and - `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. - -1. [Reconfigure GitLab][reconfigure] - -### Option 3. Wildcard HTTPS domain without custom domains - -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `https://page.example.io` | yes | no | no | no | - -Pages enabled, daemon is enabled and NGINX will proxy all requests to the -daemon. Pages daemon doesn't listen to the outside world. - -**Source installations:** - -1. Install the Pages daemon: - - ``` - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git - cd gitlab-pages - sudo -u git -H git checkout v0.2.1 - sudo -u git -H make - ``` -1. In `gitlab.yml`, set the port to `443` and https to `true`: - - ```bash - ## GitLab Pages - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - # path: shared/pages - - host: example.io - port: 443 - https: true - ``` - -1. Copy the `gitlab-pages-ssl` Nginx configuration file: - - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf - ``` - - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - ---- - -**Omnibus installations:** - -1. Place the certificate and key inside `/etc/gitlab/ssl` -1. In `/etc/gitlab/gitlab.rb` specify the following configuration: - - ```ruby - pages_external_url 'https://example.io' - - pages_nginx['redirect_http_to_https'] = true - pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt" - pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key" - ``` - - where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key, - respectively. - -1. [Reconfigure GitLab][reconfigure] - -### Option 4. Wildcard HTTP domain without custom domains - -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `http://page.example.io` | no | no | no | no | - -Pages enabled, daemon is enabled and NGINX will proxy all requests to the -daemon. Pages daemon doesn't listen to the outside world. - -**Source installations:** - -1. Install the Pages daemon: - - ``` - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git - cd gitlab-pages - sudo -u git -H git checkout v0.2.1 - sudo -u git -H make - ``` - -1. Go to the GitLab installation directory: - - ```bash - cd /home/git/gitlab - ``` - -1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and - the `host` to the FQDN under which GitLab Pages will be served: - - ```yaml - ## GitLab Pages - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - # path: shared/pages - - host: example.io - port: 80 - https: false - ``` - -1. Copy the `gitlab-pages-ssl` Nginx configuration file: - - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf - ``` - - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - -1. Restart NGINX -1. [Restart GitLab][restart] - ---- - -**Omnibus installations:** - -1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: - - ```ruby - pages_external_url 'http://example.io' - ``` - -1. [Reconfigure GitLab][reconfigure] - -## NGINX caveats - ->**Note:** -The following information applies only for installations from source. - -Be extra careful when setting up the domain name in the NGINX config. You must -not remove the backslashes. - -If your GitLab pages domain is `example.io`, replace: - -```bash -server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$; -``` - -with: - -``` -server_name ~^.*\.example\.io$; -``` - -If you are using a subdomain, make sure to escape all dots (`.`) except from -the first one with a backslash (\). For example `pages.example.io` would be: - -``` -server_name ~^.*\.pages\.example\.io$; -``` - -## Set maximum pages size - -The maximum size of the unpacked archive per project can be configured in the -Admin area under the Application settings in the **Maximum size of pages (MB)**. -The default is 100MB. - -## Change storage path - -**Source installations:** - -1. Pages are stored by default in `/home/git/gitlab/shared/pages`. - If you wish to store them in another location you must set it up in - `gitlab.yml` under the `pages` section: - - ```yaml - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - path: /mnt/storage/pages - ``` - -1. [Restart GitLab][restart] - -**Omnibus installations:** - -1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`. - If you wish to store them in another location you must set it up in - `/etc/gitlab/gitlab.rb`: - - ```ruby - gitlab_rails['pages_path'] = "/mnt/storage/pages" - ``` - -1. [Reconfigure GitLab][reconfigure] - -## Backup - -Pages are part of the [regular backup][backup] so there is nothing to configure. - -## Security - -You should strongly consider running GitLab pages under a different hostname -than GitLab to prevent XSS attacks. - -## Changelog - -GitLab Pages were first introduced in GitLab EE 8.3. Since then, many features -where added, like custom CNAME and TLS support, and many more are likely to -come. Below is a brief changelog. If no changes were introduced or a version is -missing from the changelog, assume that the documentation is the same as the -latest previous version. - ---- - -**GitLab 8.16 ([documentation][8-16-docs])** - -- GitLab Pages were ported to Community Edition in GitLab 8.16. -- Documentation was refactored to be more modular and easy to follow. - -**GitLab 8.5 ([documentation][8-5-docs])** - -- In GitLab 8.5 we introduced the [gitlab-pages][] daemon which is now the - recommended way to set up GitLab Pages. -- The [NGINX configs][] have changed to reflect this change. So make sure to - update them. -- Custom CNAME and TLS certificates support. -- Documentation was moved to one place. - ---- - -**GitLab 8.3 ([documentation][8-3-docs])** - -- GitLab Pages feature was introduced. - -[8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md -[8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md -[8-16-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable-ce/doc/pages/administration.md -[backup]: ../raketasks/backup_restore.md -[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 -[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 -[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages -[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx -[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md -[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure -[restart]: ../administration/restart_gitlab.md#installations-from-source -[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.1 +This document was moved to [administration/pages](../administration/pages/index.md). diff --git a/doc/pages/img/pages_create_project.png b/doc/pages/img/pages_create_project.png deleted file mode 100644 index a936d8e5dbd..00000000000 Binary files a/doc/pages/img/pages_create_project.png and /dev/null differ diff --git a/doc/pages/img/pages_create_user_page.png b/doc/pages/img/pages_create_user_page.png deleted file mode 100644 index 3f615d3757d..00000000000 Binary files a/doc/pages/img/pages_create_user_page.png and /dev/null differ diff --git a/doc/pages/img/pages_dns_details.png b/doc/pages/img/pages_dns_details.png deleted file mode 100644 index 8d34f3b7f38..00000000000 Binary files a/doc/pages/img/pages_dns_details.png and /dev/null differ diff --git a/doc/pages/img/pages_multiple_domains.png b/doc/pages/img/pages_multiple_domains.png deleted file mode 100644 index 2bc7cee07a6..00000000000 Binary files a/doc/pages/img/pages_multiple_domains.png and /dev/null differ diff --git a/doc/pages/img/pages_new_domain_button.png b/doc/pages/img/pages_new_domain_button.png deleted file mode 100644 index c3640133bb2..00000000000 Binary files a/doc/pages/img/pages_new_domain_button.png and /dev/null differ diff --git a/doc/pages/img/pages_remove.png b/doc/pages/img/pages_remove.png deleted file mode 100644 index adbfb654877..00000000000 Binary files a/doc/pages/img/pages_remove.png and /dev/null differ diff --git a/doc/pages/img/pages_upload_cert.png b/doc/pages/img/pages_upload_cert.png deleted file mode 100644 index 06d85ab1971..00000000000 Binary files a/doc/pages/img/pages_upload_cert.png and /dev/null differ diff --git a/doc/university/README.md b/doc/university/README.md index 12727e9d56f..379a7b4b40f 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -91,7 +91,7 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project 1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) 1. [Securing GitLab Pages with SSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/) -1. [GitLab Pages Documentation](https://docs.gitlab.com/ee/pages/README.html) +1. [GitLab Pages Documentation](https://docs.gitlab.com/ce/user/project/pages/) #### 2.2. GitLab Issues diff --git a/doc/university/support/README.md b/doc/university/support/README.md index 6e415e4d219..ca538ef6dc3 100644 --- a/doc/university/support/README.md +++ b/doc/university/support/README.md @@ -172,7 +172,7 @@ Move on to understanding some of GitLab's more advanced features. You can make u - Get to know the [GitLab API](https://docs.gitlab.com/ee/api/README.html), its capabilities and shortcomings - Learn how to [migrate from SVN to Git](https://docs.gitlab.com/ee/workflow/importing/migrating_from_svn.html) - Set up [GitLab CI](https://docs.gitlab.com/ee/ci/quick_start/README.html) -- Create your first [GitLab Page](https://docs.gitlab.com/ee/pages/administration.html) +- Create your first [GitLab Page](https://docs.gitlab.com/ce/administration/pages/) - Get to know the GitLab Codebase by reading through the source code: - Find the differences between the [EE codebase](https://gitlab.com/gitlab-org/gitlab-ce) and the [CE codebase](https://gitlab.com/gitlab-org/gitlab-ce) diff --git a/doc/user/project/pages/img/pages_create_project.png b/doc/user/project/pages/img/pages_create_project.png new file mode 100644 index 00000000000..a936d8e5dbd Binary files /dev/null and b/doc/user/project/pages/img/pages_create_project.png differ diff --git a/doc/user/project/pages/img/pages_create_user_page.png b/doc/user/project/pages/img/pages_create_user_page.png new file mode 100644 index 00000000000..3f615d3757d Binary files /dev/null and b/doc/user/project/pages/img/pages_create_user_page.png differ diff --git a/doc/user/project/pages/img/pages_dns_details.png b/doc/user/project/pages/img/pages_dns_details.png new file mode 100644 index 00000000000..8d34f3b7f38 Binary files /dev/null and b/doc/user/project/pages/img/pages_dns_details.png differ diff --git a/doc/user/project/pages/img/pages_multiple_domains.png b/doc/user/project/pages/img/pages_multiple_domains.png new file mode 100644 index 00000000000..2bc7cee07a6 Binary files /dev/null and b/doc/user/project/pages/img/pages_multiple_domains.png differ diff --git a/doc/user/project/pages/img/pages_new_domain_button.png b/doc/user/project/pages/img/pages_new_domain_button.png new file mode 100644 index 00000000000..c3640133bb2 Binary files /dev/null and b/doc/user/project/pages/img/pages_new_domain_button.png differ diff --git a/doc/user/project/pages/img/pages_remove.png b/doc/user/project/pages/img/pages_remove.png new file mode 100644 index 00000000000..adbfb654877 Binary files /dev/null and b/doc/user/project/pages/img/pages_remove.png differ diff --git a/doc/user/project/pages/img/pages_upload_cert.png b/doc/user/project/pages/img/pages_upload_cert.png new file mode 100644 index 00000000000..06d85ab1971 Binary files /dev/null and b/doc/user/project/pages/img/pages_upload_cert.png differ diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md new file mode 100644 index 00000000000..e427d7f283d --- /dev/null +++ b/doc/user/project/pages/index.md @@ -0,0 +1,435 @@ +# GitLab Pages + +> **Note:** +> This feature was [introduced][ee-80] in GitLab EE 8.3. +> Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. + +> **Note:** +> This document is about the user guide. To learn how to enable GitLab Pages +> across your GitLab instance, visit the [administrator documentation](administration.md). + +With GitLab Pages you can host for free your static websites on GitLab. +Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can +deploy static pages for your individual projects, your user or your group. + +Read [GitLab Pages on GitLab.com](#gitlab-pages-on-gitlab-com) for specific +information, if you are using GitLab.com to host your website. + +## Getting started with GitLab Pages + +> **Note:** +> In the rest of this document we will assume that the general domain name that +> is used for GitLab Pages is `example.io`. + +In general there are two types of pages one might create: + +- Pages per user (`username.example.io`) or per group (`groupname.example.io`) +- Pages per project (`username.example.io/projectname` or `groupname.example.io/projectname`) + +In GitLab, usernames and groupnames are unique and we often refer to them +as namespaces. There can be only one namespace in a GitLab instance. Below you +can see the connection between the type of GitLab Pages, what the project name +that is created on GitLab looks like and the website URL it will be ultimately +be served on. + +| Type of GitLab Pages | The name of the project created in GitLab | Website URL | +| -------------------- | ------------ | ----------- | +| User pages | `username.example.io` | `http(s)://username.example.io` | +| Group pages | `groupname.example.io` | `http(s)://groupname.example.io` | +| Project pages owned by a user | `projectname` | `http(s)://username.example.io/projectname` | +| Project pages owned by a group | `projectname` | `http(s)://groupname.example.io/projectname`| + +> **Warning:** +> There are some known [limitations](#limitations) regarding namespaces served +> under the general domain name and HTTPS. Make sure to read that section. + +### GitLab Pages requirements + +In brief, this is what you need to upload your website in GitLab Pages: + +1. Find out the general domain name that is used for GitLab Pages + (ask your administrator). This is very important, so you should first make + sure you get that right. +1. Create a project +1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory + of your repository with a specific job named [`pages`][pages] +1. Set up a GitLab Runner to build your website + +> **Note:** +> If [shared runners](../ci/runners/README.md) are enabled by your GitLab +> administrator, you should be able to use them instead of bringing your own. + +### User or group Pages + +For user and group pages, the name of the project should be specific to the +username or groupname and the general domain name that is used for GitLab Pages. +Head over your GitLab instance that supports GitLab Pages and create a +repository named `username.example.io`, where `username` is your username on +GitLab. If the first part of the project name doesn't match exactly your +username, it won’t work, so make sure to get it right. + +To create a group page, the steps are the same like when creating a website for +users. Just make sure that you are creating the project within the group's +namespace. + +![Create a user-based pages project](img/pages_create_user_page.png) + +--- + +After you push some static content to your repository and GitLab Runner uploads +the artifacts to GitLab CI, you will be able to access your website under +`http(s)://username.example.io`. Keep reading to find out how. + +>**Note:** +If your username/groupname contains a dot, for example `foo.bar`, you will not +be able to use the wildcard domain HTTPS, read more at [limitations](#limitations). + +### Project Pages + +GitLab Pages for projects can be created by both user and group accounts. +The steps to create a project page for a user or a group are identical: + +1. Create a new project +1. Push a [`.gitlab-ci.yml` file](../ci/yaml/README.md) in the root directory + of your repository with a specific job named [`pages`][pages]. +1. Set up a GitLab Runner to build your website + +A user's project will be served under `http(s)://username.example.io/projectname` +whereas a group's project under `http(s)://groupname.example.io/projectname`. + +### Explore the contents of `.gitlab-ci.yml` + +The key thing about GitLab Pages is the `.gitlab-ci.yml` file, something that +gives you absolute control over the build process. You can actually watch your +website being built live by following the CI build traces. + +> **Note:** +> Before reading this section, make sure you familiarize yourself with GitLab CI +> and the specific syntax of[`.gitlab-ci.yml`](../ci/yaml/README.md) by +> following our [quick start guide](../ci/quick_start/README.md). + +To make use of GitLab Pages, the contents of `.gitlab-ci.yml` must follow the +rules below: + +1. A special job named [`pages`][pages] must be defined +1. Any static content which will be served by GitLab Pages must be placed under + a `public/` directory +1. `artifacts` with a path to the `public/` directory must be defined + +In its simplest form, `.gitlab-ci.yml` looks like: + +```yaml +pages: + script: + - my_commands + artifacts: + paths: + - public +``` + +When the Runner reaches to build the `pages` job, it executes whatever is +defined in the `script` parameter and if the build completes with a non-zero +exit status, it then uploads the `public/` directory to GitLab Pages. + +The `public/` directory should contain all the static content of your website. +Depending on how you plan to publish your website, the steps defined in the +[`script` parameter](../ci/yaml/README.md#script) may differ. + +Be aware that Pages are by default branch/tag agnostic and their deployment +relies solely on what you specify in `.gitlab-ci.yml`. If you don't limit the +`pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except), +whenever a new commit is pushed to whatever branch or tag, the Pages will be +overwritten. In the example below, we limit the Pages to be deployed whenever +a commit is pushed only on the `master` branch: + +```yaml +pages: + script: + - my_commands + artifacts: + paths: + - public + only: + - master +``` + +We then tell the Runner to treat the `public/` directory as `artifacts` and +upload it to GitLab. And since all these parameters were all under a `pages` +job, the contents of the `public` directory will be served by GitLab Pages. + +#### How `.gitlab-ci.yml` looks like when the static content is in your repository + +Supposedly your repository contained the following files: + +``` +├── index.html +├── css +│   └── main.css +└── js + └── main.js +``` + +Then the `.gitlab-ci.yml` example below simply moves all files from the root +directory of the project to the `public/` directory. The `.public` workaround +is so `cp` doesn't also copy `public/` to itself in an infinite loop: + +```yaml +pages: + script: + - mkdir .public + - cp -r * .public + - mv .public public + artifacts: + paths: + - public + only: + - master +``` + +#### How `.gitlab-ci.yml` looks like when using a static generator + +In general, GitLab Pages support any kind of [static site generator][staticgen], +since `.gitlab-ci.yml` can be configured to run any possible command. + +In the root directory of your Git repository, place the source files of your +favorite static generator. Then provide a `.gitlab-ci.yml` file which is +specific to your static generator. + +The example below, uses [Jekyll] to build the static site: + +```yaml +image: ruby:2.1 # the script will run in Ruby 2.1 using the Docker image ruby:2.1 + +pages: # the build job must be named pages + script: + - gem install jekyll # we install jekyll + - jekyll build -d public/ # we tell jekyll to build the site for us + artifacts: + paths: + - public # this is where the site will live and the Runner uploads it in GitLab + only: + - master # this script is only affecting the master branch +``` + +Here, we used the Docker executor and in the first line we specified the base +image against which our builds will run. + +You have to make sure that the generated static files are ultimately placed +under the `public` directory, that's why in the `script` section we run the +`jekyll` command that builds the website and puts all content in the `public/` +directory. Depending on the static generator of your choice, this command will +differ. Search in the documentation of the static generator you will use if +there is an option to explicitly set the output directory. If there is not +such an option, you can always add one more line under `script` to rename the +resulting directory in `public/`. + +We then tell the Runner to treat the `public/` directory as `artifacts` and +upload it to GitLab. + +--- + +See the [jekyll example project][pages-jekyll] to better understand how this +works. + +For a list of Pages projects, see the [example projects](#example-projects) to +get you started. + +#### How to set up GitLab Pages in a repository where there's also actual code + +Remember that GitLab Pages are by default branch/tag agnostic and their +deployment relies solely on what you specify in `.gitlab-ci.yml`. You can limit +the `pages` job with the [`only` parameter](../ci/yaml/README.md#only-and-except), +whenever a new commit is pushed to a branch that will be used specifically for +your pages. + +That way, you can have your project's code in the `master` branch and use an +orphan branch (let's name it `pages`) that will host your static generator site. + +You can create a new empty branch like this: + +```bash +git checkout --orphan pages +``` + +The first commit made on this new branch will have no parents and it will be +the root of a new history totally disconnected from all the other branches and +commits. Push the source files of your static generator in the `pages` branch. + +Below is a copy of `.gitlab-ci.yml` where the most significant line is the last +one, specifying to execute everything in the `pages` branch: + +``` +image: ruby:2.1 + +pages: + script: + - gem install jekyll + - jekyll build -d public/ + artifacts: + paths: + - public + only: + - pages +``` + +See an example that has different files in the [`master` branch][jekyll-master] +and the source files for Jekyll are in a [`pages` branch][jekyll-pages] which +also includes `.gitlab-ci.yml`. + +[jekyll-master]: https://gitlab.com/pages/jekyll-branched/tree/master +[jekyll-pages]: https://gitlab.com/pages/jekyll-branched/tree/pages + +## Next steps + +So you have successfully deployed your website, congratulations! Let's check +what more you can do with GitLab Pages. + +### Example projects + +Below is a list of example projects for GitLab Pages with a plain HTML website +or various static site generators. Contributions are very welcome. + +- [Plain HTML](https://gitlab.com/pages/plain-html) +- [Jekyll](https://gitlab.com/pages/jekyll) +- [Hugo](https://gitlab.com/pages/hugo) +- [Middleman](https://gitlab.com/pages/middleman) +- [Hexo](https://gitlab.com/pages/hexo) +- [Brunch](https://gitlab.com/pages/brunch) +- [Metalsmith](https://gitlab.com/pages/metalsmith) +- [Harp](https://gitlab.com/pages/harp) + +Visit the GitLab Pages group for a full list of example projects: +<https://gitlab.com/groups/pages>. + +### Add a custom domain to your Pages website + +If this setting is enabled by your GitLab administrator, you should be able to +see the **New Domain** button when visiting your project's settings through the +gear icon in the top right and then navigating to **Pages**. + +![New domain button](img/pages_new_domain_button.png) + +--- + +You can add multiple domains pointing to your website hosted under GitLab. +Once the domain is added, you can see it listed under the **Domains** section. + +![Pages multiple domains](img/pages_multiple_domains.png) + +--- + +As a last step, you need to configure your DNS and add a CNAME pointing to your +user/group page. Click on the **Details** button of a domain for further +instructions. + +![Pages DNS details](img/pages_dns_details.png) + +--- + +>**Note:** +Currently there is support only for custom domains on per-project basis. That +means that if you add a custom domain (`example.com`) for your user website +(`username.example.io`), a project that is served under `username.example.io/foo`, +will not be accessible under `example.com/foo`. + +### Secure your custom domain website with TLS + +When you add a new custom domain, you also have the chance to add a TLS +certificate. If this setting is enabled by your GitLab administrator, you +should be able to see the option to upload the public certificate and the +private key when adding a new domain. + +![Pages upload cert](img/pages_upload_cert.png) + +### Custom error codes pages + +You can provide your own 403 and 404 error pages by creating the `403.html` and +`404.html` files respectively in the root directory of the `public/` directory +that will be included in the artifacts. Usually this is the root directory of +your project, but that may differ depending on your static generator +configuration. + +If the case of `404.html`, there are different scenarios. For example: + +- If you use project Pages (served under `/projectname/`) and try to access + `/projectname/non/exsiting_file`, GitLab Pages will try to serve first + `/projectname/404.html`, and then `/404.html`. +- If you use user/group Pages (served under `/`) and try to access + `/non/existing_file` GitLab Pages will try to serve `/404.html`. +- If you use a custom domain and try to access `/non/existing_file`, GitLab + Pages will try to serve only `/404.html`. + +### Remove the contents of your pages + +If you ever feel the need to purge your Pages content, you can do so by going +to your project's settings through the gear icon in the top right, and then +navigating to **Pages**. Hit the **Remove pages** button and your Pages website +will be deleted. Simple as that. + +![Remove pages](img/pages_remove.png) + +## GitLab Pages on GitLab.com + +If you are using GitLab.com to host your website, then: + +- The general domain name for GitLab Pages on GitLab.com is `gitlab.io`. +- Custom domains and TLS support are enabled. +- Shared runners are enabled by default, provided for free and can be used to + build your website. If you want you can still bring your own Runner. + +The rest of the guide still applies. + +## Limitations + +When using Pages under the general domain of a GitLab instance (`*.example.io`), +you _cannot_ use HTTPS with sub-subdomains. That means that if your +username/groupname contains a dot, for example `foo.bar`, the domain +`https://foo.bar.example.io` will _not_ work. This is a limitation of the +[HTTP Over TLS protocol][rfc]. HTTP pages will continue to work provided you +don't redirect HTTP to HTTPS. + +[rfc]: https://tools.ietf.org/html/rfc2818#section-3.1 "HTTP Over TLS RFC" + +## Redirects in GitLab Pages + +Since you cannot use any custom server configuration files, like `.htaccess` or +any `.conf` file for that matter, if you want to redirect a web page to another +location, you can use the [HTTP meta refresh tag][metarefresh]. + +Some static site generators provide plugins for that functionality so that you +don't have to create and edit HTML files manually. For example, Jekyll has the +[redirect-from plugin](https://github.com/jekyll/jekyll-redirect-from). + +## Frequently Asked Questions + +### Can I download my generated pages? + +Sure. All you need to do is download the artifacts archive from the build page. + +### Can I use GitLab Pages if my project is private? + +Yes. GitLab Pages don't care whether you set your project's visibility level +to private, internal or public. + +### Do I need to create a user/group website before creating a project website? + +No, you don't. You can create your project first and it will be accessed under +`http(s)://namespace.example.io/projectname`. + +## Known issues + +For a list of known issues, visit GitLab's [public issue tracker]. + +--- + +[jekyll]: http://jekyllrb.com/ +[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 +[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 +[pages-daemon]: https://gitlab.com/gitlab-org/gitlab-pages +[gitlab ci]: https://about.gitlab.com/gitlab-ci +[gitlab runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner +[pages]: ../ci/yaml/README.md#pages +[staticgen]: https://www.staticgen.com/ +[pages-jekyll]: https://gitlab.com/pages/jekyll +[metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh +[public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages -- cgit v1.2.1 From 749d8051ed379f79cab9c378ee1ac564d2d09e9b Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 23 Jan 2017 13:47:39 +0100 Subject: Bump GitLab version that Pages where ported to CE --- doc/administration/pages/index.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index da148a0f2bb..c460c8e1b86 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -462,9 +462,9 @@ latest previous version. --- -**GitLab 8.16 ([documentation][8-16-docs])** +**GitLab 8.17 ([documentation][8-17-docs])** -- GitLab Pages were ported to Community Edition in GitLab 8.16. +- GitLab Pages were ported to Community Edition in GitLab 8.17. - Documentation was refactored to be more modular and easy to follow. **GitLab 8.5 ([documentation][8-5-docs])** @@ -476,15 +476,13 @@ latest previous version. - Custom CNAME and TLS certificates support. - Documentation was moved to one place. ---- - **GitLab 8.3 ([documentation][8-3-docs])** - GitLab Pages feature was introduced. [8-3-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-3-stable-ee/doc/pages/administration.md [8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md -[8-16-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable-ce/doc/administration/pages/index.md +[8-17-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable-ce/doc/administration/pages/index.md [backup]: ../raketasks/backup_restore.md [ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 [ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 -- cgit v1.2.1 From 39e74b1cba3773fd0fb37bc78a7400ec7c6b7ab3 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 23 Jan 2017 13:48:27 +0100 Subject: Fix link to new pages location --- doc/pages/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/pages/README.md b/doc/pages/README.md index 44b74513fd9..c9715eed598 100644 --- a/doc/pages/README.md +++ b/doc/pages/README.md @@ -1 +1 @@ -This document was moved to [user/project/pages](../user/project/pages). +This document was moved to [user/project/pages](../user/project/pages/index.md). -- cgit v1.2.1 From 452b9c8b955274f8892d3fa4a0fc335d28515272 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 23 Jan 2017 13:48:43 +0100 Subject: Merge multiple notes into one in Pages user docs --- doc/user/project/pages/index.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index e427d7f283d..a07c23a3274 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -1,12 +1,10 @@ # GitLab Pages -> **Note:** -> This feature was [introduced][ee-80] in GitLab EE 8.3. -> Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. - -> **Note:** -> This document is about the user guide. To learn how to enable GitLab Pages -> across your GitLab instance, visit the [administrator documentation](administration.md). +> **Notes:** +> - This feature was [introduced][ee-80] in GitLab EE 8.3. +> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. +> - This document is about the user guide. To learn how to enable GitLab Pages +> across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md). With GitLab Pages you can host for free your static websites on GitLab. Combined with the power of [GitLab CI] and the help of [GitLab Runner] you can -- cgit v1.2.1 From 2c787bca1f0566517a2ddae7d1f23a822285a44d Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 23 Jan 2017 13:54:09 +0100 Subject: Better highlight prerequisites for Pages --- doc/administration/pages/index.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index c460c8e1b86..5e84eb85667 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -47,12 +47,13 @@ In this document, we will proceed assuming the first option. Before proceeding with the Pages configuration, you will need to: -1. Have a separate domain under which the GitLab Pages will be served -1. (Optional) Have a wildcard certificate for that domain if you decide to serve - Pages under HTTPS -1. Configure a wildcard DNS record +1. Have a separate domain under which the GitLab Pages will be served. In this + document we assume that to be `example.io`. +1. Configure a **wildcard DNS record**. +1. (Optional) Have a **wildcard certificate** for that domain if you decide to + serve Pages under HTTPS. 1. (Optional but recommended) Enable [Shared runners](../ci/runners/README.md) - so that your users don't have to bring their own + so that your users don't have to bring their own. ### DNS configuration -- cgit v1.2.1 From e9c08231d73d2f3c73eb2687bc887076c9b6ed83 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 23 Jan 2017 14:03:27 +0100 Subject: Bump pages daemon and place Omnibus settings on top --- doc/administration/pages/index.md | 216 ++++++++++++++++++++------------------ 1 file changed, 112 insertions(+), 104 deletions(-) diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 5e84eb85667..5b1d6ee9998 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -89,6 +89,27 @@ In that case, the pages daemon is running, NGINX still proxies requests to the daemon but the daemon is also able to receive requests from the outside world. Custom domains and TLS are supported. +**Omnibus installations:** + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + pages_external_url "https://example.io" + nginx['listen_addresses'] = ['1.1.1.1'] + pages_nginx['enable'] = false + gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt" + gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key" + gitlab_pages['external_http'] = '1.1.1.2:80' + gitlab_pages['external_https'] = '1.1.1.2:443' + ``` + + where `1.1.1.1` is the primary IP address that GitLab is listening to and + `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + +1. [Reconfigure GitLab][reconfigure] + +--- + **Source installations:** 1. Install the Pages daemon: @@ -97,7 +118,7 @@ world. Custom domains and TLS are supported. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.2.1 + sudo -u git -H git checkout v0.2.4 sudo -u git -H make ``` @@ -148,20 +169,26 @@ world. Custom domains and TLS are supported. 1. Restart NGINX 1. [Restart GitLab][restart] ---- +### Option 2. Custom domains without HTTPS support + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `http://page.example.io` and `http://page.com` | no | yes | no | yes | + +Pages enabled, daemon is enabled AND pages has external IP support enabled. +In that case, the pages daemon is running, NGINX still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. **Omnibus installations:** 1. Edit `/etc/gitlab/gitlab.rb`: ```ruby - pages_external_url "https://example.io" + pages_external_url "http://example.io" nginx['listen_addresses'] = ['1.1.1.1'] pages_nginx['enable'] = false - gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt" - gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key" gitlab_pages['external_http'] = '1.1.1.2:80' - gitlab_pages['external_https'] = '1.1.1.2:443' ``` where `1.1.1.1` is the primary IP address that GitLab is listening to and @@ -169,16 +196,7 @@ world. Custom domains and TLS are supported. 1. [Reconfigure GitLab][reconfigure] -### Option 2. Custom domains without HTTPS support - -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `http://page.example.io` and `http://page.com` | no | yes | no | yes | - -Pages enabled, daemon is enabled AND pages has external IP support enabled. -In that case, the pages daemon is running, NGINX still proxies requests to -the daemon but the daemon is also able to receive requests from the outside -world. Custom domains and TLS are supported. +--- **Source installations:** @@ -188,7 +206,7 @@ world. Custom domains and TLS are supported. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.2.1 + sudo -u git -H git checkout v0.2.4 sudo -u git -H make ``` @@ -235,32 +253,34 @@ world. Custom domains and TLS are supported. 1. Restart NGINX 1. [Restart GitLab][restart] ---- +### Option 3. Wildcard HTTPS domain without custom domains + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `https://page.example.io` | yes | no | no | no | + +Pages enabled, daemon is enabled and NGINX will proxy all requests to the +daemon. Pages daemon doesn't listen to the outside world. **Omnibus installations:** -1. Edit `/etc/gitlab/gitlab.rb`: +1. Place the certificate and key inside `/etc/gitlab/ssl` +1. In `/etc/gitlab/gitlab.rb` specify the following configuration: ```ruby - pages_external_url "http://example.io" - nginx['listen_addresses'] = ['1.1.1.1'] - pages_nginx['enable'] = false - gitlab_pages['external_http'] = '1.1.1.2:80' + pages_external_url 'https://example.io' + + pages_nginx['redirect_http_to_https'] = true + pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt" + pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key" ``` - where `1.1.1.1` is the primary IP address that GitLab is listening to and - `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. + where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key, + respectively. 1. [Reconfigure GitLab][reconfigure] -### Option 3. Wildcard HTTPS domain without custom domains - -| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | -| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| -| `https://page.example.io` | yes | no | no | no | - -Pages enabled, daemon is enabled and NGINX will proxy all requests to the -daemon. Pages daemon doesn't listen to the outside world. +--- **Source installations:** @@ -270,7 +290,7 @@ daemon. Pages daemon doesn't listen to the outside world. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.2.1 + sudo -u git -H git checkout v0.2.4 sudo -u git -H make ``` 1. In `gitlab.yml`, set the port to `443` and https to `true`: @@ -296,25 +316,8 @@ daemon. Pages daemon doesn't listen to the outside world. Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. ---- - -**Omnibus installations:** - -1. Place the certificate and key inside `/etc/gitlab/ssl` -1. In `/etc/gitlab/gitlab.rb` specify the following configuration: - - ```ruby - pages_external_url 'https://example.io' - - pages_nginx['redirect_http_to_https'] = true - pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt" - pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key" - ``` - - where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key, - respectively. - -1. [Reconfigure GitLab][reconfigure] +1. Restart NGINX +1. [Restart GitLab][restart] ### Option 4. Wildcard HTTP domain without custom domains @@ -325,6 +328,18 @@ daemon. Pages daemon doesn't listen to the outside world. Pages enabled, daemon is enabled and NGINX will proxy all requests to the daemon. Pages daemon doesn't listen to the outside world. +**Omnibus installations:** + +1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: + + ```ruby + pages_external_url 'http://example.io' + ``` + +1. [Reconfigure GitLab][reconfigure] + +--- + **Source installations:** 1. Install the Pages daemon: @@ -333,7 +348,7 @@ daemon. Pages daemon doesn't listen to the outside world. cd /home/git sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git cd gitlab-pages - sudo -u git -H git checkout v0.2.1 + sudo -u git -H git checkout v0.2.4 sudo -u git -H make ``` @@ -370,18 +385,55 @@ daemon. Pages daemon doesn't listen to the outside world. 1. Restart NGINX 1. [Restart GitLab][restart] ---- +## Set maximum pages size + +The maximum size of the unpacked archive per project can be configured in the +Admin area under the Application settings in the **Maximum size of pages (MB)**. +The default is 100MB. + +## Change storage path + +Follow the steps below to change the default path where GitLab Pages' contents +are stored. **Omnibus installations:** -1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: +1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`. + If you wish to store them in another location you must set it up in + `/etc/gitlab/gitlab.rb`: - ```ruby - pages_external_url 'http://example.io' - ``` + ```ruby + gitlab_rails['pages_path'] = "/mnt/storage/pages" + ``` 1. [Reconfigure GitLab][reconfigure] +--- + +**Source installations:** + +1. Pages are stored by default in `/home/git/gitlab/shared/pages`. + If you wish to store them in another location you must set it up in + `gitlab.yml` under the `pages` section: + + ```yaml + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + path: /mnt/storage/pages + ``` + +1. [Restart GitLab][restart] + +## Backup + +Pages are part of the [regular backup][backup] so there is nothing to configure. + +## Security + +You should strongly consider running GitLab pages under a different hostname +than GitLab to prevent XSS attacks. + ## NGINX caveats >**Note:** @@ -409,50 +461,6 @@ the first one with a backslash (\). For example `pages.example.io` would be: server_name ~^.*\.pages\.example\.io$; ``` -## Set maximum pages size - -The maximum size of the unpacked archive per project can be configured in the -Admin area under the Application settings in the **Maximum size of pages (MB)**. -The default is 100MB. - -## Change storage path - -**Source installations:** - -1. Pages are stored by default in `/home/git/gitlab/shared/pages`. - If you wish to store them in another location you must set it up in - `gitlab.yml` under the `pages` section: - - ```yaml - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - path: /mnt/storage/pages - ``` - -1. [Restart GitLab][restart] - -**Omnibus installations:** - -1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`. - If you wish to store them in another location you must set it up in - `/etc/gitlab/gitlab.rb`: - - ```ruby - gitlab_rails['pages_path'] = "/mnt/storage/pages" - ``` - -1. [Reconfigure GitLab][reconfigure] - -## Backup - -Pages are part of the [regular backup][backup] so there is nothing to configure. - -## Security - -You should strongly consider running GitLab pages under a different hostname -than GitLab to prevent XSS attacks. - ## Changelog GitLab Pages were first introduced in GitLab EE 8.3. Since then, many features @@ -493,4 +501,4 @@ latest previous version. [pages-userguide]: ../../user/project/pages/index.md [reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure [restart]: ../administration/restart_gitlab.md#installations-from-source -[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.1 +[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4 -- cgit v1.2.1 From e2123de5fca072306f7247931863418864739035 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Mon, 23 Jan 2017 14:37:52 +0100 Subject: Split Omnibus and source installation Pages admin docs [ci skip] --- doc/administration/pages/index.md | 275 ++----------------------------- doc/administration/pages/source.md | 323 +++++++++++++++++++++++++++++++++++++ 2 files changed, 333 insertions(+), 265 deletions(-) create mode 100644 doc/administration/pages/source.md diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md index 5b1d6ee9998..c352caf1115 100644 --- a/doc/administration/pages/index.md +++ b/doc/administration/pages/index.md @@ -1,9 +1,11 @@ # GitLab Pages Administration > **Notes:** -> - [Introduced][ee-80] in GitLab EE 8.3. -> - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. -> - GitLab Pages were ported to Community Edition in GitLab 8.16. +- [Introduced][ee-80] in GitLab EE 8.3. +- Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. +- GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. +- This guide is for Omnibus GitLab installations. If you have installed + GitLab from source, follow the [Pages source installation document](source.md). --- @@ -89,8 +91,6 @@ In that case, the pages daemon is running, NGINX still proxies requests to the daemon but the daemon is also able to receive requests from the outside world. Custom domains and TLS are supported. -**Omnibus installations:** - 1. Edit `/etc/gitlab/gitlab.rb`: ```ruby @@ -108,67 +108,6 @@ world. Custom domains and TLS are supported. 1. [Reconfigure GitLab][reconfigure] ---- - -**Source installations:** - -1. Install the Pages daemon: - - ``` - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git - cd gitlab-pages - sudo -u git -H git checkout v0.2.4 - sudo -u git -H make - ``` - -1. Edit `gitlab.yml` to look like the example below. You need to change the - `host` to the FQDN under which GitLab Pages will be served. Set - `external_http` and `external_https` to the secondary IP on which the pages - daemon will listen for connections: - - ```yaml - ## GitLab Pages - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - # path: shared/pages - - host: example.io - port: 443 - https: true - - external_http: 1.1.1.1:80 - external_https: 1.1.1.1:443 - ``` - -1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in - order to enable the pages daemon. In `gitlab_pages_options` the - `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`, - `external_http` and `external_https` settings that you set above respectively. - The `-root-cert` and `-root-key` settings are the wildcard TLS certificates - of the `example.io` domain: - - ``` - gitlab_pages_enabled=true - gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80 -listen-https 1.1.1.1:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key - ``` - -1. Copy the `gitlab-pages-ssl` Nginx configuration file: - - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf - ``` - - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - -1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace - `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab - listens to. -1. Restart NGINX -1. [Restart GitLab][restart] - ### Option 2. Custom domains without HTTPS support | URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | @@ -180,8 +119,6 @@ In that case, the pages daemon is running, NGINX still proxies requests to the daemon but the daemon is also able to receive requests from the outside world. Custom domains and TLS are supported. -**Omnibus installations:** - 1. Edit `/etc/gitlab/gitlab.rb`: ```ruby @@ -196,63 +133,6 @@ world. Custom domains and TLS are supported. 1. [Reconfigure GitLab][reconfigure] ---- - -**Source installations:** - -1. Install the Pages daemon: - - ``` - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git - cd gitlab-pages - sudo -u git -H git checkout v0.2.4 - sudo -u git -H make - ``` - -1. Edit `gitlab.yml` to look like the example below. You need to change the - `host` to the FQDN under which GitLab Pages will be served. Set - `external_http` to the secondary IP on which the pages daemon will listen - for connections: - - ```yaml - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - # path: shared/pages - - host: example.io - port: 80 - https: false - - external_http: 1.1.1.1:80 - ``` - -1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in - order to enable the pages daemon. In `gitlab_pages_options` the - `-pages-domain` and `-listen-http` must match the `host` and `external_http` - settings that you set above respectively: - - ``` - gitlab_pages_enabled=true - gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.1:80" - ``` - -1. Copy the `gitlab-pages-ssl` Nginx configuration file: - - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf - ``` - - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - -1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace - `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab - listens to. -1. Restart NGINX -1. [Restart GitLab][restart] - ### Option 3. Wildcard HTTPS domain without custom domains | URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | @@ -262,8 +142,6 @@ world. Custom domains and TLS are supported. Pages enabled, daemon is enabled and NGINX will proxy all requests to the daemon. Pages daemon doesn't listen to the outside world. -**Omnibus installations:** - 1. Place the certificate and key inside `/etc/gitlab/ssl` 1. In `/etc/gitlab/gitlab.rb` specify the following configuration: @@ -280,45 +158,6 @@ daemon. Pages daemon doesn't listen to the outside world. 1. [Reconfigure GitLab][reconfigure] ---- - -**Source installations:** - -1. Install the Pages daemon: - - ``` - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git - cd gitlab-pages - sudo -u git -H git checkout v0.2.4 - sudo -u git -H make - ``` -1. In `gitlab.yml`, set the port to `443` and https to `true`: - - ```bash - ## GitLab Pages - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - # path: shared/pages - - host: example.io - port: 443 - https: true - ``` - -1. Copy the `gitlab-pages-ssl` Nginx configuration file: - - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf - ``` - - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - -1. Restart NGINX -1. [Restart GitLab][restart] - ### Option 4. Wildcard HTTP domain without custom domains | URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | @@ -328,8 +167,6 @@ daemon. Pages daemon doesn't listen to the outside world. Pages enabled, daemon is enabled and NGINX will proxy all requests to the daemon. Pages daemon doesn't listen to the outside world. -**Omnibus installations:** - 1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: ```ruby @@ -338,66 +175,11 @@ daemon. Pages daemon doesn't listen to the outside world. 1. [Reconfigure GitLab][reconfigure] ---- - -**Source installations:** - -1. Install the Pages daemon: - - ``` - cd /home/git - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git - cd gitlab-pages - sudo -u git -H git checkout v0.2.4 - sudo -u git -H make - ``` - -1. Go to the GitLab installation directory: - - ```bash - cd /home/git/gitlab - ``` - -1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and - the `host` to the FQDN under which GitLab Pages will be served: - - ```yaml - ## GitLab Pages - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - # path: shared/pages - - host: example.io - port: 80 - https: false - ``` - -1. Copy the `gitlab-pages-ssl` Nginx configuration file: - - ```bash - sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf - sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf - ``` - - Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. - -1. Restart NGINX -1. [Restart GitLab][restart] - -## Set maximum pages size - -The maximum size of the unpacked archive per project can be configured in the -Admin area under the Application settings in the **Maximum size of pages (MB)**. -The default is 100MB. - ## Change storage path Follow the steps below to change the default path where GitLab Pages' contents are stored. -**Omnibus installations:** - 1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`. If you wish to store them in another location you must set it up in `/etc/gitlab/gitlab.rb`: @@ -408,22 +190,11 @@ are stored. 1. [Reconfigure GitLab][reconfigure] ---- - -**Source installations:** - -1. Pages are stored by default in `/home/git/gitlab/shared/pages`. - If you wish to store them in another location you must set it up in - `gitlab.yml` under the `pages` section: - - ```yaml - pages: - enabled: true - # The location where pages are stored (default: shared/pages). - path: /mnt/storage/pages - ``` +## Set maximum pages size -1. [Restart GitLab][restart] +The maximum size of the unpacked archive per project can be configured in the +Admin area under the Application settings in the **Maximum size of pages (MB)**. +The default is 100MB. ## Backup @@ -434,33 +205,6 @@ Pages are part of the [regular backup][backup] so there is nothing to configure. You should strongly consider running GitLab pages under a different hostname than GitLab to prevent XSS attacks. -## NGINX caveats - ->**Note:** -The following information applies only for installations from source. - -Be extra careful when setting up the domain name in the NGINX config. You must -not remove the backslashes. - -If your GitLab pages domain is `example.io`, replace: - -```bash -server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$; -``` - -with: - -``` -server_name ~^.*\.example\.io$; -``` - -If you are using a subdomain, make sure to escape all dots (`.`) except from -the first one with a backslash (\). For example `pages.example.io` would be: - -``` -server_name ~^.*\.pages\.example\.io$; -``` - ## Changelog GitLab Pages were first introduced in GitLab EE 8.3. Since then, many features @@ -493,6 +237,7 @@ latest previous version. [8-5-docs]: https://gitlab.com/gitlab-org/gitlab-ee/blob/8-5-stable-ee/doc/pages/administration.md [8-17-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-17-stable-ce/doc/administration/pages/index.md [backup]: ../raketasks/backup_restore.md +[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 [ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 [ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 [gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md new file mode 100644 index 00000000000..d4468b99992 --- /dev/null +++ b/doc/administration/pages/source.md @@ -0,0 +1,323 @@ +# GitLab Pages administration for source installations + +This is the documentation for configuring a GitLab Pages when you have installed +GitLab from source and not using the Omnibus packages. + +You are encouraged to read the [Omnibus documentation](index.md) as it provides +some invaluable information to the configuration of GitLab Pages. Please proceed +to read it before going forward with this guide. + +We also highly recommend that you use the Omnibus GitLab packages, as we +optimize them specifically for GitLab, and we will take care of upgrading GitLab +Pages to the latest supported version. + +## Overview + +[Read the Omnibus overview section.](index.md#overview) + +## Prerequisites + +[Read the Omnibus prerequisites section.](index.md#prerequisites) + +## Configuration + +Depending on your needs, you can install GitLab Pages in four different ways. + +### Option 1. Custom domains with HTTPS support + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes | + +Pages enabled, daemon is enabled AND pages has external IP support enabled. +In that case, the pages daemon is running, NGINX still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. + +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.4 + sudo -u git -H make + ``` + +1. Edit `gitlab.yml` to look like the example below. You need to change the + `host` to the FQDN under which GitLab Pages will be served. Set + `external_http` and `external_https` to the secondary IP on which the pages + daemon will listen for connections: + + ```yaml + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 443 + https: true + + external_http: 1.1.1.2:80 + external_https: 1.1.1.2:443 + ``` + +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain`, `-listen-http` and `-listen-https` must match the `host`, + `external_http` and `external_https` settings that you set above respectively. + The `-root-cert` and `-root-key` settings are the wildcard TLS certificates + of the `example.io` domain: + + ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace + `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + listens to. +1. Restart NGINX +1. [Restart GitLab][restart] + +### Option 2. Custom domains without HTTPS support + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `http://page.example.io` and `http://page.com` | no | yes | no | yes | + +Pages enabled, daemon is enabled AND pages has external IP support enabled. +In that case, the pages daemon is running, NGINX still proxies requests to +the daemon but the daemon is also able to receive requests from the outside +world. Custom domains and TLS are supported. + +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.4 + sudo -u git -H make + ``` + +1. Edit `gitlab.yml` to look like the example below. You need to change the + `host` to the FQDN under which GitLab Pages will be served. Set + `external_http` to the secondary IP on which the pages daemon will listen + for connections: + + ```yaml + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 80 + https: false + + external_http: 1.1.1.2:80 + ``` + +1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in + order to enable the pages daemon. In `gitlab_pages_options` the + `-pages-domain` and `-listen-http` must match the `host` and `external_http` + settings that you set above respectively: + + ``` + gitlab_pages_enabled=true + gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80" + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace + `0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab + listens to. +1. Restart NGINX +1. [Restart GitLab][restart] + +### Option 3. Wildcard HTTPS domain without custom domains + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `https://page.example.io` | yes | no | no | no | + +Pages enabled, daemon is enabled and NGINX will proxy all requests to the +daemon. Pages daemon doesn't listen to the outside world. + +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.4 + sudo -u git -H make + ``` +1. In `gitlab.yml`, set the port to `443` and https to `true`: + + ```bash + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 443 + https: true + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Restart NGINX +1. [Restart GitLab][restart] + +### Option 4. Wildcard HTTP domain without custom domains + +| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | +| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| +| `http://page.example.io` | no | no | no | no | + +Pages enabled, daemon is enabled and NGINX will proxy all requests to the +daemon. Pages daemon doesn't listen to the outside world. + +1. Install the Pages daemon: + + ``` + cd /home/git + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-pages.git + cd gitlab-pages + sudo -u git -H git checkout v0.2.4 + sudo -u git -H make + ``` + +1. Go to the GitLab installation directory: + + ```bash + cd /home/git/gitlab + ``` + +1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and + the `host` to the FQDN under which GitLab Pages will be served: + + ```yaml + ## GitLab Pages + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + # path: shared/pages + + host: example.io + port: 80 + https: false + ``` + +1. Copy the `gitlab-pages-ssl` Nginx configuration file: + + ```bash + sudo cp lib/support/nginx/gitlab-pages-ssl /etc/nginx/sites-available/gitlab-pages-ssl.conf + sudo ln -sf /etc/nginx/sites-{available,enabled}/gitlab-pages-ssl.conf + ``` + + Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. + +1. Restart NGINX +1. [Restart GitLab][restart] + +## NGINX caveats + +>**Note:** +The following information applies only for installations from source. + +Be extra careful when setting up the domain name in the NGINX config. You must +not remove the backslashes. + +If your GitLab pages domain is `example.io`, replace: + +```bash +server_name ~^.*\.YOUR_GITLAB_PAGES\.DOMAIN$; +``` + +with: + +``` +server_name ~^.*\.example\.io$; +``` + +If you are using a subdomain, make sure to escape all dots (`.`) except from +the first one with a backslash (\). For example `pages.example.io` would be: + +``` +server_name ~^.*\.pages\.example\.io$; +``` + +## Change storage path + +Follow the steps below to change the default path where GitLab Pages' contents +are stored. + +1. Pages are stored by default in `/home/git/gitlab/shared/pages`. + If you wish to store them in another location you must set it up in + `gitlab.yml` under the `pages` section: + + ```yaml + pages: + enabled: true + # The location where pages are stored (default: shared/pages). + path: /mnt/storage/pages + ``` + +1. [Restart GitLab][restart] + +## Set maximum Pages size + +The maximum size of the unpacked archive per project can be configured in the +Admin area under the Application settings in the **Maximum size of pages (MB)**. +The default is 100MB. + +## Backup + +Pages are part of the [regular backup][backup] so there is nothing to configure. + +## Security + +You should strongly consider running GitLab pages under a different hostname +than GitLab to prevent XSS attacks. + +[backup]: ../raketasks/backup_restore.md +[ee-80]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/80 +[ee-173]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/173 +[gitlab pages daemon]: https://gitlab.com/gitlab-org/gitlab-pages +[NGINX configs]: https://gitlab.com/gitlab-org/gitlab-ee/tree/8-5-stable-ee/lib/support/nginx +[pages-readme]: https://gitlab.com/gitlab-org/gitlab-pages/blob/master/README.md +[pages-userguide]: ../../user/project/pages/index.md +[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure +[restart]: ../administration/restart_gitlab.md#installations-from-source +[gitlab-pages]: https://gitlab.com/gitlab-org/gitlab-pages/tree/v0.2.4 -- cgit v1.2.1 From f31fb929a39b3540a1fb6f4426227e6d388d0765 Mon Sep 17 00:00:00 2001 From: samrose3 <sam@gitlab.com> Date: Mon, 30 Jan 2017 19:22:45 -0500 Subject: Adjust pipeline graph height so they don't touch --- app/assets/stylesheets/pages/pipelines.scss | 3 ++- ...aph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 47dfc22d533..8304fdc7646 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -201,7 +201,8 @@ .stage-container { display: inline-block; position: relative; - margin-right: 6px; + height: 22px; + margin: 3px 6px 3px 0; .tooltip { white-space: nowrap; diff --git a/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml new file mode 100644 index 00000000000..79316abbaf7 --- /dev/null +++ b/changelogs/unreleased/27332-mini-pipeline-graph-with-many-stages-has-no-line-spacing-in-firefox-and-safari.yml @@ -0,0 +1,4 @@ +--- +title: Fix pipeline graph vertical spacing in Firefox and Safari +merge_request: 8886 +author: -- cgit v1.2.1 From 2916ea82d9e3bdb69e6eeb9544f494d7269214bd Mon Sep 17 00:00:00 2001 From: Sam Rose <sam@gitlab.com> Date: Wed, 28 Dec 2016 11:53:05 -0500 Subject: Update pipeline and commit URL and text on CI status change --- app/assets/javascripts/merge_request_widget.js.es6 | 22 +++++++++++++++++- .../projects/merge_requests_controller.rb | 3 ++- .../merge_requests/widget/_heading.html.haml | 2 +- .../projects/merge_requests/widget/_show.html.haml | 4 ++++ ...o-not-update-when-new-pipeline-is-triggered.yml | 4 ++++ spec/javascripts/merge_request_widget_spec.js | 26 ++++++++++++++++++++-- 6 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index 7cc319e2f4e..fa782ebbedf 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -154,12 +154,22 @@ return; } if (data.environments && data.environments.length) _this.renderEnvironments(data.environments); - if (data.status !== _this.opts.ci_status && (data.status != null)) { + if (data.status !== _this.opts.ci_status || + data.sha !== _this.opts.ci_sha || + data.pipeline !== _this.opts.ci_pipeline) { _this.opts.ci_status = data.status; _this.showCIStatus(data.status); if (data.coverage) { _this.showCICoverage(data.coverage); } + if (data.pipeline) { + _this.opts.ci_pipeline = data.pipeline; + _this.updatePipelineUrls(data.pipeline); + } + if (data.sha) { + _this.opts.ci_sha = data.sha; + _this.updateCommitUrls(data.sha); + } if (showNotification) { status = _this.ciLabelForStatus(data.status); if (status === "preparing") { @@ -248,6 +258,16 @@ return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class); }; + MergeRequestWidget.prototype.updatePipelineUrls = function(id) { + const pipelineUrl = this.opts.pipeline_path; + $('.pipeline').text(`#${id}`).attr('href', [pipelineUrl, id].join('/')); + }; + + MergeRequestWidget.prototype.updateCommitUrls = function(id) { + const commitsUrl = this.opts.commits_path; + $('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/')); + }; + return MergeRequestWidget; })(); })(window.gl || (window.gl = {})); diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 3492502e296..6eb542e4bd8 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -434,7 +434,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController title: merge_request.title, sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha), status: status, - coverage: coverage + coverage: coverage, + pipeline: pipeline.try(:id) } render json: response diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 804a4a2473b..0e3af62ebc2 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -10,7 +10,7 @@ = ci_label_for_status(status) for = succeed "." do - = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace" + = link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link" %span.ci-coverage - elsif @merge_request.has_ci? diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index 38328501ffd..f07e6b3ad54 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -24,6 +24,10 @@ preparing: "{{status}} build", normal: "Build {{status}}" }, + ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}", + ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json}, + commits_path: "#{project_commits_path(@project)}", + pipeline_path: "#{project_pipelines_path(@project)}", pipelines_path: "#{pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}" }; diff --git a/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml new file mode 100644 index 00000000000..f74e9fa8b6d --- /dev/null +++ b/changelogs/unreleased/25811-pipeline-number-and-url-do-not-update-when-new-pipeline-is-triggered.yml @@ -0,0 +1,4 @@ +--- +title: Update pipeline and commit links when CI status is updated +merge_request: 8351 +author: diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js index bf45100af03..6f1d6406897 100644 --- a/spec/javascripts/merge_request_widget_spec.js +++ b/spec/javascripts/merge_request_widget_spec.js @@ -1,6 +1,7 @@ /* eslint-disable space-before-function-paren, quotes, comma-dangle, dot-notation, quote-props, no-var, max-len */ /*= require merge_request_widget */ +/*= require smart_interval */ /*= require lib/utils/datetime_utility */ (function() { @@ -21,7 +22,11 @@ normal: "Build {{status}}" }, gitlab_icon: "gitlab_logo.png", - builds_path: "http://sampledomain.local/sampleBuildsPath" + ci_pipeline: 80, + ci_sha: "12a34bc5", + builds_path: "http://sampledomain.local/sampleBuildsPath", + commits_path: "http://sampledomain.local/commits", + pipeline_path: "http://sampledomain.local/pipelines" }; this["class"] = new window.gl.MergeRequestWidget(this.opts); }); @@ -118,10 +123,11 @@ }); }); - return describe('getCIStatus', function() { + describe('getCIStatus', function() { beforeEach(function() { this.ciStatusData = { "title": "Sample MR title", + "pipeline": 80, "sha": "12a34bc5", "status": "success", "coverage": 98 @@ -165,6 +171,22 @@ this["class"].getCIStatus(true); return expect(spy).not.toHaveBeenCalled(); }); + it('should update the pipeline URL when the pipeline changes', function() { + var spy; + spy = spyOn(this["class"], 'updatePipelineUrls').and.stub(); + this["class"].getCIStatus(false); + this.ciStatusData.pipeline += 1; + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalled(); + }); + it('should update the commit URL when the sha changes', function() { + var spy; + spy = spyOn(this["class"], 'updateCommitUrls').and.stub(); + this["class"].getCIStatus(false); + this.ciStatusData.sha = "9b50b99a"; + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalled(); + }); }); }); }).call(this); -- cgit v1.2.1 From f799585c41d801bc657f992adf3d4b201af927d2 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Wed, 1 Feb 2017 16:38:52 +0000 Subject: Keep snippet visibility on error When a snippet is submitted, but there's an error, we didn't keep the visibility level. As the default is private, this means that submitting a public snippet that failed would then fall back to being a private snippet. --- app/helpers/visibility_level_helper.rb | 4 ---- app/models/snippet.rb | 2 +- app/views/projects/snippets/edit.html.haml | 2 +- app/views/projects/snippets/new.html.haml | 2 +- app/views/shared/snippets/_form.html.haml | 3 +-- app/views/snippets/edit.html.haml | 2 +- app/views/snippets/new.html.haml | 2 +- 7 files changed, 6 insertions(+), 11 deletions(-) diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index 3a83ae15dd8..fc93acfe63e 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -93,10 +93,6 @@ module VisibilityLevelHelper current_application_settings.default_project_visibility end - def default_snippet_visibility - current_application_settings.default_snippet_visibility - end - def default_group_visibility current_application_settings.default_group_visibility end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 771a7350556..960f1521be9 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -17,7 +17,7 @@ class Snippet < ActiveRecord::Base default_content_html_invalidator || file_name_changed? end - default_value_for :visibility_level, Snippet::PRIVATE + default_value_for(:visibility_level) { current_application_settings.default_snippet_visibility } belongs_to :author, class_name: 'User' belongs_to :project diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml index 216f70f5605..fb39028529d 100644 --- a/app/views/projects/snippets/edit.html.haml +++ b/app/views/projects/snippets/edit.html.haml @@ -3,4 +3,4 @@ %h3.page-title Edit Snippet %hr -= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level += render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet) diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml index 772a594269c..cfed3a79bc5 100644 --- a/app/views/projects/snippets/new.html.haml +++ b/app/views/projects/snippets/new.html.haml @@ -3,4 +3,4 @@ %h3.page-title New Snippet %hr -= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility += render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet) diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 0c788032020..2d22782eb36 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -11,7 +11,7 @@ .col-sm-10 = f.text_field :title, class: 'form-control', required: true, autofocus: true - = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet + = render 'shared/visibility_level', f: f, visibility_level: @snippet.visibility_level, can_change_visibility_level: true, form_model: @snippet .file-editor .form-group @@ -34,4 +34,3 @@ = link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel" - else = link_to "Cancel", snippets_path(@project), class: "btn btn-cancel" - diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml index 82f44a9a5c3..915bf98eb3e 100644 --- a/app/views/snippets/edit.html.haml +++ b/app/views/snippets/edit.html.haml @@ -2,4 +2,4 @@ %h3.page-title Edit Snippet %hr -= render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level += render 'shared/snippets/form', url: snippet_path(@snippet) diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml index 79e2392490d..ca8afb4bb6a 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -2,4 +2,4 @@ %h3.page-title New Snippet %hr -= render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility += render "shared/snippets/form", url: snippets_path(@snippet) -- cgit v1.2.1 From c63194ce6f952173649d7de4038aa96348e90565 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Wed, 1 Feb 2017 18:15:59 +0000 Subject: Check public snippets for spam Apply the same spam checks to public snippets (either personal snippets that are public, or public snippets on public projects) as to issues on public projects. --- app/controllers/concerns/spammable_actions.rb | 2 +- app/controllers/projects/snippets_controller.rb | 8 ++- app/controllers/snippets_controller.rb | 6 +- app/models/concerns/spammable.rb | 8 ++- app/models/project_snippet.rb | 4 ++ app/models/snippet.rb | 12 ++++ app/services/create_snippet_service.rb | 9 ++- app/views/projects/snippets/_actions.html.haml | 5 ++ app/views/snippets/_actions.html.haml | 5 ++ changelogs/unreleased/snippet-spam.yml | 4 ++ config/routes/project.rb | 1 + config/routes/snippets.rb | 1 + lib/api/project_snippets.rb | 2 +- lib/api/snippets.rb | 2 +- .../projects/snippets_controller_spec.rb | 80 ++++++++++++++++++++++ spec/controllers/snippets_controller_spec.rb | 59 ++++++++++++++++ spec/lib/gitlab/import_export/all_models.yml | 1 + spec/requests/api/project_snippets_spec.rb | 48 ++++++++++++- spec/requests/api/snippets_spec.rb | 32 ++++++++- 19 files changed, 277 insertions(+), 12 deletions(-) create mode 100644 changelogs/unreleased/snippet-spam.yml diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index 99acd98ae13..562f92bd83c 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -7,7 +7,7 @@ module SpammableActions def mark_as_spam if SpamService.new(spammable).mark_as_spam! - redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully." + redirect_to spammable, notice: "#{spammable.spammable_entity_type.titlecase} was submitted to Akismet successfully." else redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.' end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 02a97c1c574..5d193f26a8e 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,8 +1,9 @@ class Projects::SnippetsController < Projects::ApplicationController include ToggleAwardEmoji + include SpammableActions before_action :module_enabled - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam] # Allow read any snippet before_action :authorize_read_project_snippet!, except: [:new, :create, :index] @@ -36,8 +37,8 @@ class Projects::SnippetsController < Projects::ApplicationController end def create - @snippet = CreateSnippetService.new(@project, current_user, - snippet_params).execute + create_params = snippet_params.merge(request: request) + @snippet = CreateSnippetService.new(@project, current_user, create_params).execute if @snippet.valid? respond_with(@snippet, @@ -88,6 +89,7 @@ class Projects::SnippetsController < Projects::ApplicationController @snippet ||= @project.snippets.find(params[:id]) end alias_method :awardable, :snippet + alias_method :spammable, :snippet def authorize_read_project_snippet! return render_404 unless can?(current_user, :read_project_snippet, @snippet) diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index dee57e4a388..b169d993688 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,5 +1,6 @@ class SnippetsController < ApplicationController include ToggleAwardEmoji + include SpammableActions before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] @@ -40,8 +41,8 @@ class SnippetsController < ApplicationController end def create - @snippet = CreateSnippetService.new(nil, current_user, - snippet_params).execute + create_params = snippet_params.merge(request: request) + @snippet = CreateSnippetService.new(nil, current_user, create_params).execute respond_with @snippet.becomes(Snippet) end @@ -96,6 +97,7 @@ class SnippetsController < ApplicationController end end alias_method :awardable, :snippet + alias_method :spammable, :snippet def authorize_read_snippet! authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet) diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb index 1aa97debe42..1acff093aa1 100644 --- a/app/models/concerns/spammable.rb +++ b/app/models/concerns/spammable.rb @@ -34,7 +34,13 @@ module Spammable end def check_for_spam - self.errors.add(:base, "Your #{self.class.name.underscore} has been recognized as spam and has been discarded.") if spam? + if spam? + self.errors.add(:base, "Your #{spammable_entity_type} has been recognized as spam and has been discarded.") + end + end + + def spammable_entity_type + self.class.name.underscore end def spam_title diff --git a/app/models/project_snippet.rb b/app/models/project_snippet.rb index 25b5d777641..9bb456eee24 100644 --- a/app/models/project_snippet.rb +++ b/app/models/project_snippet.rb @@ -9,4 +9,8 @@ class ProjectSnippet < Snippet participant :author participant :notes_with_associations + + def check_for_spam? + super && project.public? + end end diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 960f1521be9..2665a7249a3 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -7,6 +7,7 @@ class Snippet < ActiveRecord::Base include Sortable include Awardable include Mentionable + include Spammable cache_markdown_field :title, pipeline: :single_line cache_markdown_field :content @@ -46,6 +47,9 @@ class Snippet < ActiveRecord::Base participant :author participant :notes_with_associations + attr_spammable :title, spam_title: true + attr_spammable :content, spam_description: true + def self.reference_prefix '$' end @@ -127,6 +131,14 @@ class Snippet < ActiveRecord::Base notes.includes(:author) end + def check_for_spam? + public? + end + + def spammable_entity_type + 'snippet' + end + class << self # Searches for snippets with a matching title or file name. # diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb index 95cc9baf406..14f5ba064ff 100644 --- a/app/services/create_snippet_service.rb +++ b/app/services/create_snippet_service.rb @@ -1,5 +1,8 @@ class CreateSnippetService < BaseService def execute + request = params.delete(:request) + api = params.delete(:api) + snippet = if project project.snippets.build(params) else @@ -12,8 +15,12 @@ class CreateSnippetService < BaseService end snippet.author = current_user + snippet.spam = SpamService.new(snippet, request).check(api) + + if snippet.save + UserAgentDetailService.new(snippet, request).create + end - snippet.save snippet end end diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 068a6610350..e2a5107a883 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -8,6 +8,8 @@ - if can?(current_user, :create_project_snippet, @project) = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-inverted btn-create', title: "New snippet" do New snippet + - if @snippet.submittable_as_spam? && current_user.admin? + = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' - if can?(current_user, :create_project_snippet, @project) || can?(current_user, :update_project_snippet, @snippet) .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -27,3 +29,6 @@ %li = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet) do Edit + - if @snippet.submittable_as_spam? && current_user.admin? + %li + = link_to 'Submit as spam', mark_as_spam_namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :post diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 95fc7198104..9a9a3ff9220 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -8,6 +8,8 @@ - if current_user = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do New snippet + - if @snippet.submittable_as_spam? && current_user.admin? + = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' - if current_user .visible-xs-block.dropdown %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } @@ -26,3 +28,6 @@ %li = link_to edit_snippet_path(@snippet) do Edit + - if @snippet.submittable_as_spam? && current_user.admin? + %li + = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post diff --git a/changelogs/unreleased/snippet-spam.yml b/changelogs/unreleased/snippet-spam.yml new file mode 100644 index 00000000000..4867f088953 --- /dev/null +++ b/changelogs/unreleased/snippet-spam.yml @@ -0,0 +1,4 @@ +--- +title: Check public snippets for spam +merge_request: +author: diff --git a/config/routes/project.rb b/config/routes/project.rb index f36febc6e04..efe2fbc521d 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -64,6 +64,7 @@ constraints(ProjectUrlConstrainer.new) do resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do member do get 'raw' + post :mark_as_spam end end diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb index 3ca096f31ba..ce0d1314292 100644 --- a/config/routes/snippets.rb +++ b/config/routes/snippets.rb @@ -2,6 +2,7 @@ resources :snippets, concerns: :awardable do member do get 'raw' get 'download' + post :mark_as_spam end end diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb index 9d8c5b63685..dcc0c82ee27 100644 --- a/lib/api/project_snippets.rb +++ b/lib/api/project_snippets.rb @@ -58,7 +58,7 @@ module API end post ":id/snippets" do authorize! :create_project_snippet, user_project - snippet_params = declared_params + snippet_params = declared_params.merge(request: request, api: true) snippet_params[:content] = snippet_params.delete(:code) snippet = CreateSnippetService.new(user_project, current_user, snippet_params).execute diff --git a/lib/api/snippets.rb b/lib/api/snippets.rb index e096e636806..eb9ece49e7f 100644 --- a/lib/api/snippets.rb +++ b/lib/api/snippets.rb @@ -64,7 +64,7 @@ module API desc: 'The visibility level of the snippet' end post do - attrs = declared_params(include_missing: false) + attrs = declared_params(include_missing: false).merge(request: request, api: true) snippet = CreateSnippetService.new(nil, current_user, attrs).execute if snippet.persisted? diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 32b0e42c3cd..88e4f81f232 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -69,6 +69,86 @@ describe Projects::SnippetsController do end end + describe 'POST #create' do + def create_snippet(project, snippet_params = {}) + sign_in(user) + + project.team << [user, :developer] + + post :create, { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + project_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + } + end + + context 'when the snippet is spam' do + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the project is private' do + let(:private_project) { create(:project_empty_repo, :private) } + + context 'when the snippet is public' do + it 'creates the snippet' do + expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }. + to change { Snippet.count }.by(1) + end + end + end + + context 'when the project is public' do + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to render_template(:new) + end + + it 'creates a spam log' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end + end + + describe 'POST #mark_as_spam' do + let(:snippet) { create(:project_snippet, :private, project: project, author: user) } + + before do + allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true) + stub_application_setting(akismet_enabled: true) + end + + def mark_as_spam + admin = create(:admin) + create(:user_agent_detail, subject: snippet) + project.team << [admin, :master] + sign_in(admin) + + post :mark_as_spam, + namespace_id: project.namespace.path, + project_id: project.path, + id: snippet.id + end + + it 'updates the snippet' do + mark_as_spam + + expect(snippet.reload).not_to be_submittable_as_spam + end + end + %w[show raw].each do |action| describe "GET ##{action}" do context 'when the project snippet is private' do diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index d76fe9f580f..dadcb90cfc2 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -138,6 +138,65 @@ describe SnippetsController do end end + describe 'POST #create' do + def create_snippet(snippet_params = {}) + sign_in(user) + + post :create, { + personal_snippet: { title: 'Title', content: 'Content' }.merge(snippet_params) + } + end + + context 'when the snippet is spam' do + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to render_template(:new) + end + + it 'creates a spam log' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end + + describe 'POST #mark_as_spam' do + let(:snippet) { create(:personal_snippet, :public, author: user) } + + before do + allow_any_instance_of(AkismetService).to receive_messages(submit_spam: true) + stub_application_setting(akismet_enabled: true) + end + + def mark_as_spam + admin = create(:admin) + create(:user_agent_detail, subject: snippet) + sign_in(admin) + + post :mark_as_spam, id: snippet.id + end + + it 'updates the snippet' do + mark_as_spam + + expect(snippet.reload).not_to be_submittable_as_spam + end + end + %w(raw download).each do |action| describe "GET #{action}" do context 'when the personal snippet is private' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 7fb6829f582..20241d4d63e 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -52,6 +52,7 @@ snippets: - project - notes - award_emoji +- user_agent_detail releases: - project project_members: diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 01032c0929b..9e25e30bc86 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -4,6 +4,7 @@ describe API::ProjectSnippets, api: true do include ApiHelpers let(:project) { create(:empty_project, :public) } + let(:user) { create(:user) } let(:admin) { create(:admin) } describe 'GET /projects/:project_id/snippets/:id' do @@ -50,7 +51,7 @@ describe API::ProjectSnippets, api: true do title: 'Test Title', file_name: 'test.rb', code: 'puts "hello world"', - visibility_level: Gitlab::VisibilityLevel::PUBLIC + visibility_level: Snippet::PUBLIC } end @@ -72,6 +73,51 @@ describe API::ProjectSnippets, api: true do expect(response).to have_http_status(400) end + + context 'when the snippet is spam' do + def create_snippet(project, snippet_params = {}) + project.team << [user, :developer] + + post api("/projects/#{project.id}/snippets", user), params.merge(snippet_params) + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the project is private' do + let(:private_project) { create(:project_empty_repo, :private) } + + context 'when the snippet is public' do + it 'creates the snippet' do + expect { create_snippet(private_project, visibility_level: Snippet::PUBLIC) }. + to change { Snippet.count }.by(1) + end + end + end + + context 'when the project is public' do + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(project, visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to have_http_status(400) + end + + it 'creates a spam log' do + expect { create_snippet(project, visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end + end end describe 'PUT /projects/:project_id/snippets/:id/' do diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index f6fb6ea5506..6b9a739b439 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -80,7 +80,7 @@ describe API::Snippets, api: true do title: 'Test Title', file_name: 'test.rb', content: 'puts "hello world"', - visibility_level: Gitlab::VisibilityLevel::PUBLIC + visibility_level: Snippet::PUBLIC } end @@ -101,6 +101,36 @@ describe API::Snippets, api: true do expect(response).to have_http_status(400) end + + context 'when the snippet is spam' do + def create_snippet(snippet_params = {}) + post api('/snippets', user), params.merge(snippet_params) + end + + before do + allow_any_instance_of(AkismetService).to receive(:is_spam?).and_return(true) + end + + context 'when the snippet is private' do + it 'creates the snippet' do + expect { create_snippet(visibility_level: Snippet::PRIVATE) }. + to change { Snippet.count }.by(1) + end + end + + context 'when the snippet is public' do + it 'rejects the shippet' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + not_to change { Snippet.count } + expect(response).to have_http_status(400) + end + + it 'creates a spam log' do + expect { create_snippet(visibility_level: Snippet::PUBLIC) }. + to change { SpamLog.count }.by(1) + end + end + end end describe 'PUT /snippets/:id' do -- cgit v1.2.1 From 19dda1606b4dc76160bf2198ab95f2998eccaec8 Mon Sep 17 00:00:00 2001 From: George Andrinopoulos <geoandri@gmail.com> Date: Thu, 2 Feb 2017 12:46:14 +0200 Subject: Force new password after password reset via API --- changelogs/unreleased/24606-force-password-reset-on-next-login.yml | 4 ++++ lib/api/users.rb | 2 ++ spec/requests/api/users_spec.rb | 6 ++++++ 3 files changed, 12 insertions(+) create mode 100644 changelogs/unreleased/24606-force-password-reset-on-next-login.yml diff --git a/changelogs/unreleased/24606-force-password-reset-on-next-login.yml b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml new file mode 100644 index 00000000000..fd671d04a9f --- /dev/null +++ b/changelogs/unreleased/24606-force-password-reset-on-next-login.yml @@ -0,0 +1,4 @@ +--- +title: Force new password after password reset via API +merge_request: +author: George Andrinopoulos diff --git a/lib/api/users.rb b/lib/api/users.rb index 11a7368b4c0..0ed468626b7 100644 --- a/lib/api/users.rb +++ b/lib/api/users.rb @@ -160,6 +160,8 @@ module API end end + user_params.merge!(password_expires_at: Time.now) if user_params[:password].present? + if user.update_attributes(user_params.except(:extern_uid, :provider)) present user, with: Entities::UserPublic else diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5bf5bf0739e..f9127096953 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -305,6 +305,12 @@ describe API::Users, api: true do expect(user.reload.bio).to eq('new test bio') end + it "updates user with new password and forces reset on next login" do + put api("/users/#{user.id}", admin), { password: '12345678' } + expect(response).to have_http_status(200) + expect(user.reload.password_expires_at).to be < Time.now + end + it "updates user with organization" do put api("/users/#{user.id}", admin), { organization: 'GitLab' } -- cgit v1.2.1 From 21ea4863ea42a83566193862579329d865176ff9 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 2 Feb 2017 11:09:24 +0000 Subject: Fixes broken build: Use jquery to get the element position in the page --- spec/features/merge_requests/toggler_behavior_spec.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb index 6958f6a2c9f..44a9b545ff8 100644 --- a/spec/features/merge_requests/toggler_behavior_spec.rb +++ b/spec/features/merge_requests/toggler_behavior_spec.rb @@ -18,11 +18,10 @@ feature 'toggler_behavior', js: true, feature: true do it 'should be scrolled down to fragment' do page_height = page.current_window.size[1] page_scroll_y = page.evaluate_script("window.scrollY") - fragment_position_top = page.evaluate_script("document.querySelector('#{fragment_id}').getBoundingClientRect().top") - + fragment_position_top = page.evaluate_script("$('#{fragment_id}').offset().top") expect(find('.js-toggle-content').visible?).to eq true expect(find(fragment_id).visible?).to eq true - expect(fragment_position_top).to be > page_scroll_y + expect(fragment_position_top).to be >= page_scroll_y expect(fragment_position_top).to be < (page_scroll_y + page_height) end end -- cgit v1.2.1 From b329a4675ab3641e5b0526da40ed4f47d61b53d4 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Thu, 2 Feb 2017 18:22:32 +0600 Subject: removes old css class from everywhere --- app/assets/stylesheets/framework/buttons.scss | 4 ---- app/helpers/blob_helper.rb | 4 ++-- app/views/projects/merge_requests/conflicts/_file_actions.html.haml | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index bb6129158d9..cda46223492 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -330,10 +330,6 @@ } } -.btn-file-option { - background: linear-gradient(180deg, $white-light 25%, $gray-light 100%); -} - .btn-build { margin-left: 10px; diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index c3508443d8a..311a70725ab 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -21,7 +21,7 @@ module BlobHelper options[:link_opts]) if !on_top_of_branch?(project, ref) - button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' } + button_tag "Edit", class: "btn disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' } elsif can_edit_blob?(blob, project, ref) link_to "Edit", edit_path, class: 'btn btn-sm' elsif can?(current_user, :fork_project, project) @@ -32,7 +32,7 @@ module BlobHelper } fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params) - link_to "Edit", fork_path, class: 'btn btn-file-option', method: :post + link_to "Edit", fork_path, class: 'btn', method: :post end end diff --git a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml index 2595ce74ac0..0839880713f 100644 --- a/app/views/projects/merge_requests/conflicts/_file_actions.html.haml +++ b/app/views/projects/merge_requests/conflicts/_file_actions.html.haml @@ -8,5 +8,5 @@ '@click' => "onClickResolveModeButton(file, 'edit')", type: 'button' } Edit inline - %a.btn.view-file.btn-file-option{ ":href" => "file.blobPath" } + %a.btn.view-file{ ":href" => "file.blobPath" } View file @{{conflictsData.shortCommitSha}} -- cgit v1.2.1 From 34918d94c011e8f81bd962d43d67fe8bd9f21e3e Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Thu, 2 Feb 2017 13:10:42 +0000 Subject: Use `add_$role` helper in snippets specs --- spec/controllers/projects/snippets_controller_spec.rb | 8 ++++---- spec/requests/api/project_snippets_spec.rb | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 88e4f81f232..19e948d8fb8 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -6,8 +6,8 @@ describe Projects::SnippetsController do let(:user2) { create(:user) } before do - project.team << [user, :master] - project.team << [user2, :master] + project.add_master(user) + project.add_master(user2) end describe 'GET #index' do @@ -73,7 +73,7 @@ describe Projects::SnippetsController do def create_snippet(project, snippet_params = {}) sign_in(user) - project.team << [user, :developer] + project.add_developer(user) post :create, { namespace_id: project.namespace.to_param, @@ -133,7 +133,7 @@ describe Projects::SnippetsController do def mark_as_spam admin = create(:admin) create(:user_agent_detail, subject: snippet) - project.team << [admin, :master] + project.add_master(admin) sign_in(admin) post :mark_as_spam, diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb index 9e25e30bc86..45d5ae267c5 100644 --- a/spec/requests/api/project_snippets_spec.rb +++ b/spec/requests/api/project_snippets_spec.rb @@ -23,7 +23,7 @@ describe API::ProjectSnippets, api: true do let(:user) { create(:user) } it 'returns all snippets available to team member' do - project.team << [user, :developer] + project.add_developer(user) public_snippet = create(:project_snippet, :public, project: project) internal_snippet = create(:project_snippet, :internal, project: project) private_snippet = create(:project_snippet, :private, project: project) @@ -76,7 +76,7 @@ describe API::ProjectSnippets, api: true do context 'when the snippet is spam' do def create_snippet(project, snippet_params = {}) - project.team << [user, :developer] + project.add_developer(user) post api("/projects/#{project.id}/snippets", user), params.merge(snippet_params) end -- cgit v1.2.1 From 161d74f1a6e58dae29ad6ee37b7d70cdb4999cc4 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 2 Feb 2017 09:31:09 +0000 Subject: Fixed group label in issuable sidebar Group label link was pointing to group#issues rather than the projects issues. This fixes that by sending the correct subject to the link_to_label helper method. Closes #27253 --- app/views/shared/issuable/_sidebar.html.haml | 2 +- changelogs/unreleased/group-label-sidebar-link.yml | 4 ++++ spec/features/issues/group_label_sidebar_spec.rb | 21 +++++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/group-label-sidebar-link.yml create mode 100644 spec/features/issues/group_label_sidebar_spec.rb diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index ec9bcaf63dd..10fa7901874 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -130,7 +130,7 @@ .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) } - if selected_labels.any? - selected_labels.each do |label| - = link_to_label(label, type: issuable.to_ability_name) + = link_to_label(label, subject: issuable.project, type: issuable.to_ability_name) - else %span.no-value None .selectbox.hide-collapsed diff --git a/changelogs/unreleased/group-label-sidebar-link.yml b/changelogs/unreleased/group-label-sidebar-link.yml new file mode 100644 index 00000000000..c11c2d4ede1 --- /dev/null +++ b/changelogs/unreleased/group-label-sidebar-link.yml @@ -0,0 +1,4 @@ +--- +title: Fixed group label links in issue/merge request sidebar +merge_request: +author: diff --git a/spec/features/issues/group_label_sidebar_spec.rb b/spec/features/issues/group_label_sidebar_spec.rb new file mode 100644 index 00000000000..fc8515cfe9b --- /dev/null +++ b/spec/features/issues/group_label_sidebar_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe 'Group label on issue', :feature do + it 'renders link to the project issues page' do + group = create(:group) + project = create(:empty_project, :public, namespace: group) + feature = create(:group_label, group: group, title: 'feature') + issue = create(:labeled_issue, project: project, labels: [feature]) + label_link = namespace_project_issues_path( + project.namespace, + project, + label_name: [feature.name] + ) + + visit namespace_project_issue_path(project.namespace, project, issue) + + link = find('.issuable-show-labels a') + + expect(link[:href]).to eq(label_link) + end +end -- cgit v1.2.1 From d796e4fc371a8613e77c70f5571813818c6a35ad Mon Sep 17 00:00:00 2001 From: George Andrinopoulos <geoandri@gmail.com> Date: Thu, 2 Feb 2017 17:15:02 +0200 Subject: Update api docs and minor changes --- doc/api/users.md | 1 + spec/requests/api/users_spec.rb | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/api/users.md b/doc/api/users.md index 28b6c7bd491..fea9bdf9639 100644 --- a/doc/api/users.md +++ b/doc/api/users.md @@ -271,6 +271,7 @@ Parameters: - `can_create_group` (optional) - User can create groups - true or false - `external` (optional) - Flags the user as external - true or false(default) +On password update, user will be forced to change it upon next login. Note, at the moment this method does only return a `404` error, even in cases where a `409` (Conflict) would be more appropriate, e.g. when renaming the email address to some existing one. diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index f9127096953..8692f9da976 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -306,9 +306,10 @@ describe API::Users, api: true do end it "updates user with new password and forces reset on next login" do - put api("/users/#{user.id}", admin), { password: '12345678' } + put api("/users/#{user.id}", admin), password: '12345678' + expect(response).to have_http_status(200) - expect(user.reload.password_expires_at).to be < Time.now + expect(user.reload.password_expires_at).to be <= Time.now end it "updates user with organization" do -- cgit v1.2.1 From 01b14b2d050ed32dd25907cda5a5ea49dc99d6a0 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones <jedwardsjones@gitlab.com> Date: Thu, 2 Feb 2017 15:56:29 +0000 Subject: Fix rubocop error from pages EE->CE port --- config/initializers/1_settings.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 0015ddf902d..71046d7860e 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -268,8 +268,8 @@ Settings.registry['path'] = File.expand_path(Settings.registry['path' Settings['pages'] ||= Settingslogic.new({}) Settings.pages['enabled'] = false if Settings.pages['enabled'].nil? Settings.pages['path'] = File.expand_path(Settings.pages['path'] || File.join(Settings.shared['path'], "pages"), Rails.root) -Settings.pages['host'] ||= "example.com" Settings.pages['https'] = false if Settings.pages['https'].nil? +Settings.pages['host'] ||= "example.com" Settings.pages['port'] ||= Settings.pages.https ? 443 : 80 Settings.pages['protocol'] ||= Settings.pages.https ? "https" : "http" Settings.pages['url'] ||= Settings.send(:build_pages_url) -- cgit v1.2.1 From 6dcfc4002e24c54c2f60b53bb2761a300bf68735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me> Date: Thu, 2 Feb 2017 10:21:14 +0100 Subject: Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Rémy Coutable <remy@rymai.me> --- .../20452-remove-require-from-request_profiler-initializer.yml | 4 ++++ config/initializers/request_profiler.rb | 2 -- lib/gitlab/request_profiler/middleware.rb | 3 +-- 3 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml diff --git a/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml new file mode 100644 index 00000000000..965d0648adf --- /dev/null +++ b/changelogs/unreleased/20452-remove-require-from-request_profiler-initializer.yml @@ -0,0 +1,4 @@ +--- +title: Don't require lib/gitlab/request_profiler/middleware.rb in config/initializers/request_profiler.rb +merge_request: +author: diff --git a/config/initializers/request_profiler.rb b/config/initializers/request_profiler.rb index a9aa802681a..fb5a7b8372e 100644 --- a/config/initializers/request_profiler.rb +++ b/config/initializers/request_profiler.rb @@ -1,5 +1,3 @@ -require 'gitlab/request_profiler/middleware' - Rails.application.configure do |config| config.middleware.use(Gitlab::RequestProfiler::Middleware) end diff --git a/lib/gitlab/request_profiler/middleware.rb b/lib/gitlab/request_profiler/middleware.rb index 786e1d49f5e..ef42b0557e0 100644 --- a/lib/gitlab/request_profiler/middleware.rb +++ b/lib/gitlab/request_profiler/middleware.rb @@ -1,5 +1,4 @@ require 'ruby-prof' -require_dependency 'gitlab/request_profiler' module Gitlab module RequestProfiler @@ -20,7 +19,7 @@ module Gitlab header_token = env['HTTP_X_PROFILE_TOKEN'] return unless header_token.present? - profile_token = RequestProfiler.profile_token + profile_token = Gitlab::RequestProfiler.profile_token return unless profile_token.present? header_token == profile_token -- cgit v1.2.1 From 5a099315eb29c345925141609dc5a3a395312016 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 10:46:47 -0600 Subject: disable webpack proxy in rspec environment due to conflicts with webmock gem --- config/initializers/static_files.rb | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index 718cdd51782..cb332e15c11 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -16,21 +16,29 @@ if app.config.serve_static_files # If webpack-dev-server is configured, proxy webpack's public directory # instead of looking for static assets if Gitlab.config.webpack.dev_server.enabled - app.config.webpack.dev_server.merge!( + dev_server = { enabled: true, - host: Gitlab.config.gitlab.host, - port: Gitlab.config.gitlab.port, - https: Gitlab.config.gitlab.https, + host: Gitlab.config.webpack.dev_server.host, + port: Gitlab.config.webpack.dev_server.port, manifest_host: Gitlab.config.webpack.dev_server.host, manifest_port: Gitlab.config.webpack.dev_server.port, - ) + } - app.config.middleware.insert_before( - Gitlab::Middleware::Static, - Gitlab::Middleware::WebpackProxy, - proxy_path: app.config.webpack.public_path, - proxy_host: Gitlab.config.webpack.dev_server.host, - proxy_port: Gitlab.config.webpack.dev_server.port, - ) + if Rails.env.development? + dev_server.merge!( + host: Gitlab.config.gitlab.host, + port: Gitlab.config.gitlab.port, + https: Gitlab.config.gitlab.https, + ) + app.config.middleware.insert_before( + Gitlab::Middleware::Static, + Gitlab::Middleware::WebpackProxy, + proxy_path: app.config.webpack.public_path, + proxy_host: Gitlab.config.webpack.dev_server.host, + proxy_port: Gitlab.config.webpack.dev_server.port, + ) + end + + app.config.webpack.dev_server.merge!(dev_server) end end -- cgit v1.2.1 From 32b3a82d81b76a0748ea316405a38b4abeb3a512 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@mcgivern.me.uk> Date: Thu, 2 Feb 2017 17:22:11 +0000 Subject: Fix Ruby verification command --- doc/install/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/installation.md b/doc/install/installation.md index 425c5d93efb..b2d5d51d37d 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -124,7 +124,7 @@ Download Ruby and compile it: mkdir /tmp/ruby && cd /tmp/ruby curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.3.tar.gz - echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz + echo '1014ee699071aa2ddd501907d18cbe15399c997d ruby-2.3.3.tar.gz' | shasum -c - && tar xzf ruby-2.3.3.tar.gz cd ruby-2.3.3 ./configure --disable-install-rdoc make -- cgit v1.2.1 From 4fc0a6e943cedbf41ced19ef1c094e1c56f5ca9b Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 12:40:49 -0600 Subject: ignore node_modules in rubocop --- .rubocop.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.rubocop.yml b/.rubocop.yml index bf2b2d8afc2..cfff42e5c99 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -17,6 +17,7 @@ AllCops: # Exclude some GitLab files Exclude: - 'vendor/**/*' + - 'node_modules/**/*' - 'db/*' - 'db/fixtures/**/*' - 'tmp/**/*' -- cgit v1.2.1 From 4fb757407d7ebae64ba73a3ce94bf33302cad500 Mon Sep 17 00:00:00 2001 From: Cindy Pallares <cindy@gitlab.com> Date: Thu, 2 Feb 2017 12:30:07 -0600 Subject: Update installation docs to include Docker, others As per this comment https://gitlab.com/gitlab-com/support/issues/374#note_22056587 we're moving documentation around from the support workflows. --- doc/install/README.md | 3 + doc/install/digitaloceandocker.md | 136 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 doc/install/digitaloceandocker.md diff --git a/doc/install/README.md b/doc/install/README.md index 239f5f301ec..2d2fd8cb380 100644 --- a/doc/install/README.md +++ b/doc/install/README.md @@ -4,3 +4,6 @@ - [Requirements](requirements.md) - [Structure](structure.md) - [Database MySQL](database_mysql.md) +- [Digital Ocean and Docker](digitaloceandocker.md) +- [Docker](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/docker) +- [All installation methods](https://about.gitlab.com/installation/) diff --git a/doc/install/digitaloceandocker.md b/doc/install/digitaloceandocker.md new file mode 100644 index 00000000000..820060a489b --- /dev/null +++ b/doc/install/digitaloceandocker.md @@ -0,0 +1,136 @@ +# Digital Ocean and Docker + +## Initial setup + +In this guide you'll configure a Digital Ocean droplet and set up Docker +locally on either macOS or Linux. + +### On macOS + +#### Install Docker Toolbox + +1. [https://www.docker.com/products/docker-toolbox](https://www.docker.com/products/docker-toolbox) + +### On Linux + +#### Install Docker Engine + +1. [https://docs.docker.com/engine/installation/linux](https://docs.docker.com/engine/installation/linux/) + +#### Install Docker Machine + +1. [https://docs.docker.com/machine/install-machine](https://docs.docker.com/machine/install-machine/) + +_The rest of the steps are identical for macOS and Linux_ + +### Create new docker host + +1. Login to Digital Ocean +1. Generate a new API token at https://cloud.digitalocean.com/settings/api/tokens + + +This command will create a new DO droplet called `gitlab-test-env-do` that will act as a docker host. + +**Note: 4GB is the minimum requirement for a Docker host that will run more then one GitLab instance** + ++ RAM: 4GB ++ Name: `gitlab-test-env-do` ++ Driver: `digitalocean` + + +**Set the DO token** - Replace the string below with your generated token + +``` +export DOTOKEN=cf3dfd0662933203005c4a73396214b7879d70aabc6352573fe178d340a80248 +``` + +**Create the machine** + +``` +docker-machine create \ + --driver digitalocean \ + --digitalocean-access-token=$DOTOKEN \ + --digitalocean-size "4gb" \ + gitlab-test-env-do +``` + ++ Resource: https://docs.docker.com/machine/drivers/digital-ocean/ + + +### Creating GitLab test instance + + +#### Connect your shell to the new machine + + +In this example we'll create a GitLab EE 8.10.8 instance. + + +First connect the docker client to the docker host you created previously. + +``` +eval "$(docker-machine env gitlab-test-env-do)" +``` + +You can add this to your `~/.bash_profile` file to ensure the `docker` client uses the `gitlab-test-env-do` docker host + + +#### Create new GitLab container + ++ HTTP port: `8888` ++ SSH port: `2222` + + Set `gitlab_shell_ssh_port` using `--env GITLAB_OMNIBUS_CONFIG ` ++ Hostname: IP of docker host ++ Container name: `gitlab-test-8.10` ++ GitLab version: **EE** `8.10.8-ee.0` + +##### Setup container settings + +``` +export SSH_PORT=2222 +export HTTP_PORT=8888 +export VERSION=8.10.8-ee.0 +export NAME=gitlab-test-8.10 +``` + +##### Create container +``` +docker run --detach \ +--env GITLAB_OMNIBUS_CONFIG="external_url 'http://$(docker-machine ip gitlab-test-env-do):$HTTP_PORT'; gitlab_rails['gitlab_shell_ssh_port'] = $SSH_PORT;" \ +--hostname $(docker-machine ip gitlab-test-env-do) \ +-p $HTTP_PORT:$HTTP_PORT -p $SSH_PORT:22 \ +--name $NAME \ +gitlab/gitlab-ee:$VERSION +``` + +#### Connect to the GitLab container + +##### Retrieve the docker host IP + +``` +docker-machine ip gitlab-test-env-do +# example output: 192.168.151.134 +``` + + ++ Browse to: http://192.168.151.134:8888/ + + +##### Execute interactive shell/edit configuration + + +``` +docker exec -it $NAME /bin/bash +``` + +``` +# example commands +root@192:/# vi /etc/gitlab/gitlab.rb +root@192:/# gitlab-ctl reconfigure +``` + +#### Resources + ++ [https://docs.gitlab.com/omnibus/docker/](https://docs.gitlab.com/omnibus/docker/) ++ [https://docs.docker.com/machine/get-started/](https://docs.docker.com/machine/get-started/) ++ [https://docs.docker.com/machine/reference/ip/](https://docs.docker.com/machine/reference/ip/)+ -- cgit v1.2.1 From 42086615e5bede8b0720b410b18017d12c87adfa Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 12:55:01 -0600 Subject: fix intermittant errors in merge_commit_message_toggle_spec.rb --- app/assets/javascripts/merge_request.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js index 09ee8dbe9d7..37af422a09e 100644 --- a/app/assets/javascripts/merge_request.js +++ b/app/assets/javascripts/merge_request.js @@ -110,9 +110,8 @@ }; MergeRequest.prototype.initCommitMessageListeners = function() { - var textarea = $('textarea.js-commit-message'); - - $('a.js-with-description-link').on('click', function(e) { + $(document).on('click', 'a.js-with-description-link', function(e) { + var textarea = $('textarea.js-commit-message'); e.preventDefault(); textarea.val(textarea.data('messageWithDescription')); @@ -120,7 +119,8 @@ $('p.js-without-description-hint').show(); }); - $('a.js-without-description-link').on('click', function(e) { + $(document).on('click', 'a.js-without-description-link', function(e) { + var textarea = $('textarea.js-commit-message'); e.preventDefault(); textarea.val(textarea.data('messageWithoutDescription')); -- cgit v1.2.1 From bccd791d4d698b6011ecea781d0ebc7c9ec1ddf8 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 12:57:28 -0600 Subject: fix errors within gl_dropdown_spec.js when running in Karma --- app/assets/javascripts/gl_dropdown.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index d2f66cf5249..5c86e98567a 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -249,7 +249,7 @@ _this.fullData = data; _this.parseData(_this.fullData); _this.focusTextInput(); - if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val().trim() !== '') { + if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') { return _this.filter.input.trigger('input'); } }; -- cgit v1.2.1 From 36025b67d90633e2c64c182727424dd8dce1b03f Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:00:42 -0600 Subject: fix broken reference to formatDate in a CommonJS environment --- app/assets/javascripts/users/calendar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index e7280d643d3..e0e40ad3adb 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -33,7 +33,7 @@ date.setDate(date.getDate() + i); var day = date.getDay(); - var count = timestamps[dateFormat(date, 'yyyy-mm-dd')]; + var count = timestamps[date.format('yyyy-mm-dd')]; // Create a new group array if this is the first day of the week // or if is first object @@ -122,7 +122,7 @@ if (stamp.count > 0) { contribText = stamp.count + " contribution" + (stamp.count > 1 ? 's' : ''); } - dateText = dateFormat(date, 'mmm d, yyyy'); + dateText = date.format('mmm d, yyyy'); return contribText + "<br />" + (gl.utils.getDayName(date)) + " " + dateText; }; })(this)).attr('class', 'user-contrib-cell js-tooltip').attr('fill', (function(_this) { -- cgit v1.2.1 From 96bbe965d2bfd564d82067d1e21beac17434f525 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:02:26 -0600 Subject: remove redundant "data-toggle" attribute so Vue doesn't complain --- app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index b195b0ef3ba..a7176e27ea1 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -28,7 +28,6 @@ data-toggle="dropdown" title="Manual build" data-placement="top" - data-toggle="dropdown" aria-label="Manual build" > <span v-html='svgs.iconPlay' aria-hidden="true"></span> @@ -54,7 +53,6 @@ data-toggle="dropdown" title="Artifacts" data-placement="top" - data-toggle="dropdown" aria-label="Artifacts" > <i class="fa fa-download" aria-hidden="true"></i> -- cgit v1.2.1 From fbd09871ca7003242053fbca10d9c0e96e7a799d Mon Sep 17 00:00:00 2001 From: Bryce Johnson <bryce@gitlab.com> Date: Fri, 13 Jan 2017 16:54:16 -0500 Subject: Remove turbolinks. --- Gemfile | 1 - Gemfile.lock | 3 - app/assets/javascripts/admin.js | 5 +- app/assets/javascripts/application.js | 4 +- app/assets/javascripts/breakpoints.js | 1 + app/assets/javascripts/build.js | 3 +- .../filtered_search_dropdown_manager.js.es6 | 4 +- .../filtered_search/filtered_search_manager.js.es6 | 10 +-- app/assets/javascripts/gl_dropdown.js | 3 +- app/assets/javascripts/issuable.js.es6 | 5 +- .../lib/utils/bootstrap_linked_tabs.js.es6 | 1 - app/assets/javascripts/lib/utils/url_utility.js | 80 -------------------- .../javascripts/lib/utils/url_utility.js.es6 | 86 ++++++++++++++++++++++ app/assets/javascripts/line_highlighter.js | 1 - app/assets/javascripts/logo.js | 9 +-- app/assets/javascripts/merge_request_tabs.js.es6 | 3 +- app/assets/javascripts/merge_request_widget.js.es6 | 5 +- app/assets/javascripts/project.js | 3 +- app/assets/javascripts/project_import.js | 3 +- app/assets/javascripts/render_gfm.js | 2 +- app/assets/javascripts/shortcuts.js | 3 +- app/assets/javascripts/shortcuts_issuable.js | 3 +- app/assets/javascripts/sidebar.js.es6 | 2 +- app/assets/javascripts/smart_interval.js.es6 | 5 +- app/assets/javascripts/todos.js.es6 | 7 +- app/assets/javascripts/tree.js | 6 +- app/assets/javascripts/user_tabs.js.es6 | 1 - app/assets/javascripts/vue_pagination/index.js.es6 | 4 +- .../vue_pipelines_index/pipelines.js.es6 | 4 +- .../javascripts/vue_realtime_listener/index.js.es6 | 4 +- app/assets/stylesheets/framework.scss | 1 - app/assets/stylesheets/framework/progress.scss | 5 -- app/helpers/javascript_helper.rb | 2 +- app/views/devise/shared/_omniauth_box.html.haml | 2 +- app/views/import/bitbucket/status.html.haml | 2 +- app/views/layouts/_head.html.haml | 2 + app/views/layouts/application.html.haml | 3 - app/views/profiles/accounts/show.html.haml | 2 +- app/views/projects/merge_requests/_show.html.haml | 8 +- app/views/projects/new.html.haml | 2 +- app/views/shared/issuable/_filter.html.haml | 2 +- ...anticipate-obstacles-to-removing-turbolinks.yml | 4 + features/steps/project/merge_requests.rb | 1 + spec/javascripts/behaviors/autosize_spec.js | 2 +- spec/javascripts/behaviors/requires_input_spec.js | 6 -- spec/javascripts/bootstrap_linked_tabs_spec.js.es6 | 1 - spec/javascripts/build_spec.js.es6 | 7 +- spec/javascripts/gl_dropdown_spec.js.es6 | 7 +- spec/javascripts/issuable_spec.js.es6 | 17 ++--- spec/javascripts/merge_request_tabs_spec.js | 1 - spec/javascripts/search_autocomplete_spec.js | 2 - spec/javascripts/smart_interval_spec.js.es6 | 2 +- spec/javascripts/spec_helper.js | 1 - vendor/assets/javascripts/jquery.turbolinks.js | 49 ------------ 54 files changed, 161 insertions(+), 241 deletions(-) delete mode 100644 app/assets/javascripts/lib/utils/url_utility.js create mode 100644 app/assets/javascripts/lib/utils/url_utility.js.es6 delete mode 100644 app/assets/stylesheets/framework/progress.scss create mode 100644 changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml delete mode 100644 vendor/assets/javascripts/jquery.turbolinks.js diff --git a/Gemfile b/Gemfile index dd7c93c5a75..62064fa82b6 100644 --- a/Gemfile +++ b/Gemfile @@ -222,7 +222,6 @@ gem 'chronic_duration', '~> 0.10.6' gem 'sass-rails', '~> 5.0.6' gem 'coffee-rails', '~> 4.1.0' gem 'uglifier', '~> 2.7.2' -gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6' gem 'addressable', '~> 2.3.8' gem 'bootstrap-sass', '~> 3.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 3b207d19d1f..6db54b77979 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -266,8 +266,6 @@ GEM mime-types (>= 1.16, < 3) posix-spawn (~> 0.3) gitlab-markup (1.5.1) - gitlab-turbolinks-classic (2.5.6) - coffee-rails gitlab_omniauth-ldap (1.2.1) net-ldap (~> 0.9) omniauth (~> 1.0) @@ -891,7 +889,6 @@ DEPENDENCIES github-linguist (~> 4.7.0) gitlab-flowdock-git-hook (~> 1.0.1) gitlab-markup (~> 1.5.1) - gitlab-turbolinks-classic (~> 2.5, >= 2.5.6) gitlab_omniauth-ldap (~> 1.2.1) gollum-lib (~> 4.2) gollum-rugged_adapter (~> 0.4.2) diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js index 993f427c9fb..424dc719c78 100644 --- a/app/assets/javascripts/admin.js +++ b/app/assets/javascripts/admin.js @@ -1,5 +1,4 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, max-len */ -/* global Turbolinks */ (function() { this.Admin = (function() { @@ -42,10 +41,10 @@ return $('.change-owner-link').show(); }); $('li.project_member').bind('ajax:success', function() { - return Turbolinks.visit(location.href); + return gl.utils.refreshCurrentPage(); }); $('li.group_member').bind('ajax:success', function() { - return Turbolinks.visit(location.href); + return gl.utils.refreshCurrentPage(); }); showBlacklistType = function() { if ($("input[name='blacklist_type']:checked").val() === 'file') { diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 4849aab50f4..ad95c1b9dfb 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -24,9 +24,7 @@ /*= require jquery.waitforimages */ /*= require jquery.atwho */ /*= require jquery.scrollTo */ -/*= require jquery.turbolinks */ /*= require js.cookie */ -/*= require turbolinks */ /*= require autosave */ /*= require bootstrap/affix */ /*= require bootstrap/alert */ @@ -64,7 +62,7 @@ /*= require es6-promise.auto */ (function () { - document.addEventListener('page:fetch', function () { + document.addEventListener('beforeunload', function () { // Unbind scroll events $(document).off('scroll'); // Close any open tooltips diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js index eae062a3aa3..f8dac1ff56e 100644 --- a/app/assets/javascripts/breakpoints.js +++ b/app/assets/javascripts/breakpoints.js @@ -43,6 +43,7 @@ BreakpointInstance.prototype.getBreakpointSize = function() { var $visibleDevice; $visibleDevice = this.visibleDevice; + // TODO: Consider refactoring in light of turbolinks removal. // the page refreshed via turbolinks if (!$visibleDevice().length) { this.setup(); diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js index 0df84234520..0152be88b48 100644 --- a/app/assets/javascripts/build.js +++ b/app/assets/javascripts/build.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top */ /* global Breakpoints */ -/* global Turbolinks */ (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; @@ -127,7 +126,7 @@ pageUrl += DOWN_BUILD_TRACE; } - return Turbolinks.visit(pageUrl); + return gl.utils.visitUrl(pageUrl); } }; })(this) diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 index 00e1c28692f..547989a6ff5 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js.es6 @@ -9,7 +9,7 @@ this.setupMapping(); this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('page:fetch', this.cleanupWrapper); + document.addEventListener('beforeunload', this.cleanupWrapper); } cleanup() { @@ -20,7 +20,7 @@ this.setupMapping(); - document.removeEventListener('page:fetch', this.cleanupWrapper); + document.removeEventListener('beforeunload', this.cleanupWrapper); } setupMapping() { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 index 029564ffc61..4e02ab7c8c1 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 @@ -1,5 +1,3 @@ -/* global Turbolinks */ - (() => { class FilteredSearchManager { constructor() { @@ -15,13 +13,13 @@ this.dropdownManager.setDropdown(); this.cleanupWrapper = this.cleanup.bind(this); - document.addEventListener('page:fetch', this.cleanupWrapper); + document.addEventListener('beforeunload', this.cleanupWrapper); } } cleanup() { this.unbindEvents(); - document.removeEventListener('page:fetch', this.cleanupWrapper); + document.removeEventListener('beforeunload', this.cleanupWrapper); } bindEvents() { @@ -200,7 +198,9 @@ paths.push(`search=${sanitized}`); } - Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`); + const parameterizedUrl = `?scope=all&utf8=✓&${paths.join('&')}`; + + gl.utils.visitUrl(parameterizedUrl); } getUsernameParams() { diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js index d2f66cf5249..dbc9b2e2a1c 100644 --- a/app/assets/javascripts/gl_dropdown.js +++ b/app/assets/javascripts/gl_dropdown.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, prefer-rest-params, max-len, vars-on-top, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, prefer-template, no-param-reassign, no-loop-func, no-mixed-operators */ /* global fuzzaldrinPlus */ -/* global Turbolinks */ (function() { var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote, @@ -723,7 +722,7 @@ if ($el.length) { var href = $el.attr('href'); if (href && href !== '#') { - Turbolinks.visit(href); + gl.utils.visitUrl(href); } else { $el.first().trigger('click'); } diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6 index f63d700fd65..8df86f68218 100644 --- a/app/assets/javascripts/issuable.js.es6 +++ b/app/assets/javascripts/issuable.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */ /* global Issuable */ -/* global Turbolinks */ ((global) => { var issuable_created; @@ -119,7 +118,7 @@ issuesUrl = formAction; issuesUrl += "" + (formAction.indexOf('?') < 0 ? '?' : '&'); issuesUrl += formData; - return Turbolinks.visit(issuesUrl); + return gl.utils.visitUrl(issuesUrl); }; })(this), initResetFilters: function() { @@ -130,7 +129,7 @@ const baseIssuesUrl = target.href; $form.attr('action', baseIssuesUrl); - Turbolinks.visit(baseIssuesUrl); + gl.utils.visitUrl(baseIssuesUrl); }); }, initChecks: function() { diff --git a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 index e810ee85bd3..2955bda1a36 100644 --- a/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 +++ b/app/assets/javascripts/lib/utils/bootstrap_linked_tabs.js.es6 @@ -95,7 +95,6 @@ const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`; history.replaceState({ - turbolinks: true, url: newState, }, document.title, newState); return newState; diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js deleted file mode 100644 index 8e15bf0735c..00000000000 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ /dev/null @@ -1,80 +0,0 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */ -(function() { - (function(w) { - var base; - if (w.gl == null) { - w.gl = {}; - } - if ((base = w.gl).utils == null) { - base.utils = {}; - } - // Returns an array containing the value(s) of the - // of the key passed as an argument - w.gl.utils.getParameterValues = function(sParam) { - var i, sPageURL, sParameterName, sURLVariables, values; - sPageURL = decodeURIComponent(window.location.search.substring(1)); - sURLVariables = sPageURL.split('&'); - sParameterName = void 0; - values = []; - i = 0; - while (i < sURLVariables.length) { - sParameterName = sURLVariables[i].split('='); - if (sParameterName[0] === sParam) { - values.push(sParameterName[1].replace(/\+/g, ' ')); - } - i += 1; - } - return values; - }; - // @param {Object} params - url keys and value to merge - // @param {String} url - w.gl.utils.mergeUrlParams = function(params, url) { - var lastChar, newUrl, paramName, paramValue, pattern; - newUrl = decodeURIComponent(url); - for (paramName in params) { - paramValue = params[paramName]; - pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)"); - if (paramValue == null) { - newUrl = newUrl.replace(pattern, ''); - } else if (url.search(pattern) !== -1) { - newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2"); - } else { - newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue; - } - } - // Remove a trailing ampersand - lastChar = newUrl[newUrl.length - 1]; - if (lastChar === '&') { - newUrl = newUrl.slice(0, -1); - } - return newUrl; - }; - // removes parameter query string from url. returns the modified url - w.gl.utils.removeParamQueryString = function(url, param) { - var urlVariables, variables; - url = decodeURIComponent(url); - urlVariables = url.split('&'); - return ((function() { - var j, len, results; - results = []; - for (j = 0, len = urlVariables.length; j < len; j += 1) { - variables = urlVariables[j]; - if (variables.indexOf(param) === -1) { - results.push(variables); - } - } - return results; - })()).join('&'); - }; - w.gl.utils.getLocationHash = function(url) { - var hashIndex; - if (typeof url === 'undefined') { - // Note: We can't use window.location.hash here because it's - // not consistent across browsers - Firefox will pre-decode it - url = window.location.href; - } - hashIndex = url.indexOf('#'); - return hashIndex === -1 ? null : url.substring(hashIndex + 1); - }; - })(window); -}).call(this); diff --git a/app/assets/javascripts/lib/utils/url_utility.js.es6 b/app/assets/javascripts/lib/utils/url_utility.js.es6 new file mode 100644 index 00000000000..a1558b371f0 --- /dev/null +++ b/app/assets/javascripts/lib/utils/url_utility.js.es6 @@ -0,0 +1,86 @@ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */ +(function() { + (function(w) { + var base; + if (w.gl == null) { + w.gl = {}; + } + if ((base = w.gl).utils == null) { + base.utils = {}; + } + // Returns an array containing the value(s) of the + // of the key passed as an argument + w.gl.utils.getParameterValues = function(sParam) { + var i, sPageURL, sParameterName, sURLVariables, values; + sPageURL = decodeURIComponent(window.location.search.substring(1)); + sURLVariables = sPageURL.split('&'); + sParameterName = void 0; + values = []; + i = 0; + while (i < sURLVariables.length) { + sParameterName = sURLVariables[i].split('='); + if (sParameterName[0] === sParam) { + values.push(sParameterName[1].replace(/\+/g, ' ')); + } + i += 1; + } + return values; + }; + // @param {Object} params - url keys and value to merge + // @param {String} url + w.gl.utils.mergeUrlParams = function(params, url) { + var lastChar, newUrl, paramName, paramValue, pattern; + newUrl = decodeURIComponent(url); + for (paramName in params) { + paramValue = params[paramName]; + pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)"); + if (paramValue == null) { + newUrl = newUrl.replace(pattern, ''); + } else if (url.search(pattern) !== -1) { + newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2"); + } else { + newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue; + } + } + // Remove a trailing ampersand + lastChar = newUrl[newUrl.length - 1]; + if (lastChar === '&') { + newUrl = newUrl.slice(0, -1); + } + return newUrl; + }; + // removes parameter query string from url. returns the modified url + w.gl.utils.removeParamQueryString = function(url, param) { + var urlVariables, variables; + url = decodeURIComponent(url); + urlVariables = url.split('&'); + return ((function() { + var j, len, results; + results = []; + for (j = 0, len = urlVariables.length; j < len; j += 1) { + variables = urlVariables[j]; + if (variables.indexOf(param) === -1) { + results.push(variables); + } + } + return results; + })()).join('&'); + }; + w.gl.utils.getLocationHash = function(url) { + var hashIndex; + if (typeof url === 'undefined') { + // Note: We can't use window.location.hash here because it's + // not consistent across browsers - Firefox will pre-decode it + url = window.location.href; + } + hashIndex = url.indexOf('#'); + return hashIndex === -1 ? null : url.substring(hashIndex + 1); + }; + + w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href); + + w.gl.utils.visitUrl = (url) => { + document.location.href = url; + }; + })(window); +}).call(this); diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js index 2f147704c22..78e338033e3 100644 --- a/app/assets/javascripts/line_highlighter.js +++ b/app/assets/javascripts/line_highlighter.js @@ -171,7 +171,6 @@ // This method is stubbed in tests. LineHighlighter.prototype.__setLocationHash__ = function(value) { return history.pushState({ - turbolinks: false, url: value // We're using pushState instead of assigning location.hash directly to // prevent the page from scrolling on the hashchange event diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js index ea9bfb4860a..1b0d0768db8 100644 --- a/app/assets/javascripts/logo.js +++ b/app/assets/javascripts/logo.js @@ -1,14 +1,7 @@ /* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback */ -/* global Turbolinks */ (function() { - Turbolinks.enableProgressBar(); - - $(document).on('page:fetch', function() { + window.addEventListener('beforeunload', function() { $('.tanuki-logo').addClass('animate'); }); - - $(document).on('page:change', function() { - $('.tanuki-logo').removeClass('animate'); - }); }).call(this); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 4c8c28af755..33463b46008 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -184,12 +184,13 @@ // Ensure parameters and hash come along for the ride newState += location.search + location.hash; + // TODO: Consider refactoring in light of turbolinks removal. + // Replace the current history state with the new one without breaking // Turbolinks' history. // // See https://github.com/rails/turbolinks/issues/363 window.history.replaceState({ - turbolinks: true, url: newState, }, document.title, newState); diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6 index fa782ebbedf..2c19029d175 100644 --- a/app/assets/javascripts/merge_request_widget.js.es6 +++ b/app/assets/javascripts/merge_request_widget.js.es6 @@ -2,7 +2,6 @@ /* global notify */ /* global notifyPermissions */ /* global merge_request_widget */ -/* global Turbolinks */ ((global) => { var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i += 1) { if (i in this && this[i] === item) return i; } return -1; }; @@ -69,13 +68,13 @@ } MergeRequestWidget.prototype.clearEventListeners = function() { - return $(document).off('page:change.merge_request'); + return $(document).off('DOMContentLoaded'); }; MergeRequestWidget.prototype.addEventListeners = function() { var allowedPages; allowedPages = ['show', 'commits', 'pipelines', 'changes']; - $(document).on('page:change.merge_request', (function(_this) { + $(document).on('DOMContentLoaded', (function(_this) { return function() { var page; page = $('body').data('page').split(':').last(); diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 7cf630a1d76..399b331c941 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, vars-on-top, prefer-template, max-len */ /* global Cookies */ -/* global Turbolinks */ /* global ProjectSelect */ (function() { @@ -99,7 +98,7 @@ var $form = $dropdown.closest('form'); var action = $form.attr('action'); var divider = action.indexOf('?') < 0 ? '?' : '&'; - Turbolinks.visit(action + '' + divider + '' + $form.serialize()); + gl.utils.visitUrl(action + '' + divider + '' + $form.serialize()); } } }); diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js index 6614d8952cd..d7943959238 100644 --- a/app/assets/javascripts/project_import.js +++ b/app/assets/javascripts/project_import.js @@ -1,11 +1,10 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, max-len */ -/* global Turbolinks */ (function() { this.ProjectImport = (function() { function ProjectImport() { setTimeout(function() { - return Turbolinks.visit(location.href); + return gl.utils.visitUrl(location.href); }, 5000); } diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js index 0caf8ba4344..bdbad93ad04 100644 --- a/app/assets/javascripts/render_gfm.js +++ b/app/assets/javascripts/render_gfm.js @@ -9,7 +9,7 @@ this.find('.js-render-math').renderMath(); }; - $(document).on('ready page:load', function() { + $(document).on('ready load', function() { return $('body').renderGFM(); }); }).call(this); diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js index c56ee429b8e..c6d9b007ad1 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/shortcuts.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */ /* global Mousetrap */ -/* global Turbolinks */ /* global findFileURL */ (function() { @@ -23,7 +22,7 @@ Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview); if (typeof findFileURL !== "undefined" && findFileURL !== null) { Mousetrap.bind('t', function() { - return Turbolinks.visit(findFileURL); + return gl.utils.visitUrl(findFileURL); }); } } diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js index 4dcc5ebe28f..3501974a8c9 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/shortcuts_issuable.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators */ /* global Mousetrap */ -/* global Turbolinks */ /* global ShortcutsNavigation */ /* global sidebar */ @@ -80,7 +79,7 @@ ShortcutsIssuable.prototype.editIssue = function() { var $editBtn; $editBtn = $('.issuable-edit'); - return Turbolinks.visit($editBtn.attr('href')); + return gl.utils.visitUrl($editBtn.attr('href')); }; ShortcutsIssuable.prototype.openSidebarDropdown = function(name) { diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6 index 05234643c18..ee172f2fa6f 100644 --- a/app/assets/javascripts/sidebar.js.es6 +++ b/app/assets/javascripts/sidebar.js.es6 @@ -40,7 +40,7 @@ .on('click', sidebarToggleSelector, () => this.toggleSidebar()) .on('click', pinnedToggleSelector, () => this.togglePinnedState()) .on('click', 'html, body', (e) => this.handleClickEvent(e)) - .on('page:change', () => this.renderState()) + .on('DOMContentLoaded', () => this.renderState()) .on('todo:toggle', (e, count) => this.updateTodoCount(count)); this.renderState(); } diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6 index 40f67637c7c..d1bdc353be2 100644 --- a/app/assets/javascripts/smart_interval.js.es6 +++ b/app/assets/javascripts/smart_interval.js.es6 @@ -89,7 +89,7 @@ destroy() { this.cancel(); document.removeEventListener('visibilitychange', this.handleVisibilityChange); - $(document).off('visibilitychange').off('page:before-unload'); + $(document).off('visibilitychange').off('beforeunload'); } /* private */ @@ -111,8 +111,9 @@ } initPageUnloadHandling() { + // TODO: Consider refactoring in light of turbolinks removal. // prevent interval continuing after page change, when kept in cache by Turbolinks - $(document).on('page:before-unload', () => this.cancel()); + $(document).on('beforeunload', () => this.cancel()); } handleVisibilityChange(e) { diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index 05622916ff8..96c7d927509 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -1,6 +1,5 @@ /* eslint-disable class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, no-param-reassign, max-len */ /* global UsersSelect */ -/* global Turbolinks */ ((global) => { class Todos { @@ -34,7 +33,7 @@ $('form.filter-form').on('submit', function (event) { event.preventDefault(); - Turbolinks.visit(this.action + '&' + $(this).serialize()); + gl.utils.visitUrl(this.action + '&' + $(this).serialize()); }); } @@ -142,7 +141,7 @@ }; url = gl.utils.mergeUrlParams(pageParams, url); } - return Turbolinks.visit(url); + return gl.utils.visitUrl(url); } } @@ -156,7 +155,7 @@ e.preventDefault(); return window.open(todoLink, '_blank'); } else { - return Turbolinks.visit(todoLink); + return gl.utils.visitUrl(todoLink); } } } diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js index d124ca4f88b..b1b35fdbd6c 100644 --- a/app/assets/javascripts/tree.js +++ b/app/assets/javascripts/tree.js @@ -1,5 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */ -/* global Turbolinks */ + (function() { this.TreeView = (function() { function TreeView() { @@ -15,7 +15,7 @@ e.preventDefault(); return window.open(path, '_blank'); } else { - return Turbolinks.visit(path); + return gl.utils.visitUrl(path); } } }); @@ -57,7 +57,7 @@ } else if (e.which === 13) { path = $('.tree-item.selected .tree-item-file-name a').attr('href'); if (path) { - return Turbolinks.visit(path); + return gl.utils.visitUrl(path); } } }); diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6 index 313fb17aee8..465618e3d53 100644 --- a/app/assets/javascripts/user_tabs.js.es6 +++ b/app/assets/javascripts/user_tabs.js.es6 @@ -149,7 +149,6 @@ content on the Users#show page. new_state = new_state.replace(/\/+$/, ''); new_state += this._location.search + this._location.hash; history.replaceState({ - turbolinks: true, url: new_state }, document.title, new_state); return new_state; diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_pagination/index.js.es6 index 605824fa939..d94caa983cd 100644 --- a/app/assets/javascripts/vue_pagination/index.js.es6 +++ b/app/assets/javascripts/vue_pagination/index.js.es6 @@ -13,6 +13,8 @@ gl.VueGlPagination = Vue.extend({ props: { + // TODO: Consider refactoring in light of turbolinks removal. + /** This function will take the information given by the pagination component And make a new Turbolinks call @@ -20,7 +22,7 @@ Here is an example `change` method: change(pagenum, apiScope) { - Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); }, */ diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index b2ed05503c9..194bbae07d9 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -1,4 +1,4 @@ -/* global Vue, Turbolinks, gl */ +/* global Vue, gl */ /* eslint-disable no-param-reassign */ ((gl) => { @@ -36,7 +36,7 @@ }, methods: { change(pagenum, apiScope) { - Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); }, author(pipeline) { if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; diff --git a/app/assets/javascripts/vue_realtime_listener/index.js.es6 b/app/assets/javascripts/vue_realtime_listener/index.js.es6 index 23cac1466d2..95564152cce 100644 --- a/app/assets/javascripts/vue_realtime_listener/index.js.es6 +++ b/app/assets/javascripts/vue_realtime_listener/index.js.es6 @@ -7,12 +7,12 @@ window.removeEventListener('beforeunload', removeIntervals); window.removeEventListener('focus', startIntervals); window.removeEventListener('blur', removeIntervals); - document.removeEventListener('page:fetch', removeAll); + document.removeEventListener('beforeunload', removeAll); }; window.addEventListener('beforeunload', removeIntervals); window.addEventListener('focus', startIntervals); window.addEventListener('blur', removeIntervals); - document.addEventListener('page:fetch', removeAll); + document.addEventListener('beforeunload', removeAll); }; })(window.gl || (window.gl = {})); diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index 3cf49f4ff1b..08f203a1bf6 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -31,7 +31,6 @@ @import "framework/modal.scss"; @import "framework/nav.scss"; @import "framework/pagination.scss"; -@import "framework/progress.scss"; @import "framework/panels.scss"; @import "framework/selects.scss"; @import "framework/sidebar.scss"; diff --git a/app/assets/stylesheets/framework/progress.scss b/app/assets/stylesheets/framework/progress.scss deleted file mode 100644 index e9800bd24b5..00000000000 --- a/app/assets/stylesheets/framework/progress.scss +++ /dev/null @@ -1,5 +0,0 @@ -html.turbolinks-progress-bar::before { - background-color: $progress-color!important; - height: 2px!important; - box-shadow: 0 0 10px $progress-color, 0 0 5px $progress-color; -} diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb index 0e456214d37..cd4075b340d 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), { "data-turbolinks-track" => true } + javascript_include_tag asset_path(js) end end diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index e87a16a5157..f92f89e73ff 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -6,4 +6,4 @@ - providers.each do |provider| %span.light - has_icon = provider_has_icon?(provider) - = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" + = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn') diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 7f1b9ee7141..e18bd47798b 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -82,7 +82,7 @@ rather than Git. Please convert = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview' and go through the - = link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true' + = link_to 'import flow', status_import_bitbucket_path again. .js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } } diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 3096f0ee19e..703c1009d5f 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -33,6 +33,8 @@ - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts + = yield :scripts_body_top + = csrf_meta_tags - unless browser.safari? diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 935517d4913..248d439cd05 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -4,9 +4,6 @@ %body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } = Gon::Base.render_data - -# Ideally this would be inside the head, but turbolinks only evaluates page-specific JS in the body. - = yield :scripts_body_top - = render "layouts/header/default", title: header_title = render 'layouts/page', sidebar: sidebar, nav: nav diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index 14b330d16ad..a4f4079d556 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -82,7 +82,7 @@ = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do Disconnect - else - = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active', "data-no-turbolink" => "true" do + = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn not-active' do Connect %hr - if current_user.can_change_username? diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 9585a9a3ad4..b38bd32745b 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -108,10 +108,10 @@ = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title :javascript - var merge_request; - - merge_request = new MergeRequest({ - action: "#{controller.action_name}" + $(function () { + new MergeRequest({ + action: "#{controller.action_name}" + }); }); var mrRefreshWidgetUrl = "#{mr_widget_refresh_url(@merge_request)}"; diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index 064e92b15eb..cd685f7d0eb 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -50,7 +50,7 @@ = icon('github', text: 'GitHub') %div - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", "data-no-turbolink" => "true" do + = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do = icon('bitbucket', text: 'Bitbucket') - unless bitbucket_import_configured? = render 'bitbucket_import_modal' diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index b42eaabb111..55777f21040 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -91,5 +91,5 @@ new SubscriptionSelect(); $('form.filter-form').on('submit', function (event) { event.preventDefault(); - Turbolinks.visit(this.action + '&' + $(this).serialize()); + gl.utils.visitUrl(this.action + '&' + $(this).serialize()); }); diff --git a/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml b/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml new file mode 100644 index 00000000000..d7f950d7be9 --- /dev/null +++ b/changelogs/unreleased/25624-anticipate-obstacles-to-removing-turbolinks.yml @@ -0,0 +1,4 @@ +--- +title: Remove turbolinks. +merge_request: !8570 +author: diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index d2fa8cd39af..480906929b2 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -501,6 +501,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I fill in merge request search with "Fe"' do fill_in 'issuable_search', with: "Fe" + sleep 3 end step 'I click the "Target branch" dropdown' do diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js index 51d911792ba..5f2c73634a8 100644 --- a/spec/javascripts/behaviors/autosize_spec.js +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -15,7 +15,7 @@ }); }); return load = function() { - return $(document).trigger('page:load'); + return $(document).trigger('load'); }; }); }).call(this); diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js index 9467056f04c..b1b1f1f437b 100644 --- a/spec/javascripts/behaviors/requires_input_spec.js +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -34,11 +34,5 @@ $('#required5').val('1').change(); return expect($('.submit')).not.toBeDisabled(); }); - return it('is called on page:load event', function() { - var spy; - spy = spyOn($.fn, 'requiresInput'); - $(document).trigger('page:load'); - return expect(spy).toHaveBeenCalled(); - }); }); }).call(this); diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 index ea953d0f5a5..cac77cf67a0 100644 --- a/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 +++ b/spec/javascripts/bootstrap_linked_tabs_spec.js.es6 @@ -50,7 +50,6 @@ secondTab.click(); expect(historySpy).toHaveBeenCalledWith({ - turbolinks: true, url: newState, }, document.title, newState); }); diff --git a/spec/javascripts/build_spec.js.es6 b/spec/javascripts/build_spec.js.es6 index 0c556382980..d813d6b39d0 100644 --- a/spec/javascripts/build_spec.js.es6 +++ b/spec/javascripts/build_spec.js.es6 @@ -1,12 +1,11 @@ /* eslint-disable no-new */ /* global Build */ -/* global Turbolinks */ //= require lib/utils/datetime_utility +//= require lib/utils/url_utility //= require build //= require breakpoints //= require jquery.nicescroll -//= require turbolinks describe('Build', () => { const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/builds/1`; @@ -167,7 +166,7 @@ describe('Build', () => { }); it('reloads the page when the build is done', () => { - spyOn(Turbolinks, 'visit'); + spyOn(gl.utils, 'visitUrl'); jasmine.clock().tick(4001); const [{ success, context }] = $.ajax.calls.argsFor(1); @@ -177,7 +176,7 @@ describe('Build', () => { append: true, }); - expect(Turbolinks.visit).toHaveBeenCalledWith(BUILD_URL); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(BUILD_URL); }); }); }); diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 06fa64b1b4e..b8d39019183 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -1,11 +1,10 @@ /* eslint-disable comma-dangle, no-param-reassign, no-unused-expressions, max-len */ -/* global Turbolinks */ /*= require jquery */ /*= require gl_dropdown */ -/*= require turbolinks */ /*= require lib/utils/common_utils */ /*= require lib/utils/type_utility */ +//= require lib/utils/url_utility (() => { const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link'; @@ -112,13 +111,13 @@ expect(this.dropdownContainerElement).toHaveClass('open'); const randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0; navigateWithKeys('down', randomIndex, () => { - spyOn(Turbolinks, 'visit').and.stub(); + spyOn(gl.utils, 'visitUrl').and.stub(); navigateWithKeys('enter', null, () => { expect(this.dropdownContainerElement).not.toHaveClass('open'); const link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement); expect(link).toHaveClass('is-active'); const linkedLocation = link.attr('href'); - if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation); + if (linkedLocation && linkedLocation !== '#') expect(gl.utils.visitUrl).toHaveBeenCalledWith(linkedLocation); }); }); }); diff --git a/spec/javascripts/issuable_spec.js.es6 b/spec/javascripts/issuable_spec.js.es6 index 917a6267b92..9bea404379b 100644 --- a/spec/javascripts/issuable_spec.js.es6 +++ b/spec/javascripts/issuable_spec.js.es6 @@ -1,8 +1,7 @@ /* global Issuable */ -/* global Turbolinks */ +//= require lib/utils/url_utility //= require issuable -//= require turbolinks (() => { const BASE_URL = '/user/project/issues?scope=all&state=closed'; @@ -42,39 +41,39 @@ }); it('should contain only the default parameters', () => { - spyOn(Turbolinks, 'visit'); + spyOn(gl.utils, 'visitUrl'); Issuable.filterResults($filtersForm); - expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS); }); it('should filter for the phrase "broken"', () => { - spyOn(Turbolinks, 'visit'); + spyOn(gl.utils, 'visitUrl'); updateForm({ search: 'broken' }, $filtersForm); Issuable.filterResults($filtersForm); const params = `${DEFAULT_PARAMS}&search=broken`; - expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); }); it('should keep query parameters after modifying filter', () => { - spyOn(Turbolinks, 'visit'); + spyOn(gl.utils, 'visitUrl'); // initial filter updateForm({ milestone_title: 'v1.0' }, $filtersForm); Issuable.filterResults($filtersForm); let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`; - expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); // update filter updateForm({ label_name: 'Frontend' }, $filtersForm); Issuable.filterResults($filtersForm); params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`; - expect(Turbolinks.visit).toHaveBeenCalledWith(BASE_URL + params); + expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params); }); }); }); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index 98201fb98ed..6009f0dbfc2 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -99,7 +99,6 @@ }); newState = this.subject('commits'); expect(this.spies.history).toHaveBeenCalledWith({ - turbolinks: true, url: newState }, document.title, newState); }); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 7ac9710654f..0b48a53f4bc 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -6,8 +6,6 @@ /*= require lib/utils/common_utils */ /*= require lib/utils/type_utility */ /*= require fuzzaldrin-plus */ -/*= require turbolinks */ -/*= require jquery.turbolinks */ (function() { var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; diff --git a/spec/javascripts/smart_interval_spec.js.es6 b/spec/javascripts/smart_interval_spec.js.es6 index 39d236986b9..e695454b8f7 100644 --- a/spec/javascripts/smart_interval_spec.js.es6 +++ b/spec/javascripts/smart_interval_spec.js.es6 @@ -164,7 +164,7 @@ const interval = this.smartInterval; setTimeout(() => { - $(document).trigger('page:before-unload'); + $(document).trigger('beforeunload'); expect(interval.state.intervalId).toBeUndefined(); expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval); done(); diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js index f8e3aca29fa..a89176e9ef4 100644 --- a/spec/javascripts/spec_helper.js +++ b/spec/javascripts/spec_helper.js @@ -8,7 +8,6 @@ // everything in application, however you may get better load performance if you // require the specific files that are being used in the spec that tests them. /*= require jquery */ -/*= require jquery.turbolinks */ /*= require bootstrap */ /*= require underscore */ diff --git a/vendor/assets/javascripts/jquery.turbolinks.js b/vendor/assets/javascripts/jquery.turbolinks.js deleted file mode 100644 index fd6e95e75d5..00000000000 --- a/vendor/assets/javascripts/jquery.turbolinks.js +++ /dev/null @@ -1,49 +0,0 @@ -// Generated by CoffeeScript 1.7.1 - -/* -jQuery.Turbolinks ~ https://github.com/kossnocorp/jquery.turbolinks -jQuery plugin for drop-in fix binded events problem caused by Turbolinks - -The MIT License -Copyright (c) 2012-2013 Sasha Koss & Rico Sta. Cruz - */ - -(function() { - var $, $document; - - $ = window.jQuery || (typeof require === "function" ? require('jquery') : void 0); - - $document = $(document); - - $.turbo = { - version: '2.1.0', - isReady: false, - use: function(load, fetch) { - return $document.off('.turbo').on("" + load + ".turbo", this.onLoad).on("" + fetch + ".turbo", this.onFetch); - }, - addCallback: function(callback) { - if ($.turbo.isReady) { - callback($); - } - return $document.on('turbo:ready', function() { - return callback($); - }); - }, - onLoad: function() { - $.turbo.isReady = true; - return $document.trigger('turbo:ready'); - }, - onFetch: function() { - return $.turbo.isReady = false; - }, - register: function() { - $(this.onLoad); - return $.fn.ready = this.addCallback; - } - }; - - $.turbo.register(); - - $.turbo.use('page:load', 'page:fetch'); - -}).call(this); -- cgit v1.2.1 From f1bcf1e583274ebf4e7d25e983aa3ba803b55f2c Mon Sep 17 00:00:00 2001 From: Bryce Johnson <bryce@gitlab.com> Date: Wed, 1 Feb 2017 15:13:42 -0500 Subject: Remove turbolinks from filtered search after rebase. --- .../filtered_search/filtered_search_manager_spec.js.es6 | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 index a508dacf7f0..8600c90b926 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js.es6 @@ -1,6 +1,4 @@ -/* global Turbolinks */ - -//= require turbolinks +//= require lib/utils/url_utility //= require lib/utils/common_utils //= require filtered_search/filtered_search_token_keys //= require filtered_search/filtered_search_tokenizer @@ -38,7 +36,7 @@ it('should search with a single word', () => { getInput().value = 'searchTerm'; - spyOn(Turbolinks, 'visit').and.callFake((url) => { + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=searchTerm`); }); @@ -48,7 +46,7 @@ it('should search with multiple words', () => { getInput().value = 'awesome search terms'; - spyOn(Turbolinks, 'visit').and.callFake((url) => { + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=awesome+search+terms`); }); @@ -58,7 +56,7 @@ it('should search with special characters', () => { getInput().value = '~!@#$%^&*()_+{}:<>,.?/'; - spyOn(Turbolinks, 'visit').and.callFake((url) => { + spyOn(gl.utils, 'visitUrl').and.callFake((url) => { expect(url).toEqual(`${defaultParams}&search=~!%40%23%24%25%5E%26*()_%2B%7B%7D%3A%3C%3E%2C.%3F%2F`); }); -- cgit v1.2.1 From 030e766d29703baec8690b7dd54bf213f0d93d14 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:05:51 -0600 Subject: allow console.xxx in tests, reorder eslint rules alphabetically --- spec/javascripts/.eslintrc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/javascripts/.eslintrc b/spec/javascripts/.eslintrc index 3cd419b37c9..fbd9bb9f0ff 100644 --- a/spec/javascripts/.eslintrc +++ b/spec/javascripts/.eslintrc @@ -22,9 +22,10 @@ }, "plugins": ["jasmine"], "rules": { - "prefer-arrow-callback": 0, "func-names": 0, "jasmine/no-suite-dupes": [1, "branch"], - "jasmine/no-spec-dupes": [1, "branch"] + "jasmine/no-spec-dupes": [1, "branch"], + "no-console": 0, + "prefer-arrow-callback": 0 } } -- cgit v1.2.1 From 70c4badf4e1192413fb4d8e95248533ca1e88c9d Mon Sep 17 00:00:00 2001 From: Bryce Johnson <bryce@gitlab.com> Date: Thu, 2 Feb 2017 14:06:43 -0500 Subject: Rename scripts_body_top to project_javascripts. --- app/views/layouts/_head.html.haml | 2 +- app/views/layouts/project.html.haml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 703c1009d5f..79e96f54936 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -33,7 +33,7 @@ - if content_for?(:page_specific_javascripts) = yield :page_specific_javascripts - = yield :scripts_body_top + = yield :project_javascripts = csrf_meta_tags diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 277eb71ea73..f5e7ea7710d 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -3,7 +3,7 @@ - header_title project_title(@project) unless header_title - nav "project" -- content_for :scripts_body_top do +- content_for :project_javascripts do - project = @target_project || @project - if @project_wiki && @page - preview_markdown_path = namespace_project_wiki_preview_markdown_path(project.namespace, project, @page.slug) -- cgit v1.2.1 From 86f4166e3151bec9b5903bd77c6373ca9ee2fc62 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:08:52 -0600 Subject: fix fixture references in environments_spec --- spec/javascripts/environments/environment_spec.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6 index 20e11ca3738..239cd69dd3a 100644 --- a/spec/javascripts/environments/environment_spec.js.es6 +++ b/spec/javascripts/environments/environment_spec.js.es6 @@ -8,12 +8,12 @@ //= require ./mock_data describe('Environment', () => { - preloadFixtures('environments/environments'); + preloadFixtures('static/environments/environments.html.raw'); let component; beforeEach(() => { - loadFixtures('environments/environments'); + loadFixtures('static/environments/environments.html.raw'); }); describe('successfull request', () => { -- cgit v1.2.1 From 7e7875b10c80ed19a3286ee1c544038e9306da4f Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:11:40 -0600 Subject: preload projects.json fixture --- spec/javascripts/gl_dropdown_spec.js.es6 | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 06fa64b1b4e..4e7eed2767c 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -44,6 +44,7 @@ describe('Dropdown', function describeDropdown() { preloadFixtures('static/gl_dropdown.html.raw'); + loadJSONFixtures('projects.json'); function initDropDown(hasRemote, isFilterable) { this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({ -- cgit v1.2.1 From b7ddbf24011381a5ce0f3c0a98d3ef3e93d07769 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:17:38 -0600 Subject: ensure helper classes and constants are exposed globally --- spec/javascripts/boards/mock_data.js.es6 | 5 +++++ spec/javascripts/environments/mock_data.js.es6 | 6 +++++- spec/javascripts/helpers/class_spec_helper.js.es6 | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index 8d3e2237fda..7a399b307ad 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -56,3 +56,8 @@ const boardsMockInterceptor = (request, next) => { status: 200 })); }; + +window.listObj = listObj; +window.listObjDuplicate = listObjDuplicate; +window.BoardsMockData = BoardsMockData; +window.boardsMockInterceptor = boardsMockInterceptor; diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 8ecd01f9a83..58f6fb96afb 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-unused-vars */ + const environmentsList = [ { id: 31, @@ -134,6 +134,8 @@ const environmentsList = [ }, ]; +window.environmentsList = environmentsList; + const environment = { id: 4, name: 'production', @@ -147,3 +149,5 @@ const environment = { created_at: '2016-12-16T11:51:04.690Z', updated_at: '2016-12-16T12:04:51.133Z', }; + +window.environment = environment; diff --git a/spec/javascripts/helpers/class_spec_helper.js.es6 b/spec/javascripts/helpers/class_spec_helper.js.es6 index 92a20687ec5..d3c37d39431 100644 --- a/spec/javascripts/helpers/class_spec_helper.js.es6 +++ b/spec/javascripts/helpers/class_spec_helper.js.es6 @@ -1,5 +1,3 @@ -/* eslint-disable no-unused-vars */ - class ClassSpecHelper { static itShouldBeAStaticMethod(base, method) { return it('should be a static method', () => { @@ -7,3 +5,5 @@ class ClassSpecHelper { }); } } + +window.ClassSpecHelper = ClassSpecHelper; -- cgit v1.2.1 From f38b7cee7bdc261892c3b9354c56d54633c46054 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:18:21 -0600 Subject: use setFixtures instead of fixture.set --- spec/javascripts/issuable_time_tracker_spec.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/javascripts/issuable_time_tracker_spec.js.es6 b/spec/javascripts/issuable_time_tracker_spec.js.es6 index a1e979e8d09..c5671af235e 100644 --- a/spec/javascripts/issuable_time_tracker_spec.js.es6 +++ b/spec/javascripts/issuable_time_tracker_spec.js.es6 @@ -4,7 +4,7 @@ //= require issuable/time_tracking/components/time_tracker function initTimeTrackingComponent(opts) { - fixture.set(` + setFixtures(` <div> <div id="mock-container"></div> </div> -- cgit v1.2.1 From 48a053f334f289ab5283fed9c000594c16fbd27f Mon Sep 17 00:00:00 2001 From: Bryce Johnson <bryce@gitlab.com> Date: Wed, 1 Feb 2017 15:55:55 -0500 Subject: Only render hr when user can't archive project. --- app/views/projects/edit.html.haml | 2 +- .../27321-double-separator-line-in-edit-projects-settings.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index ec944d4ffb7..4a0ce995165 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -185,8 +185,8 @@ %li Container registry images %li CI variables %li Any encrypted tokens - %hr - if can? current_user, :archive_project, @project + %hr .row.prepend-top-default .col-lg-3 %h4.warning-title.prepend-top-0 diff --git a/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml new file mode 100644 index 00000000000..502927cd160 --- /dev/null +++ b/changelogs/unreleased/27321-double-separator-line-in-edit-projects-settings.yml @@ -0,0 +1,4 @@ +--- +title: Only render hr when user can't archive project. +merge_request: !8917 +author: -- cgit v1.2.1 From 9e8762f898b9bc424600969b8d25eeb6549fe159 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:20:23 -0600 Subject: rework tests which rely on teaspoon-specific behavior --- spec/javascripts/lib/utils/common_utils_spec.js.es6 | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 1ce8f28e568..32c96e2a088 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -10,9 +10,9 @@ // IE11 will return a relative pathname while other browsers will return a full pathname. // parseUrl uses an anchor element for parsing an url. With relative urls, the anchor // element will create an absolute url relative to the current execution context. - // The JavaScript test suite is executed at '/teaspoon' which will lead to an absolute - // url starting with '/teaspoon'. - expect(gl.utils.parseUrl('" test="asf"').pathname).toEqual('/teaspoon/%22%20test=%22asf%22'); + // The JavaScript test suite is executed at '/' which will lead to an absolute url + // starting with '/'. + expect(gl.utils.parseUrl('" test="asf"').pathname).toContain('/%22%20test=%22asf%22'); }); }); @@ -42,9 +42,13 @@ }); describe('gl.utils.getParameterByName', () => { + beforeEach(() => { + window.history.pushState({}, null, '?scope=all&p=2'); + }); + it('should return valid parameter', () => { - const value = gl.utils.getParameterByName('reporter'); - expect(value).toBe('Console'); + const value = gl.utils.getParameterByName('scope'); + expect(value).toBe('all'); }); it('should return invalid parameter', () => { -- cgit v1.2.1 From ed8b6ecb9d18ea53074b9c0402ee87c361661e16 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:22:57 -0600 Subject: preload projects.json fixture --- spec/javascripts/project_title_spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 0202c9ba85e..e562385a6c6 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -17,6 +17,8 @@ describe('Project Title', function() { preloadFixtures('static/project_title.html.raw'); + loadJSONFixtures('projects.json'); + beforeEach(function() { loadFixtures('static/project_title.html.raw'); return this.project = new Project(); -- cgit v1.2.1 From 66f9086fbc8f68574d9754367a25157480b23e0e Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:24:41 -0600 Subject: preload projects.json fixture --- spec/javascripts/right_sidebar_spec.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 942778229b5..3a01a534557 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -37,6 +37,8 @@ describe('RightSidebar', function() { var fixtureName = 'issues/open-issue.html.raw'; preloadFixtures(fixtureName); + loadJSONFixtures('todos.json'); + beforeEach(function() { loadFixtures(fixtureName); this.sidebar = new Sidebar; -- cgit v1.2.1 From f4fca2de924a05cccfebc23229f43884ec0b1048 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:25:56 -0600 Subject: simplify test for focus state --- spec/javascripts/shortcuts_issuable_spec.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index db11c2516a6..e0a5a7927bb 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -59,12 +59,8 @@ expect(triggered).toBe(true); }); it('triggers `focus`', function() { - var focused = false; - $(this.selector).on('focus', function() { - focused = true; - }); this.shortcut.replyWithSelectedText(); - expect(focused).toBe(true); + expect(document.activeElement).toBe(document.querySelector(this.selector)); }); }); describe('with a one-line selection', function() { -- cgit v1.2.1 From 85f2dcf535dba851a0e7690d358dac7f68379734 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:28:35 -0600 Subject: prevent u2f tests from triggering a form submission while testing --- spec/javascripts/u2f/authenticate_spec.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 80163fd72d3..0e2fb07ba7f 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -25,19 +25,20 @@ document.querySelector('#js-login-2fa-device'), document.querySelector('.js-2fa-form') ); + + // bypass automatic form submission within renderAuthenticated + spyOn(this.component, 'renderAuthenticated').and.returnValue(true); + return this.component.start(); }); it('allows authenticating via a U2F device', function() { - var authenticatedMessage, deviceResponse, inProgressMessage; + var inProgressMessage; inProgressMessage = this.container.find("p"); expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); this.u2fDevice.respondToAuthenticateRequest({ deviceData: "this is data from the device" }); - authenticatedMessage = this.container.find("p"); - deviceResponse = this.container.find('#js-device-response'); - expect(authenticatedMessage.text()).toContain('We heard back from your U2F device. You have been authenticated.'); - return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); + expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); }); return describe("errors", function() { it("displays an error message", function() { @@ -51,7 +52,7 @@ return expect(errorMessage.text()).toContain("There was a problem communicating with your device"); }); return it("allows retrying authentication after an error", function() { - var authenticatedMessage, retryButton, setupButton; + var retryButton, setupButton; setupButton = this.container.find("#js-login-u2f-device"); setupButton.trigger('click'); this.u2fDevice.respondToAuthenticateRequest({ @@ -64,8 +65,7 @@ this.u2fDevice.respondToAuthenticateRequest({ deviceData: "this is data from the device" }); - authenticatedMessage = this.container.find("p"); - return expect(authenticatedMessage.text()).toContain("We heard back from your U2F device. You have been authenticated."); + expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}'); }); }); }); -- cgit v1.2.1 From 99f2e9a1f536afa862390dae003fd86a34a7ad54 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:29:41 -0600 Subject: use setFixtures instead of fixture.set --- spec/javascripts/vue_pagination/pagination_spec.js.es6 | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_pagination/pagination_spec.js.es6 index 1a7f2bb5fb8..efb11211ce2 100644 --- a/spec/javascripts/vue_pagination/pagination_spec.js.es6 +++ b/spec/javascripts/vue_pagination/pagination_spec.js.es6 @@ -1,7 +1,6 @@ //= require vue //= require lib/utils/common_utils //= require vue_pagination/index -/* global fixture, gl */ describe('Pagination component', () => { let component; @@ -17,7 +16,7 @@ describe('Pagination component', () => { }; it('should render and start at page 1', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -40,7 +39,7 @@ describe('Pagination component', () => { }); it('should go to the previous page', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -61,7 +60,7 @@ describe('Pagination component', () => { }); it('should go to the next page', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -82,7 +81,7 @@ describe('Pagination component', () => { }); it('should go to the last page', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -103,7 +102,7 @@ describe('Pagination component', () => { }); it('should go to the first page', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), @@ -124,7 +123,7 @@ describe('Pagination component', () => { }); it('should do nothing', () => { - fixture.set('<div class="test-pagination-container"></div>'); + setFixtures('<div class="test-pagination-container"></div>'); component = new window.gl.VueGlPagination({ el: document.querySelector('.test-pagination-container'), -- cgit v1.2.1 From b00f53bea1dc68756f19d8bbfe4682d395e85280 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 13:31:30 -0600 Subject: fix relative paths to xterm.js within fit.js --- vendor/assets/javascripts/xterm/fit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/vendor/assets/javascripts/xterm/fit.js b/vendor/assets/javascripts/xterm/fit.js index 7e24fd9b36e..55438452cad 100644 --- a/vendor/assets/javascripts/xterm/fit.js +++ b/vendor/assets/javascripts/xterm/fit.js @@ -16,12 +16,12 @@ /* * CommonJS environment */ - module.exports = fit(require('../../xterm')); + module.exports = fit(require('./xterm')); } else if (typeof define == 'function') { /* * Require.js is available */ - define(['../../xterm'], fit); + define(['./xterm'], fit); } else { /* * Plain browser environment -- cgit v1.2.1 From e8fd6c7e66f8a37b60d9c8cf17ec601df465dc2a Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira <oswaldo@gitlab.com> Date: Thu, 2 Feb 2017 17:41:21 -0200 Subject: Update V3 to V4 docs --- doc/api/README.md | 1 + doc/api/v3_to_v4.md | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 doc/api/v3_to_v4.md diff --git a/doc/api/README.md b/doc/api/README.md index 20f28e8d30e..b334ca46caf 100644 --- a/doc/api/README.md +++ b/doc/api/README.md @@ -49,6 +49,7 @@ following locations: - [Todos](todos.md) - [Users](users.md) - [Validate CI configuration](ci/lint.md) +- [V3 to V4](v3_to_v4.md) - [Version](version.md) ### Internal CI API diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md new file mode 100644 index 00000000000..01de1e59fcb --- /dev/null +++ b/doc/api/v3_to_v4.md @@ -0,0 +1,10 @@ +# V3 to V4 version + +Our V4 API version is currently available as *Beta*! It means that V3 +will still be supported and remain unchanged for now, but be aware that the following +changes are in V4: + +### Changes + +- Removed `/projects/:search` (use: `/projects?search=x`) + -- cgit v1.2.1 From aa91f508369a795878b3ee95556302cdb55d9f6c Mon Sep 17 00:00:00 2001 From: Bryce Johnson <bryce@gitlab.com> Date: Thu, 2 Feb 2017 14:53:46 -0500 Subject: Find .merge-request instead of sleep in MR search spec. --- features/steps/project/merge_requests.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index 480906929b2..9f0057cace7 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -501,7 +501,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps step 'I fill in merge request search with "Fe"' do fill_in 'issuable_search', with: "Fe" - sleep 3 + page.within '.merge-requests-holder' do + find('.merge-request') + end end step 'I click the "Target branch" dropdown' do -- cgit v1.2.1 From dbcf24c268a4bb9b949a6c94d40a04fc4c56ff90 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 14:07:33 -0600 Subject: DRY with Gitlab.config.webpack.dev_server references --- config/initializers/static_files.rb | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb index cb332e15c11..74aba6c5d06 100644 --- a/config/initializers/static_files.rb +++ b/config/initializers/static_files.rb @@ -15,17 +15,19 @@ if app.config.serve_static_files # If webpack-dev-server is configured, proxy webpack's public directory # instead of looking for static assets - if Gitlab.config.webpack.dev_server.enabled - dev_server = { + dev_server = Gitlab.config.webpack.dev_server + + if dev_server.enabled + settings = { enabled: true, - host: Gitlab.config.webpack.dev_server.host, - port: Gitlab.config.webpack.dev_server.port, - manifest_host: Gitlab.config.webpack.dev_server.host, - manifest_port: Gitlab.config.webpack.dev_server.port, + host: dev_server.host, + port: dev_server.port, + manifest_host: dev_server.host, + manifest_port: dev_server.port, } if Rails.env.development? - dev_server.merge!( + settings.merge!( host: Gitlab.config.gitlab.host, port: Gitlab.config.gitlab.port, https: Gitlab.config.gitlab.https, @@ -34,11 +36,11 @@ if app.config.serve_static_files Gitlab::Middleware::Static, Gitlab::Middleware::WebpackProxy, proxy_path: app.config.webpack.public_path, - proxy_host: Gitlab.config.webpack.dev_server.host, - proxy_port: Gitlab.config.webpack.dev_server.port, + proxy_host: dev_server.host, + proxy_port: dev_server.port, ) end - app.config.webpack.dev_server.merge!(dev_server) + app.config.webpack.dev_server.merge!(settings) end end -- cgit v1.2.1 From 3c571baf5d8e2a81f8722a3b5cd38ae1f51988d8 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones <jedwardsjones@gitlab.com> Date: Thu, 2 Feb 2017 17:28:53 +0000 Subject: =?UTF-8?q?Use=20non-downtime=20migration=20for=20ApplicationSetti?= =?UTF-8?q?ng=E2=80=99s=20max=5Fpages=5Fsize?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...215132013_add_pages_size_to_application_settings.rb | 11 ++++++++++- db/migrate/20160210105555_create_pages_domain.rb | 2 ++ db/schema.rb | 18 +++++++++--------- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/db/migrate/20151215132013_add_pages_size_to_application_settings.rb b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb index e7fb73190e8..f3a663f805b 100644 --- a/db/migrate/20151215132013_add_pages_size_to_application_settings.rb +++ b/db/migrate/20151215132013_add_pages_size_to_application_settings.rb @@ -1,5 +1,14 @@ class AddPagesSizeToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + def up - add_column :application_settings, :max_pages_size, :integer, default: 100, null: false + add_column_with_default :application_settings, :max_pages_size, :integer, default: 100, allow_null: false + end + + def down + remove_column(:application_settings, :max_pages_size) end end diff --git a/db/migrate/20160210105555_create_pages_domain.rb b/db/migrate/20160210105555_create_pages_domain.rb index 9af206143bd..0e8507c7e9a 100644 --- a/db/migrate/20160210105555_create_pages_domain.rb +++ b/db/migrate/20160210105555_create_pages_domain.rb @@ -1,4 +1,6 @@ class CreatePagesDomain < ActiveRecord::Migration + DOWNTIME = false + def change create_table :pages_domains do |t| t.integer :project_id diff --git a/db/schema.rb b/db/schema.rb index dc3d8c22e8d..9863367e312 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -61,7 +61,6 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.boolean "shared_runners_enabled", default: true, null: false t.integer "max_artifacts_size", default: 100, null: false t.string "runners_registration_token" - t.integer "max_pages_size", default: 100, null: false t.boolean "require_two_factor_authentication", default: false t.integer "two_factor_grace_period", default: 48 t.boolean "metrics_enabled", default: false @@ -99,17 +98,18 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true t.string "plantuml_url" t.boolean "plantuml_enabled" + t.integer "max_pages_size", default: 100, null: false end create_table "audit_events", force: :cascade do |t| @@ -857,11 +857,11 @@ ActiveRecord::Schema.define(version: 20170130204620) do create_table "pages_domains", force: :cascade do |t| t.integer "project_id" - t.text "certificate" - t.text "encrypted_key" - t.string "encrypted_key_iv" - t.string "encrypted_key_salt" - t.string "domain" + t.text "certificate" + t.text "encrypted_key" + t.string "encrypted_key_iv" + t.string "encrypted_key_salt" + t.string "domain" end add_index "pages_domains", ["domain"], name: "index_pages_domains_on_domain", unique: true, using: :btree -- cgit v1.2.1 From a14f11390db5b848989e00b05a63e160bf5e0fdf Mon Sep 17 00:00:00 2001 From: James Edwards-Jones <jedwardsjones@gitlab.com> Date: Thu, 2 Feb 2017 17:31:54 +0000 Subject: Added Changelog for Pages to CE port --- changelogs/unreleased/jej-pages-picked-from-ee.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/jej-pages-picked-from-ee.yml diff --git a/changelogs/unreleased/jej-pages-picked-from-ee.yml b/changelogs/unreleased/jej-pages-picked-from-ee.yml new file mode 100644 index 00000000000..ee4a43a93db --- /dev/null +++ b/changelogs/unreleased/jej-pages-picked-from-ee.yml @@ -0,0 +1,4 @@ +--- +title: Added GitLab Pages to CE +merge_request: 8463 +author: -- cgit v1.2.1 From 4be73c9fe06104c425dc0e860c9c28225a51531a Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 14:12:16 -0600 Subject: remove dateFormat global exception --- app/assets/javascripts/users/calendar.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js index e0e40ad3adb..6e40dfdf3d8 100644 --- a/app/assets/javascripts/users/calendar.js +++ b/app/assets/javascripts/users/calendar.js @@ -1,6 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, camelcase, vars-on-top, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, max-len */ /* global d3 */ -/* global dateFormat */ (function() { var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; -- cgit v1.2.1 From c6dac7da6f5402333ccdd6f95d9b7b11300719f4 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones <jedwardsjones@gitlab.com> Date: Thu, 2 Feb 2017 21:23:42 +0000 Subject: Mentioned pages to CE port in project/pages/index.md --- doc/user/project/pages/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index a07c23a3274..fe477b1ed0b 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -3,6 +3,7 @@ > **Notes:** > - This feature was [introduced][ee-80] in GitLab EE 8.3. > - Custom CNAMEs with TLS support were [introduced][ee-173] in GitLab EE 8.5. +> - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. > - This document is about the user guide. To learn how to enable GitLab Pages > across your GitLab instance, visit the [administrator documentation](../../../administration/pages/index.md). -- cgit v1.2.1 From caa7344d48080c07658331b991399cf7a86f926f Mon Sep 17 00:00:00 2001 From: Robert Speicher <rspeicher@gmail.com> Date: Thu, 2 Feb 2017 16:44:58 -0500 Subject: Update CHANGELOG.md for 8.16.4 [ci skip] --- CHANGELOG.md | 15 +++++++++++++++ changelogs/unreleased/19164-mobile-settings.yml | 4 ---- ...n-does-not-suggest-by-non-ascii-characters-in-name.yml | 4 ---- ...6860-27151-fix-discussion-note-permalink-collapsed.yml | 4 ---- ...oes-not-allow-filtering-labels-with-multiple-words.yml | 4 ---- ...bel-for-references-the-wrong-associated-text-input.yml | 4 ---- changelogs/unreleased/fix-cancel-integration-settings.yml | 4 ---- .../fix-filtering-username-with-multiple-words.yml | 4 ---- .../unreleased/fix-import-user-validation-error.yml | 4 ---- changelogs/unreleased/fix-search-bar-search-param.yml | 4 ---- .../sh-add-project-id-index-project-authorizations.yml | 4 ---- changelogs/unreleased/snippet-spam.yml | 4 ---- changelogs/unreleased/zj-slow-service-fetch.yml | 4 ---- 13 files changed, 15 insertions(+), 48 deletions(-) delete mode 100644 changelogs/unreleased/19164-mobile-settings.yml delete mode 100644 changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml delete mode 100644 changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml delete mode 100644 changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml delete mode 100644 changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml delete mode 100644 changelogs/unreleased/fix-cancel-integration-settings.yml delete mode 100644 changelogs/unreleased/fix-filtering-username-with-multiple-words.yml delete mode 100644 changelogs/unreleased/fix-import-user-validation-error.yml delete mode 100644 changelogs/unreleased/fix-search-bar-search-param.yml delete mode 100644 changelogs/unreleased/sh-add-project-id-index-project-authorizations.yml delete mode 100644 changelogs/unreleased/snippet-spam.yml delete mode 100644 changelogs/unreleased/zj-slow-service-fetch.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index 25e02b1ae1c..71d38e5453d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 8.16.4 (2017-02-02) + +- Support non-ASCII characters in GFM autocomplete. !8729 +- Fix search bar search param encoding. !8753 +- Fix project name label's for reference in project settings. !8795 +- Fix filtering with multiple words. !8830 +- Fixed services form cancel not redirecting back the integrations settings view. !8843 +- Fix filtering usernames with multiple words. !8851 +- Improve performance of slash commands. !8876 +- Remove old project members when retrying an export. +- Fix permalink discussion note being collapsed. +- Add project ID index to `project_authorizations` table to optimize queries. +- Check public snippets for spam. +- 19164 Add settings dropdown to mobile screens. + ## 8.16.3 (2017-01-27) - Add caching of droplab ajax requests. !8725 diff --git a/changelogs/unreleased/19164-mobile-settings.yml b/changelogs/unreleased/19164-mobile-settings.yml deleted file mode 100644 index c26a20f87e2..00000000000 --- a/changelogs/unreleased/19164-mobile-settings.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: 19164 Add settings dropdown to mobile screens -merge_request: -author: diff --git a/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml b/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml deleted file mode 100644 index 1758ed9e9ea..00000000000 --- a/changelogs/unreleased/27067-mention-user-dropdown-does-not-suggest-by-non-ascii-characters-in-name.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Support non-ASCII characters in GFM autocomplete -merge_request: 8729 -author: diff --git a/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml b/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml deleted file mode 100644 index ddd454da376..00000000000 --- a/changelogs/unreleased/27089-26860-27151-fix-discussion-note-permalink-collapsed.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix permalink discussion note being collapsed -merge_request: -author: diff --git a/changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml b/changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml deleted file mode 100644 index 6e036923158..00000000000 --- a/changelogs/unreleased/27248-filtered-search-does-not-allow-filtering-labels-with-multiple-words.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix filtering with multiple words -merge_request: 8830 -author: diff --git a/changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml b/changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml deleted file mode 100644 index 2591f161bc5..00000000000 --- a/changelogs/unreleased/27259-label-for-references-the-wrong-associated-text-input.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix project name label's for reference in project settings -merge_request: 8795 -author: diff --git a/changelogs/unreleased/fix-cancel-integration-settings.yml b/changelogs/unreleased/fix-cancel-integration-settings.yml deleted file mode 100644 index 294b0aa5db9..00000000000 --- a/changelogs/unreleased/fix-cancel-integration-settings.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fixed services form cancel not redirecting back the integrations settings view -merge_request: 8843 -author: diff --git a/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml b/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml deleted file mode 100644 index 3513f5afdfb..00000000000 --- a/changelogs/unreleased/fix-filtering-username-with-multiple-words.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix filtering usernames with multiple words -merge_request: 8851 -author: diff --git a/changelogs/unreleased/fix-import-user-validation-error.yml b/changelogs/unreleased/fix-import-user-validation-error.yml deleted file mode 100644 index 985a3b0b26f..00000000000 --- a/changelogs/unreleased/fix-import-user-validation-error.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Remove old project members when retrying an export -merge_request: -author: diff --git a/changelogs/unreleased/fix-search-bar-search-param.yml b/changelogs/unreleased/fix-search-bar-search-param.yml deleted file mode 100644 index 4df14d3bf13..00000000000 --- a/changelogs/unreleased/fix-search-bar-search-param.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Fix search bar search param encoding -merge_request: 8753 -author: diff --git a/changelogs/unreleased/sh-add-project-id-index-project-authorizations.yml b/changelogs/unreleased/sh-add-project-id-index-project-authorizations.yml deleted file mode 100644 index e69fcd2aa63..00000000000 --- a/changelogs/unreleased/sh-add-project-id-index-project-authorizations.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Add project ID index to `project_authorizations` table to optimize queries -merge_request: -author: diff --git a/changelogs/unreleased/snippet-spam.yml b/changelogs/unreleased/snippet-spam.yml deleted file mode 100644 index 4867f088953..00000000000 --- a/changelogs/unreleased/snippet-spam.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Check public snippets for spam -merge_request: -author: diff --git a/changelogs/unreleased/zj-slow-service-fetch.yml b/changelogs/unreleased/zj-slow-service-fetch.yml deleted file mode 100644 index 8037361d2fc..00000000000 --- a/changelogs/unreleased/zj-slow-service-fetch.yml +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Improve performance of slash commands -merge_request: 8876 -author: -- cgit v1.2.1 From 67c8526033241f6bd190fa16622970b3919e6dbd Mon Sep 17 00:00:00 2001 From: James Edwards-Jones <jedwardsjones@gitlab.com> Date: Thu, 2 Feb 2017 22:21:06 +0000 Subject: Ported max_pages_size in settings API to CE --- lib/api/settings.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/api/settings.rb b/lib/api/settings.rb index c5eff16a5de..5206ee4f521 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -57,6 +57,7 @@ module API requires :shared_runners_text, type: String, desc: 'Shared runners text ' end optional :max_artifacts_size, type: Integer, desc: "Set the maximum file size each build's artifacts can have" + optional :max_pages_size, type: Integer, desc: 'Maximum size of pages in MB' optional :container_registry_token_expire_delay, type: Integer, desc: 'Authorization token duration (minutes)' optional :metrics_enabled, type: Boolean, desc: 'Enable the InfluxDB metrics' given metrics_enabled: ->(val) { val } do @@ -115,7 +116,7 @@ module API :send_user_confirmation_email, :domain_whitelist, :domain_blacklist_enabled, :after_sign_up_text, :signin_enabled, :require_two_factor_authentication, :home_page_url, :after_sign_out_path, :sign_in_text, :help_page_text, - :shared_runners_enabled, :max_artifacts_size, :container_registry_token_expire_delay, + :shared_runners_enabled, :max_artifacts_size, :max_pages_size, :container_registry_token_expire_delay, :metrics_enabled, :sidekiq_throttling_enabled, :recaptcha_enabled, :akismet_enabled, :admin_notification_email, :sentry_enabled, :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled, -- cgit v1.2.1 From ae1d69c29a69412847b6aaa80a04b24932f751d1 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 16:29:40 -0600 Subject: allow application.js to require other scripts which start with application* --- app/assets/javascripts/application.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index ae81eecfe6a..f3896749476 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -55,7 +55,7 @@ requireAll(require.context('./extensions', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./lib/utils', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/)); requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/)); -requireAll(require.context('.', false, /^\.\/(?!application).*\.(js|es6)$/)); +requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/)); require('vendor/fuzzaldrin-plus'); window.ES6Promise = require('vendor/es6-promise.auto'); window.ES6Promise.polyfill(); -- cgit v1.2.1 From 152b292d0b566547875a44470c76e9a43cb28a36 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Thu, 2 Feb 2017 16:30:59 -0600 Subject: consistently use single quotes --- config/webpack.config.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index db4ce84f376..7cd92af7d93 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -4,7 +4,7 @@ var fs = require('fs'); var path = require('path'); var webpack = require('webpack'); var StatsPlugin = require('stats-webpack-plugin'); -var CompressionPlugin = require("compression-webpack-plugin"); +var CompressionPlugin = require('compression-webpack-plugin'); var ROOT_PATH = path.resolve(__dirname, '..'); var IS_PRODUCTION = process.env.NODE_ENV === 'production'; @@ -53,10 +53,10 @@ var config = { exclude: /node_modules/, loader: 'babel-loader', query: { - // "use strict" was broken in sprockets-es6 due to sprockets concatination method. + // 'use strict' was broken in sprockets-es6 due to sprockets concatination method. // many es5 strict errors which were never caught ended up in our es6 assets as a result. // this hack is necessary until they can be fixed. - blacklist: ["useStrict"] + blacklist: ['useStrict'] } }, { -- cgit v1.2.1 From d9e9ad2211c06159914e0183e4842abc16e5666f Mon Sep 17 00:00:00 2001 From: Horacio Sanson <horacio@allm.net> Date: Mon, 16 Jan 2017 09:07:02 +0900 Subject: PlantUML support for Markdown Allow rendering of PlantUML diagrams in Markdown documents using fenced blocks: ```plantuml Bob -> Sara : Hello Sara -> Bob : Go away ``` Closes: #4048 --- Gemfile | 2 +- Gemfile.lock | 4 +-- changelogs/unreleased/markdown-plantuml.yml | 4 +++ config/initializers/plantuml_lexer.rb | 2 ++ doc/administration/integration/plantuml.md | 18 ++++++++---- lib/banzai/filter/plantuml_filter.rb | 39 ++++++++++++++++++++++++++ lib/banzai/pipeline/gfm_pipeline.rb | 1 + lib/rouge/lexers/plantuml.rb | 21 ++++++++++++++ spec/lib/banzai/filter/plantuml_filter_spec.rb | 32 +++++++++++++++++++++ 9 files changed, 115 insertions(+), 8 deletions(-) create mode 100644 changelogs/unreleased/markdown-plantuml.yml create mode 100644 config/initializers/plantuml_lexer.rb create mode 100644 lib/banzai/filter/plantuml_filter.rb create mode 100644 lib/rouge/lexers/plantuml.rb create mode 100644 spec/lib/banzai/filter/plantuml_filter_spec.rb diff --git a/Gemfile b/Gemfile index dd7c93c5a75..479df411881 100644 --- a/Gemfile +++ b/Gemfile @@ -109,7 +109,7 @@ gem 'org-ruby', '~> 0.9.12' gem 'creole', '~> 0.5.0' gem 'wikicloth', '0.8.1' gem 'asciidoctor', '~> 1.5.2' -gem 'asciidoctor-plantuml', '0.0.6' +gem 'asciidoctor-plantuml', '0.0.7' gem 'rouge', '~> 2.0' gem 'truncato', '~> 0.7.8' diff --git a/Gemfile.lock b/Gemfile.lock index 3b207d19d1f..f6b889dcca4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -54,7 +54,7 @@ GEM faraday_middleware-multi_json (~> 0.0) oauth2 (~> 1.0) asciidoctor (1.5.3) - asciidoctor-plantuml (0.0.6) + asciidoctor-plantuml (0.0.7) asciidoctor (~> 1.5) ast (2.3.0) attr_encrypted (3.0.3) @@ -841,7 +841,7 @@ DEPENDENCIES allocations (~> 1.0) asana (~> 0.4.0) asciidoctor (~> 1.5.2) - asciidoctor-plantuml (= 0.0.6) + asciidoctor-plantuml (= 0.0.7) attr_encrypted (~> 3.0.0) awesome_print (~> 1.2.0) babosa (~> 1.0.2) diff --git a/changelogs/unreleased/markdown-plantuml.yml b/changelogs/unreleased/markdown-plantuml.yml new file mode 100644 index 00000000000..c855f0cbcf7 --- /dev/null +++ b/changelogs/unreleased/markdown-plantuml.yml @@ -0,0 +1,4 @@ +--- +title: PlantUML support for Markdown +merge_request: 8588 +author: Horacio Sanson diff --git a/config/initializers/plantuml_lexer.rb b/config/initializers/plantuml_lexer.rb new file mode 100644 index 00000000000..e8a77b146fa --- /dev/null +++ b/config/initializers/plantuml_lexer.rb @@ -0,0 +1,2 @@ +# Touch the lexers so it is registered with Rouge +Rouge::Lexers::Plantuml diff --git a/doc/administration/integration/plantuml.md b/doc/administration/integration/plantuml.md index e5cf592e0a6..6515b1a264a 100644 --- a/doc/administration/integration/plantuml.md +++ b/doc/administration/integration/plantuml.md @@ -3,8 +3,8 @@ > [Introduced][ce-7810] in GitLab 8.16. When [PlantUML](http://plantuml.com) integration is enabled and configured in -GitLab we are able to create simple diagrams in AsciiDoc documents created in -snippets, wikis, and repos. +GitLab we are able to create simple diagrams in AsciiDoc and Markdown documents +created in snippets, wikis, and repos. ## PlantUML Server @@ -54,7 +54,7 @@ that, login with an Admin account and do following: ## Creating Diagrams With PlantUML integration enabled and configured, we can start adding diagrams to -our AsciiDoc snippets, wikis and repos using blocks: +our AsciiDoc snippets, wikis and repos using delimited blocks: ``` [plantuml, format="png", id="myDiagram", width="200px"] @@ -64,7 +64,14 @@ Alice -> Bob : Go Away -- ``` -The above block will be converted to an HTML img tag with source pointing to the +And in Markdown using fenced code blocks: + + ```plantuml + Bob -> Alice : hello + Alice -> Bob : Go Away + ``` + +The above blocks will be converted to an HTML img tag with source pointing to the PlantUML instance. If the PlantUML server is correctly configured, this should render a nice diagram instead of the block: @@ -77,7 +84,7 @@ Inside the block you can add any of the supported diagrams by PlantUML such as and [Object](http://plantuml.com/object-diagram) diagrams. You do not need to use the PlantUML diagram delimiters `@startuml`/`@enduml` as these are replaced by the AsciiDoc `plantuml` block. -Some parameters can be added to the block definition: +Some parameters can be added to the AsciiDoc block definition: - *format*: Can be either `png` or `svg`. Note that `svg` is not supported by all browsers so use with care. The default is `png`. @@ -85,3 +92,4 @@ Some parameters can be added to the block definition: - *width*: Width attribute added to the img tag. - *height*: Height attribute added to the img tag. +Markdown does not support any parameters and will always use PNG format. diff --git a/lib/banzai/filter/plantuml_filter.rb b/lib/banzai/filter/plantuml_filter.rb new file mode 100644 index 00000000000..e194cf59275 --- /dev/null +++ b/lib/banzai/filter/plantuml_filter.rb @@ -0,0 +1,39 @@ +require "nokogiri" +require "asciidoctor-plantuml/plantuml" + +module Banzai + module Filter + # HTML that replaces all `code plantuml` tags with PlantUML img tags. + # + class PlantumlFilter < HTML::Pipeline::Filter + def call + return doc unless doc.at('pre.plantuml') and settings.plantuml_enabled + + plantuml_setup + + doc.css('pre.plantuml').each do |el| + img_tag = Nokogiri::HTML::DocumentFragment.parse( + Asciidoctor::PlantUml::Processor.plantuml_content(el.content, {})) + el.replace img_tag + end + + doc + end + + private + + def settings + ApplicationSetting.current || ApplicationSetting.create_from_defaults + end + + def plantuml_setup + Asciidoctor::PlantUml.configure do |conf| + conf.url = settings.plantuml_url + conf.png_enable = settings.plantuml_enabled + conf.svg_enable = false + conf.txt_enable = false + end + end + end + end +end diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb index ac95a79009b..b25d6f18d59 100644 --- a/lib/banzai/pipeline/gfm_pipeline.rb +++ b/lib/banzai/pipeline/gfm_pipeline.rb @@ -10,6 +10,7 @@ module Banzai def self.filters @filters ||= FilterArray[ Filter::SyntaxHighlightFilter, + Filter::PlantumlFilter, Filter::SanitizationFilter, Filter::MathFilter, diff --git a/lib/rouge/lexers/plantuml.rb b/lib/rouge/lexers/plantuml.rb new file mode 100644 index 00000000000..7d5700b7f6d --- /dev/null +++ b/lib/rouge/lexers/plantuml.rb @@ -0,0 +1,21 @@ +module Rouge + module Lexers + class Plantuml < Lexer + title "A passthrough lexer used for PlantUML input" + desc "A boring lexer that doesn't highlight anything" + + tag 'plantuml' + mimetypes 'text/plain' + + default_options token: 'Text' + + def token + @token ||= Token[option :token] + end + + def stream_tokens(string, &b) + yield self.token, string + end + end + end +end diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb new file mode 100644 index 00000000000..f85a5dcbd8b --- /dev/null +++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe Banzai::Filter::PlantumlFilter, lib: true do + include FilterSpecHelper + + it 'should replace plantuml pre tag with img tag' do + stub_application_setting(plantuml_enabled: true, plantuml_url: "http://localhost:8080") + input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + output = '<div class="imageblock"><div class="content"><img class="plantuml" src="http://localhost:8080/png/U9npoazIqBLJ24uiIbImKl18pSd91m0rkGMq"></div></div>' + doc = filter(input) + + expect(doc.to_s).to eq output + end + + it 'should not replace plantuml pre tag with img tag if disabled' do + stub_application_setting(plantuml_enabled: false) + input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + output = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre></pre></pre>' + doc = filter(input) + + expect(doc.to_s).to eq output + end + + it 'should not replace plantuml pre tag with img tag if url is invalid' do + stub_application_setting(plantuml_enabled: true, plantuml_url: "invalid") + input = '<pre class="plantuml"><code>Bob -> Sara : Hello</code><pre>' + output = '<div class="listingblock"><div class="content"><pre class="plantuml plantuml-error"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>' + doc = filter(input) + + expect(doc.to_s).to eq output + end +end -- cgit v1.2.1 From a0586dbc165cc09422412149712a218938137308 Mon Sep 17 00:00:00 2001 From: Adam Pahlevi <adam.pahlevi@gmail.com> Date: Fri, 3 Feb 2017 06:43:19 +0700 Subject: replace `find_with_namespace` with `find_by_full_path` add complete changelog for !8949 --- app/controllers/admin/projects_controller.rb | 2 +- app/controllers/admin/runner_projects_controller.rb | 2 +- app/controllers/projects/application_controller.rb | 2 +- app/controllers/projects/git_http_client_controller.rb | 2 +- app/controllers/projects/uploads_controller.rb | 2 +- app/helpers/application_helper.rb | 2 +- app/models/project.rb | 6 +----- app/services/auth/container_registry_authentication_service.rb | 2 +- changelogs/unreleased/fwn-to-find-by-full-path.yml | 4 ++++ db/fixtures/development/10_merge_requests.rb | 2 +- lib/api/helpers.rb | 2 +- lib/api/helpers/internal_helpers.rb | 4 ++-- lib/banzai/cross_project_reference.rb | 2 +- lib/constraints/project_url_constrainer.rb | 2 +- lib/gitlab/email/handler/create_issue_handler.rb | 2 +- lib/gitlab/git_post_receive.rb | 4 ++-- lib/tasks/gitlab/cleanup.rake | 2 +- lib/tasks/gitlab/import.rake | 2 +- lib/tasks/gitlab/sidekiq.rake | 2 +- spec/lib/banzai/cross_project_reference_spec.rb | 2 +- spec/routing/project_routing_spec.rb | 8 ++++---- spec/workers/post_receive_spec.rb | 6 +++--- 22 files changed, 32 insertions(+), 32 deletions(-) create mode 100644 changelogs/unreleased/fwn-to-find-by-full-path.yml diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index b09ae423096..39c8c6d8a0c 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -45,7 +45,7 @@ class Admin::ProjectsController < Admin::ApplicationController protected def project - @project = Project.find_with_namespace( + @project = Project.find_by_full_path( [params[:namespace_id], '/', params[:id]].join('') ) @project || render_404 diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb index bc65dcc33d3..70ac6a75434 100644 --- a/app/controllers/admin/runner_projects_controller.rb +++ b/app/controllers/admin/runner_projects_controller.rb @@ -24,7 +24,7 @@ class Admin::RunnerProjectsController < Admin::ApplicationController private def project - @project = Project.find_with_namespace( + @project = Project.find_by_full_path( [params[:namespace_id], '/', params[:project_id]].join('') ) @project || render_404 diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index b2ff36f6538..ba523b190bf 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -24,7 +24,7 @@ class Projects::ApplicationController < ApplicationController end project_path = "#{namespace}/#{id}" - @project = Project.find_with_namespace(project_path) + @project = Project.find_by_full_path(project_path) if can?(current_user, :read_project, @project) && !@project.pending_delete? if @project.path_with_namespace != project_path diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 70845617d3c..216c158e41e 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -79,7 +79,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController if project_id.blank? @project = nil else - @project = Project.find_with_namespace("#{params[:namespace_id]}/#{project_id}") + @project = Project.find_by_full_path("#{params[:namespace_id]}/#{project_id}") end end diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index e617be8f9fb..50ba33ed570 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -36,7 +36,7 @@ class Projects::UploadsController < Projects::ApplicationController namespace = params[:namespace_id] id = params[:project_id] - file_project = Project.find_with_namespace("#{namespace}/#{id}") + file_project = Project.find_by_full_path("#{namespace}/#{id}") if file_project.nil? @uploader = nil diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index a112928c6de..bee323993a0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -37,7 +37,7 @@ module ApplicationHelper if project_id.is_a?(Project) project_id else - Project.find_with_namespace(project_id) + Project.find_by_full_path(project_id) end if project.avatar_url diff --git a/app/models/project.rb b/app/models/project.rb index 59faf35e051..c3eced65e28 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -369,10 +369,6 @@ class Project < ActiveRecord::Base def group_ids joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id) end - - # Add alias for Routable method for compatibility with old code. - # In future all calls `find_with_namespace` should be replaced with `find_by_full_path` - alias_method :find_with_namespace, :find_by_full_path end def lfs_enabled? @@ -1345,6 +1341,6 @@ class Project < ActiveRecord::Base def pending_delete_twin return false unless path - Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace) + Project.unscoped.where(pending_delete: true).find_by_full_path(path_with_namespace) end end diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb index c00c5aebf57..5cb7a86a5ee 100644 --- a/app/services/auth/container_registry_authentication_service.rb +++ b/app/services/auth/container_registry_authentication_service.rb @@ -61,7 +61,7 @@ module Auth end def process_repository_access(type, name, actions) - requested_project = Project.find_with_namespace(name) + requested_project = Project.find_by_full_path(name) return unless requested_project actions = actions.select do |action| diff --git a/changelogs/unreleased/fwn-to-find-by-full-path.yml b/changelogs/unreleased/fwn-to-find-by-full-path.yml new file mode 100644 index 00000000000..1427e4e7624 --- /dev/null +++ b/changelogs/unreleased/fwn-to-find-by-full-path.yml @@ -0,0 +1,4 @@ +--- +title: replace `find_with_namespace` with `find_by_full_path` +merge_request: 8949 +author: Adam Pahlevi diff --git a/db/fixtures/development/10_merge_requests.rb b/db/fixtures/development/10_merge_requests.rb index c04afe97277..c304e0706dc 100644 --- a/db/fixtures/development/10_merge_requests.rb +++ b/db/fixtures/development/10_merge_requests.rb @@ -26,7 +26,7 @@ Gitlab::Seeder.quiet do end end - project = Project.find_with_namespace('gitlab-org/gitlab-test') + project = Project.find_by_full_path('gitlab-org/gitlab-test') params = { source_branch: 'feature', diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index a1d7b323f4f..eb5b947172a 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -45,7 +45,7 @@ module API if id =~ /^\d+$/ Project.find_by(id: id) else - Project.find_with_namespace(id) + Project.find_by_full_path(id) end end diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb index e8975eb57e0..080a6274957 100644 --- a/lib/api/helpers/internal_helpers.rb +++ b/lib/api/helpers/internal_helpers.rb @@ -30,7 +30,7 @@ module API def wiki? @wiki ||= project_path.end_with?('.wiki') && - !Project.find_with_namespace(project_path) + !Project.find_by_full_path(project_path) end def project @@ -41,7 +41,7 @@ module API # the wiki repository as well. project_path.chomp!('.wiki') if wiki? - Project.find_with_namespace(project_path) + Project.find_by_full_path(project_path) end end diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb index 0257848b6bc..e2b57adf611 100644 --- a/lib/banzai/cross_project_reference.rb +++ b/lib/banzai/cross_project_reference.rb @@ -14,7 +14,7 @@ module Banzai def project_from_ref(ref) return context[:project] unless ref - Project.find_with_namespace(ref) + Project.find_by_full_path(ref) end end end diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb index 730b05bed97..a10b4657d7d 100644 --- a/lib/constraints/project_url_constrainer.rb +++ b/lib/constraints/project_url_constrainer.rb @@ -8,6 +8,6 @@ class ProjectUrlConstrainer return false end - Project.find_with_namespace(full_path).present? + Project.find_by_full_path(full_path).present? end end diff --git a/lib/gitlab/email/handler/create_issue_handler.rb b/lib/gitlab/email/handler/create_issue_handler.rb index 127fae159d5..b8ec9138c10 100644 --- a/lib/gitlab/email/handler/create_issue_handler.rb +++ b/lib/gitlab/email/handler/create_issue_handler.rb @@ -34,7 +34,7 @@ module Gitlab end def project - @project ||= Project.find_with_namespace(project_path) + @project ||= Project.find_by_full_path(project_path) end private diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb index d32bdd86427..6babea144c7 100644 --- a/lib/gitlab/git_post_receive.rb +++ b/lib/gitlab/git_post_receive.rb @@ -30,11 +30,11 @@ module Gitlab def retrieve_project_and_type @type = :project - @project = Project.find_with_namespace(@repo_path) + @project = Project.find_by_full_path(@repo_path) if @repo_path.end_with?('.wiki') && !@project @type = :wiki - @project = Project.find_with_namespace(@repo_path.gsub(/\.wiki\z/, '')) + @project = Project.find_by_full_path(@repo_path.gsub(/\.wiki\z/, '')) end end diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake index 4a696a52b4d..967f630ef20 100644 --- a/lib/tasks/gitlab/cleanup.rake +++ b/lib/tasks/gitlab/cleanup.rake @@ -58,7 +58,7 @@ namespace :gitlab do sub(%r{^/*}, ''). chomp('.git'). chomp('.wiki') - next if Project.find_with_namespace(repo_with_namespace) + next if Project.find_by_full_path(repo_with_namespace) new_path = path + move_suffix puts path.inspect + ' -> ' + new_path.inspect File.rename(path, new_path) diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index a2eca74a3c8..690899b9ff4 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -29,7 +29,7 @@ namespace :gitlab do next end - project = Project.find_with_namespace(path) + project = Project.find_by_full_path(path) if project puts " * #{project.name} (#{repo_path}) exists" diff --git a/lib/tasks/gitlab/sidekiq.rake b/lib/tasks/gitlab/sidekiq.rake index 7e2a6668e59..f2e12d85045 100644 --- a/lib/tasks/gitlab/sidekiq.rake +++ b/lib/tasks/gitlab/sidekiq.rake @@ -7,7 +7,7 @@ namespace :gitlab do unless args.project.present? abort "Please specify the project you want to drop PostReceive jobs for:\n rake gitlab:sidekiq:drop_post_receive[group/project]" end - project_path = Project.find_with_namespace(args.project).repository.path_to_repo + project_path = Project.find_by_full_path(args.project).repository.path_to_repo Sidekiq.redis do |redis| unless redis.exists(QUEUE) diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb index 81b9a513ce3..deaabceef1c 100644 --- a/spec/lib/banzai/cross_project_reference_spec.rb +++ b/spec/lib/banzai/cross_project_reference_spec.rb @@ -24,7 +24,7 @@ describe Banzai::CrossProjectReference, lib: true do it 'returns the referenced project' do project2 = double('referenced project') - expect(Project).to receive(:find_with_namespace). + expect(Project).to receive(:find_by_full_path). with('cross/reference').and_return(project2) expect(project_from_ref('cross/reference')).to eq project2 diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 77549db2927..96889abee79 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe 'project routing' do before do - allow(Project).to receive(:find_with_namespace).and_return(false) - allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq').and_return(true) + allow(Project).to receive(:find_by_full_path).and_return(false) + allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq').and_return(true) end # Shared examples for a resource inside a Project @@ -86,13 +86,13 @@ describe 'project routing' do end context 'name with dot' do - before { allow(Project).to receive(:find_with_namespace).with('gitlab/gitlabhq.keys').and_return(true) } + before { allow(Project).to receive(:find_by_full_path).with('gitlab/gitlabhq.keys').and_return(true) } it { expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') } end context 'with nested group' do - before { allow(Project).to receive(:find_with_namespace).with('gitlab/subgroup/gitlabhq').and_return(true) } + before { allow(Project).to receive(:find_by_full_path).with('gitlab/subgroup/gitlabhq').and_return(true) } it { expect(get('/gitlab/subgroup/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlabhq') } end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 984acdade36..5919b99a6ed 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -74,7 +74,7 @@ describe PostReceive do context "webhook" do it "fetches the correct project" do - expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project) + expect(Project).to receive(:find_by_full_path).with(project.path_with_namespace).and_return(project) PostReceive.new.perform(pwd(project), key_id, base64_changes) end @@ -89,7 +89,7 @@ describe PostReceive do end it "asks the project to trigger all hooks" do - allow(Project).to receive(:find_with_namespace).and_return(project) + allow(Project).to receive(:find_by_full_path).and_return(project) expect(project).to receive(:execute_hooks).twice expect(project).to receive(:execute_services).twice @@ -97,7 +97,7 @@ describe PostReceive do end it "enqueues a UpdateMergeRequestsWorker job" do - allow(Project).to receive(:find_with_namespace).and_return(project) + allow(Project).to receive(:find_by_full_path).and_return(project) expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args) PostReceive.new.perform(pwd(project), key_id, base64_changes) -- cgit v1.2.1 From 80bd5717367ae4af63ac476244e6adee8ccefad5 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Fri, 3 Feb 2017 14:44:12 +0600 Subject: fixes flickers in avatar mention dropdown --- app/assets/stylesheets/framework/animations.scss | 21 +++++++++++++++++++++ app/assets/stylesheets/framework/markdown_area.scss | 1 + 2 files changed, 22 insertions(+) diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index 8d38fc78a19..f1ff9faef7f 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -71,6 +71,27 @@ transition: $unfoldedTransitions; } +@mixin disableAllAnimation { + /*CSS transitions*/ + -o-transition-property: none !important; + -moz-transition-property: none !important; + -ms-transition-property: none !important; + -webkit-transition-property: none !important; + transition-property: none !important; + /*CSS transforms*/ + -o-transform: none !important; + -moz-transform: none !important; + -ms-transform: none !important; + -webkit-transform: none !important; + transform: none !important; + /*CSS animations*/ + -webkit-animation: none !important; + -moz-animation: none !important; + -o-animation: none !important; + -ms-animation: none !important; + animation: none !important; +} + @function unfoldTransition ($transition) { // Default values $property: all; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index 5bff694658c..d4758d90352 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -159,6 +159,7 @@ .cur { .avatar { border: 1px solid $white-light; + @include disableAllAnimation; } } } -- cgit v1.2.1 From 22b77948ec8743574bdd74c98888c5c91eb1bb74 Mon Sep 17 00:00:00 2001 From: Nur Rony <pro.nmrony@gmail.com> Date: Fri, 3 Feb 2017 14:56:49 +0600 Subject: adds changelog --- .../unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml diff --git a/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml b/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml new file mode 100644 index 00000000000..a5bb37ec8a9 --- /dev/null +++ b/changelogs/unreleased/27602-fix-avatar-border-flicker-mention-dropdown.yml @@ -0,0 +1,4 @@ +--- +title: Fixes flickering of avatar border in mention dropdown +merge_request: 8950 +author: -- cgit v1.2.1 From f30e2a6ec7c011d0649aad9f118bf8c5a57ecdbc Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Fri, 3 Feb 2017 17:29:08 +0800 Subject: Use message_id_regexp variable for the regexp Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8620#note_22021001 --- lib/gitlab/incoming_email.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index a386d9b36fb..0ea42148c58 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -35,7 +35,9 @@ module Gitlab end def key_from_fallback_message_id(mail_id) - mail_id[/\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/, 1] + message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/ + + mail_id[message_id_regexp, 1] end def scan_fallback_references(references) -- cgit v1.2.1 From 849d09cfd692c0c54d45baf1ce71cd9c1ea2c6ab Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Fri, 3 Feb 2017 17:30:54 +0800 Subject: Use references variable Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8620#note_22020035 --- lib/gitlab/email/receiver.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb index fa08b5c668f..b64db5d01ae 100644 --- a/lib/gitlab/email/receiver.rb +++ b/lib/gitlab/email/receiver.rb @@ -56,7 +56,9 @@ module Gitlab end def key_from_additional_headers(mail) - find_key_from_references(ensure_references_array(mail.references)) + references = ensure_references_array(mail.references) + + find_key_from_references(references) end def ensure_references_array(references) -- cgit v1.2.1 From 2f80cbb6759beb412491f9b1b4f0dbcbec6619c0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Fri, 3 Feb 2017 17:37:06 +0800 Subject: Freeze regexp and add a comment Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8620#note_21590440 --- lib/gitlab/incoming_email.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index 0ea42148c58..a492f904303 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -35,12 +35,14 @@ module Gitlab end def key_from_fallback_message_id(mail_id) - message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/ + message_id_regexp = + /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/.freeze mail_id[message_id_regexp, 1] end def scan_fallback_references(references) + # It's looking for each <...> references.scan(/(?!<)[^<>]+(?=>)/.freeze) end -- cgit v1.2.1 From b5e2422939440cb2015558f26f9d361a569f2164 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 25 Jan 2017 15:51:11 +0000 Subject: Change "Build" to "Job" in builds show page header and sidebar Change "Builds" to "Job" in Builds table Change "Get started with Builds" to "Get started with CI/CD Pipelines" in builds list view Change "Build" to "Jobs" in builds show view --- app/views/projects/builds/_header.html.haml | 2 +- app/views/projects/builds/_sidebar.html.haml | 8 ++++---- app/views/projects/builds/_table.html.haml | 4 ++-- app/views/projects/builds/index.html.haml | 4 ++-- app/views/projects/builds/show.html.haml | 14 +++++++------- app/views/projects/pipelines/_head.html.haml | 2 +- app/views/projects/pipelines/_with_tabs.html.haml | 4 ++-- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index 736b485bf06..a904bce065d 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -1,7 +1,7 @@ .content-block.build-header .header-content = render 'ci/status/badge', status: @build.detailed_status(current_user), link: false - Build + Job %strong.js-build-id ##{@build.id} in pipeline = link_to pipeline_path(@build.pipeline) do diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 37bf085130a..65869538ab2 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -2,7 +2,7 @@ %aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar .block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default - Build + Job %strong ##{@build.id} %a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" } = icon('angle-double-right') @@ -17,7 +17,7 @@ - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) .block{ class: ("block-first" if !@build.coverage) } .title - Build artifacts + Job artifacts - if @build.artifacts_expired? %p.build-detail-row The artifacts were removed @@ -42,7 +42,7 @@ .block{ class: ("block-first" if !@build.coverage && !(can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?))) } .title - Build details + Job details - if can?(current_user, :update_build, @build) && @build.retryable? = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post - if @build.merge_request @@ -136,4 +136,4 @@ - else = build.id - if build.retried? - %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Build was retried' } + %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' } diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml index 028664f5bba..acfdb250aff 100644 --- a/app/views/projects/builds/_table.html.haml +++ b/app/views/projects/builds/_table.html.haml @@ -2,14 +2,14 @@ - if builds.blank? %div - .nothing-here-block No builds to show + .nothing-here-block No jobs to show - else .table-holder %table.table.ci-table.builds-page %thead %tr %th Status - %th Build + %th Job %th Pipeline - if admin %th Project diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml index c623e39b21f..5ffc0e20d10 100644 --- a/app/views/projects/builds/index.html.haml +++ b/app/views/projects/builds/index.html.haml @@ -1,5 +1,5 @@ - @no_container = true -- page_title "Builds" +- page_title "Jobs" = render "projects/pipelines/head" %div{ class: container_class } @@ -14,7 +14,7 @@ data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post - unless @repository.gitlab_ci_yml - = link_to 'Get started with Builds', help_page_path('ci/quick_start/README'), class: 'btn btn-info' + = link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info' = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index c613e473e4c..351f9c9067f 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -1,5 +1,5 @@ - @no_container = true -- page_title "#{@build.name} (##{@build.id})", "Builds" +- page_title "#{@build.name} (##{@build.id})", "Jobs" - trace_with_state = @build.trace_with_state = render "projects/pipelines/head", build_subnav: true @@ -37,14 +37,14 @@ - environment = environment_for_build(@build.project, @build) - if @build.success? && @build.last_deployment.present? - if @build.last_deployment.last? - This build is the most recent deployment to #{environment_link_for_build(@build.project, @build)}. + This job is the most recent deployment to #{environment_link_for_build(@build.project, @build)}. - else - This build is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}. + This job is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}. View the most recent deployment #{deployment_link(environment.last_deployment)}. - elsif @build.complete? && !@build.success? - The deployment of this build to #{environment_link_for_build(@build.project, @build)} did not succeed. + The deployment of this job to #{environment_link_for_build(@build.project, @build)} did not succeed. - else - This build is creating a deployment to #{environment_link_for_build(@build.project, @build)} + This job is creating a deployment to #{environment_link_for_build(@build.project, @build)} - if environment.try(:last_deployment) and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')} @@ -52,9 +52,9 @@ - if @build.erased? .erased.alert.alert-warning - if @build.erased_by_user? - Build has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} + Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} - else - Build has been erased #{time_ago_with_tooltip(@build.erased_at)} + Job has been erased #{time_ago_with_tooltip(@build.erased_at)} - else #js-build-scroll.scroll-controls .scroll-step diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index b10dd47709f..2f766a828a9 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -13,7 +13,7 @@ = nav_link(controller: %w(builds)) do = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do %span - Builds + Jobs - if project_nav_tab? :environments = nav_link(controller: %w(environments)) do diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml index 88af41aa835..53067cdcba4 100644 --- a/app/views/projects/pipelines/_with_tabs.html.haml +++ b/app/views/projects/pipelines/_with_tabs.html.haml @@ -5,7 +5,7 @@ Pipeline %li.js-builds-tab-link = link_to builds_namespace_project_pipeline_path(@project.namespace, @project, @pipeline), data: {target: 'div#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do - Builds + Jobs %span.badge.js-builds-counter= pipeline.statuses.count @@ -33,7 +33,7 @@ %thead %tr %th Status - %th Build ID + %th Job ID %th Name %th - if pipeline.project.build_coverage_enabled? -- cgit v1.2.1 From 5b0f492b0de31e7f608527a85da9e28f9f92276f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 25 Jan 2017 16:14:58 +0000 Subject: Adds changelog entry Replace "builds" in project settings Replace "builds" in admin area --- app/views/admin/builds/index.html.haml | 2 +- app/views/admin/dashboard/_head.html.haml | 4 ++-- app/views/admin/runners/index.html.haml | 10 +++++----- app/views/admin/runners/show.html.haml | 8 ++++---- app/views/projects/_merge_request_merge_settings.html.haml | 4 ++-- app/views/projects/edit.html.haml | 4 ++-- app/views/projects/graphs/ci/_builds.haml | 8 ++++---- app/views/projects/pipelines_settings/show.html.haml | 2 +- app/views/projects/runners/index.html.haml | 6 +++--- app/views/projects/triggers/index.html.haml | 2 +- app/views/shared/web_hooks/_form.html.haml | 2 +- changelogs/unreleased/17662-rename-builds.yml | 4 ++++ 12 files changed, 30 insertions(+), 26 deletions(-) create mode 100644 changelogs/unreleased/17662-rename-builds.yml diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml index 5e3f105d41f..66d633119c2 100644 --- a/app/views/admin/builds/index.html.haml +++ b/app/views/admin/builds/index.html.haml @@ -12,7 +12,7 @@ = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post .row-content-block.second-block - #{(@scope || 'all').capitalize} builds + #{(@scope || 'all').capitalize} jobs %ul.content-list.builds-content-list.admin-builds-table = render "projects/builds/table", builds: @builds, admin: true diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml index b5f96363230..7893c1dee97 100644 --- a/app/views/admin/dashboard/_head.html.haml +++ b/app/views/admin/dashboard/_head.html.haml @@ -20,9 +20,9 @@ %span Groups = nav_link path: 'builds#index' do - = link_to admin_builds_path, title: 'Builds' do + = link_to admin_builds_path, title: 'Jobs' do %span - Builds + Jobs = nav_link path: ['runners#index', 'runners#show'] do = link_to admin_runners_path, title: 'Runners' do %span diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 124f970524e..721bc77cc2f 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -26,7 +26,7 @@ .bs-callout %p - A 'Runner' is a process which runs a build. + A 'Runner' is a process which runs a job. You can setup as many Runners as you need. %br Runners can be placed on separate users, servers, even on your local machine. @@ -37,16 +37,16 @@ %ul %li %span.label.label-success shared - \- Runner runs builds from all unassigned projects + \- Runner runs jobs from all unassigned projects %li %span.label.label-info specific - \- Runner runs builds from assigned projects + \- Runner runs jobs from assigned projects %li %span.label.label-warning locked \- Runner cannot be assigned to other projects %li %span.label.label-danger paused - \- Runner will not receive any new builds + \- Runner will not receive any new jobs .append-bottom-20.clearfix .pull-left @@ -68,7 +68,7 @@ %th Runner token %th Description %th Projects - %th Builds + %th Jobs %th Tags %th Last contact %th diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 39e103e3062..dc4116e1ce0 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -11,13 +11,13 @@ - if @runner.shared? .bs-callout.bs-callout-success - %h4 This Runner will process builds from ALL UNASSIGNED projects + %h4 This Runner will process jobs from ALL UNASSIGNED projects %p If you want Runners to build only specific projects, enable them in the table below. Keep in mind that this is a one way transition. - else .bs-callout.bs-callout-info - %h4 This Runner will process builds only from ASSIGNED projects + %h4 This Runner will process jobs only from ASSIGNED projects %p You can't make this a shared Runner. %hr @@ -70,11 +70,11 @@ = paginate @projects, theme: "gitlab" .col-md-6 - %h4 Recent builds served by this Runner + %h4 Recent jobs served by this Runner %table.table.ci-table.runner-builds %thead %tr - %th Build + %th Job %th Status %th Project %th Commit diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml index 1a1327fb53c..27d25a6b682 100644 --- a/app/views/projects/_merge_request_merge_settings.html.haml +++ b/app/views/projects/_merge_request_merge_settings.html.haml @@ -4,10 +4,10 @@ .checkbox.builds-feature = form.label :only_allow_merge_if_build_succeeds do = form.check_box :only_allow_merge_if_build_succeeds - %strong Only allow merge requests to be merged if the build succeeds + %strong Only allow merge requests to be merged if the pipeline succeeds %br %span.descr - Builds need to be configured to enable this feature. + Pipelines need to be configured to enable this feature. = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_pipeline_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-pipeline-succeeds') .checkbox = form.label :only_allow_merge_if_all_discussions_are_resolved do diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index 4a0ce995165..7a2dacdb1e7 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -63,7 +63,7 @@ .row .col-md-9.project-feature.nested - = feature_fields.label :builds_access_level, "Builds", class: 'label-light' + = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light' %span.help-block Submit, test and deploy your changes before merge .col-md-3 = project_feature_access_select(:builds_access_level) @@ -180,7 +180,7 @@ %p The following items will NOT be exported: %ul - %li Build traces and artifacts + %li Job traces and artifacts %li LFS objects %li Container registry images %li CI variables diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml index 431657c4dcb..b6f453b9736 100644 --- a/app/views/projects/graphs/ci/_builds.haml +++ b/app/views/projects/graphs/ci/_builds.haml @@ -1,4 +1,4 @@ -%h4 Build charts +%h4 Pipelines charts %p   %span.cgreen @@ -11,19 +11,19 @@ .prepend-top-default %p.light - Builds for last week + Jobs for last week (#{date_from_to(Date.today - 7.days, Date.today)}) %canvas#weekChart{ height: 200 } .prepend-top-default %p.light - Builds for last month + Jobs for last month (#{date_from_to(Date.today - 30.days, Date.today)}) %canvas#monthChart{ height: 200 } .prepend-top-default %p.light - Builds for last year + Jobs for last year %canvas#yearChart.padded{ height: 250 } - [:week, :month, :year].each do |scope| diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml index 1f698558bce..18328c67f02 100644 --- a/app/views/projects/pipelines_settings/show.html.haml +++ b/app/views/projects/pipelines_settings/show.html.haml @@ -66,7 +66,7 @@ %span.input-group-addon / %p.help-block A regular expression that will be used to find the test coverage - output in the build trace. Leave blank to disable + output in the job trace. Leave blank to disable = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing') .bs-callout.bs-callout-info %p Below are examples of regex for existing tools: diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml index 92957470070..9d41b18b934 100644 --- a/app/views/projects/runners/index.html.haml +++ b/app/views/projects/runners/index.html.haml @@ -12,14 +12,14 @@ %ul %li %span.label.label-success active - \- Runner is active and can process any new builds + \- Runner is active and can process any new jobs %li %span.label.label-danger paused - \- Runner is paused and will not receive any new builds + \- Runner is paused and will not receive any new jobs %hr -%p.lead To start serving your builds you can either add specific Runners to your project or use shared Runners +%p.lead To start serving your jobs you can either add specific Runners to your project or use shared Runners .row .col-sm-6 = render 'specific_runners' diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index 6e5dd1b196d..10243fe9fd0 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -86,7 +86,7 @@ :plain #{builds_trigger_url(@project.id, ref: 'REF_NAME')}?token=TOKEN %h5.prepend-top-default - Pass build variables + Pass job variables %p.light Add diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 13586a5a12a..3a1a6a8fb7a 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -68,7 +68,7 @@ = f.label :build_events, class: 'list-label' do %strong Build events %p.light - This URL will be triggered when the build status changes + This URL will be triggered when the job status changes %li = f.check_box :pipeline_events, class: 'pull-left' .prepend-left-20 diff --git a/changelogs/unreleased/17662-rename-builds.yml b/changelogs/unreleased/17662-rename-builds.yml new file mode 100644 index 00000000000..12f2998d1c8 --- /dev/null +++ b/changelogs/unreleased/17662-rename-builds.yml @@ -0,0 +1,4 @@ +--- +title: Rename Builds to Pipelines, CI/CD Pipelines, or Jobs everywhere +merge_request: 8787 +author: -- cgit v1.2.1 From c5f5ce8807bf7cbf81b43d0caf1df089d39b880e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 26 Jan 2017 11:52:58 +0000 Subject: Fix broken tests Rename Build to Job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace "Builds" by "Jobs" and fix broken specs Replace "Builds" by "Jobs" Fix broken spinach test Fix broken test Remove `˙` at the beginning of the file Fix broken spinach test Fix broken tests Changes after review --- .../environments/components/environment.js.es6 | 2 +- .../vue_pipelines_index/pipeline_actions.js.es6 | 4 +- .../admin/application_settings/_form.html.haml | 2 +- app/views/help/_shortcuts.html.haml | 2 +- app/views/layouts/nav/_project.html.haml | 4 +- app/views/notify/build_fail_email.html.haml | 4 +- app/views/notify/build_fail_email.text.erb | 2 +- app/views/notify/build_success_email.html.haml | 4 +- app/views/notify/build_success_email.text.erb | 2 +- app/views/notify/links/ci/builds/_build.text.erb | 2 +- .../_generic_commit_status.text.erb | 2 +- app/views/projects/_customize_workflow.html.haml | 2 +- app/views/projects/artifacts/browse.html.haml | 2 +- app/views/projects/builds/_header.html.haml | 2 +- app/views/projects/builds/_sidebar.html.haml | 2 +- app/views/projects/builds/show.html.haml | 6 +-- app/views/projects/ci/builds/_build.html.haml | 4 +- .../projects/ci/pipelines/_pipeline.html.haml | 4 +- app/views/projects/commit/_pipeline.html.haml | 4 +- app/views/projects/environments/show.html.haml | 2 +- .../merge_requests/widget/_heading.html.haml | 2 +- .../projects/merge_requests/widget/_show.html.haml | 8 +-- .../widget/open/_build_failed.html.haml | 4 +- app/views/projects/pipelines/_head.html.haml | 2 +- app/views/projects/pipelines/_info.html.haml | 2 +- app/views/projects/runners/_form.html.haml | 2 +- app/views/projects/runners/index.html.haml | 2 +- app/views/projects/triggers/index.html.haml | 4 +- app/views/projects/variables/_content.html.haml | 2 +- app/views/shared/web_hooks/_form.html.haml | 2 +- features/steps/project/builds/summary.rb | 2 +- features/steps/project/graph.rb | 6 +-- features/steps/shared/builds.rb | 4 +- lib/api/builds.rb | 2 +- spec/features/admin/admin_builds_spec.rb | 34 ++++++------- .../only_allow_merge_if_build_succeeds_spec.rb | 4 +- spec/features/projects/builds_spec.rb | 44 ++++++++-------- spec/features/projects/pipelines/pipeline_spec.rb | 42 ++++++++-------- .../settings/merge_requests_settings_spec.rb | 18 +++---- .../fixtures/environments/table.html.haml | 2 +- spec/models/ci/build_spec.rb | 4 +- spec/requests/api/builds_spec.rb | 58 +++++++++++----------- spec/requests/ci/api/builds_spec.rb | 2 +- spec/views/projects/builds/show.html.haml_spec.rb | 40 +++++++-------- 44 files changed, 175 insertions(+), 175 deletions(-) diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index 971be04e2d2..558828e1bc9 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -180,7 +180,7 @@ <tr> <th class="environments-name">Environment</th> <th class="environments-deploy">Last deployment</th> - <th class="environments-build">Build</th> + <th class="environments-build">Job</th> <th class="environments-commit">Commit</th> <th class="environments-date">Updated</th> <th class="hidden-xs environments-actions"></th> diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index a7176e27ea1..01f8b6519a4 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -26,9 +26,9 @@ v-if='actions' class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" data-toggle="dropdown" - title="Manual build" + title="Manual job" data-placement="top" - aria-label="Manual build" + aria-label="Manual job" > <span v-html='svgs.iconPlay' aria-hidden="true"></span> <i class="fa fa-caret-down" aria-hidden="true"></i> diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 558bbe07b16..e7701d75a6e 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -204,7 +204,7 @@ .col-sm-10 = f.number_field :max_artifacts_size, class: 'form-control' .help-block - Set the maximum file size each build's artifacts can have + Set the maximum file size each jobs's artifacts can have = link_to "(?)", help_page_path("user/admin_area/settings/continuous_integration", anchor: "maximum-artifacts-size") - if Gitlab.config.registry.enabled diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml index b74cc822295..da2df0d8080 100644 --- a/app/views/help/_shortcuts.html.haml +++ b/app/views/help/_shortcuts.html.haml @@ -143,7 +143,7 @@ .key g .key b %td - Go to builds + Go to jobs %tr %td.shortcut .key g diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml index a8bbd67de80..7883823b21e 100644 --- a/app/views/layouts/nav/_project.html.haml +++ b/app/views/layouts/nav/_project.html.haml @@ -96,8 +96,8 @@ -# Shortcut to builds page - if project_nav_tab? :builds %li.hidden - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do - Builds + = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do + Jobs -# Shortcut to commits page - if project_nav_tab? :commits diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml index a744c4be9d6..060b50ffc69 100644 --- a/app/views/notify/build_fail_email.html.haml +++ b/app/views/notify/build_fail_email.html.haml @@ -1,6 +1,6 @@ - content_for :header do %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" } - GitLab (build failed) + GitLab (job failed) %h3 Project: @@ -21,4 +21,4 @@ Message: #{@build.pipeline.git_commit_message} %p - Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} + Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb index 9d497983498..2a94688a6b0 100644 --- a/app/views/notify/build_fail_email.text.erb +++ b/app/views/notify/build_fail_email.text.erb @@ -1,4 +1,4 @@ -Build failed for <%= @project.name %> +Job failed for <%= @project.name %> Status: <%= @build.status %> Commit: <%= @build.pipeline.short_sha %> diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml index 8c2e6db1426..ca0eaa96a9d 100644 --- a/app/views/notify/build_success_email.html.haml +++ b/app/views/notify/build_success_email.html.haml @@ -1,6 +1,6 @@ - content_for :header do %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" } - GitLab (build successful) + GitLab (job successful) %h3 Project: @@ -21,4 +21,4 @@ Message: #{@build.pipeline.git_commit_message} %p - Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} + Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)} diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb index c5ed4f84861..445cd46e64f 100644 --- a/app/views/notify/build_success_email.text.erb +++ b/app/views/notify/build_success_email.text.erb @@ -1,4 +1,4 @@ -Build successful for <%= @project.name %> +Job successful for <%= @project.name %> Status: <%= @build.status %> Commit: <%= @build.pipeline.short_sha %> diff --git a/app/views/notify/links/ci/builds/_build.text.erb b/app/views/notify/links/ci/builds/_build.text.erb index f495a2e5486..741c7f344c8 100644 --- a/app/views/notify/links/ci/builds/_build.text.erb +++ b/app/views/notify/links/ci/builds/_build.text.erb @@ -1 +1 @@ -Build #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> ) +Job #<%= build.id %> ( <%= pipeline_build_url(pipeline, build) %> ) diff --git a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb index 8e89c52a1f3..af8924bad57 100644 --- a/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb +++ b/app/views/notify/links/generic_commit_statuses/_generic_commit_status.text.erb @@ -1 +1 @@ -Build #<%= build.id %> +Job #<%= build.id %> diff --git a/app/views/projects/_customize_workflow.html.haml b/app/views/projects/_customize_workflow.html.haml index e2b73cee5a9..a41791f0eca 100644 --- a/app/views/projects/_customize_workflow.html.haml +++ b/app/views/projects/_customize_workflow.html.haml @@ -3,6 +3,6 @@ %h4 Customize your workflow! %p - Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and builds, GitLab can help manage your workflow from idea to production! + Get started with GitLab by enabling features that work best for your project. From issues and wikis, to merge requests and pipelines, GitLab can help manage your workflow from idea to production! - if can?(current_user, :admin_project, @project) = link_to "Get started", edit_project_path(@project), class: "btn btn-success" diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml index d0ff14e45e6..edf55d59f28 100644 --- a/app/views/projects/artifacts/browse.html.haml +++ b/app/views/projects/artifacts/browse.html.haml @@ -1,4 +1,4 @@ -- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds' +- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Jobs' .top-block.row-content-block.clearfix .pull-right diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml index a904bce065d..27e81c2bec3 100644 --- a/app/views/projects/builds/_header.html.haml +++ b/app/views/projects/builds/_header.html.haml @@ -17,6 +17,6 @@ = render "user" = time_ago_with_tooltip(@build.created_at) - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post + = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted-secondary pull-right', method: :post %button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml index 65869538ab2..56fc5f5e68b 100644 --- a/app/views/projects/builds/_sidebar.html.haml +++ b/app/views/projects/builds/_sidebar.html.haml @@ -44,7 +44,7 @@ .title Job details - if can?(current_user, :update_build, @build) && @build.retryable? - = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post + = link_to "Retry job", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post - if @build.merge_request %p.build-detail-row %span.build-light-text Merge Request: diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml index 351f9c9067f..228dad528ab 100644 --- a/app/views/projects/builds/show.html.haml +++ b/app/views/projects/builds/show.html.haml @@ -12,14 +12,14 @@ .bs-callout.bs-callout-warning %p - if no_runners_for_project?(@build.project) - This build is stuck, because the project doesn't have any runners online assigned to it. + This job is stuck, because the project doesn't have any runners online assigned to it. - elsif @build.tags.any? - This build is stuck, because you don't have any active runners online with any of these tags assigned to them: + This job is stuck, because you don't have any active runners online with any of these tags assigned to them: - @build.tags.each do |tag| %span.label.label-primary = tag - else - This build is stuck, because you don't have any active runners that can run this build. + This job is stuck, because you don't have any active runners that can run this job. %br Go to diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index c1e496455d1..5ea85f9fd4c 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -32,10 +32,10 @@ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace" - if build.stuck? - = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.') + = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.') - if retried - = icon('refresh', class: 'text-warning has-tooltip', title: 'Build was retried') + = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried') .label-container - if build.tags.any? diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml index 818a70f38f1..cdab1e1b1a6 100644 --- a/app/views/projects/ci/pipelines/_pipeline.html.haml +++ b/app/views/projects/ci/pipelines/_pipeline.html.haml @@ -15,7 +15,7 @@ - else %span.api.monospace API - if pipeline.latest? - %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest + %span.label.label-success.has-tooltip{ title: 'Latest job for this branch' } latest - if pipeline.triggered? %span.label.label-primary triggered - if pipeline.yaml_errors.present? @@ -78,7 +78,7 @@ .btn-group.inline - if actions.any? .btn-group - %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual build', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual build' } + %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual job', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label' => 'Manual job' } = custom_icon('icon_play') = icon('caret-down', 'aria-hidden' => 'true') %ul.dropdown-menu.dropdown-menu-align-right diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml index 08d3443b3d0..6abff6aaf95 100644 --- a/app/views/projects/commit/_pipeline.html.haml +++ b/app/views/projects/commit/_pipeline.html.haml @@ -13,7 +13,7 @@ Pipeline = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace" with - = pluralize pipeline.statuses.count(:id), "build" + = pluralize pipeline.statuses.count(:id), "job" - if pipeline.ref for = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace" @@ -44,7 +44,7 @@ %thead %tr %th Status - %th Build ID + %th Job ID %th Name %th - if pipeline.project.build_coverage_enabled? diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index f3179dce5f2..7800d6ac382 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -32,7 +32,7 @@ %tr %th ID %th Commit - %th Build + %th Job %th Created %th.hidden-xs diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml index 0e3af62ebc2..ae134563ead 100644 --- a/app/views/projects/merge_requests/widget/_heading.html.haml +++ b/app/views/projects/merge_requests/widget/_heading.html.haml @@ -21,7 +21,7 @@ .ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: "display:none" } = ci_icon_for_status(status) %span - CI build + CI job = ci_label_for_status(status) for - commit = @merge_request.diff_head_commit diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml index f07e6b3ad54..5de59473840 100644 --- a/app/views/projects/merge_requests/widget/_show.html.haml +++ b/app/views/projects/merge_requests/widget/_show.html.haml @@ -16,13 +16,13 @@ gitlab_icon: "#{asset_path 'gitlab_logo.png'}", ci_status: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.status : ''}", ci_message: { - normal: "Build {{status}} for \"{{title}}\"", - preparing: "{{status}} build for \"{{title}}\"" + normal: "Job {{status}} for \"{{title}}\"", + preparing: "{{status}} job for \"{{title}}\"" }, ci_enable: #{@project.ci_service ? "true" : "false"}, ci_title: { - preparing: "{{status}} build", - normal: "Build {{status}}" + preparing: "{{status}} job", + normal: "Job {{status}}" }, ci_sha: "#{@merge_request.head_pipeline ? @merge_request.head_pipeline.short_sha : ''}", ci_pipeline: #{@merge_request.head_pipeline.try(:id).to_json}, diff --git a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml index 14f51af5360..a18c2ad768f 100644 --- a/app/views/projects/merge_requests/widget/open/_build_failed.html.haml +++ b/app/views/projects/merge_requests/widget/open/_build_failed.html.haml @@ -1,6 +1,6 @@ %h4 = icon('exclamation-triangle') - The build for this merge request failed + The job for this merge request failed %p - Please retry the build or push a new commit to fix the failure. + Please retry the job or push a new commit to fix the failure. diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml index 2f766a828a9..721a9b6beb5 100644 --- a/app/views/projects/pipelines/_head.html.haml +++ b/app/views/projects/pipelines/_head.html.haml @@ -11,7 +11,7 @@ - if project_nav_tab? :builds = nav_link(controller: %w(builds)) do - = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do + = link_to project_builds_path(@project), title: 'Jobs', class: 'shortcuts-builds' do %span Jobs diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml index 6caa5f16dc6..a6cd2d83bd5 100644 --- a/app/views/projects/pipelines/_info.html.haml +++ b/app/views/projects/pipelines/_info.html.haml @@ -25,7 +25,7 @@ .well-segment.pipeline-info .icon-container = icon('clock-o') - = pluralize @pipeline.statuses.count(:id), "build" + = pluralize @pipeline.statuses.count(:id), "job" - if @pipeline.ref from = link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace" diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml index 33a9a96183c..98e72f6c547 100644 --- a/app/views/projects/runners/_form.html.haml +++ b/app/views/projects/runners/_form.html.haml @@ -5,7 +5,7 @@ .col-sm-10 .checkbox = f.check_box :active - %span.light Paused Runners don't accept new builds + %span.light Paused Runners don't accept new jobs .form-group = label :run_untagged, 'Run untagged jobs', class: 'control-label' .col-sm-10 diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml index 9d41b18b934..d6f691d9c24 100644 --- a/app/views/projects/runners/index.html.haml +++ b/app/views/projects/runners/index.html.haml @@ -2,7 +2,7 @@ .light.prepend-top-default %p - A 'Runner' is a process which runs a build. + A 'Runner' is a process which runs a job. You can setup as many Runners as you need. %br Runners can be placed on separate users, servers, and even on your local machine. diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml index 10243fe9fd0..b9c4e323430 100644 --- a/app/views/projects/triggers/index.html.haml +++ b/app/views/projects/triggers/index.html.haml @@ -67,7 +67,7 @@ In the %code .gitlab-ci.yml of another project, include the following snippet. - The project will be rebuilt at the end of the build. + The project will be rebuilt at the end of the job. %pre :plain @@ -91,7 +91,7 @@ %p.light Add %code variables[VARIABLE]=VALUE - to an API request. Variable values can be used to distinguish between triggered builds and normal builds. + to an API request. Variable values can be used to distinguish between triggered jobs and normal jobs. With cURL: diff --git a/app/views/projects/variables/_content.html.haml b/app/views/projects/variables/_content.html.haml index 0249e0c1bf1..06477aba103 100644 --- a/app/views/projects/variables/_content.html.haml +++ b/app/views/projects/variables/_content.html.haml @@ -5,4 +5,4 @@ %p So you can use them for passwords, secret keys or whatever you want. %p - The value of the variable can be visible in build log if explicitly asked to do so. + The value of the variable can be visible in job log if explicitly asked to do so. diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 3a1a6a8fb7a..c212d1c86bf 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -66,7 +66,7 @@ = f.check_box :build_events, class: 'pull-left' .prepend-left-20 = f.label :build_events, class: 'list-label' do - %strong Build events + %strong Jobs events %p.light This URL will be triggered when the job status changes %li diff --git a/features/steps/project/builds/summary.rb b/features/steps/project/builds/summary.rb index 374eb0b0e07..19ff92f6dc6 100644 --- a/features/steps/project/builds/summary.rb +++ b/features/steps/project/builds/summary.rb @@ -33,7 +33,7 @@ class Spinach::Features::ProjectBuildsSummary < Spinach::FeatureSteps step 'recent build summary contains information saying that build has been erased' do page.within('.erased') do - expect(page).to have_content 'Build has been erased' + expect(page).to have_content 'Job has been erased' end end diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb index 7490d2bc6e7..48ac7a98f0d 100644 --- a/features/steps/project/graph.rb +++ b/features/steps/project/graph.rb @@ -34,9 +34,9 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps step 'page should have CI graphs' do expect(page).to have_content 'Overall' - expect(page).to have_content 'Builds for last week' - expect(page).to have_content 'Builds for last month' - expect(page).to have_content 'Builds for last year' + expect(page).to have_content 'Jobs for last week' + expect(page).to have_content 'Jobs for last month' + expect(page).to have_content 'Jobs for last year' expect(page).to have_content 'Commit duration in minutes for last 30 commits' end diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb index 70e6d4836b2..d008a8a26af 100644 --- a/features/steps/shared/builds.rb +++ b/features/steps/shared/builds.rb @@ -47,7 +47,7 @@ module SharedBuilds end step 'recent build has a build trace' do - @build.trace = 'build trace' + @build.trace = 'job trace' end step 'download of build artifacts archive starts' do @@ -60,7 +60,7 @@ module SharedBuilds end step 'I see details of a build' do - expect(page).to have_content "Build ##{@build.id}" + expect(page).to have_content "Job ##{@build.id}" end step 'I see build trace' do diff --git a/lib/api/builds.rb b/lib/api/builds.rb index af61be343be..44fe0fc4a95 100644 --- a/lib/api/builds.rb +++ b/lib/api/builds.rb @@ -209,7 +209,7 @@ module API build = get_build!(params[:build_id]) - bad_request!("Unplayable Build") unless build.playable? + bad_request!("Unplayable Job") unless build.playable? build.play(current_user) diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index e177059d959..9d5ce876c29 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -9,8 +9,8 @@ describe 'Admin Builds' do let(:pipeline) { create(:ci_pipeline) } context 'All tab' do - context 'when have builds' do - it 'shows all builds' do + context 'when have jobs' do + it 'shows all jobs' do create(:ci_build, pipeline: pipeline, status: :pending) create(:ci_build, pipeline: pipeline, status: :running) create(:ci_build, pipeline: pipeline, status: :success) @@ -19,26 +19,26 @@ describe 'Admin Builds' do visit admin_builds_path expect(page).to have_selector('.nav-links li.active', text: 'All') - expect(page).to have_selector('.row-content-block', text: 'All builds') + expect(page).to have_selector('.row-content-block', text: 'All jobs') expect(page.all('.build-link').size).to eq(4) expect(page).to have_link 'Cancel all' end end - context 'when have no builds' do + context 'when have no jobs' do it 'shows a message' do visit admin_builds_path expect(page).to have_selector('.nav-links li.active', text: 'All') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).not_to have_link 'Cancel all' end end end context 'Pending tab' do - context 'when have pending builds' do - it 'shows pending builds' do + context 'when have pending jobs' do + it 'shows pending jobs' do build1 = create(:ci_build, pipeline: pipeline, status: :pending) build2 = create(:ci_build, pipeline: pipeline, status: :running) build3 = create(:ci_build, pipeline: pipeline, status: :success) @@ -55,22 +55,22 @@ describe 'Admin Builds' do end end - context 'when have no builds pending' do + context 'when have no jobs pending' do it 'shows a message' do create(:ci_build, pipeline: pipeline, status: :success) visit admin_builds_path(scope: :pending) expect(page).to have_selector('.nav-links li.active', text: 'Pending') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).not_to have_link 'Cancel all' end end end context 'Running tab' do - context 'when have running builds' do - it 'shows running builds' do + context 'when have running jobs' do + it 'shows running jobs' do build1 = create(:ci_build, pipeline: pipeline, status: :running) build2 = create(:ci_build, pipeline: pipeline, status: :success) build3 = create(:ci_build, pipeline: pipeline, status: :failed) @@ -87,22 +87,22 @@ describe 'Admin Builds' do end end - context 'when have no builds running' do + context 'when have no jobs running' do it 'shows a message' do create(:ci_build, pipeline: pipeline, status: :success) visit admin_builds_path(scope: :running) expect(page).to have_selector('.nav-links li.active', text: 'Running') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).not_to have_link 'Cancel all' end end end context 'Finished tab' do - context 'when have finished builds' do - it 'shows finished builds' do + context 'when have finished jobs' do + it 'shows finished jobs' do build1 = create(:ci_build, pipeline: pipeline, status: :pending) build2 = create(:ci_build, pipeline: pipeline, status: :running) build3 = create(:ci_build, pipeline: pipeline, status: :success) @@ -117,14 +117,14 @@ describe 'Admin Builds' do end end - context 'when have no builds finished' do + context 'when have no jobs finished' do it 'shows a message' do create(:ci_build, pipeline: pipeline, status: :running) visit admin_builds_path(scope: :finished) expect(page).to have_selector('.nav-links li.active', text: 'Finished') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).to have_link 'Cancel all' end end diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb index 7e2907cd26f..d2f5c4afc93 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb @@ -50,7 +50,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: visit_merge_request(merge_request) expect(page).not_to have_button 'Accept Merge Request' - expect(page).to have_content('Please retry the build or push a new commit to fix the failure.') + expect(page).to have_content('Please retry the job or push a new commit to fix the failure.') end end @@ -61,7 +61,7 @@ feature 'Only allow merge requests to be merged if the build succeeds', feature: visit_merge_request(merge_request) expect(page).not_to have_button 'Accept Merge Request' - expect(page).to have_content('Please retry the build or push a new commit to fix the failure.') + expect(page).to have_content('Please retry the job or push a new commit to fix the failure.') end end diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb index 11d27feab0b..f7e0115643e 100644 --- a/spec/features/projects/builds_spec.rb +++ b/spec/features/projects/builds_spec.rb @@ -27,7 +27,7 @@ feature 'Builds', :feature do visit namespace_project_builds_path(project.namespace, project, scope: :pending) end - it "shows Pending tab builds" do + it "shows Pending tab jobs" do expect(page).to have_link 'Cancel running' expect(page).to have_selector('.nav-links li.active', text: 'Pending') expect(page).to have_content build.short_sha @@ -42,7 +42,7 @@ feature 'Builds', :feature do visit namespace_project_builds_path(project.namespace, project, scope: :running) end - it "shows Running tab builds" do + it "shows Running tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Running') expect(page).to have_link 'Cancel running' expect(page).to have_content build.short_sha @@ -57,20 +57,20 @@ feature 'Builds', :feature do visit namespace_project_builds_path(project.namespace, project, scope: :finished) end - it "shows Finished tab builds" do + it "shows Finished tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'Finished') - expect(page).to have_content 'No builds to show' + expect(page).to have_content 'No jobs to show' expect(page).to have_link 'Cancel running' end end - context "All builds" do + context "All jobs" do before do project.builds.running_or_pending.each(&:success) visit namespace_project_builds_path(project.namespace, project) end - it "shows All tab builds" do + it "shows All tab jobs" do expect(page).to have_selector('.nav-links li.active', text: 'All') expect(page).to have_content build.short_sha expect(page).to have_content build.ref @@ -98,7 +98,7 @@ feature 'Builds', :feature do end describe "GET /:project/builds/:id" do - context "Build from project" do + context "Job from project" do before do visit namespace_project_build_path(project.namespace, project, build) end @@ -111,7 +111,7 @@ feature 'Builds', :feature do end end - context "Build from other project" do + context "Job from other project" do before do visit namespace_project_build_path(project.namespace, project, build2) end @@ -149,7 +149,7 @@ feature 'Builds', :feature do context 'when expire date is defined' do let(:expire_at) { Time.now + 7.days } - context 'when user has ability to update build' do + context 'when user has ability to update job' do it 'keeps artifacts when keep button is clicked' do expect(page).to have_content 'The artifacts will be removed' @@ -160,7 +160,7 @@ feature 'Builds', :feature do end end - context 'when user does not have ability to update build' do + context 'when user does not have ability to update job' do let(:user_access_level) { :guest } it 'does not have keep button' do @@ -197,8 +197,8 @@ feature 'Builds', :feature do visit namespace_project_build_path(project.namespace, project, build) end - context 'when build has an initial trace' do - it 'loads build trace' do + context 'when job has an initial trace' do + it 'loads job trace' do expect(page).to have_content 'BUILD TRACE' build.append_trace(' and more trace', 11) @@ -242,32 +242,32 @@ feature 'Builds', :feature do end end - context 'when build starts environment' do + context 'when job starts environment' do let(:environment) { create(:environment, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) } - context 'build is successfull and has deployment' do + context 'job is successfull and has deployment' do let(:deployment) { create(:deployment) } let(:build) { create(:ci_build, :success, environment: environment.name, deployments: [deployment], pipeline: pipeline) } - it 'shows a link for the build' do + it 'shows a link for the job' do visit namespace_project_build_path(project.namespace, project, build) expect(page).to have_link environment.name end end - context 'build is complete and not successfull' do + context 'job is complete and not successfull' do let(:build) { create(:ci_build, :failed, environment: environment.name, pipeline: pipeline) } - it 'shows a link for the build' do + it 'shows a link for the job' do visit namespace_project_build_path(project.namespace, project, build) expect(page).to have_link environment.name end end - context 'build creates a new deployment' do + context 'job creates a new deployment' do let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } @@ -281,7 +281,7 @@ feature 'Builds', :feature do end describe "POST /:project/builds/:id/cancel" do - context "Build from project" do + context "Job from project" do before do build.run! visit namespace_project_build_path(project.namespace, project, build) @@ -295,7 +295,7 @@ feature 'Builds', :feature do end end - context "Build from other project" do + context "Job from other project" do before do build.run! visit namespace_project_build_path(project.namespace, project, build) @@ -307,13 +307,13 @@ feature 'Builds', :feature do end describe "POST /:project/builds/:id/retry" do - context "Build from project" do + context "Job from project" do before do build.run! visit namespace_project_build_path(project.namespace, project, build) click_link 'Cancel' page.within('.build-header') do - click_link 'Retry build' + click_link 'Retry job' end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 917b545e98b..0b5ccc8c515 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -91,10 +91,10 @@ describe 'Pipeline', :feature, :js do end end - it 'should be possible to retry the success build' do + it 'should be possible to retry the success job' do find('#ci-badge-build .ci-action-icon-container').trigger('click') - expect(page).not_to have_content('Retry build') + expect(page).not_to have_content('Retry job') end end @@ -113,11 +113,11 @@ describe 'Pipeline', :feature, :js do it 'should be possible to retry the failed build' do find('#ci-badge-test .ci-action-icon-container').trigger('click') - expect(page).not_to have_content('Retry build') + expect(page).not_to have_content('Retry job') end end - context 'when pipeline has manual builds' do + context 'when pipeline has manual jobs' do it 'shows the skipped icon and a play action for the manual build' do page.within('#ci-badge-manual-build') do expect(page).to have_selector('.js-ci-status-icon-manual') @@ -129,14 +129,14 @@ describe 'Pipeline', :feature, :js do end end - it 'should be possible to play the manual build' do + it 'should be possible to play the manual job' do find('#ci-badge-manual-build .ci-action-icon-container').trigger('click') - expect(page).not_to have_content('Play build') + expect(page).not_to have_content('Play job') end end - context 'when pipeline has external build' do + context 'when pipeline has external job' do it 'shows the success icon and the generic comit status build' do expect(page).to have_selector('.js-ci-status-icon-success') expect(page).to have_content('jenkins') @@ -146,12 +146,12 @@ describe 'Pipeline', :feature, :js do end context 'page tabs' do - it 'shows Pipeline and Builds tabs with link' do + it 'shows Pipeline and Jobs tabs with link' do expect(page).to have_link('Pipeline') - expect(page).to have_link('Builds') + expect(page).to have_link('Jobs') end - it 'shows counter in Builds tab' do + it 'shows counter in Jobs tab' do expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) end @@ -160,7 +160,7 @@ describe 'Pipeline', :feature, :js do end end - context 'retrying builds' do + context 'retrying jobs' do it { expect(page).not_to have_content('retried') } context 'when retrying' do @@ -170,7 +170,7 @@ describe 'Pipeline', :feature, :js do end end - context 'canceling builds' do + context 'canceling jobs' do it { expect(page).not_to have_selector('.ci-canceled') } context 'when canceling' do @@ -191,7 +191,7 @@ describe 'Pipeline', :feature, :js do visit builds_namespace_project_pipeline_path(project.namespace, project, pipeline) end - it 'shows a list of builds' do + it 'shows a list of jobs' do expect(page).to have_content('Test') expect(page).to have_content(build_passed.id) expect(page).to have_content('Deploy') @@ -203,26 +203,26 @@ describe 'Pipeline', :feature, :js do expect(page).to have_link('Play') end - it 'shows Builds tab pane as active' do + it 'shows jobs tab pane as active' do expect(page).to have_css('#js-tab-builds.active') end context 'page tabs' do - it 'shows Pipeline and Builds tabs with link' do + it 'shows Pipeline and Jobs tabs with link' do expect(page).to have_link('Pipeline') - expect(page).to have_link('Builds') + expect(page).to have_link('Jobs') end - it 'shows counter in Builds tab' do + it 'shows counter in Jobs tab' do expect(page.find('.js-builds-counter').text).to eq(pipeline.statuses.count.to_s) end - it 'shows Builds tab as active' do + it 'shows Jobs tab as active' do expect(page).to have_css('li.js-builds-tab-link.active') end end - context 'retrying builds' do + context 'retrying jobs' do it { expect(page).not_to have_content('retried') } context 'when retrying' do @@ -233,7 +233,7 @@ describe 'Pipeline', :feature, :js do end end - context 'canceling builds' do + context 'canceling jobs' do it { expect(page).not_to have_selector('.ci-canceled') } context 'when canceling' do @@ -244,7 +244,7 @@ describe 'Pipeline', :feature, :js do end end - context 'playing manual build' do + context 'playing manual job' do before do within '.pipeline-holder' do click_link('Play') diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb index 4bfaa499272..034b75c2e51 100644 --- a/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -11,41 +11,41 @@ feature 'Project settings > Merge Requests', feature: true, js: true do login_as(user) end - context 'when Merge Request and Builds are initially enabled' do + context 'when Merge Request and Pipelines are initially enabled' do before do project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::ENABLED) end - context 'when Builds are initially enabled' do + context 'when Pipelines are initially enabled' do before do project.project_feature.update_attribute('builds_access_level', ProjectFeature::ENABLED) visit edit_project_path(project) end scenario 'shows the Merge Requests settings' do - expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level" - expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') end end - context 'when Builds are initially disabled' do + context 'when Pipelines are initially disabled' do before do project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED) visit edit_project_path(project) end scenario 'shows the Merge Requests settings that do not depend on Builds feature' do - expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level" - expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') end end @@ -58,12 +58,12 @@ feature 'Project settings > Merge Requests', feature: true, js: true do end scenario 'does not show the Merge Requests settings' do - expect(page).not_to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved') select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level" - expect(page).to have_content('Only allow merge requests to be merged if the build succeeds') + expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds') expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved') end end diff --git a/spec/javascripts/fixtures/environments/table.html.haml b/spec/javascripts/fixtures/environments/table.html.haml index 1ea1725c561..59edc0396d2 100644 --- a/spec/javascripts/fixtures/environments/table.html.haml +++ b/spec/javascripts/fixtures/environments/table.html.haml @@ -3,7 +3,7 @@ %tr %th Environment %th Last deployment - %th Build + %th Job %th Commit %th %th diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index e20b394c525..4080092405d 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -484,11 +484,11 @@ describe Ci::Build, :models do let!(:build) { create(:ci_build, :trace, :success, :artifacts) } subject { build.erased? } - context 'build has not been erased' do + context 'job has not been erased' do it { is_expected.to be_falsey } end - context 'build has been erased' do + context 'job has been erased' do before do build.erase end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index bd6e23ee769..f197fadebab 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -86,7 +86,7 @@ describe API::Builds, api: true do context 'when commit exists in repository' do context 'when user is authorized' do - context 'when pipeline has builds' do + context 'when pipeline has jobs' do before do create(:ci_pipeline, project: project, sha: project.commit.id) create(:ci_build, pipeline: pipeline) @@ -95,7 +95,7 @@ describe API::Builds, api: true do get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", api_user) end - it 'returns project builds for specific commit' do + it 'returns project jobs for specific commit' do expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.size).to eq 2 @@ -111,7 +111,7 @@ describe API::Builds, api: true do end end - context 'when pipeline has no builds' do + context 'when pipeline has no jobs' do before do branch_head = project.commit('feature').id get api("/projects/#{project.id}/repository/commits/#{branch_head}/builds", api_user) @@ -133,7 +133,7 @@ describe API::Builds, api: true do get api("/projects/#{project.id}/repository/commits/#{project.commit.id}/builds", nil) end - it 'does not return project builds' do + it 'does not return project jobs' do expect(response).to have_http_status(401) expect(json_response.except('message')).to be_empty end @@ -147,7 +147,7 @@ describe API::Builds, api: true do end context 'authorized user' do - it 'returns specific build data' do + it 'returns specific job data' do expect(response).to have_http_status(200) expect(json_response['name']).to eq('test') end @@ -165,7 +165,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not return specific build data' do + it 'does not return specific job data' do expect(response).to have_http_status(401) end end @@ -176,7 +176,7 @@ describe API::Builds, api: true do get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) end - context 'build with artifacts' do + context 'job with artifacts' do let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } context 'authorized user' do @@ -185,7 +185,7 @@ describe API::Builds, api: true do 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' } end - it 'returns specific build artifacts' do + it 'returns specific job artifacts' do expect(response).to have_http_status(200) expect(response.headers).to include(download_headers) end @@ -194,13 +194,13 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not return specific build artifacts' do + it 'does not return specific job artifacts' do expect(response).to have_http_status(401) end end end - it 'does not return build artifacts if not uploaded' do + it 'does not return job artifacts if not uploaded' do expect(response).to have_http_status(404) end end @@ -241,7 +241,7 @@ describe API::Builds, api: true do end end - context 'non-existing build' do + context 'non-existing job' do shared_examples 'not found' do it { expect(response).to have_http_status(:not_found) } end @@ -254,7 +254,7 @@ describe API::Builds, api: true do it_behaves_like 'not found' end - context 'has no such build' do + context 'has no such job' do before do get path_for_ref(pipeline.ref, 'NOBUILD') end @@ -263,7 +263,7 @@ describe API::Builds, api: true do end end - context 'find proper build' do + context 'find proper job' do shared_examples 'a valid file' do let(:download_headers) do { 'Content-Transfer-Encoding' => 'binary', @@ -311,7 +311,7 @@ describe API::Builds, api: true do end context 'authorized user' do - it 'returns specific build trace' do + it 'returns specific job trace' do expect(response).to have_http_status(200) expect(response.body).to eq(build.trace) end @@ -320,7 +320,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not return specific build trace' do + it 'does not return specific job trace' do expect(response).to have_http_status(401) end end @@ -333,7 +333,7 @@ describe API::Builds, api: true do context 'authorized user' do context 'user with :update_build persmission' do - it 'cancels running or pending build' do + it 'cancels running or pending job' do expect(response).to have_http_status(201) expect(project.builds.first.status).to eq('canceled') end @@ -342,7 +342,7 @@ describe API::Builds, api: true do context 'user without :update_build permission' do let(:api_user) { reporter.user } - it 'does not cancel build' do + it 'does not cancel job' do expect(response).to have_http_status(403) end end @@ -351,7 +351,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not cancel build' do + it 'does not cancel job' do expect(response).to have_http_status(401) end end @@ -366,7 +366,7 @@ describe API::Builds, api: true do context 'authorized user' do context 'user with :update_build permission' do - it 'retries non-running build' do + it 'retries non-running job' do expect(response).to have_http_status(201) expect(project.builds.first.status).to eq('canceled') expect(json_response['status']).to eq('pending') @@ -376,7 +376,7 @@ describe API::Builds, api: true do context 'user without :update_build permission' do let(:api_user) { reporter.user } - it 'does not retry build' do + it 'does not retry job' do expect(response).to have_http_status(403) end end @@ -385,7 +385,7 @@ describe API::Builds, api: true do context 'unauthorized user' do let(:api_user) { nil } - it 'does not retry build' do + it 'does not retry job' do expect(response).to have_http_status(401) end end @@ -396,23 +396,23 @@ describe API::Builds, api: true do post api("/projects/#{project.id}/builds/#{build.id}/erase", user) end - context 'build is erasable' do + context 'job is erasable' do let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, pipeline: pipeline) } - it 'erases build content' do + it 'erases job content' do expect(response.status).to eq 201 expect(build.trace).to be_empty expect(build.artifacts_file.exists?).to be_falsy expect(build.artifacts_metadata.exists?).to be_falsy end - it 'updates build' do + it 'updates job' do expect(build.reload.erased_at).to be_truthy expect(build.reload.erased_by).to eq user end end - context 'build is not erasable' do + context 'job is not erasable' do let(:build) { create(:ci_build, :trace, project: project, pipeline: pipeline) } it 'responds with forbidden' do @@ -452,20 +452,20 @@ describe API::Builds, api: true do post api("/projects/#{project.id}/builds/#{build.id}/play", user) end - context 'on an playable build' do + context 'on an playable job' do let(:build) { create(:ci_build, :manual, project: project, pipeline: pipeline) } - it 'plays the build' do + it 'plays the job' do expect(response).to have_http_status 200 expect(json_response['user']['id']).to eq(user.id) expect(json_response['id']).to eq(build.id) end end - context 'on a non-playable build' do + context 'on a non-playable job' do it 'returns a status code 400, Bad Request' do expect(response).to have_http_status 400 - expect(response.body).to match("Unplayable Build") + expect(response.body).to match("Unplayable Job") end end end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index 1cedaa4ba63..d85afdeab42 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -288,7 +288,7 @@ describe Ci::API::Builds do expect(build.reload.trace).to eq 'BUILD TRACE' end - context 'build has been erased' do + context 'job has been erased' do let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } it 'responds with forbidden' do diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index 44870cfcfb3..b6f6e7b7a2b 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -15,7 +15,7 @@ describe 'projects/builds/show', :view do allow(view).to receive(:can?).and_return(true) end - describe 'build information in header' do + describe 'job information in header' do let(:build) do create(:ci_build, :success, environment: 'staging') end @@ -28,11 +28,11 @@ describe 'projects/builds/show', :view do expect(rendered).to have_css('.ci-status.ci-success', text: 'passed') end - it 'does not render a link to the build' do + it 'does not render a link to the job' do expect(rendered).not_to have_link('passed') end - it 'shows build id' do + it 'shows job id' do expect(rendered).to have_css('.js-build-id', text: build.id) end @@ -45,8 +45,8 @@ describe 'projects/builds/show', :view do end end - describe 'environment info in build view' do - context 'build with latest deployment' do + describe 'environment info in job view' do + context 'job with latest deployment' do let(:build) do create(:ci_build, :success, environment: 'staging') end @@ -57,7 +57,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'This build is the most recent deployment' + expected_text = 'This job is the most recent deployment' render expect(rendered).to have_css( @@ -65,7 +65,7 @@ describe 'projects/builds/show', :view do end end - context 'build with outdated deployment' do + context 'job with outdated deployment' do let(:build) do create(:ci_build, :success, environment: 'staging', pipeline: pipeline) end @@ -87,7 +87,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'This build is an out-of-date deployment ' \ + expected_text = 'This job is an out-of-date deployment ' \ "to staging.\nView the most recent deployment ##{second_deployment.iid}." render @@ -95,7 +95,7 @@ describe 'projects/builds/show', :view do end end - context 'build failed to deploy' do + context 'job failed to deploy' do let(:build) do create(:ci_build, :failed, environment: 'staging', pipeline: pipeline) end @@ -105,7 +105,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'The deployment of this build to staging did not succeed.' + expected_text = 'The deployment of this job to staging did not succeed.' render expect(rendered).to have_css( @@ -113,7 +113,7 @@ describe 'projects/builds/show', :view do end end - context 'build will deploy' do + context 'job will deploy' do let(:build) do create(:ci_build, :running, environment: 'staging', pipeline: pipeline) end @@ -124,7 +124,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'This build is creating a deployment to staging' + expected_text = 'This job is creating a deployment to staging' render expect(rendered).to have_css( @@ -137,7 +137,7 @@ describe 'projects/builds/show', :view do end it 'shows that deployment will be overwritten' do - expected_text = 'This build is creating a deployment to staging' + expected_text = 'This job is creating a deployment to staging' render expect(rendered).to have_css( @@ -150,7 +150,7 @@ describe 'projects/builds/show', :view do context 'when environment does not exist' do it 'shows deployment message' do - expected_text = 'This build is creating a deployment to staging' + expected_text = 'This job is creating a deployment to staging' render expect(rendered).to have_css( @@ -161,7 +161,7 @@ describe 'projects/builds/show', :view do end end - context 'build that failed to deploy and environment has not been created' do + context 'job that failed to deploy and environment has not been created' do let(:build) do create(:ci_build, :failed, environment: 'staging', pipeline: pipeline) end @@ -171,7 +171,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'The deployment of this build to staging did not succeed' + expected_text = 'The deployment of this job to staging did not succeed' render expect(rendered).to have_css( @@ -179,7 +179,7 @@ describe 'projects/builds/show', :view do end end - context 'build that will deploy and environment has not been created' do + context 'job that will deploy and environment has not been created' do let(:build) do create(:ci_build, :running, environment: 'staging', pipeline: pipeline) end @@ -189,7 +189,7 @@ describe 'projects/builds/show', :view do end it 'shows deployment message' do - expected_text = 'This build is creating a deployment to staging' + expected_text = 'This job is creating a deployment to staging' render expect(rendered).to have_css( @@ -200,7 +200,7 @@ describe 'projects/builds/show', :view do end end - context 'when build is running' do + context 'when job is running' do before do build.run! render @@ -211,7 +211,7 @@ describe 'projects/builds/show', :view do end end - context 'when build is not running' do + context 'when job is not running' do before do build.success! render -- cgit v1.2.1 From 037b4fe939696eebe6295a858470f2661d1e3878 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 27 Jan 2017 22:24:08 +0000 Subject: First iteration Create shared folder for vue common files Update paths Second iteration - refactor main component to be 100% reusable between the 3 tables --- .../javascripts/commit/pipelines_bundle.js.es6 | 61 +++++ .../javascripts/commit/pipelines_service.js.es6 | 77 ++++++ .../javascripts/commit/pipelines_store.js.es6 | 30 +++ .../components/environment_item.js.es6 | 2 +- .../javascripts/vue_common_component/commit.js.es6 | 163 ------------- .../javascripts/vue_pipelines_index/index.js.es6 | 2 +- .../vue_shared/components/commit.js.es6 | 163 +++++++++++++ .../vue_shared/components/pipelines_table.js.es6 | 270 +++++++++++++++++++++ .../vue_shared/vue_resource_interceptor.js.es6 | 10 + app/views/projects/commit/_pipelines_list.haml | 15 -- app/views/projects/commit/pipelines.html.haml | 27 ++- config/application.rb | 1 + .../vue_common_components/commit_spec.js.es6 | 2 +- 13 files changed, 641 insertions(+), 182 deletions(-) create mode 100644 app/assets/javascripts/commit/pipelines_bundle.js.es6 create mode 100644 app/assets/javascripts/commit/pipelines_service.js.es6 create mode 100644 app/assets/javascripts/commit/pipelines_store.js.es6 delete mode 100644 app/assets/javascripts/vue_common_component/commit.js.es6 create mode 100644 app/assets/javascripts/vue_shared/components/commit.js.es6 create mode 100644 app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 create mode 100644 app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 diff --git a/app/assets/javascripts/commit/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines_bundle.js.es6 new file mode 100644 index 00000000000..d2547f0b4a8 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines_bundle.js.es6 @@ -0,0 +1,61 @@ +/* eslint-disable no-new */ +/* global Vue, VueResource */ + +//= require vue +//= require vue-resource +//= require ./pipelines_store +//= require ./pipelines_service +//= require vue_shared/components/commit +//= require vue_shared/vue_resource_interceptor +//= require vue_shared/components/pipelines_table + +/** + * Commits View > Pipelines Tab > Pipelines Table. + * + * Renders Pipelines table in pipelines tab in the commits show view. + * + * Uses `pipelines-table-component` to render Pipelines table with an API call. + * Endpoint is provided in HTML and passed as scope. + * We need a store to make the request and store the received environemnts. + * + * Necessary SVG in the table are provided as props. This should be refactored + * as soon as we have Webpack and can load them directly into JS files. + */ +(() => { + window.gl = window.gl || {}; + gl.Commits = gl.Commits || {}; + + if (gl.Commits.PipelinesTableView) { + gl.Commits.PipelinesTableView.$destroy(true); + } + + gl.Commits.PipelinesTableView = new Vue({ + + el: document.querySelector('#commit-pipeline-table-view'), + + /** + * Accesses the DOM to provide the needed data. + * Returns the necessary props to render `pipelines-table-component` component. + * + * @return {Object} Props for `pipelines-table-component` + */ + data() { + const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; + + return { + scope: pipelinesTableData.pipelinesData, + store: new CommitsPipelineStore(), + service: new PipelinesService(), + svgs: pipelinesTableData, + }; + }, + + components: { + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + }, + + template: ` + <pipelines-table-component :scope='scope' :store='store' :svgs='svgs'></pipelines-table-component> + `, + }); +}); diff --git a/app/assets/javascripts/commit/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines_service.js.es6 new file mode 100644 index 00000000000..7d773e0d361 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines_service.js.es6 @@ -0,0 +1,77 @@ +/* globals Vue */ +/* eslint-disable no-unused-vars, no-param-reassign */ + +/** + * Pipelines service. + * + * Used to fetch the data used to render the pipelines table. + * Used Vue.Resource + */ + +window.gl = window.gl || {}; +gl.pipelines = gl.pipelines || {}; + +class PipelinesService { + constructor(root) { + Vue.http.options.root = root; + + this.pipelines = Vue.resource(root); + + Vue.http.interceptors.push((request, next) => { + // needed in order to not break the tests. + if ($.rails) { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + } + next(); + }); + } + + /** + * Given the root param provided when the class is initialized, will + * make a GET request. + * + * @return {Promise} + */ + all() { + return this.pipelines.get(); + } +} + +gl.pipelines.PipelinesService = PipelinesService; + +// const pageValues = (headers) => { +// const normalized = gl.utils.normalizeHeaders(headers); +// +// const paginationInfo = { +// perPage: +normalized['X-PER-PAGE'], +// page: +normalized['X-PAGE'], +// total: +normalized['X-TOTAL'], +// totalPages: +normalized['X-TOTAL-PAGES'], +// nextPage: +normalized['X-NEXT-PAGE'], +// previousPage: +normalized['X-PREV-PAGE'], +// }; +// +// return paginationInfo; +// }; + +// gl.PipelineStore = class { +// fetchDataLoop(Vue, pageNum, url, apiScope) { +// const goFetch = () => +// this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) +// .then((response) => { +// const pageInfo = pageValues(response.headers); +// this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); +// +// const res = JSON.parse(response.body); +// this.count = Object.assign({}, this.count, res.count); +// this.pipelines = Object.assign([], this.pipelines, res); +// +// this.pageRequest = false; +// }, () => { +// this.pageRequest = false; +// return new Flash('Something went wrong on our end.'); +// }); +// +// goFetch(); +// } +// }; diff --git a/app/assets/javascripts/commit/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines_store.js.es6 new file mode 100644 index 00000000000..bc748bece5d --- /dev/null +++ b/app/assets/javascripts/commit/pipelines_store.js.es6 @@ -0,0 +1,30 @@ +/* global gl, Flash */ +/* eslint-disable no-param-reassign, no-underscore-dangle */ +/*= require vue_realtime_listener/index.js */ + +/** + * Pipelines' Store for commits view. + * + * Used to store the Pipelines rendered in the commit view in the pipelines table. + * + * TODO: take care of timeago instances in here + */ + +(() => { + const CommitPipelineStore = { + state: {}, + + create() { + this.state.pipelines = []; + + return this; + }, + + storePipelines(pipelines = []) { + this.state.pipelines = pipelines; + return pipelines; + }, + }; + + return CommitPipelineStore; +})(); diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 0e6bc3fdb2c..c7cb0913213 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -3,7 +3,7 @@ /*= require timeago */ /*= require lib/utils/text_utility */ -/*= require vue_common_component/commit */ +/*= require vue_shared/components/commit */ /*= require ./environment_actions */ /*= require ./environment_external_url */ /*= require ./environment_stop */ diff --git a/app/assets/javascripts/vue_common_component/commit.js.es6 b/app/assets/javascripts/vue_common_component/commit.js.es6 deleted file mode 100644 index 62a22e39a3b..00000000000 --- a/app/assets/javascripts/vue_common_component/commit.js.es6 +++ /dev/null @@ -1,163 +0,0 @@ -/*= require vue */ -/* global Vue */ -(() => { - window.gl = window.gl || {}; - - window.gl.CommitComponent = Vue.component('commit-component', { - - props: { - /** - * Indicates the existance of a tag. - * Used to render the correct icon, if true will render `fa-tag` icon, - * if false will render `fa-code-fork` icon. - */ - tag: { - type: Boolean, - required: false, - default: false, - }, - - /** - * If provided is used to render the branch name and url. - * Should contain the following properties: - * name - * ref_url - */ - commitRef: { - type: Object, - required: false, - default: () => ({}), - }, - - /** - * Used to link to the commit sha. - */ - commitUrl: { - type: String, - required: false, - default: '', - }, - - /** - * Used to show the commit short sha that links to the commit url. - */ - shortSha: { - type: String, - required: false, - default: '', - }, - - /** - * If provided shows the commit tile. - */ - title: { - type: String, - required: false, - default: '', - }, - - /** - * If provided renders information about the author of the commit. - * When provided should include: - * `avatar_url` to render the avatar icon - * `web_url` to link to user profile - * `username` to render alt and title tags - */ - author: { - type: Object, - required: false, - default: () => ({}), - }, - - commitIconSvg: { - type: String, - required: false, - }, - }, - - computed: { - /** - * Used to verify if all the properties needed to render the commit - * ref section were provided. - * - * TODO: Improve this! Use lodash _.has when we have it. - * - * @returns {Boolean} - */ - hasCommitRef() { - return this.commitRef && this.commitRef.name && this.commitRef.ref_url; - }, - - /** - * Used to verify if all the properties needed to render the commit - * author section were provided. - * - * TODO: Improve this! Use lodash _.has when we have it. - * - * @returns {Boolean} - */ - hasAuthor() { - return this.author && - this.author.avatar_url && - this.author.web_url && - this.author.username; - }, - - /** - * If information about the author is provided will return a string - * to be rendered as the alt attribute of the img tag. - * - * @returns {String} - */ - userImageAltDescription() { - return this.author && - this.author.username ? `${this.author.username}'s avatar` : null; - }, - }, - - template: ` - <div class="branch-commit"> - - <div v-if="hasCommitRef" class="icon-container"> - <i v-if="tag" class="fa fa-tag"></i> - <i v-if="!tag" class="fa fa-code-fork"></i> - </div> - - <a v-if="hasCommitRef" - class="monospace branch-name" - :href="commitRef.ref_url"> - {{commitRef.name}} - </a> - - <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div> - - <a class="commit-id monospace" - :href="commitUrl"> - {{shortSha}} - </a> - - <p class="commit-title"> - <span v-if="title"> - <a v-if="hasAuthor" - class="avatar-image-container" - :href="author.web_url"> - <img - class="avatar has-tooltip s20" - :src="author.avatar_url" - :alt="userImageAltDescription" - :title="author.username" /> - </a> - - <a class="commit-row-message" - :href="commitUrl"> - {{title}} - </a> - </span> - <span v-else> - Cant find HEAD commit for this branch - </span> - </p> - </div> - `, - }); -})(); diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index edd01f17a97..36f861a7d02 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -1,5 +1,5 @@ /* global Vue, VueResource, gl */ -/*= require vue_common_component/commit */ +/*= require vue_shared/components/commit */ /*= require vue_pagination/index */ /*= require vue-resource /*= require boards/vue_resource_interceptor */ diff --git a/app/assets/javascripts/vue_shared/components/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6 new file mode 100644 index 00000000000..62a22e39a3b --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/commit.js.es6 @@ -0,0 +1,163 @@ +/*= require vue */ +/* global Vue */ +(() => { + window.gl = window.gl || {}; + + window.gl.CommitComponent = Vue.component('commit-component', { + + props: { + /** + * Indicates the existance of a tag. + * Used to render the correct icon, if true will render `fa-tag` icon, + * if false will render `fa-code-fork` icon. + */ + tag: { + type: Boolean, + required: false, + default: false, + }, + + /** + * If provided is used to render the branch name and url. + * Should contain the following properties: + * name + * ref_url + */ + commitRef: { + type: Object, + required: false, + default: () => ({}), + }, + + /** + * Used to link to the commit sha. + */ + commitUrl: { + type: String, + required: false, + default: '', + }, + + /** + * Used to show the commit short sha that links to the commit url. + */ + shortSha: { + type: String, + required: false, + default: '', + }, + + /** + * If provided shows the commit tile. + */ + title: { + type: String, + required: false, + default: '', + }, + + /** + * If provided renders information about the author of the commit. + * When provided should include: + * `avatar_url` to render the avatar icon + * `web_url` to link to user profile + * `username` to render alt and title tags + */ + author: { + type: Object, + required: false, + default: () => ({}), + }, + + commitIconSvg: { + type: String, + required: false, + }, + }, + + computed: { + /** + * Used to verify if all the properties needed to render the commit + * ref section were provided. + * + * TODO: Improve this! Use lodash _.has when we have it. + * + * @returns {Boolean} + */ + hasCommitRef() { + return this.commitRef && this.commitRef.name && this.commitRef.ref_url; + }, + + /** + * Used to verify if all the properties needed to render the commit + * author section were provided. + * + * TODO: Improve this! Use lodash _.has when we have it. + * + * @returns {Boolean} + */ + hasAuthor() { + return this.author && + this.author.avatar_url && + this.author.web_url && + this.author.username; + }, + + /** + * If information about the author is provided will return a string + * to be rendered as the alt attribute of the img tag. + * + * @returns {String} + */ + userImageAltDescription() { + return this.author && + this.author.username ? `${this.author.username}'s avatar` : null; + }, + }, + + template: ` + <div class="branch-commit"> + + <div v-if="hasCommitRef" class="icon-container"> + <i v-if="tag" class="fa fa-tag"></i> + <i v-if="!tag" class="fa fa-code-fork"></i> + </div> + + <a v-if="hasCommitRef" + class="monospace branch-name" + :href="commitRef.ref_url"> + {{commitRef.name}} + </a> + + <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div> + + <a class="commit-id monospace" + :href="commitUrl"> + {{shortSha}} + </a> + + <p class="commit-title"> + <span v-if="title"> + <a v-if="hasAuthor" + class="avatar-image-container" + :href="author.web_url"> + <img + class="avatar has-tooltip s20" + :src="author.avatar_url" + :alt="userImageAltDescription" + :title="author.username" /> + </a> + + <a class="commit-row-message" + :href="commitUrl"> + {{title}} + </a> + </span> + <span v-else> + Cant find HEAD commit for this branch + </span> + </p> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 new file mode 100644 index 00000000000..0b20bf66a69 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -0,0 +1,270 @@ +/* eslint-disable no-param-reassign, no-new */ +/* global Vue */ +/* global PipelinesService */ +/* global Flash */ + +//= require vue_pipelines_index/status.js.es6 +//= require vue_pipelines_index/pipeline_url.js.es6 +//= require vue_pipelines_index/stage.js.es6 +//= require vue_pipelines_index/pipeline_actions.js.es6 +//= require vue_pipelines_index/time_ago.js.es6 +//= require vue_pipelines_index/pipelines.js.es6 + +(() => { + window.gl = window.gl || {}; + gl.pipelines = gl.pipelines || {}; + + gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', { + + props: { + + /** + * Stores the Pipelines to render. + * It's passed as a prop to allow different stores to use this Component. + * Different API calls can result in different responses, using a custom + * store allows us to use the same pipeline component. + */ + store: { + type: Object, + required: true, + default: () => ({}), + }, + + /** + * Will be used to fetch the needed data. + * This component is used in different and therefore different API calls + * to different endpoints will be made. To guarantee this is a reusable + * component, the endpoint must be provided. + */ + endpoint: { + type: String, + required: true, + }, + + /** + * Remove this. Find a better way to do this. don't want to provide this 3 times. + */ + svgs: { + type: Object, + required: true, + default: () => ({}), + }, + }, + + components: { + 'commit-component': gl.CommitComponent, + runningPipeline: gl.VueRunningPipeline, + pipelineActions: gl.VuePipelineActions, + 'vue-stage': gl.VueStage, + pipelineUrl: gl.VuePipelineUrl, + pipelineHead: gl.VuePipelineHead, + statusScope: gl.VueStatusScope, + }, + + data() { + return { + state: this.store.state, + isLoading: false, + }; + }, + + computed: { + /** + * If provided, returns the commit tag. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if (this.pipeline && + this.pipeline.commit && + this.pipeline.commit.author) { + return this.pipeline.commit.author; + } + + return undefined; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.model.last_deployment && + this.model.last_deployment.tag) { + return this.model.last_deployment.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.pipeline.ref) { + return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { + if (prop === 'url') { + accumulator.path = this.pipeline.ref[prop]; + } else { + accumulator[prop] = this.pipeline.ref[prop]; + } + return accumulator; + }, {}); + } + + return undefined; + }, + + /** + * If provided, returns the commit url. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.pipeline.commit && + this.pipeline.commit.commit_path) { + return this.pipeline.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.pipeline.commit && + this.pipeline.commit.short_id) { + return this.pipeline.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.pipeline.commit && + this.pipeline.commit.title) { + return this.pipeline.commit.title; + } + return undefined; + }, + + /** + * Figure this out! + */ + author(pipeline) { + if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; + if (pipeline.commit.author) return pipeline.commit.author; + return { + avatar_url: pipeline.commit.author_gravatar_url, + web_url: `mailto:${pipeline.commit.author_email}`, + username: pipeline.commit.author_name, + }; + }, + + /** + * Figure this out + */ + match(string) { + return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); + }, + }, + + /** + * When the component is created the service to fetch the data will be + * initialized with the correct endpoint. + * + * A request to fetch the pipelines will be made. + * In case of a successfull response we will store the data in the provided + * store, in case of a failed response we need to warn the user. + * + */ + created() { + gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); + + this.isLoading = true; + + return gl.pipelines.pipelinesService.all() + .then(resp => resp.json()) + .then((json) => { + this.store.storePipelines(json); + this.isLoading = false; + }).catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the pipelines.', 'alert'); + }); + }, + // this need to be reusable between the 3 tables :/ + template: ` + <div> + <div class="pipelines realtime-loading" v-if='isLoading'> + <i class="fa fa-spinner fa-spin"></i> + </div> + + + <div class="blank-state blank-state-no-icon" + v-if="!isLoading && state.pipelines.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + You don't have any pipelines. + </h2> + Put get started with pipelines button here!!! + </div> + + <div class="table-holder" v-if='!isLoading state.pipelines.length > 0'> + <table class="table ci-table"> + <thead> + <tr> + <th class="pipeline-status">Status</th> + <th class="pipeline-info">Pipeline</th> + <th class="pipeline-commit">Commit</th> + <th class="pipeline-stages">Stages</th> + <th class="pipeline-date"></th> + <th class="pipeline-actions hidden-xs"></th> + </tr> + </thead> + <tbody> + <tr class="commit" v-for='pipeline in state.pipelines'> + <status-scope + :pipeline='pipeline' + :match='match' + :svgs='svgs'> + </status-scope> + + <pipeline-url :pipeline='pipeline'></pipeline-url> + + <td> + <commit-component + :tag="commitTag" + :commit-ref="commitRef" + :commit-url="commitUrl" + :short-sha="commitShortSha" + :title="commitTitle" + :author="commitAuthor" + :commit-icon-svg="commitIconSvg"> + </commit-component> + </td> + + <td class="stage-cell"> + <div class="stage-container dropdown js-mini-pipeline-graph" v-for='stage in pipeline.details.stages'> + <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage> + </div> + </td> + + <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago> + + <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions> + </tr> + </tbody> + </table> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 new file mode 100644 index 00000000000..54c2b4ad369 --- /dev/null +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 @@ -0,0 +1,10 @@ +/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */ +/* global Vue */ + +Vue.http.interceptors.push((request, next) => { + Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; + + next(function (response) { + Vue.activeResources -= 1; + }); +}); diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index 1164627fa11..e69de29bb2d 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -1,15 +0,0 @@ -%div - - if pipelines.blank? - %div - .nothing-here-block No pipelines to show - - else - .table-holder.pipelines - %table.table.ci-table.js-pipeline-table - %thead - %th.pipeline-status Status - %th.pipeline-info Pipeline - %th.pipeline-commit Commit - %th.pipeline-stages Stages - %th.pipeline-date - %th.pipeline-actions - = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index 89968cf4e0d..f4937a03f03 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -2,4 +2,29 @@ = render 'commit_box' = render 'ci_menu' -= render 'pipelines_list', pipelines: @pipelines + +- content_for :page_specific_javascripts do + = page_specific_javascript_tag("commit/pipelines_bundle.js") + +#commit-pipeline-table-view{ data: { pipelines_data: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id)}} +.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), + "icon_status_canceled" => custom_icon("icon_status_canceled"), + "icon_status_running" => custom_icon("icon_status_running"), + "icon_status_skipped" => custom_icon("icon_status_skipped"), + "icon_status_created" => custom_icon("icon_status_created"), + "icon_status_pending" => custom_icon("icon_status_pending"), + "icon_status_success" => custom_icon("icon_status_success"), + "icon_status_failed" => custom_icon("icon_status_failed"), + "icon_status_warning" => custom_icon("icon_status_warning"), + "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), + "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), + "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), + "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), + "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), + "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), + "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), + "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), + "icon_play" => custom_icon("icon_play"), + "icon_timer" => custom_icon("icon_timer"), + "icon_status_manual" => custom_icon("icon_status_manual"), +} } diff --git a/config/application.rb b/config/application.rb index f00e58a36ca..88c5e27d17d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -105,6 +105,7 @@ module Gitlab config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" config.assets.precompile << "boards/test_utils/simulate_drag.js" config.assets.precompile << "environments/environments_bundle.js" + config.assets.precompile << "commit/pipelines_bundle.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js" config.assets.precompile << "terminal/terminal_bundle.js" diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 index d6c6f786fb1..caf84ec63e2 100644 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ b/spec/javascripts/vue_common_components/commit_spec.js.es6 @@ -1,4 +1,4 @@ -//= require vue_common_component/commit +//= require vue_shared/components/commit describe('Commit component', () => { let props; -- cgit v1.2.1 From 9972f59f94ab017d27d9278dd1c9dd89da489e64 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 28 Jan 2017 18:37:02 +0000 Subject: Use single source of truth for vue_resource_interceptor --- app/assets/javascripts/boards/boards_bundle.js.es6 | 2 +- app/assets/javascripts/boards/vue_resource_interceptor.js.es6 | 10 ---------- app/assets/javascripts/vue_pipelines_index/index.js.es6 | 2 +- .../javascripts/vue_shared/components/pipelines_table.js.es6 | 5 ++++- 4 files changed, 6 insertions(+), 13 deletions(-) delete mode 100644 app/assets/javascripts/boards/vue_resource_interceptor.js.es6 diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index f9766471780..5b53cfe59cd 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -13,7 +13,7 @@ //= require ./components/board //= require ./components/board_sidebar //= require ./components/new_list_dropdown -//= require ./vue_resource_interceptor +//= require vue_shared/vue_resource_interceptor $(() => { const $boardApp = document.getElementById('board-app'); diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 deleted file mode 100644 index 54c2b4ad369..00000000000 --- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 +++ /dev/null @@ -1,10 +0,0 @@ -/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */ -/* global Vue */ - -Vue.http.interceptors.push((request, next) => { - Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - - next(function (response) { - Vue.activeResources -= 1; - }); -}); diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index 36f861a7d02..9ca7b1a746c 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -2,7 +2,7 @@ /*= require vue_shared/components/commit */ /*= require vue_pagination/index */ /*= require vue-resource -/*= require boards/vue_resource_interceptor */ +/*= require vue_shared/vue_resource_interceptor */ /*= require ./status.js.es6 */ /*= require ./store.js.es6 */ /*= require ./pipeline_url.js.es6 */ diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index 0b20bf66a69..f602a0c44c2 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -19,10 +19,13 @@ props: { /** - * Stores the Pipelines to render. + * Object used to store the Pipelines to render. * It's passed as a prop to allow different stores to use this Component. * Different API calls can result in different responses, using a custom * store allows us to use the same pipeline component. + * + * Note: All provided stores need to have a `storePipelines` method. + * Find a better way to do this. */ store: { type: Object, -- cgit v1.2.1 From 7ad626e348faaea6f186759dada36079d531f6fd Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 28 Jan 2017 18:39:01 +0000 Subject: Use same folder structure in spec for vue shared resources --- .../commit/pipelines/pipelines_bundle.js.es6 | 61 ++++++++++ .../commit/pipelines/pipelines_service.js.es6 | 77 ++++++++++++ .../commit/pipelines/pipelines_store.js.es6 | 30 +++++ .../javascripts/commit/pipelines_bundle.js.es6 | 61 ---------- .../javascripts/commit/pipelines_service.js.es6 | 77 ------------ .../javascripts/commit/pipelines_store.js.es6 | 30 ----- app/views/projects/commit/pipelines.html.haml | 2 +- config/application.rb | 2 +- .../vue_common_components/commit_spec.js.es6 | 131 --------------------- .../vue_shared/components/commit_spec.js.es6 | 131 +++++++++++++++++++++ 10 files changed, 301 insertions(+), 301 deletions(-) create mode 100644 app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 create mode 100644 app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 create mode 100644 app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 delete mode 100644 app/assets/javascripts/commit/pipelines_bundle.js.es6 delete mode 100644 app/assets/javascripts/commit/pipelines_service.js.es6 delete mode 100644 app/assets/javascripts/commit/pipelines_store.js.es6 delete mode 100644 spec/javascripts/vue_common_components/commit_spec.js.es6 create mode 100644 spec/javascripts/vue_shared/components/commit_spec.js.es6 diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 new file mode 100644 index 00000000000..d2547f0b4a8 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 @@ -0,0 +1,61 @@ +/* eslint-disable no-new */ +/* global Vue, VueResource */ + +//= require vue +//= require vue-resource +//= require ./pipelines_store +//= require ./pipelines_service +//= require vue_shared/components/commit +//= require vue_shared/vue_resource_interceptor +//= require vue_shared/components/pipelines_table + +/** + * Commits View > Pipelines Tab > Pipelines Table. + * + * Renders Pipelines table in pipelines tab in the commits show view. + * + * Uses `pipelines-table-component` to render Pipelines table with an API call. + * Endpoint is provided in HTML and passed as scope. + * We need a store to make the request and store the received environemnts. + * + * Necessary SVG in the table are provided as props. This should be refactored + * as soon as we have Webpack and can load them directly into JS files. + */ +(() => { + window.gl = window.gl || {}; + gl.Commits = gl.Commits || {}; + + if (gl.Commits.PipelinesTableView) { + gl.Commits.PipelinesTableView.$destroy(true); + } + + gl.Commits.PipelinesTableView = new Vue({ + + el: document.querySelector('#commit-pipeline-table-view'), + + /** + * Accesses the DOM to provide the needed data. + * Returns the necessary props to render `pipelines-table-component` component. + * + * @return {Object} Props for `pipelines-table-component` + */ + data() { + const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; + + return { + scope: pipelinesTableData.pipelinesData, + store: new CommitsPipelineStore(), + service: new PipelinesService(), + svgs: pipelinesTableData, + }; + }, + + components: { + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + }, + + template: ` + <pipelines-table-component :scope='scope' :store='store' :svgs='svgs'></pipelines-table-component> + `, + }); +}); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 new file mode 100644 index 00000000000..7d773e0d361 --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 @@ -0,0 +1,77 @@ +/* globals Vue */ +/* eslint-disable no-unused-vars, no-param-reassign */ + +/** + * Pipelines service. + * + * Used to fetch the data used to render the pipelines table. + * Used Vue.Resource + */ + +window.gl = window.gl || {}; +gl.pipelines = gl.pipelines || {}; + +class PipelinesService { + constructor(root) { + Vue.http.options.root = root; + + this.pipelines = Vue.resource(root); + + Vue.http.interceptors.push((request, next) => { + // needed in order to not break the tests. + if ($.rails) { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + } + next(); + }); + } + + /** + * Given the root param provided when the class is initialized, will + * make a GET request. + * + * @return {Promise} + */ + all() { + return this.pipelines.get(); + } +} + +gl.pipelines.PipelinesService = PipelinesService; + +// const pageValues = (headers) => { +// const normalized = gl.utils.normalizeHeaders(headers); +// +// const paginationInfo = { +// perPage: +normalized['X-PER-PAGE'], +// page: +normalized['X-PAGE'], +// total: +normalized['X-TOTAL'], +// totalPages: +normalized['X-TOTAL-PAGES'], +// nextPage: +normalized['X-NEXT-PAGE'], +// previousPage: +normalized['X-PREV-PAGE'], +// }; +// +// return paginationInfo; +// }; + +// gl.PipelineStore = class { +// fetchDataLoop(Vue, pageNum, url, apiScope) { +// const goFetch = () => +// this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) +// .then((response) => { +// const pageInfo = pageValues(response.headers); +// this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); +// +// const res = JSON.parse(response.body); +// this.count = Object.assign({}, this.count, res.count); +// this.pipelines = Object.assign([], this.pipelines, res); +// +// this.pageRequest = false; +// }, () => { +// this.pageRequest = false; +// return new Flash('Something went wrong on our end.'); +// }); +// +// goFetch(); +// } +// }; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 new file mode 100644 index 00000000000..bc748bece5d --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -0,0 +1,30 @@ +/* global gl, Flash */ +/* eslint-disable no-param-reassign, no-underscore-dangle */ +/*= require vue_realtime_listener/index.js */ + +/** + * Pipelines' Store for commits view. + * + * Used to store the Pipelines rendered in the commit view in the pipelines table. + * + * TODO: take care of timeago instances in here + */ + +(() => { + const CommitPipelineStore = { + state: {}, + + create() { + this.state.pipelines = []; + + return this; + }, + + storePipelines(pipelines = []) { + this.state.pipelines = pipelines; + return pipelines; + }, + }; + + return CommitPipelineStore; +})(); diff --git a/app/assets/javascripts/commit/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines_bundle.js.es6 deleted file mode 100644 index d2547f0b4a8..00000000000 --- a/app/assets/javascripts/commit/pipelines_bundle.js.es6 +++ /dev/null @@ -1,61 +0,0 @@ -/* eslint-disable no-new */ -/* global Vue, VueResource */ - -//= require vue -//= require vue-resource -//= require ./pipelines_store -//= require ./pipelines_service -//= require vue_shared/components/commit -//= require vue_shared/vue_resource_interceptor -//= require vue_shared/components/pipelines_table - -/** - * Commits View > Pipelines Tab > Pipelines Table. - * - * Renders Pipelines table in pipelines tab in the commits show view. - * - * Uses `pipelines-table-component` to render Pipelines table with an API call. - * Endpoint is provided in HTML and passed as scope. - * We need a store to make the request and store the received environemnts. - * - * Necessary SVG in the table are provided as props. This should be refactored - * as soon as we have Webpack and can load them directly into JS files. - */ -(() => { - window.gl = window.gl || {}; - gl.Commits = gl.Commits || {}; - - if (gl.Commits.PipelinesTableView) { - gl.Commits.PipelinesTableView.$destroy(true); - } - - gl.Commits.PipelinesTableView = new Vue({ - - el: document.querySelector('#commit-pipeline-table-view'), - - /** - * Accesses the DOM to provide the needed data. - * Returns the necessary props to render `pipelines-table-component` component. - * - * @return {Object} Props for `pipelines-table-component` - */ - data() { - const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; - - return { - scope: pipelinesTableData.pipelinesData, - store: new CommitsPipelineStore(), - service: new PipelinesService(), - svgs: pipelinesTableData, - }; - }, - - components: { - 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, - }, - - template: ` - <pipelines-table-component :scope='scope' :store='store' :svgs='svgs'></pipelines-table-component> - `, - }); -}); diff --git a/app/assets/javascripts/commit/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines_service.js.es6 deleted file mode 100644 index 7d773e0d361..00000000000 --- a/app/assets/javascripts/commit/pipelines_service.js.es6 +++ /dev/null @@ -1,77 +0,0 @@ -/* globals Vue */ -/* eslint-disable no-unused-vars, no-param-reassign */ - -/** - * Pipelines service. - * - * Used to fetch the data used to render the pipelines table. - * Used Vue.Resource - */ - -window.gl = window.gl || {}; -gl.pipelines = gl.pipelines || {}; - -class PipelinesService { - constructor(root) { - Vue.http.options.root = root; - - this.pipelines = Vue.resource(root); - - Vue.http.interceptors.push((request, next) => { - // needed in order to not break the tests. - if ($.rails) { - request.headers['X-CSRF-Token'] = $.rails.csrfToken(); - } - next(); - }); - } - - /** - * Given the root param provided when the class is initialized, will - * make a GET request. - * - * @return {Promise} - */ - all() { - return this.pipelines.get(); - } -} - -gl.pipelines.PipelinesService = PipelinesService; - -// const pageValues = (headers) => { -// const normalized = gl.utils.normalizeHeaders(headers); -// -// const paginationInfo = { -// perPage: +normalized['X-PER-PAGE'], -// page: +normalized['X-PAGE'], -// total: +normalized['X-TOTAL'], -// totalPages: +normalized['X-TOTAL-PAGES'], -// nextPage: +normalized['X-NEXT-PAGE'], -// previousPage: +normalized['X-PREV-PAGE'], -// }; -// -// return paginationInfo; -// }; - -// gl.PipelineStore = class { -// fetchDataLoop(Vue, pageNum, url, apiScope) { -// const goFetch = () => -// this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) -// .then((response) => { -// const pageInfo = pageValues(response.headers); -// this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); -// -// const res = JSON.parse(response.body); -// this.count = Object.assign({}, this.count, res.count); -// this.pipelines = Object.assign([], this.pipelines, res); -// -// this.pageRequest = false; -// }, () => { -// this.pageRequest = false; -// return new Flash('Something went wrong on our end.'); -// }); -// -// goFetch(); -// } -// }; diff --git a/app/assets/javascripts/commit/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines_store.js.es6 deleted file mode 100644 index bc748bece5d..00000000000 --- a/app/assets/javascripts/commit/pipelines_store.js.es6 +++ /dev/null @@ -1,30 +0,0 @@ -/* global gl, Flash */ -/* eslint-disable no-param-reassign, no-underscore-dangle */ -/*= require vue_realtime_listener/index.js */ - -/** - * Pipelines' Store for commits view. - * - * Used to store the Pipelines rendered in the commit view in the pipelines table. - * - * TODO: take care of timeago instances in here - */ - -(() => { - const CommitPipelineStore = { - state: {}, - - create() { - this.state.pipelines = []; - - return this; - }, - - storePipelines(pipelines = []) { - this.state.pipelines = pipelines; - return pipelines; - }, - }; - - return CommitPipelineStore; -})(); diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index f4937a03f03..09bd4288b9c 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -4,7 +4,7 @@ = render 'ci_menu' - content_for :page_specific_javascripts do - = page_specific_javascript_tag("commit/pipelines_bundle.js") + = page_specific_javascript_tag("commit/pipelines/pipelines_bundle.js") #commit-pipeline-table-view{ data: { pipelines_data: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id)}} .pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), diff --git a/config/application.rb b/config/application.rb index 88c5e27d17d..281a660ddee 100644 --- a/config/application.rb +++ b/config/application.rb @@ -105,7 +105,7 @@ module Gitlab config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js" config.assets.precompile << "boards/test_utils/simulate_drag.js" config.assets.precompile << "environments/environments_bundle.js" - config.assets.precompile << "commit/pipelines_bundle.js" + config.assets.precompile << "commit/pipelines/pipelines_bundle.js" config.assets.precompile << "blob_edit/blob_edit_bundle.js" config.assets.precompile << "snippet/snippet_bundle.js" config.assets.precompile << "terminal/terminal_bundle.js" diff --git a/spec/javascripts/vue_common_components/commit_spec.js.es6 b/spec/javascripts/vue_common_components/commit_spec.js.es6 deleted file mode 100644 index caf84ec63e2..00000000000 --- a/spec/javascripts/vue_common_components/commit_spec.js.es6 +++ /dev/null @@ -1,131 +0,0 @@ -//= require vue_shared/components/commit - -describe('Commit component', () => { - let props; - let component; - - it('should render a code-fork icon if it does not represent a tag', () => { - setFixtures('<div class="test-commit-container"></div>'); - component = new window.gl.CommitComponent({ - el: document.querySelector('.test-commit-container'), - propsData: { - tag: false, - commitRef: { - name: 'master', - ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', - }, - commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', - shortSha: 'b7836edd', - title: 'Commit message', - author: { - avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', - web_url: 'https://gitlab.com/jschatz1', - username: 'jschatz1', - }, - }, - }); - - expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork'); - }); - - describe('Given all the props', () => { - beforeEach(() => { - setFixtures('<div class="test-commit-container"></div>'); - - props = { - tag: true, - commitRef: { - name: 'master', - ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', - }, - commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', - shortSha: 'b7836edd', - title: 'Commit message', - author: { - avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', - web_url: 'https://gitlab.com/jschatz1', - username: 'jschatz1', - }, - commitIconSvg: '<svg></svg>', - }; - - component = new window.gl.CommitComponent({ - el: document.querySelector('.test-commit-container'), - propsData: props, - }); - }); - - it('should render a tag icon if it represents a tag', () => { - expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-tag'); - }); - - it('should render a link to the ref url', () => { - expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url); - }); - - it('should render the ref name', () => { - expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name); - }); - - it('should render the commit short sha with a link to the commit url', () => { - expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl); - expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha); - }); - - it('should render the given commitIconSvg', () => { - expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg'); - }); - - describe('Given commit title and author props', () => { - it('should render a link to the author profile', () => { - expect( - component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'), - ).toEqual(props.author.web_url); - }); - - it('Should render the author avatar with title and alt attributes', () => { - expect( - component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title'), - ).toContain(props.author.username); - expect( - component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'), - ).toContain(`${props.author.username}'s avatar`); - }); - }); - - it('should render the commit title', () => { - expect( - component.$el.querySelector('a.commit-row-message').getAttribute('href'), - ).toEqual(props.commitUrl); - expect( - component.$el.querySelector('a.commit-row-message').textContent, - ).toContain(props.title); - }); - }); - - describe('When commit title is not provided', () => { - it('should render default message', () => { - setFixtures('<div class="test-commit-container"></div>'); - props = { - tag: false, - commitRef: { - name: 'master', - ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', - }, - commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', - shortSha: 'b7836edd', - title: null, - author: {}, - }; - - component = new window.gl.CommitComponent({ - el: document.querySelector('.test-commit-container'), - propsData: props, - }); - - expect( - component.$el.querySelector('.commit-title span').textContent, - ).toContain('Cant find HEAD commit for this branch'); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/commit_spec.js.es6 b/spec/javascripts/vue_shared/components/commit_spec.js.es6 new file mode 100644 index 00000000000..caf84ec63e2 --- /dev/null +++ b/spec/javascripts/vue_shared/components/commit_spec.js.es6 @@ -0,0 +1,131 @@ +//= require vue_shared/components/commit + +describe('Commit component', () => { + let props; + let component; + + it('should render a code-fork icon if it does not represent a tag', () => { + setFixtures('<div class="test-commit-container"></div>'); + component = new window.gl.CommitComponent({ + el: document.querySelector('.test-commit-container'), + propsData: { + tag: false, + commitRef: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', + }, + commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', + title: 'Commit message', + author: { + avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', + web_url: 'https://gitlab.com/jschatz1', + username: 'jschatz1', + }, + }, + }); + + expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork'); + }); + + describe('Given all the props', () => { + beforeEach(() => { + setFixtures('<div class="test-commit-container"></div>'); + + props = { + tag: true, + commitRef: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', + }, + commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', + title: 'Commit message', + author: { + avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', + web_url: 'https://gitlab.com/jschatz1', + username: 'jschatz1', + }, + commitIconSvg: '<svg></svg>', + }; + + component = new window.gl.CommitComponent({ + el: document.querySelector('.test-commit-container'), + propsData: props, + }); + }); + + it('should render a tag icon if it represents a tag', () => { + expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-tag'); + }); + + it('should render a link to the ref url', () => { + expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.commitRef.ref_url); + }); + + it('should render the ref name', () => { + expect(component.$el.querySelector('.branch-name').textContent).toContain(props.commitRef.name); + }); + + it('should render the commit short sha with a link to the commit url', () => { + expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commitUrl); + expect(component.$el.querySelector('.commit-id').textContent).toContain(props.shortSha); + }); + + it('should render the given commitIconSvg', () => { + expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg'); + }); + + describe('Given commit title and author props', () => { + it('should render a link to the author profile', () => { + expect( + component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'), + ).toEqual(props.author.web_url); + }); + + it('Should render the author avatar with title and alt attributes', () => { + expect( + component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title'), + ).toContain(props.author.username); + expect( + component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'), + ).toContain(`${props.author.username}'s avatar`); + }); + }); + + it('should render the commit title', () => { + expect( + component.$el.querySelector('a.commit-row-message').getAttribute('href'), + ).toEqual(props.commitUrl); + expect( + component.$el.querySelector('a.commit-row-message').textContent, + ).toContain(props.title); + }); + }); + + describe('When commit title is not provided', () => { + it('should render default message', () => { + setFixtures('<div class="test-commit-container"></div>'); + props = { + tag: false, + commitRef: { + name: 'master', + ref_url: 'http://localhost/namespace2/gitlabhq/tree/master', + }, + commitUrl: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067', + shortSha: 'b7836edd', + title: null, + author: {}, + }; + + component = new window.gl.CommitComponent({ + el: document.querySelector('.test-commit-container'), + propsData: props, + }); + + expect( + component.$el.querySelector('.commit-title span').textContent, + ).toContain('Cant find HEAD commit for this branch'); + }); + }); +}); -- cgit v1.2.1 From 7ef21460d1ad47c1e140b5cf2977ebc90f8c6dd1 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 28 Jan 2017 20:06:15 +0000 Subject: Transform vue_pipelines index into a non-dependent table component. --- .../commit/pipelines/pipelines_bundle.js.es6 | 85 +++++-- .../commit/pipelines/pipelines_service.js.es6 | 45 +--- .../commit/pipelines/pipelines_store.js.es6 | 12 +- .../javascripts/vue_pipelines_index/stage.js.es6 | 1 + .../vue_shared/components/pipelines_table.js.es6 | 277 +++------------------ .../components/pipelines_table_row.js.es6 | 192 ++++++++++++++ app/views/projects/commit/pipelines.html.haml | 6 +- 7 files changed, 306 insertions(+), 312 deletions(-) create mode 100644 app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 index d2547f0b4a8..d42f2d15f19 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 @@ -1,11 +1,10 @@ /* eslint-disable no-new */ -/* global Vue, VueResource */ +/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ +//= require vue +//= require_tree . //= require vue //= require vue-resource -//= require ./pipelines_store -//= require ./pipelines_service -//= require vue_shared/components/commit //= require vue_shared/vue_resource_interceptor //= require vue_shared/components/pipelines_table @@ -21,18 +20,23 @@ * Necessary SVG in the table are provided as props. This should be refactored * as soon as we have Webpack and can load them directly into JS files. */ -(() => { +$(() => { window.gl = window.gl || {}; - gl.Commits = gl.Commits || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; - if (gl.Commits.PipelinesTableView) { - gl.Commits.PipelinesTableView.$destroy(true); + if (gl.commits.PipelinesTableView) { + gl.commits.PipelinesTableView.$destroy(true); } - gl.Commits.PipelinesTableView = new Vue({ + gl.commits.pipelines.PipelinesTableView = new Vue({ el: document.querySelector('#commit-pipeline-table-view'), + components: { + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + }, + /** * Accesses the DOM to provide the needed data. * Returns the necessary props to render `pipelines-table-component` component. @@ -41,21 +45,70 @@ */ data() { const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; + const svgsData = document.querySelector('.pipeline-svgs').dataset; + const store = gl.commits.pipelines.PipelinesStore.create(); + + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = Object.keys(svgsData).reduce((acc, element) => { + acc[element] = svgsData[element]; + return acc; + }, {}); return { - scope: pipelinesTableData.pipelinesData, - store: new CommitsPipelineStore(), - service: new PipelinesService(), - svgs: pipelinesTableData, + endpoint: pipelinesTableData.pipelinesData, + svgs: svgsObject, + store, + state: store.state, + isLoading: false, }; }, - components: { - 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + /** + * When the component is created the service to fetch the data will be + * initialized with the correct endpoint. + * + * A request to fetch the pipelines will be made. + * In case of a successfull response we will store the data in the provided + * store, in case of a failed response we need to warn the user. + * + */ + created() { + gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); + + this.isLoading = true; + + return gl.pipelines.pipelinesService.all() + .then(response => response.json()) + .then((json) => { + this.store.store(json); + this.isLoading = false; + }).catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the pipelines.', 'alert'); + }); }, template: ` - <pipelines-table-component :scope='scope' :store='store' :svgs='svgs'></pipelines-table-component> + <div> + <div class="pipelines realtime-loading" v-if='isLoading'> + <i class="fa fa-spinner fa-spin"></i> + </div> + + <div class="blank-state blank-state-no-icon" + v-if="!isLoading && state.pipelines.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + You don't have any pipelines. + </h2> + Put get started with pipelines button here!!! + </div> + + <div class="table-holder" v-if='!isLoading && state.pipelines.length > 0'> + <pipelines-table-component + :pipelines='state.pipelines' + :svgs='svgs'> + </pipelines-table-component> + </div> + </div> `, }); }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 index 7d773e0d361..1e6aa73d9cf 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 @@ -5,12 +5,8 @@ * Pipelines service. * * Used to fetch the data used to render the pipelines table. - * Used Vue.Resource + * Uses Vue.Resource */ - -window.gl = window.gl || {}; -gl.pipelines = gl.pipelines || {}; - class PipelinesService { constructor(root) { Vue.http.options.root = root; @@ -36,42 +32,3 @@ class PipelinesService { return this.pipelines.get(); } } - -gl.pipelines.PipelinesService = PipelinesService; - -// const pageValues = (headers) => { -// const normalized = gl.utils.normalizeHeaders(headers); -// -// const paginationInfo = { -// perPage: +normalized['X-PER-PAGE'], -// page: +normalized['X-PAGE'], -// total: +normalized['X-TOTAL'], -// totalPages: +normalized['X-TOTAL-PAGES'], -// nextPage: +normalized['X-NEXT-PAGE'], -// previousPage: +normalized['X-PREV-PAGE'], -// }; -// -// return paginationInfo; -// }; - -// gl.PipelineStore = class { -// fetchDataLoop(Vue, pageNum, url, apiScope) { -// const goFetch = () => -// this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) -// .then((response) => { -// const pageInfo = pageValues(response.headers); -// this.pageInfo = Object.assign({}, this.pageInfo, pageInfo); -// -// const res = JSON.parse(response.body); -// this.count = Object.assign({}, this.count, res.count); -// this.pipelines = Object.assign([], this.pipelines, res); -// -// this.pageRequest = false; -// }, () => { -// this.pageRequest = false; -// return new Flash('Something went wrong on our end.'); -// }); -// -// goFetch(); -// } -// }; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 index bc748bece5d..5c2e1b33cd1 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -6,12 +6,14 @@ * Pipelines' Store for commits view. * * Used to store the Pipelines rendered in the commit view in the pipelines table. - * - * TODO: take care of timeago instances in here */ (() => { - const CommitPipelineStore = { + window.gl = window.gl || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; + + gl.commits.pipelines.PipelinesStore = { state: {}, create() { @@ -20,11 +22,9 @@ return this; }, - storePipelines(pipelines = []) { + store(pipelines = []) { this.state.pipelines = pipelines; return pipelines; }, }; - - return CommitPipelineStore; })(); diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index 496df9aaced..572644c8e6e 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -14,6 +14,7 @@ type: Object, required: true, }, + //FIXME: DOMStringMap is non standard, let's use a plain object. svgs: { type: DOMStringMap, required: true, diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index f602a0c44c2..e606632306f 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -1,14 +1,14 @@ -/* eslint-disable no-param-reassign, no-new */ +/* eslint-disable no-param-reassign */ /* global Vue */ -/* global PipelinesService */ -/* global Flash */ -//= require vue_pipelines_index/status.js.es6 -//= require vue_pipelines_index/pipeline_url.js.es6 -//= require vue_pipelines_index/stage.js.es6 -//= require vue_pipelines_index/pipeline_actions.js.es6 -//= require vue_pipelines_index/time_ago.js.es6 -//= require vue_pipelines_index/pipelines.js.es6 +//=require ./pipelines_table_row + +/** + * Pipelines Table Component + * + * Given an array of pipelines, renders a table. + * + */ (() => { window.gl = window.gl || {}; @@ -17,31 +17,10 @@ gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', { props: { - - /** - * Object used to store the Pipelines to render. - * It's passed as a prop to allow different stores to use this Component. - * Different API calls can result in different responses, using a custom - * store allows us to use the same pipeline component. - * - * Note: All provided stores need to have a `storePipelines` method. - * Find a better way to do this. - */ - store: { - type: Object, - required: true, - default: () => ({}), - }, - - /** - * Will be used to fetch the needed data. - * This component is used in different and therefore different API calls - * to different endpoints will be made. To guarantee this is a reusable - * component, the endpoint must be provided. - */ - endpoint: { - type: String, + pipelines: { + type: Array, required: true, + default: [], }, /** @@ -55,219 +34,31 @@ }, components: { - 'commit-component': gl.CommitComponent, - runningPipeline: gl.VueRunningPipeline, - pipelineActions: gl.VuePipelineActions, - 'vue-stage': gl.VueStage, - pipelineUrl: gl.VuePipelineUrl, - pipelineHead: gl.VuePipelineHead, - statusScope: gl.VueStatusScope, + 'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent, }, - data() { - return { - state: this.store.state, - isLoading: false, - }; - }, - - computed: { - /** - * If provided, returns the commit tag. - * - * @returns {Object|Undefined} - */ - commitAuthor() { - if (this.pipeline && - this.pipeline.commit && - this.pipeline.commit.author) { - return this.pipeline.commit.author; - } - - return undefined; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.model.last_deployment && - this.model.last_deployment.tag) { - return this.model.last_deployment.tag; - } - return undefined; - }, - - /** - * If provided, returns the commit ref. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.pipeline.ref) { - return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { - if (prop === 'url') { - accumulator.path = this.pipeline.ref[prop]; - } else { - accumulator[prop] = this.pipeline.ref[prop]; - } - return accumulator; - }, {}); - } - - return undefined; - }, - - /** - * If provided, returns the commit url. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.pipeline.commit && - this.pipeline.commit.commit_path) { - return this.pipeline.commit.commit_path; - } - return undefined; - }, - - /** - * If provided, returns the commit short sha. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.pipeline.commit && - this.pipeline.commit.short_id) { - return this.pipeline.commit.short_id; - } - return undefined; - }, - - /** - * If provided, returns the commit title. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.pipeline.commit && - this.pipeline.commit.title) { - return this.pipeline.commit.title; - } - return undefined; - }, - - /** - * Figure this out! - */ - author(pipeline) { - if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; - if (pipeline.commit.author) return pipeline.commit.author; - return { - avatar_url: pipeline.commit.author_gravatar_url, - web_url: `mailto:${pipeline.commit.author_email}`, - username: pipeline.commit.author_name, - }; - }, - - /** - * Figure this out - */ - match(string) { - return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); - }, - }, - - /** - * When the component is created the service to fetch the data will be - * initialized with the correct endpoint. - * - * A request to fetch the pipelines will be made. - * In case of a successfull response we will store the data in the provided - * store, in case of a failed response we need to warn the user. - * - */ - created() { - gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); - - this.isLoading = true; - - return gl.pipelines.pipelinesService.all() - .then(resp => resp.json()) - .then((json) => { - this.store.storePipelines(json); - this.isLoading = false; - }).catch(() => { - this.isLoading = false; - new Flash('An error occurred while fetching the pipelines.', 'alert'); - }); - }, - // this need to be reusable between the 3 tables :/ template: ` - <div> - <div class="pipelines realtime-loading" v-if='isLoading'> - <i class="fa fa-spinner fa-spin"></i> - </div> - - - <div class="blank-state blank-state-no-icon" - v-if="!isLoading && state.pipelines.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - You don't have any pipelines. - </h2> - Put get started with pipelines button here!!! - </div> - - <div class="table-holder" v-if='!isLoading state.pipelines.length > 0'> - <table class="table ci-table"> - <thead> - <tr> - <th class="pipeline-status">Status</th> - <th class="pipeline-info">Pipeline</th> - <th class="pipeline-commit">Commit</th> - <th class="pipeline-stages">Stages</th> - <th class="pipeline-date"></th> - <th class="pipeline-actions hidden-xs"></th> - </tr> - </thead> - <tbody> - <tr class="commit" v-for='pipeline in state.pipelines'> - <status-scope - :pipeline='pipeline' - :match='match' - :svgs='svgs'> - </status-scope> - - <pipeline-url :pipeline='pipeline'></pipeline-url> - - <td> - <commit-component - :tag="commitTag" - :commit-ref="commitRef" - :commit-url="commitUrl" - :short-sha="commitShortSha" - :title="commitTitle" - :author="commitAuthor" - :commit-icon-svg="commitIconSvg"> - </commit-component> - </td> - - <td class="stage-cell"> - <div class="stage-container dropdown js-mini-pipeline-graph" v-for='stage in pipeline.details.stages'> - <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage> - </div> - </td> - - <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago> - - <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions> - </tr> - </tbody> - </table> - </div> - </div> + <table class="table ci-table"> + <thead> + <tr> + <th class="pipeline-status">Status</th> + <th class="pipeline-info">Pipeline</th> + <th class="pipeline-commit">Commit</th> + <th class="pipeline-stages">Stages</th> + <th class="pipeline-date"></th> + <th class="pipeline-actions hidden-xs"></th> + </tr> + </thead> + <tbody> + <template v-for="model in pipelines" + v-bind:model="model"> + <tr + is="pipelines-table-row-component" + :pipeline="model" + :svgs="svgs"></tr> + </template> + </tbody> + </table> `, }); })(); diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 new file mode 100644 index 00000000000..1e55cce1c41 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -0,0 +1,192 @@ +/* eslint-disable no-param-reassign */ +/* global Vue */ + +//= require vue_pipelines_index/status +//= require vue_pipelines_index/pipeline_url +//= require vue_pipelines_index/stage +//= require vue_shared/components/commit +//= require vue_pipelines_index/pipeline_actions +//= require vue_pipelines_index/time_ago +(() => { + window.gl = window.gl || {}; + gl.pipelines = gl.pipelines || {}; + + gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', { + + props: { + pipeline: { + type: Object, + required: true, + default: () => ({}), + }, + + /** + * Remove this. Find a better way to do this. don't want to provide this 3 times. + */ + svgs: { + type: Object, + required: true, + default: () => ({}), + }, + }, + + components: { + 'commit-component': gl.CommitComponent, + runningPipeline: gl.VueRunningPipeline, + pipelineActions: gl.VuePipelineActions, + 'vue-stage': gl.VueStage, + pipelineUrl: gl.VuePipelineUrl, + pipelineHead: gl.VuePipelineHead, + statusScope: gl.VueStatusScope, + }, + + computed: { + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if (this.pipeline && + this.pipeline.commit && + this.pipeline.commit.author) { + return this.pipeline.commit.author; + } + + return undefined; + }, + + /** + * If provided, returns the commit tag. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTag() { + // if (this.model.last_deployment && + // this.model.last_deployment.tag) { + // return this.model.last_deployment.tag; + // } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * Needed to render the commit component column. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.pipeline.ref) { + return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { + if (prop === 'url') { + accumulator.path = this.pipeline.ref[prop]; + } else { + accumulator[prop] = this.pipeline.ref[prop]; + } + return accumulator; + }, {}); + } + + return undefined; + }, + + /** + * If provided, returns the commit url. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitUrl() { + if (this.pipeline.commit && + this.pipeline.commit.commit_path) { + return this.pipeline.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if (this.pipeline.commit && + this.pipeline.commit.short_id) { + return this.pipeline.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * Needed to render the commit component column. + * + * @returns {String|Undefined} + */ + commitTitle() { + if (this.pipeline.commit && + this.pipeline.commit.title) { + return this.pipeline.commit.title; + } + return undefined; + }, + + /** + * Figure this out! + * Needed to render the commit component column. + */ + author(pipeline) { + if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; + if (pipeline.commit.author) return pipeline.commit.author; + return { + avatar_url: pipeline.commit.author_gravatar_url, + web_url: `mailto:${pipeline.commit.author_email}`, + username: pipeline.commit.author_name, + }; + }, + }, + + methods: { + match(string) { + return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); + }, + }, + + template: ` + <tr class="commit"> + <status-scope + :pipeline='pipeline' + :svgs='svgs' + :match="match"> + </status-scope> + + <pipeline-url :pipeline='pipeline'></pipeline-url> + + <td> + <commit-component + :tag="commitTag" + :commit-ref="commitRef" + :commit-url="commitUrl" + :short-sha="commitShortSha" + :title="commitTitle" + :author="commitAuthor" + :commit-icon-svg="svgs.commitIconSvg"> + </commit-component> + </td> + + <td class="stage-cell"> + <div class="stage-container dropdown js-mini-pipeline-graph" v-for='stage in pipeline.details.stages'> + <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage> + </div> + </td> + + <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago> + + <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions> + </tr> + `, + }); +})(); diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index 09bd4288b9c..f62fbe4d9cd 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -3,9 +3,6 @@ = render 'commit_box' = render 'ci_menu' -- content_for :page_specific_javascripts do - = page_specific_javascript_tag("commit/pipelines/pipelines_bundle.js") - #commit-pipeline-table-view{ data: { pipelines_data: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id)}} .pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), "icon_status_canceled" => custom_icon("icon_status_canceled"), @@ -28,3 +25,6 @@ "icon_timer" => custom_icon("icon_timer"), "icon_status_manual" => custom_icon("icon_status_manual"), } } + +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('commit/pipelines/pipelines_bundle.js') -- cgit v1.2.1 From 2c2da2c07b775f1677456376d311560f1e43226f Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 28 Jan 2017 21:26:04 +0000 Subject: Use new vue js pipelines table to render in merge request view Remove duplicate data-toggle attributes. Reuse the same pipeline table Remove unneeded required resources Remove unused file; Fix mr pipelines loading Updates documentation --- .../commit/pipelines/pipelines_bundle.js.es6 | 17 +++-- .../commit/pipelines/pipelines_store.js.es6 | 4 - app/assets/javascripts/dispatcher.js.es6 | 5 -- app/assets/javascripts/merge_request_tabs.js.es6 | 24 ------ .../javascripts/vue_pipelines_index/index.js.es6 | 43 +++++------ .../vue_pipelines_index/pipeline_actions.js.es6 | 12 +-- .../vue_pipelines_index/pipelines.js.es6 | 87 +++------------------- .../javascripts/vue_pipelines_index/stage.js.es6 | 3 +- .../javascripts/vue_pipelines_index/stages.js.es6 | 21 ------ .../vue_shared/components/pipelines_table.js.es6 | 2 +- .../components/pipelines_table_row.js.es6 | 55 +++++++++----- .../projects/merge_requests_controller.rb | 11 +-- app/views/projects/commit/_pipelines_list.haml | 25 +++++++ app/views/projects/commit/pipelines.html.haml | 27 +------ .../projects/merge_requests/_new_submit.html.haml | 6 +- app/views/projects/merge_requests/_show.html.haml | 3 +- .../merge_requests/show/_pipelines.html.haml | 2 +- 17 files changed, 124 insertions(+), 223 deletions(-) delete mode 100644 app/assets/javascripts/vue_pipelines_index/stages.js.es6 diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 index d42f2d15f19..a06aad17824 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 @@ -1,4 +1,4 @@ -/* eslint-disable no-new */ +/* eslint-disable no-new, no-param-reassign */ /* global Vue, CommitsPipelineStore, PipelinesService, Flash */ //= require vue @@ -10,8 +10,10 @@ /** * Commits View > Pipelines Tab > Pipelines Table. + * Merge Request View > Pipelines Tab > Pipelines Table. * * Renders Pipelines table in pipelines tab in the commits show view. + * Renders Pipelines table in pipelines tab in the merge request show view. * * Uses `pipelines-table-component` to render Pipelines table with an API call. * Endpoint is provided in HTML and passed as scope. @@ -20,6 +22,7 @@ * Necessary SVG in the table are provided as props. This should be refactored * as soon as we have Webpack and can load them directly into JS files. */ + $(() => { window.gl = window.gl || {}; gl.commits = gl.commits || {}; @@ -55,11 +58,12 @@ $(() => { }, {}); return { - endpoint: pipelinesTableData.pipelinesData, + endpoint: pipelinesTableData.endpoint, svgs: svgsObject, store, state: store.state, isLoading: false, + error: false, }; }, @@ -82,7 +86,9 @@ $(() => { .then((json) => { this.store.store(json); this.isLoading = false; + this.error = false; }).catch(() => { + this.error = true; this.isLoading = false; new Flash('An error occurred while fetching the pipelines.', 'alert'); }); @@ -95,14 +101,15 @@ $(() => { </div> <div class="blank-state blank-state-no-icon" - v-if="!isLoading && state.pipelines.length === 0"> + v-if="!isLoading && !error && state.pipelines.length === 0"> <h2 class="blank-state-title js-blank-state-title"> You don't have any pipelines. </h2> - Put get started with pipelines button here!!! </div> - <div class="table-holder" v-if='!isLoading && state.pipelines.length > 0'> + <div + class="table-holder pipelines" + v-if='!isLoading && state.pipelines.length > 0'> <pipelines-table-component :pipelines='state.pipelines' :svgs='svgs'> diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 index 5c2e1b33cd1..b7d8e97fed3 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -1,7 +1,3 @@ -/* global gl, Flash */ -/* eslint-disable no-param-reassign, no-underscore-dangle */ -/*= require vue_realtime_listener/index.js */ - /** * Pipelines' Store for commits view. * diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6 index edec21e3b63..70f467d608f 100644 --- a/app/assets/javascripts/dispatcher.js.es6 +++ b/app/assets/javascripts/dispatcher.js.es6 @@ -159,11 +159,6 @@ new ZenMode(); shortcut_handler = new ShortcutsNavigation(); break; - case 'projects:commit:pipelines': - new gl.MiniPipelineGraph({ - container: '.js-pipeline-table', - }); - break; case 'projects:commits:show': case 'projects:activity': shortcut_handler = new ShortcutsNavigation(); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 4c8c28af755..dabba9e1fa9 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -61,7 +61,6 @@ constructor({ action, setUrl, stubLocation } = {}) { this.diffsLoaded = false; - this.pipelinesLoaded = false; this.commitsLoaded = false; this.fixedLayoutPref = null; @@ -116,10 +115,6 @@ $.scrollTo('.merge-request-details .merge-request-tabs', { offset: -navBarHeight, }); - } else if (action === 'pipelines') { - this.loadPipelines($target.attr('href')); - this.expandView(); - this.resetViewContainer(); } else { this.expandView(); this.resetViewContainer(); @@ -243,25 +238,6 @@ }); } - loadPipelines(source) { - if (this.pipelinesLoaded) { - return; - } - this.ajaxGet({ - url: `${source}.json`, - success: (data) => { - $('#pipelines').html(data.html); - gl.utils.localTimeAgo($('.js-timeago', '#pipelines')); - this.pipelinesLoaded = true; - this.scrollToElement('#pipelines'); - - new gl.MiniPipelineGraph({ - container: '.js-pipeline-table', - }); - }, - }); - } - // Show or hide the loading spinner // // status - Boolean, true to show, false to hide diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index 9ca7b1a746c..e5359ba5398 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -1,31 +1,32 @@ +/* eslint-disable no-param-reassign */ /* global Vue, VueResource, gl */ -/*= require vue_shared/components/commit */ -/*= require vue_pagination/index */ + +//= require vue /*= require vue-resource /*= require vue_shared/vue_resource_interceptor */ -/*= require ./status.js.es6 */ -/*= require ./store.js.es6 */ -/*= require ./pipeline_url.js.es6 */ -/*= require ./stage.js.es6 */ -/*= require ./stages.js.es6 */ -/*= require ./pipeline_actions.js.es6 */ -/*= require ./time_ago.js.es6 */ /*= require ./pipelines.js.es6 */ -(() => { - const project = document.querySelector('.pipelines'); - const entry = document.querySelector('.vue-pipelines-index'); - const svgs = document.querySelector('.pipeline-svgs'); - +$(() => { Vue.use(VueResource); - if (!entry) return null; return new Vue({ - el: entry, - data: { - scope: project.dataset.url, - store: new gl.PipelineStore(), - svgs: svgs.dataset, + el: document.querySelector('.vue-pipelines-index'), + + data() { + const project = document.querySelector('.pipelines'); + const svgs = document.querySelector('.pipeline-svgs').dataset; + + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = Object.keys(svgs).reduce((acc, element) => { + acc[element] = svgs[element]; + return acc; + }, {}); + + return { + scope: project.dataset.url, + store: new gl.PipelineStore(), + svgs: svgsObject, + }; }, components: { 'vue-pipelines': gl.VuePipelines, @@ -39,4 +40,4 @@ </vue-pipelines> `, }); -})(); +}); diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index a7176e27ea1..9b4897b1a9e 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -25,7 +25,6 @@ <button v-if='actions' class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions" - data-toggle="dropdown" title="Manual build" data-placement="top" aria-label="Manual build" @@ -50,7 +49,6 @@ <button v-if='artifacts' class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" - data-toggle="dropdown" title="Artifacts" data-placement="top" aria-label="Artifacts" @@ -72,7 +70,7 @@ </div> </div> <div class="cancel-retry-btns inline"> - <a + <button v-if='pipeline.flags.retryable' class="btn has-tooltip" title="Retry" @@ -81,11 +79,10 @@ data-placement="top" data-toggle="dropdown" :href='pipeline.retry_path' - aria-label="Retry" - > + aria-label="Retry"> <i class="fa fa-repeat" aria-hidden="true"></i> </a> - <a + <button v-if='pipeline.flags.cancelable' class="btn btn-remove has-tooltip" title="Cancel" @@ -94,8 +91,7 @@ data-placement="top" data-toggle="dropdown" :href='pipeline.cancel_path' - aria-label="Cancel" - > + aria-label="Cancel"> <i class="fa fa-remove" aria-hidden="true"></i> </a> </div> diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index b2ed05503c9..34d93ce1b7f 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -1,19 +1,18 @@ /* global Vue, Turbolinks, gl */ /* eslint-disable no-param-reassign */ +//= require vue_pagination/index +//= require ./store.js.es6 +//= require vue_shared/components/pipelines_table + ((gl) => { gl.VuePipelines = Vue.extend({ + components: { - runningPipeline: gl.VueRunningPipeline, - pipelineActions: gl.VuePipelineActions, - stages: gl.VueStages, - commit: gl.CommitComponent, - pipelineUrl: gl.VuePipelineUrl, - pipelineHead: gl.VuePipelineHead, glPagination: gl.VueGlPagination, - statusScope: gl.VueStatusScope, - timeAgo: gl.VueTimeAgo, + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, }, + data() { return { pipelines: [], @@ -38,31 +37,6 @@ change(pagenum, apiScope) { Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); }, - author(pipeline) { - if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; - if (pipeline.commit.author) return pipeline.commit.author; - return { - avatar_url: pipeline.commit.author_gravatar_url, - web_url: `mailto:${pipeline.commit.author_email}`, - username: pipeline.commit.author_name, - }; - }, - ref(pipeline) { - const { ref } = pipeline; - return { name: ref.name, tag: ref.tag, ref_url: ref.path }; - }, - commitTitle(pipeline) { - return pipeline.commit ? pipeline.commit.title : ''; - }, - commitSha(pipeline) { - return pipeline.commit ? pipeline.commit.short_id : ''; - }, - commitUrl(pipeline) { - return pipeline.commit ? pipeline.commit.commit_path : ''; - }, - match(string) { - return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); - }, }, template: ` <div> @@ -70,49 +44,10 @@ <i class="fa fa-spinner fa-spin"></i> </div> <div class="table-holder" v-if='pipelines.length'> - <table class="table ci-table"> - <thead> - <tr> - <th class="pipeline-status">Status</th> - <th class="pipeline-info">Pipeline</th> - <th class="pipeline-commit">Commit</th> - <th class="pipeline-stages">Stages</th> - <th class="pipeline-date"></th> - <th class="pipeline-actions hidden-xs"></th> - </tr> - </thead> - <tbody> - <tr class="commit" v-for='pipeline in pipelines'> - <status-scope - :pipeline='pipeline' - :match='match' - :svgs='svgs' - > - </status-scope> - <pipeline-url :pipeline='pipeline'></pipeline-url> - <td> - <commit - :commit-icon-svg='svgs.commitIconSvg' - :author='author(pipeline)' - :tag="pipeline.ref.tag" - :title='commitTitle(pipeline)' - :commit-ref='ref(pipeline)' - :short-sha='commitSha(pipeline)' - :commit-url='commitUrl(pipeline)' - > - </commit> - </td> - <stages - :pipeline='pipeline' - :svgs='svgs' - :match='match' - > - </stages> - <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago> - <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions> - </tr> - </tbody> - </table> + <pipelines-table-component + :pipelines='pipelines' + :svgs='svgs'> + </pipelines-table-component> </div> <div class="pipelines realtime-loading" v-if='pageRequest'> <i class="fa fa-spinner fa-spin"></i> diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 index 572644c8e6e..8cc417a9966 100644 --- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6 @@ -14,9 +14,8 @@ type: Object, required: true, }, - //FIXME: DOMStringMap is non standard, let's use a plain object. svgs: { - type: DOMStringMap, + type: Object, required: true, }, match: { diff --git a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 b/app/assets/javascripts/vue_pipelines_index/stages.js.es6 deleted file mode 100644 index cb176b3f0c6..00000000000 --- a/app/assets/javascripts/vue_pipelines_index/stages.js.es6 +++ /dev/null @@ -1,21 +0,0 @@ -/* global Vue, gl */ -/* eslint-disable no-param-reassign */ - -((gl) => { - gl.VueStages = Vue.extend({ - components: { - 'vue-stage': gl.VueStage, - }, - props: ['pipeline', 'svgs', 'match'], - template: ` - <td class="stage-cell"> - <div - class="stage-container dropdown js-mini-pipeline-graph" - v-for='stage in pipeline.details.stages' - > - <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage> - </div> - </td> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index e606632306f..4b6bba461d7 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -1,7 +1,7 @@ /* eslint-disable no-param-reassign */ /* global Vue */ -//=require ./pipelines_table_row +//= require ./pipelines_table_row /** * Pipelines Table Component diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 index 1e55cce1c41..c0ff0c90e4e 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -38,6 +38,7 @@ pipelineUrl: gl.VuePipelineUrl, pipelineHead: gl.VuePipelineHead, statusScope: gl.VueStatusScope, + 'time-ago': gl.VueTimeAgo, }, computed: { @@ -45,18 +46,50 @@ * If provided, returns the commit tag. * Needed to render the commit component column. * + * TODO: Document this logic, need to ask @grzesiek and @selfup + * * @returns {Object|Undefined} */ commitAuthor() { + if (!this.pipeline.commit) { + return { avatar_url: '', web_url: '', username: '' }; + } + if (this.pipeline && this.pipeline.commit && this.pipeline.commit.author) { return this.pipeline.commit.author; } + if (this.pipeline && + this.pipeline.commit && + this.pipeline.commit.author_gravatar_url && + this.pipeline.commit.author_name && + this.pipeline.commit.author_email) { + return { + avatar_url: this.pipeline.commit.author_gravatar_url, + web_url: `mailto:${this.pipeline.commit.author_email}`, + username: this.pipeline.commit.author_name, + }; + } + return undefined; }, + /** + * Figure this out! + * Needed to render the commit component column. + */ + author(pipeline) { + if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; + if (pipeline.commit.author) return pipeline.commit.author; + return { + avatar_url: pipeline.commit.author_gravatar_url, + web_url: `mailto:${pipeline.commit.author_email}`, + username: pipeline.commit.author_name, + }; + }, + /** * If provided, returns the commit tag. * Needed to render the commit component column. @@ -64,10 +97,10 @@ * @returns {String|Undefined} */ commitTag() { - // if (this.model.last_deployment && - // this.model.last_deployment.tag) { - // return this.model.last_deployment.tag; - // } + if (this.pipeline.ref && + this.pipeline.ref.tag) { + return this.pipeline.ref.tag; + } return undefined; }, @@ -133,20 +166,6 @@ } return undefined; }, - - /** - * Figure this out! - * Needed to render the commit component column. - */ - author(pipeline) { - if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; - if (pipeline.commit.author) return pipeline.commit.author; - return { - avatar_url: pipeline.commit.author_gravatar_url, - web_url: `mailto:${pipeline.commit.author_email}`, - username: pipeline.commit.author_name, - }; - }, }, methods: { diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 6eb542e4bd8..deb084c2e91 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -216,13 +216,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController end format.json do - render json: { - html: view_to_html_string('projects/merge_requests/show/_pipelines'), - pipelines: PipelineSerializer - .new(project: @project, user: @current_user) - .with_pagination(request, response) - .represent(@pipelines) - } + render json: PipelineSerializer + .new(project: @project, user: @current_user) + .with_pagination(request, response) + .represent(@pipelines) end end end diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index e69de29bb2d..bfe5eb18ad1 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -0,0 +1,25 @@ +#commit-pipeline-table-view{ data: { endpoint: endpoint } } +.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), + "icon_status_canceled" => custom_icon("icon_status_canceled"), + "icon_status_running" => custom_icon("icon_status_running"), + "icon_status_skipped" => custom_icon("icon_status_skipped"), + "icon_status_created" => custom_icon("icon_status_created"), + "icon_status_pending" => custom_icon("icon_status_pending"), + "icon_status_success" => custom_icon("icon_status_success"), + "icon_status_failed" => custom_icon("icon_status_failed"), + "icon_status_warning" => custom_icon("icon_status_warning"), + "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), + "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), + "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), + "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), + "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), + "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), + "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), + "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), + "icon_play" => custom_icon("icon_play"), + "icon_timer" => custom_icon("icon_timer"), + "icon_status_manual" => custom_icon("icon_status_manual"), +} } + +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('commit/pipelines/pipelines_bundle.js') diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml index f62fbe4d9cd..ac93eac41ac 100644 --- a/app/views/projects/commit/pipelines.html.haml +++ b/app/views/projects/commit/pipelines.html.haml @@ -2,29 +2,4 @@ = render 'commit_box' = render 'ci_menu' - -#commit-pipeline-table-view{ data: { pipelines_data: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id)}} -.pipeline-svgs{ data: { "commit_icon_svg" => custom_icon("icon_commit"), - "icon_status_canceled" => custom_icon("icon_status_canceled"), - "icon_status_running" => custom_icon("icon_status_running"), - "icon_status_skipped" => custom_icon("icon_status_skipped"), - "icon_status_created" => custom_icon("icon_status_created"), - "icon_status_pending" => custom_icon("icon_status_pending"), - "icon_status_success" => custom_icon("icon_status_success"), - "icon_status_failed" => custom_icon("icon_status_failed"), - "icon_status_warning" => custom_icon("icon_status_warning"), - "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), - "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), - "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), - "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), - "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), - "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), - "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), - "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), - "icon_play" => custom_icon("icon_play"), - "icon_timer" => custom_icon("icon_timer"), - "icon_status_manual" => custom_icon("icon_status_manual"), -} } - -- content_for :page_specific_javascripts do - = page_specific_javascript_tag('commit/pipelines/pipelines_bundle.js') += render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index d3c013b3f21..c1f48837e0e 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -44,9 +44,9 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane -# This tab is always loaded via AJAX - - if @pipelines.any? - #pipelines.pipelines.tab-pane - = render "projects/merge_requests/show/pipelines" + #pipelines.pipelines.tab-pane + //TODO: This needs to make a new request every time is opened! + = render "projects/merge_requests/show/pipelines", endpoint: link_to url_for(params) .mr-loading-status = spinner diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 9585a9a3ad4..8dfe967a937 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -94,7 +94,8 @@ #commits.commits.tab-pane -# This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - -# This tab is always loaded via AJAX + //TODO: This needs to make a new request every time is opened! + = render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) #diffs.diffs.tab-pane -# This tab is always loaded via AJAX diff --git a/app/views/projects/merge_requests/show/_pipelines.html.haml b/app/views/projects/merge_requests/show/_pipelines.html.haml index afe3f3430c6..cbe534abedb 100644 --- a/app/views/projects/merge_requests/show/_pipelines.html.haml +++ b/app/views/projects/merge_requests/show/_pipelines.html.haml @@ -1 +1 @@ -= render "projects/commit/pipelines_list", pipelines: @pipelines, link_to_commit: true += render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) -- cgit v1.2.1 From 184f60a06f828ccbc9264d40e6daa48d60dca629 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sun, 29 Jan 2017 15:30:04 +0000 Subject: Moves pagination to shared folder Document and remove unused code Declare components in a consistent way; Use " instead of ' to improve consistency; Update documentation; Fix commit author verification to match the use cases; Adds tests for the added components Fix paths in pagination spec Adds tests to pipelines table used in merge requests and commits Use same resource interceptor Fix eslint error --- .../commit/pipelines/pipelines_bundle.js.es6 | 100 +----------- .../commit/pipelines/pipelines_service.js.es6 | 5 + .../commit/pipelines/pipelines_table.js.es6 | 104 +++++++++++++ .../environments/environments_bundle.js.es6 | 2 +- .../environments/vue_resource_interceptor.js.es6 | 12 -- app/assets/javascripts/vue_pagination/index.js.es6 | 148 ------------------ .../vue_pipelines_index/pipelines.js.es6 | 4 +- .../vue_pipelines_index/time_ago.js.es6 | 2 + .../vue_shared/components/pipelines_table.js.es6 | 24 ++- .../components/pipelines_table_row.js.es6 | 102 ++++++++----- .../vue_shared/components/table_pagination.js.es6 | 148 ++++++++++++++++++ .../vue_shared/vue_resource_interceptor.js.es6 | 11 +- app/controllers/projects/commit_controller.rb | 1 - .../projects/merge_requests_controller.rb | 1 - .../projects/merge_requests/_new_submit.html.haml | 6 +- app/views/projects/merge_requests/_show.html.haml | 1 - spec/javascripts/commit/pipelines/mock_data.js.es6 | 90 +++++++++++ .../commit/pipelines/pipelines_spec.js.es6 | 107 +++++++++++++ .../commit/pipelines/pipelines_store_spec.js.es6 | 31 ++++ .../javascripts/fixtures/pipelines_table.html.haml | 2 + .../vue_pagination/pagination_spec.js.es6 | 167 -------------------- .../components/pipelines_table_row_spec.js.es6 | 90 +++++++++++ .../components/pipelines_table_spec.js.es6 | 67 ++++++++ .../components/table_pagination_spec.js.es6 | 168 +++++++++++++++++++++ 24 files changed, 905 insertions(+), 488 deletions(-) create mode 100644 app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 delete mode 100644 app/assets/javascripts/environments/vue_resource_interceptor.js.es6 delete mode 100644 app/assets/javascripts/vue_pagination/index.js.es6 create mode 100644 app/assets/javascripts/vue_shared/components/table_pagination.js.es6 create mode 100644 spec/javascripts/commit/pipelines/mock_data.js.es6 create mode 100644 spec/javascripts/commit/pipelines/pipelines_spec.js.es6 create mode 100644 spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 create mode 100644 spec/javascripts/fixtures/pipelines_table.html.haml delete mode 100644 spec/javascripts/vue_pagination/pagination_spec.js.es6 create mode 100644 spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 create mode 100644 spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 create mode 100644 spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 index a06aad17824..b21d13842a4 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 @@ -3,10 +3,6 @@ //= require vue //= require_tree . -//= require vue -//= require vue-resource -//= require vue_shared/vue_resource_interceptor -//= require vue_shared/components/pipelines_table /** * Commits View > Pipelines Tab > Pipelines Table. @@ -14,13 +10,6 @@ * * Renders Pipelines table in pipelines tab in the commits show view. * Renders Pipelines table in pipelines tab in the merge request show view. - * - * Uses `pipelines-table-component` to render Pipelines table with an API call. - * Endpoint is provided in HTML and passed as scope. - * We need a store to make the request and store the received environemnts. - * - * Necessary SVG in the table are provided as props. This should be refactored - * as soon as we have Webpack and can load them directly into JS files. */ $(() => { @@ -28,94 +17,11 @@ $(() => { gl.commits = gl.commits || {}; gl.commits.pipelines = gl.commits.pipelines || {}; - if (gl.commits.PipelinesTableView) { - gl.commits.PipelinesTableView.$destroy(true); + if (gl.commits.PipelinesTableBundle) { + gl.commits.PipelinesTableBundle.$destroy(true); } - gl.commits.pipelines.PipelinesTableView = new Vue({ - + gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({ el: document.querySelector('#commit-pipeline-table-view'), - - components: { - 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, - }, - - /** - * Accesses the DOM to provide the needed data. - * Returns the necessary props to render `pipelines-table-component` component. - * - * @return {Object} Props for `pipelines-table-component` - */ - data() { - const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; - const svgsData = document.querySelector('.pipeline-svgs').dataset; - const store = gl.commits.pipelines.PipelinesStore.create(); - - // Transform svgs DOMStringMap to a plain Object. - const svgsObject = Object.keys(svgsData).reduce((acc, element) => { - acc[element] = svgsData[element]; - return acc; - }, {}); - - return { - endpoint: pipelinesTableData.endpoint, - svgs: svgsObject, - store, - state: store.state, - isLoading: false, - error: false, - }; - }, - - /** - * When the component is created the service to fetch the data will be - * initialized with the correct endpoint. - * - * A request to fetch the pipelines will be made. - * In case of a successfull response we will store the data in the provided - * store, in case of a failed response we need to warn the user. - * - */ - created() { - gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); - - this.isLoading = true; - - return gl.pipelines.pipelinesService.all() - .then(response => response.json()) - .then((json) => { - this.store.store(json); - this.isLoading = false; - this.error = false; - }).catch(() => { - this.error = true; - this.isLoading = false; - new Flash('An error occurred while fetching the pipelines.', 'alert'); - }); - }, - - template: ` - <div> - <div class="pipelines realtime-loading" v-if='isLoading'> - <i class="fa fa-spinner fa-spin"></i> - </div> - - <div class="blank-state blank-state-no-icon" - v-if="!isLoading && !error && state.pipelines.length === 0"> - <h2 class="blank-state-title js-blank-state-title"> - You don't have any pipelines. - </h2> - </div> - - <div - class="table-holder pipelines" - v-if='!isLoading && state.pipelines.length > 0'> - <pipelines-table-component - :pipelines='state.pipelines' - :svgs='svgs'> - </pipelines-table-component> - </div> - </div> - `, }); }); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 index 1e6aa73d9cf..f4ed986b0c5 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 @@ -32,3 +32,8 @@ class PipelinesService { return this.pipelines.get(); } } + +window.gl = window.gl || {}; +gl.commits = gl.commits || {}; +gl.commits.pipelines = gl.commits.pipelines || {}; +gl.commits.pipelines.PipelinesService = PipelinesService; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 new file mode 100644 index 00000000000..df7a6455eed --- /dev/null +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 @@ -0,0 +1,104 @@ +/* eslint-disable no-new, no-param-reassign */ +/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ + +//= require vue +//= require vue-resource +//= require vue_shared/vue_resource_interceptor +//= require vue_shared/components/pipelines_table + +/** + * + * Uses `pipelines-table-component` to render Pipelines table with an API call. + * Endpoint is provided in HTML and passed as `endpoint`. + * We need a store to store the received environemnts. + * We need a service to communicate with the server. + * + * Necessary SVG in the table are provided as props. This should be refactored + * as soon as we have Webpack and can load them directly into JS files. + */ + +(() => { + window.gl = window.gl || {}; + gl.commits = gl.commits || {}; + gl.commits.pipelines = gl.commits.pipelines || {}; + + gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', { + + components: { + 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, + }, + + /** + * Accesses the DOM to provide the needed data. + * Returns the necessary props to render `pipelines-table-component` component. + * + * @return {Object} + */ + data() { + const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; + const svgsData = document.querySelector('.pipeline-svgs').dataset; + const store = gl.commits.pipelines.PipelinesStore.create(); + + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = Object.keys(svgsData).reduce((acc, element) => { + acc[element] = svgsData[element]; + return acc; + }, {}); + + return { + endpoint: pipelinesTableData.endpoint, + svgs: svgsObject, + store, + state: store.state, + isLoading: false, + }; + }, + + /** + * When the component is created the service to fetch the data will be + * initialized with the correct endpoint. + * + * A request to fetch the pipelines will be made. + * In case of a successfull response we will store the data in the provided + * store, in case of a failed response we need to warn the user. + * + */ + created() { + gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); + + this.isLoading = true; + return gl.pipelines.pipelinesService.all() + .then(response => response.json()) + .then((json) => { + this.store.store(json); + this.isLoading = false; + }).catch(() => { + this.isLoading = false; + new Flash('An error occurred while fetching the pipelines.', 'alert'); + }); + }, + + template: ` + <div> + <div class="pipelines realtime-loading" v-if="isLoading"> + <i class="fa fa-spinner fa-spin"></i> + </div> + + <div class="blank-state blank-state-no-icon" + v-if="!isLoading && state.pipelines.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + No pipelines to show + </h2> + </div> + + <div class="table-holder pipelines" + v-if="!isLoading && state.pipelines.length > 0"> + <pipelines-table-component + :pipelines="state.pipelines" + :svgs="svgs"> + </pipelines-table-component> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6 index 3b003f6f661..cd205617a97 100644 --- a/app/assets/javascripts/environments/environments_bundle.js.es6 +++ b/app/assets/javascripts/environments/environments_bundle.js.es6 @@ -1,7 +1,7 @@ //= require vue //= require_tree ./stores/ //= require ./components/environment -//= require ./vue_resource_interceptor +//= require vue_shared/vue_resource_interceptor $(() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 b/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 deleted file mode 100644 index 406bdbc1c7d..00000000000 --- a/app/assets/javascripts/environments/vue_resource_interceptor.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -/* global Vue */ -Vue.http.interceptors.push((request, next) => { - Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - - next((response) => { - if (typeof response.data === 'string') { - response.data = JSON.parse(response.data); // eslint-disable-line - } - - Vue.activeResources--; // eslint-disable-line - }); -}); diff --git a/app/assets/javascripts/vue_pagination/index.js.es6 b/app/assets/javascripts/vue_pagination/index.js.es6 deleted file mode 100644 index 605824fa939..00000000000 --- a/app/assets/javascripts/vue_pagination/index.js.es6 +++ /dev/null @@ -1,148 +0,0 @@ -/* global Vue, gl */ -/* eslint-disable no-param-reassign, no-plusplus */ - -((gl) => { - const PAGINATION_UI_BUTTON_LIMIT = 4; - const UI_LIMIT = 6; - const SPREAD = '...'; - const PREV = 'Prev'; - const NEXT = 'Next'; - const FIRST = '<< First'; - const LAST = 'Last >>'; - - gl.VueGlPagination = Vue.extend({ - props: { - - /** - This function will take the information given by the pagination component - And make a new Turbolinks call - - Here is an example `change` method: - - change(pagenum, apiScope) { - Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); - }, - */ - - change: { - type: Function, - required: true, - }, - - /** - pageInfo will come from the headers of the API call - in the `.then` clause of the VueResource API call - there should be a function that contructs the pageInfo for this component - - This is an example: - - const pageInfo = headers => ({ - perPage: +headers['X-Per-Page'], - page: +headers['X-Page'], - total: +headers['X-Total'], - totalPages: +headers['X-Total-Pages'], - nextPage: +headers['X-Next-Page'], - previousPage: +headers['X-Prev-Page'], - }); - */ - - pageInfo: { - type: Object, - required: true, - }, - }, - methods: { - changePage(e) { - let apiScope = gl.utils.getParameterByName('scope'); - - if (!apiScope) apiScope = 'all'; - - const text = e.target.innerText; - const { totalPages, nextPage, previousPage } = this.pageInfo; - - switch (text) { - case SPREAD: - break; - case LAST: - this.change(totalPages, apiScope); - break; - case NEXT: - this.change(nextPage, apiScope); - break; - case PREV: - this.change(previousPage, apiScope); - break; - case FIRST: - this.change(1, apiScope); - break; - default: - this.change(+text, apiScope); - break; - } - }, - }, - computed: { - prev() { - return this.pageInfo.previousPage; - }, - next() { - return this.pageInfo.nextPage; - }, - getItems() { - const total = this.pageInfo.totalPages; - const page = this.pageInfo.page; - const items = []; - - if (page > 1) items.push({ title: FIRST }); - - if (page > 1) { - items.push({ title: PREV, prev: true }); - } else { - items.push({ title: PREV, disabled: true, prev: true }); - } - - if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); - - const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); - const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); - - for (let i = start; i <= end; i++) { - const isActive = i === page; - items.push({ title: i, active: isActive, page: true }); - } - - if (total - page > PAGINATION_UI_BUTTON_LIMIT) { - items.push({ title: SPREAD, separator: true, page: true }); - } - - if (page === total) { - items.push({ title: NEXT, disabled: true, next: true }); - } else if (total - page >= 1) { - items.push({ title: NEXT, next: true }); - } - - if (total - page >= 1) items.push({ title: LAST, last: true }); - - return items; - }, - }, - template: ` - <div class="gl-pagination"> - <ul class="pagination clearfix"> - <li v-for='item in getItems' - :class='{ - page: item.page, - prev: item.prev, - next: item.next, - separator: item.separator, - active: item.active, - disabled: item.disabled - }' - > - <a @click="changePage($event)">{{item.title}}</a> - </li> - </ul> - </div> - `, - }); -})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index 34d93ce1b7f..c1daf816060 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -1,7 +1,7 @@ /* global Vue, Turbolinks, gl */ /* eslint-disable no-param-reassign */ -//= require vue_pagination/index +//= require vue_shared/components/table_pagination //= require ./store.js.es6 //= require vue_shared/components/pipelines_table @@ -9,7 +9,7 @@ gl.VuePipelines = Vue.extend({ components: { - glPagination: gl.VueGlPagination, + 'gl-pagination': gl.VueGlPagination, 'pipelines-table-component': gl.pipelines.PipelinesTableComponent, }, diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 index 655110feba1..61417b28630 100644 --- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -1,6 +1,8 @@ /* global Vue, gl */ /* eslint-disable no-param-reassign */ +//= require lib/utils/datetime_utility + ((gl) => { gl.VueTimeAgo = Vue.extend({ data() { diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index 4b6bba461d7..9bc1ea65e53 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -4,10 +4,9 @@ //= require ./pipelines_table_row /** - * Pipelines Table Component - * - * Given an array of pipelines, renders a table. + * Pipelines Table Component. * + * Given an array of objects, renders a table. */ (() => { @@ -20,11 +19,11 @@ pipelines: { type: Array, required: true, - default: [], + default: () => ([]), }, /** - * Remove this. Find a better way to do this. don't want to provide this 3 times. + * TODO: Remove this when we have webpack. */ svgs: { type: Object, @@ -41,19 +40,18 @@ <table class="table ci-table"> <thead> <tr> - <th class="pipeline-status">Status</th> - <th class="pipeline-info">Pipeline</th> - <th class="pipeline-commit">Commit</th> - <th class="pipeline-stages">Stages</th> - <th class="pipeline-date"></th> - <th class="pipeline-actions hidden-xs"></th> + <th class="js-pipeline-status pipeline-status">Status</th> + <th class="js-pipeline-info pipeline-info">Pipeline</th> + <th class="js-pipeline-commit pipeline-commit">Commit</th> + <th class="js-pipeline-stages pipeline-stages">Stages</th> + <th class="js-pipeline-date pipeline-date"></th> + <th class="js-pipeline-actions pipeline-actions hidden-xs"></th> </tr> </thead> <tbody> <template v-for="model in pipelines" v-bind:model="model"> - <tr - is="pipelines-table-row-component" + <tr is="pipelines-table-row-component" :pipeline="model" :svgs="svgs"></tr> </template> diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 index c0ff0c90e4e..375516e3804 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -7,6 +7,12 @@ //= require vue_shared/components/commit //= require vue_pipelines_index/pipeline_actions //= require vue_pipelines_index/time_ago + +/** + * Pipeline table row. + * + * Given the received object renders a table row in the pipelines' table. + */ (() => { window.gl = window.gl || {}; gl.pipelines = gl.pipelines || {}; @@ -21,7 +27,7 @@ }, /** - * Remove this. Find a better way to do this. don't want to provide this 3 times. + * TODO: Remove this when we have webpack; */ svgs: { type: Object, @@ -32,12 +38,10 @@ components: { 'commit-component': gl.CommitComponent, - runningPipeline: gl.VueRunningPipeline, - pipelineActions: gl.VuePipelineActions, - 'vue-stage': gl.VueStage, - pipelineUrl: gl.VuePipelineUrl, - pipelineHead: gl.VuePipelineHead, - statusScope: gl.VueStatusScope, + 'pipeline-actions': gl.VuePipelineActions, + 'dropdown-stage': gl.VueStage, + 'pipeline-url': gl.VuePipelineUrl, + 'status-scope': gl.VueStatusScope, 'time-ago': gl.VueTimeAgo, }, @@ -46,48 +50,48 @@ * If provided, returns the commit tag. * Needed to render the commit component column. * - * TODO: Document this logic, need to ask @grzesiek and @selfup + * This field needs a lot of verification, because of different possible cases: + * + * 1. person who is an author of a commit might be a GitLab user + * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar + * 3. If GitLab user does not have avatar he/she might have a Gravatar + * 4. If committer is not a GitLab User he/she can have a Gravatar + * 5. We do not have consistent API object in this case + * 6. We should improve API and the code * * @returns {Object|Undefined} */ commitAuthor() { - if (!this.pipeline.commit) { - return { avatar_url: '', web_url: '', username: '' }; - } + let commitAuthorInformation; + // 1. person who is an author of a commit might be a GitLab user if (this.pipeline && this.pipeline.commit && this.pipeline.commit.author) { - return this.pipeline.commit.author; + // 2. if person who is an author of a commit is a GitLab user + // he/she can have a GitLab avatar + if (this.pipeline.commit.author.avatar_url) { + commitAuthorInformation = this.pipeline.commit.author; + + // 3. If GitLab user does not have avatar he/she might have a Gravatar + } else if (this.pipeline.commit.author_gravatar_url) { + commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, { + avatar_url: this.pipeline.commit.author_gravatar_url, + }); + } } + // 4. If committer is not a GitLab User he/she can have a Gravatar if (this.pipeline && - this.pipeline.commit && - this.pipeline.commit.author_gravatar_url && - this.pipeline.commit.author_name && - this.pipeline.commit.author_email) { - return { + this.pipeline.commit) { + commitAuthorInformation = { avatar_url: this.pipeline.commit.author_gravatar_url, web_url: `mailto:${this.pipeline.commit.author_email}`, username: this.pipeline.commit.author_name, }; } - return undefined; - }, - - /** - * Figure this out! - * Needed to render the commit component column. - */ - author(pipeline) { - if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' }; - if (pipeline.commit.author) return pipeline.commit.author; - return { - avatar_url: pipeline.commit.author_gravatar_url, - web_url: `mailto:${pipeline.commit.author_email}`, - username: pipeline.commit.author_name, - }; + return commitAuthorInformation; }, /** @@ -108,6 +112,9 @@ * If provided, returns the commit ref. * Needed to render the commit component column. * + * Matched `url` prop sent in the API to `path` prop needed + * in the commit component. + * * @returns {Object|Undefined} */ commitRef() { @@ -169,6 +176,17 @@ }, methods: { + /** + * FIXME: This should not be in this component but in the components that + * need this function. + * + * Used to render SVGs in the following components: + * - status-scope + * - dropdown-stage + * + * @param {String} string + * @return {String} + */ match(string) { return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase()); }, @@ -177,12 +195,12 @@ template: ` <tr class="commit"> <status-scope - :pipeline='pipeline' - :svgs='svgs' + :pipeline="pipeline" + :svgs="svgs" :match="match"> </status-scope> - <pipeline-url :pipeline='pipeline'></pipeline-url> + <pipeline-url :pipeline="pipeline"></pipeline-url> <td> <commit-component @@ -197,14 +215,20 @@ </td> <td class="stage-cell"> - <div class="stage-container dropdown js-mini-pipeline-graph" v-for='stage in pipeline.details.stages'> - <vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage> + <div class="stage-container dropdown js-mini-pipeline-graph" + v-if="pipeline.details.stages.length > 0" + v-for="stage in pipeline.details.stages"> + <dropdown-stage + :stage="stage" + :svgs="svgs" + :match="match"> + </dropdown-stage> </div> </td> - <time-ago :pipeline='pipeline' :svgs='svgs'></time-ago> + <time-ago :pipeline="pipeline" :svgs="svgs"></time-ago> - <pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions> + <pipeline-actions :pipeline="pipeline" :svgs="svgs"></pipeline-actions> </tr> `, }); diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 new file mode 100644 index 00000000000..605824fa939 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/table_pagination.js.es6 @@ -0,0 +1,148 @@ +/* global Vue, gl */ +/* eslint-disable no-param-reassign, no-plusplus */ + +((gl) => { + const PAGINATION_UI_BUTTON_LIMIT = 4; + const UI_LIMIT = 6; + const SPREAD = '...'; + const PREV = 'Prev'; + const NEXT = 'Next'; + const FIRST = '<< First'; + const LAST = 'Last >>'; + + gl.VueGlPagination = Vue.extend({ + props: { + + /** + This function will take the information given by the pagination component + And make a new Turbolinks call + + Here is an example `change` method: + + change(pagenum, apiScope) { + Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`); + }, + */ + + change: { + type: Function, + required: true, + }, + + /** + pageInfo will come from the headers of the API call + in the `.then` clause of the VueResource API call + there should be a function that contructs the pageInfo for this component + + This is an example: + + const pageInfo = headers => ({ + perPage: +headers['X-Per-Page'], + page: +headers['X-Page'], + total: +headers['X-Total'], + totalPages: +headers['X-Total-Pages'], + nextPage: +headers['X-Next-Page'], + previousPage: +headers['X-Prev-Page'], + }); + */ + + pageInfo: { + type: Object, + required: true, + }, + }, + methods: { + changePage(e) { + let apiScope = gl.utils.getParameterByName('scope'); + + if (!apiScope) apiScope = 'all'; + + const text = e.target.innerText; + const { totalPages, nextPage, previousPage } = this.pageInfo; + + switch (text) { + case SPREAD: + break; + case LAST: + this.change(totalPages, apiScope); + break; + case NEXT: + this.change(nextPage, apiScope); + break; + case PREV: + this.change(previousPage, apiScope); + break; + case FIRST: + this.change(1, apiScope); + break; + default: + this.change(+text, apiScope); + break; + } + }, + }, + computed: { + prev() { + return this.pageInfo.previousPage; + }, + next() { + return this.pageInfo.nextPage; + }, + getItems() { + const total = this.pageInfo.totalPages; + const page = this.pageInfo.page; + const items = []; + + if (page > 1) items.push({ title: FIRST }); + + if (page > 1) { + items.push({ title: PREV, prev: true }); + } else { + items.push({ title: PREV, disabled: true, prev: true }); + } + + if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true }); + + const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); + const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); + + for (let i = start; i <= end; i++) { + const isActive = i === page; + items.push({ title: i, active: isActive, page: true }); + } + + if (total - page > PAGINATION_UI_BUTTON_LIMIT) { + items.push({ title: SPREAD, separator: true, page: true }); + } + + if (page === total) { + items.push({ title: NEXT, disabled: true, next: true }); + } else if (total - page >= 1) { + items.push({ title: NEXT, next: true }); + } + + if (total - page >= 1) items.push({ title: LAST, last: true }); + + return items; + }, + }, + template: ` + <div class="gl-pagination"> + <ul class="pagination clearfix"> + <li v-for='item in getItems' + :class='{ + page: item.page, + prev: item.prev, + next: item.next, + separator: item.separator, + active: item.active, + disabled: item.disabled + }' + > + <a @click="changePage($event)">{{item.title}}</a> + </li> + </ul> + </div> + `, + }); +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 index 54c2b4ad369..d627fa2b88a 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 @@ -1,10 +1,15 @@ -/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars */ +/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, +no-param-reassign, no-plusplus */ /* global Vue */ Vue.http.interceptors.push((request, next) => { Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; - next(function (response) { - Vue.activeResources -= 1; + next((response) => { + if (typeof response.data === 'string') { + response.data = JSON.parse(response.data); + } + + Vue.activeResources--; }); }); diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index b5a7078a3a1..f880a9862c6 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -37,7 +37,6 @@ class Projects::CommitController < Projects::ApplicationController format.json do render json: PipelineSerializer .new(project: @project, user: @current_user) - .with_pagination(request, response) .represent(@pipelines) end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index deb084c2e91..68f6208c2be 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -218,7 +218,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController format.json do render json: PipelineSerializer .new(project: @project, user: @current_user) - .with_pagination(request, response) .represent(@pipelines) end end diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index c1f48837e0e..e00ae629e4b 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -44,9 +44,9 @@ = render "projects/merge_requests/show/commits" #diffs.diffs.tab-pane -# This tab is always loaded via AJAX - #pipelines.pipelines.tab-pane - //TODO: This needs to make a new request every time is opened! - = render "projects/merge_requests/show/pipelines", endpoint: link_to url_for(params) + - if @pipelines.any? + #pipelines.pipelines.tab-pane + = render "projects/merge_requests/show/pipelines", endpoint: link_to url_for(params) .mr-loading-status = spinner diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 8dfe967a937..f131836058b 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -94,7 +94,6 @@ #commits.commits.tab-pane -# This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - //TODO: This needs to make a new request every time is opened! = render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) #diffs.diffs.tab-pane -# This tab is always loaded via AJAX diff --git a/spec/javascripts/commit/pipelines/mock_data.js.es6 b/spec/javascripts/commit/pipelines/mock_data.js.es6 new file mode 100644 index 00000000000..5f0f26a013c --- /dev/null +++ b/spec/javascripts/commit/pipelines/mock_data.js.es6 @@ -0,0 +1,90 @@ +/* eslint-disable no-unused-vars */ +const pipeline = { + id: 73, + user: { + name: 'Administrator', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://localhost:3000/root', + }, + path: '/root/review-app/pipelines/73', + details: { + status: { + icon: 'icon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/review-app/pipelines/73', + }, + duration: null, + finished_at: '2017-01-25T00:00:17.130Z', + stages: [{ + name: 'build', + title: 'build: failed', + status: { + icon: 'icon_status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + has_details: true, + details_path: '/root/review-app/pipelines/73#build', + }, + path: '/root/review-app/pipelines/73#build', + dropdown_path: '/root/review-app/pipelines/73/stage.json?stage=build', + }], + artifacts: [], + manual_actions: [ + { + name: 'stop_review', + path: '/root/review-app/builds/1463/play', + }, + { + name: 'name', + path: '/root/review-app/builds/1490/play', + }, + ], + }, + flags: { + latest: true, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: true, + cancelable: false, + }, + ref: + { + name: 'master', + path: '/root/review-app/tree/master', + tag: false, + branch: true, + }, + commit: { + id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4', + short_id: 'fbd79f04', + title: 'Update .gitlab-ci.yml', + author_name: 'Administrator', + author_email: 'admin@example.com', + created_at: '2017-01-16T12:13:57.000-05:00', + committer_name: 'Administrator', + committer_email: 'admin@example.com', + message: 'Update .gitlab-ci.yml', + author: { + name: 'Administrator', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + web_url: 'http://localhost:3000/root', + }, + author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + commit_url: 'http://localhost:3000/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4', + commit_path: '/root/review-app/commit/fbd79f04fa98717641deaaeb092a4d417237c2e4', + }, + retry_path: '/root/review-app/pipelines/73/retry', + created_at: '2017-01-16T17:13:59.800Z', + updated_at: '2017-01-25T00:00:17.132Z', +}; diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 new file mode 100644 index 00000000000..3bcc0d1eb18 --- /dev/null +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 @@ -0,0 +1,107 @@ +/* global pipeline, Vue */ + +//= require vue +//= require vue-resource +//= require flash +//= require commit/pipelines/pipelines_store +//= require commit/pipelines/pipelines_service +//= require commit/pipelines/pipelines_table +//= require vue_shared/vue_resource_interceptor +//= require ./mock_data + +describe('Pipelines table in Commits and Merge requests', () => { + preloadFixtures('pipelines_table'); + + beforeEach(() => { + loadFixtures('pipelines_table'); + }); + + describe('successfull request', () => { + describe('without pipelines', () => { + const pipelinesEmptyResponse = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesEmptyResponse); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesEmptyResponse, + ); + }); + + it('should render the empty state', (done) => { + const component = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); + + setTimeout(() => { + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); + done(); + }, 1); + }); + }); + + describe('with pipelines', () => { + const pipelinesResponse = (request, next) => { + next(request.respondWith(JSON.stringify([pipeline]), { + status: 200, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesResponse); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesResponse, + ); + }); + + it('should render a table with the received pipelines', (done) => { + const component = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); + + setTimeout(() => { + expect(component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1); + done(); + }, 0); + }); + }); + }); + + describe('unsuccessfull request', () => { + const pipelinesErrorResponse = (request, next) => { + next(request.respondWith(JSON.stringify([]), { + status: 500, + })); + }; + + beforeEach(() => { + Vue.http.interceptors.push(pipelinesErrorResponse); + }); + + afterEach(() => { + Vue.http.interceptors = _.without( + Vue.http.interceptors, pipelinesErrorResponse, + ); + }); + + it('should render empty state', (done) => { + const component = new gl.commits.pipelines.PipelinesTableView({ + el: document.querySelector('#commit-pipeline-table-view'), + }); + + setTimeout(() => { + expect(component.$el.querySelector('.js-blank-state-title').textContent).toContain('No pipelines to show'); + done(); + }, 0); + }); + }); +}); diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 new file mode 100644 index 00000000000..46a7df3bb21 --- /dev/null +++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 @@ -0,0 +1,31 @@ +//= require vue +//= require commit/pipelines/pipelines_store + +describe('Store', () => { + const store = gl.commits.pipelines.PipelinesStore; + + beforeEach(() => { + store.create(); + }); + + it('should start with a blank state', () => { + expect(store.state.pipelines.length).toBe(0); + }); + + it('should store an array of pipelines', () => { + const pipelines = [ + { + id: '1', + name: 'pipeline', + }, + { + id: '2', + name: 'pipeline_2', + }, + ]; + + store.store(pipelines); + + expect(store.state.pipelines.length).toBe(pipelines.length); + }); +}); diff --git a/spec/javascripts/fixtures/pipelines_table.html.haml b/spec/javascripts/fixtures/pipelines_table.html.haml new file mode 100644 index 00000000000..fbe4a434f76 --- /dev/null +++ b/spec/javascripts/fixtures/pipelines_table.html.haml @@ -0,0 +1,2 @@ +#commit-pipeline-table-view{ data: { endpoint: "endpoint" } } +.pipeline-svgs{ data: { "commit_icon_svg": "svg"} } diff --git a/spec/javascripts/vue_pagination/pagination_spec.js.es6 b/spec/javascripts/vue_pagination/pagination_spec.js.es6 deleted file mode 100644 index efb11211ce2..00000000000 --- a/spec/javascripts/vue_pagination/pagination_spec.js.es6 +++ /dev/null @@ -1,167 +0,0 @@ -//= require vue -//= require lib/utils/common_utils -//= require vue_pagination/index - -describe('Pagination component', () => { - let component; - - const changeChanges = { - one: '', - two: '', - }; - - const change = (one, two) => { - changeChanges.one = one; - changeChanges.two = two; - }; - - it('should render and start at page 1', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), - propsData: { - pageInfo: { - totalPages: 10, - nextPage: 2, - previousPage: '', - }, - change, - }, - }); - - expect(component.$el.classList).toContain('gl-pagination'); - - component.changePage({ target: { innerText: '1' } }); - - expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); - }); - - it('should go to the previous page', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), - propsData: { - pageInfo: { - totalPages: 10, - nextPage: 3, - previousPage: 1, - }, - change, - }, - }); - - component.changePage({ target: { innerText: 'Prev' } }); - - expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); - }); - - it('should go to the next page', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), - propsData: { - pageInfo: { - totalPages: 10, - nextPage: 5, - previousPage: 3, - }, - change, - }, - }); - - component.changePage({ target: { innerText: 'Next' } }); - - expect(changeChanges.one).toEqual(5); - expect(changeChanges.two).toEqual('all'); - }); - - it('should go to the last page', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), - propsData: { - pageInfo: { - totalPages: 10, - nextPage: 5, - previousPage: 3, - }, - change, - }, - }); - - component.changePage({ target: { innerText: 'Last >>' } }); - - expect(changeChanges.one).toEqual(10); - expect(changeChanges.two).toEqual('all'); - }); - - it('should go to the first page', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), - propsData: { - pageInfo: { - totalPages: 10, - nextPage: 5, - previousPage: 3, - }, - change, - }, - }); - - component.changePage({ target: { innerText: '<< First' } }); - - expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); - }); - - it('should do nothing', () => { - setFixtures('<div class="test-pagination-container"></div>'); - - component = new window.gl.VueGlPagination({ - el: document.querySelector('.test-pagination-container'), - propsData: { - pageInfo: { - totalPages: 10, - nextPage: 2, - previousPage: '', - }, - change, - }, - }); - - component.changePage({ target: { innerText: '...' } }); - - expect(changeChanges.one).toEqual(1); - expect(changeChanges.two).toEqual('all'); - }); -}); - -describe('paramHelper', () => { - it('can parse url parameters correctly', () => { - window.history.pushState({}, null, '?scope=all&p=2'); - - const scope = gl.utils.getParameterByName('scope'); - const p = gl.utils.getParameterByName('p'); - - expect(scope).toEqual('all'); - expect(p).toEqual('2'); - }); - - it('returns null if param not in url', () => { - window.history.pushState({}, null, '?p=2'); - - const scope = gl.utils.getParameterByName('scope'); - const p = gl.utils.getParameterByName('p'); - - expect(scope).toEqual(null); - expect(p).toEqual('2'); - }); -}); diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 new file mode 100644 index 00000000000..6825de069e4 --- /dev/null +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 @@ -0,0 +1,90 @@ +/* global pipeline */ + +//= require vue +//= require vue_shared/components/pipelines_table_row +//= require commit/pipelines/mock_data + +describe('Pipelines Table Row', () => { + let component; + preloadFixtures('static/environments/element.html.raw'); + + beforeEach(() => { + loadFixtures('static/environments/element.html.raw'); + + component = new gl.pipelines.PipelinesTableRowComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipeline, + svgs: {}, + }, + }); + }); + + it('should render a table row', () => { + expect(component.$el).toEqual('TR'); + }); + + describe('status column', () => { + it('should render a pipeline link', () => { + expect( + component.$el.querySelector('td.commit-link a').getAttribute('href'), + ).toEqual(pipeline.path); + }); + + it('should render status text', () => { + expect( + component.$el.querySelector('td.commit-link a').textContent, + ).toContain(pipeline.details.status.text); + }); + }); + + describe('information column', () => { + it('should render a pipeline link', () => { + expect( + component.$el.querySelector('td:nth-child(2) a').getAttribute('href'), + ).toEqual(pipeline.path); + }); + + it('should render pipeline ID', () => { + expect( + component.$el.querySelector('td:nth-child(2) a > span').textContent, + ).toEqual(`#${pipeline.id}`); + }); + + describe('when a user is provided', () => { + it('should render user information', () => { + expect( + component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'), + ).toEqual(pipeline.user.web_url); + + expect( + component.$el.querySelector('td:nth-child(2) img').getAttribute('title'), + ).toEqual(pipeline.user.name); + }); + }); + }); + + describe('commit column', () => { + it('should render link to commit', () => { + expect( + component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'), + ).toEqual(pipeline.commit.commit_path); + }); + }); + + describe('stages column', () => { + it('should render an icon for each stage', () => { + expect( + component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length, + ).toEqual(pipeline.details.stages.length); + }); + }); + + describe('actions column', () => { + it('should render the provided actions', () => { + expect( + component.$el.querySelectorAll('td:nth-child(6) ul li').length, + ).toEqual(pipeline.details.manual_actions.length); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 new file mode 100644 index 00000000000..cb1006d44dc --- /dev/null +++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 @@ -0,0 +1,67 @@ +/* global pipeline */ + +//= require vue +//= require vue_shared/components/pipelines_table +//= require commit/pipelines/mock_data +//= require lib/utils/datetime_utility + +describe('Pipelines Table', () => { + preloadFixtures('static/environments/element.html.raw'); + + beforeEach(() => { + loadFixtures('static/environments/element.html.raw'); + }); + + describe('table', () => { + let component; + beforeEach(() => { + component = new gl.pipelines.PipelinesTableComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipelines: [], + svgs: {}, + }, + }); + }); + + it('should render a table', () => { + expect(component.$el).toEqual('TABLE'); + }); + + it('should render table head with correct columns', () => { + expect(component.$el.querySelector('th.js-pipeline-status').textContent).toEqual('Status'); + expect(component.$el.querySelector('th.js-pipeline-info').textContent).toEqual('Pipeline'); + expect(component.$el.querySelector('th.js-pipeline-commit').textContent).toEqual('Commit'); + expect(component.$el.querySelector('th.js-pipeline-stages').textContent).toEqual('Stages'); + expect(component.$el.querySelector('th.js-pipeline-date').textContent).toEqual(''); + expect(component.$el.querySelector('th.js-pipeline-actions').textContent).toEqual(''); + }); + }); + + describe('without data', () => { + it('should render an empty table', () => { + const component = new gl.pipelines.PipelinesTableComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipelines: [], + svgs: {}, + }, + }); + expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0); + }); + }); + + describe('with data', () => { + it('should render rows', () => { + const component = new gl.pipelines.PipelinesTableComponent({ + el: document.querySelector('.test-dom-element'), + propsData: { + pipelines: [pipeline], + svgs: {}, + }, + }); + + expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 new file mode 100644 index 00000000000..6a0fec43d2e --- /dev/null +++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js.es6 @@ -0,0 +1,168 @@ +//= require vue +//= require lib/utils/common_utils +//= require vue_shared/components/table_pagination +/* global fixture, gl */ + +describe('Pagination component', () => { + let component; + + const changeChanges = { + one: '', + two: '', + }; + + const change = (one, two) => { + changeChanges.one = one; + changeChanges.two = two; + }; + + it('should render and start at page 1', () => { + setFixtures('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 2, + previousPage: '', + }, + change, + }, + }); + + expect(component.$el.classList).toContain('gl-pagination'); + + component.changePage({ target: { innerText: '1' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the previous page', () => { + setFixtures('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 3, + previousPage: 1, + }, + change, + }, + }); + + component.changePage({ target: { innerText: 'Prev' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the next page', () => { + setFixtures('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 5, + previousPage: 3, + }, + change, + }, + }); + + component.changePage({ target: { innerText: 'Next' } }); + + expect(changeChanges.one).toEqual(5); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the last page', () => { + setFixtures('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 5, + previousPage: 3, + }, + change, + }, + }); + + component.changePage({ target: { innerText: 'Last >>' } }); + + expect(changeChanges.one).toEqual(10); + expect(changeChanges.two).toEqual('all'); + }); + + it('should go to the first page', () => { + setFixtures('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 5, + previousPage: 3, + }, + change, + }, + }); + + component.changePage({ target: { innerText: '<< First' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); + + it('should do nothing', () => { + setFixtures('<div class="test-pagination-container"></div>'); + + component = new window.gl.VueGlPagination({ + el: document.querySelector('.test-pagination-container'), + propsData: { + pageInfo: { + totalPages: 10, + nextPage: 2, + previousPage: '', + }, + change, + }, + }); + + component.changePage({ target: { innerText: '...' } }); + + expect(changeChanges.one).toEqual(1); + expect(changeChanges.two).toEqual('all'); + }); +}); + +describe('paramHelper', () => { + it('can parse url parameters correctly', () => { + window.history.pushState({}, null, '?scope=all&p=2'); + + const scope = gl.utils.getParameterByName('scope'); + const p = gl.utils.getParameterByName('p'); + + expect(scope).toEqual('all'); + expect(p).toEqual('2'); + }); + + it('returns null if param not in url', () => { + window.history.pushState({}, null, '?p=2'); + + const scope = gl.utils.getParameterByName('scope'); + const p = gl.utils.getParameterByName('p'); + + expect(scope).toEqual(null); + expect(p).toEqual('2'); + }); +}); -- cgit v1.2.1 From 45966b0abc70986f8dbd1694f8cef23546c81385 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 1 Feb 2017 19:17:58 +0100 Subject: Fix syntax error in the new merge request view --- app/views/projects/merge_requests/_new_submit.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml index e00ae629e4b..38259faf62f 100644 --- a/app/views/projects/merge_requests/_new_submit.html.haml +++ b/app/views/projects/merge_requests/_new_submit.html.haml @@ -46,7 +46,7 @@ -# This tab is always loaded via AJAX - if @pipelines.any? #pipelines.pipelines.tab-pane - = render "projects/merge_requests/show/pipelines", endpoint: link_to url_for(params) + = render "projects/merge_requests/show/pipelines", endpoint: link_to(url_for(params)) .mr-loading-status = spinner -- cgit v1.2.1 From 921141aebdf70161ecd3b2eb9038d271f5a3331c Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 1 Feb 2017 19:20:08 +0100 Subject: Serialize pipelines in the new merge request action --- app/controllers/projects/merge_requests_controller.rb | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 68f6208c2be..38a1946a71e 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -224,7 +224,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController end def new - define_new_vars + respond_to do |format| + format.html { define_new_vars } + format.json do + render json: { pipelines: PipelineSerializer + .new(project: @project, user: @current_user) + .represent(@pipelines) } + end + end end def new_diffs -- cgit v1.2.1 From 562b5015edaecb09d1237cba7ed820b95ec425f7 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Wed, 1 Feb 2017 20:06:11 +0100 Subject: Add basic specs for new merge requests pipelines API --- .../projects/merge_requests_controller_spec.rb | 34 +++++++++++++++------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index e019541e74f..e100047579d 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -22,23 +22,35 @@ describe Projects::MergeRequestsController do render_views let(:fork_project) { create(:forked_project_with_submodules) } + before { fork_project.team << [user, :master] } - before do - fork_project.team << [user, :master] + context 'when rendering HTML response' do + it 'renders new merge request widget template' do + submit_new_merge_request + + expect(response).to be_success + end end - it 'renders it' do - get :new, - namespace_id: fork_project.namespace.to_param, - project_id: fork_project.to_param, - merge_request: { - source_branch: 'remove-submodule', - target_branch: 'master' - } + context 'when rendering JSON response' do + it 'renders JSON including serialized pipelines' do + submit_new_merge_request(format: :json) - expect(response).to be_success + expect(json_response).to have_key('pipelines') + expect(response).to be_ok + end end end + + def submit_new_merge_request(format: :html) + get :new, + namespace_id: fork_project.namespace.to_param, + project_id: fork_project.to_param, + merge_request: { + source_branch: 'remove-submodule', + target_branch: 'master' }, + format: format + end end shared_examples "loads labels" do |action| -- cgit v1.2.1 From afa929143251e0c0558657899132fa11823a2e57 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Wed, 1 Feb 2017 19:53:03 +0000 Subject: Adds changelog entry --- changelogs/unreleased/fe-commit-mr-pipelines.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fe-commit-mr-pipelines.yml diff --git a/changelogs/unreleased/fe-commit-mr-pipelines.yml b/changelogs/unreleased/fe-commit-mr-pipelines.yml new file mode 100644 index 00000000000..b5cc6bbf8b6 --- /dev/null +++ b/changelogs/unreleased/fe-commit-mr-pipelines.yml @@ -0,0 +1,4 @@ +--- +title: Use vue.js Pipelines table in commit and merge request view +merge_request: 8844 +author: -- cgit v1.2.1 From 035cb734d27cb6df56803d10be408c6e0cf764f0 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Thu, 2 Feb 2017 19:43:22 +0000 Subject: Add time ago auto-update to the 2 newest tables --- .../commit/pipelines/pipelines_store.js.es6 | 28 ++++++++++++++++++++++ .../commit/pipelines/pipelines_table.js.es6 | 7 ++++-- .../vue_pipelines_index/pipeline_actions.js.es6 | 4 ++-- .../javascripts/vue_pipelines_index/store.js.es6 | 8 ++++--- app/views/projects/pipelines/index.html.haml | 3 ++- .../projects/merge_requests_controller_spec.rb | 9 +------ 6 files changed, 43 insertions(+), 16 deletions(-) diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 index b7d8e97fed3..fe90e7bac0a 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable no-underscore-dangle*/ /** * Pipelines' Store for commits view. * @@ -20,7 +21,34 @@ store(pipelines = []) { this.state.pipelines = pipelines; + return pipelines; }, + + /** + * Once the data is received we will start the time ago loops. + * + * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we + * update the time to show how long as passed. + * + */ + startTimeAgoLoops() { + const startTimeLoops = () => { + this.timeLoopInterval = setInterval(() => { + this.$children[0].$children.reduce((acc, component) => { + const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; + acc.push(timeAgoComponent); + return acc; + }, []).forEach(e => e.changeTime()); + }, 10000); + }; + + startTimeLoops(); + + const removeIntervals = () => clearInterval(this.timeLoopInterval); + const startIntervals = () => startTimeLoops(); + + gl.VueRealtimeListener(removeIntervals, startIntervals); + }, }; })(); diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 index df7a6455eed..18d57333f61 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 @@ -5,6 +5,7 @@ //= require vue-resource //= require vue_shared/vue_resource_interceptor //= require vue_shared/components/pipelines_table +//= require vue_realtime_listener/index /** * @@ -71,10 +72,12 @@ .then(response => response.json()) .then((json) => { this.store.store(json); + this.store.startTimeAgoLoops.call(this, Vue); this.isLoading = false; - }).catch(() => { + }) + .catch(() => { this.isLoading = false; - new Flash('An error occurred while fetching the pipelines.', 'alert'); + new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert'); }); }, diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index 9b4897b1a9e..e8f91227345 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -70,7 +70,7 @@ </div> </div> <div class="cancel-retry-btns inline"> - <button + <a v-if='pipeline.flags.retryable' class="btn has-tooltip" title="Retry" @@ -82,7 +82,7 @@ aria-label="Retry"> <i class="fa fa-repeat" aria-hidden="true"></i> </a> - <button + <a v-if='pipeline.flags.cancelable' class="btn btn-remove has-tooltip" title="Cancel" diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6 index 9e19b1564dc..0c4a3b77153 100644 --- a/app/assets/javascripts/vue_pipelines_index/store.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6 @@ -48,9 +48,11 @@ const startTimeLoops = () => { this.timeLoopInterval = setInterval(() => { - this.$children - .filter(e => e.$options._componentTag === 'time-ago') - .forEach(e => e.changeTime()); + this.$children[0].$children.reduce((acc, component) => { + const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; + acc.push(timeAgoComponent); + return acc; + }, []).forEach(e => e.changeTime()); }, 10000); }; diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index df36279ed75..96f625bc924 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -64,4 +64,5 @@ .vue-pipelines-index -= page_specific_javascript_tag('vue_pipelines_index/index.js') +- content_for :page_specific_javascripts do + = page_specific_javascript_tag('vue_pipelines_index/index.js') diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index e100047579d..d18e8c37901 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -701,15 +701,8 @@ describe Projects::MergeRequestsController do format: :json end - it 'responds with a rendered HTML partial' do - expect(response) - .to render_template('projects/merge_requests/show/_pipelines') - expect(json_response).to have_key 'html' - end - it 'responds with serialized pipelines' do - expect(json_response).to have_key 'pipelines' - expect(json_response['pipelines']).not_to be_empty + expect(json_response).not_to be_empty end end end -- cgit v1.2.1 From 7550f60ddeecebac3f84e8690ab3f42428600ff7 Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Wed, 1 Feb 2017 14:28:04 +0000 Subject: Backport changes from EE squash Backport changes from the EE-only squash implementation, which would otherwise conflict when merge CE into EE. <https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1024> --- app/assets/stylesheets/pages/merge_requests.scss | 17 +++++++++++++---- app/helpers/merge_requests_helper.rb | 12 ++++++++++++ app/models/repository.rb | 4 ++-- app/services/merge_requests/merge_service.rb | 14 ++++++++++---- .../merge_requests/widget/open/_accept.html.haml | 4 ++-- .../widget/open/_merge_when_build_succeeds.html.haml | 2 +- app/views/shared/issuable/_form.html.haml | 2 ++ .../shared/issuable/form/_branch_chooser.html.haml | 9 --------- app/views/shared/issuable/form/_merge_params.html.haml | 16 ++++++++++++++++ spec/lib/gitlab/diff/position_tracer_spec.rb | 4 +++- spec/models/repository_spec.rb | 14 +++++++++++--- spec/services/merge_requests/refresh_service_spec.rb | 2 +- spec/support/test_env.rb | 3 ++- 13 files changed, 75 insertions(+), 28 deletions(-) create mode 100644 app/views/shared/issuable/form/_merge_params.html.haml diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index ab68b360f93..0c013915a63 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -56,15 +56,24 @@ &.right { float: right; padding-right: 0; + } - a { - color: $gl-text-color; - } + .modify-merge-commit-link { + color: $gl-text-color; } - .remove_source_checkbox { + .merge-param-checkbox { margin: 0; } + + a .fa-question-circle { + color: $gl-text-color-secondary; + + &:hover, + &:focus { + color: $link-hover-color; + } + } } } diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 8c2c4e8833b..83ff898e68a 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -143,4 +143,16 @@ module MergeRequestsHelper def different_base?(version1, version2) version1 && version2 && version1.base_commit_sha != version2.base_commit_sha end + + def merge_params(merge_request) + { + merge_when_build_succeeds: true, + should_remove_source_branch: true, + sha: merge_request.diff_head_sha + }.merge(merge_params_ee(merge_request)) + end + + def merge_params_ee(merge_request) + {} + end end diff --git a/app/models/repository.rb b/app/models/repository.rb index a54d75f7019..7cf09c52bf4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -913,11 +913,11 @@ class Repository end end - def merge(user, merge_request, options = {}) + def merge(user, source, merge_request, options = {}) GitOperationService.new(user, self).with_branch( merge_request.target_branch) do |start_commit| our_commit = start_commit.sha - their_commit = merge_request.diff_head_sha + their_commit = source raise 'Invalid merge target' unless our_commit raise 'Invalid merge source' unless their_commit diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index ab9056a3250..5ca6fec962d 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -6,13 +6,17 @@ module MergeRequests # Executed when you do merge via GitLab UI # class MergeService < MergeRequests::BaseService - attr_reader :merge_request + attr_reader :merge_request, :source def execute(merge_request) @merge_request = merge_request return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable? + @source = find_merge_source + + return log_merge_error('No source for merge', true) unless @source + merge_request.in_locked_state do if commit after_merge @@ -34,7 +38,7 @@ module MergeRequests committer: committer } - commit_id = repository.merge(current_user, merge_request, options) + commit_id = repository.merge(current_user, source, merge_request, options) if commit_id merge_request.update(merge_commit_sha: commit_id) @@ -73,9 +77,11 @@ module MergeRequests end def merge_request_info - project = merge_request.project + merge_request.to_reference(full: true) + end - "#{project.to_reference}#{merge_request.to_reference}" + def find_merge_source + merge_request.diff_head_sha end end end diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index 7809e9c8c72..39cb0ca9cdc 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -35,10 +35,10 @@ The source branch will be removed. - elsif @merge_request.can_remove_source_branch?(current_user) .accept-control.checkbox - = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do + = label_tag :should_remove_source_branch, class: "merge-param-checkbox" do = check_box_tag :should_remove_source_branch Remove source branch - .accept-control.right + .accept-control = link_to "#", class: "modify-merge-commit-link js-toggle-button" do = icon('edit') Modify commit message diff --git a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml index f70cd09c5f4..304b0afcf93 100644 --- a/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml +++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml @@ -19,7 +19,7 @@ - if remove_source_branch_button || user_can_cancel_automatic_merge .clearfix.prepend-top-10 - if remove_source_branch_button - = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_when_build_succeeds: true, should_remove_source_branch: true, sha: @merge_request.diff_head_sha), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do + = link_to merge_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, merge_params(@merge_request)), remote: true, method: :post, class: "btn btn-grouped btn-primary btn-sm remove_source_branch" do = icon('times') Remove Source Branch When Merged diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 0a4de709fcd..cb92b2e97a7 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -43,6 +43,8 @@ = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form += render 'shared/issuable/form/merge_params', issuable: issuable + - if @merge_request_for_resolving_discussions .form-group .col-sm-10.col-sm-offset-2 diff --git a/app/views/shared/issuable/form/_branch_chooser.html.haml b/app/views/shared/issuable/form/_branch_chooser.html.haml index b757893ea04..2793e7bcff4 100644 --- a/app/views/shared/issuable/form/_branch_chooser.html.haml +++ b/app/views/shared/issuable/form/_branch_chooser.html.haml @@ -19,12 +19,3 @@ - if issuable.new_record?   = link_to 'Change branches', mr_change_branches_path(issuable) - -- if issuable.can_remove_source_branch?(current_user) - .form-group - .col-sm-10.col-sm-offset-2 - .checkbox - = label_tag 'merge_request[force_remove_source_branch]' do - = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil - = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? - Remove source branch when merge request is accepted. diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml new file mode 100644 index 00000000000..03309722326 --- /dev/null +++ b/app/views/shared/issuable/form/_merge_params.html.haml @@ -0,0 +1,16 @@ +- issuable = local_assigns.fetch(:issuable) + +- return unless issuable.is_a?(MergeRequest) +- return if issuable.closed_without_fork? + +-# This check is duplicated below, to avoid conflicts with EE. +- return unless issuable.can_remove_source_branch?(current_user) + +.form-group + .col-sm-10.col-sm-offset-2 + - if issuable.can_remove_source_branch?(current_user) + .checkbox + = label_tag 'merge_request[force_remove_source_branch]' do + = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil + = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch? + Remove source branch when merge request is accepted. diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index ffb9ba86fbb..8e3e4034c8f 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -1640,7 +1640,9 @@ describe Gitlab::Diff::PositionTracer, lib: true do } merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project) - repository.merge(current_user, merge_request, options) + + repository.merge(current_user, merge_request.diff_head_sha, merge_request, options) + project.commit(branch_name) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 901cfb907f2..53b98ba05f8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -15,7 +15,12 @@ describe Repository, models: true do let(:merge_commit) do merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) - merge_commit_id = repository.merge(user, merge_request, commit_options) + + merge_commit_id = repository.merge(user, + merge_request.diff_head_sha, + merge_request, + commit_options) + repository.commit(merge_commit_id) end @@ -1082,8 +1087,11 @@ describe Repository, models: true do it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) - merge_commit_id = repository.merge(user, merge_request, commit_options) - repository.commit(merge_commit_id) + + merge_commit_id = repository.merge(user, + merge_request.diff_head_sha, + merge_request, + commit_options) expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id) end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 314ea670a71..2cc21acab7b 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -89,7 +89,7 @@ describe MergeRequests::RefreshService, services: true do # Merge master -> feature branch author = { email: 'test@gitlab.com', time: Time.now, name: "Me" } commit_options = { message: 'Test message', committer: author, author: author } - @project.repository.merge(@user, @merge_request, commit_options) + @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, commit_options) commit = @project.repository.commit('feature') service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature') reload_mrs diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 90f1a9c8798..b87232a350b 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -36,7 +36,8 @@ module TestEnv 'conflict-non-utf8' => 'd0a293c', 'conflict-too-large' => '39fa04f', 'deleted-image-test' => '6c17798', - 'wip' => 'b9238ee' + 'wip' => 'b9238ee', + 'csv' => '3dd0896' } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily -- cgit v1.2.1 From 9bb08a7e53b22d7af8484e3921b6fe51996ca981 Mon Sep 17 00:00:00 2001 From: YarNayar <YarTheGreat@gmail.com> Date: Tue, 31 Jan 2017 16:27:29 +0300 Subject: Adds /target_branch slash command functionality for merge requests Allows to use slash command /target_branch <target_branch_name> in merge requests notes and description. Command allows to specify target branch for current merge request. Proposed in #23619 --- app/services/slash_commands/interpret_service.rb | 12 ++++ ...ash-command-for-target-merge-request-branch.yml | 4 ++ doc/user/project/slash_commands.md | 1 + .../user_uses_slash_commands_spec.rb | 74 ++++++++++++++++++++++ .../slash_commands/interpret_service_spec.rb | 32 ++++++++++ 5 files changed, 123 insertions(+) create mode 100644 changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb index 3566a8ba92f..3e0a85cf059 100644 --- a/app/services/slash_commands/interpret_service.rb +++ b/app/services/slash_commands/interpret_service.rb @@ -304,6 +304,18 @@ module SlashCommands params '@user' command :cc + desc 'Defines target branch for MR' + params '<Local branch name>' + condition do + issuable.respond_to?(:target_branch) && + (current_user.can?(:"update_#{issuable.to_ability_name}", issuable) || + issuable.new_record?) + end + command :target_branch do |target_branch_param| + branch_name = target_branch_param.strip + @updates[:target_branch] = branch_name if project.repository.branch_names.include?(branch_name) + end + def find_label_ids(labels_param) label_ids_by_reference = extract_references(labels_param, :label).map(&:id) labels_ids_by_name = LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute.select(:id) diff --git a/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml b/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml new file mode 100644 index 00000000000..9fd6ea5bc52 --- /dev/null +++ b/changelogs/unreleased/Add-a-shash-command-for-target-merge-request-branch.yml @@ -0,0 +1,4 @@ +--- +title: Adds /target_branch slash command functionality for merge requests +merge_request: +author: YarNayar diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md index a6546cffce2..a53c547e7d2 100644 --- a/doc/user/project/slash_commands.md +++ b/doc/user/project/slash_commands.md @@ -34,3 +34,4 @@ do. | `/remove_estimate` | Remove estimated time | | <code>/spend <1h 30m | -1h 5m></code> | Add or substract spent time | | `/remove_time_spent` | Remove time spent | +| `/target_branch <Branch Name>` | Set target branch for current merge request | diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index b13674b4db9..cf56715f508 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -120,5 +120,79 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do expect(page).not_to have_content '/due 2016-08-28' end end + + describe '/target_branch command in merge request' do + let(:another_project) { create(:project, :public) } + let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } } + + before do + logout + another_project.team << [user, :master] + login_with(user) + end + + it 'changes target_branch in new merge_request' do + visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts) + fill_in "merge_request_title", with: 'My brand new feature' + fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:" + click_button "Submit merge request" + + merge_request = another_project.merge_requests.first + expect(merge_request.description).to eq "le feature \nFeature description:" + expect(merge_request.target_branch).to eq 'fix' + end + + it 'does not change target branch when merge request is edited' do + new_merge_request = create(:merge_request, source_project: another_project) + + visit edit_namespace_project_merge_request_path(another_project.namespace, another_project, new_merge_request) + fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n" + click_button "Save changes" + + new_merge_request = another_project.merge_requests.first + expect(new_merge_request.description).to include('/target_branch') + expect(new_merge_request.target_branch).not_to eq('fix') + end + end + + describe '/target_branch command from note' do + context 'when the current user can change target branch' do + it 'changes target branch from a note' do + write_note("message start \n/target_branch merge-test\n message end.") + + expect(page).not_to have_content('/target_branch') + expect(page).to have_content('message start') + expect(page).to have_content('message end.') + + expect(merge_request.reload.target_branch).to eq 'merge-test' + end + + it 'does not fail when target branch does not exists' do + write_note('/target_branch totally_not_existing_branch') + + expect(page).not_to have_content('/target_branch') + + expect(merge_request.target_branch).to eq 'feature' + end + end + + context 'when current user can not change target branch' do + let(:guest) { create(:user) } + before do + project.team << [guest, :guest] + logout + login_with(guest) + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + it 'does not change target branch' do + write_note('/target_branch merge-test') + + expect(page).not_to have_content '/target_branch merge-test' + + expect(merge_request.target_branch).to eq 'feature' + end + end + end end end diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb index 66fc8fc360b..0b0925983eb 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/slash_commands/interpret_service_spec.rb @@ -653,5 +653,37 @@ describe SlashCommands::InterpretService, services: true do let(:issuable) { issue } end end + + context '/target_branch command' do + let(:non_empty_project) { create(:project) } + let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) } + let(:service) { described_class.new(non_empty_project, developer)} + + it 'updates target_branch if /target_branch command is executed' do + _, updates = service.execute('/target_branch merge-test', merge_request) + + expect(updates).to eq(target_branch: 'merge-test') + end + + it 'handles blanks around param' do + _, updates = service.execute('/target_branch merge-test ', merge_request) + + expect(updates).to eq(target_branch: 'merge-test') + end + + context 'ignores command with no argument' do + it_behaves_like 'empty command' do + let(:content) { '/target_branch' } + let(:issuable) { another_merge_request } + end + end + + context 'ignores non-existing target branch' do + it_behaves_like 'empty command' do + let(:content) { '/target_branch totally_non_existing_branch' } + let(:issuable) { another_merge_request } + end + end + end end end -- cgit v1.2.1 From 6e1d675de97122a966b16b7b732b2b145bbfc201 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Fri, 3 Feb 2017 12:42:11 +0100 Subject: API: Fix file downloading --- changelogs/unreleased/api-fix-files.yml | 4 ++++ lib/api/helpers.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/api-fix-files.yml diff --git a/changelogs/unreleased/api-fix-files.yml b/changelogs/unreleased/api-fix-files.yml new file mode 100644 index 00000000000..8a9e29109a8 --- /dev/null +++ b/changelogs/unreleased/api-fix-files.yml @@ -0,0 +1,4 @@ +--- +title: 'API: Fix file downloading' +merge_request: Robert Schilling +author: 8267 diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb index eb5b947172a..dfab60f7fa5 100644 --- a/lib/api/helpers.rb +++ b/lib/api/helpers.rb @@ -304,7 +304,7 @@ module API header['X-Sendfile'] = path body else - path + file path end end -- cgit v1.2.1 From bfd9bbba2b617200ecd65c9e9a9c35c2ff3cad2f Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 2 Feb 2017 16:57:41 +0000 Subject: Added specs for merge request environment list Ported from https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1154 --- spec/features/merge_requests/widget_spec.rb | 53 +++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index 7d1805f5001..fb3a1ae4bd0 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -5,30 +5,53 @@ describe 'Merge request', :feature, :js do let(:project) { create(:project) } let(:user) { create(:user) } + let(:merge_request) { create(:merge_request, source_project: project) } before do project.team << [user, :master] login_as(user) + end - visit new_namespace_project_merge_request_path( - project.namespace, - project, - merge_request: { - source_project_id: project.id, - target_project_id: project.id, - source_branch: 'feature', - target_branch: 'master' - } - ) + context 'new merge request' do + before do + visit new_namespace_project_merge_request_path( + project.namespace, + project, + merge_request: { + source_project_id: project.id, + target_project_id: project.id, + source_branch: 'feature', + target_branch: 'master' + } + ) + end + + it 'shows widget status after creating new merge request' do + click_button 'Submit merge request' + + expect(find('.mr-state-widget')).to have_content('Checking ability to merge automatically') + + wait_for_ajax + + expect(page).to have_selector('.accept_merge_request') + end end - it 'shows widget status after creating new merge request' do - click_button 'Submit merge request' + context 'view merge request' do + let!(:environment) { create(:environment, project: project) } + let!(:deployment) { create(:deployment, environment: environment, ref: 'feature', sha: merge_request.diff_head_sha) } - expect(find('.mr-state-widget')).to have_content('Checking ability to merge automatically') + before do + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end - wait_for_ajax + it 'shows environments link' do + wait_for_ajax - expect(page).to have_selector('.accept_merge_request') + page.within('.mr-widget-heading') do + expect(page).to have_content("Deployed to #{environment.name}") + expect(find('.js-environment-link')[:href]).to include(environment.formatted_external_url) + end + end end end -- cgit v1.2.1 From 63dac85385c6f82db6c6465876d76f67173ebc3b Mon Sep 17 00:00:00 2001 From: blackst0ne <blackst0ne.ru@gmail.com> Date: Sat, 4 Feb 2017 00:00:26 +1100 Subject: Fixed redirection from http://someproject.git to http://someproject --- app/controllers/projects/application_controller.rb | 2 +- changelogs/unreleased/git_to_html_redirection.yml | 4 ++++ spec/controllers/projects_controller_spec.rb | 11 +++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/git_to_html_redirection.yml diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb index b2ff36f6538..f9d550ffb85 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -18,7 +18,7 @@ class Projects::ApplicationController < ApplicationController # to # localhost/group/project # - if id =~ /\.git\Z/ + if params[:format] == 'git' redirect_to request.original_url.gsub(/\.git\/?\Z/, '') return end diff --git a/changelogs/unreleased/git_to_html_redirection.yml b/changelogs/unreleased/git_to_html_redirection.yml new file mode 100644 index 00000000000..b2959c02c07 --- /dev/null +++ b/changelogs/unreleased/git_to_html_redirection.yml @@ -0,0 +1,4 @@ +--- +title: Redirect http://someproject.git to http://someproject +merge_request: +author: blackst0ne diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 9323f723bdb..e7aa8745b99 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -213,6 +213,17 @@ describe ProjectsController do expect(response.status).to eq 404 end end + + context "redirection from http://someproject.git" do + it 'redirects to project page (format.html)' do + project = create(:project, :public) + + get :show, namespace_id: project.namespace.path, id: project.path, format: :git + + expect(response).to have_http_status(302) + expect(response).to redirect_to(namespace_project_path) + end + end end describe "#update" do -- cgit v1.2.1 From a132b7d8ce0d962272de5932568a46b64b6dfc5e Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 24 Jan 2017 10:27:41 +0000 Subject: Add issues to boards list This removes the backlog list & instead creates a modal window that will list all issues that are not part of a list for easy adding onto the board Closes #26205 --- app/assets/javascripts/boards/boards_bundle.js.es6 | 17 ++++- .../boards/components/board_card.js.es6 | 29 +------ .../boards/components/issue_card_inner.js.es6 | 89 ++++++++++++++++++++++ .../boards/components/modal/dismiss.js.es6 | 28 +++++++ .../boards/components/modal/footer.js.es6 | 33 ++++++++ .../boards/components/modal/header.js.es6 | 31 ++++++++ .../javascripts/boards/components/modal/index.es6 | 32 ++++++++ .../boards/components/modal/list.js.es6 | 51 +++++++++++++ .../boards/components/modal/search.js.es6 | 14 ++++ .../boards/components/modal/tabs.js.es6 | 46 +++++++++++ .../boards/services/board_service.js.es6 | 10 +++ .../javascripts/boards/stores/boards_store.js.es6 | 5 ++ app/assets/stylesheets/pages/boards.scss | 52 ++++++++++++- app/controllers/projects/boards_controller.rb | 23 +++++- app/views/projects/boards/_show.html.haml | 1 + .../projects/boards/components/_card.html.haml | 25 +----- app/views/shared/issuable/_filter.html.haml | 2 + config/routes/project.rb | 2 + 18 files changed, 438 insertions(+), 52 deletions(-) create mode 100644 app/assets/javascripts/boards/components/issue_card_inner.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/dismiss.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/footer.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/header.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/index.es6 create mode 100644 app/assets/javascripts/boards/components/modal/list.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/search.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/tabs.js.es6 diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index f9766471780..d87a1de7eee 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -13,6 +13,7 @@ //= require ./components/board //= require ./components/board_sidebar //= require ./components/new_list_dropdown +//= require ./components/modal //= require ./vue_resource_interceptor $(() => { @@ -31,7 +32,8 @@ $(() => { el: $boardApp, components: { 'board': gl.issueBoards.Board, - 'board-sidebar': gl.issueBoards.BoardSidebar + 'board-sidebar': gl.issueBoards.BoardSidebar, + 'board-add-issues-modal': gl.issueBoards.IssuesModal, }, data: { state: Store.state, @@ -55,12 +57,12 @@ $(() => { gl.boardService.all() .then((resp) => { resp.json().forEach((board) => { + if (board.list_type === 'backlog') return; + const list = Store.addList(board); if (list.type === 'done') { list.position = Infinity; - } else if (list.type === 'backlog') { - list.position = -1; } }); @@ -81,4 +83,13 @@ $(() => { gl.issueBoards.newListDropdownInit(); } }); + + // This element is outside the Vue app + $(document) + .off('click', '.js-show-add-issues') + .on('click', '.js-show-add-issues', (e) => { + e.preventDefault(); + + Store.modal.showAddIssuesModal = true; + }); }); diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 index 5fc50280811..ab4226ded1d 100644 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -1,4 +1,5 @@ /* eslint-disable comma-dangle, space-before-function-paren, dot-notation */ +//= require ./issue_card_inner /* global Vue */ (() => { @@ -9,6 +10,9 @@ gl.issueBoards.BoardCard = Vue.extend({ template: '#js-board-list-card', + components: { + 'issue-card-inner': gl.issueBoards.IssueCardInner, + }, props: { list: Object, issue: Object, @@ -28,31 +32,6 @@ } }, methods: { - filterByLabel (label, e) { - let labelToggleText = label.title; - const labelIndex = Store.state.filters['label_name'].indexOf(label.title); - $(e.target).tooltip('hide'); - - if (labelIndex === -1) { - Store.state.filters['label_name'].push(label.title); - $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`); - } else { - Store.state.filters['label_name'].splice(labelIndex, 1); - labelToggleText = Store.state.filters['label_name'][0]; - $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove(); - } - - const selectedLabels = Store.state.filters['label_name']; - if (selectedLabels.length === 0) { - labelToggleText = 'Label'; - } else if (selectedLabels.length > 1) { - labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`; - } - - $('.labels-filter .dropdown-toggle-text').text(labelToggleText); - - Store.updateFiltersUrl(); - }, mouseDown () { this.showDetail = true; }, diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 new file mode 100644 index 00000000000..6a7e9419503 --- /dev/null +++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 @@ -0,0 +1,89 @@ +/* global Vue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.IssueCardInner = Vue.extend({ + props: [ + 'issue', 'issueLinkBase', 'list', + ], + methods: { + showLabel(label) { + if (!this.list) return true; + + return !this.list.label || label.id !== this.list.label.id; + }, + filterByLabel(label, e) { + let labelToggleText = label.title; + const labelIndex = Store.state.filters.label_name.indexOf(label.title); + $(e.target).tooltip('hide'); + + if (labelIndex === -1) { + Store.state.filters.label_name.push(label.title); + $('.labels-filter').prepend(`<input type="hidden" name="label_name[]" value="${label.title}" />`); + } else { + Store.state.filters.label_name.splice(labelIndex, 1); + labelToggleText = Store.state.filters.label_name[0]; + $(`.labels-filter input[name="label_name[]"][value="${label.title}"]`).remove(); + } + + const selectedLabels = Store.state.filters.label_name; + if (selectedLabels.length === 0) { + labelToggleText = 'Label'; + } else if (selectedLabels.length > 1) { + labelToggleText = `${selectedLabels[0]} + ${selectedLabels.length - 1} more`; + } + + $('.labels-filter .dropdown-toggle-text').text(labelToggleText); + + Store.updateFiltersUrl(); + }, + }, + template: ` + <div> + <h4 class="card-title"> + <i + class="fa fa-eye-flash" + v-if="issue.confidential"></i> + <a + :href="issueLinkBase + '/' + issue.id" + :title="issue.title"> + {{ issue.title }} + </a> + </h4> + <div class="card-footer"> + <span + class="card-number" + v-if="issue.id"> + #{{issue.id}} + </span> + <a + class="has-tooltip" + :href="issue.assignee.username" + :title="'Assigned to ' + issue.assignee.name" + v-if="issue.assignee" + data-container="body"> + <img + class="avatar avatar-inline s20" + :src="issue.assignee.avatar" + width="20" + height="20" /> + </a> + <button + class="label color-label has-tooltip" + v-for="label in issue.labels" + type="button" + v-if="showLabel(label)" + @click="filterByLabel(label, $event)" + :style="{ backgroundColor: label.color, color: label.textColor }" + :title="label.description" + data-container="body"> + {{ label.title }} + </button> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/dismiss.js.es6 b/app/assets/javascripts/boards/components/modal/dismiss.js.es6 new file mode 100644 index 00000000000..b5027f004c6 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/dismiss.js.es6 @@ -0,0 +1,28 @@ +/* global Vue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.DismissModal = Vue.extend({ + data() { + return Store.modal; + }, + methods: { + toggleModal(toggle) { + this.showAddIssuesModal = toggle; + }, + }, + template: ` + <button + type="button" + class="close" + data-dismiss="modal" + aria-label="Close" + @click="toggleModal(false)"> + <span aria-hidden="true">×</span> + </button> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 new file mode 100644 index 00000000000..9cb48448a87 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -0,0 +1,33 @@ +/* global Vue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.ModalFooter = Vue.extend({ + data() { + return Store.modal; + }, + methods: { + hideModal() { + this.showAddIssuesModal = false; + }, + }, + template: ` + <footer class="form-actions add-issues-footer"> + <button + class="btn btn-success pull-left" + type="button"> + Add issues + </button> + <button + class="btn btn-default pull-right" + type="button" + @click="hideModal"> + Cancel + </button> + </footer> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 new file mode 100644 index 00000000000..74681fa4bcf --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -0,0 +1,31 @@ +//= require ./dismiss +//= require ./tabs +//= require ./search +/* global Vue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.IssuesModalHeader = Vue.extend({ + data() { + return Store.modal; + }, + components: { + 'modal-dismiss': gl.issueBoards.DismissModal, + 'modal-tabs': gl.issueBoards.ModalTabs, + 'modal-search': gl.issueBoards.ModalSearch, + }, + template: ` + <header class="add-issues-header"> + <h2> + Add issues to board + <modal-dismiss></modal-dismiss> + </h2> + <modal-tabs></modal-tabs> + <modal-search></modal-search> + </header> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/index.es6 b/app/assets/javascripts/boards/components/modal/index.es6 new file mode 100644 index 00000000000..bedd7c9735a --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/index.es6 @@ -0,0 +1,32 @@ +//= require ./header +//= require ./list +//= require ./footer +/* global Vue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.IssuesModal = Vue.extend({ + data() { + return Store.modal; + }, + components: { + 'modal-header': gl.issueBoards.IssuesModalHeader, + 'modal-list': gl.issueBoards.ModalList, + 'modal-footer': gl.issueBoards.ModalFooter, + }, + template: ` + <div + class="add-issues-modal" + v-if="showAddIssuesModal"> + <div class="add-issues-container"> + <modal-header></modal-header> + <modal-list></modal-list> + <modal-footer></modal-footer> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 new file mode 100644 index 00000000000..4e060895c5d --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -0,0 +1,51 @@ +/* global Vue */ +/* global ListIssue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.ModalList = Vue.extend({ + data() { + return Store.modal; + }, + computed: { + loading() { + return this.issues.length === 0; + }, + }, + mounted() { + gl.boardService.getBacklog() + .then((res) => { + const data = res.json(); + + data.forEach((issueObj) => { + this.issues.push(new ListIssue(issueObj)); + }); + }); + }, + components: { + 'issue-card-inner': gl.issueBoards.IssueCardInner, + }, + template: ` + <section class="add-issues-list"> + <i + class="fa fa-spinner fa-spin" + v-if="loading"></i> + <ul + class="list-unstyled" + v-if="!loading"> + <li + class="card" + v-for="issue in issues"> + <issue-card-inner + :issue="issue" + :issue-link-base="'/'"> + </issue-card-inner> + </li> + </ul> + </section> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/search.js.es6 b/app/assets/javascripts/boards/components/modal/search.js.es6 new file mode 100644 index 00000000000..714c9240d4d --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/search.js.es6 @@ -0,0 +1,14 @@ +/* global Vue */ +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.ModalSearch = Vue.extend({ + template: ` + <input + placeholder="Search issues..." + class="form-control" + type="search" /> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 new file mode 100644 index 00000000000..bfdfd7e2bf5 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -0,0 +1,46 @@ +/* global Vue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.ModalTabs = Vue.extend({ + data() { + return Store.modal; + }, + methods: { + changeTab(tab) { + this.activeTab = tab; + }, + }, + template: ` + <div class="top-area"> + <ul class="nav-links issues-state-filters"> + <li :class="{ 'active': activeTab == 'all' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('all')"> + <span>All issues</span> + <span class="badge"> + {{ issues.length }} + </span> + </a> + </li> + <li :class="{ 'active': activeTab == 'selected' }"> + <a + href="#" + role="button" + @click.prevent="changeTab('selected')"> + <span>Selected issues</span> + <span class="badge"> + 0 + </span> + </a> + </li> + </ul> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index ea55158306b..4625b13d5f3 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -3,6 +3,12 @@ class BoardService { constructor (root, boardId) { + this.boards = Vue.resource(`${root}{/id}.json`, {}, { + backlog: { + method: 'GET', + url: `${root}/${boardId}/backlog.json` + } + }); this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { generate: { method: 'POST', @@ -65,6 +71,10 @@ class BoardService { issue }); } + + getBacklog() { + return this.boards.backlog(); + } } window.BoardService = BoardService; diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index cdf1b09c0a4..57de80e448c 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -12,6 +12,11 @@ detail: { issue: {} }, + modal: { + issues: [], + showAddIssuesModal: false, + activeTab: 'all', + }, moving: { issue: {}, list: {} diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index f2d60bff2b5..5390bad1b33 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -250,11 +250,12 @@ } .issue-boards-search { - width: 290px; + width: 395px; .form-control { display: inline-block; width: 210px; + margin-right: 10px } } @@ -354,3 +355,52 @@ padding-right: 0; } } + +.add-issues-modal { + display: flex; + align-items: center; + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + background-color: rgba($black, .3); + z-index: 9999; +} + +.add-issues-container { + display: flex; + flex-direction: column; + width: 90vw; + height: 85vh; + margin-left: auto; + margin-right: auto; + padding: 25px 15px 0; + background-color: $white-light; + border-radius: $border-radius-default; + box-shadow: 0 2px 12px rgba($black, .5); +} + +.add-issues-header { + > h2 { + margin: 0; + font-size: 18px; + } + + .top-area { + margin-bottom: 10px; + } +} + +.add-issues-list { + flex: 1; + overflow-y: scroll; +} + +.add-issues-footer { + margin-top: auto; + margin-left: -15px; + margin-right: -15px; + padding-left: 15px; + padding-right: 15px; +} diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 808affa4f98..7a2e2324323 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -1,7 +1,7 @@ class Projects::BoardsController < Projects::ApplicationController include IssuableCollections - before_action :authorize_read_board!, only: [:index, :show] + # before_action :authorize_read_board!, only: [:index, :show, :backlog] def index @boards = ::Boards::ListService.new(project, current_user).execute @@ -25,6 +25,27 @@ class Projects::BoardsController < Projects::ApplicationController end end + def backlog + board = project.boards.find(params[:id]) + + @issues = issues_collection + @issues = @issues.where.not( + LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") + .where(label_id: board.lists.movable.pluck(:label_id)).limit(1).arel.exists + ) + @issues = @issues.page(params[:page]) + + render json: @issues.as_json( + labels: true, + only: [:iid, :title, :confidential, :due_date], + include: { + assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, + milestone: { only: [:id, :title] } + }, + user: current_user + ) + end + private def authorize_read_board! diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 356bd50f7f3..798eace7a82 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -26,3 +26,4 @@ ":issue-link-base" => "issueLinkBase", ":key" => "_uid" } = render "projects/boards/components/sidebar" + %board-add-issues-modal diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index e4c2aff46ec..51e5d739537 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -4,25 +4,6 @@ "@mousedown" => "mouseDown", "@mousemove" => "mouseMove", "@mouseup" => "showIssue($event)" } - %h4.card-title - = icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential") - %a{ ":href" => 'issueLinkBase + "/" + issue.id', - ":title" => "issue.title" } - {{ issue.title }} - .card-footer - %span.card-number{ "v-if" => "issue.id" } - = precede '#' do - {{ issue.id }} - %a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username", - ":title" => '"Assigned to " + issue.assignee.name', - "v-if" => "issue.assignee", - data: { container: 'body' } } - %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20, alt: "Avatar" } - %button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels", - type: "button", - "v-if" => "(!list.label || label.id !== list.label.id)", - "@click" => "filterByLabel(label, $event)", - ":style" => "{ backgroundColor: label.color, color: label.textColor }", - ":title" => "label.description", - data: { container: 'body' } } - {{ label.title }} + %issue-card-inner{ ":list" => "list", + ":issue" => "issue", + ":issue-link-base" => "issueLinkBase" } diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index b42eaabb111..b94bdf14d5e 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -38,6 +38,8 @@ #js-boards-search.issue-boards-search %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } - if can?(current_user, :admin_list, @project) + %button.btn.btn-create.btn-inverted.js-show-add-issues{ type: "button" } + Add issues .dropdown.pull-right %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } Add list diff --git a/config/routes/project.rb b/config/routes/project.rb index efe2fbc521d..5c33bb4a6ca 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -266,6 +266,8 @@ constraints(ProjectUrlConstrainer.new) do end resources :boards, only: [:index, :show] do + get :backlog, on: :member + scope module: :boards do resources :issues, only: [:update] -- cgit v1.2.1 From 94c07c21ae5cc505a5b34a71975198efe400f5a7 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 24 Jan 2017 10:49:39 +0000 Subject: Styling fix for list --- app/assets/javascripts/boards/boards_bundle.js.es6 | 2 +- .../javascripts/boards/components/modal/index.es6 | 32 ---------------------- .../boards/components/modal/list.js.es6 | 2 +- .../javascripts/boards/components/modal/modal.es6 | 32 ++++++++++++++++++++++ app/assets/stylesheets/pages/boards.scss | 7 +++++ 5 files changed, 41 insertions(+), 34 deletions(-) delete mode 100644 app/assets/javascripts/boards/components/modal/index.es6 create mode 100644 app/assets/javascripts/boards/components/modal/modal.es6 diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index d87a1de7eee..09f5caee921 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -13,7 +13,7 @@ //= require ./components/board //= require ./components/board_sidebar //= require ./components/new_list_dropdown -//= require ./components/modal +//= require ./components/modal/modal //= require ./vue_resource_interceptor $(() => { diff --git a/app/assets/javascripts/boards/components/modal/index.es6 b/app/assets/javascripts/boards/components/modal/index.es6 deleted file mode 100644 index bedd7c9735a..00000000000 --- a/app/assets/javascripts/boards/components/modal/index.es6 +++ /dev/null @@ -1,32 +0,0 @@ -//= require ./header -//= require ./list -//= require ./footer -/* global Vue */ -(() => { - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.IssuesModal = Vue.extend({ - data() { - return Store.modal; - }, - components: { - 'modal-header': gl.issueBoards.IssuesModalHeader, - 'modal-list': gl.issueBoards.ModalList, - 'modal-footer': gl.issueBoards.ModalFooter, - }, - template: ` - <div - class="add-issues-modal" - v-if="showAddIssuesModal"> - <div class="add-issues-container"> - <modal-header></modal-header> - <modal-list></modal-list> - <modal-footer></modal-footer> - </div> - </div> - `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 4e060895c5d..d4b3bcc71d7 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -34,7 +34,7 @@ class="fa fa-spinner fa-spin" v-if="loading"></i> <ul - class="list-unstyled" + class="add-issues-list-columns list-unstyled" v-if="!loading"> <li class="card" diff --git a/app/assets/javascripts/boards/components/modal/modal.es6 b/app/assets/javascripts/boards/components/modal/modal.es6 new file mode 100644 index 00000000000..bedd7c9735a --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/modal.es6 @@ -0,0 +1,32 @@ +//= require ./header +//= require ./list +//= require ./footer +/* global Vue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.IssuesModal = Vue.extend({ + data() { + return Store.modal; + }, + components: { + 'modal-header': gl.issueBoards.IssuesModalHeader, + 'modal-list': gl.issueBoards.ModalList, + 'modal-footer': gl.issueBoards.ModalFooter, + }, + template: ` + <div + class="add-issues-modal" + v-if="showAddIssuesModal"> + <div class="add-issues-container"> + <modal-header></modal-header> + <modal-list></modal-list> + <modal-footer></modal-footer> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 5390bad1b33..6b36fe8bac7 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -404,3 +404,10 @@ padding-left: 15px; padding-right: 15px; } + +.add-issues-list-columns { + padding-left: 5px; + padding-right: 5px; + margin-left: -5px; + margin-right: -5px; +} -- cgit v1.2.1 From 1fae4d8184e200f8e6a4ccafafd77b031240f04d Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 24 Jan 2017 12:20:49 +0000 Subject: Added masonry layout Possibly need to change masonry library but so far it is the only one I found that works well --- app/assets/javascripts/boards/boards_bundle.js.es6 | 1 + .../boards/components/modal/list.js.es6 | 65 +- .../boards/components/modal/tabs.js.es6 | 15 +- app/assets/javascripts/boards/models/issue.js.es6 | 1 + .../javascripts/boards/stores/boards_store.js.es6 | 2 +- app/assets/stylesheets/pages/boards.scss | 18 +- vendor/assets/javascripts/masonry.js | 2463 ++++++++++++++++++++ 7 files changed, 2548 insertions(+), 17 deletions(-) create mode 100644 vendor/assets/javascripts/masonry.js diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 09f5caee921..ef73d381fb4 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -5,6 +5,7 @@ //= require vue //= require vue-resource //= require Sortable +//= require masonry //= require_tree ./models //= require_tree ./stores //= require_tree ./services diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index d4b3bcc71d7..e06fa58b3b6 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -10,11 +10,39 @@ data() { return Store.modal; }, + watch: { + activeTab() { + this.$nextTick(() => { + this.destroyMasonry(); + this.initMasonry(); + }); + }, + }, computed: { loading() { return this.issues.length === 0; }, }, + methods: { + toggleIssue(issue) { + issue.selected = !issue.selected; + }, + showIssue(issue) { + if (this.activeTab === 'all') return true; + + return issue.selected; + }, + initMasonry() { + listMasonry = new Masonry(this.$refs.list, { + transitionDuration: 0, + }); + }, + destroyMasonry() { + if (listMasonry) { + listMasonry.destroy(); + } + } + }, mounted() { gl.boardService.getBacklog() .then((res) => { @@ -23,8 +51,16 @@ data.forEach((issueObj) => { this.issues.push(new ListIssue(issueObj)); }); + + this.$nextTick(() => { + this.initMasonry(); + }); }); }, + destroyed() { + this.issues = []; + this.destroyMasonry(); + }, components: { 'issue-card-inner': gl.issueBoards.IssueCardInner, }, @@ -33,18 +69,25 @@ <i class="fa fa-spinner fa-spin" v-if="loading"></i> - <ul + <div class="add-issues-list-columns list-unstyled" - v-if="!loading"> - <li - class="card" - v-for="issue in issues"> - <issue-card-inner - :issue="issue" - :issue-link-base="'/'"> - </issue-card-inner> - </li> - </ul> + ref="list" + v-show="!loading"> + <div + v-for="issue in issues" + v-if="showIssue(issue)" + class="card-parent"> + <div + class="card" + :class="{ 'is-active': issue.selected }" + @click="toggleIssue(issue)"> + <issue-card-inner + :issue="issue" + :issue-link-base="'/'"> + </issue-card-inner> + </div> + </div> + </div> </section> `, }); diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 index bfdfd7e2bf5..58fb75f839f 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js.es6 +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -14,6 +14,19 @@ this.activeTab = tab; }, }, + computed: { + selectedCount() { + let count = 0; + + this.issues.forEach((issue) => { + if (issue.selected) { + count += 1; + } + }); + + return count; + }, + }, template: ` <div class="top-area"> <ul class="nav-links issues-state-filters"> @@ -35,7 +48,7 @@ @click.prevent="changeTab('selected')"> <span>Selected issues</span> <span class="badge"> - 0 + {{ selectedCount }} </span> </a> </li> diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index 31531c3ee34..a36a146b023 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -12,6 +12,7 @@ class ListIssue { this.dueDate = obj.due_date; this.subscribed = obj.subscribed; this.labels = []; + this.selected = false; if (obj.assignee) { this.assignee = new ListUser(obj.assignee); diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 57de80e448c..42216d429c6 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -14,7 +14,7 @@ }, modal: { issues: [], - showAddIssuesModal: false, + showAddIssuesModal: true, activeTab: 'all', }, moving: { diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 6b36fe8bac7..f41d4cdd45d 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -390,10 +390,16 @@ .top-area { margin-bottom: 10px; } + + .form-control { + margin-bottom: 10px; + } } .add-issues-list { flex: 1; + margin-left: -$gl-vert-padding; + margin-right: -$gl-vert-padding; overflow-y: scroll; } @@ -406,8 +412,12 @@ } .add-issues-list-columns { - padding-left: 5px; - padding-right: 5px; - margin-left: -5px; - margin-right: -5px; + .card-parent { + width: (100% / 3); + padding: 0 $gl-vert-padding ($gl-vert-padding * 2); + } + + .card { + cursor: pointer; + } } diff --git a/vendor/assets/javascripts/masonry.js b/vendor/assets/javascripts/masonry.js new file mode 100644 index 00000000000..adf62864671 --- /dev/null +++ b/vendor/assets/javascripts/masonry.js @@ -0,0 +1,2463 @@ +/*! + * Masonry PACKAGED v4.1.1 + * Cascading grid layout library + * http://masonry.desandro.com + * MIT License + * by David DeSandro + */ + +/** + * Bridget makes jQuery widgets + * v2.0.1 + * MIT license + */ + +/* jshint browser: true, strict: true, undef: true, unused: true */ + +( function( window, factory ) { + // universal module definition + /*jshint strict: false */ /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'jquery-bridget/jquery-bridget',[ 'jquery' ], function( jQuery ) { + return factory( window, jQuery ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('jquery') + ); + } else { + // browser global + window.jQueryBridget = factory( + window, + window.jQuery + ); + } + +}( window, function factory( window, jQuery ) { +'use strict'; + +// ----- utils ----- // + +var arraySlice = Array.prototype.slice; + +// helper function for logging errors +// $.error breaks jQuery chaining +var console = window.console; +var logError = typeof console == 'undefined' ? function() {} : + function( message ) { + console.error( message ); + }; + +// ----- jQueryBridget ----- // + +function jQueryBridget( namespace, PluginClass, $ ) { + $ = $ || jQuery || window.jQuery; + if ( !$ ) { + return; + } + + // add option method -> $().plugin('option', {...}) + if ( !PluginClass.prototype.option ) { + // option setter + PluginClass.prototype.option = function( opts ) { + // bail out if not an object + if ( !$.isPlainObject( opts ) ){ + return; + } + this.options = $.extend( true, this.options, opts ); + }; + } + + // make jQuery plugin + $.fn[ namespace ] = function( arg0 /*, arg1 */ ) { + if ( typeof arg0 == 'string' ) { + // method call $().plugin( 'methodName', { options } ) + // shift arguments by 1 + var args = arraySlice.call( arguments, 1 ); + return methodCall( this, arg0, args ); + } + // just $().plugin({ options }) + plainCall( this, arg0 ); + return this; + }; + + // $().plugin('methodName') + function methodCall( $elems, methodName, args ) { + var returnValue; + var pluginMethodStr = '$().' + namespace + '("' + methodName + '")'; + + $elems.each( function( i, elem ) { + // get instance + var instance = $.data( elem, namespace ); + if ( !instance ) { + logError( namespace + ' not initialized. Cannot call methods, i.e. ' + + pluginMethodStr ); + return; + } + + var method = instance[ methodName ]; + if ( !method || methodName.charAt(0) == '_' ) { + logError( pluginMethodStr + ' is not a valid method' ); + return; + } + + // apply method, get return value + var value = method.apply( instance, args ); + // set return value if value is returned, use only first value + returnValue = returnValue === undefined ? value : returnValue; + }); + + return returnValue !== undefined ? returnValue : $elems; + } + + function plainCall( $elems, options ) { + $elems.each( function( i, elem ) { + var instance = $.data( elem, namespace ); + if ( instance ) { + // set options & init + instance.option( options ); + instance._init(); + } else { + // initialize new instance + instance = new PluginClass( elem, options ); + $.data( elem, namespace, instance ); + } + }); + } + + updateJQuery( $ ); + +} + +// ----- updateJQuery ----- // + +// set $.bridget for v1 backwards compatibility +function updateJQuery( $ ) { + if ( !$ || ( $ && $.bridget ) ) { + return; + } + $.bridget = jQueryBridget; +} + +updateJQuery( jQuery || window.jQuery ); + +// ----- ----- // + +return jQueryBridget; + +})); + +/** + * EvEmitter v1.0.3 + * Lil' event emitter + * MIT License + */ + +/* jshint unused: true, undef: true, strict: true */ + +( function( global, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module, window */ + if ( typeof define == 'function' && define.amd ) { + // AMD - RequireJS + define( 'ev-emitter/ev-emitter',factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS - Browserify, Webpack + module.exports = factory(); + } else { + // Browser globals + global.EvEmitter = factory(); + } + +}( typeof window != 'undefined' ? window : this, function() { + + + +function EvEmitter() {} + +var proto = EvEmitter.prototype; + +proto.on = function( eventName, listener ) { + if ( !eventName || !listener ) { + return; + } + // set events hash + var events = this._events = this._events || {}; + // set listeners array + var listeners = events[ eventName ] = events[ eventName ] || []; + // only add once + if ( listeners.indexOf( listener ) == -1 ) { + listeners.push( listener ); + } + + return this; +}; + +proto.once = function( eventName, listener ) { + if ( !eventName || !listener ) { + return; + } + // add event + this.on( eventName, listener ); + // set once flag + // set onceEvents hash + var onceEvents = this._onceEvents = this._onceEvents || {}; + // set onceListeners object + var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {}; + // set flag + onceListeners[ listener ] = true; + + return this; +}; + +proto.off = function( eventName, listener ) { + var listeners = this._events && this._events[ eventName ]; + if ( !listeners || !listeners.length ) { + return; + } + var index = listeners.indexOf( listener ); + if ( index != -1 ) { + listeners.splice( index, 1 ); + } + + return this; +}; + +proto.emitEvent = function( eventName, args ) { + var listeners = this._events && this._events[ eventName ]; + if ( !listeners || !listeners.length ) { + return; + } + var i = 0; + var listener = listeners[i]; + args = args || []; + // once stuff + var onceListeners = this._onceEvents && this._onceEvents[ eventName ]; + + while ( listener ) { + var isOnce = onceListeners && onceListeners[ listener ]; + if ( isOnce ) { + // remove listener + // remove before trigger to prevent recursion + this.off( eventName, listener ); + // unset once flag + delete onceListeners[ listener ]; + } + // trigger listener + listener.apply( this, args ); + // get next listener + i += isOnce ? 0 : 1; + listener = listeners[i]; + } + + return this; +}; + +return EvEmitter; + +})); + +/*! + * getSize v2.0.2 + * measure size of elements + * MIT license + */ + +/*jshint browser: true, strict: true, undef: true, unused: true */ +/*global define: false, module: false, console: false */ + +( function( window, factory ) { + 'use strict'; + + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'get-size/get-size',[],function() { + return factory(); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory(); + } else { + // browser global + window.getSize = factory(); + } + +})( window, function factory() { +'use strict'; + +// -------------------------- helpers -------------------------- // + +// get a number from a string, not a percentage +function getStyleSize( value ) { + var num = parseFloat( value ); + // not a percent like '100%', and a number + var isValid = value.indexOf('%') == -1 && !isNaN( num ); + return isValid && num; +} + +function noop() {} + +var logError = typeof console == 'undefined' ? noop : + function( message ) { + console.error( message ); + }; + +// -------------------------- measurements -------------------------- // + +var measurements = [ + 'paddingLeft', + 'paddingRight', + 'paddingTop', + 'paddingBottom', + 'marginLeft', + 'marginRight', + 'marginTop', + 'marginBottom', + 'borderLeftWidth', + 'borderRightWidth', + 'borderTopWidth', + 'borderBottomWidth' +]; + +var measurementsLength = measurements.length; + +function getZeroSize() { + var size = { + width: 0, + height: 0, + innerWidth: 0, + innerHeight: 0, + outerWidth: 0, + outerHeight: 0 + }; + for ( var i=0; i < measurementsLength; i++ ) { + var measurement = measurements[i]; + size[ measurement ] = 0; + } + return size; +} + +// -------------------------- getStyle -------------------------- // + +/** + * getStyle, get style of element, check for Firefox bug + * https://bugzilla.mozilla.org/show_bug.cgi?id=548397 + */ +function getStyle( elem ) { + var style = getComputedStyle( elem ); + if ( !style ) { + logError( 'Style returned ' + style + + '. Are you running this code in a hidden iframe on Firefox? ' + + 'See http://bit.ly/getsizebug1' ); + } + return style; +} + +// -------------------------- setup -------------------------- // + +var isSetup = false; + +var isBoxSizeOuter; + +/** + * setup + * check isBoxSizerOuter + * do on first getSize() rather than on page load for Firefox bug + */ +function setup() { + // setup once + if ( isSetup ) { + return; + } + isSetup = true; + + // -------------------------- box sizing -------------------------- // + + /** + * WebKit measures the outer-width on style.width on border-box elems + * IE & Firefox<29 measures the inner-width + */ + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.padding = '1px 2px 3px 4px'; + div.style.borderStyle = 'solid'; + div.style.borderWidth = '1px 2px 3px 4px'; + div.style.boxSizing = 'border-box'; + + var body = document.body || document.documentElement; + body.appendChild( div ); + var style = getStyle( div ); + + getSize.isBoxSizeOuter = isBoxSizeOuter = getStyleSize( style.width ) == 200; + body.removeChild( div ); + +} + +// -------------------------- getSize -------------------------- // + +function getSize( elem ) { + setup(); + + // use querySeletor if elem is string + if ( typeof elem == 'string' ) { + elem = document.querySelector( elem ); + } + + // do not proceed on non-objects + if ( !elem || typeof elem != 'object' || !elem.nodeType ) { + return; + } + + var style = getStyle( elem ); + + // if hidden, everything is 0 + if ( style.display == 'none' ) { + return getZeroSize(); + } + + var size = {}; + size.width = elem.offsetWidth; + size.height = elem.offsetHeight; + + var isBorderBox = size.isBorderBox = style.boxSizing == 'border-box'; + + // get all measurements + for ( var i=0; i < measurementsLength; i++ ) { + var measurement = measurements[i]; + var value = style[ measurement ]; + var num = parseFloat( value ); + // any 'auto', 'medium' value will be 0 + size[ measurement ] = !isNaN( num ) ? num : 0; + } + + var paddingWidth = size.paddingLeft + size.paddingRight; + var paddingHeight = size.paddingTop + size.paddingBottom; + var marginWidth = size.marginLeft + size.marginRight; + var marginHeight = size.marginTop + size.marginBottom; + var borderWidth = size.borderLeftWidth + size.borderRightWidth; + var borderHeight = size.borderTopWidth + size.borderBottomWidth; + + var isBorderBoxSizeOuter = isBorderBox && isBoxSizeOuter; + + // overwrite width and height if we can get it from style + var styleWidth = getStyleSize( style.width ); + if ( styleWidth !== false ) { + size.width = styleWidth + + // add padding and border unless it's already including it + ( isBorderBoxSizeOuter ? 0 : paddingWidth + borderWidth ); + } + + var styleHeight = getStyleSize( style.height ); + if ( styleHeight !== false ) { + size.height = styleHeight + + // add padding and border unless it's already including it + ( isBorderBoxSizeOuter ? 0 : paddingHeight + borderHeight ); + } + + size.innerWidth = size.width - ( paddingWidth + borderWidth ); + size.innerHeight = size.height - ( paddingHeight + borderHeight ); + + size.outerWidth = size.width + marginWidth; + size.outerHeight = size.height + marginHeight; + + return size; +} + +return getSize; + +}); + +/** + * matchesSelector v2.0.1 + * matchesSelector( element, '.selector' ) + * MIT license + */ + +/*jshint browser: true, strict: true, undef: true, unused: true */ + +( function( window, factory ) { + /*global define: false, module: false */ + 'use strict'; + // universal module definition + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'desandro-matches-selector/matches-selector',factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory(); + } else { + // browser global + window.matchesSelector = factory(); + } + +}( window, function factory() { + 'use strict'; + + var matchesMethod = ( function() { + var ElemProto = Element.prototype; + // check for the standard method name first + if ( ElemProto.matches ) { + return 'matches'; + } + // check un-prefixed + if ( ElemProto.matchesSelector ) { + return 'matchesSelector'; + } + // check vendor prefixes + var prefixes = [ 'webkit', 'moz', 'ms', 'o' ]; + + for ( var i=0; i < prefixes.length; i++ ) { + var prefix = prefixes[i]; + var method = prefix + 'MatchesSelector'; + if ( ElemProto[ method ] ) { + return method; + } + } + })(); + + return function matchesSelector( elem, selector ) { + return elem[ matchesMethod ]( selector ); + }; + +})); + +/** + * Fizzy UI utils v2.0.2 + * MIT license + */ + +/*jshint browser: true, undef: true, unused: true, strict: true */ + +( function( window, factory ) { + // universal module definition + /*jshint strict: false */ /*globals define, module, require */ + + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'fizzy-ui-utils/utils',[ + 'desandro-matches-selector/matches-selector' + ], function( matchesSelector ) { + return factory( window, matchesSelector ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('desandro-matches-selector') + ); + } else { + // browser global + window.fizzyUIUtils = factory( + window, + window.matchesSelector + ); + } + +}( window, function factory( window, matchesSelector ) { + + + +var utils = {}; + +// ----- extend ----- // + +// extends objects +utils.extend = function( a, b ) { + for ( var prop in b ) { + a[ prop ] = b[ prop ]; + } + return a; +}; + +// ----- modulo ----- // + +utils.modulo = function( num, div ) { + return ( ( num % div ) + div ) % div; +}; + +// ----- makeArray ----- // + +// turn element or nodeList into an array +utils.makeArray = function( obj ) { + var ary = []; + if ( Array.isArray( obj ) ) { + // use object if already an array + ary = obj; + } else if ( obj && typeof obj.length == 'number' ) { + // convert nodeList to array + for ( var i=0; i < obj.length; i++ ) { + ary.push( obj[i] ); + } + } else { + // array of single index + ary.push( obj ); + } + return ary; +}; + +// ----- removeFrom ----- // + +utils.removeFrom = function( ary, obj ) { + var index = ary.indexOf( obj ); + if ( index != -1 ) { + ary.splice( index, 1 ); + } +}; + +// ----- getParent ----- // + +utils.getParent = function( elem, selector ) { + while ( elem != document.body ) { + elem = elem.parentNode; + if ( matchesSelector( elem, selector ) ) { + return elem; + } + } +}; + +// ----- getQueryElement ----- // + +// use element as selector string +utils.getQueryElement = function( elem ) { + if ( typeof elem == 'string' ) { + return document.querySelector( elem ); + } + return elem; +}; + +// ----- handleEvent ----- // + +// enable .ontype to trigger from .addEventListener( elem, 'type' ) +utils.handleEvent = function( event ) { + var method = 'on' + event.type; + if ( this[ method ] ) { + this[ method ]( event ); + } +}; + +// ----- filterFindElements ----- // + +utils.filterFindElements = function( elems, selector ) { + // make array of elems + elems = utils.makeArray( elems ); + var ffElems = []; + + elems.forEach( function( elem ) { + // check that elem is an actual element + if ( !( elem instanceof HTMLElement ) ) { + return; + } + // add elem if no selector + if ( !selector ) { + ffElems.push( elem ); + return; + } + // filter & find items if we have a selector + // filter + if ( matchesSelector( elem, selector ) ) { + ffElems.push( elem ); + } + // find children + var childElems = elem.querySelectorAll( selector ); + // concat childElems to filterFound array + for ( var i=0; i < childElems.length; i++ ) { + ffElems.push( childElems[i] ); + } + }); + + return ffElems; +}; + +// ----- debounceMethod ----- // + +utils.debounceMethod = function( _class, methodName, threshold ) { + // original method + var method = _class.prototype[ methodName ]; + var timeoutName = methodName + 'Timeout'; + + _class.prototype[ methodName ] = function() { + var timeout = this[ timeoutName ]; + if ( timeout ) { + clearTimeout( timeout ); + } + var args = arguments; + + var _this = this; + this[ timeoutName ] = setTimeout( function() { + method.apply( _this, args ); + delete _this[ timeoutName ]; + }, threshold || 100 ); + }; +}; + +// ----- docReady ----- // + +utils.docReady = function( callback ) { + var readyState = document.readyState; + if ( readyState == 'complete' || readyState == 'interactive' ) { + callback(); + } else { + document.addEventListener( 'DOMContentLoaded', callback ); + } +}; + +// ----- htmlInit ----- // + +// http://jamesroberts.name/blog/2010/02/22/string-functions-for-javascript-trim-to-camel-case-to-dashed-and-to-underscore/ +utils.toDashed = function( str ) { + return str.replace( /(.)([A-Z])/g, function( match, $1, $2 ) { + return $1 + '-' + $2; + }).toLowerCase(); +}; + +var console = window.console; +/** + * allow user to initialize classes via [data-namespace] or .js-namespace class + * htmlInit( Widget, 'widgetName' ) + * options are parsed from data-namespace-options + */ +utils.htmlInit = function( WidgetClass, namespace ) { + utils.docReady( function() { + var dashedNamespace = utils.toDashed( namespace ); + var dataAttr = 'data-' + dashedNamespace; + var dataAttrElems = document.querySelectorAll( '[' + dataAttr + ']' ); + var jsDashElems = document.querySelectorAll( '.js-' + dashedNamespace ); + var elems = utils.makeArray( dataAttrElems ) + .concat( utils.makeArray( jsDashElems ) ); + var dataOptionsAttr = dataAttr + '-options'; + var jQuery = window.jQuery; + + elems.forEach( function( elem ) { + var attr = elem.getAttribute( dataAttr ) || + elem.getAttribute( dataOptionsAttr ); + var options; + try { + options = attr && JSON.parse( attr ); + } catch ( error ) { + // log error, do not initialize + if ( console ) { + console.error( 'Error parsing ' + dataAttr + ' on ' + elem.className + + ': ' + error ); + } + return; + } + // initialize + var instance = new WidgetClass( elem, options ); + // make available via $().data('layoutname') + if ( jQuery ) { + jQuery.data( elem, namespace, instance ); + } + }); + + }); +}; + +// ----- ----- // + +return utils; + +})); + +/** + * Outlayer Item + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD - RequireJS + define( 'outlayer/item',[ + 'ev-emitter/ev-emitter', + 'get-size/get-size' + ], + factory + ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS - Browserify, Webpack + module.exports = factory( + require('ev-emitter'), + require('get-size') + ); + } else { + // browser global + window.Outlayer = {}; + window.Outlayer.Item = factory( + window.EvEmitter, + window.getSize + ); + } + +}( window, function factory( EvEmitter, getSize ) { +'use strict'; + +// ----- helpers ----- // + +function isEmptyObj( obj ) { + for ( var prop in obj ) { + return false; + } + prop = null; + return true; +} + +// -------------------------- CSS3 support -------------------------- // + + +var docElemStyle = document.documentElement.style; + +var transitionProperty = typeof docElemStyle.transition == 'string' ? + 'transition' : 'WebkitTransition'; +var transformProperty = typeof docElemStyle.transform == 'string' ? + 'transform' : 'WebkitTransform'; + +var transitionEndEvent = { + WebkitTransition: 'webkitTransitionEnd', + transition: 'transitionend' +}[ transitionProperty ]; + +// cache all vendor properties that could have vendor prefix +var vendorProperties = { + transform: transformProperty, + transition: transitionProperty, + transitionDuration: transitionProperty + 'Duration', + transitionProperty: transitionProperty + 'Property', + transitionDelay: transitionProperty + 'Delay' +}; + +// -------------------------- Item -------------------------- // + +function Item( element, layout ) { + if ( !element ) { + return; + } + + this.element = element; + // parent layout class, i.e. Masonry, Isotope, or Packery + this.layout = layout; + this.position = { + x: 0, + y: 0 + }; + + this._create(); +} + +// inherit EvEmitter +var proto = Item.prototype = Object.create( EvEmitter.prototype ); +proto.constructor = Item; + +proto._create = function() { + // transition objects + this._transn = { + ingProperties: {}, + clean: {}, + onEnd: {} + }; + + this.css({ + position: 'absolute' + }); +}; + +// trigger specified handler for event type +proto.handleEvent = function( event ) { + var method = 'on' + event.type; + if ( this[ method ] ) { + this[ method ]( event ); + } +}; + +proto.getSize = function() { + this.size = getSize( this.element ); +}; + +/** + * apply CSS styles to element + * @param {Object} style + */ +proto.css = function( style ) { + var elemStyle = this.element.style; + + for ( var prop in style ) { + // use vendor property if available + var supportedProp = vendorProperties[ prop ] || prop; + elemStyle[ supportedProp ] = style[ prop ]; + } +}; + + // measure position, and sets it +proto.getPosition = function() { + var style = getComputedStyle( this.element ); + var isOriginLeft = this.layout._getOption('originLeft'); + var isOriginTop = this.layout._getOption('originTop'); + var xValue = style[ isOriginLeft ? 'left' : 'right' ]; + var yValue = style[ isOriginTop ? 'top' : 'bottom' ]; + // convert percent to pixels + var layoutSize = this.layout.size; + var x = xValue.indexOf('%') != -1 ? + ( parseFloat( xValue ) / 100 ) * layoutSize.width : parseInt( xValue, 10 ); + var y = yValue.indexOf('%') != -1 ? + ( parseFloat( yValue ) / 100 ) * layoutSize.height : parseInt( yValue, 10 ); + + // clean up 'auto' or other non-integer values + x = isNaN( x ) ? 0 : x; + y = isNaN( y ) ? 0 : y; + // remove padding from measurement + x -= isOriginLeft ? layoutSize.paddingLeft : layoutSize.paddingRight; + y -= isOriginTop ? layoutSize.paddingTop : layoutSize.paddingBottom; + + this.position.x = x; + this.position.y = y; +}; + +// set settled position, apply padding +proto.layoutPosition = function() { + var layoutSize = this.layout.size; + var style = {}; + var isOriginLeft = this.layout._getOption('originLeft'); + var isOriginTop = this.layout._getOption('originTop'); + + // x + var xPadding = isOriginLeft ? 'paddingLeft' : 'paddingRight'; + var xProperty = isOriginLeft ? 'left' : 'right'; + var xResetProperty = isOriginLeft ? 'right' : 'left'; + + var x = this.position.x + layoutSize[ xPadding ]; + // set in percentage or pixels + style[ xProperty ] = this.getXValue( x ); + // reset other property + style[ xResetProperty ] = ''; + + // y + var yPadding = isOriginTop ? 'paddingTop' : 'paddingBottom'; + var yProperty = isOriginTop ? 'top' : 'bottom'; + var yResetProperty = isOriginTop ? 'bottom' : 'top'; + + var y = this.position.y + layoutSize[ yPadding ]; + // set in percentage or pixels + style[ yProperty ] = this.getYValue( y ); + // reset other property + style[ yResetProperty ] = ''; + + this.css( style ); + this.emitEvent( 'layout', [ this ] ); +}; + +proto.getXValue = function( x ) { + var isHorizontal = this.layout._getOption('horizontal'); + return this.layout.options.percentPosition && !isHorizontal ? + ( ( x / this.layout.size.width ) * 100 ) + '%' : x + 'px'; +}; + +proto.getYValue = function( y ) { + var isHorizontal = this.layout._getOption('horizontal'); + return this.layout.options.percentPosition && isHorizontal ? + ( ( y / this.layout.size.height ) * 100 ) + '%' : y + 'px'; +}; + +proto._transitionTo = function( x, y ) { + this.getPosition(); + // get current x & y from top/left + var curX = this.position.x; + var curY = this.position.y; + + var compareX = parseInt( x, 10 ); + var compareY = parseInt( y, 10 ); + var didNotMove = compareX === this.position.x && compareY === this.position.y; + + // save end position + this.setPosition( x, y ); + + // if did not move and not transitioning, just go to layout + if ( didNotMove && !this.isTransitioning ) { + this.layoutPosition(); + return; + } + + var transX = x - curX; + var transY = y - curY; + var transitionStyle = {}; + transitionStyle.transform = this.getTranslate( transX, transY ); + + this.transition({ + to: transitionStyle, + onTransitionEnd: { + transform: this.layoutPosition + }, + isCleaning: true + }); +}; + +proto.getTranslate = function( x, y ) { + // flip cooridinates if origin on right or bottom + var isOriginLeft = this.layout._getOption('originLeft'); + var isOriginTop = this.layout._getOption('originTop'); + x = isOriginLeft ? x : -x; + y = isOriginTop ? y : -y; + return 'translate3d(' + x + 'px, ' + y + 'px, 0)'; +}; + +// non transition + transform support +proto.goTo = function( x, y ) { + this.setPosition( x, y ); + this.layoutPosition(); +}; + +proto.moveTo = proto._transitionTo; + +proto.setPosition = function( x, y ) { + this.position.x = parseInt( x, 10 ); + this.position.y = parseInt( y, 10 ); +}; + +// ----- transition ----- // + +/** + * @param {Object} style - CSS + * @param {Function} onTransitionEnd + */ + +// non transition, just trigger callback +proto._nonTransition = function( args ) { + this.css( args.to ); + if ( args.isCleaning ) { + this._removeStyles( args.to ); + } + for ( var prop in args.onTransitionEnd ) { + args.onTransitionEnd[ prop ].call( this ); + } +}; + +/** + * proper transition + * @param {Object} args - arguments + * @param {Object} to - style to transition to + * @param {Object} from - style to start transition from + * @param {Boolean} isCleaning - removes transition styles after transition + * @param {Function} onTransitionEnd - callback + */ +proto.transition = function( args ) { + // redirect to nonTransition if no transition duration + if ( !parseFloat( this.layout.options.transitionDuration ) ) { + this._nonTransition( args ); + return; + } + + var _transition = this._transn; + // keep track of onTransitionEnd callback by css property + for ( var prop in args.onTransitionEnd ) { + _transition.onEnd[ prop ] = args.onTransitionEnd[ prop ]; + } + // keep track of properties that are transitioning + for ( prop in args.to ) { + _transition.ingProperties[ prop ] = true; + // keep track of properties to clean up when transition is done + if ( args.isCleaning ) { + _transition.clean[ prop ] = true; + } + } + + // set from styles + if ( args.from ) { + this.css( args.from ); + // force redraw. http://blog.alexmaccaw.com/css-transitions + var h = this.element.offsetHeight; + // hack for JSHint to hush about unused var + h = null; + } + // enable transition + this.enableTransition( args.to ); + // set styles that are transitioning + this.css( args.to ); + + this.isTransitioning = true; + +}; + +// dash before all cap letters, including first for +// WebkitTransform => -webkit-transform +function toDashedAll( str ) { + return str.replace( /([A-Z])/g, function( $1 ) { + return '-' + $1.toLowerCase(); + }); +} + +var transitionProps = 'opacity,' + toDashedAll( transformProperty ); + +proto.enableTransition = function(/* style */) { + // HACK changing transitionProperty during a transition + // will cause transition to jump + if ( this.isTransitioning ) { + return; + } + + // make `transition: foo, bar, baz` from style object + // HACK un-comment this when enableTransition can work + // while a transition is happening + // var transitionValues = []; + // for ( var prop in style ) { + // // dash-ify camelCased properties like WebkitTransition + // prop = vendorProperties[ prop ] || prop; + // transitionValues.push( toDashedAll( prop ) ); + // } + // munge number to millisecond, to match stagger + var duration = this.layout.options.transitionDuration; + duration = typeof duration == 'number' ? duration + 'ms' : duration; + // enable transition styles + this.css({ + transitionProperty: transitionProps, + transitionDuration: duration, + transitionDelay: this.staggerDelay || 0 + }); + // listen for transition end event + this.element.addEventListener( transitionEndEvent, this, false ); +}; + +// ----- events ----- // + +proto.onwebkitTransitionEnd = function( event ) { + this.ontransitionend( event ); +}; + +proto.onotransitionend = function( event ) { + this.ontransitionend( event ); +}; + +// properties that I munge to make my life easier +var dashedVendorProperties = { + '-webkit-transform': 'transform' +}; + +proto.ontransitionend = function( event ) { + // disregard bubbled events from children + if ( event.target !== this.element ) { + return; + } + var _transition = this._transn; + // get property name of transitioned property, convert to prefix-free + var propertyName = dashedVendorProperties[ event.propertyName ] || event.propertyName; + + // remove property that has completed transitioning + delete _transition.ingProperties[ propertyName ]; + // check if any properties are still transitioning + if ( isEmptyObj( _transition.ingProperties ) ) { + // all properties have completed transitioning + this.disableTransition(); + } + // clean style + if ( propertyName in _transition.clean ) { + // clean up style + this.element.style[ event.propertyName ] = ''; + delete _transition.clean[ propertyName ]; + } + // trigger onTransitionEnd callback + if ( propertyName in _transition.onEnd ) { + var onTransitionEnd = _transition.onEnd[ propertyName ]; + onTransitionEnd.call( this ); + delete _transition.onEnd[ propertyName ]; + } + + this.emitEvent( 'transitionEnd', [ this ] ); +}; + +proto.disableTransition = function() { + this.removeTransitionStyles(); + this.element.removeEventListener( transitionEndEvent, this, false ); + this.isTransitioning = false; +}; + +/** + * removes style property from element + * @param {Object} style +**/ +proto._removeStyles = function( style ) { + // clean up transition styles + var cleanStyle = {}; + for ( var prop in style ) { + cleanStyle[ prop ] = ''; + } + this.css( cleanStyle ); +}; + +var cleanTransitionStyle = { + transitionProperty: '', + transitionDuration: '', + transitionDelay: '' +}; + +proto.removeTransitionStyles = function() { + // remove transition + this.css( cleanTransitionStyle ); +}; + +// ----- stagger ----- // + +proto.stagger = function( delay ) { + delay = isNaN( delay ) ? 0 : delay; + this.staggerDelay = delay + 'ms'; +}; + +// ----- show/hide/remove ----- // + +// remove element from DOM +proto.removeElem = function() { + this.element.parentNode.removeChild( this.element ); + // remove display: none + this.css({ display: '' }); + this.emitEvent( 'remove', [ this ] ); +}; + +proto.remove = function() { + // just remove element if no transition support or no transition + if ( !transitionProperty || !parseFloat( this.layout.options.transitionDuration ) ) { + this.removeElem(); + return; + } + + // start transition + this.once( 'transitionEnd', function() { + this.removeElem(); + }); + this.hide(); +}; + +proto.reveal = function() { + delete this.isHidden; + // remove display: none + this.css({ display: '' }); + + var options = this.layout.options; + + var onTransitionEnd = {}; + var transitionEndProperty = this.getHideRevealTransitionEndProperty('visibleStyle'); + onTransitionEnd[ transitionEndProperty ] = this.onRevealTransitionEnd; + + this.transition({ + from: options.hiddenStyle, + to: options.visibleStyle, + isCleaning: true, + onTransitionEnd: onTransitionEnd + }); +}; + +proto.onRevealTransitionEnd = function() { + // check if still visible + // during transition, item may have been hidden + if ( !this.isHidden ) { + this.emitEvent('reveal'); + } +}; + +/** + * get style property use for hide/reveal transition end + * @param {String} styleProperty - hiddenStyle/visibleStyle + * @returns {String} + */ +proto.getHideRevealTransitionEndProperty = function( styleProperty ) { + var optionStyle = this.layout.options[ styleProperty ]; + // use opacity + if ( optionStyle.opacity ) { + return 'opacity'; + } + // get first property + for ( var prop in optionStyle ) { + return prop; + } +}; + +proto.hide = function() { + // set flag + this.isHidden = true; + // remove display: none + this.css({ display: '' }); + + var options = this.layout.options; + + var onTransitionEnd = {}; + var transitionEndProperty = this.getHideRevealTransitionEndProperty('hiddenStyle'); + onTransitionEnd[ transitionEndProperty ] = this.onHideTransitionEnd; + + this.transition({ + from: options.visibleStyle, + to: options.hiddenStyle, + // keep hidden stuff hidden + isCleaning: true, + onTransitionEnd: onTransitionEnd + }); +}; + +proto.onHideTransitionEnd = function() { + // check if still hidden + // during transition, item may have been un-hidden + if ( this.isHidden ) { + this.css({ display: 'none' }); + this.emitEvent('hide'); + } +}; + +proto.destroy = function() { + this.css({ + position: '', + left: '', + right: '', + top: '', + bottom: '', + transition: '', + transform: '' + }); +}; + +return Item; + +})); + +/*! + * Outlayer v2.1.0 + * the brains and guts of a layout library + * MIT license + */ + +( function( window, factory ) { + 'use strict'; + // universal module definition + /* jshint strict: false */ /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD - RequireJS + define( 'outlayer/outlayer',[ + 'ev-emitter/ev-emitter', + 'get-size/get-size', + 'fizzy-ui-utils/utils', + './item' + ], + function( EvEmitter, getSize, utils, Item ) { + return factory( window, EvEmitter, getSize, utils, Item); + } + ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS - Browserify, Webpack + module.exports = factory( + window, + require('ev-emitter'), + require('get-size'), + require('fizzy-ui-utils'), + require('./item') + ); + } else { + // browser global + window.Outlayer = factory( + window, + window.EvEmitter, + window.getSize, + window.fizzyUIUtils, + window.Outlayer.Item + ); + } + +}( window, function factory( window, EvEmitter, getSize, utils, Item ) { +'use strict'; + +// ----- vars ----- // + +var console = window.console; +var jQuery = window.jQuery; +var noop = function() {}; + +// -------------------------- Outlayer -------------------------- // + +// globally unique identifiers +var GUID = 0; +// internal store of all Outlayer intances +var instances = {}; + + +/** + * @param {Element, String} element + * @param {Object} options + * @constructor + */ +function Outlayer( element, options ) { + var queryElement = utils.getQueryElement( element ); + if ( !queryElement ) { + if ( console ) { + console.error( 'Bad element for ' + this.constructor.namespace + + ': ' + ( queryElement || element ) ); + } + return; + } + this.element = queryElement; + // add jQuery + if ( jQuery ) { + this.$element = jQuery( this.element ); + } + + // options + this.options = utils.extend( {}, this.constructor.defaults ); + this.option( options ); + + // add id for Outlayer.getFromElement + var id = ++GUID; + this.element.outlayerGUID = id; // expando + instances[ id ] = this; // associate via id + + // kick it off + this._create(); + + var isInitLayout = this._getOption('initLayout'); + if ( isInitLayout ) { + this.layout(); + } +} + +// settings are for internal use only +Outlayer.namespace = 'outlayer'; +Outlayer.Item = Item; + +// default options +Outlayer.defaults = { + containerStyle: { + position: 'relative' + }, + initLayout: true, + originLeft: true, + originTop: true, + resize: true, + resizeContainer: true, + // item options + transitionDuration: '0.4s', + hiddenStyle: { + opacity: 0, + transform: 'scale(0.001)' + }, + visibleStyle: { + opacity: 1, + transform: 'scale(1)' + } +}; + +var proto = Outlayer.prototype; +// inherit EvEmitter +utils.extend( proto, EvEmitter.prototype ); + +/** + * set options + * @param {Object} opts + */ +proto.option = function( opts ) { + utils.extend( this.options, opts ); +}; + +/** + * get backwards compatible option value, check old name + */ +proto._getOption = function( option ) { + var oldOption = this.constructor.compatOptions[ option ]; + return oldOption && this.options[ oldOption ] !== undefined ? + this.options[ oldOption ] : this.options[ option ]; +}; + +Outlayer.compatOptions = { + // currentName: oldName + initLayout: 'isInitLayout', + horizontal: 'isHorizontal', + layoutInstant: 'isLayoutInstant', + originLeft: 'isOriginLeft', + originTop: 'isOriginTop', + resize: 'isResizeBound', + resizeContainer: 'isResizingContainer' +}; + +proto._create = function() { + // get items from children + this.reloadItems(); + // elements that affect layout, but are not laid out + this.stamps = []; + this.stamp( this.options.stamp ); + // set container style + utils.extend( this.element.style, this.options.containerStyle ); + + // bind resize method + var canBindResize = this._getOption('resize'); + if ( canBindResize ) { + this.bindResize(); + } +}; + +// goes through all children again and gets bricks in proper order +proto.reloadItems = function() { + // collection of item elements + this.items = this._itemize( this.element.children ); +}; + + +/** + * turn elements into Outlayer.Items to be used in layout + * @param {Array or NodeList or HTMLElement} elems + * @returns {Array} items - collection of new Outlayer Items + */ +proto._itemize = function( elems ) { + + var itemElems = this._filterFindItemElements( elems ); + var Item = this.constructor.Item; + + // create new Outlayer Items for collection + var items = []; + for ( var i=0; i < itemElems.length; i++ ) { + var elem = itemElems[i]; + var item = new Item( elem, this ); + items.push( item ); + } + + return items; +}; + +/** + * get item elements to be used in layout + * @param {Array or NodeList or HTMLElement} elems + * @returns {Array} items - item elements + */ +proto._filterFindItemElements = function( elems ) { + return utils.filterFindElements( elems, this.options.itemSelector ); +}; + +/** + * getter method for getting item elements + * @returns {Array} elems - collection of item elements + */ +proto.getItemElements = function() { + return this.items.map( function( item ) { + return item.element; + }); +}; + +// ----- init & layout ----- // + +/** + * lays out all items + */ +proto.layout = function() { + this._resetLayout(); + this._manageStamps(); + + // don't animate first layout + var layoutInstant = this._getOption('layoutInstant'); + var isInstant = layoutInstant !== undefined ? + layoutInstant : !this._isLayoutInited; + this.layoutItems( this.items, isInstant ); + + // flag for initalized + this._isLayoutInited = true; +}; + +// _init is alias for layout +proto._init = proto.layout; + +/** + * logic before any new layout + */ +proto._resetLayout = function() { + this.getSize(); +}; + + +proto.getSize = function() { + this.size = getSize( this.element ); +}; + +/** + * get measurement from option, for columnWidth, rowHeight, gutter + * if option is String -> get element from selector string, & get size of element + * if option is Element -> get size of element + * else use option as a number + * + * @param {String} measurement + * @param {String} size - width or height + * @private + */ +proto._getMeasurement = function( measurement, size ) { + var option = this.options[ measurement ]; + var elem; + if ( !option ) { + // default to 0 + this[ measurement ] = 0; + } else { + // use option as an element + if ( typeof option == 'string' ) { + elem = this.element.querySelector( option ); + } else if ( option instanceof HTMLElement ) { + elem = option; + } + // use size of element, if element + this[ measurement ] = elem ? getSize( elem )[ size ] : option; + } +}; + +/** + * layout a collection of item elements + * @api public + */ +proto.layoutItems = function( items, isInstant ) { + items = this._getItemsForLayout( items ); + + this._layoutItems( items, isInstant ); + + this._postLayout(); +}; + +/** + * get the items to be laid out + * you may want to skip over some items + * @param {Array} items + * @returns {Array} items + */ +proto._getItemsForLayout = function( items ) { + return items.filter( function( item ) { + return !item.isIgnored; + }); +}; + +/** + * layout items + * @param {Array} items + * @param {Boolean} isInstant + */ +proto._layoutItems = function( items, isInstant ) { + this._emitCompleteOnItems( 'layout', items ); + + if ( !items || !items.length ) { + // no items, emit event with empty array + return; + } + + var queue = []; + + items.forEach( function( item ) { + // get x/y object from method + var position = this._getItemLayoutPosition( item ); + // enqueue + position.item = item; + position.isInstant = isInstant || item.isLayoutInstant; + queue.push( position ); + }, this ); + + this._processLayoutQueue( queue ); +}; + +/** + * get item layout position + * @param {Outlayer.Item} item + * @returns {Object} x and y position + */ +proto._getItemLayoutPosition = function( /* item */ ) { + return { + x: 0, + y: 0 + }; +}; + +/** + * iterate over array and position each item + * Reason being - separating this logic prevents 'layout invalidation' + * thx @paul_irish + * @param {Array} queue + */ +proto._processLayoutQueue = function( queue ) { + this.updateStagger(); + queue.forEach( function( obj, i ) { + this._positionItem( obj.item, obj.x, obj.y, obj.isInstant, i ); + }, this ); +}; + +// set stagger from option in milliseconds number +proto.updateStagger = function() { + var stagger = this.options.stagger; + if ( stagger === null || stagger === undefined ) { + this.stagger = 0; + return; + } + this.stagger = getMilliseconds( stagger ); + return this.stagger; +}; + +/** + * Sets position of item in DOM + * @param {Outlayer.Item} item + * @param {Number} x - horizontal position + * @param {Number} y - vertical position + * @param {Boolean} isInstant - disables transitions + */ +proto._positionItem = function( item, x, y, isInstant, i ) { + if ( isInstant ) { + // if not transition, just set CSS + item.goTo( x, y ); + } else { + item.stagger( i * this.stagger ); + item.moveTo( x, y ); + } +}; + +/** + * Any logic you want to do after each layout, + * i.e. size the container + */ +proto._postLayout = function() { + this.resizeContainer(); +}; + +proto.resizeContainer = function() { + var isResizingContainer = this._getOption('resizeContainer'); + if ( !isResizingContainer ) { + return; + } + var size = this._getContainerSize(); + if ( size ) { + this._setContainerMeasure( size.width, true ); + this._setContainerMeasure( size.height, false ); + } +}; + +/** + * Sets width or height of container if returned + * @returns {Object} size + * @param {Number} width + * @param {Number} height + */ +proto._getContainerSize = noop; + +/** + * @param {Number} measure - size of width or height + * @param {Boolean} isWidth + */ +proto._setContainerMeasure = function( measure, isWidth ) { + if ( measure === undefined ) { + return; + } + + var elemSize = this.size; + // add padding and border width if border box + if ( elemSize.isBorderBox ) { + measure += isWidth ? elemSize.paddingLeft + elemSize.paddingRight + + elemSize.borderLeftWidth + elemSize.borderRightWidth : + elemSize.paddingBottom + elemSize.paddingTop + + elemSize.borderTopWidth + elemSize.borderBottomWidth; + } + + measure = Math.max( measure, 0 ); + this.element.style[ isWidth ? 'width' : 'height' ] = measure + 'px'; +}; + +/** + * emit eventComplete on a collection of items events + * @param {String} eventName + * @param {Array} items - Outlayer.Items + */ +proto._emitCompleteOnItems = function( eventName, items ) { + var _this = this; + function onComplete() { + _this.dispatchEvent( eventName + 'Complete', null, [ items ] ); + } + + var count = items.length; + if ( !items || !count ) { + onComplete(); + return; + } + + var doneCount = 0; + function tick() { + doneCount++; + if ( doneCount == count ) { + onComplete(); + } + } + + // bind callback + items.forEach( function( item ) { + item.once( eventName, tick ); + }); +}; + +/** + * emits events via EvEmitter and jQuery events + * @param {String} type - name of event + * @param {Event} event - original event + * @param {Array} args - extra arguments + */ +proto.dispatchEvent = function( type, event, args ) { + // add original event to arguments + var emitArgs = event ? [ event ].concat( args ) : args; + this.emitEvent( type, emitArgs ); + + if ( jQuery ) { + // set this.$element + this.$element = this.$element || jQuery( this.element ); + if ( event ) { + // create jQuery event + var $event = jQuery.Event( event ); + $event.type = type; + this.$element.trigger( $event, args ); + } else { + // just trigger with type if no event available + this.$element.trigger( type, args ); + } + } +}; + +// -------------------------- ignore & stamps -------------------------- // + + +/** + * keep item in collection, but do not lay it out + * ignored items do not get skipped in layout + * @param {Element} elem + */ +proto.ignore = function( elem ) { + var item = this.getItem( elem ); + if ( item ) { + item.isIgnored = true; + } +}; + +/** + * return item to layout collection + * @param {Element} elem + */ +proto.unignore = function( elem ) { + var item = this.getItem( elem ); + if ( item ) { + delete item.isIgnored; + } +}; + +/** + * adds elements to stamps + * @param {NodeList, Array, Element, or String} elems + */ +proto.stamp = function( elems ) { + elems = this._find( elems ); + if ( !elems ) { + return; + } + + this.stamps = this.stamps.concat( elems ); + // ignore + elems.forEach( this.ignore, this ); +}; + +/** + * removes elements to stamps + * @param {NodeList, Array, or Element} elems + */ +proto.unstamp = function( elems ) { + elems = this._find( elems ); + if ( !elems ){ + return; + } + + elems.forEach( function( elem ) { + // filter out removed stamp elements + utils.removeFrom( this.stamps, elem ); + this.unignore( elem ); + }, this ); +}; + +/** + * finds child elements + * @param {NodeList, Array, Element, or String} elems + * @returns {Array} elems + */ +proto._find = function( elems ) { + if ( !elems ) { + return; + } + // if string, use argument as selector string + if ( typeof elems == 'string' ) { + elems = this.element.querySelectorAll( elems ); + } + elems = utils.makeArray( elems ); + return elems; +}; + +proto._manageStamps = function() { + if ( !this.stamps || !this.stamps.length ) { + return; + } + + this._getBoundingRect(); + + this.stamps.forEach( this._manageStamp, this ); +}; + +// update boundingLeft / Top +proto._getBoundingRect = function() { + // get bounding rect for container element + var boundingRect = this.element.getBoundingClientRect(); + var size = this.size; + this._boundingRect = { + left: boundingRect.left + size.paddingLeft + size.borderLeftWidth, + top: boundingRect.top + size.paddingTop + size.borderTopWidth, + right: boundingRect.right - ( size.paddingRight + size.borderRightWidth ), + bottom: boundingRect.bottom - ( size.paddingBottom + size.borderBottomWidth ) + }; +}; + +/** + * @param {Element} stamp +**/ +proto._manageStamp = noop; + +/** + * get x/y position of element relative to container element + * @param {Element} elem + * @returns {Object} offset - has left, top, right, bottom + */ +proto._getElementOffset = function( elem ) { + var boundingRect = elem.getBoundingClientRect(); + var thisRect = this._boundingRect; + var size = getSize( elem ); + var offset = { + left: boundingRect.left - thisRect.left - size.marginLeft, + top: boundingRect.top - thisRect.top - size.marginTop, + right: thisRect.right - boundingRect.right - size.marginRight, + bottom: thisRect.bottom - boundingRect.bottom - size.marginBottom + }; + return offset; +}; + +// -------------------------- resize -------------------------- // + +// enable event handlers for listeners +// i.e. resize -> onresize +proto.handleEvent = utils.handleEvent; + +/** + * Bind layout to window resizing + */ +proto.bindResize = function() { + window.addEventListener( 'resize', this ); + this.isResizeBound = true; +}; + +/** + * Unbind layout to window resizing + */ +proto.unbindResize = function() { + window.removeEventListener( 'resize', this ); + this.isResizeBound = false; +}; + +proto.onresize = function() { + this.resize(); +}; + +utils.debounceMethod( Outlayer, 'onresize', 100 ); + +proto.resize = function() { + // don't trigger if size did not change + // or if resize was unbound. See #9 + if ( !this.isResizeBound || !this.needsResizeLayout() ) { + return; + } + + this.layout(); +}; + +/** + * check if layout is needed post layout + * @returns Boolean + */ +proto.needsResizeLayout = function() { + var size = getSize( this.element ); + // check that this.size and size are there + // IE8 triggers resize on body size change, so they might not be + var hasSizes = this.size && size; + return hasSizes && size.innerWidth !== this.size.innerWidth; +}; + +// -------------------------- methods -------------------------- // + +/** + * add items to Outlayer instance + * @param {Array or NodeList or Element} elems + * @returns {Array} items - Outlayer.Items +**/ +proto.addItems = function( elems ) { + var items = this._itemize( elems ); + // add items to collection + if ( items.length ) { + this.items = this.items.concat( items ); + } + return items; +}; + +/** + * Layout newly-appended item elements + * @param {Array or NodeList or Element} elems + */ +proto.appended = function( elems ) { + var items = this.addItems( elems ); + if ( !items.length ) { + return; + } + // layout and reveal just the new items + this.layoutItems( items, true ); + this.reveal( items ); +}; + +/** + * Layout prepended elements + * @param {Array or NodeList or Element} elems + */ +proto.prepended = function( elems ) { + var items = this._itemize( elems ); + if ( !items.length ) { + return; + } + // add items to beginning of collection + var previousItems = this.items.slice(0); + this.items = items.concat( previousItems ); + // start new layout + this._resetLayout(); + this._manageStamps(); + // layout new stuff without transition + this.layoutItems( items, true ); + this.reveal( items ); + // layout previous items + this.layoutItems( previousItems ); +}; + +/** + * reveal a collection of items + * @param {Array of Outlayer.Items} items + */ +proto.reveal = function( items ) { + this._emitCompleteOnItems( 'reveal', items ); + if ( !items || !items.length ) { + return; + } + var stagger = this.updateStagger(); + items.forEach( function( item, i ) { + item.stagger( i * stagger ); + item.reveal(); + }); +}; + +/** + * hide a collection of items + * @param {Array of Outlayer.Items} items + */ +proto.hide = function( items ) { + this._emitCompleteOnItems( 'hide', items ); + if ( !items || !items.length ) { + return; + } + var stagger = this.updateStagger(); + items.forEach( function( item, i ) { + item.stagger( i * stagger ); + item.hide(); + }); +}; + +/** + * reveal item elements + * @param {Array}, {Element}, {NodeList} items + */ +proto.revealItemElements = function( elems ) { + var items = this.getItems( elems ); + this.reveal( items ); +}; + +/** + * hide item elements + * @param {Array}, {Element}, {NodeList} items + */ +proto.hideItemElements = function( elems ) { + var items = this.getItems( elems ); + this.hide( items ); +}; + +/** + * get Outlayer.Item, given an Element + * @param {Element} elem + * @param {Function} callback + * @returns {Outlayer.Item} item + */ +proto.getItem = function( elem ) { + // loop through items to get the one that matches + for ( var i=0; i < this.items.length; i++ ) { + var item = this.items[i]; + if ( item.element == elem ) { + // return item + return item; + } + } +}; + +/** + * get collection of Outlayer.Items, given Elements + * @param {Array} elems + * @returns {Array} items - Outlayer.Items + */ +proto.getItems = function( elems ) { + elems = utils.makeArray( elems ); + var items = []; + elems.forEach( function( elem ) { + var item = this.getItem( elem ); + if ( item ) { + items.push( item ); + } + }, this ); + + return items; +}; + +/** + * remove element(s) from instance and DOM + * @param {Array or NodeList or Element} elems + */ +proto.remove = function( elems ) { + var removeItems = this.getItems( elems ); + + this._emitCompleteOnItems( 'remove', removeItems ); + + // bail if no items to remove + if ( !removeItems || !removeItems.length ) { + return; + } + + removeItems.forEach( function( item ) { + item.remove(); + // remove item from collection + utils.removeFrom( this.items, item ); + }, this ); +}; + +// ----- destroy ----- // + +// remove and disable Outlayer instance +proto.destroy = function() { + // clean up dynamic styles + var style = this.element.style; + style.height = ''; + style.position = ''; + style.width = ''; + // destroy items + this.items.forEach( function( item ) { + item.destroy(); + }); + + this.unbindResize(); + + var id = this.element.outlayerGUID; + delete instances[ id ]; // remove reference to instance by id + delete this.element.outlayerGUID; + // remove data for jQuery + if ( jQuery ) { + jQuery.removeData( this.element, this.constructor.namespace ); + } + +}; + +// -------------------------- data -------------------------- // + +/** + * get Outlayer instance from element + * @param {Element} elem + * @returns {Outlayer} + */ +Outlayer.data = function( elem ) { + elem = utils.getQueryElement( elem ); + var id = elem && elem.outlayerGUID; + return id && instances[ id ]; +}; + + +// -------------------------- create Outlayer class -------------------------- // + +/** + * create a layout class + * @param {String} namespace + */ +Outlayer.create = function( namespace, options ) { + // sub-class Outlayer + var Layout = subclass( Outlayer ); + // apply new options and compatOptions + Layout.defaults = utils.extend( {}, Outlayer.defaults ); + utils.extend( Layout.defaults, options ); + Layout.compatOptions = utils.extend( {}, Outlayer.compatOptions ); + + Layout.namespace = namespace; + + Layout.data = Outlayer.data; + + // sub-class Item + Layout.Item = subclass( Item ); + + // -------------------------- declarative -------------------------- // + + utils.htmlInit( Layout, namespace ); + + // -------------------------- jQuery bridge -------------------------- // + + // make into jQuery plugin + if ( jQuery && jQuery.bridget ) { + jQuery.bridget( namespace, Layout ); + } + + return Layout; +}; + +function subclass( Parent ) { + function SubClass() { + Parent.apply( this, arguments ); + } + + SubClass.prototype = Object.create( Parent.prototype ); + SubClass.prototype.constructor = SubClass; + + return SubClass; +} + +// ----- helpers ----- // + +// how many milliseconds are in each unit +var msUnits = { + ms: 1, + s: 1000 +}; + +// munge time-like parameter into millisecond number +// '0.4s' -> 40 +function getMilliseconds( time ) { + if ( typeof time == 'number' ) { + return time; + } + var matches = time.match( /(^\d*\.?\d*)(\w*)/ ); + var num = matches && matches[1]; + var unit = matches && matches[2]; + if ( !num.length ) { + return 0; + } + num = parseFloat( num ); + var mult = msUnits[ unit ] || 1; + return num * mult; +} + +// ----- fin ----- // + +// back in global +Outlayer.Item = Item; + +return Outlayer; + +})); + +/*! + * Masonry v4.1.1 + * Cascading grid layout library + * http://masonry.desandro.com + * MIT License + * by David DeSandro + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /*globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( [ + 'outlayer/outlayer', + 'get-size/get-size' + ], + factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + require('outlayer'), + require('get-size') + ); + } else { + // browser global + window.Masonry = factory( + window.Outlayer, + window.getSize + ); + } + +}( window, function factory( Outlayer, getSize ) { + + + +// -------------------------- masonryDefinition -------------------------- // + + // create an Outlayer layout class + var Masonry = Outlayer.create('masonry'); + // isFitWidth -> fitWidth + Masonry.compatOptions.fitWidth = 'isFitWidth'; + + Masonry.prototype._resetLayout = function() { + this.getSize(); + this._getMeasurement( 'columnWidth', 'outerWidth' ); + this._getMeasurement( 'gutter', 'outerWidth' ); + this.measureColumns(); + + // reset column Y + this.colYs = []; + for ( var i=0; i < this.cols; i++ ) { + this.colYs.push( 0 ); + } + + this.maxY = 0; + }; + + Masonry.prototype.measureColumns = function() { + this.getContainerWidth(); + // if columnWidth is 0, default to outerWidth of first item + if ( !this.columnWidth ) { + var firstItem = this.items[0]; + var firstItemElem = firstItem && firstItem.element; + // columnWidth fall back to item of first element + this.columnWidth = firstItemElem && getSize( firstItemElem ).outerWidth || + // if first elem has no width, default to size of container + this.containerWidth; + } + + var columnWidth = this.columnWidth += this.gutter; + + // calculate columns + var containerWidth = this.containerWidth + this.gutter; + var cols = containerWidth / columnWidth; + // fix rounding errors, typically with gutters + var excess = columnWidth - containerWidth % columnWidth; + // if overshoot is less than a pixel, round up, otherwise floor it + var mathMethod = excess && excess < 1 ? 'round' : 'floor'; + cols = Math[ mathMethod ]( cols ); + this.cols = Math.max( cols, 1 ); + }; + + Masonry.prototype.getContainerWidth = function() { + // container is parent if fit width + var isFitWidth = this._getOption('fitWidth'); + var container = isFitWidth ? this.element.parentNode : this.element; + // check that this.size and size are there + // IE8 triggers resize on body size change, so they might not be + var size = getSize( container ); + this.containerWidth = size && size.innerWidth; + }; + + Masonry.prototype._getItemLayoutPosition = function( item ) { + item.getSize(); + // how many columns does this brick span + var remainder = item.size.outerWidth % this.columnWidth; + var mathMethod = remainder && remainder < 1 ? 'round' : 'ceil'; + // round if off by 1 pixel, otherwise use ceil + var colSpan = Math[ mathMethod ]( item.size.outerWidth / this.columnWidth ); + colSpan = Math.min( colSpan, this.cols ); + + var colGroup = this._getColGroup( colSpan ); + // get the minimum Y value from the columns + var minimumY = Math.min.apply( Math, colGroup ); + var shortColIndex = colGroup.indexOf( minimumY ); + + // position the brick + var position = { + x: this.columnWidth * shortColIndex, + y: minimumY + }; + + // apply setHeight to necessary columns + var setHeight = minimumY + item.size.outerHeight; + var setSpan = this.cols + 1 - colGroup.length; + for ( var i = 0; i < setSpan; i++ ) { + this.colYs[ shortColIndex + i ] = setHeight; + } + + return position; + }; + + /** + * @param {Number} colSpan - number of columns the element spans + * @returns {Array} colGroup + */ + Masonry.prototype._getColGroup = function( colSpan ) { + if ( colSpan < 2 ) { + // if brick spans only one column, use all the column Ys + return this.colYs; + } + + var colGroup = []; + // how many different places could this brick fit horizontally + var groupCount = this.cols + 1 - colSpan; + // for each group potential horizontal position + for ( var i = 0; i < groupCount; i++ ) { + // make an array of colY values for that one group + var groupColYs = this.colYs.slice( i, i + colSpan ); + // and get the max value of the array + colGroup[i] = Math.max.apply( Math, groupColYs ); + } + return colGroup; + }; + + Masonry.prototype._manageStamp = function( stamp ) { + var stampSize = getSize( stamp ); + var offset = this._getElementOffset( stamp ); + // get the columns that this stamp affects + var isOriginLeft = this._getOption('originLeft'); + var firstX = isOriginLeft ? offset.left : offset.right; + var lastX = firstX + stampSize.outerWidth; + var firstCol = Math.floor( firstX / this.columnWidth ); + firstCol = Math.max( 0, firstCol ); + var lastCol = Math.floor( lastX / this.columnWidth ); + // lastCol should not go over if multiple of columnWidth #425 + lastCol -= lastX % this.columnWidth ? 0 : 1; + lastCol = Math.min( this.cols - 1, lastCol ); + // set colYs to bottom of the stamp + + var isOriginTop = this._getOption('originTop'); + var stampMaxY = ( isOriginTop ? offset.top : offset.bottom ) + + stampSize.outerHeight; + for ( var i = firstCol; i <= lastCol; i++ ) { + this.colYs[i] = Math.max( stampMaxY, this.colYs[i] ); + } + }; + + Masonry.prototype._getContainerSize = function() { + this.maxY = Math.max.apply( Math, this.colYs ); + var size = { + height: this.maxY + }; + + if ( this._getOption('fitWidth') ) { + size.width = this._getContainerFitWidth(); + } + + return size; + }; + + Masonry.prototype._getContainerFitWidth = function() { + var unusedCols = 0; + // count unused columns + var i = this.cols; + while ( --i ) { + if ( this.colYs[i] !== 0 ) { + break; + } + unusedCols++; + } + // fit container to columns that have been used + return ( this.cols - unusedCols ) * this.columnWidth - this.gutter; + }; + + Masonry.prototype.needsResizeLayout = function() { + var previousWidth = this.containerWidth; + this.getContainerWidth(); + return previousWidth != this.containerWidth; + }; + + return Masonry; + +})); + -- cgit v1.2.1 From 48b3623fb5688bb5dfc3f1a5e2436c29afc968a2 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 24 Jan 2017 15:41:53 +0000 Subject: Selected issues show with checkmark Submit button disables/enables Submit button selects ids - but does not push it yet --- .../boards/components/modal/footer.js.es6 | 27 ++++++++++++-- .../boards/components/modal/list.js.es6 | 29 ++++++++++++--- .../boards/components/modal/search.js.es6 | 42 +++++++++++++++++++--- .../boards/components/modal/tabs.js.es6 | 13 +++---- .../javascripts/boards/stores/boards_store.js.es6 | 13 ++++++- app/assets/stylesheets/pages/boards.scss | 41 +++++++++++++++++++-- 6 files changed, 141 insertions(+), 24 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 9cb48448a87..dec0196a32c 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -7,19 +7,40 @@ gl.issueBoards.ModalFooter = Vue.extend({ data() { - return Store.modal; + return Object.assign({}, Store.modal, { + disabled: false, + }); + }, + computed: { + submitDisabled() { + if (this.disabled) return true; + + return !Store.modalSelectedCount(); + }, + submitText() { + const count = Store.modalSelectedCount(); + + return `Add ${count} issue${count > 1 || !count ? 's' : ''}`; + }, }, methods: { hideModal() { this.showAddIssuesModal = false; }, + addIssues() { + const issueIds = this.issues.filter(issue => issue.selected).map(issue => issue.id); + + this.disabled = true; + }, }, template: ` <footer class="form-actions add-issues-footer"> <button class="btn btn-success pull-left" - type="button"> - Add issues + type="button" + :disabled="submitDisabled" + @click="addIssues"> + {{ submitText }} </button> <button class="btn btn-default pull-right" diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index e06fa58b3b6..e700161f642 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -1,6 +1,8 @@ /* global Vue */ /* global ListIssue */ +/* global Masonry */ (() => { + let listMasonry; const Store = gl.issueBoards.BoardsStore; window.gl = window.gl || {}; @@ -22,9 +24,14 @@ loading() { return this.issues.length === 0; }, + selectedCount() { + return Store.modalSelectedCount(); + }, }, methods: { - toggleIssue(issue) { + toggleIssue(issueObj) { + const issue = issueObj; + issue.selected = !issue.selected; }, showIssue(issue) { @@ -41,7 +48,7 @@ if (listMasonry) { listMasonry.destroy(); } - } + }, }, mounted() { gl.boardService.getBacklog() @@ -66,9 +73,11 @@ }, template: ` <section class="add-issues-list"> - <i - class="fa fa-spinner fa-spin" - v-if="loading"></i> + <div + class="add-issues-list-loading" + v-if="loading"> + <i class="fa fa-spinner fa-spin"></i> + </div> <div class="add-issues-list-columns list-unstyled" ref="list" @@ -85,9 +94,19 @@ :issue="issue" :issue-link-base="'/'"> </issue-card-inner> + <span + v-if="issue.selected" + class="issue-card-selected"> + <i class="fa fa-check"></i> + </span> </div> </div> </div> + <p + class="all-issues-selected-empty" + v-if="activeTab == 'selected' && selectedCount == 0"> + You don't have any issues selected, <a href="#" @click="activeTab = 'all'">select some</a>. + </p> </section> `, }); diff --git a/app/assets/javascripts/boards/components/modal/search.js.es6 b/app/assets/javascripts/boards/components/modal/search.js.es6 index 714c9240d4d..59aeb17baa5 100644 --- a/app/assets/javascripts/boards/components/modal/search.js.es6 +++ b/app/assets/javascripts/boards/components/modal/search.js.es6 @@ -1,14 +1,48 @@ /* global Vue */ (() => { + const Store = gl.issueBoards.BoardsStore; + window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.ModalSearch = Vue.extend({ + data() { + return Store.modal; + }, + computed: { + selectAllText() { + if (Store.modalSelectedCount() !== this.issues.length || this.issues.length === 0) { + return 'Select all'; + } + + return 'Un-select all'; + }, + }, + methods: { + toggleAll() { + const select = Store.modalSelectedCount() !== this.issues.length; + + this.issues.forEach((issue) => { + const issueUpdate = issue; + issueUpdate.selected = select; + }); + }, + }, template: ` - <input - placeholder="Search issues..." - class="form-control" - type="search" /> + <div + class="add-issues-search" + v-if="activeTab == 'all'"> + <input + placeholder="Search issues..." + class="form-control" + type="search" /> + <button + type="button" + class="btn btn-success btn-inverted" + @click="toggleAll"> + {{ selectAllText }} + </button> + </div> `, }); })(); diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 index 58fb75f839f..a1da7840036 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js.es6 +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -16,17 +16,12 @@ }, computed: { selectedCount() { - let count = 0; - - this.issues.forEach((issue) => { - if (issue.selected) { - count += 1; - } - }); - - return count; + return Store.modalSelectedCount(); }, }, + destroyed() { + this.activeTab = 'all'; + }, template: ` <div class="top-area"> <ul class="nav-links issues-state-filters"> diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 42216d429c6..4c5eb57f5c9 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -125,6 +125,17 @@ }, updateFiltersUrl () { history.pushState(null, null, `?${$.param(this.state.filters)}`); - } + }, + modalSelectedCount() { + let count = 0; + + this.modal.issues.forEach((issue) => { + if (issue.selected) { + count += 1; + } + }); + + return count; + }, }; })(); diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index f41d4cdd45d..73ed8dd8ab1 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -390,19 +390,34 @@ .top-area { margin-bottom: 10px; } +} - .form-control { - margin-bottom: 10px; +.add-issues-search { + display: flex; + margin-bottom: 10px; + + .btn { + margin-left: 10px; } } .add-issues-list { + display: flex; flex: 1; margin-left: -$gl-vert-padding; margin-right: -$gl-vert-padding; overflow-y: scroll; } +.add-issues-list-loading { + align-self: center; + width: 100%; + padding-left: $gl-vert-padding; + padding-right: $gl-vert-padding; + font-size: 35px; + text-align: center; +} + .add-issues-footer { margin-top: auto; margin-left: -15px; @@ -412,6 +427,9 @@ } .add-issues-list-columns { + width: 100%; + padding-top: 3px; + .card-parent { width: (100% / 3); padding: 0 $gl-vert-padding ($gl-vert-padding * 2); @@ -421,3 +439,22 @@ cursor: pointer; } } + +.all-issues-selected-empty { + align-self: center; + margin-bottom: 0; +} + +.issue-card-selected { + position: absolute; + right: -3px; + top: -3px; + width: 20px; + height: 20px; + background-color: $blue-dark; + color: $white-light; + font-size: 12px; + text-align: center; + line-height: 20px; + border-radius: 50%; +} -- cgit v1.2.1 From 7d0feab3abd1fa6459b9be81b340a9f9121d84b7 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 24 Jan 2017 15:51:15 +0000 Subject: Fixes layout issue on selected tab Fixed modal not closing on cancel clicked --- app/assets/javascripts/boards/components/modal/footer.js.es6 | 9 +++++---- app/assets/javascripts/boards/components/modal/list.js.es6 | 10 ++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index dec0196a32c..aadb754fd9e 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -7,9 +7,10 @@ gl.issueBoards.ModalFooter = Vue.extend({ data() { - return Object.assign({}, Store.modal, { + return { + store: Store.modal, disabled: false, - }); + }; }, computed: { submitDisabled() { @@ -25,10 +26,10 @@ }, methods: { hideModal() { - this.showAddIssuesModal = false; + this.store.showAddIssuesModal = false; }, addIssues() { - const issueIds = this.issues.filter(issue => issue.selected).map(issue => issue.id); + const issueIds = this.store.issues.filter(issue => issue.selected).map(issue => issue.id); this.disabled = true; }, diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index e700161f642..067a6322bca 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -19,6 +19,16 @@ this.initMasonry(); }); }, + issues: { + handler() { + if (this.activeTab === 'selected') { + this.$nextTick(() => { + listMasonry.layout(); + }); + } + }, + deep: true, + } }, computed: { loading() { -- cgit v1.2.1 From e786ba98904eeb7ce252b28aa631ef959e7af8c2 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 25 Jan 2017 08:48:29 +0000 Subject: Mobile sizes --- app/assets/stylesheets/pages/boards.scss | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 73ed8dd8ab1..cd70a14bec5 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -373,6 +373,7 @@ flex-direction: column; width: 90vw; height: 85vh; + min-height: 500px; margin-left: auto; margin-right: auto; padding: 25px 15px 0; @@ -431,11 +432,20 @@ padding-top: 3px; .card-parent { - width: (100% / 3); + width: 100%; padding: 0 $gl-vert-padding ($gl-vert-padding * 2); + + @media (min-width: $screen-sm-min) { + width: 50%; + } + + @media (min-width: $screen-md-min) { + width: (100% / 3); + } } .card { + border: 1px solid $border-gray-dark; cursor: pointer; } } -- cgit v1.2.1 From 97fbb3d1ff1d22aa8597ff0bad0520634d9e351e Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 25 Jan 2017 11:33:45 +0000 Subject: Added selectable list before adding issues Adds issues to list on frontend - need to push to backend --- app/assets/javascripts/boards/boards_bundle.js.es6 | 6 +++ .../boards/components/modal/footer.js.es6 | 31 ++++++++--- .../boards/components/modal/lists_dropdown.js.es6 | 61 ++++++++++++++++++++++ .../javascripts/boards/stores/boards_store.js.es6 | 3 +- app/assets/stylesheets/pages/boards.scss | 7 +++ 5 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index ef73d381fb4..67d9f023866 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -71,6 +71,12 @@ $(() => { Store.addBlankState(); this.loading = false; + + if (this.state.lists.length > 0) { + Store.modal.selectedList = this.state.lists[0]; + } + + Store.modal.showAddIssuesModal = true; }); } }); diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index aadb754fd9e..e9f400952b7 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -1,3 +1,4 @@ +//= require ./lists_dropdown /* global Vue */ (() => { const Store = gl.issueBoards.BoardsStore; @@ -31,18 +32,34 @@ addIssues() { const issueIds = this.store.issues.filter(issue => issue.selected).map(issue => issue.id); + issueIds.forEach((id) => { + const issue = this.store.issues.filter(issue => issue.id == id)[0]; + this.store.selectedList.addIssue(issue); + this.store.selectedList.issuesSize += 1; + }); + this.disabled = true; + this.hideModal(); }, }, + components: { + 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, + }, template: ` <footer class="form-actions add-issues-footer"> - <button - class="btn btn-success pull-left" - type="button" - :disabled="submitDisabled" - @click="addIssues"> - {{ submitText }} - </button> + <div class="pull-left"> + <button + class="btn btn-success" + type="button" + :disabled="submitDisabled" + @click="addIssues"> + {{ submitText }} + </button> + <span class="add-issues-footer-to-list"> + to list + </span> + <lists-dropdown></lists-dropdown> + </div> <button class="btn btn-default pull-right" type="button" diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 new file mode 100644 index 00000000000..27da5057083 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 @@ -0,0 +1,61 @@ +/* global Vue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ + data() { + return { + modal: Store.modal, + state: Store.state, + } + }, + computed: { + selected() { + return this.modal.selectedList; + }, + }, + methods: { + selectList(list) { + this.modal.selectedList = list; + }, + }, + template: ` + <div class="dropdown inline"> + <button + class="dropdown-menu-toggle" + type="button" + data-toggle="dropdown" + aria-expanded="false"> + {{ selected.title }} + <span + class="dropdown-label-box pull-right" + :style="{ backgroundColor: selected.label.color }"> + </span> + <i class="fa fa-chevron-down"></i> + </button> + <div class="dropdown-menu dropdown-menu-selectable"> + <ul> + <li + v-for="list in state.lists" + v-if="list.type == 'label'"> + <a + href="#" + role="button" + :class="{ 'is-active': list.id == selected.id }" + @click="selectList(list)"> + <span + class="dropdown-label-box" + :style="{ backgroundColor: list.label.color }"> + </span> + {{ list.title }} + </a> + </li> + </ul> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 4c5eb57f5c9..fa0224beee5 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -14,8 +14,9 @@ }, modal: { issues: [], - showAddIssuesModal: true, + showAddIssuesModal: false, activeTab: 'all', + selectedList: {} }, moving: { issue: {}, diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index cd70a14bec5..bf1c0d1842b 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -427,6 +427,13 @@ padding-right: 15px; } +.add-issues-footer-to-list { + display: inline-block; + padding-left: 6px; + padding-right: 6px; + line-height: 34px; +} + .add-issues-list-columns { width: 100%; padding-top: 3px; -- cgit v1.2.1 From 38d84c1396b5264ba171df109c873469d371be73 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 25 Jan 2017 13:07:44 +0000 Subject: Changed where data gets loaded Changed what is hidden when data is loading --- .../boards/components/modal/footer.js.es6 | 13 ++++----- .../boards/components/modal/header.js.es6 | 18 +++++++------ .../boards/components/modal/list.js.es6 | 31 ++++------------------ .../javascripts/boards/components/modal/modal.es6 | 19 ++++++++++++- .../boards/services/board_service.js.es6 | 12 ++++++++- app/assets/stylesheets/pages/boards.scss | 5 ++++ .../projects/boards/lists_controller.rb | 4 +++ app/controllers/projects/boards_controller.rb | 2 +- config/routes/project.rb | 3 ++- 9 files changed, 63 insertions(+), 44 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index e9f400952b7..978900c866a 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -10,13 +10,10 @@ data() { return { store: Store.modal, - disabled: false, }; }, computed: { submitDisabled() { - if (this.disabled) return true; - return !Store.modalSelectedCount(); }, submitText() { @@ -30,15 +27,19 @@ this.store.showAddIssuesModal = false; }, addIssues() { + const list = this.store.selectedList; const issueIds = this.store.issues.filter(issue => issue.selected).map(issue => issue.id); + // Post the data to the backend + gl.boardService.addMultipleIssues(list, issueIds); + + // Add the issues on the frontend issueIds.forEach((id) => { const issue = this.store.issues.filter(issue => issue.id == id)[0]; - this.store.selectedList.addIssue(issue); - this.store.selectedList.issuesSize += 1; + list.addIssue(issue); + list.issuesSize += 1; }); - this.disabled = true; this.hideModal(); }, }, diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index 74681fa4bcf..f4c722c0974 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -18,14 +18,16 @@ 'modal-search': gl.issueBoards.ModalSearch, }, template: ` - <header class="add-issues-header"> - <h2> - Add issues to board - <modal-dismiss></modal-dismiss> - </h2> - <modal-tabs></modal-tabs> - <modal-search></modal-search> - </header> + <div> + <header class="add-issues-header form-actions"> + <h2> + Add issues + <modal-dismiss></modal-dismiss> + </h2> + </header> + <modal-tabs v-if="issues.length"></modal-tabs> + <modal-search v-if="issues.length"></modal-search> + </div> `, }); })(); diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 067a6322bca..e2056f1ada3 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -21,11 +21,11 @@ }, issues: { handler() { - if (this.activeTab === 'selected') { - this.$nextTick(() => { + this.$nextTick(() => { + if (this.activeTab === 'selected') { listMasonry.layout(); - }); - } + } + }); }, deep: true, } @@ -61,18 +61,7 @@ }, }, mounted() { - gl.boardService.getBacklog() - .then((res) => { - const data = res.json(); - - data.forEach((issueObj) => { - this.issues.push(new ListIssue(issueObj)); - }); - - this.$nextTick(() => { - this.initMasonry(); - }); - }); + this.initMasonry(); }, destroyed() { this.issues = []; @@ -83,11 +72,6 @@ }, template: ` <section class="add-issues-list"> - <div - class="add-issues-list-loading" - v-if="loading"> - <i class="fa fa-spinner fa-spin"></i> - </div> <div class="add-issues-list-columns list-unstyled" ref="list" @@ -112,11 +96,6 @@ </div> </div> </div> - <p - class="all-issues-selected-empty" - v-if="activeTab == 'selected' && selectedCount == 0"> - You don't have any issues selected, <a href="#" @click="activeTab = 'all'">select some</a>. - </p> </section> `, }); diff --git a/app/assets/javascripts/boards/components/modal/modal.es6 b/app/assets/javascripts/boards/components/modal/modal.es6 index bedd7c9735a..7a15beb10f0 100644 --- a/app/assets/javascripts/boards/components/modal/modal.es6 +++ b/app/assets/javascripts/boards/components/modal/modal.es6 @@ -12,6 +12,16 @@ data() { return Store.modal; }, + mounted() { + gl.boardService.getBacklog() + .then((res) => { + const data = res.json(); + + data.forEach((issueObj) => { + this.issues.push(new ListIssue(issueObj)); + }); + }); + }, components: { 'modal-header': gl.issueBoards.IssuesModalHeader, 'modal-list': gl.issueBoards.ModalList, @@ -23,7 +33,14 @@ v-if="showAddIssuesModal"> <div class="add-issues-container"> <modal-header></modal-header> - <modal-list></modal-list> + <modal-list v-if="issues.length"></modal-list> + <section + class="add-issues-list" + v-if="issues.length == 0"> + <div class="add-issues-list-loading"> + <i class="fa fa-spinner fa-spin"></i> + </div> + </section> <modal-footer></modal-footer> </div> </div> diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index 4625b13d5f3..335950caf10 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -13,7 +13,11 @@ class BoardService { generate: { method: 'POST', url: `${root}/${boardId}/lists/generate.json` - } + }, + multiple: { + method: 'POST', + url: `${root}/${boardId}/lists{/id}/multiple` + }, }); this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}); @@ -75,6 +79,12 @@ class BoardService { getBacklog() { return this.boards.backlog(); } + + addMultipleIssues(list, issue_ids) { + return this.lists.multiple(list.id, { + issue_ids, + }); + } } window.BoardService = BoardService; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index bf1c0d1842b..683ef9f2388 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -383,6 +383,10 @@ } .add-issues-header { + margin: -25px -15px -5px; + border-top: 0; + border-bottom: 1px solid $border-color; + > h2 { margin: 0; font-size: 18px; @@ -396,6 +400,7 @@ .add-issues-search { display: flex; margin-bottom: 10px; + margin-top: 10px; .btn { margin-left: 10px; diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb index 67e3c9add81..ed65cae82dc 100644 --- a/app/controllers/projects/boards/lists_controller.rb +++ b/app/controllers/projects/boards/lists_controller.rb @@ -50,6 +50,10 @@ module Projects end end + def multiple + head :ok + end + private def authorize_admin_list! diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 7a2e2324323..9e699fb9b3b 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -33,7 +33,7 @@ class Projects::BoardsController < Projects::ApplicationController LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") .where(label_id: board.lists.movable.pluck(:label_id)).limit(1).arel.exists ) - @issues = @issues.page(params[:page]) + @issues = @issues.page(params[:page]).per(50) render json: @issues.as_json( labels: true, diff --git a/config/routes/project.rb b/config/routes/project.rb index 5c33bb4a6ca..afd895a5a1e 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -267,13 +267,14 @@ constraints(ProjectUrlConstrainer.new) do resources :boards, only: [:index, :show] do get :backlog, on: :member - + scope module: :boards do resources :issues, only: [:update] resources :lists, only: [:index, :create, :update, :destroy] do collection do post :generate + post :multiple end resources :issues, only: [:index, :create] -- cgit v1.2.1 From acd86c120f012b38108971ec466c6c141405f1d1 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 25 Jan 2017 13:54:45 +0000 Subject: Search bar now returns data Selected data is added into a different array --- .../boards/components/modal/footer.js.es6 | 2 +- .../boards/components/modal/header.js.es6 | 4 +-- .../boards/components/modal/list.js.es6 | 23 ++++++++++---- .../javascripts/boards/components/modal/modal.es6 | 35 ++++++++++++++++++---- .../boards/components/modal/search.js.es6 | 15 ++++++++-- .../boards/services/board_service.js.es6 | 4 +-- .../javascripts/boards/stores/boards_store.js.es6 | 24 +++++---------- 7 files changed, 72 insertions(+), 35 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 978900c866a..cacc8d69a3c 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -28,7 +28,7 @@ }, addIssues() { const list = this.store.selectedList; - const issueIds = this.store.issues.filter(issue => issue.selected).map(issue => issue.id); + const issueIds = this.store.selectedIssues.map(issue => issue.id); // Post the data to the backend gl.boardService.addMultipleIssues(list, issueIds); diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index f4c722c0974..a89b2bbc515 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -25,8 +25,8 @@ <modal-dismiss></modal-dismiss> </h2> </header> - <modal-tabs v-if="issues.length"></modal-tabs> - <modal-search v-if="issues.length"></modal-search> + <modal-tabs v-if="!loading"></modal-tabs> + <modal-search v-if="!loading"></modal-search> </div> `, }); diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index e2056f1ada3..3520c49699a 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -22,9 +22,7 @@ issues: { handler() { this.$nextTick(() => { - if (this.activeTab === 'selected') { - listMasonry.layout(); - } + listMasonry.layout(); }); }, deep: true, @@ -37,12 +35,26 @@ selectedCount() { return Store.modalSelectedCount(); }, + loopIssues() { + if (this.activeTab === 'all') { + return this.issues; + } + + return this.selectedIssues; + }, }, methods: { toggleIssue(issueObj) { const issue = issueObj; - issue.selected = !issue.selected; + + if (issue.selected) { + this.selectedIssues.push(issue); + } else { + // Remove this issue + const index = this.selectedIssues.indexOf(issue); + this.selectedIssues.splice(index, 1); + } }, showIssue(issue) { if (this.activeTab === 'all') return true; @@ -64,7 +76,6 @@ this.initMasonry(); }, destroyed() { - this.issues = []; this.destroyMasonry(); }, components: { @@ -77,7 +88,7 @@ ref="list" v-show="!loading"> <div - v-for="issue in issues" + v-for="issue in loopIssues" v-if="showIssue(issue)" class="card-parent"> <div diff --git a/app/assets/javascripts/boards/components/modal/modal.es6 b/app/assets/javascripts/boards/components/modal/modal.es6 index 7a15beb10f0..8d593459935 100644 --- a/app/assets/javascripts/boards/components/modal/modal.es6 +++ b/app/assets/javascripts/boards/components/modal/modal.es6 @@ -12,15 +12,40 @@ data() { return Store.modal; }, + watch: { + searchTerm() { + this.searchOperation(); + }, + }, mounted() { - gl.boardService.getBacklog() - .then((res) => { + this.loading = true; + + this.loadIssues() + .then(() => { + this.loading = false; + }); + }, + methods: { + searchOperation: _.debounce(function() { + this.loadIssues(); + }, 500), + loadIssues() { + return gl.boardService.getBacklog({ + search: this.searchTerm, + }).then((res) => { const data = res.json(); + this.issues = []; data.forEach((issueObj) => { - this.issues.push(new ListIssue(issueObj)); + const issue = new ListIssue(issueObj); + const foundSelectedIssue = this.selectedIssues + .filter(filteredIssue => filteredIssue.id === issue.id)[0]; + issue.selected = foundSelectedIssue !== undefined; + + this.issues.push(issue); }); }); + }, }, components: { 'modal-header': gl.issueBoards.IssuesModalHeader, @@ -33,10 +58,10 @@ v-if="showAddIssuesModal"> <div class="add-issues-container"> <modal-header></modal-header> - <modal-list v-if="issues.length"></modal-list> + <modal-list v-if="!loading"></modal-list> <section class="add-issues-list" - v-if="issues.length == 0"> + v-if="loading"> <div class="add-issues-list-loading"> <i class="fa fa-spinner fa-spin"></i> </div> diff --git a/app/assets/javascripts/boards/components/modal/search.js.es6 b/app/assets/javascripts/boards/components/modal/search.js.es6 index 59aeb17baa5..8c3814740f1 100644 --- a/app/assets/javascripts/boards/components/modal/search.js.es6 +++ b/app/assets/javascripts/boards/components/modal/search.js.es6 @@ -24,7 +24,17 @@ this.issues.forEach((issue) => { const issueUpdate = issue; - issueUpdate.selected = select; + + if (issueUpdate.selected !== select) { + issueUpdate.selected = select; + + if (select) { + this.selectedIssues.push(issueUpdate); + } else { + const index = this.selectedIssues.indexOf(issue); + this.selectedIssues.splice(index, 1); + } + } }); }, }, @@ -35,7 +45,8 @@ <input placeholder="Search issues..." class="form-control" - type="search" /> + type="search" + v-model="searchTerm" /> <button type="button" class="btn btn-success btn-inverted" diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index 335950caf10..c8db107d890 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -76,8 +76,8 @@ class BoardService { }); } - getBacklog() { - return this.boards.backlog(); + getBacklog(data) { + return this.boards.backlog(data); } addMultipleIssues(list, issue_ids) { diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index fa0224beee5..d29731ad0aa 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -14,9 +14,12 @@ }, modal: { issues: [], + selectedIssues: [], showAddIssuesModal: false, activeTab: 'all', - selectedList: {} + selectedList: {}, + searchTerm: '', + loading: false, }, moving: { issue: {}, @@ -40,15 +43,10 @@ }, new (listObj) { const list = this.addList(listObj); - const backlogList = this.findList('type', 'backlog', 'backlog'); list .save() .then(() => { - // Remove any new issues from the backlog - // as they will be visible in the new list - list.issues.forEach(backlogList.removeIssue.bind(backlogList)); - this.state.lists = _.sortBy(this.state.lists, 'position'); }); this.removeBlankState(); @@ -58,7 +56,7 @@ }, shouldAddBlankState () { // Decide whether to add the blank state - return !(this.state.lists.filter(list => list.type !== 'backlog' && list.type !== 'done')[0]); + return !(this.state.lists.filter(list => list.type !== 'done')[0]); }, addBlankState () { if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return; @@ -108,7 +106,7 @@ listTo.addIssue(issue, listFrom, newIndex); } - if (listTo.type === 'done' && listFrom.type !== 'backlog') { + if (listTo.type === 'done') { issueLists.forEach((list) => { list.removeIssue(issue); }); @@ -128,15 +126,7 @@ history.pushState(null, null, `?${$.param(this.state.filters)}`); }, modalSelectedCount() { - let count = 0; - - this.modal.issues.forEach((issue) => { - if (issue.selected) { - count += 1; - } - }); - - return count; + return this.modal.selectedIssues.length; }, }; })(); -- cgit v1.2.1 From 240d8c8d307e4675e72d854f4d70d722075c02c4 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 25 Jan 2017 14:15:48 +0000 Subject: Created a modal store --- .../boards/components/modal/dismiss.js.es6 | 4 +- .../boards/components/modal/footer.js.es6 | 18 +++---- .../boards/components/modal/header.js.es6 | 4 +- .../boards/components/modal/list.js.es6 | 42 +++++---------- .../boards/components/modal/lists_dropdown.js.es6 | 2 +- .../javascripts/boards/components/modal/modal.es6 | 7 ++- .../boards/components/modal/search.js.es6 | 25 ++------- .../boards/components/modal/tabs.js.es6 | 6 +-- .../javascripts/boards/stores/modal_store.js.es6 | 63 ++++++++++++++++++++++ 9 files changed, 98 insertions(+), 73 deletions(-) create mode 100644 app/assets/javascripts/boards/stores/modal_store.js.es6 diff --git a/app/assets/javascripts/boards/components/modal/dismiss.js.es6 b/app/assets/javascripts/boards/components/modal/dismiss.js.es6 index b5027f004c6..0483b4b412d 100644 --- a/app/assets/javascripts/boards/components/modal/dismiss.js.es6 +++ b/app/assets/javascripts/boards/components/modal/dismiss.js.es6 @@ -1,13 +1,13 @@ /* global Vue */ (() => { - const Store = gl.issueBoards.BoardsStore; + const ModalStore = gl.issueBoards.ModalStore; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.DismissModal = Vue.extend({ data() { - return Store.modal; + return ModalStore.globalStore; }, methods: { toggleModal(toggle) { diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index cacc8d69a3c..66414d96100 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -1,41 +1,39 @@ //= require ./lists_dropdown /* global Vue */ (() => { - const Store = gl.issueBoards.BoardsStore; + const ModalStore = gl.issueBoards.ModalStore; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.ModalFooter = Vue.extend({ data() { - return { - store: Store.modal, - }; + return ModalStore.globalStore; }, computed: { submitDisabled() { - return !Store.modalSelectedCount(); + return !ModalStore.selectedCount(); }, submitText() { - const count = Store.modalSelectedCount(); + const count = ModalStore.selectedCount(); return `Add ${count} issue${count > 1 || !count ? 's' : ''}`; }, }, methods: { hideModal() { - this.store.showAddIssuesModal = false; + this.showAddIssuesModal = false; }, addIssues() { - const list = this.store.selectedList; - const issueIds = this.store.selectedIssues.map(issue => issue.id); + const list = this.selectedList; + const issueIds = this.selectedIssues.map(issue => issue.id); // Post the data to the backend gl.boardService.addMultipleIssues(list, issueIds); // Add the issues on the frontend issueIds.forEach((id) => { - const issue = this.store.issues.filter(issue => issue.id == id)[0]; + const issue = this.issues.filter(fIssue => fIssue.id === id)[0]; list.addIssue(issue); list.issuesSize += 1; }); diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index a89b2bbc515..cab08076b15 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -3,14 +3,14 @@ //= require ./search /* global Vue */ (() => { - const Store = gl.issueBoards.BoardsStore; + const ModalStore = gl.issueBoards.ModalStore; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.IssuesModalHeader = Vue.extend({ data() { - return Store.modal; + return ModalStore.globalStore; }, components: { 'modal-dismiss': gl.issueBoards.DismissModal, diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 3520c49699a..0a9da849ef9 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -3,38 +3,27 @@ /* global Masonry */ (() => { let listMasonry; - const Store = gl.issueBoards.BoardsStore; + const ModalStore = gl.issueBoards.ModalStore; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.ModalList = Vue.extend({ data() { - return Store.modal; + return ModalStore.globalStore; }, watch: { activeTab() { - this.$nextTick(() => { - this.destroyMasonry(); - this.initMasonry(); - }); + this.initMasonry(); }, issues: { handler() { - this.$nextTick(() => { - listMasonry.layout(); - }); + this.initMasonry(); }, deep: true, - } + }, }, computed: { - loading() { - return this.issues.length === 0; - }, - selectedCount() { - return Store.modalSelectedCount(); - }, loopIssues() { if (this.activeTab === 'all') { return this.issues; @@ -44,31 +33,24 @@ }, }, methods: { - toggleIssue(issueObj) { - const issue = issueObj; - issue.selected = !issue.selected; - - if (issue.selected) { - this.selectedIssues.push(issue); - } else { - // Remove this issue - const index = this.selectedIssues.indexOf(issue); - this.selectedIssues.splice(index, 1); - } - }, + toggleIssue: ModalStore.toggleIssue.bind(ModalStore), showIssue(issue) { if (this.activeTab === 'all') return true; return issue.selected; }, initMasonry() { - listMasonry = new Masonry(this.$refs.list, { - transitionDuration: 0, + this.$nextTick(() => { + this.destroyMasonry(); + listMasonry = new Masonry(this.$refs.list, { + transitionDuration: 0, + }); }); }, destroyMasonry() { if (listMasonry) { listMasonry.destroy(); + listMasonry = undefined; } }, }, diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 index 27da5057083..a147bd0d2aa 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 @@ -10,7 +10,7 @@ return { modal: Store.modal, state: Store.state, - } + }; }, computed: { selected() { diff --git a/app/assets/javascripts/boards/components/modal/modal.es6 b/app/assets/javascripts/boards/components/modal/modal.es6 index 8d593459935..501f493a1d8 100644 --- a/app/assets/javascripts/boards/components/modal/modal.es6 +++ b/app/assets/javascripts/boards/components/modal/modal.es6 @@ -3,14 +3,14 @@ //= require ./footer /* global Vue */ (() => { - const Store = gl.issueBoards.BoardsStore; + const ModalStore = gl.issueBoards.ModalStore; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.IssuesModal = Vue.extend({ data() { - return Store.modal; + return ModalStore.globalStore; }, watch: { searchTerm() { @@ -38,8 +38,7 @@ this.issues = []; data.forEach((issueObj) => { const issue = new ListIssue(issueObj); - const foundSelectedIssue = this.selectedIssues - .filter(filteredIssue => filteredIssue.id === issue.id)[0]; + const foundSelectedIssue = ModalStore.findSelectedIssue(issue); issue.selected = foundSelectedIssue !== undefined; this.issues.push(issue); diff --git a/app/assets/javascripts/boards/components/modal/search.js.es6 b/app/assets/javascripts/boards/components/modal/search.js.es6 index 8c3814740f1..6753656ec4a 100644 --- a/app/assets/javascripts/boards/components/modal/search.js.es6 +++ b/app/assets/javascripts/boards/components/modal/search.js.es6 @@ -1,17 +1,17 @@ /* global Vue */ (() => { - const Store = gl.issueBoards.BoardsStore; + const ModalStore = gl.issueBoards.ModalStore; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.ModalSearch = Vue.extend({ data() { - return Store.modal; + return ModalStore.globalStore; }, computed: { selectAllText() { - if (Store.modalSelectedCount() !== this.issues.length || this.issues.length === 0) { + if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { return 'Select all'; } @@ -19,24 +19,7 @@ }, }, methods: { - toggleAll() { - const select = Store.modalSelectedCount() !== this.issues.length; - - this.issues.forEach((issue) => { - const issueUpdate = issue; - - if (issueUpdate.selected !== select) { - issueUpdate.selected = select; - - if (select) { - this.selectedIssues.push(issueUpdate); - } else { - const index = this.selectedIssues.indexOf(issue); - this.selectedIssues.splice(index, 1); - } - } - }); - }, + toggleAll: ModalStore.toggleAll.bind(ModalStore), }, template: ` <div diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 index a1da7840036..2730893df76 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js.es6 +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -1,13 +1,13 @@ /* global Vue */ (() => { - const Store = gl.issueBoards.BoardsStore; + const ModalStore = gl.issueBoards.ModalStore; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.ModalTabs = Vue.extend({ data() { - return Store.modal; + return ModalStore.globalStore; }, methods: { changeTab(tab) { @@ -16,7 +16,7 @@ }, computed: { selectedCount() { - return Store.modalSelectedCount(); + return ModalStore.selectedCount(); }, }, destroyed() { diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 new file mode 100644 index 00000000000..68f5e57a154 --- /dev/null +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -0,0 +1,63 @@ +(() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + class ModalStore { + constructor() { + this.globalStore = gl.issueBoards.BoardsStore.modal; + } + + selectedCount() { + return this.globalStore.selectedIssues.length; + } + + toggleIssue(issueObj) { + const issue = issueObj; + issue.selected = !issue.selected; + + if (issue.selected) { + this.addSelectedIssue(issue); + } else { + this.removeSelectedIssue(issue); + } + } + + toggleAll() { + const select = this.selectedCount() !== this.globalStore.issues.length; + + this.globalStore.issues.forEach((issue) => { + const issueUpdate = issue; + + if (issueUpdate.selected !== select) { + issueUpdate.selected = select; + + if (select) { + this.addSelectedIssue(issue); + } else { + this.removeSelectedIssue(issue); + } + } + }); + } + + addSelectedIssue(issue) { + this.globalStore.selectedIssues.push(issue); + } + + removeSelectedIssue(issue) { + const index = this.selectedIssueIndex(issue); + this.globalStore.selectedIssues.splice(index, 1); + } + + selectedIssueIndex(issue) { + return this.globalStore.selectedIssues.indexOf(issue); + } + + findSelectedIssue(issue) { + return this.globalStore.selectedIssues + .filter(filteredIssue => filteredIssue.id === issue.id)[0]; + } + } + + gl.issueBoards.ModalStore = new ModalStore(); +})(); -- cgit v1.2.1 From 6adb6caed9c6a25aa68ee9ceb37419543bb49e23 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 25 Jan 2017 16:53:09 +0000 Subject: Started tests --- app/assets/javascripts/boards/boards_bundle.js.es6 | 9 +- .../boards/components/modal/dismiss.js.es6 | 2 +- .../boards/components/modal/footer.js.es6 | 2 +- .../boards/components/modal/header.js.es6 | 6 +- .../boards/components/modal/index.js.es6 | 73 +++++++ .../boards/components/modal/list.js.es6 | 2 +- .../boards/components/modal/lists_dropdown.js.es6 | 3 +- .../javascripts/boards/components/modal/modal.es6 | 73 ------- .../boards/components/modal/search.js.es6 | 2 +- .../boards/components/modal/tabs.js.es6 | 2 +- .../javascripts/boards/stores/boards_store.js.es6 | 9 - .../javascripts/boards/stores/modal_store.js.es6 | 24 ++- spec/features/boards/add_issues_modal_spec.rb | 210 +++++++++++++++++++++ 13 files changed, 313 insertions(+), 104 deletions(-) create mode 100644 app/assets/javascripts/boards/components/modal/index.js.es6 delete mode 100644 app/assets/javascripts/boards/components/modal/modal.es6 create mode 100644 spec/features/boards/add_issues_modal_spec.rb diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 67d9f023866..e2b3d2c7df6 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -14,12 +14,13 @@ //= require ./components/board //= require ./components/board_sidebar //= require ./components/new_list_dropdown -//= require ./components/modal/modal +//= require ./components/modal/index //= require ./vue_resource_interceptor $(() => { const $boardApp = document.getElementById('board-app'); const Store = gl.issueBoards.BoardsStore; + const ModalStore = gl.issueBoards.ModalStore; window.gl = window.gl || {}; @@ -73,10 +74,8 @@ $(() => { this.loading = false; if (this.state.lists.length > 0) { - Store.modal.selectedList = this.state.lists[0]; + ModalStore.store.selectedList = this.state.lists[0]; } - - Store.modal.showAddIssuesModal = true; }); } }); @@ -97,6 +96,6 @@ $(() => { .on('click', '.js-show-add-issues', (e) => { e.preventDefault(); - Store.modal.showAddIssuesModal = true; + ModalStore.store.showAddIssuesModal = true; }); }); diff --git a/app/assets/javascripts/boards/components/modal/dismiss.js.es6 b/app/assets/javascripts/boards/components/modal/dismiss.js.es6 index 0483b4b412d..cf952837d39 100644 --- a/app/assets/javascripts/boards/components/modal/dismiss.js.es6 +++ b/app/assets/javascripts/boards/components/modal/dismiss.js.es6 @@ -7,7 +7,7 @@ gl.issueBoards.DismissModal = Vue.extend({ data() { - return ModalStore.globalStore; + return ModalStore.store; }, methods: { toggleModal(toggle) { diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 66414d96100..e4d49b914bd 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -8,7 +8,7 @@ gl.issueBoards.ModalFooter = Vue.extend({ data() { - return ModalStore.globalStore; + return ModalStore.store; }, computed: { submitDisabled() { diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index cab08076b15..6896a0fcf45 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -1,7 +1,7 @@ -//= require ./dismiss +/* global Vue */ //= require ./tabs +//= require ./dismiss //= require ./search -/* global Vue */ (() => { const ModalStore = gl.issueBoards.ModalStore; @@ -10,7 +10,7 @@ gl.issueBoards.IssuesModalHeader = Vue.extend({ data() { - return ModalStore.globalStore; + return ModalStore.store; }, components: { 'modal-dismiss': gl.issueBoards.DismissModal, diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 new file mode 100644 index 00000000000..693182fe37e --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -0,0 +1,73 @@ +/* global Vue */ +//= require ./header +//= require ./list +//= require ./footer +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.IssuesModal = Vue.extend({ + data() { + return ModalStore.store; + }, + watch: { + searchTerm() { + this.searchOperation(); + }, + }, + mounted() { + this.loading = true; + + this.loadIssues() + .then(() => { + this.loading = false; + }); + }, + methods: { + searchOperation: _.debounce(function() { + this.loadIssues(); + }, 500), + loadIssues() { + return gl.boardService.getBacklog({ + search: this.searchTerm, + }).then((res) => { + const data = res.json(); + + this.issues = []; + data.forEach((issueObj) => { + const issue = new ListIssue(issueObj); + const foundSelectedIssue = ModalStore.findSelectedIssue(issue); + issue.selected = foundSelectedIssue !== undefined; + + this.issues.push(issue); + }); + }); + }, + }, + components: { + 'modal-header': gl.issueBoards.IssuesModalHeader, + 'modal-list': gl.issueBoards.ModalList, + 'modal-footer': gl.issueBoards.ModalFooter, + }, + template: ` + <div + class="add-issues-modal" + v-if="showAddIssuesModal"> + <div class="add-issues-container"> + <modal-header></modal-header> + <modal-list v-if="!loading"></modal-list> + <section + class="add-issues-list" + v-if="loading"> + <div class="add-issues-list-loading"> + <i class="fa fa-spinner fa-spin"></i> + </div> + </section> + <modal-footer></modal-footer> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 0a9da849ef9..55e3ff3c7f4 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -10,7 +10,7 @@ gl.issueBoards.ModalList = Vue.extend({ data() { - return ModalStore.globalStore; + return ModalStore.store; }, watch: { activeTab() { diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 index a147bd0d2aa..e68ad30500c 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 @@ -1,5 +1,6 @@ /* global Vue */ (() => { + const ModalStore = gl.issueBoards.ModalStore; const Store = gl.issueBoards.BoardsStore; window.gl = window.gl || {}; @@ -8,7 +9,7 @@ gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ data() { return { - modal: Store.modal, + modal: ModalStore.store, state: Store.state, }; }, diff --git a/app/assets/javascripts/boards/components/modal/modal.es6 b/app/assets/javascripts/boards/components/modal/modal.es6 deleted file mode 100644 index 501f493a1d8..00000000000 --- a/app/assets/javascripts/boards/components/modal/modal.es6 +++ /dev/null @@ -1,73 +0,0 @@ -//= require ./header -//= require ./list -//= require ./footer -/* global Vue */ -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.IssuesModal = Vue.extend({ - data() { - return ModalStore.globalStore; - }, - watch: { - searchTerm() { - this.searchOperation(); - }, - }, - mounted() { - this.loading = true; - - this.loadIssues() - .then(() => { - this.loading = false; - }); - }, - methods: { - searchOperation: _.debounce(function() { - this.loadIssues(); - }, 500), - loadIssues() { - return gl.boardService.getBacklog({ - search: this.searchTerm, - }).then((res) => { - const data = res.json(); - - this.issues = []; - data.forEach((issueObj) => { - const issue = new ListIssue(issueObj); - const foundSelectedIssue = ModalStore.findSelectedIssue(issue); - issue.selected = foundSelectedIssue !== undefined; - - this.issues.push(issue); - }); - }); - }, - }, - components: { - 'modal-header': gl.issueBoards.IssuesModalHeader, - 'modal-list': gl.issueBoards.ModalList, - 'modal-footer': gl.issueBoards.ModalFooter, - }, - template: ` - <div - class="add-issues-modal" - v-if="showAddIssuesModal"> - <div class="add-issues-container"> - <modal-header></modal-header> - <modal-list v-if="!loading"></modal-list> - <section - class="add-issues-list" - v-if="loading"> - <div class="add-issues-list-loading"> - <i class="fa fa-spinner fa-spin"></i> - </div> - </section> - <modal-footer></modal-footer> - </div> - </div> - `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/search.js.es6 b/app/assets/javascripts/boards/components/modal/search.js.es6 index 6753656ec4a..764340bc5f3 100644 --- a/app/assets/javascripts/boards/components/modal/search.js.es6 +++ b/app/assets/javascripts/boards/components/modal/search.js.es6 @@ -7,7 +7,7 @@ gl.issueBoards.ModalSearch = Vue.extend({ data() { - return ModalStore.globalStore; + return ModalStore.store; }, computed: { selectAllText() { diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 index 2730893df76..f9026cea90b 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js.es6 +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -7,7 +7,7 @@ gl.issueBoards.ModalTabs = Vue.extend({ data() { - return ModalStore.globalStore; + return ModalStore.store; }, methods: { changeTab(tab) { diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index d29731ad0aa..66ecae1c01d 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -12,15 +12,6 @@ detail: { issue: {} }, - modal: { - issues: [], - selectedIssues: [], - showAddIssuesModal: false, - activeTab: 'all', - selectedList: {}, - searchTerm: '', - loading: false, - }, moving: { issue: {}, list: {} diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index 68f5e57a154..54419751433 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -4,11 +4,19 @@ class ModalStore { constructor() { - this.globalStore = gl.issueBoards.BoardsStore.modal; + this.store = { + issues: [], + selectedIssues: [], + showAddIssuesModal: false, + activeTab: 'all', + selectedList: {}, + searchTerm: '', + loading: false, + }; } selectedCount() { - return this.globalStore.selectedIssues.length; + return this.store.selectedIssues.length; } toggleIssue(issueObj) { @@ -23,9 +31,9 @@ } toggleAll() { - const select = this.selectedCount() !== this.globalStore.issues.length; + const select = this.selectedCount() !== this.store.issues.length; - this.globalStore.issues.forEach((issue) => { + this.store.issues.forEach((issue) => { const issueUpdate = issue; if (issueUpdate.selected !== select) { @@ -41,20 +49,20 @@ } addSelectedIssue(issue) { - this.globalStore.selectedIssues.push(issue); + this.store.selectedIssues.push(issue); } removeSelectedIssue(issue) { const index = this.selectedIssueIndex(issue); - this.globalStore.selectedIssues.splice(index, 1); + this.store.selectedIssues.splice(index, 1); } selectedIssueIndex(issue) { - return this.globalStore.selectedIssues.indexOf(issue); + return this.store.selectedIssues.indexOf(issue); } findSelectedIssue(issue) { - return this.globalStore.selectedIssues + return this.store.selectedIssues .filter(filteredIssue => filteredIssue.id === issue.id)[0]; } } diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb new file mode 100644 index 00000000000..e6065c0740d --- /dev/null +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -0,0 +1,210 @@ +require 'rails_helper' + +describe 'Issue Boards add issue modal', :feature, :js do + include WaitForAjax + include WaitForVueResource + + let(:project) { create(:empty_project, :public) } + let(:board) { create(:board, project: project) } + let(:user) { create(:user) } + let!(:planning) { create(:label, project: project, name: 'Planning') } + let!(:label) { create(:label, project: project) } + let!(:list1) { create(:list, board: board, label: planning, position: 0) } + let!(:issue) { create(:issue, project: project) } + let!(:issue2) { create(:issue, project: project) } + + before do + project.team << [user, :master] + + login_as(user) + + visit namespace_project_board_path(project.namespace, project, board) + wait_for_vue_resource + end + + context 'modal interaction' do + it 'opens modal' do + click_button('Add issues') + + expect(page).to have_selector('.add-issues-modal') + end + + it 'closes modal' do + click_button('Add issues') + + page.within('.add-issues-modal') do + find('.close').click + end + + expect(page).not_to have_selector('.add-issues-modal') + end + + it 'closes modal if cancel button clicked' do + click_button('Add issues') + + page.within('.add-issues-modal') do + click_button 'Cancel' + end + + expect(page).not_to have_selector('.add-issues-modal') + end + end + + context 'issues list' do + before do + click_button('Add issues') + + wait_for_vue_resource + end + + it 'loads issues' do + page.within('.add-issues-modal') do + page.within('.nav-links') do + expect(page).to have_content('2') + end + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'shows selected issues' do + page.within('.add-issues-modal') do + click_link 'Selected issues' + + expect(page).not_to have_selector('.card') + end + end + + context 'search' do + it 'returns issues' do + page.within('.add-issues-modal') do + find('.form-control').native.send_keys(issue.title) + + expect(page).to have_selector('.card', count: 1) + end + end + + it 'returns no issues' do + page.within('.add-issues-modal') do + find('.form-control').native.send_keys('testing search') + + expect(page).not_to have_selector('.card') + end + end + end + + context 'selecing issues' do + it 'selects single issue' do + page.within('.add-issues-modal') do + first('.card').click + + page.within('.nav-links') do + expect(page).to have_content('Selected issues 1') + end + end + end + + it 'changes button text' do + page.within('.add-issues-modal') do + first('.card').click + + expect(first('.add-issues-footer .btn')).to have_content('Add 1 issue') + end + end + + it 'changes button text with plural' do + page.within('.add-issues-modal') do + all('.card').each do |el| + el.click + end + + expect(first('.add-issues-footer .btn')).to have_content('Add 2 issues') + end + end + + it 'shows only selected issues on selected tab' do + page.within('.add-issues-modal') do + first('.card').click + + click_link 'Selected issues' + + expect(page).to have_selector('.card', count: 1) + end + end + + it 'selects all issues' do + page.within('.add-issues-modal') do + click_button 'Select all' + + expect(page).to have_selector('.is-active', count: 2) + end + end + + it 'un-selects all issues' do + page.within('.add-issues-modal') do + click_button 'Select all' + + expect(page).to have_selector('.is-active', count: 2) + + click_button 'Un-select all' + + expect(page).not_to have_selector('.is-active') + end + end + + it 'selects all that arent already selected' do + page.within('.add-issues-modal') do + first('.card').click + + expect(page).to have_selector('.is-active', count: 1) + + click_button 'Select all' + + expect(page).to have_selector('.is-active', count: 2) + end + end + + it 'unselects from selected tab' do + page.within('.add-issues-modal') do + first('.card').click + + click_link 'Selected issues' + + first('.card').click + + expect(page).not_to have_selector('.card') + end + end + end + + context 'adding issues' do + it 'adds to board' do + page.within('.add-issues-modal') do + first('.card').click + + click_button 'Add 1 issue' + end + + page.within(first('.board')) do + expect(page).to have_selector('.card') + end + end + + it 'adds to second list' do + page.within('.add-issues-modal') do + first('.card').click + + click_button planning.title + + click_link label.title + + click_button 'Add 1 issue' + end + + page.within(find('.board:nth-child(2)')) do + expect(page).to have_selector('.card') + end + end + end + end +end -- cgit v1.2.1 From d9894a2fddb70f37432a3605eac85c7b9e5d0fee Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 26 Jan 2017 10:55:40 +0000 Subject: Cleaned up some code --- .../boards/components/modal/dismiss.js.es6 | 28 --------- .../boards/components/modal/footer.js.es6 | 5 +- .../boards/components/modal/header.js.es6 | 41 +++++++++++-- .../boards/components/modal/index.js.es6 | 18 +++--- .../boards/components/modal/list.js.es6 | 42 +++++++------- .../boards/components/modal/search.js.es6 | 42 -------------- .../boards/components/modal/tabs.js.es6 | 11 +--- app/assets/stylesheets/pages/boards.scss | 67 +++++++--------------- 8 files changed, 91 insertions(+), 163 deletions(-) delete mode 100644 app/assets/javascripts/boards/components/modal/dismiss.js.es6 delete mode 100644 app/assets/javascripts/boards/components/modal/search.js.es6 diff --git a/app/assets/javascripts/boards/components/modal/dismiss.js.es6 b/app/assets/javascripts/boards/components/modal/dismiss.js.es6 deleted file mode 100644 index cf952837d39..00000000000 --- a/app/assets/javascripts/boards/components/modal/dismiss.js.es6 +++ /dev/null @@ -1,28 +0,0 @@ -/* global Vue */ -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.DismissModal = Vue.extend({ - data() { - return ModalStore.store; - }, - methods: { - toggleModal(toggle) { - this.showAddIssuesModal = toggle; - }, - }, - template: ` - <button - type="button" - class="close" - data-dismiss="modal" - aria-label="Close" - @click="toggleModal(false)"> - <span aria-hidden="true">×</span> - </button> - `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index e4d49b914bd..d2156759785 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -32,8 +32,7 @@ gl.boardService.addMultipleIssues(list, issueIds); // Add the issues on the frontend - issueIds.forEach((id) => { - const issue = this.issues.filter(fIssue => fIssue.id === id)[0]; + this.selectedIssues.forEach((issue) => { list.addIssue(issue); list.issuesSize += 1; }); @@ -54,7 +53,7 @@ @click="addIssues"> {{ submitText }} </button> - <span class="add-issues-footer-to-list"> + <span class="inline add-issues-footer-to-list"> to list </span> <lists-dropdown></lists-dropdown> diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index 6896a0fcf45..a91e3da2060 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -1,7 +1,5 @@ /* global Vue */ //= require ./tabs -//= require ./dismiss -//= require ./search (() => { const ModalStore = gl.issueBoards.ModalStore; @@ -12,21 +10,52 @@ data() { return ModalStore.store; }, + computed: { + selectAllText() { + if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { + return 'Select all'; + } + + return 'Un-select all'; + }, + }, + methods: { + toggleAll: ModalStore.toggleAll.bind(ModalStore), + }, components: { - 'modal-dismiss': gl.issueBoards.DismissModal, 'modal-tabs': gl.issueBoards.ModalTabs, - 'modal-search': gl.issueBoards.ModalSearch, }, template: ` <div> <header class="add-issues-header form-actions"> <h2> Add issues - <modal-dismiss></modal-dismiss> + <button + type="button" + class="close" + data-dismiss="modal" + aria-label="Close" + @click="showAddIssuesModal = false"> + <span aria-hidden="true">×</span> + </button> </h2> </header> <modal-tabs v-if="!loading"></modal-tabs> - <modal-search v-if="!loading"></modal-search> + <div + class="add-issues-search prepend-top-10 append-bottom-10" + v-if="activeTab == 'all' && !loading"> + <input + placeholder="Search issues..." + class="form-control" + type="search" + v-model="searchTerm" /> + <button + type="button" + class="btn btn-success btn-inverted prepend-left-10" + @click="toggleAll"> + {{ selectAllText }} + </button> + </div> </div> `, }); diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index 693182fe37e..2dfc7f16c21 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -16,14 +16,16 @@ searchTerm() { this.searchOperation(); }, - }, - mounted() { - this.loading = true; + showAddIssuesModal() { + if (this.showAddIssuesModal && !this.issues.length) { + this.loading = true; - this.loadIssues() - .then(() => { - this.loading = false; - }); + this.loadIssues() + .then(() => { + this.loading = false; + }); + } + }, }, methods: { searchOperation: _.debounce(function() { @@ -59,7 +61,7 @@ <modal-header></modal-header> <modal-list v-if="!loading"></modal-list> <section - class="add-issues-list" + class="add-issues-list text-center" v-if="loading"> <div class="add-issues-list-loading"> <i class="fa fa-spinner fa-spin"></i> diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 55e3ff3c7f4..4f6da0b493c 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -64,32 +64,30 @@ 'issue-card-inner': gl.issueBoards.IssueCardInner, }, template: ` - <section class="add-issues-list"> + <div + class="add-issues-list add-issues-list-columns" + ref="list" + v-show="!loading"> <div - class="add-issues-list-columns list-unstyled" - ref="list" - v-show="!loading"> + v-for="issue in loopIssues" + v-if="showIssue(issue)" + class="card-parent"> <div - v-for="issue in loopIssues" - v-if="showIssue(issue)" - class="card-parent"> - <div - class="card" - :class="{ 'is-active': issue.selected }" - @click="toggleIssue(issue)"> - <issue-card-inner - :issue="issue" - :issue-link-base="'/'"> - </issue-card-inner> - <span - v-if="issue.selected" - class="issue-card-selected"> - <i class="fa fa-check"></i> - </span> - </div> + class="card" + :class="{ 'is-active': issue.selected }" + @click="toggleIssue(issue)"> + <issue-card-inner + :issue="issue" + :issue-link-base="'/'"> + </issue-card-inner> + <span + v-if="issue.selected" + class="issue-card-selected text-center"> + <i class="fa fa-check"></i> + </span> </div> </div> - </section> + </div> `, }); })(); diff --git a/app/assets/javascripts/boards/components/modal/search.js.es6 b/app/assets/javascripts/boards/components/modal/search.js.es6 deleted file mode 100644 index 764340bc5f3..00000000000 --- a/app/assets/javascripts/boards/components/modal/search.js.es6 +++ /dev/null @@ -1,42 +0,0 @@ -/* global Vue */ -(() => { - const ModalStore = gl.issueBoards.ModalStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - - gl.issueBoards.ModalSearch = Vue.extend({ - data() { - return ModalStore.store; - }, - computed: { - selectAllText() { - if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) { - return 'Select all'; - } - - return 'Un-select all'; - }, - }, - methods: { - toggleAll: ModalStore.toggleAll.bind(ModalStore), - }, - template: ` - <div - class="add-issues-search" - v-if="activeTab == 'all'"> - <input - placeholder="Search issues..." - class="form-control" - type="search" - v-model="searchTerm" /> - <button - type="button" - class="btn btn-success btn-inverted" - @click="toggleAll"> - {{ selectAllText }} - </button> - </div> - `, - }); -})(); diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 index f9026cea90b..c165b81a774 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js.es6 +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -9,11 +9,6 @@ data() { return ModalStore.store; }, - methods: { - changeTab(tab) { - this.activeTab = tab; - }, - }, computed: { selectedCount() { return ModalStore.selectedCount(); @@ -23,13 +18,13 @@ this.activeTab = 'all'; }, template: ` - <div class="top-area"> + <div class="top-area prepend-top-10"> <ul class="nav-links issues-state-filters"> <li :class="{ 'active': activeTab == 'all' }"> <a href="#" role="button" - @click.prevent="changeTab('all')"> + @click.prevent="activeTab = 'all'"> <span>All issues</span> <span class="badge"> {{ issues.length }} @@ -40,7 +35,7 @@ <a href="#" role="button" - @click.prevent="changeTab('selected')"> + @click.prevent="activeTab = 'selected'"> <span>Selected issues</span> <span class="badge"> {{ selectedCount }} diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 683ef9f2388..842ae0c083f 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -358,7 +358,6 @@ .add-issues-modal { display: flex; - align-items: center; position: fixed; top: 0; right: 0; @@ -374,8 +373,7 @@ width: 90vw; height: 85vh; min-height: 500px; - margin-left: auto; - margin-right: auto; + margin: auto; padding: 25px 15px 0; background-color: $white-light; border-radius: $border-radius-default; @@ -391,57 +389,19 @@ margin: 0; font-size: 18px; } - - .top-area { - margin-bottom: 10px; - } } .add-issues-search { display: flex; - margin-bottom: 10px; - margin-top: 10px; - - .btn { - margin-left: 10px; - } } .add-issues-list { display: flex; flex: 1; + padding-top: 3px; margin-left: -$gl-vert-padding; margin-right: -$gl-vert-padding; overflow-y: scroll; -} - -.add-issues-list-loading { - align-self: center; - width: 100%; - padding-left: $gl-vert-padding; - padding-right: $gl-vert-padding; - font-size: 35px; - text-align: center; -} - -.add-issues-footer { - margin-top: auto; - margin-left: -15px; - margin-right: -15px; - padding-left: 15px; - padding-right: 15px; -} - -.add-issues-footer-to-list { - display: inline-block; - padding-left: 6px; - padding-right: 6px; - line-height: 34px; -} - -.add-issues-list-columns { - width: 100%; - padding-top: 3px; .card-parent { width: 100%; @@ -462,9 +422,26 @@ } } -.all-issues-selected-empty { +.add-issues-list-loading { align-self: center; - margin-bottom: 0; + width: 100%; + padding-left: $gl-vert-padding; + padding-right: $gl-vert-padding; + font-size: 35px; +} + +.add-issues-footer { + margin-top: auto; + margin-left: -15px; + margin-right: -15px; + padding-left: 15px; + padding-right: 15px; +} + +.add-issues-footer-to-list { + padding-left: $gl-vert-padding; + padding-right: $gl-vert-padding; + line-height: 34px; } .issue-card-selected { @@ -472,11 +449,9 @@ right: -3px; top: -3px; width: 20px; - height: 20px; background-color: $blue-dark; color: $white-light; font-size: 12px; - text-align: center; line-height: 20px; border-radius: 50%; } -- cgit v1.2.1 From ff98a7434ffe848738228cd51d9deee3b8373d91 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 26 Jan 2017 11:29:04 +0000 Subject: Added empty state --- .../boards/components/modal/empty_state.js.es6 | 65 ++++++++++++++++++++++ .../boards/components/modal/footer.js.es6 | 3 +- .../boards/components/modal/header.js.es6 | 6 +- .../boards/components/modal/index.js.es6 | 20 ++++++- .../boards/components/modal/list.js.es6 | 3 +- .../boards/components/modal/tabs.js.es6 | 2 +- app/assets/stylesheets/pages/boards.scss | 12 ++++ app/views/projects/boards/_show.html.haml | 3 +- 8 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 app/assets/javascripts/boards/components/modal/empty_state.js.es6 diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 new file mode 100644 index 00000000000..efea12ef7d8 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 @@ -0,0 +1,65 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.ModalEmptyState = Vue.extend({ + data() { + return ModalStore.store; + }, + props: [ + 'image', 'newIssuePath', + ], + computed: { + contents() { + const obj = { + title: 'You haven\'t added any issues to your project yet', + content: ` + An issue can be a bug, a todo or a feature request that needs to be + discussed in a project. Besides, issues are searchable and filterable. + `, + }; + + if (this.activeTab === 'selected') { + obj.title = 'You haven\'t selected any issues yet'; + obj.content = ` + Go back to <strong>All issues</strong> and select some issues + to add to your board. + `; + } + + return obj; + } + }, + template: ` + <div class="empty-state"> + <div class="row"> + <div class="col-xs-12 col-sm-6 col-sm-push-6"> + <div class="svg-content" v-html="image"></div> + </div> + <div class="col-xs-12 col-sm-6 col-sm-pull-6"> + <div class="text-content"> + <h4>{{ contents.title }}</h4> + <p v-html="contents.content"></p> + <a + :href="newIssuePath" + class="btn btn-success btn-inverted" + v-if="activeTab === 'all'"> + Create issue + </a> + <button + type="button" + class="btn btn-default" + @click="activeTab = 'all'" + v-if="activeTab === 'selected'"> + All issues + </button> + </div> + </div> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index d2156759785..9d60d59bcb1 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -44,7 +44,8 @@ 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, }, template: ` - <footer class="form-actions add-issues-footer"> + <footer + class="form-actions add-issues-footer"> <div class="pull-left"> <button class="btn btn-success" diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index a91e3da2060..f6acfed9270 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -40,10 +40,10 @@ </button> </h2> </header> - <modal-tabs v-if="!loading"></modal-tabs> + <modal-tabs v-if="!loading && issues.length > 0"></modal-tabs> <div - class="add-issues-search prepend-top-10 append-bottom-10" - v-if="activeTab == 'all' && !loading"> + class="add-issues-search append-bottom-10" + v-if="activeTab == 'all' && !loading && issues.length > 0"> <input placeholder="Search issues..." class="form-control" diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index 2dfc7f16c21..30fe66703b7 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -2,6 +2,7 @@ //= require ./header //= require ./list //= require ./footer +//= require ./empty_state (() => { const ModalStore = gl.issueBoards.ModalStore; @@ -9,6 +10,9 @@ window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.IssuesModal = Vue.extend({ + props: [ + 'blankStateImage', 'newIssuePath', + ], data() { return ModalStore.store; }, @@ -48,10 +52,20 @@ }); }, }, + computed: { + showList() { + if (this.activeTab === 'selected') { + return this.selectedIssues.length > 0; + } + + return this.issues.length > 0; + }, + }, components: { 'modal-header': gl.issueBoards.IssuesModalHeader, 'modal-list': gl.issueBoards.ModalList, 'modal-footer': gl.issueBoards.ModalFooter, + 'empty-state': gl.issueBoards.ModalEmptyState, }, template: ` <div @@ -59,7 +73,11 @@ v-if="showAddIssuesModal"> <div class="add-issues-container"> <modal-header></modal-header> - <modal-list v-if="!loading"></modal-list> + <modal-list v-if="!loading && showList"></modal-list> + <empty-state + v-if="(!loading && issues.length === 0) || (activeTab === 'selected' && selectedIssues.length === 0)" + :image="blankStateImage" + :new-issue-path="newIssuePath"></empty-state> <section class="add-issues-list text-center" v-if="loading"> diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 4f6da0b493c..8180f200649 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -66,8 +66,7 @@ template: ` <div class="add-issues-list add-issues-list-columns" - ref="list" - v-show="!loading"> + ref="list"> <div v-for="issue in loopIssues" v-if="showIssue(issue)" diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 index c165b81a774..fecd92f70ba 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js.es6 +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -18,7 +18,7 @@ this.activeTab = 'all'; }, template: ` - <div class="top-area prepend-top-10"> + <div class="top-area prepend-top-10 append-bottom-10"> <ul class="nav-links issues-state-filters"> <li :class="{ 'active': activeTab == 'all' }"> <a diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 842ae0c083f..04e7aea63fd 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -378,6 +378,18 @@ background-color: $white-light; border-radius: $border-radius-default; box-shadow: 0 2px 12px rgba($black, .5); + + .empty-state { + display: flex; + flex: 1; + margin-top: 0; + + > .row { + width: 100%; + margin-top: auto; + margin-bottom: auto; + } + } } .add-issues-header { diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 798eace7a82..a64f66b5b64 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -26,4 +26,5 @@ ":issue-link-base" => "issueLinkBase", ":key" => "_uid" } = render "projects/boards/components/sidebar" - %board-add-issues-modal + %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), + "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project) } -- cgit v1.2.1 From 76264cc01c112590454a19690bd5abb1ed4db7dc Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 26 Jan 2017 11:41:43 +0000 Subject: Reloads issues when re-opening modal --- .../javascripts/boards/components/issue_card_inner.js.es6 | 3 --- .../javascripts/boards/components/modal/empty_state.js.es6 | 3 --- .../javascripts/boards/components/modal/footer.js.es6 | 3 --- .../javascripts/boards/components/modal/header.js.es6 | 3 --- app/assets/javascripts/boards/components/modal/index.js.es6 | 6 +++--- app/assets/javascripts/boards/components/modal/list.js.es6 | 3 --- .../boards/components/modal/lists_dropdown.js.es6 | 13 ++----------- app/assets/javascripts/boards/components/modal/tabs.js.es6 | 3 --- app/assets/javascripts/boards/stores/modal_store.js.es6 | 3 --- 9 files changed, 5 insertions(+), 35 deletions(-) diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 index 6a7e9419503..b7eeb5e04c9 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 +++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 @@ -2,9 +2,6 @@ (() => { const Store = gl.issueBoards.BoardsStore; - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - gl.issueBoards.IssueCardInner = Vue.extend({ props: [ 'issue', 'issueLinkBase', 'list', diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 index efea12ef7d8..689504f397c 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 +++ b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 @@ -2,9 +2,6 @@ (() => { const ModalStore = gl.issueBoards.ModalStore; - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - gl.issueBoards.ModalEmptyState = Vue.extend({ data() { return ModalStore.store; diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 9d60d59bcb1..51a9444ee61 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -3,9 +3,6 @@ (() => { const ModalStore = gl.issueBoards.ModalStore; - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - gl.issueBoards.ModalFooter = Vue.extend({ data() { return ModalStore.store; diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index f6acfed9270..d7d896e3be1 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -3,9 +3,6 @@ (() => { const ModalStore = gl.issueBoards.ModalStore; - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - gl.issueBoards.IssuesModalHeader = Vue.extend({ data() { return ModalStore.store; diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index 30fe66703b7..a6718e899f7 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -6,9 +6,6 @@ (() => { const ModalStore = gl.issueBoards.ModalStore; - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - gl.issueBoards.IssuesModal = Vue.extend({ props: [ 'blankStateImage', 'newIssuePath', @@ -28,6 +25,9 @@ .then(() => { this.loading = false; }); + } else if (!this.showAddIssuesModal) { + this.issues = []; + this.selectedIssues = []; } }, }, diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 8180f200649..6a681d9278f 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -5,9 +5,6 @@ let listMasonry; const ModalStore = gl.issueBoards.ModalStore; - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - gl.issueBoards.ModalList = Vue.extend({ data() { return ModalStore.store; diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 index e68ad30500c..b205c019a31 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 @@ -1,16 +1,12 @@ /* global Vue */ (() => { const ModalStore = gl.issueBoards.ModalStore; - const Store = gl.issueBoards.BoardsStore; - - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.ModalFooterListsDropdown = Vue.extend({ data() { return { modal: ModalStore.store, - state: Store.state, + state: gl.issueBoards.BoardsStore.state, }; }, computed: { @@ -18,11 +14,6 @@ return this.modal.selectedList; }, }, - methods: { - selectList(list) { - this.modal.selectedList = list; - }, - }, template: ` <div class="dropdown inline"> <button @@ -46,7 +37,7 @@ href="#" role="button" :class="{ 'is-active': list.id == selected.id }" - @click="selectList(list)"> + @click="modal.selectedList = list"> <span class="dropdown-label-box" :style="{ backgroundColor: list.label.color }"> diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 index fecd92f70ba..94ff8ec999a 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js.es6 +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -2,9 +2,6 @@ (() => { const ModalStore = gl.issueBoards.ModalStore; - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - gl.issueBoards.ModalTabs = Vue.extend({ data() { return ModalStore.store; diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index 54419751433..b5d183b1bf4 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -1,7 +1,4 @@ (() => { - window.gl = window.gl || {}; - window.gl.issueBoards = window.gl.issueBoards || {}; - class ModalStore { constructor() { this.store = { -- cgit v1.2.1 From 54461ce2a3a35071e82fac11f70d39373de424a6 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 26 Jan 2017 12:55:08 +0000 Subject: Added more tests --- .../boards/components/issue_card_inner.js.es6 | 5 +- .../boards/components/modal/empty_state.js.es6 | 2 +- .../boards/components/modal/index.js.es6 | 3 +- app/assets/javascripts/boards/models/issue.js.es6 | 1 + .../javascripts/boards/stores/modal_store.js.es6 | 3 + spec/javascripts/boards/issue_card_spec.js.es6 | 191 +++++++++++++++++++++ spec/javascripts/boards/modal_store_spec.js.es6 | 115 +++++++++++++ 7 files changed, 317 insertions(+), 3 deletions(-) create mode 100644 spec/javascripts/boards/issue_card_spec.js.es6 create mode 100644 spec/javascripts/boards/modal_store_spec.js.es6 diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 index b7eeb5e04c9..383b329edc3 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 +++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 @@ -2,6 +2,9 @@ (() => { const Store = gl.issueBoards.BoardsStore; + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + gl.issueBoards.IssueCardInner = Vue.extend({ props: [ 'issue', 'issueLinkBase', 'list', @@ -57,7 +60,7 @@ #{{issue.id}} </span> <a - class="has-tooltip" + class="card-assignee has-tooltip" :href="issue.assignee.username" :title="'Assigned to ' + issue.assignee.name" v-if="issue.assignee" diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 index 689504f397c..70f2ea51620 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 +++ b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 @@ -28,7 +28,7 @@ } return obj; - } + }, }, template: ` <div class="empty-state"> diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index a6718e899f7..52f652ccfe1 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -1,4 +1,5 @@ /* global Vue */ +/* global ListIssue */ //= require ./header //= require ./list //= require ./footer @@ -32,7 +33,7 @@ }, }, methods: { - searchOperation: _.debounce(function() { + searchOperation: _.debounce(function searchOperationDebounce() { this.loadIssues(); }, 500), loadIssues() { diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index a36a146b023..8d93ac637f9 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -13,6 +13,7 @@ class ListIssue { this.subscribed = obj.subscribed; this.labels = []; this.selected = false; + this.assignee = false; if (obj.assignee) { this.assignee = new ListUser(obj.assignee); diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index b5d183b1bf4..54419751433 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -1,4 +1,7 @@ (() => { + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + class ModalStore { constructor() { this.store = { diff --git a/spec/javascripts/boards/issue_card_spec.js.es6 b/spec/javascripts/boards/issue_card_spec.js.es6 new file mode 100644 index 00000000000..181fb684c23 --- /dev/null +++ b/spec/javascripts/boards/issue_card_spec.js.es6 @@ -0,0 +1,191 @@ +/* global Vue */ +/* global ListUser */ +/* global ListLabel */ +/* global listObj */ +/* global ListIssue */ + +//= require jquery +//= require vue +//= require boards/models/issue +//= require boards/models/label +//= require boards/models/list +//= require boards/models/user +//= require boards/stores/boards_store +//= require boards/components/issue_card_inner +//= require ./mock_data + +describe('Issue card component', () => { + const user = new ListUser({ + id: 1, + name: 'testing 123', + username: 'test', + avatar: 'test_image', + }); + const label1 = new ListLabel({ + id: 3, + title: 'testing 123', + color: 'blue', + text_color: 'white', + description: 'test', + }); + let component; + let issue; + let list; + + beforeEach(() => { + setFixtures('<div class="test-container"></div>'); + + list = listObj; + issue = new ListIssue({ + title: 'Testing', + iid: 1, + confidential: false, + labels: [list.label], + }); + + component = new Vue({ + el: document.querySelector('.test-container'), + data() { + return { + list, + issue, + issueLinkBase: '/test', + }; + }, + components: { + 'issue-card': gl.issueBoards.IssueCardInner, + }, + template: ` + <issue-card + :issue="issue" + :list="list" + :issue-link-base="issueLinkBase"></issue-card> + `, + }); + }); + + it('renders issue title', () => { + expect( + component.$el.querySelector('.card-title').textContent, + ).toContain(issue.title); + }); + + it('includes issue base in link', () => { + expect( + component.$el.querySelector('.card-title a').getAttribute('href'), + ).toContain('/test'); + }); + + it('includes issue title on link', () => { + expect( + component.$el.querySelector('.card-title a').getAttribute('title'), + ).toBe(issue.title); + }); + + it('does not render confidential icon', () => { + expect( + component.$el.querySelector('.fa-eye-flash'), + ).toBeNull(); + }); + + it('renders confidential icon', (done) => { + component.issue.confidential = true; + + setTimeout(() => { + expect( + component.$el.querySelector('.fa-eye-flash'), + ).not.toBeNull(); + done(); + }, 0); + }); + + it('renders issue ID with #', () => { + expect( + component.$el.querySelector('.card-number').textContent, + ).toContain(`#${issue.id}`); + }); + + describe('assignee', () => { + it('does not render assignee', () => { + expect( + component.$el.querySelector('.card-assignee'), + ).toBeNull(); + }); + + describe('exists', () => { + beforeEach((done) => { + component.issue.assignee = user; + + setTimeout(() => { + done(); + }, 0); + }); + + it('renders assignee', () => { + expect( + component.$el.querySelector('.card-assignee'), + ).not.toBeNull(); + }); + + it('sets title', () => { + expect( + component.$el.querySelector('.card-assignee').getAttribute('title'), + ).toContain(`Assigned to ${user.name}`); + }); + + it('sets users path', () => { + expect( + component.$el.querySelector('.card-assignee').getAttribute('href'), + ).toBe('test'); + }); + + it('renders avatar', () => { + expect( + component.$el.querySelector('.card-assignee img'), + ).not.toBeNull(); + }); + }); + }); + + describe('labels', () => { + it('does not render any', () => { + expect( + component.$el.querySelector('.label'), + ).toBeNull(); + }); + + describe('exists', () => { + beforeEach((done) => { + component.issue.addLabel(label1); + + setTimeout(() => { + done(); + }, 0); + }); + + it('does not render list label', () => { + expect( + component.$el.querySelectorAll('.label').length, + ).toBe(1); + }); + + it('renders label', () => { + expect( + component.$el.querySelector('.label').textContent, + ).toContain(label1.title); + }); + + it('sets label description as title', () => { + expect( + component.$el.querySelector('.label').getAttribute('title'), + ).toContain(label1.description); + }); + + it('sets background color of button', () => { + expect( + component.$el.querySelector('.label').style.backgroundColor, + ).toContain(label1.color); + }); + }); + }); +}); diff --git a/spec/javascripts/boards/modal_store_spec.js.es6 b/spec/javascripts/boards/modal_store_spec.js.es6 new file mode 100644 index 00000000000..9c0625357bd --- /dev/null +++ b/spec/javascripts/boards/modal_store_spec.js.es6 @@ -0,0 +1,115 @@ +/* global Vue */ +/* global ListIssue */ + +//= require jquery +//= require vue +//= require boards/models/issue +//= require boards/models/label +//= require boards/models/list +//= require boards/models/user +//= require boards/stores/modal_store + +describe('Modal store', () => { + let issue; + let issue2; + const Store = gl.issueBoards.ModalStore; + + beforeEach(() => { + // Setup default state + Store.store.issues = []; + Store.store.selectedIssues = []; + + issue = new ListIssue({ + title: 'Testing', + iid: 1, + confidential: false, + labels: [], + }); + issue2 = new ListIssue({ + title: 'Testing', + iid: 2, + confidential: false, + labels: [], + }); + Store.store.issues.push(issue); + Store.store.issues.push(issue2); + }); + + it('returns selected count', () => { + expect(Store.selectedCount()).toBe(0); + }); + + it('toggles the issue as selected', () => { + Store.toggleIssue(issue); + + expect(issue.selected).toBe(true); + expect(Store.selectedCount()).toBe(1); + }); + + it('toggles the issue as un-selected', () => { + Store.toggleIssue(issue); + Store.toggleIssue(issue); + + expect(issue.selected).toBe(false); + expect(Store.selectedCount()).toBe(0); + }); + + it('toggles all issues as selected', () => { + Store.toggleAll(); + + expect(issue.selected).toBe(true); + expect(issue2.selected).toBe(true); + expect(Store.selectedCount()).toBe(2); + }); + + it('toggles all issues as un-selected', () => { + Store.toggleAll(); + Store.toggleAll(); + + expect(issue.selected).toBe(false); + expect(issue2.selected).toBe(false); + expect(Store.selectedCount()).toBe(0); + }); + + it('toggles all if a single issue is selected', () => { + Store.toggleIssue(issue); + Store.toggleAll(); + + expect(issue.selected).toBe(true); + expect(issue2.selected).toBe(true); + expect(Store.selectedCount()).toBe(2); + }); + + it('adds issue to selected array', () => { + Store.addSelectedIssue(issue); + + expect(Store.selectedCount()).toBe(1); + }); + + it('removes issue from selected array', () => { + Store.addSelectedIssue(issue); + Store.removeSelectedIssue(issue); + + expect(Store.selectedCount()).toBe(0); + }); + + it('returns selected issue index if present', () => { + Store.toggleIssue(issue); + + expect(Store.selectedIssueIndex(issue)).toBe(0); + }); + + it('returns -1 if issue is not selected', () => { + expect(Store.selectedIssueIndex(issue)).toBe(-1); + }); + + it('finds the selected issue', () => { + Store.toggleIssue(issue); + + expect(Store.findSelectedIssue(issue)).toBe(issue); + }); + + it('does not find a selected issue', () => { + expect(Store.findSelectedIssue(issue)).toBe(undefined); + }); +}); -- cgit v1.2.1 From 3aabf0c6aa2b76ef4458250ca98876e3f268fe14 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 26 Jan 2017 16:03:07 +0000 Subject: Used issue bulk update instead of different endpoint --- app/assets/javascripts/boards/components/modal/footer.js.es6 | 12 ++++++++++-- app/assets/javascripts/boards/components/modal/index.js.es6 | 4 ++-- app/assets/javascripts/boards/models/issue.js.es6 | 1 + app/assets/javascripts/boards/services/board_service.js.es6 | 10 ---------- app/controllers/projects/boards/lists_controller.rb | 4 ---- app/controllers/projects/boards_controller.rb | 2 +- app/views/projects/boards/_show.html.haml | 3 ++- config/routes/project.rb | 1 - 8 files changed, 16 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 51a9444ee61..1be58b017ef 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -4,6 +4,9 @@ const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalFooter = Vue.extend({ + props: [ + 'bulkUpdatePath', + ], data() { return ModalStore.store; }, @@ -23,10 +26,15 @@ }, addIssues() { const list = this.selectedList; - const issueIds = this.selectedIssues.map(issue => issue.id); + const issueIds = this.selectedIssues.map(issue => issue._id); // Post the data to the backend - gl.boardService.addMultipleIssues(list, issueIds); + this.$http.post(this.bulkUpdatePath, { + update: { + issuable_ids: issueIds.join(','), + add_label_ids: [list.label.id], + }, + }); // Add the issues on the frontend this.selectedIssues.forEach((issue) => { diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index 52f652ccfe1..9cab701bb5d 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -9,7 +9,7 @@ gl.issueBoards.IssuesModal = Vue.extend({ props: [ - 'blankStateImage', 'newIssuePath', + 'blankStateImage', 'newIssuePath', 'bulkUpdatePath', ], data() { return ModalStore.store; @@ -86,7 +86,7 @@ <i class="fa fa-spinner fa-spin"></i> </div> </section> - <modal-footer></modal-footer> + <modal-footer :bulk-update-path="bulkUpdatePath"></modal-footer> </div> </div> `, diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index 8d93ac637f9..5b435a76467 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -6,6 +6,7 @@ class ListIssue { constructor (obj) { + this._id = obj.id; this.id = obj.iid; this.title = obj.title; this.confidential = obj.confidential; diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index c8db107d890..20df8f2b373 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -14,10 +14,6 @@ class BoardService { method: 'POST', url: `${root}/${boardId}/lists/generate.json` }, - multiple: { - method: 'POST', - url: `${root}/${boardId}/lists{/id}/multiple` - }, }); this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}); @@ -79,12 +75,6 @@ class BoardService { getBacklog(data) { return this.boards.backlog(data); } - - addMultipleIssues(list, issue_ids) { - return this.lists.multiple(list.id, { - issue_ids, - }); - } } window.BoardService = BoardService; diff --git a/app/controllers/projects/boards/lists_controller.rb b/app/controllers/projects/boards/lists_controller.rb index ed65cae82dc..67e3c9add81 100644 --- a/app/controllers/projects/boards/lists_controller.rb +++ b/app/controllers/projects/boards/lists_controller.rb @@ -50,10 +50,6 @@ module Projects end end - def multiple - head :ok - end - private def authorize_admin_list! diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 9e699fb9b3b..ba9d0eaa265 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -37,7 +37,7 @@ class Projects::BoardsController < Projects::ApplicationController render json: @issues.as_json( labels: true, - only: [:iid, :title, :confidential, :due_date], + only: [:id, :iid, :title, :confidential, :due_date], include: { assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, milestone: { only: [:id, :title] } diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index a64f66b5b64..068e69bada6 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -27,4 +27,5 @@ ":key" => "_uid" } = render "projects/boards/components/sidebar" %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), - "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project) } + "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project), + "bulk-update-path" => bulk_update_namespace_project_issues_path(@project.namespace, @project) } diff --git a/config/routes/project.rb b/config/routes/project.rb index afd895a5a1e..46d6530333d 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -274,7 +274,6 @@ constraints(ProjectUrlConstrainer.new) do resources :lists, only: [:index, :create, :update, :destroy] do collection do post :generate - post :multiple end resources :issues, only: [:index, :create] -- cgit v1.2.1 From 682d213f431ea83f277fc2a362c994ee17c2f44b Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 27 Jan 2017 03:23:07 -0200 Subject: Remove backlog lists from boards --- app/models/board.rb | 4 --- app/models/list.rb | 2 +- ...70127032550_remove_backlog_lists_from_boards.rb | 19 ++++++++++ db/schema.rb | 41 ++++++++++++++-------- spec/models/list_spec.rb | 39 +++----------------- 5 files changed, 50 insertions(+), 55 deletions(-) create mode 100644 db/migrate/20170127032550_remove_backlog_lists_from_boards.rb diff --git a/app/models/board.rb b/app/models/board.rb index c56422914a9..2780acc67c0 100644 --- a/app/models/board.rb +++ b/app/models/board.rb @@ -5,10 +5,6 @@ class Board < ActiveRecord::Base validates :project, presence: true - def backlog_list - lists.merge(List.backlog).take - end - def done_list lists.merge(List.done).take end diff --git a/app/models/list.rb b/app/models/list.rb index 065d75bd1dc..1e5da7f4dd4 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -2,7 +2,7 @@ class List < ActiveRecord::Base belongs_to :board belongs_to :label - enum list_type: { backlog: 0, label: 1, done: 2 } + enum list_type: { label: 1, done: 2 } validates :board, :list_type, presence: true validates :label, :position, presence: true, if: :label? diff --git a/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb b/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb new file mode 100644 index 00000000000..3369bd4d123 --- /dev/null +++ b/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb @@ -0,0 +1,19 @@ +class RemoveBacklogListsFromBoards < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + execute <<-SQL + DELETE FROM lists WHERE list_type = 0; + SQL + end + + def down + execute <<-SQL + INSERT INTO lists (board_id, list_type, created_at, updated_at) + SELECT boards.id, 0, NOW(), NOW() + FROM boards; + SQL + end +end diff --git a/db/schema.rb b/db/schema.rb index c73c311ccb2..d887b441189 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -87,9 +87,9 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.boolean "send_user_confirmation_email", default: false t.integer "container_registry_token_expire_delay", default: 5 t.text "after_sign_up_text" - t.boolean "user_default_external", default: false, null: false t.string "repository_storages", default: "default" t.string "enabled_git_access_protocol" + t.boolean "user_default_external", default: false, null: false t.boolean "domain_blacklist_enabled", default: false t.text "domain_blacklist" t.boolean "koding_enabled" @@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true t.string "plantuml_url" t.boolean "plantuml_enabled" @@ -398,22 +398,22 @@ ActiveRecord::Schema.define(version: 20170130204620) do add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree create_table "deployments", force: :cascade do |t| - t.integer "iid", null: false - t.integer "project_id", null: false - t.integer "environment_id", null: false - t.string "ref", null: false - t.boolean "tag", null: false - t.string "sha", null: false + t.integer "iid" + t.integer "project_id" + t.integer "environment_id" + t.string "ref" + t.boolean "tag" + t.string "sha" t.integer "user_id" - t.integer "deployable_id" - t.string "deployable_type" + t.integer "deployable_id", null: false + t.string "deployable_type", null: false t.datetime "created_at" t.datetime "updated_at" t.string "on_stop" end add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree - add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree + add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", using: :btree create_table "emails", force: :cascade do |t| t.integer "user_id", null: false @@ -685,8 +685,8 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.integer "merge_user_id" t.string "merge_commit_sha" t.datetime "deleted_at" - t.string "in_progress_merge_commit_sha" t.integer "lock_version" + t.string "in_progress_merge_commit_sha" t.text "title_html" t.text "description_html" t.integer "time_estimate" @@ -763,6 +763,16 @@ ActiveRecord::Schema.define(version: 20170130204620) do add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree + create_table "note_templates", force: :cascade do |t| + t.integer "user_id" + t.string "title" + t.text "note" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "note_templates", ["user_id"], name: "index_note_templates_on_user_id", using: :btree + create_table "notes", force: :cascade do |t| t.text "note" t.string "noteable_type" @@ -778,6 +788,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.text "st_diff" t.integer "updated_by_id" t.string "type" + t.string "system_type" t.text "position" t.text "original_position" t.datetime "resolved_at" @@ -960,7 +971,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.boolean "has_external_wiki" t.boolean "lfs_enabled" t.text "description_html" - t.boolean "only_allow_merge_if_all_discussions_are_resolved" + t.boolean "only_allow_merge_if_all_discussions_are_resolved", default: false, null: false end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index 9e1a52011c3..e6ca4853873 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -19,13 +19,6 @@ describe List do expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id) end - context 'when list_type is set to backlog' do - subject { described_class.new(list_type: :backlog) } - - it { is_expected.not_to validate_presence_of(:label) } - it { is_expected.not_to validate_presence_of(:position) } - end - context 'when list_type is set to done' do subject { described_class.new(list_type: :done) } @@ -41,12 +34,6 @@ describe List do expect(subject.destroy).to be_truthy end - it 'can not be destroyed when list_type is set to backlog' do - subject = create(:backlog_list) - - expect(subject.destroy).to be_falsey - end - it 'can not be destroyed when when list_type is set to done' do subject = create(:done_list) @@ -55,19 +42,13 @@ describe List do end describe '#destroyable?' do - it 'retruns true when list_type is set to label' do + it 'returns true when list_type is set to label' do subject.list_type = :label expect(subject).to be_destroyable end - it 'retruns false when list_type is set to backlog' do - subject.list_type = :backlog - - expect(subject).not_to be_destroyable - end - - it 'retruns false when list_type is set to done' do + it 'returns false when list_type is set to done' do subject.list_type = :done expect(subject).not_to be_destroyable @@ -75,19 +56,13 @@ describe List do end describe '#movable?' do - it 'retruns true when list_type is set to label' do + it 'returns true when list_type is set to label' do subject.list_type = :label expect(subject).to be_movable end - it 'retruns false when list_type is set to backlog' do - subject.list_type = :backlog - - expect(subject).not_to be_movable - end - - it 'retruns false when list_type is set to done' do + it 'returns false when list_type is set to done' do subject.list_type = :done expect(subject).not_to be_movable @@ -102,12 +77,6 @@ describe List do expect(subject.title).to eq 'Development' end - it 'returns Backlog when list_type is set to backlog' do - subject.list_type = :backlog - - expect(subject.title).to eq 'Backlog' - end - it 'returns Done when list_type is set to done' do subject.list_type = :done -- cgit v1.2.1 From 860fafee137507c465e5a7be19ebedff540d7a08 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 27 Jan 2017 03:23:55 -0200 Subject: Remove backlog lists from board services --- app/services/boards/create_service.rb | 1 - spec/services/boards/create_service_spec.rb | 7 ++-- spec/services/boards/issues/list_service_spec.rb | 13 ------ spec/services/boards/issues/move_service_spec.rb | 49 ---------------------- spec/services/boards/lists/create_service_spec.rb | 4 +- spec/services/boards/lists/destroy_service_spec.rb | 9 ---- spec/services/boards/lists/list_service_spec.rb | 2 +- spec/services/boards/lists/move_service_spec.rb | 9 ---- 8 files changed, 6 insertions(+), 88 deletions(-) diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index 9bdd7b6f0cf..f6275a63109 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -12,7 +12,6 @@ module Boards def create_board! board = project.boards.create - board.lists.create(list_type: :backlog) board.lists.create(list_type: :done) board diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb index fde807cc410..7b29b043296 100644 --- a/spec/services/boards/create_service_spec.rb +++ b/spec/services/boards/create_service_spec.rb @@ -11,12 +11,11 @@ describe Boards::CreateService, services: true do expect { service.execute }.to change(Board, :count).by(1) end - it 'creates default lists' do + it 'creates the default lists' do board = service.execute - expect(board.lists.size).to eq 2 - expect(board.lists.first).to be_backlog - expect(board.lists.last).to be_done + expect(board.lists.size).to eq 1 + expect(board.lists.first).to be_done end end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 7c206cf3ce7..5d53c54254c 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -13,15 +13,10 @@ describe Boards::Issues::ListService, services: true do let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } let(:p3) { create(:label, title: 'P3', project: project, priority: 3) } - let!(:backlog) { create(:backlog_list, board: board) } let!(:list1) { create(:list, board: board, label: development, position: 0) } let!(:list2) { create(:list, board: board, label: testing, position: 1) } let!(:done) { create(:done_list, board: board) } - let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) } - let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) } - let!(:reopened_issue1) { create(:issue, :reopened, project: project) } - let!(:list1_issue1) { create(:labeled_issue, project: project, labels: [p2, development]) } let!(:list1_issue2) { create(:labeled_issue, project: project, labels: [development]) } let!(:list1_issue3) { create(:labeled_issue, project: project, labels: [development, p1]) } @@ -45,14 +40,6 @@ describe Boards::Issues::ListService, services: true do end context 'sets default order to priority' do - it 'returns opened issues when listing issues from Backlog' do - params = { board_id: board.id, id: backlog.id } - - issues = described_class.new(project, user, params).execute - - expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] - end - it 'returns closed issues when listing issues from Done' do params = { board_id: board.id, id: done.id } diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index c43b2aec490..77f75167b3d 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -10,7 +10,6 @@ describe Boards::Issues::MoveService, services: true do let(:development) { create(:label, project: project, name: 'Development') } let(:testing) { create(:label, project: project, name: 'Testing') } - let!(:backlog) { create(:backlog_list, board: board1) } let!(:list1) { create(:list, board: board1, label: development, position: 0) } let!(:list2) { create(:list, board: board1, label: testing, position: 1) } let!(:done) { create(:done_list, board: board1) } @@ -19,41 +18,6 @@ describe Boards::Issues::MoveService, services: true do project.team << [user, :developer] end - context 'when moving from backlog' do - it 'adds the label of the list it goes to' do - issue = create(:labeled_issue, project: project, labels: [bug]) - params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: list1.id } - - described_class.new(project, user, params).execute(issue) - - expect(issue.reload.labels).to contain_exactly(bug, development) - end - end - - context 'when moving to backlog' do - it 'removes all list-labels' do - issue = create(:labeled_issue, project: project, labels: [bug, development, testing]) - params = { board_id: board1.id, from_list_id: list1.id, to_list_id: backlog.id } - - described_class.new(project, user, params).execute(issue) - - expect(issue.reload.labels).to contain_exactly(bug) - end - end - - context 'when moving from backlog to done' do - it 'closes the issue' do - issue = create(:labeled_issue, project: project, labels: [bug]) - params = { board_id: board1.id, from_list_id: backlog.id, to_list_id: done.id } - - described_class.new(project, user, params).execute(issue) - issue.reload - - expect(issue.labels).to contain_exactly(bug) - expect(issue).to be_closed - end - end - context 'when moving an issue between lists' do let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) } let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } } @@ -113,19 +77,6 @@ describe Boards::Issues::MoveService, services: true do end end - context 'when moving from done to backlog' do - it 'reopens the issue' do - issue = create(:labeled_issue, :closed, project: project, labels: [bug]) - params = { board_id: board1.id, from_list_id: done.id, to_list_id: backlog.id } - - described_class.new(project, user, params).execute(issue) - issue.reload - - expect(issue.labels).to contain_exactly(bug) - expect(issue).to be_reopened - end - end - context 'when moving to same list' do let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) } let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } } diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb index a7e9efcf93f..ebac38e68f1 100644 --- a/spec/services/boards/lists/create_service_spec.rb +++ b/spec/services/boards/lists/create_service_spec.rb @@ -21,7 +21,7 @@ describe Boards::Lists::CreateService, services: true do end end - context 'when board lists has backlog, and done lists' do + context 'when board lists has the done list' do it 'creates a new list at beginning of the list' do list = service.execute(board) @@ -40,7 +40,7 @@ describe Boards::Lists::CreateService, services: true do end end - context 'when board lists has backlog, label and done lists' do + context 'when board lists has label and done lists' do it 'creates a new list at end of the label lists' do list1 = create(:list, board: board, position: 0) diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb index 628caf03476..a30860f828a 100644 --- a/spec/services/boards/lists/destroy_service_spec.rb +++ b/spec/services/boards/lists/destroy_service_spec.rb @@ -15,7 +15,6 @@ describe Boards::Lists::DestroyService, services: true do end it 'decrements position of higher lists' do - backlog = board.backlog_list development = create(:list, board: board, position: 0) review = create(:list, board: board, position: 1) staging = create(:list, board: board, position: 2) @@ -23,20 +22,12 @@ describe Boards::Lists::DestroyService, services: true do described_class.new(project, user).execute(development) - expect(backlog.reload.position).to be_nil expect(review.reload.position).to eq 0 expect(staging.reload.position).to eq 1 expect(done.reload.position).to be_nil end end - it 'does not remove list from board when list type is backlog' do - list = board.backlog_list - service = described_class.new(project, user) - - expect { service.execute(list) }.not_to change(board.lists, :count) - end - it 'does not remove list from board when list type is done' do list = board.done_list service = described_class.new(project, user) diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb index 334cee3f06d..2dffc62b215 100644 --- a/spec/services/boards/lists/list_service_spec.rb +++ b/spec/services/boards/lists/list_service_spec.rb @@ -10,7 +10,7 @@ describe Boards::Lists::ListService, services: true do service = described_class.new(project, double) - expect(service.execute(board)).to eq [board.backlog_list, list, board.done_list] + expect(service.execute(board)).to eq [list, board.done_list] end end end diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb index 63fa0bb8c5f..3786dc82bf0 100644 --- a/spec/services/boards/lists/move_service_spec.rb +++ b/spec/services/boards/lists/move_service_spec.rb @@ -6,7 +6,6 @@ describe Boards::Lists::MoveService, services: true do let(:board) { create(:board, project: project) } let(:user) { create(:user) } - let!(:backlog) { create(:backlog_list, board: board) } let!(:planning) { create(:list, board: board, position: 0) } let!(:development) { create(:list, board: board, position: 1) } let!(:review) { create(:list, board: board, position: 2) } @@ -87,14 +86,6 @@ describe Boards::Lists::MoveService, services: true do end end - it 'keeps position of lists when list type is backlog' do - service = described_class.new(project, user, position: 2) - - service.execute(backlog) - - expect(current_list_positions).to eq [0, 1, 2, 3] - end - it 'keeps position of lists when list type is done' do service = described_class.new(project, user, position: 2) -- cgit v1.2.1 From 79a132a38d3840074a3ab573b97d56155c84aed4 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 27 Jan 2017 03:25:16 -0200 Subject: Fix spec for Projects::Boards::ListsController --- spec/controllers/projects/boards/lists_controller_spec.rb | 2 +- spec/fixtures/api/schemas/list.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb index 34d6119429d..b3f9f76a50c 100644 --- a/spec/controllers/projects/boards/lists_controller_spec.rb +++ b/spec/controllers/projects/boards/lists_controller_spec.rb @@ -27,7 +27,7 @@ describe Projects::Boards::ListsController do parsed_response = JSON.parse(response.body) expect(response).to match_response_schema('lists') - expect(parsed_response.length).to eq 3 + expect(parsed_response.length).to eq 2 end context 'with unauthorized user' do diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index 8d94cf26ecb..819287bf919 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -10,7 +10,7 @@ "id": { "type": "integer" }, "list_type": { "type": "string", - "enum": ["backlog", "label", "done"] + "enum": ["label", "done"] }, "label": { "type": ["object", "null"], -- cgit v1.2.1 From 158173a126bcb73af3b2d8b16f767a404c7b91a5 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 27 Jan 2017 03:26:48 -0200 Subject: Update the API endpoint to get the lists of a project board --- lib/api/boards.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/api/boards.rb b/lib/api/boards.rb index 4ac491edc1b..13752eb4947 100644 --- a/lib/api/boards.rb +++ b/lib/api/boards.rb @@ -37,7 +37,7 @@ module API end desc 'Get the lists of a project board' do - detail 'Does not include `backlog` and `done` lists. This feature was introduced in 8.13' + detail 'Does not include `done` list. This feature was introduced in 8.13' success Entities::List end get '/lists' do -- cgit v1.2.1 From a3edd5f1fd154865bfe014d628844796090998e5 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 27 Jan 2017 03:27:15 -0200 Subject: Remove backlog lists from factories --- spec/factories/boards.rb | 1 - spec/factories/lists.rb | 6 ------ 2 files changed, 7 deletions(-) diff --git a/spec/factories/boards.rb b/spec/factories/boards.rb index ec46146d9b5..a581725245a 100644 --- a/spec/factories/boards.rb +++ b/spec/factories/boards.rb @@ -3,7 +3,6 @@ FactoryGirl.define do project factory: :empty_project after(:create) do |board| - board.lists.create(list_type: :backlog) board.lists.create(list_type: :done) end end diff --git a/spec/factories/lists.rb b/spec/factories/lists.rb index 9e3f06c682c..2a2f3cca91c 100644 --- a/spec/factories/lists.rb +++ b/spec/factories/lists.rb @@ -6,12 +6,6 @@ FactoryGirl.define do sequence(:position) end - factory :backlog_list, parent: :list do - list_type :backlog - label nil - position nil - end - factory :done_list, parent: :list do list_type :done label nil -- cgit v1.2.1 From 8a2ecebe439629239ffe3e414477b7fe6a033177 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 27 Jan 2017 03:28:06 -0200 Subject: Fix feature spec to create new issue on issue boards --- spec/features/boards/new_issue_spec.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index a03cd6fbf2d..6d14a8cf483 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -6,6 +6,7 @@ describe 'Issue Boards new issue', feature: true, js: true do let(:project) { create(:empty_project, :public) } let(:board) { create(:board, project: project) } + let!(:list) { create(:list, board: board, position: 0) } let(:user) { create(:user) } context 'authorized user' do @@ -17,7 +18,7 @@ describe 'Issue Boards new issue', feature: true, js: true do visit namespace_project_board_path(project.namespace, project, board) wait_for_vue_resource - expect(page).to have_selector('.board', count: 3) + expect(page).to have_selector('.board', count: 2) end it 'displays new issue button' do @@ -25,7 +26,7 @@ describe 'Issue Boards new issue', feature: true, js: true do end it 'does not display new issue button in done list' do - page.within('.board:nth-child(3)') do + page.within('.board:nth-child(2)') do expect(page).not_to have_selector('.board-issue-count-holder .btn') end end -- cgit v1.2.1 From f99a1edf705c26cb9602596e18a9213857894cab Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 27 Jan 2017 03:28:45 -0200 Subject: Fix feature spec for issue sidebar on issue boards --- spec/features/boards/sidebar_spec.rb | 63 +++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index c28bb0dcdae..a0f32d2ab76 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -4,14 +4,17 @@ describe 'Issue Boards', feature: true, js: true do include WaitForAjax include WaitForVueResource - let(:project) { create(:empty_project, :public) } - let(:board) { create(:board, project: project) } - let(:user) { create(:user) } - let!(:label) { create(:label, project: project) } - let!(:label2) { create(:label, project: project) } - let!(:milestone) { create(:milestone, project: project) } - let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) } - let!(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + let!(:milestone) { create(:milestone, project: project) } + let!(:development) { create(:label, project: project, name: 'Development') } + let!(:bug) { create(:label, project: project, name: 'Bug') } + let!(:regression) { create(:label, project: project, name: 'Regression') } + let!(:stretch) { create(:label, project: project, name: 'Stretch') } + let!(:issue1) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [development]) } + let!(:issue2) { create(:labeled_issue, project: project, labels: [development, stretch]) } + let(:board) { create(:board, project: project) } + let!(:list) { create(:list, board: board, label: development, position: 0) } before do project.team << [user, :master] @@ -62,8 +65,8 @@ describe 'Issue Boards', feature: true, js: true do end page.within('.issue-boards-sidebar') do - expect(page).to have_content(issue.title) - expect(page).to have_content(issue.to_reference) + expect(page).to have_content(issue2.title) + expect(page).to have_content(issue2.to_reference) end end @@ -244,22 +247,22 @@ describe 'Issue Boards', feature: true, js: true do wait_for_ajax - click_link label.title + click_link bug.title wait_for_vue_resource find('.dropdown-menu-close-icon').click page.within('.value') do - expect(page).to have_selector('.label', count: 1) - expect(page).to have_content(label.title) + expect(page).to have_selector('.label', count: 3) + expect(page).to have_content(bug.title) end end page.within(first('.board')) do page.within(first('.card')) do - expect(page).to have_selector('.label', count: 1) - expect(page).to have_content(label.title) + expect(page).to have_selector('.label', count: 2) + expect(page).to have_content(bug.title) end end end @@ -274,32 +277,32 @@ describe 'Issue Boards', feature: true, js: true do wait_for_ajax - click_link label.title - click_link label2.title + click_link bug.title + click_link regression.title wait_for_vue_resource find('.dropdown-menu-close-icon').click page.within('.value') do - expect(page).to have_selector('.label', count: 2) - expect(page).to have_content(label.title) - expect(page).to have_content(label2.title) + expect(page).to have_selector('.label', count: 4) + expect(page).to have_content(bug.title) + expect(page).to have_content(regression.title) end end page.within(first('.board')) do page.within(first('.card')) do - expect(page).to have_selector('.label', count: 2) - expect(page).to have_content(label.title) - expect(page).to have_content(label2.title) + expect(page).to have_selector('.label', count: 3) + expect(page).to have_content(bug.title) + expect(page).to have_content(regression.title) end end end it 'removes a label' do page.within(first('.board')) do - find('.card:nth-child(2)').click + first('.card').click end page.within('.labels') do @@ -307,22 +310,22 @@ describe 'Issue Boards', feature: true, js: true do wait_for_ajax - click_link label.title + click_link stretch.title wait_for_vue_resource find('.dropdown-menu-close-icon').click page.within('.value') do - expect(page).to have_selector('.label', count: 0) - expect(page).not_to have_content(label.title) + expect(page).to have_selector('.label', count: 1) + expect(page).not_to have_content(stretch.title) end end page.within(first('.board')) do - page.within(find('.card:nth-child(2)')) do - expect(page).not_to have_selector('.label', count: 1) - expect(page).not_to have_content(label.title) + page.within(first('.card')) do + expect(page).not_to have_selector('.label') + expect(page).not_to have_content(stretch.title) end end end -- cgit v1.2.1 From c4fc17f258d10ec86d0023c44126362ca8fa63c4 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 27 Jan 2017 11:14:50 +0000 Subject: Boards spec update to take into account removed backlog --- .../boards/components/issue_card_inner.js.es6 | 2 +- spec/features/boards/boards_spec.rb | 217 +++++++-------------- 2 files changed, 73 insertions(+), 146 deletions(-) diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 index 383b329edc3..61fcfde0a02 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 +++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 @@ -45,7 +45,7 @@ <div> <h4 class="card-title"> <i - class="fa fa-eye-flash" + class="fa fa-eye-slash confidential-icon" v-if="issue.confidential"></i> <a :href="issueLinkBase + '/' + issue.id" diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index bfac5a1b8ab..34f47daf0e5 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -20,7 +20,7 @@ describe 'Issue Boards', feature: true, js: true do before do visit namespace_project_board_path(project.namespace, project, board) wait_for_vue_resource - expect(page).to have_selector('.board', count: 3) + expect(page).to have_selector('.board', count: 2) end it 'shows blank state' do @@ -31,18 +31,18 @@ describe 'Issue Boards', feature: true, js: true do page.within(find('.board-blank-state')) do click_button("Nevermind, I'll use my own") end - expect(page).to have_selector('.board', count: 2) + expect(page).to have_selector('.board', count: 1) end it 'creates default lists' do - lists = ['Backlog', 'To Do', 'Doing', 'Done'] + lists = ['To Do', 'Doing', 'Done'] page.within(find('.board-blank-state')) do click_button('Add default lists') end wait_for_vue_resource - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 3) page.all('.board').each_with_index do |list, i| expect(list.find('.board-title')).to have_content(lists[i]) @@ -64,42 +64,41 @@ describe 'Issue Boards', feature: true, js: true do let!(:list1) { create(:list, board: board, label: planning, position: 0) } let!(:list2) { create(:list, board: board, label: development, position: 1) } - let!(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } - let!(:issue1) { create(:issue, project: project, assignee: user) } - let!(:issue2) { create(:issue, project: project, author: user2) } - let!(:issue3) { create(:issue, project: project) } - let!(:issue4) { create(:issue, project: project) } + let!(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning]) } + let!(:issue1) { create(:labeled_issue, project: project, assignee: user, labels: [planning]) } + let!(:issue2) { create(:labeled_issue, project: project, author: user2, labels: [planning]) } + let!(:issue3) { create(:labeled_issue, project: project, labels: [planning]) } + let!(:issue4) { create(:labeled_issue, project: project, labels: [planning]) } let!(:issue5) { create(:labeled_issue, project: project, labels: [planning], milestone: milestone) } let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) } let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) } let!(:issue8) { create(:closed_issue, project: project) } - let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) } + let!(:issue9) { create(:labeled_issue, project: project, labels: [planning, testing, bug, accepting]) } before do visit namespace_project_board_path(project.namespace, project, board) wait_for_vue_resource - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 3) expect(find('.board:nth-child(1)')).to have_selector('.card') expect(find('.board:nth-child(2)')).to have_selector('.card') expect(find('.board:nth-child(3)')).to have_selector('.card') - expect(find('.board:nth-child(4)')).to have_selector('.card') end it 'shows lists' do - expect(page).to have_selector('.board', count: 4) + expect(page).to have_selector('.board', count: 3) end it 'shows description tooltip on list title' do - page.within('.board:nth-child(2)') do + page.within('.board:nth-child(1)') do expect(find('.board-title span.has-tooltip')[:title]).to eq('Test') end end it 'shows issues in lists' do + wait_for_board_cards(1, 8) wait_for_board_cards(2, 2) - wait_for_board_cards(3, 2) end it 'shows confidential issues with icon' do @@ -108,19 +107,6 @@ describe 'Issue Boards', feature: true, js: true do end end - it 'search backlog list' do - page.within('#js-boards-search') do - find('.form-control').set(issue1.title) - end - - wait_for_vue_resource - - expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) - expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) - end - it 'search done list' do page.within('#js-boards-search') do find('.form-control').set(issue8.title) @@ -130,8 +116,7 @@ describe 'Issue Boards', feature: true, js: true do expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 1) end it 'search list' do @@ -141,157 +126,135 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource - expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1) + expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0) expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0) - expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0) end it 'allows user to delete board' do - page.within(find('.board:nth-child(2)')) do + page.within(find('.board:nth-child(1)')) do find('.board-delete').click end wait_for_vue_resource - expect(page).to have_selector('.board', count: 3) + expect(page).to have_selector('.board', count: 2) end it 'removes checkmark in new list dropdown after deleting' do click_button 'Add list' wait_for_ajax - page.within(find('.board:nth-child(2)')) do + page.within(find('.board:nth-child(1)')) do find('.board-delete').click end wait_for_vue_resource - expect(page).to have_selector('.board', count: 3) - expect(find(".js-board-list-#{planning.id}", visible: false)).not_to have_css('.is-active') + expect(page).to have_selector('.board', count: 2) end it 'infinite scrolls list' do 50.times do - create(:issue, project: project) + create(:labeled_issue, project: project, labels: [planning]) end visit namespace_project_board_path(project.namespace, project, board) wait_for_vue_resource page.within(find('.board', match: :first)) do - expect(page.find('.board-header')).to have_content('56') + expect(page.find('.board-header')).to have_content('58') expect(page).to have_selector('.card', count: 20) - expect(page).to have_content('Showing 20 of 56 issues') + expect(page).to have_content('Showing 20 of 58 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") wait_for_vue_resource expect(page).to have_selector('.card', count: 40) - expect(page).to have_content('Showing 40 of 56 issues') + expect(page).to have_content('Showing 40 of 58 issues') evaluate_script("document.querySelectorAll('.board .board-list')[0].scrollTop = document.querySelectorAll('.board .board-list')[0].scrollHeight") wait_for_vue_resource - expect(page).to have_selector('.card', count: 56) + expect(page).to have_selector('.card', count: 58) expect(page).to have_content('Showing all issues') end end - context 'backlog' do - it 'shows issues in backlog with no labels' do - wait_for_board_cards(1, 6) - end - - it 'moves issue from backlog into list' do - drag_to(list_to_index: 1) - - wait_for_vue_resource - wait_for_board_cards(1, 5) - wait_for_board_cards(2, 3) - end - end - context 'done' do it 'shows list of done issues' do - wait_for_board_cards(4, 1) + wait_for_board_cards(3, 1) wait_for_ajax end it 'moves issue to done' do - drag_to(list_from_index: 0, list_to_index: 3) + drag_to(list_from_index: 0, list_to_index: 2) - wait_for_board_cards(1, 5) + wait_for_board_cards(1, 7) wait_for_board_cards(2, 2) wait_for_board_cards(3, 2) - wait_for_board_cards(4, 2) expect(find('.board:nth-child(1)')).not_to have_content(issue9.title) - expect(find('.board:nth-child(4)')).to have_selector('.card', count: 2) - expect(find('.board:nth-child(4)')).to have_content(issue9.title) - expect(find('.board:nth-child(4)')).not_to have_content(planning.title) + expect(find('.board:nth-child(3)')).to have_selector('.card', count: 2) + expect(find('.board:nth-child(3)')).to have_content(issue9.title) + expect(find('.board:nth-child(3)')).not_to have_content(planning.title) end it 'removes all of the same issue to done' do - drag_to(list_from_index: 1, list_to_index: 3) + drag_to(list_from_index: 0, list_to_index: 2) - wait_for_board_cards(1, 6) - wait_for_board_cards(2, 1) - wait_for_board_cards(3, 1) - wait_for_board_cards(4, 2) + wait_for_board_cards(1, 7) + wait_for_board_cards(2, 2) + wait_for_board_cards(3, 2) - expect(find('.board:nth-child(2)')).not_to have_content(issue6.title) - expect(find('.board:nth-child(4)')).to have_content(issue6.title) - expect(find('.board:nth-child(4)')).not_to have_content(planning.title) + expect(find('.board:nth-child(1)')).not_to have_content(issue9.title) + expect(find('.board:nth-child(3)')).to have_content(issue9.title) + expect(find('.board:nth-child(3)')).not_to have_content(planning.title) end end context 'lists' do it 'changes position of list' do - drag_to(list_from_index: 1, list_to_index: 2, selector: '.board-header') + drag_to(list_from_index: 1, list_to_index: 0, selector: '.board-header') - wait_for_board_cards(1, 6) - wait_for_board_cards(2, 2) - wait_for_board_cards(3, 2) - wait_for_board_cards(4, 1) + wait_for_board_cards(1, 2) + wait_for_board_cards(2, 8) + wait_for_board_cards(3, 1) - expect(find('.board:nth-child(2)')).to have_content(development.title) - expect(find('.board:nth-child(2)')).to have_content(planning.title) + expect(find('.board:nth-child(1)')).to have_content(development.title) + expect(find('.board:nth-child(1)')).to have_content(planning.title) end it 'issue moves between lists' do - drag_to(list_from_index: 1, card_index: 1, list_to_index: 2) + drag_to(list_from_index: 0, card_index: 1, list_to_index: 1) - wait_for_board_cards(1, 6) - wait_for_board_cards(2, 1) - wait_for_board_cards(3, 3) - wait_for_board_cards(4, 1) + wait_for_board_cards(1, 7) + wait_for_board_cards(2, 2) + wait_for_board_cards(3, 1) - expect(find('.board:nth-child(3)')).to have_content(issue6.title) - expect(find('.board:nth-child(3)').all('.card').last).not_to have_content(development.title) + expect(find('.board:nth-child(2)')).to have_content(issue6.title) + expect(find('.board:nth-child(2)').all('.card').last).not_to have_content(development.title) end it 'issue moves between lists' do - drag_to(list_from_index: 2, list_to_index: 1) + drag_to(list_from_index: 1, list_to_index: 0) - wait_for_board_cards(1, 6) - wait_for_board_cards(2, 3) + wait_for_board_cards(1, 9) + wait_for_board_cards(2, 1) wait_for_board_cards(3, 1) - wait_for_board_cards(4, 1) - expect(find('.board:nth-child(2)')).to have_content(issue7.title) - expect(find('.board:nth-child(2)').all('.card').first).not_to have_content(planning.title) + expect(find('.board:nth-child(1)')).to have_content(issue7.title) + expect(find('.board:nth-child(1)').all('.card').first).not_to have_content(planning.title) end it 'issue moves from done' do - drag_to(list_from_index: 3, list_to_index: 1) + drag_to(list_from_index: 2, list_to_index: 1) expect(find('.board:nth-child(2)')).to have_content(issue8.title) - wait_for_board_cards(1, 6) + wait_for_board_cards(1, 8) wait_for_board_cards(2, 3) - wait_for_board_cards(3, 2) - wait_for_board_cards(4, 0) + wait_for_board_cards(3, 0) end context 'issue card' do @@ -324,7 +287,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource - expect(page).to have_selector('.board', count: 5) + expect(page).to have_selector('.board', count: 4) end it 'creates new list for Backlog label' do @@ -337,7 +300,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource - expect(page).to have_selector('.board', count: 5) + expect(page).to have_selector('.board', count: 4) end it 'creates new list for Done label' do @@ -350,7 +313,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource - expect(page).to have_selector('.board', count: 5) + expect(page).to have_selector('.board', count: 4) end it 'keeps dropdown open after adding new list' do @@ -366,21 +329,6 @@ describe 'Issue Boards', feature: true, js: true do expect(find('.issue-boards-search')).to have_selector('.open') end - it 'moves issues from backlog into new list' do - wait_for_board_cards(1, 6) - - click_button 'Add list' - wait_for_ajax - - page.within('.dropdown-menu-issues-board-new') do - click_link testing.title - end - - wait_for_vue_resource - - wait_for_board_cards(1, 5) - end - it 'creates new list from a new label' do click_button 'Add list' @@ -397,7 +345,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_ajax wait_for_vue_resource - expect(page).to have_selector('.board', count: 5) + expect(page).to have_selector('.board', count: 4) end end end @@ -418,7 +366,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource wait_for_board_cards(1, 1) - wait_for_empty_boards((2..4)) + wait_for_empty_boards((2..3)) end it 'filters by assignee' do @@ -437,7 +385,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource wait_for_board_cards(1, 1) - wait_for_empty_boards((2..4)) + wait_for_empty_boards((2..3)) end it 'filters by milestone' do @@ -454,10 +402,9 @@ describe 'Issue Boards', feature: true, js: true do end wait_for_vue_resource - wait_for_board_cards(1, 0) - wait_for_board_cards(2, 1) + wait_for_board_cards(1, 1) + wait_for_board_cards(2, 0) wait_for_board_cards(3, 0) - wait_for_board_cards(4, 0) end it 'filters by label' do @@ -474,7 +421,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource wait_for_board_cards(1, 1) - wait_for_empty_boards((2..4)) + wait_for_empty_boards((2..3)) end it 'filters by label with space after reload' do @@ -530,7 +477,7 @@ describe 'Issue Boards', feature: true, js: true do it 'infinite scrolls list with label filter' do 50.times do - create(:labeled_issue, project: project, labels: [testing]) + create(:labeled_issue, project: project, labels: [planning, testing]) end page.within '.issues-filters' do @@ -580,32 +527,12 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource wait_for_board_cards(1, 1) - wait_for_empty_boards((2..4)) - end - - it 'filters by no label' do - page.within '.issues-filters' do - click_button('Label') - wait_for_ajax - - page.within '.dropdown-menu-labels' do - click_link("No Label") - wait_for_vue_resource - find('.dropdown-menu-close').click - end - end - - wait_for_vue_resource - - wait_for_board_cards(1, 5) - wait_for_board_cards(2, 0) - wait_for_board_cards(3, 0) - wait_for_board_cards(4, 1) + wait_for_empty_boards((2..3)) end it 'filters by clicking label button on issue' do page.within(find('.board', match: :first)) do - expect(page).to have_selector('.card', count: 6) + expect(page).to have_selector('.card', count: 8) expect(find('.card', match: :first)).to have_content(bug.title) click_button(bug.title) wait_for_vue_resource @@ -614,7 +541,7 @@ describe 'Issue Boards', feature: true, js: true do wait_for_vue_resource wait_for_board_cards(1, 1) - wait_for_empty_boards((2..4)) + wait_for_empty_boards((2..3)) page.within('.labels-filter') do expect(find('.dropdown-toggle-text')).to have_content(bug.title) -- cgit v1.2.1 From ac7949db174210a84fd4d2af43652559f56dbdb9 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 27 Jan 2017 11:20:29 +0000 Subject: Fixed masonry re-triggering when it didnt need to Fixed scroll position being lost --- app/assets/javascripts/boards/components/modal/list.js.es6 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 6a681d9278f..4ef40f851d4 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -14,8 +14,10 @@ this.initMasonry(); }, issues: { - handler() { - this.initMasonry(); + handler(issues, oldIssues) { + if (this.activeTab === 'selected' || issues.length !== oldIssues.length) { + this.initMasonry(); + } }, deep: true, }, @@ -37,11 +39,15 @@ return issue.selected; }, initMasonry() { + const listScrollTop = this.$refs.list.scrollTop; + this.$nextTick(() => { this.destroyMasonry(); listMasonry = new Masonry(this.$refs.list, { transitionDuration: 0, }); + + this.$refs.list.scrollTop = listScrollTop; }); }, destroyMasonry() { -- cgit v1.2.1 From 0904e9b107ce78fed80830e00b8fc3621cb98f8f Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 27 Jan 2017 11:58:43 +0000 Subject: Infinite scrolling --- .../boards/components/modal/index.js.es6 | 9 +++++++- .../boards/components/modal/list.js.es6 | 24 ++++++++++++++++++---- .../javascripts/boards/stores/modal_store.js.es6 | 3 +++ app/assets/stylesheets/pages/boards.scss | 5 +++++ app/controllers/projects/boards_controller.rb | 2 +- 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index 9cab701bb5d..eedfd826787 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -15,6 +15,9 @@ return ModalStore.store; }, watch: { + page() { + this.loadIssues(); + }, searchTerm() { this.searchOperation(); }, @@ -34,15 +37,17 @@ }, methods: { searchOperation: _.debounce(function searchOperationDebounce() { + this.issues = []; this.loadIssues(); }, 500), loadIssues() { return gl.boardService.getBacklog({ search: this.searchTerm, + page: this.page, + per: this.perPage, }).then((res) => { const data = res.json(); - this.issues = []; data.forEach((issueObj) => { const issue = new ListIssue(issueObj); const foundSelectedIssue = ModalStore.findSelectedIssue(issue); @@ -50,6 +55,8 @@ this.issues.push(issue); }); + + this.loadingNewPage = false; }); }, }, diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 4ef40f851d4..a0b91e6f16a 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -14,10 +14,8 @@ this.initMasonry(); }, issues: { - handler(issues, oldIssues) { - if (this.activeTab === 'selected' || issues.length !== oldIssues.length) { - this.initMasonry(); - } + handler() { + this.initMasonry(); }, deep: true, }, @@ -33,6 +31,15 @@ }, methods: { toggleIssue: ModalStore.toggleIssue.bind(ModalStore), + listHeight () { + return this.$refs.list.getBoundingClientRect().height; + }, + scrollHeight () { + return this.$refs.list.scrollHeight; + }, + scrollTop () { + return this.$refs.list.scrollTop + this.listHeight(); + }, showIssue(issue) { if (this.activeTab === 'all') return true; @@ -59,6 +66,15 @@ }, mounted() { this.initMasonry(); + + this.$refs.list.onscroll = () => { + const currentPage = Math.floor(this.issues.length / this.perPage); + + if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage && currentPage === this.page) { + this.loadingNewPage = true; + this.page += 1; + } + }; }, destroyed() { this.destroyMasonry(); diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index 54419751433..2d8da482e5f 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -12,6 +12,9 @@ selectedList: {}, searchTerm: '', loading: false, + loadingNewPage: false, + page: 1, + perPage: 50, }; } diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 04e7aea63fd..d3911ae233a 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -372,6 +372,7 @@ flex-direction: column; width: 90vw; height: 85vh; + max-width: 1100px; min-height: 500px; margin: auto; padding: 25px 15px 0; @@ -396,6 +397,8 @@ margin: -25px -15px -5px; border-top: 0; border-bottom: 1px solid $border-color; + border-top-right-radius: $border-radius-default; + border-top-left-radius: $border-radius-default; > h2 { margin: 0; @@ -448,6 +451,8 @@ margin-right: -15px; padding-left: 15px; padding-right: 15px; + border-bottom-right-radius: $border-radius-default; + border-bottom-left-radius: $border-radius-default; } .add-issues-footer-to-list { diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index ba9d0eaa265..1eaa2ffdf29 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -33,7 +33,7 @@ class Projects::BoardsController < Projects::ApplicationController LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") .where(label_id: board.lists.movable.pluck(:label_id)).limit(1).arel.exists ) - @issues = @issues.page(params[:page]).per(50) + @issues = @issues.page(params[:page]).per(params[:per]) render json: @issues.as_json( labels: true, -- cgit v1.2.1 From 8b977b295e887f7d134cd92ec352a24c0afd5950 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 27 Jan 2017 13:03:17 +0000 Subject: Fixed some failing lint tests --- app/assets/javascripts/boards/components/modal/footer.js.es6 | 2 +- app/assets/javascripts/boards/components/modal/list.js.es6 | 9 +++++---- app/assets/javascripts/boards/models/issue.js.es6 | 2 +- app/assets/stylesheets/pages/boards.scss | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 1be58b017ef..81b1aa1da77 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -26,7 +26,7 @@ }, addIssues() { const list = this.selectedList; - const issueIds = this.selectedIssues.map(issue => issue._id); + const issueIds = this.selectedIssues.map(issue => issue.globalId); // Post the data to the backend this.$http.post(this.bulkUpdatePath, { diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index a0b91e6f16a..c0c3f4b8d8f 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -31,13 +31,13 @@ }, methods: { toggleIssue: ModalStore.toggleIssue.bind(ModalStore), - listHeight () { + listHeight() { return this.$refs.list.getBoundingClientRect().height; }, - scrollHeight () { + scrollHeight() { return this.$refs.list.scrollHeight; }, - scrollTop () { + scrollTop() { return this.$refs.list.scrollTop + this.listHeight(); }, showIssue(issue) { @@ -70,7 +70,8 @@ this.$refs.list.onscroll = () => { const currentPage = Math.floor(this.issues.length / this.perPage); - if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage && currentPage === this.page) { + if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage + && currentPage === this.page) { this.loadingNewPage = true; this.page += 1; } diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6 index 5b435a76467..2d0a295ae4d 100644 --- a/app/assets/javascripts/boards/models/issue.js.es6 +++ b/app/assets/javascripts/boards/models/issue.js.es6 @@ -6,7 +6,7 @@ class ListIssue { constructor (obj) { - this._id = obj.id; + this.globalId = obj.id; this.id = obj.iid; this.title = obj.title; this.confidential = obj.confidential; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index d3911ae233a..cf1c52a2b38 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -255,7 +255,7 @@ .form-control { display: inline-block; width: 210px; - margin-right: 10px + margin-right: 10px; } } -- cgit v1.2.1 From a943241056961c7b820adfd8fd08edd25c3a563a Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 27 Jan 2017 16:49:56 +0000 Subject: Some styling updates Does not remove the issue from the selected tab when it it de-selected, it instead gets purged when changing tab --- .../boards/components/modal/footer.js.es6 | 8 ++++--- .../boards/components/modal/header.js.es6 | 7 +++++- .../boards/components/modal/list.js.es6 | 8 ++++++- .../boards/components/modal/lists_dropdown.js.es6 | 4 ++-- .../javascripts/boards/stores/modal_store.js.es6 | 28 ++++++++++++++++++---- app/assets/stylesheets/pages/boards.scss | 24 ++++++++++--------- app/views/shared/issuable/_filter.html.haml | 4 ++-- spec/features/boards/add_issues_modal_spec.rb | 3 ++- spec/javascripts/boards/modal_store_spec.js.es6 | 19 +++++++++++++++ 9 files changed, 79 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 81b1aa1da77..1a147daca7a 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -17,7 +17,7 @@ submitText() { const count = ModalStore.selectedCount(); - return `Add ${count} issue${count > 1 || !count ? 's' : ''}`; + return `Add ${count > 0 ? count : ''} issue${count > 1 || !count ? 's' : ''}`; }, }, methods: { @@ -26,7 +26,9 @@ }, addIssues() { const list = this.selectedList; - const issueIds = this.selectedIssues.map(issue => issue.globalId); + const selectedIssues = ModalStore.getSelectedIssues(); + const issueIds = selectedIssues.filter(issue => issue.selected) + .map(issue => issue.globalId); // Post the data to the backend this.$http.post(this.bulkUpdatePath, { @@ -37,7 +39,7 @@ }); // Add the issues on the frontend - this.selectedIssues.forEach((issue) => { + selectedIssues.forEach((issue) => { list.addIssue(issue); list.issuesSize += 1; }); diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index d7d896e3be1..ebb2ee1a82f 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -17,7 +17,11 @@ }, }, methods: { - toggleAll: ModalStore.toggleAll.bind(ModalStore), + toggleAll() { + this.$refs.selectAllBtn.blur(); + + ModalStore.toggleAll(); + } }, components: { 'modal-tabs': gl.issueBoards.ModalTabs, @@ -49,6 +53,7 @@ <button type="button" class="btn btn-success btn-inverted prepend-left-10" + ref="selectAllBtn" @click="toggleAll"> {{ selectAllText }} </button> diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index c0c3f4b8d8f..605c1101666 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -12,6 +12,10 @@ watch: { activeTab() { this.initMasonry(); + + if (this.activeTab === 'all') { + ModalStore.purgeUnselectedIssues(); + } }, issues: { handler() { @@ -43,7 +47,9 @@ showIssue(issue) { if (this.activeTab === 'all') return true; - return issue.selected; + const index = ModalStore.selectedIssueIndex(issue); + + return index !== -1; }, initMasonry() { const listScrollTop = this.$refs.list.scrollTop; diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 index b205c019a31..d6bb755001a 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 @@ -21,11 +21,11 @@ type="button" data-toggle="dropdown" aria-expanded="false"> - {{ selected.title }} <span - class="dropdown-label-box pull-right" + class="dropdown-label-box" :style="{ backgroundColor: selected.label.color }"> </span> + {{ selected.title }} <i class="fa fa-chevron-down"></i> </button> <div class="dropdown-menu dropdown-menu-selectable"> diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index 2d8da482e5f..391765c4978 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -19,7 +19,7 @@ } selectedCount() { - return this.store.selectedIssues.length; + return this.store.selectedIssues.filter(issue => issue.selected).length; } toggleIssue(issueObj) { @@ -51,13 +51,31 @@ }); } - addSelectedIssue(issue) { - this.store.selectedIssues.push(issue); + getSelectedIssues() { + return this.store.selectedIssues.filter(issue => issue.selected); } - removeSelectedIssue(issue) { + addSelectedIssue(issue) { const index = this.selectedIssueIndex(issue); - this.store.selectedIssues.splice(index, 1); + + if (index === -1) { + this.store.selectedIssues.push(issue); + } + } + + removeSelectedIssue(issue, forcePurge = false) { + if (this.store.activeTab === 'all' || forcePurge) { + const index = this.selectedIssueIndex(issue); + this.store.selectedIssues.splice(index, 1); + } + } + + purgeUnselectedIssues() { + this.store.selectedIssues.forEach((issue) => { + if (!issue.selected) { + this.removeSelectedIssue(issue, true); + } + }); } selectedIssueIndex(issue) { diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index cf1c52a2b38..e7321b19e15 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -255,7 +255,6 @@ .form-control { display: inline-block; width: 210px; - margin-right: 10px; } } @@ -387,8 +386,11 @@ > .row { width: 100%; - margin-top: auto; - margin-bottom: auto; + margin: auto 0; + } + + .svg-content { + margin-top: -40px; } } } @@ -420,7 +422,7 @@ .card-parent { width: 100%; - padding: 0 $gl-vert-padding ($gl-vert-padding * 2); + padding: 0 5px 5px; @media (min-width: $screen-sm-min) { width: 50%; @@ -433,6 +435,7 @@ .card { border: 1px solid $border-gray-dark; + box-shadow: 0 1px 2px rgba($issue-boards-card-shadow, .3); cursor: pointer; } } @@ -446,9 +449,7 @@ } .add-issues-footer { - margin-top: auto; - margin-left: -15px; - margin-right: -15px; + margin: auto -15px 0; padding-left: 15px; padding-right: 15px; border-bottom-right-radius: $border-radius-default; @@ -465,10 +466,11 @@ position: absolute; right: -3px; top: -3px; - width: 20px; - background-color: $blue-dark; + width: 17px; + background-color: $blue-light; color: $white-light; - font-size: 12px; - line-height: 20px; + border: 1px solid $border-blue-light; + font-size: 9px; + line-height: 17px; border-radius: 50%; } diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index b94bdf14d5e..1b1348d435a 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -38,10 +38,10 @@ #js-boards-search.issue-boards-search %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } - if can?(current_user, :admin_list, @project) - %button.btn.btn-create.btn-inverted.js-show-add-issues{ type: "button" } + %button.btn.btn-create.pull-right.prepend-left-10.js-show-add-issues{ type: "button" } Add issues .dropdown.pull-right - %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } + %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } Add list .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index e6065c0740d..5ba044a4f3c 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -10,6 +10,7 @@ describe 'Issue Boards add issue modal', :feature, :js do let!(:planning) { create(:label, project: project, name: 'Planning') } let!(:label) { create(:label, project: project) } let!(:list1) { create(:list, board: board, label: planning, position: 0) } + let!(:list2) { create(:list, board: board, label: label, position: 1) } let!(:issue) { create(:issue, project: project) } let!(:issue2) { create(:issue, project: project) } @@ -172,7 +173,7 @@ describe 'Issue Boards add issue modal', :feature, :js do first('.card').click - expect(page).not_to have_selector('.card') + expect(page).not_to have_selector('.is-active') end end end diff --git a/spec/javascripts/boards/modal_store_spec.js.es6 b/spec/javascripts/boards/modal_store_spec.js.es6 index 9c0625357bd..3f44e427201 100644 --- a/spec/javascripts/boards/modal_store_spec.js.es6 +++ b/spec/javascripts/boards/modal_store_spec.js.es6 @@ -81,6 +81,7 @@ describe('Modal store', () => { }); it('adds issue to selected array', () => { + issue.selected = true; Store.addSelectedIssue(issue); expect(Store.selectedCount()).toBe(1); @@ -112,4 +113,22 @@ describe('Modal store', () => { it('does not find a selected issue', () => { expect(Store.findSelectedIssue(issue)).toBe(undefined); }); + + it('does not remove from selected issue if tab is not all', () => { + Store.store.activeTab = 'selected'; + + Store.toggleIssue(issue); + Store.toggleIssue(issue); + + expect(Store.store.selectedIssues.length).toBe(1); + expect(Store.selectedCount()).toBe(0); + }); + + it('gets selected issue array with only selected issues', () => { + Store.toggleIssue(issue); + Store.toggleIssue(issue2); + Store.toggleIssue(issue2); + + expect(Store.getSelectedIssues().length).toBe(1); + }); }); -- cgit v1.2.1 From 26d61113cf74d5714dafd635951962a9d61cec55 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 27 Jan 2017 17:01:39 +0000 Subject: Lists dropdown drops up --- app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 | 2 +- app/assets/stylesheets/framework/dropdowns.scss | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 index d6bb755001a..bb2d43c4a21 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 @@ -28,7 +28,7 @@ {{ selected.title }} <i class="fa fa-chevron-down"></i> </button> - <div class="dropdown-menu dropdown-menu-selectable"> + <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up"> <ul> <li v-for="list in state.lists" diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 6bfb9a6d1cb..ca5861bf3e6 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -227,6 +227,11 @@ } } +.dropdown-menu-drop-up { + top: auto; + bottom: 100%; +} + .dropdown-menu-large { width: 340px; } -- cgit v1.2.1 From 7f58fc0e997a6784d978396747f39ce573dfe289 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 27 Jan 2017 17:13:37 +0000 Subject: Removed backlog frontend code --- app/assets/javascripts/boards/boards_bundle.js.es6 | 2 -- app/assets/javascripts/boards/models/list.js.es6 | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index e2b3d2c7df6..0e2794334fa 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -59,8 +59,6 @@ $(() => { gl.boardService.all() .then((resp) => { resp.json().forEach((board) => { - if (board.list_type === 'backlog') return; - const list = Store.addList(board); if (list.type === 'done') { diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6 index 3dd5f273057..5152be56b66 100644 --- a/app/assets/javascripts/boards/models/list.js.es6 +++ b/app/assets/javascripts/boards/models/list.js.es6 @@ -9,7 +9,7 @@ class List { this.position = obj.position; this.title = obj.title; this.type = obj.list_type; - this.preset = ['backlog', 'done', 'blank'].indexOf(this.type) > -1; + this.preset = ['done', 'blank'].indexOf(this.type) > -1; this.filters = gl.issueBoards.BoardsStore.state.filters; this.page = 1; this.loading = true; -- cgit v1.2.1 From 274987d5c0f7b04a9b7510c318c8df6dfab477df Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Fri, 27 Jan 2017 16:21:11 -0200 Subject: Reuse endpoint to list issues for a list instead of create a new one --- .../boards/components/modal/index.js.es6 | 2 +- .../boards/services/board_service.js.es6 | 6 +- .../projects/boards/issues_controller.rb | 2 +- app/controllers/projects/boards_controller.rb | 23 +------ app/services/boards/issues/list_service.rb | 14 ++-- config/routes/project.rb | 4 +- .../projects/boards/issues_controller_spec.rb | 76 ++++++++++++++-------- spec/services/boards/issues/list_service_spec.rb | 12 ++++ 8 files changed, 79 insertions(+), 60 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index eedfd826787..ababe3af0e3 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -48,7 +48,7 @@ }).then((res) => { const data = res.json(); - data.forEach((issueObj) => { + data.issues.forEach((issueObj) => { const issue = new ListIssue(issueObj); const foundSelectedIssue = ModalStore.findSelectedIssue(issue); issue.selected = foundSelectedIssue !== undefined; diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index 20df8f2b373..993f8599fd4 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -4,9 +4,9 @@ class BoardService { constructor (root, boardId) { this.boards = Vue.resource(`${root}{/id}.json`, {}, { - backlog: { + issues: { method: 'GET', - url: `${root}/${boardId}/backlog.json` + url: `${root}/${boardId}/issues.json` } }); this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, { @@ -73,7 +73,7 @@ class BoardService { } getBacklog(data) { - return this.boards.backlog(data); + return this.boards.issues(data); } } diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index dc33e1405f2..a061b575e21 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -59,7 +59,7 @@ module Projects end def filter_params - params.merge(board_id: params[:board_id], id: params[:list_id]) + params.merge(board_id: params[:board_id], id: params[:list_id]).compact end def move_params diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb index 1eaa2ffdf29..808affa4f98 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -1,7 +1,7 @@ class Projects::BoardsController < Projects::ApplicationController include IssuableCollections - # before_action :authorize_read_board!, only: [:index, :show, :backlog] + before_action :authorize_read_board!, only: [:index, :show] def index @boards = ::Boards::ListService.new(project, current_user).execute @@ -25,27 +25,6 @@ class Projects::BoardsController < Projects::ApplicationController end end - def backlog - board = project.boards.find(params[:id]) - - @issues = issues_collection - @issues = @issues.where.not( - LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") - .where(label_id: board.lists.movable.pluck(:label_id)).limit(1).arel.exists - ) - @issues = @issues.page(params[:page]).per(params[:per]) - - render json: @issues.as_json( - labels: true, - only: [:id, :iid, :title, :confidential, :due_date], - include: { - assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, - milestone: { only: [:id, :title] } - }, - user: current_user - ) - end - private def authorize_read_board! diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index fd4a462c7b2..8a94c54b6ab 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -3,8 +3,8 @@ module Boards class ListService < BaseService def execute issues = IssuesFinder.new(current_user, filter_params).execute - issues = without_board_labels(issues) unless list.movable? - issues = with_list_label(issues) if list.movable? + issues = without_board_labels(issues) unless movable_list? + issues = with_list_label(issues) if movable_list? issues end @@ -15,7 +15,13 @@ module Boards end def list - @list ||= board.lists.find(params[:id]) + return @list if defined?(@list) + + @list = board.lists.find(params[:id]) if params.key?(:id) + end + + def movable_list? + @movable_list ||= list.present? && list.movable? end def filter_params @@ -40,7 +46,7 @@ module Boards end def set_state - params[:state] = list.done? ? 'closed' : 'opened' + params[:state] = list && list.done? ? 'closed' : 'opened' end def board_label_ids diff --git a/config/routes/project.rb b/config/routes/project.rb index 46d6530333d..7cd4a73b1a0 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -266,10 +266,8 @@ constraints(ProjectUrlConstrainer.new) do end resources :boards, only: [:index, :show] do - get :backlog, on: :member - scope module: :boards do - resources :issues, only: [:update] + resources :issues, only: [:index, :update] resources :lists, only: [:index, :create, :update, :destroy] do collection do diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb index 299d2c981d3..ad15e3942a5 100644 --- a/spec/controllers/projects/boards/issues_controller_spec.rb +++ b/spec/controllers/projects/boards/issues_controller_spec.rb @@ -18,23 +18,7 @@ describe Projects::Boards::IssuesController do end describe 'GET index' do - context 'with valid list id' do - it 'returns issues that have the list label applied' do - johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) - issue = create(:labeled_issue, project: project, labels: [planning]) - create(:labeled_issue, project: project, labels: [planning]) - create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow) - create(:labeled_issue, project: project, labels: [development], assignee: johndoe) - issue.subscribe(johndoe, project) - - list_issues user: user, board: board, list: list2 - - parsed_response = JSON.parse(response.body) - - expect(response).to match_response_schema('issues') - expect(parsed_response.length).to eq 2 - end - end + let(:johndoe) { create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) } context 'with invalid board id' do it 'returns a not found 404 response' do @@ -44,11 +28,47 @@ describe Projects::Boards::IssuesController do end end - context 'with invalid list id' do - it 'returns a not found 404 response' do - list_issues user: user, board: board, list: 999 + context 'when list id is present' do + context 'with valid list id' do + it 'returns issues that have the list label applied' do + issue = create(:labeled_issue, project: project, labels: [planning]) + create(:labeled_issue, project: project, labels: [planning]) + create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow) + create(:labeled_issue, project: project, labels: [development], assignee: johndoe) + issue.subscribe(johndoe, project) - expect(response).to have_http_status(404) + list_issues user: user, board: board, list: list2 + + parsed_response = JSON.parse(response.body) + + expect(response).to match_response_schema('issues') + expect(parsed_response.length).to eq 2 + end + end + + context 'with invalid list id' do + it 'returns a not found 404 response' do + list_issues user: user, board: board, list: 999 + + expect(response).to have_http_status(404) + end + end + end + + context 'when list id is missing' do + it 'returns opened issues without board labels applied' do + bug = create(:label, project: project, name: 'Bug') + create(:issue, project: project) + create(:labeled_issue, project: project, labels: [planning]) + create(:labeled_issue, project: project, labels: [development]) + create(:labeled_issue, project: project, labels: [bug]) + + list_issues user: user, board: board + + parsed_response = JSON.parse(response.body) + + expect(response).to match_response_schema('issues') + expect(parsed_response.length).to eq 2 end end @@ -65,13 +85,17 @@ describe Projects::Boards::IssuesController do end end - def list_issues(user:, board:, list:) + def list_issues(user:, board:, list: nil) sign_in(user) - get :index, namespace_id: project.namespace.to_param, - project_id: project.to_param, - board_id: board.to_param, - list_id: list.to_param + params = { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + board_id: board.to_param, + list_id: list.try(:to_param) + } + + get :index, params.compact end end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 5d53c54254c..305278843f5 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -17,6 +17,10 @@ describe Boards::Issues::ListService, services: true do let!(:list2) { create(:list, board: board, label: testing, position: 1) } let!(:done) { create(:done_list, board: board) } + let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) } + let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) } + let!(:reopened_issue1) { create(:issue, :reopened, project: project) } + let!(:list1_issue1) { create(:labeled_issue, project: project, labels: [p2, development]) } let!(:list1_issue2) { create(:labeled_issue, project: project, labels: [development]) } let!(:list1_issue3) { create(:labeled_issue, project: project, labels: [development, p1]) } @@ -40,6 +44,14 @@ describe Boards::Issues::ListService, services: true do end context 'sets default order to priority' do + it 'returns opened issues when list id is missing' do + params = { board_id: board.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] + end + it 'returns closed issues when listing issues from Done' do params = { board_id: board.id, id: done.id } -- cgit v1.2.1 From 8323bc4919b9c13680fdc69e4ae25947b6ab0026 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 30 Jan 2017 14:11:09 +0000 Subject: Fixed up JS tests --- .../javascripts/boards/components/modal/header.js.es6 | 2 +- spec/javascripts/boards/boards_store_spec.js.es6 | 17 +---------------- spec/javascripts/boards/issue_card_spec.js.es6 | 2 +- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index ebb2ee1a82f..1b01a597874 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -21,7 +21,7 @@ this.$refs.selectAllBtn.blur(); ModalStore.toggleAll(); - } + }, }, components: { 'modal-tabs': gl.issueBoards.ModalTabs, diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index 7c5850111cb..a0d434e3588 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -61,18 +61,6 @@ describe('Store', () => { expect(list).toBeDefined(); }); - it('finds list limited by type', () => { - gl.issueBoards.BoardsStore.addList({ - id: 1, - position: 0, - title: 'Test', - list_type: 'backlog' - }); - const list = gl.issueBoards.BoardsStore.findList('id', 1, 'backlog'); - - expect(list).toBeDefined(); - }); - it('gets issue when new list added', (done) => { gl.issueBoards.BoardsStore.addList(listObj); const list = gl.issueBoards.BoardsStore.findList('id', 1); @@ -117,10 +105,7 @@ describe('Store', () => { expect(gl.issueBoards.BoardsStore.shouldAddBlankState()).toBe(false); }); - it('check for blank state adding when backlog & done list exist', () => { - gl.issueBoards.BoardsStore.addList({ - list_type: 'backlog' - }); + it('check for blank state adding when done list exist', () => { gl.issueBoards.BoardsStore.addList({ list_type: 'done' }); diff --git a/spec/javascripts/boards/issue_card_spec.js.es6 b/spec/javascripts/boards/issue_card_spec.js.es6 index 181fb684c23..d68108c6f6f 100644 --- a/spec/javascripts/boards/issue_card_spec.js.es6 +++ b/spec/javascripts/boards/issue_card_spec.js.es6 @@ -93,7 +93,7 @@ describe('Issue card component', () => { setTimeout(() => { expect( - component.$el.querySelector('.fa-eye-flash'), + component.$el.querySelector('.confidential-icon'), ).not.toBeNull(); done(); }, 0); -- cgit v1.2.1 From c368b28cc9615b40881f5f39473314950e77aaa4 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 30 Jan 2017 15:39:40 +0000 Subject: Fixed issue with issue not persisting in list --- app/controllers/projects/boards/issues_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index a061b575e21..7fe61c6800d 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -73,7 +73,7 @@ module Projects def serialize_as_json(resource) resource.as_json( labels: true, - only: [:iid, :title, :confidential, :due_date], + only: [:id, :iid, :title, :confidential, :due_date], include: { assignee: { only: [:id, :name, :username], methods: [:avatar_url] }, milestone: { only: [:id, :title] } -- cgit v1.2.1 From b129187267292fb1052c984878f5d6f41483c204 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Mon, 30 Jan 2017 13:51:09 -0200 Subject: Remove unnecessary include from RemoveBacklogListsFromBoards migration --- db/migrate/20170127032550_remove_backlog_lists_from_boards.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb b/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb index 3369bd4d123..0ee4229d1f8 100644 --- a/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb +++ b/db/migrate/20170127032550_remove_backlog_lists_from_boards.rb @@ -1,6 +1,4 @@ class RemoveBacklogListsFromBoards < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - DOWNTIME = false def up -- cgit v1.2.1 From 6c828906a24e374c68884c8884135ec1472721b8 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 30 Jan 2017 15:56:45 +0000 Subject: Disabled add issues button if no lists exist --- app/assets/javascripts/boards/boards_bundle.js.es6 | 29 ++++++++++++++++------ .../projects/boards/issues_controller.rb | 2 +- app/views/shared/issuable/_filter.html.haml | 3 +-- 3 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 0e2794334fa..18b67abfccc 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -88,12 +88,25 @@ $(() => { } }); - // This element is outside the Vue app - $(document) - .off('click', '.js-show-add-issues') - .on('click', '.js-show-add-issues', (e) => { - e.preventDefault(); - - ModalStore.store.showAddIssuesModal = true; - }); + gl.IssueBoardsModalAddBtn = new Vue({ + el: '#js-add-issues-btn', + data: { + modal: ModalStore.store, + store: Store.state, + }, + computed: { + disabled() { + return Store.shouldAddBlankState(); + }, + }, + template: ` + <button + class="btn btn-create pull-right prepend-left-10 has-tooltip" + type="button" + :disabled="disabled" + @click="modal.showAddIssuesModal = true"> + Add issues + </button> + `, + }); }); diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb index 7fe61c6800d..61fef4dc133 100644 --- a/app/controllers/projects/boards/issues_controller.rb +++ b/app/controllers/projects/boards/issues_controller.rb @@ -7,7 +7,7 @@ module Projects def index issues = ::Boards::Issues::ListService.new(project, current_user, filter_params).execute - issues = issues.page(params[:page]) + issues = issues.page(params[:page]).per(params[:per] || 20) render json: { issues: serialize_as_json(issues), diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 1b1348d435a..1eed314d068 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -38,8 +38,7 @@ #js-boards-search.issue-boards-search %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" } - if can?(current_user, :admin_list, @project) - %button.btn.btn-create.pull-right.prepend-left-10.js-show-add-issues{ type: "button" } - Add issues + #js-add-issues-btn.pull-right.prepend-left-10 .dropdown.pull-right %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } } Add list -- cgit v1.2.1 From e140675eef49e65e8b80909cf22c05284514cdef Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 30 Jan 2017 16:26:42 +0000 Subject: Fixed issue link href --- app/assets/javascripts/boards/components/modal/index.js.es6 | 6 ++++-- app/assets/javascripts/boards/components/modal/list.js.es6 | 13 ++++++++++--- app/views/projects/boards/_show.html.haml | 3 ++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index ababe3af0e3..f7f50455acc 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -9,7 +9,7 @@ gl.issueBoards.IssuesModal = Vue.extend({ props: [ - 'blankStateImage', 'newIssuePath', 'bulkUpdatePath', + 'blankStateImage', 'newIssuePath', 'bulkUpdatePath', 'issueLinkBase', ], data() { return ModalStore.store; @@ -81,7 +81,9 @@ v-if="showAddIssuesModal"> <div class="add-issues-container"> <modal-header></modal-header> - <modal-list v-if="!loading && showList"></modal-list> + <modal-list + :issue-link-base="issueLinkBase" + v-if="!loading && showList"></modal-list> <empty-state v-if="(!loading && issues.length === 0) || (activeTab === 'selected' && selectedIssues.length === 0)" :image="blankStateImage" diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 605c1101666..0df09d42059 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -6,6 +6,9 @@ const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalList = Vue.extend({ + props: [ + 'issueLinkBase', + ], data() { return ModalStore.store; }, @@ -34,7 +37,11 @@ }, }, methods: { - toggleIssue: ModalStore.toggleIssue.bind(ModalStore), + toggleIssue(e, issue) { + if (e.target.tagName !== 'A') { + ModalStore.toggleIssue(issue); + } + }, listHeight() { return this.$refs.list.getBoundingClientRect().height; }, @@ -100,10 +107,10 @@ <div class="card" :class="{ 'is-active': issue.selected }" - @click="toggleIssue(issue)"> + @click="toggleIssue($event, issue)"> <issue-card-inner :issue="issue" - :issue-link-base="'/'"> + :issue-link-base="issueLinkBase"> </issue-card-inner> <span v-if="issue.selected" diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 068e69bada6..a0f9dfc5b5b 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -28,4 +28,5 @@ = render "projects/boards/components/sidebar" %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project), - "bulk-update-path" => bulk_update_namespace_project_issues_path(@project.namespace, @project) } + "bulk-update-path" => bulk_update_namespace_project_issues_path(@project.namespace, @project), + ":issue-link-base" => "issueLinkBase" } -- cgit v1.2.1 From ffeb3200c1c8558345c99de64723de2747b7ffe8 Mon Sep 17 00:00:00 2001 From: Douglas Barbosa Alexandre <dbalexandre@gmail.com> Date: Mon, 30 Jan 2017 14:34:47 -0200 Subject: Add optional id property to the issue schema --- spec/fixtures/api/schemas/issue.json | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index 77f2bcee1f3..8e19cee5440 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -6,6 +6,7 @@ "confidential" ], "properties" : { + "id": { "type": "integer" }, "iid": { "type": "integer" }, "title": { "type": "string" }, "confidential": { "type": "boolean" }, -- cgit v1.2.1 From 954deefa2253dae2c65246a19d6c7202b07715e1 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 30 Jan 2017 17:11:08 +0000 Subject: Added remove button --- .../boards/components/board_card.js.es6 | 1 + .../boards/components/board_sidebar.js.es6 | 10 +++++-- .../boards/components/sidebar/remove_issue.js.es6 | 34 ++++++++++++++++++++++ .../projects/boards/components/_sidebar.html.haml | 2 ++ spec/features/boards/sidebar_spec.rb | 32 ++++++++++++++++++++ 5 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 index ab4226ded1d..037100c0859 100644 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -50,6 +50,7 @@ Store.detail.issue = {}; } else { Store.detail.issue = this.issue; + Store.detail.list = this.list; } } } diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 index 75dfcb66bb0..e5937c178f2 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -4,6 +4,7 @@ /* global MilestoneSelect */ /* global LabelsSelect */ /* global Sidebar */ +//= require ./sidebar/remove_issue (() => { const Store = gl.issueBoards.BoardsStore; @@ -18,7 +19,8 @@ data() { return { detail: Store.detail, - issue: {} + issue: {}, + list: {}, }; }, computed: { @@ -36,6 +38,7 @@ } this.issue = this.detail.issue; + this.list = this.detail.list; }, deep: true }, @@ -60,6 +63,9 @@ new LabelsSelect(); new Sidebar(); gl.Subscription.bindAll('.subscription'); - } + }, + components: { + 'remove-btn': gl.issueBoards.RemoveIssueBtn, + }, }); })(); diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 new file mode 100644 index 00000000000..3f965b7b9b2 --- /dev/null +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 @@ -0,0 +1,34 @@ +/* global Vue */ +(() => { + const Store = gl.issueBoards.BoardsStore; + + window.gl = window.gl || {}; + window.gl.issueBoards = window.gl.issueBoards || {}; + + gl.issueBoards.RemoveIssueBtn = Vue.extend({ + props: [ + 'issue', 'list', + ], + methods: { + removeIssue() { + const doneList = Store.findList('type', 'done', false); + + Store.moveIssueToList(this.list, doneList, this.issue, 0); + + Store.detail.issue = {}; + }, + }, + template: ` + <div + class="block list" + v-if="list.type !== 'done'"> + <button + class="btn btn-default btn-block" + type="button" + @click="removeIssue"> + Remove from board + </button> + </div> + `, + }); +})(); diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml index df7fa9ddaf2..24d76da6f06 100644 --- a/app/views/projects/boards/components/_sidebar.html.haml +++ b/app/views/projects/boards/components/_sidebar.html.haml @@ -22,3 +22,5 @@ = render "projects/boards/components/sidebar/due_date" = render "projects/boards/components/sidebar/labels" = render "projects/boards/components/sidebar/notifications" + %remove-btn{ ":issue" => "issue", + ":list" => "list" } diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index a0f32d2ab76..ea135d0cc95 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -70,6 +70,38 @@ describe 'Issue Boards', feature: true, js: true do end end + it 'removes card from board when clicking remove button' do + page.within(first('.board')) do + first('.card').click + end + + page.within('.issue-boards-sidebar') do + click_button 'Remove from board' + end + + page.within(first('.board')) do + expect(page).to have_selector('.card', count: 1) + end + end + + it 'does not show remove issue button when issue is closed' do + page.within(first('.board')) do + first('.card').click + end + + page.within('.issue-boards-sidebar') do + click_button 'Remove from board' + end + + page.within(find('.board:nth-child(2)')) do + first('.card').click + end + + page.within('.issue-boards-sidebar') do + expect(page).not_to have_button 'Remove from board' + end + end + context 'assignee' do it 'updates the issues assignee' do page.within(first('.board')) do -- cgit v1.2.1 From d5c9d7e7530589e21f13816f3dba5de91f108d8b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 31 Jan 2017 09:39:53 +0000 Subject: Fixed DB schema Changed how components are added in objects --- .../boards/components/modal/footer.js.es6 | 2 +- .../boards/components/modal/header.js.es6 | 2 +- .../boards/components/modal/index.js.es6 | 8 ++--- .../boards/components/modal/list.js.es6 | 2 +- db/schema.rb | 41 ++++++++-------------- 5 files changed, 22 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 1a147daca7a..566303431f6 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -48,7 +48,7 @@ }, }, components: { - 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, + listsDropdown: gl.issueBoards.ModalFooterListsDropdown, }, template: ` <footer diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index 1b01a597874..f6d891a1633 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -24,7 +24,7 @@ }, }, components: { - 'modal-tabs': gl.issueBoards.ModalTabs, + modalTabs: gl.issueBoards.ModalTabs, }, template: ` <div> diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index f7f50455acc..dd40075ac47 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -70,10 +70,10 @@ }, }, components: { - 'modal-header': gl.issueBoards.IssuesModalHeader, - 'modal-list': gl.issueBoards.ModalList, - 'modal-footer': gl.issueBoards.ModalFooter, - 'empty-state': gl.issueBoards.ModalEmptyState, + modalHeader: gl.issueBoards.IssuesModalHeader, + modalList: gl.issueBoards.ModalList, + modalFooter: gl.issueBoards.ModalFooter, + emptyState: gl.issueBoards.ModalEmptyState, }, template: ` <div diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 0df09d42059..309696c7078 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -94,7 +94,7 @@ this.destroyMasonry(); }, components: { - 'issue-card-inner': gl.issueBoards.IssueCardInner, + issueCardInner: gl.issueBoards.IssueCardInner, }, template: ` <div diff --git a/db/schema.rb b/db/schema.rb index d887b441189..c73c311ccb2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -87,9 +87,9 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.boolean "send_user_confirmation_email", default: false t.integer "container_registry_token_expire_delay", default: 5 t.text "after_sign_up_text" + t.boolean "user_default_external", default: false, null: false t.string "repository_storages", default: "default" t.string "enabled_git_access_protocol" - t.boolean "user_default_external", default: false, null: false t.boolean "domain_blacklist_enabled", default: false t.text "domain_blacklist" t.boolean "koding_enabled" @@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.text "help_page_text_html" t.text "shared_runners_text_html" t.text "after_sign_up_text_html" + t.boolean "sidekiq_throttling_enabled", default: false + t.string "sidekiq_throttling_queues" + t.decimal "sidekiq_throttling_factor" t.boolean "housekeeping_enabled", default: true, null: false t.boolean "housekeeping_bitmaps_enabled", default: true, null: false t.integer "housekeeping_incremental_repack_period", default: 10, null: false t.integer "housekeeping_full_repack_period", default: 50, null: false t.integer "housekeeping_gc_period", default: 200, null: false - t.boolean "sidekiq_throttling_enabled", default: false - t.string "sidekiq_throttling_queues" - t.decimal "sidekiq_throttling_factor" t.boolean "html_emails_enabled", default: true t.string "plantuml_url" t.boolean "plantuml_enabled" @@ -398,22 +398,22 @@ ActiveRecord::Schema.define(version: 20170130204620) do add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree create_table "deployments", force: :cascade do |t| - t.integer "iid" - t.integer "project_id" - t.integer "environment_id" - t.string "ref" - t.boolean "tag" - t.string "sha" + t.integer "iid", null: false + t.integer "project_id", null: false + t.integer "environment_id", null: false + t.string "ref", null: false + t.boolean "tag", null: false + t.string "sha", null: false t.integer "user_id" - t.integer "deployable_id", null: false - t.string "deployable_type", null: false + t.integer "deployable_id" + t.string "deployable_type" t.datetime "created_at" t.datetime "updated_at" t.string "on_stop" end add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree - add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", using: :btree + add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree create_table "emails", force: :cascade do |t| t.integer "user_id", null: false @@ -685,8 +685,8 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.integer "merge_user_id" t.string "merge_commit_sha" t.datetime "deleted_at" - t.integer "lock_version" t.string "in_progress_merge_commit_sha" + t.integer "lock_version" t.text "title_html" t.text "description_html" t.integer "time_estimate" @@ -763,16 +763,6 @@ ActiveRecord::Schema.define(version: 20170130204620) do add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"} add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree - create_table "note_templates", force: :cascade do |t| - t.integer "user_id" - t.string "title" - t.text "note" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - end - - add_index "note_templates", ["user_id"], name: "index_note_templates_on_user_id", using: :btree - create_table "notes", force: :cascade do |t| t.text "note" t.string "noteable_type" @@ -788,7 +778,6 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.text "st_diff" t.integer "updated_by_id" t.string "type" - t.string "system_type" t.text "position" t.text "original_position" t.datetime "resolved_at" @@ -971,7 +960,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do t.boolean "has_external_wiki" t.boolean "lfs_enabled" t.text "description_html" - t.boolean "only_allow_merge_if_all_discussions_are_resolved", default: false, null: false + t.boolean "only_allow_merge_if_all_discussions_are_resolved" end add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree -- cgit v1.2.1 From 1b01386a9513ad71f07aab79a29ee1f877db8df6 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 31 Jan 2017 09:41:57 +0000 Subject: Fixed bug where 2 un-selected issues would stay on selected tab --- app/assets/javascripts/boards/stores/modal_store.js.es6 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index 391765c4978..e2234290657 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -65,8 +65,9 @@ removeSelectedIssue(issue, forcePurge = false) { if (this.store.activeTab === 'all' || forcePurge) { - const index = this.selectedIssueIndex(issue); - this.store.selectedIssues.splice(index, 1); + this.store.selectedIssues = this.store.selectedIssues.filter((fIssue) => { + return fIssue.id !== issue.id; + }); } } -- cgit v1.2.1 From 39fbd18951e7c6bd4b403cd82dd4e008fd00d6fe Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 31 Jan 2017 10:39:44 +0000 Subject: Fixed bug with empty state showing after search Fixed users href path being incorrect --- app/assets/javascripts/boards/boards_bundle.js.es6 | 1 + app/assets/javascripts/boards/components/board.js.es6 | 3 ++- app/assets/javascripts/boards/components/board_card.js.es6 | 3 ++- app/assets/javascripts/boards/components/board_list.js.es6 | 1 + app/assets/javascripts/boards/components/board_sidebar.js.es6 | 2 +- .../javascripts/boards/components/issue_card_inner.js.es6 | 9 +++++---- .../javascripts/boards/components/modal/empty_state.js.es6 | 6 +++--- app/assets/javascripts/boards/components/modal/footer.js.es6 | 3 +-- app/assets/javascripts/boards/components/modal/header.js.es6 | 4 ++-- app/assets/javascripts/boards/components/modal/index.js.es6 | 10 ++++++++-- app/assets/javascripts/boards/components/modal/list.js.es6 | 11 +++++++---- app/assets/javascripts/boards/components/modal/tabs.js.es6 | 2 +- app/assets/javascripts/boards/services/board_service.js.es6 | 2 +- app/assets/javascripts/boards/stores/boards_store.js.es6 | 5 +---- app/assets/javascripts/boards/stores/modal_store.js.es6 | 3 ++- app/helpers/boards_helper.rb | 3 ++- app/views/projects/boards/_show.html.haml | 4 +++- app/views/projects/boards/components/_board.html.haml | 1 + app/views/projects/boards/components/_board_list.html.haml | 1 + app/views/projects/boards/components/_card.html.haml | 3 ++- spec/features/boards/add_issues_modal_spec.rb | 1 + 21 files changed, 48 insertions(+), 30 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 18b67abfccc..1cce580df79 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -44,6 +44,7 @@ $(() => { boardId: $boardApp.dataset.boardId, disabled: $boardApp.dataset.disabled === 'true', issueLinkBase: $boardApp.dataset.issueLinkBase, + rootPath: $boardApp.dataset.rootPath, detailIssue: Store.detail }, computed: { diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6 index a32881116d5..d6148ae748a 100644 --- a/app/assets/javascripts/boards/components/board.js.es6 +++ b/app/assets/javascripts/boards/components/board.js.es6 @@ -22,7 +22,8 @@ props: { list: Object, disabled: Boolean, - issueLinkBase: String + issueLinkBase: String, + rootPath: String, }, data () { return { diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6 index 037100c0859..032b93da021 100644 --- a/app/assets/javascripts/boards/components/board_card.js.es6 +++ b/app/assets/javascripts/boards/components/board_card.js.es6 @@ -18,7 +18,8 @@ issue: Object, issueLinkBase: String, disabled: Boolean, - index: Number + index: Number, + rootPath: String, }, data () { return { diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6 index 630fe084175..6906a910a2f 100644 --- a/app/assets/javascripts/boards/components/board_list.js.es6 +++ b/app/assets/javascripts/boards/components/board_list.js.es6 @@ -23,6 +23,7 @@ issues: Array, loading: Boolean, issueLinkBase: String, + rootPath: String, }, data () { return { diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6 index e5937c178f2..126ccdb4978 100644 --- a/app/assets/javascripts/boards/components/board_sidebar.js.es6 +++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6 @@ -65,7 +65,7 @@ gl.Subscription.bindAll('.subscription'); }, components: { - 'remove-btn': gl.issueBoards.RemoveIssueBtn, + removeBtn: gl.issueBoards.RemoveIssueBtn, }, }); })(); diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 index 61fcfde0a02..73db6480269 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 +++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 @@ -7,7 +7,7 @@ gl.issueBoards.IssueCardInner = Vue.extend({ props: [ - 'issue', 'issueLinkBase', 'list', + 'issue', 'issueLinkBase', 'list', 'rootPath', ], methods: { showLabel(label) { @@ -57,11 +57,11 @@ <span class="card-number" v-if="issue.id"> - #{{issue.id}} + #{{ issue.id }} </span> <a class="card-assignee has-tooltip" - :href="issue.assignee.username" + :href="rootPath + issue.assignee.username" :title="'Assigned to ' + issue.assignee.name" v-if="issue.assignee" data-container="body"> @@ -69,7 +69,8 @@ class="avatar avatar-inline s20" :src="issue.assignee.avatar" width="20" - height="20" /> + height="20" + :alt="'Avatar for ' + issue.assignee.name" /> </a> <button class="label color-label has-tooltip" diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 index 70f2ea51620..c5137d966e2 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 +++ b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 @@ -31,10 +31,10 @@ }, }, template: ` - <div class="empty-state"> + <section class="empty-state"> <div class="row"> <div class="col-xs-12 col-sm-6 col-sm-push-6"> - <div class="svg-content" v-html="image"></div> + <aside class="svg-content" v-html="image"></aside> </div> <div class="col-xs-12 col-sm-6 col-sm-pull-6"> <div class="text-content"> @@ -56,7 +56,7 @@ </div> </div> </div> - </div> + </section> `, }); })(); diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 566303431f6..2ed81abe625 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -27,8 +27,7 @@ addIssues() { const list = this.selectedList; const selectedIssues = ModalStore.getSelectedIssues(); - const issueIds = selectedIssues.filter(issue => issue.selected) - .map(issue => issue.globalId); + const issueIds = selectedIssues.map(issue => issue.globalId); // Post the data to the backend this.$http.post(this.bulkUpdatePath, { diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index f6d891a1633..8024e89a29b 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -41,10 +41,10 @@ </button> </h2> </header> - <modal-tabs v-if="!loading && issues.length > 0"></modal-tabs> + <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs> <div class="add-issues-search append-bottom-10" - v-if="activeTab == 'all' && !loading && issues.length > 0"> + v-if="activeTab == 'all' && !loading && issuesCount > 0"> <input placeholder="Search issues..." class="form-control" diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index dd40075ac47..4536f5cfe6f 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -10,6 +10,7 @@ gl.issueBoards.IssuesModal = Vue.extend({ props: [ 'blankStateImage', 'newIssuePath', 'bulkUpdatePath', 'issueLinkBase', + 'rootPath', ], data() { return ModalStore.store; @@ -57,6 +58,10 @@ }); this.loadingNewPage = false; + + if (!this.issuesCount) { + this.issuesCount = this.issues.length; + } }); }, }, @@ -66,7 +71,7 @@ return this.selectedIssues.length > 0; } - return this.issues.length > 0; + return this.issuesCount > 0; }, }, components: { @@ -83,9 +88,10 @@ <modal-header></modal-header> <modal-list :issue-link-base="issueLinkBase" + :root-path="rootPath" v-if="!loading && showList"></modal-list> <empty-state - v-if="(!loading && issues.length === 0) || (activeTab === 'selected' && selectedIssues.length === 0)" + v-if="(!loading && issuesCount === 0) || (activeTab === 'selected' && selectedIssues.length === 0)" :image="blankStateImage" :new-issue-path="newIssuePath"></empty-state> <section diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 309696c7078..8db1ab4df5e 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -7,7 +7,7 @@ gl.issueBoards.ModalList = Vue.extend({ props: [ - 'issueLinkBase', + 'issueLinkBase', 'rootPath', ], data() { return ModalStore.store; @@ -97,7 +97,7 @@ issueCardInner: gl.issueBoards.IssueCardInner, }, template: ` - <div + <section class="add-issues-list add-issues-list-columns" ref="list"> <div @@ -110,16 +110,19 @@ @click="toggleIssue($event, issue)"> <issue-card-inner :issue="issue" - :issue-link-base="issueLinkBase"> + :issue-link-base="issueLinkBase" + :root-path="rootPath"> </issue-card-inner> <span + :aria-label="'Issue #' + issue.id + ' selected'" + aria-checked="true" v-if="issue.selected" class="issue-card-selected text-center"> <i class="fa fa-check"></i> </span> </div> </div> - </div> + </section> `, }); })(); diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 index 94ff8ec999a..007e01f7d82 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js.es6 +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -24,7 +24,7 @@ @click.prevent="activeTab = 'all'"> <span>All issues</span> <span class="badge"> - {{ issues.length }} + {{ issuesCount }} </span> </a> </li> diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index 993f8599fd4..ad58abdcb45 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -13,7 +13,7 @@ class BoardService { generate: { method: 'POST', url: `${root}/${boardId}/lists/generate.json` - }, + } }); this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}); diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 66ecae1c01d..50842ecbaaa 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -115,9 +115,6 @@ }, updateFiltersUrl () { history.pushState(null, null, `?${$.param(this.state.filters)}`); - }, - modalSelectedCount() { - return this.modal.selectedIssues.length; - }, + } }; })(); diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index e2234290657..66305347b8c 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -6,6 +6,7 @@ constructor() { this.store = { issues: [], + issuesCount: false, selectedIssues: [], showAddIssuesModal: false, activeTab: 'all', @@ -19,7 +20,7 @@ } selectedCount() { - return this.store.selectedIssues.filter(issue => issue.selected).length; + return this.getSelectedIssues().length; } toggleIssue(issueObj) { diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 38c586ccd31..0b1b79ce15b 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -6,7 +6,8 @@ module BoardsHelper endpoint: namespace_project_boards_path(@project.namespace, @project), board_id: board.id, disabled: "#{!can?(current_user, :admin_list, @project)}", - issue_link_base: namespace_project_issues_path(@project.namespace, @project) + issue_link_base: namespace_project_issues_path(@project.namespace, @project), + root_path: root_path, } end end diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index a0f9dfc5b5b..a35f0ab557d 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -24,9 +24,11 @@ ":list" => "list", ":disabled" => "disabled", ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath", ":key" => "_uid" } = render "projects/boards/components/sidebar" %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project), "bulk-update-path" => bulk_update_namespace_project_issues_path(@project.namespace, @project), - ":issue-link-base" => "issueLinkBase" } + ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath" } diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml index a2e5118a9f3..72bce4049de 100644 --- a/app/views/projects/boards/components/_board.html.haml +++ b/app/views/projects/boards/components/_board.html.haml @@ -29,6 +29,7 @@ ":loading" => "list.loading", ":disabled" => "disabled", ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath", "ref" => "board-list" } - if can?(current_user, :admin_list, @project) = render "projects/boards/components/blank_state" diff --git a/app/views/projects/boards/components/_board_list.html.haml b/app/views/projects/boards/components/_board_list.html.haml index 34fdb1f6a74..f413a5e94c1 100644 --- a/app/views/projects/boards/components/_board_list.html.haml +++ b/app/views/projects/boards/components/_board_list.html.haml @@ -34,6 +34,7 @@ ":list" => "list", ":issue" => "issue", ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath", ":disabled" => "disabled", ":key" => "issue.id" } %li.board-list-count.text-center{ "v-if" => "showCount" } diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml index 51e5d739537..891c2c46251 100644 --- a/app/views/projects/boards/components/_card.html.haml +++ b/app/views/projects/boards/components/_card.html.haml @@ -6,4 +6,5 @@ "@mouseup" => "showIssue($event)" } %issue-card-inner{ ":list" => "list", ":issue" => "issue", - ":issue-link-base" => "issueLinkBase" } + ":issue-link-base" => "issueLinkBase", + ":root-path" => "rootPath" } diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index 5ba044a4f3c..ae245d7469b 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -90,6 +90,7 @@ describe 'Issue Boards add issue modal', :feature, :js do find('.form-control').native.send_keys('testing search') expect(page).not_to have_selector('.card') + expect(page).not_to have_content("You haven't added any issues to your project yet") end end end -- cgit v1.2.1 From 1b840452b9ae5749cdc95ca391d029fbb40842c6 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 31 Jan 2017 10:58:28 +0000 Subject: Added webkit CSS properties --- app/assets/stylesheets/pages/boards.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index e7321b19e15..ded15dc493e 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -356,6 +356,7 @@ } .add-issues-modal { + display: -webkit-flex; display: flex; position: fixed; top: 0; @@ -367,7 +368,9 @@ } .add-issues-container { + display: -webkit-flex; display: flex; + -webkit-flex-direction: column; flex-direction: column; width: 90vw; height: 85vh; @@ -380,7 +383,9 @@ box-shadow: 0 2px 12px rgba($black, .5); .empty-state { + display: -webkit-flex; display: flex; + -webkit-flex: 1; flex: 1; margin-top: 0; @@ -409,11 +414,14 @@ } .add-issues-search { + display: -webkit-flex; display: flex; } .add-issues-list { + display: -webkit-flex; display: flex; + -webkit-flex: 1; flex: 1; padding-top: 3px; margin-left: -$gl-vert-padding; @@ -441,6 +449,7 @@ } .add-issues-list-loading { + -webkit-align-self: center; align-self: center; width: 100%; padding-left: $gl-vert-padding; -- cgit v1.2.1 From a51aa6ab42a52b3c1d246b72ab3933ac3134cf4b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 31 Jan 2017 11:54:04 +0000 Subject: Fixed issue card spec --- spec/javascripts/boards/issue_card_spec.js.es6 | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/spec/javascripts/boards/issue_card_spec.js.es6 b/spec/javascripts/boards/issue_card_spec.js.es6 index d68108c6f6f..1d6940ca3a1 100644 --- a/spec/javascripts/boards/issue_card_spec.js.es6 +++ b/spec/javascripts/boards/issue_card_spec.js.es6 @@ -50,6 +50,7 @@ describe('Issue card component', () => { list, issue, issueLinkBase: '/test', + rootPath: '/', }; }, components: { @@ -59,7 +60,8 @@ describe('Issue card component', () => { <issue-card :issue="issue" :list="list" - :issue-link-base="issueLinkBase"></issue-card> + :issue-link-base="issueLinkBase" + :root-path="rootPath"></issue-card> `, }); }); @@ -136,7 +138,7 @@ describe('Issue card component', () => { it('sets users path', () => { expect( component.$el.querySelector('.card-assignee').getAttribute('href'), - ).toBe('test'); + ).toBe('/test'); }); it('renders avatar', () => { -- cgit v1.2.1 From 32a97ef19c9adf30bd67bb310551dff883231dbc Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 31 Jan 2017 13:35:11 +0000 Subject: Fixed JS lint errors --- app/assets/javascripts/boards/stores/modal_store.js.es6 | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index 66305347b8c..9c498ba48c4 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -66,9 +66,8 @@ removeSelectedIssue(issue, forcePurge = false) { if (this.store.activeTab === 'all' || forcePurge) { - this.store.selectedIssues = this.store.selectedIssues.filter((fIssue) => { - return fIssue.id !== issue.id; - }); + this.store.selectedIssues = this.store.selectedIssues + .filter(fIssue => fIssue.id !== issue.id); } } -- cgit v1.2.1 From 103c78f18c0642c36a6093508707b82eb8d1dd77 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 31 Jan 2017 14:35:53 +0000 Subject: Removes labels instead of closing issue when clicking remove button --- app/assets/javascripts/boards/boards_bundle.js.es6 | 3 ++- .../boards/components/modal/empty_state.js.es6 | 2 +- .../javascripts/boards/components/modal/footer.js.es6 | 10 ++-------- .../javascripts/boards/components/modal/header.js.es6 | 2 +- .../javascripts/boards/components/modal/index.js.es6 | 5 +++-- .../boards/components/sidebar/remove_issue.js.es6 | 13 +++++++++++-- .../javascripts/boards/services/board_service.js.es6 | 19 +++++++++++++++++-- app/helpers/boards_helper.rb | 1 + app/views/projects/boards/_show.html.haml | 1 - 9 files changed, 38 insertions(+), 18 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 1cce580df79..81df823cad1 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -45,6 +45,7 @@ $(() => { disabled: $boardApp.dataset.disabled === 'true', issueLinkBase: $boardApp.dataset.issueLinkBase, rootPath: $boardApp.dataset.rootPath, + bulkUpdatePath: $boardApp.dataset.bulkUpdatePath, detailIssue: Store.detail }, computed: { @@ -53,7 +54,7 @@ $(() => { }, }, created () { - gl.boardService = new BoardService(this.endpoint, this.boardId); + gl.boardService = new BoardService(this.endpoint, this.bulkUpdatePath, this.boardId); }, mounted () { Store.disabled = this.disabled; diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 index c5137d966e2..93d250e07f5 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 +++ b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 @@ -44,7 +44,7 @@ :href="newIssuePath" class="btn btn-success btn-inverted" v-if="activeTab === 'all'"> - Create issue + New issue </a> <button type="button" diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 2ed81abe625..4c0f21fa1d6 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -4,9 +4,6 @@ const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalFooter = Vue.extend({ - props: [ - 'bulkUpdatePath', - ], data() { return ModalStore.store; }, @@ -30,11 +27,8 @@ const issueIds = selectedIssues.map(issue => issue.globalId); // Post the data to the backend - this.$http.post(this.bulkUpdatePath, { - update: { - issuable_ids: issueIds.join(','), - add_label_ids: [list.label.id], - }, + gl.boardService.bulkUpdate(issueIds, { + add_label_ids: [list.label.id], }); // Add the issues on the frontend diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index 8024e89a29b..4a1845e4580 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -13,7 +13,7 @@ return 'Select all'; } - return 'Un-select all'; + return 'Deselect all'; }, }, methods: { diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index 4536f5cfe6f..612657753d5 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -9,7 +9,7 @@ gl.issueBoards.IssuesModal = Vue.extend({ props: [ - 'blankStateImage', 'newIssuePath', 'bulkUpdatePath', 'issueLinkBase', + 'blankStateImage', 'newIssuePath', 'issueLinkBase', 'rootPath', ], data() { @@ -33,6 +33,7 @@ } else if (!this.showAddIssuesModal) { this.issues = []; this.selectedIssues = []; + this.issuesCount = false; } }, }, @@ -101,7 +102,7 @@ <i class="fa fa-spinner fa-spin"></i> </div> </section> - <modal-footer :bulk-update-path="bulkUpdatePath"></modal-footer> + <modal-footer></modal-footer> </div> </div> `, diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 index 3f965b7b9b2..70f7da17d49 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 @@ -11,9 +11,18 @@ ], methods: { removeIssue() { - const doneList = Store.findList('type', 'done', false); + const lists = this.issue.getLists(); + const labelIds = lists.map(list => list.label.id); - Store.moveIssueToList(this.list, doneList, this.issue, 0); + // Post the remove data + gl.boardService.bulkUpdate([this.issue.globalId], { + remove_label_ids: labelIds, + }); + + // Remove from the frontend store + lists.forEach((list) => { + list.removeIssue(this.issue); + }); Store.detail.issue = {}; }, diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6 index ad58abdcb45..065e90518df 100644 --- a/app/assets/javascripts/boards/services/board_service.js.es6 +++ b/app/assets/javascripts/boards/services/board_service.js.es6 @@ -2,7 +2,7 @@ /* global Vue */ class BoardService { - constructor (root, boardId) { + constructor (root, bulkUpdatePath, boardId) { this.boards = Vue.resource(`${root}{/id}.json`, {}, { issues: { method: 'GET', @@ -16,7 +16,12 @@ class BoardService { } }); this.issue = Vue.resource(`${root}/${boardId}/issues{/id}`, {}); - this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}); + this.issues = Vue.resource(`${root}/${boardId}/lists{/id}/issues`, {}, { + bulkUpdate: { + method: 'POST', + url: bulkUpdatePath, + }, + }); Vue.http.interceptors.push((request, next) => { request.headers['X-CSRF-Token'] = $.rails.csrfToken(); @@ -75,6 +80,16 @@ class BoardService { getBacklog(data) { return this.boards.issues(data); } + + bulkUpdate(issueIds, extraData = {}) { + const data = { + update: Object.assign(extraData, { + issuable_ids: issueIds.join(','), + }), + }; + + return this.issues.bulkUpdate(data); + } } window.BoardService = BoardService; diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 0b1b79ce15b..f43827da446 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -8,6 +8,7 @@ module BoardsHelper disabled: "#{!can?(current_user, :admin_list, @project)}", issue_link_base: namespace_project_issues_path(@project.namespace, @project), root_path: root_path, + bulk_update_path: bulk_update_namespace_project_issues_path(@project.namespace, @project), } end end diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index a35f0ab557d..13bc20f2ae2 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -29,6 +29,5 @@ = render "projects/boards/components/sidebar" %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project), - "bulk-update-path" => bulk_update_namespace_project_issues_path(@project.namespace, @project), ":issue-link-base" => "issueLinkBase", ":root-path" => "rootPath" } -- cgit v1.2.1 From 00b835bad9d68dbc16367bd66afcbf99ca248203 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 31 Jan 2017 14:57:33 +0000 Subject: Props use objects with required & type values --- .../boards/components/issue_card_inner.js.es6 | 21 ++++++++++++--- .../boards/components/modal/empty_state.js.es6 | 13 +++++++--- .../boards/components/modal/footer.js.es6 | 2 +- .../boards/components/modal/header.js.es6 | 2 +- .../boards/components/modal/index.js.es6 | 30 ++++++++++++++++------ .../boards/components/modal/list.js.es6 | 15 ++++++++--- .../boards/components/sidebar/remove_issue.js.es6 | 13 +++++++--- 7 files changed, 73 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 index 73db6480269..10b82ba0998 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 +++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 @@ -6,9 +6,24 @@ window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.IssueCardInner = Vue.extend({ - props: [ - 'issue', 'issueLinkBase', 'list', 'rootPath', - ], + props: { + issue: { + type: Object, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + list: { + type: Object, + required: false, + }, + rootPath: { + type: String, + required: true, + }, + }, methods: { showLabel(label) { if (!this.list) return true; diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 index 93d250e07f5..7bd7c27b579 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 +++ b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 @@ -6,9 +6,16 @@ data() { return ModalStore.store; }, - props: [ - 'image', 'newIssuePath', - ], + props: { + image: { + type: String, + required: true, + }, + newIssuePath: { + type: String, + required: true, + }, + }, computed: { contents() { const obj = { diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 4c0f21fa1d6..059e2416b05 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -41,7 +41,7 @@ }, }, components: { - listsDropdown: gl.issueBoards.ModalFooterListsDropdown, + 'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown, }, template: ` <footer diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index 4a1845e4580..63268fcdb3d 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -24,7 +24,7 @@ }, }, components: { - modalTabs: gl.issueBoards.ModalTabs, + 'modal-tabs': gl.issueBoards.ModalTabs, }, template: ` <div> diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index 612657753d5..43d2fa03d92 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -8,10 +8,24 @@ const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.IssuesModal = Vue.extend({ - props: [ - 'blankStateImage', 'newIssuePath', 'issueLinkBase', - 'rootPath', - ], + props: { + blankStateImage: { + type: String, + required: true, + }, + newIssuePath: { + type: String, + required: true, + }, + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + }, data() { return ModalStore.store; }, @@ -76,10 +90,10 @@ }, }, components: { - modalHeader: gl.issueBoards.IssuesModalHeader, - modalList: gl.issueBoards.ModalList, - modalFooter: gl.issueBoards.ModalFooter, - emptyState: gl.issueBoards.ModalEmptyState, + 'modal-header': gl.issueBoards.IssuesModalHeader, + 'modal-list': gl.issueBoards.ModalList, + 'modal-footer': gl.issueBoards.ModalFooter, + 'empty-state': gl.issueBoards.ModalEmptyState, }, template: ` <div diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index 8db1ab4df5e..ae3e405e70e 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -6,9 +6,16 @@ const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalList = Vue.extend({ - props: [ - 'issueLinkBase', 'rootPath', - ], + props: { + issueLinkBase: { + type: String, + required: true, + }, + rootPath: { + type: String, + required: true, + }, + }, data() { return ModalStore.store; }, @@ -94,7 +101,7 @@ this.destroyMasonry(); }, components: { - issueCardInner: gl.issueBoards.IssueCardInner, + 'issue-card-inner': gl.issueBoards.IssueCardInner, }, template: ` <section diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 index 70f7da17d49..124baaae42a 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 @@ -6,9 +6,16 @@ window.gl.issueBoards = window.gl.issueBoards || {}; gl.issueBoards.RemoveIssueBtn = Vue.extend({ - props: [ - 'issue', 'list', - ], + props: { + issue: { + type: Object, + required: true, + }, + list: { + type: Object, + required: true, + }, + }, methods: { removeIssue() { const lists = this.issue.getLists(); -- cgit v1.2.1 From cf5396d4b7158a97a49b3a75c6ebc8938954bd2b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 31 Jan 2017 16:35:04 +0000 Subject: Fixed up specs --- spec/features/boards/add_issues_modal_spec.rb | 4 ++-- spec/features/boards/sidebar_spec.rb | 18 ------------------ spec/javascripts/boards/boards_store_spec.js.es6 | 2 +- 3 files changed, 3 insertions(+), 21 deletions(-) diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index ae245d7469b..b953480cec2 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -142,13 +142,13 @@ describe 'Issue Boards add issue modal', :feature, :js do end end - it 'un-selects all issues' do + it 'deselects all issues' do page.within('.add-issues-modal') do click_button 'Select all' expect(page).to have_selector('.is-active', count: 2) - click_button 'Un-select all' + click_button 'Deselect all' expect(page).not_to have_selector('.is-active') end diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index ea135d0cc95..9cc50167395 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -84,24 +84,6 @@ describe 'Issue Boards', feature: true, js: true do end end - it 'does not show remove issue button when issue is closed' do - page.within(first('.board')) do - first('.card').click - end - - page.within('.issue-boards-sidebar') do - click_button 'Remove from board' - end - - page.within(find('.board:nth-child(2)')) do - first('.card').click - end - - page.within('.issue-boards-sidebar') do - expect(page).not_to have_button 'Remove from board' - end - end - context 'assignee' do it 'updates the issues assignee' do page.within(first('.board')) do diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index a0d434e3588..0c9c889a444 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -23,7 +23,7 @@ describe('Store', () => { beforeEach(() => { Vue.http.interceptors.push(boardsMockInterceptor); - gl.boardService = new BoardService('/test/issue-boards/board', '1'); + gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); gl.issueBoards.BoardsStore.create(); Cookies.set('issue_board_welcome_hidden', 'false', { -- cgit v1.2.1 From b4113dba0378936024c496b15b3c8a5f1c0a1021 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 31 Jan 2017 16:46:57 +0000 Subject: Uses mixins for repeated functions --- app/assets/javascripts/boards/boards_bundle.js.es6 | 3 ++- .../javascripts/boards/components/modal/empty_state.js.es6 | 3 ++- .../javascripts/boards/components/modal/footer.js.es6 | 8 +++----- .../javascripts/boards/components/modal/header.js.es6 | 3 ++- app/assets/javascripts/boards/components/modal/tabs.js.es6 | 5 +++-- app/assets/javascripts/boards/mixins/modal_mixins.js.es6 | 14 ++++++++++++++ 6 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 app/assets/javascripts/boards/mixins/modal_mixins.js.es6 diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 81df823cad1..afe4c9f9175 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -91,6 +91,7 @@ $(() => { }); gl.IssueBoardsModalAddBtn = new Vue({ + mixins: [gl.issueBoards.ModalMixins], el: '#js-add-issues-btn', data: { modal: ModalStore.store, @@ -106,7 +107,7 @@ $(() => { class="btn btn-create pull-right prepend-left-10 has-tooltip" type="button" :disabled="disabled" - @click="modal.showAddIssuesModal = true"> + @click="toggleModal(true)"> Add issues </button> `, diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 index 7bd7c27b579..9538f5b69e9 100644 --- a/app/assets/javascripts/boards/components/modal/empty_state.js.es6 +++ b/app/assets/javascripts/boards/components/modal/empty_state.js.es6 @@ -3,6 +3,7 @@ const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalEmptyState = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], data() { return ModalStore.store; }, @@ -56,7 +57,7 @@ <button type="button" class="btn btn-default" - @click="activeTab = 'all'" + @click="changeTab('all')" v-if="activeTab === 'selected'"> All issues </button> diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 059e2416b05..8883beb1290 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -4,6 +4,7 @@ const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalFooter = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], data() { return ModalStore.store; }, @@ -18,9 +19,6 @@ }, }, methods: { - hideModal() { - this.showAddIssuesModal = false; - }, addIssues() { const list = this.selectedList; const selectedIssues = ModalStore.getSelectedIssues(); @@ -37,7 +35,7 @@ list.issuesSize += 1; }); - this.hideModal(); + this.toggleModal(false); }, }, components: { @@ -62,7 +60,7 @@ <button class="btn btn-default pull-right" type="button" - @click="hideModal"> + @click="toggleModal(false)"> Cancel </button> </footer> diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index 63268fcdb3d..194d598d42e 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -4,6 +4,7 @@ const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.IssuesModalHeader = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], data() { return ModalStore.store; }, @@ -36,7 +37,7 @@ class="close" data-dismiss="modal" aria-label="Close" - @click="showAddIssuesModal = false"> + @click="toggleModal(false)"> <span aria-hidden="true">×</span> </button> </h2> diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 index 007e01f7d82..d556c6d3e04 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js.es6 +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -3,6 +3,7 @@ const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalTabs = Vue.extend({ + mixins: [gl.issueBoards.ModalMixins], data() { return ModalStore.store; }, @@ -21,7 +22,7 @@ <a href="#" role="button" - @click.prevent="activeTab = 'all'"> + @click.prevent="changeTab('all')"> <span>All issues</span> <span class="badge"> {{ issuesCount }} @@ -32,7 +33,7 @@ <a href="#" role="button" - @click.prevent="activeTab = 'selected'"> + @click.prevent="changeTab('selected')"> <span>Selected issues</span> <span class="badge"> {{ selectedCount }} diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 new file mode 100644 index 00000000000..d378b7d4baf --- /dev/null +++ b/app/assets/javascripts/boards/mixins/modal_mixins.js.es6 @@ -0,0 +1,14 @@ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalMixins = { + methods: { + toggleModal(toggle) { + ModalStore.store.showAddIssuesModal = toggle; + }, + changeTab(tab) { + ModalStore.store.activeTab = tab; + }, + }, + }; +})(); -- cgit v1.2.1 From 4428bb27b78bf8f75d8ff15c227a8dfbb82aaa8e Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 1 Feb 2017 15:23:01 +0000 Subject: Removed Masonry, instead uses groups of data Added some error handling which reverts the frontend data changes & notifies the user --- app/assets/javascripts/boards/boards_bundle.js.es6 | 1 - .../boards/components/issue_card_inner.js.es6 | 10 +- .../boards/components/modal/footer.js.es6 | 11 +- .../boards/components/modal/header.js.es6 | 7 +- .../boards/components/modal/index.js.es6 | 24 +- .../boards/components/modal/list.js.es6 | 117 +- .../boards/components/modal/lists_dropdown.js.es6 | 2 +- .../boards/components/modal/tabs.js.es6 | 4 +- .../boards/components/sidebar/remove_issue.js.es6 | 15 +- .../javascripts/boards/stores/modal_store.js.es6 | 7 +- app/assets/javascripts/lib/utils/text_utility.js | 3 + app/assets/stylesheets/pages/boards.scss | 23 +- spec/javascripts/boards/issue_spec.js.es6 | 2 +- spec/javascripts/boards/list_spec.js.es6 | 2 +- .../javascripts/lib/utils/text_utility_spec.js.es6 | 14 + vendor/assets/javascripts/masonry.js | 2463 -------------------- 16 files changed, 154 insertions(+), 2551 deletions(-) delete mode 100644 vendor/assets/javascripts/masonry.js diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index afe4c9f9175..4ac91786762 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -5,7 +5,6 @@ //= require vue //= require vue-resource //= require Sortable -//= require masonry //= require_tree ./models //= require_tree ./stores //= require_tree ./services diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 index 10b82ba0998..22a8b971ff8 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js.es6 +++ b/app/assets/javascripts/boards/components/issue_card_inner.js.es6 @@ -33,7 +33,7 @@ filterByLabel(label, e) { let labelToggleText = label.title; const labelIndex = Store.state.filters.label_name.indexOf(label.title); - $(e.target).tooltip('hide'); + $(e.currentTarget).tooltip('hide'); if (labelIndex === -1) { Store.state.filters.label_name.push(label.title); @@ -55,6 +55,12 @@ Store.updateFiltersUrl(); }, + labelStyle(label) { + return { + backgroundColor: label.color, + color: label.textColor, + }; + }, }, template: ` <div> @@ -93,7 +99,7 @@ type="button" v-if="showLabel(label)" @click="filterByLabel(label, $event)" - :style="{ backgroundColor: label.color, color: label.textColor }" + :style="labelStyle(label)" :title="label.description" data-container="body"> {{ label.title }} diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 8883beb1290..4085a22b25a 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -1,5 +1,7 @@ +/* eslint-disable no-new */ //= require ./lists_dropdown /* global Vue */ +/* global Flash */ (() => { const ModalStore = gl.issueBoards.ModalStore; @@ -15,7 +17,7 @@ submitText() { const count = ModalStore.selectedCount(); - return `Add ${count > 0 ? count : ''} issue${count > 1 || !count ? 's' : ''}`; + return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`; }, }, methods: { @@ -27,6 +29,13 @@ // Post the data to the backend gl.boardService.bulkUpdate(issueIds, { add_label_ids: [list.label.id], + }).catch(() => { + new Flash('Failed to update issues, please try again.', 'alert'); + + selectedIssues.forEach((issue) => { + list.removeIssue(issue); + list.issuesSize -= 1; + }); }); // Add the issues on the frontend diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index 194d598d42e..dbbcd73f1fe 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -3,7 +3,7 @@ (() => { const ModalStore = gl.issueBoards.ModalStore; - gl.issueBoards.IssuesModalHeader = Vue.extend({ + gl.issueBoards.ModalHeader = Vue.extend({ mixins: [gl.issueBoards.ModalMixins], data() { return ModalStore.store; @@ -16,6 +16,9 @@ return 'Deselect all'; }, + showSearch() { + return this.activeTab === 'all' && !this.loading && this.issuesCount > 0; + }, }, methods: { toggleAll() { @@ -45,7 +48,7 @@ <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs> <div class="add-issues-search append-bottom-10" - v-if="activeTab == 'all' && !loading && issuesCount > 0"> + v-if="showSearch"> <input placeholder="Search issues..." class="form-control" diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index 43d2fa03d92..666f4e16793 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -53,10 +53,9 @@ }, methods: { searchOperation: _.debounce(function searchOperationDebounce() { - this.issues = []; - this.loadIssues(); + this.loadIssues(true); }, 500), - loadIssues() { + loadIssues(clearIssues = false) { return gl.boardService.getBacklog({ search: this.searchTerm, page: this.page, @@ -64,10 +63,14 @@ }).then((res) => { const data = res.json(); + if (clearIssues) { + this.issues = []; + } + data.issues.forEach((issueObj) => { const issue = new ListIssue(issueObj); const foundSelectedIssue = ModalStore.findSelectedIssue(issue); - issue.selected = foundSelectedIssue !== undefined; + issue.selected = !!foundSelectedIssue; this.issues.push(issue); }); @@ -75,7 +78,7 @@ this.loadingNewPage = false; if (!this.issuesCount) { - this.issuesCount = this.issues.length; + this.issuesCount = data.size; } }); }, @@ -88,9 +91,16 @@ return this.issuesCount > 0; }, + showEmptyState() { + if (!this.loading && this.issuesCount === 0) { + return true; + } + + return this.activeTab === 'selected' && this.selectedIssues.length === 0; + }, }, components: { - 'modal-header': gl.issueBoards.IssuesModalHeader, + 'modal-header': gl.issueBoards.ModalHeader, 'modal-list': gl.issueBoards.ModalList, 'modal-footer': gl.issueBoards.ModalFooter, 'empty-state': gl.issueBoards.ModalEmptyState, @@ -106,7 +116,7 @@ :root-path="rootPath" v-if="!loading && showList"></modal-list> <empty-state - v-if="(!loading && issuesCount === 0) || (activeTab === 'selected' && selectedIssues.length === 0)" + v-if="showEmptyState" :image="blankStateImage" :new-issue-path="newIssuePath"></empty-state> <section diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index ae3e405e70e..d0901219216 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -1,8 +1,7 @@ /* global Vue */ /* global ListIssue */ -/* global Masonry */ +/* global bp */ (() => { - let listMasonry; const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalList = Vue.extend({ @@ -21,18 +20,10 @@ }, watch: { activeTab() { - this.initMasonry(); - if (this.activeTab === 'all') { ModalStore.purgeUnselectedIssues(); } }, - issues: { - handler() { - this.initMasonry(); - }, - deep: true, - }, }, computed: { loopIssues() { @@ -42,8 +33,31 @@ return this.selectedIssues; }, + groupedIssues() { + const groups = []; + this.loopIssues.forEach((issue, i) => { + const index = i % this.columns; + + if (!groups[index]) { + groups.push([]); + } + + groups[index].push(issue); + }); + + return groups; + }, }, methods: { + scrollHandler() { + const currentPage = Math.floor(this.issues.length / this.perPage); + + if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage + && currentPage === this.page) { + this.loadingNewPage = true; + this.page += 1; + } + }, toggleIssue(e, issue) { if (e.target.tagName !== 'A') { ModalStore.toggleIssue(issue); @@ -65,40 +79,29 @@ return index !== -1; }, - initMasonry() { - const listScrollTop = this.$refs.list.scrollTop; - - this.$nextTick(() => { - this.destroyMasonry(); - listMasonry = new Masonry(this.$refs.list, { - transitionDuration: 0, - }); + setColumnCount() { + const breakpoint = bp.getBreakpointSize(); - this.$refs.list.scrollTop = listScrollTop; - }); - }, - destroyMasonry() { - if (listMasonry) { - listMasonry.destroy(); - listMasonry = undefined; + if (breakpoint === 'lg' || breakpoint === 'md') { + this.columns = 3; + } else if (breakpoint === 'sm') { + this.columns = 2; + } else { + this.columns = 1; } }, }, mounted() { - this.initMasonry(); - - this.$refs.list.onscroll = () => { - const currentPage = Math.floor(this.issues.length / this.perPage); + this.scrollHandlerWrapper = this.scrollHandler.bind(this); + this.setColumnCountWrapper = this.setColumnCount.bind(this); + this.setColumnCount(); - if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage - && currentPage === this.page) { - this.loadingNewPage = true; - this.page += 1; - } - }; + this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper); + window.addEventListener('resize', this.setColumnCountWrapper); }, - destroyed() { - this.destroyMasonry(); + beforeDestroy() { + this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper); + window.removeEventListener('resize', this.setColumnCountWrapper); }, components: { 'issue-card-inner': gl.issueBoards.IssueCardInner, @@ -108,25 +111,29 @@ class="add-issues-list add-issues-list-columns" ref="list"> <div - v-for="issue in loopIssues" - v-if="showIssue(issue)" - class="card-parent"> + v-for="group in groupedIssues" + class="add-issues-list-column"> <div - class="card" - :class="{ 'is-active': issue.selected }" - @click="toggleIssue($event, issue)"> - <issue-card-inner - :issue="issue" - :issue-link-base="issueLinkBase" - :root-path="rootPath"> - </issue-card-inner> - <span - :aria-label="'Issue #' + issue.id + ' selected'" - aria-checked="true" - v-if="issue.selected" - class="issue-card-selected text-center"> - <i class="fa fa-check"></i> - </span> + v-for="issue in group" + v-if="showIssue(issue)" + class="card-parent"> + <div + class="card" + :class="{ 'is-active': issue.selected }" + @click="toggleIssue($event, issue)"> + <issue-card-inner + :issue="issue" + :issue-link-base="issueLinkBase" + :root-path="rootPath"> + </issue-card-inner> + <span + :aria-label="'Issue #' + issue.id + ' selected'" + aria-checked="true" + v-if="issue.selected" + class="issue-card-selected text-center"> + <i class="fa fa-check"></i> + </span> + </div> </div> </div> </section> diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 index bb2d43c4a21..96f12da3753 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 @@ -37,7 +37,7 @@ href="#" role="button" :class="{ 'is-active': list.id == selected.id }" - @click="modal.selectedList = list"> + @click.prevent="modal.selectedList = list"> <span class="dropdown-label-box" :style="{ backgroundColor: list.label.color }"> diff --git a/app/assets/javascripts/boards/components/modal/tabs.js.es6 b/app/assets/javascripts/boards/components/modal/tabs.js.es6 index d556c6d3e04..e8cb43f3503 100644 --- a/app/assets/javascripts/boards/components/modal/tabs.js.es6 +++ b/app/assets/javascripts/boards/components/modal/tabs.js.es6 @@ -23,7 +23,7 @@ href="#" role="button" @click.prevent="changeTab('all')"> - <span>All issues</span> + All issues <span class="badge"> {{ issuesCount }} </span> @@ -34,7 +34,7 @@ href="#" role="button" @click.prevent="changeTab('selected')"> - <span>Selected issues</span> + Selected issues <span class="badge"> {{ selectedCount }} </span> diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 index 124baaae42a..e74935e1cb0 100644 --- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 +++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js.es6 @@ -1,4 +1,6 @@ +/* eslint-disable no-new */ /* global Vue */ +/* global Flash */ (() => { const Store = gl.issueBoards.BoardsStore; @@ -18,17 +20,24 @@ }, methods: { removeIssue() { - const lists = this.issue.getLists(); + const issue = this.issue; + const lists = issue.getLists(); const labelIds = lists.map(list => list.label.id); // Post the remove data - gl.boardService.bulkUpdate([this.issue.globalId], { + gl.boardService.bulkUpdate([issue.globalId], { remove_label_ids: labelIds, + }).catch(() => { + new Flash('Failed to remove issue from board, please try again.', 'alert'); + + lists.forEach((list) => { + list.addIssue(issue); + }); }); // Remove from the frontend store lists.forEach((list) => { - list.removeIssue(this.issue); + list.removeIssue(issue); }); Store.detail.issue = {}; diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index 9c498ba48c4..fa46b6135e0 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -5,6 +5,7 @@ class ModalStore { constructor() { this.store = { + columns: 3, issues: [], issuesCount: false, selectedIssues: [], @@ -25,9 +26,11 @@ toggleIssue(issueObj) { const issue = issueObj; - issue.selected = !issue.selected; + const selected = issue.selected; - if (issue.selected) { + issue.selected = !selected; + + if (!selected) { this.addSelectedIssue(issue); } else { this.removeSelectedIssue(issue); diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 6bb575059b7..d9370db0cf2 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -161,6 +161,9 @@ gl.text.humanize = function(string) { return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1); }; + gl.text.pluralize = function(str, count) { + return str + (count > 1 || count === 0 ? 's' : ''); + }; return gl.text.truncate = function(string, maxLength) { return string.substr(0, (maxLength - 3)) + '...'; }; diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index ded15dc493e..9b413f3e61c 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -418,6 +418,18 @@ display: flex; } +.add-issues-list-column { + width: 100%; + + @media (min-width: $screen-sm-min) { + width: 50%; + } + + @media (min-width: $screen-md-min) { + width: (100% / 3); + } +} + .add-issues-list { display: -webkit-flex; display: flex; @@ -429,16 +441,7 @@ overflow-y: scroll; .card-parent { - width: 100%; padding: 0 5px 5px; - - @media (min-width: $screen-sm-min) { - width: 50%; - } - - @media (min-width: $screen-md-min) { - width: (100% / 3); - } } .card { @@ -480,6 +483,6 @@ color: $white-light; border: 1px solid $border-blue-light; font-size: 9px; - line-height: 17px; + line-height: 15px; border-radius: 50%; } diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index c8a61a0a9b5..1d33490fc75 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -20,7 +20,7 @@ describe('Issue model', () => { let issue; beforeEach(() => { - gl.boardService = new BoardService('/test/issue-boards/board', '1'); + gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); gl.issueBoards.BoardsStore.create(); issue = new ListIssue({ diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index 7d942ec3d65..770aa981bcb 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -24,7 +24,7 @@ describe('List model', () => { beforeEach(() => { Vue.http.interceptors.push(boardsMockInterceptor); - gl.boardService = new BoardService('/test/issue-boards/board', '1'); + gl.boardService = new BoardService('/test/issue-boards/board', '', '1'); gl.issueBoards.BoardsStore.create(); list = new List(listObj); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6 index e97356b65d5..329e18f405a 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js.es6 +++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6 @@ -21,5 +21,19 @@ expect(largeFont > regular).toBe(true); }); }); + + describe('gl.text.pluralize', () => { + it('returns pluralized', () => { + expect(gl.text.pluralize('test', 2)).toBe('tests'); + }); + + it('returns pluralized', () => { + expect(gl.text.pluralize('test', 0)).toBe('tests'); + }); + + it('does not return pluralized', () => { + expect(gl.text.pluralize('test', 1)).toBe('test'); + }); + }); }); })(); diff --git a/vendor/assets/javascripts/masonry.js b/vendor/assets/javascripts/masonry.js deleted file mode 100644 index adf62864671..00000000000 --- a/vendor/assets/javascripts/masonry.js +++ /dev/null @@ -1,2463 +0,0 @@ -/*! - * Masonry PACKAGED v4.1.1 - * Cascading grid layout library - * http://masonry.desandro.com - * MIT License - * by David DeSandro - */ - -/** - * Bridget makes jQuery widgets - * v2.0.1 - * MIT license - */ - -/* jshint browser: true, strict: true, undef: true, unused: true */ - -( function( window, factory ) { - // universal module definition - /*jshint strict: false */ /* globals define, module, require */ - if ( typeof define == 'function' && define.amd ) { - // AMD - define( 'jquery-bridget/jquery-bridget',[ 'jquery' ], function( jQuery ) { - return factory( window, jQuery ); - }); - } else if ( typeof module == 'object' && module.exports ) { - // CommonJS - module.exports = factory( - window, - require('jquery') - ); - } else { - // browser global - window.jQueryBridget = factory( - window, - window.jQuery - ); - } - -}( window, function factory( window, jQuery ) { -'use strict'; - -// ----- utils ----- // - -var arraySlice = Array.prototype.slice; - -// helper function for logging errors -// $.error breaks jQuery chaining -var console = window.console; -var logError = typeof console == 'undefined' ? function() {} : - function( message ) { - console.error( message ); - }; - -// ----- jQueryBridget ----- // - -function jQueryBridget( namespace, PluginClass, $ ) { - $ = $ || jQuery || window.jQuery; - if ( !$ ) { - return; - } - - // add option method -> $().plugin('option', {...}) - if ( !PluginClass.prototype.option ) { - // option setter - PluginClass.prototype.option = function( opts ) { - // bail out if not an object - if ( !$.isPlainObject( opts ) ){ - return; - } - this.options = $.extend( true, this.options, opts ); - }; - } - - // make jQuery plugin - $.fn[ namespace ] = function( arg0 /*, arg1 */ ) { - if ( typeof arg0 == 'string' ) { - // method call $().plugin( 'methodName', { options } ) - // shift arguments by 1 - var args = arraySlice.call( arguments, 1 ); - return methodCall( this, arg0, args ); - } - // just $().plugin({ options }) - plainCall( this, arg0 ); - return this; - }; - - // $().plugin('methodName') - function methodCall( $elems, methodName, args ) { - var returnValue; - var pluginMethodStr = '$().' + namespace + '("' + methodName + '")'; - - $elems.each( function( i, elem ) { - // get instance - var instance = $.data( elem, namespace ); - if ( !instance ) { - logError( namespace + ' not initialized. Cannot call methods, i.e. ' + - pluginMethodStr ); - return; - } - - var method = instance[ methodName ]; - if ( !method || methodName.charAt(0) == '_' ) { - logError( pluginMethodStr + ' is not a valid method' ); - return; - } - - // apply method, get return value - var value = method.apply( instance, args ); - // set return value if value is returned, use only first value - returnValue = returnValue === undefined ? value : returnValue; - }); - - return returnValue !== undefined ? returnValue : $elems; - } - - function plainCall( $elems, options ) { - $elems.each( function( i, elem ) { - var instance = $.data( elem, namespace ); - if ( instance ) { - // set options & init - instance.option( options ); - instance._init(); - } else { - // initialize new instance - instance = new PluginClass( elem, options ); - $.data( elem, namespace, instance ); - } - }); - } - - updateJQuery( $ ); - -} - -// ----- updateJQuery ----- // - -// set $.bridget for v1 backwards compatibility -function updateJQuery( $ ) { - if ( !$ || ( $ && $.bridget ) ) { - return; - } - $.bridget = jQueryBridget; -} - -updateJQuery( jQuery || window.jQuery ); - -// ----- ----- // - -return jQueryBridget; - -})); - -/** - * EvEmitter v1.0.3 - * Lil' event emitter - * MIT License - */ - -/* jshint unused: true, undef: true, strict: true */ - -( function( global, factory ) { - // universal module definition - /* jshint strict: false */ /* globals define, module, window */ - if ( typeof define == 'function' && define.amd ) { - // AMD - RequireJS - define( 'ev-emitter/ev-emitter',factory ); - } else if ( typeof module == 'object' && module.exports ) { - // CommonJS - Browserify, Webpack - module.exports = factory(); - } else { - // Browser globals - global.EvEmitter = factory(); - } - -}( typeof window != 'undefined' ? window : this, function() { - - - -function EvEmitter() {} - -var proto = EvEmitter.prototype; - -proto.on = function( eventName, listener ) { - if ( !eventName || !listener ) { - return; - } - // set events hash - var events = this._events = this._events || {}; - // set listeners array - var listeners = events[ eventName ] = events[ eventName ] || []; - // only add once - if ( listeners.indexOf( listener ) == -1 ) { - listeners.push( listener ); - } - - return this; -}; - -proto.once = function( eventName, listener ) { - if ( !eventName || !listener ) { - return; - } - // add event - this.on( eventName, listener ); - // set once flag - // set onceEvents hash - var onceEvents = this._onceEvents = this._onceEvents || {}; - // set onceListeners object - var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {}; - // set flag - onceListeners[ listener ] = true; - - return this; -}; - -proto.off = function( eventName, listener ) { - var listeners = this._events && this._events[ eventName ]; - if ( !listeners || !listeners.length ) { - return; - } - var index = listeners.indexOf( listener ); - if ( index != -1 ) { - listeners.splice( index, 1 ); - } - - return this; -}; - -proto.emitEvent = function( eventName, args ) { - var listeners = this._events && this._events[ eventName ]; - if ( !listeners || !listeners.length ) { - return; - } - var i = 0; - var listener = listeners[i]; - args = args || []; - // once stuff - var onceListeners = this._onceEvents && this._onceEvents[ eventName ]; - - while ( listener ) { - var isOnce = onceListeners && onceListeners[ listener ]; - if ( isOnce ) { - // remove listener - // remove before trigger to prevent recursion - this.off( eventName, listener ); - // unset once flag - delete onceListeners[ listener ]; - } - // trigger listener - listener.apply( this, args ); - // get next listener - i += isOnce ? 0 : 1; - listener = listeners[i]; - } - - return this; -}; - -return EvEmitter; - -})); - -/*! - * getSize v2.0.2 - * measure size of elements - * MIT license - */ - -/*jshint browser: true, strict: true, undef: true, unused: true */ -/*global define: false, module: false, console: false */ - -( function( window, factory ) { - 'use strict'; - - if ( typeof define == 'function' && define.amd ) { - // AMD - define( 'get-size/get-size',[],function() { - return factory(); - }); - } else if ( typeof module == 'object' && module.exports ) { - // CommonJS - module.exports = factory(); - } else { - // browser global - window.getSize = factory(); - } - -})( window, function factory() { -'use strict'; - -// -------------------------- helpers -------------------------- // - -// get a number from a string, not a percentage -function getStyleSize( value ) { - var num = parseFloat( value ); - // not a percent like '100%', and a number - var isValid = value.indexOf('%') == -1 && !isNaN( num ); - return isValid && num; -} - -function noop() {} - -var logError = typeof console == 'undefined' ? noop : - function( message ) { - console.error( message ); - }; - -// -------------------------- measurements -------------------------- // - -var measurements = [ - 'paddingLeft', - 'paddingRight', - 'paddingTop', - 'paddingBottom', - 'marginLeft', - 'marginRight', - 'marginTop', - 'marginBottom', - 'borderLeftWidth', - 'borderRightWidth', - 'borderTopWidth', - 'borderBottomWidth' -]; - -var measurementsLength = measurements.length; - -function getZeroSize() { - var size = { - width: 0, - height: 0, - innerWidth: 0, - innerHeight: 0, - outerWidth: 0, - outerHeight: 0 - }; - for ( var i=0; i < measurementsLength; i++ ) { - var measurement = measurements[i]; - size[ measurement ] = 0; - } - return size; -} - -// -------------------------- getStyle -------------------------- // - -/** - * getStyle, get style of element, check for Firefox bug - * https://bugzilla.mozilla.org/show_bug.cgi?id=548397 - */ -function getStyle( elem ) { - var style = getComputedStyle( elem ); - if ( !style ) { - logError( 'Style returned ' + style + - '. Are you running this code in a hidden iframe on Firefox? ' + - 'See http://bit.ly/getsizebug1' ); - } - return style; -} - -// -------------------------- setup -------------------------- // - -var isSetup = false; - -var isBoxSizeOuter; - -/** - * setup - * check isBoxSizerOuter - * do on first getSize() rather than on page load for Firefox bug - */ -function setup() { - // setup once - if ( isSetup ) { - return; - } - isSetup = true; - - // -------------------------- box sizing -------------------------- // - - /** - * WebKit measures the outer-width on style.width on border-box elems - * IE & Firefox<29 measures the inner-width - */ - var div = document.createElement('div'); - div.style.width = '200px'; - div.style.padding = '1px 2px 3px 4px'; - div.style.borderStyle = 'solid'; - div.style.borderWidth = '1px 2px 3px 4px'; - div.style.boxSizing = 'border-box'; - - var body = document.body || document.documentElement; - body.appendChild( div ); - var style = getStyle( div ); - - getSize.isBoxSizeOuter = isBoxSizeOuter = getStyleSize( style.width ) == 200; - body.removeChild( div ); - -} - -// -------------------------- getSize -------------------------- // - -function getSize( elem ) { - setup(); - - // use querySeletor if elem is string - if ( typeof elem == 'string' ) { - elem = document.querySelector( elem ); - } - - // do not proceed on non-objects - if ( !elem || typeof elem != 'object' || !elem.nodeType ) { - return; - } - - var style = getStyle( elem ); - - // if hidden, everything is 0 - if ( style.display == 'none' ) { - return getZeroSize(); - } - - var size = {}; - size.width = elem.offsetWidth; - size.height = elem.offsetHeight; - - var isBorderBox = size.isBorderBox = style.boxSizing == 'border-box'; - - // get all measurements - for ( var i=0; i < measurementsLength; i++ ) { - var measurement = measurements[i]; - var value = style[ measurement ]; - var num = parseFloat( value ); - // any 'auto', 'medium' value will be 0 - size[ measurement ] = !isNaN( num ) ? num : 0; - } - - var paddingWidth = size.paddingLeft + size.paddingRight; - var paddingHeight = size.paddingTop + size.paddingBottom; - var marginWidth = size.marginLeft + size.marginRight; - var marginHeight = size.marginTop + size.marginBottom; - var borderWidth = size.borderLeftWidth + size.borderRightWidth; - var borderHeight = size.borderTopWidth + size.borderBottomWidth; - - var isBorderBoxSizeOuter = isBorderBox && isBoxSizeOuter; - - // overwrite width and height if we can get it from style - var styleWidth = getStyleSize( style.width ); - if ( styleWidth !== false ) { - size.width = styleWidth + - // add padding and border unless it's already including it - ( isBorderBoxSizeOuter ? 0 : paddingWidth + borderWidth ); - } - - var styleHeight = getStyleSize( style.height ); - if ( styleHeight !== false ) { - size.height = styleHeight + - // add padding and border unless it's already including it - ( isBorderBoxSizeOuter ? 0 : paddingHeight + borderHeight ); - } - - size.innerWidth = size.width - ( paddingWidth + borderWidth ); - size.innerHeight = size.height - ( paddingHeight + borderHeight ); - - size.outerWidth = size.width + marginWidth; - size.outerHeight = size.height + marginHeight; - - return size; -} - -return getSize; - -}); - -/** - * matchesSelector v2.0.1 - * matchesSelector( element, '.selector' ) - * MIT license - */ - -/*jshint browser: true, strict: true, undef: true, unused: true */ - -( function( window, factory ) { - /*global define: false, module: false */ - 'use strict'; - // universal module definition - if ( typeof define == 'function' && define.amd ) { - // AMD - define( 'desandro-matches-selector/matches-selector',factory ); - } else if ( typeof module == 'object' && module.exports ) { - // CommonJS - module.exports = factory(); - } else { - // browser global - window.matchesSelector = factory(); - } - -}( window, function factory() { - 'use strict'; - - var matchesMethod = ( function() { - var ElemProto = Element.prototype; - // check for the standard method name first - if ( ElemProto.matches ) { - return 'matches'; - } - // check un-prefixed - if ( ElemProto.matchesSelector ) { - return 'matchesSelector'; - } - // check vendor prefixes - var prefixes = [ 'webkit', 'moz', 'ms', 'o' ]; - - for ( var i=0; i < prefixes.length; i++ ) { - var prefix = prefixes[i]; - var method = prefix + 'MatchesSelector'; - if ( ElemProto[ method ] ) { - return method; - } - } - })(); - - return function matchesSelector( elem, selector ) { - return elem[ matchesMethod ]( selector ); - }; - -})); - -/** - * Fizzy UI utils v2.0.2 - * MIT license - */ - -/*jshint browser: true, undef: true, unused: true, strict: true */ - -( function( window, factory ) { - // universal module definition - /*jshint strict: false */ /*globals define, module, require */ - - if ( typeof define == 'function' && define.amd ) { - // AMD - define( 'fizzy-ui-utils/utils',[ - 'desandro-matches-selector/matches-selector' - ], function( matchesSelector ) { - return factory( window, matchesSelector ); - }); - } else if ( typeof module == 'object' && module.exports ) { - // CommonJS - module.exports = factory( - window, - require('desandro-matches-selector') - ); - } else { - // browser global - window.fizzyUIUtils = factory( - window, - window.matchesSelector - ); - } - -}( window, function factory( window, matchesSelector ) { - - - -var utils = {}; - -// ----- extend ----- // - -// extends objects -utils.extend = function( a, b ) { - for ( var prop in b ) { - a[ prop ] = b[ prop ]; - } - return a; -}; - -// ----- modulo ----- // - -utils.modulo = function( num, div ) { - return ( ( num % div ) + div ) % div; -}; - -// ----- makeArray ----- // - -// turn element or nodeList into an array -utils.makeArray = function( obj ) { - var ary = []; - if ( Array.isArray( obj ) ) { - // use object if already an array - ary = obj; - } else if ( obj && typeof obj.length == 'number' ) { - // convert nodeList to array - for ( var i=0; i < obj.length; i++ ) { - ary.push( obj[i] ); - } - } else { - // array of single index - ary.push( obj ); - } - return ary; -}; - -// ----- removeFrom ----- // - -utils.removeFrom = function( ary, obj ) { - var index = ary.indexOf( obj ); - if ( index != -1 ) { - ary.splice( index, 1 ); - } -}; - -// ----- getParent ----- // - -utils.getParent = function( elem, selector ) { - while ( elem != document.body ) { - elem = elem.parentNode; - if ( matchesSelector( elem, selector ) ) { - return elem; - } - } -}; - -// ----- getQueryElement ----- // - -// use element as selector string -utils.getQueryElement = function( elem ) { - if ( typeof elem == 'string' ) { - return document.querySelector( elem ); - } - return elem; -}; - -// ----- handleEvent ----- // - -// enable .ontype to trigger from .addEventListener( elem, 'type' ) -utils.handleEvent = function( event ) { - var method = 'on' + event.type; - if ( this[ method ] ) { - this[ method ]( event ); - } -}; - -// ----- filterFindElements ----- // - -utils.filterFindElements = function( elems, selector ) { - // make array of elems - elems = utils.makeArray( elems ); - var ffElems = []; - - elems.forEach( function( elem ) { - // check that elem is an actual element - if ( !( elem instanceof HTMLElement ) ) { - return; - } - // add elem if no selector - if ( !selector ) { - ffElems.push( elem ); - return; - } - // filter & find items if we have a selector - // filter - if ( matchesSelector( elem, selector ) ) { - ffElems.push( elem ); - } - // find children - var childElems = elem.querySelectorAll( selector ); - // concat childElems to filterFound array - for ( var i=0; i < childElems.length; i++ ) { - ffElems.push( childElems[i] ); - } - }); - - return ffElems; -}; - -// ----- debounceMethod ----- // - -utils.debounceMethod = function( _class, methodName, threshold ) { - // original method - var method = _class.prototype[ methodName ]; - var timeoutName = methodName + 'Timeout'; - - _class.prototype[ methodName ] = function() { - var timeout = this[ timeoutName ]; - if ( timeout ) { - clearTimeout( timeout ); - } - var args = arguments; - - var _this = this; - this[ timeoutName ] = setTimeout( function() { - method.apply( _this, args ); - delete _this[ timeoutName ]; - }, threshold || 100 ); - }; -}; - -// ----- docReady ----- // - -utils.docReady = function( callback ) { - var readyState = document.readyState; - if ( readyState == 'complete' || readyState == 'interactive' ) { - callback(); - } else { - document.addEventListener( 'DOMContentLoaded', callback ); - } -}; - -// ----- htmlInit ----- // - -// http://jamesroberts.name/blog/2010/02/22/string-functions-for-javascript-trim-to-camel-case-to-dashed-and-to-underscore/ -utils.toDashed = function( str ) { - return str.replace( /(.)([A-Z])/g, function( match, $1, $2 ) { - return $1 + '-' + $2; - }).toLowerCase(); -}; - -var console = window.console; -/** - * allow user to initialize classes via [data-namespace] or .js-namespace class - * htmlInit( Widget, 'widgetName' ) - * options are parsed from data-namespace-options - */ -utils.htmlInit = function( WidgetClass, namespace ) { - utils.docReady( function() { - var dashedNamespace = utils.toDashed( namespace ); - var dataAttr = 'data-' + dashedNamespace; - var dataAttrElems = document.querySelectorAll( '[' + dataAttr + ']' ); - var jsDashElems = document.querySelectorAll( '.js-' + dashedNamespace ); - var elems = utils.makeArray( dataAttrElems ) - .concat( utils.makeArray( jsDashElems ) ); - var dataOptionsAttr = dataAttr + '-options'; - var jQuery = window.jQuery; - - elems.forEach( function( elem ) { - var attr = elem.getAttribute( dataAttr ) || - elem.getAttribute( dataOptionsAttr ); - var options; - try { - options = attr && JSON.parse( attr ); - } catch ( error ) { - // log error, do not initialize - if ( console ) { - console.error( 'Error parsing ' + dataAttr + ' on ' + elem.className + - ': ' + error ); - } - return; - } - // initialize - var instance = new WidgetClass( elem, options ); - // make available via $().data('layoutname') - if ( jQuery ) { - jQuery.data( elem, namespace, instance ); - } - }); - - }); -}; - -// ----- ----- // - -return utils; - -})); - -/** - * Outlayer Item - */ - -( function( window, factory ) { - // universal module definition - /* jshint strict: false */ /* globals define, module, require */ - if ( typeof define == 'function' && define.amd ) { - // AMD - RequireJS - define( 'outlayer/item',[ - 'ev-emitter/ev-emitter', - 'get-size/get-size' - ], - factory - ); - } else if ( typeof module == 'object' && module.exports ) { - // CommonJS - Browserify, Webpack - module.exports = factory( - require('ev-emitter'), - require('get-size') - ); - } else { - // browser global - window.Outlayer = {}; - window.Outlayer.Item = factory( - window.EvEmitter, - window.getSize - ); - } - -}( window, function factory( EvEmitter, getSize ) { -'use strict'; - -// ----- helpers ----- // - -function isEmptyObj( obj ) { - for ( var prop in obj ) { - return false; - } - prop = null; - return true; -} - -// -------------------------- CSS3 support -------------------------- // - - -var docElemStyle = document.documentElement.style; - -var transitionProperty = typeof docElemStyle.transition == 'string' ? - 'transition' : 'WebkitTransition'; -var transformProperty = typeof docElemStyle.transform == 'string' ? - 'transform' : 'WebkitTransform'; - -var transitionEndEvent = { - WebkitTransition: 'webkitTransitionEnd', - transition: 'transitionend' -}[ transitionProperty ]; - -// cache all vendor properties that could have vendor prefix -var vendorProperties = { - transform: transformProperty, - transition: transitionProperty, - transitionDuration: transitionProperty + 'Duration', - transitionProperty: transitionProperty + 'Property', - transitionDelay: transitionProperty + 'Delay' -}; - -// -------------------------- Item -------------------------- // - -function Item( element, layout ) { - if ( !element ) { - return; - } - - this.element = element; - // parent layout class, i.e. Masonry, Isotope, or Packery - this.layout = layout; - this.position = { - x: 0, - y: 0 - }; - - this._create(); -} - -// inherit EvEmitter -var proto = Item.prototype = Object.create( EvEmitter.prototype ); -proto.constructor = Item; - -proto._create = function() { - // transition objects - this._transn = { - ingProperties: {}, - clean: {}, - onEnd: {} - }; - - this.css({ - position: 'absolute' - }); -}; - -// trigger specified handler for event type -proto.handleEvent = function( event ) { - var method = 'on' + event.type; - if ( this[ method ] ) { - this[ method ]( event ); - } -}; - -proto.getSize = function() { - this.size = getSize( this.element ); -}; - -/** - * apply CSS styles to element - * @param {Object} style - */ -proto.css = function( style ) { - var elemStyle = this.element.style; - - for ( var prop in style ) { - // use vendor property if available - var supportedProp = vendorProperties[ prop ] || prop; - elemStyle[ supportedProp ] = style[ prop ]; - } -}; - - // measure position, and sets it -proto.getPosition = function() { - var style = getComputedStyle( this.element ); - var isOriginLeft = this.layout._getOption('originLeft'); - var isOriginTop = this.layout._getOption('originTop'); - var xValue = style[ isOriginLeft ? 'left' : 'right' ]; - var yValue = style[ isOriginTop ? 'top' : 'bottom' ]; - // convert percent to pixels - var layoutSize = this.layout.size; - var x = xValue.indexOf('%') != -1 ? - ( parseFloat( xValue ) / 100 ) * layoutSize.width : parseInt( xValue, 10 ); - var y = yValue.indexOf('%') != -1 ? - ( parseFloat( yValue ) / 100 ) * layoutSize.height : parseInt( yValue, 10 ); - - // clean up 'auto' or other non-integer values - x = isNaN( x ) ? 0 : x; - y = isNaN( y ) ? 0 : y; - // remove padding from measurement - x -= isOriginLeft ? layoutSize.paddingLeft : layoutSize.paddingRight; - y -= isOriginTop ? layoutSize.paddingTop : layoutSize.paddingBottom; - - this.position.x = x; - this.position.y = y; -}; - -// set settled position, apply padding -proto.layoutPosition = function() { - var layoutSize = this.layout.size; - var style = {}; - var isOriginLeft = this.layout._getOption('originLeft'); - var isOriginTop = this.layout._getOption('originTop'); - - // x - var xPadding = isOriginLeft ? 'paddingLeft' : 'paddingRight'; - var xProperty = isOriginLeft ? 'left' : 'right'; - var xResetProperty = isOriginLeft ? 'right' : 'left'; - - var x = this.position.x + layoutSize[ xPadding ]; - // set in percentage or pixels - style[ xProperty ] = this.getXValue( x ); - // reset other property - style[ xResetProperty ] = ''; - - // y - var yPadding = isOriginTop ? 'paddingTop' : 'paddingBottom'; - var yProperty = isOriginTop ? 'top' : 'bottom'; - var yResetProperty = isOriginTop ? 'bottom' : 'top'; - - var y = this.position.y + layoutSize[ yPadding ]; - // set in percentage or pixels - style[ yProperty ] = this.getYValue( y ); - // reset other property - style[ yResetProperty ] = ''; - - this.css( style ); - this.emitEvent( 'layout', [ this ] ); -}; - -proto.getXValue = function( x ) { - var isHorizontal = this.layout._getOption('horizontal'); - return this.layout.options.percentPosition && !isHorizontal ? - ( ( x / this.layout.size.width ) * 100 ) + '%' : x + 'px'; -}; - -proto.getYValue = function( y ) { - var isHorizontal = this.layout._getOption('horizontal'); - return this.layout.options.percentPosition && isHorizontal ? - ( ( y / this.layout.size.height ) * 100 ) + '%' : y + 'px'; -}; - -proto._transitionTo = function( x, y ) { - this.getPosition(); - // get current x & y from top/left - var curX = this.position.x; - var curY = this.position.y; - - var compareX = parseInt( x, 10 ); - var compareY = parseInt( y, 10 ); - var didNotMove = compareX === this.position.x && compareY === this.position.y; - - // save end position - this.setPosition( x, y ); - - // if did not move and not transitioning, just go to layout - if ( didNotMove && !this.isTransitioning ) { - this.layoutPosition(); - return; - } - - var transX = x - curX; - var transY = y - curY; - var transitionStyle = {}; - transitionStyle.transform = this.getTranslate( transX, transY ); - - this.transition({ - to: transitionStyle, - onTransitionEnd: { - transform: this.layoutPosition - }, - isCleaning: true - }); -}; - -proto.getTranslate = function( x, y ) { - // flip cooridinates if origin on right or bottom - var isOriginLeft = this.layout._getOption('originLeft'); - var isOriginTop = this.layout._getOption('originTop'); - x = isOriginLeft ? x : -x; - y = isOriginTop ? y : -y; - return 'translate3d(' + x + 'px, ' + y + 'px, 0)'; -}; - -// non transition + transform support -proto.goTo = function( x, y ) { - this.setPosition( x, y ); - this.layoutPosition(); -}; - -proto.moveTo = proto._transitionTo; - -proto.setPosition = function( x, y ) { - this.position.x = parseInt( x, 10 ); - this.position.y = parseInt( y, 10 ); -}; - -// ----- transition ----- // - -/** - * @param {Object} style - CSS - * @param {Function} onTransitionEnd - */ - -// non transition, just trigger callback -proto._nonTransition = function( args ) { - this.css( args.to ); - if ( args.isCleaning ) { - this._removeStyles( args.to ); - } - for ( var prop in args.onTransitionEnd ) { - args.onTransitionEnd[ prop ].call( this ); - } -}; - -/** - * proper transition - * @param {Object} args - arguments - * @param {Object} to - style to transition to - * @param {Object} from - style to start transition from - * @param {Boolean} isCleaning - removes transition styles after transition - * @param {Function} onTransitionEnd - callback - */ -proto.transition = function( args ) { - // redirect to nonTransition if no transition duration - if ( !parseFloat( this.layout.options.transitionDuration ) ) { - this._nonTransition( args ); - return; - } - - var _transition = this._transn; - // keep track of onTransitionEnd callback by css property - for ( var prop in args.onTransitionEnd ) { - _transition.onEnd[ prop ] = args.onTransitionEnd[ prop ]; - } - // keep track of properties that are transitioning - for ( prop in args.to ) { - _transition.ingProperties[ prop ] = true; - // keep track of properties to clean up when transition is done - if ( args.isCleaning ) { - _transition.clean[ prop ] = true; - } - } - - // set from styles - if ( args.from ) { - this.css( args.from ); - // force redraw. http://blog.alexmaccaw.com/css-transitions - var h = this.element.offsetHeight; - // hack for JSHint to hush about unused var - h = null; - } - // enable transition - this.enableTransition( args.to ); - // set styles that are transitioning - this.css( args.to ); - - this.isTransitioning = true; - -}; - -// dash before all cap letters, including first for -// WebkitTransform => -webkit-transform -function toDashedAll( str ) { - return str.replace( /([A-Z])/g, function( $1 ) { - return '-' + $1.toLowerCase(); - }); -} - -var transitionProps = 'opacity,' + toDashedAll( transformProperty ); - -proto.enableTransition = function(/* style */) { - // HACK changing transitionProperty during a transition - // will cause transition to jump - if ( this.isTransitioning ) { - return; - } - - // make `transition: foo, bar, baz` from style object - // HACK un-comment this when enableTransition can work - // while a transition is happening - // var transitionValues = []; - // for ( var prop in style ) { - // // dash-ify camelCased properties like WebkitTransition - // prop = vendorProperties[ prop ] || prop; - // transitionValues.push( toDashedAll( prop ) ); - // } - // munge number to millisecond, to match stagger - var duration = this.layout.options.transitionDuration; - duration = typeof duration == 'number' ? duration + 'ms' : duration; - // enable transition styles - this.css({ - transitionProperty: transitionProps, - transitionDuration: duration, - transitionDelay: this.staggerDelay || 0 - }); - // listen for transition end event - this.element.addEventListener( transitionEndEvent, this, false ); -}; - -// ----- events ----- // - -proto.onwebkitTransitionEnd = function( event ) { - this.ontransitionend( event ); -}; - -proto.onotransitionend = function( event ) { - this.ontransitionend( event ); -}; - -// properties that I munge to make my life easier -var dashedVendorProperties = { - '-webkit-transform': 'transform' -}; - -proto.ontransitionend = function( event ) { - // disregard bubbled events from children - if ( event.target !== this.element ) { - return; - } - var _transition = this._transn; - // get property name of transitioned property, convert to prefix-free - var propertyName = dashedVendorProperties[ event.propertyName ] || event.propertyName; - - // remove property that has completed transitioning - delete _transition.ingProperties[ propertyName ]; - // check if any properties are still transitioning - if ( isEmptyObj( _transition.ingProperties ) ) { - // all properties have completed transitioning - this.disableTransition(); - } - // clean style - if ( propertyName in _transition.clean ) { - // clean up style - this.element.style[ event.propertyName ] = ''; - delete _transition.clean[ propertyName ]; - } - // trigger onTransitionEnd callback - if ( propertyName in _transition.onEnd ) { - var onTransitionEnd = _transition.onEnd[ propertyName ]; - onTransitionEnd.call( this ); - delete _transition.onEnd[ propertyName ]; - } - - this.emitEvent( 'transitionEnd', [ this ] ); -}; - -proto.disableTransition = function() { - this.removeTransitionStyles(); - this.element.removeEventListener( transitionEndEvent, this, false ); - this.isTransitioning = false; -}; - -/** - * removes style property from element - * @param {Object} style -**/ -proto._removeStyles = function( style ) { - // clean up transition styles - var cleanStyle = {}; - for ( var prop in style ) { - cleanStyle[ prop ] = ''; - } - this.css( cleanStyle ); -}; - -var cleanTransitionStyle = { - transitionProperty: '', - transitionDuration: '', - transitionDelay: '' -}; - -proto.removeTransitionStyles = function() { - // remove transition - this.css( cleanTransitionStyle ); -}; - -// ----- stagger ----- // - -proto.stagger = function( delay ) { - delay = isNaN( delay ) ? 0 : delay; - this.staggerDelay = delay + 'ms'; -}; - -// ----- show/hide/remove ----- // - -// remove element from DOM -proto.removeElem = function() { - this.element.parentNode.removeChild( this.element ); - // remove display: none - this.css({ display: '' }); - this.emitEvent( 'remove', [ this ] ); -}; - -proto.remove = function() { - // just remove element if no transition support or no transition - if ( !transitionProperty || !parseFloat( this.layout.options.transitionDuration ) ) { - this.removeElem(); - return; - } - - // start transition - this.once( 'transitionEnd', function() { - this.removeElem(); - }); - this.hide(); -}; - -proto.reveal = function() { - delete this.isHidden; - // remove display: none - this.css({ display: '' }); - - var options = this.layout.options; - - var onTransitionEnd = {}; - var transitionEndProperty = this.getHideRevealTransitionEndProperty('visibleStyle'); - onTransitionEnd[ transitionEndProperty ] = this.onRevealTransitionEnd; - - this.transition({ - from: options.hiddenStyle, - to: options.visibleStyle, - isCleaning: true, - onTransitionEnd: onTransitionEnd - }); -}; - -proto.onRevealTransitionEnd = function() { - // check if still visible - // during transition, item may have been hidden - if ( !this.isHidden ) { - this.emitEvent('reveal'); - } -}; - -/** - * get style property use for hide/reveal transition end - * @param {String} styleProperty - hiddenStyle/visibleStyle - * @returns {String} - */ -proto.getHideRevealTransitionEndProperty = function( styleProperty ) { - var optionStyle = this.layout.options[ styleProperty ]; - // use opacity - if ( optionStyle.opacity ) { - return 'opacity'; - } - // get first property - for ( var prop in optionStyle ) { - return prop; - } -}; - -proto.hide = function() { - // set flag - this.isHidden = true; - // remove display: none - this.css({ display: '' }); - - var options = this.layout.options; - - var onTransitionEnd = {}; - var transitionEndProperty = this.getHideRevealTransitionEndProperty('hiddenStyle'); - onTransitionEnd[ transitionEndProperty ] = this.onHideTransitionEnd; - - this.transition({ - from: options.visibleStyle, - to: options.hiddenStyle, - // keep hidden stuff hidden - isCleaning: true, - onTransitionEnd: onTransitionEnd - }); -}; - -proto.onHideTransitionEnd = function() { - // check if still hidden - // during transition, item may have been un-hidden - if ( this.isHidden ) { - this.css({ display: 'none' }); - this.emitEvent('hide'); - } -}; - -proto.destroy = function() { - this.css({ - position: '', - left: '', - right: '', - top: '', - bottom: '', - transition: '', - transform: '' - }); -}; - -return Item; - -})); - -/*! - * Outlayer v2.1.0 - * the brains and guts of a layout library - * MIT license - */ - -( function( window, factory ) { - 'use strict'; - // universal module definition - /* jshint strict: false */ /* globals define, module, require */ - if ( typeof define == 'function' && define.amd ) { - // AMD - RequireJS - define( 'outlayer/outlayer',[ - 'ev-emitter/ev-emitter', - 'get-size/get-size', - 'fizzy-ui-utils/utils', - './item' - ], - function( EvEmitter, getSize, utils, Item ) { - return factory( window, EvEmitter, getSize, utils, Item); - } - ); - } else if ( typeof module == 'object' && module.exports ) { - // CommonJS - Browserify, Webpack - module.exports = factory( - window, - require('ev-emitter'), - require('get-size'), - require('fizzy-ui-utils'), - require('./item') - ); - } else { - // browser global - window.Outlayer = factory( - window, - window.EvEmitter, - window.getSize, - window.fizzyUIUtils, - window.Outlayer.Item - ); - } - -}( window, function factory( window, EvEmitter, getSize, utils, Item ) { -'use strict'; - -// ----- vars ----- // - -var console = window.console; -var jQuery = window.jQuery; -var noop = function() {}; - -// -------------------------- Outlayer -------------------------- // - -// globally unique identifiers -var GUID = 0; -// internal store of all Outlayer intances -var instances = {}; - - -/** - * @param {Element, String} element - * @param {Object} options - * @constructor - */ -function Outlayer( element, options ) { - var queryElement = utils.getQueryElement( element ); - if ( !queryElement ) { - if ( console ) { - console.error( 'Bad element for ' + this.constructor.namespace + - ': ' + ( queryElement || element ) ); - } - return; - } - this.element = queryElement; - // add jQuery - if ( jQuery ) { - this.$element = jQuery( this.element ); - } - - // options - this.options = utils.extend( {}, this.constructor.defaults ); - this.option( options ); - - // add id for Outlayer.getFromElement - var id = ++GUID; - this.element.outlayerGUID = id; // expando - instances[ id ] = this; // associate via id - - // kick it off - this._create(); - - var isInitLayout = this._getOption('initLayout'); - if ( isInitLayout ) { - this.layout(); - } -} - -// settings are for internal use only -Outlayer.namespace = 'outlayer'; -Outlayer.Item = Item; - -// default options -Outlayer.defaults = { - containerStyle: { - position: 'relative' - }, - initLayout: true, - originLeft: true, - originTop: true, - resize: true, - resizeContainer: true, - // item options - transitionDuration: '0.4s', - hiddenStyle: { - opacity: 0, - transform: 'scale(0.001)' - }, - visibleStyle: { - opacity: 1, - transform: 'scale(1)' - } -}; - -var proto = Outlayer.prototype; -// inherit EvEmitter -utils.extend( proto, EvEmitter.prototype ); - -/** - * set options - * @param {Object} opts - */ -proto.option = function( opts ) { - utils.extend( this.options, opts ); -}; - -/** - * get backwards compatible option value, check old name - */ -proto._getOption = function( option ) { - var oldOption = this.constructor.compatOptions[ option ]; - return oldOption && this.options[ oldOption ] !== undefined ? - this.options[ oldOption ] : this.options[ option ]; -}; - -Outlayer.compatOptions = { - // currentName: oldName - initLayout: 'isInitLayout', - horizontal: 'isHorizontal', - layoutInstant: 'isLayoutInstant', - originLeft: 'isOriginLeft', - originTop: 'isOriginTop', - resize: 'isResizeBound', - resizeContainer: 'isResizingContainer' -}; - -proto._create = function() { - // get items from children - this.reloadItems(); - // elements that affect layout, but are not laid out - this.stamps = []; - this.stamp( this.options.stamp ); - // set container style - utils.extend( this.element.style, this.options.containerStyle ); - - // bind resize method - var canBindResize = this._getOption('resize'); - if ( canBindResize ) { - this.bindResize(); - } -}; - -// goes through all children again and gets bricks in proper order -proto.reloadItems = function() { - // collection of item elements - this.items = this._itemize( this.element.children ); -}; - - -/** - * turn elements into Outlayer.Items to be used in layout - * @param {Array or NodeList or HTMLElement} elems - * @returns {Array} items - collection of new Outlayer Items - */ -proto._itemize = function( elems ) { - - var itemElems = this._filterFindItemElements( elems ); - var Item = this.constructor.Item; - - // create new Outlayer Items for collection - var items = []; - for ( var i=0; i < itemElems.length; i++ ) { - var elem = itemElems[i]; - var item = new Item( elem, this ); - items.push( item ); - } - - return items; -}; - -/** - * get item elements to be used in layout - * @param {Array or NodeList or HTMLElement} elems - * @returns {Array} items - item elements - */ -proto._filterFindItemElements = function( elems ) { - return utils.filterFindElements( elems, this.options.itemSelector ); -}; - -/** - * getter method for getting item elements - * @returns {Array} elems - collection of item elements - */ -proto.getItemElements = function() { - return this.items.map( function( item ) { - return item.element; - }); -}; - -// ----- init & layout ----- // - -/** - * lays out all items - */ -proto.layout = function() { - this._resetLayout(); - this._manageStamps(); - - // don't animate first layout - var layoutInstant = this._getOption('layoutInstant'); - var isInstant = layoutInstant !== undefined ? - layoutInstant : !this._isLayoutInited; - this.layoutItems( this.items, isInstant ); - - // flag for initalized - this._isLayoutInited = true; -}; - -// _init is alias for layout -proto._init = proto.layout; - -/** - * logic before any new layout - */ -proto._resetLayout = function() { - this.getSize(); -}; - - -proto.getSize = function() { - this.size = getSize( this.element ); -}; - -/** - * get measurement from option, for columnWidth, rowHeight, gutter - * if option is String -> get element from selector string, & get size of element - * if option is Element -> get size of element - * else use option as a number - * - * @param {String} measurement - * @param {String} size - width or height - * @private - */ -proto._getMeasurement = function( measurement, size ) { - var option = this.options[ measurement ]; - var elem; - if ( !option ) { - // default to 0 - this[ measurement ] = 0; - } else { - // use option as an element - if ( typeof option == 'string' ) { - elem = this.element.querySelector( option ); - } else if ( option instanceof HTMLElement ) { - elem = option; - } - // use size of element, if element - this[ measurement ] = elem ? getSize( elem )[ size ] : option; - } -}; - -/** - * layout a collection of item elements - * @api public - */ -proto.layoutItems = function( items, isInstant ) { - items = this._getItemsForLayout( items ); - - this._layoutItems( items, isInstant ); - - this._postLayout(); -}; - -/** - * get the items to be laid out - * you may want to skip over some items - * @param {Array} items - * @returns {Array} items - */ -proto._getItemsForLayout = function( items ) { - return items.filter( function( item ) { - return !item.isIgnored; - }); -}; - -/** - * layout items - * @param {Array} items - * @param {Boolean} isInstant - */ -proto._layoutItems = function( items, isInstant ) { - this._emitCompleteOnItems( 'layout', items ); - - if ( !items || !items.length ) { - // no items, emit event with empty array - return; - } - - var queue = []; - - items.forEach( function( item ) { - // get x/y object from method - var position = this._getItemLayoutPosition( item ); - // enqueue - position.item = item; - position.isInstant = isInstant || item.isLayoutInstant; - queue.push( position ); - }, this ); - - this._processLayoutQueue( queue ); -}; - -/** - * get item layout position - * @param {Outlayer.Item} item - * @returns {Object} x and y position - */ -proto._getItemLayoutPosition = function( /* item */ ) { - return { - x: 0, - y: 0 - }; -}; - -/** - * iterate over array and position each item - * Reason being - separating this logic prevents 'layout invalidation' - * thx @paul_irish - * @param {Array} queue - */ -proto._processLayoutQueue = function( queue ) { - this.updateStagger(); - queue.forEach( function( obj, i ) { - this._positionItem( obj.item, obj.x, obj.y, obj.isInstant, i ); - }, this ); -}; - -// set stagger from option in milliseconds number -proto.updateStagger = function() { - var stagger = this.options.stagger; - if ( stagger === null || stagger === undefined ) { - this.stagger = 0; - return; - } - this.stagger = getMilliseconds( stagger ); - return this.stagger; -}; - -/** - * Sets position of item in DOM - * @param {Outlayer.Item} item - * @param {Number} x - horizontal position - * @param {Number} y - vertical position - * @param {Boolean} isInstant - disables transitions - */ -proto._positionItem = function( item, x, y, isInstant, i ) { - if ( isInstant ) { - // if not transition, just set CSS - item.goTo( x, y ); - } else { - item.stagger( i * this.stagger ); - item.moveTo( x, y ); - } -}; - -/** - * Any logic you want to do after each layout, - * i.e. size the container - */ -proto._postLayout = function() { - this.resizeContainer(); -}; - -proto.resizeContainer = function() { - var isResizingContainer = this._getOption('resizeContainer'); - if ( !isResizingContainer ) { - return; - } - var size = this._getContainerSize(); - if ( size ) { - this._setContainerMeasure( size.width, true ); - this._setContainerMeasure( size.height, false ); - } -}; - -/** - * Sets width or height of container if returned - * @returns {Object} size - * @param {Number} width - * @param {Number} height - */ -proto._getContainerSize = noop; - -/** - * @param {Number} measure - size of width or height - * @param {Boolean} isWidth - */ -proto._setContainerMeasure = function( measure, isWidth ) { - if ( measure === undefined ) { - return; - } - - var elemSize = this.size; - // add padding and border width if border box - if ( elemSize.isBorderBox ) { - measure += isWidth ? elemSize.paddingLeft + elemSize.paddingRight + - elemSize.borderLeftWidth + elemSize.borderRightWidth : - elemSize.paddingBottom + elemSize.paddingTop + - elemSize.borderTopWidth + elemSize.borderBottomWidth; - } - - measure = Math.max( measure, 0 ); - this.element.style[ isWidth ? 'width' : 'height' ] = measure + 'px'; -}; - -/** - * emit eventComplete on a collection of items events - * @param {String} eventName - * @param {Array} items - Outlayer.Items - */ -proto._emitCompleteOnItems = function( eventName, items ) { - var _this = this; - function onComplete() { - _this.dispatchEvent( eventName + 'Complete', null, [ items ] ); - } - - var count = items.length; - if ( !items || !count ) { - onComplete(); - return; - } - - var doneCount = 0; - function tick() { - doneCount++; - if ( doneCount == count ) { - onComplete(); - } - } - - // bind callback - items.forEach( function( item ) { - item.once( eventName, tick ); - }); -}; - -/** - * emits events via EvEmitter and jQuery events - * @param {String} type - name of event - * @param {Event} event - original event - * @param {Array} args - extra arguments - */ -proto.dispatchEvent = function( type, event, args ) { - // add original event to arguments - var emitArgs = event ? [ event ].concat( args ) : args; - this.emitEvent( type, emitArgs ); - - if ( jQuery ) { - // set this.$element - this.$element = this.$element || jQuery( this.element ); - if ( event ) { - // create jQuery event - var $event = jQuery.Event( event ); - $event.type = type; - this.$element.trigger( $event, args ); - } else { - // just trigger with type if no event available - this.$element.trigger( type, args ); - } - } -}; - -// -------------------------- ignore & stamps -------------------------- // - - -/** - * keep item in collection, but do not lay it out - * ignored items do not get skipped in layout - * @param {Element} elem - */ -proto.ignore = function( elem ) { - var item = this.getItem( elem ); - if ( item ) { - item.isIgnored = true; - } -}; - -/** - * return item to layout collection - * @param {Element} elem - */ -proto.unignore = function( elem ) { - var item = this.getItem( elem ); - if ( item ) { - delete item.isIgnored; - } -}; - -/** - * adds elements to stamps - * @param {NodeList, Array, Element, or String} elems - */ -proto.stamp = function( elems ) { - elems = this._find( elems ); - if ( !elems ) { - return; - } - - this.stamps = this.stamps.concat( elems ); - // ignore - elems.forEach( this.ignore, this ); -}; - -/** - * removes elements to stamps - * @param {NodeList, Array, or Element} elems - */ -proto.unstamp = function( elems ) { - elems = this._find( elems ); - if ( !elems ){ - return; - } - - elems.forEach( function( elem ) { - // filter out removed stamp elements - utils.removeFrom( this.stamps, elem ); - this.unignore( elem ); - }, this ); -}; - -/** - * finds child elements - * @param {NodeList, Array, Element, or String} elems - * @returns {Array} elems - */ -proto._find = function( elems ) { - if ( !elems ) { - return; - } - // if string, use argument as selector string - if ( typeof elems == 'string' ) { - elems = this.element.querySelectorAll( elems ); - } - elems = utils.makeArray( elems ); - return elems; -}; - -proto._manageStamps = function() { - if ( !this.stamps || !this.stamps.length ) { - return; - } - - this._getBoundingRect(); - - this.stamps.forEach( this._manageStamp, this ); -}; - -// update boundingLeft / Top -proto._getBoundingRect = function() { - // get bounding rect for container element - var boundingRect = this.element.getBoundingClientRect(); - var size = this.size; - this._boundingRect = { - left: boundingRect.left + size.paddingLeft + size.borderLeftWidth, - top: boundingRect.top + size.paddingTop + size.borderTopWidth, - right: boundingRect.right - ( size.paddingRight + size.borderRightWidth ), - bottom: boundingRect.bottom - ( size.paddingBottom + size.borderBottomWidth ) - }; -}; - -/** - * @param {Element} stamp -**/ -proto._manageStamp = noop; - -/** - * get x/y position of element relative to container element - * @param {Element} elem - * @returns {Object} offset - has left, top, right, bottom - */ -proto._getElementOffset = function( elem ) { - var boundingRect = elem.getBoundingClientRect(); - var thisRect = this._boundingRect; - var size = getSize( elem ); - var offset = { - left: boundingRect.left - thisRect.left - size.marginLeft, - top: boundingRect.top - thisRect.top - size.marginTop, - right: thisRect.right - boundingRect.right - size.marginRight, - bottom: thisRect.bottom - boundingRect.bottom - size.marginBottom - }; - return offset; -}; - -// -------------------------- resize -------------------------- // - -// enable event handlers for listeners -// i.e. resize -> onresize -proto.handleEvent = utils.handleEvent; - -/** - * Bind layout to window resizing - */ -proto.bindResize = function() { - window.addEventListener( 'resize', this ); - this.isResizeBound = true; -}; - -/** - * Unbind layout to window resizing - */ -proto.unbindResize = function() { - window.removeEventListener( 'resize', this ); - this.isResizeBound = false; -}; - -proto.onresize = function() { - this.resize(); -}; - -utils.debounceMethod( Outlayer, 'onresize', 100 ); - -proto.resize = function() { - // don't trigger if size did not change - // or if resize was unbound. See #9 - if ( !this.isResizeBound || !this.needsResizeLayout() ) { - return; - } - - this.layout(); -}; - -/** - * check if layout is needed post layout - * @returns Boolean - */ -proto.needsResizeLayout = function() { - var size = getSize( this.element ); - // check that this.size and size are there - // IE8 triggers resize on body size change, so they might not be - var hasSizes = this.size && size; - return hasSizes && size.innerWidth !== this.size.innerWidth; -}; - -// -------------------------- methods -------------------------- // - -/** - * add items to Outlayer instance - * @param {Array or NodeList or Element} elems - * @returns {Array} items - Outlayer.Items -**/ -proto.addItems = function( elems ) { - var items = this._itemize( elems ); - // add items to collection - if ( items.length ) { - this.items = this.items.concat( items ); - } - return items; -}; - -/** - * Layout newly-appended item elements - * @param {Array or NodeList or Element} elems - */ -proto.appended = function( elems ) { - var items = this.addItems( elems ); - if ( !items.length ) { - return; - } - // layout and reveal just the new items - this.layoutItems( items, true ); - this.reveal( items ); -}; - -/** - * Layout prepended elements - * @param {Array or NodeList or Element} elems - */ -proto.prepended = function( elems ) { - var items = this._itemize( elems ); - if ( !items.length ) { - return; - } - // add items to beginning of collection - var previousItems = this.items.slice(0); - this.items = items.concat( previousItems ); - // start new layout - this._resetLayout(); - this._manageStamps(); - // layout new stuff without transition - this.layoutItems( items, true ); - this.reveal( items ); - // layout previous items - this.layoutItems( previousItems ); -}; - -/** - * reveal a collection of items - * @param {Array of Outlayer.Items} items - */ -proto.reveal = function( items ) { - this._emitCompleteOnItems( 'reveal', items ); - if ( !items || !items.length ) { - return; - } - var stagger = this.updateStagger(); - items.forEach( function( item, i ) { - item.stagger( i * stagger ); - item.reveal(); - }); -}; - -/** - * hide a collection of items - * @param {Array of Outlayer.Items} items - */ -proto.hide = function( items ) { - this._emitCompleteOnItems( 'hide', items ); - if ( !items || !items.length ) { - return; - } - var stagger = this.updateStagger(); - items.forEach( function( item, i ) { - item.stagger( i * stagger ); - item.hide(); - }); -}; - -/** - * reveal item elements - * @param {Array}, {Element}, {NodeList} items - */ -proto.revealItemElements = function( elems ) { - var items = this.getItems( elems ); - this.reveal( items ); -}; - -/** - * hide item elements - * @param {Array}, {Element}, {NodeList} items - */ -proto.hideItemElements = function( elems ) { - var items = this.getItems( elems ); - this.hide( items ); -}; - -/** - * get Outlayer.Item, given an Element - * @param {Element} elem - * @param {Function} callback - * @returns {Outlayer.Item} item - */ -proto.getItem = function( elem ) { - // loop through items to get the one that matches - for ( var i=0; i < this.items.length; i++ ) { - var item = this.items[i]; - if ( item.element == elem ) { - // return item - return item; - } - } -}; - -/** - * get collection of Outlayer.Items, given Elements - * @param {Array} elems - * @returns {Array} items - Outlayer.Items - */ -proto.getItems = function( elems ) { - elems = utils.makeArray( elems ); - var items = []; - elems.forEach( function( elem ) { - var item = this.getItem( elem ); - if ( item ) { - items.push( item ); - } - }, this ); - - return items; -}; - -/** - * remove element(s) from instance and DOM - * @param {Array or NodeList or Element} elems - */ -proto.remove = function( elems ) { - var removeItems = this.getItems( elems ); - - this._emitCompleteOnItems( 'remove', removeItems ); - - // bail if no items to remove - if ( !removeItems || !removeItems.length ) { - return; - } - - removeItems.forEach( function( item ) { - item.remove(); - // remove item from collection - utils.removeFrom( this.items, item ); - }, this ); -}; - -// ----- destroy ----- // - -// remove and disable Outlayer instance -proto.destroy = function() { - // clean up dynamic styles - var style = this.element.style; - style.height = ''; - style.position = ''; - style.width = ''; - // destroy items - this.items.forEach( function( item ) { - item.destroy(); - }); - - this.unbindResize(); - - var id = this.element.outlayerGUID; - delete instances[ id ]; // remove reference to instance by id - delete this.element.outlayerGUID; - // remove data for jQuery - if ( jQuery ) { - jQuery.removeData( this.element, this.constructor.namespace ); - } - -}; - -// -------------------------- data -------------------------- // - -/** - * get Outlayer instance from element - * @param {Element} elem - * @returns {Outlayer} - */ -Outlayer.data = function( elem ) { - elem = utils.getQueryElement( elem ); - var id = elem && elem.outlayerGUID; - return id && instances[ id ]; -}; - - -// -------------------------- create Outlayer class -------------------------- // - -/** - * create a layout class - * @param {String} namespace - */ -Outlayer.create = function( namespace, options ) { - // sub-class Outlayer - var Layout = subclass( Outlayer ); - // apply new options and compatOptions - Layout.defaults = utils.extend( {}, Outlayer.defaults ); - utils.extend( Layout.defaults, options ); - Layout.compatOptions = utils.extend( {}, Outlayer.compatOptions ); - - Layout.namespace = namespace; - - Layout.data = Outlayer.data; - - // sub-class Item - Layout.Item = subclass( Item ); - - // -------------------------- declarative -------------------------- // - - utils.htmlInit( Layout, namespace ); - - // -------------------------- jQuery bridge -------------------------- // - - // make into jQuery plugin - if ( jQuery && jQuery.bridget ) { - jQuery.bridget( namespace, Layout ); - } - - return Layout; -}; - -function subclass( Parent ) { - function SubClass() { - Parent.apply( this, arguments ); - } - - SubClass.prototype = Object.create( Parent.prototype ); - SubClass.prototype.constructor = SubClass; - - return SubClass; -} - -// ----- helpers ----- // - -// how many milliseconds are in each unit -var msUnits = { - ms: 1, - s: 1000 -}; - -// munge time-like parameter into millisecond number -// '0.4s' -> 40 -function getMilliseconds( time ) { - if ( typeof time == 'number' ) { - return time; - } - var matches = time.match( /(^\d*\.?\d*)(\w*)/ ); - var num = matches && matches[1]; - var unit = matches && matches[2]; - if ( !num.length ) { - return 0; - } - num = parseFloat( num ); - var mult = msUnits[ unit ] || 1; - return num * mult; -} - -// ----- fin ----- // - -// back in global -Outlayer.Item = Item; - -return Outlayer; - -})); - -/*! - * Masonry v4.1.1 - * Cascading grid layout library - * http://masonry.desandro.com - * MIT License - * by David DeSandro - */ - -( function( window, factory ) { - // universal module definition - /* jshint strict: false */ /*globals define, module, require */ - if ( typeof define == 'function' && define.amd ) { - // AMD - define( [ - 'outlayer/outlayer', - 'get-size/get-size' - ], - factory ); - } else if ( typeof module == 'object' && module.exports ) { - // CommonJS - module.exports = factory( - require('outlayer'), - require('get-size') - ); - } else { - // browser global - window.Masonry = factory( - window.Outlayer, - window.getSize - ); - } - -}( window, function factory( Outlayer, getSize ) { - - - -// -------------------------- masonryDefinition -------------------------- // - - // create an Outlayer layout class - var Masonry = Outlayer.create('masonry'); - // isFitWidth -> fitWidth - Masonry.compatOptions.fitWidth = 'isFitWidth'; - - Masonry.prototype._resetLayout = function() { - this.getSize(); - this._getMeasurement( 'columnWidth', 'outerWidth' ); - this._getMeasurement( 'gutter', 'outerWidth' ); - this.measureColumns(); - - // reset column Y - this.colYs = []; - for ( var i=0; i < this.cols; i++ ) { - this.colYs.push( 0 ); - } - - this.maxY = 0; - }; - - Masonry.prototype.measureColumns = function() { - this.getContainerWidth(); - // if columnWidth is 0, default to outerWidth of first item - if ( !this.columnWidth ) { - var firstItem = this.items[0]; - var firstItemElem = firstItem && firstItem.element; - // columnWidth fall back to item of first element - this.columnWidth = firstItemElem && getSize( firstItemElem ).outerWidth || - // if first elem has no width, default to size of container - this.containerWidth; - } - - var columnWidth = this.columnWidth += this.gutter; - - // calculate columns - var containerWidth = this.containerWidth + this.gutter; - var cols = containerWidth / columnWidth; - // fix rounding errors, typically with gutters - var excess = columnWidth - containerWidth % columnWidth; - // if overshoot is less than a pixel, round up, otherwise floor it - var mathMethod = excess && excess < 1 ? 'round' : 'floor'; - cols = Math[ mathMethod ]( cols ); - this.cols = Math.max( cols, 1 ); - }; - - Masonry.prototype.getContainerWidth = function() { - // container is parent if fit width - var isFitWidth = this._getOption('fitWidth'); - var container = isFitWidth ? this.element.parentNode : this.element; - // check that this.size and size are there - // IE8 triggers resize on body size change, so they might not be - var size = getSize( container ); - this.containerWidth = size && size.innerWidth; - }; - - Masonry.prototype._getItemLayoutPosition = function( item ) { - item.getSize(); - // how many columns does this brick span - var remainder = item.size.outerWidth % this.columnWidth; - var mathMethod = remainder && remainder < 1 ? 'round' : 'ceil'; - // round if off by 1 pixel, otherwise use ceil - var colSpan = Math[ mathMethod ]( item.size.outerWidth / this.columnWidth ); - colSpan = Math.min( colSpan, this.cols ); - - var colGroup = this._getColGroup( colSpan ); - // get the minimum Y value from the columns - var minimumY = Math.min.apply( Math, colGroup ); - var shortColIndex = colGroup.indexOf( minimumY ); - - // position the brick - var position = { - x: this.columnWidth * shortColIndex, - y: minimumY - }; - - // apply setHeight to necessary columns - var setHeight = minimumY + item.size.outerHeight; - var setSpan = this.cols + 1 - colGroup.length; - for ( var i = 0; i < setSpan; i++ ) { - this.colYs[ shortColIndex + i ] = setHeight; - } - - return position; - }; - - /** - * @param {Number} colSpan - number of columns the element spans - * @returns {Array} colGroup - */ - Masonry.prototype._getColGroup = function( colSpan ) { - if ( colSpan < 2 ) { - // if brick spans only one column, use all the column Ys - return this.colYs; - } - - var colGroup = []; - // how many different places could this brick fit horizontally - var groupCount = this.cols + 1 - colSpan; - // for each group potential horizontal position - for ( var i = 0; i < groupCount; i++ ) { - // make an array of colY values for that one group - var groupColYs = this.colYs.slice( i, i + colSpan ); - // and get the max value of the array - colGroup[i] = Math.max.apply( Math, groupColYs ); - } - return colGroup; - }; - - Masonry.prototype._manageStamp = function( stamp ) { - var stampSize = getSize( stamp ); - var offset = this._getElementOffset( stamp ); - // get the columns that this stamp affects - var isOriginLeft = this._getOption('originLeft'); - var firstX = isOriginLeft ? offset.left : offset.right; - var lastX = firstX + stampSize.outerWidth; - var firstCol = Math.floor( firstX / this.columnWidth ); - firstCol = Math.max( 0, firstCol ); - var lastCol = Math.floor( lastX / this.columnWidth ); - // lastCol should not go over if multiple of columnWidth #425 - lastCol -= lastX % this.columnWidth ? 0 : 1; - lastCol = Math.min( this.cols - 1, lastCol ); - // set colYs to bottom of the stamp - - var isOriginTop = this._getOption('originTop'); - var stampMaxY = ( isOriginTop ? offset.top : offset.bottom ) + - stampSize.outerHeight; - for ( var i = firstCol; i <= lastCol; i++ ) { - this.colYs[i] = Math.max( stampMaxY, this.colYs[i] ); - } - }; - - Masonry.prototype._getContainerSize = function() { - this.maxY = Math.max.apply( Math, this.colYs ); - var size = { - height: this.maxY - }; - - if ( this._getOption('fitWidth') ) { - size.width = this._getContainerFitWidth(); - } - - return size; - }; - - Masonry.prototype._getContainerFitWidth = function() { - var unusedCols = 0; - // count unused columns - var i = this.cols; - while ( --i ) { - if ( this.colYs[i] !== 0 ) { - break; - } - unusedCols++; - } - // fit container to columns that have been used - return ( this.cols - unusedCols ) * this.columnWidth - this.gutter; - }; - - Masonry.prototype.needsResizeLayout = function() { - var previousWidth = this.containerWidth; - this.getContainerWidth(); - return previousWidth != this.containerWidth; - }; - - return Masonry; - -})); - -- cgit v1.2.1 From e85cd9eedef18e2109ca8578380bcc91578b14f3 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 1 Feb 2017 18:42:47 +0000 Subject: Removed duplicated test --- spec/javascripts/lib/utils/text_utility_spec.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/javascripts/lib/utils/text_utility_spec.js.es6 b/spec/javascripts/lib/utils/text_utility_spec.js.es6 index 329e18f405a..f7d627ceac5 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js.es6 +++ b/spec/javascripts/lib/utils/text_utility_spec.js.es6 @@ -27,7 +27,7 @@ expect(gl.text.pluralize('test', 2)).toBe('tests'); }); - it('returns pluralized', () => { + it('returns pluralized when count is 0', () => { expect(gl.text.pluralize('test', 0)).toBe('tests'); }); -- cgit v1.2.1 From 8c5e50e12b72ca4a80dab02534739bde6e5a9eb5 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 3 Feb 2017 13:17:11 +0000 Subject: Fixed remove btn error after creating new issue in list --- app/assets/javascripts/boards/components/board_new_issue.js.es6 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6 index 2386d3a613c..b5c14a198ba 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.js.es6 +++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6 @@ -37,6 +37,7 @@ $(this.$refs.submitButton).enable(); Store.detail.issue = issue; + Store.detail.list = this.list; }) .catch(() => { // Need this because our jQuery very kindly disables buttons on ALL form submissions -- cgit v1.2.1 From fee064be01e6e37076b9af4028ade12a2ce0180a Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 3 Feb 2017 14:48:31 +0000 Subject: Fixed modal lists dropdown not updating when list is deleted --- app/assets/javascripts/boards/boards_bundle.js.es6 | 4 ---- .../boards/components/modal/lists_dropdown.js.es6 | 5 ++++- .../javascripts/boards/stores/modal_store.js.es6 | 2 +- spec/features/boards/add_issues_modal_spec.rb | 21 +++++++++++++++++++++ 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 4ac91786762..529ea9aec5b 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -71,10 +71,6 @@ $(() => { Store.addBlankState(); this.loading = false; - - if (this.state.lists.length > 0) { - ModalStore.store.selectedList = this.state.lists[0]; - } }); } }); diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 index 96f12da3753..3c05120a2da 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js.es6 @@ -11,9 +11,12 @@ }, computed: { selected() { - return this.modal.selectedList; + return this.modal.selectedList || this.state.lists[0]; }, }, + destroyed() { + this.modal.selectedList = null; + }, template: ` <div class="dropdown inline"> <button diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index fa46b6135e0..73518b42b84 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -11,7 +11,7 @@ selectedIssues: [], showAddIssuesModal: false, activeTab: 'all', - selectedList: {}, + selectedList: null, searchTerm: '', loading: false, loadingNewPage: false, diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index b953480cec2..2875fc1e533 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -76,6 +76,27 @@ describe 'Issue Boards add issue modal', :feature, :js do end end + context 'list dropdown' do + it 'resets after deleting list' do + page.within('.add-issues-modal') do + expect(find('.add-issues-footer')).to have_button(planning.title) + + click_button 'Cancel' + end + + first('.board-delete').click + + click_button('Add issues') + + wait_for_vue_resource + + page.within('.add-issues-modal') do + expect(find('.add-issues-footer')).not_to have_button(planning.title) + expect(find('.add-issues-footer')).to have_button(label.title) + end + end + end + context 'search' do it 'returns issues' do page.within('.add-issues-modal') do -- cgit v1.2.1 From cdd2346d45f31482371520748289fcab4c423297 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira <oswaldo@gitlab.com> Date: Fri, 3 Feb 2017 13:11:55 -0200 Subject: Adjust V3 projects typo on spec --- spec/requests/api/v3/projects_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index c3f53d0da37..a495122bba7 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe API::V3::Projects, v3_api: true do +describe API::V3::Projects, api: true do include ApiHelpers include Gitlab::CurrentSettings -- cgit v1.2.1 From ea43f58689bd5ee26a7672889913c2bb8a30d842 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 3 Feb 2017 15:14:20 +0000 Subject: Only load pipelines vue component when there are pipelines. Move Empty state to vue component --- .../vue_pipelines_index/pipelines.js.es6 | 18 ++++++--- .../javascripts/vue_pipelines_index/store.js.es6 | 3 +- app/views/projects/merge_requests/_show.html.haml | 3 +- app/views/projects/pipelines/index.html.haml | 46 ++++++++++------------ 4 files changed, 37 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index c1daf816060..ac2fe99af1c 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -40,20 +40,26 @@ }, template: ` <div> - <div class="pipelines realtime-loading" v-if='pipelines.length < 1'> + <div class="pipelines realtime-loading" v-if='pageRequest'> <i class="fa fa-spinner fa-spin"></i> </div> - <div class="table-holder" v-if='pipelines.length'> + + <div class="blank-state blank-state-no-icon" + v-if="!pageRequest && pipelines.length === 0"> + <h2 class="blank-state-title js-blank-state-title"> + No pipelines to show + </h2> + </div> + + <div class="table-holder" v-if='!pageRequest && pipelines.length'> <pipelines-table-component :pipelines='pipelines' :svgs='svgs'> </pipelines-table-component> </div> - <div class="pipelines realtime-loading" v-if='pageRequest'> - <i class="fa fa-spinner fa-spin"></i> - </div> + <gl-pagination - v-if='pageInfo.total > pageInfo.perPage' + v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage' :pagenum='pagenum' :change='change' :count='count.all' diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6 index 0c4a3b77153..4d0e2ccaf87 100644 --- a/app/assets/javascripts/vue_pipelines_index/store.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6 @@ -20,6 +20,7 @@ gl.PipelineStore = class { fetchDataLoop(Vue, pageNum, url, apiScope) { + this.pageRequest = true; const updatePipelineNums = (count) => { const { all } = count; const running = count.running_or_pending; @@ -41,7 +42,7 @@ this.pageRequest = false; }, () => { this.pageRequest = false; - return new Flash('Something went wrong on our end.'); + return new Flash('An error occurred while fetching the pipelines, please reload the page again.'); }); goFetch(); diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index f131836058b..3e554063d8b 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -94,7 +94,8 @@ #commits.commits.tab-pane -# This tab is always loaded via AJAX #pipelines.pipelines.tab-pane - = render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) + - if @pipelines.any? + = render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) #diffs.diffs.tab-pane -# This tab is always loaded via AJAX diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 96f625bc924..58e76afa09a 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -36,31 +36,27 @@ = link_to ci_lint_path, class: 'btn btn-default' do %span CI Lint .content-list.pipelines{ data: { url: namespace_project_pipelines_path(@project.namespace, @project, format: :json) } } - - if @pipelines.blank? - %div - .nothing-here-block No pipelines to show - - else - .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"), - "icon_status_canceled" => custom_icon("icon_status_canceled"), - "icon_status_running" => custom_icon("icon_status_running"), - "icon_status_skipped" => custom_icon("icon_status_skipped"), - "icon_status_created" => custom_icon("icon_status_created"), - "icon_status_pending" => custom_icon("icon_status_pending"), - "icon_status_success" => custom_icon("icon_status_success"), - "icon_status_failed" => custom_icon("icon_status_failed"), - "icon_status_warning" => custom_icon("icon_status_warning"), - "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), - "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), - "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), - "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), - "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), - "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), - "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), - "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), - "icon_play" => custom_icon("icon_play"), - "icon_timer" => custom_icon("icon_timer"), - "icon_status_manual" => custom_icon("icon_status_manual"), - } } + .pipeline-svgs{ "data" => {"commit_icon_svg" => custom_icon("icon_commit"), + "icon_status_canceled" => custom_icon("icon_status_canceled"), + "icon_status_running" => custom_icon("icon_status_running"), + "icon_status_skipped" => custom_icon("icon_status_skipped"), + "icon_status_created" => custom_icon("icon_status_created"), + "icon_status_pending" => custom_icon("icon_status_pending"), + "icon_status_success" => custom_icon("icon_status_success"), + "icon_status_failed" => custom_icon("icon_status_failed"), + "icon_status_warning" => custom_icon("icon_status_warning"), + "stage_icon_status_canceled" => custom_icon("icon_status_canceled_borderless"), + "stage_icon_status_running" => custom_icon("icon_status_running_borderless"), + "stage_icon_status_skipped" => custom_icon("icon_status_skipped_borderless"), + "stage_icon_status_created" => custom_icon("icon_status_created_borderless"), + "stage_icon_status_pending" => custom_icon("icon_status_pending_borderless"), + "stage_icon_status_success" => custom_icon("icon_status_success_borderless"), + "stage_icon_status_failed" => custom_icon("icon_status_failed_borderless"), + "stage_icon_status_warning" => custom_icon("icon_status_warning_borderless"), + "icon_play" => custom_icon("icon_play"), + "icon_timer" => custom_icon("icon_timer"), + "icon_status_manual" => custom_icon("icon_status_manual"), + } } .vue-pipelines-index -- cgit v1.2.1 From c9d307d908972d7336be2738d075e54c689709ad Mon Sep 17 00:00:00 2001 From: Rick Gudmundson <rickg421@gmail.com> Date: Fri, 3 Feb 2017 15:26:45 +0000 Subject: Rename example from file_name to file_path Applied in example response for create, update, and delete operations Fixes #27643 --- doc/api/repository_files.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md index 8a6baed5987..73dde599b7e 100644 --- a/doc/api/repository_files.md +++ b/doc/api/repository_files.md @@ -53,7 +53,7 @@ Example response: ```json { - "file_name": "app/project.rb", + "file_path": "app/project.rb", "branch_name": "master" } ``` @@ -82,7 +82,7 @@ Example response: ```json { - "file_name": "app/project.rb", + "file_path": "app/project.rb", "branch_name": "master" } ``` @@ -120,7 +120,7 @@ Example response: ```json { - "file_name": "app/project.rb", + "file_path": "app/project.rb", "branch_name": "master" } ``` -- cgit v1.2.1 From aaf382d52b87ba13f0cf904e076d9e3eac8d77d9 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Fri, 3 Feb 2017 23:21:10 +0800 Subject: Skip or retain project while deleting the project: * Skip Ci::Build#update_project_statistics whenever there's no project (i.e. we're deleting the project!) * Retain the unscoped_project before deleting the build, so that we could use the data to delete the artifacts. Note that carrierwave uses `after_commit` for this, so we need to retain it in the memory. Closes #15005 --- app/models/ci/build.rb | 21 ++++++++++++++++---- .../unreleased/fix-deleting-project-again.yml | 4 ++++ spec/workers/project_destroy_worker_spec.rb | 23 ++++++++++++++-------- 3 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 changelogs/unreleased/fix-deleting-project-again.yml diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index b1f77bf242c..7c21284b99c 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -41,7 +41,7 @@ module Ci before_save :update_artifacts_size, if: :artifacts_file_changed? before_save :ensure_token - before_destroy { project } + before_destroy { unscoped_project } after_create :execute_hooks after_save :update_project_statistics, if: :artifacts_size_changed? @@ -416,16 +416,23 @@ module Ci # This method returns old path to artifacts only if it already exists. # def artifacts_path + # We need the project even if it's soft deleted, because whenever + # we're really deleting the project, we'll also delete the builds, + # and in order to delete the builds, we need to know where to find + # the artifacts, which is depending on the data of the project. + # We need to retain the project in this case. + the_project = project || unscoped_project + old = File.join(created_at.utc.strftime('%Y_%m'), - project.ci_id.to_s, + the_project.ci_id.to_s, id.to_s) old_store = File.join(ArtifactUploader.artifacts_path, old) - return old if project.ci_id && File.directory?(old_store) + return old if the_project.ci_id && File.directory?(old_store) File.join( created_at.utc.strftime('%Y_%m'), - project.id.to_s, + the_project.id.to_s, id.to_s ) end @@ -559,6 +566,10 @@ module Ci self.update(erased_by: user, erased_at: Time.now, artifacts_expire_at: nil) end + def unscoped_project + @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id) + end + def predefined_variables variables = [ { key: 'CI', value: 'true', public: true }, @@ -597,6 +608,8 @@ module Ci end def update_project_statistics + return unless project + ProjectCacheWorker.perform_async(project_id, [], [:build_artifacts_size]) end end diff --git a/changelogs/unreleased/fix-deleting-project-again.yml b/changelogs/unreleased/fix-deleting-project-again.yml new file mode 100644 index 00000000000..e13215f22a7 --- /dev/null +++ b/changelogs/unreleased/fix-deleting-project-again.yml @@ -0,0 +1,4 @@ +--- +title: Fix deleting projects with pipelines and builds +merge_request: 8960 +author: diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb index 1f4c39eb64a..4b1a342930c 100644 --- a/spec/workers/project_destroy_worker_spec.rb +++ b/spec/workers/project_destroy_worker_spec.rb @@ -1,21 +1,28 @@ require 'spec_helper' describe ProjectDestroyWorker do - let(:project) { create(:project) } + let(:project) { create(:project, pending_delete: true) } let(:path) { project.repository.path_to_repo } subject { ProjectDestroyWorker.new } - describe "#perform" do - it "deletes the project" do - subject.perform(project.id, project.owner.id, {}) + describe '#perform' do + context 'with pipelines and builds' do + let!(:pipeline) { create(:ci_pipeline, project: project) } + let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } - expect(Project.all).not_to include(project) - expect(Dir.exist?(path)).to be_falsey + it 'deletes the project along with pipelines and builds' do + subject.perform(project.id, project.owner.id, {}) + + expect(Project.all).not_to include(project) + expect(Ci::Pipeline.all).not_to include(pipeline) + expect(Ci::Build.all).not_to include(build) + expect(Dir.exist?(path)).to be_falsey + end end - it "deletes the project but skips repo deletion" do - subject.perform(project.id, project.owner.id, { "skip_repo" => true }) + it 'deletes the project but skips repo deletion' do + subject.perform(project.id, project.owner.id, { 'skip_repo' => true }) expect(Project.all).not_to include(project) expect(Dir.exist?(path)).to be_truthy -- cgit v1.2.1 From 1868b8af25a6ecf3e893782b3ff750da57dc07c3 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Fri, 3 Feb 2017 23:44:35 +0800 Subject: Move the tests to spec/services/projects/destroy_service_spec.rb --- spec/services/projects/destroy_service_spec.rb | 3 ++- spec/workers/project_destroy_worker_spec.rb | 23 ++++++++--------------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 90771825f5c..38bd2ed773e 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -47,8 +47,9 @@ describe Projects::DestroyService, services: true do it_behaves_like 'deleting the project' end - context 'delete with pipeline' do # which has optimistic locking + context 'delete with pipeline and build' do # which has optimistic locking let!(:pipeline) { create(:ci_pipeline, project: project) } + let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } before do expect(project).to receive(:destroy!).and_call_original diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb index 4b1a342930c..1f4c39eb64a 100644 --- a/spec/workers/project_destroy_worker_spec.rb +++ b/spec/workers/project_destroy_worker_spec.rb @@ -1,28 +1,21 @@ require 'spec_helper' describe ProjectDestroyWorker do - let(:project) { create(:project, pending_delete: true) } + let(:project) { create(:project) } let(:path) { project.repository.path_to_repo } subject { ProjectDestroyWorker.new } - describe '#perform' do - context 'with pipelines and builds' do - let!(:pipeline) { create(:ci_pipeline, project: project) } - let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + describe "#perform" do + it "deletes the project" do + subject.perform(project.id, project.owner.id, {}) - it 'deletes the project along with pipelines and builds' do - subject.perform(project.id, project.owner.id, {}) - - expect(Project.all).not_to include(project) - expect(Ci::Pipeline.all).not_to include(pipeline) - expect(Ci::Build.all).not_to include(build) - expect(Dir.exist?(path)).to be_falsey - end + expect(Project.all).not_to include(project) + expect(Dir.exist?(path)).to be_falsey end - it 'deletes the project but skips repo deletion' do - subject.perform(project.id, project.owner.id, { 'skip_repo' => true }) + it "deletes the project but skips repo deletion" do + subject.perform(project.id, project.owner.id, { "skip_repo" => true }) expect(Project.all).not_to include(project) expect(Dir.exist?(path)).to be_truthy -- cgit v1.2.1 From 3d7ace470b1f4c0f4cf90db6b1f7f1243c112de9 Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira <oswaldo@gitlab.com> Date: Thu, 2 Feb 2017 21:16:23 -0200 Subject: Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index --- app/controllers/dashboard/projects_controller.rb | 4 +--- ...7267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml | 4 ++++ 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index c08eb811532..3ba8c2f8bb9 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -10,10 +10,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.page(params[:page]) - @last_push = current_user.recent_push - respond_to do |format| - format.html + format.html { @last_push = current_user.recent_push } format.atom do event_filter load_events diff --git a/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml b/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml new file mode 100644 index 00000000000..7b307b501f4 --- /dev/null +++ b/changelogs/unreleased/27267-unnecessary-queries-from-projects-dashboard-atom-and-json.yml @@ -0,0 +1,4 @@ +--- +title: Remove unnecessary queries for .atom and .json in Dashboard::ProjectsController#index +merge_request: 8956 +author: -- cgit v1.2.1 From ad59f123f24617834ec705b39e96dd4ff5f8d3bf Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Sat, 4 Feb 2017 00:05:31 +0800 Subject: Test both execute and async_execute --- spec/services/projects/destroy_service_spec.rb | 50 +++++++++++++++----------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 38bd2ed773e..3faa88c00a1 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -9,12 +9,27 @@ describe Projects::DestroyService, services: true do shared_examples 'deleting the project' do it 'deletes the project' do - expect(Project.all).not_to include(project) + expect(Project.unscoped.all).not_to include(project) expect(Dir.exist?(path)).to be_falsey expect(Dir.exist?(remove_path)).to be_falsey end end + shared_examples 'deleting the project with pipeline and build' do + context 'with pipeline and build' do # which has optimistic locking + let!(:pipeline) { create(:ci_pipeline, project: project) } + let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + + before do + perform_enqueued_jobs do + destroy_project(project, user, {}) + end + end + + it_behaves_like 'deleting the project' + end + end + context 'Sidekiq inline' do before do # Run sidekiq immediatly to check that renamed repository will be removed @@ -35,31 +50,24 @@ describe Projects::DestroyService, services: true do it { expect(Dir.exist?(remove_path)).to be_truthy } end - context 'async delete of project with private issue visibility' do - let!(:async) { true } + context 'with async_execute' do + let(:async) { true } - before do - project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE) - # Run sidekiq immediately to check that renamed repository will be removed - Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + context 'async delete of project with private issue visibility' do + before do + project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE) + # Run sidekiq immediately to check that renamed repository will be removed + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + end + + it_behaves_like 'deleting the project' end - it_behaves_like 'deleting the project' + it_behaves_like 'deleting the project with pipeline and build' end - context 'delete with pipeline and build' do # which has optimistic locking - let!(:pipeline) { create(:ci_pipeline, project: project) } - let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } - - before do - expect(project).to receive(:destroy!).and_call_original - - perform_enqueued_jobs do - destroy_project(project, user, {}) - end - end - - it_behaves_like 'deleting the project' + context 'with execute' do + it_behaves_like 'deleting the project with pipeline and build' end context 'container registry' do -- cgit v1.2.1 From a810e2e2f28cdc5f204717f5caf24e4e9db4d22b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 3 Feb 2017 16:05:15 +0000 Subject: Fixed adding to list bug --- app/assets/javascripts/boards/components/modal/footer.js.es6 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/footer.js.es6 b/app/assets/javascripts/boards/components/modal/footer.js.es6 index 4085a22b25a..a71d71106b4 100644 --- a/app/assets/javascripts/boards/components/modal/footer.js.es6 +++ b/app/assets/javascripts/boards/components/modal/footer.js.es6 @@ -8,7 +8,10 @@ gl.issueBoards.ModalFooter = Vue.extend({ mixins: [gl.issueBoards.ModalMixins], data() { - return ModalStore.store; + return { + modal: ModalStore.store, + state: gl.issueBoards.BoardsStore.state, + }; }, computed: { submitDisabled() { @@ -22,7 +25,7 @@ }, methods: { addIssues() { - const list = this.selectedList; + const list = this.modal.selectedList || this.state.lists[0]; const selectedIssues = ModalStore.getSelectedIssues(); const issueIds = selectedIssues.map(issue => issue.globalId); -- cgit v1.2.1 From 644d8eb9d47835a748280aeb9a6cb154b65b2a35 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 3 Feb 2017 17:53:36 +0000 Subject: Fixed eslint test failure --- app/assets/javascripts/project.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js index 67f8804666d..ba99d1fd913 100644 --- a/app/assets/javascripts/project.js +++ b/app/assets/javascripts/project.js @@ -58,8 +58,8 @@ }; Project.prototype.initRefSwitcher = function() { - var refListItem = document.createElement('li'), - refLink = document.createElement('a'); + var refListItem = document.createElement('li'); + var refLink = document.createElement('a'); refLink.href = '#'; -- cgit v1.2.1 From 183772c4488a82364425f5bc39e551988a2ac8ec Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 3 Feb 2017 18:01:24 +0000 Subject: Fix broken tests --- app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 | 1 + spec/features/projects/commit/builds_spec.rb | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 index 5131448561e..8106934e864 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 @@ -52,6 +52,7 @@ class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download" title="Artifacts" data-placement="top" + data-toggle="dropdown" aria-label="Artifacts" > <i class="fa fa-download" aria-hidden="true"></i> diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb index 33f1c323af1..268d420c594 100644 --- a/spec/features/projects/commit/builds_spec.rb +++ b/spec/features/projects/commit/builds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'project commit pipelines' do +feature 'project commit pipelines', js: true do given(:project) { create(:project) } background do -- cgit v1.2.1 From 9e4335363c9e7d2ea758c9c601314b88534dc9ae Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 31 Jan 2017 10:53:20 +0000 Subject: Started adding filters to issues modal --- .../boards/components/modal/filters.js.es6 | 47 ++++++++++++++++++++++ .../boards/components/modal/header.js.es6 | 2 + app/assets/stylesheets/pages/boards.scss | 17 ++++++++ 3 files changed, 66 insertions(+) create mode 100644 app/assets/javascripts/boards/components/modal/filters.js.es6 diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6 new file mode 100644 index 00000000000..f36d3c9ffe6 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters.js.es6 @@ -0,0 +1,47 @@ +/* global Vue */ +(() => { + const ModalStore = gl.issueBoards.ModalStore; + + gl.issueBoards.ModalFilters = Vue.extend({ + template: ` + <div class="modal-filters"> + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-user-search js-author-search" + type="button" + data-toggle="dropdown"> + Author + <i class="fa fa-chevron-down"></i> + </button> + </div> + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-user-search js-assignee-search" + type="button" + data-toggle="dropdown"> + Assignee + <i class="fa fa-chevron-down"></i> + </button> + </div> + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-milestone-select" + type="button" + data-toggle="dropdown"> + Milestone + <i class="fa fa-chevron-down"></i> + </button> + </div> + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-label-select js-multiselect" + type="button" + data-toggle="dropdown"> + Label + <i class="fa fa-chevron-down"></i> + </button> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index dbbcd73f1fe..87c407f5fec 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -1,5 +1,6 @@ /* global Vue */ //= require ./tabs +//= require ./filters (() => { const ModalStore = gl.issueBoards.ModalStore; @@ -49,6 +50,7 @@ <div class="add-issues-search append-bottom-10" v-if="showSearch"> + <modal-filters></modal-filters> <input placeholder="Search issues..." class="form-control" diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 9b413f3e61c..415eccf1764 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -416,6 +416,11 @@ .add-issues-search { display: -webkit-flex; display: flex; + + .form-control { + max-width: 200px; + margin-left: auto; + } } .add-issues-list-column { @@ -486,3 +491,15 @@ line-height: 15px; border-radius: 50%; } + +.modal-filters { + display: flex; + + > .dropdown { + margin-right: 10px; + } + + .dropdown-menu-toggle { + width: 140px; + } +} -- cgit v1.2.1 From 4652b08f19fb1dcdea5f062cb3be017518192a34 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Tue, 31 Jan 2017 11:48:43 +0000 Subject: Hardcoded author, assignee & milestone dropdowns --- .../boards/components/modal/filters.js.es6 | 99 +++++++++++++++++++++- .../boards/components/modal/index.js.es6 | 13 ++- .../javascripts/boards/stores/modal_store.js.es6 | 5 ++ app/assets/javascripts/milestone_select.js | 4 +- app/assets/javascripts/users_select.js | 4 +- app/views/projects/boards/_show.html.haml | 3 +- 6 files changed, 120 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6 index f36d3c9ffe6..3a4d2675acb 100644 --- a/app/assets/javascripts/boards/components/modal/filters.js.es6 +++ b/app/assets/javascripts/boards/components/modal/filters.js.es6 @@ -1,36 +1,129 @@ /* global Vue */ +/* global UsersSelect */ +/* global MilestoneSelect */ (() => { const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalFilters = Vue.extend({ + computed: { + currentUsername() { + return gon.current_username; + }, + }, + mounted() { + new UsersSelect(); + new MilestoneSelect(); + }, template: ` <div class="modal-filters"> <div class="dropdown"> <button class="dropdown-menu-toggle js-user-search js-author-search" type="button" - data-toggle="dropdown"> + data-toggle="dropdown" + data-any-user="Any Author" + data-current-user="true" + data-field-name="author_id" + :data-project-id="12" + :data-first-user="currentUsername"> Author <i class="fa fa-chevron-down"></i> </button> + <div class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable dropdown-menu-author"> + <div class="dropdown-title"> + <span>Filter by author</span> + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i class="fa fa-times dropdown-menu-close-icon"></i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + placeholder="Search authors" + autocomplete="off" /> + <i class="fa fa-search dropdown-input-search"></i> + <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> + </div> </div> <div class="dropdown"> <button class="dropdown-menu-toggle js-user-search js-assignee-search" type="button" - data-toggle="dropdown"> + data-toggle="dropdown" + data-any-user="Any Assignee" + data-null-user="true" + data-current-user="true" + data-field-name="assignee_id" + :data-project-id="12" + :data-first-user="currentUsername"> Assignee <i class="fa fa-chevron-down"></i> </button> + <div class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable dropdown-menu-author"> + <div class="dropdown-title"> + <span>Filter by assignee</span> + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i class="fa fa-times dropdown-menu-close-icon"></i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + placeholder="Search assignee" + autocomplete="off" /> + <i class="fa fa-search dropdown-input-search"></i> + <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> + </div> </div> <div class="dropdown"> <button class="dropdown-menu-toggle js-milestone-select" type="button" - data-toggle="dropdown"> + data-toggle="dropdown" + data-show-any="true" + data-show-upcoming="true" + data-field-name="milestone_title" + :data-project-id="12" + :data-milestones="'/root/test/milestones.json'"> Milestone <i class="fa fa-chevron-down"></i> </button> + <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone"> + <div class="dropdown-title"> + <span>Filter by milestone</span> + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i class="fa fa-times dropdown-menu-close-icon"></i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + placeholder="Search milestones" + autocomplete="off" /> + <i class="fa fa-search dropdown-input-search"></i> + <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> + </div> </div> <div class="dropdown"> <button diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index 666f4e16793..5bcbd2668af 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -50,17 +50,26 @@ this.issuesCount = false; } }, + filter: { + handler() { + this.issues = []; + this.loadIssues(); + }, + deep: true, + } }, methods: { searchOperation: _.debounce(function searchOperationDebounce() { this.loadIssues(true); }, 500), loadIssues(clearIssues = false) { - return gl.boardService.getBacklog({ + const data = Object.assign({}, this.filter, { search: this.searchTerm, page: this.page, per: this.perPage, - }).then((res) => { + }); + + return gl.boardService.getBacklog(data).then((res) => { const data = res.json(); if (clearIssues) { diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index 73518b42b84..95b0b3f9484 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -17,6 +17,11 @@ loadingNewPage: false, page: 1, perPage: 50, + filter: { + author_id: '', + assignee_id: '', + milestone_title: '', + }, }; } diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 7ab39ffbd05..f7e1f4a0816 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -116,7 +116,9 @@ e.preventDefault(); return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { + if ($el.closest('.add-issues-modal').length) { + gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = selected.name; + } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name; gl.issueBoards.BoardsStore.updateFiltersUrl(); e.preventDefault(); diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 77d2764cdf0..8ad0aa7aee8 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -193,7 +193,9 @@ selectedId = user.id; return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { + if ($el.closest('.add-issues-modal').length) { + gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; + } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { selectedId = user.id; gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id; gl.issueBoards.BoardsStore.updateFiltersUrl(); diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 13bc20f2ae2..855a4d4a241 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -30,4 +30,5 @@ %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project), ":issue-link-base" => "issueLinkBase", - ":root-path" => "rootPath" } + ":root-path" => "rootPath", + ":project-id" => @project.try(:id) } -- cgit v1.2.1 From e6add00cece0d53b7072096e23af82d1804b8342 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 1 Feb 2017 15:52:12 +0000 Subject: Added user filter component --- .../boards/components/modal/filters.js.es6 | 95 ++++------------------ .../boards/components/modal/filters/user.js.es6 | 89 ++++++++++++++++++++ 2 files changed, 104 insertions(+), 80 deletions(-) create mode 100644 app/assets/javascripts/boards/components/modal/filters/user.js.es6 diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6 index 3a4d2675acb..206948b70f4 100644 --- a/app/assets/javascripts/boards/components/modal/filters.js.es6 +++ b/app/assets/javascripts/boards/components/modal/filters.js.es6 @@ -1,94 +1,29 @@ /* global Vue */ -/* global UsersSelect */ /* global MilestoneSelect */ +//= require_tree ./filters (() => { const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalFilters = Vue.extend({ - computed: { - currentUsername() { - return gon.current_username; - }, - }, mounted() { - new UsersSelect(); new MilestoneSelect(); }, + components: { + 'user-filter': gl.issueBoards.ModalFilterUser, + }, template: ` <div class="modal-filters"> - <div class="dropdown"> - <button - class="dropdown-menu-toggle js-user-search js-author-search" - type="button" - data-toggle="dropdown" - data-any-user="Any Author" - data-current-user="true" - data-field-name="author_id" - :data-project-id="12" - :data-first-user="currentUsername"> - Author - <i class="fa fa-chevron-down"></i> - </button> - <div class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable dropdown-menu-author"> - <div class="dropdown-title"> - <span>Filter by author</span> - <button - class="dropdown-title-button dropdown-menu-close" - aria-label="Close" - type="button"> - <i class="fa fa-times dropdown-menu-close-icon"></i> - </button> - </div> - <div class="dropdown-input"> - <input - type="search" - class="dropdown-input-field" - placeholder="Search authors" - autocomplete="off" /> - <i class="fa fa-search dropdown-input-search"></i> - <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> - </div> - <div class="dropdown-content"></div> - <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> - </div> - </div> - <div class="dropdown"> - <button - class="dropdown-menu-toggle js-user-search js-assignee-search" - type="button" - data-toggle="dropdown" - data-any-user="Any Assignee" - data-null-user="true" - data-current-user="true" - data-field-name="assignee_id" - :data-project-id="12" - :data-first-user="currentUsername"> - Assignee - <i class="fa fa-chevron-down"></i> - </button> - <div class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable dropdown-menu-author"> - <div class="dropdown-title"> - <span>Filter by assignee</span> - <button - class="dropdown-title-button dropdown-menu-close" - aria-label="Close" - type="button"> - <i class="fa fa-times dropdown-menu-close-icon"></i> - </button> - </div> - <div class="dropdown-input"> - <input - type="search" - class="dropdown-input-field" - placeholder="Search assignee" - autocomplete="off" /> - <i class="fa fa-search dropdown-input-search"></i> - <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> - </div> - <div class="dropdown-content"></div> - <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> - </div> - </div> + <user-filter + dropdown-class-name="dropdown-menu-author" + toggle-class-name="js-user-search js-author-search" + toggle-label="Author" + field-name="author_id"></user-filter> + <user-filter + dropdown-class-name="dropdown-menu-author" + toggle-class-name="js-assignee-search" + toggle-label="Assignee" + field-name="assignee_id" + :null-user="true"></user-filter> <div class="dropdown"> <button class="dropdown-menu-toggle js-milestone-select" diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 b/app/assets/javascripts/boards/components/modal/filters/user.js.es6 new file mode 100644 index 00000000000..b440abf84e9 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters/user.js.es6 @@ -0,0 +1,89 @@ +/* global Vue */ +/* global UsersSelect */ +(() => { + gl.issueBoards.ModalFilterUser = Vue.extend({ + props: { + toggleClassName: { + type: String, + required: true, + }, + dropdownClassName: { + type: String, + required: false, + default: '', + }, + toggleLabel: { + type: String, + required: true, + }, + fieldName: { + type: String, + required: true, + }, + nullUser: { + type: Boolean, + required: false, + default: false, + }, + }, + mounted() { + new UsersSelect(); + }, + computed: { + currentUsername() { + return gon.current_username; + }, + dropdownTitle() { + return `Filter by ${this.toggleLabel.toLowerCase()}`; + }, + inputPlaceholder() { + return `Search ${this.toggleLabel.toLowerCase()}`; + }, + }, + template: ` + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-user-search" + :class="toggleClassName" + type="button" + data-toggle="dropdown" + data-current-user="true" + :data-any-user="'Any ' + toggleLabel" + :data-null-user="nullUser" + :data-field-name="fieldName" + :data-project-id="12" + :data-first-user="currentUsername"> + {{ toggleLabel }} + <i class="fa fa-chevron-down"></i> + </button> + <div + class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable" + :class="dropdownClassName"> + <div class="dropdown-title"> + {{ dropdownTitle }} + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i class="fa fa-times dropdown-menu-close-icon"></i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + autocomplete="off" + :placeholder="inputPlaceholder" /> + <i class="fa fa-search dropdown-input-search"></i> + <i + role="button" + class="fa fa-times dropdown-input-clear js-dropdown-input-clear"> + </i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> + </div> + </div> + `, + }); +})(); -- cgit v1.2.1 From e4d1e1437d43a34656f4343fc53431695742a989 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 1 Feb 2017 17:02:17 +0000 Subject: Added all filters --- .../boards/components/modal/filters.js.es6 | 63 +++++----------------- .../boards/components/modal/filters/label.js.es6 | 48 +++++++++++++++++ .../components/modal/filters/milestone.js.es6 | 49 +++++++++++++++++ .../boards/components/modal/filters/user.js.es6 | 15 ++++-- .../boards/components/modal/header.js.es6 | 11 +++- .../boards/components/modal/index.js.es6 | 11 ++-- .../javascripts/boards/stores/modal_store.js.es6 | 1 + app/assets/javascripts/labels_select.js | 36 +++++++++---- app/assets/javascripts/milestone_select.js | 29 +++++++--- app/assets/javascripts/users_select.js | 12 ++++- 10 files changed, 198 insertions(+), 77 deletions(-) create mode 100644 app/assets/javascripts/boards/components/modal/filters/label.js.es6 create mode 100644 app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6 index 206948b70f4..c6c22dd24c9 100644 --- a/app/assets/javascripts/boards/components/modal/filters.js.es6 +++ b/app/assets/javascripts/boards/components/modal/filters.js.es6 @@ -1,15 +1,19 @@ /* global Vue */ -/* global MilestoneSelect */ //= require_tree ./filters (() => { const ModalStore = gl.issueBoards.ModalStore; gl.issueBoards.ModalFilters = Vue.extend({ - mounted() { - new MilestoneSelect(); + props: { + projectId: { + type: Number, + required: true, + }, }, components: { 'user-filter': gl.issueBoards.ModalFilterUser, + 'milestone-filter': gl.issueBoards.ModalFilterMilestone, + 'label-filter': gl.issueBoards.ModalLabelFilter, }, template: ` <div class="modal-filters"> @@ -17,58 +21,17 @@ dropdown-class-name="dropdown-menu-author" toggle-class-name="js-user-search js-author-search" toggle-label="Author" - field-name="author_id"></user-filter> + field-name="author_id" + :project-id="projectId"></user-filter> <user-filter dropdown-class-name="dropdown-menu-author" toggle-class-name="js-assignee-search" toggle-label="Assignee" field-name="assignee_id" - :null-user="true"></user-filter> - <div class="dropdown"> - <button - class="dropdown-menu-toggle js-milestone-select" - type="button" - data-toggle="dropdown" - data-show-any="true" - data-show-upcoming="true" - data-field-name="milestone_title" - :data-project-id="12" - :data-milestones="'/root/test/milestones.json'"> - Milestone - <i class="fa fa-chevron-down"></i> - </button> - <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone"> - <div class="dropdown-title"> - <span>Filter by milestone</span> - <button - class="dropdown-title-button dropdown-menu-close" - aria-label="Close" - type="button"> - <i class="fa fa-times dropdown-menu-close-icon"></i> - </button> - </div> - <div class="dropdown-input"> - <input - type="search" - class="dropdown-input-field" - placeholder="Search milestones" - autocomplete="off" /> - <i class="fa fa-search dropdown-input-search"></i> - <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> - </div> - <div class="dropdown-content"></div> - <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> - </div> - </div> - <div class="dropdown"> - <button - class="dropdown-menu-toggle js-label-select js-multiselect" - type="button" - data-toggle="dropdown"> - Label - <i class="fa fa-chevron-down"></i> - </button> - </div> + :null-user="true" + :project-id="projectId"></user-filter> + <milestone-filter></milestone-filter> + <label-filter></label-filter> </div> `, }); diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 b/app/assets/javascripts/boards/components/modal/filters/label.js.es6 new file mode 100644 index 00000000000..cfea0780983 --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters/label.js.es6 @@ -0,0 +1,48 @@ +/* global Vue */ +/* global LabelSelect */ +(() => { + gl.issueBoards.ModalLabelFilter = Vue.extend({ + mounted() { + new LabelsSelect(this.$refs.dropdown); + }, + template: ` + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options" + type="button" + data-toggle="dropdown" + data-labels="/root/test/labels.json" + data-show-any="true" + data-show-no="true" + ref="dropdown"> + <span class="dropdown-toggle-text"> + Label + </span> + <i class="fa fa-chevron-down"></i> + </button> + <div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable"> + <div class="dropdown-title"> + Filter by label + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i class="fa fa-times dropdown-menu-close-icon"></i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + placeholder="Search" + autocomplete="off" /> + <i class="fa fa-search dropdown-input-search"></i> + <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 new file mode 100644 index 00000000000..14c95cb2dae --- /dev/null +++ b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 @@ -0,0 +1,49 @@ +/* global Vue */ +/* global MilestoneSelect */ +(() => { + gl.issueBoards.ModalFilterMilestone = Vue.extend({ + mounted() { + new MilestoneSelect(null, this.$refs.dropdown); + }, + template: ` + <div class="dropdown"> + <button + class="dropdown-menu-toggle js-milestone-select" + type="button" + data-toggle="dropdown" + data-show-any="true" + data-show-upcoming="true" + data-field-name="milestone_title" + :data-milestones="'/root/test/milestones.json'" + ref="dropdown"> + <span class="dropdown-toggle-text"> + Milestone + </span> + <i class="fa fa-chevron-down"></i> + </button> + <div class="dropdown-menu dropdown-select dropdown-menu-selectable dropdown-menu-milestone"> + <div class="dropdown-title"> + <span>Filter by milestone</span> + <button + class="dropdown-title-button dropdown-menu-close" + aria-label="Close" + type="button"> + <i class="fa fa-times dropdown-menu-close-icon"></i> + </button> + </div> + <div class="dropdown-input"> + <input + type="search" + class="dropdown-input-field" + placeholder="Search milestones" + autocomplete="off" /> + <i class="fa fa-search dropdown-input-search"></i> + <i role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i> + </div> + <div class="dropdown-content"></div> + <div class="dropdown-loading"><i class="fa fa-spinner fa-spin"></i></div> + </div> + </div> + `, + }); +})(); diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 b/app/assets/javascripts/boards/components/modal/filters/user.js.es6 index b440abf84e9..d45649af4cd 100644 --- a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 +++ b/app/assets/javascripts/boards/components/modal/filters/user.js.es6 @@ -25,9 +25,13 @@ required: false, default: false, }, + projectId: { + type: Number, + required: true, + }, }, mounted() { - new UsersSelect(); + new UsersSelect(null, this.$refs.dropdown); }, computed: { currentUsername() { @@ -51,9 +55,12 @@ :data-any-user="'Any ' + toggleLabel" :data-null-user="nullUser" :data-field-name="fieldName" - :data-project-id="12" - :data-first-user="currentUsername"> - {{ toggleLabel }} + :data-project-id="projectId" + :data-first-user="currentUsername" + ref="dropdown"> + <span class="dropdown-toggle-text"> + {{ toggleLabel }} + </span> <i class="fa fa-chevron-down"></i> </button> <div diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index 87c407f5fec..cd672e0f1b2 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -6,6 +6,12 @@ gl.issueBoards.ModalHeader = Vue.extend({ mixins: [gl.issueBoards.ModalMixins], + props: { + projectId: { + type: Number, + required: true, + }, + }, data() { return ModalStore.store; }, @@ -30,6 +36,7 @@ }, components: { 'modal-tabs': gl.issueBoards.ModalTabs, + 'modal-filters': gl.issueBoards.ModalFilters, }, template: ` <div> @@ -50,7 +57,9 @@ <div class="add-issues-search append-bottom-10" v-if="showSearch"> - <modal-filters></modal-filters> + <modal-filters + :project-id="projectId"> + </modal-filters> <input placeholder="Search issues..." class="form-control" diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index 5bcbd2668af..49f4c73151e 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -25,6 +25,10 @@ type: String, required: true, }, + projectId: { + type: Number, + required: true, + }, }, data() { return ModalStore.store; @@ -52,8 +56,7 @@ }, filter: { handler() { - this.issues = []; - this.loadIssues(); + this.loadIssues(true); }, deep: true, } @@ -119,7 +122,9 @@ class="add-issues-modal" v-if="showAddIssuesModal"> <div class="add-issues-container"> - <modal-header></modal-header> + <modal-header + :project-id="projectId"> + </modal-header> <modal-list :issue-link-base="issueLinkBase" :root-path="rootPath" diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index 95b0b3f9484..ed42af301cd 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -21,6 +21,7 @@ author_id: '', assignee_id: '', milestone_title: '', + label_name: [], }, }; } diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 70dc0d06b7b..e4cf9057e6d 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,10 +4,17 @@ (function() { this.LabelsSelect = (function() { - function LabelsSelect() { - var _this; + function LabelsSelect(els) { + var _this, $els; _this = this; - $('.js-label-select').each(function(i, dropdown) { + + $els = $(els); + + if (!els) { + $els = $('.js-label-select'); + } + + $els.each(function(i, dropdown) { var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer; $dropdown = $(dropdown); $dropdownContainer = $dropdown.closest('.labels-filter'); @@ -324,7 +331,7 @@ multiSelect: $dropdown.hasClass('js-multiselect'), vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: function(label, $el, e, isMarking) { - var isIssueIndex, isMRIndex, page; + var isIssueIndex, isMRIndex, page, boardsModel; page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; @@ -346,22 +353,31 @@ return; } - if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { + if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && + !$dropdown.closest('.add-issues-modal').length) { + boardsModel = gl.issueBoards.BoardsStore.state.filters; + } else if ($dropdown.closest('.add-issues-modal').length) { + boardsModel = gl.issueBoards.ModalStore.store.filter; + } + + if (boardsModel) { if (label.isAny) { - gl.issueBoards.BoardsStore.state.filters['label_name'] = []; + boardsModel['label_name'] = []; } else if ($el.hasClass('is-active')) { - gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title); + boardsModel['label_name'].push(label.title); } else { - var filters = gl.issueBoards.BoardsStore.state.filters['label_name']; + var filters = boardsModel['label_name']; filters = filters.filter(function (filteredLabel) { return filteredLabel !== label.title; }); - gl.issueBoards.BoardsStore.state.filters['label_name'] = filters; + boardsModel['label_name'] = filters; } - gl.issueBoards.BoardsStore.updateFiltersUrl(); + if (!$dropdown.closest('.add-issues-modal').length) { + gl.issueBoards.BoardsStore.updateFiltersUrl(); + } e.preventDefault(); return; } diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index f7e1f4a0816..a026f178720 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -5,12 +5,19 @@ (function() { this.MilestoneSelect = (function() { - function MilestoneSelect(currentProject) { + function MilestoneSelect(currentProject, els) { var _this; if (currentProject != null) { _this = this; this.currentProject = JSON.parse(currentProject); } + + $els = $(els); + + if (!els) { + $els = $('.js-label-select'); + } + $('.js-milestone-select').each(function(i, dropdown) { var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove; $dropdown = $(dropdown); @@ -108,7 +115,7 @@ }, vue: $dropdown.hasClass('js-issue-board-sidebar'), clicked: function(selected, $el, e) { - var data, isIssueIndex, isMRIndex, page; + var data, isIssueIndex, isMRIndex, page, boardsStore; page = $('body').data('page'); isIssueIndex = page === 'projects:issues:index'; isMRIndex = (page === page && page === 'projects:merge_requests:index'); @@ -116,11 +123,19 @@ e.preventDefault(); return; } - if ($el.closest('.add-issues-modal').length) { - gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = selected.name; - } else if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) { - gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name; - gl.issueBoards.BoardsStore.updateFiltersUrl(); + + if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar') && + !$dropdown.closest('.add-issues-modal').length) { + boardsStore = gl.issueBoards.BoardsStore.state.filters; + } else if ($dropdown.closest('.add-issues-modal').length) { + boardsStore = gl.issueBoards.ModalStore.store.filter; + } + + if (boardsStore) { + boardsStore[$dropdown.data('field-name')] = selected.name; + if (!$dropdown.closest('.add-issues-modal').length) { + gl.issueBoards.BoardsStore.updateFiltersUrl(); + } e.preventDefault(); } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { if (selected.name != null) { diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 8ad0aa7aee8..07458bed2e6 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -8,7 +8,8 @@ slice = [].slice; this.UsersSelect = (function() { - function UsersSelect(currentUser) { + function UsersSelect(currentUser, els) { + var $els; this.users = bind(this.users, this); this.user = bind(this.user, this); this.usersPath = "/autocomplete/users.json"; @@ -20,7 +21,14 @@ this.currentUser = JSON.parse(currentUser); } } - $('.js-user-search').each((function(_this) { + + $els = $(els); + + if (!els) { + $els = $('.js-label-select'); + } + + $els.each((function(_this) { return function(i, dropdown) { var options = {}; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove; -- cgit v1.2.1 From b934a123fb047555633fb8c34c0d0c1a6b152f19 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 1 Feb 2017 17:10:24 +0000 Subject: Responsive styling fixes for the filters --- app/assets/stylesheets/pages/boards.scss | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 415eccf1764..506cb108249 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -418,8 +418,11 @@ display: flex; .form-control { - max-width: 200px; margin-left: auto; + + @media (min-width: $screen-sm-min) { + max-width: 200px; + } } } @@ -496,10 +499,19 @@ display: flex; > .dropdown { + display: none; margin-right: 10px; + + @media (min-width: $screen-sm-min) { + display: block; + } } .dropdown-menu-toggle { - width: 140px; + width: 100px; + + @media (min-width: $screen-md-min) { + width: 140px; + } } } -- cgit v1.2.1 From 10f805c2dcf88e359d3a858c45f8476995cab323 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Wed, 1 Feb 2017 17:26:03 +0000 Subject: Passes through the correct endpoints --- .../javascripts/boards/components/modal/filters.js.es6 | 12 ++++++++++-- .../javascripts/boards/components/modal/filters/label.js.es6 | 8 +++++++- .../boards/components/modal/filters/milestone.js.es6 | 8 +++++++- app/assets/javascripts/boards/components/modal/header.js.es6 | 12 +++++++++++- app/assets/javascripts/boards/components/modal/index.js.es6 | 12 +++++++++++- app/views/projects/boards/_show.html.haml | 2 ++ 6 files changed, 48 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6 index c6c22dd24c9..8f923f65306 100644 --- a/app/assets/javascripts/boards/components/modal/filters.js.es6 +++ b/app/assets/javascripts/boards/components/modal/filters.js.es6 @@ -9,6 +9,14 @@ type: Number, required: true, }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, }, components: { 'user-filter': gl.issueBoards.ModalFilterUser, @@ -30,8 +38,8 @@ field-name="assignee_id" :null-user="true" :project-id="projectId"></user-filter> - <milestone-filter></milestone-filter> - <label-filter></label-filter> + <milestone-filter :milestone-path="milestonePath"></milestone-filter> + <label-filter :label-path="labelPath"></label-filter> </div> `, }); diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 b/app/assets/javascripts/boards/components/modal/filters/label.js.es6 index cfea0780983..a95ef90f97f 100644 --- a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 +++ b/app/assets/javascripts/boards/components/modal/filters/label.js.es6 @@ -2,6 +2,12 @@ /* global LabelSelect */ (() => { gl.issueBoards.ModalLabelFilter = Vue.extend({ + props: { + labelPath: { + type: String, + required: true, + }, + }, mounted() { new LabelsSelect(this.$refs.dropdown); }, @@ -11,9 +17,9 @@ class="dropdown-menu-toggle js-label-select js-multiselect js-extra-options" type="button" data-toggle="dropdown" - data-labels="/root/test/labels.json" data-show-any="true" data-show-no="true" + :data-labels="labelPath" ref="dropdown"> <span class="dropdown-toggle-text"> Label diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 index 14c95cb2dae..0511ffe81a7 100644 --- a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 +++ b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 @@ -2,6 +2,12 @@ /* global MilestoneSelect */ (() => { gl.issueBoards.ModalFilterMilestone = Vue.extend({ + props: { + milestonePath: { + type: String, + required: true, + }, + }, mounted() { new MilestoneSelect(null, this.$refs.dropdown); }, @@ -14,7 +20,7 @@ data-show-any="true" data-show-upcoming="true" data-field-name="milestone_title" - :data-milestones="'/root/test/milestones.json'" + :data-milestones="milestonePath" ref="dropdown"> <span class="dropdown-toggle-text"> Milestone diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index cd672e0f1b2..c220c200d9a 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -11,6 +11,14 @@ type: Number, required: true, }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, }, data() { return ModalStore.store; @@ -58,7 +66,9 @@ class="add-issues-search append-bottom-10" v-if="showSearch"> <modal-filters - :project-id="projectId"> + :project-id="projectId" + :milestone-path="milestonePath" + :label-path="labelPath"> </modal-filters> <input placeholder="Search issues..." diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index 49f4c73151e..c0871d74565 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -29,6 +29,14 @@ type: Number, required: true, }, + milestonePath: { + type: String, + required: true, + }, + labelPath: { + type: String, + required: true, + }, }, data() { return ModalStore.store; @@ -123,7 +131,9 @@ v-if="showAddIssuesModal"> <div class="add-issues-container"> <modal-header - :project-id="projectId"> + :project-id="projectId" + :milestone-path="milestonePath" + :label-path="labelPath"> </modal-header> <modal-list :issue-link-base="issueLinkBase" diff --git a/app/views/projects/boards/_show.html.haml b/app/views/projects/boards/_show.html.haml index 855a4d4a241..bf863431b63 100644 --- a/app/views/projects/boards/_show.html.haml +++ b/app/views/projects/boards/_show.html.haml @@ -29,6 +29,8 @@ = render "projects/boards/components/sidebar" %board-add-issues-modal{ "blank-state-image" => render('shared/empty_states/icons/issues.svg'), "new-issue-path" => new_namespace_project_issue_path(@project.namespace, @project), + "milestone-path" => milestones_filter_dropdown_path, + "label-path" => labels_filter_path, ":issue-link-base" => "issueLinkBase", ":root-path" => "rootPath", ":project-id" => @project.try(:id) } -- cgit v1.2.1 From 7e253790623895ba0e91f637e64e9e3306999951 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 2 Feb 2017 11:16:16 +0000 Subject: Added tests for modal filters --- app/assets/javascripts/users_select.js | 2 +- spec/features/boards/modal_filter_spec.rb | 219 ++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 spec/features/boards/modal_filter_spec.rb diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js index 07458bed2e6..d4b24d13299 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -25,7 +25,7 @@ $els = $(els); if (!els) { - $els = $('.js-label-select'); + $els = $('.js-user-search'); } $els.each((function(_this) { diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb new file mode 100644 index 00000000000..aede3882f54 --- /dev/null +++ b/spec/features/boards/modal_filter_spec.rb @@ -0,0 +1,219 @@ +require 'rails_helper' + +describe 'Issue Boards add issue modal filtering', :feature, :js do + include WaitForAjax + include WaitForVueResource + + let(:project) { create(:empty_project, :public) } + let(:board) { create(:board, project: project) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:issue1) { create(:issue, project: project) } + + before do + project.team << [user, :master] + + login_as(user) + end + + context 'author' do + let!(:issue) { create(:issue, project: project, author: user2) } + + before do + project.team << [user2, :developer] + + visit_board + end + + it 'filters by any author' do + page.within('.add-issues-modal') do + click_button 'Author' + + wait_for_ajax + + click_link 'Any Author' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'filters by selected user' do + page.within('.add-issues-modal') do + click_button 'Author' + + wait_for_ajax + + click_link user2.name + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + end + + context 'assignee' do + let!(:issue) { create(:issue, project: project, assignee: user2) } + + before do + project.team << [user2, :developer] + + visit_board + end + + it 'filters by any assignee' do + page.within('.add-issues-modal') do + click_button 'Assignee' + + wait_for_ajax + + click_link 'Any Assignee' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'filters by unassigned' do + page.within('.add-issues-modal') do + click_button 'Assignee' + + wait_for_ajax + + click_link 'Unassigned' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + + it 'filters by selected user' do + page.within('.add-issues-modal') do + click_button 'Assignee' + + wait_for_ajax + + page.within '.dropdown-menu-user' do + click_link user2.name + end + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + end + + context 'milestone' do + let(:milestone) { create(:milestone, project: project) } + let!(:issue) { create(:issue, project: project, milestone: milestone) } + + before do + visit_board + end + + it 'filters by any milestone' do + page.within('.add-issues-modal') do + click_button 'Milestone' + + wait_for_ajax + + click_link 'Any Milestone' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'filters by upcoming milestone' do + page.within('.add-issues-modal') do + click_button 'Milestone' + + wait_for_ajax + + click_link 'Upcoming' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 0) + end + end + + it 'filters by selected milestone' do + page.within('.add-issues-modal') do + click_button 'Milestone' + + wait_for_ajax + + click_link milestone.name + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + end + + context 'label' do + let(:label) { create(:label, project: project) } + let!(:issue) { create(:labeled_issue, project: project, labels: [label]) } + + before do + visit_board + end + + it 'filters by any label' do + page.within('.add-issues-modal') do + click_button 'Label' + + wait_for_ajax + + click_link 'Any Label' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 2) + end + end + + it 'filters by no label' do + page.within('.add-issues-modal') do + click_button 'Label' + + wait_for_ajax + + click_link 'No Label' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + + it 'filters by label' do + page.within('.add-issues-modal') do + click_button 'Label' + + wait_for_ajax + + click_link label.title + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + end + + def visit_board + visit namespace_project_board_path(project.namespace, project, board) + wait_for_vue_resource + + click_button('Add issues') + end +end -- cgit v1.2.1 From c3339b332bb2ed95c2a7b09ce258ecbb23b0cfc5 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 2 Feb 2017 14:34:28 +0000 Subject: Resets modal filters when closing --- .../boards/components/modal/filters.js.es6 | 3 +++ .../boards/components/modal/index.js.es6 | 2 ++ .../javascripts/boards/stores/modal_store.js.es6 | 17 +++++++++----- spec/features/boards/modal_filter_spec.rb | 26 ++++++++++++++++++++++ 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6 index 8f923f65306..f1828781c5a 100644 --- a/app/assets/javascripts/boards/components/modal/filters.js.es6 +++ b/app/assets/javascripts/boards/components/modal/filters.js.es6 @@ -18,6 +18,9 @@ required: true, }, }, + destroyed() { + ModalStore.setDefaultFilter(); + }, components: { 'user-filter': gl.issueBoards.ModalFilterUser, 'milestone-filter': gl.issueBoards.ModalFilterMilestone, diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index c0871d74565..71f7f7eeb20 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -74,6 +74,8 @@ this.loadIssues(true); }, 500), loadIssues(clearIssues = false) { + if (!this.showAddIssuesModal) return; + const data = Object.assign({}, this.filter, { search: this.searchTerm, page: this.page, diff --git a/app/assets/javascripts/boards/stores/modal_store.js.es6 b/app/assets/javascripts/boards/stores/modal_store.js.es6 index ed42af301cd..15fc6c79e8d 100644 --- a/app/assets/javascripts/boards/stores/modal_store.js.es6 +++ b/app/assets/javascripts/boards/stores/modal_store.js.es6 @@ -17,12 +17,17 @@ loadingNewPage: false, page: 1, perPage: 50, - filter: { - author_id: '', - assignee_id: '', - milestone_title: '', - label_name: [], - }, + }; + + this.setDefaultFilter(); + } + + setDefaultFilter() { + this.store.filter = { + author_id: '', + assignee_id: '', + milestone_title: '', + label_name: [], }; } diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index aede3882f54..44daa50a006 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -16,6 +16,32 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do login_as(user) end + it 'restores filters when closing' do + visit_board + + page.within('.add-issues-modal') do + click_button 'Milestone' + + wait_for_ajax + + click_link 'Upcoming' + + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 0) + + click_button 'Cancel' + end + + click_button('Add issues') + + page.within('.add-issues-modal') do + wait_for_vue_resource + + expect(page).to have_selector('.card', count: 1) + end + end + context 'author' do let!(:issue) { create(:issue, project: project, author: user2) } -- cgit v1.2.1 From 619f7ec8eff2999d8b9bef72e0345c4f390d031f Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 2 Feb 2017 14:54:45 +0000 Subject: Show empty state if filter returns empty results --- .../javascripts/boards/components/modal/index.js.es6 | 3 ++- .../javascripts/boards/components/modal/list.js.es6 | 17 +++++++++++++++++ app/assets/stylesheets/pages/boards.scss | 6 ++++++ spec/features/boards/modal_filter_spec.rb | 12 ++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index 71f7f7eeb20..cab7d576194 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -75,7 +75,7 @@ }, 500), loadIssues(clearIssues = false) { if (!this.showAddIssuesModal) return; - + const data = Object.assign({}, this.filter, { search: this.searchTerm, page: this.page, @@ -138,6 +138,7 @@ :label-path="labelPath"> </modal-header> <modal-list + :image="blankStateImage" :issue-link-base="issueLinkBase" :root-path="rootPath" v-if="!loading && showList"></modal-list> diff --git a/app/assets/javascripts/boards/components/modal/list.js.es6 b/app/assets/javascripts/boards/components/modal/list.js.es6 index d0901219216..3730c1ecaeb 100644 --- a/app/assets/javascripts/boards/components/modal/list.js.es6 +++ b/app/assets/javascripts/boards/components/modal/list.js.es6 @@ -14,6 +14,10 @@ type: String, required: true, }, + image: { + type: String, + required: true, + }, }, data() { return ModalStore.store; @@ -110,6 +114,19 @@ <section class="add-issues-list add-issues-list-columns" ref="list"> + <div + class="empty-state add-issues-empty-state-filter text-center" + v-if="issuesCount > 0 && issues.length === 0"> + <div + class="svg-content" + v-html="image"> + </div> + <div class="text-content"> + <h4> + There are no issues to show. + </h4> + </div> + </div> <div v-for="group in groupedIssues" class="add-issues-list-column"> diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 506cb108249..0d336a34131 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -389,6 +389,12 @@ flex: 1; margin-top: 0; + &.add-issues-empty-state-filter { + -webkit-flex-direction: column; + flex-direction: column; + margin-top: 50px; + } + > .row { width: 100%; margin: auto 0; diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index 44daa50a006..62b0efdb51c 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -16,6 +16,18 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do login_as(user) end + it 'shows empty state when no results found' do + visit_board + + page.within('.add-issues-modal') do + find('.form-control').native.send_keys('testing empty state') + + wait_for_vue_resource + + expect(page).to have_content('There are no issues to show.') + end + end + it 'restores filters when closing' do visit_board -- cgit v1.2.1 From b866d9f087043de2d5c9cd311f68aa30b2ecc39b Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Thu, 2 Feb 2017 15:12:44 +0000 Subject: Fixed eslint errors --- .../javascripts/boards/components/modal/filters/label.js.es6 | 3 ++- .../javascripts/boards/components/modal/filters/milestone.js.es6 | 1 + .../javascripts/boards/components/modal/filters/user.js.es6 | 1 + app/assets/javascripts/boards/components/modal/index.js.es6 | 8 ++++---- app/assets/javascripts/milestone_select.js | 4 ++-- app/assets/stylesheets/pages/boards.scss | 3 ++- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 b/app/assets/javascripts/boards/components/modal/filters/label.js.es6 index a95ef90f97f..ac5fbc487d9 100644 --- a/app/assets/javascripts/boards/components/modal/filters/label.js.es6 +++ b/app/assets/javascripts/boards/components/modal/filters/label.js.es6 @@ -1,5 +1,6 @@ +/* eslint-disable no-new */ /* global Vue */ -/* global LabelSelect */ +/* global LabelsSelect */ (() => { gl.issueBoards.ModalLabelFilter = Vue.extend({ props: { diff --git a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 index 0511ffe81a7..e84345ddc2b 100644 --- a/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 +++ b/app/assets/javascripts/boards/components/modal/filters/milestone.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable no-new */ /* global Vue */ /* global MilestoneSelect */ (() => { diff --git a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 b/app/assets/javascripts/boards/components/modal/filters/user.js.es6 index d45649af4cd..6462e1fe2af 100644 --- a/app/assets/javascripts/boards/components/modal/filters/user.js.es6 +++ b/app/assets/javascripts/boards/components/modal/filters/user.js.es6 @@ -1,3 +1,4 @@ +/* eslint-disable no-new */ /* global Vue */ /* global UsersSelect */ (() => { diff --git a/app/assets/javascripts/boards/components/modal/index.js.es6 b/app/assets/javascripts/boards/components/modal/index.js.es6 index cab7d576194..30c0098d35f 100644 --- a/app/assets/javascripts/boards/components/modal/index.js.es6 +++ b/app/assets/javascripts/boards/components/modal/index.js.es6 @@ -67,22 +67,22 @@ this.loadIssues(true); }, deep: true, - } + }, }, methods: { searchOperation: _.debounce(function searchOperationDebounce() { this.loadIssues(true); }, 500), loadIssues(clearIssues = false) { - if (!this.showAddIssuesModal) return; + if (!this.showAddIssuesModal) return false; - const data = Object.assign({}, this.filter, { + const queryData = Object.assign({}, this.filter, { search: this.searchTerm, page: this.page, per: this.perPage, }); - return gl.boardService.getBacklog(data).then((res) => { + return gl.boardService.getBacklog(queryData).then((res) => { const data = res.json(); if (clearIssues) { diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index a026f178720..6ca60676fc1 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -6,7 +6,7 @@ (function() { this.MilestoneSelect = (function() { function MilestoneSelect(currentProject, els) { - var _this; + var _this, $els; if (currentProject != null) { _this = this; this.currentProject = JSON.parse(currentProject); @@ -18,7 +18,7 @@ $els = $('.js-label-select'); } - $('.js-milestone-select').each(function(i, dropdown) { + $els.each(function(i, dropdown) { var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove; $dropdown = $(dropdown); projectId = $dropdown.data('project-id'); diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss index 0d336a34131..b362cc758cc 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -392,7 +392,8 @@ &.add-issues-empty-state-filter { -webkit-flex-direction: column; flex-direction: column; - margin-top: 50px; + -webkit-justify-content: center; + justify-content: center; } > .row { -- cgit v1.2.1 From b52600b8bfb3c8e549e9cbcd45db2b18b8eee24a Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Fri, 3 Feb 2017 12:51:04 +0000 Subject: Fixed milestone select failing tests --- app/assets/javascripts/milestone_select.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js index 6ca60676fc1..2f08aa7fe8b 100644 --- a/app/assets/javascripts/milestone_select.js +++ b/app/assets/javascripts/milestone_select.js @@ -15,7 +15,7 @@ $els = $(els); if (!els) { - $els = $('.js-label-select'); + $els = $('.js-milestone-select'); } $els.each(function(i, dropdown) { -- cgit v1.2.1 From 5af4cae544c8526de63e639bd6c7db730526add3 Mon Sep 17 00:00:00 2001 From: James Edwards-Jones <jedwardsjones@gitlab.com> Date: Fri, 3 Feb 2017 18:25:29 +0000 Subject: Fix documentation link in doc/user/project/pages/index.md --- doc/user/project/pages/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/user/project/pages/index.md b/doc/user/project/pages/index.md index fe477b1ed0b..b814e3fccb2 100644 --- a/doc/user/project/pages/index.md +++ b/doc/user/project/pages/index.md @@ -432,3 +432,4 @@ For a list of known issues, visit GitLab's [public issue tracker]. [pages-jekyll]: https://gitlab.com/pages/jekyll [metarefresh]: https://en.wikipedia.org/wiki/Meta_refresh [public issue tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Pages +[ce-14605]: https://gitlab.com/gitlab-org/gitlab-ce/issues/14605 -- cgit v1.2.1 From 8063628b903d4a8498152d4c3c7c22fca2768957 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 3 Feb 2017 19:26:48 +0100 Subject: Move webhooks to new a location under Integrations --- app/helpers/search_helper.rb | 2 +- app/views/shared/web_hooks/_form.html.haml | 2 +- doc/README.md | 2 +- doc/administration/custom_hooks.md | 3 +- doc/security/webhooks.md | 4 +- doc/university/glossary/README.md | 2 +- doc/user/project/integrations/img/webhooks_ssl.png | Bin 0 -> 27799 bytes doc/user/project/integrations/webhooks.md | 1025 +++++++++++++++++++ doc/web_hooks/ssl.png | Bin 27799 -> 0 bytes doc/web_hooks/web_hooks.md | 1026 +------------------- 10 files changed, 1034 insertions(+), 1032 deletions(-) create mode 100644 doc/user/project/integrations/img/webhooks_ssl.png create mode 100644 doc/user/project/integrations/webhooks.md delete mode 100644 doc/web_hooks/ssl.png diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 37b69423c97..8ff8db16514 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -56,7 +56,7 @@ module SearchHelper { category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") }, { category: "Help", label: "SSH Keys Help", url: help_page_path("ssh/README") }, { category: "Help", label: "System Hooks Help", url: help_page_path("system_hooks/system_hooks") }, - { category: "Help", label: "Webhooks Help", url: help_page_path("web_hooks/web_hooks") }, + { category: "Help", label: "Webhooks Help", url: help_page_path("user/project/integrations/webhooks") }, { category: "Help", label: "Workflow Help", url: help_page_path("workflow/README") }, ] end diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 13586a5a12a..0236541fd9b 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -3,7 +3,7 @@ %h4.prepend-top-0 = page_title %p - #{link_to "Webhooks", help_page_path("web_hooks/web_hooks")} can be + #{link_to "Webhooks", help_page_path("user/project/integrations//webhooks")} can be used for binding events when something is happening within the project. .col-lg-9.append-bottom-default = form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f| diff --git a/doc/README.md b/doc/README.md index 909740211a6..70392eb5aad 100644 --- a/doc/README.md +++ b/doc/README.md @@ -21,7 +21,7 @@ - [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. -- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project. +- [Webhooks](user/project/integrations/webhooks.md) Let GitLab notify you when new code has been pushed to your project. - [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN. - [Git Attributes](user/project/git_attributes.md) Managing Git attributes using a `.gitattributes` file. - [Git cheatsheet](https://gitlab.com/gitlab-com/marketing/raw/master/design/print/git-cheatsheet/print-pdf/git-cheatsheet.pdf) Download a PDF describing the most used Git operations. diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md index 80e5d80aa41..4d35b20d0c3 100644 --- a/doc/administration/custom_hooks.md +++ b/doc/administration/custom_hooks.md @@ -3,7 +3,7 @@ > **Note:** Custom Git hooks must be configured on the filesystem of the GitLab server. Only GitLab server administrators will be able to complete these tasks. -Please explore [webhooks](../web_hooks/web_hooks.md) as an option if you do not +Please explore [webhooks] as an option if you do not have filesystem access. For a user configurable Git hook interface, please see [GitLab Enterprise Edition Git Hooks](http://docs.gitlab.com/ee/git_hooks/git_hooks.html). @@ -80,5 +80,6 @@ STDERR takes precedence over STDOUT. ![Custom message from custom Git hook](img/custom_hooks_error_msg.png) [hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks +[webhooks]: ../user/project/integrations/webhooks.md [5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073 [93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93 diff --git a/doc/security/webhooks.md b/doc/security/webhooks.md index bb46aebf4b5..faabc53ce72 100644 --- a/doc/security/webhooks.md +++ b/doc/security/webhooks.md @@ -2,7 +2,7 @@ If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks. -With [Webhooks](../web_hooks/web_hooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way. +With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way. Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent. @@ -10,4 +10,4 @@ Because Webhook requests are made by the GitLab server itself, these have comple If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete". -To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough. \ No newline at end of file +To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough. diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md index 20e7ea1987f..979a1c5d310 100644 --- a/doc/university/glossary/README.md +++ b/doc/university/glossary/README.md @@ -573,7 +573,7 @@ A [model](http://www.umsl.edu/~hugheyd/is6840/waterfall.html) of building softwa ### Webhooks -A way for for an app to [provide](https://docs.gitlab.com/ce/web_hooks/web_hooks.html) other applications with real-time information (e.g., send a message to a slack channel when a commit is pushed.) Read about setting up [custom git hooks](https://gitlab.com/help/administration/custom_hooks.md) for when webhooks are insufficient. +A way for for an app to [provide](https://docs.gitlab.com/ce/user/project/integrations/webhooks.html) other applications with real-time information (e.g., send a message to a slack channel when a commit is pushed.) Read about setting up [custom git hooks](https://gitlab.com/help/administration/custom_hooks.md) for when webhooks are insufficient. ### Wiki diff --git a/doc/user/project/integrations/img/webhooks_ssl.png b/doc/user/project/integrations/img/webhooks_ssl.png new file mode 100644 index 00000000000..21ddec4ebdf Binary files /dev/null and b/doc/user/project/integrations/img/webhooks_ssl.png differ diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md new file mode 100644 index 00000000000..9d775355c4c --- /dev/null +++ b/doc/user/project/integrations/webhooks.md @@ -0,0 +1,1025 @@ +# Webhooks + +>**Note:** +Starting from GitLab 8.5: +- the `repository` key is deprecated in favor of the `project` key +- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key +- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key + +Project webhooks allow you to trigger a URL if for example new code is pushed or +a new issue is created. You can configure webhooks to listen for specific events +like pushes, issues or merge requests. GitLab will send a POST request with data +to the webhook URL. + +Webhooks can be used to update an external issue tracker, trigger CI builds, +update a backup mirror, or even deploy to your production server. + +Navigate to the webhooks page by choosing **Webhooks** from your project's +settings which can be found under the wheel icon in the upper right corner. + +## Webhook endpoint tips + +If you are writing your own endpoint (web server) that will receive +GitLab webhooks keep in mind the following things: + +- Your endpoint should send its HTTP response as fast as possible. If + you wait too long, GitLab may decide the hook failed and retry it. +- Your endpoint should ALWAYS return a valid HTTP response. If you do + not do this then GitLab will think the hook failed and retry it. + Most HTTP libraries take care of this for you automatically but if + you are writing a low-level hook this is important to remember. +- GitLab ignores the HTTP status code returned by your endpoint. + +## Secret token + +If you specify a secret token, it will be sent with the hook request in the +`X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify +that the request is legitimate. + +## SSL verification + +By default, the SSL certificate of the webhook endpoint is verified based on +an internal list of Certificate Authorities, which means the certificate cannot +be self-signed. + +You can turn this off in the webhook settings in your GitLab projects. + +![SSL Verification](img/webhooks_ssl.png) + +## Events + +Below are described the supported events. + +### Push events + +Triggered when you push to the repository except when pushing tags. + +**Request header**: + +``` +X-Gitlab-Event: Push Hook +``` + +**Request body:** + +```json +{ + "object_kind": "push", + "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", + "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "ref": "refs/heads/master", + "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "user_id": 4, + "user_name": "John Smith", + "user_email": "john@example.com", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 15, + "project":{ + "name":"Diaspora", + "description":"", + "web_url":"http://example.com/mike/diaspora", + "avatar_url":null, + "git_ssh_url":"git@example.com:mike/diaspora.git", + "git_http_url":"http://example.com/mike/diaspora.git", + "namespace":"Mike", + "visibility_level":0, + "path_with_namespace":"mike/diaspora", + "default_branch":"master", + "homepage":"http://example.com/mike/diaspora", + "url":"git@example.com:mike/diaspora.git", + "ssh_url":"git@example.com:mike/diaspora.git", + "http_url":"http://example.com/mike/diaspora.git" + }, + "repository":{ + "name": "Diaspora", + "url": "git@example.com:mike/diaspora.git", + "description": "", + "homepage": "http://example.com/mike/diaspora", + "git_http_url":"http://example.com/mike/diaspora.git", + "git_ssh_url":"git@example.com:mike/diaspora.git", + "visibility_level":0 + }, + "commits": [ + { + "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "message": "Update Catalan translation to e38cb41.", + "timestamp": "2011-12-12T14:27:31+02:00", + "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", + "author": { + "name": "Jordi Mallach", + "email": "jordi@softcatala.org" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + }, + { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + }, + "added": ["CHANGELOG"], + "modified": ["app/controller/application.rb"], + "removed": [] + } + ], + "total_commits_count": 4 +} +``` + +### Tag events + +Triggered when you create (or delete) tags to the repository. + +**Request header**: + +``` +X-Gitlab-Event: Tag Push Hook +``` + +**Request body:** + +```json +{ + "object_kind": "tag_push", + "before": "0000000000000000000000000000000000000000", + "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", + "ref": "refs/tags/v1.0.0", + "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", + "user_id": 1, + "user_name": "John Smith", + "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", + "project_id": 1, + "project":{ + "name":"Example", + "description":"", + "web_url":"http://example.com/jsmith/example", + "avatar_url":null, + "git_ssh_url":"git@example.com:jsmith/example.git", + "git_http_url":"http://example.com/jsmith/example.git", + "namespace":"Jsmith", + "visibility_level":0, + "path_with_namespace":"jsmith/example", + "default_branch":"master", + "homepage":"http://example.com/jsmith/example", + "url":"git@example.com:jsmith/example.git", + "ssh_url":"git@example.com:jsmith/example.git", + "http_url":"http://example.com/jsmith/example.git" + }, + "repository":{ + "name": "Example", + "url": "ssh://git@example.com/jsmith/example.git", + "description": "", + "homepage": "http://example.com/jsmith/example", + "git_http_url":"http://example.com/jsmith/example.git", + "git_ssh_url":"git@example.com:jsmith/example.git", + "visibility_level":0 + }, + "commits": [], + "total_commits_count": 0 +} +``` + +### Issues events + +Triggered when a new issue is created or an existing issue was updated/closed/reopened. + +**Request header**: + +``` +X-Gitlab-Event: Issue Hook +``` + +**Request body:** + +```json +{ + "object_kind": "issue", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlabhq/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", + "namespace":"GitlabHQ", + "visibility_level":20, + "path_with_namespace":"gitlabhq/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlabhq/gitlab-test", + "url":"http://example.com/gitlabhq/gitlab-test.git", + "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "http_url":"http://example.com/gitlabhq/gitlab-test.git" + }, + "repository":{ + "name": "Gitlab Test", + "url": "http://example.com/gitlabhq/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlabhq/gitlab-test" + }, + "object_attributes": { + "id": 301, + "title": "New API: create/update/delete file", + "assignee_id": 51, + "author_id": 51, + "project_id": 14, + "created_at": "2013-12-03T17:15:43Z", + "updated_at": "2013-12-03T17:15:43Z", + "position": 0, + "branch_name": null, + "description": "Create new API for manipulations with repository", + "milestone_id": null, + "state": "opened", + "iid": 23, + "url": "http://example.com/diaspora/issues/23", + "action": "open" + }, + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } +} +``` +### Comment events + +Triggered when a new comment is made on commits, merge requests, issues, and code snippets. +The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The +payload will also include information about the target of the comment. For example, +a comment on a issue will include the specific issue information under the `issue` key. +Valid target types: + +1. `commit` +2. `merge_request` +3. `issue` +4. `snippet` + +#### Comment on commit + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlabhq/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", + "namespace":"GitlabHQ", + "visibility_level":20, + "path_with_namespace":"gitlabhq/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlabhq/gitlab-test", + "url":"http://example.com/gitlabhq/gitlab-test.git", + "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", + "http_url":"http://example.com/gitlabhq/gitlab-test.git" + }, + "repository":{ + "name": "Gitlab Test", + "url": "http://example.com/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1243, + "note": "This is a commit comment. How does this work?", + "noteable_type": "Commit", + "author_id": 1, + "created_at": "2015-05-17 18:08:09 UTC", + "updated_at": "2015-05-17 18:08:09 UTC", + "project_id": 5, + "attachment":null, + "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1", + "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "noteable_id": null, + "system": false, + "st_diff": { + "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n", + "new_path": "six", + "old_path": "six", + "a_mode": "0", + "b_mode": "160000", + "new_file": true, + "renamed_file": false, + "deleted_file": false + }, + "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243" + }, + "commit": { + "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", + "timestamp": "2014-02-27T10:06:20+02:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660", + "author": { + "name": "Dmitriy Zaporozhets", + "email": "dmitriy.zaporozhets@gmail.com" + } + } +} +``` + +#### Comment on merge request + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name": "Gitlab Test", + "url": "http://localhost/gitlab-org/gitlab-test.git", + "description": "Aut reprehenderit ut est.", + "homepage": "http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1244, + "note": "This MR needs work.", + "noteable_type": "MergeRequest", + "author_id": 1, + "created_at": "2015-05-17 18:21:36 UTC", + "updated_at": "2015-05-17 18:21:36 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 7, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244" + }, + "merge_request": { + "id": 7, + "target_branch": "markdown", + "source_branch": "master", + "source_project_id": 5, + "author_id": 8, + "assignee_id": 28, + "title": "Tempora et eos debitis quae laborum et.", + "created_at": "2015-03-01 20:12:53 UTC", + "updated_at": "2015-03-21 18:27:27 UTC", + "milestone_id": 11, + "state": "opened", + "merge_status": "cannot_be_merged", + "target_project_id": 5, + "iid": 1, + "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.", + "position": 0, + "locked_at": null, + "source":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "target": { + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "last_commit": { + "id": "562e173be03b8ff2efb05345d12df18815438a4b", + "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n", + "timestamp": "2015-04-08T21: 00:25-07:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b", + "author": { + "name": "John Smith", + "email": "john@example.com" + } + }, + "work_in_progress": false, + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } + } +} +``` + +#### Comment on issue + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name":"diaspora", + "url":"git@example.com:mike/diaspora.git", + "description":"", + "homepage":"http://example.com/mike/diaspora" + }, + "object_attributes": { + "id": 1241, + "note": "Hello world", + "noteable_type": "Issue", + "author_id": 1, + "created_at": "2015-05-17 17:06:40 UTC", + "updated_at": "2015-05-17 17:06:40 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 92, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241" + }, + "issue": { + "id": 92, + "title": "test", + "assignee_id": null, + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-12 14:53:17 UTC", + "updated_at": "2015-04-26 08:28:42 UTC", + "position": 0, + "branch_name": null, + "description": "test", + "milestone_id": null, + "state": "closed", + "iid": 17 + } +} +``` + +#### Comment on code snippet + +**Request header**: + +``` +X-Gitlab-Event: Note Hook +``` + +**Request body:** + +```json +{ + "object_kind": "note", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "project_id": 5, + "project":{ + "name":"Gitlab Test", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/gitlab-org/gitlab-test", + "avatar_url":null, + "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", + "namespace":"Gitlab Org", + "visibility_level":10, + "path_with_namespace":"gitlab-org/gitlab-test", + "default_branch":"master", + "homepage":"http://example.com/gitlab-org/gitlab-test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", + "http_url":"http://example.com/gitlab-org/gitlab-test.git" + }, + "repository":{ + "name":"Gitlab Test", + "url":"http://example.com/gitlab-org/gitlab-test.git", + "description":"Aut reprehenderit ut est.", + "homepage":"http://example.com/gitlab-org/gitlab-test" + }, + "object_attributes": { + "id": 1245, + "note": "Is this snippet doing what it's supposed to be doing?", + "noteable_type": "Snippet", + "author_id": 1, + "created_at": "2015-05-17 18:35:50 UTC", + "updated_at": "2015-05-17 18:35:50 UTC", + "project_id": 5, + "attachment": null, + "line_code": null, + "commit_id": "", + "noteable_id": 53, + "system": false, + "st_diff": null, + "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245" + }, + "snippet": { + "id": 53, + "title": "test", + "content": "puts 'Hello world'", + "author_id": 1, + "project_id": 5, + "created_at": "2015-04-09 02:40:38 UTC", + "updated_at": "2015-04-09 02:40:38 UTC", + "file_name": "test.rb", + "expires_at": null, + "type": "ProjectSnippet", + "visibility_level": 0 + } +} +``` + +### Merge request events + +Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch. + +**Request header**: + +``` +X-Gitlab-Event: Merge Request Hook +``` + +**Request body:** + +```json +{ + "object_kind": "merge_request", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + }, + "object_attributes": { + "id": 99, + "target_branch": "master", + "source_branch": "ms-viewport", + "source_project_id": 14, + "author_id": 51, + "assignee_id": 6, + "title": "MS-Viewport", + "created_at": "2013-12-03T17:23:34Z", + "updated_at": "2013-12-03T17:23:34Z", + "st_commits": null, + "st_diffs": null, + "milestone_id": null, + "state": "opened", + "merge_status": "unchecked", + "target_project_id": 14, + "iid": 1, + "description": "", + "source":{ + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":null, + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility_level":20, + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" + }, + "target": { + "name":"Awesome Project", + "description":"Aut reprehenderit ut est.", + "web_url":"http://example.com/awesome_space/awesome_project", + "avatar_url":null, + "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", + "git_http_url":"http://example.com/awesome_space/awesome_project.git", + "namespace":"Awesome Space", + "visibility_level":20, + "path_with_namespace":"awesome_space/awesome_project", + "default_branch":"master", + "homepage":"http://example.com/awesome_space/awesome_project", + "url":"http://example.com/awesome_space/awesome_project.git", + "ssh_url":"git@example.com:awesome_space/awesome_project.git", + "http_url":"http://example.com/awesome_space/awesome_project.git" + }, + "last_commit": { + "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "message": "fixed readme", + "timestamp": "2012-01-03T23:36:29+02:00", + "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", + "author": { + "name": "GitLab dev user", + "email": "gitlabdev@dv6700.(none)" + } + }, + "work_in_progress": false, + "url": "http://example.com/diaspora/merge_requests/1", + "action": "open", + "assignee": { + "name": "User1", + "username": "user1", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" + } + } +} +``` + +### Wiki Page events + +Triggered when a wiki page is created or edited. + +**Request Header**: + +``` +X-Gitlab-Event: Wiki Page Hook +``` + +**Request Body**: + +```json +{ + "object_kind": "wiki_page", + "user": { + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" + }, + "project": { + "name": "awesome-project", + "description": "This is awesome", + "web_url": "http://example.com/root/awesome-project", + "avatar_url": null, + "git_ssh_url": "git@example.com:root/awesome-project.git", + "git_http_url": "http://example.com/root/awesome-project.git", + "namespace": "root", + "visibility_level": 0, + "path_with_namespace": "root/awesome-project", + "default_branch": "master", + "homepage": "http://example.com/root/awesome-project", + "url": "git@example.com:root/awesome-project.git", + "ssh_url": "git@example.com:root/awesome-project.git", + "http_url": "http://example.com/root/awesome-project.git" + }, + "wiki": { + "web_url": "http://example.com/root/awesome-project/wikis/home", + "git_ssh_url": "git@example.com:root/awesome-project.wiki.git", + "git_http_url": "http://example.com/root/awesome-project.wiki.git", + "path_with_namespace": "root/awesome-project.wiki", + "default_branch": "master" + }, + "object_attributes": { + "title": "Awesome", + "content": "awesome content goes here", + "format": "markdown", + "message": "adding an awesome page to the wiki", + "slug": "awesome", + "url": "http://example.com/root/awesome-project/wikis/awesome", + "action": "create" + } +} +``` + +### Pipeline events + +Triggered on status change of Pipeline. + +**Request Header**: + +``` +X-Gitlab-Event: Pipeline Hook +``` + +**Request Body**: + +```json +{ + "object_kind": "pipeline", + "object_attributes":{ + "id": 31, + "ref": "master", + "tag": false, + "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "status": "success", + "stages":[ + "build", + "test", + "deploy" + ], + "created_at": "2016-08-12 15:23:28 UTC", + "finished_at": "2016-08-12 15:26:29 UTC", + "duration": 63 + }, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "project":{ + "name": "Gitlab Test", + "description": "Atque in sunt eos similique dolores voluptatem.", + "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test", + "avatar_url": null, + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", + "namespace": "Gitlab Org", + "visibility_level": 20, + "path_with_namespace": "gitlab-org/gitlab-test", + "default_branch": "master" + }, + "commit":{ + "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "message": "test\n", + "timestamp": "2016-08-12T17:23:21+02:00", + "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2", + "author":{ + "name": "User", + "email": "user@gitlab.com" + } + }, + "builds":[ + { + "id": 380, + "stage": "deploy", + "name": "production", + "status": "skipped", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": null, + "finished_at": null, + "when": "manual", + "manual": true, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 377, + "stage": "test", + "name": "test-image", + "status": "success", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": "2016-08-12 15:26:12 UTC", + "finished_at": null, + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 378, + "stage": "test", + "name": "test-build", + "status": "success", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": "2016-08-12 15:26:12 UTC", + "finished_at": "2016-08-12 15:26:29 UTC", + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 376, + "stage": "build", + "name": "build-image", + "status": "success", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": "2016-08-12 15:24:56 UTC", + "finished_at": "2016-08-12 15:25:26 UTC", + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + }, + { + "id": 379, + "stage": "deploy", + "name": "staging", + "status": "created", + "created_at": "2016-08-12 15:23:28 UTC", + "started_at": null, + "finished_at": null, + "when": "on_success", + "manual": false, + "user":{ + "name": "Administrator", + "username": "root", + "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" + }, + "runner": null, + "artifacts_file":{ + "filename": null, + "size": null + } + } + ] +} +``` + +### Build events + +Triggered on status change of a Build. + +**Request Header**: + +``` +X-Gitlab-Event: Build Hook +``` + +**Request Body**: + +```json +{ + "object_kind": "build", + "ref": "gitlab-script-trigger", + "tag": false, + "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "build_id": 1977, + "build_name": "test", + "build_stage": "test", + "build_status": "created", + "build_started_at": null, + "build_finished_at": null, + "build_duration": null, + "build_allow_failure": false, + "project_id": 380, + "project_name": "gitlab-org/gitlab-test", + "user": { + "id": 3, + "name": "User", + "email": "user@gitlab.com" + }, + "commit": { + "id": 2366, + "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", + "message": "test\n", + "author_name": "User", + "author_email": "user@gitlab.com", + "status": "created", + "duration": null, + "started_at": null, + "finished_at": null + }, + "repository": { + "name": "gitlab_test", + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "description": "Atque in sunt eos similique dolores voluptatem.", + "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test", + "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", + "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", + "visibility_level": 20 + } +} +``` + +## Example webhook receiver + +If you want to see GitLab's webhooks in action for testing purposes you can use +a simple echo script running in a console session. For the following script to +work you need to have Ruby installed. + +Save the following file as `print_http_body.rb`: + +```ruby +require 'webrick' + +server = WEBrick::HTTPServer.new(:Port => ARGV.first) +server.mount_proc '/' do |req, res| + puts req.body +end + +trap 'INT' do + server.shutdown +end +server.start +``` + +Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb +8000`. Then add your server as a webhook receiver in GitLab as +`http://my.host:8000/`. + +When you press 'Test Hook' in GitLab, you should see something like this in the +console: + +``` +{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>} +example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0 +- -> / +``` diff --git a/doc/web_hooks/ssl.png b/doc/web_hooks/ssl.png deleted file mode 100644 index 21ddec4ebdf..00000000000 Binary files a/doc/web_hooks/ssl.png and /dev/null differ diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md index 1659dd1f6cb..0ebe5eea173 100644 --- a/doc/web_hooks/web_hooks.md +++ b/doc/web_hooks/web_hooks.md @@ -1,1025 +1 @@ -# Webhooks - ->**Note:** -Starting from GitLab 8.5: -- the `repository` key is deprecated in favor of the `project` key -- the `project.ssh_url` key is deprecated in favor of the `project.git_ssh_url` key -- the `project.http_url` key is deprecated in favor of the `project.git_http_url` key - -Project webhooks allow you to trigger a URL if for example new code is pushed or -a new issue is created. You can configure webhooks to listen for specific events -like pushes, issues or merge requests. GitLab will send a POST request with data -to the webhook URL. - -Webhooks can be used to update an external issue tracker, trigger CI builds, -update a backup mirror, or even deploy to your production server. - -Navigate to the webhooks page by choosing **Webhooks** from your project's -settings which can be found under the wheel icon in the upper right corner. - -## Webhook endpoint tips - -If you are writing your own endpoint (web server) that will receive -GitLab webhooks keep in mind the following things: - -- Your endpoint should send its HTTP response as fast as possible. If - you wait too long, GitLab may decide the hook failed and retry it. -- Your endpoint should ALWAYS return a valid HTTP response. If you do - not do this then GitLab will think the hook failed and retry it. - Most HTTP libraries take care of this for you automatically but if - you are writing a low-level hook this is important to remember. -- GitLab ignores the HTTP status code returned by your endpoint. - -## Secret token - -If you specify a secret token, it will be sent with the hook request in the -`X-Gitlab-Token` HTTP header. Your webhook endpoint can check that to verify -that the request is legitimate. - -## SSL verification - -By default, the SSL certificate of the webhook endpoint is verified based on -an internal list of Certificate Authorities, which means the certificate cannot -be self-signed. - -You can turn this off in the webhook settings in your GitLab projects. - -![SSL Verification](ssl.png) - -## Events - -Below are described the supported events. - -### Push events - -Triggered when you push to the repository except when pushing tags. - -**Request header**: - -``` -X-Gitlab-Event: Push Hook -``` - -**Request body:** - -```json -{ - "object_kind": "push", - "before": "95790bf891e76fee5e1747ab589903a6a1f80f22", - "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "ref": "refs/heads/master", - "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "user_id": 4, - "user_name": "John Smith", - "user_email": "john@example.com", - "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", - "project_id": 15, - "project":{ - "name":"Diaspora", - "description":"", - "web_url":"http://example.com/mike/diaspora", - "avatar_url":null, - "git_ssh_url":"git@example.com:mike/diaspora.git", - "git_http_url":"http://example.com/mike/diaspora.git", - "namespace":"Mike", - "visibility_level":0, - "path_with_namespace":"mike/diaspora", - "default_branch":"master", - "homepage":"http://example.com/mike/diaspora", - "url":"git@example.com:mike/diaspora.git", - "ssh_url":"git@example.com:mike/diaspora.git", - "http_url":"http://example.com/mike/diaspora.git" - }, - "repository":{ - "name": "Diaspora", - "url": "git@example.com:mike/diaspora.git", - "description": "", - "homepage": "http://example.com/mike/diaspora", - "git_http_url":"http://example.com/mike/diaspora.git", - "git_ssh_url":"git@example.com:mike/diaspora.git", - "visibility_level":0 - }, - "commits": [ - { - "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", - "message": "Update Catalan translation to e38cb41.", - "timestamp": "2011-12-12T14:27:31+02:00", - "url": "http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327", - "author": { - "name": "Jordi Mallach", - "email": "jordi@softcatala.org" - }, - "added": ["CHANGELOG"], - "modified": ["app/controller/application.rb"], - "removed": [] - }, - { - "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "message": "fixed readme", - "timestamp": "2012-01-03T23:36:29+02:00", - "url": "http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "author": { - "name": "GitLab dev user", - "email": "gitlabdev@dv6700.(none)" - }, - "added": ["CHANGELOG"], - "modified": ["app/controller/application.rb"], - "removed": [] - } - ], - "total_commits_count": 4 -} -``` - -### Tag events - -Triggered when you create (or delete) tags to the repository. - -**Request header**: - -``` -X-Gitlab-Event: Tag Push Hook -``` - -**Request body:** - -```json -{ - "object_kind": "tag_push", - "before": "0000000000000000000000000000000000000000", - "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", - "ref": "refs/tags/v1.0.0", - "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7", - "user_id": 1, - "user_name": "John Smith", - "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80", - "project_id": 1, - "project":{ - "name":"Example", - "description":"", - "web_url":"http://example.com/jsmith/example", - "avatar_url":null, - "git_ssh_url":"git@example.com:jsmith/example.git", - "git_http_url":"http://example.com/jsmith/example.git", - "namespace":"Jsmith", - "visibility_level":0, - "path_with_namespace":"jsmith/example", - "default_branch":"master", - "homepage":"http://example.com/jsmith/example", - "url":"git@example.com:jsmith/example.git", - "ssh_url":"git@example.com:jsmith/example.git", - "http_url":"http://example.com/jsmith/example.git" - }, - "repository":{ - "name": "Example", - "url": "ssh://git@example.com/jsmith/example.git", - "description": "", - "homepage": "http://example.com/jsmith/example", - "git_http_url":"http://example.com/jsmith/example.git", - "git_ssh_url":"git@example.com:jsmith/example.git", - "visibility_level":0 - }, - "commits": [], - "total_commits_count": 0 -} -``` - -### Issues events - -Triggered when a new issue is created or an existing issue was updated/closed/reopened. - -**Request header**: - -``` -X-Gitlab-Event: Issue Hook -``` - -**Request body:** - -```json -{ - "object_kind": "issue", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - }, - "project":{ - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlabhq/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", - "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", - "namespace":"GitlabHQ", - "visibility_level":20, - "path_with_namespace":"gitlabhq/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlabhq/gitlab-test", - "url":"http://example.com/gitlabhq/gitlab-test.git", - "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", - "http_url":"http://example.com/gitlabhq/gitlab-test.git" - }, - "repository":{ - "name": "Gitlab Test", - "url": "http://example.com/gitlabhq/gitlab-test.git", - "description": "Aut reprehenderit ut est.", - "homepage": "http://example.com/gitlabhq/gitlab-test" - }, - "object_attributes": { - "id": 301, - "title": "New API: create/update/delete file", - "assignee_id": 51, - "author_id": 51, - "project_id": 14, - "created_at": "2013-12-03T17:15:43Z", - "updated_at": "2013-12-03T17:15:43Z", - "position": 0, - "branch_name": null, - "description": "Create new API for manipulations with repository", - "milestone_id": null, - "state": "opened", - "iid": 23, - "url": "http://example.com/diaspora/issues/23", - "action": "open" - }, - "assignee": { - "name": "User1", - "username": "user1", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - } -} -``` -### Comment events - -Triggered when a new comment is made on commits, merge requests, issues, and code snippets. -The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The -payload will also include information about the target of the comment. For example, -a comment on a issue will include the specific issue information under the `issue` key. -Valid target types: - -1. `commit` -2. `merge_request` -3. `issue` -4. `snippet` - -#### Comment on commit - -**Request header**: - -``` -X-Gitlab-Event: Note Hook -``` - -**Request body:** - -```json -{ - "object_kind": "note", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - }, - "project_id": 5, - "project":{ - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlabhq/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlabhq/gitlab-test.git", - "git_http_url":"http://example.com/gitlabhq/gitlab-test.git", - "namespace":"GitlabHQ", - "visibility_level":20, - "path_with_namespace":"gitlabhq/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlabhq/gitlab-test", - "url":"http://example.com/gitlabhq/gitlab-test.git", - "ssh_url":"git@example.com:gitlabhq/gitlab-test.git", - "http_url":"http://example.com/gitlabhq/gitlab-test.git" - }, - "repository":{ - "name": "Gitlab Test", - "url": "http://example.com/gitlab-org/gitlab-test.git", - "description": "Aut reprehenderit ut est.", - "homepage": "http://example.com/gitlab-org/gitlab-test" - }, - "object_attributes": { - "id": 1243, - "note": "This is a commit comment. How does this work?", - "noteable_type": "Commit", - "author_id": 1, - "created_at": "2015-05-17 18:08:09 UTC", - "updated_at": "2015-05-17 18:08:09 UTC", - "project_id": 5, - "attachment":null, - "line_code": "bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1", - "commit_id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", - "noteable_id": null, - "system": false, - "st_diff": { - "diff": "--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n", - "new_path": "six", - "old_path": "six", - "a_mode": "0", - "b_mode": "160000", - "new_file": true, - "renamed_file": false, - "deleted_file": false - }, - "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243" - }, - "commit": { - "id": "cfe32cf61b73a0d5e9f13e774abde7ff789b1660", - "message": "Add submodule\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n", - "timestamp": "2014-02-27T10:06:20+02:00", - "url": "http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660", - "author": { - "name": "Dmitriy Zaporozhets", - "email": "dmitriy.zaporozhets@gmail.com" - } - } -} -``` - -#### Comment on merge request - -**Request header**: - -``` -X-Gitlab-Event: Note Hook -``` - -**Request body:** - -```json -{ - "object_kind": "note", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - }, - "project_id": 5, - "project":{ - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlab-org/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", - "namespace":"Gitlab Org", - "visibility_level":10, - "path_with_namespace":"gitlab-org/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlab-org/gitlab-test", - "url":"http://example.com/gitlab-org/gitlab-test.git", - "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "http_url":"http://example.com/gitlab-org/gitlab-test.git" - }, - "repository":{ - "name": "Gitlab Test", - "url": "http://localhost/gitlab-org/gitlab-test.git", - "description": "Aut reprehenderit ut est.", - "homepage": "http://example.com/gitlab-org/gitlab-test" - }, - "object_attributes": { - "id": 1244, - "note": "This MR needs work.", - "noteable_type": "MergeRequest", - "author_id": 1, - "created_at": "2015-05-17 18:21:36 UTC", - "updated_at": "2015-05-17 18:21:36 UTC", - "project_id": 5, - "attachment": null, - "line_code": null, - "commit_id": "", - "noteable_id": 7, - "system": false, - "st_diff": null, - "url": "http://example.com/gitlab-org/gitlab-test/merge_requests/1#note_1244" - }, - "merge_request": { - "id": 7, - "target_branch": "markdown", - "source_branch": "master", - "source_project_id": 5, - "author_id": 8, - "assignee_id": 28, - "title": "Tempora et eos debitis quae laborum et.", - "created_at": "2015-03-01 20:12:53 UTC", - "updated_at": "2015-03-21 18:27:27 UTC", - "milestone_id": 11, - "state": "opened", - "merge_status": "cannot_be_merged", - "target_project_id": 5, - "iid": 1, - "description": "Et voluptas corrupti assumenda temporibus. Architecto cum animi eveniet amet asperiores. Vitae numquam voluptate est natus sit et ad id.", - "position": 0, - "locked_at": null, - "source":{ - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlab-org/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", - "namespace":"Gitlab Org", - "visibility_level":10, - "path_with_namespace":"gitlab-org/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlab-org/gitlab-test", - "url":"http://example.com/gitlab-org/gitlab-test.git", - "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "http_url":"http://example.com/gitlab-org/gitlab-test.git" - }, - "target": { - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlab-org/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", - "namespace":"Gitlab Org", - "visibility_level":10, - "path_with_namespace":"gitlab-org/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlab-org/gitlab-test", - "url":"http://example.com/gitlab-org/gitlab-test.git", - "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "http_url":"http://example.com/gitlab-org/gitlab-test.git" - }, - "last_commit": { - "id": "562e173be03b8ff2efb05345d12df18815438a4b", - "message": "Merge branch 'another-branch' into 'master'\n\nCheck in this test\n", - "timestamp": "2015-04-08T21: 00:25-07:00", - "url": "http://example.com/gitlab-org/gitlab-test/commit/562e173be03b8ff2efb05345d12df18815438a4b", - "author": { - "name": "John Smith", - "email": "john@example.com" - } - }, - "work_in_progress": false, - "assignee": { - "name": "User1", - "username": "user1", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - } - } -} -``` - -#### Comment on issue - -**Request header**: - -``` -X-Gitlab-Event: Note Hook -``` - -**Request body:** - -```json -{ - "object_kind": "note", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - }, - "project_id": 5, - "project":{ - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlab-org/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", - "namespace":"Gitlab Org", - "visibility_level":10, - "path_with_namespace":"gitlab-org/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlab-org/gitlab-test", - "url":"http://example.com/gitlab-org/gitlab-test.git", - "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "http_url":"http://example.com/gitlab-org/gitlab-test.git" - }, - "repository":{ - "name":"diaspora", - "url":"git@example.com:mike/diaspora.git", - "description":"", - "homepage":"http://example.com/mike/diaspora" - }, - "object_attributes": { - "id": 1241, - "note": "Hello world", - "noteable_type": "Issue", - "author_id": 1, - "created_at": "2015-05-17 17:06:40 UTC", - "updated_at": "2015-05-17 17:06:40 UTC", - "project_id": 5, - "attachment": null, - "line_code": null, - "commit_id": "", - "noteable_id": 92, - "system": false, - "st_diff": null, - "url": "http://example.com/gitlab-org/gitlab-test/issues/17#note_1241" - }, - "issue": { - "id": 92, - "title": "test", - "assignee_id": null, - "author_id": 1, - "project_id": 5, - "created_at": "2015-04-12 14:53:17 UTC", - "updated_at": "2015-04-26 08:28:42 UTC", - "position": 0, - "branch_name": null, - "description": "test", - "milestone_id": null, - "state": "closed", - "iid": 17 - } -} -``` - -#### Comment on code snippet - -**Request header**: - -``` -X-Gitlab-Event: Note Hook -``` - -**Request body:** - -```json -{ - "object_kind": "note", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - }, - "project_id": 5, - "project":{ - "name":"Gitlab Test", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/gitlab-org/gitlab-test", - "avatar_url":null, - "git_ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "git_http_url":"http://example.com/gitlab-org/gitlab-test.git", - "namespace":"Gitlab Org", - "visibility_level":10, - "path_with_namespace":"gitlab-org/gitlab-test", - "default_branch":"master", - "homepage":"http://example.com/gitlab-org/gitlab-test", - "url":"http://example.com/gitlab-org/gitlab-test.git", - "ssh_url":"git@example.com:gitlab-org/gitlab-test.git", - "http_url":"http://example.com/gitlab-org/gitlab-test.git" - }, - "repository":{ - "name":"Gitlab Test", - "url":"http://example.com/gitlab-org/gitlab-test.git", - "description":"Aut reprehenderit ut est.", - "homepage":"http://example.com/gitlab-org/gitlab-test" - }, - "object_attributes": { - "id": 1245, - "note": "Is this snippet doing what it's supposed to be doing?", - "noteable_type": "Snippet", - "author_id": 1, - "created_at": "2015-05-17 18:35:50 UTC", - "updated_at": "2015-05-17 18:35:50 UTC", - "project_id": 5, - "attachment": null, - "line_code": null, - "commit_id": "", - "noteable_id": 53, - "system": false, - "st_diff": null, - "url": "http://example.com/gitlab-org/gitlab-test/snippets/53#note_1245" - }, - "snippet": { - "id": 53, - "title": "test", - "content": "puts 'Hello world'", - "author_id": 1, - "project_id": 5, - "created_at": "2015-04-09 02:40:38 UTC", - "updated_at": "2015-04-09 02:40:38 UTC", - "file_name": "test.rb", - "expires_at": null, - "type": "ProjectSnippet", - "visibility_level": 0 - } -} -``` - -### Merge request events - -Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch. - -**Request header**: - -``` -X-Gitlab-Event: Merge Request Hook -``` - -**Request body:** - -```json -{ - "object_kind": "merge_request", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - }, - "object_attributes": { - "id": 99, - "target_branch": "master", - "source_branch": "ms-viewport", - "source_project_id": 14, - "author_id": 51, - "assignee_id": 6, - "title": "MS-Viewport", - "created_at": "2013-12-03T17:23:34Z", - "updated_at": "2013-12-03T17:23:34Z", - "st_commits": null, - "st_diffs": null, - "milestone_id": null, - "state": "opened", - "merge_status": "unchecked", - "target_project_id": 14, - "iid": 1, - "description": "", - "source":{ - "name":"Awesome Project", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/awesome_space/awesome_project", - "avatar_url":null, - "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", - "git_http_url":"http://example.com/awesome_space/awesome_project.git", - "namespace":"Awesome Space", - "visibility_level":20, - "path_with_namespace":"awesome_space/awesome_project", - "default_branch":"master", - "homepage":"http://example.com/awesome_space/awesome_project", - "url":"http://example.com/awesome_space/awesome_project.git", - "ssh_url":"git@example.com:awesome_space/awesome_project.git", - "http_url":"http://example.com/awesome_space/awesome_project.git" - }, - "target": { - "name":"Awesome Project", - "description":"Aut reprehenderit ut est.", - "web_url":"http://example.com/awesome_space/awesome_project", - "avatar_url":null, - "git_ssh_url":"git@example.com:awesome_space/awesome_project.git", - "git_http_url":"http://example.com/awesome_space/awesome_project.git", - "namespace":"Awesome Space", - "visibility_level":20, - "path_with_namespace":"awesome_space/awesome_project", - "default_branch":"master", - "homepage":"http://example.com/awesome_space/awesome_project", - "url":"http://example.com/awesome_space/awesome_project.git", - "ssh_url":"git@example.com:awesome_space/awesome_project.git", - "http_url":"http://example.com/awesome_space/awesome_project.git" - }, - "last_commit": { - "id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "message": "fixed readme", - "timestamp": "2012-01-03T23:36:29+02:00", - "url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7", - "author": { - "name": "GitLab dev user", - "email": "gitlabdev@dv6700.(none)" - } - }, - "work_in_progress": false, - "url": "http://example.com/diaspora/merge_requests/1", - "action": "open", - "assignee": { - "name": "User1", - "username": "user1", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon" - } - } -} -``` - -### Wiki Page events - -Triggered when a wiki page is created or edited. - -**Request Header**: - -``` -X-Gitlab-Event: Wiki Page Hook -``` - -**Request Body**: - -```json -{ - "object_kind": "wiki_page", - "user": { - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon" - }, - "project": { - "name": "awesome-project", - "description": "This is awesome", - "web_url": "http://example.com/root/awesome-project", - "avatar_url": null, - "git_ssh_url": "git@example.com:root/awesome-project.git", - "git_http_url": "http://example.com/root/awesome-project.git", - "namespace": "root", - "visibility_level": 0, - "path_with_namespace": "root/awesome-project", - "default_branch": "master", - "homepage": "http://example.com/root/awesome-project", - "url": "git@example.com:root/awesome-project.git", - "ssh_url": "git@example.com:root/awesome-project.git", - "http_url": "http://example.com/root/awesome-project.git" - }, - "wiki": { - "web_url": "http://example.com/root/awesome-project/wikis/home", - "git_ssh_url": "git@example.com:root/awesome-project.wiki.git", - "git_http_url": "http://example.com/root/awesome-project.wiki.git", - "path_with_namespace": "root/awesome-project.wiki", - "default_branch": "master" - }, - "object_attributes": { - "title": "Awesome", - "content": "awesome content goes here", - "format": "markdown", - "message": "adding an awesome page to the wiki", - "slug": "awesome", - "url": "http://example.com/root/awesome-project/wikis/awesome", - "action": "create" - } -} -``` - -### Pipeline events - -Triggered on status change of Pipeline. - -**Request Header**: - -``` -X-Gitlab-Event: Pipeline Hook -``` - -**Request Body**: - -```json -{ - "object_kind": "pipeline", - "object_attributes":{ - "id": 31, - "ref": "master", - "tag": false, - "sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", - "before_sha": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", - "status": "success", - "stages":[ - "build", - "test", - "deploy" - ], - "created_at": "2016-08-12 15:23:28 UTC", - "finished_at": "2016-08-12 15:26:29 UTC", - "duration": 63 - }, - "user":{ - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" - }, - "project":{ - "name": "Gitlab Test", - "description": "Atque in sunt eos similique dolores voluptatem.", - "web_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test", - "avatar_url": null, - "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", - "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", - "namespace": "Gitlab Org", - "visibility_level": 20, - "path_with_namespace": "gitlab-org/gitlab-test", - "default_branch": "master" - }, - "commit":{ - "id": "bcbb5ec396a2c0f828686f14fac9b80b780504f2", - "message": "test\n", - "timestamp": "2016-08-12T17:23:21+02:00", - "url": "http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2", - "author":{ - "name": "User", - "email": "user@gitlab.com" - } - }, - "builds":[ - { - "id": 380, - "stage": "deploy", - "name": "production", - "status": "skipped", - "created_at": "2016-08-12 15:23:28 UTC", - "started_at": null, - "finished_at": null, - "when": "manual", - "manual": true, - "user":{ - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" - }, - "runner": null, - "artifacts_file":{ - "filename": null, - "size": null - } - }, - { - "id": 377, - "stage": "test", - "name": "test-image", - "status": "success", - "created_at": "2016-08-12 15:23:28 UTC", - "started_at": "2016-08-12 15:26:12 UTC", - "finished_at": null, - "when": "on_success", - "manual": false, - "user":{ - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" - }, - "runner": null, - "artifacts_file":{ - "filename": null, - "size": null - } - }, - { - "id": 378, - "stage": "test", - "name": "test-build", - "status": "success", - "created_at": "2016-08-12 15:23:28 UTC", - "started_at": "2016-08-12 15:26:12 UTC", - "finished_at": "2016-08-12 15:26:29 UTC", - "when": "on_success", - "manual": false, - "user":{ - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" - }, - "runner": null, - "artifacts_file":{ - "filename": null, - "size": null - } - }, - { - "id": 376, - "stage": "build", - "name": "build-image", - "status": "success", - "created_at": "2016-08-12 15:23:28 UTC", - "started_at": "2016-08-12 15:24:56 UTC", - "finished_at": "2016-08-12 15:25:26 UTC", - "when": "on_success", - "manual": false, - "user":{ - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" - }, - "runner": null, - "artifacts_file":{ - "filename": null, - "size": null - } - }, - { - "id": 379, - "stage": "deploy", - "name": "staging", - "status": "created", - "created_at": "2016-08-12 15:23:28 UTC", - "started_at": null, - "finished_at": null, - "when": "on_success", - "manual": false, - "user":{ - "name": "Administrator", - "username": "root", - "avatar_url": "http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon" - }, - "runner": null, - "artifacts_file":{ - "filename": null, - "size": null - } - } - ] -} -``` - -### Build events - -Triggered on status change of a Build. - -**Request Header**: - -``` -X-Gitlab-Event: Build Hook -``` - -**Request Body**: - -```json -{ - "object_kind": "build", - "ref": "gitlab-script-trigger", - "tag": false, - "before_sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", - "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", - "build_id": 1977, - "build_name": "test", - "build_stage": "test", - "build_status": "created", - "build_started_at": null, - "build_finished_at": null, - "build_duration": null, - "build_allow_failure": false, - "project_id": 380, - "project_name": "gitlab-org/gitlab-test", - "user": { - "id": 3, - "name": "User", - "email": "user@gitlab.com" - }, - "commit": { - "id": 2366, - "sha": "2293ada6b400935a1378653304eaf6221e0fdb8f", - "message": "test\n", - "author_name": "User", - "author_email": "user@gitlab.com", - "status": "created", - "duration": null, - "started_at": null, - "finished_at": null - }, - "repository": { - "name": "gitlab_test", - "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", - "description": "Atque in sunt eos similique dolores voluptatem.", - "homepage": "http://192.168.64.1:3005/gitlab-org/gitlab-test", - "git_ssh_url": "git@192.168.64.1:gitlab-org/gitlab-test.git", - "git_http_url": "http://192.168.64.1:3005/gitlab-org/gitlab-test.git", - "visibility_level": 20 - } -} -``` - -## Example webhook receiver - -If you want to see GitLab's webhooks in action for testing purposes you can use -a simple echo script running in a console session. For the following script to -work you need to have Ruby installed. - -Save the following file as `print_http_body.rb`: - -```ruby -require 'webrick' - -server = WEBrick::HTTPServer.new(:Port => ARGV.first) -server.mount_proc '/' do |req, res| - puts req.body -end - -trap 'INT' do - server.shutdown -end -server.start -``` - -Pick an unused port (e.g. 8000) and start the script: `ruby print_http_body.rb -8000`. Then add your server as a webhook receiver in GitLab as -`http://my.host:8000/`. - -When you press 'Test Hook' in GitLab, you should see something like this in the -console: - -``` -{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>} -example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0 -- -> / -``` +This document was moved to [project/integrations/webhooks](../user/project/integrations/webhooks.md). -- cgit v1.2.1 From a6ac23250a41bd6be7f4e530bff22a8e1cfd3104 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis <axilleas@axilleas.me> Date: Fri, 3 Feb 2017 19:41:35 +0100 Subject: Move project services to new location under Integrations --- app/views/shared/web_hooks/_form.html.haml | 2 +- doc/README.md | 2 +- doc/administration/integration/terminal.md | 16 +- doc/api/services.md | 2 +- doc/ci/autodeploy/index.md | 4 +- doc/ci/environments.md | 7 +- doc/ci/quick_start/README.md | 2 +- doc/ci/variables/README.md | 4 +- doc/integration/README.md | 11 +- doc/integration/external-issue-tracker.md | 8 +- doc/integration/jira.md | 4 +- doc/project_services/bamboo.md | 61 +----- doc/project_services/bugzilla.md | 18 +- doc/project_services/builds_emails.md | 17 +- doc/project_services/emails_on_push.md | 18 +- doc/project_services/hipchat.md | 55 +----- doc/project_services/img/builds_emails_service.png | Bin 19203 -> 0 bytes .../img/emails_on_push_service.png | Bin 28535 -> 0 bytes .../img/jira_add_user_to_group.png | Bin 24838 -> 0 bytes doc/project_services/img/jira_create_new_group.png | Bin 19127 -> 0 bytes .../img/jira_create_new_group_name.png | Bin 5168 -> 0 bytes doc/project_services/img/jira_create_new_user.png | Bin 12625 -> 0 bytes doc/project_services/img/jira_group_access.png | Bin 19235 -> 0 bytes doc/project_services/img/jira_issue_reference.png | Bin 18399 -> 0 bytes .../img/jira_merge_request_close.png | Bin 21172 -> 0 bytes doc/project_services/img/jira_project_name.png | Bin 26685 -> 0 bytes doc/project_services/img/jira_service.png | Bin 37869 -> 0 bytes .../img/jira_service_close_comment.png | Bin 11893 -> 0 bytes .../img/jira_service_close_issue.png | Bin 30570 -> 0 bytes doc/project_services/img/jira_service_page.png | Bin 12228 -> 0 bytes .../img/jira_user_management_link.png | Bin 23921 -> 0 bytes .../img/jira_workflow_screenshot.png | Bin 66685 -> 0 bytes .../img/kubernetes_configuration.png | Bin 113827 -> 0 bytes .../img/mattermost_add_slash_command.png | Bin 9265 -> 0 bytes doc/project_services/img/mattermost_bot_auth.png | Bin 8676 -> 0 bytes .../img/mattermost_bot_available_commands.png | Bin 4647 -> 0 bytes .../img/mattermost_config_help.png | Bin 63138 -> 0 bytes .../img/mattermost_configuration.png | Bin 73502 -> 0 bytes .../img/mattermost_console_integrations.png | Bin 314642 -> 0 bytes .../img/mattermost_gitlab_token.png | Bin 3688 -> 0 bytes .../img/mattermost_goto_console.png | Bin 7754 -> 0 bytes .../img/mattermost_slash_command_configuration.png | Bin 24169 -> 0 bytes .../img/mattermost_slash_command_token.png | Bin 8624 -> 0 bytes .../img/mattermost_team_integrations.png | Bin 4766 -> 0 bytes doc/project_services/img/redmine_configuration.png | Bin 10266 -> 0 bytes .../img/services_templates_redmine_example.png | Bin 8776 -> 0 bytes doc/project_services/img/slack_configuration.png | Bin 29825 -> 0 bytes doc/project_services/img/slack_setup.png | Bin 126412 -> 0 bytes doc/project_services/irker.md | 52 +---- doc/project_services/jira.md | 209 +-------------------- doc/project_services/kubernetes.md | 64 +------ doc/project_services/mattermost.md | 46 +---- doc/project_services/mattermost_slash_commands.md | 164 +--------------- doc/project_services/project_services.md | 60 +----- doc/project_services/redmine.md | 22 +-- doc/project_services/services_templates.md | 26 +-- doc/project_services/slack.md | 51 +---- doc/project_services/slack_slash_commands.md | 24 +-- doc/university/README.md | 6 +- doc/user/project/integrations/bamboo.md | 60 ++++++ doc/user/project/integrations/bugzilla.md | 17 ++ doc/user/project/integrations/builds_emails.md | 16 ++ doc/user/project/integrations/emails_on_push.md | 17 ++ doc/user/project/integrations/hipchat.md | 54 ++++++ .../integrations/img/builds_emails_service.png | Bin 0 -> 19203 bytes .../integrations/img/emails_on_push_service.png | Bin 0 -> 28535 bytes .../integrations/img/jira_add_user_to_group.png | Bin 0 -> 24838 bytes .../integrations/img/jira_create_new_group.png | Bin 0 -> 19127 bytes .../img/jira_create_new_group_name.png | Bin 0 -> 5168 bytes .../integrations/img/jira_create_new_user.png | Bin 0 -> 12625 bytes .../project/integrations/img/jira_group_access.png | Bin 0 -> 19235 bytes .../integrations/img/jira_issue_reference.png | Bin 0 -> 18399 bytes .../integrations/img/jira_merge_request_close.png | Bin 0 -> 21172 bytes .../project/integrations/img/jira_project_name.png | Bin 0 -> 26685 bytes doc/user/project/integrations/img/jira_service.png | Bin 0 -> 37869 bytes .../img/jira_service_close_comment.png | Bin 0 -> 11893 bytes .../integrations/img/jira_service_close_issue.png | Bin 0 -> 30570 bytes .../project/integrations/img/jira_service_page.png | Bin 0 -> 12228 bytes .../integrations/img/jira_user_management_link.png | Bin 0 -> 23921 bytes .../integrations/img/jira_workflow_screenshot.png | Bin 0 -> 66685 bytes .../integrations/img/kubernetes_configuration.png | Bin 0 -> 113827 bytes .../img/mattermost_add_slash_command.png | Bin 0 -> 9265 bytes .../integrations/img/mattermost_bot_auth.png | Bin 0 -> 8676 bytes .../img/mattermost_bot_available_commands.png | Bin 0 -> 4647 bytes .../integrations/img/mattermost_config_help.png | Bin 0 -> 63138 bytes .../integrations/img/mattermost_configuration.png | Bin 0 -> 73502 bytes .../img/mattermost_console_integrations.png | Bin 0 -> 314642 bytes .../integrations/img/mattermost_gitlab_token.png | Bin 0 -> 3688 bytes .../integrations/img/mattermost_goto_console.png | Bin 0 -> 7754 bytes .../img/mattermost_slash_command_configuration.png | Bin 0 -> 24169 bytes .../img/mattermost_slash_command_token.png | Bin 0 -> 8624 bytes .../img/mattermost_team_integrations.png | Bin 0 -> 4766 bytes .../integrations/img/redmine_configuration.png | Bin 0 -> 10266 bytes .../img/services_templates_redmine_example.png | Bin 0 -> 8776 bytes .../integrations/img/slack_configuration.png | Bin 0 -> 29825 bytes doc/user/project/integrations/img/slack_setup.png | Bin 0 -> 126412 bytes doc/user/project/integrations/index.md | 18 ++ doc/user/project/integrations/irker.md | 51 +++++ doc/user/project/integrations/jira.md | 208 ++++++++++++++++++++ doc/user/project/integrations/kubernetes.md | 63 +++++++ doc/user/project/integrations/mattermost.md | 45 +++++ .../integrations/mattermost_slash_commands.md | 163 ++++++++++++++++ doc/user/project/integrations/project_services.md | 59 ++++++ doc/user/project/integrations/redmine.md | 21 +++ .../project/integrations/services_templates.md | 25 +++ doc/user/project/integrations/slack.md | 50 +++++ .../project/integrations/slack_slash_commands.md | 23 +++ 107 files changed, 938 insertions(+), 907 deletions(-) delete mode 100644 doc/project_services/img/builds_emails_service.png delete mode 100644 doc/project_services/img/emails_on_push_service.png delete mode 100644 doc/project_services/img/jira_add_user_to_group.png delete mode 100644 doc/project_services/img/jira_create_new_group.png delete mode 100644 doc/project_services/img/jira_create_new_group_name.png delete mode 100644 doc/project_services/img/jira_create_new_user.png delete mode 100644 doc/project_services/img/jira_group_access.png delete mode 100644 doc/project_services/img/jira_issue_reference.png delete mode 100644 doc/project_services/img/jira_merge_request_close.png delete mode 100644 doc/project_services/img/jira_project_name.png delete mode 100644 doc/project_services/img/jira_service.png delete mode 100644 doc/project_services/img/jira_service_close_comment.png delete mode 100644 doc/project_services/img/jira_service_close_issue.png delete mode 100644 doc/project_services/img/jira_service_page.png delete mode 100644 doc/project_services/img/jira_user_management_link.png delete mode 100644 doc/project_services/img/jira_workflow_screenshot.png delete mode 100644 doc/project_services/img/kubernetes_configuration.png delete mode 100644 doc/project_services/img/mattermost_add_slash_command.png delete mode 100644 doc/project_services/img/mattermost_bot_auth.png delete mode 100644 doc/project_services/img/mattermost_bot_available_commands.png delete mode 100644 doc/project_services/img/mattermost_config_help.png delete mode 100644 doc/project_services/img/mattermost_configuration.png delete mode 100644 doc/project_services/img/mattermost_console_integrations.png delete mode 100644 doc/project_services/img/mattermost_gitlab_token.png delete mode 100644 doc/project_services/img/mattermost_goto_console.png delete mode 100644 doc/project_services/img/mattermost_slash_command_configuration.png delete mode 100644 doc/project_services/img/mattermost_slash_command_token.png delete mode 100644 doc/project_services/img/mattermost_team_integrations.png delete mode 100644 doc/project_services/img/redmine_configuration.png delete mode 100644 doc/project_services/img/services_templates_redmine_example.png delete mode 100644 doc/project_services/img/slack_configuration.png delete mode 100644 doc/project_services/img/slack_setup.png create mode 100644 doc/user/project/integrations/bamboo.md create mode 100644 doc/user/project/integrations/bugzilla.md create mode 100644 doc/user/project/integrations/builds_emails.md create mode 100644 doc/user/project/integrations/emails_on_push.md create mode 100644 doc/user/project/integrations/hipchat.md create mode 100644 doc/user/project/integrations/img/builds_emails_service.png create mode 100644 doc/user/project/integrations/img/emails_on_push_service.png create mode 100644 doc/user/project/integrations/img/jira_add_user_to_group.png create mode 100644 doc/user/project/integrations/img/jira_create_new_group.png create mode 100644 doc/user/project/integrations/img/jira_create_new_group_name.png create mode 100644 doc/user/project/integrations/img/jira_create_new_user.png create mode 100644 doc/user/project/integrations/img/jira_group_access.png create mode 100644 doc/user/project/integrations/img/jira_issue_reference.png create mode 100644 doc/user/project/integrations/img/jira_merge_request_close.png create mode 100644 doc/user/project/integrations/img/jira_project_name.png create mode 100644 doc/user/project/integrations/img/jira_service.png create mode 100644 doc/user/project/integrations/img/jira_service_close_comment.png create mode 100644 doc/user/project/integrations/img/jira_service_close_issue.png create mode 100644 doc/user/project/integrations/img/jira_service_page.png create mode 100644 doc/user/project/integrations/img/jira_user_management_link.png create mode 100644 doc/user/project/integrations/img/jira_workflow_screenshot.png create mode 100644 doc/user/project/integrations/img/kubernetes_configuration.png create mode 100644 doc/user/project/integrations/img/mattermost_add_slash_command.png create mode 100644 doc/user/project/integrations/img/mattermost_bot_auth.png create mode 100644 doc/user/project/integrations/img/mattermost_bot_available_commands.png create mode 100644 doc/user/project/integrations/img/mattermost_config_help.png create mode 100644 doc/user/project/integrations/img/mattermost_configuration.png create mode 100644 doc/user/project/integrations/img/mattermost_console_integrations.png create mode 100644 doc/user/project/integrations/img/mattermost_gitlab_token.png create mode 100644 doc/user/project/integrations/img/mattermost_goto_console.png create mode 100644 doc/user/project/integrations/img/mattermost_slash_command_configuration.png create mode 100644 doc/user/project/integrations/img/mattermost_slash_command_token.png create mode 100644 doc/user/project/integrations/img/mattermost_team_integrations.png create mode 100644 doc/user/project/integrations/img/redmine_configuration.png create mode 100644 doc/user/project/integrations/img/services_templates_redmine_example.png create mode 100644 doc/user/project/integrations/img/slack_configuration.png create mode 100644 doc/user/project/integrations/img/slack_setup.png create mode 100644 doc/user/project/integrations/index.md create mode 100644 doc/user/project/integrations/irker.md create mode 100644 doc/user/project/integrations/jira.md create mode 100644 doc/user/project/integrations/kubernetes.md create mode 100644 doc/user/project/integrations/mattermost.md create mode 100644 doc/user/project/integrations/mattermost_slash_commands.md create mode 100644 doc/user/project/integrations/project_services.md create mode 100644 doc/user/project/integrations/redmine.md create mode 100644 doc/user/project/integrations/services_templates.md create mode 100644 doc/user/project/integrations/slack.md create mode 100644 doc/user/project/integrations/slack_slash_commands.md diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 0236541fd9b..8f64ba043b5 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -3,7 +3,7 @@ %h4.prepend-top-0 = page_title %p - #{link_to "Webhooks", help_page_path("user/project/integrations//webhooks")} can be + #{link_to "Webhooks", help_page_path("user/project/integrations/webhooks")} can be used for binding events when something is happening within the project. .col-lg-9.append-bottom-default = form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f| diff --git a/doc/README.md b/doc/README.md index 70392eb5aad..d5f0c37325e 100644 --- a/doc/README.md +++ b/doc/README.md @@ -18,7 +18,7 @@ - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab. - [Permissions](user/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do. - [Profile Settings](profile/README.md) -- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat. +- [Project Services](user/project/integrations//project_services.md) Integrate a project with external services, such as CI and chat. - [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects. - [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects. - [Webhooks](user/project/integrations/webhooks.md) Let GitLab notify you when new code has been pushed to your project. diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index a1d1bb03b50..3fbb13704aa 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -1,13 +1,12 @@ # Web terminals -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690) -in GitLab 8.15. Only project masters and owners can access web terminals. +> [Introduced][ce-7690] in GitLab 8.15. Only project masters and owners can + access web terminals. -With the introduction of the [Kubernetes](../../project_services/kubernetes.md) -project service, GitLab gained the ability to store and use credentials for a -Kubernetes cluster. One of the things it uses these credentials for is providing -access to [web terminals](../../ci/environments.html#web-terminals) -for environments. +With the introduction of the [Kubernetes project service][kubservice], GitLab +gained the ability to store and use credentials for a Kubernetes cluster. One +of the things it uses these credentials for is providing access to +[web terminals](../../ci/environments.html#web-terminals) for environments. ## How it works @@ -71,3 +70,6 @@ by the above guides. When these headers are not passed through, Workhorse will return a `400 Bad Request` response to users attempting to use a web terminal. In turn, they will receive a `Connection failed` message. + +[ce-7690]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690 +[kubservice]: ../../user/project/integrations/kubernetes.md) diff --git a/doc/api/services.md b/doc/api/services.md index 1466b8189b0..fba5da6587d 100644 --- a/doc/api/services.md +++ b/doc/api/services.md @@ -808,5 +808,5 @@ Get JetBrains TeamCity CI service settings for a project. GET /projects/:id/services/teamcity ``` -[jira-doc]: ../project_services/jira.md +[jira-doc]: ../user/project/integrations/jira.md [old-jira-api]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/api/services.md#jira diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md index c4c4d95b68a..4028a5efa9e 100644 --- a/doc/ci/autodeploy/index.md +++ b/doc/ci/autodeploy/index.md @@ -34,8 +34,8 @@ created automatically for you. [mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135 [project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html -[project-services]: ../../project_services/project_services.md +[project-services]: ../../user/project/integrations/project_services.md [auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy -[kubernetes-service]: ../../project_services/kubernetes.md +[kubernetes-service]: ../../user/project/integrations/kubernetes.md [docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor [review-app]: ../review_apps/index.md diff --git a/doc/ci/environments.md b/doc/ci/environments.md index ef04c537367..579135c2052 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -1,7 +1,6 @@ # Introduction to environments and deployments ->**Note:** -Introduced in GitLab 8.9. +> Introduced in GitLab 8.9. During the development of software, there can be many stages until it's ready for public consumption. You sure want to first test your code and then deploy it @@ -242,7 +241,7 @@ Web terminals were added in GitLab 8.15 and are only available to project masters and owners. If you deploy to your environments with the help of a deployment service (e.g., -the [Kubernetes](../project_services/kubernetes.md) service), GitLab can open +the [Kubernetes service][kubernetes-service], GitLab can open a terminal session to your environment! This is a very powerful feature that allows you to debug issues without leaving the comfort of your web browser. To enable it, just follow the instructions given in the service documentation. @@ -566,7 +565,7 @@ Below are some links you may find interesting: [Pipelines]: pipelines.md [jobs]: yaml/README.md#jobs [yaml]: yaml/README.md -[kubernetes-service]: ../project_services/kubernetes.md] +[kubernetes-service]: ../user/project/integrations/kubernetes.md [environments]: #environments [deployments]: #deployments [permissions]: ../user/permissions.md diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md index c40cdd55ea5..1104edaabe9 100644 --- a/doc/ci/quick_start/README.md +++ b/doc/ci/quick_start/README.md @@ -217,7 +217,7 @@ builds, you should explicitly enable the **Builds Emails** service under your project's settings. For more information read the -[Builds emails service documentation](../../project_services/builds_emails.md). +[Builds emails service documentation](../../user/project/integrations/builds_emails.md). ## Examples diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index d3b9611b02e..49fca884f35 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -157,14 +157,14 @@ Once you set them, they will be available for all subsequent builds. >**Note:** This feature requires GitLab CI 8.15 or higher. -[Project services](../../project_services/project_services.md) that are +[Project services](../../user/project/integrations/project_services.md) that are responsible for deployment configuration may define their own variables that are set in the build environment. These variables are only defined for [deployment builds](../environments.md). Please consult the documentation of the project services that you are using to learn which variables they define. An example project service that defines deployment variables is -[Kubernetes Service](../../project_services/kubernetes.md). +[Kubernetes Service](../../user/project/integrations/kubernetes.md). ## Debug tracing diff --git a/doc/integration/README.md b/doc/integration/README.md index e97430feb57..22bdf33443d 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -5,7 +5,7 @@ trackers and external authentication. See the documentation below for details on how to configure these services. -- [JIRA](../project_services/jira.md) Integrate with the JIRA issue tracker +- [JIRA](../user/project/integrations/jira.md) Integrate with the JIRA issue tracker - [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID @@ -18,17 +18,14 @@ See the documentation below for details on how to configure these services. - [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration - [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents. -GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. - -[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html - +> GitLab Enterprise Edition contains [advanced Jenkins support][jenkins]. ## Project services Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a [Project Service][]. -[Project Service]: ../project_services/project_services.md +[Project Service]: ../user/project/integrations/project_services.md ## SSL certificate errors @@ -64,3 +61,5 @@ After that restart GitLab with: ```bash sudo gitlab-ctl restart ``` + +[jenkins]: http://docs.gitlab.com/ee/integration/jenkins.html diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md index 8d2c6351fb8..265c891cf83 100644 --- a/doc/integration/external-issue-tracker.md +++ b/doc/integration/external-issue-tracker.md @@ -18,9 +18,9 @@ The configuration is done via a project's **Services**. To enable an external issue tracker you must configure the appropriate **Service**. Visit the links below for details: -- [Redmine](../project_services/redmine.md) -- [Jira](../project_services/jira.md) -- [Bugzilla](../project_services/bugzilla.md) +- [Redmine](../user/project/integrations/redmine.md) +- [Jira](../user/project/integrations/jira.md) +- [Bugzilla](../user/project/integrations/bugzilla.md) ### Service Template @@ -28,4 +28,4 @@ To save you the hassle from configuring each project's service individually, GitLab provides the ability to set Service Templates which can then be overridden in each project's settings. -Read more on [Services Templates](../project_services/services_templates.md). +Read more on [Services Templates](../user/project/integrations/services_templates.md). diff --git a/doc/integration/jira.md b/doc/integration/jira.md index e2f136bcc35..b6923f74e28 100644 --- a/doc/integration/jira.md +++ b/doc/integration/jira.md @@ -1,3 +1 @@ -# GitLab JIRA integration - -This document was moved to [project_services/jira](../project_services/jira.md). +This document was moved to [integrations/jira](../user/project/integrations/jira.md). diff --git a/doc/project_services/bamboo.md b/doc/project_services/bamboo.md index 51668128c62..5b171080c72 100644 --- a/doc/project_services/bamboo.md +++ b/doc/project_services/bamboo.md @@ -1,60 +1 @@ -# Atlassian Bamboo CI Service - -GitLab provides integration with Atlassian Bamboo for continuous integration. -When configured, pushes to a project will trigger a build in Bamboo automatically. -Merge requests will also display CI status showing whether the build is pending, -failed, or completed successfully. It also provides a link to the Bamboo build -page for more information. - -Bamboo doesn't quite provide the same features as a traditional build system when -it comes to accepting webhooks and commit data. There are a few things that -need to be configured in a Bamboo build plan before GitLab can integrate. - -## Setup - -### Complete these steps in Bamboo: - -1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions' -dropdown. -1. Select the 'Triggers' tab. -1. Click 'Add trigger'. -1. Enter a description such as 'GitLab trigger' -1. Choose 'Repository triggers the build when changes are committed' -1. Check one or more repositories checkboxes -1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a -whitelist of IP addresses that are allowed to trigger Bamboo builds. -1. Save the trigger. -1. In the left pane, select a build stage. If you have multiple build stages -you want to select the last stage that contains the git checkout task. -1. Select the 'Miscellaneous' tab. -1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}' -in the 'Labels' box. -1. Save - -Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo -service in GitLab - -### Complete these steps in GitLab: - -1. Navigate to the project you want to configure to trigger builds. -1. Select 'Settings' in the top navigation. -1. Select 'Services' in the left navigation. -1. Click 'Atlassian Bamboo CI' -1. Select the 'Active' checkbox. -1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com' -1. Enter the build key from your Bamboo build plan. Build keys are a short, -all capital letter, identifier that is unique. It will be something like PR-BLD -1. If necessary, enter username and password for a Bamboo user that has -access to trigger the build plan. Leave these fields blank if you do not require -authentication. -1. Save or optionally click 'Test Settings'. Please note that 'Test Settings' -will actually trigger a build in Bamboo. - -## Troubleshooting - -If builds are not triggered, these are a couple of things to keep in mind. - -1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger -IP addresses'. -1. Remember that GitLab only triggers builds on push events. A commit via the -web interface will not trigger CI currently. +This document was moved to [user/project/integrations/bamboo.md](../user/project/integrations/bamboo.md). diff --git a/doc/project_services/bugzilla.md b/doc/project_services/bugzilla.md index 215ed6fe9cc..e67055d5616 100644 --- a/doc/project_services/bugzilla.md +++ b/doc/project_services/bugzilla.md @@ -1,17 +1 @@ -# Bugzilla Service - -Go to your project's **Settings > Services > Bugzilla** and fill in the required -details as described in the table below. - -| Field | Description | -| ----- | ----------- | -| `description` | A name for the issue tracker (to differentiate between instances, for example) | -| `project_url` | The URL to the project in Bugzilla which is being linked to this GitLab project. Note that the `project_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. | -| `issues_url` | The URL to the issue in Bugzilla project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | -| `new_issue_url` | This is the URL to create a new issue in Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. | - -Once you have configured and enabled Bugzilla: - -- the **Issues** link on the GitLab project pages takes you to the appropriate - Bugzilla product page -- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue +This document was moved to [user/project/integrations/bugzilla.md](../user/project/integrations/bugzilla.md). diff --git a/doc/project_services/builds_emails.md b/doc/project_services/builds_emails.md index af0b1a287c7..ee54d865225 100644 --- a/doc/project_services/builds_emails.md +++ b/doc/project_services/builds_emails.md @@ -1,16 +1 @@ -## Enabling build emails - -To receive e-mail notifications about the result status of your builds, visit -your project's **Settings > Services > Builds emails** and activate the service. - -In the _Recipients_ area, provide a list of e-mails separated by comma. - -Check the _Add pusher_ checkbox if you want the committer to also receive -e-mail notifications about each build's status. - -If you enable the _Notify only broken builds_ option, e-mail notifications will -be sent only for failed builds. - ---- - -![Builds emails service settings](img/builds_emails_service.png) +This document was moved to [user/project/integrations/builds_emails.md](../user/project/integrations/builds_emails.md). diff --git a/doc/project_services/emails_on_push.md b/doc/project_services/emails_on_push.md index 2f9f36f962e..a2e831ada34 100644 --- a/doc/project_services/emails_on_push.md +++ b/doc/project_services/emails_on_push.md @@ -1,17 +1 @@ -## Enabling emails on push - -To receive email notifications for every change that is pushed to the project, visit -your project's **Settings > Services > Emails on push** and activate the service. - -In the _Recipients_ area, provide a list of emails separated by commas. - -You can configure any of the following settings depending on your preference. - -+ **Push events** - Email will be triggered when a push event is recieved -+ **Tag push events** - Email will be triggered when a tag is created and pushed -+ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`). -+ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body. - ---- - -![Email on push service settings](img/emails_on_push_service.png) +This document was moved to [user/project/integrations/emails_on_push.md](../user/project/integrations/emails_on_push.md). diff --git a/doc/project_services/hipchat.md b/doc/project_services/hipchat.md index 021a93a288f..4ae9f6c6b2e 100644 --- a/doc/project_services/hipchat.md +++ b/doc/project_services/hipchat.md @@ -1,54 +1 @@ -# Atlassian HipChat - -GitLab provides a way to send HipChat notifications upon a number of events, -such as when a user pushes code, creates a branch or tag, adds a comment, and -creates a merge request. - -## Setup - -GitLab requires the use of a HipChat v2 API token to work. v1 tokens are -not supported at this time. Note the differences between v1 and v2 tokens: - -HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1 -token is allowed to send messages to *any* room. - -HipChat v2 API has tokens that are can be created using the Integrations tab -in the Group or Room admin page. By design, these are lightweight tokens that -allow GitLab to send messages only to *one* room. - -### Complete these steps in HipChat: - -1. Go to: https://admin.hipchat.com/admin -1. Click on "Group Admin" -> "Integrations". -1. Find "Build Your Own!" and click "Create". -1. Select the desired room, name the integration "GitLab", and click "Create". -1. In the "Send messages to this room by posting this URL" column, you should -see a URL in the format: - -``` - https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token> -``` - -HipChat is now ready to accept messages from GitLab. Next, set up the HipChat -service in GitLab. - -### Complete these steps in GitLab: - -1. Navigate to the project you want to configure for notifications. -1. Select "Settings" in the top navigation. -1. Select "Services" in the left navigation. -1. Click "HipChat". -1. Select the "Active" checkbox. -1. Insert the `token` field from the URL into the `Token` field on the Web page. -1. Insert the `room` field from the URL into the `Room` field on the Web page. -1. Save or optionally click "Test Settings". - -## Troubleshooting - -If you do not see notifications, make sure you are using a HipChat v2 API -token, not a v1 token. - -Note that the v2 token is tied to a specific room. If you want to be able to -specify arbitrary rooms, you can create an API token for a specific user in -HipChat under "Account settings" and "API access". Use the `XXX` value under -`auth_token=XXX`. +This document was moved to [user/project/integrations/hipchat.md](../user/project/integrations/hipchat.md). diff --git a/doc/project_services/img/builds_emails_service.png b/doc/project_services/img/builds_emails_service.png deleted file mode 100644 index 9dbbed03833..00000000000 Binary files a/doc/project_services/img/builds_emails_service.png and /dev/null differ diff --git a/doc/project_services/img/emails_on_push_service.png b/doc/project_services/img/emails_on_push_service.png deleted file mode 100644 index df301aa1eeb..00000000000 Binary files a/doc/project_services/img/emails_on_push_service.png and /dev/null differ diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/project_services/img/jira_add_user_to_group.png deleted file mode 100644 index 27dac49260c..00000000000 Binary files a/doc/project_services/img/jira_add_user_to_group.png and /dev/null differ diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/project_services/img/jira_create_new_group.png deleted file mode 100644 index 06c4e84fc61..00000000000 Binary files a/doc/project_services/img/jira_create_new_group.png and /dev/null differ diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/project_services/img/jira_create_new_group_name.png deleted file mode 100644 index bfc0dc6b2e9..00000000000 Binary files a/doc/project_services/img/jira_create_new_group_name.png and /dev/null differ diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/project_services/img/jira_create_new_user.png deleted file mode 100644 index e9c03ed770d..00000000000 Binary files a/doc/project_services/img/jira_create_new_user.png and /dev/null differ diff --git a/doc/project_services/img/jira_group_access.png b/doc/project_services/img/jira_group_access.png deleted file mode 100644 index 9d64cc57269..00000000000 Binary files a/doc/project_services/img/jira_group_access.png and /dev/null differ diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/project_services/img/jira_issue_reference.png deleted file mode 100644 index 72c81460df7..00000000000 Binary files a/doc/project_services/img/jira_issue_reference.png and /dev/null differ diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png deleted file mode 100644 index 0f82ceba557..00000000000 Binary files a/doc/project_services/img/jira_merge_request_close.png and /dev/null differ diff --git a/doc/project_services/img/jira_project_name.png b/doc/project_services/img/jira_project_name.png deleted file mode 100644 index 8540a427461..00000000000 Binary files a/doc/project_services/img/jira_project_name.png and /dev/null differ diff --git a/doc/project_services/img/jira_service.png b/doc/project_services/img/jira_service.png deleted file mode 100644 index 8e073b84ff9..00000000000 Binary files a/doc/project_services/img/jira_service.png and /dev/null differ diff --git a/doc/project_services/img/jira_service_close_comment.png b/doc/project_services/img/jira_service_close_comment.png deleted file mode 100644 index bb9cd7e3d13..00000000000 Binary files a/doc/project_services/img/jira_service_close_comment.png and /dev/null differ diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/project_services/img/jira_service_close_issue.png deleted file mode 100644 index c85b1d1dd97..00000000000 Binary files a/doc/project_services/img/jira_service_close_issue.png and /dev/null differ diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png deleted file mode 100644 index c74351b57b8..00000000000 Binary files a/doc/project_services/img/jira_service_page.png and /dev/null differ diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/project_services/img/jira_user_management_link.png deleted file mode 100644 index f81c5b5fc87..00000000000 Binary files a/doc/project_services/img/jira_user_management_link.png and /dev/null differ diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/project_services/img/jira_workflow_screenshot.png deleted file mode 100644 index e62fb202613..00000000000 Binary files a/doc/project_services/img/jira_workflow_screenshot.png and /dev/null differ diff --git a/doc/project_services/img/kubernetes_configuration.png b/doc/project_services/img/kubernetes_configuration.png deleted file mode 100644 index 349a2dc8456..00000000000 Binary files a/doc/project_services/img/kubernetes_configuration.png and /dev/null differ diff --git a/doc/project_services/img/mattermost_add_slash_command.png b/doc/project_services/img/mattermost_add_slash_command.png deleted file mode 100644 index 7759efa183c..00000000000 Binary files a/doc/project_services/img/mattermost_add_slash_command.png and /dev/null differ diff --git a/doc/project_services/img/mattermost_bot_auth.png b/doc/project_services/img/mattermost_bot_auth.png deleted file mode 100644 index 830b7849f3d..00000000000 Binary files a/doc/project_services/img/mattermost_bot_auth.png and /dev/null differ diff --git a/doc/project_services/img/mattermost_bot_available_commands.png b/doc/project_services/img/mattermost_bot_available_commands.png deleted file mode 100644 index b51798cf10d..00000000000 Binary files a/doc/project_services/img/mattermost_bot_available_commands.png and /dev/null differ diff --git a/doc/project_services/img/mattermost_config_help.png b/doc/project_services/img/mattermost_config_help.png deleted file mode 100644 index a62e4b792f9..00000000000 Binary files a/doc/project_services/img/mattermost_config_help.png and /dev/null differ diff --git a/doc/project_services/img/mattermost_configuration.png b/doc/project_services/img/mattermost_configuration.png deleted file mode 100644 index 3c5ff5ee317..00000000000 Binary files a/doc/project_services/img/mattermost_configuration.png and /dev/null differ diff --git a/doc/project_services/img/mattermost_console_integrations.png b/doc/project_services/img/mattermost_console_integrations.png deleted file mode 100644 index 92a30da5be0..00000000000 Binary files a/doc/project_services/img/mattermost_console_integrations.png and /dev/null differ diff --git a/doc/project_services/img/mattermost_gitlab_token.png b/doc/project_services/img/mattermost_gitlab_token.png deleted file mode 100644 index 257018914d2..00000000000 Binary files a/doc/project_services/img/mattermost_gitlab_token.png and /dev/null differ diff --git a/doc/project_services/img/mattermost_goto_console.png b/doc/project_services/img/mattermost_goto_console.png deleted file mode 100644 index 3354c2a24b4..00000000000 Binary files a/doc/project_services/img/mattermost_goto_console.png and /dev/null differ diff --git a/doc/project_services/img/mattermost_slash_command_configuration.png b/doc/project_services/img/mattermost_slash_command_configuration.png deleted file mode 100644 index 12766ab2b34..00000000000 Binary files a/doc/project_services/img/mattermost_slash_command_configuration.png and /dev/null differ diff --git a/doc/project_services/img/mattermost_slash_command_token.png b/doc/project_services/img/mattermost_slash_command_token.png deleted file mode 100644 index c38f37c203c..00000000000 Binary files a/doc/project_services/img/mattermost_slash_command_token.png and /dev/null differ diff --git a/doc/project_services/img/mattermost_team_integrations.png b/doc/project_services/img/mattermost_team_integrations.png deleted file mode 100644 index 69d4a231e5a..00000000000 Binary files a/doc/project_services/img/mattermost_team_integrations.png and /dev/null differ diff --git a/doc/project_services/img/redmine_configuration.png b/doc/project_services/img/redmine_configuration.png deleted file mode 100644 index 7b6dd271401..00000000000 Binary files a/doc/project_services/img/redmine_configuration.png and /dev/null differ diff --git a/doc/project_services/img/services_templates_redmine_example.png b/doc/project_services/img/services_templates_redmine_example.png deleted file mode 100644 index 50d20510daf..00000000000 Binary files a/doc/project_services/img/services_templates_redmine_example.png and /dev/null differ diff --git a/doc/project_services/img/slack_configuration.png b/doc/project_services/img/slack_configuration.png deleted file mode 100644 index fc8e58e686b..00000000000 Binary files a/doc/project_services/img/slack_configuration.png and /dev/null differ diff --git a/doc/project_services/img/slack_setup.png b/doc/project_services/img/slack_setup.png deleted file mode 100644 index f69817f2b78..00000000000 Binary files a/doc/project_services/img/slack_setup.png and /dev/null differ diff --git a/doc/project_services/irker.md b/doc/project_services/irker.md index 25c0c3ad2a6..7f0850dcc24 100644 --- a/doc/project_services/irker.md +++ b/doc/project_services/irker.md @@ -1,51 +1 @@ -# Irker IRC Gateway - -GitLab provides a way to push update messages to an Irker server. When -configured, pushes to a project will trigger the service to send data directly -to the Irker server. - -See the project homepage for further info: https://gitlab.com/esr/irker - -## Needed setup - -You will first need an Irker daemon. You can download the Irker code from its -repository on https://gitlab.com/esr/irker: - -``` -git clone https://gitlab.com/esr/irker.git -``` - -Once you have downloaded the code, you can run the python script named `irkerd`. -This script is the gateway script, it acts both as an IRC client, for sending -messages to an IRC server obviously, and as a TCP server, for receiving messages -from the GitLab service. - -If the Irker server runs on the same machine, you are done. If not, you will -need to follow the firsts steps of the next section. - -## Complete these steps in GitLab: - -1. Navigate to the project you want to configure for notifications. -1. Select "Settings" in the top navigation. -1. Select "Services" in the left navigation. -1. Click "Irker". -1. Select the "Active" checkbox. -1. Enter the server host address where `irkerd` runs (defaults to `localhost`) -in the `Server host` field on the Web page -1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the -`Server port` field on the Web page. -1. Optional: if `Default IRC URI` is set, it has to be in the format -`irc[s]://domain.name` and will be prepend to each and every channel provided -by the user which is not a full URI. -1. Specify the recipients (e.g. #channel1, user1, etc.) -1. Save or optionally click "Test Settings". - -## Note on Irker recipients - -Irker accepts channel names of the form `chan` and `#chan`, both for the -`#chan` channel. If you want to send messages in query, you will need to add -`,isnick` after the channel name, in this form: `Aorimn,isnick`. In this latter -case, `Aorimn` is treated as a nick and no more as a channel name. - -Irker can also join password-protected channels. Users need to append -`?key=thesecretpassword` to the chan name. +This document was moved to [user/project/integrations/irker.md](../user/project/integrations/irker.md). diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md index 390066c9989..63614feba82 100644 --- a/doc/project_services/jira.md +++ b/doc/project_services/jira.md @@ -1,208 +1 @@ -# GitLab JIRA integration - -GitLab can be configured to interact with JIRA. Configuration happens via -user name and password. Connecting to a JIRA server via CAS is not possible. - -Each project can be configured to connect to a different JIRA instance, see the -[configuration](#configuration) section. If you have one JIRA instance you can -pre-fill the settings page with a default template. To configure the template -see the [Services Templates][services-templates] document. - -Once the project is connected to JIRA, you can reference and close the issues -in JIRA directly from GitLab. - -## Configuration - -In order to enable the JIRA service in GitLab, you need to first configure the -project in JIRA and then enter the correct values in GitLab. - -### Configuring JIRA - -We need to create a user in JIRA which will have access to all projects that -need to integrate with GitLab. Login to your JIRA instance as admin and under -Administration go to User Management and create a new user. - -As an example, we'll create a user named `gitlab` and add it to `JIRA-developers` -group. - -**It is important that the user `GitLab` has write-access to projects in JIRA** - -We have split this stage in steps so it is easier to follow. - ---- - -1. Login to your JIRA instance as an administrator and under **Administration** - go to **User Management** to create a new user. - - ![JIRA user management link](img/jira_user_management_link.png) - - --- - -1. The next step is to create a new user (e.g., `gitlab`) who has write access - to projects in JIRA. Enter the user's name and a _valid_ e-mail address - since JIRA sends a verification e-mail to set-up the password. - _**Note:** JIRA creates the username automatically by using the e-mail - prefix. You can change it later if you want._ - - ![JIRA create new user](img/jira_create_new_user.png) - - --- - -1. Now, let's create a `gitlab-developers` group which will have write access - to projects in JIRA. Go to the **Groups** tab and select **Create group**. - - ![JIRA create new user](img/jira_create_new_group.png) - - --- - - Give it an optional description and hit **Create group**. - - ![jira create new group](img/jira_create_new_group_name.png) - - --- - -1. Give the newly-created group write access by going to - **Application access ➔ View configuration** and adding the `gitlab-developers` - group to JIRA Core. - - ![JIRA group access](img/jira_group_access.png) - - --- - -1. Add the `gitlab` user to the `gitlab-developers` group by going to - **Users ➔ GitLab user ➔ Add group** and selecting the `gitlab-developers` - group from the dropdown menu. Notice that the group says _Access_ which is - what we aim for. - - ![JIRA add user to group](img/jira_add_user_to_group.png) - ---- - -The JIRA configuration is over. Write down the new JIRA username and its -password as they will be needed when configuring GitLab in the next section. - -### Configuring GitLab - ->**Notes:** -- The currently supported JIRA versions are `v6.x` and `v7.x.`. GitLab 7.8 or - higher is required. -- GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified - the configuration options you have to enter. If you are using an older version, - [follow this documentation][jira-repo-docs]. - -To enable JIRA integration in a project, navigate to your project's -**Services ➔ JIRA** and fill in the required details on the page as described -in the table below. - -| Field | Description | -| ----- | ----------- | -| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. E.g., `https://jira.example.com`. | -| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | -| `Username` | The user name created in [configuring JIRA step](#configuring-jira). | -| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | -| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). | - -After saving the configuration, your GitLab project will be able to interact -with the linked JIRA project. - -![JIRA service page](img/jira_service_page.png) - ---- - -## JIRA issues - -By now you should have [configured JIRA](#configuring-jira) and enabled the -[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly -you should be able to reference and close JIRA issues by just mentioning their -ID in GitLab commits and merge requests. - -### Referencing JIRA Issues - -When GitLab project has JIRA issue tracker configured and enabled, mentioning -JIRA issue in GitLab will automatically add a comment in JIRA issue with the -link back to GitLab. This means that in comments in merge requests and commits -referencing an issue, e.g., `PROJECT-7`, will add a comment in JIRA issue in the -format: - -``` -USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]: -ENTITY_TITLE -``` - -* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. -* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned. -* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request. -* `PROJECT_NAME` GitLab project name. -* `ENTITY_TITLE` Merge request title or commit message first line. - -![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) - ---- - -### Closing JIRA Issues - -JIRA issues can be closed directly from GitLab by using trigger words in -commits and merge requests. When a commit which contains the trigger word -followed by the JIRA issue ID in the commit message is pushed, GitLab will -add a comment in the mentioned JIRA issue and immediately close it (provided -the transition ID was set up correctly). - -There are currently three trigger words, and you can use either one to achieve -the same goal: - -- `Resolves PROJECT-1` -- `Closes PROJECT-1` -- `Fixes PROJECT-1` - -where `PROJECT-1` is the issue ID of the JIRA project. - -### JIRA issue closing example - -Let's consider the following example: - -1. For the project named `PROJECT` in JIRA, we implemented a new feature - and created a merge request in GitLab. -1. This feature was requested in JIRA issue `PROJECT-7` and the merge request - in GitLab contains the improvement -1. In the merge request description we use the issue closing trigger - `Closes PROJECT-7`. -1. Once the merge request is merged, the JIRA issue will be automatically closed - with a comment and an associated link to the commit that resolved the issue. - ---- - -In the following screenshot you can see what the link references to the JIRA -issue look like. - -![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) - ---- - -Once this merge request is merged, the JIRA issue will be automatically closed -with a link to the commit that resolved the issue. - -![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png) - ---- - -![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png) - -## Troubleshooting - -If things don't work as expected that's usually because you have configured -incorrectly the JIRA-GitLab integration. - -### GitLab is unable to comment on a ticket - -Make sure that the user you set up for GitLab to communicate with JIRA has the -correct access permission to post comments on a ticket and to also transition -the ticket, if you'd like GitLab to also take care of closing them. -JIRA issue references and update comments will not work if the GitLab issue tracker is disabled. - -### GitLab is unable to close a ticket - -Make sure the `Transition ID` you set within the JIRA settings matches the one -your project needs to close a ticket. - -[services-templates]: ../project_services/services_templates.md -[jira-repo-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md +This document was moved to [user/project/integrations/jira.md](../user/project/integrations/jira.md). diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md index 99aa9e44bdb..0497a13c2b7 100644 --- a/doc/project_services/kubernetes.md +++ b/doc/project_services/kubernetes.md @@ -1,63 +1 @@ -# GitLab Kubernetes / OpenShift integration - -GitLab can be configured to interact with Kubernetes, or other systems using the -Kubernetes API (such as OpenShift). - -Each project can be configured to connect to a different Kubernetes cluster, see -the [configuration](#configuration) section. - -If you have a single cluster that you want to use for all your projects, -you can pre-fill the settings page with a default template. To configure the -template, see the [Services Templates](services_templates.md) document. - -## Configuration - -![Kubernetes configuration settings](img/kubernetes_configuration.png) - -The Kubernetes service takes the following arguments: - -1. Kubernetes namespace -1. API URL -1. Service token -1. Custom CA bundle - -The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes -exposes several APIs - we want the "base" URL that is common to all of them, -e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`. - -GitLab authenticates against Kubernetes using service tokens, which are -scoped to a particular `namespace`. If you don't have a service token yet, -you can follow the -[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/) -to create one. You can also view or create service tokens in the -[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit -`Config -> Secrets`. - -Fill in the service token and namespace according to the values you just got. -If the API is using a self-signed TLS certificate, you'll also need to include -the `ca.crt` contents as the `Custom CA bundle`. - -## Deployment variables - -The Kubernetes service exposes following -[deployment variables](../ci/variables/README.md#deployment-variables) in the -GitLab CI build environment: - -- `KUBE_URL` - equal to the API URL -- `KUBE_TOKEN` -- `KUBE_NAMESPACE` -- `KUBE_CA_PEM` - only if a custom CA bundle was specified - -## Web terminals - ->**NOTE:** -Added in GitLab 8.15. You must be the project owner or have `master` permissions -to use terminals. Support is currently limited to the first container in the -first pod of your environment. - -When enabled, the Kubernetes service adds [web terminal](../ci/environments.md#web-terminals) -support to your environments. This is based on the `exec` functionality found in -Docker and Kubernetes, so you get a new shell session within your existing -containers. To use this integration, you should deploy to Kubernetes using -the deployment variables above, ensuring any pods you create are labelled with -`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest! +This document was moved to [user/project/integrations/kubernetes.md](../user/project/integrations/kubernetes.md). diff --git a/doc/project_services/mattermost.md b/doc/project_services/mattermost.md index fbc7dfeee6d..554a028853e 100644 --- a/doc/project_services/mattermost.md +++ b/doc/project_services/mattermost.md @@ -1,45 +1 @@ -# Mattermost Notifications Service - -## On Mattermost - -To enable Mattermost integration you must create an incoming webhook integration: - -1. Sign in to your Mattermost instance -1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add -1. Choose a display name, description and channel, those can be overridden on GitLab -1. Save it, copy the **Webhook URL**, we'll need this later for GitLab. - -There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable -it on https://mattermost.example/admin_console/integrations/custom. - -Display name override is not enabled by default, you need to ask your admin to enable it on that same section. - -## On GitLab - -After you set up Mattermost, it's time to set up GitLab. - -Go to your project's **Settings > Services > Mattermost Notifications** and you will see a -checkbox with the following events that can be triggered: - -- Push -- Issue -- Merge request -- Note -- Tag push -- Build -- Wiki page - -Bellow each of these event checkboxes, you will have an input field to insert -which Mattermost channel you want to send that event message, with `#town-square` -being the default. The hash sign is optional. - -At the end, fill in your Mattermost details: - -| Field | Description | -| ----- | ----------- | -| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... | -| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. | -| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | - - -![Mattermost configuration](img/mattermost_configuration.png) +This document was moved to [user/project/integrations/mattermost.md](../user/project/integrations/mattermost.md). diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md index 67cb88104c1..7c238b5dc37 100644 --- a/doc/project_services/mattermost_slash_commands.md +++ b/doc/project_services/mattermost_slash_commands.md @@ -1,163 +1 @@ -# Mattermost slash commands - -> Introduced in GitLab 8.14 - -Mattermost commands give users an extra interface to perform common operations -from the chat environment. This allows one to, for example, create an issue as -soon as the idea was discussed in Mattermost. - -## Prerequisites - -Mattermost 3.4 and up is required. - -If you have the Omnibus GitLab package installed, Mattermost is already bundled -in it. All you have to do is configure it. Read more in the -[Omnibus GitLab Mattermost documentation][omnimmdocs]. - -## Automated Configuration - -If Mattermost is installed on the same server as GitLab, the configuration process can be -done for you by GitLab. - -Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button. - -## Manual Configuration - -The configuration consists of two parts. First you need to enable the slash -commands in Mattermost and then enable the service in GitLab. - -### Step 1. Enable custom slash commands in Mattermost - -This step is only required when using a source install, omnibus installs will be -preconfigured with the right settings. - -The first thing to do in Mattermost is to enable custom slash commands from -the administrator console. - -1. Log in with an account that has admin privileges and navigate to the system - console. - - ![Mattermost go to console](img/mattermost_goto_console.png) - - --- - -1. Click **Custom integrations** and set **Enable Custom Slash Commands**, - **Enable custom integrations to override usernames**, and **Override - custom integrations to override profile picture icons** to true - - ![Mattermost console](img/mattermost_console_integrations.png) - - --- - -1. Click **Save** at the bottom to save the changes. - -### Step 2. Open the Mattermost slash commands service in GitLab - -1. Open a new tab for GitLab and go to your project's settings - **Services ➔ Mattermost command**. A screen will appear with all the values you - need to copy in Mattermost as described in the next step. Leave the window open. - - >**Note:** - GitLab will propose some values for the Mattermost settings. The only one - required to copy-paste as-is is the **Request URL**, all the others are just - suggestions. - - ![Mattermost setup instructions](img/mattermost_config_help.png) - - --- - -1. Proceed to the next step and create a slash command in Mattermost with the - above values. - -### Step 3. Create a new custom slash command in Mattermost - -Now that you have enabled custom slash commands in Mattermost and opened -the Mattermost slash commands service in GitLab, it's time to copy these values -in a new slash command. - -1. Back to Mattermost, under your team page settings, you should see the - **Integrations** option. - - ![Mattermost team integrations](img/mattermost_team_integrations.png) - - --- - -1. Go to the **Slash Commands** integration and add a new one by clicking the - **Add Slash Command** button. - - ![Mattermost add command](img/mattermost_add_slash_command.png) - - --- - -1. Fill in the options for the custom command as described in - [step 2](#step-2-open-the-mattermost-slash-commands-service-in-gitlab). - - >**Note:** - If you plan on connecting multiple projects, pick a slash command trigger - word that relates to your projects such as `/gitlab-project-name` or even - just `/project-name`. Only use `/gitlab` if you will only connect a single - project to your Mattermost team. - - ![Mattermost add command configuration](img/mattermost_slash_command_configuration.png) - -1. After you setup all the values, copy the token (we will use it below) and - click **Done**. - - ![Mattermost slash command token](img/mattermost_slash_command_token.png) - -### Step 4. Copy the Mattermost token into the Mattermost slash command service - -1. In GitLab, paste the Mattermost token you copied in the previous step and - check the **Active** checkbox. - - ![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png) - -1. Click **Save changes** for the changes to take effect. - ---- - -You are now set to start using slash commands in Mattermost that talk to the -GitLab project you configured. - -## Authorizing Mattermost to interact with GitLab - -The first time a user will interact with the newly created slash commands, -Mattermost will trigger an authorization process. - -![Mattermost bot authorize](img/mattermost_bot_auth.png) - -This will connect your Mattermost user with your GitLab user. You can -see all authorized chat accounts in your profile's page under **Chat**. - -When the authorization process is complete, you can start interacting with -GitLab using the Mattermost commands. - -## Available slash commands - -The available slash commands are: - -| Command | Description | Example | -| ------- | ----------- | ------- | -| <kbd>/<trigger> issue new <title> <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> <description></kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> | -| <kbd>/<trigger> issue show <issue-number></kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> | -| <kbd>/<trigger> deploy <environment> to <environment></kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> | - -To see a list of available commands to interact with GitLab, type the -trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp> - -![Mattermost bot available commands](img/mattermost_bot_available_commands.png) - -## Permissions - -The permissions to run the [available commands](#available-commands) derive from -the [permissions you have on the project](../user/permissions.md#project). - -## Further reading - -- [Mattermost slash commands documentation][mmslashdocs] -- [Omnibus GitLab Mattermost][omnimmdocs] - - -[omnimmdocs]: https://docs.gitlab.com/omnibus/gitlab-mattermost/ -[mmslashdocs]: https://docs.mattermost.com/developer/slash-commands.html -[ciyaml]: ../ci/yaml/README.md +This document was moved to [user/project/integrations/mattermost_slash_commands.md](../user/project/integrations/mattermost_slash_commands.md). diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md index 547d855d777..2c555c4edae 100644 --- a/doc/project_services/project_services.md +++ b/doc/project_services/project_services.md @@ -1,59 +1 @@ -# Project Services - -Project services allow you to integrate GitLab with other applications. Below -is list of the currently supported ones. - -You can find these within GitLab in the Services page under Project Settings if -you are at least a master on the project. -Project Services are a bit like plugins in that they allow a lot of freedom in -adding functionality to GitLab. For example there is also a service that can -send an email every time someone pushes new commits. - -Because GitLab is open source we can ship with the code and tests for all -plugins. This allows the community to keep the plugins up to date so that they -always work in newer GitLab versions. - -For an overview of what projects services are available without logging in, -please see the [project_services directory][projects-code]. - -[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services - -Click on the service links to see -further configuration instructions and details. Contributions are welcome. - -## Services - -| Service | Description | -| ------- | ----------- | -| Asana | Asana - Teamwork without email | -| Assembla | Project Management Software (Source Commits Endpoint) | -| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server | -| Buildkite | Continuous integration and deployments | -| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients | -| [Bugzilla](bugzilla.md) | Bugzilla issue tracker | -| Campfire | Simple web-based real-time group chat | -| Custom Issue Tracker | Custom issue tracker | -| Drone CI | Continuous Integration platform built on Docker, written in Go | -| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients | -| External Wiki | Replaces the link to the internal wiki with a link to an external wiki | -| Flowdock | Flowdock is a collaboration web app for technical teams | -| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | -| [HipChat](hipchat.md) | Private group chat and IM | -| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | -| [JIRA](jira.md) | JIRA issue tracker | -| JetBrains TeamCity CI | A continuous integration and build server | -| [Kubernetes](kubernetes.md) | A containerized deployment service | -| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | -| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost | -| [Slack Notifications](slack.md) | Receive event notifications in Slack | -| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands | -| PivotalTracker | Project Management Software (Source Commits Endpoint) | -| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | -| [Redmine](redmine.md) | Redmine issue tracker | - -## Services Templates - -Services templates is a way to set some predefined values in the Service of -your liking which will then be pre-filled on each project's Service. - -Read more about [Services Templates in this document](services_templates.md). +This document was moved to [user/project/integrations/project_services.md](../user/project/integrations/project_services.md). diff --git a/doc/project_services/redmine.md b/doc/project_services/redmine.md index b9830ea7c38..6010aa4dc75 100644 --- a/doc/project_services/redmine.md +++ b/doc/project_services/redmine.md @@ -1,21 +1 @@ -# Redmine Service - -Go to your project's **Settings > Services > Redmine** and fill in the required -details as described in the table below. - -| Field | Description | -| ----- | ----------- | -| `description` | A name for the issue tracker (to differentiate between instances, for example) | -| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project | -| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | -| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project | - -Once you have configured and enabled Redmine: - -- the **Issues** link on the GitLab project pages takes you to the appropriate - Redmine issue index -- clicking **New issue** on the project dashboard creates a new Redmine issue - -As an example, below is a configuration for a project named gitlab-ci. - -![Redmine configuration](img/redmine_configuration.png) +This document was moved to [user/project/integrations/redmine.md](../user/project/integrations/redmine.md). diff --git a/doc/project_services/services_templates.md b/doc/project_services/services_templates.md index be6d13b6d2b..8905d667c5a 100644 --- a/doc/project_services/services_templates.md +++ b/doc/project_services/services_templates.md @@ -1,25 +1 @@ -# Services Templates - -A GitLab administrator can add a service template that sets a default for each -project. This makes it much easier to configure individual projects. - -After the template is created, the template details will be pre-filled on a -project's Service page. - -## Enable a Service template - -In GitLab's Admin area, navigate to **Service Templates** and choose the -service template you wish to create. - -For example, in the image below you can see Redmine. - -![Redmine service template](img/services_templates_redmine_example.png) - ---- - -**NOTE:** For each project, you will still need to configure the issue tracking -URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used -by your external issue tracker. Prior to GitLab v7.8, this ID was configured in -the project settings, and GitLab would automatically update the URL configured -in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs -must be configured directly within the project's **Services** settings. +This document was moved to [user/project/integrations/services_templates.md](../user/project/integrations/services_templates.md). diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md index eaceb2be137..1d3f98705e3 100644 --- a/doc/project_services/slack.md +++ b/doc/project_services/slack.md @@ -1,50 +1 @@ -# Slack Notifications Service - -## On Slack - -To enable Slack integration you must create an incoming webhook integration on -Slack: - -1. [Sign in to Slack](https://slack.com/signin) -1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/) -1. Choose the channel name you want to send notifications to. -1. Click **Add Incoming WebHooks Integration** -1. Copy the **Webhook URL**, we'll need this later for GitLab. - -## On GitLab - -After you set up Slack, it's time to set up GitLab. - -Go to your project's **Settings > Integrations > Slack Notifications** and you will see a -checkbox with the following events that can be triggered: - -- Push -- Issue -- Merge request -- Note -- Tag push -- Build -- Wiki page - -Bellow each of these event checkboxes, you will have an input field to insert -which Slack channel you want to send that event message, with `#general` -being the default. Enter your preferred channel **without** the hash sign (`#`). - -At the end, fill in your Slack details: - -| Field | Description | -| ----- | ----------- | -| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. | -| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. | -| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | - -After you are all done, click **Save changes** for the changes to take effect. - ->**Note:** -You can set "branch,pushed,Compare changes" as highlight words on your Slack -profile settings, so that you can be aware of new commits when somebody pushes -them. - -![Slack configuration](img/slack_configuration.png) - -[slackhook]: https://my.slack.com/services/new/incoming-webhook +This document was moved to [user/project/integrations/slack.md](../user/project/integrations/slack.md). diff --git a/doc/project_services/slack_slash_commands.md b/doc/project_services/slack_slash_commands.md index d9ff573d185..9554c8decc8 100644 --- a/doc/project_services/slack_slash_commands.md +++ b/doc/project_services/slack_slash_commands.md @@ -1,23 +1 @@ -# Slack slash commands - -> Introduced in GitLab 8.15 - -Slack commands give users an extra interface to perform common operations -from the chat environment. This allows one to, for example, create an issue as -soon as the idea was discussed in chat. -For all available commands try the help subcommand, for example: `/gitlab help`, -all review the [full list of commands](../integration/chat_commands.md). - -## Prerequisites - -A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in Slack should be created beforehand, GitLab cannot create it for you. - -## Configuration - -First, navigate to the Slack Slash commands service page, found at your project's -**Settings** > **Services**, and you find the instructions there: - - ![Slack setup instructions](img/slack_setup.png) - -Once you've followed the instructions, mark the service as active and insert the token -you've received from Slack. After saving the service you are good to go! +This document was moved to [user/project/integrations/slack_slash_commands.md](../user/project/integrations/slack_slash_commands.md). diff --git a/doc/university/README.md b/doc/university/README.md index 12727e9d56f..c798e0d760d 100644 --- a/doc/university/README.md +++ b/doc/university/README.md @@ -189,10 +189,10 @@ The curriculum is composed of GitLab videos, screencasts, presentations, project #### 3.9. Integrations 1. [How to Integrate JIRA and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415) -1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ee/integration/jira.html) +1. [How to Integrate Jira with GitLab](https://docs.gitlab.com/ce/user/project/integrations/jira.html) 1. [How to Integrate Jenkins with GitLab](https://docs.gitlab.com/ee/integration/jenkins.html) -1. [How to Integrate Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md) -1. [How to Integrate Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md) +1. [How to Integrate Bamboo with GitLab](https://docs.gitlab.com/ce/user/project/integrations/bamboo.html) +1. [How to Integrate Slack with GitLab](https://docs.gitlab.com/ce/user/project/integrations/slack.html) 1. [How to Integrate Convox with GitLab](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/) 1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/) diff --git a/doc/user/project/integrations/bamboo.md b/doc/user/project/integrations/bamboo.md new file mode 100644 index 00000000000..51668128c62 --- /dev/null +++ b/doc/user/project/integrations/bamboo.md @@ -0,0 +1,60 @@ +# Atlassian Bamboo CI Service + +GitLab provides integration with Atlassian Bamboo for continuous integration. +When configured, pushes to a project will trigger a build in Bamboo automatically. +Merge requests will also display CI status showing whether the build is pending, +failed, or completed successfully. It also provides a link to the Bamboo build +page for more information. + +Bamboo doesn't quite provide the same features as a traditional build system when +it comes to accepting webhooks and commit data. There are a few things that +need to be configured in a Bamboo build plan before GitLab can integrate. + +## Setup + +### Complete these steps in Bamboo: + +1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions' +dropdown. +1. Select the 'Triggers' tab. +1. Click 'Add trigger'. +1. Enter a description such as 'GitLab trigger' +1. Choose 'Repository triggers the build when changes are committed' +1. Check one or more repositories checkboxes +1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a +whitelist of IP addresses that are allowed to trigger Bamboo builds. +1. Save the trigger. +1. In the left pane, select a build stage. If you have multiple build stages +you want to select the last stage that contains the git checkout task. +1. Select the 'Miscellaneous' tab. +1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}' +in the 'Labels' box. +1. Save + +Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo +service in GitLab + +### Complete these steps in GitLab: + +1. Navigate to the project you want to configure to trigger builds. +1. Select 'Settings' in the top navigation. +1. Select 'Services' in the left navigation. +1. Click 'Atlassian Bamboo CI' +1. Select the 'Active' checkbox. +1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com' +1. Enter the build key from your Bamboo build plan. Build keys are a short, +all capital letter, identifier that is unique. It will be something like PR-BLD +1. If necessary, enter username and password for a Bamboo user that has +access to trigger the build plan. Leave these fields blank if you do not require +authentication. +1. Save or optionally click 'Test Settings'. Please note that 'Test Settings' +will actually trigger a build in Bamboo. + +## Troubleshooting + +If builds are not triggered, these are a couple of things to keep in mind. + +1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger +IP addresses'. +1. Remember that GitLab only triggers builds on push events. A commit via the +web interface will not trigger CI currently. diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md new file mode 100644 index 00000000000..215ed6fe9cc --- /dev/null +++ b/doc/user/project/integrations/bugzilla.md @@ -0,0 +1,17 @@ +# Bugzilla Service + +Go to your project's **Settings > Services > Bugzilla** and fill in the required +details as described in the table below. + +| Field | Description | +| ----- | ----------- | +| `description` | A name for the issue tracker (to differentiate between instances, for example) | +| `project_url` | The URL to the project in Bugzilla which is being linked to this GitLab project. Note that the `project_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. | +| `issues_url` | The URL to the issue in Bugzilla project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | +| `new_issue_url` | This is the URL to create a new issue in Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. | + +Once you have configured and enabled Bugzilla: + +- the **Issues** link on the GitLab project pages takes you to the appropriate + Bugzilla product page +- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue diff --git a/doc/user/project/integrations/builds_emails.md b/doc/user/project/integrations/builds_emails.md new file mode 100644 index 00000000000..af0b1a287c7 --- /dev/null +++ b/doc/user/project/integrations/builds_emails.md @@ -0,0 +1,16 @@ +## Enabling build emails + +To receive e-mail notifications about the result status of your builds, visit +your project's **Settings > Services > Builds emails** and activate the service. + +In the _Recipients_ area, provide a list of e-mails separated by comma. + +Check the _Add pusher_ checkbox if you want the committer to also receive +e-mail notifications about each build's status. + +If you enable the _Notify only broken builds_ option, e-mail notifications will +be sent only for failed builds. + +--- + +![Builds emails service settings](img/builds_emails_service.png) diff --git a/doc/user/project/integrations/emails_on_push.md b/doc/user/project/integrations/emails_on_push.md new file mode 100644 index 00000000000..2f9f36f962e --- /dev/null +++ b/doc/user/project/integrations/emails_on_push.md @@ -0,0 +1,17 @@ +## Enabling emails on push + +To receive email notifications for every change that is pushed to the project, visit +your project's **Settings > Services > Emails on push** and activate the service. + +In the _Recipients_ area, provide a list of emails separated by commas. + +You can configure any of the following settings depending on your preference. + ++ **Push events** - Email will be triggered when a push event is recieved ++ **Tag push events** - Email will be triggered when a tag is created and pushed ++ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`). ++ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body. + +--- + +![Email on push service settings](img/emails_on_push_service.png) diff --git a/doc/user/project/integrations/hipchat.md b/doc/user/project/integrations/hipchat.md new file mode 100644 index 00000000000..021a93a288f --- /dev/null +++ b/doc/user/project/integrations/hipchat.md @@ -0,0 +1,54 @@ +# Atlassian HipChat + +GitLab provides a way to send HipChat notifications upon a number of events, +such as when a user pushes code, creates a branch or tag, adds a comment, and +creates a merge request. + +## Setup + +GitLab requires the use of a HipChat v2 API token to work. v1 tokens are +not supported at this time. Note the differences between v1 and v2 tokens: + +HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1 +token is allowed to send messages to *any* room. + +HipChat v2 API has tokens that are can be created using the Integrations tab +in the Group or Room admin page. By design, these are lightweight tokens that +allow GitLab to send messages only to *one* room. + +### Complete these steps in HipChat: + +1. Go to: https://admin.hipchat.com/admin +1. Click on "Group Admin" -> "Integrations". +1. Find "Build Your Own!" and click "Create". +1. Select the desired room, name the integration "GitLab", and click "Create". +1. In the "Send messages to this room by posting this URL" column, you should +see a URL in the format: + +``` + https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token> +``` + +HipChat is now ready to accept messages from GitLab. Next, set up the HipChat +service in GitLab. + +### Complete these steps in GitLab: + +1. Navigate to the project you want to configure for notifications. +1. Select "Settings" in the top navigation. +1. Select "Services" in the left navigation. +1. Click "HipChat". +1. Select the "Active" checkbox. +1. Insert the `token` field from the URL into the `Token` field on the Web page. +1. Insert the `room` field from the URL into the `Room` field on the Web page. +1. Save or optionally click "Test Settings". + +## Troubleshooting + +If you do not see notifications, make sure you are using a HipChat v2 API +token, not a v1 token. + +Note that the v2 token is tied to a specific room. If you want to be able to +specify arbitrary rooms, you can create an API token for a specific user in +HipChat under "Account settings" and "API access". Use the `XXX` value under +`auth_token=XXX`. diff --git a/doc/user/project/integrations/img/builds_emails_service.png b/doc/user/project/integrations/img/builds_emails_service.png new file mode 100644 index 00000000000..9dbbed03833 Binary files /dev/null and b/doc/user/project/integrations/img/builds_emails_service.png differ diff --git a/doc/user/project/integrations/img/emails_on_push_service.png b/doc/user/project/integrations/img/emails_on_push_service.png new file mode 100644 index 00000000000..df301aa1eeb Binary files /dev/null and b/doc/user/project/integrations/img/emails_on_push_service.png differ diff --git a/doc/user/project/integrations/img/jira_add_user_to_group.png b/doc/user/project/integrations/img/jira_add_user_to_group.png new file mode 100644 index 00000000000..27dac49260c Binary files /dev/null and b/doc/user/project/integrations/img/jira_add_user_to_group.png differ diff --git a/doc/user/project/integrations/img/jira_create_new_group.png b/doc/user/project/integrations/img/jira_create_new_group.png new file mode 100644 index 00000000000..06c4e84fc61 Binary files /dev/null and b/doc/user/project/integrations/img/jira_create_new_group.png differ diff --git a/doc/user/project/integrations/img/jira_create_new_group_name.png b/doc/user/project/integrations/img/jira_create_new_group_name.png new file mode 100644 index 00000000000..bfc0dc6b2e9 Binary files /dev/null and b/doc/user/project/integrations/img/jira_create_new_group_name.png differ diff --git a/doc/user/project/integrations/img/jira_create_new_user.png b/doc/user/project/integrations/img/jira_create_new_user.png new file mode 100644 index 00000000000..e9c03ed770d Binary files /dev/null and b/doc/user/project/integrations/img/jira_create_new_user.png differ diff --git a/doc/user/project/integrations/img/jira_group_access.png b/doc/user/project/integrations/img/jira_group_access.png new file mode 100644 index 00000000000..9d64cc57269 Binary files /dev/null and b/doc/user/project/integrations/img/jira_group_access.png differ diff --git a/doc/user/project/integrations/img/jira_issue_reference.png b/doc/user/project/integrations/img/jira_issue_reference.png new file mode 100644 index 00000000000..72c81460df7 Binary files /dev/null and b/doc/user/project/integrations/img/jira_issue_reference.png differ diff --git a/doc/user/project/integrations/img/jira_merge_request_close.png b/doc/user/project/integrations/img/jira_merge_request_close.png new file mode 100644 index 00000000000..0f82ceba557 Binary files /dev/null and b/doc/user/project/integrations/img/jira_merge_request_close.png differ diff --git a/doc/user/project/integrations/img/jira_project_name.png b/doc/user/project/integrations/img/jira_project_name.png new file mode 100644 index 00000000000..8540a427461 Binary files /dev/null and b/doc/user/project/integrations/img/jira_project_name.png differ diff --git a/doc/user/project/integrations/img/jira_service.png b/doc/user/project/integrations/img/jira_service.png new file mode 100644 index 00000000000..8e073b84ff9 Binary files /dev/null and b/doc/user/project/integrations/img/jira_service.png differ diff --git a/doc/user/project/integrations/img/jira_service_close_comment.png b/doc/user/project/integrations/img/jira_service_close_comment.png new file mode 100644 index 00000000000..bb9cd7e3d13 Binary files /dev/null and b/doc/user/project/integrations/img/jira_service_close_comment.png differ diff --git a/doc/user/project/integrations/img/jira_service_close_issue.png b/doc/user/project/integrations/img/jira_service_close_issue.png new file mode 100644 index 00000000000..c85b1d1dd97 Binary files /dev/null and b/doc/user/project/integrations/img/jira_service_close_issue.png differ diff --git a/doc/user/project/integrations/img/jira_service_page.png b/doc/user/project/integrations/img/jira_service_page.png new file mode 100644 index 00000000000..c74351b57b8 Binary files /dev/null and b/doc/user/project/integrations/img/jira_service_page.png differ diff --git a/doc/user/project/integrations/img/jira_user_management_link.png b/doc/user/project/integrations/img/jira_user_management_link.png new file mode 100644 index 00000000000..f81c5b5fc87 Binary files /dev/null and b/doc/user/project/integrations/img/jira_user_management_link.png differ diff --git a/doc/user/project/integrations/img/jira_workflow_screenshot.png b/doc/user/project/integrations/img/jira_workflow_screenshot.png new file mode 100644 index 00000000000..e62fb202613 Binary files /dev/null and b/doc/user/project/integrations/img/jira_workflow_screenshot.png differ diff --git a/doc/user/project/integrations/img/kubernetes_configuration.png b/doc/user/project/integrations/img/kubernetes_configuration.png new file mode 100644 index 00000000000..349a2dc8456 Binary files /dev/null and b/doc/user/project/integrations/img/kubernetes_configuration.png differ diff --git a/doc/user/project/integrations/img/mattermost_add_slash_command.png b/doc/user/project/integrations/img/mattermost_add_slash_command.png new file mode 100644 index 00000000000..7759efa183c Binary files /dev/null and b/doc/user/project/integrations/img/mattermost_add_slash_command.png differ diff --git a/doc/user/project/integrations/img/mattermost_bot_auth.png b/doc/user/project/integrations/img/mattermost_bot_auth.png new file mode 100644 index 00000000000..830b7849f3d Binary files /dev/null and b/doc/user/project/integrations/img/mattermost_bot_auth.png differ diff --git a/doc/user/project/integrations/img/mattermost_bot_available_commands.png b/doc/user/project/integrations/img/mattermost_bot_available_commands.png new file mode 100644 index 00000000000..b51798cf10d Binary files /dev/null and b/doc/user/project/integrations/img/mattermost_bot_available_commands.png differ diff --git a/doc/user/project/integrations/img/mattermost_config_help.png b/doc/user/project/integrations/img/mattermost_config_help.png new file mode 100644 index 00000000000..a62e4b792f9 Binary files /dev/null and b/doc/user/project/integrations/img/mattermost_config_help.png differ diff --git a/doc/user/project/integrations/img/mattermost_configuration.png b/doc/user/project/integrations/img/mattermost_configuration.png new file mode 100644 index 00000000000..3c5ff5ee317 Binary files /dev/null and b/doc/user/project/integrations/img/mattermost_configuration.png differ diff --git a/doc/user/project/integrations/img/mattermost_console_integrations.png b/doc/user/project/integrations/img/mattermost_console_integrations.png new file mode 100644 index 00000000000..92a30da5be0 Binary files /dev/null and b/doc/user/project/integrations/img/mattermost_console_integrations.png differ diff --git a/doc/user/project/integrations/img/mattermost_gitlab_token.png b/doc/user/project/integrations/img/mattermost_gitlab_token.png new file mode 100644 index 00000000000..257018914d2 Binary files /dev/null and b/doc/user/project/integrations/img/mattermost_gitlab_token.png differ diff --git a/doc/user/project/integrations/img/mattermost_goto_console.png b/doc/user/project/integrations/img/mattermost_goto_console.png new file mode 100644 index 00000000000..3354c2a24b4 Binary files /dev/null and b/doc/user/project/integrations/img/mattermost_goto_console.png differ diff --git a/doc/user/project/integrations/img/mattermost_slash_command_configuration.png b/doc/user/project/integrations/img/mattermost_slash_command_configuration.png new file mode 100644 index 00000000000..12766ab2b34 Binary files /dev/null and b/doc/user/project/integrations/img/mattermost_slash_command_configuration.png differ diff --git a/doc/user/project/integrations/img/mattermost_slash_command_token.png b/doc/user/project/integrations/img/mattermost_slash_command_token.png new file mode 100644 index 00000000000..c38f37c203c Binary files /dev/null and b/doc/user/project/integrations/img/mattermost_slash_command_token.png differ diff --git a/doc/user/project/integrations/img/mattermost_team_integrations.png b/doc/user/project/integrations/img/mattermost_team_integrations.png new file mode 100644 index 00000000000..69d4a231e5a Binary files /dev/null and b/doc/user/project/integrations/img/mattermost_team_integrations.png differ diff --git a/doc/user/project/integrations/img/redmine_configuration.png b/doc/user/project/integrations/img/redmine_configuration.png new file mode 100644 index 00000000000..7b6dd271401 Binary files /dev/null and b/doc/user/project/integrations/img/redmine_configuration.png differ diff --git a/doc/user/project/integrations/img/services_templates_redmine_example.png b/doc/user/project/integrations/img/services_templates_redmine_example.png new file mode 100644 index 00000000000..50d20510daf Binary files /dev/null and b/doc/user/project/integrations/img/services_templates_redmine_example.png differ diff --git a/doc/user/project/integrations/img/slack_configuration.png b/doc/user/project/integrations/img/slack_configuration.png new file mode 100644 index 00000000000..fc8e58e686b Binary files /dev/null and b/doc/user/project/integrations/img/slack_configuration.png differ diff --git a/doc/user/project/integrations/img/slack_setup.png b/doc/user/project/integrations/img/slack_setup.png new file mode 100644 index 00000000000..f69817f2b78 Binary files /dev/null and b/doc/user/project/integrations/img/slack_setup.png differ diff --git a/doc/user/project/integrations/index.md b/doc/user/project/integrations/index.md new file mode 100644 index 00000000000..766ffb1f65c --- /dev/null +++ b/doc/user/project/integrations/index.md @@ -0,0 +1,18 @@ +# Project integrations + +## Project services + +Project services allow you to integrate GitLab with other applications. +They are a bit like plugins in that they allow a lot of freedom in +adding functionality to GitLab. + +[Learn more about project services.](project_services.md) + +## Webhooks + +Project webhooks allow you to trigger a URL if for example new code is pushed or +a new issue is created. You can configure webhooks to listen for specific events +like pushes, issues or merge requests. GitLab will send a POST request with data +to the webhook URL. + +[Learn more about webhooks.](webhooks.md) diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md new file mode 100644 index 00000000000..25c0c3ad2a6 --- /dev/null +++ b/doc/user/project/integrations/irker.md @@ -0,0 +1,51 @@ +# Irker IRC Gateway + +GitLab provides a way to push update messages to an Irker server. When +configured, pushes to a project will trigger the service to send data directly +to the Irker server. + +See the project homepage for further info: https://gitlab.com/esr/irker + +## Needed setup + +You will first need an Irker daemon. You can download the Irker code from its +repository on https://gitlab.com/esr/irker: + +``` +git clone https://gitlab.com/esr/irker.git +``` + +Once you have downloaded the code, you can run the python script named `irkerd`. +This script is the gateway script, it acts both as an IRC client, for sending +messages to an IRC server obviously, and as a TCP server, for receiving messages +from the GitLab service. + +If the Irker server runs on the same machine, you are done. If not, you will +need to follow the firsts steps of the next section. + +## Complete these steps in GitLab: + +1. Navigate to the project you want to configure for notifications. +1. Select "Settings" in the top navigation. +1. Select "Services" in the left navigation. +1. Click "Irker". +1. Select the "Active" checkbox. +1. Enter the server host address where `irkerd` runs (defaults to `localhost`) +in the `Server host` field on the Web page +1. Enter the server port of `irkerd` (e.g. defaults to 6659) in the +`Server port` field on the Web page. +1. Optional: if `Default IRC URI` is set, it has to be in the format +`irc[s]://domain.name` and will be prepend to each and every channel provided +by the user which is not a full URI. +1. Specify the recipients (e.g. #channel1, user1, etc.) +1. Save or optionally click "Test Settings". + +## Note on Irker recipients + +Irker accepts channel names of the form `chan` and `#chan`, both for the +`#chan` channel. If you want to send messages in query, you will need to add +`,isnick` after the channel name, in this form: `Aorimn,isnick`. In this latter +case, `Aorimn` is treated as a nick and no more as a channel name. + +Irker can also join password-protected channels. Users need to append +`?key=thesecretpassword` to the chan name. diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md new file mode 100644 index 00000000000..233a2583c36 --- /dev/null +++ b/doc/user/project/integrations/jira.md @@ -0,0 +1,208 @@ +# GitLab JIRA integration + +GitLab can be configured to interact with JIRA. Configuration happens via +user name and password. Connecting to a JIRA server via CAS is not possible. + +Each project can be configured to connect to a different JIRA instance, see the +[configuration](#configuration) section. If you have one JIRA instance you can +pre-fill the settings page with a default template. To configure the template +see the [Services Templates][services-templates] document. + +Once the project is connected to JIRA, you can reference and close the issues +in JIRA directly from GitLab. + +## Configuration + +In order to enable the JIRA service in GitLab, you need to first configure the +project in JIRA and then enter the correct values in GitLab. + +### Configuring JIRA + +We need to create a user in JIRA which will have access to all projects that +need to integrate with GitLab. Login to your JIRA instance as admin and under +Administration go to User Management and create a new user. + +As an example, we'll create a user named `gitlab` and add it to `JIRA-developers` +group. + +**It is important that the user `GitLab` has write-access to projects in JIRA** + +We have split this stage in steps so it is easier to follow. + +--- + +1. Login to your JIRA instance as an administrator and under **Administration** + go to **User Management** to create a new user. + + ![JIRA user management link](img/jira_user_management_link.png) + + --- + +1. The next step is to create a new user (e.g., `gitlab`) who has write access + to projects in JIRA. Enter the user's name and a _valid_ e-mail address + since JIRA sends a verification e-mail to set-up the password. + _**Note:** JIRA creates the username automatically by using the e-mail + prefix. You can change it later if you want._ + + ![JIRA create new user](img/jira_create_new_user.png) + + --- + +1. Now, let's create a `gitlab-developers` group which will have write access + to projects in JIRA. Go to the **Groups** tab and select **Create group**. + + ![JIRA create new user](img/jira_create_new_group.png) + + --- + + Give it an optional description and hit **Create group**. + + ![jira create new group](img/jira_create_new_group_name.png) + + --- + +1. Give the newly-created group write access by going to + **Application access ➔ View configuration** and adding the `gitlab-developers` + group to JIRA Core. + + ![JIRA group access](img/jira_group_access.png) + + --- + +1. Add the `gitlab` user to the `gitlab-developers` group by going to + **Users ➔ GitLab user ➔ Add group** and selecting the `gitlab-developers` + group from the dropdown menu. Notice that the group says _Access_ which is + what we aim for. + + ![JIRA add user to group](img/jira_add_user_to_group.png) + +--- + +The JIRA configuration is over. Write down the new JIRA username and its +password as they will be needed when configuring GitLab in the next section. + +### Configuring GitLab + +>**Notes:** +- The currently supported JIRA versions are `v6.x` and `v7.x.`. GitLab 7.8 or + higher is required. +- GitLab 8.14 introduced a new way to integrate with JIRA which greatly simplified + the configuration options you have to enter. If you are using an older version, + [follow this documentation][jira-repo-old-docs]. + +To enable JIRA integration in a project, navigate to your project's +**Services ➔ JIRA** and fill in the required details on the page as described +in the table below. + +| Field | Description | +| ----- | ----------- | +| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. E.g., `https://jira.example.com`. | +| `Project key` | The short identifier for your JIRA project, all uppercase, e.g., `PROJ`. | +| `Username` | The user name created in [configuring JIRA step](#configuring-jira). | +| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). | +| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). | + +After saving the configuration, your GitLab project will be able to interact +with the linked JIRA project. + +![JIRA service page](img/jira_service_page.png) + +--- + +## JIRA issues + +By now you should have [configured JIRA](#configuring-jira) and enabled the +[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly +you should be able to reference and close JIRA issues by just mentioning their +ID in GitLab commits and merge requests. + +### Referencing JIRA Issues + +When GitLab project has JIRA issue tracker configured and enabled, mentioning +JIRA issue in GitLab will automatically add a comment in JIRA issue with the +link back to GitLab. This means that in comments in merge requests and commits +referencing an issue, e.g., `PROJECT-7`, will add a comment in JIRA issue in the +format: + +``` +USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]: +ENTITY_TITLE +``` + +* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab. +* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned. +* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request. +* `PROJECT_NAME` GitLab project name. +* `ENTITY_TITLE` Merge request title or commit message first line. + +![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png) + +--- + +### Closing JIRA Issues + +JIRA issues can be closed directly from GitLab by using trigger words in +commits and merge requests. When a commit which contains the trigger word +followed by the JIRA issue ID in the commit message is pushed, GitLab will +add a comment in the mentioned JIRA issue and immediately close it (provided +the transition ID was set up correctly). + +There are currently three trigger words, and you can use either one to achieve +the same goal: + +- `Resolves PROJECT-1` +- `Closes PROJECT-1` +- `Fixes PROJECT-1` + +where `PROJECT-1` is the issue ID of the JIRA project. + +### JIRA issue closing example + +Let's consider the following example: + +1. For the project named `PROJECT` in JIRA, we implemented a new feature + and created a merge request in GitLab. +1. This feature was requested in JIRA issue `PROJECT-7` and the merge request + in GitLab contains the improvement +1. In the merge request description we use the issue closing trigger + `Closes PROJECT-7`. +1. Once the merge request is merged, the JIRA issue will be automatically closed + with a comment and an associated link to the commit that resolved the issue. + +--- + +In the following screenshot you can see what the link references to the JIRA +issue look like. + +![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png) + +--- + +Once this merge request is merged, the JIRA issue will be automatically closed +with a link to the commit that resolved the issue. + +![The GitLab integration closes JIRA issue](img/jira_service_close_issue.png) + +--- + +![The GitLab integration creates a comment and a link on JIRA issue.](img/jira_service_close_comment.png) + +## Troubleshooting + +If things don't work as expected that's usually because you have configured +incorrectly the JIRA-GitLab integration. + +### GitLab is unable to comment on a ticket + +Make sure that the user you set up for GitLab to communicate with JIRA has the +correct access permission to post comments on a ticket and to also transition +the ticket, if you'd like GitLab to also take care of closing them. +JIRA issue references and update comments will not work if the GitLab issue tracker is disabled. + +### GitLab is unable to close a ticket + +Make sure the `Transition ID` you set within the JIRA settings matches the one +your project needs to close a ticket. + +[services-templates]: services_templates.md +[jira-repo-old-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/doc/project_services/jira.md diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md new file mode 100644 index 00000000000..99aa9e44bdb --- /dev/null +++ b/doc/user/project/integrations/kubernetes.md @@ -0,0 +1,63 @@ +# GitLab Kubernetes / OpenShift integration + +GitLab can be configured to interact with Kubernetes, or other systems using the +Kubernetes API (such as OpenShift). + +Each project can be configured to connect to a different Kubernetes cluster, see +the [configuration](#configuration) section. + +If you have a single cluster that you want to use for all your projects, +you can pre-fill the settings page with a default template. To configure the +template, see the [Services Templates](services_templates.md) document. + +## Configuration + +![Kubernetes configuration settings](img/kubernetes_configuration.png) + +The Kubernetes service takes the following arguments: + +1. Kubernetes namespace +1. API URL +1. Service token +1. Custom CA bundle + +The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes +exposes several APIs - we want the "base" URL that is common to all of them, +e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`. + +GitLab authenticates against Kubernetes using service tokens, which are +scoped to a particular `namespace`. If you don't have a service token yet, +you can follow the +[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/) +to create one. You can also view or create service tokens in the +[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit +`Config -> Secrets`. + +Fill in the service token and namespace according to the values you just got. +If the API is using a self-signed TLS certificate, you'll also need to include +the `ca.crt` contents as the `Custom CA bundle`. + +## Deployment variables + +The Kubernetes service exposes following +[deployment variables](../ci/variables/README.md#deployment-variables) in the +GitLab CI build environment: + +- `KUBE_URL` - equal to the API URL +- `KUBE_TOKEN` +- `KUBE_NAMESPACE` +- `KUBE_CA_PEM` - only if a custom CA bundle was specified + +## Web terminals + +>**NOTE:** +Added in GitLab 8.15. You must be the project owner or have `master` permissions +to use terminals. Support is currently limited to the first container in the +first pod of your environment. + +When enabled, the Kubernetes service adds [web terminal](../ci/environments.md#web-terminals) +support to your environments. This is based on the `exec` functionality found in +Docker and Kubernetes, so you get a new shell session within your existing +containers. To use this integration, you should deploy to Kubernetes using +the deployment variables above, ensuring any pods you create are labelled with +`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest! diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md new file mode 100644 index 00000000000..fbc7dfeee6d --- /dev/null +++ b/doc/user/project/integrations/mattermost.md @@ -0,0 +1,45 @@ +# Mattermost Notifications Service + +## On Mattermost + +To enable Mattermost integration you must create an incoming webhook integration: + +1. Sign in to your Mattermost instance +1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add +1. Choose a display name, description and channel, those can be overridden on GitLab +1. Save it, copy the **Webhook URL**, we'll need this later for GitLab. + +There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable +it on https://mattermost.example/admin_console/integrations/custom. + +Display name override is not enabled by default, you need to ask your admin to enable it on that same section. + +## On GitLab + +After you set up Mattermost, it's time to set up GitLab. + +Go to your project's **Settings > Services > Mattermost Notifications** and you will see a +checkbox with the following events that can be triggered: + +- Push +- Issue +- Merge request +- Note +- Tag push +- Build +- Wiki page + +Bellow each of these event checkboxes, you will have an input field to insert +which Mattermost channel you want to send that event message, with `#town-square` +being the default. The hash sign is optional. + +At the end, fill in your Mattermost details: + +| Field | Description | +| ----- | ----------- | +| **Webhook** | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... | +| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. | +| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | + + +![Mattermost configuration](img/mattermost_configuration.png) diff --git a/doc/user/project/integrations/mattermost_slash_commands.md b/doc/user/project/integrations/mattermost_slash_commands.md new file mode 100644 index 00000000000..67cb88104c1 --- /dev/null +++ b/doc/user/project/integrations/mattermost_slash_commands.md @@ -0,0 +1,163 @@ +# Mattermost slash commands + +> Introduced in GitLab 8.14 + +Mattermost commands give users an extra interface to perform common operations +from the chat environment. This allows one to, for example, create an issue as +soon as the idea was discussed in Mattermost. + +## Prerequisites + +Mattermost 3.4 and up is required. + +If you have the Omnibus GitLab package installed, Mattermost is already bundled +in it. All you have to do is configure it. Read more in the +[Omnibus GitLab Mattermost documentation][omnimmdocs]. + +## Automated Configuration + +If Mattermost is installed on the same server as GitLab, the configuration process can be +done for you by GitLab. + +Go to the Mattermost Slash Command service on your project and click the 'Add to Mattermost' button. + +## Manual Configuration + +The configuration consists of two parts. First you need to enable the slash +commands in Mattermost and then enable the service in GitLab. + +### Step 1. Enable custom slash commands in Mattermost + +This step is only required when using a source install, omnibus installs will be +preconfigured with the right settings. + +The first thing to do in Mattermost is to enable custom slash commands from +the administrator console. + +1. Log in with an account that has admin privileges and navigate to the system + console. + + ![Mattermost go to console](img/mattermost_goto_console.png) + + --- + +1. Click **Custom integrations** and set **Enable Custom Slash Commands**, + **Enable custom integrations to override usernames**, and **Override + custom integrations to override profile picture icons** to true + + ![Mattermost console](img/mattermost_console_integrations.png) + + --- + +1. Click **Save** at the bottom to save the changes. + +### Step 2. Open the Mattermost slash commands service in GitLab + +1. Open a new tab for GitLab and go to your project's settings + **Services ➔ Mattermost command**. A screen will appear with all the values you + need to copy in Mattermost as described in the next step. Leave the window open. + + >**Note:** + GitLab will propose some values for the Mattermost settings. The only one + required to copy-paste as-is is the **Request URL**, all the others are just + suggestions. + + ![Mattermost setup instructions](img/mattermost_config_help.png) + + --- + +1. Proceed to the next step and create a slash command in Mattermost with the + above values. + +### Step 3. Create a new custom slash command in Mattermost + +Now that you have enabled custom slash commands in Mattermost and opened +the Mattermost slash commands service in GitLab, it's time to copy these values +in a new slash command. + +1. Back to Mattermost, under your team page settings, you should see the + **Integrations** option. + + ![Mattermost team integrations](img/mattermost_team_integrations.png) + + --- + +1. Go to the **Slash Commands** integration and add a new one by clicking the + **Add Slash Command** button. + + ![Mattermost add command](img/mattermost_add_slash_command.png) + + --- + +1. Fill in the options for the custom command as described in + [step 2](#step-2-open-the-mattermost-slash-commands-service-in-gitlab). + + >**Note:** + If you plan on connecting multiple projects, pick a slash command trigger + word that relates to your projects such as `/gitlab-project-name` or even + just `/project-name`. Only use `/gitlab` if you will only connect a single + project to your Mattermost team. + + ![Mattermost add command configuration](img/mattermost_slash_command_configuration.png) + +1. After you setup all the values, copy the token (we will use it below) and + click **Done**. + + ![Mattermost slash command token](img/mattermost_slash_command_token.png) + +### Step 4. Copy the Mattermost token into the Mattermost slash command service + +1. In GitLab, paste the Mattermost token you copied in the previous step and + check the **Active** checkbox. + + ![Mattermost copy token to GitLab](img/mattermost_gitlab_token.png) + +1. Click **Save changes** for the changes to take effect. + +--- + +You are now set to start using slash commands in Mattermost that talk to the +GitLab project you configured. + +## Authorizing Mattermost to interact with GitLab + +The first time a user will interact with the newly created slash commands, +Mattermost will trigger an authorization process. + +![Mattermost bot authorize](img/mattermost_bot_auth.png) + +This will connect your Mattermost user with your GitLab user. You can +see all authorized chat accounts in your profile's page under **Chat**. + +When the authorization process is complete, you can start interacting with +GitLab using the Mattermost commands. + +## Available slash commands + +The available slash commands are: + +| Command | Description | Example | +| ------- | ----------- | ------- | +| <kbd>/<trigger> issue new <title> <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> <description></kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> | +| <kbd>/<trigger> issue show <issue-number></kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> | +| <kbd>/<trigger> deploy <environment> to <environment></kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> | + +To see a list of available commands to interact with GitLab, type the +trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp> + +![Mattermost bot available commands](img/mattermost_bot_available_commands.png) + +## Permissions + +The permissions to run the [available commands](#available-commands) derive from +the [permissions you have on the project](../user/permissions.md#project). + +## Further reading + +- [Mattermost slash commands documentation][mmslashdocs] +- [Omnibus GitLab Mattermost][omnimmdocs] + + +[omnimmdocs]: https://docs.gitlab.com/omnibus/gitlab-mattermost/ +[mmslashdocs]: https://docs.mattermost.com/developer/slash-commands.html +[ciyaml]: ../ci/yaml/README.md diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md new file mode 100644 index 00000000000..547d855d777 --- /dev/null +++ b/doc/user/project/integrations/project_services.md @@ -0,0 +1,59 @@ +# Project Services + +Project services allow you to integrate GitLab with other applications. Below +is list of the currently supported ones. + +You can find these within GitLab in the Services page under Project Settings if +you are at least a master on the project. +Project Services are a bit like plugins in that they allow a lot of freedom in +adding functionality to GitLab. For example there is also a service that can +send an email every time someone pushes new commits. + +Because GitLab is open source we can ship with the code and tests for all +plugins. This allows the community to keep the plugins up to date so that they +always work in newer GitLab versions. + +For an overview of what projects services are available without logging in, +please see the [project_services directory][projects-code]. + +[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services + +Click on the service links to see +further configuration instructions and details. Contributions are welcome. + +## Services + +| Service | Description | +| ------- | ----------- | +| Asana | Asana - Teamwork without email | +| Assembla | Project Management Software (Source Commits Endpoint) | +| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server | +| Buildkite | Continuous integration and deployments | +| [Builds emails](builds_emails.md) | Email the builds status to a list of recipients | +| [Bugzilla](bugzilla.md) | Bugzilla issue tracker | +| Campfire | Simple web-based real-time group chat | +| Custom Issue Tracker | Custom issue tracker | +| Drone CI | Continuous Integration platform built on Docker, written in Go | +| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients | +| External Wiki | Replaces the link to the internal wiki with a link to an external wiki | +| Flowdock | Flowdock is a collaboration web app for technical teams | +| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities | +| [HipChat](hipchat.md) | Private group chat and IM | +| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway | +| [JIRA](jira.md) | JIRA issue tracker | +| JetBrains TeamCity CI | A continuous integration and build server | +| [Kubernetes](kubernetes.md) | A containerized deployment service | +| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands | +| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost | +| [Slack Notifications](slack.md) | Receive event notifications in Slack | +| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands | +| PivotalTracker | Project Management Software (Source Commits Endpoint) | +| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop | +| [Redmine](redmine.md) | Redmine issue tracker | + +## Services Templates + +Services templates is a way to set some predefined values in the Service of +your liking which will then be pre-filled on each project's Service. + +Read more about [Services Templates in this document](services_templates.md). diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md new file mode 100644 index 00000000000..b9830ea7c38 --- /dev/null +++ b/doc/user/project/integrations/redmine.md @@ -0,0 +1,21 @@ +# Redmine Service + +Go to your project's **Settings > Services > Redmine** and fill in the required +details as described in the table below. + +| Field | Description | +| ----- | ----------- | +| `description` | A name for the issue tracker (to differentiate between instances, for example) | +| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project | +| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. | +| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project | + +Once you have configured and enabled Redmine: + +- the **Issues** link on the GitLab project pages takes you to the appropriate + Redmine issue index +- clicking **New issue** on the project dashboard creates a new Redmine issue + +As an example, below is a configuration for a project named gitlab-ci. + +![Redmine configuration](img/redmine_configuration.png) diff --git a/doc/user/project/integrations/services_templates.md b/doc/user/project/integrations/services_templates.md new file mode 100644 index 00000000000..be6d13b6d2b --- /dev/null +++ b/doc/user/project/integrations/services_templates.md @@ -0,0 +1,25 @@ +# Services Templates + +A GitLab administrator can add a service template that sets a default for each +project. This makes it much easier to configure individual projects. + +After the template is created, the template details will be pre-filled on a +project's Service page. + +## Enable a Service template + +In GitLab's Admin area, navigate to **Service Templates** and choose the +service template you wish to create. + +For example, in the image below you can see Redmine. + +![Redmine service template](img/services_templates_redmine_example.png) + +--- + +**NOTE:** For each project, you will still need to configure the issue tracking +URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used +by your external issue tracker. Prior to GitLab v7.8, this ID was configured in +the project settings, and GitLab would automatically update the URL configured +in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs +must be configured directly within the project's **Services** settings. diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md new file mode 100644 index 00000000000..eaceb2be137 --- /dev/null +++ b/doc/user/project/integrations/slack.md @@ -0,0 +1,50 @@ +# Slack Notifications Service + +## On Slack + +To enable Slack integration you must create an incoming webhook integration on +Slack: + +1. [Sign in to Slack](https://slack.com/signin) +1. Visit [Incoming WebHooks](https://my.slack.com/services/new/incoming-webhook/) +1. Choose the channel name you want to send notifications to. +1. Click **Add Incoming WebHooks Integration** +1. Copy the **Webhook URL**, we'll need this later for GitLab. + +## On GitLab + +After you set up Slack, it's time to set up GitLab. + +Go to your project's **Settings > Integrations > Slack Notifications** and you will see a +checkbox with the following events that can be triggered: + +- Push +- Issue +- Merge request +- Note +- Tag push +- Build +- Wiki page + +Bellow each of these event checkboxes, you will have an input field to insert +which Slack channel you want to send that event message, with `#general` +being the default. Enter your preferred channel **without** the hash sign (`#`). + +At the end, fill in your Slack details: + +| Field | Description | +| ----- | ----------- | +| **Webhook** | The [incoming webhook URL][slackhook] which you have to setup on Slack. | +| **Username** | Optional username which can be on messages sent to slack. Fill this in if you want to change the username of the bot. | +| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. | + +After you are all done, click **Save changes** for the changes to take effect. + +>**Note:** +You can set "branch,pushed,Compare changes" as highlight words on your Slack +profile settings, so that you can be aware of new commits when somebody pushes +them. + +![Slack configuration](img/slack_configuration.png) + +[slackhook]: https://my.slack.com/services/new/incoming-webhook diff --git a/doc/user/project/integrations/slack_slash_commands.md b/doc/user/project/integrations/slack_slash_commands.md new file mode 100644 index 00000000000..d9ff573d185 --- /dev/null +++ b/doc/user/project/integrations/slack_slash_commands.md @@ -0,0 +1,23 @@ +# Slack slash commands + +> Introduced in GitLab 8.15 + +Slack commands give users an extra interface to perform common operations +from the chat environment. This allows one to, for example, create an issue as +soon as the idea was discussed in chat. +For all available commands try the help subcommand, for example: `/gitlab help`, +all review the [full list of commands](../integration/chat_commands.md). + +## Prerequisites + +A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in Slack should be created beforehand, GitLab cannot create it for you. + +## Configuration + +First, navigate to the Slack Slash commands service page, found at your project's +**Settings** > **Services**, and you find the instructions there: + + ![Slack setup instructions](img/slack_setup.png) + +Once you've followed the instructions, mark the service as active and insert the token +you've received from Slack. After saving the service you are good to go! -- cgit v1.2.1 From 9c60e8fb5963c55e1ea6cf28e3851d822f0bb540 Mon Sep 17 00:00:00 2001 From: Clement Ho <ClemMakesApps@gmail.com> Date: Fri, 3 Feb 2017 13:51:56 -0600 Subject: Fix slash commands spec error --- spec/features/merge_requests/user_uses_slash_commands_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index 2b454b38231..2f3c3e45ae6 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -133,6 +133,8 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do it 'changes target_branch in new merge_request' do visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts) + click_button "Compare branches and continue" + fill_in "merge_request_title", with: 'My brand new feature' fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:" click_button "Submit merge request" -- cgit v1.2.1 From b63d7d4f8965597fd0a7291830ad391d1c74334e Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Fri, 3 Feb 2017 17:02:05 +0000 Subject: Change window size before visiting page, to get correct scroll position --- changelogs/unreleased/fix-scroll-test.yml | 4 ++++ spec/features/merge_requests/toggler_behavior_spec.rb | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/fix-scroll-test.yml diff --git a/changelogs/unreleased/fix-scroll-test.yml b/changelogs/unreleased/fix-scroll-test.yml new file mode 100644 index 00000000000..e98ac755b88 --- /dev/null +++ b/changelogs/unreleased/fix-scroll-test.yml @@ -0,0 +1,4 @@ +--- +title: Change rspec test to guarantee window is resized before visiting page +merge_request: +author: diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb index 44a9b545ff8..a2cf9b18bf2 100644 --- a/spec/features/merge_requests/toggler_behavior_spec.rb +++ b/spec/features/merge_requests/toggler_behavior_spec.rb @@ -10,8 +10,8 @@ feature 'toggler_behavior', js: true, feature: true do before do login_as :admin project = merge_request.source_project - visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}" page.current_window.resize_to(1000, 300) + visit "#{namespace_project_merge_request_path(project.namespace, project, merge_request)}#{fragment_id}" end describe 'scroll position' do -- cgit v1.2.1 From 8f9be6d312345a9127c13df876dc91f6b9e6d408 Mon Sep 17 00:00:00 2001 From: tauriedavis <taurie@gitlab.com> Date: Tue, 31 Jan 2017 13:26:32 -0800 Subject: 27240 Make progress bars consistent --- app/assets/stylesheets/framework/common.scss | 2 ++ changelogs/unreleased/27240-make-progress-bars-consistent.yml | 4 ++++ 2 files changed, 6 insertions(+) create mode 100644 changelogs/unreleased/27240-make-progress-bars-consistent.yml diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 0ce94a26a7f..a4b38723bbd 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -253,6 +253,8 @@ li.note { .progress { margin-bottom: 0; margin-top: 4px; + box-shadow: none; + background-color: $border-gray-light; } } diff --git a/changelogs/unreleased/27240-make-progress-bars-consistent.yml b/changelogs/unreleased/27240-make-progress-bars-consistent.yml new file mode 100644 index 00000000000..3f902fb324e --- /dev/null +++ b/changelogs/unreleased/27240-make-progress-bars-consistent.yml @@ -0,0 +1,4 @@ +--- +title: 27240 Make progress bars consistent +merge_request: +author: -- cgit v1.2.1 From 186e60f2e74305ef476299e558a17014bdc37d2c Mon Sep 17 00:00:00 2001 From: Allison Whilden <allison@gitlab.com> Date: Fri, 3 Feb 2017 15:08:20 -0800 Subject: [ci skip] UX Guide: Button placement in groups --- doc/development/ux_guide/components.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/development/ux_guide/components.md b/doc/development/ux_guide/components.md index 1b19587a0b8..18d0647c798 100644 --- a/doc/development/ux_guide/components.md +++ b/doc/development/ux_guide/components.md @@ -96,6 +96,20 @@ Since secondary buttons only have a border on their resting state, their hover a | Background: `$color-light` <br> Border: `$border-color-light` | ![](img/button-success-secondary--hover.png) | ![](img/button-close--hover.png) | ![](img/button-spam--hover.png) | | Background: `$color-normal` <br> Border: `$border-color-normal` | ![](img/button-success-secondary--active.png) | ![](img/button-close--active.png) | ![](img/button-spam--active.png) | +### Placement + +When there are a group of buttons in a dialog or a form, we need to be consistent with the placement. + +#### Dismissive actions on the left +The dismissive action returns the user to the previous state. + +> Example: Cancel + +#### Affirmative actions on the right +Affirmative actions continue to progress towards the user goal that triggered the dialog or form. + +> Example: Submit, Ok, Delete + --- -- cgit v1.2.1 From ce4877e21882ddc2f5bdae5ab0768f02a32f6e84 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 3 Feb 2017 14:42:30 -0600 Subject: fix Vue warnings for missing element --- app/assets/javascripts/boards/boards_bundle.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6 index 1053d167131..e3241974e59 100644 --- a/app/assets/javascripts/boards/boards_bundle.js.es6 +++ b/app/assets/javascripts/boards/boards_bundle.js.es6 @@ -78,7 +78,7 @@ $(() => { }); gl.IssueBoardsSearch = new Vue({ - el: '#js-boards-search', + el: document.getElementById('js-boards-search'), data: { filters: Store.state.filters }, @@ -89,7 +89,7 @@ $(() => { gl.IssueBoardsModalAddBtn = new Vue({ mixins: [gl.issueBoards.ModalMixins], - el: '#js-add-issues-btn', + el: document.getElementById('js-add-issues-btn'), data: { modal: ModalStore.store, store: Store.state, -- cgit v1.2.1 From 56e5404dd57c85cbd2e9dbde6694cdf1222af8a1 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Fri, 3 Feb 2017 15:07:52 -0600 Subject: fix failing test --- app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 index 5969d2ba56b..5840916846b 100644 --- a/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 +++ b/app/assets/javascripts/merge_request_widget/ci_bundle.js.es6 @@ -47,7 +47,7 @@ $('.js-rebase-button').html("<i class='fa fa-spinner fa-spin'></i> Rebase in progress"); }); } else { - merge_request_widget.getMergeStatus(); + setTimeout(() => merge_request_widget.getMergeStatus(), 200); } }); })(); -- cgit v1.2.1 From e29b1c6c9e62a363156ce14e0879cf4ff0f7e548 Mon Sep 17 00:00:00 2001 From: Jason Aquino <gitlabjason@mindtester.com> Date: Sat, 4 Feb 2017 09:36:12 -0500 Subject: Fixed typo Fixes gitlab-org/gitlab-ce#27674 --- changelogs/unreleased/slash-commands-typo.yml | 4 ++++ doc/user/project/slash_commands.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 changelogs/unreleased/slash-commands-typo.yml diff --git a/changelogs/unreleased/slash-commands-typo.yml b/changelogs/unreleased/slash-commands-typo.yml new file mode 100644 index 00000000000..e6ffb94bd08 --- /dev/null +++ b/changelogs/unreleased/slash-commands-typo.yml @@ -0,0 +1,4 @@ +--- +title: Fixed "substract" typo on /help/user/project/slash_commands +merge_request: 8976 +author: Jason Aquino diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md index a53c547e7d2..2fddd7c6503 100644 --- a/doc/user/project/slash_commands.md +++ b/doc/user/project/slash_commands.md @@ -32,6 +32,6 @@ do. | `/wip` | Toggle the Work In Progress status | | <code>/estimate <1w 3d 2h 14m></code> | Set time estimate | | `/remove_estimate` | Remove estimated time | -| <code>/spend <1h 30m | -1h 5m></code> | Add or substract spent time | +| <code>/spend <1h 30m | -1h 5m></code> | Add or subtract spent time | | `/remove_time_spent` | Remove time spent | | `/target_branch <Branch Name>` | Set target branch for current merge request | -- cgit v1.2.1 From d29a527ae80060b5dc1244364d495758195496e1 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Sat, 4 Feb 2017 09:52:03 -0600 Subject: fix rack-proxy dependency in production --- Gemfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile b/Gemfile index 4e04215e0b2..c3d9c6e7857 100644 --- a/Gemfile +++ b/Gemfile @@ -219,6 +219,8 @@ gem 'chronic', '~> 0.10.2' gem 'chronic_duration', '~> 0.10.6' gem 'webpack-rails', '~> 0.9.9' +gem 'rack-proxy', '~> 0.6.0' + gem 'sass-rails', '~> 5.0.6' gem 'coffee-rails', '~> 4.1.0' gem 'uglifier', '~> 2.7.2' @@ -311,8 +313,6 @@ group :development, :test do gem 'activerecord_sane_schema_dumper', '0.2' gem 'stackprof', '~> 0.2.10' - - gem 'rack-proxy', '~> 0.6.0' end group :test do -- cgit v1.2.1 From 1e97a6df24f66f70811fdd4b1412432e40ab8ebe Mon Sep 17 00:00:00 2001 From: Stan Hu <stanhu@gmail.com> Date: Sat, 4 Feb 2017 10:20:39 -0800 Subject: Add index to labels for `type` and project_id` When loading pages that display the number of open issues, the backend runs a query such as: ```sql SELECT "labels"."id" FROM "labels" WHERE "labels"."type" IN ('ProjectLabel') AND "labels"."project_id" = 1000 ``` This results in an entire scan of the `labels` table. To optimize performance, add the appropriate index to the table. Closes #27676 --- ...20170204181513_add_index_to_labels_for_type_and_project.rb | 11 +++++++++++ db/schema.rb | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb diff --git a/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb new file mode 100644 index 00000000000..8f944930807 --- /dev/null +++ b/db/migrate/20170204181513_add_index_to_labels_for_type_and_project.rb @@ -0,0 +1,11 @@ +class AddIndexToLabelsForTypeAndProject < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def change + add_concurrent_index :labels, [:type, :project_id] + end +end diff --git a/db/schema.rb b/db/schema.rb index c73c311ccb2..92b36218a15 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170130204620) do +ActiveRecord::Schema.define(version: 20170204181513) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -576,6 +576,7 @@ ActiveRecord::Schema.define(version: 20170130204620) do end add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree + add_index "labels", ["type", "project_id"], name: "index_labels_on_type_and_project_id", using: :btree create_table "lfs_objects", force: :cascade do |t| t.string "oid", null: false -- cgit v1.2.1 From 149f67cc2e43a579b0696509515d7ed9b3ecebd7 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Sat, 4 Feb 2017 23:04:17 +0100 Subject: Remove coverage entry from global CI/CD options --- lib/gitlab/ci/config/entry/global.rb | 5 +---- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 25 ++++++++----------------- spec/lib/gitlab/ci/config/entry/global_spec.rb | 12 ++++++------ 3 files changed, 15 insertions(+), 27 deletions(-) diff --git a/lib/gitlab/ci/config/entry/global.rb b/lib/gitlab/ci/config/entry/global.rb index ede97cc0504..a4ec8f0ff2f 100644 --- a/lib/gitlab/ci/config/entry/global.rb +++ b/lib/gitlab/ci/config/entry/global.rb @@ -33,11 +33,8 @@ module Gitlab entry :cache, Entry::Cache, description: 'Configure caching between build jobs.' - entry :coverage, Entry::Coverage, - description: 'Coverage configuration for this pipeline.' - helpers :before_script, :image, :services, :after_script, - :variables, :stages, :types, :cache, :coverage, :jobs + :variables, :stages, :types, :cache, :jobs def compose!(_deps = nil) super(self) do diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 49349035b3b..09d1dd806e6 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -5,27 +5,18 @@ module Ci let(:path) { 'path' } describe '#build_attributes' do - context 'Coverage entry' do + describe 'coverage entry' do subject { described_class.new(config, path).build_attributes(:rspec) } - let(:config_base) { { rspec: { script: "rspec" } } } - let(:config) { YAML.dump(config_base) } - - context 'when config has coverage set at the global scope' do - before do - config_base.update(coverage: '/\(\d+\.\d+\) covered/') - end - - context "and 'rspec' job doesn't have coverage set" do - it { is_expected.to include(coverage_regex: '\(\d+\.\d+\) covered') } + describe 'code coverage regexp' do + let(:config) do + YAML.dump(rspec: { script: 'rspec', + coverage: '/Code coverage: \d+\.\d+/'}) end - context "but 'rspec' job also has coverage set" do - before do - config_base[:rspec][:coverage] = '/Code coverage: \d+\.\d+/' - end - - it { is_expected.to include(coverage_regex: 'Code coverage: \d+\.\d+') } + it 'includes coverage regexp in build attributes' do + expect(subject) + .to include(coverage_regex: 'Code coverage: \d+\.\d+') end end end diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb index d4f1780b174..432a99dce33 100644 --- a/spec/lib/gitlab/ci/config/entry/global_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb @@ -10,10 +10,10 @@ describe Gitlab::Ci::Config::Entry::Global do context 'when filtering all the entry/node names' do it 'contains the expected node names' do - node_names = described_class.nodes.keys - expect(node_names).to match_array(%i[before_script image services - after_script variables stages - types cache coverage]) + expect(described_class.nodes.keys) + .to match_array(%i[before_script image services + after_script variables stages + types cache]) end end end @@ -40,7 +40,7 @@ describe Gitlab::Ci::Config::Entry::Global do end it 'creates node object for each entry' do - expect(global.descendants.count).to eq 9 + expect(global.descendants.count).to eq 8 end it 'creates node object using valid class' do @@ -181,7 +181,7 @@ describe Gitlab::Ci::Config::Entry::Global do describe '#nodes' do it 'instantizes all nodes' do - expect(global.descendants.count).to eq 9 + expect(global.descendants.count).to eq 8 end it 'contains unspecified nodes' do -- cgit v1.2.1 From 3deb66ea569cde62c3213b2671532c0db3c7263d Mon Sep 17 00:00:00 2001 From: Semyon Pupkov <mail@semyonpupkov.com> Date: Thu, 2 Feb 2017 15:10:25 +0500 Subject: Add traits for ProjectFeatures to Project factory https://gitlab.com/gitlab-org/gitlab-ce/issues/24007 --- spec/controllers/search_controller_spec.rb | 16 +++------------- spec/factories/projects.rb | 19 +++++++++++++++++++ spec/features/groups/merge_requests_spec.rb | 2 +- .../projects/settings/merge_requests_settings_spec.rb | 5 ----- .../wiki/user_views_wiki_in_project_page_spec.rb | 12 +++++------- spec/finders/issues_finder_spec.rb | 2 +- spec/finders/notes_finder_spec.rb | 10 +++++----- spec/helpers/projects_helper_spec.rb | 1 - spec/lib/gitlab/contributions_calendar_spec.rb | 2 +- spec/lib/gitlab/git_access_spec.rb | 2 -- spec/lib/gitlab/git_access_wiki_spec.rb | 2 -- spec/lib/gitlab/github_import/importer_spec.rb | 2 +- .../import_export/project_tree_restorer_spec.rb | 14 +++++++------- .../gitlab/import_export/project_tree_saver_spec.rb | 7 +++---- spec/lib/gitlab/project_search_results_spec.rb | 4 ++-- spec/models/ability_spec.rb | 2 +- spec/models/guest_spec.rb | 2 -- spec/models/project_feature_spec.rb | 2 -- spec/models/project_spec.rb | 2 +- spec/models/user_spec.rb | 2 +- spec/requests/api/issues_spec.rb | 2 +- spec/requests/git_http_spec.rb | 12 ++++-------- spec/services/projects/create_service_spec.rb | 4 ---- .../issuable_create_service_shared_examples.rb | 4 ++-- .../repository_check/single_repository_worker_spec.rb | 8 ++++---- 25 files changed, 62 insertions(+), 78 deletions(-) diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index b7bb9290712..3173aae664c 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe SearchController do let(:user) { create(:user) } - let(:project) { create(:empty_project, :public) } before do sign_in(user) @@ -22,7 +21,7 @@ describe SearchController do before { sign_out(user) } it "doesn't expose comments on issues" do - project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + project = create(:empty_project, :public, :issues_private) note = create(:note_on_issue, project: project) get :show, project_id: project.id, scope: 'notes', search: note.note @@ -31,17 +30,8 @@ describe SearchController do end end - it "doesn't expose comments on issues" do - project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) - note = create(:note_on_issue, project: project) - - get :show, project_id: project.id, scope: 'notes', search: note.note - - expect(assigns[:search_objects].count).to eq(0) - end - it "doesn't expose comments on merge_requests" do - project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + project = create(:empty_project, :public, :merge_requests_private) note = create(:note_on_merge_request, project: project) get :show, project_id: project.id, scope: 'notes', search: note.note @@ -50,7 +40,7 @@ describe SearchController do end it "doesn't expose comments on snippets" do - project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE) + project = create(:empty_project, :public, :snippets_private) note = create(:note_on_project_snippet, project: project) get :show, project_id: project.id, scope: 'notes', search: note.note diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 715b2a27b30..c80b09e9b9d 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -56,6 +56,25 @@ FactoryGirl.define do end end + trait(:wiki_enabled) { wiki_access_level ProjectFeature::ENABLED } + trait(:wiki_disabled) { wiki_access_level ProjectFeature::DISABLED } + trait(:wiki_private) { wiki_access_level ProjectFeature::PRIVATE } + trait(:builds_enabled) { builds_access_level ProjectFeature::ENABLED } + trait(:builds_disabled) { builds_access_level ProjectFeature::DISABLED } + trait(:builds_private) { builds_access_level ProjectFeature::PRIVATE } + trait(:snippets_enabled) { snippets_access_level ProjectFeature::ENABLED } + trait(:snippets_disabled) { snippets_access_level ProjectFeature::DISABLED } + trait(:snippets_private) { snippets_access_level ProjectFeature::PRIVATE } + trait(:issues_disabled) { issues_access_level ProjectFeature::DISABLED } + trait(:issues_enabled) { issues_access_level ProjectFeature::ENABLED } + trait(:issues_private) { issues_access_level ProjectFeature::PRIVATE } + trait(:merge_requests_enabled) { merge_requests_access_level ProjectFeature::ENABLED } + trait(:merge_requests_disabled) { merge_requests_access_level ProjectFeature::DISABLED } + trait(:merge_requests_private) { merge_requests_access_level ProjectFeature::PRIVATE } + trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED } + trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED } + trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE } + # Nest Project Feature attributes transient do wiki_access_level ProjectFeature::ENABLED diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb index 78a11ffee99..b55078c3bf6 100644 --- a/spec/features/groups/merge_requests_spec.rb +++ b/spec/features/groups/merge_requests_spec.rb @@ -7,7 +7,7 @@ feature 'Group merge requests page', feature: true do include_examples 'project features apply to issuables', MergeRequest context 'archived issuable' do - let(:project_archived) { create(:project, :archived, group: group, merge_requests_access_level: ProjectFeature::ENABLED) } + let(:project_archived) { create(:project, :archived, :merge_requests_enabled, group: group) } let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') } let(:access_level) { ProjectFeature::ENABLED } let(:user) { user_in_group } diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb index 034b75c2e51..6815039d5ed 100644 --- a/spec/features/projects/settings/merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/merge_requests_settings_spec.rb @@ -12,13 +12,8 @@ feature 'Project settings > Merge Requests', feature: true, js: true do end context 'when Merge Request and Pipelines are initially enabled' do - before do - project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::ENABLED) - end - context 'when Pipelines are initially enabled' do before do - project.project_feature.update_attribute('builds_access_level', ProjectFeature::ENABLED) visit edit_project_path(project) end diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb index b4f5f6b3fc5..20219f3cc9a 100644 --- a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb +++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe 'Projects > Wiki > User views wiki in project page', feature: true do let(:user) { create(:user) } - let(:project) { create(:empty_project) } before do project.team << [user, :master] @@ -10,12 +9,11 @@ describe 'Projects > Wiki > User views wiki in project page', feature: true do end context 'when repository is disabled for project' do - before do - project.project_feature.update!( - repository_access_level: ProjectFeature::DISABLED, - merge_requests_access_level: ProjectFeature::DISABLED, - builds_access_level: ProjectFeature::DISABLED - ) + let(:project) do + create(:empty_project, + :repository_disabled, + :merge_requests_disabled, + :builds_disabled) end context 'when wiki homepage contains a link' do diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 97737d7ddc7..12ab1d6dde8 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -224,7 +224,7 @@ describe IssuesFinder do let(:scope) { nil } it "doesn't return team-only issues to non team members" do - project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + project = create(:empty_project, :public, :issues_private) issue = create(:issue, project: project) expect(issues).not_to include(issue) diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index bac653ea451..f8b05d4e9bc 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -9,8 +9,6 @@ describe NotesFinder do end describe '#execute' do - it 'finds notes on snippets when project is public and user isnt a member' - it 'finds notes on merge requests' do create(:note_on_merge_request, project: project) @@ -45,9 +43,11 @@ describe NotesFinder do context 'on restricted projects' do let(:project) do - create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE, - snippets_access_level: ProjectFeature::PRIVATE, - merge_requests_access_level: ProjectFeature::PRIVATE) + create(:empty_project, + :public, + :issues_private, + :snippets_private, + :merge_requests_private) end it 'publicly excludes notes on merge requests' do diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 8d1570aa6f3..aca0bb1d794 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -203,7 +203,6 @@ describe ProjectsHelper do context "when project moves from public to private" do before do - project.project_feature.update_attributes(issues_access_level: ProjectFeature::ENABLED) project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) end diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index 01b2a55b63c..e18a219ef36 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -17,7 +17,7 @@ describe Gitlab::ContributionsCalendar do end let(:feature_project) do - create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) do |project| + create(:empty_project, :public, :issues_private) do |project| create(:project_member, user: contributor, project: project).project end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 116ab16ae74..a55bd4387e0 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -94,8 +94,6 @@ describe Gitlab::GitAccess, lib: true do context 'when repository is enabled' do it 'give access to download code' do - public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED) - expect(subject.allowed?).to be_truthy end end diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 4a0cdc6887e..1ae293416e4 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -36,8 +36,6 @@ describe Gitlab::GitAccessWiki, lib: true do context 'when wiki feature is enabled' do it 'give access to download wiki code' do - project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) - expect(subject.allowed?).to be_truthy end end diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb index 72421832ffc..afd78abdc9b 100644 --- a/spec/lib/gitlab/github_import/importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer_spec.rb @@ -202,7 +202,7 @@ describe Gitlab::GithubImport::Importer, lib: true do end end - let(:project) { create(:project, import_url: "#{repo_root}/octocat/Hello-World.git", wiki_access_level: ProjectFeature::DISABLED) } + let(:project) { create(:project, :wiki_disabled, import_url: "#{repo_root}/octocat/Hello-World.git") } let(:credentials) { { user: 'joe' } } context 'when importing a GitHub project' do diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 40d7d59f03b..0af13ba8e47 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do let(:user) { create(:user) } let(:namespace) { create(:namespace, owner: user) } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } - let!(:project) { create(:empty_project, name: 'project', path: 'project', builds_access_level: ProjectFeature::DISABLED, issues_access_level: ProjectFeature::DISABLED) } + let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } let(:restored_project_json) { project_tree_restorer.restore } @@ -121,13 +121,13 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do end context 'with group' do - let!(:project) do + let!(:project) do create(:empty_project, - name: 'project', - path: 'project', - builds_access_level: ProjectFeature::DISABLED, - issues_access_level: ProjectFeature::DISABLED, - group: create(:group)) + :builds_disabled, + :issues_disabled, + name: 'project', + path: 'project', + group: create(:group)) end it 'has group labels' do diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 1d65b24c2c9..550daa44010 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -152,6 +152,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do project = create(:project, :public, :repository, + :issues_disabled, + :wiki_enabled, + :builds_private, issues: [issue], snippets: [snippet], releases: [release], @@ -185,10 +188,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do create(:event, :created, target: milestone, project: project, author: user) create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker') - project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) - project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED) - project.project_feature.update_attribute(:builds_access_level, ProjectFeature::PRIVATE) - project end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 92e3624a8d8..9a8096208db 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -163,7 +163,7 @@ describe Gitlab::ProjectSearchResults, lib: true do end it "doesn't list issue notes when access is restricted" do - project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + project = create(:empty_project, :public, :issues_private) note = create(:note_on_issue, project: project) results = described_class.new(user, project, note.note) @@ -172,7 +172,7 @@ describe Gitlab::ProjectSearchResults, lib: true do end it "doesn't list merge_request notes when access is restricted" do - project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + project = create(:empty_project, :public, :merge_requests_private) note = create(:note_on_merge_request, project: project) results = described_class.new(user, project, note.note) diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 2f4a33a1868..30f8fdf91b2 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -247,7 +247,7 @@ describe Ability, lib: true do end describe '.project_disabled_features_rules' do - let(:project) { create(:empty_project, wiki_access_level: ProjectFeature::DISABLED) } + let(:project) { create(:empty_project, :wiki_disabled) } subject { described_class.allowed(project.owner, project) } diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb index 582b54c0712..c60bd7af958 100644 --- a/spec/models/guest_spec.rb +++ b/spec/models/guest_spec.rb @@ -37,8 +37,6 @@ describe Guest, lib: true do context 'when repository is enabled' do it 'allows to pull the repo' do - public_project.project_feature.update_attribute(:repository_access_level, ProjectFeature::ENABLED) - expect(Guest.can?(:download_code, public_project)).to eq(true) end end diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index 8589f1eb712..09a4448d387 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -57,7 +57,6 @@ describe ProjectFeature do context 'when feature is enabled for everyone' do it "returns true" do features.each do |feature| - project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED) expect(project.feature_available?(:issues, user)).to eq(true) end end @@ -104,7 +103,6 @@ describe ProjectFeature do it "returns true when feature is enabled for everyone" do features.each do |feature| - project.project_feature.update_attribute("#{feature}_access_level".to_sym, ProjectFeature::ENABLED) expect(project.public_send("#{feature}_enabled?")).to eq(true) end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 48b085781e7..264a5b04a26 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -632,7 +632,7 @@ describe Project, models: true do end describe '#has_wiki?' do - let(:no_wiki_project) { create(:empty_project, wiki_access_level: ProjectFeature::DISABLED, has_external_wiki: false) } + let(:no_wiki_project) { create(:empty_project, :wiki_disabled, has_external_wiki: false) } let(:wiki_enabled_project) { create(:empty_project) } let(:external_wiki_project) { create(:empty_project, has_external_wiki: true) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6d58b1455c4..6c4c82cef53 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1232,7 +1232,7 @@ describe User, models: true do end it 'does not include projects for which issues are disabled' do - project = create(:empty_project, issues_access_level: ProjectFeature::DISABLED) + project = create(:empty_project, :issues_disabled) expect(user.projects_where_can_admin_issues.to_a).to be_empty expect(user.can?(:admin_issue, project)).to eq(false) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 62f1b8d7ca2..834c83166c0 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -425,7 +425,7 @@ describe API::Issues, api: true do end it 'returns no issues when user has access to project but not issues' do - restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + restricted_project = create(:empty_project, :public, :issues_private) create(:issue, project: restricted_project) get api("/projects/#{restricted_project.id}/issues", non_member) diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 4a16824de04..87786e85621 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -57,7 +57,7 @@ describe 'Git HTTP requests', lib: true do end context 'but the repo is disabled' do - let(:project) { create(:project, repository_access_level: ProjectFeature::DISABLED, wiki_access_level: ProjectFeature::ENABLED) } + let(:project) { create(:project, :repository_disabled, :wiki_enabled) } let(:wiki) { ProjectWiki.new(project) } let(:path) { "/#{wiki.repository.path_with_namespace}.git" } @@ -141,7 +141,7 @@ describe 'Git HTTP requests', lib: true do context 'when the repo is public' do context 'but the repo is disabled' do it 'does not allow to clone the repo' do - project = create(:project, :public, repository_access_level: ProjectFeature::DISABLED) + project = create(:project, :public, :repository_disabled) download("#{project.path_with_namespace}.git", {}) do |response| expect(response).to have_http_status(:unauthorized) @@ -151,7 +151,7 @@ describe 'Git HTTP requests', lib: true do context 'but the repo is enabled' do it 'allows to clone the repo' do - project = create(:project, :public, repository_access_level: ProjectFeature::ENABLED) + project = create(:project, :public, :repository_enabled) download("#{project.path_with_namespace}.git", {}) do |response| expect(response).to have_http_status(:ok) @@ -161,7 +161,7 @@ describe 'Git HTTP requests', lib: true do context 'but only project members are allowed' do it 'does not allow to clone the repo' do - project = create(:project, :public, repository_access_level: ProjectFeature::PRIVATE) + project = create(:project, :public, :repository_private) download("#{project.path_with_namespace}.git", {}) do |response| expect(response).to have_http_status(:unauthorized) @@ -360,10 +360,6 @@ describe 'Git HTTP requests', lib: true do let(:project) { build.project } let(:other_project) { create(:empty_project) } - before do - project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED) - end - context 'when build created by system is authenticated' do it "downloads get status 200" do clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index a1539b69401..af515ad2e0e 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -90,10 +90,6 @@ describe Projects::CreateService, '#execute', services: true do end context 'global builds_enabled true does enable CI by default' do - before do - project.project_feature.update_attribute(:builds_access_level, ProjectFeature::ENABLED) - end - it { is_expected.to be_truthy } end end diff --git a/spec/support/services/issuable_create_service_shared_examples.rb b/spec/support/services/issuable_create_service_shared_examples.rb index 93c0267d2db..4f0c745b7ee 100644 --- a/spec/support/services/issuable_create_service_shared_examples.rb +++ b/spec/support/services/issuable_create_service_shared_examples.rb @@ -31,8 +31,8 @@ shared_examples 'issuable create service' do context "when issuable feature is private" do before do - project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE) - project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE) + project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE) end levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC] diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb index 59cfb2c8e3a..d2609d21546 100644 --- a/spec/workers/repository_check/single_repository_worker_spec.rb +++ b/spec/workers/repository_check/single_repository_worker_spec.rb @@ -5,7 +5,7 @@ describe RepositoryCheck::SingleRepositoryWorker do subject { described_class.new } it 'passes when the project has no push events' do - project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED) + project = create(:project_empty_repo, :wiki_disabled) project.events.destroy_all break_repo(project) @@ -25,7 +25,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'fails if the wiki repository is broken' do - project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED) + project = create(:project_empty_repo, :wiki_enabled) project.create_wiki # Test sanity: everything should be fine before the wiki repo is broken @@ -39,7 +39,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'skips wikis when disabled' do - project = create(:project_empty_repo, wiki_access_level: ProjectFeature::DISABLED) + project = create(:project_empty_repo, :wiki_disabled) # Make sure the test would fail if the wiki repo was checked break_wiki(project) @@ -49,7 +49,7 @@ describe RepositoryCheck::SingleRepositoryWorker do end it 'creates missing wikis' do - project = create(:project_empty_repo, wiki_access_level: ProjectFeature::ENABLED) + project = create(:project_empty_repo, :wiki_enabled) FileUtils.rm_rf(wiki_path(project)) subject.perform(project.id) -- cgit v1.2.1 From f5de8f159e9fb79ed523fcb41c4b062eeaf3f811 Mon Sep 17 00:00:00 2001 From: Robert Schilling <rschilling@student.tugraz.at> Date: Sun, 5 Feb 2017 10:57:14 +0100 Subject: Ensure the right content is served for the build artifacts API --- spec/requests/api/builds_spec.rb | 1 + spec/support/matchers/match_file.rb | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 spec/support/matchers/match_file.rb diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index bd6e23ee769..acc2b163eaf 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -188,6 +188,7 @@ describe API::Builds, api: true do it 'returns specific build artifacts' do expect(response).to have_http_status(200) expect(response.headers).to include(download_headers) + expect(response.body).to match_file(build.artifacts_file.file.file) end end diff --git a/spec/support/matchers/match_file.rb b/spec/support/matchers/match_file.rb new file mode 100644 index 00000000000..d1888b3376a --- /dev/null +++ b/spec/support/matchers/match_file.rb @@ -0,0 +1,5 @@ +RSpec::Matchers.define :match_file do |expected| + match do |actual| + expect(Digest::MD5.hexdigest(actual)).to eq(Digest::MD5.hexdigest(File.read(expected))) + end +end -- cgit v1.2.1 From 77c97e3dd13effec43780f64d947013f951b9f04 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sat, 4 Feb 2017 21:59:47 +0000 Subject: Fix broken path to file Fix broken path to file --- app/assets/javascripts/environments/components/environment_item.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index 93e4078ba4f..6a3d996f69c 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -4,7 +4,7 @@ window.Vue = require('vue'); window.timeago = require('vendor/timeago'); require('../../lib/utils/text_utility'); -require('../vue_shared/vue_common_component/commit'); +require('../../vue_shared/components/commit'); require('./environment_actions'); require('./environment_external_url'); require('./environment_stop'); -- cgit v1.2.1 From 9b4fcd52694cf8d771edbd4680b82101ae4ff85e Mon Sep 17 00:00:00 2001 From: David <david.piegza@mailbox.org> Date: Thu, 2 Feb 2017 21:42:03 +0000 Subject: Fix markdown links in PROCESS.md --- PROCESS.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/PROCESS.md b/PROCESS.md index 993d60bbba8..6eabaf05d24 100644 --- a/PROCESS.md +++ b/PROCESS.md @@ -33,7 +33,7 @@ core team members will mention this person. ### Merge request coaching Several people from the [GitLab team][team] are helping community members to get -their contributions accepted by meeting our [Definition of done][CONTRIBUTING.md#definition-of-done]. +their contributions accepted by meeting our [Definition of done](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done). What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/. @@ -79,47 +79,47 @@ not be merged into any stable branches. ### Improperly formatted issue -Thanks for the issue report. Please reformat your issue to conform to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). +Thanks for the issue report. Please reformat your issue to conform to the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). ### Issue report for old version -Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). +Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). ### Support requests and configuration questions Thanks for your interest in GitLab. We don't use the issue tracker for support requests and configuration questions. Please check our -\[getting help\]\(https://about.gitlab.com/getting-help/) page to see all of the available -support options. Also, have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) +[getting help](https://about.gitlab.com/getting-help/) page to see all of the available +support options. Also, have a look at the [contribution guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information. ### Code format -Please use ``` to format console output, logs, and code as it's very hard to read otherwise. +Please use \`\`\` to format console output, logs, and code as it's very hard to read otherwise. ### Issue fixed in newer version -Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please \[upgrade\]\(https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). +Thanks for the issue report. This issue has already been fixed in newer versions of GitLab. Due to the size of this project and our limited resources we are only able to support the latest stable release as outlined in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker). In order to get this bug fix and enjoy many new features please [upgrade](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/update). If you still experience issues at that time please open a new issue following our issue tracker guidelines found in the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). ### Improperly formatted merge request -Thanks for your interest in improving the GitLab codebase! Please update your merge request according to the \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-request-guidelines). +Thanks for your interest in improving the GitLab codebase! Please update your merge request according to the [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-request-guidelines). ### Inactivity close of an issue -It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). +It's been at least 2 weeks (and a new release) since we heard from you. I'm closing this issue but if you still experience this problem, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines). ### Inactivity close of a merge request -This merge request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the merge request guidelines in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this merge request. +This merge request has been closed because a request for more information has not been reacted to for more than 2 weeks. If you respond and conform to the merge request guidelines in our [contributing guidelines](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#pull-requests) we will reopen this merge request. ### Accepting merge requests Is there an issue on the -\[issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues) that is +[issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues) that is similar to this? Could you please link it here? Please be aware that new functionality that is not marked -\[accepting merge requests\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests) +[accepting merge requests](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests) might not make it into GitLab. ### Only accepting merge requests with green tests @@ -134,7 +134,7 @@ rebase with master to see if that solves the issue. We are currently in the process of closing down the issue tracker on GitHub, to prevent duplication with the GitLab.com issue tracker. Since this is an older issue I'll be closing this for now. If you think this is -still an issue I encourage you to open it on the \[GitLab.com issue tracker\]\(https://gitlab.com/gitlab-org/gitlab-ce/issues). +still an issue I encourage you to open it on the [GitLab.com issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues). [team]: https://about.gitlab.com/team/ [contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria -- cgit v1.2.1 From 2621ce5ca08445b365508cbd536c0ab6d0b69a25 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sun, 5 Feb 2017 17:36:00 +0000 Subject: Move repeated function to utilities --- app/assets/javascripts/lib/utils/common_utils.js.es6 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index e3bff2559fd..cde48474906 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -230,5 +230,13 @@ return upperCaseHeaders; }; + + /** + * Transforms a DOMStringMap into a plain object. + */ + w.gl.utils.DOMStringMapToObject = DOMStringMapObject => Object.keys(DOMStringMapObject).reduce((acc, element) => { + acc[element] = DOMStringMapObject[element]; + return acc; + }, {}); })(window); }).call(this); -- cgit v1.2.1 From d5a223bdec686419c32072795eaebfc3f96a7c42 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sun, 5 Feb 2017 17:36:35 +0000 Subject: Use webpack --- .../commit/pipelines/pipelines_bundle.js.es6 | 7 +- .../commit/pipelines/pipelines_service.js.es6 | 16 +--- .../commit/pipelines/pipelines_store.js.es6 | 87 ++++++++++------------ .../commit/pipelines/pipelines_table.js.es6 | 19 ++--- 4 files changed, 55 insertions(+), 74 deletions(-) diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 index b21d13842a4..3a06e41848b 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 @@ -1,9 +1,10 @@ /* eslint-disable no-new, no-param-reassign */ /* global Vue, CommitsPipelineStore, PipelinesService, Flash */ -//= require vue -//= require_tree . - +window.Vue = require('vue'); +require('./pipelines_store'); +require('./pipelines_service'); +require('./pipelines_table'); /** * Commits View > Pipelines Tab > Pipelines Table. * Merge Request View > Pipelines Tab > Pipelines Table. diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 index f4ed986b0c5..ef3902fba50 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 @@ -8,18 +8,8 @@ * Uses Vue.Resource */ class PipelinesService { - constructor(root) { - Vue.http.options.root = root; - - this.pipelines = Vue.resource(root); - - Vue.http.interceptors.push((request, next) => { - // needed in order to not break the tests. - if ($.rails) { - request.headers['X-CSRF-Token'] = $.rails.csrfToken(); - } - next(); - }); + constructor(endpoint) { + this.pipelines = Vue.resource(endpoint); } /** @@ -28,7 +18,7 @@ class PipelinesService { * * @return {Promise} */ - all() { + get() { return this.pipelines.get(); } } diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 index fe90e7bac0a..178e7bc9cd7 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -5,50 +5,43 @@ * Used to store the Pipelines rendered in the commit view in the pipelines table. */ -(() => { - window.gl = window.gl || {}; - gl.commits = gl.commits || {}; - gl.commits.pipelines = gl.commits.pipelines || {}; - - gl.commits.pipelines.PipelinesStore = { - state: {}, - - create() { - this.state.pipelines = []; - - return this; - }, - - store(pipelines = []) { - this.state.pipelines = pipelines; - - return pipelines; - }, - - /** - * Once the data is received we will start the time ago loops. - * - * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we - * update the time to show how long as passed. - * - */ - startTimeAgoLoops() { - const startTimeLoops = () => { - this.timeLoopInterval = setInterval(() => { - this.$children[0].$children.reduce((acc, component) => { - const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; - acc.push(timeAgoComponent); - return acc; - }, []).forEach(e => e.changeTime()); - }, 10000); - }; - - startTimeLoops(); - - const removeIntervals = () => clearInterval(this.timeLoopInterval); - const startIntervals = () => startTimeLoops(); - - gl.VueRealtimeListener(removeIntervals, startIntervals); - }, - }; -})(); +class PipelinesStore { + constructor() { + this.state = {}; + this.state.pipelines = []; + } + + storePipelines(pipelines = []) { + this.state.pipelines = pipelines; + + return pipelines; + } + + /** + * Once the data is received we will start the time ago loops. + * + * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we + * update the time to show how long as passed. + * + */ + startTimeAgoLoops() { + const startTimeLoops = () => { + this.timeLoopInterval = setInterval(() => { + this.$children[0].$children.reduce((acc, component) => { + const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0]; + acc.push(timeAgoComponent); + return acc; + }, []).forEach(e => e.changeTime()); + }, 10000); + }; + + startTimeLoops(); + + const removeIntervals = () => clearInterval(this.timeLoopInterval); + const startIntervals = () => startTimeLoops(); + + gl.VueRealtimeListener(removeIntervals, startIntervals); + } +} + +return PipelinesStore; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 index 18d57333f61..93fca933b0d 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 @@ -1,11 +1,11 @@ /* eslint-disable no-new, no-param-reassign */ /* global Vue, CommitsPipelineStore, PipelinesService, Flash */ -//= require vue -//= require vue-resource -//= require vue_shared/vue_resource_interceptor -//= require vue_shared/components/pipelines_table -//= require vue_realtime_listener/index +window.Vue = require('vue'); +window.Vue.use(require('vue-resource')); +require('../../vue_shared/vue_resource_interceptor'); +require('../../vue_shared/components/pipelines_table'); +require('../vue_realtime_listener/index'); /** * @@ -38,13 +38,10 @@ data() { const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; const svgsData = document.querySelector('.pipeline-svgs').dataset; - const store = gl.commits.pipelines.PipelinesStore.create(); + const store = new gl.commits.pipelines.PipelinesStore(); // Transform svgs DOMStringMap to a plain Object. - const svgsObject = Object.keys(svgsData).reduce((acc, element) => { - acc[element] = svgsData[element]; - return acc; - }, {}); + const svgsObject = gl.utils.DOMStringMapToObject(svgsData); return { endpoint: pipelinesTableData.endpoint, @@ -71,7 +68,7 @@ return gl.pipelines.pipelinesService.all() .then(response => response.json()) .then((json) => { - this.store.store(json); + this.store.storePipelines(json); this.store.startTimeAgoLoops.call(this, Vue); this.isLoading = false; }) -- cgit v1.2.1 From daf5edf7ba39133be8477e7fb965e69e6ddc4a27 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sun, 5 Feb 2017 17:36:52 +0000 Subject: Move interceptor to common interceptors files --- app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 index d627fa2b88a..d3229f9f730 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js.es6 @@ -13,3 +13,11 @@ Vue.http.interceptors.push((request, next) => { Vue.activeResources--; }); }); + +Vue.http.interceptors.push((request, next) => { + // needed in order to not break the tests. + if ($.rails) { + request.headers['X-CSRF-Token'] = $.rails.csrfToken(); + } + next(); +}); -- cgit v1.2.1 From d97b96228973658fbd390823c1ab6f5364b33d00 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sun, 5 Feb 2017 18:17:38 +0000 Subject: Use webpack to require files --- app/assets/javascripts/vue_pipelines_index/index.js.es6 | 2 -- .../javascripts/vue_pipelines_index/pipelines.js.es6 | 7 ++++--- .../javascripts/vue_pipelines_index/time_ago.js.es6 | 3 ++- .../vue_shared/components/pipelines_table.js.es6 | 4 ++-- .../vue_shared/components/pipelines_table_row.js.es6 | 14 +++++++------- spec/javascripts/commit/pipelines/pipelines_spec.js.es6 | 15 +++++++-------- .../commit/pipelines/pipelines_store_spec.js.es6 | 3 +-- .../vue_shared/components/pipelines_table_row_spec.js.es6 | 5 ++--- .../vue_shared/components/pipelines_table_spec.js.es6 | 7 +++---- 9 files changed, 28 insertions(+), 32 deletions(-) diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index 4eeca43c6b9..9acb22d937f 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -6,8 +6,6 @@ require('../vue_shared/vue_resource_interceptor'); require('./pipelines'); $(() => { - Vue.use(VueResource); - return new Vue({ el: document.querySelector('.vue-pipelines-index'), diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 index ec4d6ee51d2..e47dc6935d6 100644 --- a/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js.es6 @@ -1,9 +1,10 @@ /* global Vue, gl */ /* eslint-disable no-param-reassign */ -//= require vue_shared/components/table_pagination -//= require ./store.js.es6 -//= require vue_shared/components/pipelines_table +window.Vue = require('vue'); +require('../vue_shared/components/table_pagination'); +require('./store'); +require('../vue_shared/components/pipelines_table'); ((gl) => { gl.VuePipelines = Vue.extend({ diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 index 61417b28630..3598da11573 100644 --- a/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/time_ago.js.es6 @@ -1,7 +1,8 @@ /* global Vue, gl */ /* eslint-disable no-param-reassign */ -//= require lib/utils/datetime_utility +window.Vue = require('vue'); +require('../lib/utils/datetime_utility'); ((gl) => { gl.VueTimeAgo = Vue.extend({ diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index 9bc1ea65e53..e4176252c8f 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -1,8 +1,8 @@ /* eslint-disable no-param-reassign */ /* global Vue */ -//= require ./pipelines_table_row - +window.Vue = require('vue'); +require('./pipelines_table_row'); /** * Pipelines Table Component. * diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 index 375516e3804..8707ab5d3e0 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -1,13 +1,13 @@ /* eslint-disable no-param-reassign */ /* global Vue */ -//= require vue_pipelines_index/status -//= require vue_pipelines_index/pipeline_url -//= require vue_pipelines_index/stage -//= require vue_shared/components/commit -//= require vue_pipelines_index/pipeline_actions -//= require vue_pipelines_index/time_ago - +window.Vue = require('vue'); +require('../../vue_pipelines_index/status'); +require('../../vue_pipelines_index/pipeline_url'); +require('../../vue_pipelines_index/stage'); +require('../../vue_pipelines_index/pipeline_actions'); +require('../../vue_pipelines_index/time_ago'); +require('./commit'); /** * Pipeline table row. * diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 index 3bcc0d1eb18..c2b61632827 100644 --- a/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 +++ b/spec/javascripts/commit/pipelines/pipelines_spec.js.es6 @@ -1,13 +1,12 @@ /* global pipeline, Vue */ -//= require vue -//= require vue-resource -//= require flash -//= require commit/pipelines/pipelines_store -//= require commit/pipelines/pipelines_service -//= require commit/pipelines/pipelines_table -//= require vue_shared/vue_resource_interceptor -//= require ./mock_data +require('vue-resource'); +require('flash'); +require('~/commit/pipelines/pipelines_store'); +require('~/commit/pipelines/pipelines_service'); +require('~/commit/pipelines/pipelines_table'); +require('~vue_shared/vue_resource_interceptor'); +require('./mock_data'); describe('Pipelines table in Commits and Merge requests', () => { preloadFixtures('pipelines_table'); diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 index 46a7df3bb21..a5a16544ffb 100644 --- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 +++ b/spec/javascripts/commit/pipelines/pipelines_store_spec.js.es6 @@ -1,5 +1,4 @@ -//= require vue -//= require commit/pipelines/pipelines_store +require('~commit/pipelines/pipelines_store'); describe('Store', () => { const store = gl.commits.pipelines.PipelinesStore; diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 index 6825de069e4..e83a1749e82 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js.es6 @@ -1,8 +1,7 @@ /* global pipeline */ -//= require vue -//= require vue_shared/components/pipelines_table_row -//= require commit/pipelines/mock_data +require('~vue_shared/components/pipelines_table_row'); +require('./mock_data'); describe('Pipelines Table Row', () => { let component; diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 index cb1006d44dc..adc9ea904cc 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 +++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js.es6 @@ -1,9 +1,8 @@ /* global pipeline */ -//= require vue -//= require vue_shared/components/pipelines_table -//= require commit/pipelines/mock_data -//= require lib/utils/datetime_utility +require('~vue_shared/components/pipelines_table'); +require('~lib/utils/datetime_utility'); +require('./mock_data'); describe('Pipelines Table', () => { preloadFixtures('static/environments/element.html.raw'); -- cgit v1.2.1 From 2e0e2b22d65efddf21c03d8c0785281724675ece Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Mon, 6 Feb 2017 00:24:19 +0530 Subject: Backport changes from gitlab-org/gitlab-ee!998 Some changes in EE for the auditor user feature need to be backported to CE to avoid merge conflicts. This commit encapsulates all these backports. --- app/policies/project_policy.rb | 47 +++++++++++++--------- app/policies/project_snippet_policy.rb | 2 +- .../projects/notes/_notes_with_form.html.haml | 2 +- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 71ef8901932..be1c4d868ed 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -214,25 +214,7 @@ class ProjectPolicy < BasePolicy def anonymous_rules return unless project.public? - can! :read_project - can! :read_board - can! :read_list - can! :read_wiki - can! :read_label - can! :read_milestone - can! :read_project_snippet - can! :read_project_member - can! :read_merge_request - can! :read_note - can! :read_pipeline - can! :read_commit_status - can! :read_container_image - can! :download_code - can! :download_wiki_code - can! :read_cycle_analytics - - # NOTE: may be overridden by IssuePolicy - can! :read_issue + base_readonly_access! # Allow to read builds by anonymous user if guests are allowed can! :read_build if project.public_builds? @@ -265,4 +247,31 @@ class ProjectPolicy < BasePolicy :"admin_#{name}" ] end + + private + + # A base set of abilities for read-only users, which + # is then augmented as necessary for anonymous and other + # read-only users. + def base_readonly_access! + can! :read_project + can! :read_board + can! :read_list + can! :read_wiki + can! :read_label + can! :read_milestone + can! :read_project_snippet + can! :read_project_member + can! :read_merge_request + can! :read_note + can! :read_pipeline + can! :read_commit_status + can! :read_container_image + can! :download_code + can! :download_wiki_code + can! :read_cycle_analytics + + # NOTE: may be overridden by IssuePolicy + can! :read_issue + end end diff --git a/app/policies/project_snippet_policy.rb b/app/policies/project_snippet_policy.rb index 57acccfafd9..3a96836917e 100644 --- a/app/policies/project_snippet_policy.rb +++ b/app/policies/project_snippet_policy.rb @@ -3,7 +3,7 @@ class ProjectSnippetPolicy < BasePolicy can! :read_project_snippet if @subject.public? return unless @user - if @user && @subject.author == @user || @user.admin? + if @user && (@subject.author == @user || @user.admin?) can! :read_project_snippet can! :update_project_snippet can! :admin_project_snippet diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index fbd2bff5bbb..08c73d94a09 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -13,7 +13,7 @@ = image_tag avatar_icon(current_user), alt: current_user.to_reference, class: 'avatar s40' .timeline-content.timeline-content-form = render "projects/notes/form", view: diff_view - - else + - elsif !current_user .disabled-comment.text-center .disabled-comment-text.inline Please -- cgit v1.2.1 From d5093ef569050996aaed038f97c5a3257a77f504 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda <filipa@gitlab.com> Date: Sun, 5 Feb 2017 20:44:01 +0000 Subject: Use webpack to require files Changes after review --- .../commit/pipelines/pipelines_bundle.js.es6 | 2 - .../commit/pipelines/pipelines_service.js.es6 | 2 +- .../commit/pipelines/pipelines_store.js.es6 | 5 +- .../commit/pipelines/pipelines_table.js.es6 | 9 ++-- .../javascripts/lib/utils/common_utils.js.es6 | 3 ++ .../javascripts/vue_pipelines_index/index.js.es6 | 56 ++++++++++------------ .../vue_shared/components/commit.js.es6 | 2 - .../vue_shared/components/pipelines_table.js.es6 | 1 - .../components/pipelines_table_row.js.es6 | 1 - app/views/projects/commit/_pipelines_list.haml | 2 +- app/views/projects/pipelines/index.html.haml | 3 +- config/webpack.config.js | 1 + 12 files changed, 43 insertions(+), 44 deletions(-) diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 index 3a06e41848b..fbfec7743c7 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js.es6 @@ -2,8 +2,6 @@ /* global Vue, CommitsPipelineStore, PipelinesService, Flash */ window.Vue = require('vue'); -require('./pipelines_store'); -require('./pipelines_service'); require('./pipelines_table'); /** * Commits View > Pipelines Tab > Pipelines Table. diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 index ef3902fba50..483b414126a 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_service.js.es6 @@ -18,7 +18,7 @@ class PipelinesService { * * @return {Promise} */ - get() { + all() { return this.pipelines.get(); } } diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 index 178e7bc9cd7..f1b41911b73 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_store.js.es6 @@ -44,4 +44,7 @@ class PipelinesStore { } } -return PipelinesStore; +window.gl = window.gl || {}; +gl.commits = gl.commits || {}; +gl.commits.pipelines = gl.commits.pipelines || {}; +gl.commits.pipelines.PipelinesStore = PipelinesStore; diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 index 93fca933b0d..ce0dbd4d56b 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js.es6 @@ -3,9 +3,12 @@ window.Vue = require('vue'); window.Vue.use(require('vue-resource')); +require('../../lib/utils/common_utils'); require('../../vue_shared/vue_resource_interceptor'); require('../../vue_shared/components/pipelines_table'); -require('../vue_realtime_listener/index'); +require('../../vue_realtime_listener/index'); +require('./pipelines_service'); +require('./pipelines_store'); /** * @@ -62,10 +65,10 @@ require('../vue_realtime_listener/index'); * */ created() { - gl.pipelines.pipelinesService = new PipelinesService(this.endpoint); + const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint); this.isLoading = true; - return gl.pipelines.pipelinesService.all() + return pipelinesService.all() .then(response => response.json()) .then((json) => { this.store.storePipelines(json); diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index cde48474906..0ee29a75c62 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -233,6 +233,9 @@ /** * Transforms a DOMStringMap into a plain object. + * + * @param {DOMStringMap} DOMStringMapObject + * @returns {Object} */ w.gl.utils.DOMStringMapToObject = DOMStringMapObject => Object.keys(DOMStringMapObject).reduce((acc, element) => { acc[element] = DOMStringMapObject[element]; diff --git a/app/assets/javascripts/vue_pipelines_index/index.js.es6 b/app/assets/javascripts/vue_pipelines_index/index.js.es6 index 9acb22d937f..e7432afb56e 100644 --- a/app/assets/javascripts/vue_pipelines_index/index.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/index.js.es6 @@ -2,39 +2,35 @@ /* global Vue, VueResource, gl */ window.Vue = require('vue'); window.Vue.use(require('vue-resource')); +require('../lib/utils/common_utils'); require('../vue_shared/vue_resource_interceptor'); require('./pipelines'); -$(() => { - return new Vue({ - el: document.querySelector('.vue-pipelines-index'), +$(() => new Vue({ + el: document.querySelector('.vue-pipelines-index'), - data() { - const project = document.querySelector('.pipelines'); - const svgs = document.querySelector('.pipeline-svgs').dataset; + data() { + const project = document.querySelector('.pipelines'); + const svgs = document.querySelector('.pipeline-svgs').dataset; - // Transform svgs DOMStringMap to a plain Object. - const svgsObject = Object.keys(svgs).reduce((acc, element) => { - acc[element] = svgs[element]; - return acc; - }, {}); + // Transform svgs DOMStringMap to a plain Object. + const svgsObject = gl.utils.DOMStringMapToObject(svgs); - return { - scope: project.dataset.url, - store: new gl.PipelineStore(), - svgs: svgsObject, - }; - }, - components: { - 'vue-pipelines': gl.VuePipelines, - }, - template: ` - <vue-pipelines - :scope='scope' - :store='store' - :svgs='svgs' - > - </vue-pipelines> - `, - }); -}); + return { + scope: project.dataset.url, + store: new gl.PipelineStore(), + svgs: svgsObject, + }; + }, + components: { + 'vue-pipelines': gl.VuePipelines, + }, + template: ` + <vue-pipelines + :scope='scope' + :store='store' + :svgs='svgs' + > + </vue-pipelines> + `, +})); diff --git a/app/assets/javascripts/vue_shared/components/commit.js.es6 b/app/assets/javascripts/vue_shared/components/commit.js.es6 index 4adad7bea31..7f7c18ddeb1 100644 --- a/app/assets/javascripts/vue_shared/components/commit.js.es6 +++ b/app/assets/javascripts/vue_shared/components/commit.js.es6 @@ -1,7 +1,5 @@ /* global Vue */ -window.Vue = require('vue'); - (() => { window.gl = window.gl || {}; diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 index e4176252c8f..4bdaef31ee9 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js.es6 @@ -1,7 +1,6 @@ /* eslint-disable no-param-reassign */ /* global Vue */ -window.Vue = require('vue'); require('./pipelines_table_row'); /** * Pipelines Table Component. diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 index 8707ab5d3e0..c819f0dd7cd 100644 --- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 +++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js.es6 @@ -1,7 +1,6 @@ /* eslint-disable no-param-reassign */ /* global Vue */ -window.Vue = require('vue'); require('../../vue_pipelines_index/status'); require('../../vue_pipelines_index/pipeline_url'); require('../../vue_pipelines_index/stage'); diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml index bfe5eb18ad1..aae2cb8a04b 100644 --- a/app/views/projects/commit/_pipelines_list.haml +++ b/app/views/projects/commit/_pipelines_list.haml @@ -22,4 +22,4 @@ } } - content_for :page_specific_javascripts do - = page_specific_javascript_tag('commit/pipelines/pipelines_bundle.js') + = page_specific_javascript_bundle_tag('commit_pipelines') diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml index 58e76afa09a..81e393d7626 100644 --- a/app/views/projects/pipelines/index.html.haml +++ b/app/views/projects/pipelines/index.html.haml @@ -60,5 +60,4 @@ .vue-pipelines-index -- content_for :page_specific_javascripts do - = page_specific_javascript_tag('vue_pipelines_index/index.js') += page_specific_javascript_bundle_tag('vue_pipelines') diff --git a/config/webpack.config.js b/config/webpack.config.js index 7cd92af7d93..750d841ff84 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -19,6 +19,7 @@ var config = { boards: './boards/boards_bundle.js', boards_test: './boards/test_utils/simulate_drag.js', cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', + commit_pipelines: './commit/pipelines/pipelines_bundle.js', diff_notes: './diff_notes/diff_notes_bundle.js', environments: './environments/environments_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js', -- cgit v1.2.1 From 70c3b984f4293090e4a4b68368572fbe4a3dd5bc Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Sun, 29 Jan 2017 15:21:05 -0600 Subject: fix ace editor modules to include asset digest in production --- app/assets/javascripts/lib/ace/ace_config_paths.js.erb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/lib/ace/ace_config_paths.js.erb b/app/assets/javascripts/lib/ace/ace_config_paths.js.erb index 25e623f0fdc..5cb1037bc86 100644 --- a/app/assets/javascripts/lib/ace/ace_config_paths.js.erb +++ b/app/assets/javascripts/lib/ace/ace_config_paths.js.erb @@ -15,11 +15,13 @@ end // configure paths for all worker modules <% ace_workers.each do |worker| %> - ace.config.setModuleUrl('ace/mode/<%= worker %>_worker', basePath + '/worker-<%= worker %>.js'); + <% filename = File.basename(asset_path("ace/worker-#{worker}.js")) %> + ace.config.setModuleUrl('ace/mode/<%= worker %>_worker', basePath + '/<%= filename %>'); <% end %> // configure paths for all mode modules <% ace_modes.each do |mode| %> - ace.config.setModuleUrl('ace/mode/<%= mode %>', basePath + '/mode-<%= mode %>.js'); + <% filename = File.basename(asset_path("ace/mode-#{mode}.js")) %> + ace.config.setModuleUrl('ace/mode/<%= mode %>', basePath + '/<%= filename %>'); <% end %> })(); -- cgit v1.2.1 From 55d05bf8d8e4ecab456e751b550a5046a966d3b2 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Sun, 29 Jan 2017 16:54:08 -0600 Subject: lazy-load ace config so that we can ensure window.gon exists --- .../javascripts/lib/ace/ace_config_paths.js.erb | 27 ++++++++++++++-------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/lib/ace/ace_config_paths.js.erb b/app/assets/javascripts/lib/ace/ace_config_paths.js.erb index 5cb1037bc86..976769ba84a 100644 --- a/app/assets/javascripts/lib/ace/ace_config_paths.js.erb +++ b/app/assets/javascripts/lib/ace/ace_config_paths.js.erb @@ -7,21 +7,28 @@ ace_modes = Dir[ace_gem_path + '/vendor/assets/javascripts/ace/mode-*.js'].sort. File.basename(file, '.js').sub(/^mode-/, '') end %> - +// Lazy-load configuration when ace.edit is called (function() { - window.gon = window.gon || {}; - var basePath = (window.gon.relative_url_root || '').replace(/\/$/, '') + '/assets/ace'; - ace.config.set('basePath', basePath); + var basePath; + var ace = window.ace; + var edit = ace.edit; + ace.edit = function() { + window.gon = window.gon || {}; + basePath = (window.gon.relative_url_root || '').replace(/\/$/, '') + '/assets/ace'; + ace.config.set('basePath', basePath); - // configure paths for all worker modules + // configure paths for all worker modules <% ace_workers.each do |worker| %> - <% filename = File.basename(asset_path("ace/worker-#{worker}.js")) %> - ace.config.setModuleUrl('ace/mode/<%= worker %>_worker', basePath + '/<%= filename %>'); + ace.config.setModuleUrl('ace/mode/<%= worker %>_worker', basePath + '/<%= File.basename(asset_path("ace/worker-#{worker}.js")) %>'); <% end %> - // configure paths for all mode modules + // configure paths for all mode modules <% ace_modes.each do |mode| %> - <% filename = File.basename(asset_path("ace/mode-#{mode}.js")) %> - ace.config.setModuleUrl('ace/mode/<%= mode %>', basePath + '/<%= filename %>'); + ace.config.setModuleUrl('ace/mode/<%= mode %>', basePath + '/<%= File.basename(asset_path("ace/mode-#{mode}.js")) %>'); <% end %> + + // restore original method + ace.edit = edit; + return ace.edit.apply(ace, arguments); + }; })(); -- cgit v1.2.1 From 61ef5f0edd746c86d8545ceb9c0bebc9eabdbcf0 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 6 Feb 2017 00:23:50 -0600 Subject: add npm run webpack command --- package.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 73fb487b973..9581d966237 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,14 @@ { "private": true, "scripts": { - "dev-server": "node_modules/.bin/webpack-dev-server --config config/webpack.config.js", + "dev-server": "webpack-dev-server --config config/webpack.config.js", "eslint": "eslint --max-warnings 0 --ext .js,.js.es6 .", "eslint-fix": "npm run eslint -- --fix", "eslint-report": "npm run eslint -- --format html --output-file ./eslint-report.html", "karma": "karma start config/karma.config.js --single-run", - "karma-start": "karma start config/karma.config.js" + "karma-start": "karma start config/karma.config.js", + "webpack": "webpack --config config/webpack.config.js", + "webpack-prod": "NODE_ENV=production npm run webpack" }, "dependencies": { "babel": "^5.8.38", -- cgit v1.2.1 From b5d2edabd88dd875e69ab1b37abac9627c8ff697 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 6 Feb 2017 00:24:23 -0600 Subject: transpile all javascript files with babel --- config/webpack.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/webpack.config.js b/config/webpack.config.js index 7cd92af7d93..a156756f9ff 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -49,8 +49,8 @@ var config = { module: { loaders: [ { - test: /\.es6$/, - exclude: /node_modules/, + test: /\.(js|es6)$/, + exclude: /(node_modules|vendor\/assets)/, loader: 'babel-loader', query: { // 'use strict' was broken in sprockets-es6 due to sprockets concatination method. -- cgit v1.2.1 From 63e79294369d4066f141aa6fc16eaf51de547c07 Mon Sep 17 00:00:00 2001 From: Mike Greiling <mike@pixelcog.com> Date: Mon, 6 Feb 2017 00:31:30 -0600 Subject: add CHANGELOG entry for !8988 --- changelogs/unreleased/babel-all-the-things.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/babel-all-the-things.yml diff --git a/changelogs/unreleased/babel-all-the-things.yml b/changelogs/unreleased/babel-all-the-things.yml new file mode 100644 index 00000000000..fda1c3bd562 --- /dev/null +++ b/changelogs/unreleased/babel-all-the-things.yml @@ -0,0 +1,5 @@ +--- +title: use babel to transpile all non-vendor javascript assets regardless of file + extension +merge_request: 8988 +author: -- cgit v1.2.1 From 7c271fb5495e551c79e0d0342871fd2a2f9076f5 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 6 Feb 2017 10:46:21 +0100 Subject: Fix Rubocop offense in legacy CI/CD config specs --- spec/lib/ci/gitlab_ci_yaml_processor_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index 09d1dd806e6..008c15c4de3 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -11,7 +11,7 @@ module Ci describe 'code coverage regexp' do let(:config) do YAML.dump(rspec: { script: 'rspec', - coverage: '/Code coverage: \d+\.\d+/'}) + coverage: '/Code coverage: \d+\.\d+/' }) end it 'includes coverage regexp in build attributes' do -- cgit v1.2.1 From 19593b0b8766b9d61c589f21ba069dd73d1a30d0 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 6 Feb 2017 10:55:56 +0100 Subject: Update docs on setting up a CI/CD coverage regexp --- doc/ci/yaml/README.md | 44 ++++++++++++-------------------------------- 1 file changed, 12 insertions(+), 32 deletions(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 06810898cfe..ec31d91bce9 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -76,7 +76,6 @@ There are a few reserved `keywords` that **cannot** be used as job names: | after_script | no | Define commands that run after each job's script | | variables | no | Define build variables | | cache | no | Define list of files that should be cached between subsequent runs | -| coverage | no | Define coverage settings for all jobs | ### image and services @@ -279,23 +278,6 @@ cache: untracked: true ``` -### coverage - -`coverage` allows you to configure how coverage will be filtered out from the -build outputs. Setting this up globally will make all the jobs to use this -setting for output filtering and extracting the coverage information from your -builds. - -Regular expressions are the only valid kind of value expected here. So, using -surrounding `/` is mandatory in order to consistently and explicitly represent -a regular expression string. You must escape special characters if you want to -match them literally. - -A simple example: -```yaml -coverage: /\(\d+\.\d+\) covered\./ -``` - ## Jobs `.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job @@ -337,7 +319,7 @@ job_name: | before_script | no | Override a set of commands that are executed before build | | after_script | no | Override a set of commands that are executed after build | | environment | no | Defines a name of environment to which deployment is done by this build | -| coverage | no | Define coverage settings for a given job | +| coverage | no | Define code coverage settings for a given job | ### script @@ -1012,25 +994,23 @@ job: - execute this after my script ``` -### job coverage +### coverage -This entry is pretty much the same as described in the global context in -[`coverage`](#coverage). The only difference is that, by setting it inside -the job level, whatever is set in there will take precedence over what has -been defined in the global level. A quick example of one overriding the -other would be: +`coverage` allows you to configure how coverage will be filtered out from the +build outputs. Setting this in the job context will define how the output +filtering and extracting the coverage information from your builds will work. + +Regular expressions are the only valid kind of value expected here. So, using +surrounding `/` is mandatory in order to consistently and explicitly represent +a regular expression string. You must escape special characters if you want to +match them literally. + +A simple example: ```yaml coverage: /\(\d+\.\d+\) covered\./ - -job1: - coverage: /Code coverage: \d+\.\d+/ ``` -In the example above, considering the context of the job `job1`, the coverage -regex that would be used is `/Code coverage: \d+\.\d+/` instead of -`/\(\d+\.\d+\) covered\./`. - ## Git Strategy > Introduced in GitLab 8.9 as an experimental feature. May change or be removed -- cgit v1.2.1 From 213d55d79ac8a0587c2c01f6dac9480222dd8132 Mon Sep 17 00:00:00 2001 From: Marin Jankovski <maxlazio@gmail.com> Date: Mon, 6 Feb 2017 11:04:11 +0100 Subject: Map configuration to directory locations and add defaults to NFS HA doc. --- doc/administration/high_availability/nfs.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md index 5602d70f1ef..3893d837006 100644 --- a/doc/administration/high_availability/nfs.md +++ b/doc/administration/high_availability/nfs.md @@ -47,13 +47,13 @@ When using default Omnibus configuration you will need to share 5 data locations between all GitLab cluster nodes. No other locations should be shared. The following are the 5 locations you need to mount: -| Location | Description | -| -------- | ----------- | -| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data -| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services -| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments -| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data -| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces +| Location | Description | Default configuration | +| -------- | ----------- | --------------------- | +| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})` +| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'` +| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'` +| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'` +| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'` Other GitLab directories should not be shared between nodes. They contain node-specific files and GitLab code that does not need to be shared. To ship @@ -73,10 +73,10 @@ as subdirectories. Mount `/gitlab-data` then use the following Omnibus configuration to move each data location to a subdirectory: ```ruby +git_data_dirs({"default" => "/gitlab-data/git-data"}) user['home'] = '/gitlab-data/home' -git_data_dir '/gitlab-data/git-data' -gitlab_rails['shared_path'] = '/gitlab-data/shared' gitlab_rails['uploads_directory'] = '/gitlab-data/uploads' +gitlab_rails['shared_path'] = '/gitlab-data/shared' gitlab_ci['builds_directory'] = '/gitlab-data/builds' ``` -- cgit v1.2.1 From 5fe9ec2496c81696716fb3387ad856073e03c397 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 6 Feb 2017 11:42:09 +0100 Subject: Add Changelog entry for CI config compatibility fix --- .../unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml diff --git a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml new file mode 100644 index 00000000000..7972e9802a4 --- /dev/null +++ b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml @@ -0,0 +1,4 @@ +--- +title: Remove global CI/CD setting to preserve backward compatibility +merge_request: 8981 +author: -- cgit v1.2.1 From 5d3816652e13cde6bf5e9de814d2c9d1e6593601 Mon Sep 17 00:00:00 2001 From: Adam Niedzielski <adamsunday@gmail.com> Date: Thu, 26 Jan 2017 19:16:50 +0100 Subject: Introduce maximum session time for terminal websocket connection Store the value in application settings. Expose the value to Workhorse. --- .../admin/application_settings_controller.rb | 1 + app/models/application_setting.rb | 7 ++++- app/models/project_services/kubernetes_service.rb | 11 +++++++- .../admin/application_settings/_form.html.haml | 10 +++++++ .../unreleased/terminal-max-session-time.yml | 4 +++ ...nal_max_session_time_to_application_settings.rb | 33 ++++++++++++++++++++++ db/schema.rb | 1 + doc/administration/integration/terminal.md | 10 +++++++ doc/api/settings.md | 7 +++-- lib/api/entities.rb | 1 + lib/api/settings.rb | 3 +- lib/gitlab/kubernetes.rb | 4 +-- lib/gitlab/workhorse.rb | 3 +- spec/lib/gitlab/workhorse_spec.rb | 6 ++-- .../project_services/kubernetes_service_spec.rb | 16 +++++++++-- spec/support/kubernetes_helpers.rb | 3 +- 16 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 changelogs/unreleased/terminal-max-session-time.yml create mode 100644 db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 543d5eac504..3f10ae81767 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -137,6 +137,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :user_default_external, :user_oauth_applications, :version_check_enabled, + :terminal_max_session_time, disabled_oauth_sign_in_sources: [], import_sources: [], diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 2df8b071e13..9a4557524c4 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -111,6 +111,10 @@ class ApplicationSetting < ActiveRecord::Base presence: true, numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period } + validates :terminal_max_session_time, + presence: true, + numericality: { only_integer: true, greater_than_or_equal_to: 0 } + validates_each :restricted_visibility_levels do |record, attr, value| unless value.nil? value.each do |level| @@ -204,7 +208,8 @@ class ApplicationSetting < ActiveRecord::Base signin_enabled: Settings.gitlab['signin_enabled'], signup_enabled: Settings.gitlab['signup_enabled'], two_factor_grace_period: 48, - user_default_external: false + user_default_external: false, + terminal_max_session_time: 0 } end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index fa3cedc4354..f2f019c43bb 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -1,4 +1,5 @@ class KubernetesService < DeploymentService + include Gitlab::CurrentSettings include Gitlab::Kubernetes include ReactiveCaching @@ -110,7 +111,7 @@ class KubernetesService < DeploymentService pods = data.fetch(:pods, nil) filter_pods(pods, app: environment.slug). flat_map { |pod| terminals_for_pod(api_url, namespace, pod) }. - map { |terminal| add_terminal_auth(terminal, token, ca_pem) } + each { |terminal| add_terminal_auth(terminal, terminal_auth) } end end @@ -170,4 +171,12 @@ class KubernetesService < DeploymentService url.to_s end + + def terminal_auth + { + token: token, + ca_pem: ca_pem, + max_session_time: current_application_settings.terminal_max_session_time + } + end end diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index e7701d75a6e..2c64de1d530 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -509,5 +509,15 @@ .help-block Number of Git pushes after which 'git gc' is run. + %fieldset + %legend Web terminal + .form-group + = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :terminal_max_session_time, class: 'form-control' + .help-block + Maximum time for web terminal websocket connection (in seconds). + Set to 0 for unlimited time. + .form-actions = f.submit 'Save', class: 'btn btn-save' diff --git a/changelogs/unreleased/terminal-max-session-time.yml b/changelogs/unreleased/terminal-max-session-time.yml new file mode 100644 index 00000000000..db1e66770d1 --- /dev/null +++ b/changelogs/unreleased/terminal-max-session-time.yml @@ -0,0 +1,4 @@ +--- +title: Introduce maximum session time for terminal websocket connection +merge_request: 8413 +author: diff --git a/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb b/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb new file mode 100644 index 00000000000..334f53f9145 --- /dev/null +++ b/db/migrate/20170126174819_add_terminal_max_session_time_to_application_settings.rb @@ -0,0 +1,33 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddTerminalMaxSessionTimeToApplicationSettings < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + # DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + disable_ddl_transaction! + + def up + add_column_with_default :application_settings, :terminal_max_session_time, :integer, default: 0, allow_null: false + end + + def down + remove_column :application_settings, :terminal_max_session_time + end +end diff --git a/db/schema.rb b/db/schema.rb index 92b36218a15..a9f4e865d60 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -109,6 +109,7 @@ ActiveRecord::Schema.define(version: 20170204181513) do t.boolean "html_emails_enabled", default: true t.string "plantuml_url" t.boolean "plantuml_enabled" + t.integer "terminal_max_session_time", default: 0, null: false end create_table "audit_events", force: :cascade do |t| diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 3fbb13704aa..11444464537 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -71,5 +71,15 @@ When these headers are not passed through, Workhorse will return a `400 Bad Request` response to users attempting to use a web terminal. In turn, they will receive a `Connection failed` message. +## Limiting WebSocket connection time + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8413) +in GitLab 8.17. + +Terminal sessions use long-lived connections; by default, these may last +forever. You can configure a maximum session time in the Admin area of your +GitLab instance if you find this undesirable from a scalability or security +point of view. + [ce-7690]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690 [kubservice]: ../../user/project/integrations/kubernetes.md) diff --git a/doc/api/settings.md b/doc/api/settings.md index f86c7cc2f94..ca6b9347877 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -46,7 +46,8 @@ Example response: "koding_enabled": false, "koding_url": null, "plantuml_enabled": false, - "plantuml_url": null + "plantuml_url": null, + "terminal_max_session_time": 0 } ``` @@ -84,6 +85,7 @@ PUT /application/settings | `disabled_oauth_sign_in_sources` | Array of strings | no | Disabled OAuth sign-in sources | | `plantuml_enabled` | boolean | no | Enable PlantUML integration. Default is `false`. | | `plantuml_url` | string | yes (if `plantuml_enabled` is `true`) | The PlantUML instance URL for integration. | +| `terminal_max_session_time` | integer | no | Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time. | ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1 @@ -118,6 +120,7 @@ Example response: "koding_enabled": false, "koding_url": null, "plantuml_enabled": false, - "plantuml_url": null + "plantuml_url": null, + "terminal_max_session_time": 0 } ``` diff --git a/lib/api/entities.rb b/lib/api/entities.rb index a07b2a9ca0f..b1ead48caf7 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -575,6 +575,7 @@ module API expose :koding_url expose :plantuml_enabled expose :plantuml_url + expose :terminal_max_session_time end class Release < Grape::Entity diff --git a/lib/api/settings.rb b/lib/api/settings.rb index c5eff16a5de..a1d1c1432d3 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -107,6 +107,7 @@ module API requires :housekeeping_full_repack_period, type: Integer, desc: "Number of Git pushes after which a full 'git repack' is run." requires :housekeeping_gc_period, type: Integer, desc: "Number of Git pushes after which 'git gc' is run." end + optional :terminal_max_session_time, type: Integer, desc: 'Maximum time for web terminal websocket connection (in seconds). Set to 0 for unlimited time.' at_least_one_of :default_branch_protection, :default_project_visibility, :default_snippet_visibility, :default_group_visibility, :restricted_visibility_levels, :import_sources, :enabled_git_access_protocol, :gravatar_enabled, :default_projects_limit, @@ -120,7 +121,7 @@ module API :akismet_enabled, :admin_notification_email, :sentry_enabled, :repository_storage, :repository_checks_enabled, :koding_enabled, :plantuml_enabled, :version_check_enabled, :email_author_in_body, :html_emails_enabled, - :housekeeping_enabled + :housekeeping_enabled, :terminal_max_session_time end put "application/settings" do if current_settings.update_attributes(declared_params(include_missing: false)) diff --git a/lib/gitlab/kubernetes.rb b/lib/gitlab/kubernetes.rb index 288771c1c12..3a7af363548 100644 --- a/lib/gitlab/kubernetes.rb +++ b/lib/gitlab/kubernetes.rb @@ -43,10 +43,10 @@ module Gitlab end end - def add_terminal_auth(terminal, token, ca_pem = nil) + def add_terminal_auth(terminal, token:, max_session_time:, ca_pem: nil) terminal[:headers]['Authorization'] << "Bearer #{token}" + terminal[:max_session_time] = max_session_time terminal[:ca_pem] = ca_pem if ca_pem.present? - terminal end def container_exec_url(api_url, namespace, pod_name, container_name) diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb index a3b502ffd6a..c8872df8a93 100644 --- a/lib/gitlab/workhorse.rb +++ b/lib/gitlab/workhorse.rb @@ -107,7 +107,8 @@ module Gitlab 'Terminal' => { 'Subprotocols' => terminal[:subprotocols], 'Url' => terminal[:url], - 'Header' => terminal[:headers] + 'Header' => terminal[:headers], + 'MaxSessionTime' => terminal[:max_session_time], } } details['Terminal']['CAPem'] = terminal[:ca_pem] if terminal.has_key?(:ca_pem) diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 7dd4d76d1a3..a32c6131030 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -42,7 +42,8 @@ describe Gitlab::Workhorse, lib: true do out = { subprotocols: ['foo'], url: 'wss://example.com/terminal.ws', - headers: { 'Authorization' => ['Token x'] } + headers: { 'Authorization' => ['Token x'] }, + max_session_time: 600 } out[:ca_pem] = ca_pem if ca_pem out @@ -53,7 +54,8 @@ describe Gitlab::Workhorse, lib: true do 'Terminal' => { 'Subprotocols' => ['foo'], 'Url' => 'wss://example.com/terminal.ws', - 'Header' => { 'Authorization' => ['Token x'] } + 'Header' => { 'Authorization' => ['Token x'] }, + 'MaxSessionTime' => 600 } } out['Terminal']['CAPem'] = ca_pem if ca_pem diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index 4f3cd14e941..9052479d35e 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -181,11 +181,23 @@ describe KubernetesService, models: true, caching: true do let(:pod) { kube_pod(app: environment.slug) } let(:terminals) { kube_terminals(service, pod) } - it 'returns terminals' do - stub_reactive_cache(service, pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ]) + before do + stub_reactive_cache( + service, + pods: [ pod, pod, kube_pod(app: "should-be-filtered-out") ] + ) + end + it 'returns terminals' do is_expected.to eq(terminals + terminals) end + + it 'uses max session time from settings' do + stub_application_setting(terminal_max_session_time: 600) + + times = subject.map { |terminal| terminal[:max_session_time] } + expect(times).to eq [600, 600, 600, 600] + end end end diff --git a/spec/support/kubernetes_helpers.rb b/spec/support/kubernetes_helpers.rb index 6c4c246a68b..444612cf871 100644 --- a/spec/support/kubernetes_helpers.rb +++ b/spec/support/kubernetes_helpers.rb @@ -43,7 +43,8 @@ module KubernetesHelpers url: container_exec_url(service.api_url, service.namespace, pod_name, container['name']), subprotocols: ['channel.k8s.io'], headers: { 'Authorization' => ["Bearer #{service.token}"] }, - created_at: DateTime.parse(pod['metadata']['creationTimestamp']) + created_at: DateTime.parse(pod['metadata']['creationTimestamp']), + max_session_time: 0 } terminal[:ca_pem] = service.ca_pem if service.ca_pem.present? terminal -- cgit v1.2.1 From 8417f336727f5a63cd7c51f4832b4239e5e3da1e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 6 Feb 2017 11:50:22 +0100 Subject: Refine docs on code coverage regexp on job level --- doc/ci/yaml/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index ec31d91bce9..5643bf0e2d9 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1008,7 +1008,8 @@ match them literally. A simple example: ```yaml -coverage: /\(\d+\.\d+\) covered\./ +job1: + coverage: /Code coverage: \d+\.\d+/ ``` ## Git Strategy -- cgit v1.2.1 From c07b1d3bbedb590b2f588857b2383f9e8f51a250 Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 6 Feb 2017 11:56:52 +0100 Subject: Improve Changelog entry for CI config compatibility fix --- .../unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml index 7972e9802a4..df7e3776700 100644 --- a/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml +++ b/changelogs/unreleased/fix-gb-backwards-compatibility-coverage-ci-yml.yml @@ -1,4 +1,4 @@ --- -title: Remove global CI/CD setting to preserve backward compatibility +title: Preserve backward compatibility CI/CD and disallow setting `coverage` regexp in global context merge_request: 8981 author: -- cgit v1.2.1 From a5cf7c2ef9d906b91372d93dbb6077db718dc89e Mon Sep 17 00:00:00 2001 From: Grzegorz Bizon <grzesiek.bizon@gmail.com> Date: Mon, 6 Feb 2017 12:01:54 +0100 Subject: Improve docs on CI/CD code coverage regexp setting --- doc/ci/yaml/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 5643bf0e2d9..865158187b6 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -996,9 +996,8 @@ job: ### coverage -`coverage` allows you to configure how coverage will be filtered out from the -build outputs. Setting this in the job context will define how the output -filtering and extracting the coverage information from your builds will work. +`coverage` allows you to configure how code coverage will be extracted from the +job output. Regular expressions are the only valid kind of value expected here. So, using surrounding `/` is mandatory in order to consistently and explicitly represent -- cgit v1.2.1 From 049d603e1b581ddacc9522814c8827e2453c50a7 Mon Sep 17 00:00:00 2001 From: dimitrieh <dimitriehoekstra@gmail.com> Date: Mon, 6 Feb 2017 13:00:31 +0100 Subject: Removed additional dropdown list animations --- app/assets/stylesheets/framework/animations.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss index a5f15f4836d..be90c82dda7 100644 --- a/app/assets/stylesheets/framework/animations.scss +++ b/app/assets/stylesheets/framework/animations.scss @@ -120,6 +120,9 @@ a { @include transition(background-color, box-shadow); } -.nav-sidebar a { +.nav-sidebar a, +.dropdown-menu a, +.dropdown-menu button, +.dropdown-menu-nav a { transition: none; } -- cgit v1.2.1 From c2d64d67027ca4170deb24c8b5868d127bba157c Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira <oswaldo@gitlab.com> Date: Wed, 25 Jan 2017 19:35:27 -0200 Subject: Remove deprecated MR and Issue endpoints and preserve V3 namespace --- changelogs/unreleased/9-0-api-changes.yml | 4 + doc/api/issues.md | 1 - doc/api/merge_requests.md | 3 +- doc/api/v3_to_v4.md | 3 + lib/api/api.rb | 2 + lib/api/issues.rb | 3 - lib/api/merge_requests.rb | 274 +++--- lib/api/v3/issues.rb | 231 +++++ lib/api/v3/merge_requests.rb | 280 ++++++ spec/requests/api/issues_spec.rb | 17 - spec/requests/api/merge_requests_spec.rb | 28 +- spec/requests/api/v3/issues_spec.rb | 1259 +++++++++++++++++++++++++++ spec/requests/api/v3/merge_requests_spec.rb | 726 +++++++++++++++ 13 files changed, 2647 insertions(+), 184 deletions(-) create mode 100644 changelogs/unreleased/9-0-api-changes.yml create mode 100644 lib/api/v3/issues.rb create mode 100644 lib/api/v3/merge_requests.rb create mode 100644 spec/requests/api/v3/issues_spec.rb create mode 100644 spec/requests/api/v3/merge_requests_spec.rb diff --git a/changelogs/unreleased/9-0-api-changes.yml b/changelogs/unreleased/9-0-api-changes.yml new file mode 100644 index 00000000000..2f0f1887257 --- /dev/null +++ b/changelogs/unreleased/9-0-api-changes.yml @@ -0,0 +1,4 @@ +--- +title: Remove deprecated MR and Issue endpoints and preserve V3 namespace +merge_request: 8967 +author: diff --git a/doc/api/issues.md b/doc/api/issues.md index b276d1ad918..7c0a444d4fa 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -181,7 +181,6 @@ GET /projects/:id/issues?labels=foo,bar GET /projects/:id/issues?labels=foo,bar&state=opened GET /projects/:id/issues?milestone=1.0.0 GET /projects/:id/issues?milestone=1.0.0&state=opened -GET /projects/:id/issues?iid=42 ``` | Attribute | Type | Required | Description | diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index 7b005591545..1cf7632d60c 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -10,8 +10,7 @@ The pagination parameters `page` and `per_page` can be used to restrict the list GET /projects/:id/merge_requests GET /projects/:id/merge_requests?state=opened GET /projects/:id/merge_requests?state=all -GET /projects/:id/merge_requests?iid=42 -GET /projects/:id/merge_requests?iid[]=42&iid[]=43 +GET /projects/:id/merge_requests?iids[]=42&iids[]=43 ``` Parameters: diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md index 01de1e59fcb..9748aec17ad 100644 --- a/doc/api/v3_to_v4.md +++ b/doc/api/v3_to_v4.md @@ -7,4 +7,7 @@ changes are in V4: ### Changes - Removed `/projects/:search` (use: `/projects?search=x`) +- `iid` filter has been removed from `projects/:id/issues` +- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids` +- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`) diff --git a/lib/api/api.rb b/lib/api/api.rb index 090109d5e6f..1950d2791ab 100644 --- a/lib/api/api.rb +++ b/lib/api/api.rb @@ -5,6 +5,8 @@ module API version %w(v3 v4), using: :path version 'v3', using: :path do + mount ::API::V3::Issues + mount ::API::V3::MergeRequests mount ::API::V3::Projects end diff --git a/lib/api/issues.rb b/lib/api/issues.rb index fe016c1ec0a..90fca20d4fa 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -15,8 +15,6 @@ module API labels = args.delete(:labels) args[:label_name] = labels if match_all_labels - args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid) - issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations # TODO: Remove in 9.0 pass `label_name: args.delete(:labels)` to IssuesFinder @@ -97,7 +95,6 @@ module API params do optional :state, type: String, values: %w[opened closed all], default: 'all', desc: 'Return opened, closed, or all issues' - optional :iid, type: Integer, desc: 'Return the issue having the given `iid`' use :issues_params end get ":id/issues" do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 7ffb38e62da..782147883c8 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -2,8 +2,6 @@ module API class MergeRequests < Grape::API include PaginationParams - DEPRECATION_MESSAGE = 'This endpoint is deprecated and will be removed in GitLab 9.0.'.freeze - before { authenticate! } params do @@ -46,14 +44,14 @@ module API desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.' optional :sort, type: String, values: %w[asc desc], default: 'desc', desc: 'Return merge requests sorted in `asc` or `desc` order.' - optional :iid, type: Array[Integer], desc: 'The IID of the merge requests' + optional :iids, type: Array[Integer], desc: 'The IID array of merge requests' use :pagination end get ":id/merge_requests" do authorize! :read_merge_request, user_project merge_requests = user_project.merge_requests.inc_notes_with_associations - merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present? + merge_requests = filter_by_iid(merge_requests, params[:iids]) if params[:iids].present? merge_requests = case params[:state] @@ -104,177 +102,167 @@ module API merge_request.destroy end - # Routing "merge_request/:merge_request_id/..." is DEPRECATED and WILL BE REMOVED in version 9.0 - # Use "merge_requests/:merge_request_id/..." instead. - # params do requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' end - { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status| - desc 'Get a single merge request' do - if status == :deprecated - detail DEPRECATION_MESSAGE - end - success Entities::MergeRequest - end - get path do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + desc 'Get a single merge request' do + success Entities::MergeRequest + end + get ':id/merge_requests/:merge_request_id' do + merge_request = find_merge_request_with_access(params[:merge_request_id]) - present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project - end + present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project + end - desc 'Get the commits of a merge request' do - success Entities::RepoCommit - end - get "#{path}/commits" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + desc 'Get the commits of a merge request' do + success Entities::RepoCommit + end + get ':id/merge_requests/:merge_request_id/commits' do + merge_request = find_merge_request_with_access(params[:merge_request_id]) - present merge_request.commits, with: Entities::RepoCommit - end + present merge_request.commits, with: Entities::RepoCommit + end - desc 'Show the merge request changes' do - success Entities::MergeRequestChanges - end - get "#{path}/changes" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) + desc 'Show the merge request changes' do + success Entities::MergeRequestChanges + end + get ':id/merge_requests/:merge_request_id/changes' do + merge_request = find_merge_request_with_access(params[:merge_request_id]) - present merge_request, with: Entities::MergeRequestChanges, current_user: current_user - end + present merge_request, with: Entities::MergeRequestChanges, current_user: current_user + end - desc 'Update a merge request' do - success Entities::MergeRequest - end - params do - optional :title, type: String, allow_blank: false, desc: 'The title of the merge request' - optional :target_branch, type: String, allow_blank: false, desc: 'The target branch' - optional :state_event, type: String, values: %w[close reopen merge], - desc: 'Status of the merge request' - use :optional_params - at_least_one_of :title, :target_branch, :description, :assignee_id, - :milestone_id, :labels, :state_event, - :remove_source_branch - end - put path do - merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request) + desc 'Update a merge request' do + success Entities::MergeRequest + end + params do + optional :title, type: String, allow_blank: false, desc: 'The title of the merge request' + optional :target_branch, type: String, allow_blank: false, desc: 'The target branch' + optional :state_event, type: String, values: %w[close reopen merge], + desc: 'Status of the merge request' + use :optional_params + at_least_one_of :title, :target_branch, :description, :assignee_id, + :milestone_id, :labels, :state_event, + :remove_source_branch + end + put ':id/merge_requests/:merge_request_id' do + merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request) - mr_params = declared_params(include_missing: false) - mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? + mr_params = declared_params(include_missing: false) + mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? - merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) + merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) - if merge_request.valid? - present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project - else - handle_merge_request_errors! merge_request.errors - end + if merge_request.valid? + present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project + else + handle_merge_request_errors! merge_request.errors end + end - desc 'Merge a merge request' do - success Entities::MergeRequest - end - params do - optional :merge_commit_message, type: String, desc: 'Custom merge commit message' - optional :should_remove_source_branch, type: Boolean, - desc: 'When true, the source branch will be deleted if possible' - optional :merge_when_build_succeeds, type: Boolean, - desc: 'When true, this merge request will be merged when the pipeline succeeds' - optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' - end - put "#{path}/merge" do - merge_request = find_project_merge_request(params[:merge_request_id]) + desc 'Merge a merge request' do + success Entities::MergeRequest + end + params do + optional :merge_commit_message, type: String, desc: 'Custom merge commit message' + optional :should_remove_source_branch, type: Boolean, + desc: 'When true, the source branch will be deleted if possible' + optional :merge_when_build_succeeds, type: Boolean, + desc: 'When true, this merge request will be merged when the pipeline succeeds' + optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' + end + put ':id/merge_requests/:merge_request_id/merge' do + merge_request = find_project_merge_request(params[:merge_request_id]) - # Merge request can not be merged - # because user dont have permissions to push into target branch - unauthorized! unless merge_request.can_be_merged_by?(current_user) + # Merge request can not be merged + # because user dont have permissions to push into target branch + unauthorized! unless merge_request.can_be_merged_by?(current_user) - not_allowed! unless merge_request.mergeable_state? + not_allowed! unless merge_request.mergeable_state? - render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable? + render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable? - if params[:sha] && merge_request.diff_head_sha != params[:sha] - render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409) - end + if params[:sha] && merge_request.diff_head_sha != params[:sha] + render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409) + end - merge_params = { - commit_message: params[:merge_commit_message], - should_remove_source_branch: params[:should_remove_source_branch] - } - - if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active? - ::MergeRequests::MergeWhenPipelineSucceedsService - .new(merge_request.target_project, current_user, merge_params) - .execute(merge_request) - else - ::MergeRequests::MergeService - .new(merge_request.target_project, current_user, merge_params) - .execute(merge_request) - end + merge_params = { + commit_message: params[:merge_commit_message], + should_remove_source_branch: params[:should_remove_source_branch] + } - present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project + if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active? + ::MergeRequests::MergeWhenPipelineSucceedsService + .new(merge_request.target_project, current_user, merge_params) + .execute(merge_request) + else + ::MergeRequests::MergeService + .new(merge_request.target_project, current_user, merge_params) + .execute(merge_request) end - desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do - success Entities::MergeRequest - end - post "#{path}/cancel_merge_when_build_succeeds" do - merge_request = find_project_merge_request(params[:merge_request_id]) + present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project + end - unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) + desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do + success Entities::MergeRequest + end + post ':id/merge_requests/:merge_request_id/cancel_merge_when_build_succeeds' do + merge_request = find_project_merge_request(params[:merge_request_id]) - ::MergeRequest::MergeWhenPipelineSucceedsService - .new(merge_request.target_project, current_user) - .cancel(merge_request) - end + unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) - desc 'Get the comments of a merge request' do - detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0' - success Entities::MRNote - end - params do - use :pagination - end - get "#{path}/comments" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - present paginate(merge_request.notes.fresh), with: Entities::MRNote - end + ::MergeRequest::MergeWhenPipelineSucceedsService + .new(merge_request.target_project, current_user) + .cancel(merge_request) + end - desc 'Post a comment to a merge request' do - detail 'Duplicate. DEPRECATED and WILL BE REMOVED in 9.0' - success Entities::MRNote - end - params do - requires :note, type: String, desc: 'The text of the comment' - end - post "#{path}/comments" do - merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note) + desc 'Get the comments of a merge request' do + success Entities::MRNote + end + params do + use :pagination + end + get ':id/merge_requests/:merge_request_id/comments' do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + present paginate(merge_request.notes.fresh), with: Entities::MRNote + end - opts = { - note: params[:note], - noteable_type: 'MergeRequest', - noteable_id: merge_request.id - } + desc 'Post a comment to a merge request' do + success Entities::MRNote + end + params do + requires :note, type: String, desc: 'The text of the comment' + end + post ':id/merge_requests/:merge_request_id/comments' do + merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note) - note = ::Notes::CreateService.new(user_project, current_user, opts).execute + opts = { + note: params[:note], + noteable_type: 'MergeRequest', + noteable_id: merge_request.id + } - if note.save - present note, with: Entities::MRNote - else - render_api_error!("Failed to save note #{note.errors.messages}", 400) - end - end + note = ::Notes::CreateService.new(user_project, current_user, opts).execute - desc 'List issues that will be closed on merge' do - success Entities::MRNote - end - params do - use :pagination - end - get "#{path}/closes_issues" do - merge_request = find_merge_request_with_access(params[:merge_request_id]) - issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) - present paginate(issues), with: issue_entity(user_project), current_user: current_user + if note.save + present note, with: Entities::MRNote + else + render_api_error!("Failed to save note #{note.errors.messages}", 400) end end + + desc 'List issues that will be closed on merge' do + success Entities::MRNote + end + params do + use :pagination + end + get ':id/merge_requests/:merge_request_id/closes_issues' do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) + present paginate(issues), with: issue_entity(user_project), current_user: current_user + end end end end diff --git a/lib/api/v3/issues.rb b/lib/api/v3/issues.rb new file mode 100644 index 00000000000..be3ecc29449 --- /dev/null +++ b/lib/api/v3/issues.rb @@ -0,0 +1,231 @@ +module API + module V3 + class Issues < Grape::API + include PaginationParams + + before { authenticate! } + + helpers do + def find_issues(args = {}) + args = params.merge(args) + + args.delete(:id) + args[:milestone_title] = args.delete(:milestone) + + match_all_labels = args.delete(:match_all_labels) + labels = args.delete(:labels) + args[:label_name] = labels if match_all_labels + + args[:search] = "#{Issue.reference_prefix}#{args.delete(:iid)}" if args.key?(:iid) + + issues = IssuesFinder.new(current_user, args).execute.inc_notes_with_associations + + if !match_all_labels && labels.present? + issues = issues.includes(:labels).where('labels.title' => labels.split(',')) + end + + issues.reorder(args[:order_by] => args[:sort]) + end + + params :issues_params do + optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :milestone, type: String, desc: 'Milestone title' + optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', + desc: 'Return issues ordered by `created_at` or `updated_at` fields.' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return issues sorted in `asc` or `desc` order.' + optional :milestone, type: String, desc: 'Return issues for a specific milestone' + use :pagination + end + + params :issue_params do + optional :description, type: String, desc: 'The description of an issue' + optional :assignee_id, type: Integer, desc: 'The ID of a user to assign issue' + optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign issue' + optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :due_date, type: String, desc: 'Date time string in the format YEAR-MONTH-DAY' + optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' + end + end + + resource :issues do + desc "Get currently authenticated user's issues" do + success Entities::Issue + end + params do + optional :state, type: String, values: %w[opened closed all], default: 'all', + desc: 'Return opened, closed, or all issues' + use :issues_params + end + get do + issues = find_issues(scope: 'authored') + + present paginate(issues), with: Entities::Issue, current_user: current_user + end + end + + params do + requires :id, type: String, desc: 'The ID of a group' + end + resource :groups do + desc 'Get a list of group issues' do + success Entities::Issue + end + params do + optional :state, type: String, values: %w[opened closed all], default: 'opened', + desc: 'Return opened, closed, or all issues' + use :issues_params + end + get ":id/issues" do + group = find_group!(params[:id]) + + issues = find_issues(group_id: group.id, state: params[:state] || 'opened', match_all_labels: true) + + present paginate(issues), with: Entities::Issue, current_user: current_user + end + end + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + include TimeTrackingEndpoints + + desc 'Get a list of project issues' do + detail 'iid filter is deprecated have been removed on V4' + success Entities::Issue + end + params do + optional :state, type: String, values: %w[opened closed all], default: 'all', + desc: 'Return opened, closed, or all issues' + optional :iid, type: Integer, desc: 'Return the issue having the given `iid`' + use :issues_params + end + get ":id/issues" do + project = find_project(params[:id]) + + issues = find_issues(project_id: project.id) + + present paginate(issues), with: Entities::Issue, current_user: current_user, project: user_project + end + + desc 'Get a single project issue' do + success Entities::Issue + end + params do + requires :issue_id, type: Integer, desc: 'The ID of a project issue' + end + get ":id/issues/:issue_id" do + issue = find_project_issue(params[:issue_id]) + present issue, with: Entities::Issue, current_user: current_user, project: user_project + end + + desc 'Create a new project issue' do + success Entities::Issue + end + params do + requires :title, type: String, desc: 'The title of an issue' + optional :created_at, type: DateTime, + desc: 'Date time when the issue was created. Available only for admins and project owners.' + optional :merge_request_for_resolving_discussions, type: Integer, + desc: 'The IID of a merge request for which to resolve discussions' + use :issue_params + end + post ':id/issues' do + # Setting created_at time only allowed for admins and project owners + unless current_user.admin? || user_project.owner == current_user + params.delete(:created_at) + end + + issue_params = declared_params(include_missing: false) + + if merge_request_iid = params[:merge_request_for_resolving_discussions] + issue_params[:merge_request_for_resolving_discussions] = MergeRequestsFinder.new(current_user, project_id: user_project.id). + execute. + find_by(iid: merge_request_iid) + end + + issue = ::Issues::CreateService.new(user_project, + current_user, + issue_params.merge(request: request, api: true)).execute + if issue.spam? + render_api_error!({ error: 'Spam detected' }, 400) + end + + if issue.valid? + present issue, with: Entities::Issue, current_user: current_user, project: user_project + else + render_validation_error!(issue) + end + end + + desc 'Update an existing issue' do + success Entities::Issue + end + params do + requires :issue_id, type: Integer, desc: 'The ID of a project issue' + optional :title, type: String, desc: 'The title of an issue' + optional :updated_at, type: DateTime, + desc: 'Date time when the issue was updated. Available only for admins and project owners.' + optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue' + use :issue_params + at_least_one_of :title, :description, :assignee_id, :milestone_id, + :labels, :created_at, :due_date, :confidential, :state_event + end + put ':id/issues/:issue_id' do + issue = user_project.issues.find(params.delete(:issue_id)) + authorize! :update_issue, issue + + # Setting created_at time only allowed for admins and project owners + unless current_user.admin? || user_project.owner == current_user + params.delete(:updated_at) + end + + issue = ::Issues::UpdateService.new(user_project, + current_user, + declared_params(include_missing: false)).execute(issue) + + if issue.valid? + present issue, with: Entities::Issue, current_user: current_user, project: user_project + else + render_validation_error!(issue) + end + end + + desc 'Move an existing issue' do + success Entities::Issue + end + params do + requires :issue_id, type: Integer, desc: 'The ID of a project issue' + requires :to_project_id, type: Integer, desc: 'The ID of the new project' + end + post ':id/issues/:issue_id/move' do + issue = user_project.issues.find_by(id: params[:issue_id]) + not_found!('Issue') unless issue + + new_project = Project.find_by(id: params[:to_project_id]) + not_found!('Project') unless new_project + + begin + issue = ::Issues::MoveService.new(user_project, current_user).execute(issue, new_project) + present issue, with: Entities::Issue, current_user: current_user, project: user_project + rescue ::Issues::MoveService::MoveError => error + render_api_error!(error.message, 400) + end + end + + desc 'Delete a project issue' + params do + requires :issue_id, type: Integer, desc: 'The ID of a project issue' + end + delete ":id/issues/:issue_id" do + issue = user_project.issues.find_by(id: params[:issue_id]) + not_found!('Issue') unless issue + + authorize!(:destroy_issue, issue) + issue.destroy + end + end + end + end +end diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb new file mode 100644 index 00000000000..1af70cf58cc --- /dev/null +++ b/lib/api/v3/merge_requests.rb @@ -0,0 +1,280 @@ +module API + module V3 + class MergeRequests < Grape::API + include PaginationParams + + DEPRECATION_MESSAGE = 'This endpoint is deprecated and has been removed on V4'.freeze + + before { authenticate! } + + params do + requires :id, type: String, desc: 'The ID of a project' + end + resource :projects do + include TimeTrackingEndpoints + + helpers do + def handle_merge_request_errors!(errors) + if errors[:project_access].any? + error!(errors[:project_access], 422) + elsif errors[:branch_conflict].any? + error!(errors[:branch_conflict], 422) + elsif errors[:validate_fork].any? + error!(errors[:validate_fork], 422) + elsif errors[:validate_branches].any? + conflict!(errors[:validate_branches]) + end + + render_api_error!(errors, 400) + end + + params :optional_params do + optional :description, type: String, desc: 'The description of the merge request' + optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request' + optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request' + optional :labels, type: String, desc: 'Comma-separated list of label names' + optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging' + end + end + + desc 'List merge requests' do + detail 'iid filter is deprecated have been removed on V4' + success Entities::MergeRequest + end + params do + optional :state, type: String, values: %w[opened closed merged all], default: 'all', + desc: 'Return opened, closed, merged, or all merge requests' + optional :order_by, type: String, values: %w[created_at updated_at], default: 'created_at', + desc: 'Return merge requests ordered by `created_at` or `updated_at` fields.' + optional :sort, type: String, values: %w[asc desc], default: 'desc', + desc: 'Return merge requests sorted in `asc` or `desc` order.' + optional :iid, type: Array[Integer], desc: 'The IID of the merge requests' + use :pagination + end + get ":id/merge_requests" do + authorize! :read_merge_request, user_project + + merge_requests = user_project.merge_requests.inc_notes_with_associations + merge_requests = filter_by_iid(merge_requests, params[:iid]) if params[:iid].present? + + merge_requests = + case params[:state] + when 'opened' then merge_requests.opened + when 'closed' then merge_requests.closed + when 'merged' then merge_requests.merged + else merge_requests + end + + merge_requests = merge_requests.reorder(params[:order_by] => params[:sort]) + present paginate(merge_requests), with: Entities::MergeRequest, current_user: current_user, project: user_project + end + + desc 'Create a merge request' do + success Entities::MergeRequest + end + params do + requires :title, type: String, desc: 'The title of the merge request' + requires :source_branch, type: String, desc: 'The source branch' + requires :target_branch, type: String, desc: 'The target branch' + optional :target_project_id, type: Integer, + desc: 'The target project of the merge request defaults to the :id of the project' + use :optional_params + end + post ":id/merge_requests" do + authorize! :create_merge_request, user_project + + mr_params = declared_params(include_missing: false) + mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? + + merge_request = ::MergeRequests::CreateService.new(user_project, current_user, mr_params).execute + + if merge_request.valid? + present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project + else + handle_merge_request_errors! merge_request.errors + end + end + + desc 'Delete a merge request' + params do + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + end + delete ":id/merge_requests/:merge_request_id" do + merge_request = find_project_merge_request(params[:merge_request_id]) + + authorize!(:destroy_merge_request, merge_request) + merge_request.destroy + end + + params do + requires :merge_request_id, type: Integer, desc: 'The ID of a merge request' + end + { ":id/merge_request/:merge_request_id" => :deprecated, ":id/merge_requests/:merge_request_id" => :ok }.each do |path, status| + desc 'Get a single merge request' do + if status == :deprecated + detail DEPRECATION_MESSAGE + end + success Entities::MergeRequest + end + get path do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + + present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project + end + + desc 'Get the commits of a merge request' do + success Entities::RepoCommit + end + get "#{path}/commits" do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + + present merge_request.commits, with: Entities::RepoCommit + end + + desc 'Show the merge request changes' do + success Entities::MergeRequestChanges + end + get "#{path}/changes" do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + + present merge_request, with: Entities::MergeRequestChanges, current_user: current_user + end + + desc 'Update a merge request' do + success Entities::MergeRequest + end + params do + optional :title, type: String, allow_blank: false, desc: 'The title of the merge request' + optional :target_branch, type: String, allow_blank: false, desc: 'The target branch' + optional :state_event, type: String, values: %w[close reopen merge], + desc: 'Status of the merge request' + use :optional_params + at_least_one_of :title, :target_branch, :description, :assignee_id, + :milestone_id, :labels, :state_event, + :remove_source_branch + end + put path do + merge_request = find_merge_request_with_access(params.delete(:merge_request_id), :update_merge_request) + + mr_params = declared_params(include_missing: false) + mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params[:remove_source_branch].present? + + merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request) + + if merge_request.valid? + present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project + else + handle_merge_request_errors! merge_request.errors + end + end + + desc 'Merge a merge request' do + success Entities::MergeRequest + end + params do + optional :merge_commit_message, type: String, desc: 'Custom merge commit message' + optional :should_remove_source_branch, type: Boolean, + desc: 'When true, the source branch will be deleted if possible' + optional :merge_when_build_succeeds, type: Boolean, + desc: 'When true, this merge request will be merged when the pipeline succeeds' + optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch' + end + put "#{path}/merge" do + merge_request = find_project_merge_request(params[:merge_request_id]) + + # Merge request can not be merged + # because user dont have permissions to push into target branch + unauthorized! unless merge_request.can_be_merged_by?(current_user) + + not_allowed! unless merge_request.mergeable_state? + + render_api_error!('Branch cannot be merged', 406) unless merge_request.mergeable? + + if params[:sha] && merge_request.diff_head_sha != params[:sha] + render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409) + end + + merge_params = { + commit_message: params[:merge_commit_message], + should_remove_source_branch: params[:should_remove_source_branch] + } + + if params[:merge_when_build_succeeds] && merge_request.head_pipeline && merge_request.head_pipeline.active? + ::MergeRequests::MergeWhenPipelineSucceedsService + .new(merge_request.target_project, current_user, merge_params) + .execute(merge_request) + else + ::MergeRequests::MergeService + .new(merge_request.target_project, current_user, merge_params) + .execute(merge_request) + end + + present merge_request, with: Entities::MergeRequest, current_user: current_user, project: user_project + end + + desc 'Cancel merge if "Merge When Pipeline Succeeds" is enabled' do + success Entities::MergeRequest + end + post "#{path}/cancel_merge_when_build_succeeds" do + merge_request = find_project_merge_request(params[:merge_request_id]) + + unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user) + + ::MergeRequest::MergeWhenPipelineSucceedsService + .new(merge_request.target_project, current_user) + .cancel(merge_request) + end + + desc 'Get the comments of a merge request' do + detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4' + success Entities::MRNote + end + params do + use :pagination + end + get "#{path}/comments" do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + present paginate(merge_request.notes.fresh), with: Entities::MRNote + end + + desc 'Post a comment to a merge request' do + detail 'Duplicate. DEPRECATED and HAS BEEN REMOVED in V4' + success Entities::MRNote + end + params do + requires :note, type: String, desc: 'The text of the comment' + end + post "#{path}/comments" do + merge_request = find_merge_request_with_access(params[:merge_request_id], :create_note) + + opts = { + note: params[:note], + noteable_type: 'MergeRequest', + noteable_id: merge_request.id + } + + note = ::Notes::CreateService.new(user_project, current_user, opts).execute + + if note.save + present note, with: Entities::MRNote + else + render_api_error!("Failed to save note #{note.errors.messages}", 400) + end + end + + desc 'List issues that will be closed on merge' do + success Entities::MRNote + end + params do + use :pagination + end + get "#{path}/closes_issues" do + merge_request = find_merge_request_with_access(params[:merge_request_id]) + issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user)) + present paginate(issues), with: issue_entity(user_project), current_user: current_user + end + end + end + end + end +end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 62f1b8d7ca2..863da19f294 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -612,23 +612,6 @@ describe API::Issues, api: true do expect(json_response['iid']).to eq(issue.iid) end - it 'returns a project issue by iid' do - get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user) - - expect(response.status).to eq 200 - expect(json_response.length).to eq 1 - expect(json_response.first['title']).to eq issue.title - expect(json_response.first['id']).to eq issue.id - expect(json_response.first['iid']).to eq issue.iid - end - - it 'returns an empty array for an unknown project issue iid' do - get api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user) - - expect(response.status).to eq 200 - expect(json_response.length).to eq 0 - end - it "returns 404 if issue id not found" do get api("/projects/#{project.id}/issues/54321", user) expect(response).to have_http_status(404) diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 21a2c583aa8..ff10e79e417 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -73,6 +73,16 @@ describe API::MergeRequests, api: true do expect(json_response.first['title']).to eq(merge_request_merged.title) end + it 'returns merge_request by "iids" array' do + get api("/projects/#{project.id}/merge_requests", user), iids: [merge_request.iid, merge_request_closed.iid] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['title']).to eq merge_request_closed.title + expect(json_response.first['id']).to eq merge_request_closed.id + end + context "with ordering" do before do @mr_later = mr_with_later_created_and_updated_at_time @@ -159,24 +169,6 @@ describe API::MergeRequests, api: true do expect(json_response['force_close_merge_request']).to be_falsy end - it 'returns merge_request by iid' do - url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}" - get api(url, user) - expect(response.status).to eq 200 - expect(json_response.first['title']).to eq merge_request.title - expect(json_response.first['id']).to eq merge_request.id - end - - it 'returns merge_request by iid array' do - get api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid] - - expect(response).to have_http_status(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(2) - expect(json_response.first['title']).to eq merge_request_closed.title - expect(json_response.first['id']).to eq merge_request_closed.id - end - it "returns a 404 error if merge_request_id not found" do get api("/projects/#{project.id}/merge_requests/999", user) expect(response).to have_http_status(404) diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb new file mode 100644 index 00000000000..33a127de98a --- /dev/null +++ b/spec/requests/api/v3/issues_spec.rb @@ -0,0 +1,1259 @@ +require 'spec_helper' + +describe API::V3::Issues, api: true do + include ApiHelpers + include EmailHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:non_member) { create(:user) } + let(:guest) { create(:user) } + let(:author) { create(:author) } + let(:assignee) { create(:assignee) } + let(:admin) { create(:user, :admin) } + let!(:project) { create(:empty_project, :public, creator_id: user.id, namespace: user.namespace ) } + let!(:closed_issue) do + create :closed_issue, + author: user, + assignee: user, + project: project, + state: :closed, + milestone: milestone, + created_at: generate(:issue_created_at), + updated_at: 3.hours.ago + end + let!(:confidential_issue) do + create :issue, + :confidential, + project: project, + author: author, + assignee: assignee, + created_at: generate(:issue_created_at), + updated_at: 2.hours.ago + end + let!(:issue) do + create :issue, + author: user, + assignee: user, + project: project, + milestone: milestone, + created_at: generate(:issue_created_at), + updated_at: 1.hour.ago + end + let!(:label) do + create(:label, title: 'label', color: '#FFAABB', project: project) + end + let!(:label_link) { create(:label_link, label: label, target: issue) } + let!(:milestone) { create(:milestone, title: '1.0.0', project: project) } + let!(:empty_milestone) do + create(:milestone, title: '2.0.0', project: project) + end + let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) } + + let(:no_milestone_title) { URI.escape(Milestone::None.title) } + + before do + project.team << [user, :reporter] + project.team << [guest, :guest] + end + + describe "GET /issues" do + context "when unauthenticated" do + it "returns authentication error" do + get v3_api("/issues") + + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns an array of issues" do + get v3_api("/issues", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.first['title']).to eq(issue.title) + expect(json_response.last).to have_key('web_url') + end + + it 'returns an array of closed issues' do + get v3_api('/issues?state=closed', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) + end + + it 'returns an array of opened issues' do + get v3_api('/issues?state=opened', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(issue.id) + end + + it 'returns an array of all issues' do + get v3_api('/issues?state=all', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) + end + + it 'returns an array of labeled issues' do + get v3_api("/issues?labels=#{label.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) + end + + it 'returns an array of labeled issues when at least one label matches' do + get v3_api("/issues?labels=#{label.title},foo,bar", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) + end + + it 'returns an empty array if no issue matches labels' do + get v3_api('/issues?labels=foo,bar', user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'returns an array of labeled issues matching given state' do + get v3_api("/issues?labels=#{label.title}&state=opened", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) + expect(json_response.first['state']).to eq('opened') + end + + it 'returns an empty array if no issue matches labels and state filters' do + get v3_api("/issues?labels=#{label.title}&state=closed", user) + + expect(response).to have_http_status(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 v3_api("/issues?milestone=#{empty_milestone.title}", user) + + expect(response).to have_http_status(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 v3_api("/issues?milestone=foo", user) + + expect(response).to have_http_status(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 v3_api("/issues?milestone=#{milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues matching state in milestone' do + get v3_api("/issues?milestone=#{milestone.title}", user), + '&state=closed' + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues with no milestone' do + get v3_api("/issues?milestone=#{no_milestone_title}", author) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(confidential_issue.id) + end + + it 'sorts by created_at descending by default' do + get v3_api('/issues', user) + + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts ascending when requested' do + get v3_api('/issues?sort=asc', user) + + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + + it 'sorts by updated_at descending when requested' do + get v3_api('/issues?order_by=updated_at', user) + + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get v3_api('/issues?order_by=updated_at&sort=asc', user) + + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + end + end + + describe "GET /groups/:id/issues" do + let!(:group) { create(:group) } + let!(:group_project) { create(:empty_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, + updated_at: 3.hours.ago + end + let!(:group_confidential_issue) do + create :issue, + :confidential, + project: group_project, + author: author, + assignee: assignee, + updated_at: 2.hours.ago + end + let!(:group_issue) do + create :issue, + author: user, + assignee: user, + project: group_project, + milestone: group_milestone, + updated_at: 1.hour.ago + 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 v3_api(base_url, non_member) + + expect(response).to have_http_status(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 v3_api(base_url, author) + + expect(response).to have_http_status(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 v3_api(base_url, assignee) + + expect(response).to have_http_status(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 v3_api(base_url, user) + + expect(response).to have_http_status(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 v3_api(base_url, admin) + + expect(response).to have_http_status(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 v3_api("#{base_url}?labels=#{group_label.title}", user) + + expect(response).to have_http_status(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 v3_api("#{base_url}?labels=#{group_label.title},foo,bar", user) + + expect(response).to have_http_status(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 v3_api("#{base_url}?labels=foo,bar", user) + + expect(response).to have_http_status(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 v3_api("#{base_url}?milestone=#{group_empty_milestone.title}", user) + + expect(response).to have_http_status(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 v3_api("#{base_url}?milestone=foo", user) + + expect(response).to have_http_status(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 v3_api("#{base_url}?milestone=#{group_milestone.title}", user) + + expect(response).to have_http_status(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 v3_api("#{base_url}?milestone=#{group_milestone.title}", user), + '&state=closed' + + expect(response).to have_http_status(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 + + it 'returns an array of issues with no milestone' do + get v3_api("#{base_url}?milestone=#{no_milestone_title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(group_confidential_issue.id) + end + + it 'sorts by created_at descending by default' do + get v3_api(base_url, user) + + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts ascending when requested' do + get v3_api("#{base_url}?sort=asc", user) + + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + + it 'sorts by updated_at descending when requested' do + get v3_api("#{base_url}?order_by=updated_at", user) + + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get v3_api("#{base_url}?order_by=updated_at&sort=asc", user) + + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + end + + describe "GET /projects/:id/issues" do + let(:base_url) { "/projects/#{project.id}" } + + it "returns 404 on private projects for other users" do + private_project = create(:empty_project, :private) + create(:issue, project: private_project) + + get v3_api("/projects/#{private_project.id}/issues", non_member) + + expect(response).to have_http_status(404) + end + + it 'returns no issues when user has access to project but not issues' do + restricted_project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + create(:issue, project: restricted_project) + + get v3_api("/projects/#{restricted_project.id}/issues", non_member) + + expect(json_response).to eq([]) + end + + it 'returns project issues without confidential issues for non project members' do + get v3_api("#{base_url}/issues", non_member) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'returns project issues without confidential issues for project members with guest role' do + get v3_api("#{base_url}/issues", guest) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'returns project confidential issues for author' do + get v3_api("#{base_url}/issues", author) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'returns project confidential issues for assignee' do + get v3_api("#{base_url}/issues", assignee) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'returns project issues with confidential issues for project members' do + get v3_api("#{base_url}/issues", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'returns project confidential issues for admin' do + get v3_api("#{base_url}/issues", admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'returns an array of labeled project issues' do + get v3_api("#{base_url}/issues?labels=#{label.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) + end + + it 'returns an array of labeled project issues where all labels match' do + get v3_api("#{base_url}/issues?labels=#{label.title},foo,bar", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['labels']).to eq([label.title]) + end + + it 'returns an empty array if no project issue matches labels' do + get v3_api("#{base_url}/issues?labels=foo,bar", user) + + expect(response).to have_http_status(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 v3_api("#{base_url}/issues?milestone=#{empty_milestone.title}", user) + + expect(response).to have_http_status(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 v3_api("#{base_url}/issues?milestone=foo", user) + + expect(response).to have_http_status(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 v3_api("#{base_url}/issues?milestone=#{milestone.title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['id']).to eq(issue.id) + expect(json_response.second['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues matching state in milestone' do + get v3_api("#{base_url}/issues?milestone=#{milestone.title}", user), + '&state=closed' + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(closed_issue.id) + end + + it 'returns an array of issues with no milestone' do + get v3_api("#{base_url}/issues?milestone=#{no_milestone_title}", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(confidential_issue.id) + end + + it 'sorts by created_at descending by default' do + get v3_api("#{base_url}/issues", user) + + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts ascending when requested' do + get v3_api("#{base_url}/issues?sort=asc", user) + + response_dates = json_response.map { |issue| issue['created_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + + it 'sorts by updated_at descending when requested' do + get v3_api("#{base_url}/issues?order_by=updated_at", user) + + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it 'sorts by updated_at ascending when requested' do + get v3_api("#{base_url}/issues?order_by=updated_at&sort=asc", user) + + response_dates = json_response.map { |issue| issue['updated_at'] } + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(response_dates).to eq(response_dates.sort) + end + end + + describe "GET /projects/:id/issues/:issue_id" do + it 'exposes known attributes' do + get v3_api("/projects/#{project.id}/issues/#{issue.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(issue.id) + expect(json_response['iid']).to eq(issue.iid) + expect(json_response['project_id']).to eq(issue.project.id) + expect(json_response['title']).to eq(issue.title) + expect(json_response['description']).to eq(issue.description) + expect(json_response['state']).to eq(issue.state) + expect(json_response['created_at']).to be_present + expect(json_response['updated_at']).to be_present + expect(json_response['labels']).to eq(issue.label_names) + expect(json_response['milestone']).to be_a Hash + expect(json_response['assignee']).to be_a Hash + expect(json_response['author']).to be_a Hash + expect(json_response['confidential']).to be_falsy + end + + it "returns a project issue by id" do + get v3_api("/projects/#{project.id}/issues/#{issue.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(issue.title) + expect(json_response['iid']).to eq(issue.iid) + end + + it 'returns a project issue by iid' do + get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid}", user) + + expect(response.status).to eq 200 + expect(json_response.length).to eq 1 + expect(json_response.first['title']).to eq issue.title + expect(json_response.first['id']).to eq issue.id + expect(json_response.first['iid']).to eq issue.iid + end + + it 'returns an empty array for an unknown project issue iid' do + get v3_api("/projects/#{project.id}/issues?iid=#{issue.iid + 10}", user) + + expect(response.status).to eq 200 + expect(json_response.length).to eq 0 + end + + it "returns 404 if issue id not found" do + get v3_api("/projects/#{project.id}/issues/54321", user) + + expect(response).to have_http_status(404) + end + + context 'confidential issues' do + it "returns 404 for non project members" do + get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member) + + expect(response).to have_http_status(404) + end + + it "returns 404 for project members with guest role" do + get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest) + + expect(response).to have_http_status(404) + end + + it "returns confidential issue for project members" do + get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it "returns confidential issue for author" do + get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it "returns confidential issue for assignee" do + get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it "returns confidential issue for admin" do + get v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin) + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + end + end + + describe "POST /projects/:id/issues" do + it 'creates a new project issue' do + post v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', labels: 'label, label2' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['description']).to be_nil + expect(json_response['labels']).to eq(['label', 'label2']) + expect(json_response['confidential']).to be_falsy + end + + it 'creates a new confidential project issue' do + post v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: true + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_truthy + end + + it 'creates a new confidential project issue with a different param' do + post v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: 'y' + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_truthy + end + + it 'creates a public issue when confidential param is false' do + post v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: false + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['confidential']).to be_falsy + end + + it 'creates a public issue when confidential param is invalid' do + post v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', confidential: 'foo' + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('confidential is invalid') + end + + it "sends notifications for subscribers of newly added labels" do + label = project.labels.first + label.toggle_subscription(user2, project) + + perform_enqueued_jobs do + post v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', labels: label.title + end + + should_email(user2) + end + + it "returns a 400 bad request if title not given" do + post v3_api("/projects/#{project.id}/issues", user), labels: 'label, label2' + + expect(response).to have_http_status(400) + end + + it 'allows special label names' do + post v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', + labels: 'label, label?, label&foo, ?, &' + + expect(response.status).to eq(201) + expect(json_response['labels']).to include 'label' + expect(json_response['labels']).to include 'label?' + expect(json_response['labels']).to include 'label&foo' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + it 'returns 400 if title is too long' do + post v3_api("/projects/#{project.id}/issues", user), + title: 'g' * 256 + + expect(response).to have_http_status(400) + expect(json_response['message']['title']).to eq([ + 'is too long (maximum is 255 characters)' + ]) + end + + context 'resolving issues in a merge request' do + let(:discussion) { Discussion.for_diff_notes([create(:diff_note_on_merge_request)]).first } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + before do + project.team << [user, :master] + post v3_api("/projects/#{project.id}/issues", user), + title: 'New Issue', + merge_request_for_resolving_discussions: merge_request.iid + end + + it 'creates a new project issue' do + expect(response).to have_http_status(:created) + end + + it 'resolves the discussions in a merge request' do + discussion.first_note.reload + + expect(discussion.resolved?).to be(true) + end + + it 'assigns a description to the issue mentioning the merge request' do + expect(json_response['description']).to include(merge_request.to_reference) + end + end + + context 'with due date' do + it 'creates a new project issue' do + due_date = 2.weeks.from_now.strftime('%Y-%m-%d') + + post v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', due_date: due_date + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('new issue') + expect(json_response['description']).to be_nil + expect(json_response['due_date']).to eq(due_date) + end + end + + context 'when an admin or owner makes the request' do + it 'accepts the creation date to be set' do + creation_time = 2.weeks.ago + post v3_api("/projects/#{project.id}/issues", user), + title: 'new issue', labels: 'label, label2', created_at: creation_time + + expect(response).to have_http_status(201) + expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time) + end + end + + context 'the user can only read the issue' do + it 'cannot create new labels' do + expect do + post v3_api("/projects/#{project.id}/issues", non_member), title: 'new issue', labels: 'label, label2' + end.not_to change { project.labels.count } + end + end + end + + describe 'POST /projects/:id/issues with spam filtering' do + before do + allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true) + allow_any_instance_of(AkismetService).to receive_messages(is_spam?: true) + end + + let(:params) do + { + title: 'new issue', + description: 'content here', + labels: 'label, label2' + } + end + + it "does not create a new project issue" do + expect { post v3_api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count) + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) + + spam_logs = SpamLog.all + + expect(spam_logs.count).to eq(1) + expect(spam_logs[0].title).to eq('new issue') + expect(spam_logs[0].description).to eq('content here') + expect(spam_logs[0].user).to eq(user) + expect(spam_logs[0].noteable_type).to eq('Issue') + end + end + + describe "PUT /projects/:id/issues/:issue_id to update only title" do + it "updates a project issue" do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title' + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it "returns 404 error if issue id not found" do + put v3_api("/projects/#{project.id}/issues/44444", user), + title: 'updated title' + + expect(response).to have_http_status(404) + end + + it 'allows special label names' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title', + labels: 'label, label?, label&foo, ?, &' + + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'label' + expect(json_response['labels']).to include 'label?' + expect(json_response['labels']).to include 'label&foo' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + context 'confidential issues' do + it "returns 403 for non project members" do + put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member), + title: 'updated title' + + expect(response).to have_http_status(403) + end + + it "returns 403 for project members with guest role" do + put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", guest), + title: 'updated title' + + expect(response).to have_http_status(403) + end + + it "updates a confidential issue for project members" do + put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + title: 'updated title' + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it "updates a confidential issue for author" do + put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", author), + title: 'updated title' + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it "updates a confidential issue for admin" do + put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin), + title: 'updated title' + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('updated title') + end + + it 'sets an issue to confidential' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + confidential: true + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_truthy + end + + it 'makes a confidential issue public' do + put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + confidential: false + + expect(response).to have_http_status(200) + expect(json_response['confidential']).to be_falsy + end + + it 'does not update a confidential issue with wrong confidential flag' do + put v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + confidential: 'foo' + + expect(response).to have_http_status(400) + expect(json_response['error']).to eq('confidential is invalid') + end + end + end + + describe 'PUT /projects/:id/issues/:issue_id to update labels' do + let!(:label) { create(:label, title: 'dummy', project: project) } + let!(:label_link) { create(:label_link, label: label, target: issue) } + + it 'does not update labels if not present' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title' + + expect(response).to have_http_status(200) + expect(json_response['labels']).to eq([label.title]) + end + + it "sends notifications for subscribers of newly added labels when issue is updated" do + label = create(:label, title: 'foo', color: '#FFAABB', project: project) + label.toggle_subscription(user2, project) + + perform_enqueued_jobs do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'updated title', labels: label.title + end + + should_email(user2) + end + + it 'removes all labels' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), labels: '' + + expect(response).to have_http_status(200) + expect(json_response['labels']).to eq([]) + end + + it 'updates labels' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + labels: 'foo,bar' + + expect(response).to have_http_status(200) + expect(json_response['labels']).to include 'foo' + expect(json_response['labels']).to include 'bar' + end + + it 'allows special label names' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' + + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'label:foo' + expect(json_response['labels']).to include 'label-bar' + expect(json_response['labels']).to include 'label_bar' + expect(json_response['labels']).to include 'label/bar' + expect(json_response['labels']).to include 'label?bar' + expect(json_response['labels']).to include 'label&bar' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + it 'returns 400 if title is too long' do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + title: 'g' * 256 + + expect(response).to have_http_status(400) + expect(json_response['message']['title']).to eq([ + 'is too long (maximum is 255 characters)' + ]) + end + end + + describe "PUT /projects/:id/issues/:issue_id to update state and label" do + it "updates a project issue" do + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + labels: 'label2', state_event: "close" + + expect(response).to have_http_status(200) + expect(json_response['labels']).to include 'label2' + expect(json_response['state']).to eq "closed" + end + + it 'reopens a project isssue' do + put v3_api("/projects/#{project.id}/issues/#{closed_issue.id}", user), state_event: 'reopen' + + expect(response).to have_http_status(200) + expect(json_response['state']).to eq 'reopened' + end + + context 'when an admin or owner makes the request' do + it 'accepts the update date to be set' do + update_time = 2.weeks.ago + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), + labels: 'label3', state_event: 'close', updated_at: update_time + + expect(response).to have_http_status(200) + expect(json_response['labels']).to include 'label3' + expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time) + end + end + end + + describe 'PUT /projects/:id/issues/:issue_id to update due date' do + it 'creates a new project issue' do + due_date = 2.weeks.from_now.strftime('%Y-%m-%d') + + put v3_api("/projects/#{project.id}/issues/#{issue.id}", user), due_date: due_date + + expect(response).to have_http_status(200) + expect(json_response['due_date']).to eq(due_date) + end + end + + describe "DELETE /projects/:id/issues/:issue_id" do + it "rejects a non member from deleting an issue" do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}", non_member) + + expect(response).to have_http_status(403) + end + + it "rejects a developer from deleting an issue" do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}", author) + + expect(response).to have_http_status(403) + end + + context "when the user is project owner" do + let(:owner) { create(:user) } + let(:project) { create(:empty_project, namespace: owner.namespace) } + + it "deletes the issue if an admin requests it" do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}", owner) + + expect(response).to have_http_status(200) + expect(json_response['state']).to eq 'opened' + end + end + + context 'when issue does not exist' do + it 'returns 404 when trying to move an issue' do + delete v3_api("/projects/#{project.id}/issues/123", user) + + expect(response).to have_http_status(404) + end + end + end + + describe '/projects/:id/issues/:issue_id/move' do + let!(:target_project) { create(:empty_project, path: 'project2', creator_id: user.id, namespace: user.namespace ) } + let!(:target_project2) { create(:empty_project, creator_id: non_member.id, namespace: non_member.namespace ) } + + it 'moves an issue' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project.id + + expect(response).to have_http_status(201) + expect(json_response['project_id']).to eq(target_project.id) + end + + context 'when source and target projects are the same' do + it 'returns 400 when trying to move an issue' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: project.id + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Cannot move issue to project it originates from!') + end + end + + context 'when the user does not have the permission to move issues' do + it 'returns 400 when trying to move an issue' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: target_project2.id + + expect(response).to have_http_status(400) + expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!') + end + end + + it 'moves the issue to another namespace if I am admin' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", admin), + to_project_id: target_project2.id + + expect(response).to have_http_status(201) + expect(json_response['project_id']).to eq(target_project2.id) + end + + context 'when issue does not exist' do + it 'returns 404 when trying to move an issue' do + post v3_api("/projects/#{project.id}/issues/123/move", user), + to_project_id: target_project.id + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Issue Not Found') + end + end + + context 'when source project does not exist' do + it 'returns 404 when trying to move an issue' do + post v3_api("/projects/123/issues/#{issue.id}/move", user), + to_project_id: target_project.id + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + end + + context 'when target project does not exist' do + it 'returns 404 when trying to move an issue' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user), + to_project_id: 123 + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST :id/issues/:issue_id/subscription' do + it 'subscribes to an issue' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) + + expect(response).to have_http_status(201) + expect(json_response['subscribed']).to eq(true) + end + + it 'returns 304 if already subscribed' do + post v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) + + expect(response).to have_http_status(304) + end + + it 'returns 404 if the issue is not found' do + post v3_api("/projects/#{project.id}/issues/123/subscription", user) + + expect(response).to have_http_status(404) + end + + it 'returns 404 if the issue is confidential' do + post v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) + + expect(response).to have_http_status(404) + end + end + + describe 'DELETE :id/issues/:issue_id/subscription' do + it 'unsubscribes from an issue' do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user) + + expect(response).to have_http_status(200) + expect(json_response['subscribed']).to eq(false) + end + + it 'returns 304 if not subscribed' do + delete v3_api("/projects/#{project.id}/issues/#{issue.id}/subscription", user2) + + expect(response).to have_http_status(304) + end + + it 'returns 404 if the issue is not found' do + delete v3_api("/projects/#{project.id}/issues/123/subscription", user) + + expect(response).to have_http_status(404) + end + + it 'returns 404 if the issue is confidential' do + delete v3_api("/projects/#{project.id}/issues/#{confidential_issue.id}/subscription", non_member) + + expect(response).to have_http_status(404) + end + end + + describe 'time tracking endpoints' do + let(:issuable) { issue } + + include_examples 'time tracking endpoints', 'issue' + end +end diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb new file mode 100644 index 00000000000..b94e1ef4ced --- /dev/null +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -0,0 +1,726 @@ +require "spec_helper" + +describe API::MergeRequests, api: true do + include ApiHelpers + let(:base_time) { Time.now } + let(:user) { create(:user) } + let(:admin) { create(:user, :admin) } + let(:non_member) { create(:user) } + let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace) } + let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, title: "Test", created_at: base_time) } + let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, title: "Closed test", created_at: base_time + 1.second) } + let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') } + let(:milestone) { create(:milestone, title: '1.0.0', project: project) } + + before do + project.team << [user, :reporter] + end + + describe "GET /projects/:id/merge_requests" do + context "when unauthenticated" do + it "returns authentication error" do + get v3_api("/projects/#{project.id}/merge_requests") + expect(response).to have_http_status(401) + end + end + + context "when authenticated" do + it "returns an array of all merge_requests" do + get v3_api("/projects/#{project.id}/merge_requests", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['title']).to eq(merge_request.title) + expect(json_response.last).to have_key('web_url') + expect(json_response.last['sha']).to eq(merge_request.diff_head_sha) + expect(json_response.last['merge_commit_sha']).to be_nil + expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha) + expect(json_response.first['title']).to eq(merge_request_merged.title) + expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha) + expect(json_response.first['merge_commit_sha']).not_to be_nil + expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha) + end + + it "returns an array of all merge_requests" do + get v3_api("/projects/#{project.id}/merge_requests?state", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.last['title']).to eq(merge_request.title) + end + + it "returns an array of open merge_requests" do + get v3_api("/projects/#{project.id}/merge_requests?state=opened", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.last['title']).to eq(merge_request.title) + end + + it "returns an array of closed merge_requests" do + get v3_api("/projects/#{project.id}/merge_requests?state=closed", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['title']).to eq(merge_request_closed.title) + end + + it "returns an array of merged merge_requests" do + get v3_api("/projects/#{project.id}/merge_requests?state=merged", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['title']).to eq(merge_request_merged.title) + end + + context "with ordering" do + before do + @mr_later = mr_with_later_created_and_updated_at_time + @mr_earlier = mr_with_earlier_created_and_updated_at_time + end + + it "returns an array of merge_requests in ascending order" do + get v3_api("/projects/#{project.id}/merge_requests?sort=asc", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + expect(response_dates).to eq(response_dates.sort) + end + + it "returns an array of merge_requests in descending order" do + get v3_api("/projects/#{project.id}/merge_requests?sort=desc", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it "returns an array of merge_requests ordered by updated_at" do + get v3_api("/projects/#{project.id}/merge_requests?order_by=updated_at", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + response_dates = json_response.map{ |merge_request| merge_request['updated_at'] } + expect(response_dates).to eq(response_dates.sort.reverse) + end + + it "returns an array of merge_requests ordered by created_at" do + get v3_api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + response_dates = json_response.map{ |merge_request| merge_request['created_at'] } + expect(response_dates).to eq(response_dates.sort) + end + end + end + end + + describe "GET /projects/:id/merge_requests/:merge_request_id" do + it 'exposes known attributes' do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + + expect(response).to have_http_status(200) + expect(json_response['id']).to eq(merge_request.id) + expect(json_response['iid']).to eq(merge_request.iid) + expect(json_response['project_id']).to eq(merge_request.project.id) + expect(json_response['title']).to eq(merge_request.title) + expect(json_response['description']).to eq(merge_request.description) + expect(json_response['state']).to eq(merge_request.state) + expect(json_response['created_at']).to be_present + expect(json_response['updated_at']).to be_present + expect(json_response['labels']).to eq(merge_request.label_names) + expect(json_response['milestone']).to be_nil + expect(json_response['assignee']).to be_a Hash + expect(json_response['author']).to be_a Hash + expect(json_response['target_branch']).to eq(merge_request.target_branch) + expect(json_response['source_branch']).to eq(merge_request.source_branch) + expect(json_response['upvotes']).to eq(0) + expect(json_response['downvotes']).to eq(0) + expect(json_response['source_project_id']).to eq(merge_request.source_project.id) + expect(json_response['target_project_id']).to eq(merge_request.target_project.id) + expect(json_response['work_in_progress']).to be_falsy + expect(json_response['merge_when_build_succeeds']).to be_falsy + expect(json_response['merge_status']).to eq('can_be_merged') + expect(json_response['should_close_merge_request']).to be_falsy + expect(json_response['force_close_merge_request']).to be_falsy + end + + it "returns merge_request" do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + expect(response).to have_http_status(200) + expect(json_response['title']).to eq(merge_request.title) + expect(json_response['iid']).to eq(merge_request.iid) + expect(json_response['work_in_progress']).to eq(false) + expect(json_response['merge_status']).to eq('can_be_merged') + expect(json_response['should_close_merge_request']).to be_falsy + expect(json_response['force_close_merge_request']).to be_falsy + end + + it 'returns merge_request by iid' do + url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}" + get v3_api(url, user) + expect(response.status).to eq 200 + expect(json_response.first['title']).to eq merge_request.title + expect(json_response.first['id']).to eq merge_request.id + end + + it 'returns merge_request by iid array' do + get v3_api("/projects/#{project.id}/merge_requests", user), iid: [merge_request.iid, merge_request_closed.iid] + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['title']).to eq merge_request_closed.title + expect(json_response.first['id']).to eq merge_request_closed.id + end + + it "returns a 404 error if merge_request_id not found" do + get v3_api("/projects/#{project.id}/merge_requests/999", user) + expect(response).to have_http_status(404) + end + + context 'Work in Progress' do + let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) } + + it "returns merge_request" do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request_wip.id}", user) + expect(response).to have_http_status(200) + expect(json_response['work_in_progress']).to eq(true) + end + end + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do + it 'returns a 200 when merge request is valid' do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) + commit = merge_request.commits.first + + expect(response.status).to eq 200 + expect(json_response.size).to eq(merge_request.commits.size) + expect(json_response.first['id']).to eq(commit.id) + expect(json_response.first['title']).to eq(commit.title) + end + + it 'returns a 404 when merge_request_id not found' do + get v3_api("/projects/#{project.id}/merge_requests/999/commits", user) + expect(response).to have_http_status(404) + end + end + + describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do + it 'returns the change information of the merge_request' do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) + expect(response.status).to eq 200 + expect(json_response['changes'].size).to eq(merge_request.diffs.size) + end + + it 'returns a 404 when merge_request_id not found' do + get v3_api("/projects/#{project.id}/merge_requests/999/changes", user) + expect(response).to have_http_status(404) + end + end + + describe "POST /projects/:id/merge_requests" do + context 'between branches projects' do + it "returns merge_request" do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: 'Test merge_request', + source_branch: 'feature_conflict', + target_branch: 'master', + author: user, + labels: 'label, label2', + milestone_id: milestone.id, + remove_source_branch: true + + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('Test merge_request') + expect(json_response['labels']).to eq(['label', 'label2']) + expect(json_response['milestone']['id']).to eq(milestone.id) + expect(json_response['force_remove_source_branch']).to be_truthy + end + + it "returns 422 when source_branch equals target_branch" do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: "Test merge_request", source_branch: "master", target_branch: "master", author: user + expect(response).to have_http_status(422) + end + + it "returns 400 when source_branch is missing" do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: "Test merge_request", target_branch: "master", author: user + expect(response).to have_http_status(400) + end + + it "returns 400 when target_branch is missing" do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: "Test merge_request", source_branch: "markdown", author: user + expect(response).to have_http_status(400) + end + + it "returns 400 when title is missing" do + post v3_api("/projects/#{project.id}/merge_requests", user), + target_branch: 'master', source_branch: 'markdown' + expect(response).to have_http_status(400) + end + + it 'allows special label names' do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: 'Test merge_request', + source_branch: 'markdown', + target_branch: 'master', + author: user, + labels: 'label, label?, label&foo, ?, &' + expect(response.status).to eq(201) + expect(json_response['labels']).to include 'label' + expect(json_response['labels']).to include 'label?' + expect(json_response['labels']).to include 'label&foo' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + context 'with existing MR' do + before do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: 'Test merge_request', + source_branch: 'feature_conflict', + target_branch: 'master', + author: user + @mr = MergeRequest.all.last + end + + it 'returns 409 when MR already exists for source/target' do + expect do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: 'New test merge_request', + source_branch: 'feature_conflict', + target_branch: 'master', + author: user + end.to change { MergeRequest.count }.by(0) + expect(response).to have_http_status(409) + end + end + end + + context 'forked projects' do + let!(:user2) { create(:user) } + let!(:fork_project) { create(:empty_project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) } + let!(:unrelated_project) { create(:empty_project, namespace: create(:user).namespace, creator_id: user2.id) } + + before :each do |each| + fork_project.team << [user2, :reporter] + end + + it "returns merge_request" do + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", + author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('Test merge_request') + expect(json_response['description']).to eq('Test description for Test merge_request') + end + + it "does not return 422 when source_branch equals target_branch" do + expect(project.id).not_to eq(fork_project.id) + expect(fork_project.forked?).to be_truthy + expect(fork_project.forked_from_project).to eq(project) + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id + expect(response).to have_http_status(201) + expect(json_response['title']).to eq('Test merge_request') + end + + it "returns 400 when source_branch is missing" do + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id + expect(response).to have_http_status(400) + end + + it "returns 400 when target_branch is missing" do + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id + expect(response).to have_http_status(400) + end + + it "returns 400 when title is missing" do + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id + expect(response).to have_http_status(400) + end + + context 'when target_branch is specified' do + it 'returns 422 if not a forked project' do + post v3_api("/projects/#{project.id}/merge_requests", user), + title: 'Test merge_request', + target_branch: 'master', + source_branch: 'markdown', + author: user, + target_project_id: fork_project.id + expect(response).to have_http_status(422) + end + + it 'returns 422 if targeting a different fork' do + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', + target_branch: 'master', + source_branch: 'markdown', + author: user2, + target_project_id: unrelated_project.id + expect(response).to have_http_status(422) + end + end + + it "returns 201 when target_branch is specified and for the same project" do + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id + expect(response).to have_http_status(201) + end + end + end + + describe "DELETE /projects/:id/merge_requests/:merge_request_id" do + context "when the user is developer" do + let(:developer) { create(:user) } + + before do + project.team << [developer, :developer] + end + + it "denies the deletion of the merge request" do + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", developer) + expect(response).to have_http_status(403) + end + end + + context "when the user is project owner" do + it "destroys the merge request owners can destroy" do + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) + + expect(response).to have_http_status(200) + end + end + end + + describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do + let(:pipeline) { create(:ci_pipeline_without_jobs) } + + it "returns merge_request in case of success" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + + expect(response).to have_http_status(200) + end + + it "returns 406 if branch can't be merged" do + allow_any_instance_of(MergeRequest). + to receive(:can_be_merged?).and_return(false) + + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + + expect(response).to have_http_status(406) + expect(json_response['message']).to eq('Branch cannot be merged') + end + + it "returns 405 if merge_request is not open" do + merge_request.close + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + expect(response).to have_http_status(405) + expect(json_response['message']).to eq('405 Method Not Allowed') + end + + it "returns 405 if merge_request is a work in progress" do + merge_request.update_attribute(:title, "WIP: #{merge_request.title}") + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + expect(response).to have_http_status(405) + expect(json_response['message']).to eq('405 Method Not Allowed') + end + + it 'returns 405 if the build failed for a merge request that requires success' do + allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false) + + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) + + expect(response).to have_http_status(405) + expect(json_response['message']).to eq('405 Method Not Allowed') + end + + it "returns 401 if user has no permissions to merge" do + user2 = create(:user) + project.team << [user2, :reporter] + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2) + expect(response).to have_http_status(401) + expect(json_response['message']).to eq('401 Unauthorized') + end + + it "returns 409 if the SHA parameter doesn't match" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha.reverse + + expect(response).to have_http_status(409) + expect(json_response['message']).to start_with('SHA does not match HEAD of source branch') + end + + it "succeeds if the SHA parameter matches" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), sha: merge_request.diff_head_sha + + expect(response).to have_http_status(200) + end + + it "enables merge when pipeline succeeds if the pipeline is active" do + allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline) + allow(pipeline).to receive(:active?).and_return(true) + + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true + + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('Test') + expect(json_response['merge_when_build_succeeds']).to eq(true) + end + end + + describe "PUT /projects/:id/merge_requests/:merge_request_id" do + context "to close a MR" do + it "returns merge_request" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" + + expect(response).to have_http_status(200) + expect(json_response['state']).to eq('closed') + end + end + + it "updates title and returns merge_request" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" + expect(response).to have_http_status(200) + expect(json_response['title']).to eq('New title') + end + + it "updates description and returns merge_request" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description" + expect(response).to have_http_status(200) + expect(json_response['description']).to eq('New description') + end + + it "updates milestone_id and returns merge_request" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id + expect(response).to have_http_status(200) + expect(json_response['milestone']['id']).to eq(milestone.id) + end + + it "returns merge_request with renamed target_branch" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki" + expect(response).to have_http_status(200) + expect(json_response['target_branch']).to eq('wiki') + end + + it "returns merge_request that removes the source branch" do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), remove_source_branch: true + + expect(response).to have_http_status(200) + expect(json_response['force_remove_source_branch']).to be_truthy + end + + it 'allows special label names' do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), + title: 'new issue', + labels: 'label, label?, label&foo, ?, &' + + expect(response.status).to eq(200) + expect(json_response['labels']).to include 'label' + expect(json_response['labels']).to include 'label?' + expect(json_response['labels']).to include 'label&foo' + expect(json_response['labels']).to include '?' + expect(json_response['labels']).to include '&' + end + + it 'does not update state when title is empty' do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', title: nil + + merge_request.reload + expect(response).to have_http_status(400) + expect(merge_request.state).to eq('opened') + end + + it 'does not update state when target_branch is empty' do + put v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: 'close', target_branch: nil + + merge_request.reload + expect(response).to have_http_status(400) + expect(merge_request.state).to eq('opened') + end + end + + describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do + it "returns comment" do + original_count = merge_request.notes.size + + post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment" + + expect(response).to have_http_status(201) + expect(json_response['note']).to eq('My comment') + expect(json_response['author']['name']).to eq(user.name) + expect(json_response['author']['username']).to eq(user.username) + expect(merge_request.reload.notes.size).to eq(original_count + 1) + end + + it "returns 400 if note is missing" do + post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + expect(response).to have_http_status(400) + end + + it "returns 404 if note is attached to non existent merge request" do + post v3_api("/projects/#{project.id}/merge_requests/404/comments", user), + note: 'My comment' + expect(response).to have_http_status(404) + end + end + + describe "GET :id/merge_requests/:merge_request_id/comments" do + let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } + let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } + + it "returns merge_request comments ordered by created_at" do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['note']).to eq("a comment on a MR") + expect(json_response.first['author']['id']).to eq(user.id) + expect(json_response.last['note']).to eq("another comment on a MR") + end + + it "returns a 404 error if merge_request_id not found" do + get v3_api("/projects/#{project.id}/merge_requests/999/comments", user) + expect(response).to have_http_status(404) + end + end + + describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do + it 'returns the issue that will be closed on merge' do + issue = create(:issue, project: project) + mr = merge_request.tap do |mr| + mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}") + end + + get v3_api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(issue.id) + end + + it 'returns an empty array when there are no issues to be closed' do + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + + it 'handles external issues' do + jira_project = create(:jira_project, :public, name: 'JIR_EXT1') + issue = ExternalIssue.new("#{jira_project.name}-123", jira_project) + merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project) + merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}") + + get v3_api("/projects/#{jira_project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['title']).to eq(issue.title) + expect(json_response.first['id']).to eq(issue.id) + end + + it 'returns 403 if the user has no access to the merge request' do + project = create(:empty_project, :private) + merge_request = create(:merge_request, :simple, source_project: project) + guest = create(:user) + project.team << [guest, :guest] + + get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", guest) + + expect(response).to have_http_status(403) + end + end + + describe 'POST :id/merge_requests/:merge_request_id/subscription' do + it 'subscribes to a merge request' do + post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) + + expect(response).to have_http_status(201) + expect(json_response['subscribed']).to eq(true) + end + + it 'returns 304 if already subscribed' do + post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) + + expect(response).to have_http_status(304) + end + + it 'returns 404 if the merge request is not found' do + post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user) + + expect(response).to have_http_status(404) + end + + it 'returns 403 if user has no access to read code' do + guest = create(:user) + project.team << [guest, :guest] + + post v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) + + expect(response).to have_http_status(403) + end + end + + describe 'DELETE :id/merge_requests/:merge_request_id/subscription' do + it 'unsubscribes from a merge request' do + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", user) + + expect(response).to have_http_status(200) + expect(json_response['subscribed']).to eq(false) + end + + it 'returns 304 if not subscribed' do + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", admin) + + expect(response).to have_http_status(304) + end + + it 'returns 404 if the merge request is not found' do + post v3_api("/projects/#{project.id}/merge_requests/123/subscription", user) + + expect(response).to have_http_status(404) + end + + it 'returns 403 if user has no access to read code' do + guest = create(:user) + project.team << [guest, :guest] + + delete v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/subscription", guest) + + expect(response).to have_http_status(403) + end + end + + describe 'Time tracking' do + let(:issuable) { merge_request } + + include_examples 'time tracking endpoints', 'merge_request' + end + + def mr_with_later_created_and_updated_at_time + merge_request + merge_request.created_at += 1.hour + merge_request.updated_at += 30.minutes + merge_request.save + merge_request + end + + def mr_with_earlier_created_and_updated_at_time + merge_request_closed + merge_request_closed.created_at -= 1.hour + merge_request_closed.updated_at -= 30.minutes + merge_request_closed.save + merge_request_closed + end +end -- cgit v1.2.1 From a08bbcbc4417baf0316e77501b44e5db6d75fe9a Mon Sep 17 00:00:00 2001 From: Sean McGivern <sean@gitlab.com> Date: Mon, 6 Feb 2017 12:17:26 +0000 Subject: Don't render snippet actions for logged-out users --- app/views/projects/snippets/_actions.html.haml | 2 ++ app/views/snippets/_actions.html.haml | 46 +++++++++++++------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index e2a5107a883..dde2e2b644d 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -1,3 +1,5 @@ +- return unless current_user + .hidden-xs - if can?(current_user, :update_project_snippet, @snippet) = link_to edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-grouped" do diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index 9a9a3ff9220..855a995afa9 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -1,3 +1,5 @@ +- return unless current_user + .hidden-xs - if can?(current_user, :update_personal_snippet, @snippet) = link_to edit_snippet_path(@snippet), class: "btn btn-grouped" do @@ -5,29 +7,27 @@ - if can?(current_user, :admin_personal_snippet, @snippet) = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do Delete - - if current_user - = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do - New snippet + = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do + New snippet - if @snippet.submittable_as_spam? && current_user.admin? = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' -- if current_user - .visible-xs-block.dropdown - %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } - Options - = icon('caret-down') - .dropdown-menu.dropdown-menu-full-width - %ul +.visible-xs-block.dropdown + %button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } } + Options + = icon('caret-down') + .dropdown-menu.dropdown-menu-full-width + %ul + %li + = link_to new_snippet_path, title: "New snippet" do + New snippet + - if can?(current_user, :admin_personal_snippet, @snippet) %li - = link_to new_snippet_path, title: "New snippet" do - New snippet - - if can?(current_user, :admin_personal_snippet, @snippet) - %li - = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do - Delete - - if can?(current_user, :update_personal_snippet, @snippet) - %li - = link_to edit_snippet_path(@snippet) do - Edit - - if @snippet.submittable_as_spam? && current_user.admin? - %li - = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post + = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do + Delete + - if can?(current_user, :update_personal_snippet, @snippet) + %li + = link_to edit_snippet_path(@snippet) do + Edit + - if @snippet.submittable_as_spam? && current_user.admin? + %li + = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post -- cgit v1.2.1 From 05ec0f281e0dabcf8282206503046cf9894698f8 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 6 Feb 2017 12:23:40 +0000 Subject: Fixed eslint --- app/assets/javascripts/boards/components/modal/filters.js.es6 | 10 +++++++--- app/assets/javascripts/boards/components/modal/header.js.es6 | 4 +++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/boards/components/modal/filters.js.es6 b/app/assets/javascripts/boards/components/modal/filters.js.es6 index f7d89855998..6de06811d94 100644 --- a/app/assets/javascripts/boards/components/modal/filters.js.es6 +++ b/app/assets/javascripts/boards/components/modal/filters.js.es6 @@ -1,4 +1,8 @@ /* global Vue */ +const userFilter = require('./filters/user'); +const milestoneFilter = require('./filters/milestone'); +const labelFilter = require('./filters/label'); + module.exports = Vue.extend({ name: 'modal-filters', props: { @@ -19,9 +23,9 @@ module.exports = Vue.extend({ gl.issueBoards.ModalStore.setDefaultFilter(); }, components: { - 'user-filter': require('./filters/user'), - 'milestone-filter': require('./filters/milestone'), - 'label-filter': require('./filters/label'), + userFilter, + milestoneFilter, + labelFilter, }, template: ` <div class="modal-filters"> diff --git a/app/assets/javascripts/boards/components/modal/header.js.es6 b/app/assets/javascripts/boards/components/modal/header.js.es6 index 01f5b318fb2..70c088f9054 100644 --- a/app/assets/javascripts/boards/components/modal/header.js.es6 +++ b/app/assets/javascripts/boards/components/modal/header.js.es6 @@ -1,5 +1,7 @@ /* global Vue */ require('./tabs'); +const modalFilters = require('./filters'); + (() => { const ModalStore = gl.issueBoards.ModalStore; @@ -43,7 +45,7 @@ require('./tabs'); }, components: { 'modal-tabs': gl.issueBoards.ModalTabs, - 'modal-filters': require('./filters'), + modalFilters, }, template: ` <div> -- cgit v1.2.1 From ed834c9ff1e7fcb8d5792b64c9f0e69e0eebfbe8 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva <pedro@gitlab.com> Date: Thu, 26 Jan 2017 12:33:35 +0000 Subject: Make all system notes lowercase --- app/services/system_note_service.rb | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index a11bca00687..6f43f59a985 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -118,16 +118,18 @@ module SystemNoteService # # Example Note text: # - # "Changed estimate of this issue to 3d 5h" + # "removed time estimate on this issue" + # + # "changed time estimate of this issue to 3d 5h" # # Returns the created Note object def change_time_estimate(noteable, project, author) parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate) body = if noteable.time_estimate == 0 - "Removed time estimate on this #{noteable.human_class_name}" + "removed time estimate on this #{noteable.human_class_name}" else - "Changed time estimate of this #{noteable.human_class_name} to #{parsed_time}" + "changed time estimate of this #{noteable.human_class_name} to #{parsed_time}" end create_note(noteable: noteable, project: project, author: author, note: body) @@ -142,7 +144,9 @@ module SystemNoteService # # Example Note text: # - # "Added 2h 30m of time spent on this issue" + # "removed time spent on this issue" + # + # "added 2h 30m of time spent on this issue" # # Returns the created Note object @@ -150,10 +154,10 @@ module SystemNoteService time_spent = noteable.time_spent if time_spent == :reset - body = "Removed time spent on this #{noteable.human_class_name}" + body = "removed time spent on this #{noteable.human_class_name}" else parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs) - action = time_spent > 0 ? 'Added' : 'Subtracted' + action = time_spent > 0 ? 'added' : 'subtracted' body = "#{action} #{parsed_time} of time spent on this #{noteable.human_class_name}" end @@ -221,7 +225,7 @@ module SystemNoteService end def discussion_continued_in_issue(discussion, project, author, issue) - body = "Added #{issue.to_reference} to continue this discussion" + body = "created #{issue.to_reference} to continue this discussion" note_attributes = discussion.reply_attributes.merge(project: project, author: author, note: body) note_attributes[:type] = note_attributes.delete(:note_type) @@ -260,7 +264,7 @@ module SystemNoteService # # Example Note text: # - # "made the issue confidential" + # "made the issue confidential" # # Returns the created Note object def change_issue_confidentiality(issue, project, author) -- cgit v1.2.1 From 5316d1cf456df6707f2fb92807162541f96939a0 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva <pedro@gitlab.com> Date: Thu, 26 Jan 2017 13:00:32 +0000 Subject: Remove noteable object in time tracking system notes [ci-skip] --- app/services/system_note_service.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 6f43f59a985..110072e3a16 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -118,18 +118,18 @@ module SystemNoteService # # Example Note text: # - # "removed time estimate on this issue" + # "removed time estimate" # - # "changed time estimate of this issue to 3d 5h" + # "changed time estimate to 3d 5h" # # Returns the created Note object def change_time_estimate(noteable, project, author) parsed_time = Gitlab::TimeTrackingFormatter.output(noteable.time_estimate) body = if noteable.time_estimate == 0 - "removed time estimate on this #{noteable.human_class_name}" + "removed time estimate" else - "changed time estimate of this #{noteable.human_class_name} to #{parsed_time}" + "changed time estimate to #{parsed_time}" end create_note(noteable: noteable, project: project, author: author, note: body) @@ -144,9 +144,9 @@ module SystemNoteService # # Example Note text: # - # "removed time spent on this issue" + # "removed time spent" # - # "added 2h 30m of time spent on this issue" + # "added 2h 30m of time spent" # # Returns the created Note object @@ -154,11 +154,11 @@ module SystemNoteService time_spent = noteable.time_spent if time_spent == :reset - body = "removed time spent on this #{noteable.human_class_name}" + body = "removed time spent" else parsed_time = Gitlab::TimeTrackingFormatter.output(time_spent.abs) action = time_spent > 0 ? 'added' : 'subtracted' - body = "#{action} #{parsed_time} of time spent on this #{noteable.human_class_name}" + body = "#{action} #{parsed_time} of time spent" end create_note(noteable: noteable, project: project, author: author, note: body) -- cgit v1.2.1 From bbdb6a111106ca17573a83f52afa043b7f5e0299 Mon Sep 17 00:00:00 2001 From: Pedro Moreira da Silva <pedro@gitlab.com> Date: Fri, 3 Feb 2017 16:57:44 +0000 Subject: Update system_note_service_spec.rb --- spec/services/system_note_service_spec.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index bd7269045e1..7913a180f9b 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -752,13 +752,13 @@ describe SystemNoteService, services: true do it 'sets the note text' do noteable.update_attribute(:time_estimate, 277200) - expect(subject.note).to eq "Changed time estimate of this issue to 1w 4d 5h" + expect(subject.note).to eq "changed time estimate to 1w 4d 5h" end end context 'without a time estimate' do it 'sets the note text' do - expect(subject.note).to eq "Removed time estimate on this issue" + expect(subject.note).to eq "removed time estimate" end end end @@ -782,7 +782,7 @@ describe SystemNoteService, services: true do it 'sets the note text' do spend_time!(277200) - expect(subject.note).to eq "Added 1w 4d 5h of time spent on this merge request" + expect(subject.note).to eq "added 1w 4d 5h of time spent" end end @@ -790,7 +790,7 @@ describe SystemNoteService, services: true do it 'sets the note text' do spend_time!(-277200) - expect(subject.note).to eq "Subtracted 1w 4d 5h of time spent on this merge request" + expect(subject.note).to eq "subtracted 1w 4d 5h of time spent" end end @@ -798,7 +798,7 @@ describe SystemNoteService, services: true do it 'sets the note text' do spend_time!(:reset) - expect(subject.note).to eq "Removed time spent on this merge request" + expect(subject.note).to eq "removed time spent" end end -- cgit v1.2.1 From 77e7f512c38183c70ea40a245da80b36bbd82f77 Mon Sep 17 00:00:00 2001 From: Job van der Voort <jobvandervoort@gmail.com> Date: Mon, 6 Feb 2017 13:37:41 +0000 Subject: add the stewardship label to contributing.md --- CONTRIBUTING.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8d1f3d3f926..315cd1e598c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,6 +15,7 @@ - [Issue weight](#issue-weight) - [Regression issues](#regression-issues) - [Technical debt](#technical-debt) + - [Stewardship][#stewardship] - [Merge requests](#merge-requests) - [Merge request guidelines](#merge-request-guidelines) - [Contribution acceptance criteria](#contribution-acceptance-criteria) @@ -230,6 +231,21 @@ for a release by the appropriate person. Make sure to mention the merge request that the `technical debt` issue is associated with in the description of the issue. +### Stewardship + +For issues related to the open source stewardship of GitLab, +there is the ~"stewardship" label. + +This label is to be used for issues in which the stewardship of GitLab +is a topic of discussion. For instance if GitLab Inc. is planning to remove +features from GitLab CE to make exclusive in GitLab EE, related issues +would be labelled with ~"stewardship". + +A recent example of this was the issue for +[bringing the time tracking API to GitLab CE][time-tracking-issue]. + +[time-tracking-issue]: https://gitlab.com/gitlab-org/gitlab-ce/issues/25517#note_20019084 + ## Merge requests We welcome merge requests with fixes and improvements to GitLab code, tests, -- cgit v1.2.1 From ab92e02503b79e7e11dffeb69d505a9049f83f3d Mon Sep 17 00:00:00 2001 From: Oswaldo Ferreira <oswaldo@gitlab.com> Date: Mon, 6 Feb 2017 11:45:21 -0200 Subject: Add changelog file --- changelogs/unreleased/pms-lowercase-system-notes.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelogs/unreleased/pms-lowercase-system-notes.yml diff --git a/changelogs/unreleased/pms-lowercase-system-notes.yml b/changelogs/unreleased/pms-lowercase-system-notes.yml new file mode 100644 index 00000000000..c2fa1a7fad0 --- /dev/null +++ b/changelogs/unreleased/pms-lowercase-system-notes.yml @@ -0,0 +1,4 @@ +--- +title: Make all system notes lowercase +merge_request: 8807 +author: -- cgit v1.2.1 From b27f66380f36095373fd18ee92d7dce64cf3fdc3 Mon Sep 17 00:00:00 2001 From: Phil Hughes <me@iamphill.com> Date: Mon, 6 Feb 2017 13:55:36 +0000 Subject: Fixed failing tests because list doens't exist --- spec/features/boards/modal_filter_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb index 62b0efdb51c..1cf0d11d448 100644 --- a/spec/features/boards/modal_filter_spec.rb +++ b/spec/features/boards/modal_filter_spec.rb @@ -6,6 +6,8 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do let(:project) { create(:empty_project, :public) } let(:board) { create(:board, project: project) } + let(:planning) { create(:label, project: project, name: 'Planning') } + let!(:list1) { create(:list, board: board, label: planning, position: 0) } let(:user) { create(:user) } let(:user2) { create(:user) } let!(:issue1) { create(:issue, project: project) } -- cgit v1.2.1 From f116f87c91d9d65f570c98071f6aa9506300f766 Mon Sep 17 00:00:00 2001 From: Yorick Peterse <yorickpeterse@gmail.com> Date: Mon, 6 Feb 2017 16:33:49 +0100 Subject: Don't use backup AR connections for Sidekiq Adding two extra connections does nothing other than increasing the number of idle database connections. Given Sidekiq uses N threads it can never use more than N AR connections at a time, thus we don't need more. The initializer mentioned the Sidekiq upgrade guide stating this was required. This is false, the Sidekiq upgrade guide states this is necessary for Redis and not ActiveRecord. On GitLab.com this resulted in a reduction of about 80-100 PostgreSQL connections. Fixes #27713 --- changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml | 4 ++++ config/initializers/sidekiq.rb | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml diff --git a/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml b/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml new file mode 100644 index 00000000000..f42aa6fae79 --- /dev/null +++ b/changelogs/unreleased/remove-sidekiq-backup-ar-threads.yml @@ -0,0 +1,4 @@ +--- +title: Don't use backup Active Record connections for Sidekiq +merge_request: +author: diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index fa318384405..0c4516b70f0 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -36,11 +36,9 @@ Sidekiq.configure_server do |config| Gitlab::SidekiqThrottler.execute! - # Database pool should be at least `sidekiq_concurrency` + 2 - # For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md config = ActiveRecord::Base.configurations[Rails.env] || Rails.application.config.database_configuration[Rails.env] - config['pool'] = Sidekiq.options[:concurrency] + 2 + config['pool'] = Sidekiq.options[:concurrency] ActiveRecord::Base.establish_connection(config) Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}") -- cgit v1.2.1 From bec10d5852a8820de4002af25dd7fbb705561718 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin <godfat@godfat.org> Date: Tue, 7 Feb 2017 01:28:58 +0800 Subject: No strong reasons to freeze them Feedback: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8620#note_22868713 --- lib/gitlab/incoming_email.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/incoming_email.rb b/lib/gitlab/incoming_email.rb index a492f904303..c9122a23568 100644 --- a/lib/gitlab/incoming_email.rb +++ b/lib/gitlab/incoming_email.rb @@ -35,15 +35,14 @@ module Gitlab end def key_from_fallback_message_id(mail_id) - message_id_regexp = - /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/.freeze + message_id_regexp = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\z/ mail_id[message_id_regexp, 1] end def scan_fallback_references(references) # It's looking for each <...> - references.scan(/(?!<)[^<>]+(?=>)/.freeze) + references.scan(/(?!<)[^<>]+(?=>)/) end def config -- cgit v1.2.1 From 54ff60f7064b4287ce3727c9a6f373a77b03ef5f Mon Sep 17 00:00:00 2001 From: Timothy Andrew <mail@timothyandrew.net> Date: Mon, 6 Feb 2017 22:28:05 +0530 Subject: Backport UI changes from gitlab-org/gitlab-ee!998 --- app/views/admin/users/_access_levels.html.haml | 22 ++++++++++++++++++++++ app/views/admin/users/_form.html.haml | 23 +---------------------- 2 files changed, 23 insertions(+), 22 deletions(-) create mode 100644 app/views/admin/users/_access_levels.html.haml diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml new file mode 100644 index 00000000000..81337db3aae --- /dev/null +++ b/app/views/admin/users/_access_levels.html.haml @@ -0,0 +1,22 @@ +%fieldset + %legend Access + .form-group + = f.label :projects_limit, class: 'control-label' + .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control' + + .form-group + = f.label :can_create_group, class: 'control-label' + .col-sm-10= f.check_box :can_create_group + + .form-group + = f.label :admin, class: 'control-label' + - if current_user == @user + .col-sm-10= f.check_box :admin, disabled: true + .col-sm-10 You cannot remove your own admin rights. + - else + .col-sm-10= f.check_box :admin + + .form-group + = f.label :external, class: 'control-label' + .col-sm-10= f.check_box :external + .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 3145212728f..e911af3f6f9 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -40,28 +40,7 @@ = f.label :password_confirmation, class: 'control-label' .col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control' - %fieldset - %legend Access - .form-group - = f.label :projects_limit, class: 'control-label' - .col-sm-10= f.number_field :projects_limit, min: 0, class: 'form-control' - - .form-group - = f.label :can_create_group, class: 'control-label' - .col-sm-10= f.check_box :can_create_group - - .form-group - = f.label :admin, class: 'control-label' - - if current_user == @user - .col-sm-10= f.check_box :admin, disabled: true - .col-sm-10 You cannot remove your own admin rights. - - else - .col-sm-10= f.check_box :admin - - .form-group - = f.label :external, class: 'control-label' - .col-sm-10= f.check_box :external - .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. + = render partial: 'access_levels', locals: { f: f } %fieldset %legend Profile -- cgit v1.2.1 From 5c2ffef76639c14cd6cfc7d0d5c4e97c6c0d3bbb Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray <annabel.dunstone@gmail.com> Date: Mon, 6 Feb 2017 12:17:44 -0600 Subject: Remove lighter colored commit description border --- app/assets/stylesheets/pages/commits.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss index fef8e8eec27..c3d45d708c1 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -159,7 +159,6 @@ .commit-row-description { font-size: 14px; - border-left: 1px solid $white-normal; padding: 10px 15px; margin: 10px 0; background: $gray-light; -- cgit v1.2.1 From 08d4f0d654c757a4dc3018bf05959295beac882c Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Wed, 18 Jan 2017 17:13:28 -0600 Subject: Don't delete assigned MRs/issues when user is deleted --- app/models/user.rb | 5 +++-- changelogs/unreleased/dont-delete-assigned-issuables.yml | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelogs/unreleased/dont-delete-assigned-issuables.yml diff --git a/app/models/user.rb b/app/models/user.rb index 54f5388eb2c..6c98224de35 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -83,8 +83,6 @@ class User < ActiveRecord::Base has_many :events, dependent: :destroy, foreign_key: :author_id has_many :subscriptions, dependent: :destroy has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event" - has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue" - has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest" has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy has_one :abuse_report, dependent: :destroy has_many :spam_logs, dependent: :destroy @@ -94,6 +92,9 @@ class User < ActiveRecord::Base has_many :notification_settings, dependent: :destroy has_many :award_emoji, dependent: :destroy + has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue" + has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" + # # Validations # diff --git a/changelogs/unreleased/dont-delete-assigned-issuables.yml b/changelogs/unreleased/dont-delete-assigned-issuables.yml new file mode 100644 index 00000000000..fb589a053c0 --- /dev/null +++ b/changelogs/unreleased/dont-delete-assigned-issuables.yml @@ -0,0 +1,4 @@ +--- +title: Don't delete assigned MRs/issues when user is deleted +merge_request: +author: -- cgit v1.2.1 From 937cb72763a7bf088b6cbf9657813f4b1800f3c0 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Mon, 6 Feb 2017 13:45:33 -0600 Subject: Update spec --- spec/models/user_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6c4c82cef53..2d09d7c7fed 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -23,9 +23,9 @@ describe User, models: true do it { is_expected.to have_many(:recent_events).class_name('Event') } it { is_expected.to have_many(:issues).dependent(:destroy) } it { is_expected.to have_many(:notes).dependent(:destroy) } - it { is_expected.to have_many(:assigned_issues).dependent(:destroy) } + it { is_expected.to have_many(:assigned_issues).dependent(:nullify) } it { is_expected.to have_many(:merge_requests).dependent(:destroy) } - it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) } + it { is_expected.to have_many(:assigned_merge_requests).dependent(:nullify) } it { is_expected.to have_many(:identities).dependent(:destroy) } it { is_expected.to have_one(:abuse_report) } it { is_expected.to have_many(:spam_logs).dependent(:destroy) } -- cgit v1.2.1 From 426680def4bdeb7c6b37d8a0538fc73c39942495 Mon Sep 17 00:00:00 2001 From: Jose Ivan Vargas <jvargas@gitlab.com> Date: Mon, 6 Feb 2017 15:38:08 -0600 Subject: Backport of the frontend view, including tests --- app/controllers/admin/users_controller.rb | 2 +- app/models/user.rb | 15 +++++++++++++++ app/views/admin/users/_access_levels.html.haml | 18 ++++++++++++------ spec/features/admin/admin_users_spec.rb | 2 +- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index aa0f8d434dc..1cd50852e89 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -175,7 +175,7 @@ class Admin::UsersController < Admin::ApplicationController def user_params_ce [ - :admin, + :access_level, :avatar, :bio, :can_create_group, diff --git a/app/models/user.rb b/app/models/user.rb index 54f5388eb2c..79c83f7bcf4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -903,6 +903,21 @@ class User < ActiveRecord::Base end end + def access_level + if admin? + :admin + else + :regular + end + end + + def access_level=(new_level) + new_level = new_level.to_s + return unless %w(admin regular).include?(new_level) + + self.admin = (new_level == 'admin') + end + private def ci_projects_union diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 81337db3aae..18fdb1435a9 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -9,12 +9,18 @@ .col-sm-10= f.check_box :can_create_group .form-group - = f.label :admin, class: 'control-label' - - if current_user == @user - .col-sm-10= f.check_box :admin, disabled: true - .col-sm-10 You cannot remove your own admin rights. - - else - .col-sm-10= f.check_box :admin + = f.label :access_level, class: 'control-label' + .col-sm-10 + = f.radio_button :access_level, :regular, disabled: (current_user == @user && @user.is_admin?) + = label_tag :regular do + Regular + %p.light + Regular users have access to their groups and projects + = f.radio_button :access_level, :admin + = label_tag :admin do + Admin + %p.light + Administrators have access to all groups, projects and users and can manage all features in this installation .form-group = f.label :external, class: 'control-label' diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index a586f8d3184..c0807b8c507 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -211,7 +211,7 @@ describe "Admin::Users", feature: true do fill_in "user_email", with: "bigbang@mail.com" fill_in "user_password", with: "AValidPassword1" fill_in "user_password_confirmation", with: "AValidPassword1" - check "user_admin" + choose "user_access_level_admin" click_button "Save changes" end -- cgit v1.2.1 From e369323d2e922d695f303378b4a9fbc88c6bee0b Mon Sep 17 00:00:00 2001 From: Clement Ho <ClemMakesApps@gmail.com> Date: Mon, 30 Jan 2017 15:02:19 -0600 Subject: Fix filtered search user autocomplete for gitlab instances that are hosted on a subdirectory --- .../filtered_search/dropdown_user.js.es6 | 2 +- ...-post-to-wrong-url-when-not-hosting-in-root.yml | 5 ++++ .../filtered_search/dropdown_user_spec.js.es6 | 35 ++++++++++++++++++++++ spec/javascripts/project_title_spec.js | 15 ++++++---- spec/javascripts/search_autocomplete_spec.js | 15 ++++++---- 5 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 index f93605a5a21..7e9c6f74aa5 100644 --- a/app/assets/javascripts/filtered_search/dropdown_user.js.es6 +++ b/app/assets/javascripts/filtered_search/dropdown_user.js.es6 @@ -8,7 +8,7 @@ require('./filtered_search_dropdown'); super(droplab, dropdown, input, filter); this.config = { droplabAjaxFilter: { - endpoint: '/autocomplete/users.json', + endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, searchKey: 'search', params: { per_page: 20, diff --git a/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml b/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml new file mode 100644 index 00000000000..8f061a34ac0 --- /dev/null +++ b/changelogs/unreleased/27343-autocomplete-post-to-wrong-url-when-not-hosting-in-root.yml @@ -0,0 +1,5 @@ +--- +title: Fix filtered search user autocomplete for gitlab instances that are hosted + on a subdirectory +merge_request: 8891 +author: diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 index 10a316f31b4..f4b0d60db34 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js.es6 @@ -36,5 +36,40 @@ require('~/filtered_search/dropdown_user'); expect(dropdownUser.getSearchInput()).toBe('larry boy'); }); }); + + describe('config droplabAjaxFilter\'s endpoint', () => { + beforeEach(() => { + spyOn(gl.FilteredSearchDropdown.prototype, 'constructor').and.callFake(() => {}); + spyOn(gl.DropdownUser.prototype, 'getProjectId').and.callFake(() => {}); + }); + + it('should return endpoint', () => { + window.gon = { + relative_url_root: '', + }; + const dropdown = new gl.DropdownUser(); + + expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json'); + }); + + it('should return endpoint when relative_url_root is undefined', () => { + const dropdown = new gl.DropdownUser(); + + expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/autocomplete/users.json'); + }); + + it('should return endpoint with relative url when available', () => { + window.gon = { + relative_url_root: '/gitlab_directory', + }; + const dropdown = new gl.DropdownUser(); + + expect(dropdown.config.droplabAjaxFilter.endpoint).toBe('/gitlab_directory/autocomplete/users.json'); + }); + + afterEach(() => { + window.gon = {}; + }); + }); }); })(); diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index e0b52f767e4..bfe3d2df79d 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -9,19 +9,20 @@ require('~/project_select'); require('~/project'); (function() { - window.gon || (window.gon = {}); - - window.gon.api_version = 'v3'; - describe('Project Title', function() { preloadFixtures('static/project_title.html.raw'); loadJSONFixtures('projects.json'); beforeEach(function() { loadFixtures('static/project_title.html.raw'); + + window.gon = {}; + window.gon.api_version = 'v3'; + return this.project = new Project(); }); - return describe('project list', function() { + + describe('project list', function() { var fakeAjaxResponse = function fakeAjaxResponse(req) { var d; expect(req.url).toBe('/api/v3/projects.json?simple=true'); @@ -48,5 +49,9 @@ require('~/project'); return expect($('.header-content').hasClass('open')).toBe(false); }); }); + + afterEach(() => { + window.gon = {}; + }); }); }).call(this); diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index c79e30e9481..9572b52ec1e 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -14,11 +14,6 @@ require('vendor/fuzzaldrin-plus'); userId = 1; - window.gon || (window.gon = {}); - - window.gon.current_user_id = userId; - window.gon.current_username = userName; - dashboardIssuesPath = '/dashboard/issues'; dashboardMRsPath = '/dashboard/merge_requests'; @@ -117,6 +112,16 @@ require('vendor/fuzzaldrin-plus'); widget = new gl.SearchAutocomplete; // Prevent turbolinks from triggering within gl_dropdown spyOn(window.gl.utils, 'visitUrl').and.returnValue(true); + + window.gon = {}; + window.gon.current_user_id = userId; + window.gon.current_username = userName; + + return widget = new gl.SearchAutocomplete; + }); + + afterEach(function() { + window.gon = {}; }); it('should show Dashboard specific dropdown menu', function() { var list; -- cgit v1.2.1 From 46dff6910d2f618222e4213dca55ba68b5b66984 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Mon, 6 Feb 2017 17:19:37 -0600 Subject: More backport --- .flayignore | 1 + app/views/admin/users/_access_levels.html.haml | 17 ++++- spec/models/user_spec.rb | 33 ++++++++ spec/policies/project_policy_spec.rb | 62 ++++++++------- spec/policies/project_snippet_policy_spec.rb | 101 +++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 36 deletions(-) create mode 100644 spec/policies/project_snippet_policy_spec.rb diff --git a/.flayignore b/.flayignore index 44df2ba2371..fc64b0b5892 100644 --- a/.flayignore +++ b/.flayignore @@ -1,3 +1,4 @@ *.erb lib/gitlab/sanitizers/svg/whitelist.rb lib/gitlab/diff/position_tracer.rb +app/policies/project_policy.rb diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml index 18fdb1435a9..7855239dfe5 100644 --- a/app/views/admin/users/_access_levels.html.haml +++ b/app/views/admin/users/_access_levels.html.haml @@ -11,18 +11,27 @@ .form-group = f.label :access_level, class: 'control-label' .col-sm-10 - = f.radio_button :access_level, :regular, disabled: (current_user == @user && @user.is_admin?) + - editing_current_user = (current_user == @user) + + = f.radio_button :access_level, :regular, disabled: editing_current_user = label_tag :regular do Regular %p.light Regular users have access to their groups and projects - = f.radio_button :access_level, :admin + + = f.radio_button :access_level, :admin, disabled: editing_current_user = label_tag :admin do Admin %p.light Administrators have access to all groups, projects and users and can manage all features in this installation + - if editing_current_user + %p.light + You cannot remove your own admin rights. .form-group = f.label :external, class: 'control-label' - .col-sm-10= f.check_box :external - .col-sm-10 External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. + .col-sm-10 + = f.check_box :external do + External + %p.light + External users cannot see internal or private projects unless access is explicitly granted. Also, external users cannot create projects or groups. diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 6d58b1455c4..c7c3bfdd0e7 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1422,4 +1422,37 @@ describe User, models: true do expect(user.project_authorizations.where(access_level: Gitlab::Access::REPORTER).exists?).to eq(true) end end + + describe '#access_level=' do + let(:user) { build(:user) } + + it 'does nothing for an invalid access level' do + user.access_level = :invalid_access_level + + expect(user.access_level).to eq(:regular) + expect(user.admin).to be false + end + + it "assigns the 'admin' access level" do + user.access_level = :admin + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + end + + it "doesn't clear existing access levels when an invalid access level is passed in" do + user.access_level = :admin + user.access_level = :invalid_access_level + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + end + + it "accepts string values in addition to symbols" do + user.access_level = 'admin' + + expect(user.access_level).to eq(:admin) + expect(user.admin).to be true + end + end end diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index eeab9827d99..0a5edf35f59 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -10,61 +10,59 @@ describe ProjectPolicy, models: true do let(:project) { create(:empty_project, :public, namespace: owner.namespace) } let(:guest_permissions) do - [ - :read_project, :read_board, :read_list, :read_wiki, :read_issue, :read_label, - :read_milestone, :read_project_snippet, :read_project_member, - :read_note, :create_project, :create_issue, :create_note, - :upload_file + %i[ + read_project read_board read_list read_wiki read_issue read_label + read_milestone read_project_snippet read_project_member + read_note create_project create_issue create_note + upload_file ] end let(:reporter_permissions) do - [ - :download_code, :fork_project, :create_project_snippet, :update_issue, - :admin_issue, :admin_label, :admin_list, :read_commit_status, :read_build, - :read_container_image, :read_pipeline, :read_environment, :read_deployment, - :read_merge_request, :download_wiki_code + %i[ + download_code fork_project create_project_snippet update_issue + admin_issue admin_label admin_list read_commit_status read_build + read_container_image read_pipeline read_environment read_deployment + read_merge_request download_wiki_code ] end let(:team_member_reporter_permissions) do - [ - :build_download_code, :build_read_container_image - ] + %i[build_download_code build_read_container_image] end let(:developer_permissions) do - [ - :admin_merge_request, :update_merge_request, :create_commit_status, - :update_commit_status, :create_build, :update_build, :create_pipeline, - :update_pipeline, :create_merge_request, :create_wiki, :push_code, - :resolve_note, :create_container_image, :update_container_image, - :create_environment, :create_deployment + %i[ + admin_merge_request update_merge_request create_commit_status + update_commit_status create_build update_build create_pipeline + update_pipeline create_merge_request create_wiki push_code + resolve_note create_container_image update_container_image + create_environment create_deployment ] end let(:master_permissions) do - [ - :push_code_to_protected_branches, :update_project_snippet, :update_environment, - :update_deployment, :admin_milestone, :admin_project_snippet, - :admin_project_member, :admin_note, :admin_wiki, :admin_project, - :admin_commit_status, :admin_build, :admin_container_image, - :admin_pipeline, :admin_environment, :admin_deployment + %i[ + push_code_to_protected_branches update_project_snippet update_environment + update_deployment admin_milestone admin_project_snippet + admin_project_member admin_note admin_wiki admin_project + admin_commit_status admin_build admin_container_image + admin_pipeline admin_environment admin_deployment ] end let(:public_permissions) do - [ - :download_code, :fork_project, :read_commit_status, :read_pipeline, - :read_container_image, :build_download_code, :build_read_container_image, - :download_wiki_code + %i[ + download_code fork_project read_commit_status read_pipeline + read_container_image build_download_code build_read_container_image + download_wiki_code ] end let(:owner_permissions) do - [ - :change_namespace, :change_visibility_level, :rename_project, :remove_project, - :archive_project, :remove_fork_project, :destroy_merge_request, :destroy_issue + %i[ + change_namespace change_visibility_level rename_project remove_project + archive_project remove_fork_project destroy_merge_request destroy_issue ] end diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb new file mode 100644 index 00000000000..d0758af57dd --- /dev/null +++ b/spec/policies/project_snippet_policy_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe ProjectSnippetPolicy, models: true do + let(:current_user) { create(:user) } + + let(:author_permissions) do + [ + :update_project_snippet, + :admin_project_snippet + ] + end + + subject { described_class.abilities(current_user, project_snippet).to_set } + + context 'public snippet' do + let(:project_snippet) { create(:project_snippet, :public) } + + context 'no user' do + let(:current_user) { nil } + + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'regular user' do + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + end + + context 'internal snippet' do + let(:project_snippet) { create(:project_snippet, :internal) } + + context 'no user' do + let(:current_user) { nil } + + it do + is_expected.not_to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'regular user' do + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + end + + context 'private snippet' do + let(:project_snippet) { create(:project_snippet, :private) } + + context 'no user' do + let(:current_user) { nil } + + it do + is_expected.not_to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'regular user' do + it do + is_expected.not_to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'snippet author' do + let(:project_snippet) { create(:project_snippet, :private, author: current_user) } + + it do + is_expected.to include(:read_project_snippet) + is_expected.to include(*author_permissions) + end + end + + context 'project team member' do + before { project_snippet.project.team << [current_user, :developer] } + + it do + is_expected.to include(:read_project_snippet) + is_expected.not_to include(*author_permissions) + end + end + + context 'admin user' do + let(:current_user) { create(:admin) } + + it do + is_expected.to include(:read_project_snippet) + is_expected.to include(*author_permissions) + end + end + end +end -- cgit v1.2.1 From 87cdb156ce51a1300f833c633b802b9be5c980d0 Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Mon, 6 Feb 2017 18:25:24 -0600 Subject: Fix indentation --- app/models/user.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 79c83f7bcf4..7023a7956d1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -912,11 +912,11 @@ class User < ActiveRecord::Base end def access_level=(new_level) - new_level = new_level.to_s - return unless %w(admin regular).include?(new_level) + new_level = new_level.to_s + return unless %w(admin regular).include?(new_level) - self.admin = (new_level == 'admin') - end + self.admin = (new_level == 'admin') + end private -- cgit v1.2.1 From ffcbc636930ab4d844da7677ab8439d0ede5f12f Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Mon, 6 Feb 2017 20:32:34 -0600 Subject: List all groups/projects for admins on explore pages --- app/finders/group_projects_finder.rb | 2 +- lib/gitlab/visibility_level.rb | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index aa8f4c1d0e4..3b9a421b118 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -18,7 +18,7 @@ class GroupProjectsFinder < UnionFinder projects = [] if current_user - if @group.users.include?(current_user) || current_user.admin? + if @group.users.include?(current_user) projects << @group.projects unless only_shared projects << @group.shared_projects unless only_owned else diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb index c7953af29dd..a4e966e4016 100644 --- a/lib/gitlab/visibility_level.rb +++ b/lib/gitlab/visibility_level.rb @@ -13,7 +13,19 @@ module Gitlab scope :public_and_internal_only, -> { where(visibility_level: [PUBLIC, INTERNAL] ) } scope :non_public_only, -> { where.not(visibility_level: PUBLIC) } - scope :public_to_user, -> (user) { user && !user.external ? public_and_internal_only : public_only } + scope :public_to_user, -> (user) do + if user + if user.admin? + all + elsif !user.external? + public_and_internal_only + else + public_only + end + else + public_only + end + end end PRIVATE = 0 unless const_defined?(:PRIVATE) -- cgit v1.2.1 From f5a798c7434bf236f36b399347c49fa3edf1f04e Mon Sep 17 00:00:00 2001 From: Douwe Maan <douwe@selenight.nl> Date: Mon, 6 Feb 2017 22:08:51 -0600 Subject: Use random group name to prevent conflicts --- spec/services/groups/update_service_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 531180e48a1..7c0fccb9d41 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -51,7 +51,7 @@ describe Groups::UpdateService, services: true do end context 'rename group' do - let!(:service) { described_class.new(internal_group, user, path: 'new_path') } + let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) } before do internal_group.add_user(user, Gitlab::Access::MASTER) -- cgit v1.2.1