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 ...@@ -264,11 +264,15 @@ spinach mysql 9 10: *spinach-knapsack-mysql
static-analysis: static-analysis:
<<: *ruby-static-analysis <<: *ruby-static-analysis
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: test stage: test
script: script:
- scripts/static-analysis - 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" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
...@@ -276,6 +280,7 @@ docs:check:links: ...@@ -276,6 +280,7 @@ docs:check:links:
dependencies: [] dependencies: []
before_script: [] before_script: []
script: script:
- scripts/lint-doc.sh
- mv doc/ /nanoc/content/ - mv doc/ /nanoc/content/
- cd /nanoc - cd /nanoc
# Build HTML from Markdown # Build HTML from Markdown
......
...@@ -19,12 +19,10 @@ ...@@ -19,12 +19,10 @@
}); });
}; };
Milestone.sortIssues = function(data) { Milestone.sortIssues = function(url, data) {
var sort_issues_url;
sort_issues_url = location.href + "/sort_issues";
return $.ajax({ return $.ajax({
type: "PUT", type: "PUT",
url: sort_issues_url, url,
data: data, data: data,
success: function(_data) { success: function(_data) {
return Milestone.successCallback(_data); return Milestone.successCallback(_data);
...@@ -36,12 +34,10 @@ ...@@ -36,12 +34,10 @@
}); });
}; };
Milestone.sortMergeRequests = function(data) { Milestone.sortMergeRequests = function(url, data) {
var sort_mr_url;
sort_mr_url = location.href + "/sort_merge_requests";
return $.ajax({ return $.ajax({
type: "PUT", type: "PUT",
url: sort_mr_url, url,
data: data, data: data,
success: function(_data) { success: function(_data) {
return Milestone.successCallback(_data); return Milestone.successCallback(_data);
...@@ -81,42 +77,55 @@ ...@@ -81,42 +77,55 @@
}; };
function Milestone() { function Milestone() {
var oldMouseStart; this.issuesSortEndpoint = $('#tab-issues').data('sort-endpoint');
this.mergeRequestsSortEndpoint = $('#tab-merge-requests').data('sort-endpoint');
this.bindIssuesSorting(); this.bindIssuesSorting();
this.bindMergeRequestSorting();
this.bindTabsSwitching(); 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() { Milestone.prototype.bindIssuesSorting = function() {
if (!this.issuesSortEndpoint) return;
$('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) { $('#issues-list-unassigned, #issues-list-ongoing, #issues-list-closed').each(function (i, el) {
this.createSortable(el, { this.createSortable(el, {
group: 'issue-list', group: 'issue-list',
listEls: $('.issues-sortable-list'), listEls: $('.issues-sortable-list'),
fieldName: 'issue', fieldName: 'issue',
sortCallback: Milestone.sortIssues, sortCallback: (data) => {
Milestone.sortIssues(this.issuesSortEndpoint, data);
},
updateCallback: Milestone.updateIssue, updateCallback: Milestone.updateIssue,
}); });
}.bind(this)); }.bind(this));
}; };
Milestone.prototype.bindTabsSwitching = function() { Milestone.prototype.bindTabsSwitching = function() {
return $('a[data-toggle="tab"]').on('show.bs.tab', function(e) { return $('a[data-toggle="tab"]').on('show.bs.tab', (e) => {
var currentTabClass, previousTabClass; const $target = $(e.target);
currentTabClass = $(e.target).data('show');
previousTabClass = $(e.relatedTarget).data('show'); location.hash = $target.attr('href');
$(previousTabClass).hide(); this.loadTab($target);
$(currentTabClass).removeClass('hidden');
return $(currentTabClass).show();
}); });
}; };
Milestone.prototype.bindMergeRequestSorting = function() { Milestone.prototype.bindMergeRequestSorting = function() {
if (!this.mergeRequestsSortEndpoint) return;
$("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) { $("#merge_requests-list-unassigned, #merge_requests-list-ongoing, #merge_requests-list-closed").each(function (i, el) {
this.createSortable(el, { this.createSortable(el, {
group: 'merge-request-list', group: 'merge-request-list',
listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"), listEls: $(".merge_requests-sortable-list:not(#merge_requests-list-merged)"),
fieldName: 'merge_request', fieldName: 'merge_request',
sortCallback: Milestone.sortMergeRequests, sortCallback: (data) => {
Milestone.sortMergeRequests(this.mergeRequestsSortEndpoint, data);
},
updateCallback: Milestone.updateMergeRequest, updateCallback: Milestone.updateMergeRequest,
}); });
}.bind(this)); }.bind(this));
...@@ -169,6 +178,35 @@ ...@@ -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; return Milestone;
})(); })();
}).call(window); }).call(window);
...@@ -6,7 +6,13 @@ ...@@ -6,7 +6,13 @@
} }
.limit-container-width { .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; @extend .fixed-width-container;
} }
...@@ -36,8 +42,7 @@ ...@@ -36,8 +42,7 @@
} }
.diffs { .diffs {
.mr-version-controls, .mr-version-controls {
.files-changed {
@extend .fixed-width-container; @extend .fixed-width-container;
} }
} }
......
...@@ -133,3 +133,55 @@ ...@@ -133,3 +133,55 @@
right: 160px; 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 @@ ...@@ -71,7 +71,6 @@
.nav-controls { .nav-controls {
width: auto; width: auto;
min-width: 50%; 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 class Groups::MilestonesController < Groups::ApplicationController
include MilestoneActions
before_action :group_projects 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] before_action :authorize_admin_milestones!, only: [:new, :create, :update]
def index def index
......
class Projects::MilestonesController < Projects::ApplicationController class Projects::MilestonesController < Projects::ApplicationController
include MilestoneActions
before_action :module_enabled 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 # Allow read any milestone
before_action :authorize_read_milestone! before_action :authorize_read_milestone!
# Allow admin 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 respond_to :html
......
...@@ -9,19 +9,19 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -9,19 +9,19 @@ class Projects::PipelinesController < Projects::ApplicationController
def index def index
@scope = params[:scope] @scope = params[:scope]
@pipelines = PipelinesFinder @pipelines = PipelinesFinder
.new(project) .new(project, scope: @scope)
.execute(scope: @scope) .execute
.page(params[:page]) .page(params[:page])
.per(30) .per(30)
@running_count = PipelinesFinder @running_count = PipelinesFinder
.new(project).execute(scope: 'running').count .new(project, scope: 'running').execute.count
@pending_count = PipelinesFinder @pending_count = PipelinesFinder
.new(project).execute(scope: 'pending').count .new(project, scope: 'pending').execute.count
@finished_count = PipelinesFinder @finished_count = PipelinesFinder
.new(project).execute(scope: 'finished').count .new(project, scope: 'finished').execute.count
@pipelines_count = PipelinesFinder @pipelines_count = PipelinesFinder
.new(project).execute.count .new(project).execute.count
......
class PipelinesFinder 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 @project = project
@pipelines = project.pipelines @pipelines = project.pipelines
@params = params
end end
def execute(scope: nil) def execute
scoped_pipelines = items = pipelines
case scope items = by_scope(items)
when 'running' items = by_status(items)
pipelines.running items = by_ref(items)
when 'pending' items = by_name(items)
pipelines.pending items = by_username(items)
when 'finished' items = by_yaml_errors(items)
pipelines.finished sort_items(items)
when 'branches'
from_ids(ids_for_ref(branches))
when 'tags'
from_ids(ids_for_ref(tags))
else
pipelines
end
scoped_pipelines.order(id: :desc)
end end
private private
...@@ -43,4 +37,78 @@ class PipelinesFinder ...@@ -43,4 +37,78 @@ class PipelinesFinder
def tags def tags
project.repository.tag_names project.repository.tag_names
end 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 end
...@@ -115,4 +115,28 @@ module MilestonesHelper ...@@ -115,4 +115,28 @@ module MilestonesHelper
end end
end 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 end
...@@ -4,7 +4,10 @@ module Projects ...@@ -4,7 +4,10 @@ module Projects
key = accessible_keys.find_by(id: params[:key_id] || params[:id]) key = accessible_keys.find_by(id: params[:key_id] || params[:id])
return unless key return unless key
project.deploy_keys << key unless project.deploy_keys.include?(key)
project.deploy_keys << key
end
key key
end end
......
...@@ -175,11 +175,7 @@ ...@@ -175,11 +175,7 @@
.panel-body .panel-body
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user) - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p Deleting a user has the following effects: %p Deleting a user has the following effects:
%ul = render 'users/deletion_guidance', user: @user
%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
%br %br
= link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove" = link_to 'Remove user', [:admin, @user], data: { confirm: "USER #{@user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-remove"
- else - else
......
...@@ -118,11 +118,7 @@ ...@@ -118,11 +118,7 @@
- if @user.can_be_removed? && can?(current_user, :destroy_user, @user) - if @user.can_be_removed? && can?(current_user, :destroy_user, @user)
%p %p
Deleting an account has the following effects: Deleting an account has the following effects:
%ul = render 'users/deletion_guidance', user: current_user
%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
= link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove" = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
- else - else
- if @user.solo_owned_groups.present? - if @user.solo_owned_groups.present?
......
- blame = local_assigns.fetch(:blame, false) - blame = local_assigns.fetch(:blame, false)
.js-file-title.file-title-flex-parent .js-file-title.file-title-flex-parent
.file-header-content = render 'projects/blob/header_content', blob: blob
= 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)
.file-actions.hidden-xs .file-actions.hidden-xs
= render 'projects/blob/viewer_switcher', blob: blob unless blame = 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 @@ ...@@ -48,7 +48,7 @@
- if @build.merge_request - if @build.merge_request
%p.build-detail-row %p.build-detail-row
%span.build-light-text Merge Request: %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 - if @build.duration
%p.build-detail-row %p.build-detail-row
%span.build-light-text Duration: %span.build-light-text Duration:
......
- @no_container = true - @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_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description - page_description @commit.description
= render "projects/commits/head" = render "projects/commits/head"
%div{ class: container_class } .container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box" = render "commit_box"
- if @commit.status - if @commit.status
= render "ci_menu" = render "ci_menu"
......
...@@ -6,6 +6,12 @@ ...@@ -6,6 +6,12 @@
%p %p
Add a new member to Add a new member to
%strong= @project.name %strong= @project.name
- else
%p
Members can be added by project
%i Masters
or
%i Owners
.col-lg-9 .col-lg-9
.light.prepend-top-default .light.prepend-top-default
- if can?(current_user, :admin_project_member, @project) - if can?(current_user, :admin_project_member, @project)
......
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading.flex-project-members-panel
Members with access to %span.flex-project-title
%strong= @project.name Members of
%strong
#{@project.name}
%span.badge= @project_members.total_count %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 .form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false } = 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" } %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
......
...@@ -136,7 +136,7 @@ ...@@ -136,7 +136,7 @@
- selected_labels.each do |label| - selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown .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?) } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
= multi_label_name(selected_labels, "Labels") = multi_label_name(selected_labels, "Labels")
= icon('chevron-down', 'aria-hidden': 'true') = 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 .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= icon('angle-left') .fade-left= icon('angle-left')
.fade-right= icon('angle-right') .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) - if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project)
%li.active %li.active
= link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do = link_to '#tab-issues', 'data-toggle' => 'tab', 'data-show' => '.tab-issues-buttons' do
Issues Issues
%span.badge= milestone.issues_visible_to_user(current_user).size %span.badge= milestone.issues_visible_to_user(current_user).size
%li %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 Merge Requests
%span.badge= milestone.merge_requests.size %span.badge= milestone.merge_requests.size
- else - else
%li.active %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 Merge Requests
%span.badge= milestone.merge_requests.size %span.badge= milestone.merge_requests.size
%li %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 Participants
%span.badge= milestone.participants.count %span.badge= milestone.participants.count
%li %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 Labels
%span.badge= milestone.labels.count %span.badge= milestone.labels.count
...@@ -30,14 +30,18 @@ ...@@ -30,14 +30,18 @@
.tab-content.milestone-content .tab-content.milestone-content
- if milestone.is_a?(GlobalMilestone) || can?(current_user, :read_issue, @project) - 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 = 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 .tab-pane#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } }
= 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 -# loaded async
= render "shared/milestones/tab_loading"
- else - else
.tab-pane.active#tab-merge-requests .tab-pane.active#tab-merge-requests{ data: { sort_endpoint: (sort_merge_requests_namespace_project_milestone_path(@project.namespace, @project, @milestone) if @project && current_user) } }
= 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 -# loaded async
= render "shared/milestones/tab_loading"
.tab-pane#tab-participants .tab-pane#tab-participants
= render 'shared/milestones/participants_tab', users: milestone.participants -# loaded async
= render "shared/milestones/tab_loading"
.tab-pane#tab-labels .tab-pane#tab-labels
= render 'shared/milestones/labels_tab', labels: milestone.labels -# loaded async
= render "shared/milestones/tab_loading"
- blob = @snippet.blob - blob = @snippet.blob
.js-file-title.file-title-flex-parent .js-file-title.file-title-flex-parent
.file-header-content = render 'projects/blob/header_content', blob: blob
= 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)
.file-actions.hidden-xs .file-actions.hidden-xs
= render 'projects/blob/viewer_switcher', blob: blob = 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', ...@@ -10,7 +10,13 @@ scope(path: 'groups/*group_id',
end end
resource :avatar, only: [:destroy] 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 resources :labels, except: [:show] do
post :toggle_subscription, on: :member post :toggle_subscription, on: :member
......
...@@ -207,6 +207,9 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -207,6 +207,9 @@ constraints(ProjectUrlConstrainer.new) do
member do member do
put :sort_issues put :sort_issues
put :sort_merge_requests put :sort_merge_requests
get :merge_requests
get :participants
get :labels
end end
end end
......
...@@ -11,6 +11,14 @@ GET /projects/:id/pipelines ...@@ -11,6 +11,14 @@ GET /projects/:id/pipelines
| Attribute | Type | Required | Description | | 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 | | `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" 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 ...@@ -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 They are written by members of the GitLab Team and by
[Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/). [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
- **GitLab Pages from A to Z** - **GitLab Pages from A to Z**
......
...@@ -31,16 +31,26 @@ files it can find, also the ones in `/tmp` ...@@ -31,16 +31,26 @@ files it can find, also the ones in `/tmp`
To run a single test file you can use: To run a single test file you can use:
- `bundle exec rspec spec/controllers/commit_controller_spec.rb` for a rspec test - `bin/rspec spec/controllers/commit_controller_spec.rb` for a rspec test
- `bundle exec spinach features/project/issues/milestones.feature` for a spinach test - `bin/spinach features/project/issues/milestones.feature` for a spinach test
To run several tests inside one directory: To run several tests inside one directory:
- `bundle exec rspec spec/requests/api/` for the rspec tests if you want to test API only - `bin/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/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 ### Speed-up tests, rake tasks, and migrations
`ENABLE_SPRING=1` in your environment.
[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 ## Compile Frontend Assets
......
...@@ -18,6 +18,8 @@ This page gathers all the resources for the topic **Authentication** within GitL ...@@ -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) - [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) - [Enforce Two-factor Authentication (2FA)](../../security/two_factor_authentication.md#enforce-two-factor-authentication-2fa)
- **Articles:** - **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/) - [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) - [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/ldap/debugging_ldap.html)
- **Integrations:** - **Integrations:**
......
...@@ -38,6 +38,7 @@ Feature: Group Milestones ...@@ -38,6 +38,7 @@ Feature: Group Milestones
And I should see the "feature" label And I should see the "feature" label
And I should see the project name in the Issue row And I should see the project name in the Issue row
@javascript
Scenario: I should see the Labels tab Scenario: I should see the Labels tab
Given Group has projects with milestones Given Group has projects with milestones
When I visit group "Owned" page When I visit group "Owned" page
......
...@@ -7,14 +7,6 @@ Feature: Project Milestone ...@@ -7,14 +7,6 @@ Feature: Project Milestone
And milestone has issue "Bugfix1" with labels: "bug", "feature" And milestone has issue "Bugfix1" with labels: "bug", "feature"
And milestone has issue "Bugfix2" with labels: "bug", "enhancement" 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 @javascript
Scenario: Listing labels from labels tab Scenario: Listing labels from labels tab
Given I visit project "Shop" milestones page Given I visit project "Shop" milestones page
......
class Spinach::Features::GroupMilestones < Spinach::FeatureSteps class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
include WaitForAjax
include SharedAuthentication include SharedAuthentication
include SharedPaths include SharedPaths
include SharedGroup include SharedGroup
...@@ -90,6 +91,8 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps ...@@ -90,6 +91,8 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end end
step 'I should see the list of labels' do step 'I should see the list of labels' do
wait_for_ajax
page.within('#tab-labels') do page.within('#tab-labels') do
expect(page).to have_content 'bug' expect(page).to have_content 'bug'
expect(page).to have_content 'feature' expect(page).to have_content 'feature'
......
...@@ -2,6 +2,7 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps ...@@ -2,6 +2,7 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps
include SharedAuthentication include SharedAuthentication
include SharedProject include SharedProject
include SharedPaths include SharedPaths
include WaitForAjax
step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do step 'milestone has issue "Bugfix1" with labels: "bug", "feature"' do
project = Project.find_by(name: "Shop") project = Project.find_by(name: "Shop")
...@@ -34,6 +35,8 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps ...@@ -34,6 +35,8 @@ class Spinach::Features::ProjectMilestone < Spinach::FeatureSteps
end end
step 'I should see the labels "bug", "enhancement" and "feature"' do step 'I should see the labels "bug", "enhancement" and "feature"' do
wait_for_ajax
page.within('#tab-issues') do page.within('#tab-issues') do
expect(page).to have_content 'bug' expect(page).to have_content 'bug'
expect(page).to have_content 'enhancement' expect(page).to have_content 'enhancement'
......
...@@ -14,13 +14,23 @@ module API ...@@ -14,13 +14,23 @@ module API
end end
params do params do
use :pagination use :pagination
optional :scope, type: String, values: %w(running branches tags), optional :scope, type: String, values: %w[running pending finished branches tags],
desc: 'Either running, branches, or 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 end
get ':id/pipelines' do get ':id/pipelines' do
authorize! :read_pipeline, user_project 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 present paginate(pipelines), with: Entities::PipelineBasic
end end
......
...@@ -21,7 +21,7 @@ module API ...@@ -21,7 +21,7 @@ module API
get ':id/pipelines' do get ':id/pipelines' do
authorize! :read_pipeline, user_project 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 present paginate(pipelines), with: ::API::Entities::Pipeline
end end
end end
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
def initialize(cron, cron_timezone = 'UTC') def initialize(cron, cron_timezone = 'UTC')
@cron = cron @cron = cron
@cron_timezone = cron_timezone @cron_timezone = ActiveSupport::TimeZone.find_tzinfo(cron_timezone).name
end end
def next_time_from(time) def next_time_from(time)
...@@ -24,8 +24,23 @@ module Gitlab ...@@ -24,8 +24,23 @@ module Gitlab
private 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) 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 rescue
# noop # noop
end end
......
...@@ -84,6 +84,7 @@ excluded_attributes: ...@@ -84,6 +84,7 @@ excluded_attributes:
- :import_jid - :import_jid
- :id - :id
- :star_count - :star_count
- :last_activity_at
snippets: snippets:
- :expired_at - :expired_at
merge_request_diff: merge_request_diff:
......
...@@ -9,7 +9,6 @@ tasks = [ ...@@ -9,7 +9,6 @@ tasks = [
%w[bundle exec rake scss_lint], %w[bundle exec rake scss_lint],
%w[bundle exec rake brakeman], %w[bundle exec rake brakeman],
%w[bundle exec license_finder], %w[bundle exec license_finder],
%w[scripts/lint-doc.sh],
%w[yarn run eslint], %w[yarn run eslint],
%w[bundle exec rubocop --require rubocop-rspec] %w[bundle exec rubocop --require rubocop-rspec]
] ]
......
...@@ -6,6 +6,16 @@ describe Groups::MilestonesController do ...@@ -6,6 +6,16 @@ describe Groups::MilestonesController do
let(:project2) { create(:empty_project, group: group) } let(:project2) { create(:empty_project, group: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:title) { '肯定不是中文的问题' } 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 before do
sign_in(user) sign_in(user)
...@@ -14,6 +24,8 @@ describe Groups::MilestonesController do ...@@ -14,6 +24,8 @@ describe Groups::MilestonesController do
controller.instance_variable_set(:@group, group) controller.instance_variable_set(:@group, group)
end end
it_behaves_like 'milestone tabs'
describe "#create" do describe "#create" do
it "creates group milestone with Chinese title" do it "creates group milestone with Chinese title" do
post :create, post :create,
......
...@@ -7,6 +7,7 @@ describe Projects::MilestonesController do ...@@ -7,6 +7,7 @@ describe Projects::MilestonesController do
let(:issue) { create(:issue, project: project, milestone: milestone) } let(:issue) { create(:issue, project: project, milestone: milestone) }
let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) } 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!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
let(:milestone_path) { namespace_project_milestone_path }
before do before do
sign_in(user) sign_in(user)
...@@ -14,6 +15,8 @@ describe Projects::MilestonesController do ...@@ -14,6 +15,8 @@ describe Projects::MilestonesController do
controller.instance_variable_set(:@project, project) controller.instance_variable_set(:@project, project)
end end
it_behaves_like 'milestone tabs'
describe "#show" do describe "#show" do
render_views render_views
......
...@@ -3,7 +3,8 @@ require 'rails_helper' ...@@ -3,7 +3,8 @@ require 'rails_helper'
feature 'Issue Sidebar', feature: true do feature 'Issue Sidebar', feature: true do
include MobileHelpers 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(:issue) { create(:issue, project: project) }
let!(:user) { create(:user)} let!(:user) { create(:user)}
let!(:label) { create(:label, project: project, title: 'bug') } let!(:label) { create(:label, project: project, title: 'bug') }
...@@ -151,9 +152,7 @@ feature 'Issue Sidebar', feature: true do ...@@ -151,9 +152,7 @@ feature 'Issue Sidebar', feature: true do
end end
def open_issue_sidebar def open_issue_sidebar
page.within('aside.right-sidebar.right-sidebar-collapsed') do find('aside.right-sidebar.right-sidebar-collapsed .js-sidebar-toggle').click
find('.js-sidebar-toggle').click find('aside.right-sidebar.right-sidebar-expanded')
sleep 1
end
end end
end end
...@@ -20,7 +20,6 @@ feature 'Create New Merge Request', feature: true, js: true do ...@@ -20,7 +20,6 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content('Target branch') expect(page).to have_content('Target branch')
first('.js-source-branch').click first('.js-source-branch').click
first('.dropdown-source-branch .dropdown-content')
find('.dropdown-source-branch .dropdown-content a', match: :first).click find('.dropdown-source-branch .dropdown-content a', match: :first).click
expect(page).to have_content "b83d6e3" expect(page).to have_content "b83d6e3"
...@@ -35,8 +34,7 @@ feature 'Create New Merge Request', feature: true, js: true do ...@@ -35,8 +34,7 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content('Target branch') expect(page).to have_content('Target branch')
first('.js-target-branch').click first('.js-target-branch').click
first('.dropdown-target-branch .dropdown-content') find('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0', match: :first).click
first('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0').click
expect(page).to have_content "b83d6e3" expect(page).to have_content "b83d6e3"
end end
......
...@@ -86,6 +86,9 @@ describe 'Milestone draggable', feature: true, js: true do ...@@ -86,6 +86,9 @@ describe 'Milestone draggable', feature: true, js: true do
visit namespace_project_milestone_path(project.namespace, project, milestone) visit namespace_project_milestone_path(project.namespace, project, milestone)
page.find("a[href='#tab-merge-requests']").click page.find("a[href='#tab-merge-requests']").click
wait_for_ajax
scroll_into_view('.milestone-content') scroll_into_view('.milestone-content')
drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1) drag_to(selector: '.merge_requests-sortable-list', list_to_index: 1)
......
...@@ -104,6 +104,24 @@ feature 'Triggers', feature: true, js: true do ...@@ -104,6 +104,24 @@ feature 'Triggers', feature: true, js: true do
expect(page).to have_content 'The form contains the following errors' expect(page).to have_content 'The form contains the following errors'
end 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 end
context 'disabling schedule' do context 'disabling schedule' do
......
...@@ -3,50 +3,205 @@ require 'spec_helper' ...@@ -3,50 +3,205 @@ require 'spec_helper'
describe PipelinesFinder do describe PipelinesFinder do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let!(:tag_pipeline) { create(:ci_pipeline, project: project, ref: 'v1.0.0') } subject { described_class.new(project, params).execute }
let!(:branch_pipeline) { create(:ci_pipeline, project: project) }
subject { described_class.new(project).execute(params) }
describe "#execute" do describe "#execute" do
context 'when a scope is passed' do context 'when params is empty' do
context 'when scope is nil' do let(:params) { {} }
let(:params) { { scope: nil } } 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 it 'returns matched pipelines' do
expect(subject.count).to be 2 is_expected.to eq([pipeline])
expect(subject).to include tag_pipeline
expect(subject).to include branch_pipeline
end end
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' } } let(:params) { { scope: 'branches' } }
it 'excludes tags' do it 'returns matched pipelines' do
expect(subject).not_to include tag_pipeline is_expected.to eq([pipeline_branch])
expect(subject).to include branch_pipeline
end end
end end
context 'when selecting tags' do context 'when scope is tags' do
let(:params) { { scope: 'tags' } } let(:params) { { scope: 'tags' } }
it 'excludes branches' do it 'returns matched pipelines' do
expect(subject).to include tag_pipeline is_expected.to eq([pipeline_tag])
expect(subject).not_to include branch_pipeline 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 end
end end
# Scoping to pending will speed up the test as it doesn't hit the FS context 'when ref is specified' do
let(:params) { { scope: 'pending' } } 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 context 'when username is specified' do
feature_pipeline = create(:ci_pipeline, project: project, ref: 'feature') 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 context 'when username exists' do
expect(subject.map(&:id)).to eq expected_ids 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 end
end end
...@@ -60,14 +60,60 @@ describe Gitlab::Ci::CronParser do ...@@ -60,14 +60,60 @@ describe Gitlab::Ci::CronParser do
end end
end end
context 'when cron_timezone is US/Pacific' do context 'when cron_timezone is TZInfo format' do
let(:cron) { '0 0 * * *' } before do
let(:cron_timezone) { 'US/Pacific' } 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 context 'when cron_timezone is Eastern Time (US & Canada)' do
expect(subject.hour).to eq((Time.zone.now.in_time_zone(cron_timezone).utc_offset / 60 / 60).abs) 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 end
end end
...@@ -76,9 +122,21 @@ describe Gitlab::Ci::CronParser do ...@@ -76,9 +122,21 @@ describe Gitlab::Ci::CronParser do
let(:cron) { 'invalid_cron' } let(:cron) { 'invalid_cron' }
let(:cron_timezone) { 'invalid_cron_timezone' } let(:cron_timezone) { 'invalid_cron_timezone' }
it 'returns nil' do it { is_expected.to be_nil }
is_expected.to be_nil end
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
end end
...@@ -96,6 +154,12 @@ describe Gitlab::Ci::CronParser do ...@@ -96,6 +154,12 @@ describe Gitlab::Ci::CronParser do
it { is_expected.to eq(false) } it { is_expected.to eq(false) }
end end
context 'when cron syntax is quoted' do
let(:cron) { "'0 * * * *'" }
it { is_expected.to eq(false) }
end
end end
describe '#cron_timezone_valid?' do describe '#cron_timezone_valid?' do
...@@ -112,5 +176,11 @@ describe Gitlab::Ci::CronParser do ...@@ -112,5 +176,11 @@ describe Gitlab::Ci::CronParser do
it { is_expected.to eq(false) } it { is_expected.to eq(false) }
end 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
end end
...@@ -6,7 +6,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do ...@@ -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(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { setup_project } let!(:project) { setup_project }
before do before do
project.team << [user, :master] project.team << [user, :master]
...@@ -219,7 +219,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do ...@@ -219,7 +219,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
releases: [release], releases: [release],
group: group group: group
) )
project.update(description_html: 'description') project.update_column(:description_html, 'description')
project_label = create(:label, project: project) project_label = create(:label, project: project)
group_label = create(:group_label, group: group) group_label = create(:group_label, group: group)
create(:label_link, label: project_label, target: issue) create(:label_link, label: project_label, target: issue)
......
...@@ -73,4 +73,36 @@ describe Ci::TriggerSchedule, models: true do ...@@ -73,4 +73,36 @@ describe Ci::TriggerSchedule, models: true do
end end
end 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 end
...@@ -24,6 +24,245 @@ describe API::Pipelines do ...@@ -24,6 +24,245 @@ describe API::Pipelines do
expect(json_response.first['id']).to eq pipeline.id expect(json_response.first['id']).to eq pipeline.id
expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status]) expect(json_response.first.keys).to contain_exactly(*%w[id sha ref status])
end 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 end
context 'unauthorized user' do context 'unauthorized user' do
......
...@@ -21,6 +21,16 @@ describe Projects::EnableDeployKeyService, services: true do ...@@ -21,6 +21,16 @@ describe Projects::EnableDeployKeyService, services: true do
end end
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 def service
Projects::EnableDeployKeyService.new(project, user, params) Projects::EnableDeployKeyService.new(project, user, params)
end 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