Commit 8b98ca50 authored by Luke "Jared" Bennett's avatar Luke "Jared" Bennett

Merge branch 'master' into new-resolvable-discussion

parents 501d4032 d37f1f1c
...@@ -347,7 +347,7 @@ migration paths: ...@@ -347,7 +347,7 @@ migration paths:
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- master@gitlab/gitlab-ee - master@gitlab/gitlab-ee
script: script:
- git fetch origin v8.5.9 - git fetch origin v8.14.10
- git checkout -f FETCH_HEAD - git checkout -f FETCH_HEAD
- cp config/resque.yml.example config/resque.yml - cp config/resque.yml.example config/resque.yml
- sed -i 's/localhost/redis/g' config/resque.yml - sed -i 's/localhost/redis/g' config/resque.yml
......
...@@ -51,7 +51,7 @@ function renderCategory(name, emojiList, opts = {}) { ...@@ -51,7 +51,7 @@ function renderCategory(name, emojiList, opts = {}) {
<h5 class="emoji-menu-title"> <h5 class="emoji-menu-title">
${name} ${name}
</h5> </h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass}"> <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
${emojiList.map(emojiName => ` ${emojiList.map(emojiName => `
<li class="emoji-menu-list-item"> <li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button"> <button class="emoji-menu-btn text-center js-emoji-btn" type="button">
......
...@@ -38,6 +38,10 @@ $(() => { ...@@ -38,6 +38,10 @@ $(() => {
Store.create(); Store.create();
// hack to allow sidebar scripts like milestone_select manipulate the BoardsStore
gl.issueBoards.boardStoreIssueSet = (...args) => Vue.set(Store.detail.issue, ...args);
gl.issueBoards.boardStoreIssueDelete = (...args) => Vue.delete(Store.detail.issue, ...args);
gl.IssueBoardsApp = new Vue({ gl.IssueBoardsApp = new Vue({
el: $boardApp, el: $boardApp,
components: { components: {
......
...@@ -12,20 +12,18 @@ Vue.use(VueResource); ...@@ -12,20 +12,18 @@ Vue.use(VueResource);
* Renders Pipelines table in pipelines tab in the commits show view. * Renders Pipelines table in pipelines tab in the commits show view.
*/ */
// export for use in merge_request_tabs.js (TODO: remove this hack)
window.gl = window.gl || {};
window.gl.CommitPipelinesTable = CommitPipelinesTable;
$(() => { $(() => {
window.gl = window.gl || {};
gl.commits = gl.commits || {}; gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {}; gl.commits.pipelines = gl.commits.pipelines || {};
if (gl.commits.PipelinesTableBundle) {
document.querySelector('#commit-pipeline-table-view').removeChild(this.pipelinesTableBundle.$el);
gl.commits.PipelinesTableBundle.$destroy(true);
}
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount(); gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount();
document.querySelector('#commit-pipeline-table-view').appendChild(gl.commits.pipelines.PipelinesTableBundle.$el); pipelineTableViewEl.appendChild(gl.commits.pipelines.PipelinesTableBundle.$el);
} }
}); });
...@@ -44,6 +44,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ...@@ -44,6 +44,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import BlobForkSuggestion from './blob/blob_fork_suggestion'; import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout'; import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
...@@ -329,8 +330,12 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -329,8 +330,12 @@ const ShortcutsBlob = require('./shortcuts_blob');
new Search(); new Search();
break; break;
case 'projects:repository:show': case 'projects:repository:show':
// Initialize Protected Branch Settings
new gl.ProtectedBranchCreate(); new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList(); new gl.ProtectedBranchEditList();
// Initialize Protected Tag Settings
new ProtectedTagCreate();
new ProtectedTagEditList();
break; break;
case 'projects:ci_cd:show': case 'projects:ci_cd:show':
new gl.ProjectVariables(); new gl.ProjectVariables();
......
...@@ -3,9 +3,6 @@ ...@@ -3,9 +3,6 @@
/* global Flash */ /* global Flash */
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import CommitPipelinesTable from './commit/pipelines/pipelines_table';
import './breakpoints'; import './breakpoints';
import './flash'; import './flash';
...@@ -102,10 +99,10 @@ import './flash'; ...@@ -102,10 +99,10 @@ import './flash';
destroyPipelinesView() { destroyPipelinesView() {
if (this.commitPipelinesTable) { if (this.commitPipelinesTable) {
document.querySelector('#commit-pipeline-table-view')
.removeChild(this.commitPipelinesTable.$el);
this.commitPipelinesTable.$destroy(); this.commitPipelinesTable.$destroy();
this.commitPipelinesTable = null;
document.querySelector('#commit-pipeline-table-view').innerHTML = '';
} }
} }
...@@ -234,7 +231,7 @@ import './flash'; ...@@ -234,7 +231,7 @@ import './flash';
} }
mountPipelinesView() { mountPipelinesView() {
this.commitPipelinesTable = new CommitPipelinesTable().$mount(); this.commitPipelinesTable = new gl.CommitPipelinesTable().$mount();
// $mount(el) replaces the el with the new rendered component. We need it in order to mount // $mount(el) replaces the el with the new rendered component. We need it in order to mount
// it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
document.querySelector('#commit-pipeline-table-view') document.querySelector('#commit-pipeline-table-view')
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
/* global Issuable */ /* global Issuable */
/* global ListMilestone */ /* global ListMilestone */
import Vue from 'vue';
(function() { (function() {
this.MilestoneSelect = (function() { this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els) { function MilestoneSelect(currentProject, els) {
...@@ -151,12 +149,12 @@ import Vue from 'vue'; ...@@ -151,12 +149,12 @@ import Vue from 'vue';
return $dropdown.closest('form').submit(); return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) { } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1) { if (selected.id !== -1) {
Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'milestone', new ListMilestone({ gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
id: selected.id, id: selected.id,
title: selected.name title: selected.name
})); }));
} else { } else {
Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'milestone'); gl.issueBoards.boardStoreIssueDelete('milestone');
} }
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
......
export { default as ProtectedTagCreate } from './protected_tag_create';
export { default as ProtectedTagEditList } from './protected_tag_edit_list';
export default class ProtectedTagAccessDropdown {
constructor(options) {
this.options = options;
this.initDropdown();
}
initDropdown() {
const { onSelect } = this.options;
this.options.$dropdown.glDropdown({
data: this.options.data,
selectable: true,
inputId: this.options.$dropdown.data('input-id'),
fieldName: this.options.$dropdown.data('field-name'),
toggleLabel(item, $el) {
if ($el.is('.is-active')) {
return item.text;
}
return 'Select';
},
clicked(item, $el, e) {
e.preventDefault();
onSelect();
},
});
}
}
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
import ProtectedTagDropdown from './protected_tag_dropdown';
export default class ProtectedTagCreate {
constructor() {
this.$form = $('.js-new-protected-tag');
this.buildDropdowns();
}
buildDropdowns() {
const $allowedToCreateDropdown = this.$form.find('.js-allowed-to-create');
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Create dropdown
this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({
$dropdown: $allowedToCreateDropdown,
data: gon.create_access_levels,
onSelect: this.onSelectCallback,
});
// Select default
$allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0);
// Protected tag dropdown
this.protectedTagDropdown = new ProtectedTagDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'),
onSelect: this.onSelectCallback,
});
}
// This will run after clicked callback
onSelect() {
// Enable submit button
const $tagInput = this.$form.find('input[name="protected_tag[name]"]');
const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes');
this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length));
}
}
export default class ProtectedTagDropdown {
/**
* @param {Object} options containing
* `$dropdown` target element
* `onSelect` event callback
* $dropdown must be an element created using `dropdown_tag()` rails helper
*/
constructor(options) {
this.onSelect = options.onSelect;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
this.$protectedTag = this.$dropdownContainer.find('.create-new-protected-tag');
this.buildDropdown();
this.bindEvents();
// Hide footer
this.toggleFooter(true);
}
buildDropdown() {
this.$dropdown.glDropdown({
data: this.getProtectedTags.bind(this),
filterable: true,
remote: false,
search: {
fields: ['title'],
},
selectable: true,
toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : 'Protected Tag';
},
fieldName: 'protected_tag[name]',
text(protectedTag) {
return _.escape(protectedTag.title);
},
id(protectedTag) {
return _.escape(protectedTag.id);
},
onFilter: this.toggleCreateNewButton.bind(this),
clicked: (item, $el, e) => {
e.preventDefault();
this.onSelect();
},
});
}
bindEvents() {
this.$protectedTag.on('click', this.onClickCreateWildcard.bind(this));
}
onClickCreateWildcard(e) {
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
e.preventDefault();
}
getProtectedTags(term, callback) {
if (this.selectedTag) {
callback(gon.open_tags.concat(this.selectedTag));
} else {
callback(gon.open_tags);
}
}
toggleCreateNewButton(tagName) {
if (tagName) {
this.selectedTag = {
title: tagName,
id: tagName,
text: tagName,
};
this.$dropdownContainer
.find('.create-new-protected-tag code')
.text(tagName);
}
this.toggleFooter(!tagName);
}
toggleFooter(toggleState) {
this.$dropdownFooter.toggleClass('hidden', toggleState);
}
}
/* eslint-disable no-new */
/* global Flash */
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
constructor(options) {
this.$wrap = options.$wrap;
this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create');
this.onSelectCallback = this.onSelect.bind(this);
this.buildDropdowns();
}
buildDropdowns() {
// Allowed to create dropdown
this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({
$dropdown: this.$allowedToCreateDropdownButton,
data: gon.create_access_levels,
onSelect: this.onSelectCallback,
});
}
onSelect() {
const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`);
// Do not update if one dropdown has not selected any option
if (!$allowedToCreateInput.length) return;
this.$allowedToCreateDropdownButton.disable();
$.ajax({
type: 'POST',
url: this.$wrap.data('url'),
dataType: 'json',
data: {
_method: 'PATCH',
protected_tag: {
create_access_levels_attributes: [{
id: this.$allowedToCreateDropdownButton.data('access-level-id'),
access_level: $allowedToCreateInput.val(),
}],
},
},
error() {
new Flash('Failed to update tag!', null, $('.js-protected-tags-list'));
},
}).always(() => {
this.$allowedToCreateDropdownButton.enable();
});
}
}
/* eslint-disable no-new */
import ProtectedTagEdit from './protected_tag_edit';
export default class ProtectedTagEditList {
constructor() {
this.$wrap = $('.protected-tags-list');
this.initEditForm();
}
initEditForm() {
this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => {
new ProtectedTagEdit({
$wrap: $(el),
});
});
}
}
import Vue from 'vue';
(() => { (() => {
class Subscription { class Subscription {
constructor(containerElm) { constructor(containerElm) {
...@@ -29,8 +27,7 @@ import Vue from 'vue'; ...@@ -29,8 +27,7 @@ import Vue from 'vue';
// hack to allow this to work with the issue boards Vue object // hack to allow this to work with the issue boards Vue object
if (document.querySelector('html').classList.contains('issue-boards-page')) { if (document.querySelector('html').classList.contains('issue-boards-page')) {
Vue.set( gl.issueBoards.boardStoreIssueSet(
gl.issueBoards.BoardsStore.detail.issue,
'subscribed', 'subscribed',
!gl.issueBoards.BoardsStore.detail.issue.subscribed, !gl.issueBoards.BoardsStore.detail.issue.subscribed,
); );
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
/* global Issuable */ /* global Issuable */
/* global ListUser */ /* global ListUser */
import Vue from 'vue';
(function() { (function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }, var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
slice = [].slice; slice = [].slice;
...@@ -74,7 +72,7 @@ import Vue from 'vue'; ...@@ -74,7 +72,7 @@ import Vue from 'vue';
e.preventDefault(); e.preventDefault();
if ($dropdown.hasClass('js-issue-board-sidebar')) { if ($dropdown.hasClass('js-issue-board-sidebar')) {
Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({ gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
id: _this.currentUser.id, id: _this.currentUser.id,
username: _this.currentUser.username, username: _this.currentUser.username,
name: _this.currentUser.name, name: _this.currentUser.name,
...@@ -225,14 +223,14 @@ import Vue from 'vue'; ...@@ -225,14 +223,14 @@ import Vue from 'vue';
return $dropdown.closest('form').submit(); return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) { } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (user.id) { if (user.id) {
Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({ gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
id: user.id, id: user.id,
username: user.username, username: user.username,
name: user.name, name: user.name,
avatar_url: user.avatar_url avatar_url: user.avatar_url
})); }));
} else { } else {
Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'assignee'); gl.issueBoards.boardStoreIssueDelete('assignee');
} }
updateIssueBoardsIssue(); updateIssueBoardsIssue();
......
...@@ -744,7 +744,8 @@ pre.light-well { ...@@ -744,7 +744,8 @@ pre.light-well {
text-align: left; text-align: left;
} }
.protected-branches-list { .protected-branches-list,
.protected-tags-list {
margin-bottom: 30px; margin-bottom: 30px;
a { a {
...@@ -776,6 +777,17 @@ pre.light-well { ...@@ -776,6 +777,17 @@ pre.light-well {
} }
} }
.protected-tags-list {
.dropdown-menu-toggle {
width: 100%;
max-width: 300px;
}
.flash-container {
padding: 0;
}
}
.custom-notifications-form { .custom-notifications-form {
.is-loading { .is-loading {
.custom-notification-event-loading { .custom-notification-event-loading {
......
...@@ -19,6 +19,11 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -19,6 +19,11 @@ class Projects::BuildsController < Projects::ApplicationController
else else
@builds @builds
end end
@builds = @builds.includes([
{ pipeline: :project },
:project,
:tags
])
@builds = @builds.page(params[:page]).per(30) @builds = @builds.page(params[:page]).per(30)
end end
......
class Projects::ProtectedBranchesController < Projects::ApplicationController class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
include RepositorySettingsRedirect protected
# Authorize
before_action :require_non_empty_project
before_action :authorize_admin_project!
before_action :load_protected_branch, only: [:show, :update, :destroy]
layout "project_settings" def project_refs
@project.repository.branches
def index
redirect_to_repository_settings(@project)
end
def create
@protected_branch = ::ProtectedBranches::CreateService.new(@project, current_user, protected_branch_params).execute
unless @protected_branch.persisted?
flash[:alert] = @protected_branches.errors.full_messages.join(', ').html_safe
end
redirect_to_repository_settings(@project)
end
def show
@matching_branches = @protected_branch.matching(@project.repository.branches)
end end
def update def create_service_class
@protected_branch = ::ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch) ::ProtectedBranches::CreateService
if @protected_branch.valid?
respond_to do |format|
format.json { render json: @protected_branch, status: :ok }
end
else
respond_to do |format|
format.json { render json: @protected_branch.errors, status: :unprocessable_entity }
end
end
end end
def destroy def update_service_class
@protected_branch.destroy ::ProtectedBranches::UpdateService
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
format.js { head :ok }
end
end end
private def load_protected_ref
@protected_ref = @project.protected_branches.find(params[:id])
def load_protected_branch
@protected_branch = @project.protected_branches.find(params[:id])
end end
def protected_branch_params def protected_ref_params
params.require(:protected_branch).permit(:name, params.require(:protected_branch).permit(:name,
merge_access_levels_attributes: [:access_level, :id], merge_access_levels_attributes: [:access_level, :id],
push_access_levels_attributes: [:access_level, :id]) push_access_levels_attributes: [:access_level, :id])
......
class Projects::ProtectedRefsController < Projects::ApplicationController
include RepositorySettingsRedirect
# Authorize
before_action :require_non_empty_project
before_action :authorize_admin_project!
before_action :load_protected_ref, only: [:show, :update, :destroy]
layout "project_settings"
def index
redirect_to_repository_settings(@project)
end
def create
protected_ref = create_service_class.new(@project, current_user, protected_ref_params).execute
unless protected_ref.persisted?
flash[:alert] = protected_ref.errors.full_messages.join(', ').html_safe
end
redirect_to_repository_settings(@project)
end
def show
@matching_refs = @protected_ref.matching(project_refs)
end
def update
@protected_ref = update_service_class.new(@project, current_user, protected_ref_params).execute(@protected_ref)
if @protected_ref.valid?
render json: @protected_ref, status: :ok
else
render json: @protected_ref.errors, status: :unprocessable_entity
end
end
def destroy
@protected_ref.destroy
respond_to do |format|
format.html { redirect_to_repository_settings(@project) }
format.js { head :ok }
end
end
end
class Projects::ProtectedTagsController < Projects::ProtectedRefsController
protected
def project_refs
@project.repository.tags
end
def create_service_class
::ProtectedTags::CreateService
end
def update_service_class
::ProtectedTags::UpdateService
end
def load_protected_ref
@protected_ref = @project.protected_tags.find(params[:id])
end
def protected_ref_params
params.require(:protected_tag).permit(:name, create_access_levels_attributes: [:access_level, :id])
end
end
...@@ -4,46 +4,48 @@ module Projects ...@@ -4,46 +4,48 @@ module Projects
before_action :authorize_admin_project! before_action :authorize_admin_project!
def show def show
@deploy_keys = DeployKeysPresenter @deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
.new(@project, current_user: current_user)
define_protected_branches define_protected_refs
end end
private private
def define_protected_branches def define_protected_refs
load_protected_branches @protected_branches = @project.protected_branches.order(:name).page(params[:page])
@protected_tags = @project.protected_tags.order(:name).page(params[:page])
@protected_branch = @project.protected_branches.new @protected_branch = @project.protected_branches.new
@protected_tag = @project.protected_tags.new
load_gon_index load_gon_index
end end
def load_protected_branches
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
end
def access_levels_options def access_levels_options
{ {
push_access_levels: { create_access_levels: levels_for_dropdown(ProtectedTag::CreateAccessLevel),
roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text| push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel),
{ id: id, text: text, before_divider: true } merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel)
end
},
merge_access_levels: {
roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text|
{ id: id, text: text, before_divider: true }
end
}
} }
end end
def open_branches def levels_for_dropdown(access_level_type)
branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } } roles = access_level_type.human_access_levels.map do |id, text|
{ open_branches: branches } { id: id, text: text, before_divider: true }
end
{ roles: roles }
end
def protectable_tags_for_dropdown
{ open_tags: ProtectableDropdown.new(@project, :tags).hash }
end
def protectable_branches_for_dropdown
{ open_branches: ProtectableDropdown.new(@project, :branches).hash }
end end
def load_gon_index def load_gon_index
gon.push(open_branches.merge(access_levels_options)) gon.push(protectable_tags_for_dropdown)
gon.push(protectable_branches_for_dropdown)
gon.push(access_levels_options)
end end
end end
end end
......
...@@ -11,7 +11,7 @@ class Projects::TriggersController < Projects::ApplicationController ...@@ -11,7 +11,7 @@ class Projects::TriggersController < Projects::ApplicationController
end end
def create def create
@trigger = project.triggers.create(create_params.merge(owner: current_user)) @trigger = project.triggers.create(trigger_params.merge(owner: current_user))
if @trigger.valid? if @trigger.valid?
flash[:notice] = 'Trigger was created successfully.' flash[:notice] = 'Trigger was created successfully.'
...@@ -36,7 +36,7 @@ class Projects::TriggersController < Projects::ApplicationController ...@@ -36,7 +36,7 @@ class Projects::TriggersController < Projects::ApplicationController
end end
def update def update
if trigger.update(update_params) if trigger.update(trigger_params)
redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project), notice: 'Trigger was successfully updated.' redirect_to namespace_project_settings_ci_cd_path(@project.namespace, @project), notice: 'Trigger was successfully updated.'
else else
render action: "edit" render action: "edit"
...@@ -67,11 +67,10 @@ class Projects::TriggersController < Projects::ApplicationController ...@@ -67,11 +67,10 @@ class Projects::TriggersController < Projects::ApplicationController
@trigger ||= project.triggers.find(params[:id]) || render_404 @trigger ||= project.triggers.find(params[:id]) || render_404
end end
def create_params def trigger_params
params.require(:trigger).permit(:description) params.require(:trigger).permit(
end :description,
trigger_schedule_attributes: [:id, :active, :cron, :cron_timezone, :ref]
def update_params )
params.require(:trigger).permit(:description)
end end
end end
module BranchesHelper module BranchesHelper
def can_remove_branch?(project, branch_name) def can_remove_branch?(project, branch_name)
if project.protected_branch? branch_name if ProtectedBranch.protected?(project, branch_name)
false false
elsif branch_name == project.repository.root_ref elsif branch_name == project.repository.root_ref
false false
...@@ -29,4 +29,8 @@ module BranchesHelper ...@@ -29,4 +29,8 @@ module BranchesHelper
def project_branches def project_branches
options_for_select(@project.repository.branch_names, @project.default_branch) options_for_select(@project.repository.branch_names, @project.default_branch)
end end
def protected_branch?(project, branch)
ProtectedBranch.protected?(project, branch.name)
end
end end
...@@ -21,4 +21,8 @@ module TagsHelper ...@@ -21,4 +21,8 @@ module TagsHelper
html.html_safe html.html_safe
end end
def protected_tag?(project, tag)
ProtectedTag.protected?(project, tag.name)
end
end end
...@@ -31,7 +31,6 @@ module Ci ...@@ -31,7 +31,6 @@ module Ci
validate :valid_commit_sha, unless: :importing? validate :valid_commit_sha, unless: :importing?
after_create :keep_around_commits, unless: :importing? after_create :keep_around_commits, unless: :importing?
after_create :refresh_build_status_cache
state_machine :status, initial: :created do state_machine :status, initial: :created do
event :enqueue do event :enqueue do
...@@ -351,7 +350,6 @@ module Ci ...@@ -351,7 +350,6 @@ module Ci
when 'manual' then block when 'manual' then block
end end
end end
refresh_build_status_cache
end end
def predefined_variables def predefined_variables
...@@ -393,10 +391,6 @@ module Ci ...@@ -393,10 +391,6 @@ module Ci
.fabricate! .fabricate!
end end
def refresh_build_status_cache
Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
end
private private
def pipeline_data def pipeline_data
......
# This class is not backed by a table in the main database.
# It loads the latest Pipeline for the HEAD of a repository, and caches that
# in Redis.
module Ci
class PipelineStatus
attr_accessor :sha, :status, :project, :loaded
delegate :commit, to: :project
def self.load_for_project(project)
new(project).tap do |status|
status.load_status
end
end
def initialize(project, sha: nil, status: nil)
@project = project
@sha = sha
@status = status
end
def has_status?
loaded? && sha.present? && status.present?
end
def load_status
return if loaded?
if has_cache?
load_from_cache
else
load_from_commit
store_in_cache
end
self.loaded = true
end
def load_from_commit
return unless commit
self.sha = commit.sha
self.status = commit.status
end
# We only cache the status for the HEAD commit of a project
# This status is rendered in project lists
def store_in_cache_if_needed
return unless sha
return delete_from_cache unless commit
store_in_cache if commit.sha == self.sha
end
def load_from_cache
Gitlab::Redis.with do |redis|
self.sha, self.status = redis.hmget(cache_key, :sha, :status)
end
end
def store_in_cache
Gitlab::Redis.with do |redis|
redis.mapped_hmset(cache_key, { sha: sha, status: status })
end
end
def delete_from_cache
Gitlab::Redis.with do |redis|
redis.del(cache_key)
end
end
def has_cache?
Gitlab::Redis.with do |redis|
redis.exists(cache_key)
end
end
def loaded?
self.loaded
end
def cache_key
"projects/#{project.id}/build_status"
end
end
end
...@@ -14,6 +14,8 @@ module Ci ...@@ -14,6 +14,8 @@ module Ci
before_validation :set_default_values before_validation :set_default_values
accepts_nested_attributes_for :trigger_schedule
def set_default_values def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank? self.token = SecureRandom.hex(15) if self.token.blank?
end end
...@@ -37,5 +39,9 @@ module Ci ...@@ -37,5 +39,9 @@ module Ci
def can_access_project? def can_access_project?
self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project) self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project)
end end
def trigger_schedule
super || build_trigger_schedule(project: project)
end
end end
end end
...@@ -8,15 +8,19 @@ module Ci ...@@ -8,15 +8,19 @@ module Ci
belongs_to :project belongs_to :project
belongs_to :trigger belongs_to :trigger
delegate :ref, to: :trigger
validates :trigger, presence: { unless: :importing? } validates :trigger, presence: { unless: :importing? }
validates :cron, cron: true, presence: { unless: :importing? } validates :cron, unless: :importing_or_inactive?, cron: true, presence: { unless: :importing_or_inactive? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? } validates :cron_timezone, cron_timezone: true, presence: { unless: :importing_or_inactive? }
validates :ref, presence: { unless: :importing? } validates :ref, presence: { unless: :importing_or_inactive? }
before_save :set_next_run_at before_save :set_next_run_at
scope :active, -> { where(active: true) }
def importing_or_inactive?
importing? || !active?
end
def set_next_run_at def set_next_run_at
self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now) self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
end end
...@@ -26,5 +30,12 @@ module Ci ...@@ -26,5 +30,12 @@ module Ci
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid
update_attribute(:next_run_at, nil) # update without validation update_attribute(:next_run_at, nil) # update without validation
end end
def real_next_run(
worker_cron: Settings.cron_jobs['trigger_schedule_worker']['cron'],
worker_time_zone: Time.zone.name)
Gitlab::Ci::CronParser.new(worker_cron, worker_time_zone)
.next_time_from(next_run_at)
end
end end
end end
...@@ -68,7 +68,7 @@ module HasStatus ...@@ -68,7 +68,7 @@ module HasStatus
end end
scope :created, -> { where(status: 'created') } scope :created, -> { where(status: 'created') }
scope :relevant, -> { where.not(status: 'created') } scope :relevant, -> { where(status: AVAILABLE_STATUSES - ['created']) }
scope :running, -> { where(status: 'running') } scope :running, -> { where(status: 'running') }
scope :pending, -> { where(status: 'pending') } scope :pending, -> { where(status: 'pending') }
scope :success, -> { where(status: 'success') } scope :success, -> { where(status: 'success') }
......
...@@ -2,20 +2,10 @@ module ProtectedBranchAccess ...@@ -2,20 +2,10 @@ module ProtectedBranchAccess
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
belongs_to :protected_branch include ProtectedRefAccess
delegate :project, to: :protected_branch
scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
end
def humanize belongs_to :protected_branch
self.class.human_access_levels[self.access_level]
end
def check_access(user)
return true if user.is_admin?
project.team.max_member_access(user.id) >= access_level delegate :project, to: :protected_branch
end end
end end
module ProtectedRef
extend ActiveSupport::Concern
included do
belongs_to :project
validates :name, presence: true
validates :project, presence: true
delegate :matching, :matches?, :wildcard?, to: :ref_matcher
def self.protected_ref_accessible_to?(ref, user, action:)
access_levels_for_ref(ref, action: action).any? do |access_level|
access_level.check_access(user)
end
end
def self.developers_can?(action, ref)
access_levels_for_ref(ref, action: action).any? do |access_level|
access_level.access_level == Gitlab::Access::DEVELOPER
end
end
def self.access_levels_for_ref(ref, action:)
self.matching(ref).map(&:"#{action}_access_levels").flatten
end
def self.matching(ref_name, protected_refs: nil)
ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs)
end
end
def commit
project.commit(self.name)
end
private
def ref_matcher
@ref_matcher ||= ProtectedRefMatcher.new(self)
end
end
module ProtectedRefAccess
extend ActiveSupport::Concern
included do
scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
end
def humanize
self.class.human_access_levels[self.access_level]
end
def check_access(user)
return true if user.admin?
project.team.max_member_access(user.id) >= access_level
end
end
module ProtectedTagAccess
extend ActiveSupport::Concern
included do
include ProtectedRefAccess
belongs_to :protected_tag
delegate :project, to: :protected_tag
end
end
...@@ -443,7 +443,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -443,7 +443,7 @@ class MergeRequest < ActiveRecord::Base
end end
def can_remove_source_branch?(current_user) def can_remove_source_branch?(current_user)
!source_project.protected_branch?(source_branch) && !ProtectedBranch.protected?(source_project, source_branch) &&
!source_project.root_ref?(source_branch) && !source_project.root_ref?(source_branch) &&
Ability.allowed?(current_user, :push_code, source_project) && Ability.allowed?(current_user, :push_code, source_project) &&
diff_head_commit == source_branch_head diff_head_commit == source_branch_head
......
...@@ -135,6 +135,7 @@ class Project < ActiveRecord::Base ...@@ -135,6 +135,7 @@ class Project < ActiveRecord::Base
has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet' has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet'
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy has_many :protected_branches, dependent: :destroy
has_many :protected_tags, dependent: :destroy
has_many :project_authorizations has_many :project_authorizations
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User' has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
...@@ -859,14 +860,6 @@ class Project < ActiveRecord::Base ...@@ -859,14 +860,6 @@ class Project < ActiveRecord::Base
@repo_exists = false @repo_exists = false
end end
# Branches that are not _exactly_ matched by a protected branch.
def open_branches
exact_protected_branch_names = protected_branches.reject(&:wildcard?).map(&:name)
branch_names = repository.branches.map(&:name)
non_open_branch_names = Set.new(exact_protected_branch_names).intersection(Set.new(branch_names))
repository.branches.reject { |branch| non_open_branch_names.include? branch.name }
end
def root_ref?(branch) def root_ref?(branch)
repository.root_ref == branch repository.root_ref == branch
end end
...@@ -881,16 +874,8 @@ class Project < ActiveRecord::Base ...@@ -881,16 +874,8 @@ class Project < ActiveRecord::Base
Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url
end end
# Check if current branch name is marked as protected in the system
def protected_branch?(branch_name)
return true if empty_repo? && default_branch_protected?
@protected_branches ||= self.protected_branches.to_a
ProtectedBranch.matching(branch_name, protected_branches: @protected_branches).present?
end
def user_can_push_to_empty_repo?(user) def user_can_push_to_empty_repo?(user)
!default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
end end
def forked? def forked?
...@@ -1197,7 +1182,7 @@ class Project < ActiveRecord::Base ...@@ -1197,7 +1182,7 @@ class Project < ActiveRecord::Base
end end
def pipeline_status def pipeline_status
@pipeline_status ||= Ci::PipelineStatus.load_for_project(self) @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
end end
def mark_import_as_failed(error_message) def mark_import_as_failed(error_message)
...@@ -1353,11 +1338,6 @@ class Project < ActiveRecord::Base ...@@ -1353,11 +1338,6 @@ class Project < ActiveRecord::Base
"projects/#{id}/pushes_since_gc" "projects/#{id}/pushes_since_gc"
end end
def default_branch_protected?
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
end
# Similar to the normal callbacks that hook into the life cycle of an # Similar to the normal callbacks that hook into the life cycle of an
# Active Record object, you can also define callbacks that get triggered # Active Record object, you can also define callbacks that get triggered
# when you add an object to an association collection. If any of these # when you add an object to an association collection. If any of these
......
class ProtectableDropdown
def initialize(project, ref_type)
@project = project
@ref_type = ref_type
end
# Tags/branches which are yet to be individually protected
def protectable_ref_names
@protectable_ref_names ||= ref_names - non_wildcard_protected_ref_names
end
def hash
protectable_ref_names.map { |ref_name| { text: ref_name, id: ref_name, title: ref_name } }
end
private
def refs
@project.repository.public_send(@ref_type)
end
def ref_names
refs.map(&:name)
end
def protections
@project.public_send("protected_#{@ref_type}")
end
def non_wildcard_protected_ref_names
protections.reject(&:wildcard?).map(&:name)
end
end
class ProtectedBranch < ActiveRecord::Base class ProtectedBranch < ActiveRecord::Base
include Gitlab::ShellAdapter include Gitlab::ShellAdapter
include ProtectedRef
belongs_to :project
validates :name, presence: true
validates :project, presence: true
has_many :merge_access_levels, dependent: :destroy has_many :merge_access_levels, dependent: :destroy
has_many :push_access_levels, dependent: :destroy has_many :push_access_levels, dependent: :destroy
...@@ -14,54 +11,15 @@ class ProtectedBranch < ActiveRecord::Base ...@@ -14,54 +11,15 @@ class ProtectedBranch < ActiveRecord::Base
accepts_nested_attributes_for :push_access_levels accepts_nested_attributes_for :push_access_levels
accepts_nested_attributes_for :merge_access_levels accepts_nested_attributes_for :merge_access_levels
def commit # Check if branch name is marked as protected in the system
project.commit(self.name) def self.protected?(project, ref_name)
end return true if project.empty_repo? && default_branch_protected?
# Returns all protected branches that match the given branch name.
# This realizes all records from the scope built up so far, and does
# _not_ return a relation.
#
# This method optionally takes in a list of `protected_branches` to search
# through, to avoid calling out to the database.
def self.matching(branch_name, protected_branches: nil)
(protected_branches || all).select { |protected_branch| protected_branch.matches?(branch_name) }
end
# Returns all branches (among the given list of branches [`Gitlab::Git::Branch`])
# that match the current protected branch.
def matching(branches)
branches.select { |branch| self.matches?(branch.name) }
end
# Checks if the protected branch matches the given branch name.
def matches?(branch_name)
return false if self.name.blank?
exact_match?(branch_name) || wildcard_match?(branch_name)
end
# Checks if this protected branch contains a wildcard
def wildcard?
self.name && self.name.include?('*')
end
protected
def exact_match?(branch_name)
self.name == branch_name
end
def wildcard_match?(branch_name) self.matching(ref_name, protected_refs: project.protected_branches).present?
wildcard_regex === branch_name
end end
def wildcard_regex def self.default_branch_protected?
@wildcard_regex ||= begin current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_FULL ||
name = self.name.gsub('*', 'STAR_DONT_ESCAPE') current_application_settings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE
quoted_name = Regexp.quote(name)
regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?')
/\A#{regex_string}\z/
end
end end
end end
class ProtectedRefMatcher
def initialize(protected_ref)
@protected_ref = protected_ref
end
# Returns all protected refs that match the given ref name.
# This checks all records from the scope built up so far, and does
# _not_ return a relation.
#
# This method optionally takes in a list of `protected_refs` to search
# through, to avoid calling out to the database.
def self.matching(type, ref_name, protected_refs: nil)
(protected_refs || type.all).select { |protected_ref| protected_ref.matches?(ref_name) }
end
# Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`])
# that match the current protected ref.
def matching(refs)
refs.select { |ref| @protected_ref.matches?(ref.name) }
end
# Checks if the protected ref matches the given ref name.
def matches?(ref_name)
return false if @protected_ref.name.blank?
exact_match?(ref_name) || wildcard_match?(ref_name)
end
# Checks if this protected ref contains a wildcard
def wildcard?
@protected_ref.name && @protected_ref.name.include?('*')
end
protected
def exact_match?(ref_name)
@protected_ref.name == ref_name
end
def wildcard_match?(ref_name)
return false unless wildcard?
wildcard_regex === ref_name
end
def wildcard_regex
@wildcard_regex ||= begin
name = @protected_ref.name.gsub('*', 'STAR_DONT_ESCAPE')
quoted_name = Regexp.quote(name)
regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?')
/\A#{regex_string}\z/
end
end
end
class ProtectedTag < ActiveRecord::Base
include Gitlab::ShellAdapter
include ProtectedRef
has_many :create_access_levels, dependent: :destroy
validates :create_access_levels, length: { is: 1, message: "are restricted to a single instance per protected tag." }
accepts_nested_attributes_for :create_access_levels
def self.protected?(project, ref_name)
self.matching(ref_name, protected_refs: project.protected_tags).present?
end
end
class ProtectedTag::CreateAccessLevel < ActiveRecord::Base
include ProtectedTagAccess
validates :access_level, presence: true, inclusion: { in: [Gitlab::Access::MASTER,
Gitlab::Access::DEVELOPER,
Gitlab::Access::NO_ACCESS] }
def self.human_access_levels
{
Gitlab::Access::MASTER => "Masters",
Gitlab::Access::DEVELOPER => "Developers + Masters",
Gitlab::Access::NO_ACCESS => "No one"
}.with_indifferent_access
end
def check_access(user)
return false if access_level == Gitlab::Access::NO_ACCESS
super
end
end
...@@ -10,6 +10,8 @@ module Ci ...@@ -10,6 +10,8 @@ module Ci
store.touch(commit_pipelines_path) if pipeline.commit store.touch(commit_pipelines_path) if pipeline.commit
store.touch(new_merge_request_pipelines_path) store.touch(new_merge_request_pipelines_path)
merge_requests_pipelines_paths.each { |path| store.touch(path) } merge_requests_pipelines_paths.each { |path| store.touch(path) }
Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(@pipeline)
end end
private private
......
...@@ -11,7 +11,7 @@ class DeleteBranchService < BaseService ...@@ -11,7 +11,7 @@ class DeleteBranchService < BaseService
return error('Cannot remove HEAD branch', 405) return error('Cannot remove HEAD branch', 405)
end end
if project.protected_branch?(branch_name) if ProtectedBranch.protected?(project, branch_name)
return error('Protected branch cant be removed', 405) return error('Protected branch cant be removed', 405)
end end
......
...@@ -127,7 +127,7 @@ class GitPushService < BaseService ...@@ -127,7 +127,7 @@ class GitPushService < BaseService
project.change_head(branch_name) project.change_head(branch_name)
# Set protection on the default branch if configured # Set protection on the default branch if configured
if current_application_settings.default_branch_protection != PROTECTION_NONE && !@project.protected_branch?(@project.default_branch) if current_application_settings.default_branch_protection != PROTECTION_NONE && !ProtectedBranch.protected?(@project, @project.default_branch)
params = { params = {
name: @project.default_branch, name: @project.default_branch,
......
module ProtectedBranches module ProtectedBranches
class UpdateService < BaseService class UpdateService < BaseService
attr_reader :protected_branch
def execute(protected_branch) def execute(protected_branch)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
@protected_branch = protected_branch protected_branch.update(params)
@protected_branch.update(params) protected_branch
@protected_branch
end end
end end
end end
module ProtectedTags
class CreateService < BaseService
attr_reader :protected_tag
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
project.protected_tags.create(params)
end
end
end
module ProtectedTags
class UpdateService < BaseService
def execute(protected_tag)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
protected_tag.update(params)
protected_tag
end
end
end
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
%span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" } %span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" }
merged merged
- if @project.protected_branch? branch.name - if protected_branch?(@project, branch)
%span.label.label-success %span.label.label-success
protected protected
.controls.hidden-xs< .controls.hidden-xs<
......
- page_title @protected_branch.name, "Protected Branches" - page_title @protected_ref.name, "Protected Branches"
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default
.col-lg-3 .col-lg-3
%h4.prepend-top-0 %h4.prepend-top-0
= @protected_branch.name = @protected_ref.name
.col-lg-9 .col-lg-9
%h5 Matching Branches %h5 Matching Branches
- if @matching_branches.present? - if @matching_refs.present?
.table-responsive .table-responsive
%table.table.protected-branches-list %table.table.protected-branches-list
%colgroup %colgroup
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
%th Branch %th Branch
%th Last commit %th Last commit
%tbody %tbody
- @matching_branches.each do |matching_branch| - @matching_refs.each do |matching_branch|
= render partial: "matching_branch", object: matching_branch = render partial: "matching_branch", object: matching_branch
- else - else
%p.settings-message.text-center %p.settings-message.text-center
......
= form_for [@project.namespace.becomes(Namespace), @project, @protected_tag], html: { class: 'js-new-protected-tag' } do |f|
.panel.panel-default
.panel-heading
%h3.panel-title
Protect a tag
.panel-body
.form-horizontal
= form_errors(@protected_tag)
.form-group
= f.label :name, class: 'col-md-2 text-right' do
Tag:
.col-md-10
= render partial: "projects/protected_tags/dropdown", locals: { f: f }
.help-block
= link_to 'Wildcards', help_page_path('user/project/protected_tags', anchor: 'wildcard-protected-tags')
such as
%code v*
or
%code *-release
are supported
.form-group
%label.col-md-2.text-right{ for: 'create_access_levels_attributes' }
Allowed to create:
.col-md-10
.create_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-create wide',
dropdown_class: 'dropdown-menu-selectable',
data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes' }})
.panel-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true
= f.hidden_field(:name)
= dropdown_tag('Select tag or create wildcard',
options: { toggle_class: 'js-protected-tag-select js-filter-submit wide',
filter: true, dropdown_class: "dropdown-menu-selectable", placeholder: "Search protected tag",
footer_content: true,
data: { show_no: true, show_any: true, show_upcoming: true,
selected: params[:protected_tag_name],
project_id: @project.try(:id) } }) do
%ul.dropdown-footer-list
%li
= link_to '#', title: "New Protected Tag", class: "create-new-protected-tag" do
Create wildcard
%code
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('protected_tags')
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
Protected tags
%p.prepend-top-20
By default, Protected tags are designed to:
%ul
%li Prevent tag creation by everybody except Masters
%li Prevent <strong>anyone</strong> from updating the tag
%li Prevent <strong>anyone</strong> from deleting the tag
.col-lg-9
- if can? current_user, :admin_project, @project
= render 'projects/protected_tags/create_protected_tag'
= render "projects/protected_tags/tags_list"
%tr
%td
= link_to matching_tag.name, namespace_project_tree_path(@project.namespace, @project, matching_tag.name)
- if @project.root_ref?(matching_tag.name)
%span.label.label-info.prepend-left-5 default
%td
- commit = @project.commit(matching_tag.name)
= link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id')
= time_ago_with_tooltip(commit.committed_date)
%tr.js-protected-tag-edit-form{ data: { url: namespace_project_protected_tag_path(@project.namespace, @project, protected_tag) } }
%td
= protected_tag.name
- if @project.root_ref?(protected_tag.name)
%span.label.label-info.prepend-left-5 default
%td
- if protected_tag.wildcard?
- matching_tags = protected_tag.matching(repository.tags)
= link_to pluralize(matching_tags.count, "matching tag"), namespace_project_protected_tag_path(@project.namespace, @project, protected_tag)
- else
- if commit = protected_tag.commit
= link_to(commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit.id), class: 'commit_short_id')
= time_ago_with_tooltip(commit.committed_date)
- else
(tag was removed from repository)
= render partial: 'projects/protected_tags/update_protected_tag', locals: { protected_tag: protected_tag }
- if can_admin_project
%td
= link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_tag], data: { confirm: 'tag will be writable for developers. Are you sure?' }, method: :delete, class: 'btn btn-warning'
.panel.panel-default.protected-tags-list.js-protected-tags-list
- if @protected_tags.empty?
.panel-heading
%h3.panel-title
Protected tag (#{@protected_tags.size})
%p.settings-message.text-center
There are currently no protected tags, protect a tag with the form above.
- else
- can_admin_project = can?(current_user, :admin_project, @project)
%table.table.table-bordered
%colgroup
%col{ width: "25%" }
%col{ width: "25%" }
%col{ width: "50%" }
%thead
%tr
%th Protected tag (#{@protected_tags.size})
%th Last commit
%th Allowed to create
- if can_admin_project
%th
%tbody
%tr
%td.flash-container{ colspan: 4 }
= render partial: 'projects/protected_tags/protected_tag', collection: @protected_tags, locals: { can_admin_project: can_admin_project}
= paginate @protected_tags, theme: 'gitlab'
%td
= hidden_field_tag "allowed_to_create_#{protected_tag.id}", protected_tag.create_access_levels.first.access_level
= dropdown_tag( (protected_tag.create_access_levels.first.humanize || 'Select') ,
options: { toggle_class: 'js-allowed-to-create', dropdown_class: 'dropdown-menu-selectable js-allowed-to-create-container',
data: { field_name: "allowed_to_create_#{protected_tag.id}", access_level_id: protected_tag.create_access_levels.first.id }})
- page_title @protected_ref.name, "Protected Tags"
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
= @protected_ref.name
.col-lg-9
%h5 Matching Tags
- if @matching_refs.present?
.table-responsive
%table.table.protected-tags-list
%colgroup
%col{ width: "30%" }
%col{ width: "30%" }
%thead
%tr
%th Tag
%th Last commit
%tbody
- @matching_refs.each do |matching_tag|
= render partial: "matching_tag", object: matching_tag
- else
%p.settings-message.text-center
Couldn't find any matching tags.
...@@ -3,3 +3,4 @@ ...@@ -3,3 +3,4 @@
= render @deploy_keys = render @deploy_keys
= render "projects/protected_branches/index" = render "projects/protected_branches/index"
= render "projects/protected_tags/index"
...@@ -6,6 +6,11 @@ ...@@ -6,6 +6,11 @@
%span.item-title %span.item-title
= icon('tag') = icon('tag')
= tag.name = tag.name
- if protected_tag?(@project, tag)
%span.label.label-success
protected
- if tag.message.present? - if tag.message.present?
&nbsp; &nbsp;
= strip_gpg_signature(tag.message) = strip_gpg_signature(tag.message)
...@@ -30,5 +35,5 @@ ...@@ -30,5 +35,5 @@
= icon("pencil") = icon("pencil")
- if can?(current_user, :admin_project, @project) - if can?(current_user, :admin_project, @project)
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o") = icon("trash-o")
...@@ -7,6 +7,9 @@ ...@@ -7,6 +7,9 @@
.nav-text .nav-text
.title .title
%span.item-title= @tag.name %span.item-title= @tag.name
- if protected_tag?(@project, @tag)
%span.label.label-success
protected
- if @commit - if @commit
= render 'projects/branches/commit', commit: @commit, project: @project = render 'projects/branches/commit', commit: @commit, project: @project
- else - else
...@@ -24,7 +27,7 @@ ...@@ -24,7 +27,7 @@
= render 'projects/buttons/download', project: @project, ref: @tag.name = render 'projects/buttons/download', project: @project, ref: @tag.name
- if can?(current_user, :admin_project, @project) - if can?(current_user, :admin_project, @project)
.btn-container.controls-item-full .btn-container.controls-item-full
= link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-remove remove-row has-tooltip #{protected_tag?(@project, @tag) ? 'disabled' : ''}", title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
%i.fa.fa-trash-o %i.fa.fa-trash-o
- if @tag.message.present? - if @tag.message.present?
......
...@@ -8,4 +8,26 @@ ...@@ -8,4 +8,26 @@
.form-group .form-group
= f.label :key, "Description", class: "label-light" = f.label :key, "Description", class: "label-light"
= f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description" = f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description"
- if @trigger.persisted?
%hr
= f.fields_for :trigger_schedule do |schedule_fields|
= schedule_fields.hidden_field :id
.form-group
.checkbox
= schedule_fields.label :active do
= schedule_fields.check_box :active
%strong Schedule trigger (experimental)
.help-block
If checked, this trigger will be executed periodically according to cron and timezone.
= link_to icon('question-circle'), help_page_path('ci/triggers', anchor: 'schedule')
.form-group
= schedule_fields.label :cron, "Cron", class: "label-light"
= schedule_fields.text_field :cron, class: "form-control", title: 'Cron specification is required.', placeholder: "0 1 * * *"
.form-group
= schedule_fields.label :cron, "Timezone", class: "label-light"
= schedule_fields.text_field :cron_timezone, class: "form-control", title: 'Timezone is required.', placeholder: "UTC"
.form-group
= schedule_fields.label :ref, "Branch or tag", class: "label-light"
= schedule_fields.text_field :ref, class: "form-control", title: 'Branch or tag is required.', placeholder: "master"
.help-block Existing branch name, tag
= f.submit btn_text, class: "btn btn-save" = f.submit btn_text, class: "btn btn-save"
...@@ -22,6 +22,8 @@ ...@@ -22,6 +22,8 @@
%th %th
%strong Last used %strong Last used
%th %th
%strong Next run at
%th
= render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger = render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
- else - else
%p.settings-message.text-center.append-bottom-default %p.settings-message.text-center.append-bottom-default
......
...@@ -29,6 +29,12 @@ ...@@ -29,6 +29,12 @@
- else - else
Never Never
%td
- if trigger.trigger_schedule&.active?
= trigger.trigger_schedule.real_next_run
- else
Never
%td.text-right.trigger-actions %td.text-right.trigger-actions
- take_ownership_confirmation = "By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?" - take_ownership_confirmation = "By taking ownership you will bind this trigger to your user account. With this the trigger will have access to all your projects as if it was you. Are you sure?"
- revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?" - revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?"
......
...@@ -3,7 +3,7 @@ class TriggerScheduleWorker ...@@ -3,7 +3,7 @@ class TriggerScheduleWorker
include CronjobQueue include CronjobQueue
def perform def perform
Ci::TriggerSchedule.where("next_run_at < ?", Time.now).find_each do |trigger_schedule| Ci::TriggerSchedule.active.where("next_run_at < ?", Time.now).find_each do |trigger_schedule|
begin begin
Ci::CreateTriggerRequestService.new.execute(trigger_schedule.project, Ci::CreateTriggerRequestService.new.execute(trigger_schedule.project,
trigger_schedule.trigger, trigger_schedule.trigger,
......
---
title: Tags can be protected, restricting creation of matching tags by user role
merge_request: 10356
author:
---
title: Add UI for Trigger Schedule
merge_request: 10533
author: dosuken123
---
title: Optimise builds endpoint
merge_request:
author:
...@@ -135,6 +135,8 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -135,6 +135,8 @@ constraints(ProjectUrlConstrainer.new) do
end end
resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :protected_tags, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :variables, only: [:index, :show, :update, :create, :destroy] resources :variables, only: [:index, :show, :update, :create, :destroy]
resources :triggers, only: [:index, :create, :edit, :update, :destroy] do resources :triggers, only: [:index, :create, :edit, :update, :destroy] do
member do member do
......
...@@ -41,6 +41,7 @@ var config = { ...@@ -41,6 +41,7 @@ var config = {
pdf_viewer: './blob/pdf_viewer.js', pdf_viewer: './blob/pdf_viewer.js',
profile: './profile/profile_bundle.js', profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js', protected_branches: './protected_branches/protected_branches_bundle.js',
protected_tags: './protected_tags',
snippet: './snippet/snippet_bundle.js', snippet: './snippet/snippet_bundle.js',
stl_viewer: './blob/stl_viewer.js', stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js', terminal: './terminal/terminal_bundle.js',
......
class CreateProtectedTags < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
GITLAB_ACCESS_MASTER = 40
def change
create_table :protected_tags do |t|
t.integer :project_id, null: false
t.string :name, null: false
t.timestamps null: false
end
add_index :protected_tags, :project_id
create_table :protected_tag_create_access_levels do |t|
t.references :protected_tag, index: { name: "index_protected_tag_create_access" }, foreign_key: true, null: false
t.integer :access_level, default: GITLAB_ACCESS_MASTER, null: true
t.references :user, foreign_key: true, index: true
t.integer :group_id
t.timestamps null: false
end
add_foreign_key :protected_tag_create_access_levels, :namespaces, column: :group_id # rubocop: disable Migration/AddConcurrentForeignKey
end
end
class AddRefToCiTriggerSchedule < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_trigger_schedules, :ref, :string
end
end
class AddActiveToCiTriggerSchedule < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_trigger_schedules, :active, :boolean
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddIndexToNextRunAtAndActive < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_trigger_schedules, [:active, :next_run_at]
end
def down
remove_concurrent_index :ci_trigger_schedules, [:active, :next_run_at]
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170406115029) do ActiveRecord::Schema.define(version: 20170407140450) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
enable_extension "pg_trgm" enable_extension "pg_trgm"
...@@ -310,8 +310,11 @@ ActiveRecord::Schema.define(version: 20170406115029) do ...@@ -310,8 +310,11 @@ ActiveRecord::Schema.define(version: 20170406115029) do
t.string "cron" t.string "cron"
t.string "cron_timezone" t.string "cron_timezone"
t.datetime "next_run_at" t.datetime "next_run_at"
t.string "ref"
t.boolean "active"
end end
add_index "ci_trigger_schedules", ["active", "next_run_at"], name: "index_ci_trigger_schedules_on_active_and_next_run_at", using: :btree
add_index "ci_trigger_schedules", ["next_run_at"], name: "index_ci_trigger_schedules_on_next_run_at", using: :btree add_index "ci_trigger_schedules", ["next_run_at"], name: "index_ci_trigger_schedules_on_next_run_at", using: :btree
add_index "ci_trigger_schedules", ["project_id"], name: "index_ci_trigger_schedules_on_project_id", using: :btree add_index "ci_trigger_schedules", ["project_id"], name: "index_ci_trigger_schedules_on_project_id", using: :btree
...@@ -994,6 +997,27 @@ ActiveRecord::Schema.define(version: 20170406115029) do ...@@ -994,6 +997,27 @@ ActiveRecord::Schema.define(version: 20170406115029) do
add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
create_table "protected_tag_create_access_levels", force: :cascade do |t|
t.integer "protected_tag_id", null: false
t.integer "access_level", default: 40
t.integer "user_id"
t.integer "group_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "protected_tag_create_access_levels", ["protected_tag_id"], name: "index_protected_tag_create_access", using: :btree
add_index "protected_tag_create_access_levels", ["user_id"], name: "index_protected_tag_create_access_levels_on_user_id", using: :btree
create_table "protected_tags", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "protected_tags", ["project_id"], name: "index_protected_tags_on_project_id", using: :btree
create_table "releases", force: :cascade do |t| create_table "releases", force: :cascade do |t|
t.string "tag" t.string "tag"
t.text "description" t.text "description"
...@@ -1352,6 +1376,9 @@ ActiveRecord::Schema.define(version: 20170406115029) do ...@@ -1352,6 +1376,9 @@ ActiveRecord::Schema.define(version: 20170406115029) do
add_foreign_key "project_statistics", "projects", on_delete: :cascade add_foreign_key "project_statistics", "projects", on_delete: :cascade
add_foreign_key "protected_branch_merge_access_levels", "protected_branches" add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches" add_foreign_key "protected_branch_push_access_levels", "protected_branches"
add_foreign_key "protected_tag_create_access_levels", "namespaces", column: "group_id"
add_foreign_key "protected_tag_create_access_levels", "protected_tags"
add_foreign_key "protected_tag_create_access_levels", "users"
add_foreign_key "subscriptions", "projects", on_delete: :cascade add_foreign_key "subscriptions", "projects", on_delete: :cascade
add_foreign_key "system_note_metadata", "notes", name: "fk_d83a918cb1", on_delete: :cascade add_foreign_key "system_note_metadata", "notes", name: "fk_d83a918cb1", on_delete: :cascade
add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
......
...@@ -311,7 +311,7 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach ...@@ -311,7 +311,7 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ export GITLAB_USER_ID=42 ++ export GITLAB_USER_ID=42
++ GITLAB_USER_ID=42 ++ GITLAB_USER_ID=42
++ export GITLAB_USER_EMAIL=user@example.com ++ export GITLAB_USER_EMAIL=user@example.com
++ GITLAB_USER_EMAIL=axilleas@axilleas.me ++ GITLAB_USER_EMAIL=user@example.com
++ export VERY_SECURE_VARIABLE=imaverysecurevariable ++ export VERY_SECURE_VARIABLE=imaverysecurevariable
++ VERY_SECURE_VARIABLE=imaverysecurevariable ++ VERY_SECURE_VARIABLE=imaverysecurevariable
++ mkdir -p /builds/gitlab-examples/ci-debug-trace.tmp ++ mkdir -p /builds/gitlab-examples/ci-debug-trace.tmp
......
...@@ -411,6 +411,10 @@ An [object-relational](https://en.wikipedia.org/wiki/PostgreSQL) database. Toute ...@@ -411,6 +411,10 @@ An [object-relational](https://en.wikipedia.org/wiki/PostgreSQL) database. Toute
A [feature](https://docs.gitlab.com/ce/user/project/protected_branches.html) that protects branches from unauthorized pushes, force pushing or deletion. A [feature](https://docs.gitlab.com/ce/user/project/protected_branches.html) that protects branches from unauthorized pushes, force pushing or deletion.
### Protected Tags
A [feature](https://docs.gitlab.com/ce/user/project/protected_tags.html) that protects tags from unauthorized creation, update or deletion
### Pull ### Pull
Git command to [synchronize](https://git-scm.com/docs/git-pull) the local repository with the remote repository, by fetching all remote changes and merging them into the local repository. Git command to [synchronize](https://git-scm.com/docs/git-pull) the local repository with the remote repository, by fetching all remote changes and merging them into the local repository.
......
...@@ -55,6 +55,7 @@ The following table depicts the various user permission levels in a project. ...@@ -55,6 +55,7 @@ The following table depicts the various user permission levels in a project.
| Push to protected branches | | | | ✓ | ✓ | | Push to protected branches | | | | ✓ | ✓ |
| Enable/disable branch protection | | | | ✓ | ✓ | | Enable/disable branch protection | | | | ✓ | ✓ |
| Turn on/off protected branch push for devs| | | | ✓ | ✓ | | Turn on/off protected branch push for devs| | | | ✓ | ✓ |
| Enable/disable tag protections | | | | ✓ | ✓ |
| Rewrite/remove Git tags | | | | ✓ | ✓ | | Rewrite/remove Git tags | | | | ✓ | ✓ |
| Edit project | | | | ✓ | ✓ | | Edit project | | | | ✓ | ✓ |
| Add deploy keys to project | | | | ✓ | ✓ | | Add deploy keys to project | | | | ✓ | ✓ |
......
# Protected Tags
> [Introduced][ce-10356] in GitLab 9.1.
Protected Tags allow control over who has permission to create tags as well as preventing accidental update or deletion once created. Each rule allows you to match either an individual tag name, or use wildcards to control multiple tags at once.
This feature evolved out of [Protected Branches](protected_branches.md)
## Overview
Protected tags will prevent anyone from updating or deleting the tag, as and will prevent creation of matching tags based on the permissions you have selected. By default, anyone without Master permission will be prevented from creating tags.
## Configuring protected tags
To protect a tag, you need to have at least Master permission level.
1. Navigate to the project's Settings -> Repository page
![Repository Settings](img/project_repository_settings.png)
1. From the **Tag** dropdown menu, select the tag you want to protect or type and click `Create wildcard`. In the screenshot below, we chose to protect all tags matching `v*`.
![Protected tags page](img/protected_tags_page.png)
1. From the `Allowed to create` dropdown, select who will have permission to create matching tags and then click `Protect`.
![Allowed to create tags dropdown](img/protected_tags_permissions_dropdown.png)
1. Once done, the protected tag will appear in the "Protected tags" list.
![Protected tags list](img/protected_tags_list.png)
## Wildcard protected tags
You can specify a wildcard protected tag, which will protect all tags
matching the wildcard. For example:
| Wildcard Protected Tag | Matching Tags |
|------------------------+-------------------------------|
| `v*` | `v1.0.0`, `version-9.1` |
| `*-deploy` | `march-deploy`, `1.0-deploy` |
| `*gitlab*` | `gitlab`, `gitlab/v1` |
| `*` | `v1.0.1rc2`, `accidental-tag` |
Two different wildcards can potentially match the same tag. For example,
`*-stable` and `production-*` would both match a `production-stable` tag.
In that case, if _any_ of these protected tags have a setting like
"Allowed to create", then `production-stable` will also inherit this setting.
If you click on a protected tag's name, you will be presented with a list of
all matching tags:
![Protected tag matches](img/protected_tag_matches.png)
---
[ce-10356]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10356 "Protected Tags"
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
- [Project forking workflow](forking_workflow.md) - [Project forking workflow](forking_workflow.md)
- [Project users](add-user/add-user.md) - [Project users](add-user/add-user.md)
- [Protected branches](../user/project/protected_branches.md) - [Protected branches](../user/project/protected_branches.md)
- [Protected tags](../user/project/protected_tags.md)
- [Slash commands](../user/project/slash_commands.md) - [Slash commands](../user/project/slash_commands.md)
- [Sharing a project with a group](share_with_group.md) - [Sharing a project with a group](share_with_group.md)
- [Share projects with other groups](share_projects_with_other_groups.md) - [Share projects with other groups](share_projects_with_other_groups.md)
......
...@@ -251,7 +251,8 @@ module SharedProject ...@@ -251,7 +251,8 @@ module SharedProject
step 'project "Shop" has CI build' do step 'project "Shop" has CI build' do
project = Project.find_by(name: "Shop") project = Project.find_by(name: "Shop")
create :ci_pipeline, project: project, sha: project.commit.sha, ref: 'master', status: 'skipped' pipeline = create :ci_pipeline, project: project, sha: project.commit.sha, ref: 'master'
pipeline.skip
end end
step 'I should see last commit with CI status' do step 'I should see last commit with CI status' do
......
...@@ -184,19 +184,15 @@ module API ...@@ -184,19 +184,15 @@ module API
end end
expose :protected do |repo_branch, options| expose :protected do |repo_branch, options|
options[:project].protected_branch?(repo_branch.name) ProtectedBranch.protected?(options[:project], repo_branch.name)
end end
expose :developers_can_push do |repo_branch, options| expose :developers_can_push do |repo_branch, options|
project = options[:project] options[:project].protected_branches.developers_can?(:push, repo_branch.name)
access_levels = project.protected_branches.matching(repo_branch.name).map(&:push_access_levels).flatten
access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
end end
expose :developers_can_merge do |repo_branch, options| expose :developers_can_merge do |repo_branch, options|
project = options[:project] options[:project].protected_branches.developers_can?(:merge, repo_branch.name)
access_levels = project.protected_branches.matching(repo_branch.name).map(&:merge_access_levels).flatten
access_levels.any? { |access_level| access_level.access_level == Gitlab::Access::DEVELOPER }
end end
end end
......
# This class is not backed by a table in the main database.
# It loads the latest Pipeline for the HEAD of a repository, and caches that
# in Redis.
module Gitlab
module Cache
module Ci
class ProjectPipelineStatus
attr_accessor :sha, :status, :ref, :project, :loaded
delegate :commit, to: :project
def self.load_for_project(project)
new(project).tap do |status|
status.load_status
end
end
def self.update_for_pipeline(pipeline)
new(pipeline.project,
sha: pipeline.sha,
status: pipeline.status,
ref: pipeline.ref).store_in_cache_if_needed
end
def initialize(project, sha: nil, status: nil, ref: nil)
@project = project
@sha = sha
@ref = ref
@status = status
end
def has_status?
loaded? && sha.present? && status.present?
end
def load_status
return if loaded?
if has_cache?
load_from_cache
else
load_from_project
store_in_cache
end
self.loaded = true
end
def load_from_project
return unless commit
self.sha = commit.sha
self.status = commit.status
self.ref = project.default_branch
end
# We only cache the status for the HEAD commit of a project
# This status is rendered in project lists
def store_in_cache_if_needed
return delete_from_cache unless commit
return unless sha
return unless ref
if commit.sha == sha && project.default_branch == ref
store_in_cache
end
end
def load_from_cache
Gitlab::Redis.with do |redis|
self.sha, self.status, self.ref = redis.hmget(cache_key, :sha, :status, :ref)
end
end
def store_in_cache
Gitlab::Redis.with do |redis|
redis.mapped_hmset(cache_key, { sha: sha, status: status, ref: ref })
end
end
def delete_from_cache
Gitlab::Redis.with do |redis|
redis.del(cache_key)
end
end
def has_cache?
Gitlab::Redis.with do |redis|
redis.exists(cache_key)
end
end
def loaded?
self.loaded
end
def cache_key
"projects/#{project.id}/build_status"
end
end
end
end
end
...@@ -10,6 +10,7 @@ module Gitlab ...@@ -10,6 +10,7 @@ module Gitlab
) )
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref) @branch_name = Gitlab::Git.branch_name(@ref)
@tag_name = Gitlab::Git.tag_name(@ref)
@user_access = user_access @user_access = user_access
@project = project @project = project
@env = env @env = env
...@@ -32,11 +33,11 @@ module Gitlab ...@@ -32,11 +33,11 @@ module Gitlab
def protected_branch_checks def protected_branch_checks
return if skip_authorization return if skip_authorization
return unless @branch_name return unless @branch_name
return unless project.protected_branch?(@branch_name) return unless ProtectedBranch.protected?(project, @branch_name)
if forced_push? if forced_push?
return "You are not allowed to force push code to a protected branch on this project." return "You are not allowed to force push code to a protected branch on this project."
elsif Gitlab::Git.blank_ref?(@newrev) elsif deletion?
return "You are not allowed to delete protected branches from this project." return "You are not allowed to delete protected branches from this project."
end end
...@@ -58,13 +59,29 @@ module Gitlab ...@@ -58,13 +59,29 @@ module Gitlab
def tag_checks def tag_checks
return if skip_authorization return if skip_authorization
tag_ref = Gitlab::Git.tag_name(@ref) return unless @tag_name
if tag_ref && protected_tag?(tag_ref) && user_access.cannot_do_action?(:admin_project) if tag_exists? && user_access.cannot_do_action?(:admin_project)
"You are not allowed to change existing tags on this project." return "You are not allowed to change existing tags on this project."
end
protected_tag_checks
end
def protected_tag_checks
return unless tag_protected?
return "Protected tags cannot be updated." if update?
return "Protected tags cannot be deleted." if deletion?
unless user_access.can_create_tag?(@tag_name)
return "You are not allowed to create this tag as it is protected."
end end
end end
def tag_protected?
ProtectedTag.protected?(project, @tag_name)
end
def push_checks def push_checks
return if skip_authorization return if skip_authorization
...@@ -75,14 +92,22 @@ module Gitlab ...@@ -75,14 +92,22 @@ module Gitlab
private private
def protected_tag?(tag_name) def tag_exists?
project.repository.tag_exists?(tag_name) project.repository.tag_exists?(@tag_name)
end end
def forced_push? def forced_push?
Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev, env: @env) Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev, env: @env)
end end
def update?
!Gitlab::Git.blank_ref?(@oldrev) && !deletion?
end
def deletion?
Gitlab::Git.blank_ref?(@newrev)
end
def matching_merge_request? def matching_merge_request?
Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match? Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match?
end end
......
...@@ -47,6 +47,8 @@ project_tree: ...@@ -47,6 +47,8 @@ project_tree:
- protected_branches: - protected_branches:
- :merge_access_levels - :merge_access_levels
- :push_access_levels - :push_access_levels
- protected_tags:
- :create_access_levels
- :project_feature - :project_feature
# Only include the following attributes for the models specified. # Only include the following attributes for the models specified.
......
...@@ -52,7 +52,11 @@ module Gitlab ...@@ -52,7 +52,11 @@ module Gitlab
create_sub_relations(relation, @tree_hash) if relation.is_a?(Hash) create_sub_relations(relation, @tree_hash) if relation.is_a?(Hash)
relation_key = relation.is_a?(Hash) ? relation.keys.first : relation relation_key = relation.is_a?(Hash) ? relation.keys.first : relation
relation_hash = create_relation(relation_key, @tree_hash[relation_key.to_s]) relation_hash_list = @tree_hash[relation_key.to_s]
next unless relation_hash_list
relation_hash = create_relation(relation_key, relation_hash_list)
saved << restored_project.append_or_update_attribute(relation_key, relation_hash) saved << restored_project.append_or_update_attribute(relation_key, relation_hash)
end end
saved.all? saved.all?
......
...@@ -10,6 +10,7 @@ module Gitlab ...@@ -10,6 +10,7 @@ module Gitlab
hooks: 'ProjectHook', hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel', merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel', push_access_levels: 'ProtectedBranch::PushAccessLevel',
create_access_levels: 'ProtectedTag::CreateAccessLevel',
labels: :project_labels, labels: :project_labels,
priorities: :label_priorities, priorities: :label_priorities,
label: :project_label }.freeze label: :project_label }.freeze
......
...@@ -28,14 +28,23 @@ module Gitlab ...@@ -28,14 +28,23 @@ module Gitlab
true true
end end
def can_create_tag?(ref)
return false unless can_access_git?
if ProtectedTag.protected?(project, ref)
project.protected_tags.protected_ref_accessible_to?(ref, user, action: :create)
else
user.can?(:push_code, project)
end
end
def can_push_to_branch?(ref) def can_push_to_branch?(ref)
return false unless can_access_git? return false unless can_access_git?
if project.protected_branch?(ref) if ProtectedBranch.protected?(project, ref)
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user) return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten has_access = project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push)
has_access = access_levels.any? { |access_level| access_level.check_access(user) }
has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref) has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref)
else else
...@@ -46,9 +55,8 @@ module Gitlab ...@@ -46,9 +55,8 @@ module Gitlab
def can_merge_to_branch?(ref) def can_merge_to_branch?(ref)
return false unless can_access_git? return false unless can_access_git?
if project.protected_branch?(ref) if ProtectedBranch.protected?(project, ref)
access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten project.protected_branches.protected_ref_accessible_to?(ref, user, action: :merge)
access_levels.any? { |access_level| access_level.check_access(user) }
else else
user.can?(:push_code, project) user.can?(:push_code, project)
end end
......
...@@ -48,9 +48,16 @@ class NewImporter < ::Gitlab::GithubImport::Importer ...@@ -48,9 +48,16 @@ class NewImporter < ::Gitlab::GithubImport::Importer
begin begin
raise 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url) raise 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url)
gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url) project.create_repository
project.repository.add_remote(project.import_type, project.import_url)
project.repository.set_remote_as_mirror(project.import_type)
project.repository.fetch_remote(project.import_type, forced: true)
project.repository.remove_remote(project.import_type)
rescue => e rescue => e
project.repository.before_import if project.repository_exists? # Expire cache to prevent scenarios such as:
# 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
# 2. Retried import, repo is broken or not imported but +exists?+ still returns true
project.repository.expire_content_cache if project.repository_exists?
raise "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}" raise "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
end end
......
...@@ -10,6 +10,39 @@ describe Projects::BuildsController do ...@@ -10,6 +10,39 @@ describe Projects::BuildsController do
sign_in(user) sign_in(user)
end end
describe 'GET index' do
context 'number of queries' do
before do
Ci::Build::AVAILABLE_STATUSES.each do |status|
create_build(status, status)
end
RequestStore.begin!
end
after do
RequestStore.end!
RequestStore.clear!
end
def render
get :index, namespace_id: project.namespace,
project_id: project
end
it "verifies number of queries" do
recorded = ActiveRecord::QueryRecorder.new { render }
expect(recorded.count).to be_within(5).of(8)
end
def create_build(name, status)
pipeline = create(:ci_pipeline, project: project)
create(:ci_build, :tags, :triggered, :artifacts,
pipeline: pipeline, name: name, status: status)
end
end
end
describe 'GET status.json' do describe 'GET status.json' do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) }
......
...@@ -3,6 +3,7 @@ require('spec_helper') ...@@ -3,6 +3,7 @@ require('spec_helper')
describe Projects::ProtectedBranchesController do describe Projects::ProtectedBranchesController do
describe "GET #index" do describe "GET #index" do
let(:project) { create(:project_empty_repo, :public) } let(:project) { create(:project_empty_repo, :public) }
it "redirects empty repo to projects page" do it "redirects empty repo to projects page" do
get(:index, namespace_id: project.namespace.to_param, project_id: project) get(:index, namespace_id: project.namespace.to_param, project_id: project)
end end
......
require('spec_helper')
describe Projects::ProtectedTagsController do
describe "GET #index" do
let(:project) { create(:project_empty_repo, :public) }
it "redirects empty repo to projects page" do
get(:index, namespace_id: project.namespace.to_param, project_id: project)
end
end
end
...@@ -3,9 +3,11 @@ FactoryGirl.define do ...@@ -3,9 +3,11 @@ FactoryGirl.define do
trigger factory: :ci_trigger_for_trigger_schedule trigger factory: :ci_trigger_for_trigger_schedule
cron '0 1 * * *' cron '0 1 * * *'
cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
ref 'master'
active true
after(:build) do |trigger_schedule, evaluator| after(:build) do |trigger_schedule, evaluator|
trigger_schedule.update!(project: trigger_schedule.trigger.project) trigger_schedule.project ||= trigger_schedule.trigger.project
end end
trait :nightly do trait :nightly do
......
FactoryGirl.define do
factory :protected_tag do
name
project
after(:build) do |protected_tag|
protected_tag.create_access_levels.new(access_level: Gitlab::Access::MASTER)
end
trait :developers_can_create do
after(:create) do |protected_tag|
protected_tag.create_access_levels.first.update!(access_level: Gitlab::Access::DEVELOPER)
end
end
trait :no_one_can_create do
after(:create) do |protected_tag|
protected_tag.create_access_levels.first.update!(access_level: Gitlab::Access::NO_ACCESS)
end
end
end
end
...@@ -7,7 +7,6 @@ RSpec.describe 'Dashboard Projects', feature: true do ...@@ -7,7 +7,6 @@ RSpec.describe 'Dashboard Projects', feature: true do
before do before do
project.team << [user, :developer] project.team << [user, :developer]
login_as user login_as user
visit dashboard_projects_path
end end
it 'shows the project the user in a member of in the list' do it 'shows the project the user in a member of in the list' do
...@@ -15,15 +14,19 @@ RSpec.describe 'Dashboard Projects', feature: true do ...@@ -15,15 +14,19 @@ RSpec.describe 'Dashboard Projects', feature: true do
expect(page).to have_content('awesome stuff') expect(page).to have_content('awesome stuff')
end end
describe "with a pipeline" do describe "with a pipeline", redis: true do
let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) } let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
before do before do
pipeline # Since the cache isn't updated when a new pipeline is created
# we need the pipeline to advance in the pipeline since the cache was created
# by visiting the login page.
pipeline.succeed
end end
it 'shows that the last pipeline passed' do it 'shows that the last pipeline passed' do
visit dashboard_projects_path visit dashboard_projects_path
expect(page).to have_xpath("//a[@href='#{pipelines_namespace_project_commit_path(project.namespace, project, project.commit)}']") expect(page).to have_xpath("//a[@href='#{pipelines_namespace_project_commit_path(project.namespace, project, project.commit)}']")
end end
end end
......
...@@ -2,7 +2,9 @@ RSpec.shared_examples "protected branches > access control > CE" do ...@@ -2,7 +2,9 @@ RSpec.shared_examples "protected branches > access control > CE" do
ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
it "allows creating protected branches that #{access_type_name} can push to" do it "allows creating protected branches that #{access_type_name} can push to" do
visit namespace_project_protected_branches_path(project.namespace, project) visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master') set_protected_branch_name('master')
within('.new_protected_branch') do within('.new_protected_branch') do
allowed_to_push_button = find(".js-allowed-to-push") allowed_to_push_button = find(".js-allowed-to-push")
...@@ -11,6 +13,7 @@ RSpec.shared_examples "protected branches > access control > CE" do ...@@ -11,6 +13,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
within(".dropdown.open .dropdown-menu") { click_on access_type_name } within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end end
end end
click_on "Protect" click_on "Protect"
expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.count).to eq(1)
...@@ -19,7 +22,9 @@ RSpec.shared_examples "protected branches > access control > CE" do ...@@ -19,7 +22,9 @@ RSpec.shared_examples "protected branches > access control > CE" do
it "allows updating protected branches so that #{access_type_name} can push to them" do it "allows updating protected branches so that #{access_type_name} can push to them" do
visit namespace_project_protected_branches_path(project.namespace, project) visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master') set_protected_branch_name('master')
click_on "Protect" click_on "Protect"
expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.count).to eq(1)
...@@ -34,6 +39,7 @@ RSpec.shared_examples "protected branches > access control > CE" do ...@@ -34,6 +39,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
end end
wait_for_ajax wait_for_ajax
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id) expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
end end
end end
...@@ -41,7 +47,9 @@ RSpec.shared_examples "protected branches > access control > CE" do ...@@ -41,7 +47,9 @@ RSpec.shared_examples "protected branches > access control > CE" do
ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
it "allows creating protected branches that #{access_type_name} can merge to" do it "allows creating protected branches that #{access_type_name} can merge to" do
visit namespace_project_protected_branches_path(project.namespace, project) visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master') set_protected_branch_name('master')
within('.new_protected_branch') do within('.new_protected_branch') do
allowed_to_merge_button = find(".js-allowed-to-merge") allowed_to_merge_button = find(".js-allowed-to-merge")
...@@ -50,6 +58,7 @@ RSpec.shared_examples "protected branches > access control > CE" do ...@@ -50,6 +58,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
within(".dropdown.open .dropdown-menu") { click_on access_type_name } within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end end
end end
click_on "Protect" click_on "Protect"
expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.count).to eq(1)
...@@ -58,7 +67,9 @@ RSpec.shared_examples "protected branches > access control > CE" do ...@@ -58,7 +67,9 @@ RSpec.shared_examples "protected branches > access control > CE" do
it "allows updating protected branches so that #{access_type_name} can merge to them" do it "allows updating protected branches so that #{access_type_name} can merge to them" do
visit namespace_project_protected_branches_path(project.namespace, project) visit namespace_project_protected_branches_path(project.namespace, project)
set_protected_branch_name('master') set_protected_branch_name('master')
click_on "Protect" click_on "Protect"
expect(ProtectedBranch.count).to eq(1) expect(ProtectedBranch.count).to eq(1)
...@@ -73,6 +84,7 @@ RSpec.shared_examples "protected branches > access control > CE" do ...@@ -73,6 +84,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
end end
wait_for_ajax wait_for_ajax
expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id) expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
end end
end end
......
RSpec.shared_examples "protected tags > access control > CE" do
ProtectedTag::CreateAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
it "allows creating protected tags that #{access_type_name} can create" do
visit namespace_project_protected_tags_path(project.namespace, project)
set_protected_tag_name('master')
within('.js-new-protected-tag') do
allowed_to_create_button = find(".js-allowed-to-create")
unless allowed_to_create_button.text == access_type_name
allowed_to_create_button.click
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end
end
click_on "Protect"
expect(ProtectedTag.count).to eq(1)
expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to eq([access_type_id])
end
it "allows updating protected tags so that #{access_type_name} can create them" do
visit namespace_project_protected_tags_path(project.namespace, project)
set_protected_tag_name('master')
click_on "Protect"
expect(ProtectedTag.count).to eq(1)
within(".protected-tags-list") do
find(".js-allowed-to-create").click
within('.js-allowed-to-create-container') do
expect(first("li")).to have_content("Roles")
click_on access_type_name
end
end
wait_for_ajax
expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to include(access_type_id)
end
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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