Commit 2b075561 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 31156-environments-vue-service

* master: (91 commits)
  Move api lint out of static analysis job
  Fix project tree saver and fork spec failures
  Fix lazy error handling of cron parser
  Use gitlab-workhorse 2.0.0
  Revert to real click seeing as that was a bug with only the original branch
  Fixed issue_sidebar_spec.rb click as true click cannot hit the right element and removed sleep
  find and match first dropdown before clicking
  Elaborate on the usage of Spring
  Note Ghost user and refer to user deletion documentation
  Fix label creation from issuable for subgroup projects
  Detect already enabled DeployKeys in EnableDeployKeyService
  Extract common parts of snippet and blob pages into partial
  update article date
  link ldap-ee article from auth index
  add ldap article and changes from !10299
  Fix misaligned buttons in wiki pages
  Improve pipelines_finder.rb
  Improve documentation
  Correct typo in pipelines_spec.rb
  Avoid using sample
  ...
parents a5989900 e14ca539
......@@ -264,11 +264,15 @@ spinach mysql 9 10: *spinach-knapsack-mysql
static-analysis:
<<: *ruby-static-analysis
<<: *dedicated-runner
<<: *except-docs
stage: test
script:
- scripts/static-analysis
docs:check:links:
# Documentation checks:
# - Check validity of relative links
# - Make sure cURL examples in API docs use the full switches
docs lint:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
......@@ -276,6 +280,7 @@ docs:check:links:
dependencies: []
before_script: []
script:
- scripts/lint-doc.sh
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
......
......@@ -19,12 +19,10 @@
});
};
Milestone.sortIssues = function(data) {
var sort_issues_url;
sort_issues_url = location.href + "/sort_issues";
Milestone.sortIssues = function(url, data) {
return $.ajax({
type: "PUT",
url: sort_issues_url,
url,
data: data,
success: function(_data) {
return Milestone.successCallback(_data);
......@@ -36,12 +34,10 @@
});
};
Milestone.sortMergeRequests = function(data) {
var sort_mr_url;
sort_mr_url = location.href + "/sort_merge_requests";
Milestone.sortMergeRequests = function(url, data) {
return $.ajax({
type: "PUT",
url: sort_mr_url,
url,
data: data,
success: function(_data) {
return Milestone.successCallback(_data);
......@@ -81,42 +77,55 @@
};
function Milestone() {
var oldMouseStart;
this.issuesSortEndpoint = $('#tab-issues').data('sort-endpoint');
this.mergeRequestsSortEndpoint = $('#tab-merge-requests').data('sort-endpoint');
this.bindIssuesSorting();
this.bindMergeRequestSorting();
this.bindTabsSwitching();
// Load merge request tab if it is active
// merge request tab is active based on different conditions in the backend
this.loadTab($('.js-milestone-tabs .active a'));
this.loadInitialTab();
}
Milestone.prototype.bindIssuesSorting = function() {
if (!this.issuesSortEndpoint) return;
$('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) {
this.createSortable(el, {
group: 'issue-list',
listEls: $('.issues-sortable-list'),
fieldName: 'issue',
sortCallback: Milestone.sortIssues,
sortCallback: (data) => {
Milestone.sortIssues(this.issuesSortEndpoint, data);
},
updateCallback: Milestone.updateIssue,
});
}.bind(this));
};
Milestone.prototype.bindTabsSwitching = function() {
return $('a[data-toggle="tab"]').on('show.bs.tab', function(e) {
var currentTabClass, previousTabClass;
currentTabClass = $(e.target).data('show');
previousTabClass = $(e.relatedTarget).data('show');
$(previousTabClass).hide();
$(currentTabClass).removeClass('hidden');
return $(currentTabClass).show();
return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
const $target = $(e.target);
location.hash = $target.attr('href');
this.loadTab($target);
});
};
Milestone.prototype.bindMergeRequestSorting = function() {
if (!this.mergeRequestsSortEndpoint) return;
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) {
this.createSortable(el, {
group: 'merge-request-list',
listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"),
fieldName: 'merge_request',
sortCallback: Milestone.sortMergeRequests,
sortCallback: (data) => {
Milestone.sortMergeRequests(this.mergeRequestsSortEndpoint, data);
},
updateCallback: Milestone.updateMergeRequest,
});
}.bind(this));
......@@ -169,6 +178,35 @@
});
};
Milestone.prototype.loadInitialTab = function() {
const $target = $(`.js-milestone-tabs a[href="${location.hash}"]`);
if ($target.length) {
$target.tab('show');
}
};
Milestone.prototype.loadTab = function($target) {
const endpoint = $target.data('endpoint');
const tabElId = $target.attr('href');
if (endpoint && !$target.hasClass('is-loaded')) {
$.ajax({
url: endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error loading milestone tab'))
.done((data) => {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
if (tabElId === '#tab-merge-requests') {
this.bindMergeRequestSorting();
}
});
}
};
return Milestone;
})();
}).call(window);
......@@ -6,7 +6,13 @@
}
.limit-container-width {
.detail-page-header {
.detail-page-header,
.page-content-header,
.commit-box,
.info-well,
.notes,
.commit-ci-menu,
.files-changed {
@extend .fixed-width-container;
}
......@@ -36,8 +42,7 @@
}
.diffs {
.mr-version-controls,
.files-changed {
.mr-version-controls {
@extend .fixed-width-container;
}
}
......
......@@ -133,3 +133,55 @@
right: 160px;
}
}
.flex-project-members-panel {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
@media (max-width: $screen-sm-min) {
display: block;
.flex-project-title {
vertical-align: top;
display: inline-block;
max-width: 90%;
}
}
.flex-project-title {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.badge {
height: 17px;
line-height: 16px;
margin-right: 5px;
padding-top: 1px;
padding-bottom: 1px;
}
.flex-project-members-form {
flex-wrap: nowrap;
white-space: nowrap;
margin-left: auto;
}
}
.panel {
.panel-heading {
.badge {
margin-top: 0;
}
@media (max-width: $screen-sm-min) {
.badge {
margin-right: 0;
margin-left: 0;
}
}
}
}
\ No newline at end of file
......@@ -71,7 +71,6 @@
.nav-controls {
width: auto;
min-width: 50%;
white-space: nowrap;
}
}
......
module MilestoneActions
extend ActiveSupport::Concern
def merge_requests
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_merge_requests_tab", {
merge_requests: @milestone.merge_requests,
show_project_name: true
})
end
end
end
def participants
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_participants_tab", {
users: @milestone.participants
})
end
end
end
def labels
respond_to do |format|
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_labels_tab", {
labels: @milestone.labels
})
end
end
end
private
def tabs_json(partial, data = {})
{
html: view_to_html_string(partial, data)
}
end
def milestone_redirect_path
if @project
namespace_project_milestone_path(@project.namespace, @project, @milestone)
else
group_milestone_path(@group, @milestone.safe_title, title: @milestone.title)
end
end
end
class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions
before_action :group_projects
before_action :milestone, only: [:show, :update]
before_action :milestone, only: [:show, :update, :merge_requests, :participants, :labels]
before_action :authorize_admin_milestones!, only: [:new, :create, :update]
def index
......
class Projects::MilestonesController < Projects::ApplicationController
include MilestoneActions
before_action :module_enabled
before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests]
before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests, :merge_requests, :participants, :labels]
# Allow read any milestone
before_action :authorize_read_milestone!
# Allow admin milestone
before_action :authorize_admin_milestone!, except: [:index, :show]
before_action :authorize_admin_milestone!, except: [:index, :show, :merge_requests, :participants, :labels]
respond_to :html
......
......@@ -9,19 +9,19 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
@pipelines = PipelinesFinder
.new(project)
.execute(scope: @scope)
.new(project, scope: @scope)
.execute
.page(params[:page])
.per(30)
@running_count = PipelinesFinder
.new(project).execute(scope: 'running').count
.new(project, scope: 'running').execute.count
@pending_count = PipelinesFinder
.new(project).execute(scope: 'pending').count
.new(project, scope: 'pending').execute.count
@finished_count = PipelinesFinder
.new(project).execute(scope: 'finished').count
.new(project, scope: 'finished').execute.count
@pipelines_count = PipelinesFinder
.new(project).execute.count
......
class PipelinesFinder
attr_reader :project, :pipelines
attr_reader :project, :pipelines, :params
def initialize(project)
ALLOWED_INDEXED_COLUMNS = %w[id status ref user_id].freeze
def initialize(project, params = {})
@project = project
@pipelines = project.pipelines
@params = params
end
def execute(scope: nil)
scoped_pipelines =
case scope
when 'running'
pipelines.running
when 'pending'
pipelines.pending
when 'finished'
pipelines.finished
when 'branches'
from_ids(ids_for_ref(branches))
when 'tags'
from_ids(ids_for_ref(tags))
else
pipelines
end
scoped_pipelines.order(id: :desc)
def execute
items = pipelines
items = by_scope(items)
items = by_status(items)
items = by_ref(items)
items = by_name(items)
items = by_username(items)
items = by_yaml_errors(items)
sort_items(items)
end
private
......@@ -43,4 +37,78 @@ class PipelinesFinder
def tags
project.repository.tag_names
end
def by_scope(items)
case params[:scope]
when 'running'
items.running
when 'pending'
items.pending
when 'finished'
items.finished
when 'branches'
from_ids(ids_for_ref(branches))
when 'tags'
from_ids(ids_for_ref(tags))
else
items
end
end
def by_status(items)
return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status])
items.where(status: params[:status])
end
def by_ref(items)
if params[:ref].present?
items.where(ref: params[:ref])
else
items
end
end
def by_name(items)
if params[:name].present?
items.joins(:user).where(users: { name: params[:name] })
else
items
end
end
def by_username(items)
if params[:username].present?
items.joins(:user).where(users: { username: params[:username] })
else
items
end
end
def by_yaml_errors(items)
case Gitlab::Utils.to_boolean(params[:yaml_errors])
when true
items.where("yaml_errors IS NOT NULL")
when false
items.where("yaml_errors IS NULL")
else
items
end
end
def sort_items(items)
order_by = if ALLOWED_INDEXED_COLUMNS.include?(params[:order_by])
params[:order_by]
else
:id
end
sort = if params[:sort] =~ /\A(ASC|DESC)\z/i
params[:sort]
else
:desc
end
items.order(order_by => sort)
end
end
......@@ -115,4 +115,28 @@ module MilestonesHelper
end
end
end
def milestone_merge_request_tab_path(milestone)
if @project
merge_requests_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
merge_requests_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
end
end
def milestone_participants_tab_path(milestone)
if @project
participants_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
participants_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
end
end
def milestone_labels_tab_path(milestone)
if @project
labels_namespace_project_milestone_path(@project.namespace, @project, milestone, format: :json)
elsif @group
labels_group_milestone_path(@group, milestone.safe_title, title: milestone.title, format: :json)
end
end
end
......@@ -4,7 +4,10 @@ module Projects
key = accessible_keys.find_by(id: params[:key_id] || params[:id])
return unless key
project.deploy_keys << key
unless project.deploy_keys.include?(key)
project.deploy_keys << key
end
key
end
......
......@@ -175,11 +175,7 @@
.panel-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects:
%ul
%li All user content like authored issues, snippets, comments will be removed
- rp = @user.personal_projects.count
- unless rp.zero?
%li #{pluralize rp, 'personal project'} will be removed and cannot be restored
= render 'users/deletion_guidance', user: @user
%br
= link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove"
- else
......
......@@ -118,11 +118,7 @@
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p
Deleting an account has the following effects:
%ul
%li All user content like authored issues, snippets, comments will be removed
- rp = current_user.personal_projects.count
- unless rp.zero?
%li #{pluralize rp, 'personal project'} will be removed and cannot be restored
= render 'users/deletion_guidance', user: current_user
= link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
- else
- if @user.solo_owned_groups.present?
......
- blame = local_assigns.fetch(:blame, false)
.js-file-title.file-title-flex-parent
.file-header-content
= blob_icon blob.mode, blob.name
%strong.file-title-name
= blob.name
= copy_file_path_button(blob.path)
%small
= number_to_human_size(blob.raw_size)
= render 'projects/blob/header_content', blob: blob
.file-actions.hidden-xs
= render 'projects/blob/viewer_switcher', blob: blob unless blame
......
.file-header-content
= blob_icon blob.mode, blob.name
%strong.file-title-name
= blob.name
= copy_file_path_button(blob.path)
%small
= number_to_human_size(blob.raw_size)
......@@ -48,7 +48,7 @@
- if @build.merge_request
%p.build-detail-row
%span.build-light-text Merge Request:
= link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request)
= link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'bold'
- if @build.duration
%p.build-detail-row
%span.build-light-text Duration:
......
- @no_container = true
- container_class = !fluid_layout && diff_view == :inline ? 'container-limited' : ''
- limited_container_width = fluid_layout || diff_view == :inline ? '' : 'limit-container-width'
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description
= render "projects/commits/head"
%div{ class: container_class }
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
- if @commit.status
= render "ci_menu"
......
......@@ -6,6 +6,12 @@
%p
Add a new member to
%strong= @project.name
- else
%p
Members can be added by project
%i Masters
or
%i Owners
.col-lg-9
.light.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
......
.panel.panel-default
.panel-heading
Members with access to
%strong= @project.name
.panel-heading.flex-project-members-panel
%span.flex-project-title
Members of
%strong
#{@project.name}
%span.badge= @project_members.total_count
= form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
= form_tag namespace_project_settings_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form flex-project-members-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
%button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
......
......@@ -136,7 +136,7 @@
- selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } }
%button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project) } }
%span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true')
......
.text-center.prepend-top-default
= icon('spin spinner 2x', 'aria-hidden': 'true', 'aria-label': 'Loading tab content')
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left')
.fade-right= icon('angle-right')
%ul.nav-links.scrolling-tabs
%ul.nav-links.scrolling-tabs.js-milestone-tabs
- if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Issues
%span.badge= milestone.issues_visible_to_user(current_user).size
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests
%span.badge= milestone.merge_requests.size
- else
%li.active
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-show' => '.tab-merge-requests-buttons' do
= link_to '#tab-merge-requests', 'data-toggle' => 'tab', 'data-endpoint': milestone_merge_request_tab_path(milestone) do
Merge Requests
%span.badge= milestone.merge_requests.size
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
= link_to '#tab-participants', 'data-toggle' => 'tab', 'data-endpoint': milestone_participants_tab_path(milestone) do
Participants
%span.badge= milestone.participants.count
%li
= link_to '#tab-labels', 'data-toggle' => 'tab' do
= link_to '#tab-labels', 'data-toggle' => 'tab', 'data-endpoint': milestone_labels_tab_path(milestone) do
Labels
%span.badge= milestone.labels.count
......@@ -30,14 +30,18 @@
.tab-content.milestone-content
- if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
.tab-pane.active#tab-issues
.tab-pane.active#tab-issues{ data: { sort_endpoint: (sort_issues_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } }
= render 'shared/milestones/issues_tab', issues: milestone.issues_visible_to_user(current_user).include_associations, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } }
-# loaded async
= render "shared/milestones/tab_loading"
- else
.tab-pane.active#tab-merge-requests
= render 'shared/milestones/merge_requests_tab', merge_requests: milestone.merge_requests, show_project_name: show_project_name, show_full_project_name: show_full_project_name
.tab-pane.active#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } }
-# loaded async
= render "shared/milestones/tab_loading"
.tab-pane#tab-participants
= render 'shared/milestones/participants_tab', users: milestone.participants
-# loaded async
= render "shared/milestones/tab_loading"
.tab-pane#tab-labels
= render 'shared/milestones/labels_tab', labels: milestone.labels
-# loaded async
= render "shared/milestones/tab_loading"
- blob = @snippet.blob
.js-file-title.file-title-flex-parent
.file-header-content
= blob_icon blob.mode, blob.path
%strong.file-title-name
= blob.path
= copy_file_path_button(blob.path)
%small
= number_to_human_size(blob.raw_size)
= render 'projects/blob/header_content', blob: blob
.file-actions.hidden-xs
= render 'projects/blob/viewer_switcher', blob: blob
......
- user = local_assigns.fetch(:user)
%ul
%li
%p
Certain user content will be moved to a system-wide "Ghost User" in order to maintain content for posterity. For further information, please refer to the
= link_to 'user account deletion documentation.', help_page_path("user/profile/account/delete_account", anchor: "associated-records")
- personal_projects_count = user.personal_projects.count
- unless personal_projects_count.zero?
%li #{pluralize(personal_projects_count, 'personal project')} will be removed and cannot be restored
---
title: Improved UX on project members settings view
merge_request:
author:
---
title: 'API: Add parameters to allow filtering project pipelines'
merge_request: 9367
author: dosuken123
---
title: Detect already enabled DeployKeys in EnableDeployKeyService
merge_request:
author:
---
title: Fix label creation from issuable for subgroup projects
merge_request:
author:
---
title: Fix error on CI/CD Settings page related to invalid pipeline trigger
merge_request: 10948
author: dosuken123
---
title: Note Ghost user and refer to user deletion documentation
merge_request:
author:
---
title: Fix misaligned buttons in wiki pages
merge_request: 11043
author:
---
title: Load milestone tabs asynchronously to increase initial load performance
merge_request:
author:
---
title: Side-by-side view in commits correcly expands full window width
merge_request:
author:
---
title: Make MR link in build sidebar bold
merge_request:
author:
......@@ -10,7 +10,13 @@ scope(path: 'groups/*group_id',
end
resource :avatar, only: [:destroy]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create] do
member do
get :merge_requests
get :participants
get :labels
end
end
resources :labels, except: [:show] do
post :toggle_subscription, on: :member
......
......@@ -207,6 +207,9 @@ constraints(ProjectUrlConstrainer.new) do
member do
put :sort_issues
put :sort_merge_requests
get :merge_requests
get :participants
get :labels
end
end
......
......@@ -11,6 +11,14 @@ GET /projects/:id/pipelines
| Attribute | Type | Required | Description |
|-----------|---------|----------|---------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `scope` | string | no | The scope of pipelines, one of: `running`, `pending`, `finished`, `branches`, `tags` |
| `status` | string | no | The status of pipelines, one of: `running`, `pending`, `success`, `failed`, `canceled`, `skipped` |
| `ref` | string | no | The ref of pipelines |
| `yaml_errors`| boolean | no | Returns pipelines with invalid configurations |
| `name`| string | no | The name of the user who triggered pipelines |
| `username`| string | no | The username of the user who triggered pipelines |
| `order_by`| string | no | Order pipelines by `id`, `status`, `ref`, or `user_id` (default: `id`) |
| `sort` | string | no | Sort pipelines in `asc` or `desc` order (default: `desc`) |
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/pipelines"
......
This diff is collapsed.
......@@ -7,6 +7,11 @@ to provide the community with guidance on specific processes to achieve certain
They are written by members of the GitLab Team and by
[Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
## Authentication
- **LDAP**
- [How to configure LDAP with GitLab CE](how_to_configure_ldap_gitlab_ce/index.md)
## GitLab Pages
- **GitLab Pages from A to Z**
......
......@@ -31,16 +31,26 @@ files it can find, also the ones in `/tmp`
To run a single test file you can use:
- `bundle exec rspec spec/controllers/commit_controller_spec.rb` for a rspec test
- `bundle exec spinach features/project/issues/milestones.feature` for a spinach test
- `bin/rspec spec/controllers/commit_controller_spec.rb` for a rspec test
- `bin/spinach features/project/issues/milestones.feature` for a spinach test
To run several tests inside one directory:
- `bundle exec rspec spec/requests/api/` for the rspec tests if you want to test API only
- `bundle exec spinach features/profile/` for the spinach tests if you want to test only profile pages
- `bin/rspec spec/requests/api/` for the rspec tests if you want to test API only
- `bin/spinach features/profile/` for the spinach tests if you want to test only profile pages
If you want to use [Spring](https://github.com/rails/spring) set
`ENABLE_SPRING=1` in your environment.
### Speed-up tests, rake tasks, and migrations
[Spring](https://github.com/rails/spring) is a Rails application preloader. It
speeds up development by keeping your application running in the background so
you don't need to boot it every time you run a test, rake task or migration.
If you want to use it, you'll need to export the `ENABLE_SPRING` environment
variable to `1`:
```
export ENABLE_SPRING=1
```
## Compile Frontend Assets
......
......@@ -18,6 +18,8 @@ This page gathers all the resources for the topic **Authentication** within GitL
- [LDAP (Enterprise Edition)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html)
- [Enforce Two-factor Authentication (2FA)](../../security/two_factor_authentication.md#enforce-two-factor-authentication-2fa)
- **Articles:**
- [How to Configure LDAP with GitLab CE](../../articles/how_to_configure_ldap_gitlab_ce/index.md)
- [How to Configure LDAP with GitLab EE](https://docs.gitlab.com/articles/how_to_configure_ldap_gitlab_ee/)
- [Feature Highlight: LDAP Integration](https://about.gitlab.com/2014/07/10/feature-highlight-ldap-sync/)
- [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/ldap/debugging_ldap.html)
- **Integrations:**
......
......@@ -38,6 +38,7 @@ Feature: Group Milestones
And I should see the "feature" label
And I should see the project name in the Issue row
@javascript
Scenario: I should see the Labels tab
Given Group has projects with milestones
When I visit group "Owned" page
......
......@@ -7,14 +7,6 @@ Feature: Project Milestone
And milestone has issue "Bugfix1" with labels: "bug", "feature"
And milestone has issue "Bugfix2" with labels: "bug", "enhancement"
@javascript
Scenario: Listing issues from issues tab
Given I visit project "Shop" milestones page
And I click link "v2.2"
Then I should see the labels "bug", "enhancement" and "feature"
And I should see the "bug" label listed only once
@javascript
Scenario: Listing labels from labels tab
Given I visit project "Shop" milestones page
......
class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
include WaitForAjax
include SharedAuthentication
include SharedPaths
include SharedGroup
......@@ -90,6 +91,8 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end
step 'I should see the list of labels' do
wait_for_ajax
page.within('#tab-labels') do
expect(page).to have_content 'bug'
expect(page).to have_content 'feature'
......
......@@ -2,6 +2,7 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
include SharedPaths
include WaitForAjax
step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do
project = Project.find_by(name: "Shop")
......@@ -34,6 +35,8 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps
end
step 'I should see the labels "bug", "enhancement" and "feature"' do
wait_for_ajax
page.within('#tab-issues') do
expect(page).to have_content 'bug'
expect(page).to have_content 'enhancement'
......
......@@ -14,13 +14,23 @@ module API
end
params do
use :pagination
optional :scope, type: String, values: %w(running branches tags),
desc: 'Either running, branches, or tags'
optional :scope, type: String, values: %w[running pending finished branches tags],
desc: 'The scope of pipelines'
optional :status, type: String, values: HasStatus::AVAILABLE_STATUSES,
desc: 'The status of pipelines'
optional :ref, type: String, desc: 'The ref of pipelines'
optional :yaml_errors, type: Boolean, desc: 'Returns pipelines with invalid configurations'
optional :name, type: String, desc: 'The name of the user who triggered pipelines'
optional :username, type: String, desc: 'The username of the user who triggered pipelines'
optional :order_by, type: String, values: PipelinesFinder::ALLOWED_INDEXED_COLUMNS, default: 'id',
desc: 'Order pipelines'
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Sort pipelines'
end
get ':id/pipelines' do
authorize! :read_pipeline, user_project
pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
pipelines = PipelinesFinder.new(user_project, params).execute
present paginate(pipelines), with: Entities::PipelineBasic
end
......
......@@ -21,7 +21,7 @@ module API
get ':id/pipelines' do
authorize! :read_pipeline, user_project
pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
pipelines = PipelinesFinder.new(user_project, scope: params[:scope]).execute
present paginate(pipelines), with: ::API::Entities::Pipeline
end
end
......
......@@ -6,7 +6,7 @@ module Gitlab
def initialize(cron, cron_timezone = 'UTC')
@cron = cron
@cron_timezone = cron_timezone
@cron_timezone = ActiveSupport::TimeZone.find_tzinfo(cron_timezone).name
end
def next_time_from(time)
......@@ -24,8 +24,23 @@ module Gitlab
private
# NOTE:
# cron_timezone can only accept timezones listed in TZInfo::Timezone.
# Aliases of Timezones from ActiveSupport::TimeZone are NOT accepted,
# because Rufus::Scheduler only supports TZInfo::Timezone.
#
# For example, those codes have the same effect.
# Time.zone = 'Pacific Time (US & Canada)' (ActiveSupport::TimeZone)
# Time.zone = 'America/Los_Angeles' (TZInfo::Timezone)
#
# However, try_parse_cron only accepts the latter format.
# try_parse_cron('* * * * *', 'Pacific Time (US & Canada)') -> Doesn't work
# try_parse_cron('* * * * *', 'America/Los_Angeles') -> Works
# If you want to know more, please take a look
# https://github.com/rails/rails/blob/master/activesupport/lib/active_support/values/time_zone.rb
def try_parse_cron(cron, cron_timezone)
Rufus::Scheduler.parse("#{cron} #{cron_timezone}")
cron_line = Rufus::Scheduler.parse("#{cron} #{cron_timezone}")
cron_line if cron_line.is_a?(Rufus::Scheduler::CronLine)
rescue
# noop
end
......
......@@ -84,6 +84,7 @@ excluded_attributes:
- :import_jid
- :id
- :star_count
- :last_activity_at
snippets:
- :expired_at
merge_request_diff:
......
......@@ -9,7 +9,6 @@ tasks = [
%w[bundle exec rake scss_lint],
%w[bundle exec rake brakeman],
%w[bundle exec license_finder],
%w[scripts/lint-doc.sh],
%w[yarn run eslint],
%w[bundle exec rubocop --require rubocop-rspec]
]
......
......@@ -6,6 +6,16 @@ describe Groups::MilestonesController do
let(:project2) { create(:empty_project, group: group) }
let(:user) { create(:user) }
let(:title) { '肯定不是中文的问题' }
let(:milestone) do
project_milestone = create(:milestone, project: project)
GroupMilestone.build(
group,
[project],
project_milestone.title
)
end
let(:milestone_path) { group_milestone_path(group, milestone.safe_title, title: milestone.title) }
before do
sign_in(user)
......@@ -14,6 +24,8 @@ describe Groups::MilestonesController do
controller.instance_variable_set(:@group, group)
end
it_behaves_like 'milestone tabs'
describe "#create" do
it "creates group milestone with Chinese title" do
post :create,
......
......@@ -7,6 +7,7 @@ describe Projects::MilestonesController do
let(:issue) { create(:issue, project: project, milestone: milestone) }
let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
let(:milestone_path) { namespace_project_milestone_path }
before do
sign_in(user)
......@@ -14,6 +15,8 @@ describe Projects::MilestonesController do
controller.instance_variable_set(:@project, project)
end
it_behaves_like 'milestone tabs'
describe "#show" do
render_views
......
......@@ -3,7 +3,8 @@ require 'rails_helper'
feature 'Issue Sidebar', feature: true do
include MobileHelpers
let(:project) { create(:project, :public) }
let(:group) { create(:group, :nested) }
let(:project) { create(:project, :public, namespace: group) }
let(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)}
let!(:label) { create(:label, project: project, title: 'bug') }
......@@ -151,9 +152,7 @@ feature 'Issue Sidebar', feature: true do
end
def open_issue_sidebar
page.within('aside.right-sidebar.right-sidebar-collapsed') do
find('.js-sidebar-toggle').click
sleep 1
end
find('aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle').click
find('aside.right-sidebar.right-sidebar-expanded')
end
end
......@@ -20,7 +20,6 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content('Target branch')
first('.js-source-branch').click
first('.dropdown-source-branch .dropdown-content')
find('.dropdown-source-branch .dropdown-content a', match: :first).click
expect(page).to have_content "b83d6e3"
......@@ -35,8 +34,7 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content('Target branch')
first('.js-target-branch').click
first('.dropdown-target-branch .dropdown-content')
first('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0').click
find('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0', match: :first).click
expect(page).to have_content "b83d6e3"
end
......
......@@ -86,6 +86,9 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone)
page.find("a[href='#tab-merge-requests']").click
wait_for_ajax
scroll_into_view('.milestone-content')
drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1)
......
......@@ -104,6 +104,24 @@ feature 'Triggers', feature: true, js: true do
expect(page).to have_content 'The form contains the following errors'
end
context 'when GitLab time_zone is ActiveSupport::TimeZone format' do
before do
allow(Time).to receive(:zone)
.and_return(ActiveSupport::TimeZone['Eastern Time (US & Canada)'])
end
scenario 'do fill form with valid data and save' do
find('#trigger_trigger_schedule_attributes_active').click
fill_in 'trigger_trigger_schedule_attributes_cron', with: '1 * * * *'
fill_in 'trigger_trigger_schedule_attributes_cron_timezone', with: 'UTC'
fill_in 'trigger_trigger_schedule_attributes_ref', with: 'master'
click_button 'Save trigger'
expect(page.find('.flash-notice'))
.to have_content 'Trigger was successfully updated.'
end
end
end
context 'disabling schedule' do
......
......@@ -3,50 +3,205 @@ require 'spec_helper'
describe PipelinesFinder do
let(:project) { create(:project, :repository) }
let!(:tag_pipeline) { create(:ci_pipeline, project: project, ref: 'v1.0.0') }
let!(:branch_pipeline) { create(:ci_pipeline, project: project) }
subject { described_class.new(project).execute(params) }
subject { described_class.new(project, params).execute }
describe "#execute" do
context 'when a scope is passed' do
context 'when scope is nil' do
let(:params) { { scope: nil } }
context 'when params is empty' do
let(:params) { {} }
let!(:pipelines) { create_list(:ci_pipeline, 2, project: project) }
it 'returns all pipelines' do
is_expected.to match_array(pipelines)
end
end
%w[running pending].each do |target|
context "when scope is #{target}" do
let(:params) { { scope: target } }
let!(:pipeline) { create(:ci_pipeline, project: project, status: target) }
it 'selects all pipelines' do
expect(subject.count).to be 2
expect(subject).to include tag_pipeline
expect(subject).to include branch_pipeline
it 'returns matched pipelines' do
is_expected.to eq([pipeline])
end
end
end
context 'when scope is finished' do
let(:params) { { scope: 'finished' } }
let!(:pipelines) do
[create(:ci_pipeline, project: project, status: 'success'),
create(:ci_pipeline, project: project, status: 'failed'),
create(:ci_pipeline, project: project, status: 'canceled')]
end
context 'when selecting branches' do
it 'returns matched pipelines' do
is_expected.to match_array(pipelines)
end
end
context 'when scope is branches or tags' do
let!(:pipeline_branch) { create(:ci_pipeline, project: project) }
let!(:pipeline_tag) { create(:ci_pipeline, project: project, ref: 'v1.0.0', tag: true) }
context 'when scope is branches' do
let(:params) { { scope: 'branches' } }
it 'excludes tags' do
expect(subject).not_to include tag_pipeline
expect(subject).to include branch_pipeline
it 'returns matched pipelines' do
is_expected.to eq([pipeline_branch])
end
end
context 'when selecting tags' do
context 'when scope is tags' do
let(:params) { { scope: 'tags' } }
it 'excludes branches' do
expect(subject).to include tag_pipeline
expect(subject).not_to include branch_pipeline
it 'returns matched pipelines' do
is_expected.to eq([pipeline_tag])
end
end
end
HasStatus::AVAILABLE_STATUSES.each do |target|
context "when status is #{target}" do
let(:params) { { status: target } }
let!(:pipeline) { create(:ci_pipeline, project: project, status: target) }
before do
exception_status = HasStatus::AVAILABLE_STATUSES - [target]
create(:ci_pipeline, project: project, status: exception_status.first)
end
it 'returns matched pipelines' do
is_expected.to eq([pipeline])
end
end
end
# Scoping to pending will speed up the test as it doesn't hit the FS
let(:params) { { scope: 'pending' } }
context 'when ref is specified' do
let!(:pipeline) { create(:ci_pipeline, project: project) }
context 'when ref exists' do
let(:params) { { ref: 'master' } }
it 'returns matched pipelines' do
is_expected.to eq([pipeline])
end
end
context 'when ref does not exist' do
let(:params) { { ref: 'invalid-ref' } }
it 'returns empty' do
is_expected.to be_empty
end
end
end
context 'when name is specified' do
let(:user) { create(:user) }
let!(:pipeline) { create(:ci_pipeline, project: project, user: user) }
context 'when name exists' do
let(:params) { { name: user.name } }
it 'returns matched pipelines' do
is_expected.to eq([pipeline])
end
end
context 'when name does not exist' do
let(:params) { { name: 'invalid-name' } }
it 'returns empty' do
is_expected.to be_empty
end
end
end
it 'orders in descending order on ID' do
feature_pipeline = create(:ci_pipeline, project: project, ref: 'feature')
context 'when username is specified' do
let(:user) { create(:user) }
let!(:pipeline) { create(:ci_pipeline, project: project, user: user) }
expected_ids = [feature_pipeline.id, branch_pipeline.id, tag_pipeline.id].sort.reverse
expect(subject.map(&:id)).to eq expected_ids
context 'when username exists' do
let(:params) { { username: user.username } }
it 'returns matched pipelines' do
is_expected.to eq([pipeline])
end
end
context 'when username does not exist' do
let(:params) { { username: 'invalid-username' } }
it 'returns empty' do
is_expected.to be_empty
end
end
end
context 'when yaml_errors is specified' do
let!(:pipeline1) { create(:ci_pipeline, project: project, yaml_errors: 'Syntax error') }
let!(:pipeline2) { create(:ci_pipeline, project: project) }
context 'when yaml_errors is true' do
let(:params) { { yaml_errors: true } }
it 'returns matched pipelines' do
is_expected.to eq([pipeline1])
end
end
context 'when yaml_errors is false' do
let(:params) { { yaml_errors: false } }
it 'returns matched pipelines' do
is_expected.to eq([pipeline2])
end
end
context 'when yaml_errors is invalid' do
let(:params) { { yaml_errors: "invalid-yaml_errors" } }
it 'returns all pipelines' do
is_expected.to match_array([pipeline1, pipeline2])
end
end
end
context 'when order_by and sort are specified' do
context 'when order_by user_id' do
let(:params) { { order_by: 'user_id', sort: 'asc' } }
let!(:pipelines) { create_list(:ci_pipeline, 2, project: project, user: create(:user)) }
it 'sorts as user_id: :asc' do
is_expected.to match_array(pipelines)
end
context 'when sort is invalid' do
let(:params) { { order_by: 'user_id', sort: 'invalid_sort' } }
it 'sorts as user_id: :desc' do
is_expected.to eq(pipelines.sort_by { |p| -p.user.id })
end
end
end
context 'when order_by is invalid' do
let(:params) { { order_by: 'invalid_column', sort: 'asc' } }
let!(:pipelines) { create_list(:ci_pipeline, 2, project: project) }
it 'sorts as id: :asc' do
is_expected.to eq(pipelines.sort_by { |p| p.id })
end
end
context 'when both are nil' do
let(:params) { { order_by: nil, sort: nil } }
let!(:pipelines) { create_list(:ci_pipeline, 2, project: project) }
it 'sorts as id: :desc' do
is_expected.to eq(pipelines.sort_by { |p| -p.id })
end
end
end
end
end
......@@ -60,14 +60,60 @@ describe Gitlab::Ci::CronParser do
end
end
context 'when cron_timezone is US/Pacific' do
let(:cron) { '0 0 * * *' }
let(:cron_timezone) { 'US/Pacific' }
context 'when cron_timezone is TZInfo format' do
before do
allow(Time).to receive(:zone)
.and_return(ActiveSupport::TimeZone['UTC'])
end
it_behaves_like "returns time in the future"
let(:hour_in_utc) do
ActiveSupport::TimeZone[cron_timezone]
.now.change(hour: 0).in_time_zone('UTC').hour
end
context 'when cron_timezone is US/Pacific' do
let(:cron) { '* 0 * * *' }
let(:cron_timezone) { 'US/Pacific' }
it_behaves_like "returns time in the future"
it 'converts time in server time zone' do
expect(subject.hour).to eq(hour_in_utc)
end
end
end
context 'when cron_timezone is ActiveSupport::TimeZone format' do
before do
allow(Time).to receive(:zone)
.and_return(ActiveSupport::TimeZone['UTC'])
end
let(:hour_in_utc) do
ActiveSupport::TimeZone[cron_timezone]
.now.change(hour: 0).in_time_zone('UTC').hour
end
context 'when cron_timezone is Berlin' do
let(:cron) { '* 0 * * *' }
let(:cron_timezone) { 'Berlin' }
it_behaves_like "returns time in the future"
it 'converts time in server time zone' do
expect(subject.hour).to eq(hour_in_utc)
end
end
it 'converts time in server time zone' do
expect(subject.hour).to eq((Time.zone.now.in_time_zone(cron_timezone).utc_offset / 60 / 60).abs)
context 'when cron_timezone is Eastern Time (US & Canada)' do
let(:cron) { '* 0 * * *' }
let(:cron_timezone) { 'Eastern Time (US & Canada)' }
it_behaves_like "returns time in the future"
it 'converts time in server time zone' do
expect(subject.hour).to eq(hour_in_utc)
end
end
end
end
......@@ -76,9 +122,21 @@ describe Gitlab::Ci::CronParser do
let(:cron) { 'invalid_cron' }
let(:cron_timezone) { 'invalid_cron_timezone' }
it 'returns nil' do
is_expected.to be_nil
end
it { is_expected.to be_nil }
end
context 'when cron syntax is quoted' do
let(:cron) { "'0 * * * *'" }
let(:cron_timezone) { 'UTC' }
it { expect(subject).to be_nil }
end
context 'when cron syntax is rufus-scheduler syntax' do
let(:cron) { 'every 3h' }
let(:cron_timezone) { 'UTC' }
it { expect(subject).to be_nil }
end
end
......@@ -96,6 +154,12 @@ describe Gitlab::Ci::CronParser do
it { is_expected.to eq(false) }
end
context 'when cron syntax is quoted' do
let(:cron) { "'0 * * * *'" }
it { is_expected.to eq(false) }
end
end
describe '#cron_timezone_valid?' do
......@@ -112,5 +176,11 @@ describe Gitlab::Ci::CronParser do
it { is_expected.to eq(false) }
end
context 'when cron_timezone is ActiveSupport::TimeZone format' do
let(:cron_timezone) { 'Eastern Time (US & Canada)' }
it { is_expected.to eq(true) }
end
end
end
......@@ -6,7 +6,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:user) { create(:user) }
let(:project) { setup_project }
let!(:project) { setup_project }
before do
project.team << [user, :master]
......@@ -219,7 +219,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
releases: [release],
group: group
)
project.update(description_html: 'description')
project.update_column(:description_html, 'description')
project_label = create(:label, project: project)
group_label = create(:group_label, group: group)
create(:label_link, label: project_label, target: issue)
......
......@@ -73,4 +73,36 @@ describe Ci::TriggerSchedule, models: true do
end
end
end
describe '#real_next_run' do
subject do
Ci::TriggerSchedule.last.real_next_run(worker_cron: worker_cron,
worker_time_zone: worker_time_zone)
end
context 'when GitLab time_zone is UTC' do
before do
allow(Time).to receive(:zone)
.and_return(ActiveSupport::TimeZone[worker_time_zone])
end
let(:worker_time_zone) { 'UTC' }
context 'when cron_timezone is Eastern Time (US & Canada)' do
before do
create(:ci_trigger_schedule, :nightly,
cron_timezone: 'Eastern Time (US & Canada)')
end
let(:worker_cron) { '0 1 2 3 *' }
it 'returns the next time worker executes' do
expect(subject.min).to eq(0)
expect(subject.hour).to eq(1)
expect(subject.day).to eq(2)
expect(subject.month).to eq(3)
end
end
end
end
end
......@@ -24,6 +24,245 @@ describe API::Pipelines do
expect(json_response.first['id']).to eq pipeline.id
expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status])
end
context 'when parameter is passed' do
%w[running pending].each do |target|
context "when scope is #{target}" do
before do
create(:ci_pipeline, project: project, status: target)
end
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), scope: target
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
json_response.each { |r| expect(r['status']).to eq(target) }
end
end
end
context 'when scope is finished' do
before do
create(:ci_pipeline, project: project, status: 'success')
create(:ci_pipeline, project: project, status: 'failed')
create(:ci_pipeline, project: project, status: 'canceled')
end
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), scope: 'finished'
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
json_response.each { |r| expect(r['status']).to be_in(%w[success failed canceled]) }
end
end
context 'when scope is branches or tags' do
let!(:pipeline_branch) { create(:ci_pipeline, project: project) }
let!(:pipeline_tag) { create(:ci_pipeline, project: project, ref: 'v1.0.0', tag: true) }
context 'when scope is branches' do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), scope: 'branches'
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
expect(json_response.last['id']).to eq(pipeline_branch.id)
end
end
context 'when scope is tags' do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), scope: 'tags'
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
expect(json_response.last['id']).to eq(pipeline_tag.id)
end
end
end
context 'when scope is invalid' do
it 'returns bad_request' do
get api("/projects/#{project.id}/pipelines", user), scope: 'invalid-scope'
expect(response).to have_http_status(:bad_request)
end
end
HasStatus::AVAILABLE_STATUSES.each do |target|
context "when status is #{target}" do
before do
create(:ci_pipeline, project: project, status: target)
exception_status = HasStatus::AVAILABLE_STATUSES - [target]
create(:ci_pipeline, project: project, status: exception_status.sample)
end
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), status: target
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
json_response.each { |r| expect(r['status']).to eq(target) }
end
end
end
context 'when status is invalid' do
it 'returns bad_request' do
get api("/projects/#{project.id}/pipelines", user), status: 'invalid-status'
expect(response).to have_http_status(:bad_request)
end
end
context 'when ref is specified' do
before do
create(:ci_pipeline, project: project)
end
context 'when ref exists' do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), ref: 'master'
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
json_response.each { |r| expect(r['ref']).to eq('master') }
end
end
context 'when ref does not exist' do
it 'returns empty' do
get api("/projects/#{project.id}/pipelines", user), ref: 'invalid-ref'
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_empty
end
end
end
context 'when name is specified' do
let!(:pipeline) { create(:ci_pipeline, project: project, user: user) }
context 'when name exists' do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), name: user.name
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.first['id']).to eq(pipeline.id)
end
end
context 'when name does not exist' do
it 'returns empty' do
get api("/projects/#{project.id}/pipelines", user), name: 'invalid-name'
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_empty
end
end
end
context 'when username is specified' do
let!(:pipeline) { create(:ci_pipeline, project: project, user: user) }
context 'when username exists' do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), username: user.username
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.first['id']).to eq(pipeline.id)
end
end
context 'when username does not exist' do
it 'returns empty' do
get api("/projects/#{project.id}/pipelines", user), username: 'invalid-username'
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_empty
end
end
end
context 'when yaml_errors is specified' do
let!(:pipeline1) { create(:ci_pipeline, project: project, yaml_errors: 'Syntax error') }
let!(:pipeline2) { create(:ci_pipeline, project: project) }
context 'when yaml_errors is true' do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), yaml_errors: true
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.first['id']).to eq(pipeline1.id)
end
end
context 'when yaml_errors is false' do
it 'returns matched pipelines' do
get api("/projects/#{project.id}/pipelines", user), yaml_errors: false
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response.first['id']).to eq(pipeline2.id)
end
end
context 'when yaml_errors is invalid' do
it 'returns bad_request' do
get api("/projects/#{project.id}/pipelines", user), yaml_errors: 'invalid-yaml_errors'
expect(response).to have_http_status(:bad_request)
end
end
end
context 'when order_by and sort are specified' do
context 'when order_by user_id' do
let!(:pipeline) { create_list(:ci_pipeline, 2, project: project, user: create(:user)) }
it 'sorts as user_id: :asc' do
get api("/projects/#{project.id}/pipelines", user), order_by: 'user_id', sort: 'asc'
expect(response).to have_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).not_to be_empty
pipeline.sort_by { |p| p.user.id }.tap do |sorted_pipeline|
json_response.each_with_index { |r, i| expect(r['id']).to eq(sorted_pipeline[i].id) }
end
end
context 'when sort is invalid' do
it 'returns bad_request' do
get api("/projects/#{project.id}/pipelines", user), order_by: 'user_id', sort: 'invalid_sort'
expect(response).to have_http_status(:bad_request)
end
end
end
context 'when order_by is invalid' do
it 'returns bad_request' do
get api("/projects/#{project.id}/pipelines", user), order_by: 'lock_version', sort: 'asc'
expect(response).to have_http_status(:bad_request)
end
end
end
end
end
context 'unauthorized user' do
......
......@@ -21,6 +21,16 @@ describe Projects::EnableDeployKeyService, services: true do
end
end
context 'add the same key twice' do
before do
project.deploy_keys << deploy_key
end
it 'returns existing key' do
expect(service.execute).to eq(deploy_key)
end
end
def service
Projects::EnableDeployKeyService.new(project, user, params)
end
......
shared_examples 'milestone tabs' do
def go(path, extra_params = {})
params = if milestone.is_a?(GlobalMilestone)
{ group_id: group.id, id: milestone.safe_title, title: milestone.title }
else
{ namespace_id: project.namespace.to_param, project_id: project, id: milestone.iid }
end
get path, params.merge(extra_params)
end
describe '#merge_requests' do
context 'as html' do
before { go(:merge_requests, format: 'html') }
it 'redirects to milestone#show' do
expect(response).to redirect_to(milestone_path)
end
end
context 'as json' do
before { go(:merge_requests, format: 'json') }
it 'renders the merge requests tab template to a string' do
expect(response).to render_template('shared/milestones/_merge_requests_tab')
expect(json_response).to have_key('html')
end
end
end
describe '#participants' do
context 'as html' do
before { go(:participants, format: 'html') }
it 'redirects to milestone#show' do
expect(response).to redirect_to(milestone_path)
end
end
context 'as json' do
before { go(:participants, format: 'json') }
it 'renders the participants tab template to a string' do
expect(response).to render_template('shared/milestones/_participants_tab')
expect(json_response).to have_key('html')
end
end
end
describe '#labels' do
context 'as html' do
before { go(:labels, format: 'html') }
it 'redirects to milestone#show' do
expect(response).to redirect_to(milestone_path)
end
end
context 'as json' do
before { go(:labels, format: 'json') }
it 'renders the labels tab template to a string' do
expect(response).to render_template('shared/milestones/_labels_tab')
expect(json_response).to have_key('html')
end
end
end
end
require 'spec_helper'
describe 'projects/commit/show.html.haml', :view do
let(:project) { create(:project, :repository) }
before do
assign(:project, project)
assign(:repository, project.repository)
assign(:commit, project.commit)
assign(:noteable, project.commit)
assign(:notes, [])
assign(:diffs, project.commit.diffs)
allow(view).to receive(:current_user).and_return(nil)
allow(view).to receive(:can?).and_return(false)
allow(view).to receive(:can_collaborate_with_project?).and_return(false)
allow(view).to receive(:current_ref).and_return(project.repository.root_ref)
allow(view).to receive(:diff_btn).and_return('')
end
context 'inline diff view' do
before do
allow(view).to receive(:diff_view).and_return(:inline)
render
end
it 'keeps container-limited' do
expect(rendered).not_to have_selector('.limit-container-width')
end
end
context 'parallel diff view' do
before do
allow(view).to receive(:diff_view).and_return(:parallel)
render
end
it 'spans full width' do
expect(rendered).to have_selector('.limit-container-width')
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment