Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce
This commit is contained in:
commit
341541d3af
12
CHANGELOG
12
CHANGELOG
|
@ -3,25 +3,30 @@ Please view this file on the master branch, on stable branches it's out of date.
|
|||
v 8.12.0 (unreleased)
|
||||
- Make push events have equal vertical spacing.
|
||||
- Add two-factor recovery endpoint to internal API !5510
|
||||
- Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
|
||||
- Add font color contrast to external label in admin area (ClemMakesApps)
|
||||
- Change logo animation to CSS (ClemMakesApps)
|
||||
- Change merge_error column from string to text type
|
||||
- Reduce contributions calendar data payload (ClemMakesApps)
|
||||
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
|
||||
- Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
|
||||
- Shorten task status phrase (ClemMakesApps)
|
||||
- Add hover color to emoji icon (ClemMakesApps)
|
||||
- Fix branches page dropdown sort alignment (ClemMakesApps)
|
||||
- Add white background for no readme container (ClemMakesApps)
|
||||
- Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
|
||||
- Add `wiki_page_events` to project hook APIs (Ben Boeckel)
|
||||
- Remove Gitorious import
|
||||
- Fix inconsistent background color for filter input field (ClemMakesApps)
|
||||
- Add Sentry logging to API calls
|
||||
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
|
||||
- Remove unused mixins (ClemMakesApps)
|
||||
- Fix groups sort dropdown alignment (ClemMakesApps)
|
||||
- Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps)
|
||||
- Fix markdown help references (ClemMakesApps)
|
||||
- Add last commit time to repo view (ClemMakesApps)
|
||||
- Added tests for diff notes
|
||||
- Add a button to download latest successful artifacts for branches and tags !5142
|
||||
- Add delimiter to project stars and forks count (ClemMakesApps)
|
||||
- Fix badge count alignment (ClemMakesApps)
|
||||
- Fix branch title trailing space on hover (ClemMakesApps)
|
||||
|
@ -41,17 +46,24 @@ v 8.12.0 (unreleased)
|
|||
- Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger)
|
||||
- Adds response mime type to transaction metric action when it's not HTML
|
||||
- Fix hover leading space bug in pipeline graph !5980
|
||||
- User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496
|
||||
|
||||
v 8.11.4 (unreleased)
|
||||
- Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner)
|
||||
- Creating an issue through our API now emails label subscribers !5720
|
||||
- Fix resolving conflicts on forks
|
||||
- Fix diff commenting on merge requests created prior to 8.10
|
||||
|
||||
v 8.11.4 (unreleased)
|
||||
- Fix issue boards leak private label names and descriptions
|
||||
|
||||
v 8.11.3 (unreleased)
|
||||
v 8.11.3
|
||||
- Do not enforce using hash with hidden key in CI configuration. !6079
|
||||
- Allow system info page to handle case where info is unavailable
|
||||
- Label list shows all issues (opened or closed) with that label
|
||||
- Don't show resolve conflicts link before MR status is updated
|
||||
- Fix "Wiki" link not appearing in navigation for projects with external wiki
|
||||
- Fix IE11 fork button bug !598
|
||||
- Don't prevent viewing the MR when git refs for conflicts can't be found on disk
|
||||
- Fix external issue tracker "Issues" link leading to 404s
|
||||
|
|
|
@ -195,6 +195,12 @@
|
|||
.separator + .dropdown-header {
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.unclickable {
|
||||
cursor: not-allowed;
|
||||
padding: 5px 8px;
|
||||
color: $dropdown-header-color;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-large {
|
||||
|
|
|
@ -8,10 +8,7 @@
|
|||
height: 30px;
|
||||
transition-duration: .3s;
|
||||
-webkit-transform: translateZ(0);
|
||||
background: -webkit-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%);
|
||||
background: -o-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%);
|
||||
background: -moz-linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%);
|
||||
background: linear-gradient($gradient-direction, rgba($gradient-color, 0.4), $gradient-color 45%);
|
||||
background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4));
|
||||
|
||||
&.scrolling {
|
||||
visibility: visible;
|
||||
|
@ -211,12 +208,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.project-filter-form {
|
||||
input {
|
||||
background-color: $background-color;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
padding-bottom: 0;
|
||||
width: 100%;
|
||||
|
|
|
@ -43,6 +43,15 @@
|
|||
border-color: $blue-normal;
|
||||
}
|
||||
|
||||
&.ci-created {
|
||||
color: $table-text-gray;
|
||||
border-color: $table-text-gray;
|
||||
|
||||
svg {
|
||||
fill: $table-text-gray;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 13px;
|
||||
width: 13px;
|
||||
|
|
|
@ -11,6 +11,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.last-commit {
|
||||
max-width: 506px;
|
||||
|
||||
.last-commit-content {
|
||||
@include str-truncated;
|
||||
width: calc(100% - 140px);
|
||||
margin-left: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-table {
|
||||
margin-bottom: 0;
|
||||
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
class Projects::ArtifactsController < Projects::ApplicationController
|
||||
include ExtractsPath
|
||||
|
||||
layout 'project'
|
||||
before_action :authorize_read_build!
|
||||
before_action :authorize_update_build!, only: [:keep]
|
||||
before_action :extract_ref_name_and_path
|
||||
before_action :validate_artifacts!
|
||||
|
||||
def download
|
||||
unless artifacts_file.file_storage?
|
||||
return redirect_to artifacts_file.url
|
||||
if artifacts_file.file_storage?
|
||||
send_file artifacts_file.path, disposition: 'attachment'
|
||||
else
|
||||
redirect_to artifacts_file.url
|
||||
end
|
||||
|
||||
send_file artifacts_file.path, disposition: 'attachment'
|
||||
end
|
||||
|
||||
def browse
|
||||
directory = params[:path] ? "#{params[:path]}/" : ''
|
||||
@entry = build.artifacts_metadata_entry(directory)
|
||||
|
||||
return render_404 unless @entry.exists?
|
||||
render_404 unless @entry.exists?
|
||||
end
|
||||
|
||||
def file
|
||||
|
@ -34,14 +37,41 @@ class Projects::ArtifactsController < Projects::ApplicationController
|
|||
redirect_to namespace_project_build_path(project.namespace, project, build)
|
||||
end
|
||||
|
||||
def latest_succeeded
|
||||
target_path = artifacts_action_path(@path, project, build)
|
||||
|
||||
if target_path
|
||||
redirect_to(target_path)
|
||||
else
|
||||
render_404
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def extract_ref_name_and_path
|
||||
return unless params[:ref_name_and_path]
|
||||
|
||||
@ref_name, @path = extract_ref(params[:ref_name_and_path])
|
||||
end
|
||||
|
||||
def validate_artifacts!
|
||||
render_404 unless build.artifacts?
|
||||
render_404 unless build && build.artifacts?
|
||||
end
|
||||
|
||||
def build
|
||||
@build ||= project.builds.find_by!(id: params[:build_id])
|
||||
@build ||= build_from_id || build_from_ref
|
||||
end
|
||||
|
||||
def build_from_id
|
||||
project.builds.find_by(id: params[:build_id]) if params[:build_id]
|
||||
end
|
||||
|
||||
def build_from_ref
|
||||
return unless @ref_name
|
||||
|
||||
builds = project.latest_successful_builds_for(@ref_name)
|
||||
builds.find_by(name: params[:job])
|
||||
end
|
||||
|
||||
def artifacts_file
|
||||
|
|
|
@ -25,6 +25,11 @@ module CiStatusHelper
|
|||
end
|
||||
end
|
||||
|
||||
def ci_status_for_statuseable(subject)
|
||||
status = subject.try(:status) || 'not found'
|
||||
status.humanize
|
||||
end
|
||||
|
||||
def ci_icon_for_status(status)
|
||||
icon_name =
|
||||
case status
|
||||
|
@ -41,7 +46,7 @@ module CiStatusHelper
|
|||
when 'play'
|
||||
'icon_play'
|
||||
when 'created'
|
||||
'icon_status_pending'
|
||||
'icon_status_created'
|
||||
else
|
||||
'icon_status_cancel'
|
||||
end
|
||||
|
|
|
@ -149,4 +149,20 @@ module GitlabRoutingHelper
|
|||
def resend_invite_group_member_path(group_member, *args)
|
||||
resend_invite_group_group_member_path(group_member.source, group_member)
|
||||
end
|
||||
|
||||
# Artifacts
|
||||
|
||||
def artifacts_action_path(path, project, build)
|
||||
action, path_params = path.split('/', 2)
|
||||
args = [project.namespace, project, build, path_params]
|
||||
|
||||
case action
|
||||
when 'download'
|
||||
download_namespace_project_build_artifacts_path(*args)
|
||||
when 'browse'
|
||||
browse_namespace_project_build_artifacts_path(*args)
|
||||
when 'file'
|
||||
file_namespace_project_build_artifacts_path(*args)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -98,6 +98,6 @@ module MergeRequestsHelper
|
|||
end
|
||||
|
||||
def merge_request_button_visibility(merge_request, closed)
|
||||
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?)
|
||||
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
|
||||
end
|
||||
end
|
||||
|
|
|
@ -355,7 +355,7 @@ class Ability
|
|||
rules += named_abilities('project_snippet')
|
||||
end
|
||||
|
||||
unless project.wiki_enabled
|
||||
unless project.has_wiki?
|
||||
rules += named_abilities('wiki')
|
||||
end
|
||||
|
||||
|
|
|
@ -65,8 +65,8 @@ module Ci
|
|||
end
|
||||
|
||||
# ref can't be HEAD or SHA, can only be branch/tag name
|
||||
scope :latest_successful_for, ->(ref = default_branch) do
|
||||
where(ref: ref).success.order(id: :desc).limit(1)
|
||||
def self.latest_successful_for(ref)
|
||||
where(ref: ref).order(id: :desc).success.first
|
||||
end
|
||||
|
||||
def self.truncate_sha(sha)
|
||||
|
|
|
@ -28,4 +28,8 @@ module NoteOnDiff
|
|||
def can_be_award_emoji?
|
||||
false
|
||||
end
|
||||
|
||||
def to_discussion
|
||||
Discussion.new([self])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -52,11 +52,11 @@ module Taskable
|
|||
end
|
||||
|
||||
# Return a string that describes the current state of this Taskable's task
|
||||
# list items, e.g. "20 tasks (12 completed, 8 remaining)"
|
||||
# list items, e.g. "12 of 20 tasks completed"
|
||||
def task_status
|
||||
return '' if description.blank?
|
||||
|
||||
sum = tasks.summary
|
||||
"#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)"
|
||||
"#{sum.complete_count} of #{sum.item_count} #{'task'.pluralize(sum.item_count)} completed"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -107,10 +107,6 @@ class DiffNote < Note
|
|||
self.noteable.find_diff_discussion(self.discussion_id)
|
||||
end
|
||||
|
||||
def to_discussion
|
||||
Discussion.new([self])
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def supported?
|
||||
|
|
|
@ -91,13 +91,13 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
validates :source_project, presence: true, unless: [:allow_broken, :importing?]
|
||||
validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
|
||||
validates :source_branch, presence: true
|
||||
validates :target_project, presence: true
|
||||
validates :target_branch, presence: true
|
||||
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
|
||||
validate :validate_branches, unless: [:allow_broken, :importing?]
|
||||
validate :validate_fork
|
||||
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
|
||||
validate :validate_fork, unless: :closed_without_fork?
|
||||
|
||||
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
|
||||
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
|
||||
|
@ -305,19 +305,22 @@ class MergeRequest < ActiveRecord::Base
|
|||
|
||||
def validate_fork
|
||||
return true unless target_project && source_project
|
||||
return true if target_project == source_project
|
||||
return true unless forked_source_project_missing?
|
||||
|
||||
if target_project == source_project
|
||||
true
|
||||
else
|
||||
# If source and target projects are different
|
||||
# we should check if source project is actually a fork of target project
|
||||
if source_project.forked_from?(target_project)
|
||||
true
|
||||
else
|
||||
errors.add :validate_fork,
|
||||
'Source project is not a fork of target project'
|
||||
end
|
||||
end
|
||||
errors.add :validate_fork,
|
||||
'Source project is not a fork of the target project'
|
||||
end
|
||||
|
||||
def closed_without_fork?
|
||||
closed? && forked_source_project_missing?
|
||||
end
|
||||
|
||||
def forked_source_project_missing?
|
||||
return false unless for_fork?
|
||||
return true unless source_project
|
||||
|
||||
!source_project.forked_from?(target_project)
|
||||
end
|
||||
|
||||
def ensure_merge_request_diff
|
||||
|
@ -726,7 +729,9 @@ class MergeRequest < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def pipeline
|
||||
@pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project
|
||||
return unless diff_head_sha && source_project
|
||||
|
||||
@pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
|
||||
end
|
||||
|
||||
def all_pipelines
|
||||
|
|
|
@ -436,7 +436,7 @@ class Project < ActiveRecord::Base
|
|||
|
||||
# ref can't be HEAD, can only be branch/tag name or SHA
|
||||
def latest_successful_builds_for(ref = default_branch)
|
||||
latest_pipeline = pipelines.latest_successful_for(ref).first
|
||||
latest_pipeline = pipelines.latest_successful_for(ref)
|
||||
|
||||
if latest_pipeline
|
||||
latest_pipeline.builds.latest.with_artifacts
|
||||
|
@ -680,6 +680,10 @@ class Project < ActiveRecord::Base
|
|||
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
|
||||
end
|
||||
|
||||
def has_wiki?
|
||||
wiki_enabled? || has_external_wiki?
|
||||
end
|
||||
|
||||
def external_wiki
|
||||
if has_external_wiki.nil?
|
||||
cache_has_external_wiki # Populate
|
||||
|
@ -1096,12 +1100,17 @@ class Project < ActiveRecord::Base
|
|||
!namespace.share_with_group_lock
|
||||
end
|
||||
|
||||
def pipeline(sha, ref)
|
||||
def pipeline_for(ref, sha = nil)
|
||||
sha ||= commit(ref).try(:sha)
|
||||
|
||||
return unless sha
|
||||
|
||||
pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
|
||||
end
|
||||
|
||||
def ensure_pipeline(sha, ref, current_user = nil)
|
||||
pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user)
|
||||
def ensure_pipeline(ref, sha, current_user = nil)
|
||||
pipeline_for(ref, sha) ||
|
||||
pipelines.create(sha: sha, ref: ref, user: current_user)
|
||||
end
|
||||
|
||||
def enable_ci
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
module Ci
|
||||
class WebHookService
|
||||
def build_end(build)
|
||||
execute_hooks(build.project, build_data(build))
|
||||
end
|
||||
|
||||
def execute_hooks(project, data)
|
||||
project.web_hooks.each do |web_hook|
|
||||
async_execute_hook(web_hook, data)
|
||||
end
|
||||
end
|
||||
|
||||
def async_execute_hook(hook, data)
|
||||
Sidekiq::Client.enqueue(Ci::WebHookWorker, hook.id, data)
|
||||
end
|
||||
|
||||
def build_data(build)
|
||||
project = build.project
|
||||
data = {}
|
||||
data.merge!({
|
||||
build_id: build.id,
|
||||
build_name: build.name,
|
||||
build_status: build.status,
|
||||
build_started_at: build.started_at,
|
||||
build_finished_at: build.finished_at,
|
||||
project_id: project.id,
|
||||
project_name: project.name,
|
||||
gitlab_url: project.gitlab_url,
|
||||
ref: build.ref,
|
||||
before_sha: build.before_sha,
|
||||
sha: build.sha,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
|
@ -45,6 +45,7 @@ class IssuableBaseService < BaseService
|
|||
|
||||
unless can?(current_user, ability, project)
|
||||
params.delete(:milestone_id)
|
||||
params.delete(:labels)
|
||||
params.delete(:add_label_ids)
|
||||
params.delete(:remove_label_ids)
|
||||
params.delete(:label_ids)
|
||||
|
@ -72,6 +73,7 @@ class IssuableBaseService < BaseService
|
|||
filter_labels_in_param(:add_label_ids)
|
||||
filter_labels_in_param(:remove_label_ids)
|
||||
filter_labels_in_param(:label_ids)
|
||||
find_or_create_label_ids
|
||||
end
|
||||
|
||||
def filter_labels_in_param(key)
|
||||
|
@ -80,6 +82,17 @@ class IssuableBaseService < BaseService
|
|||
params[key] = project.labels.where(id: params[key]).pluck(:id)
|
||||
end
|
||||
|
||||
def find_or_create_label_ids
|
||||
labels = params.delete(:labels)
|
||||
return unless labels
|
||||
|
||||
params[:label_ids] = labels.split(",").map do |label_name|
|
||||
project.labels.create_with(color: Label::DEFAULT_COLOR)
|
||||
.find_or_create_by(title: label_name.strip)
|
||||
.id
|
||||
end
|
||||
end
|
||||
|
||||
def process_label_ids(attributes, existing_label_ids: nil)
|
||||
label_ids = attributes.delete(:label_ids)
|
||||
add_label_ids = attributes.delete(:add_label_ids)
|
||||
|
@ -162,7 +175,12 @@ class IssuableBaseService < BaseService
|
|||
|
||||
if params.present? && update_issuable(issuable, params)
|
||||
issuable.reset_events_cache
|
||||
handle_common_system_notes(issuable, old_labels: old_labels)
|
||||
|
||||
# We do not touch as it will affect a update on updated_at field
|
||||
ActiveRecord::Base.no_touching do
|
||||
handle_common_system_notes(issuable, old_labels: old_labels)
|
||||
end
|
||||
|
||||
handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
|
||||
issuable.create_new_cross_references!(current_user)
|
||||
execute_hooks(issuable, 'update')
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
module MergeRequests
|
||||
class ResolveService < MergeRequests::BaseService
|
||||
attr_accessor :conflicts, :rugged, :merge_index
|
||||
attr_accessor :conflicts, :rugged, :merge_index, :merge_request
|
||||
|
||||
def execute(merge_request)
|
||||
@conflicts = merge_request.conflicts
|
||||
@rugged = project.repository.rugged
|
||||
@merge_index = conflicts.merge_index
|
||||
@merge_request = merge_request
|
||||
|
||||
fetch_their_commit!
|
||||
|
||||
conflicts.files.each do |file|
|
||||
write_resolved_file_to_index(file, params[:sections])
|
||||
|
@ -27,5 +30,21 @@ module MergeRequests
|
|||
merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
|
||||
merge_index.conflict_remove(our_path)
|
||||
end
|
||||
|
||||
# If their commit (in the target project) doesn't exist in the source project, it
|
||||
# can't be a parent for the merge commit we're about to create. If that's the case,
|
||||
# fetch the target branch ref into the source project so the commit exists in both.
|
||||
#
|
||||
def fetch_their_commit!
|
||||
return if rugged.include?(conflicts.their_commit.oid)
|
||||
|
||||
random_string = SecureRandom.hex
|
||||
|
||||
project.repository.fetch_ref(
|
||||
merge_request.target_project.repository.path_to_repo,
|
||||
"refs/heads/#{merge_request.target_branch}",
|
||||
"refs/tmp/#{random_string}/head"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,10 @@ module MergeRequests
|
|||
params.except!(:target_project_id)
|
||||
params.except!(:source_branch)
|
||||
|
||||
if merge_request.closed_without_fork?
|
||||
params.except!(:target_branch, :force_remove_source_branch)
|
||||
end
|
||||
|
||||
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
|
||||
|
||||
update(merge_request)
|
||||
|
|
|
@ -27,6 +27,8 @@
|
|||
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do
|
||||
Compare
|
||||
|
||||
= render 'projects/buttons/download', project: @project, ref: branch.name
|
||||
|
||||
- if can_remove_branch?(@project, branch.name)
|
||||
= link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
|
||||
= icon("trash-o")
|
||||
|
|
|
@ -1,4 +1,42 @@
|
|||
- unless @project.empty_repo?
|
||||
- if can? current_user, :download_code, @project
|
||||
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has-tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do
|
||||
= icon('download')
|
||||
- if !project.empty_repo? && can?(current_user, :download_code, project)
|
||||
%span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'}
|
||||
.dropdown.inline
|
||||
%button.btn{ 'data-toggle' => 'dropdown' }
|
||||
= icon('download')
|
||||
%span.caret
|
||||
%span.sr-only
|
||||
Select Archive Format
|
||||
%ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
|
||||
%li.dropdown-header Source code
|
||||
%li
|
||||
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span Download zip
|
||||
%li
|
||||
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span Download tar.gz
|
||||
%li
|
||||
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span Download tar.bz2
|
||||
%li
|
||||
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span Download tar
|
||||
|
||||
- pipeline = project.pipelines.latest_successful_for(ref)
|
||||
- if pipeline
|
||||
- artifacts = pipeline.builds.latest.with_artifacts
|
||||
- if artifacts.any?
|
||||
%li.dropdown-header Artifacts
|
||||
- unless pipeline.latest?
|
||||
- latest_pipeline = project.pipeline_for(ref)
|
||||
%li
|
||||
.unclickable= ci_status_for_statuseable(latest_pipeline)
|
||||
%li.dropdown-header Previous Artifacts
|
||||
- artifacts.each do |job|
|
||||
%li
|
||||
= link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span Download '#{job.name}'
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
- @related_branches.each do |branch|
|
||||
%li
|
||||
- target = @project.repository.find_branch(branch).target
|
||||
- pipeline = @project.pipeline(target.sha, branch) if target
|
||||
- pipeline = @project.pipeline_for(branch, target.sha) if target
|
||||
- if pipeline
|
||||
%span.related-branch-ci-status
|
||||
= render_pipeline_status(pipeline)
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
- if @merge_request.closed_without_fork?
|
||||
.alert.alert-danger
|
||||
%p The source project of this merge request has been removed.
|
||||
|
||||
.clearfix.detail-page-header
|
||||
.issuable-header
|
||||
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
- ref = ref || nil
|
||||
- btn_class = btn_class || ''
|
||||
- split_button = split_button || false
|
||||
- if split_button == true
|
||||
%span.btn-group{class: btn_class}
|
||||
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span Download zip
|
||||
%a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' }
|
||||
%span.caret
|
||||
%span.sr-only
|
||||
Select Archive Format
|
||||
%ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
|
||||
%li
|
||||
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span Download zip
|
||||
%li
|
||||
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span Download tar.gz
|
||||
%li
|
||||
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span Download tar.bz2
|
||||
%li
|
||||
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span Download tar
|
||||
- else
|
||||
%span.btn-group{class: btn_class}
|
||||
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span zip
|
||||
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do
|
||||
%i.fa.fa-download
|
||||
%span tar.gz
|
|
@ -72,7 +72,7 @@
|
|||
= render "projects/buttons/koding"
|
||||
|
||||
.btn-group.project-repo-btn-group
|
||||
= render "projects/buttons/download"
|
||||
= render 'projects/buttons/download', project: @project, ref: @ref
|
||||
= render 'projects/buttons/dropdown'
|
||||
|
||||
= render 'shared/notifications/button', notification_setting: @notification_setting
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
%span.btn-group
|
||||
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do
|
||||
%span Source code
|
||||
%a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' }
|
||||
%span.caret
|
||||
%span.sr-only
|
||||
Select Archive Format
|
||||
%ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
|
||||
%li
|
||||
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
|
||||
%span Download zip
|
||||
%li
|
||||
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
|
||||
%span Download tar.gz
|
|
@ -11,8 +11,7 @@
|
|||
= strip_gpg_signature(tag.message)
|
||||
|
||||
.controls
|
||||
- if can?(current_user, :download_code, @project)
|
||||
= render 'projects/tags/download', ref: tag.name, project: @project
|
||||
= render 'projects/buttons/download', project: @project, ref: tag.name
|
||||
|
||||
- if can?(current_user, :push_code, @project)
|
||||
= link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
|
||||
|
|
|
@ -12,8 +12,7 @@
|
|||
= icon('files-o')
|
||||
= link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do
|
||||
= icon('history')
|
||||
- if can? current_user, :download_code, @project
|
||||
= render 'projects/tags/download', ref: @tag.name, project: @project
|
||||
= render 'projects/buttons/download', project: @project, ref: @tag.name
|
||||
- if can?(current_user, :admin_project, @project)
|
||||
.pull-right
|
||||
= link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
|
||||
|
|
|
@ -5,16 +5,17 @@
|
|||
%tr
|
||||
%th Name
|
||||
%th Last Update
|
||||
%th.hidden-xs
|
||||
.pull-left Last Commit
|
||||
.last-commit.hidden-sm.pull-left
|
||||
|
||||
%th.hidden-xs.last-commit
|
||||
Last Commit
|
||||
.last-commit-content.hidden-sm
|
||||
%i.fa.fa-angle-right
|
||||
|
||||
%small.light
|
||||
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
|
||||
–
|
||||
= truncate(@commit.title, length: 50)
|
||||
= time_ago_with_tooltip(@commit.committed_date)
|
||||
–
|
||||
= @commit.full_title
|
||||
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right'
|
||||
|
||||
- if @path.present?
|
||||
|
|
|
@ -10,8 +10,7 @@
|
|||
%div{ class: container_class }
|
||||
.tree-controls
|
||||
= render 'projects/find_file_link'
|
||||
- if can? current_user, :download_code, @project
|
||||
= render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true
|
||||
= render 'projects/buttons/download', project: @project, ref: @ref
|
||||
|
||||
#tree-holder.tree-holder.clearfix
|
||||
.nav-block
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg>
|
After Width: | Height: | Size: 339 B |
|
@ -134,7 +134,7 @@
|
|||
title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
|
||||
= icon('question-circle')
|
||||
|
||||
- if issuable.is_a?(MergeRequest)
|
||||
- if issuable.is_a?(MergeRequest) && !issuable.closed_without_fork?
|
||||
%hr
|
||||
- if @merge_request.new_record?
|
||||
.form-group
|
||||
|
@ -175,7 +175,7 @@
|
|||
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
|
||||
- else
|
||||
.pull-right
|
||||
- if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
|
||||
- if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
|
||||
= link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
|
||||
method: :delete, class: 'btn btn-danger btn-grouped'
|
||||
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
|
||||
|
|
|
@ -782,6 +782,14 @@ Rails.application.routes.draw do
|
|||
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
|
||||
collection do
|
||||
post :cancel_all
|
||||
|
||||
resources :artifacts, only: [] do
|
||||
collection do
|
||||
get :latest_succeeded,
|
||||
path: '*ref_name_and_path',
|
||||
format: false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
member do
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
class DropUnusedCiTables < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
# Set this constant to true if this migration requires downtime.
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
drop_table(:ci_services)
|
||||
drop_table(:ci_web_hooks)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
class EnsureLockVersionHasNoDefault < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
change_column_default :issues, :lock_version, nil
|
||||
change_column_default :merge_requests, :lock_version, nil
|
||||
|
||||
execute('UPDATE issues SET lock_version = 1 WHERE lock_version = 0')
|
||||
execute('UPDATE merge_requests SET lock_version = 1 WHERE lock_version = 0')
|
||||
end
|
||||
|
||||
def down
|
||||
end
|
||||
end
|
19
db/schema.rb
19
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: 20160823081327) do
|
||||
ActiveRecord::Schema.define(version: 20160827011312) do
|
||||
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
@ -295,16 +295,6 @@ ActiveRecord::Schema.define(version: 20160823081327) do
|
|||
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
|
||||
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
|
||||
|
||||
create_table "ci_services", force: :cascade do |t|
|
||||
t.string "type"
|
||||
t.string "title"
|
||||
t.integer "project_id", null: false
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
t.boolean "active", default: false, null: false
|
||||
t.text "properties"
|
||||
end
|
||||
|
||||
create_table "ci_sessions", force: :cascade do |t|
|
||||
t.string "session_id", null: false
|
||||
t.text "data"
|
||||
|
@ -360,13 +350,6 @@ ActiveRecord::Schema.define(version: 20160823081327) do
|
|||
|
||||
add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
|
||||
|
||||
create_table "ci_web_hooks", force: :cascade do |t|
|
||||
t.string "url", null: false
|
||||
t.integer "project_id", null: false
|
||||
t.datetime "created_at"
|
||||
t.datetime "updated_at"
|
||||
end
|
||||
|
||||
create_table "deploy_keys_projects", force: :cascade do |t|
|
||||
t.integer "deploy_key_id", null: false
|
||||
t.integer "project_id", null: false
|
||||
|
|
|
@ -998,6 +998,8 @@ is available before it is returned in the JSON response or an empty response is
|
|||
|
||||
## Branches
|
||||
|
||||
For more information please consult the [Branches](branches.md) documentation.
|
||||
|
||||
### List branches
|
||||
|
||||
Lists all branches of a project.
|
||||
|
@ -1016,56 +1018,46 @@ Parameters:
|
|||
"name": "async",
|
||||
"commit": {
|
||||
"id": "a2b702edecdf41f07b42653eb1abe30ce98b9fca",
|
||||
"parents": [
|
||||
{
|
||||
"id": "3f94fc7c85061973edc9906ae170cc269b07ca55"
|
||||
}
|
||||
"parent_ids": [
|
||||
"3f94fc7c85061973edc9906ae170cc269b07ca55"
|
||||
],
|
||||
"tree": "c68537c6534a02cc2b176ca1549f4ffa190b58ee",
|
||||
"message": "give Caolan credit where it's due (up top)",
|
||||
"author": {
|
||||
"name": "Jeremy Ashkenas",
|
||||
"email": "jashkenas@example.com"
|
||||
},
|
||||
"committer": {
|
||||
"name": "Jeremy Ashkenas",
|
||||
"email": "jashkenas@example.com"
|
||||
},
|
||||
"author_name": "Jeremy Ashkenas",
|
||||
"author_email": "jashkenas@example.com",
|
||||
"authored_date": "2010-12-08T21:28:50+00:00",
|
||||
"committer_name": "Jeremy Ashkenas",
|
||||
"committer_email": "jashkenas@example.com",
|
||||
"committed_date": "2010-12-08T21:28:50+00:00"
|
||||
},
|
||||
"protected": false
|
||||
"protected": false,
|
||||
"developers_can_push": false,
|
||||
"developers_can_merge": false
|
||||
},
|
||||
{
|
||||
"name": "gh-pages",
|
||||
"commit": {
|
||||
"id": "101c10a60019fe870d21868835f65c25d64968fc",
|
||||
"parents": [
|
||||
{
|
||||
"id": "9c15d2e26945a665131af5d7b6d30a06ba338aaa"
|
||||
}
|
||||
"parent_ids": [
|
||||
"9c15d2e26945a665131af5d7b6d30a06ba338aaa"
|
||||
],
|
||||
"tree": "fb5cc9d45da3014b17a876ad539976a0fb9b352a",
|
||||
"message": "Underscore.js 1.5.2",
|
||||
"author": {
|
||||
"name": "Jeremy Ashkenas",
|
||||
"email": "jashkenas@example.com"
|
||||
},
|
||||
"committer": {
|
||||
"name": "Jeremy Ashkenas",
|
||||
"email": "jashkenas@example.com"
|
||||
},
|
||||
"author_name": "Jeremy Ashkenas",
|
||||
"author_email": "jashkenas@example.com",
|
||||
"authored_date": "2013-09-07T12:58:21+00:00",
|
||||
"committer_name": "Jeremy Ashkenas",
|
||||
"committer_email": "jashkenas@example.com",
|
||||
"committed_date": "2013-09-07T12:58:21+00:00"
|
||||
},
|
||||
"protected": false
|
||||
"protected": false,
|
||||
"developers_can_push": false,
|
||||
"developers_can_merge": false
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### List single branch
|
||||
### Single branch
|
||||
|
||||
Lists a specific branch of a project.
|
||||
A specific branch of a project.
|
||||
|
||||
```
|
||||
GET /projects/:id/repository/branches/:branch
|
||||
|
@ -1075,6 +1067,8 @@ Parameters:
|
|||
|
||||
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
|
||||
- `branch` (required) - The name of the branch.
|
||||
- `developers_can_push` - Flag if developers can push to the branch.
|
||||
- `developers_can_merge` - Flag if developers can merge to the branch.
|
||||
|
||||
### Protect single branch
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ If you want a quick introduction to GitLab CI, follow our
|
|||
- [before_script and after_script](#before_script-and-after_script)
|
||||
- [Git Strategy](#git-strategy)
|
||||
- [Shallow cloning](#shallow-cloning)
|
||||
- [Hidden jobs](#hidden-jobs)
|
||||
- [Hidden keys](#hidden-keys)
|
||||
- [Special YAML features](#special-yaml-features)
|
||||
- [Anchors](#anchors)
|
||||
- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml)
|
||||
|
@ -934,24 +934,27 @@ variables:
|
|||
GIT_DEPTH: "3"
|
||||
```
|
||||
|
||||
## Hidden jobs
|
||||
## Hidden keys
|
||||
|
||||
>**Note:**
|
||||
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
|
||||
|
||||
Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can
|
||||
Keys that start with a dot (`.`) will be not processed by GitLab CI. You can
|
||||
use this feature to ignore jobs, or use the
|
||||
[special YAML features](#special-yaml-features) and transform the hidden jobs
|
||||
[special YAML features](#special-yaml-features) and transform the hidden keys
|
||||
into templates.
|
||||
|
||||
In the following example, `.job_name` will be ignored:
|
||||
In the following example, `.key_name` will be ignored:
|
||||
|
||||
```yaml
|
||||
.job_name:
|
||||
.key_name:
|
||||
script:
|
||||
- rake spec
|
||||
```
|
||||
|
||||
Hidden keys can be hashes like normal CI jobs, but you are also allowed to use
|
||||
different types of structures to leverage special YAML features.
|
||||
|
||||
## Special YAML features
|
||||
|
||||
It's possible to use special YAML features like anchors (`&`), aliases (`*`)
|
||||
|
@ -967,7 +970,7 @@ Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
|
|||
|
||||
YAML also has a handy feature called 'anchors', which let you easily duplicate
|
||||
content across your document. Anchors can be used to duplicate/inherit
|
||||
properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs)
|
||||
properties, and is a perfect example to be used with [hidden keys](#hidden-keys)
|
||||
to provide templates for your jobs.
|
||||
|
||||
The following example uses anchors and map merging. It will create two jobs,
|
||||
|
@ -975,7 +978,7 @@ The following example uses anchors and map merging. It will create two jobs,
|
|||
having their own custom `script` defined:
|
||||
|
||||
```yaml
|
||||
.job_template: &job_definition # Hidden job that defines an anchor named 'job_definition'
|
||||
.job_template: &job_definition # Hidden key that defines an anchor named 'job_definition'
|
||||
image: ruby:2.1
|
||||
services:
|
||||
- postgres
|
||||
|
@ -1081,7 +1084,7 @@ test:mysql:
|
|||
- ruby
|
||||
```
|
||||
|
||||
You can see that the hidden jobs are conveniently used as templates.
|
||||
You can see that the hidden keys are conveniently used as templates.
|
||||
|
||||
## Validate the .gitlab-ci.yml
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
This style guide recommends best practices for newlines in Ruby code.
|
||||
|
||||
## Rule: separate code with newlines only when it makes sense from logic perspectice
|
||||
## Rule: separate code with newlines only to group together related logic
|
||||
|
||||
```ruby
|
||||
# bad
|
||||
|
|
|
@ -1,111 +1,164 @@
|
|||
# Integrate your server with Bitbucket
|
||||
# Integrate your GitLab server with Bitbucket
|
||||
|
||||
Import projects from Bitbucket and login to your GitLab instance with your Bitbucket account.
|
||||
Import projects from Bitbucket.org and login to your GitLab instance with your
|
||||
Bitbucket.org account.
|
||||
|
||||
To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket.
|
||||
Bitbucket will generate an application ID and secret key for you to use.
|
||||
## Overview
|
||||
|
||||
1. Sign in to Bitbucket.
|
||||
You can set up Bitbucket.org as an OAuth provider so that you can use your
|
||||
credentials to authenticate into GitLab or import your projects from
|
||||
Bitbucket.org.
|
||||
|
||||
1. Navigate to your individual user settings or a team's settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or a team - that is entirely up to you.
|
||||
- To use Bitbucket.org as an OmniAuth provider, follow the [Bitbucket OmniAuth
|
||||
provider](#bitbucket-omniauth-provider) section.
|
||||
- To import projects from Bitbucket, follow both the
|
||||
[Bitbucket OmniAuth provider](#bitbucket-omniauth-provider) and
|
||||
[Bitbucket project import](#bitbucket-project-import) sections.
|
||||
|
||||
1. Select "OAuth" in the left menu.
|
||||
## Bitbucket OmniAuth provider
|
||||
|
||||
1. Select "Add consumer".
|
||||
> **Note:**
|
||||
Make sure to first follow the [Initial OmniAuth configuration][init-oauth]
|
||||
before proceeding with setting up the Bitbucket integration.
|
||||
|
||||
1. Provide the required details.
|
||||
- Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
|
||||
- Application description: Fill this in if you wish.
|
||||
- URL: The URL to your GitLab installation. 'https://gitlab.company.com'
|
||||
1. Select "Save".
|
||||
To enable the Bitbucket OmniAuth provider you must register your application
|
||||
with Bitbucket.org. Bitbucket will generate an application ID and secret key for
|
||||
you to use.
|
||||
|
||||
1. You should now see a Key and Secret in the list of OAuth customers.
|
||||
Keep this page open as you continue configuration.
|
||||
1. Sign in to [Bitbucket.org](https://bitbucket.org).
|
||||
1. Navigate to your individual user settings (**Bitbucket settings**) or a team's
|
||||
settings (**Manage team**), depending on how you want the application registered.
|
||||
It does not matter if the application is registered as an individual or a
|
||||
team, that is entirely up to you.
|
||||
1. Select **OAuth** in the left menu under "Access Management".
|
||||
1. Select **Add consumer**.
|
||||
1. Provide the required details:
|
||||
|
||||
1. On your GitLab server, open the configuration file.
|
||||
| Item | Description |
|
||||
| :--- | :---------- |
|
||||
| **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. |
|
||||
| **Application description** | Fill this in if you wish. |
|
||||
| **Callback URL** | Leave blank. |
|
||||
| **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
|
||||
|
||||
For omnibus package:
|
||||
And grant at least the following permissions:
|
||||
|
||||
```sh
|
||||
sudo editor /etc/gitlab/gitlab.rb
|
||||
```
|
||||
Account: Email
|
||||
Repositories: Read, Admin
|
||||
```
|
||||
|
||||
>**Note:**
|
||||
It may seem a little odd to giving GitLab admin permissions to repositories,
|
||||
but this is needed in order for GitLab to be able to clone the repositories.
|
||||
|
||||
![Bitbucket OAuth settings page](img/bitbucket_oauth_settings_page.png)
|
||||
|
||||
1. Select **Save**.
|
||||
1. Select your newly created OAuth consumer and you should now see a Key and
|
||||
Secret in the list of OAuth customers. Keep this page open as you continue
|
||||
the configuration.
|
||||
|
||||
![Bitbucket OAuth key](img/bitbucket_oauth_keys.png)
|
||||
|
||||
1. On your GitLab server, open the configuration file:
|
||||
|
||||
```
|
||||
# For Omnibus packages
|
||||
sudo editor /etc/gitlab/gitlab.rb
|
||||
|
||||
# For installations from source
|
||||
sudo -u git -H editor /home/git/gitlab/config/gitlab.yml
|
||||
```
|
||||
|
||||
1. Follow the [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
|
||||
for initial settings.
|
||||
1. Add the Bitbucket provider configuration:
|
||||
|
||||
For Omnibus packages:
|
||||
|
||||
```ruby
|
||||
gitlab_rails['omniauth_providers'] = [
|
||||
{
|
||||
"name" => "bitbucket",
|
||||
"app_id" => "BITBUCKET_APP_KEY",
|
||||
"app_secret" => "BITBUCKET_APP_SECRET",
|
||||
"url" => "https://bitbucket.org/"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
For installations from source:
|
||||
|
||||
```sh
|
||||
cd /home/git/gitlab
|
||||
|
||||
sudo -u git -H editor config/gitlab.yml
|
||||
```yaml
|
||||
- { name: 'bitbucket',
|
||||
app_id: 'BITBUCKET_APP_KEY',
|
||||
app_secret: 'BITBUCKET_APP_SECRET' }
|
||||
```
|
||||
|
||||
1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
|
||||
---
|
||||
|
||||
1. Add the provider configuration:
|
||||
|
||||
For omnibus package:
|
||||
|
||||
```ruby
|
||||
gitlab_rails['omniauth_providers'] = [
|
||||
{
|
||||
"name" => "bitbucket",
|
||||
"app_id" => "YOUR_KEY",
|
||||
"app_secret" => "YOUR_APP_SECRET",
|
||||
"url" => "https://bitbucket.org/"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
For installation from source:
|
||||
|
||||
```
|
||||
- { name: 'bitbucket', app_id: 'YOUR_KEY',
|
||||
app_secret: 'YOUR_APP_SECRET' }
|
||||
```
|
||||
|
||||
1. Change 'YOUR_APP_ID' to the key from the Bitbucket application page from step 7.
|
||||
|
||||
1. Change 'YOUR_APP_SECRET' to the secret from the Bitbucket application page from step 7.
|
||||
Where `BITBUCKET_APP_KEY` is the Key and `BITBUCKET_APP_SECRET` the Secret
|
||||
from the Bitbucket application page.
|
||||
|
||||
1. Save the configuration file.
|
||||
1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
|
||||
installed GitLab via Omnibus or from source respectively.
|
||||
|
||||
1. If you're using the omnibus package, reconfigure GitLab (```gitlab-ctl reconfigure```).
|
||||
|
||||
1. Restart GitLab for the changes to take effect.
|
||||
|
||||
On the sign in page there should now be a Bitbucket icon below the regular sign in form.
|
||||
Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application.
|
||||
If everything goes well the user will be returned to GitLab and will be signed in.
|
||||
On the sign in page there should now be a Bitbucket icon below the regular sign
|
||||
in form. Click the icon to begin the authentication process. Bitbucket will ask
|
||||
the user to sign in and authorize the GitLab application. If everything goes
|
||||
well, the user will be returned to GitLab and will be signed in.
|
||||
|
||||
## Bitbucket project import
|
||||
|
||||
To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com.
|
||||
To allow projects to be imported directly into GitLab, Bitbucket requires two
|
||||
extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md).
|
||||
|
||||
Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key.
|
||||
Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and
|
||||
instead requires GitLab to use SSH and identify itself using your GitLab
|
||||
server's SSH key.
|
||||
|
||||
### Step 1: Public key
|
||||
To be able to access repositories on Bitbucket, GitLab will automatically
|
||||
register your public key with Bitbucket as a deploy key for the repositories to
|
||||
be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which
|
||||
translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to
|
||||
`/home/git/.ssh/bitbucket_rsa.pub` for installations from source.
|
||||
|
||||
To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations.
|
||||
---
|
||||
|
||||
If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following:
|
||||
Below are the steps that will allow GitLab to be able to import your projects
|
||||
from Bitbucket.
|
||||
|
||||
1. Create a new SSH key:
|
||||
1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider).
|
||||
1. Create a new SSH key with an **empty passphrase**:
|
||||
|
||||
```sh
|
||||
sudo -u git -H ssh-keygen
|
||||
```
|
||||
|
||||
When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`.
|
||||
Make sure to use an **empty passphrase**.
|
||||
When asked to 'Enter file in which to save the key' enter:
|
||||
`/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or
|
||||
`/home/git/.ssh/bitbucket_rsa` for installations from source. The name is
|
||||
important so make sure to get it right.
|
||||
|
||||
1. Configure SSH client to use your new key:
|
||||
> **Warning:**
|
||||
This key must NOT be associated with ANY existing Bitbucket accounts. If it
|
||||
is, the import will fail with an `Access denied! Please verify you can add
|
||||
deploy keys to this repository.` error.
|
||||
|
||||
Open the SSH configuration file of the git user.
|
||||
1. Next, you need to to configure the SSH client to use your new key. Open the
|
||||
SSH configuration file of the `git` user:
|
||||
|
||||
```sh
|
||||
sudo editor /home/git/.ssh/config
|
||||
```
|
||||
# For Omnibus packages
|
||||
sudo editor /var/opt/gitlab/.ssh/config
|
||||
|
||||
# For installations from source
|
||||
sudo editor /home/git/.ssh/config
|
||||
```
|
||||
|
||||
Add a host configuration for `bitbucket.org`.
|
||||
1. Add a host configuration for `bitbucket.org`:
|
||||
|
||||
```sh
|
||||
Host bitbucket.org
|
||||
|
@ -113,28 +166,46 @@ If you have that file in place, you're all set and should see the "Import projec
|
|||
User git
|
||||
```
|
||||
|
||||
### Step 2: Known hosts
|
||||
|
||||
To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so:
|
||||
|
||||
1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use:
|
||||
1. Save the file and exit.
|
||||
1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git`
|
||||
user that GitLab will use:
|
||||
|
||||
```sh
|
||||
sudo -u git -H ssh bitbucket.org
|
||||
```
|
||||
|
||||
1. Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter):
|
||||
That step is performed because GitLab needs to connect to Bitbucket over SSH,
|
||||
in order to add `bitbucket.org` to your GitLab server's known SSH hosts.
|
||||
|
||||
1. Verify the RSA key fingerprint you'll see in the response matches the one
|
||||
in the [Bitbucket documentation][bitbucket-docs] (the specific IP address
|
||||
doesn't matter):
|
||||
|
||||
```sh
|
||||
The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established.
|
||||
RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40.
|
||||
The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established.
|
||||
RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A.
|
||||
Are you sure you want to continue connecting (yes/no)?
|
||||
```
|
||||
|
||||
1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts.
|
||||
|
||||
1. Your GitLab server is now able to connect to Bitbucket over SSH.
|
||||
|
||||
1. If the fingerprint matches, type `yes` to continue connecting and have
|
||||
`bitbucket.org` be added to your known SSH hosts. After confirming you should
|
||||
see a permission denied message. If you see an authentication successful
|
||||
message you have done something wrong. The key you are using has already been
|
||||
added to a Bitbucket account and will cause the import script to fail. Ensure
|
||||
the key you are using CANNOT authenticate with Bitbucket.
|
||||
1. Restart GitLab to allow it to find the new public key.
|
||||
|
||||
You should now see the "Import projects from Bitbucket" option on the New Project page enabled.
|
||||
Your GitLab server is now able to connect to Bitbucket over SSH. You should be
|
||||
able to see the "Import projects from Bitbucket" option on the New Project page
|
||||
enabled.
|
||||
|
||||
## Acknowledgemts
|
||||
|
||||
Special thanks to the writer behind the following article:
|
||||
|
||||
- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/
|
||||
|
||||
[init-oauth]: omniauth.md#initial-omniauth-configuration
|
||||
[bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints
|
||||
[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
|
||||
[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
|
@ -102,8 +102,8 @@ To change these settings:
|
|||
block_auto_created_users: true
|
||||
```
|
||||
|
||||
Now we can choose one or more of the Supported Providers listed above to continue
|
||||
the configuration process.
|
||||
Now we can choose one or more of the [Supported Providers](#supported-providers)
|
||||
listed above to continue the configuration process.
|
||||
|
||||
## Enable OmniAuth for an Existing User
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ GitLab 8.1.
|
|||
```bash
|
||||
cd /home/git/gitlab-workhorse
|
||||
sudo -u git -H git fetch --all
|
||||
sudo -u git -H git checkout v0.7.8
|
||||
sudo -u git -H git checkout v0.7.11
|
||||
sudo -u git -H make
|
||||
```
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ module API
|
|||
ref = branches.first
|
||||
end
|
||||
|
||||
pipeline = @project.ensure_pipeline(commit.sha, ref, current_user)
|
||||
pipeline = @project.ensure_pipeline(ref, commit.sha, current_user)
|
||||
|
||||
name = params[:name] || params[:context]
|
||||
status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref])
|
||||
|
|
|
@ -154,21 +154,15 @@ module API
|
|||
render_api_error!({ labels: errors }, 400)
|
||||
end
|
||||
|
||||
project = user_project
|
||||
attrs[:labels] = params[:labels] if params[:labels]
|
||||
|
||||
issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute
|
||||
issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute
|
||||
|
||||
if issue.spam?
|
||||
render_api_error!({ error: 'Spam detected' }, 400)
|
||||
end
|
||||
|
||||
if issue.valid?
|
||||
# Find or create labels and attach to issue. Labels are valid because
|
||||
# we already checked its name, so there can't be an error here
|
||||
if params[:labels].present?
|
||||
issue.add_labels_by_names(params[:labels].split(','))
|
||||
end
|
||||
|
||||
present issue, with: Entities::Issue, current_user: current_user
|
||||
else
|
||||
render_validation_error!(issue)
|
||||
|
@ -202,17 +196,11 @@ module API
|
|||
render_api_error!({ labels: errors }, 400)
|
||||
end
|
||||
|
||||
attrs[:labels] = params[:labels] if params[:labels]
|
||||
|
||||
issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue)
|
||||
|
||||
if issue.valid?
|
||||
# Find or create labels and attach to issue. Labels are valid because
|
||||
# we already checked its name, so there can't be an error here
|
||||
if params[:labels] && can?(current_user, :admin_issue, user_project)
|
||||
issue.remove_labels
|
||||
# Create and add labels to the new created issue
|
||||
issue.add_labels_by_names(params[:labels].split(','))
|
||||
end
|
||||
|
||||
present issue, with: Entities::Issue, current_user: current_user
|
||||
else
|
||||
render_validation_error!(issue)
|
||||
|
|
|
@ -12,9 +12,7 @@ module Gitlab
|
|||
@ref = ref
|
||||
@job = job
|
||||
|
||||
@pipeline = @project.pipelines
|
||||
.latest_successful_for(@ref)
|
||||
.first
|
||||
@pipeline = @project.pipelines.latest_successful_for(@ref)
|
||||
end
|
||||
|
||||
def entity
|
||||
|
|
|
@ -5,11 +5,10 @@ module Gitlab
|
|||
##
|
||||
# Entry that represents a hidden CI/CD job.
|
||||
#
|
||||
class HiddenJob < Entry
|
||||
class Hidden < Entry
|
||||
include Validatable
|
||||
|
||||
validations do
|
||||
validates :config, type: Hash
|
||||
validates :config, presence: true
|
||||
end
|
||||
|
|
@ -30,7 +30,7 @@ module Gitlab
|
|||
|
||||
def compose!
|
||||
@config.each do |name, config|
|
||||
node = hidden?(name) ? Node::HiddenJob : Node::Job
|
||||
node = hidden?(name) ? Node::Hidden : Node::Job
|
||||
|
||||
factory = Node::Factory.new(node)
|
||||
.value(config || {})
|
||||
|
|
|
@ -170,6 +170,35 @@ describe Projects::MergeRequestsController do
|
|||
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
|
||||
expect(merge_request.reload.closed?).to be_truthy
|
||||
end
|
||||
|
||||
it 'allows editing of a closed merge request' do
|
||||
merge_request.close!
|
||||
|
||||
put :update,
|
||||
namespace_id: project.namespace.path,
|
||||
project_id: project.path,
|
||||
id: merge_request.iid,
|
||||
merge_request: {
|
||||
title: 'New title'
|
||||
}
|
||||
|
||||
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
|
||||
expect(merge_request.reload.title).to eq 'New title'
|
||||
end
|
||||
|
||||
it 'does not allow to update target branch closed merge request' do
|
||||
merge_request.close!
|
||||
|
||||
put :update,
|
||||
namespace_id: project.namespace.path,
|
||||
project_id: project.path,
|
||||
id: merge_request.iid,
|
||||
merge_request: {
|
||||
target_branch: 'new_branch'
|
||||
}
|
||||
|
||||
expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -147,6 +147,37 @@ feature 'Diff notes', js: true, feature: true do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when the MR only supports legacy diff notes' do
|
||||
before do
|
||||
@merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
|
||||
visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
|
||||
end
|
||||
|
||||
context 'with a new line' do
|
||||
it 'should allow commenting' do
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an old line' do
|
||||
it 'should allow commenting' do
|
||||
should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an unchanged line' do
|
||||
it 'should allow commenting' do
|
||||
should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a match line' do
|
||||
it 'should not allow commenting' do
|
||||
should_not_allow_commenting(find('.match', match: :first))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def should_allow_commenting(line_holder, diff_side = nil)
|
||||
line = get_line_components(line_holder, diff_side)
|
||||
line[:content].hover
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'Download buttons in branches page', feature: true do
|
||||
given(:user) { create(:user) }
|
||||
given(:role) { :developer }
|
||||
given(:status) { 'success' }
|
||||
given(:project) { create(:project) }
|
||||
|
||||
given(:pipeline) do
|
||||
create(:ci_pipeline,
|
||||
project: project,
|
||||
sha: project.commit('binary-encoding').sha,
|
||||
ref: 'binary-encoding', # make sure the branch is in the 1st page!
|
||||
status: status)
|
||||
end
|
||||
|
||||
given!(:build) do
|
||||
create(:ci_build, :success, :artifacts,
|
||||
pipeline: pipeline,
|
||||
status: pipeline.status,
|
||||
name: 'build')
|
||||
end
|
||||
|
||||
background do
|
||||
login_as(user)
|
||||
project.team << [user, role]
|
||||
end
|
||||
|
||||
describe 'when checking branches' do
|
||||
context 'with artifacts' do
|
||||
before do
|
||||
visit namespace_project_branches_path(project.namespace, project)
|
||||
end
|
||||
|
||||
scenario 'shows download artifacts button' do
|
||||
expect(page).to have_link "Download '#{build.name}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'Download buttons in files tree', feature: true do
|
||||
given(:user) { create(:user) }
|
||||
given(:role) { :developer }
|
||||
given(:status) { 'success' }
|
||||
given(:project) { create(:project) }
|
||||
|
||||
given(:pipeline) do
|
||||
create(:ci_pipeline,
|
||||
project: project,
|
||||
sha: project.commit.sha,
|
||||
ref: project.default_branch,
|
||||
status: status)
|
||||
end
|
||||
|
||||
given!(:build) do
|
||||
create(:ci_build, :success, :artifacts,
|
||||
pipeline: pipeline,
|
||||
status: pipeline.status,
|
||||
name: 'build')
|
||||
end
|
||||
|
||||
background do
|
||||
login_as(user)
|
||||
project.team << [user, role]
|
||||
end
|
||||
|
||||
describe 'when files tree' do
|
||||
context 'with artifacts' do
|
||||
before do
|
||||
visit namespace_project_tree_path(
|
||||
project.namespace, project, project.default_branch)
|
||||
end
|
||||
|
||||
scenario 'shows download artifacts button' do
|
||||
expect(page).to have_link "Download '#{build.name}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,40 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'Download buttons in project main page', feature: true do
|
||||
given(:user) { create(:user) }
|
||||
given(:role) { :developer }
|
||||
given(:status) { 'success' }
|
||||
given(:project) { create(:project) }
|
||||
|
||||
given(:pipeline) do
|
||||
create(:ci_pipeline,
|
||||
project: project,
|
||||
sha: project.commit.sha,
|
||||
ref: project.default_branch,
|
||||
status: status)
|
||||
end
|
||||
|
||||
given!(:build) do
|
||||
create(:ci_build, :success, :artifacts,
|
||||
pipeline: pipeline,
|
||||
status: pipeline.status,
|
||||
name: 'build')
|
||||
end
|
||||
|
||||
background do
|
||||
login_as(user)
|
||||
project.team << [user, role]
|
||||
end
|
||||
|
||||
describe 'when checking project main page' do
|
||||
context 'with artifacts' do
|
||||
before do
|
||||
visit namespace_project_path(project.namespace, project)
|
||||
end
|
||||
|
||||
scenario 'shows download artifacts button' do
|
||||
expect(page).to have_link "Download '#{build.name}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'Download buttons in tags page', feature: true do
|
||||
given(:user) { create(:user) }
|
||||
given(:role) { :developer }
|
||||
given(:status) { 'success' }
|
||||
given(:tag) { 'v1.0.0' }
|
||||
given(:project) { create(:project) }
|
||||
|
||||
given(:pipeline) do
|
||||
create(:ci_pipeline,
|
||||
project: project,
|
||||
sha: project.commit(tag).sha,
|
||||
ref: tag,
|
||||
status: status)
|
||||
end
|
||||
|
||||
given!(:build) do
|
||||
create(:ci_build, :success, :artifacts,
|
||||
pipeline: pipeline,
|
||||
status: pipeline.status,
|
||||
name: 'build')
|
||||
end
|
||||
|
||||
background do
|
||||
login_as(user)
|
||||
project.team << [user, role]
|
||||
end
|
||||
|
||||
describe 'when checking tags' do
|
||||
context 'with artifacts' do
|
||||
before do
|
||||
visit namespace_project_tags_path(project.namespace, project)
|
||||
end
|
||||
|
||||
scenario 'shows download artifacts button' do
|
||||
expect(page).to have_link "Download '#{build.name}'"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -20,6 +20,22 @@ feature 'Task Lists', feature: true do
|
|||
MARKDOWN
|
||||
end
|
||||
|
||||
let(:singleIncompleteMarkdown) do
|
||||
<<-MARKDOWN.strip_heredoc
|
||||
This is a task list:
|
||||
|
||||
- [ ] Incomplete entry 1
|
||||
MARKDOWN
|
||||
end
|
||||
|
||||
let(:singleCompleteMarkdown) do
|
||||
<<-MARKDOWN.strip_heredoc
|
||||
This is a task list:
|
||||
|
||||
- [x] Incomplete entry 1
|
||||
MARKDOWN
|
||||
end
|
||||
|
||||
before do
|
||||
Warden.test_mode!
|
||||
|
||||
|
@ -34,77 +50,145 @@ feature 'Task Lists', feature: true do
|
|||
end
|
||||
|
||||
describe 'for Issues' do
|
||||
let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
|
||||
describe 'multiple tasks' do
|
||||
let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
|
||||
|
||||
it 'renders' do
|
||||
visit_issue(project, issue)
|
||||
it 'renders' do
|
||||
visit_issue(project, issue)
|
||||
|
||||
expect(page).to have_selector('ul.task-list', count: 1)
|
||||
expect(page).to have_selector('li.task-list-item', count: 6)
|
||||
expect(page).to have_selector('ul input[checked]', count: 2)
|
||||
expect(page).to have_selector('ul.task-list', count: 1)
|
||||
expect(page).to have_selector('li.task-list-item', count: 6)
|
||||
expect(page).to have_selector('ul input[checked]', count: 2)
|
||||
end
|
||||
|
||||
it 'contains the required selectors' do
|
||||
visit_issue(project, issue)
|
||||
|
||||
container = '.detail-page-description .description.js-task-list-container'
|
||||
|
||||
expect(page).to have_selector(container)
|
||||
expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
|
||||
expect(page).to have_selector("#{container} .js-task-list-field")
|
||||
expect(page).to have_selector('form.js-issuable-update')
|
||||
expect(page).to have_selector('a.btn-close')
|
||||
end
|
||||
|
||||
it 'is only editable by author' do
|
||||
visit_issue(project, issue)
|
||||
expect(page).to have_selector('.js-task-list-container')
|
||||
|
||||
logout(:user)
|
||||
|
||||
login_as(user2)
|
||||
visit current_path
|
||||
expect(page).not_to have_selector('.js-task-list-container')
|
||||
end
|
||||
|
||||
it 'provides a summary on Issues#index' do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
expect(page).to have_content("2 of 6 tasks completed")
|
||||
end
|
||||
end
|
||||
|
||||
it 'contains the required selectors' do
|
||||
visit_issue(project, issue)
|
||||
describe 'single incomplete task' do
|
||||
let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) }
|
||||
|
||||
container = '.detail-page-description .description.js-task-list-container'
|
||||
it 'renders' do
|
||||
visit_issue(project, issue)
|
||||
|
||||
expect(page).to have_selector(container)
|
||||
expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
|
||||
expect(page).to have_selector("#{container} .js-task-list-field")
|
||||
expect(page).to have_selector('form.js-issuable-update')
|
||||
expect(page).to have_selector('a.btn-close')
|
||||
expect(page).to have_selector('ul.task-list', count: 1)
|
||||
expect(page).to have_selector('li.task-list-item', count: 1)
|
||||
expect(page).to have_selector('ul input[checked]', count: 0)
|
||||
end
|
||||
|
||||
it 'provides a summary on Issues#index' do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
expect(page).to have_content("0 of 1 task completed")
|
||||
end
|
||||
end
|
||||
|
||||
it 'is only editable by author' do
|
||||
visit_issue(project, issue)
|
||||
expect(page).to have_selector('.js-task-list-container')
|
||||
describe 'single complete task' do
|
||||
let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) }
|
||||
|
||||
logout(:user)
|
||||
it 'renders' do
|
||||
visit_issue(project, issue)
|
||||
|
||||
login_as(user2)
|
||||
visit current_path
|
||||
expect(page).not_to have_selector('.js-task-list-container')
|
||||
end
|
||||
expect(page).to have_selector('ul.task-list', count: 1)
|
||||
expect(page).to have_selector('li.task-list-item', count: 1)
|
||||
expect(page).to have_selector('ul input[checked]', count: 1)
|
||||
end
|
||||
|
||||
it 'provides a summary on Issues#index' do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
expect(page).to have_content("6 tasks (2 completed, 4 remaining)")
|
||||
it 'provides a summary on Issues#index' do
|
||||
visit namespace_project_issues_path(project.namespace, project)
|
||||
expect(page).to have_content("1 of 1 task completed")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'for Notes' do
|
||||
let!(:issue) { create(:issue, author: user, project: project) }
|
||||
let!(:note) do
|
||||
create(:note, note: markdown, noteable: issue,
|
||||
project: project, author: user)
|
||||
describe 'multiple tasks' do
|
||||
let!(:note) do
|
||||
create(:note, note: markdown, noteable: issue,
|
||||
project: project, author: user)
|
||||
end
|
||||
|
||||
it 'renders for note body' do
|
||||
visit_issue(project, issue)
|
||||
|
||||
expect(page).to have_selector('.note ul.task-list', count: 1)
|
||||
expect(page).to have_selector('.note li.task-list-item', count: 6)
|
||||
expect(page).to have_selector('.note ul input[checked]', count: 2)
|
||||
end
|
||||
|
||||
it 'contains the required selectors' do
|
||||
visit_issue(project, issue)
|
||||
|
||||
expect(page).to have_selector('.note .js-task-list-container')
|
||||
expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox')
|
||||
expect(page).to have_selector('.note .js-task-list-container .js-task-list-field')
|
||||
end
|
||||
|
||||
it 'is only editable by author' do
|
||||
visit_issue(project, issue)
|
||||
expect(page).to have_selector('.js-task-list-container')
|
||||
|
||||
logout(:user)
|
||||
|
||||
login_as(user2)
|
||||
visit current_path
|
||||
expect(page).not_to have_selector('.js-task-list-container')
|
||||
end
|
||||
end
|
||||
|
||||
it 'renders for note body' do
|
||||
visit_issue(project, issue)
|
||||
describe 'single incomplete task' do
|
||||
let!(:note) do
|
||||
create(:note, note: singleIncompleteMarkdown, noteable: issue,
|
||||
project: project, author: user)
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.note ul.task-list', count: 1)
|
||||
expect(page).to have_selector('.note li.task-list-item', count: 6)
|
||||
expect(page).to have_selector('.note ul input[checked]', count: 2)
|
||||
it 'renders for note body' do
|
||||
visit_issue(project, issue)
|
||||
|
||||
expect(page).to have_selector('.note ul.task-list', count: 1)
|
||||
expect(page).to have_selector('.note li.task-list-item', count: 1)
|
||||
expect(page).to have_selector('.note ul input[checked]', count: 0)
|
||||
end
|
||||
end
|
||||
|
||||
it 'contains the required selectors' do
|
||||
visit_issue(project, issue)
|
||||
describe 'single complete task' do
|
||||
let!(:note) do
|
||||
create(:note, note: singleCompleteMarkdown, noteable: issue,
|
||||
project: project, author: user)
|
||||
end
|
||||
|
||||
expect(page).to have_selector('.note .js-task-list-container')
|
||||
expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox')
|
||||
expect(page).to have_selector('.note .js-task-list-container .js-task-list-field')
|
||||
end
|
||||
it 'renders for note body' do
|
||||
visit_issue(project, issue)
|
||||
|
||||
it 'is only editable by author' do
|
||||
visit_issue(project, issue)
|
||||
expect(page).to have_selector('.js-task-list-container')
|
||||
|
||||
logout(:user)
|
||||
|
||||
login_as(user2)
|
||||
visit current_path
|
||||
expect(page).not_to have_selector('.js-task-list-container')
|
||||
expect(page).to have_selector('.note ul.task-list', count: 1)
|
||||
expect(page).to have_selector('.note li.task-list-item', count: 1)
|
||||
expect(page).to have_selector('.note ul input[checked]', count: 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -113,42 +197,78 @@ feature 'Task Lists', feature: true do
|
|||
visit namespace_project_merge_request_path(project.namespace, project, merge)
|
||||
end
|
||||
|
||||
let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
|
||||
describe 'multiple tasks' do
|
||||
let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
|
||||
|
||||
it 'renders for description' do
|
||||
visit_merge_request(project, merge)
|
||||
it 'renders for description' do
|
||||
visit_merge_request(project, merge)
|
||||
|
||||
expect(page).to have_selector('ul.task-list', count: 1)
|
||||
expect(page).to have_selector('li.task-list-item', count: 6)
|
||||
expect(page).to have_selector('ul input[checked]', count: 2)
|
||||
expect(page).to have_selector('ul.task-list', count: 1)
|
||||
expect(page).to have_selector('li.task-list-item', count: 6)
|
||||
expect(page).to have_selector('ul input[checked]', count: 2)
|
||||
end
|
||||
|
||||
it 'contains the required selectors' do
|
||||
visit_merge_request(project, merge)
|
||||
|
||||
container = '.detail-page-description .description.js-task-list-container'
|
||||
|
||||
expect(page).to have_selector(container)
|
||||
expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
|
||||
expect(page).to have_selector("#{container} .js-task-list-field")
|
||||
expect(page).to have_selector('form.js-issuable-update')
|
||||
expect(page).to have_selector('a.btn-close')
|
||||
end
|
||||
|
||||
it 'is only editable by author' do
|
||||
visit_merge_request(project, merge)
|
||||
expect(page).to have_selector('.js-task-list-container')
|
||||
|
||||
logout(:user)
|
||||
|
||||
login_as(user2)
|
||||
visit current_path
|
||||
expect(page).not_to have_selector('.js-task-list-container')
|
||||
end
|
||||
|
||||
it 'provides a summary on MergeRequests#index' do
|
||||
visit namespace_project_merge_requests_path(project.namespace, project)
|
||||
expect(page).to have_content("2 of 6 tasks completed")
|
||||
end
|
||||
end
|
||||
|
||||
it 'contains the required selectors' do
|
||||
visit_merge_request(project, merge)
|
||||
describe 'single incomplete task' do
|
||||
let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) }
|
||||
|
||||
container = '.detail-page-description .description.js-task-list-container'
|
||||
it 'renders for description' do
|
||||
visit_merge_request(project, merge)
|
||||
|
||||
expect(page).to have_selector(container)
|
||||
expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
|
||||
expect(page).to have_selector("#{container} .js-task-list-field")
|
||||
expect(page).to have_selector('form.js-issuable-update')
|
||||
expect(page).to have_selector('a.btn-close')
|
||||
expect(page).to have_selector('ul.task-list', count: 1)
|
||||
expect(page).to have_selector('li.task-list-item', count: 1)
|
||||
expect(page).to have_selector('ul input[checked]', count: 0)
|
||||
end
|
||||
|
||||
it 'provides a summary on MergeRequests#index' do
|
||||
visit namespace_project_merge_requests_path(project.namespace, project)
|
||||
expect(page).to have_content("0 of 1 task completed")
|
||||
end
|
||||
end
|
||||
|
||||
it 'is only editable by author' do
|
||||
visit_merge_request(project, merge)
|
||||
expect(page).to have_selector('.js-task-list-container')
|
||||
describe 'single complete task' do
|
||||
let!(:merge) { create(:merge_request, :simple, description: singleCompleteMarkdown, author: user, source_project: project) }
|
||||
|
||||
logout(:user)
|
||||
it 'renders for description' do
|
||||
visit_merge_request(project, merge)
|
||||
|
||||
login_as(user2)
|
||||
visit current_path
|
||||
expect(page).not_to have_selector('.js-task-list-container')
|
||||
end
|
||||
expect(page).to have_selector('ul.task-list', count: 1)
|
||||
expect(page).to have_selector('li.task-list-item', count: 1)
|
||||
expect(page).to have_selector('ul input[checked]', count: 1)
|
||||
end
|
||||
|
||||
it 'provides a summary on MergeRequests#index' do
|
||||
visit namespace_project_merge_requests_path(project.namespace, project)
|
||||
expect(page).to have_content("6 tasks (2 completed, 4 remaining)")
|
||||
it 'provides a summary on MergeRequests#index' do
|
||||
visit namespace_project_merge_requests_path(project.namespace, project)
|
||||
expect(page).to have_content("1 of 1 task completed")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Ci::Config::Node::HiddenJob do
|
||||
describe Gitlab::Ci::Config::Node::Hidden do
|
||||
let(:entry) { described_class.new(config) }
|
||||
|
||||
describe 'validations' do
|
||||
context 'when entry config value is correct' do
|
||||
let(:config) { { image: 'ruby:2.2' } }
|
||||
let(:config) { [:some, :array] }
|
||||
|
||||
describe '#value' do
|
||||
it 'returns key value' do
|
||||
expect(entry.value).to eq(image: 'ruby:2.2')
|
||||
expect(entry.value).to eq [:some, :array]
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -21,17 +21,6 @@ describe Gitlab::Ci::Config::Node::HiddenJob do
|
|||
end
|
||||
|
||||
context 'when entry value is not correct' do
|
||||
context 'incorrect config value type' do
|
||||
let(:config) { ['incorrect'] }
|
||||
|
||||
describe '#errors' do
|
||||
it 'saves errors' do
|
||||
expect(entry.errors)
|
||||
.to include 'hidden job config should be a hash'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when config is empty' do
|
||||
let(:config) { {} }
|
||||
|
|
@ -74,7 +74,7 @@ describe Gitlab::Ci::Config::Node::Jobs do
|
|||
expect(entry.descendants.first(2))
|
||||
.to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job))
|
||||
expect(entry.descendants.last)
|
||||
.to be_an_instance_of(Gitlab::Ci::Config::Node::HiddenJob)
|
||||
.to be_an_instance_of(Gitlab::Ci::Config::Node::Hidden)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -282,4 +282,17 @@ describe Ability, lib: true do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.project_disabled_features_rules' do
|
||||
let(:project) { build(:project) }
|
||||
|
||||
subject { described_class.project_disabled_features_rules(project) }
|
||||
|
||||
context 'wiki named abilities' do
|
||||
it 'disables wiki abilities if the project has no wiki' do
|
||||
expect(project).to receive(:has_wiki?).and_return(false)
|
||||
expect(subject).to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -948,15 +948,17 @@ describe Ci::Build, models: true do
|
|||
before { build.run! }
|
||||
|
||||
it 'returns false' do
|
||||
expect(build.retryable?).to be false
|
||||
expect(build).not_to be_retryable
|
||||
end
|
||||
end
|
||||
|
||||
context 'when build is finished' do
|
||||
before { build.success! }
|
||||
before do
|
||||
build.success!
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(build.retryable?).to be true
|
||||
expect(build).to be_retryable
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -195,6 +195,36 @@ describe Ci::Pipeline, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with non-empty project' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
let(:pipeline) do
|
||||
create(:ci_pipeline,
|
||||
project: project,
|
||||
ref: project.default_branch,
|
||||
sha: project.commit.sha)
|
||||
end
|
||||
|
||||
describe '#latest?' do
|
||||
context 'with latest sha' do
|
||||
it 'returns true' do
|
||||
expect(pipeline).to be_latest
|
||||
end
|
||||
end
|
||||
|
||||
context 'with not latest sha' do
|
||||
before do
|
||||
pipeline.update(
|
||||
sha: project.commit("#{project.default_branch}~1").sha)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(pipeline).not_to be_latest
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#manual_actions' do
|
||||
subject { pipeline.manual_actions }
|
||||
|
||||
|
|
|
@ -477,8 +477,8 @@ describe MergeRequest, models: true do
|
|||
|
||||
allow(subject).to receive(:diff_head_sha).and_return('123abc')
|
||||
|
||||
expect(subject.source_project).to receive(:pipeline).
|
||||
with('123abc', 'master').
|
||||
expect(subject.source_project).to receive(:pipeline_for).
|
||||
with('master', '123abc').
|
||||
and_return(pipeline)
|
||||
|
||||
expect(subject.pipeline).to eq(pipeline)
|
||||
|
@ -962,4 +962,80 @@ describe MergeRequest, models: true do
|
|||
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
describe "#forked_source_project_missing?" do
|
||||
let(:project) { create(:project) }
|
||||
let(:fork_project) { create(:project, forked_from_project: project) }
|
||||
let(:user) { create(:user) }
|
||||
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
|
||||
|
||||
context "when the fork exists" do
|
||||
let(:merge_request) do
|
||||
create(:merge_request,
|
||||
source_project: fork_project,
|
||||
target_project: project)
|
||||
end
|
||||
|
||||
it { expect(merge_request.forked_source_project_missing?).to be_falsey }
|
||||
end
|
||||
|
||||
context "when the source project is the same as the target project" do
|
||||
let(:merge_request) { create(:merge_request, source_project: project) }
|
||||
|
||||
it { expect(merge_request.forked_source_project_missing?).to be_falsey }
|
||||
end
|
||||
|
||||
context "when the fork does not exist" do
|
||||
let(:merge_request) do
|
||||
create(:merge_request,
|
||||
source_project: fork_project,
|
||||
target_project: project)
|
||||
end
|
||||
|
||||
it "returns true" do
|
||||
unlink_project.execute
|
||||
merge_request.reload
|
||||
|
||||
expect(merge_request.forked_source_project_missing?).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "#closed_without_fork?" do
|
||||
let(:project) { create(:project) }
|
||||
let(:fork_project) { create(:project, forked_from_project: project) }
|
||||
let(:user) { create(:user) }
|
||||
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
|
||||
|
||||
context "when the merge request is closed" do
|
||||
let(:closed_merge_request) do
|
||||
create(:closed_merge_request,
|
||||
source_project: fork_project,
|
||||
target_project: project)
|
||||
end
|
||||
|
||||
it "returns false if the fork exist" do
|
||||
expect(closed_merge_request.closed_without_fork?).to be_falsey
|
||||
end
|
||||
|
||||
it "returns true if the fork does not exist" do
|
||||
unlink_project.execute
|
||||
closed_merge_request.reload
|
||||
|
||||
expect(closed_merge_request.closed_without_fork?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context "when the merge request is open" do
|
||||
let(:open_merge_request) do
|
||||
create(:merge_request,
|
||||
source_project: fork_project,
|
||||
target_project: project)
|
||||
end
|
||||
|
||||
it "returns false" do
|
||||
expect(open_merge_request.closed_without_fork?).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -506,6 +506,18 @@ describe Project, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#has_wiki?' do
|
||||
let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) }
|
||||
let(:wiki_enabled_project) { build(:project, wiki_enabled: true) }
|
||||
let(:external_wiki_project) { build(:project, has_external_wiki: true) }
|
||||
|
||||
it 'returns true if project is wiki enabled or has external wiki' do
|
||||
expect(wiki_enabled_project).to have_wiki
|
||||
expect(external_wiki_project).to have_wiki
|
||||
expect(no_wiki_project).not_to have_wiki
|
||||
end
|
||||
end
|
||||
|
||||
describe '#external_wiki' do
|
||||
let(:project) { create(:project) }
|
||||
|
||||
|
@ -685,23 +697,37 @@ describe Project, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#pipeline' do
|
||||
let(:project) { create :project }
|
||||
let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' }
|
||||
describe '#pipeline_for' do
|
||||
let(:project) { create(:project) }
|
||||
let!(:pipeline) { create_pipeline }
|
||||
|
||||
subject { project.pipeline(pipeline.sha, 'master') }
|
||||
shared_examples 'giving the correct pipeline' do
|
||||
it { is_expected.to eq(pipeline) }
|
||||
|
||||
it { is_expected.to eq(pipeline) }
|
||||
context 'return latest' do
|
||||
let!(:pipeline2) { create_pipeline }
|
||||
|
||||
context 'return latest' do
|
||||
let(:pipeline2) { create :ci_pipeline, project: project, ref: 'master' }
|
||||
|
||||
before do
|
||||
pipeline
|
||||
pipeline2
|
||||
it { is_expected.to eq(pipeline2) }
|
||||
end
|
||||
end
|
||||
|
||||
it { is_expected.to eq(pipeline2) }
|
||||
context 'with explicit sha' do
|
||||
subject { project.pipeline_for('master', pipeline.sha) }
|
||||
|
||||
it_behaves_like 'giving the correct pipeline'
|
||||
end
|
||||
|
||||
context 'with implicit sha' do
|
||||
subject { project.pipeline_for('master') }
|
||||
|
||||
it_behaves_like 'giving the correct pipeline'
|
||||
end
|
||||
|
||||
def create_pipeline
|
||||
create(:ci_pipeline,
|
||||
project: project,
|
||||
ref: 'master',
|
||||
sha: project.commit('master').sha)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -15,7 +15,9 @@ describe API::API, api: true do
|
|||
describe 'GET /projects/:id/builds ' do
|
||||
let(:query) { '' }
|
||||
|
||||
before { get api("/projects/#{project.id}/builds?#{query}", api_user) }
|
||||
before do
|
||||
get api("/projects/#{project.id}/builds?#{query}", api_user)
|
||||
end
|
||||
|
||||
context 'authorized user' do
|
||||
it 'returns project builds' do
|
||||
|
@ -122,7 +124,9 @@ describe API::API, api: true do
|
|||
end
|
||||
|
||||
describe 'GET /projects/:id/builds/:build_id' do
|
||||
before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) }
|
||||
before do
|
||||
get api("/projects/#{project.id}/builds/#{build.id}", api_user)
|
||||
end
|
||||
|
||||
context 'authorized user' do
|
||||
it 'returns specific build data' do
|
||||
|
@ -141,7 +145,9 @@ describe API::API, api: true do
|
|||
end
|
||||
|
||||
describe 'GET /projects/:id/builds/:build_id/artifacts' do
|
||||
before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) }
|
||||
before do
|
||||
get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
|
||||
end
|
||||
|
||||
context 'build with artifacts' do
|
||||
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
|
||||
|
@ -292,7 +298,9 @@ describe API::API, api: true do
|
|||
end
|
||||
|
||||
describe 'POST /projects/:id/builds/:build_id/cancel' do
|
||||
before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) }
|
||||
before do
|
||||
post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user)
|
||||
end
|
||||
|
||||
context 'authorized user' do
|
||||
context 'user with :update_build persmission' do
|
||||
|
@ -323,7 +331,9 @@ describe API::API, api: true do
|
|||
describe 'POST /projects/:id/builds/:build_id/retry' do
|
||||
let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
|
||||
|
||||
before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) }
|
||||
before do
|
||||
post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user)
|
||||
end
|
||||
|
||||
context 'authorized user' do
|
||||
context 'user with :update_build permission' do
|
||||
|
|
|
@ -95,7 +95,7 @@ describe API::API, api: true do
|
|||
end
|
||||
|
||||
it "returns status for CI" do
|
||||
pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master')
|
||||
pipeline = project.ensure_pipeline('master', project.repository.commit.sha)
|
||||
pipeline.update(status: 'success')
|
||||
|
||||
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
|
||||
|
@ -105,7 +105,7 @@ describe API::API, api: true do
|
|||
end
|
||||
|
||||
it "returns status for CI when pipeline is created" do
|
||||
project.ensure_pipeline(project.repository.commit.sha, 'master')
|
||||
project.ensure_pipeline('master', project.repository.commit.sha)
|
||||
|
||||
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ require 'spec_helper'
|
|||
|
||||
describe API::API, api: true do
|
||||
include ApiHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:user2) { create(:user) }
|
||||
let(:non_member) { create(:user) }
|
||||
|
@ -478,6 +479,18 @@ describe API::API, api: true do
|
|||
expect(json_response['labels']).to eq(['label', 'label2'])
|
||||
end
|
||||
|
||||
it "sends notifications for subscribers of newly added labels" do
|
||||
label = project.labels.first
|
||||
label.toggle_subscription(user2)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
post 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 api("/projects/#{project.id}/issues", user), labels: 'label, label2'
|
||||
expect(response).to have_http_status(400)
|
||||
|
@ -633,6 +646,18 @@ describe API::API, api: true do
|
|||
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)
|
||||
|
||||
perform_enqueued_jobs do
|
||||
put 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 api("/projects/#{project.id}/issues/#{issue.id}", user),
|
||||
labels: ''
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Projects::ArtifactsController do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
|
||||
let(:pipeline) do
|
||||
create(:ci_pipeline,
|
||||
project: project,
|
||||
sha: project.commit.sha,
|
||||
ref: project.default_branch,
|
||||
status: 'success')
|
||||
end
|
||||
|
||||
let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
|
||||
|
||||
describe 'GET /:project/builds/artifacts/:ref_name/browse?job=name' do
|
||||
before do
|
||||
project.team << [user, :developer]
|
||||
|
||||
login_as(user)
|
||||
end
|
||||
|
||||
def path_from_ref(
|
||||
ref = pipeline.ref, job = build.name, path = 'browse')
|
||||
latest_succeeded_namespace_project_artifacts_path(
|
||||
project.namespace,
|
||||
project,
|
||||
[ref, path].join('/'),
|
||||
job: job)
|
||||
end
|
||||
|
||||
context 'cannot find the build' do
|
||||
shared_examples 'not found' do
|
||||
it { expect(response).to have_http_status(:not_found) }
|
||||
end
|
||||
|
||||
context 'has no such ref' do
|
||||
before do
|
||||
get path_from_ref('TAIL', build.name)
|
||||
end
|
||||
|
||||
it_behaves_like 'not found'
|
||||
end
|
||||
|
||||
context 'has no such build' do
|
||||
before do
|
||||
get path_from_ref(pipeline.ref, 'NOBUILD')
|
||||
end
|
||||
|
||||
it_behaves_like 'not found'
|
||||
end
|
||||
|
||||
context 'has no path' do
|
||||
before do
|
||||
get path_from_ref(pipeline.sha, build.name, '')
|
||||
end
|
||||
|
||||
it_behaves_like 'not found'
|
||||
end
|
||||
end
|
||||
|
||||
context 'found the build and redirect' do
|
||||
shared_examples 'redirect to the build' do
|
||||
it 'redirects' do
|
||||
path = browse_namespace_project_build_artifacts_path(
|
||||
project.namespace,
|
||||
project,
|
||||
build)
|
||||
|
||||
expect(response).to redirect_to(path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with regular branch' do
|
||||
before do
|
||||
pipeline.update(ref: 'master',
|
||||
sha: project.commit('master').sha)
|
||||
|
||||
get path_from_ref('master')
|
||||
end
|
||||
|
||||
it_behaves_like 'redirect to the build'
|
||||
end
|
||||
|
||||
context 'with branch name containing slash' do
|
||||
before do
|
||||
pipeline.update(ref: 'improve/awesome',
|
||||
sha: project.commit('improve/awesome').sha)
|
||||
|
||||
get path_from_ref('improve/awesome')
|
||||
end
|
||||
|
||||
it_behaves_like 'redirect to the build'
|
||||
end
|
||||
|
||||
context 'with branch name and path containing slashes' do
|
||||
before do
|
||||
pipeline.update(ref: 'improve/awesome',
|
||||
sha: project.commit('improve/awesome').sha)
|
||||
|
||||
get path_from_ref('improve/awesome', build.name, 'file/README.md')
|
||||
end
|
||||
|
||||
it 'redirects' do
|
||||
path = file_namespace_project_build_artifacts_path(
|
||||
project.namespace,
|
||||
project,
|
||||
build,
|
||||
'README.md')
|
||||
|
||||
expect(response).to redirect_to(path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,7 +5,7 @@ module Ci
|
|||
let(:service) { ImageForBuildService.new }
|
||||
let(:project) { FactoryGirl.create(:empty_project) }
|
||||
let(:commit_sha) { '01234567890123456789' }
|
||||
let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') }
|
||||
let(:pipeline) { project.ensure_pipeline('master', commit_sha) }
|
||||
let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) }
|
||||
|
||||
describe '#execute' do
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe MergeRequests::ResolveService do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
|
||||
let(:fork_project) do
|
||||
create(:forked_project_with_submodules) do |fork_project|
|
||||
fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
|
||||
fork_project.save
|
||||
end
|
||||
end
|
||||
|
||||
let(:merge_request) do
|
||||
create(:merge_request,
|
||||
source_branch: 'conflict-resolvable', source_project: project,
|
||||
target_branch: 'conflict-start')
|
||||
end
|
||||
|
||||
let(:merge_request_from_fork) do
|
||||
create(:merge_request,
|
||||
source_branch: 'conflict-resolvable-fork', source_project: fork_project,
|
||||
target_branch: 'conflict-start', target_project: project)
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
context 'with valid params' do
|
||||
let(:params) do
|
||||
{
|
||||
sections: {
|
||||
'2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
|
||||
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
|
||||
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
|
||||
'6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
|
||||
},
|
||||
commit_message: 'This is a commit message!'
|
||||
}
|
||||
end
|
||||
|
||||
context 'when the source and target project are the same' do
|
||||
before do
|
||||
MergeRequests::ResolveService.new(project, user, params).execute(merge_request)
|
||||
end
|
||||
|
||||
it 'creates a commit with the message' do
|
||||
expect(merge_request.source_branch_head.message).to eq(params[:commit_message])
|
||||
end
|
||||
|
||||
it 'creates a commit with the correct parents' do
|
||||
expect(merge_request.source_branch_head.parents.map(&:id)).
|
||||
to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
|
||||
'75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b'])
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
before do
|
||||
MergeRequests::ResolveService.new(fork_project, user, params).execute(merge_request_from_fork)
|
||||
end
|
||||
|
||||
it 'creates a commit with the message' do
|
||||
expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message])
|
||||
end
|
||||
|
||||
it 'creates a commit with the correct parents' do
|
||||
expect(merge_request_from_fork.source_branch_head.parents.map(&:id)).
|
||||
to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813',
|
||||
target_head])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a resolution is missing' do
|
||||
let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } }
|
||||
let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
|
||||
|
||||
it 'raises a MissingResolution error' do
|
||||
expect { service.execute(merge_request) }.
|
||||
to raise_error(Gitlab::Conflict::File::MissingResolution)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -26,9 +26,9 @@ RSpec.configure do |config|
|
|||
config.verbose_retry = true
|
||||
config.display_try_failure_messages = true
|
||||
|
||||
config.include Devise::TestHelpers, type: :controller
|
||||
config.include LoginHelpers, type: :feature
|
||||
config.include LoginHelpers, type: :request
|
||||
config.include Devise::TestHelpers, type: :controller
|
||||
config.include Warden::Test::Helpers, type: :request
|
||||
config.include LoginHelpers, type: :feature
|
||||
config.include StubConfiguration
|
||||
config.include EmailHelpers
|
||||
config.include TestEnv
|
||||
|
|
|
@ -3,30 +3,57 @@
|
|||
# Requires a context containing:
|
||||
# subject { Issue or MergeRequest }
|
||||
shared_examples 'a Taskable' do
|
||||
before do
|
||||
subject.description = <<-EOT.strip_heredoc
|
||||
* [ ] Task 1
|
||||
* [x] Task 2
|
||||
* [x] Task 3
|
||||
* [ ] Task 4
|
||||
* [ ] Task 5
|
||||
EOT
|
||||
end
|
||||
|
||||
it 'returns the correct task status' do
|
||||
expect(subject.task_status).to match('5 tasks')
|
||||
expect(subject.task_status).to match('2 completed')
|
||||
expect(subject.task_status).to match('3 remaining')
|
||||
end
|
||||
|
||||
describe '#tasks?' do
|
||||
it 'returns true when object has tasks' do
|
||||
expect(subject.tasks?).to eq true
|
||||
describe 'with multiple tasks' do
|
||||
before do
|
||||
subject.description = <<-EOT.strip_heredoc
|
||||
* [ ] Task 1
|
||||
* [x] Task 2
|
||||
* [x] Task 3
|
||||
* [ ] Task 4
|
||||
* [ ] Task 5
|
||||
EOT
|
||||
end
|
||||
|
||||
it 'returns false when object has no tasks' do
|
||||
subject.description = 'Now I have no tasks'
|
||||
expect(subject.tasks?).to eq false
|
||||
it 'returns the correct task status' do
|
||||
expect(subject.task_status).to match('2 of')
|
||||
expect(subject.task_status).to match('5 tasks completed')
|
||||
end
|
||||
|
||||
describe '#tasks?' do
|
||||
it 'returns true when object has tasks' do
|
||||
expect(subject.tasks?).to eq true
|
||||
end
|
||||
|
||||
it 'returns false when object has no tasks' do
|
||||
subject.description = 'Now I have no tasks'
|
||||
expect(subject.tasks?).to eq false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with an incomplete task' do
|
||||
before do
|
||||
subject.description = <<-EOT.strip_heredoc
|
||||
* [ ] Task 1
|
||||
EOT
|
||||
end
|
||||
|
||||
it 'returns the correct task status' do
|
||||
expect(subject.task_status).to match('0 of')
|
||||
expect(subject.task_status).to match('1 task completed')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with a complete task' do
|
||||
before do
|
||||
subject.description = <<-EOT.strip_heredoc
|
||||
* [x] Task 1
|
||||
EOT
|
||||
end
|
||||
|
||||
it 'returns the correct task status' do
|
||||
expect(subject.task_status).to match('1 of')
|
||||
expect(subject.task_status).to match('1 task completed')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ module TestEnv
|
|||
# When developing the seed repository, comment out the branch you will modify.
|
||||
BRANCH_SHA = {
|
||||
'empty-branch' => '7efb185',
|
||||
'ends-with.json' => '98b0d8b3',
|
||||
'ends-with.json' => '98b0d8b',
|
||||
'flatten-dir' => 'e56497b',
|
||||
'feature' => '0b4bc9a',
|
||||
'feature_conflict' => 'bb5206f',
|
||||
|
@ -37,9 +37,10 @@ module TestEnv
|
|||
# need to keep all the branches in sync.
|
||||
# We currently only need a subset of the branches
|
||||
FORKED_BRANCH_SHA = {
|
||||
'add-submodule-version-bump' => '3f547c08',
|
||||
'master' => '5937ac0',
|
||||
'remove-submodule' => '2a33e0c0'
|
||||
'add-submodule-version-bump' => '3f547c0',
|
||||
'master' => '5937ac0',
|
||||
'remove-submodule' => '2a33e0c',
|
||||
'conflict-resolvable-fork' => '404fa3f'
|
||||
}
|
||||
|
||||
# Test environment
|
||||
|
@ -117,22 +118,7 @@ module TestEnv
|
|||
system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
|
||||
end
|
||||
|
||||
Dir.chdir(repo_path) do
|
||||
branch_sha.each do |branch, sha|
|
||||
# Try to reset without fetching to avoid using the network.
|
||||
reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha})
|
||||
unless system(*reset)
|
||||
if system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
|
||||
unless system(*reset)
|
||||
raise 'The fetched test seed '\
|
||||
'does not contain the required revision.'
|
||||
end
|
||||
else
|
||||
raise 'Could not fetch test seed repository.'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
set_repo_refs(repo_path, branch_sha)
|
||||
|
||||
# We must copy bare repositories because we will push to them.
|
||||
system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
|
||||
|
@ -144,6 +130,7 @@ module TestEnv
|
|||
FileUtils.mkdir_p(target_repo_path)
|
||||
FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
|
||||
FileUtils.chmod_R 0755, target_repo_path
|
||||
set_repo_refs(target_repo_path, BRANCH_SHA)
|
||||
end
|
||||
|
||||
def repos_path
|
||||
|
@ -160,6 +147,7 @@ module TestEnv
|
|||
FileUtils.mkdir_p(target_repo_path)
|
||||
FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
|
||||
FileUtils.chmod_R 0755, target_repo_path
|
||||
set_repo_refs(target_repo_path, FORKED_BRANCH_SHA)
|
||||
end
|
||||
|
||||
# When no cached assets exist, manually hit the root path to create them
|
||||
|
@ -209,4 +197,23 @@ module TestEnv
|
|||
def git_env
|
||||
{ 'GIT_TEMPLATE_DIR' => '' }
|
||||
end
|
||||
|
||||
def set_repo_refs(repo_path, branch_sha)
|
||||
Dir.chdir(repo_path) do
|
||||
branch_sha.each do |branch, sha|
|
||||
# Try to reset without fetching to avoid using the network.
|
||||
reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha})
|
||||
unless system(*reset)
|
||||
if system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
|
||||
unless system(*reset)
|
||||
raise 'The fetched test seed '\
|
||||
'does not contain the required revision.'
|
||||
end
|
||||
else
|
||||
raise 'Could not fetch test seed repository.'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'projects/merge_requests/edit.html.haml' do
|
||||
include Devise::TestHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
let(:fork_project) { create(:project, forked_from_project: project) }
|
||||
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
|
||||
|
||||
let(:closed_merge_request) do
|
||||
create(:closed_merge_request,
|
||||
source_project: fork_project,
|
||||
target_project: project,
|
||||
author: user)
|
||||
end
|
||||
|
||||
before do
|
||||
assign(:project, project)
|
||||
assign(:merge_request, closed_merge_request)
|
||||
|
||||
allow(view).to receive(:can?).and_return(true)
|
||||
allow(view).to receive(:current_user)
|
||||
.and_return(User.find(closed_merge_request.author_id))
|
||||
end
|
||||
|
||||
context 'when a merge request without fork' do
|
||||
it "shows editable fields" do
|
||||
unlink_project.execute
|
||||
closed_merge_request.reload
|
||||
|
||||
render
|
||||
|
||||
expect(rendered).to have_field('merge_request[title]')
|
||||
expect(rendered).to have_field('merge_request[description]')
|
||||
expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
|
||||
expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
|
||||
expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a merge request with an existing source project is closed' do
|
||||
it "shows editable fields" do
|
||||
render
|
||||
|
||||
expect(rendered).to have_field('merge_request[title]')
|
||||
expect(rendered).to have_field('merge_request[description]')
|
||||
expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
|
||||
expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
|
||||
expect(rendered).to have_selector('#merge_request_target_branch', visible: false)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,44 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'projects/merge_requests/show.html.haml' do
|
||||
include Devise::TestHelpers
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
let(:fork_project) { create(:project, forked_from_project: project) }
|
||||
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
|
||||
|
||||
let(:closed_merge_request) do
|
||||
create(:closed_merge_request,
|
||||
source_project: fork_project,
|
||||
target_project: project,
|
||||
author: user)
|
||||
end
|
||||
|
||||
before do
|
||||
assign(:project, project)
|
||||
assign(:merge_request, closed_merge_request)
|
||||
assign(:commits_count, 0)
|
||||
|
||||
allow(view).to receive(:can?).and_return(true)
|
||||
end
|
||||
|
||||
context 'when the merge request is closed' do
|
||||
it 'shows the "Reopen" button' do
|
||||
render
|
||||
|
||||
expect(rendered).to have_css('a', visible: true, text: 'Reopen')
|
||||
expect(rendered).to have_css('a', visible: false, text: 'Close')
|
||||
end
|
||||
|
||||
it 'does not show the "Reopen" button when the source project does not exist' do
|
||||
unlink_project.execute
|
||||
closed_merge_request.reload
|
||||
|
||||
render
|
||||
|
||||
expect(rendered).to have_css('a', visible: false, text: 'Reopen')
|
||||
expect(rendered).to have_css('a', visible: false, text: 'Close')
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue