summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke "Jared" Bennett <lbennett@gitlab.com>2017-04-07 21:00:40 +0100
committerLuke "Jared" Bennett <lbennett@gitlab.com>2017-04-07 21:00:40 +0100
commit8b98ca50b3068fecc76233714fa7d363703ee55c (patch)
tree97d67331552de76faa9bfb8cec11616a369351fe
parent501d403202d8b4f603dd1864790f56e85633b8ef (diff)
parentd37f1f1cd013a8b7a307e1004e020df086cba9d5 (diff)
downloadgitlab-ce-8b98ca50b3068fecc76233714fa7d363703ee55c.tar.gz
Merge branch 'master' into new-resolvable-discussion
-rw-r--r--.gitlab-ci.yml2
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js4
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js12
-rw-r--r--app/assets/javascripts/dispatcher.js5
-rw-r--r--app/assets/javascripts/merge_request_tabs.js11
-rw-r--r--app/assets/javascripts/milestone_select.js6
-rw-r--r--app/assets/javascripts/protected_tags/index.js2
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js26
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js41
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_dropdown.js86
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js52
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit_list.js18
-rw-r--r--app/assets/javascripts/subscription.js5
-rw-r--r--app/assets/javascripts/users_select.js8
-rw-r--r--app/assets/stylesheets/pages/projects.scss14
-rw-r--r--app/controllers/projects/builds_controller.rb5
-rw-r--r--app/controllers/projects/protected_branches_controller.rb57
-rw-r--r--app/controllers/projects/protected_refs_controller.rb47
-rw-r--r--app/controllers/projects/protected_tags_controller.rb23
-rw-r--r--app/controllers/projects/settings/repository_controller.rb48
-rw-r--r--app/controllers/projects/triggers_controller.rb15
-rw-r--r--app/helpers/branches_helper.rb6
-rw-r--r--app/helpers/tags_helper.rb4
-rw-r--r--app/models/ci/pipeline.rb6
-rw-r--r--app/models/ci/pipeline_status.rb86
-rw-r--r--app/models/ci/trigger.rb6
-rw-r--r--app/models/ci/trigger_schedule.rb21
-rw-r--r--app/models/concerns/has_status.rb2
-rw-r--r--app/models/concerns/protected_branch_access.rb16
-rw-r--r--app/models/concerns/protected_ref.rb42
-rw-r--r--app/models/concerns/protected_ref_access.rb18
-rw-r--r--app/models/concerns/protected_tag_access.rb11
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/project.rb26
-rw-r--r--app/models/protectable_dropdown.rb33
-rw-r--r--app/models/protected_branch.rb58
-rw-r--r--app/models/protected_ref_matcher.rb54
-rw-r--r--app/models/protected_tag.rb14
-rw-r--r--app/models/protected_tag/create_access_level.rb21
-rw-r--r--app/services/ci/expire_pipeline_cache_service.rb2
-rw-r--r--app/services/delete_branch_service.rb2
-rw-r--r--app/services/git_push_service.rb2
-rw-r--r--app/services/protected_branches/update_service.rb7
-rw-r--r--app/services/protected_tags/create_service.rb11
-rw-r--r--app/services/protected_tags/update_service.rb10
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/show.html.haml8
-rw-r--r--app/views/projects/protected_tags/_create_protected_tag.html.haml32
-rw-r--r--app/views/projects/protected_tags/_dropdown.html.haml15
-rw-r--r--app/views/projects/protected_tags/_index.html.haml18
-rw-r--r--app/views/projects/protected_tags/_matching_tag.html.haml9
-rw-r--r--app/views/projects/protected_tags/_protected_tag.html.haml21
-rw-r--r--app/views/projects/protected_tags/_tags_list.html.haml28
-rw-r--r--app/views/projects/protected_tags/_update_protected_tag.haml5
-rw-r--r--app/views/projects/protected_tags/show.html.haml25
-rw-r--r--app/views/projects/settings/repository/show.html.haml1
-rw-r--r--app/views/projects/tags/_tag.html.haml7
-rw-r--r--app/views/projects/tags/show.html.haml5
-rw-r--r--app/views/projects/triggers/_form.html.haml22
-rw-r--r--app/views/projects/triggers/_index.html.haml2
-rw-r--r--app/views/projects/triggers/_trigger.html.haml6
-rw-r--r--app/workers/trigger_schedule_worker.rb2
-rw-r--r--changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml4
-rw-r--r--changelogs/unreleased/add-ui-for-trigger-schedule.yml4
-rw-r--r--changelogs/unreleased/optimise-builds-view.yml4
-rw-r--r--config/routes/project.rb2
-rw-r--r--config/webpack.config.js1
-rw-r--r--db/migrate/20170309173138_create_protected_tags.rb27
-rw-r--r--db/migrate/20170407114956_add_ref_to_ci_trigger_schedule.rb9
-rw-r--r--db/migrate/20170407122426_add_active_to_ci_trigger_schedule.rb9
-rw-r--r--db/migrate/20170407140450_add_index_to_next_run_at_and_active.rb18
-rw-r--r--db/schema.rb29
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/university/glossary/README.md4
-rw-r--r--doc/user/permissions.md1
-rw-r--r--doc/user/project/img/project_repository_settings.pngbin0 -> 35236 bytes
-rw-r--r--doc/user/project/img/protected_tag_matches.pngbin0 -> 85305 bytes
-rw-r--r--doc/user/project/img/protected_tags_list.pngbin0 -> 24490 bytes
-rw-r--r--doc/user/project/img/protected_tags_page.pngbin0 -> 56112 bytes
-rw-r--r--doc/user/project/img/protected_tags_permissions_dropdown.pngbin0 -> 26514 bytes
-rw-r--r--doc/user/project/protected_tags.md60
-rw-r--r--doc/workflow/README.md1
-rw-r--r--features/steps/shared/project.rb3
-rw-r--r--lib/api/entities.rb10
-rw-r--r--lib/gitlab/cache/ci/project_pipeline_status.rb103
-rw-r--r--lib/gitlab/checks/change_access.rb39
-rw-r--r--lib/gitlab/import_export/import_export.yml2
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb6
-rw-r--r--lib/gitlab/import_export/relation_factory.rb1
-rw-r--r--lib/gitlab/user_access.rb20
-rw-r--r--lib/tasks/import.rake11
-rw-r--r--spec/controllers/projects/builds_controller_spec.rb33
-rw-r--r--spec/controllers/projects/protected_branches_controller_spec.rb1
-rw-r--r--spec/controllers/projects/protected_tags_controller_spec.rb11
-rw-r--r--spec/factories/ci/trigger_schedules.rb4
-rw-r--r--spec/factories/protected_tags.rb22
-rw-r--r--spec/features/dashboard/projects_spec.rb11
-rw-r--r--spec/features/protected_branches/access_control_ce_spec.rb12
-rw-r--r--spec/features/protected_tags/access_control_ce_spec.rb46
-rw-r--r--spec/features/protected_tags_spec.rb94
-rw-r--r--spec/features/triggers_spec.rb53
-rw-r--r--spec/helpers/ci_status_helper_spec.rb6
-rw-r--r--spec/helpers/projects_helper_spec.rb2
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js8
-rw-r--r--spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb (renamed from spec/models/ci/pipeline_status_spec.rb)33
-rw-r--r--spec/lib/gitlab/checks/change_access_spec.rb79
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml6
-rw-r--r--spec/lib/gitlab/import_export/project.json18
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml16
-rw-r--r--spec/lib/gitlab/user_access_spec.rb69
-rw-r--r--spec/models/ci/pipeline_spec.rb15
-rw-r--r--spec/models/merge_request_spec.rb2
-rw-r--r--spec/models/project_spec.rb77
-rw-r--r--spec/models/protectable_dropdown_spec.rb25
-rw-r--r--spec/models/protected_branch_spec.rb64
-rw-r--r--spec/models/protected_tag_spec.rb12
-rw-r--r--spec/services/ci/expire_pipeline_cache_service_spec.rb7
-rw-r--r--spec/services/protected_branches/update_service_spec.rb26
-rw-r--r--spec/services/protected_tags/create_service_spec.rb21
-rw-r--r--spec/services/protected_tags/update_service_spec.rb26
-rw-r--r--spec/workers/trigger_schedule_worker_spec.rb33
123 files changed, 1883 insertions, 480 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 66f8b6e6f9a..2d3e3dcd976 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -347,7 +347,7 @@ migration paths:
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
script:
- - git fetch origin v8.5.9
+ - git fetch origin v8.14.10
- git checkout -f FETCH_HEAD
- cp config/resque.yml.example config/resque.yml
- sed -i 's/localhost/redis/g' config/resque.yml
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 67106e85a37..ce426741637 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -51,7 +51,7 @@ function renderCategory(name, emojiList, opts = {}) {
<h5 class="emoji-menu-title">
${name}
</h5>
- <ul class="clearfix emoji-menu-list ${opts.menuListClass}">
+ <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
${emojiList.map(emojiName => `
<li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button">
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/boards_bundle.js
index e057ac8df02..37094c5c9be 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/boards_bundle.js
@@ -38,6 +38,10 @@ $(() => {
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({
el: $boardApp,
components: {
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 5f3ed9374bf..86d99dd87da 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -12,20 +12,18 @@ Vue.use(VueResource);
* 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.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');
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
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);
}
});
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 0a1a62fb012..4e68f9c77e9 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -44,6 +44,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
+import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
const ShortcutsBlob = require('./shortcuts_blob');
@@ -329,8 +330,12 @@ const ShortcutsBlob = require('./shortcuts_blob');
new Search();
break;
case 'projects:repository:show':
+ // Initialize Protected Branch Settings
new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList();
+ // Initialize Protected Tag Settings
+ new ProtectedTagCreate();
+ new ProtectedTagEditList();
break;
case 'projects:ci_cd:show':
new gl.ProjectVariables();
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 8528e0800ae..f7f6a773036 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -3,9 +3,6 @@
/* global Flash */
import Cookies from 'js-cookie';
-
-import CommitPipelinesTable from './commit/pipelines/pipelines_table';
-
import './breakpoints';
import './flash';
@@ -102,10 +99,10 @@ import './flash';
destroyPipelinesView() {
if (this.commitPipelinesTable) {
- document.querySelector('#commit-pipeline-table-view')
- .removeChild(this.commitPipelinesTable.$el);
-
this.commitPipelinesTable.$destroy();
+ this.commitPipelinesTable = null;
+
+ document.querySelector('#commit-pipeline-table-view').innerHTML = '';
}
}
@@ -234,7 +231,7 @@ import './flash';
}
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
// it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
document.querySelector('#commit-pipeline-table-view')
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index ac4fad88fe5..773fe3233a7 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -2,8 +2,6 @@
/* global Issuable */
/* global ListMilestone */
-import Vue from 'vue';
-
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject, els) {
@@ -151,12 +149,12 @@ import Vue from 'vue';
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (selected.id !== -1) {
- Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'milestone', new ListMilestone({
+ gl.issueBoards.boardStoreIssueSet('milestone', new ListMilestone({
id: selected.id,
title: selected.name
}));
} else {
- Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'milestone');
+ gl.issueBoards.boardStoreIssueDelete('milestone');
}
$dropdown.trigger('loading.gl.dropdown');
diff --git a/app/assets/javascripts/protected_tags/index.js b/app/assets/javascripts/protected_tags/index.js
new file mode 100644
index 00000000000..61e7ba53862
--- /dev/null
+++ b/app/assets/javascripts/protected_tags/index.js
@@ -0,0 +1,2 @@
+export { default as ProtectedTagCreate } from './protected_tag_create';
+export { default as ProtectedTagEditList } from './protected_tag_edit_list';
diff --git a/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
new file mode 100644
index 00000000000..fff83f3af3b
--- /dev/null
+++ b/app/assets/javascripts/protected_tags/protected_tag_access_dropdown.js
@@ -0,0 +1,26 @@
+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();
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
new file mode 100644
index 00000000000..91bd140bd12
--- /dev/null
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -0,0 +1,41 @@
+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));
+ }
+}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_dropdown.js b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js
new file mode 100644
index 00000000000..5ff4e443262
--- /dev/null
+++ b/app/assets/javascripts/protected_tags/protected_tag_dropdown.js
@@ -0,0 +1,86 @@
+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);
+ }
+}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
new file mode 100644
index 00000000000..09a387c0f9e
--- /dev/null
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -0,0 +1,52 @@
+/* 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();
+ });
+ }
+}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
new file mode 100644
index 00000000000..bd9fc872266
--- /dev/null
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js
@@ -0,0 +1,18 @@
+/* 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),
+ });
+ });
+ }
+}
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
index 9c307915ec4..5f9a3e00c22 100644
--- a/app/assets/javascripts/subscription.js
+++ b/app/assets/javascripts/subscription.js
@@ -1,5 +1,3 @@
-import Vue from 'vue';
-
(() => {
class Subscription {
constructor(containerElm) {
@@ -29,8 +27,7 @@ import Vue from 'vue';
// hack to allow this to work with the issue boards Vue object
if (document.querySelector('html').classList.contains('issue-boards-page')) {
- Vue.set(
- gl.issueBoards.BoardsStore.detail.issue,
+ gl.issueBoards.boardStoreIssueSet(
'subscribed',
!gl.issueBoards.BoardsStore.detail.issue.subscribed,
);
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 48e20cf501f..3325a7d429c 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -2,8 +2,6 @@
/* global Issuable */
/* global ListUser */
-import Vue from 'vue';
-
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; },
slice = [].slice;
@@ -74,7 +72,7 @@ import Vue from 'vue';
e.preventDefault();
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,
username: _this.currentUser.username,
name: _this.currentUser.name,
@@ -225,14 +223,14 @@ import Vue from 'vue';
return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (user.id) {
- Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
+ gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
id: user.id,
username: user.username,
name: user.name,
avatar_url: user.avatar_url
}));
} else {
- Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'assignee');
+ gl.issueBoards.boardStoreIssueDelete('assignee');
}
updateIssueBoardsIssue();
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 0fa1f68e034..717ebb44a23 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -744,7 +744,8 @@ pre.light-well {
text-align: left;
}
-.protected-branches-list {
+.protected-branches-list,
+.protected-tags-list {
margin-bottom: 30px;
a {
@@ -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 {
.is-loading {
.custom-notification-event-loading {
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index add66ce9f84..04e8cdf6256 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -19,6 +19,11 @@ class Projects::BuildsController < Projects::ApplicationController
else
@builds
end
+ @builds = @builds.includes([
+ { pipeline: :project },
+ :project,
+ :tags
+ ])
@builds = @builds.page(params[:page]).per(30)
end
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index a8cb07eb67a..ba24fa9acfe 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -1,58 +1,23 @@
-class Projects::ProtectedBranchesController < Projects::ApplicationController
- include RepositorySettingsRedirect
- # Authorize
- before_action :require_non_empty_project
- before_action :authorize_admin_project!
- before_action :load_protected_branch, only: [:show, :update, :destroy]
+class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
+ protected
- layout "project_settings"
-
- 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)
+ def project_refs
+ @project.repository.branches
end
- def update
- @protected_branch = ::ProtectedBranches::UpdateService.new(@project, current_user, protected_branch_params).execute(@protected_branch)
-
- 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
+ def create_service_class
+ ::ProtectedBranches::CreateService
end
- def destroy
- @protected_branch.destroy
-
- respond_to do |format|
- format.html { redirect_to_repository_settings(@project) }
- format.js { head :ok }
- end
+ def update_service_class
+ ::ProtectedBranches::UpdateService
end
- private
-
- def load_protected_branch
- @protected_branch = @project.protected_branches.find(params[:id])
+ def load_protected_ref
+ @protected_ref = @project.protected_branches.find(params[:id])
end
- def protected_branch_params
+ def protected_ref_params
params.require(:protected_branch).permit(:name,
merge_access_levels_attributes: [:access_level, :id],
push_access_levels_attributes: [:access_level, :id])
diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb
new file mode 100644
index 00000000000..083a70968e5
--- /dev/null
+++ b/app/controllers/projects/protected_refs_controller.rb
@@ -0,0 +1,47 @@
+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
diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb
new file mode 100644
index 00000000000..c61ddf145e6
--- /dev/null
+++ b/app/controllers/projects/protected_tags_controller.rb
@@ -0,0 +1,23 @@
+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
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index b6ce4abca45..44de8a49593 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -4,46 +4,48 @@ module Projects
before_action :authorize_admin_project!
def show
- @deploy_keys = DeployKeysPresenter
- .new(@project, current_user: current_user)
+ @deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
- define_protected_branches
+ define_protected_refs
end
private
- def define_protected_branches
- load_protected_branches
+ def define_protected_refs
+ @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_tag = @project.protected_tags.new
load_gon_index
end
- def load_protected_branches
- @protected_branches = @project.protected_branches.order(:name).page(params[:page])
- end
-
def access_levels_options
{
- push_access_levels: {
- roles: ProtectedBranch::PushAccessLevel.human_access_levels.map do |id, text|
- { id: id, text: text, before_divider: true }
- end
- },
- merge_access_levels: {
- roles: ProtectedBranch::MergeAccessLevel.human_access_levels.map do |id, text|
- { id: id, text: text, before_divider: true }
- end
- }
+ create_access_levels: levels_for_dropdown(ProtectedTag::CreateAccessLevel),
+ push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel),
+ merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel)
}
end
- def open_branches
- branches = @project.open_branches.map { |br| { text: br.name, id: br.name, title: br.name } }
- { open_branches: branches }
+ def levels_for_dropdown(access_level_type)
+ roles = access_level_type.human_access_levels.map do |id, text|
+ { 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
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
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index c47198c5eb6..afa56de920b 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -11,7 +11,7 @@ class Projects::TriggersController < Projects::ApplicationController
end
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?
flash[:notice] = 'Trigger was created successfully.'
@@ -36,7 +36,7 @@ class Projects::TriggersController < Projects::ApplicationController
end
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.'
else
render action: "edit"
@@ -67,11 +67,10 @@ class Projects::TriggersController < Projects::ApplicationController
@trigger ||= project.triggers.find(params[:id]) || render_404
end
- def create_params
- params.require(:trigger).permit(:description)
- end
-
- def update_params
- params.require(:trigger).permit(:description)
+ def trigger_params
+ params.require(:trigger).permit(
+ :description,
+ trigger_schedule_attributes: [:id, :active, :cron, :cron_timezone, :ref]
+ )
end
end
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index 3fc85dc6b2b..b7a28b1b4a7 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -1,6 +1,6 @@
module BranchesHelper
def can_remove_branch?(project, branch_name)
- if project.protected_branch? branch_name
+ if ProtectedBranch.protected?(project, branch_name)
false
elsif branch_name == project.repository.root_ref
false
@@ -29,4 +29,8 @@ module BranchesHelper
def project_branches
options_for_select(@project.repository.branch_names, @project.default_branch)
end
+
+ def protected_branch?(project, branch)
+ ProtectedBranch.protected?(project, branch.name)
+ end
end
diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb
index c0ec1634cdb..31aaf9e5607 100644
--- a/app/helpers/tags_helper.rb
+++ b/app/helpers/tags_helper.rb
@@ -21,4 +21,8 @@ module TagsHelper
html.html_safe
end
+
+ def protected_tag?(project, tag)
+ ProtectedTag.protected?(project, tag.name)
+ end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 37a81fa7781..445247f1b41 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -31,7 +31,6 @@ module Ci
validate :valid_commit_sha, unless: :importing?
after_create :keep_around_commits, unless: :importing?
- after_create :refresh_build_status_cache
state_machine :status, initial: :created do
event :enqueue do
@@ -351,7 +350,6 @@ module Ci
when 'manual' then block
end
end
- refresh_build_status_cache
end
def predefined_variables
@@ -393,10 +391,6 @@ module Ci
.fabricate!
end
- def refresh_build_status_cache
- Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
- end
-
private
def pipeline_data
diff --git a/app/models/ci/pipeline_status.rb b/app/models/ci/pipeline_status.rb
deleted file mode 100644
index 048047d0e34..00000000000
--- a/app/models/ci/pipeline_status.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-# 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
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 0a89f3e0640..b59e235c425 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -14,6 +14,8 @@ module Ci
before_validation :set_default_values
+ accepts_nested_attributes_for :trigger_schedule
+
def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank?
end
@@ -37,5 +39,9 @@ module Ci
def can_access_project?
self.owner_id.blank? || Ability.allowed?(self.owner, :create_build, project)
end
+
+ def trigger_schedule
+ super || build_trigger_schedule(project: project)
+ end
end
end
diff --git a/app/models/ci/trigger_schedule.rb b/app/models/ci/trigger_schedule.rb
index 10ea381ee31..012a18eb439 100644
--- a/app/models/ci/trigger_schedule.rb
+++ b/app/models/ci/trigger_schedule.rb
@@ -8,15 +8,19 @@ module Ci
belongs_to :project
belongs_to :trigger
- delegate :ref, to: :trigger
-
validates :trigger, presence: { unless: :importing? }
- validates :cron, cron: true, presence: { unless: :importing? }
- validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
- validates :ref, 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_or_inactive? }
+ validates :ref, presence: { unless: :importing_or_inactive? }
before_save :set_next_run_at
+ scope :active, -> { where(active: true) }
+
+ def importing_or_inactive?
+ importing? || !active?
+ end
+
def set_next_run_at
self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
end
@@ -26,5 +30,12 @@ module Ci
rescue ActiveRecord::RecordInvalid
update_attribute(:next_run_at, nil) # update without validation
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
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 2f61709110c..dff7b6e3523 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -68,7 +68,7 @@ module HasStatus
end
scope :created, -> { where(status: 'created') }
- scope :relevant, -> { where.not(status: 'created') }
+ scope :relevant, -> { where(status: AVAILABLE_STATUSES - ['created']) }
scope :running, -> { where(status: 'running') }
scope :pending, -> { where(status: 'pending') }
scope :success, -> { where(status: 'success') }
diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb
index 9dd4d9c6f24..c41b807df8a 100644
--- a/app/models/concerns/protected_branch_access.rb
+++ b/app/models/concerns/protected_branch_access.rb
@@ -2,20 +2,10 @@ module ProtectedBranchAccess
extend ActiveSupport::Concern
included do
- belongs_to :protected_branch
- delegate :project, to: :protected_branch
-
- scope :master, -> { where(access_level: Gitlab::Access::MASTER) }
- scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) }
- end
+ include ProtectedRefAccess
- def humanize
- self.class.human_access_levels[self.access_level]
- end
-
- def check_access(user)
- return true if user.is_admin?
+ belongs_to :protected_branch
- project.team.max_member_access(user.id) >= access_level
+ delegate :project, to: :protected_branch
end
end
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
new file mode 100644
index 00000000000..62eaec2407f
--- /dev/null
+++ b/app/models/concerns/protected_ref.rb
@@ -0,0 +1,42 @@
+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
diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb
new file mode 100644
index 00000000000..c4f158e569a
--- /dev/null
+++ b/app/models/concerns/protected_ref_access.rb
@@ -0,0 +1,18 @@
+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
diff --git a/app/models/concerns/protected_tag_access.rb b/app/models/concerns/protected_tag_access.rb
new file mode 100644
index 00000000000..ee65de24dd8
--- /dev/null
+++ b/app/models/concerns/protected_tag_access.rb
@@ -0,0 +1,11 @@
+module ProtectedTagAccess
+ extend ActiveSupport::Concern
+
+ included do
+ include ProtectedRefAccess
+
+ belongs_to :protected_tag
+
+ delegate :project, to: :protected_tag
+ end
+end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 77c902fdf09..b71a9e17a93 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -443,7 +443,7 @@ class MergeRequest < ActiveRecord::Base
end
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) &&
Ability.allowed?(current_user, :push_code, source_project) &&
diff_head_commit == source_branch_head
diff --git a/app/models/project.rb b/app/models/project.rb
index 639615b91a2..a160efba912 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -135,6 +135,7 @@ class Project < ActiveRecord::Base
has_many :snippets, dependent: :destroy, class_name: 'ProjectSnippet'
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
+ has_many :protected_tags, dependent: :destroy
has_many :project_authorizations
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
@@ -859,14 +860,6 @@ class Project < ActiveRecord::Base
@repo_exists = false
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)
repository.root_ref == branch
end
@@ -881,16 +874,8 @@ class Project < ActiveRecord::Base
Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url
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)
- !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
def forked?
@@ -1197,7 +1182,7 @@ class Project < ActiveRecord::Base
end
def pipeline_status
- @pipeline_status ||= Ci::PipelineStatus.load_for_project(self)
+ @pipeline_status ||= Gitlab::Cache::Ci::ProjectPipelineStatus.load_for_project(self)
end
def mark_import_as_failed(error_message)
@@ -1353,11 +1338,6 @@ class Project < ActiveRecord::Base
"projects/#{id}/pushes_since_gc"
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
# Active Record object, you can also define callbacks that get triggered
# when you add an object to an association collection. If any of these
diff --git a/app/models/protectable_dropdown.rb b/app/models/protectable_dropdown.rb
new file mode 100644
index 00000000000..122fbce257d
--- /dev/null
+++ b/app/models/protectable_dropdown.rb
@@ -0,0 +1,33 @@
+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
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 39e979ef15b..28b7d5ad072 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -1,9 +1,6 @@
class ProtectedBranch < ActiveRecord::Base
include Gitlab::ShellAdapter
-
- belongs_to :project
- validates :name, presence: true
- validates :project, presence: true
+ include ProtectedRef
has_many :merge_access_levels, dependent: :destroy
has_many :push_access_levels, dependent: :destroy
@@ -14,54 +11,15 @@ class ProtectedBranch < ActiveRecord::Base
accepts_nested_attributes_for :push_access_levels
accepts_nested_attributes_for :merge_access_levels
- def commit
- project.commit(self.name)
- end
-
- # 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
+ # Check if branch name is marked as protected in the system
+ def self.protected?(project, ref_name)
+ return true if project.empty_repo? && default_branch_protected?
- def wildcard_match?(branch_name)
- wildcard_regex === branch_name
+ self.matching(ref_name, protected_refs: project.protected_branches).present?
end
- def wildcard_regex
- @wildcard_regex ||= begin
- name = self.name.gsub('*', 'STAR_DONT_ESCAPE')
- quoted_name = Regexp.quote(name)
- regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?')
- /\A#{regex_string}\z/
- end
+ def self.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
end
diff --git a/app/models/protected_ref_matcher.rb b/app/models/protected_ref_matcher.rb
new file mode 100644
index 00000000000..d970f2b01fc
--- /dev/null
+++ b/app/models/protected_ref_matcher.rb
@@ -0,0 +1,54 @@
+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
diff --git a/app/models/protected_tag.rb b/app/models/protected_tag.rb
new file mode 100644
index 00000000000..83964095516
--- /dev/null
+++ b/app/models/protected_tag.rb
@@ -0,0 +1,14 @@
+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
diff --git a/app/models/protected_tag/create_access_level.rb b/app/models/protected_tag/create_access_level.rb
new file mode 100644
index 00000000000..c7e1319719d
--- /dev/null
+++ b/app/models/protected_tag/create_access_level.rb
@@ -0,0 +1,21 @@
+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
diff --git a/app/services/ci/expire_pipeline_cache_service.rb b/app/services/ci/expire_pipeline_cache_service.rb
index c0e4a798f6a..91d9c1d2ba1 100644
--- a/app/services/ci/expire_pipeline_cache_service.rb
+++ b/app/services/ci/expire_pipeline_cache_service.rb
@@ -10,6 +10,8 @@ module Ci
store.touch(commit_pipelines_path) if pipeline.commit
store.touch(new_merge_request_pipelines_path)
merge_requests_pipelines_paths.each { |path| store.touch(path) }
+
+ Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(@pipeline)
end
private
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 11a045f4c31..38a113caec7 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -11,7 +11,7 @@ class DeleteBranchService < BaseService
return error('Cannot remove HEAD branch', 405)
end
- if project.protected_branch?(branch_name)
+ if ProtectedBranch.protected?(project, branch_name)
return error('Protected branch cant be removed', 405)
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index bc7431c89a8..45411c779cc 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -127,7 +127,7 @@ class GitPushService < BaseService
project.change_head(branch_name)
# 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 = {
name: @project.default_branch,
diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb
index 89d8ba60134..4b3337a5c9d 100644
--- a/app/services/protected_branches/update_service.rb
+++ b/app/services/protected_branches/update_service.rb
@@ -1,13 +1,10 @@
module ProtectedBranches
class UpdateService < BaseService
- attr_reader :protected_branch
-
def execute(protected_branch)
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :admin_project, project)
- @protected_branch = protected_branch
- @protected_branch.update(params)
- @protected_branch
+ protected_branch.update(params)
+ protected_branch
end
end
end
diff --git a/app/services/protected_tags/create_service.rb b/app/services/protected_tags/create_service.rb
new file mode 100644
index 00000000000..faba7865a17
--- /dev/null
+++ b/app/services/protected_tags/create_service.rb
@@ -0,0 +1,11 @@
+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
diff --git a/app/services/protected_tags/update_service.rb b/app/services/protected_tags/update_service.rb
new file mode 100644
index 00000000000..aea6a48968d
--- /dev/null
+++ b/app/services/protected_tags/update_service.rb
@@ -0,0 +1,10 @@
+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
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 724675e659c..0f9ef3eded3 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -15,7 +15,7 @@
%span.label.label-info.has-tooltip{ title: "Merged into #{@repository.root_ref}" }
merged
- - if @project.protected_branch? branch.name
+ - if protected_branch?(@project, branch)
%span.label.label-success
protected
.controls.hidden-xs<
diff --git a/app/views/projects/protected_branches/show.html.haml b/app/views/projects/protected_branches/show.html.haml
index 4d8169815b3..f8cfe5e4b11 100644
--- a/app/views/projects/protected_branches/show.html.haml
+++ b/app/views/projects/protected_branches/show.html.haml
@@ -1,13 +1,13 @@
-- page_title @protected_branch.name, "Protected Branches"
+- page_title @protected_ref.name, "Protected Branches"
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
- = @protected_branch.name
+ = @protected_ref.name
.col-lg-9
%h5 Matching Branches
- - if @matching_branches.present?
+ - if @matching_refs.present?
.table-responsive
%table.table.protected-branches-list
%colgroup
@@ -18,7 +18,7 @@
%th Branch
%th Last commit
%tbody
- - @matching_branches.each do |matching_branch|
+ - @matching_refs.each do |matching_branch|
= render partial: "matching_branch", object: matching_branch
- else
%p.settings-message.text-center
diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml
new file mode 100644
index 00000000000..6e187b54a59
--- /dev/null
+++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml
@@ -0,0 +1,32 @@
+= 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
diff --git a/app/views/projects/protected_tags/_dropdown.html.haml b/app/views/projects/protected_tags/_dropdown.html.haml
new file mode 100644
index 00000000000..74851519077
--- /dev/null
+++ b/app/views/projects/protected_tags/_dropdown.html.haml
@@ -0,0 +1,15 @@
+= 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
diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml
new file mode 100644
index 00000000000..0bfb1ad191d
--- /dev/null
+++ b/app/views/projects/protected_tags/_index.html.haml
@@ -0,0 +1,18 @@
+- 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"
diff --git a/app/views/projects/protected_tags/_matching_tag.html.haml b/app/views/projects/protected_tags/_matching_tag.html.haml
new file mode 100644
index 00000000000..97e5cd6f9d2
--- /dev/null
+++ b/app/views/projects/protected_tags/_matching_tag.html.haml
@@ -0,0 +1,9 @@
+%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)
diff --git a/app/views/projects/protected_tags/_protected_tag.html.haml b/app/views/projects/protected_tags/_protected_tag.html.haml
new file mode 100644
index 00000000000..26bd3a1f5ed
--- /dev/null
+++ b/app/views/projects/protected_tags/_protected_tag.html.haml
@@ -0,0 +1,21 @@
+%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'
diff --git a/app/views/projects/protected_tags/_tags_list.html.haml b/app/views/projects/protected_tags/_tags_list.html.haml
new file mode 100644
index 00000000000..728afd75b50
--- /dev/null
+++ b/app/views/projects/protected_tags/_tags_list.html.haml
@@ -0,0 +1,28 @@
+.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'
diff --git a/app/views/projects/protected_tags/_update_protected_tag.haml b/app/views/projects/protected_tags/_update_protected_tag.haml
new file mode 100644
index 00000000000..62823bee46e
--- /dev/null
+++ b/app/views/projects/protected_tags/_update_protected_tag.haml
@@ -0,0 +1,5 @@
+%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 }})
diff --git a/app/views/projects/protected_tags/show.html.haml b/app/views/projects/protected_tags/show.html.haml
new file mode 100644
index 00000000000..63743f28b3c
--- /dev/null
+++ b/app/views/projects/protected_tags/show.html.haml
@@ -0,0 +1,25 @@
+- 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.
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 4c02302e161..5402320cb66 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -3,3 +3,4 @@
= render @deploy_keys
= render "projects/protected_branches/index"
+= render "projects/protected_tags/index"
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index dffe908e85a..451e011a4b8 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -6,6 +6,11 @@
%span.item-title
= icon('tag')
= tag.name
+
+ - if protected_tag?(@project, tag)
+ %span.label.label-success
+ protected
+
- if tag.message.present?
&nbsp;
= strip_gpg_signature(tag.message)
@@ -30,5 +35,5 @@
= icon("pencil")
- 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")
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index fad3c5c2173..1c4135c8a54 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -7,6 +7,9 @@
.nav-text
.title
%span.item-title= @tag.name
+ - if protected_tag?(@project, @tag)
+ %span.label.label-success
+ protected
- if @commit
= render 'projects/branches/commit', commit: @commit, project: @project
- else
@@ -24,7 +27,7 @@
= render 'projects/buttons/download', project: @project, ref: @tag.name
- if can?(current_user, :admin_project, @project)
.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
- if @tag.message.present?
diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml
index 5f708b3a2ed..8582bcbb8cc 100644
--- a/app/views/projects/triggers/_form.html.haml
+++ b/app/views/projects/triggers/_form.html.haml
@@ -8,4 +8,26 @@
.form-group
= f.label :key, "Description", class: "label-light"
= 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"
diff --git a/app/views/projects/triggers/_index.html.haml b/app/views/projects/triggers/_index.html.haml
index cc74e50a5e3..84e945ee0df 100644
--- a/app/views/projects/triggers/_index.html.haml
+++ b/app/views/projects/triggers/_index.html.haml
@@ -22,6 +22,8 @@
%th
%strong Last used
%th
+ %strong Next run at
+ %th
= render partial: 'projects/triggers/trigger', collection: @triggers, as: :trigger
- else
%p.settings-message.text-center.append-bottom-default
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
index 9b5f63ae81a..ebd91a8e2af 100644
--- a/app/views/projects/triggers/_trigger.html.haml
+++ b/app/views/projects/triggers/_trigger.html.haml
@@ -29,6 +29,12 @@
- else
Never
+ %td
+ - if trigger.trigger_schedule&.active?
+ = trigger.trigger_schedule.real_next_run
+ - else
+ Never
+
%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?"
- revoke_trigger_confirmation = "By revoking a trigger you will break any processes making use of it. Are you sure?"
diff --git a/app/workers/trigger_schedule_worker.rb b/app/workers/trigger_schedule_worker.rb
index 440c579b99d..9c1baf7e6c5 100644
--- a/app/workers/trigger_schedule_worker.rb
+++ b/app/workers/trigger_schedule_worker.rb
@@ -3,7 +3,7 @@ class TriggerScheduleWorker
include CronjobQueue
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
Ci::CreateTriggerRequestService.new.execute(trigger_schedule.project,
trigger_schedule.trigger,
diff --git a/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml b/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml
new file mode 100644
index 00000000000..fabe24e485a
--- /dev/null
+++ b/changelogs/unreleased/18471-restrict-tag-pushes-protected-tags.yml
@@ -0,0 +1,4 @@
+---
+title: Tags can be protected, restricting creation of matching tags by user role
+merge_request: 10356
+author:
diff --git a/changelogs/unreleased/add-ui-for-trigger-schedule.yml b/changelogs/unreleased/add-ui-for-trigger-schedule.yml
new file mode 100644
index 00000000000..9ca78983605
--- /dev/null
+++ b/changelogs/unreleased/add-ui-for-trigger-schedule.yml
@@ -0,0 +1,4 @@
+---
+title: Add UI for Trigger Schedule
+merge_request: 10533
+author: dosuken123
diff --git a/changelogs/unreleased/optimise-builds-view.yml b/changelogs/unreleased/optimise-builds-view.yml
new file mode 100644
index 00000000000..1d715ab4f47
--- /dev/null
+++ b/changelogs/unreleased/optimise-builds-view.yml
@@ -0,0 +1,4 @@
+---
+title: Optimise builds endpoint
+merge_request:
+author:
diff --git a/config/routes/project.rb b/config/routes/project.rb
index e38f0537143..f5009186344 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -135,6 +135,8 @@ constraints(ProjectUrlConstrainer.new) do
end
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 :triggers, only: [:index, :create, :edit, :update, :destroy] do
member do
diff --git a/config/webpack.config.js b/config/webpack.config.js
index dc431e4d566..987344f4eaa 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -41,6 +41,7 @@ var config = {
pdf_viewer: './blob/pdf_viewer.js',
profile: './profile/profile_bundle.js',
protected_branches: './protected_branches/protected_branches_bundle.js',
+ protected_tags: './protected_tags',
snippet: './snippet/snippet_bundle.js',
stl_viewer: './blob/stl_viewer.js',
terminal: './terminal/terminal_bundle.js',
diff --git a/db/migrate/20170309173138_create_protected_tags.rb b/db/migrate/20170309173138_create_protected_tags.rb
new file mode 100644
index 00000000000..796f3c90344
--- /dev/null
+++ b/db/migrate/20170309173138_create_protected_tags.rb
@@ -0,0 +1,27 @@
+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
diff --git a/db/migrate/20170407114956_add_ref_to_ci_trigger_schedule.rb b/db/migrate/20170407114956_add_ref_to_ci_trigger_schedule.rb
new file mode 100644
index 00000000000..523a306f127
--- /dev/null
+++ b/db/migrate/20170407114956_add_ref_to_ci_trigger_schedule.rb
@@ -0,0 +1,9 @@
+class AddRefToCiTriggerSchedule < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_trigger_schedules, :ref, :string
+ end
+end
diff --git a/db/migrate/20170407122426_add_active_to_ci_trigger_schedule.rb b/db/migrate/20170407122426_add_active_to_ci_trigger_schedule.rb
new file mode 100644
index 00000000000..36892118ac0
--- /dev/null
+++ b/db/migrate/20170407122426_add_active_to_ci_trigger_schedule.rb
@@ -0,0 +1,9 @@
+class AddActiveToCiTriggerSchedule < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_trigger_schedules, :active, :boolean
+ end
+end
diff --git a/db/migrate/20170407140450_add_index_to_next_run_at_and_active.rb b/db/migrate/20170407140450_add_index_to_next_run_at_and_active.rb
new file mode 100644
index 00000000000..626c2a67fdc
--- /dev/null
+++ b/db/migrate/20170407140450_add_index_to_next_run_at_and_active.rb
@@ -0,0 +1,18 @@
+# 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
diff --git a/db/schema.rb b/db/schema.rb
index a8ca4d4757d..d400c823387 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20170406115029) do
+ActiveRecord::Schema.define(version: 20170407140450) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
enable_extension "pg_trgm"
@@ -310,8 +310,11 @@ ActiveRecord::Schema.define(version: 20170406115029) do
t.string "cron"
t.string "cron_timezone"
t.datetime "next_run_at"
+ t.string "ref"
+ t.boolean "active"
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", ["project_id"], name: "index_ci_trigger_schedules_on_project_id", using: :btree
@@ -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
+ 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|
t.string "tag"
t.text "description"
@@ -1352,6 +1376,9 @@ ActiveRecord::Schema.define(version: 20170406115029) do
add_foreign_key "project_statistics", "projects", on_delete: :cascade
add_foreign_key "protected_branch_merge_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 "system_note_metadata", "notes", name: "fk_d83a918cb1", on_delete: :cascade
add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 53c29a4fd98..045d3821f66 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -311,7 +311,7 @@ Running on runner-8a2f473d-project-1796893-concurrent-0 via runner-8a2f473d-mach
++ export GITLAB_USER_ID=42
++ GITLAB_USER_ID=42
++ export GITLAB_USER_EMAIL=user@example.com
-++ GITLAB_USER_EMAIL=axilleas@axilleas.me
+++ GITLAB_USER_EMAIL=user@example.com
++ export VERY_SECURE_VARIABLE=imaverysecurevariable
++ VERY_SECURE_VARIABLE=imaverysecurevariable
++ mkdir -p /builds/gitlab-examples/ci-debug-trace.tmp
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
index ec565c3e7bf..0b17e4ff7c1 100644
--- a/doc/university/glossary/README.md
+++ b/doc/university/glossary/README.md
@@ -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.
+### Protected Tags
+
+A [feature](https://docs.gitlab.com/ce/user/project/protected_tags.html) that protects tags from unauthorized creation, update or deletion
+
### 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.
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 0ea6d01411f..3122e95fc0e 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -55,6 +55,7 @@ The following table depicts the various user permission levels in a project.
| Push to protected branches | | | | ✓ | ✓ |
| Enable/disable branch protection | | | | ✓ | ✓ |
| Turn on/off protected branch push for devs| | | | ✓ | ✓ |
+| Enable/disable tag protections | | | | ✓ | ✓ |
| Rewrite/remove Git tags | | | | ✓ | ✓ |
| Edit project | | | | ✓ | ✓ |
| Add deploy keys to project | | | | ✓ | ✓ |
diff --git a/doc/user/project/img/project_repository_settings.png b/doc/user/project/img/project_repository_settings.png
new file mode 100644
index 00000000000..1aa7efc36f1
--- /dev/null
+++ b/doc/user/project/img/project_repository_settings.png
Binary files differ
diff --git a/doc/user/project/img/protected_tag_matches.png b/doc/user/project/img/protected_tag_matches.png
new file mode 100644
index 00000000000..a36a11a1271
--- /dev/null
+++ b/doc/user/project/img/protected_tag_matches.png
Binary files differ
diff --git a/doc/user/project/img/protected_tags_list.png b/doc/user/project/img/protected_tags_list.png
new file mode 100644
index 00000000000..c5e42dc0705
--- /dev/null
+++ b/doc/user/project/img/protected_tags_list.png
Binary files differ
diff --git a/doc/user/project/img/protected_tags_page.png b/doc/user/project/img/protected_tags_page.png
new file mode 100644
index 00000000000..3848d91ebd6
--- /dev/null
+++ b/doc/user/project/img/protected_tags_page.png
Binary files differ
diff --git a/doc/user/project/img/protected_tags_permissions_dropdown.png b/doc/user/project/img/protected_tags_permissions_dropdown.png
new file mode 100644
index 00000000000..9e0fc4e2a43
--- /dev/null
+++ b/doc/user/project/img/protected_tags_permissions_dropdown.png
Binary files differ
diff --git a/doc/user/project/protected_tags.md b/doc/user/project/protected_tags.md
new file mode 100644
index 00000000000..0cb7aefdb2f
--- /dev/null
+++ b/doc/user/project/protected_tags.md
@@ -0,0 +1,60 @@
+# 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"
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 6a8de51a199..a1852650cfb 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -20,6 +20,7 @@
- [Project forking workflow](forking_workflow.md)
- [Project users](add-user/add-user.md)
- [Protected branches](../user/project/protected_branches.md)
+- [Protected tags](../user/project/protected_tags.md)
- [Slash commands](../user/project/slash_commands.md)
- [Sharing a project with a group](share_with_group.md)
- [Share projects with other groups](share_projects_with_other_groups.md)
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 4ee879fe922..73206c3f30d 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -251,7 +251,8 @@ module SharedProject
step 'project "Shop" has CI build' do
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
step 'I should see last commit with CI status' do
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 00d44821e3f..45625e00f7d 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -184,19 +184,15 @@ module API
end
expose :protected do |repo_branch, options|
- options[:project].protected_branch?(repo_branch.name)
+ ProtectedBranch.protected?(options[:project], repo_branch.name)
end
expose :developers_can_push do |repo_branch, options|
- project = options[:project]
- 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 }
+ options[:project].protected_branches.developers_can?(:push, repo_branch.name)
end
expose :developers_can_merge do |repo_branch, options|
- project = options[:project]
- 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 }
+ options[:project].protected_branches.developers_can?(:merge, repo_branch.name)
end
end
diff --git a/lib/gitlab/cache/ci/project_pipeline_status.rb b/lib/gitlab/cache/ci/project_pipeline_status.rb
new file mode 100644
index 00000000000..b358f2efa4f
--- /dev/null
+++ b/lib/gitlab/cache/ci/project_pipeline_status.rb
@@ -0,0 +1,103 @@
+# 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
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index c85f79127bc..eb2f2e144fd 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -10,6 +10,7 @@ module Gitlab
)
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
@branch_name = Gitlab::Git.branch_name(@ref)
+ @tag_name = Gitlab::Git.tag_name(@ref)
@user_access = user_access
@project = project
@env = env
@@ -32,11 +33,11 @@ module Gitlab
def protected_branch_checks
return if skip_authorization
return unless @branch_name
- return unless project.protected_branch?(@branch_name)
+ return unless ProtectedBranch.protected?(project, @branch_name)
if forced_push?
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."
end
@@ -58,13 +59,29 @@ module Gitlab
def tag_checks
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)
- "You are not allowed to change existing tags on this project."
+ if tag_exists? && user_access.cannot_do_action?(:admin_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
+ def tag_protected?
+ ProtectedTag.protected?(project, @tag_name)
+ end
+
def push_checks
return if skip_authorization
@@ -75,14 +92,22 @@ module Gitlab
private
- def protected_tag?(tag_name)
- project.repository.tag_exists?(tag_name)
+ def tag_exists?
+ project.repository.tag_exists?(@tag_name)
end
def forced_push?
Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev, env: @env)
end
+ def update?
+ !Gitlab::Git.blank_ref?(@oldrev) && !deletion?
+ end
+
+ def deletion?
+ Gitlab::Git.blank_ref?(@newrev)
+ end
+
def matching_merge_request?
Checks::MatchingMergeRequest.new(@newrev, @branch_name, @project).match?
end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index f5e1e385ff9..899a6567768 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -47,6 +47,8 @@ project_tree:
- protected_branches:
- :merge_access_levels
- :push_access_levels
+ - protected_tags:
+ - :create_access_levels
- :project_feature
# Only include the following attributes for the models specified.
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index df21ff22216..2e349b5f9a9 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -52,7 +52,11 @@ module Gitlab
create_sub_relations(relation, @tree_hash) if relation.is_a?(Hash)
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)
end
saved.all?
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 2ba12f5f924..71811be6f50 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -10,6 +10,7 @@ module Gitlab
hooks: 'ProjectHook',
merge_access_levels: 'ProtectedBranch::MergeAccessLevel',
push_access_levels: 'ProtectedBranch::PushAccessLevel',
+ create_access_levels: 'ProtectedTag::CreateAccessLevel',
labels: :project_labels,
priorities: :label_priorities,
label: :project_label }.freeze
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index f260c0c535f..54728e5ff0e 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -28,14 +28,23 @@ module Gitlab
true
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)
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)
- access_levels = project.protected_branches.matching(ref).map(&:push_access_levels).flatten
- has_access = access_levels.any? { |access_level| access_level.check_access(user) }
+ has_access = project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push)
has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref)
else
@@ -46,9 +55,8 @@ module Gitlab
def can_merge_to_branch?(ref)
return false unless can_access_git?
- if project.protected_branch?(ref)
- access_levels = project.protected_branches.matching(ref).map(&:merge_access_levels).flatten
- access_levels.any? { |access_level| access_level.check_access(user) }
+ if ProtectedBranch.protected?(project, ref)
+ project.protected_branches.protected_ref_accessible_to?(ref, user, action: :merge)
else
user.can?(:push_code, project)
end
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index 350afeb5c0b..15131fbf755 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -48,9 +48,16 @@ class NewImporter < ::Gitlab::GithubImport::Importer
begin
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
- 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}"
end
diff --git a/spec/controllers/projects/builds_controller_spec.rb b/spec/controllers/projects/builds_controller_spec.rb
index 683667129e5..13208d21918 100644
--- a/spec/controllers/projects/builds_controller_spec.rb
+++ b/spec/controllers/projects/builds_controller_spec.rb
@@ -10,6 +10,39 @@ describe Projects::BuildsController do
sign_in(user)
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
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:build) { create(:ci_build, pipeline: pipeline) }
diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb
index e378b5714fe..80be135b5d8 100644
--- a/spec/controllers/projects/protected_branches_controller_spec.rb
+++ b/spec/controllers/projects/protected_branches_controller_spec.rb
@@ -3,6 +3,7 @@ require('spec_helper')
describe Projects::ProtectedBranchesController 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
diff --git a/spec/controllers/projects/protected_tags_controller_spec.rb b/spec/controllers/projects/protected_tags_controller_spec.rb
new file mode 100644
index 00000000000..64658988b3f
--- /dev/null
+++ b/spec/controllers/projects/protected_tags_controller_spec.rb
@@ -0,0 +1,11 @@
+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
diff --git a/spec/factories/ci/trigger_schedules.rb b/spec/factories/ci/trigger_schedules.rb
index 315bce16995..2390706fa41 100644
--- a/spec/factories/ci/trigger_schedules.rb
+++ b/spec/factories/ci/trigger_schedules.rb
@@ -3,9 +3,11 @@ FactoryGirl.define do
trigger factory: :ci_trigger_for_trigger_schedule
cron '0 1 * * *'
cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
+ ref 'master'
+ active true
after(:build) do |trigger_schedule, evaluator|
- trigger_schedule.update!(project: trigger_schedule.trigger.project)
+ trigger_schedule.project ||= trigger_schedule.trigger.project
end
trait :nightly do
diff --git a/spec/factories/protected_tags.rb b/spec/factories/protected_tags.rb
new file mode 100644
index 00000000000..d8e90ae1ee1
--- /dev/null
+++ b/spec/factories/protected_tags.rb
@@ -0,0 +1,22 @@
+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
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index c4e58d14f75..f1789fc9d43 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -7,7 +7,6 @@ RSpec.describe 'Dashboard Projects', feature: true do
before do
project.team << [user, :developer]
login_as user
- visit dashboard_projects_path
end
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
expect(page).to have_content('awesome stuff')
end
- describe "with a pipeline" do
- let(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }
+ describe "with a pipeline", redis: true do
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) }
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
it 'shows that the last pipeline passed' do
visit dashboard_projects_path
+
expect(page).to have_xpath("//a[@href='#{pipelines_namespace_project_commit_path(project.namespace, project, project.commit)}']")
end
end
diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb
index e4aca25a339..eb3cea775da 100644
--- a/spec/features/protected_branches/access_control_ce_spec.rb
+++ b/spec/features/protected_branches/access_control_ce_spec.rb
@@ -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)|
it "allows creating protected branches that #{access_type_name} can push to" do
visit namespace_project_protected_branches_path(project.namespace, project)
+
set_protected_branch_name('master')
+
within('.new_protected_branch') do
allowed_to_push_button = find(".js-allowed-to-push")
@@ -11,6 +13,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end
end
+
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
@@ -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
visit namespace_project_protected_branches_path(project.namespace, project)
+
set_protected_branch_name('master')
+
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
@@ -34,6 +39,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
end
wait_for_ajax
+
expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
end
end
@@ -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)|
it "allows creating protected branches that #{access_type_name} can merge to" do
visit namespace_project_protected_branches_path(project.namespace, project)
+
set_protected_branch_name('master')
+
within('.new_protected_branch') do
allowed_to_merge_button = find(".js-allowed-to-merge")
@@ -50,6 +58,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
within(".dropdown.open .dropdown-menu") { click_on access_type_name }
end
end
+
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
@@ -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
visit namespace_project_protected_branches_path(project.namespace, project)
+
set_protected_branch_name('master')
+
click_on "Protect"
expect(ProtectedBranch.count).to eq(1)
@@ -73,6 +84,7 @@ RSpec.shared_examples "protected branches > access control > CE" do
end
wait_for_ajax
+
expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
end
end
diff --git a/spec/features/protected_tags/access_control_ce_spec.rb b/spec/features/protected_tags/access_control_ce_spec.rb
new file mode 100644
index 00000000000..5b2baf8616c
--- /dev/null
+++ b/spec/features/protected_tags/access_control_ce_spec.rb
@@ -0,0 +1,46 @@
+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
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
new file mode 100644
index 00000000000..09e8c850de3
--- /dev/null
+++ b/spec/features/protected_tags_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+Dir["./spec/features/protected_tags/*.rb"].sort.each { |f| require f }
+
+feature 'Projected Tags', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user, :admin) }
+ let(:project) { create(:project) }
+
+ before { login_as(user) }
+
+ def set_protected_tag_name(tag_name)
+ find(".js-protected-tag-select").click
+ find(".dropdown-input-field").set(tag_name)
+ click_on("Create wildcard #{tag_name}")
+ end
+
+ describe "explicit protected tags" do
+ it "allows creating explicit protected tags" do
+ visit namespace_project_protected_tags_path(project.namespace, project)
+ set_protected_tag_name('some-tag')
+ click_on "Protect"
+
+ within(".protected-tags-list") { expect(page).to have_content('some-tag') }
+ expect(ProtectedTag.count).to eq(1)
+ expect(ProtectedTag.last.name).to eq('some-tag')
+ end
+
+ it "displays the last commit on the matching tag if it exists" do
+ commit = create(:commit, project: project)
+ project.repository.add_tag(user, 'some-tag', commit.id)
+
+ visit namespace_project_protected_tags_path(project.namespace, project)
+ set_protected_tag_name('some-tag')
+ click_on "Protect"
+
+ within(".protected-tags-list") { expect(page).to have_content(commit.id[0..7]) }
+ end
+
+ it "displays an error message if the named tag does not exist" do
+ visit namespace_project_protected_tags_path(project.namespace, project)
+ set_protected_tag_name('some-tag')
+ click_on "Protect"
+
+ within(".protected-tags-list") { expect(page).to have_content('tag was removed') }
+ end
+ end
+
+ describe "wildcard protected tags" do
+ it "allows creating protected tags with a wildcard" do
+ visit namespace_project_protected_tags_path(project.namespace, project)
+ set_protected_tag_name('*-stable')
+ click_on "Protect"
+
+ within(".protected-tags-list") { expect(page).to have_content('*-stable') }
+ expect(ProtectedTag.count).to eq(1)
+ expect(ProtectedTag.last.name).to eq('*-stable')
+ end
+
+ it "displays the number of matching tags" do
+ project.repository.add_tag(user, 'production-stable', 'master')
+ project.repository.add_tag(user, 'staging-stable', 'master')
+
+ visit namespace_project_protected_tags_path(project.namespace, project)
+ set_protected_tag_name('*-stable')
+ click_on "Protect"
+
+ within(".protected-tags-list") { expect(page).to have_content("2 matching tags") }
+ end
+
+ it "displays all the tags matching the wildcard" do
+ project.repository.add_tag(user, 'production-stable', 'master')
+ project.repository.add_tag(user, 'staging-stable', 'master')
+ project.repository.add_tag(user, 'development', 'master')
+
+ visit namespace_project_protected_tags_path(project.namespace, project)
+ set_protected_tag_name('*-stable')
+ click_on "Protect"
+
+ visit namespace_project_protected_tags_path(project.namespace, project)
+ click_on "2 matching tags"
+
+ within(".protected-tags-list") do
+ expect(page).to have_content("production-stable")
+ expect(page).to have_content("staging-stable")
+ expect(page).not_to have_content("development")
+ end
+ end
+ end
+
+ describe "access control" do
+ include_examples "protected tags > access control > CE"
+ end
+end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index c1ae6db00c6..81fa2de1cc3 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -77,6 +77,59 @@ feature 'Triggers', feature: true, js: true do
expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
expect(page.find('.triggers-list')).to have_content new_trigger_title
end
+
+ context 'scheduled triggers' do
+ let!(:trigger) do
+ create(:ci_trigger, owner: user, project: @project, description: trigger_title)
+ end
+
+ context 'enabling schedule' do
+ before do
+ visit edit_namespace_project_trigger_path(@project.namespace, @project, trigger)
+ end
+
+ scenario 'do fill form with valid data and save' do
+ find('#trigger_trigger_schedule_attributes_active').click
+ fill_in 'trigger_trigger_schedule_attributes_cron', with: '1 * * * *'
+ fill_in 'trigger_trigger_schedule_attributes_cron_timezone', with: 'UTC'
+ fill_in 'trigger_trigger_schedule_attributes_ref', with: 'master'
+ click_button 'Save trigger'
+
+ expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
+ end
+
+ scenario 'do not fill form with valid data and save' do
+ find('#trigger_trigger_schedule_attributes_active').click
+ click_button 'Save trigger'
+
+ expect(page).to have_content 'The form contains the following errors'
+ end
+ end
+
+ context 'disabling schedule' do
+ before do
+ trigger.create_trigger_schedule(
+ project: trigger.project,
+ active: true,
+ ref: 'master',
+ cron: '1 * * * *',
+ cron_timezone: 'UTC')
+
+ visit edit_namespace_project_trigger_path(@project.namespace, @project, trigger)
+ end
+
+ scenario 'disable and save form' do
+ find('#trigger_trigger_schedule_attributes_active').click
+ click_button 'Save trigger'
+ expect(page.find('.flash-notice')).to have_content 'Trigger was successfully updated.'
+
+ visit edit_namespace_project_trigger_path(@project.namespace, @project, trigger)
+ checkbox = find_field('trigger_trigger_schedule_attributes_active')
+
+ expect(checkbox).not_to be_checked
+ end
+ end
+ end
end
describe 'trigger "Take ownership" workflow' do
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index 174cc84a97b..c795fe5a2a3 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -19,7 +19,11 @@ describe CiStatusHelper do
describe "#pipeline_status_cache_key" do
it "builds a cache key for pipeline status" do
- pipeline_status = Ci::PipelineStatus.new(build(:project), sha: "123abc", status: "success")
+ pipeline_status = Gitlab::Cache::Ci::ProjectPipelineStatus.new(
+ build(:project),
+ sha: "123abc",
+ status: "success"
+ )
expect(helper.pipeline_status_cache_key(pipeline_status)).to eq("pipeline-status/123abc-success")
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 44312ada438..40efab6e4f7 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -63,7 +63,7 @@ describe ProjectsHelper do
end
end
- describe "#project_list_cache_key" do
+ describe "#project_list_cache_key", redis: true do
let(:project) { create(:project) }
it "includes the namespace" do
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 5354e536edc..e437333d522 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,6 +1,7 @@
/* eslint-disable no-var, comma-dangle, object-shorthand */
require('~/merge_request_tabs');
+require('~/commit/pipelines/pipelines_bundle.js');
require('~/breakpoints');
require('~/lib/utils/common_utils');
require('~/diff');
@@ -222,7 +223,9 @@ require('vendor/jquery.scrollTo');
describe('#tabShown', () => {
beforeEach(function () {
- spyOn($, 'ajax').and.callFake(function () {});
+ spyOn($, 'ajax').and.callFake(function (options) {
+ options.success({ html: '' });
+ });
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
});
@@ -230,9 +233,6 @@ require('vendor/jquery.scrollTo');
beforeEach(function () {
this.class.diffViewType = () => 'parallel';
gl.Diff.prototype.diffViewType = () => 'parallel';
- spyOn($, 'ajax').and.callFake(function (options) {
- options.success({ html: '' });
- });
});
it('maintains `container-limited` for pipelines tab', function (done) {
diff --git a/spec/models/ci/pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
index bc5b71666c2..fced253dd01 100644
--- a/spec/models/ci/pipeline_status_spec.rb
+++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::PipelineStatus do
+describe Gitlab::Cache::Ci::ProjectPipelineStatus do
let(:project) { create(:project) }
let(:pipeline_status) { described_class.new(project) }
@@ -12,6 +12,20 @@ describe Ci::PipelineStatus do
end
end
+ describe '.update_for_pipeline' do
+ it 'refreshes the cache if nescessary' do
+ pipeline = build_stubbed(:ci_pipeline, sha: '123456', status: 'success')
+ fake_status = double
+ expect(described_class).to receive(:new).
+ with(pipeline.project, sha: '123456', status: 'success', ref: 'master').
+ and_return(fake_status)
+
+ expect(fake_status).to receive(:store_in_cache_if_needed)
+
+ described_class.update_for_pipeline(pipeline)
+ end
+ end
+
describe '#has_status?' do
it "is false when the status wasn't loaded yet" do
expect(pipeline_status.has_status?).to be_falsy
@@ -41,14 +55,14 @@ describe Ci::PipelineStatus do
it 'loads the status from the project commit when there is no cache' do
allow(pipeline_status).to receive(:has_cache?).and_return(false)
- expect(pipeline_status).to receive(:load_from_commit)
+ expect(pipeline_status).to receive(:load_from_project)
pipeline_status.load_status
end
it 'stores the status in the cache when it loading it from the project' do
allow(pipeline_status).to receive(:has_cache?).and_return(false)
- allow(pipeline_status).to receive(:load_from_commit)
+ allow(pipeline_status).to receive(:load_from_project)
expect(pipeline_status).to receive(:store_in_cache)
@@ -70,14 +84,15 @@ describe Ci::PipelineStatus do
end
end
- describe "#load_from_commit" do
+ describe "#load_from_project" do
let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.sha) }
it 'reads the status from the pipeline for the commit' do
- pipeline_status.load_from_commit
+ pipeline_status.load_from_project
expect(pipeline_status.status).to eq('success')
expect(pipeline_status.sha).to eq(project.commit.sha)
+ expect(pipeline_status.ref).to eq(project.default_branch)
end
it "doesn't fail for an empty project" do
@@ -108,10 +123,11 @@ describe Ci::PipelineStatus do
build_status = described_class.load_for_project(project)
build_status.store_in_cache_if_needed
- sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status) }
+ sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget("projects/#{project.id}/build_status", :sha, :status, :ref) }
expect(sha).not_to be_nil
expect(status).not_to be_nil
+ expect(ref).not_to be_nil
end
it "doesn't store the status in redis when the sha is not the head of the project" do
@@ -126,14 +142,15 @@ describe Ci::PipelineStatus do
it "deletes the cache if the repository doesn't have a head commit" do
empty_project = create(:empty_project)
- Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{empty_project.id}/build_status", { sha: "sha", status: "pending" }) }
+ Gitlab::Redis.with { |redis| redis.mapped_hmset("projects/#{empty_project.id}/build_status", { sha: "sha", status: "pending", ref: 'master' }) }
other_status = described_class.new(empty_project, sha: "123456", status: "failed")
other_status.store_in_cache_if_needed
- sha, status = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/build_status", :sha, :status) }
+ sha, status, ref = Gitlab::Redis.with { |redis| redis.hmget("projects/#{empty_project.id}/build_status", :sha, :status, :ref) }
expect(sha).to be_nil
expect(status).to be_nil
+ expect(ref).to be_nil
end
end
diff --git a/spec/lib/gitlab/checks/change_access_spec.rb b/spec/lib/gitlab/checks/change_access_spec.rb
index e22f88b7a32..959ae02c222 100644
--- a/spec/lib/gitlab/checks/change_access_spec.rb
+++ b/spec/lib/gitlab/checks/change_access_spec.rb
@@ -5,13 +5,10 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:user_access) { Gitlab::UserAccess.new(user, project: project) }
- let(:changes) do
- {
- oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
- newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51',
- ref: 'refs/heads/master'
- }
- end
+ let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+ let(:ref) { 'refs/heads/master' }
+ let(:changes) { { oldrev: oldrev, newrev: newrev, ref: ref } }
let(:protocol) { 'ssh' }
subject do
@@ -23,7 +20,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
).exec
end
- before { allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true) }
+ before { project.add_developer(user) }
context 'without failed checks' do
it "doesn't return any error" do
@@ -41,25 +38,67 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
end
context 'tags check' do
- let(:changes) do
- {
- oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
- newrev: '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51',
- ref: 'refs/tags/v1.0.0'
- }
- end
+ let(:ref) { 'refs/tags/v1.0.0' }
it 'returns an error if the user is not allowed to update tags' do
+ allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true)
expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to change existing tags on this project.')
end
+
+ context 'with protected tag' do
+ let!(:protected_tag) { create(:protected_tag, project: project, name: 'v*') }
+
+ context 'as master' do
+ before { project.add_master(user) }
+
+ context 'deletion' do
+ let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
+ let(:newrev) { '0000000000000000000000000000000000000000' }
+
+ it 'is prevented' do
+ expect(subject.status).to be(false)
+ expect(subject.message).to include('cannot be deleted')
+ end
+ end
+
+ context 'update' do
+ let(:oldrev) { 'be93687618e4b132087f430a4d8fc3a609c9b77c' }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+
+ it 'is prevented' do
+ expect(subject.status).to be(false)
+ expect(subject.message).to include('cannot be updated')
+ end
+ end
+ end
+
+ context 'creation' do
+ let(:oldrev) { '0000000000000000000000000000000000000000' }
+ let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+ let(:ref) { 'refs/tags/v9.1.0' }
+
+ it 'prevents creation below access level' do
+ expect(subject.status).to be(false)
+ expect(subject.message).to include('allowed to create this tag as it is protected')
+ end
+
+ context 'when user has access' do
+ let!(:protected_tag) { create(:protected_tag, :developers_can_create, project: project, name: 'v*') }
+
+ it 'allows tag creation' do
+ expect(subject.status).to be(true)
+ end
+ end
+ end
+ end
end
context 'protected branches check' do
before do
- allow(project).to receive(:protected_branch?).with('master').and_return(true)
+ allow(ProtectedBranch).to receive(:protected?).with(project, 'master').and_return(true)
end
it 'returns an error if the user is not allowed to do forced pushes to protected branches' do
@@ -86,13 +125,7 @@ describe Gitlab::Checks::ChangeAccess, lib: true do
end
context 'branch deletion' do
- let(:changes) do
- {
- oldrev: 'be93687618e4b132087f430a4d8fc3a609c9b77c',
- newrev: '0000000000000000000000000000000000000000',
- ref: 'refs/heads/master'
- }
- end
+ let(:newrev) { '0000000000000000000000000000000000000000' }
it 'returns an error if the user is not allowed to delete protected branches' do
expect(subject.status).to be(false)
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 9770d2e3b13..0abf89d060c 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -124,10 +124,15 @@ protected_branches:
- project
- merge_access_levels
- push_access_levels
+protected_tags:
+- project
+- create_access_levels
merge_access_levels:
- protected_branch
push_access_levels:
- protected_branch
+create_access_levels:
+- protected_tag
container_repositories:
- project
- name
@@ -188,6 +193,7 @@ project:
- snippets
- hooks
- protected_branches
+- protected_tags
- project_members
- users
- requesters
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index d9b67426818..7a0b0b06d4b 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -7455,6 +7455,24 @@
]
}
],
+ "protected_tags": [
+ {
+ "id": 1,
+ "project_id": 9,
+ "name": "v*",
+ "created_at": "2017-04-04T13:48:13.426Z",
+ "updated_at": "2017-04-04T13:48:13.426Z",
+ "create_access_levels": [
+ {
+ "id": 1,
+ "protected_tag_id": 1,
+ "access_level": 40,
+ "created_at": "2017-04-04T13:48:13.458Z",
+ "updated_at": "2017-04-04T13:48:13.458Z"
+ }
+ ]
+ }
+ ],
"project_feature": {
"builds_access_level": 0,
"created_at": "2014-12-26T09:26:45.000Z",
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index af9c25acb02..0e9607c5bd3 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -64,6 +64,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(ProtectedBranch.first.push_access_levels).not_to be_empty
end
+ it 'contains the create access levels on a protected tag' do
+ expect(ProtectedTag.first.create_access_levels).not_to be_empty
+ end
+
context 'event at forth level of the tree' do
let(:event) { Event.where(title: 'test levels').first }
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 04b5c80f22d..0372e3f7dbf 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -253,6 +253,8 @@ Ci::TriggerSchedule:
- cron
- cron_timezone
- next_run_at
+- ref
+- active
DeployKey:
- id
- user_id
@@ -313,6 +315,12 @@ ProtectedBranch:
- name
- created_at
- updated_at
+ProtectedTag:
+- id
+- project_id
+- name
+- created_at
+- updated_at
Project:
- description
- issues_enabled
@@ -346,6 +354,14 @@ ProtectedBranch::PushAccessLevel:
- access_level
- created_at
- updated_at
+ProtectedTag::CreateAccessLevel:
+- id
+- protected_tag_id
+- access_level
+- created_at
+- updated_at
+- user_id
+- group_id
AwardEmoji:
- id
- user_id
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index 369e55f61f1..611cdbbc865 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -142,4 +142,73 @@ describe Gitlab::UserAccess, lib: true do
end
end
end
+
+ describe 'can_create_tag?' do
+ describe 'push to none protected tag' do
+ it 'returns true if user is a master' do
+ project.add_user(user, :master)
+
+ expect(access.can_create_tag?('random_tag')).to be_truthy
+ end
+
+ it 'returns true if user is a developer' do
+ project.add_user(user, :developer)
+
+ expect(access.can_create_tag?('random_tag')).to be_truthy
+ end
+
+ it 'returns false if user is a reporter' do
+ project.add_user(user, :reporter)
+
+ expect(access.can_create_tag?('random_tag')).to be_falsey
+ end
+ end
+
+ describe 'push to protected tag' do
+ let(:tag) { create(:protected_tag, project: project, name: "test") }
+ let(:not_existing_tag) { create :protected_tag, project: project }
+
+ it 'returns true if user is a master' do
+ project.add_user(user, :master)
+
+ expect(access.can_create_tag?(tag.name)).to be_truthy
+ end
+
+ it 'returns false if user is a developer' do
+ project.add_user(user, :developer)
+
+ expect(access.can_create_tag?(tag.name)).to be_falsey
+ end
+
+ it 'returns false if user is a reporter' do
+ project.add_user(user, :reporter)
+
+ expect(access.can_create_tag?(tag.name)).to be_falsey
+ end
+ end
+
+ describe 'push to protected tag if allowed for developers' do
+ before do
+ @tag = create(:protected_tag, :developers_can_create, project: project)
+ end
+
+ it 'returns true if user is a master' do
+ project.add_user(user, :master)
+
+ expect(access.can_create_tag?(@tag.name)).to be_truthy
+ end
+
+ it 'returns true if user is a developer' do
+ project.add_user(user, :developer)
+
+ expect(access.can_create_tag?(@tag.name)).to be_truthy
+ end
+
+ it 'returns false if user is a reporter' do
+ project.add_user(user, :reporter)
+
+ expect(access.can_create_tag?(@tag.name)).to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 32aa2f4b336..d7d6a75d38d 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -375,7 +375,7 @@ describe Ci::Pipeline, models: true do
end
end
- describe 'pipeline ETag caching' do
+ describe 'pipeline caching' do
it 'executes ExpirePipelinesCacheService' do
expect_any_instance_of(Ci::ExpirePipelineCacheService).to receive(:execute).with(pipeline)
@@ -1079,19 +1079,6 @@ describe Ci::Pipeline, models: true do
end
end
- describe '#update_status' do
- let(:pipeline) { create(:ci_pipeline, sha: '123456') }
-
- it 'updates the cached status' do
- fake_status = double
- # after updating the status, the status is set to `skipped` for this pipeline's builds
- expect(Ci::PipelineStatus).to receive(:new).with(pipeline.project, sha: '123456', status: 'skipped').and_return(fake_status)
- expect(fake_status).to receive(:store_in_cache_if_needed)
-
- pipeline.update_status
- end
- end
-
describe 'notifications when pipeline success or failed' do
let(:project) { create(:project, :repository) }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index b615d956686..90b3a2ba42d 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -441,7 +441,7 @@ describe MergeRequest, models: true do
end
it "can't be removed when its a protected branch" do
- allow(subject.source_project).to receive(:protected_branch?).and_return(true)
+ allow(ProtectedBranch).to receive(:protected?).and_return(true)
expect(subject.can_remove_source_branch?(user)).to be_falsey
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 6d4ef78f8ec..92d420337f9 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -704,25 +704,6 @@ describe Project, models: true do
end
end
- describe '#open_branches' do
- let(:project) { create(:project, :repository) }
-
- before do
- project.protected_branches.create(name: 'master')
- end
-
- it { expect(project.open_branches.map(&:name)).to include('feature') }
- it { expect(project.open_branches.map(&:name)).not_to include('master') }
-
- it "includes branches matching a protected branch wildcard" do
- expect(project.open_branches.map(&:name)).to include('feature')
-
- create(:protected_branch, name: 'feat*', project: project)
-
- expect(Project.find(project.id).open_branches.map(&:name)).to include('feature')
- end
- end
-
describe '#star_count' do
it 'counts stars from multiple users' do
user1 = create :user
@@ -1297,62 +1278,6 @@ describe Project, models: true do
end
end
- describe '#protected_branch?' do
- context 'existing project' do
- let(:project) { create(:project, :repository) }
-
- it 'returns true when the branch matches a protected branch via direct match' do
- create(:protected_branch, project: project, name: "foo")
-
- expect(project.protected_branch?('foo')).to eq(true)
- end
-
- it 'returns true when the branch matches a protected branch via wildcard match' do
- create(:protected_branch, project: project, name: "production/*")
-
- expect(project.protected_branch?('production/some-branch')).to eq(true)
- end
-
- it 'returns false when the branch does not match a protected branch via direct match' do
- expect(project.protected_branch?('foo')).to eq(false)
- end
-
- it 'returns false when the branch does not match a protected branch via wildcard match' do
- create(:protected_branch, project: project, name: "production/*")
-
- expect(project.protected_branch?('staging/some-branch')).to eq(false)
- end
- end
-
- context "new project" do
- let(:project) { create(:empty_project) }
-
- it 'returns false when default_protected_branch is unprotected' do
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
-
- expect(project.protected_branch?('master')).to be false
- end
-
- it 'returns false when default_protected_branch lets developers push' do
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
-
- expect(project.protected_branch?('master')).to be false
- end
-
- it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
-
- expect(project.protected_branch?('master')).to be true
- end
-
- it 'returns true when default_branch_protection is in full protection' do
- stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
-
- expect(project.protected_branch?('master')).to be true
- end
- end
- end
-
describe '#user_can_push_to_empty_repo?' do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
@@ -1949,7 +1874,7 @@ describe Project, models: true do
describe '#pipeline_status' do
let(:project) { create(:project) }
it 'builds a pipeline status' do
- expect(project.pipeline_status).to be_a(Ci::PipelineStatus)
+ expect(project.pipeline_status).to be_a(Gitlab::Cache::Ci::ProjectPipelineStatus)
end
it 'hase a loaded pipeline status' do
diff --git a/spec/models/protectable_dropdown_spec.rb b/spec/models/protectable_dropdown_spec.rb
new file mode 100644
index 00000000000..4c9bade592b
--- /dev/null
+++ b/spec/models/protectable_dropdown_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe ProtectableDropdown, models: true do
+ let(:project) { create(:project, :repository) }
+ let(:subject) { described_class.new(project, :branches) }
+
+ describe '#protectable_ref_names' do
+ before do
+ project.protected_branches.create(name: 'master')
+ end
+
+ it { expect(subject.protectable_ref_names).to include('feature') }
+ it { expect(subject.protectable_ref_names).not_to include('master') }
+
+ it "includes branches matching a protected branch wildcard" do
+ expect(subject.protectable_ref_names).to include('feature')
+
+ create(:protected_branch, name: 'feat*', project: project)
+
+ subject = described_class.new(project.reload, :branches)
+
+ expect(subject.protectable_ref_names).to include('feature')
+ end
+ end
+end
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index 8bf0d24a128..179a443c43d 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -113,8 +113,8 @@ describe ProtectedBranch, models: true do
staging = build(:protected_branch, name: "staging")
expect(ProtectedBranch.matching("production")).to be_empty
- expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).to include(production)
- expect(ProtectedBranch.matching("production", protected_branches: [production, staging])).not_to include(staging)
+ expect(ProtectedBranch.matching("production", protected_refs: [production, staging])).to include(production)
+ expect(ProtectedBranch.matching("production", protected_refs: [production, staging])).not_to include(staging)
end
end
@@ -132,8 +132,64 @@ describe ProtectedBranch, models: true do
staging = build(:protected_branch, name: "staging/*")
expect(ProtectedBranch.matching("production/some-branch")).to be_empty
- expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).to include(production)
- expect(ProtectedBranch.matching("production/some-branch", protected_branches: [production, staging])).not_to include(staging)
+ expect(ProtectedBranch.matching("production/some-branch", protected_refs: [production, staging])).to include(production)
+ expect(ProtectedBranch.matching("production/some-branch", protected_refs: [production, staging])).not_to include(staging)
+ end
+ end
+ end
+
+ describe '#protected?' do
+ context 'existing project' do
+ let(:project) { create(:project, :repository) }
+
+ it 'returns true when the branch matches a protected branch via direct match' do
+ create(:protected_branch, project: project, name: "foo")
+
+ expect(ProtectedBranch.protected?(project, 'foo')).to eq(true)
+ end
+
+ it 'returns true when the branch matches a protected branch via wildcard match' do
+ create(:protected_branch, project: project, name: "production/*")
+
+ expect(ProtectedBranch.protected?(project, 'production/some-branch')).to eq(true)
+ end
+
+ it 'returns false when the branch does not match a protected branch via direct match' do
+ expect(ProtectedBranch.protected?(project, 'foo')).to eq(false)
+ end
+
+ it 'returns false when the branch does not match a protected branch via wildcard match' do
+ create(:protected_branch, project: project, name: "production/*")
+
+ expect(ProtectedBranch.protected?(project, 'staging/some-branch')).to eq(false)
+ end
+ end
+
+ context "new project" do
+ let(:project) { create(:empty_project) }
+
+ it 'returns false when default_protected_branch is unprotected' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+
+ expect(ProtectedBranch.protected?(project, 'master')).to be false
+ end
+
+ it 'returns false when default_protected_branch lets developers push' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH)
+
+ expect(ProtectedBranch.protected?(project, 'master')).to be false
+ end
+
+ it 'returns true when default_branch_protection does not let developers push but let developer merge branches' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
+
+ expect(ProtectedBranch.protected?(project, 'master')).to be true
+ end
+
+ it 'returns true when default_branch_protection is in full protection' do
+ stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL)
+
+ expect(ProtectedBranch.protected?(project, 'master')).to be true
end
end
end
diff --git a/spec/models/protected_tag_spec.rb b/spec/models/protected_tag_spec.rb
new file mode 100644
index 00000000000..51353852a93
--- /dev/null
+++ b/spec/models/protected_tag_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe ProtectedTag, models: true do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'Validation' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:name) }
+ end
+end
diff --git a/spec/services/ci/expire_pipeline_cache_service_spec.rb b/spec/services/ci/expire_pipeline_cache_service_spec.rb
index 3c735872c30..166c6dfc93e 100644
--- a/spec/services/ci/expire_pipeline_cache_service_spec.rb
+++ b/spec/services/ci/expire_pipeline_cache_service_spec.rb
@@ -16,5 +16,12 @@ describe Ci::ExpirePipelineCacheService, services: true do
subject.execute(pipeline)
end
+
+ it 'updates the cached status for a project' do
+ expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline).
+ with(pipeline)
+
+ subject.execute(pipeline)
+ end
end
end
diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb
new file mode 100644
index 00000000000..62bdd49a4d7
--- /dev/null
+++ b/spec/services/protected_branches/update_service_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe ProtectedBranches::UpdateService, services: true do
+ let(:protected_branch) { create(:protected_branch) }
+ let(:project) { protected_branch.project }
+ let(:user) { project.owner }
+ let(:params) { { name: 'new protected branch name' } }
+
+ describe '#execute' do
+ subject(:service) { described_class.new(project, user, params) }
+
+ it 'updates a protected branch' do
+ result = service.execute(protected_branch)
+
+ expect(result.reload.name).to eq(params[:name])
+ end
+
+ context 'without admin_project permissions' do
+ let(:user) { create(:user) }
+
+ it "raises error" do
+ expect{ service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+ end
+end
diff --git a/spec/services/protected_tags/create_service_spec.rb b/spec/services/protected_tags/create_service_spec.rb
new file mode 100644
index 00000000000..d91a58e8de5
--- /dev/null
+++ b/spec/services/protected_tags/create_service_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe ProtectedTags::CreateService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { project.owner }
+ let(:params) do
+ {
+ name: 'master',
+ create_access_levels_attributes: [{ access_level: Gitlab::Access::MASTER }]
+ }
+ end
+
+ describe '#execute' do
+ subject(:service) { described_class.new(project, user, params) }
+
+ it 'creates a new protected tag' do
+ expect { service.execute }.to change(ProtectedTag, :count).by(1)
+ expect(project.protected_tags.last.create_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER])
+ end
+ end
+end
diff --git a/spec/services/protected_tags/update_service_spec.rb b/spec/services/protected_tags/update_service_spec.rb
new file mode 100644
index 00000000000..e78fde4c48d
--- /dev/null
+++ b/spec/services/protected_tags/update_service_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe ProtectedTags::UpdateService, services: true do
+ let(:protected_tag) { create(:protected_tag) }
+ let(:project) { protected_tag.project }
+ let(:user) { project.owner }
+ let(:params) { { name: 'new protected tag name' } }
+
+ describe '#execute' do
+ subject(:service) { described_class.new(project, user, params) }
+
+ it 'updates a protected tag' do
+ result = service.execute(protected_tag)
+
+ expect(result.reload.name).to eq(params[:name])
+ end
+
+ context 'without admin_project permissions' do
+ let(:user) { create(:user) }
+
+ it "raises error" do
+ expect{ service.execute(protected_tag) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+ end
+end
diff --git a/spec/workers/trigger_schedule_worker_spec.rb b/spec/workers/trigger_schedule_worker_spec.rb
index 151e1c2f7b9..861bed4442e 100644
--- a/spec/workers/trigger_schedule_worker_spec.rb
+++ b/spec/workers/trigger_schedule_worker_spec.rb
@@ -8,24 +8,39 @@ describe TriggerScheduleWorker do
end
context 'when there is a scheduled trigger within next_run_at' do
+ let(:next_run_at) { 2.days.ago }
+
+ let!(:trigger_schedule) do
+ create(:ci_trigger_schedule, :nightly)
+ end
+
before do
- trigger_schedule = create(:ci_trigger_schedule, :nightly)
- time_future = Time.now + 10.days
- allow(Time).to receive(:now).and_return(time_future)
- @next_time = Gitlab::Ci::CronParser.new(trigger_schedule.cron, trigger_schedule.cron_timezone).next_time_from(time_future)
+ trigger_schedule.update_column(:next_run_at, next_run_at)
end
it 'creates a new trigger request' do
- expect { worker.perform }.to change { Ci::TriggerRequest.count }.by(1)
+ expect { worker.perform }.to change { Ci::TriggerRequest.count }
end
it 'creates a new pipeline' do
- expect { worker.perform }.to change { Ci::Pipeline.count }.by(1)
+ expect { worker.perform }.to change { Ci::Pipeline.count }
expect(Ci::Pipeline.last).to be_pending
end
it 'updates next_run_at' do
- expect { worker.perform }.to change { Ci::TriggerSchedule.last.next_run_at }.to(@next_time)
+ worker.perform
+
+ expect(trigger_schedule.reload.next_run_at).not_to eq(next_run_at)
+ end
+
+ context 'inactive schedule' do
+ before do
+ trigger_schedule.update(active: false)
+ end
+
+ it 'does not create a new trigger' do
+ expect { worker.perform }.not_to change { Ci::TriggerRequest.count }
+ end
end
end
@@ -43,8 +58,8 @@ describe TriggerScheduleWorker do
context 'when next_run_at is nil' do
before do
- trigger_schedule = create(:ci_trigger_schedule, :nightly)
- trigger_schedule.update_attribute(:next_run_at, nil)
+ schedule = create(:ci_trigger_schedule, :nightly)
+ schedule.update_column(:next_run_at, nil)
end
it 'does not create a new pipeline' do