Merge branch 'master' into auto-search-when-state-changed

This commit is contained in:
Clement Ho 2017-06-06 11:16:02 -05:00
commit b6bded14be
104 changed files with 2050 additions and 1096 deletions

View File

@ -1 +1 @@
0.4.2
0.4.3

View File

@ -3,7 +3,7 @@
/* global ActiveTabMemoizer */
/* global ShortcutsNavigation */
/* global Build */
/* global Issuable */
/* global IssuableIndex */
/* global ShortcutsIssuable */
/* global ZenMode */
/* global Milestone */
@ -127,10 +127,9 @@ import ShortcutsBlob from './shortcuts_blob';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
Issuable.init();
new gl.IssuableBulkActions({
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
});
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
IssuableIndex.init(pagePrefix);
shortcut_handler = new ShortcutsNavigation();
new UsersSelect();
break;

View File

@ -70,7 +70,7 @@ export default {
</span>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<ul class="dropdown-menu">
<li v-for="action in actions">
<button
type="button"

View File

@ -421,14 +421,19 @@ export default {
};
</script>
<template>
<tr :class="{ 'js-child-row': model.isChildren }">
<td>
<div
:class="{ 'js-child-row environment-child-row': model.isChildren, 'folder-row': model.isFolder, 'gl-responsive-table-row': !model.isFolder }">
<div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
class="table-mobile-header">
Environment
</div>
<a
v-if="!model.isFolder"
class="environment-name"
:class="{ 'prepend-left-default': model.isChildren }"
class="environment-name flex-truncate-parent table-mobile-content"
:href="environmentPath">
{{model.name}}
<span class="flex-truncate-child">{{model.name}}</span>
</a>
<span
v-else
@ -461,9 +466,9 @@ export default {
{{model.size}}
</span>
</span>
</td>
</div>
<td class="deployment-column">
<div class="table-section section-10 deployment-column hidden-xs hidden-sm" role="gridcell">
<span v-if="shouldRenderDeploymentID">
{{deploymentInternalId}}
</span>
@ -478,21 +483,26 @@ export default {
:tooltip-text="deploymentUser.username"
/>
</span>
</td>
</div>
<td class="environments-build-cell">
<div class="table-section section-15 hidden-xs hidden-sm" role="gridcell">
<a
v-if="shouldRenderBuildName"
class="build-link"
:href="buildPath">
{{buildName}}
</a>
</td>
</div>
<td>
<div class="table-section section-25" role="gridcell">
<div
v-if="!model.isFolder"
class="table-mobile-header">
Commit
</div>
<div
v-if="!model.isFolder && hasLastDeploymentKey"
class="js-commit-component">
class="js-commit-component table-mobile-content">
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
@ -501,25 +511,30 @@ export default {
:title="commitTitle"
:author="commitAuthor"/>
</div>
<p
<div
v-if="!model.isFolder && !hasLastDeploymentKey"
class="commit-title">
No deployments yet
</p>
</td>
</div>
</div>
<td>
<span
v-if="!model.isFolder && canShowDate"
class="environment-created-date-timeago">
{{createdDate}}
</span>
</td>
<td class="environments-actions">
<div class="table-section section-10" role="gridcell">
<div
v-if="!model.isFolder"
class="btn-group pull-right"
class="table-mobile-header">
Updated
</div>
<span
v-if="!model.isFolder && canShowDate"
class="environment-created-date-timeago table-mobile-content">
{{createdDate}}
</span>
</div>
<div class="table-section section-30 environments-actions table-button-footer" role="gridcell">
<div
v-if="!model.isFolder"
class="btn-group environment-action-buttons"
role="group">
<actions-component
@ -553,6 +568,6 @@ export default {
:retry-url="retryUrl"
/>
</div>
</td>
</tr>
</div>
</div>
</template>

View File

@ -19,7 +19,7 @@ export default {
</script>
<template>
<a
class="btn monitoring-url has-tooltip"
class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
data-container="body"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"

View File

@ -43,7 +43,7 @@ export default {
<template>
<button
type="button"
class="btn"
class="btn hidden-xs hidden-sm"
@click="onClick"
:disabled="isLoading">

View File

@ -47,7 +47,7 @@ export default {
<template>
<button
type="button"
class="btn stop-env-link has-tooltip"
class="btn stop-env-link has-tooltip hidden-xs hidden-sm"
data-container="body"
@click="onClick"
:disabled="isLoading"

View File

@ -29,7 +29,7 @@ export default {
</script>
<template>
<a
class="btn terminal-button has-tooltip"
class="btn terminal-button has-tooltip hidden-xs hidden-sm"
data-container="body"
:title="title"
:aria-label="title"

View File

@ -45,68 +45,59 @@ export default {
};
</script>
<template>
<table class="table ci-table">
<thead>
<tr>
<th class="environments-name">
Environment
</th>
<th class="environments-deploy">
Last deployment
</th>
<th class="environments-build">
Job
</th>
<th class="environments-commit">
Commit
</th>
<th class="environments-date">
Updated
</th>
<th class="environments-actions"></th>
</tr>
</thead>
<tbody>
<template
v-for="model in environments"
v-bind:model="model">
<tr
is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 environments-name" role="rowheader">
Environment
</div>
<div class="table-section section-10 environments-deploy" role="rowheader">
Deployment
</div>
<div class="table-section section-15 environments-build" role="rowheader">
Job
</div>
<div class="table-section section-25 environments-commit" role="rowheader">
Commit
</div>
<div class="table-section section-10 environments-date" role="rowheader">
Updated
</div>
</div>
<template
v-for="model in environments"
v-bind:model="model">
<div
is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<tr v-if="isLoadingFolderContent">
<td colspan="6">
<loading-icon size="2" />
</td>
</tr>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<div v-if="isLoadingFolderContent">
<loading-icon size="2" />
</div>
<template v-else>
<tr
is="environment-item"
v-for="children in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<template v-else>
<div
is="environment-item"
v-for="children in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<tr>
<td
colspan="6"
class="text-center">
<a
:href="folderUrl(model)"
class="btn btn-default">
Show all
</a>
</td>
</tr>
</template>
<div>
<div class="text-center prepend-top-10">
<a
:href="folderUrl(model)"
class="btn btn-default">
Show all
</a>
</div>
</div>
</template>
</template>
</tbody>
</table>
</template>
</div>
</template>

View File

@ -0,0 +1,159 @@
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* global IssuableIndex */
/* global Flash */
export default {
init({ container, form, issues, prefixId } = {}) {
this.prefixId = prefixId || 'issue_';
this.form = form || this.getElement('.bulk-update');
this.$labelDropdown = this.form.find('.js-label-select');
this.issues = issues || this.getElement('.issues-list .issue');
this.willUpdateLabels = false;
this.bindEvents();
},
bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
},
onFormSubmit(e) {
e.preventDefault();
return this.submit();
},
submit() {
const _this = this;
const xhr = $.ajax({
url: this.form.attr('action'),
method: this.form.attr('method'),
dataType: 'JSON',
data: this.getFormDataAsObject()
});
xhr.done(() => window.location.reload());
xhr.fail(() => this.onFormSubmitFailure());
},
onFormSubmitFailure() {
this.form.find('[type="submit"]').enable();
return new Flash("Issue update failed");
},
getSelectedIssues() {
return this.issues.has('.selected_issue:checked');
},
getLabelsFromSelection() {
const labels = [];
this.getSelectedIssues().map(function() {
const labelsData = $(this).data('labels');
if (labelsData) {
return labelsData.map(function(labelId) {
if (labels.indexOf(labelId) === -1) {
return labels.push(labelId);
}
});
}
});
return labels;
},
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
*/
getUnmarkedIndeterminedLabels() {
const result = [];
const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
});
return result;
},
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
*/
getFormDataAsObject() {
const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
// For Merge Requests
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
// For Issues
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
add_label_ids: [],
remove_label_ids: []
}
};
if (this.willUpdateLabels) {
formData.update.add_label_ids = this.$labelDropdown.data('marked');
formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
}
return formData;
},
setOriginalDropdownData() {
const $labelSelect = $('.bulk-update .js-label-select');
$labelSelect.data('common', this.getOriginalCommonIds());
$labelSelect.data('marked', this.getOriginalMarkedIds());
$labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
},
// From issuable's initial bulk selection
getOriginalCommonIds() {
const labelIds = [];
this.getElement('.selected_issue:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
},
// From issuable's initial bulk selection
getOriginalMarkedIds() {
const labelIds = [];
this.getElement('.selected_issue:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
},
// From issuable's initial bulk selection
getOriginalIndeterminateIds() {
const uniqueIds = [];
const labelIds = [];
let issuableLabels = [];
// Collect unique label IDs for all checked issues
this.getElement('.selected_issue:checked').each((i, el) => {
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
issuableLabels.forEach((labelId) => {
// Store unique IDs
if (uniqueIds.indexOf(labelId) === -1) {
uniqueIds.push(labelId);
}
});
// Store array of IDs per issuable
labelIds.push(issuableLabels);
});
// Add uniqueIds to add it as argument for _.intersection
labelIds.unshift(uniqueIds);
// Return IDs that are present but not in all selected issueables
return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
},
getElement(selector) {
this.scopeEl = this.scopeEl || $('.content');
return this.scopeEl.find(selector);
},
};

View File

@ -0,0 +1,165 @@
/* eslint-disable class-methods-use-this, no-new */
/* global LabelsSelect */
/* global MilestoneSelect */
/* global IssueStatusSelect */
/* global SubscriptionSelect */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
const HIDDEN_CLASS = 'hidden';
const DISABLED_CONTENT_CLASS = 'disabled-content';
const SIDEBAR_EXPANDED_CLASS = 'right-sidebar-expanded issuable-bulk-update-sidebar';
const SIDEBAR_COLLAPSED_CLASS = 'right-sidebar-collapsed issuable-bulk-update-sidebar';
export default class IssuableBulkUpdateSidebar {
constructor() {
this.initDomElements();
this.bindEvents();
this.initDropdowns();
this.setupBulkUpdateActions();
}
initDomElements() {
this.$page = $('.page-with-sidebar');
this.$sidebar = $('.right-sidebar');
this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide');
this.$bulkEditSubmitBtn = $('.update-selected-issues');
this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle');
this.$otherFilters = $('.issues-other-filters');
this.$checkAllContainer = $('.check-all-holder');
this.$issueChecks = $('.issue-check');
this.$issuesList = $('.selected_issue');
this.$issuableIdsInput = $('#update_issuable_ids');
}
bindEvents() {
this.$bulkUpdateEnableBtn.on('click', e => this.toggleBulkEdit(e, true));
this.$bulkEditCancelBtn.on('click', e => this.toggleBulkEdit(e, false));
this.$checkAllContainer.on('click', e => this.selectAll(e));
this.$issuesList.on('change', () => this.updateFormState());
this.$bulkEditSubmitBtn.on('click', () => this.prepForSubmit());
this.$checkAllContainer.on('click', () => this.updateFormState());
}
initDropdowns() {
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
new SubscriptionSelect();
}
getNavHeight() {
const navbarHeight = $('.navbar-gitlab').outerHeight();
const layoutNavHeight = $('.layout-nav').outerHeight();
const subNavScroll = $('.sub-nav-scroll').outerHeight();
return navbarHeight + layoutNavHeight + subNavScroll;
}
initSidebar() {
if (!this.navHeight) {
this.navHeight = this.getNavHeight();
}
if (!this.sidebarInitialized) {
$(document).off('scroll').on('scroll', _.throttle(this.setSidebarHeight, 10).bind(this));
$(window).off('resize').on('resize', _.throttle(this.setSidebarHeight, 10).bind(this));
this.sidebarInitialized = true;
}
}
setupBulkUpdateActions() {
IssuableBulkUpdateActions.setOriginalDropdownData();
}
updateFormState() {
const noCheckedIssues = !$('.selected_issue:checked').length;
this.toggleSubmitButtonDisabled(noCheckedIssues);
this.updateSelectedIssuableIds();
IssuableBulkUpdateActions.setOriginalDropdownData();
}
prepForSubmit() {
// if submit button is disabled, submission is blocked. This ensures we disable after
// form submission is carried out
setTimeout(() => this.$bulkEditSubmitBtn.disable());
this.updateSelectedIssuableIds();
}
toggleBulkEdit(e, enable) {
e.preventDefault();
this.toggleSidebarDisplay(enable);
this.toggleBulkEditButtonDisabled(enable);
this.toggleOtherFiltersDisabled(enable);
this.toggleCheckboxDisplay(enable);
if (enable) {
this.initSidebar();
}
}
updateSelectedIssuableIds() {
this.$issuableIdsInput.val(IssuableBulkUpdateSidebar.getCheckedIssueIds());
}
selectAll() {
const checkAllButtonState = this.$checkAllContainer.find('input').prop('checked');
this.$issuesList.prop('checked', checkAllButtonState);
}
toggleSidebarDisplay(show) {
this.$page.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
this.$page.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
this.$sidebar.toggleClass(SIDEBAR_EXPANDED_CLASS, show);
this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
}
toggleBulkEditButtonDisabled(disable) {
if (disable) {
this.$bulkUpdateEnableBtn.disable();
} else {
this.$bulkUpdateEnableBtn.enable();
}
}
toggleCheckboxDisplay(show) {
this.$checkAllContainer.toggleClass(HIDDEN_CLASS, !show);
this.$issueChecks.toggleClass(HIDDEN_CLASS, !show);
}
toggleOtherFiltersDisabled(disable) {
this.$otherFilters.toggleClass(DISABLED_CONTENT_CLASS, disable);
}
toggleSubmitButtonDisabled(disable) {
if (disable) {
this.$bulkEditSubmitBtn.disable();
} else {
this.$bulkEditSubmitBtn.enable();
}
}
// loosely based on method of the same name in right_sidebar.js
setSidebarHeight() {
const currentScrollDepth = window.pageYOffset || 0;
const diff = this.navHeight - currentScrollDepth;
if (diff > 0) {
this.$sidebar.outerHeight(window.innerHeight - diff);
} else {
this.$sidebar.outerHeight('100%');
}
}
static getCheckedIssueIds() {
const $checkedIssues = $('.selected_issue:checked');
if ($checkedIssues.length > 0) {
return $.map($checkedIssues, value => $(value).data('id'));
}
return [];
}
}

View File

@ -1,30 +1,33 @@
/* eslint-disable no-param-reassign, func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, wrap-iife, max-len */
/* global Issuable */
/* global IssuableIndex */
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
((global) => {
var issuable_created;
issuable_created = false;
global.Issuable = {
init: function() {
Issuable.initTemplates();
Issuable.initSearch();
Issuable.initChecks();
Issuable.initResetFilters();
Issuable.resetIncomingEmailToken();
return Issuable.initLabelFilterRemove();
global.IssuableIndex = {
init: function(pagePrefix) {
IssuableIndex.initTemplates();
IssuableIndex.initSearch();
IssuableIndex.initBulkUpdate(pagePrefix);
IssuableIndex.initResetFilters();
IssuableIndex.resetIncomingEmailToken();
IssuableIndex.initLabelFilterRemove();
},
initTemplates: function() {
return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
return IssuableIndex.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
},
initSearch: function() {
const $searchInput = $('#issuable_search');
Issuable.initSearchState($searchInput);
IssuableIndex.initSearchState($searchInput);
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false);
const debouncedExecSearch = _.debounce(IssuableIndex.executeSearch, 1000, false);
$searchInput.off('keyup').on('keyup', debouncedExecSearch);
@ -37,16 +40,16 @@
initSearchState: function($searchInput) {
const currentSearchVal = $searchInput.val();
Issuable.searchState = {
IssuableIndex.searchState = {
elem: $searchInput,
current: currentSearchVal
};
Issuable.maybeFocusOnSearch();
IssuableIndex.maybeFocusOnSearch();
},
accessSearchPristine: function(set) {
// store reference to previous value to prevent search on non-mutating keyup
const state = Issuable.searchState;
const state = IssuableIndex.searchState;
const currentSearchVal = state.elem.val();
if (set) {
@ -56,10 +59,10 @@
}
},
maybeFocusOnSearch: function() {
const currentSearchVal = Issuable.searchState.current;
const currentSearchVal = IssuableIndex.searchState.current;
if (currentSearchVal && currentSearchVal !== '') {
const queryLength = currentSearchVal.length;
const $searchInput = Issuable.searchState.elem;
const $searchInput = IssuableIndex.searchState.elem;
/* The following ensures that the cursor is initially placed at
* the end of search input when focus is applied. It accounts
@ -80,7 +83,7 @@
const $searchValue = $search.val();
const $filtersForm = $('.js-filter-form');
const $input = $(`input[name='${$searchName}']`, $filtersForm);
const isPristine = Issuable.accessSearchPristine();
const isPristine = IssuableIndex.accessSearchPristine();
if (isPristine) {
return;
@ -92,7 +95,7 @@
$input.val($searchValue);
}
Issuable.filterResults($filtersForm);
IssuableIndex.filterResults($filtersForm);
},
initLabelFilterRemove: function() {
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
@ -103,7 +106,7 @@
return this.value === $button.data('label');
}).remove();
// Submit the form to get new data
Issuable.filterResults($('.filter-form'));
IssuableIndex.filterResults($('.filter-form'));
});
},
filterResults: (function(_this) {
@ -132,38 +135,18 @@
gl.utils.visitUrl(baseIssuesUrl);
});
},
initChecks: function() {
this.issuableBulkActions = $('.bulk-update').data('bulkActions');
$('.check_all_issues').off('click').on('click', function() {
$('.selected_issue').prop('checked', this.checked);
return Issuable.checkChanged();
});
return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
},
checkChanged: function() {
const $checkedIssues = $('.selected_issue:checked');
const $updateIssuesIds = $('#update_issuable_ids');
const $issuesOtherFilters = $('.issues-other-filters');
const $issuesBulkUpdate = $('.issues_bulk_update');
initBulkUpdate: function(pagePrefix) {
const userCanBulkUpdate = $('.issues-bulk-update').length > 0;
const alreadyInitialized = !!this.bulkUpdateSidebar;
this.issuableBulkActions.willUpdateLabels = false;
this.issuableBulkActions.setOriginalDropdownData();
if ($checkedIssues.length > 0) {
const ids = $.map($checkedIssues, function(value) {
return $(value).data('id');
if (userCanBulkUpdate && !alreadyInitialized) {
IssuableBulkUpdateActions.init({
prefixId: pagePrefix,
});
$updateIssuesIds.val(ids);
$issuesOtherFilters.hide();
$issuesBulkUpdate.show();
} else {
$updateIssuesIds.val([]);
$issuesBulkUpdate.hide();
$issuesOtherFilters.show();
}
return true;
},
this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
}
},
resetIncomingEmailToken: function() {
$('.incoming-email-token-reset').on('click', function(e) {
e.preventDefault();

View File

@ -1,166 +0,0 @@
/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, max-len, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
/* global Issuable */
/* global Flash */
((global) => {
class IssuableBulkActions {
constructor({ container, form, issues, prefixId } = {}) {
this.prefixId = prefixId || 'issue_';
this.form = form || this.getElement('.bulk-update');
this.$labelDropdown = this.form.find('.js-label-select');
this.issues = issues || this.getElement('.issues-list .issue');
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
this.bindEvents();
// Fixes bulk-assign not working when navigating through pages
Issuable.initChecks();
}
bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
}
onFormSubmit(e) {
e.preventDefault();
return this.submit();
}
submit() {
const _this = this;
const xhr = $.ajax({
url: this.form.attr('action'),
method: this.form.attr('method'),
dataType: 'JSON',
data: this.getFormDataAsObject()
});
xhr.done(() => window.location.reload());
xhr.fail(() => new Flash("Issue update failed"));
return xhr.always(this.onFormSubmitAlways.bind(this));
}
onFormSubmitAlways() {
return this.form.find('[type="submit"]').enable();
}
getSelectedIssues() {
return this.issues.has('.selected_issue:checked');
}
getLabelsFromSelection() {
const labels = [];
this.getSelectedIssues().map(function() {
const labelsData = $(this).data('labels');
if (labelsData) {
return labelsData.map(function(labelId) {
if (labels.indexOf(labelId) === -1) {
return labels.push(labelId);
}
});
}
});
return labels;
}
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
*/
getUnmarkedIndeterminedLabels() {
const result = [];
const labelsToKeep = this.$labelDropdown.data('indeterminate');
this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
});
return result;
}
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
*/
getFormDataAsObject() {
const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
// For Merge Requests
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
// For Issues
assignee_ids: [this.form.find('input[name="update[assignee_ids][]"]').val()],
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
add_label_ids: [],
remove_label_ids: []
}
};
if (this.willUpdateLabels) {
formData.update.add_label_ids = this.$labelDropdown.data('marked');
formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
}
return formData;
}
setOriginalDropdownData() {
const $labelSelect = $('.bulk-update .js-label-select');
$labelSelect.data('common', this.getOriginalCommonIds());
$labelSelect.data('marked', this.getOriginalMarkedIds());
$labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
}
// From issuable's initial bulk selection
getOriginalCommonIds() {
const labelIds = [];
this.getElement('.selected_issue:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
}
// From issuable's initial bulk selection
getOriginalMarkedIds() {
const labelIds = [];
this.getElement('.selected_issue:checked').each((i, el) => {
labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
});
return _.intersection.apply(this, labelIds);
}
// From issuable's initial bulk selection
getOriginalIndeterminateIds() {
const uniqueIds = [];
const labelIds = [];
let issuableLabels = [];
// Collect unique label IDs for all checked issues
this.getElement('.selected_issue:checked').each((i, el) => {
issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
issuableLabels.forEach((labelId) => {
// Store unique IDs
if (uniqueIds.indexOf(labelId) === -1) {
uniqueIds.push(labelId);
}
});
// Store array of IDs per issuable
labelIds.push(issuableLabels);
});
// Add uniqueIds to add it as argument for _.intersection
labelIds.unshift(uniqueIds);
// Return IDs that are present but not in all selected issueables
return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
}
getElement(selector) {
this.scopeEl = this.scopeEl || $('.content');
return this.scopeEl.find(selector);
}
}
global.IssuableBulkActions = IssuableBulkActions;
})(window.gl || (window.gl = {}));

View File

@ -2,6 +2,8 @@
/* global Issuable */
/* global ListLabel */
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
(function() {
this.LabelsSelect = (function() {
function LabelsSelect(els) {
@ -430,20 +432,15 @@
if ($('.selected_issue:checked').length) {
return;
}
return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label');
};
LabelsSelect.prototype.enableBulkLabelDropdown = function() {
var issuableBulkActions;
if ($('.selected_issue:checked').length) {
issuableBulkActions = $('.bulk-update').data('bulkActions');
return issuableBulkActions.willUpdateLabels = true;
}
IssuableBulkUpdateActions.willUpdateLabels = true;
};
LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
var i, markedIds, unmarkedIds, indeterminateIds;
var issuableBulkActions = $('.bulk-update').data('bulkActions');
markedIds = $dropdown.data('marked') || [];
unmarkedIds = $dropdown.data('unmarked') || [];
@ -469,13 +466,13 @@
}
// If an indeterminate item is being unmarked
if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
if (IssuableBulkUpdateActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
unmarkedIds.push(value);
}
// If a marked item is being unmarked
// (a marked item could also be a label that is present in all selection)
if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) {
if (IssuableBulkUpdateActions.getOriginalCommonIds().indexOf(value) > -1) {
unmarkedIds.push(value);
}
}

View File

@ -104,12 +104,11 @@ import './group_label_subscription';
import './groups_select';
import './header';
import './importer_status';
import './issuable';
import './issuable_index';
import './issuable_context';
import './issuable_form';
import './issue';
import './issue_status_select';
import './issues_bulk_assignment';
import './label_manager';
import './labels';
import './labels_select';

View File

@ -1,11 +1,17 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, no-unused-vars, one-var, no-underscore-dangle, prefer-template, no-else-return, prefer-arrow-callback, max-len */
function highlightChanges($elm) {
$elm.addClass('highlight-changes');
setTimeout(() => $elm.removeClass('highlight-changes'), 10);
}
(function() {
this.ProjectNew = (function() {
function ProjectNew() {
this.toggleSettings = this.toggleSettings.bind(this);
this.$selects = $('.features select');
this.$repoSelects = this.$selects.filter('.js-repo-select');
this.$projectSelects = this.$selects.not('.js-repo-select');
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
@ -26,6 +32,42 @@
if (!visibilityContainer) return;
const visibilitySelect = new gl.VisibilitySelect(visibilityContainer);
visibilitySelect.init();
const $visibilitySelect = $(visibilityContainer).find('select');
let projectVisibility = $visibilitySelect.val();
const PROJECT_VISIBILITY_PRIVATE = '0';
$visibilitySelect.on('change', () => {
const newProjectVisibility = $visibilitySelect.val();
if (projectVisibility !== newProjectVisibility) {
this.$projectSelects.each((idx, select) => {
const $select = $(select);
const $options = $select.find('option');
const values = $.map($options, e => e.value);
// if switched to "private", limit visibility options
if (newProjectVisibility === PROJECT_VISIBILITY_PRIVATE) {
if ($select.val() !== values[0] && $select.val() !== values[1]) {
$select.val(values[1]).trigger('change');
highlightChanges($select);
}
$options.slice(2).disable();
}
// if switched from "private", increase visibility for non-disabled options
if (projectVisibility === PROJECT_VISIBILITY_PRIVATE) {
$options.enable();
if ($select.val() !== values[0] && $select.val() !== values[values.length - 1]) {
$select.val(values[values.length - 1]).trigger('change');
highlightChanges($select);
}
}
});
projectVisibility = newProjectVisibility;
}
});
};
ProjectNew.prototype.toggleSettings = function() {
@ -56,8 +98,10 @@
ProjectNew.prototype.toggleRepoVisibility = function () {
var $repoAccessLevel = $('.js-repo-access-level select');
var $lfsEnabledOption = $('.js-lfs-enabled select');
var containerRegistry = document.querySelectorAll('.js-container-registry')[0];
var containerRegistryCheckbox = document.getElementById('project_container_registry_enabled');
var prevSelectedVal = parseInt($repoAccessLevel.val(), 10);
this.$repoSelects.find("option[value='" + $repoAccessLevel.val() + "']")
.nextAll()
@ -71,29 +115,40 @@
var $this = $(this);
var repoSelectVal = parseInt($this.val(), 10);
$this.find('option').show();
$this.find('option').enable();
if (selectedVal < repoSelectVal) {
$this.val(selectedVal);
if (selectedVal < repoSelectVal || repoSelectVal === prevSelectedVal) {
$this.val(selectedVal).trigger('change');
highlightChanges($this);
}
$this.find("option[value='" + selectedVal + "']").nextAll().hide();
$this.find("option[value='" + selectedVal + "']").nextAll().disable();
});
if (selectedVal) {
this.$repoSelects.removeClass('disabled');
if ($lfsEnabledOption.length) {
$lfsEnabledOption.removeClass('disabled');
highlightChanges($lfsEnabledOption);
}
if (containerRegistry) {
containerRegistry.style.display = '';
}
} else {
this.$repoSelects.addClass('disabled');
if ($lfsEnabledOption.length) {
$lfsEnabledOption.val('false').addClass('disabled');
highlightChanges($lfsEnabledOption);
}
if (containerRegistry) {
containerRegistry.style.display = 'none';
containerRegistryCheckbox.checked = false;
}
}
prevSelectedVal = selectedVal;
}.bind(this));
};

View File

@ -135,8 +135,8 @@ export default {
{{shortSha}}
</a>
<p class="commit-title">
<span v-if="title">
<div class="commit-title flex-truncate-parent">
<span v-if="title" class="flex-truncate-child">
<user-avatar-link
v-if="hasAuthor"
class="avatar-image-container"
@ -153,7 +153,7 @@ export default {
<span v-else>
Cant find HEAD commit for this branch
</span>
</p>
</div>
</div>
`,
};

View File

@ -49,3 +49,4 @@
@import "framework/icons.scss";
@import "framework/snippets.scss";
@import "framework/memory_graph.scss";
@import "framework/responsive-tables.scss";

View File

@ -445,3 +445,9 @@ table {
word-wrap: break-word;
}
}
.disabled-content {
pointer-events: none;
opacity: .5;
}

View File

@ -22,12 +22,6 @@
}
@media (min-width: $screen-sm-min) {
.issues_bulk_update {
.dropdown-menu-toggle {
width: 132px;
}
}
.filter-item:not(:last-child) {
margin-right: 6px;
}
@ -376,12 +370,6 @@
padding: 0;
}
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
.issue-bulk-update-dropdown-toggle {
width: 100px;
}
}
@media (max-width: $screen-xs-max) {
.issues-details-filters {
padding: 0 0 10px;

View File

@ -29,10 +29,6 @@
display: none;
}
.issues-holder .issue-check {
display: none;
}
.rss-btn {
display: none;
}

View File

@ -0,0 +1,86 @@
@mixin flex-max-width($max) {
flex: 0 0 #{$max + '%'};
max-width: #{$max + '%'};
}
.gl-responsive-table-row {
margin-top: 10px;
border: 1px solid $border-color;
@media (min-width: $screen-md-min) {
padding: 15px 0;
margin: 0;
display: flex;
align-items: center;
border: none;
border-bottom: 1px solid $white-normal;
}
.table-section {
white-space: nowrap;
$section-widths: 10 15 25 30;
@each $width in $section-widths {
&.section-#{$width} {
flex: 0 0 #{$width + '%'};
@media (min-width: $screen-md-min) {
max-width: #{$width + '%'};
}
}
}
&:not(.table-button-footer) {
@media (max-width: $screen-sm-max) {
display: flex;
align-self: stretch;
padding: 10px;
align-items: center;
height: 62px;
&:not(:first-of-type) {
border-top: 1px solid $white-normal;
}
}
}
}
}
.table-row-header {
font-size: 13px;
@media (max-width: $screen-sm-max) {
display: none;
}
}
.table-mobile-header {
color: $gl-text-color-secondary;
@include flex-max-width(40);
@media (min-width: $screen-md-min) {
display: none;
}
}
.table-mobile-content {
@media (max-width: $screen-sm-max) {
@include flex-max-width(60);
text-align: right;
}
}
.flex-truncate-parent {
display: flex;
}
.flex-truncate-child {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@media (min-width: $screen-md-min) {
flex: 0 0 90%;
}
}

View File

@ -33,7 +33,7 @@
padding-right: 0;
@media (min-width: $screen-sm-min) {
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
@ -56,7 +56,7 @@
z-index: 300;
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.wiki-sidebar):not(.build-sidebar) .content-wrapper {
&:not(.wiki-sidebar):not(.build-sidebar):not(.issuable-bulk-update-sidebar) .content-wrapper {
padding-right: $gutter_collapsed_width;
}
}
@ -88,3 +88,35 @@
min-height: 100%;
}
}
@mixin maintain-sidebar-dimensions {
display: block;
width: $gutter-width;
padding: 10px 20px;
}
.issues-bulk-update.right-sidebar {
@include maintain-sidebar-dimensions;
transition: right $sidebar-transition-duration;
right: -$gutter-width;
&.right-sidebar-expanded {
@include maintain-sidebar-dimensions;
right: 0;
}
&.right-sidebar-collapsed {
@include maintain-sidebar-dimensions;
right: -$gutter-width;
.block {
padding: 16px 0;
width: 250px;
border-bottom: 1px solid $border-color;
}
}
.issuable-sidebar {
padding: 0 3px;
}
}

View File

@ -187,6 +187,7 @@ $divergence-graph-bar-bg: #ccc;
$divergence-graph-separator-bg: #ccc;
$general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
/*

View File

@ -11,34 +11,7 @@
}
.environments-container {
.table-holder {
width: 100%;
@media (max-width: $screen-sm-max) {
overflow: auto;
}
}
.table.ci-table {
.environments-actions {
min-width: 300px;
}
.environments-commit,
.environments-actions {
width: 20%;
}
.environments-date {
width: 10%;
}
.environments-name,
.environments-deploy,
.environments-build {
width: 15%;
}
.ci-table {
.deployment-column {
> span {
word-break: break-all;
@ -150,6 +123,49 @@
}
}
.gl-responsive-table-row {
.environments-actions {
@media (min-width: $screen-md-min) {
text-align: right;
}
@media (max-width: $screen-sm-max) {
background-color: $gray-normal;
align-self: stretch;
border-top: 1px solid $border-color;
.environment-action-buttons {
padding: 10px;
display: flex;
.btn {
border-radius: 3px;
}
> .btn-group,
.external-url {
flex: 1;
flex-basis: 28px;
}
.dropdown-new {
width: 100%;
}
}
}
}
}
.folder-row {
padding: 15px 0;
border-bottom: 1px solid $white-normal;
@media (max-width: $screen-sm-max) {
border-top: 1px solid $white-normal;
margin-top: 10px;
}
}
.prometheus-graph {
text {
fill: $gl-text-color;

View File

@ -29,6 +29,20 @@
& > .form-group {
padding-left: 0;
}
select option[disabled] {
display: none;
}
}
select {
background: transparent;
transition: background 2s ease-out;
&.highlight-changes {
background: $highlight-changes-color;
transition: none;
}
}
.help-block {

View File

@ -0,0 +1,62 @@
class EventsFinder
attr_reader :source, :params, :current_user
# Used to filter Events
#
# Arguments:
# source - which user or project to looks for events on
# current_user - only return events for projects visible to this user
# params:
# action: string
# target_type: string
# before: datetime
# after: datetime
#
def initialize(params = {})
@source = params.delete(:source)
@current_user = params.delete(:current_user)
@params = params
end
def execute
events = source.events
events = by_current_user_access(events)
events = by_action(events)
events = by_target_type(events)
events = by_created_at_before(events)
events = by_created_at_after(events)
events
end
private
def by_current_user_access(events)
events.merge(ProjectsFinder.new(current_user: current_user).execute).references(:project)
end
def by_action(events)
return events unless Event::ACTIONS[params[:action]]
events.where(action: Event::ACTIONS[params[:action]])
end
def by_target_type(events)
return events unless Event::TARGET_TYPES[params[:target_type]]
events.where(target_type: Event::TARGET_TYPES[params[:target_type]])
end
def by_created_at_before(events)
return events unless params[:before]
events.where('events.created_at < ?', params[:before].beginning_of_day)
end
def by_created_at_after(events)
return events unless params[:after]
events.where('events.created_at > ?', params[:after].end_of_day)
end
end

View File

@ -138,11 +138,15 @@ module ProjectsHelper
if @project.private?
level = @project.project_feature.send(field)
options.delete('Everyone with access')
highest_available_option = options.values.max if level == ProjectFeature::ENABLED
disabled_option = ProjectFeature::ENABLED
highest_available_option = ProjectFeature::PRIVATE if level == disabled_option
end
options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
options = options_for_select(
options,
selected: highest_available_option || @project.project_feature.public_send(field),
disabled: disabled_option
)
content_tag(
:select,

View File

@ -31,9 +31,9 @@ module VisibilityLevelHelper
when Gitlab::VisibilityLevel::PRIVATE
"Project access must be granted explicitly to each user."
when Gitlab::VisibilityLevel::INTERNAL
"The project can be cloned by any logged in user."
"The project can be accessed by any logged in user."
when Gitlab::VisibilityLevel::PUBLIC
"The project can be cloned without any authentication."
"The project can be accessed without any authentication."
end
end

View File

@ -14,6 +14,30 @@ class Event < ActiveRecord::Base
DESTROYED = 10
EXPIRED = 11 # User left project due to expiry
ACTIONS = HashWithIndifferentAccess.new(
created: CREATED,
updated: UPDATED,
closed: CLOSED,
reopened: REOPENED,
pushed: PUSHED,
commented: COMMENTED,
merged: MERGED,
joined: JOINED,
left: LEFT,
destroyed: DESTROYED,
expired: EXPIRED
).freeze
TARGET_TYPES = HashWithIndifferentAccess.new(
issue: Issue,
milestone: Milestone,
merge_request: MergeRequest,
note: Note,
project: Project,
snippet: Snippet,
user: User
).freeze
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
@ -55,6 +79,14 @@ class Event < ActiveRecord::Base
def limit_recent(limit = 20, offset = nil)
recent.limit(limit).offset(offset)
end
def actions
ACTIONS.keys
end
def target_types
TARGET_TYPES.keys
end
end
def visible_to_user?(user = nil)

View File

@ -1,7 +1,7 @@
class PagesDomain < ActiveRecord::Base
belongs_to :project
validates :domain, hostname: true
validates :domain, hostname: { allow_numeric_hostname: true }
validates :domain, uniqueness: { case_sensitive: false }
validates :certificate, certificate: true, allow_nil: true, allow_blank: true
validates :key, certificate_key: true, allow_nil: true, allow_blank: true
@ -98,7 +98,7 @@ class PagesDomain < ActiveRecord::Base
def validate_pages_domain
return unless domain
if domain.downcase.ends_with?(".#{Settings.pages.host}".downcase)
if domain.downcase.ends_with?(Settings.pages.host.downcase)
self.errors.add(:domain, "*.#{Settings.pages.host} is restricted")
end
end

View File

@ -16,7 +16,7 @@ module Users
def record_activity
Gitlab::UserActivities.record(@author.id)
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username}")
Rails.logger.debug("Recorded activity: #{@activity} for User ID: #{@author.id} (username: #{@author.username})")
end
end
end

View File

@ -2,7 +2,7 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
.project-action-button.dropdown.inline>
%button.btn{ 'data-toggle' => 'dropdown' }
%button.btn.has-tooltip{ title: 'Download', 'data-toggle' => 'dropdown', 'aria-label' => 'Download' }
= icon('download')
= icon("caret-down")
%span.sr-only

View File

@ -1,6 +1,6 @@
- if current_user
.project-action-button.dropdown.inline
%a.btn.dropdown-toggle{ href: '#', "data-toggle" => "dropdown" }
%a.btn.dropdown-toggle.has-tooltip{ href: '#', title: 'Create new...', 'data-toggle' => 'dropdown', 'data-container' => 'body', 'aria-label' => 'Create new...' }
= icon('plus')
= icon("caret-down")
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown

View File

@ -5,7 +5,7 @@
= custom_icon('icon_fork')
%span Fork
- else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do
= link_to new_namespace_project_fork_path(@project.namespace, @project), class: 'btn' do
= custom_icon('icon_fork')
%span Fork
.count-with-arrow

View File

@ -42,7 +42,7 @@
.col-md-9
.label-light
= label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
= link_to "(?)", help_page_path("public_access/public_access")
= link_to icon('question-circle'), help_page_path("public_access/public_access")
%span.help-block
.col-md-3.visibility-select-container
= render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
@ -92,14 +92,14 @@
.form-group
= render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin?
.row
.row.js-lfs-enabled
.col-md-9
= f.label :lfs_enabled, 'LFS', class: 'label-light'
%span.help-block
Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
.col-md-3
= f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control', data: { field: 'lfs_enabled' }
= f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select', data: { field: 'lfs_enabled' }
- if Gitlab.config.registry.enabled

View File

@ -1,7 +1,7 @@
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
.issue-box
- if @bulk_edit
.issue-check
- if @can_bulk_update
.issue-check.hidden
= check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
.issue-info-container
.issue-title.title

View File

@ -1,5 +1,5 @@
- @no_container = true
- @bulk_edit = can?(current_user, :admin_issue, @project)
- @can_bulk_update = can?(current_user, :admin_issue, @project)
- page_title "Issues"
- new_issue_email = @project.new_issue_address(current_user)
@ -20,6 +20,8 @@
.nav-controls
= link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
= icon('rss')
- if @can_bulk_update
= button_tag "Edit Issues", class: "btn btn-default js-bulk-update-toggle"
= link_to new_namespace_project_issue_path(@project.namespace,
@project,
issue: { assignee_id: issues_finder.assignee.try(:id),
@ -30,6 +32,9 @@
New issue
= render 'shared/issuable/search_bar', type: :issues
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :issues
.issues-holder
= render 'issues'
- if new_issue_email

View File

@ -1,6 +1,6 @@
%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
- if @bulk_edit
.issue-check
- if @can_bulk_update
.issue-check.hidden
= check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
.issue-info-container

View File

@ -1,5 +1,5 @@
- @no_container = true
- @bulk_edit = can?(current_user, :admin_merge_request, @project)
- @can_bulk_update = can?(current_user, :admin_merge_request, @project)
- page_title "Merge Requests"
- unless @project.default_issues_tracker?
@ -18,6 +18,8 @@
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.nav-controls
- if @can_bulk_update
= button_tag "Edit Merge Requests", class: "btn js-bulk-update-toggle"
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_project
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do
@ -25,6 +27,9 @@
= render 'shared/issuable/search_bar', type: :merge_requests
- if @can_bulk_update
= render 'shared/issuable/bulk_update_sidebar', type: :merge_requests
.merge-requests-holder
= render 'merge_requests'
- else

View File

@ -95,7 +95,7 @@
.form-group.project-visibility-level-holder
= f.label :visibility_level, class: 'label-light' do
Visibility Level
= link_to icon('question-circle'), help_page_path("public_access/public_access")
= link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4

View File

@ -1,16 +1,14 @@
- noteable = @sent_notification.noteable
- noteable_type = @sent_notification.noteable_type.humanize(capitalize: false)
- noteable_type = @sent_notification.noteable_type.titleize.downcase
- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
- page_title "Unsubscribe", noteable_text, @sent_notification.noteable_type.humanize.pluralize, @sent_notification.project.name_with_namespace
- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace
%h3.page-title
Unsubscribe from #{noteable_type} #{noteable_text}
Unsubscribe from #{noteable_type}
%p
= succeed '?' do
Are you sure you want to unsubscribe from #{noteable_type}
Are you sure you want to unsubscribe from the #{noteable_type}:
= link_to noteable_text, url_for([@sent_notification.project.namespace.becomes(Namespace), @sent_notification.project, noteable])
%p

View File

@ -0,0 +1,53 @@
- type = local_assigns.fetch(:type)
%aside.issues-bulk-update.js-right-sidebar.right-sidebar.affix-top{ data: { "offset-top" => "50", "spy" => "affix" }, "aria-live" => "polite" }
.issuable-sidebar
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: "bulk-update" do
.block
.filter-item.inline.update-issues-btn.pull-left
= button_tag "Update all", class: "btn update-selected-issues btn-info", disabled: true
= button_tag "Cancel", class: "btn btn-default js-bulk-update-menu-hide pull-right"
.block
.title
Status
.filter-item
= dropdown_tag("Select status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
%ul
%li
%a{ href: "#", data: { id: "reopen" } } Open
%li
%a{ href: "#", data: { id: "close" } } Closed
.block
.title
Assignee
.filter-item
- if type == :issues
- field_name = "update[assignee_ids][]"
- else
- field_name = "update[assignee_id]"
= dropdown_tag("Select assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
.block
.title
Milestone
.filter-item
= dropdown_tag("Select milestone", options: { title: "Assign milestone", toggle_class: "js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true, default_label: "Milestone" } })
.block
.title
Labels
.filter-item.labels-filter
= render "shared/issuable/label_dropdown", classes: ["js-filter-bulk-update", "js-multiselect"], dropdown_title: "Apply a label", show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: "Labels" }, label_name: "Select labels", no_default_styles: true
.block
.title
Subscriptions
.filter-item
= dropdown_tag("Select subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul
%li
%a{ href: "#", data: { id: "subscribe" } } Subscribe
%li
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
= hidden_field_tag "update[issuable_ids]", []
= hidden_field_tag :state_event, params[:state_event]

View File

@ -6,10 +6,6 @@
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
- if @bulk_edit
.check-all-holder
= check_box_tag "check_all_issues", nil, false,
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
- if params[:author_id].present?
@ -36,35 +32,6 @@
.pull-right
= render 'shared/sort_dropdown'
- if @bulk_edit
.issues_bulk_update.hide
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
%ul
%li
%a{ href: "#", data: { id: "reopen" } } Open
%li
%a{ href: "#", data: {id: "close" } } Closed
.filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } })
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'issue-bulk-update-dropdown-toggle js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", default_label: "Milestone", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } })
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "issue-bulk-update-dropdown-toggle js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul
%li
%a{ href: "#", data: { id: "subscribe" } } Subscribe
%li
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
= hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
- has_labels = @labels && @labels.any?
.row-content-block.second-block.filtered-labels{ class: ("hidden" unless has_labels) }
- if has_labels

View File

@ -11,6 +11,8 @@
- dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label")
- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
- dropdown_data.merge!(data_options)
- label_name = local_assigns.fetch(:label_name, "Labels")
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
- classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit
@ -20,8 +22,9 @@
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data }
%span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) }
= multi_label_name(selected, "Labels")
- apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
= multi_label_name(selected, label_name)
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }

View File

@ -6,10 +6,9 @@
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:search].present?
= hidden_field_tag :search, params[:search]
- if @bulk_edit
.check-all-holder
= check_box_tag "check_all_issues", nil, false,
class: "check_all_issues left"
- if @can_bulk_update
.check-all-holder.hidden
= check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
.issues-other-filters.filtered-search-wrapper
.filtered-search-box
- if type != :boards_modal && type != :boards
@ -110,55 +109,11 @@
- elsif type != :boards_modal
= render 'shared/sort_dropdown'
- if @bulk_edit
.issues_bulk_update.hide
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
%ul
%li
%a{ href: "#", data: { id: "reopen" } } Open
%li
%a{ href: "#", data: { id: "close" } } Closed
.filter-item.inline
- if type == :issues
- field_name = "update[assignee_ids][]"
- else
- field_name = "update[assignee_id]"
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: field_name } })
.filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true, default_label: "Milestone" } })
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: "Labels" }
.filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul
%li
%a{ href: "#", data: { id: "subscribe" } } Subscribe
%li
%a{ href: "#", data: { id: "unsubscribe" } } Unsubscribe
= hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline.update-issues-btn
= button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
- unless type === :boards_modal
:javascript
new LabelsSelect();
new MilestoneSelect();
new IssueStatusSelect();
new SubscriptionSelect();
$(document).off('page:restore').on('page:restore', function (event) {
if (gl.FilteredSearchManager) {
const filteredSearchManager = new gl.FilteredSearchManager();
filteredSearchManager.setup();
}
Issuable.init();
new gl.IssuableBulkActions({
prefixId: 'issue_',
});
});

View File

@ -6,14 +6,14 @@
.js-notification-toggle-btns
%div{ class: ("btn-group" if notification_setting.custom?) }
- if notification_setting.custom?
%button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } }
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
= icon('caret-down')
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
%button.dropdown-new.btn.btn-default.has-tooltip.notifications-btn#notifications-button{ type: "button", title: "Notification setting", "aria-label" => "Notification setting: #{notification_title(notification_setting.level)}", data: { container: "body", toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
= icon("caret-down")

View File

@ -0,0 +1,4 @@
---
title: Introduce an Events API
merge_request: 11755
author:

View File

@ -0,0 +1,4 @@
---
title: Automatically adjust project settings to match changes in project visibility
merge_request: 11831
author:

View File

@ -0,0 +1,4 @@
---
title: Add a Rake task to aid in rotating otp_key_base
merge_request: 11881
author:

View File

@ -0,0 +1,4 @@
---
title: 'Introduce optimistic locking support via optional parameter last_commit_sha on File Update API'
merge_request: 11694
author: electroma

View File

@ -0,0 +1,4 @@
---
title: Allow numeric pages domain
merge_request: 11550
author:

View File

@ -0,0 +1,4 @@
---
title: Fixed style on unsubscribe page
merge_request:
author: Gustav Ernberg

View File

@ -1,10 +1,7 @@
# GitLab Container Registry administration
> [Introduced][ce-4040] in GitLab 8.8.
---
> **Notes:**
- [Introduced][ce-4040] in GitLab 8.8.
- Container Registry manifest `v1` support was added in GitLab 8.9 to support
Docker versions earlier than 1.10.
- This document is about the admin guide. To learn how to use GitLab Container
@ -514,8 +511,8 @@ configurable in future releases.
## Configure Container Registry notifications
You can configure the Container Registry to send webhook notifications in
response to events happening within the registry.
You can configure the Container Registry to send webhook notifications in
response to events happening within the registry.
Read more about the Container Registry notifications config options in the
[Docker Registry notifications documentation][notifications-config].
@ -568,12 +565,25 @@ notifications:
backoff: 1000
```
## Changelog
## Using self-signed certificates with Container Registry
**GitLab 8.8 ([source docs][8-8-docs])**
If you're using a self-signed certificate with your Container Registry, you
might encounter issues during the CI jobs like the following:
- GitLab Container Registry feature was introduced.
```
Error response from daemon: Get registry.example.com/v1/users/: x509: certificate signed by unknown authority
```
The Docker daemon running the command expects a cert signed by a recognized CA,
thus the error above.
While GitLab doesn't support using self-signed certificates with Container
Registry out of the box, it is possible to make it work if you follow
[Docker's documentation][docker-insecure]. You may find some additional
information in [issue 18239][ce-18239].
[ce-18239]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18239
[docker-insecure]: https://docs.docker.com/registry/insecure/#using-self-signed-certificates
[reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure
[restart gitlab]: restart_gitlab.md#installations-from-source
[wildcard certificate]: https://en.wikipedia.org/wiki/Wildcard_certificate
@ -589,4 +599,4 @@ notifications:
[existing-domain]: #configure-container-registry-under-an-existing-gitlab-domain
[new-domain]: #configure-container-registry-under-its-own-domain
[notifications-config]: https://docs.docker.com/registry/notifications/
[registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications
[registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications

View File

@ -15,6 +15,8 @@ following locations:
- [Commits](commits.md)
- [Deployments](deployments.md)
- [Deploy Keys](deploy_keys.md)
- [Environments](environments.md)
- [Events](events.md)
- [Gitignores templates](templates/gitignores.md)
- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
- [Groups](groups.md)

347
doc/api/events.md Normal file
View File

@ -0,0 +1,347 @@
# Events
## Filter parameters
### Action Types
Available action types for the `action` parameter are:
- `created`
- `updated`
- `closed`
- `reopened`
- `pushed`
- `commented`
- `merged`
- `joined`
- `left`
- `destroyed`
- `expired`
Note that these options are downcased.
### Target Types
Available target types for the `target_type` parameter are:
- `issue`
- `milestone`
- `merge_request`
- `note`
- `project`
- `snippet`
- `user`
Note that these options are downcased.
### Date formatting
Dates for the `before` and `after` parameters should be supplied in the following format:
```
YYYY-MM-DD
```
## List currently authenticated user's events
>**Note:** This endpoint was introduced in GitLab 9.3.
Get a list of events for the authenticated user.
```
GET /events
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `action` | string | no | Include only events of a particular [action type][action-types] |
| `target_type` | string | no | Include only events of a particular [target type][target-types] |
| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
Example request:
```
curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
```
Example response:
```json
[
{
"title":null,
"project_id":1,
"action_name":"opened",
"target_id":160,
"target_type":"Issue",
"author_id":25,
"data":null,
"target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
"created_at":"2017-02-09T10:43:19.667Z",
"author":{
"name":"User 3",
"username":"user3",
"id":25,
"state":"active",
"avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon",
"web_url":"https://gitlab.example.com/user3"
},
"author_username":"user3"
},
{
"title":null,
"project_id":1,
"action_name":"opened",
"target_id":159,
"target_type":"Issue",
"author_id":21,
"data":null,
"target_title":"Nostrum enim non et sed optio illo deleniti non.",
"created_at":"2017-02-09T10:43:19.426Z",
"author":{
"name":"Test User",
"username":"ted",
"id":21,
"state":"active",
"avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon",
"web_url":"https://gitlab.example.com/ted"
},
"author_username":"ted"
}
]
```
### Get user contribution events
>**Note:** Documentation was formerly located in the [Users API pages][users-api].
Get the contribution events for the specified user, sorted from newest to oldest.
```
GET /users/:id/events
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID or Username of the user |
| `action` | string | no | Include only events of a particular [action type][action-types] |
| `target_type` | string | no | Include only events of a particular [target type][target-types] |
| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
```
Example response:
```json
[
{
"title": null,
"project_id": 15,
"action_name": "closed",
"target_id": 830,
"target_type": "Issue",
"author_id": 1,
"data": null,
"target_title": "Public project search field",
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
},
{
"title": null,
"project_id": 15,
"action_name": "opened",
"target_id": null,
"target_type": null,
"author_id": 1,
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "john",
"data": {
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
"after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"ref": "refs/heads/master",
"user_id": 1,
"user_name": "Dmitriy Zaporozhets",
"repository": {
"name": "gitlabhq",
"url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
"description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
"homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
},
"commits": [
{
"id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"message": "Add simple search to projects in public area",
"timestamp": "2013-05-13T18:18:08+00:00",
"url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}
],
"total_commits_count": 1
},
"target_title": null
},
{
"title": null,
"project_id": 15,
"action_name": "closed",
"target_id": 840,
"target_type": "Issue",
"author_id": 1,
"data": null,
"target_title": "Finish & merge Code search PR",
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
},
{
"title": null,
"project_id": 15,
"action_name": "commented on",
"target_id": 1312,
"target_type": "Note",
"author_id": 1,
"data": null,
"target_title": null,
"created_at": "2015-12-04T10:33:58.089Z",
"note": {
"id": 1312,
"body": "What an awesome day!",
"attachment": null,
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"created_at": "2015-12-04T10:33:56.698Z",
"system": false,
"noteable_id": 377,
"noteable_type": "Issue"
},
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
}
]
```
## List a Project's visible events
>**Note:** This endpoint has been around longer than the others. Documentation was formerly located in the [Projects API pages][projects-api].
Get a list of visible events for a particular project.
```
GET /:project_id/events
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `project_id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `action` | string | no | Include only events of a particular [action type][action-types] |
| `target_type` | string | no | Include only events of a particular [target type][target-types] |
| `before` | date | no | Include only events created before a particular date. Please see [here for the supported format][date-formatting] |
| `after` | date | no | Include only events created after a particular date. Please see [here for the supported format][date-formatting] |
| `sort` | string | no | Sort events in `asc` or `desc` order by `created_at`. Default is `desc` |
Example request:
```
curl --header "PRIVATE-TOKEN 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:project_id/events&target_type=issue&action=created&after=2017-01-31&before=2017-03-01
```
Example response:
```json
[
{
"title":null,
"project_id":1,
"action_name":"opened",
"target_id":160,
"target_type":"Issue",
"author_id":25,
"data":null,
"target_title":"Qui natus eos odio tempore et quaerat consequuntur ducimus cupiditate quis.",
"created_at":"2017-02-09T10:43:19.667Z",
"author":{
"name":"User 3",
"username":"user3",
"id":25,
"state":"active",
"avatar_url":"http://www.gravatar.com/avatar/97d6d9441ff85fdc730e02a6068d267b?s=80\u0026d=identicon",
"web_url":"https://gitlab.example.com/user3"
},
"author_username":"user3"
},
{
"title":null,
"project_id":1,
"action_name":"opened",
"target_id":159,
"target_type":"Issue",
"author_id":21,
"data":null,
"target_title":"Nostrum enim non et sed optio illo deleniti non.",
"created_at":"2017-02-09T10:43:19.426Z",
"author":{
"name":"Test User",
"username":"ted",
"id":21,
"state":"active",
"avatar_url":"http://www.gravatar.com/avatar/80fb888c9a48b9a3f87477214acaa63f?s=80\u0026d=identicon",
"web_url":"https://gitlab.example.com/ted"
},
"author_username":"ted"
}
]
```
[target-types]: #target-types "Target Type parameter"
[action-types]: #action-types "Action Type parameter"
[date-formatting]: #date-formatting "Date Formatting guidance"
[projects-api]: projects.md "Projects API pages"
[users-api]: users.md "Users API pages"

View File

@ -310,143 +310,7 @@ GET /projects/:id/users
### Get project events
Get the events for the specified project sorted from newest to oldest. This
endpoint can be accessed without authentication if the project is publicly
accessible.
```
GET /projects/:id/events
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
```json
[
{
"title": null,
"project_id": 15,
"action_name": "closed",
"target_id": 830,
"target_type": "Issue",
"author_id": 1,
"data": null,
"target_title": "Public project search field",
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
},
{
"title": null,
"project_id": 15,
"action_name": "opened",
"target_id": null,
"target_type": null,
"author_id": 1,
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "john",
"data": {
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
"after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"ref": "refs/heads/master",
"user_id": 1,
"user_name": "Dmitriy Zaporozhets",
"repository": {
"name": "gitlabhq",
"url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
"description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
"homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
},
"commits": [
{
"id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"message": "Add simple search to projects in public area",
"timestamp": "2013-05-13T18:18:08+00:00",
"url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}
],
"total_commits_count": 1
},
"target_title": null
},
{
"title": null,
"project_id": 15,
"action_name": "closed",
"target_id": 840,
"target_type": "Issue",
"author_id": 1,
"data": null,
"target_title": "Finish & merge Code search PR",
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
},
{
"title": null,
"project_id": 15,
"action_name": "commented on",
"target_id": 1312,
"target_type": "Note",
"author_id": 1,
"data": null,
"target_title": null,
"created_at": "2015-12-04T10:33:58.089Z",
"note": {
"id": 1312,
"body": "What an awesome day!",
"attachment": null,
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"created_at": "2015-12-04T10:33:56.698Z",
"system": false,
"noteable_id": 377,
"noteable_type": "Issue"
},
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
}
]
```
Please refer to the [Events API documentation](events.md#list-a-projects-visible-events)
### Create project

View File

@ -111,6 +111,7 @@ Parameters:
- `author_name` (optional) - Specify the commit author's name
- `content` (required) - New file content
- `commit_message` (required) - Commit message
- `last_commit_id` (optional) - Last known file commit id
If the commit fails for any reason we return a 400 error with a non-specific
error message. Possible causes for a failed commit include:

View File

@ -701,147 +701,8 @@ Will return `201 OK` on success, `404 User Not Found` is user cannot be found or
### Get user contribution events
Get the contribution events for the specified user, sorted from newest to oldest.
Please refer to the [Events API documentation](events.md#get-user-contribution-events)
```
GET /users/:id/events
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer | yes | The ID of the user |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/users/:id/events
```
Example response:
```json
[
{
"title": null,
"project_id": 15,
"action_name": "closed",
"target_id": 830,
"target_type": "Issue",
"author_id": 1,
"data": null,
"target_title": "Public project search field",
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
},
{
"title": null,
"project_id": 15,
"action_name": "opened",
"target_id": null,
"target_type": null,
"author_id": 1,
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "john",
"data": {
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
"after": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"ref": "refs/heads/master",
"user_id": 1,
"user_name": "Dmitriy Zaporozhets",
"repository": {
"name": "gitlabhq",
"url": "git@dev.gitlab.org:gitlab/gitlabhq.git",
"description": "GitLab: self hosted Git management software. \r\nDistributed under the MIT License.",
"homepage": "https://dev.gitlab.org/gitlab/gitlabhq"
},
"commits": [
{
"id": "c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"message": "Add simple search to projects in public area",
"timestamp": "2013-05-13T18:18:08+00:00",
"url": "https://dev.gitlab.org/gitlab/gitlabhq/commit/c5feabde2d8cd023215af4d2ceeb7a64839fc428",
"author": {
"name": "Dmitriy Zaporozhets",
"email": "dmitriy.zaporozhets@gmail.com"
}
}
],
"total_commits_count": 1
},
"target_title": null
},
{
"title": null,
"project_id": 15,
"action_name": "closed",
"target_id": 840,
"target_type": "Issue",
"author_id": 1,
"data": null,
"target_title": "Finish & merge Code search PR",
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
},
{
"title": null,
"project_id": 15,
"action_name": "commented on",
"target_id": 1312,
"target_type": "Note",
"author_id": 1,
"data": null,
"target_title": null,
"created_at": "2015-12-04T10:33:58.089Z",
"note": {
"id": 1312,
"body": "What an awesome day!",
"attachment": null,
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"created_at": "2015-12-04T10:33:56.698Z",
"system": false,
"noteable_id": 377,
"noteable_type": "Issue"
},
"author": {
"name": "Dmitriy Zaporozhets",
"username": "root",
"id": 1,
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
"web_url": "http://localhost:3000/root"
},
"author_username": "root"
}
]
```
## Get all impersonation tokens of a user

View File

@ -1,11 +1,11 @@
# Analyze project code quality with Code Climate CLI
This example shows how to run [Code Climate CLI][cli] on your code by using\
This example shows how to run [Code Climate CLI][cli] on your code by using
GitLab CI and Docker.
First, you need GitLab Runner with [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
First, you need GitLab Runner with [docker-in-docker executor][dind].
Once you setup the Runner add new job to `.gitlab-ci.yml`:
Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codeclimate`:
```yaml
codeclimate:
@ -25,4 +25,10 @@ codeclimate:
This will create a `codeclimate` job in your CI pipeline and will allow you to
download and analyze the report artifact in JSON format.
For GitLab [Enterprise Edition Starter][ee] users, this information can be automatically
extracted and shown right in the merge request widget. [Learn more on code quality
diffs in merge requests](http://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.md).
[cli]: https://github.com/codeclimate/codeclimate
[dind]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[ee]: https://about.gitlab.com/gitlab-ee/

View File

@ -120,7 +120,7 @@ The YAML-defined variables are also set to all created
tune them.
Variables can be defined at a global level, but also at a job level. To turn off
global defined variables in your job, define an empty array:
global defined variables in your job, define an empty hash:
```yaml
job_name:
@ -345,20 +345,45 @@ All variables are set as environment variables in the build environment, and
they are accessible with normal methods that are used to access such variables.
In most cases `bash` or `sh` is used to execute the job script.
To access the variables (predefined and user-defined) in a `bash`/`sh` environment,
prefix the variable name with the dollar sign (`$`):
To access environment variables, use the syntax for your Runner's [shell][shellexecutors].
```
| Shell | Usage |
|----------------------|-----------------|
| bash/sh | `$variable` |
| windows batch | `%variable%` |
| PowerShell | `$env:variable` |
To access environment variables in bash, prefix the variable name with (`$`):
```yaml
job_name:
script:
- echo $CI_JOB_ID
```
To access environment variables in **Windows Batch**, surround the variable
with (`%`):
```yaml
job_name:
script:
- echo %CI_JOB_ID%
```
To access environment variables in a **Windows PowerShell** environment, prefix
the variable name with (`$env:`):
```yaml
job_name:
script:
- echo $env:CI_JOB_ID
```
You can also list all environment variables with the `export` command,
but be aware that this will also expose the values of all the secret variables
you set, in the job log:
```
```yaml
job_name:
script:
- export
@ -405,3 +430,4 @@ export CI_REGISTRY_PASSWORD="longalfanumstring"
[triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger
[protected branches]: ../../user/project/protected_branches.md
[protected tags]: ../../user/project/protected_tags.md
[shellexecutors]: https://docs.gitlab.com/runner/executors/

View File

@ -297,6 +297,15 @@ cache:
untracked: true
```
If you use **Windows PowerShell** to run your shell scripts you need to replace
`$` with `$env:`:
```yaml
cache:
key: "$env:CI_JOB_STAGE/$env:CI_COMMIT_REF_NAME"
untracked: true
```
## Jobs
`.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job
@ -434,7 +443,7 @@ but allows you to define job-specific variables.
When the `variables` keyword is used on a job level, it overrides the global YAML
job variables and predefined ones. To turn off global defined variables
in your job, define an empty array:
in your job, define an empty hash:
```yaml
job_name:
@ -909,6 +918,16 @@ job:
untracked: true
```
If you use **Windows PowerShell** to run your shell scripts you need to replace
`$` with `$env:`:
```yaml
job:
artifacts:
name: "$env:CI_JOB_STAGE_$env:CI_COMMIT_REF_NAME"
untracked: true
```
#### artifacts:when
> Introduced in GitLab 8.9 and GitLab Runner v1.3.0.

View File

@ -12,7 +12,7 @@ Both EE and CE require some add-on components called gitlab-shell and Gitaly. Th
You can imagine GitLab as a physical office.
**The repositories** are the goods GitLab handling.
**The repositories** are the goods GitLab handles.
They can be stored in a warehouse.
This can be either a hard disk, or something more complex, such as a NFS filesystem;
@ -54,7 +54,7 @@ To serve repositories over SSH there's an add-on application called gitlab-shell
### Components
![GitLab Diagram Overview](gitlab_architecture_diagram.png)
<img src="https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/pub?w=987&amp;h=797">
_[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_
@ -66,7 +66,9 @@ When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to reso
The add-on component gitlab-shell serves repositories over SSH. It manages the SSH keys within `/home/git/.ssh/authorized_keys` which should not be manually edited. gitlab-shell accesses the bare repositories through Gitaly to serve git objects and communicates with redis to submit jobs to Sidekiq for GitLab to process. gitlab-shell queries the GitLab API to determine authorization and access.
Gitaly executes git operations from gitlab-shell and Workhorse, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files)
Gitaly executes git operations from gitlab-shell and the GitLab web app, and provides an API to the GitLab web app to get attributes from git (e.g. title, branches, tags, other meta data), and to get blobs (e.g. diffs, commits, files).
You may also be interested in the [production architecture of GitLab.com](https://about.gitlab.com/handbook/infrastructure/production-architecture/).
### Installation Folder Summary

View File

@ -207,7 +207,9 @@ its class in an annotation.
>**Note:**
The Ingress alone doesn't expose GitLab externally. You need to have a Ingress controller setup to do that.
Setting up an Ingress controller can be done by installing the `nginx-ingress` helm chart. But be sure
to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md)
to read the [documentation](https://github.com/kubernetes/charts/blob/master/stable/nginx-ingress/README.md).
>**Note:**
If you would like to use the Registry, you will also need to ensure your Ingress supports a [sufficiently large request size](http://nginx.org/en/docs/http/ngx_http_core_module.html#client_max_body_size).
#### Preserving Source IPs

View File

@ -141,7 +141,7 @@ Once you [have configured](#configuration) GitLab Runner in your `values.yml` fi
run the following:
```bash
helm install --namepace <NAMEPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> gitlab/gitlab-runner
helm install --namespace <NAMESPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE> gitlab/gitlab-runner
```
- `<NAMESPACE>` is the Kubernetes namespace where you want to install the GitLab Runner.
@ -153,7 +153,7 @@ helm install --namepace <NAMEPACE> --name gitlab-runner -f <CONFIG_VALUES_FILE>
Once your GitLab Runner Chart is installed, configuration changes and chart updates should we done using `helm upgrade`
```bash
helm upgrade --namepace <NAMEPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab-runner
helm upgrade --namespace <NAMESPACE> -f <CONFIG_VALUES_FILE> <RELEASE-NAME> gitlab/gitlab-runner
```
Where:

View File

@ -71,6 +71,85 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users
bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
```
## Rotate Two-factor Authentication (2FA) encryption key
GitLab stores the secret data enabling 2FA to work in an encrypted database
column. The encryption key for this data is known as `otp_key_base`, and is
stored in `config/secrets.yml`.
If that file is leaked, but the individual 2FA secrets have not, it's possible
to re-encrypt those secrets with a new encryption key. This allows you to change
the leaked key without forcing all users to change their 2FA details.
First, look up the old key. This is in the `config/secrets.yml` file, but
**make sure you're working with the production section**. The line you're
interested in will look like this:
```yaml
production:
otp_key_base: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
```
Next, generate a new secret:
```
# omnibus-gitlab
sudo gitlab-rake secret
# installation from source
bundle exec rake secret RAILS_ENV=production
```
Now you need to stop the GitLab server, back up the existing secrets file and
update the database:
```
# omnibus-gitlab
sudo gitlab-ctl stop
sudo cp config/secrets.yml config/secrets.yml.bak
sudo gitlab-rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key=<old key> new_key=<new key>
# installation from source
sudo /etc/init.d/gitlab stop
cp config/secrets.yml config/secrets.yml.bak
bundle exec rake gitlab:two_factor:rotate_key:apply filename=backup.csv old_key=<old key> new_key=<new key> RAILS_ENV=production
```
The `<old key>` value can be read from `config/secrets.yml`; `<new key>` was
generated earlier. The **encrypted** values for the user 2FA secrets will be
written to the specified `filename` - you can use this to rollback in case of
error.
Finally, change `config/secrets.yml` to set `otp_key_base` to `<new key>` and
restart. Again, make sure you're operating in the **production** section.
```
# omnibus-gitlab
sudo gitlab-ctl start
# installation from source
sudo /etc/init.d/gitlab start
```
If there are any problems (perhaps using the wrong value for `old_key`), you can
restore your backup of `config/secrets.yml` and rollback the changes:
```
# omnibus-gitlab
sudo gitlab-ctl stop
sudo gitlab-rake gitlab:two_factor:rotate_key:rollback filename=backup.csv
sudo cp config/secrets.yml.bak config/secrets.yml
sudo gitlab-ctl start
# installation from source
sudo /etc/init.d/gitlab start
bundle exec rake gitlab:two_factor:rotate_key:rollback filename=backup.csv RAILS_ENV=production
cp config/secrets.yml.bak config/secrets.yml
sudo /etc/init.d/gitlab start
```
## Clear authentication tokens for all users. Important! Data loss!
Clear authentication tokens for all users in the GitLab database. This

View File

@ -126,7 +126,7 @@ which visibility level you select on project settings.
## GitLab CI
GitLab CI permissions rely on the role the user has in GitLab. There are four
permission levels it total:
permission levels in total:
- admin
- master

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

View File

@ -1,10 +1,8 @@
# Project integrations
You can find the available integrations under the **Integrations** page by
navigating to the cog icon in the upper right corner of your project. You need
to have at least [master permission][permissions] on the project.
![Accessing the integrations](img/accessing_integrations.png)
You can find the available integrations under your project's
**Settings ➔ Integrations** page. You need to have at least
[master permission][permissions] on the project.
## Project services

View File

@ -6,18 +6,13 @@ functionality to GitLab.
## Accessing the project services
You can find the available services under the **Integrations** page in your
project's settings.
You can find the available services under your project's
**Settings ➔ Integrations** page.
1. Navigate to the cog icon in the upper right corner of your project. You need
to have at least [master permission][permissions] on the project.
There are more than 20 services to integrate with. Click on the one that you
want to configure.
![Accessing the services](img/accessing_integrations.png)
1. There are more than 20 services to integrate with. Click on the one that you
want to configure.
![Project services list](img/project_services.png)
![Project services list](img/project_services.png)
Below, you will find a list of the currently supported ones accompanied with
comprehensive documentation.

View File

@ -14,11 +14,8 @@ to the webhook URL.
Webhooks can be used to update an external issue tracker, trigger CI jobs,
update a backup mirror, or even deploy to your production server.
Navigate to the webhooks page by going to the **Integrations** page from your
project's settings which can be found under the wheel icon in the upper right
corner.
![Accessing the integrations](img/accessing_integrations.png)
Navigate to the webhooks page by going to your project's
**Settings ➔ Integrations**.
## Webhook endpoint tips

View File

@ -1,4 +1,4 @@
# GitLab Issues Documentation
# Issues documentation
The GitLab Issue Tracker is an advanced and complete tool
for tracking the evolution of a new idea or the process
@ -41,13 +41,13 @@ The image bellow illustrates how an issue looks like:
Learn more about it on the [GitLab Issues Functionalities documentation](issues_functionalities.md).
## New Issue
## New issue
Read through the [documentation on creating issues](create_new_issue.md).
## Closing issues
Read through the distinct ways to [close issues](closing_issues.md) on GitLab.
Learn distinct ways to [close issues](closing_issues.md) in GitLab.
## Create a merge request from an issue
@ -84,7 +84,7 @@ Learn more about them on the [issue templates documentation](../../project/descr
Learn more about [crosslinking](crosslinking_issues.md) issues and merge requests.
### GitLab Issue Board
### Issue Board
The [GitLab Issue Board](https://about.gitlab.com/features/issueboard/) is a way to
enhance your workflow by organizing and prioritizing issues in GitLab.

View File

@ -1,12 +1,7 @@
# Pipelines settings
To reach the pipelines settings:
1. Navigate to your project and click the cog icon in the upper right corner.
![Project settings menu](../img/project_settings_list.png)
1. Select **Pipelines** from the menu.
To reach the pipelines settings navigate to your project's
**Settings ➔ CI/CD Pipelines**.
The following settings can be configured per project.

View File

@ -27,11 +27,8 @@ See the [Changelog](#changelog) section for changes over time.
To protect a branch, you need to have at least Master permission level. Note
that the `master` branch is protected by default.
1. Navigate to the main page of the project.
1. In the upper right corner, click the settings wheel and select **Protected branches**.
![Project settings list](img/project_settings_list.png)
1. Navigate to your project's **Settings ➔ Repository**
1. Scroll to find the **Protected branches** section.
1. From the **Branch** dropdown menu, select the branch you want to protect and
click **Protect**. In the screenshot below, we chose the `develop` branch.

View File

@ -5,7 +5,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'I click link "Fork"' do
expect(page).to have_content "Shop"
click_link "Fork project"
click_link "Fork"
end
step 'I am a member of project "Shop"' do

View File

@ -94,6 +94,7 @@ module API
mount ::API::DeployKeys
mount ::API::Deployments
mount ::API::Environments
mount ::API::Events
mount ::API::Features
mount ::API::Files
mount ::API::Groups

86
lib/api/events.rb Normal file
View File

@ -0,0 +1,86 @@
module API
class Events < Grape::API
include PaginationParams
helpers do
params :event_filter_params do
optional :action, type: String, values: Event.actions, desc: 'Event action to filter on'
optional :target_type, type: String, values: Event.target_types, desc: 'Event target type to filter on'
optional :before, type: Date, desc: 'Include only events created before this date'
optional :after, type: Date, desc: 'Include only events created after this date'
end
params :sort_params do
optional :sort, type: String, values: %w[asc desc], default: 'desc',
desc: 'Return events sorted in ascending and descending order'
end
def present_events(events)
events = events.reorder(created_at: params[:sort])
present paginate(events), with: Entities::Event
end
end
resource :events do
desc "List currently authenticated user's events" do
detail 'This feature was introduced in GitLab 9.3.'
success Entities::Event
end
params do
use :pagination
use :event_filter_params
use :sort_params
end
get do
authenticate!
events = EventsFinder.new(params.merge(source: current_user, current_user: current_user)).execute.preload(:author, :target)
present_events(events)
end
end
params do
requires :id, type: String, desc: 'The ID or Username of the user'
end
resource :users do
desc 'Get the contribution events of a specified user' do
detail 'This feature was introduced in GitLab 8.13.'
success Entities::Event
end
params do
use :pagination
use :event_filter_params
use :sort_params
end
get ':id/events' do
user = find_user(params[:id])
not_found!('User') unless user
events = EventsFinder.new(params.merge(source: user, current_user: current_user)).execute.preload(:author, :target)
present_events(events)
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc "List a Project's visible events" do
success Entities::Event
end
params do
use :pagination
use :event_filter_params
use :sort_params
end
get ":id/events" do
events = EventsFinder.new(params.merge(source: user_project, current_user: current_user)).execute.preload(:author, :target)
present_events(events)
end
end
end
end

View File

@ -10,7 +10,8 @@ module API
file_content: attrs[:content],
file_content_encoding: attrs[:encoding],
author_email: attrs[:author_email],
author_name: attrs[:author_name]
author_name: attrs[:author_name],
last_commit_sha: attrs[:last_commit_id]
}
end
@ -46,6 +47,7 @@ module API
use :simple_file_params
requires :content, type: String, desc: 'File content'
optional :encoding, type: String, values: %w[base64], desc: 'File encoding'
optional :last_commit_id, type: String, desc: 'Last known commit id for this file'
end
end
@ -111,7 +113,12 @@ module API
authorize! :push_code, user_project
file_params = declared_params(include_missing: false)
result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute
begin
result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute
rescue ::Files::UpdateService::FileChangedError => e
render_api_error!(e.message, 400)
end
if result[:status] == :success
status(200)

View File

@ -167,16 +167,6 @@ module API
user_can_admin_project: can?(current_user, :admin_project, user_project), statistics: params[:statistics]
end
desc 'Get events for a single project' do
success Entities::Event
end
params do
use :pagination
end
get ":id/events" do
present paginate(user_project.events.recent), with: Entities::Event
end
desc 'Fork new project for the current user or provided namespace.' do
success Entities::Project
end

View File

@ -328,27 +328,6 @@ module API
end
end
desc 'Get the contribution events of a specified user' do
detail 'This feature was introduced in GitLab 8.13.'
success Entities::Event
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
use :pagination
end
get ':id/events' do
user = User.find_by(id: params[:id])
not_found!('User') unless user
events = user.events.
merge(ProjectsFinder.new(current_user: current_user).execute).
references(:project).
with_associations.
recent
present paginate(events), with: Entities::Event
end
params do
requires :user_id, type: Integer, desc: 'The ID of the user'
end

View File

@ -0,0 +1,87 @@
module Gitlab
# The +otp_key_base+ param is used to encrypt the User#otp_secret attribute.
#
# When +otp_key_base+ is changed, it invalidates the current encrypted values
# of User#otp_secret. This class can be used to decrypt all the values with
# the old key, encrypt them with the new key, and and update the database
# with the new values.
#
# For persistence between runs, a CSV file is used with the following columns:
#
# user_id, old_value, new_value
#
# Only the encrypted values are stored in this file.
#
# As users may have their 2FA settings changed at any time, this is only
# guaranteed to be safe if run offline.
class OtpKeyRotator
HEADERS = %w[user_id old_value new_value].freeze
attr_reader :filename
# Create a new rotator. +filename+ is used to store values by +calculate!+,
# and to update the database with new and old values in +apply!+ and
# +rollback!+, respectively.
def initialize(filename)
@filename = filename
end
def rotate!(old_key:, new_key:)
old_key ||= Gitlab::Application.secrets.otp_key_base
raise ArgumentError.new("Old key is the same as the new key") if old_key == new_key
raise ArgumentError.new("New key is too short! Must be 256 bits") if new_key.size < 64
write_csv do |csv|
ActiveRecord::Base.transaction do
User.with_two_factor.in_batches do |relation|
rows = relation.pluck(:id, :encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt)
rows.each do |row|
user = %i[id ciphertext iv salt].zip(row).to_h
new_value = reencrypt(user, old_key, new_key)
User.where(id: user[:id]).update_all(encrypted_otp_secret: new_value)
csv << [user[:id], user[:ciphertext], new_value]
end
end
end
end
end
def rollback!
ActiveRecord::Base.transaction do
CSV.foreach(filename, headers: HEADERS, return_headers: false) do |row|
User.where(id: row['user_id']).update_all(encrypted_otp_secret: row['old_value'])
end
end
end
private
attr_reader :old_key, :new_key
def otp_secret_settings
@otp_secret_settings ||= User.encrypted_attributes[:otp_secret]
end
def reencrypt(user, old_key, new_key)
original = user[:ciphertext].unpack("m").join
opts = {
iv: user[:iv].unpack("m").join,
salt: user[:salt].unpack("m").join,
algorithm: otp_secret_settings[:algorithm],
insecure_mode: otp_secret_settings[:insecure_mode]
}
decrypted = Encryptor.decrypt(original, opts.merge(key: old_key))
encrypted = Encryptor.encrypt(decrypted, opts.merge(key: new_key))
[encrypted].pack("m")
end
def write_csv(&blk)
File.open(filename, "w") do |file|
yield CSV.new(file, headers: HEADERS, write_headers: false)
end
end
end
end

View File

@ -19,5 +19,21 @@ namespace :gitlab do
puts "There are currently no users with 2FA enabled.".color(:yellow)
end
end
namespace :rotate_key do
def rotator
@rotator ||= Gitlab::OtpKeyRotator.new(ENV['filename'])
end
desc "Encrypt user OTP secrets with a new encryption key"
task apply: :environment do |t, args|
rotator.rotate!(old_key: ENV['old_key'], new_key: ENV['new_key'])
end
desc "Rollback to secrets encrypted with the old encryption key"
task rollback: :environment do
rotator.rollback!
end
end
end
end

View File

@ -18,13 +18,13 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'can bulk assign' do
before do
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
context 'a label' do
context 'to all issues' do
before do
check 'check_all_issues'
check 'check-all-issues'
open_labels_dropdown ['bug']
update_issues
end
@ -52,7 +52,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'multiple labels' do
context 'to all issues' do
before do
check 'check_all_issues'
check 'check-all-issues'
open_labels_dropdown %w(bug feature)
update_issues
end
@ -86,9 +86,10 @@ feature 'Issues > Labels bulk assignment', feature: true do
before do
issue2.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
check 'check_all_issues'
enable_bulk_update
check 'check-all-issues'
open_labels_dropdown ['bug']
update_issues
end
@ -107,9 +108,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue2.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
check 'check_all_issues'
enable_bulk_update
check 'check-all-issues'
unmark_labels_in_dropdown %w(bug feature)
update_issues
end
@ -127,8 +127,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue1.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
check_issue issue1
unmark_labels_in_dropdown ['bug']
update_issues
@ -147,8 +146,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue2.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
check_issue issue1
check_issue issue2
unmark_labels_in_dropdown ['bug']
@ -171,14 +169,15 @@ feature 'Issues > Labels bulk assignment', feature: true do
before do
issue1.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
it 'keeps labels' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
check 'check_all_issues'
check 'check-all-issues'
open_milestone_dropdown(['First Release'])
update_issues
@ -192,14 +191,13 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'setting a milestone and adding another label' do
before do
issue1.labels << bug
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
it 'keeps existing label and new label is present' do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
check 'check_all_issues'
check 'check-all-issues'
open_milestone_dropdown ['First Release']
open_labels_dropdown ['feature']
update_issues
@ -218,7 +216,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue1.labels << feature
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
it 'keeps existing label and new label is present' do
@ -226,7 +224,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
expect(find("#issue_#{issue1.id}")).to have_content 'bug'
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
check 'check_all_issues'
check 'check-all-issues'
open_milestone_dropdown ['First Release']
unmark_labels_in_dropdown ['feature']
update_issues
@ -248,7 +247,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue1.labels << bug
issue2.labels << feature
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
it 'keeps labels' do
@ -257,7 +256,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
expect(find("#issue_#{issue2.id}")).to have_content 'feature'
expect(find("#issue_#{issue2.id}")).to have_content 'First Release'
check 'check_all_issues'
check 'check-all-issues'
open_milestone_dropdown(['No Milestone'])
update_issues
@ -272,8 +271,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'toggling checked issues' do
before do
issue1.labels << bug
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
it do
@ -298,14 +296,14 @@ feature 'Issues > Labels bulk assignment', feature: true do
issue1.labels << feature
issue2.labels << bug
visit namespace_project_issues_path(project.namespace, project)
enable_bulk_update
end
it 'applies label from filtered results' do
check 'check_all_issues'
check 'check-all-issues'
page.within('.issues_bulk_update') do
click_button 'Labels'
page.within('.issues-bulk-update') do
click_button 'Select labels'
wait_for_requests
expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active')
@ -340,15 +338,16 @@ feature 'Issues > Labels bulk assignment', feature: true do
context 'cannot bulk assign labels' do
it do
expect(page).not_to have_css '.check_all_issues'
expect(page).not_to have_button 'Edit Issues'
expect(page).not_to have_css '.check-all-issues'
expect(page).not_to have_css '.issue-check'
end
end
end
def open_milestone_dropdown(items = [])
page.within('.issues_bulk_update') do
click_button 'Milestone'
page.within('.issues-bulk-update') do
click_button 'Select milestone'
wait_for_requests
items.map do |item|
click_link item
@ -357,8 +356,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
end
def open_labels_dropdown(items = [], unmark = false)
page.within('.issues_bulk_update') do
click_button 'Labels'
page.within('.issues-bulk-update') do
click_button 'Select labels'
wait_for_requests
items.map do |item|
click_link item
@ -391,7 +390,12 @@ feature 'Issues > Labels bulk assignment', feature: true do
end
def update_issues
click_button 'Update issues'
click_button 'Update all'
wait_for_requests
end
def enable_bulk_update
visit namespace_project_issues_path(project.namespace, project)
click_button 'Edit Issues'
end
end

View File

@ -14,7 +14,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
it 'sets to closed' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
click_button 'Edit Issues'
find('#check-all-issues').click
find('.js-issue-status').click
find('.dropdown-menu-status a', text: 'Closed').click
@ -26,7 +27,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
create_closed
visit namespace_project_issues_path(project.namespace, project, state: 'closed')
find('#check_all_issues').click
click_button 'Edit Issues'
find('#check-all-issues').click
find('.js-issue-status').click
find('.dropdown-menu-status a', text: 'Open').click
@ -39,7 +41,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
it 'updates to current user' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
click_button 'Edit Issues'
find('#check-all-issues').click
click_update_assignee_button
find('.dropdown-menu-user-link', text: user.username).click
@ -54,7 +57,8 @@ feature 'Multiple issue updating from issues#index', feature: true do
create_assigned
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
click_button 'Edit Issues'
find('#check-all-issues').click
click_update_assignee_button
click_link 'Unassigned'
@ -69,8 +73,9 @@ feature 'Multiple issue updating from issues#index', feature: true do
it 'updates milestone' do
visit namespace_project_issues_path(project.namespace, project)
find('#check_all_issues').click
find('.issues_bulk_update .js-milestone-select').click
click_button 'Edit Issues'
find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: milestone.title).click
click_update_issues_button
@ -84,8 +89,9 @@ feature 'Multiple issue updating from issues#index', feature: true do
expect(first('.issue')).to have_content milestone.title
find('#check_all_issues').click
find('.issues_bulk_update .js-milestone-select').click
click_button 'Edit Issues'
find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: "No Milestone").click
click_update_issues_button
@ -112,7 +118,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
end
def click_update_issues_button
find('.update_selected_issues').click
find('.update-selected-issues').click
wait_for_requests
end
end

View File

@ -98,14 +98,16 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t
end
def change_status(text)
find('#check_all_issues').click
click_button 'Edit Merge Requests'
find('#check-all-issues').click
find('.js-issue-status').click
find('.dropdown-menu-status a', text: text).click
click_update_merge_requests_button
end
def change_assignee(text)
find('#check_all_issues').click
click_button 'Edit Merge Requests'
find('#check-all-issues').click
find('.js-update-assignee').click
wait_for_requests
@ -117,14 +119,15 @@ feature 'Multiple merge requests updating from merge_requests#index', feature: t
end
def change_milestone(text)
find('#check_all_issues').click
find('.issues_bulk_update .js-milestone-select').click
click_button 'Edit Merge Requests'
find('#check-all-issues').click
find('.issues-bulk-update .js-milestone-select').click
find('.dropdown-menu-milestone a', text: text).click
click_update_merge_requests_button
end
def click_update_merge_requests_button
find('.update_selected_issues').click
find('.update-selected-issues').click
wait_for_requests
end
end

View File

@ -31,7 +31,7 @@ feature 'Environments page', :feature, :js do
it 'should show one environment' do
visit namespace_project_environments_path(project.namespace, project, scope: 'available')
expect(page).to have_css('.environments-container')
expect(page.all('tbody > tr').length).to eq(1)
expect(page.all('.environment-name').length).to eq(1)
end
end
@ -59,7 +59,7 @@ feature 'Environments page', :feature, :js do
it 'should show one environment' do
visit namespace_project_environments_path(project.namespace, project, scope: 'stopped')
expect(page).to have_css('.environments-container')
expect(page.all('tbody > tr').length).to eq(1)
expect(page.all('.environment-name').length).to eq(1)
end
end
end

View File

@ -14,7 +14,7 @@ feature 'Visibility settings', feature: true, js: true do
visibility_select_container = find('.js-visibility-select')
expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s
expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.'
expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
end
scenario 'project visibility description updates on change' do
@ -41,7 +41,7 @@ feature 'Visibility settings', feature: true, js: true do
expect(visibility_select_container).not_to have_select '.visibility-select'
expect(visibility_select_container).to have_content 'Public'
expect(visibility_select_container).to have_content 'The project can be cloned without any authentication.'
expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
end
end
end

View File

@ -24,8 +24,8 @@ describe 'Unsubscribe links', feature: true do
visit body_link
expect(current_path).to eq unsubscribe_sent_notification_path(SentNotification.last)
expect(page).to have_text(%(Unsubscribe from issue #{issue.title} (#{issue.to_reference})))
expect(page).to have_text(%(Are you sure you want to unsubscribe from issue #{issue.title} (#{issue.to_reference})?))
expect(page).to have_text(%(Unsubscribe from issue))
expect(page).to have_text(%(Are you sure you want to unsubscribe from the issue: #{issue.title} (#{issue.to_reference})?))
expect(issue.subscribed?(recipient, project)).to be_truthy
click_link 'Unsubscribe'

View File

@ -0,0 +1,44 @@
require 'spec_helper'
describe EventsFinder do
let(:user) { create(:user) }
let(:other_user) { create(:user) }
let(:project1) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
let(:project2) { create(:empty_project, :private, creator_id: user.id, namespace: user.namespace) }
let(:closed_issue) { create(:closed_issue, project: project1, author: user) }
let(:opened_merge_request) { create(:merge_request, source_project: project2, author: user) }
let!(:closed_issue_event) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
let!(:opened_merge_request_event) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 1, 31)) }
let(:closed_issue2) { create(:closed_issue, project: project1, author: user) }
let(:opened_merge_request2) { create(:merge_request, source_project: project2, author: user) }
let!(:closed_issue_event2) { create(:event, project: project1, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 2, 2)) }
let!(:opened_merge_request_event2) { create(:event, project: project2, author: user, target: opened_merge_request, action: Event::CREATED, created_at: Date.new(2017, 2, 2)) }
context 'when targeting a user' do
it 'returns events between specified dates filtered on action and type' do
events = described_class.new(source: user, current_user: user, action: 'created', target_type: 'merge_request', after: Date.new(2017, 1, 1), before: Date.new(2017, 2, 1)).execute
expect(events).to eq([opened_merge_request_event])
end
it 'does not return events the current_user does not have access to' do
events = described_class.new(source: user, current_user: other_user).execute
expect(events).not_to include(opened_merge_request_event)
end
end
context 'when targeting a project' do
it 'returns project events between specified dates filtered on action and type' do
events = described_class.new(source: project1, current_user: user, action: 'closed', target_type: 'issue', after: Date.new(2016, 12, 1), before: Date.new(2017, 1, 1)).execute
expect(events).to eq([closed_issue_event])
end
it 'does not return events the current_user does not have access to' do
events = described_class.new(source: project2, current_user: other_user).execute
expect(events).to be_empty
end
end
end

View File

@ -257,7 +257,7 @@ describe ProjectsHelper do
result = helper.project_feature_access_select(:issues_access_level)
expect(result).to include("Disabled")
expect(result).to include("Only team members")
expect(result).not_to include("Everyone with access")
expect(result).to have_selector('option[disabled]', text: "Everyone with access")
end
end
@ -272,7 +272,7 @@ describe ProjectsHelper do
expect(result).to include("Disabled")
expect(result).to include("Only team members")
expect(result).not_to include("Everyone with access")
expect(result).to have_selector('option[disabled]', text: "Everyone with access")
expect(result).to have_selector('option[selected]', text: "Only team members")
end
end

View File

@ -37,7 +37,7 @@ describe VisibilityLevelHelper do
it "describes public projects" do
expect(project_visibility_level_description(Gitlab::VisibilityLevel::PUBLIC))
.to eq "The project can be cloned without any authentication."
.to eq "The project can be accessed without any authentication."
end
end

View File

@ -71,7 +71,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
it('should render a table with the received pipelines', (done) => {
setTimeout(() => {
expect(this.component.$el.querySelectorAll('table > tbody > tr').length).toEqual(1);
expect(this.component.$el.querySelectorAll('.ci-table .commit').length).toEqual(1);
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.empty-state')).toBe(null);
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBe(null);
@ -108,7 +108,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
expect(this.component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
expect(this.component.$el.querySelector('.realtime-loading')).toBe(null);
expect(this.component.$el.querySelector('.js-empty-state')).toBe(null);
expect(this.component.$el.querySelector('table')).toBe(null);
expect(this.component.$el.querySelector('.ci-table')).toBe(null);
done();
}, 0);
});

View File

@ -271,7 +271,7 @@ describe('Environment', () => {
// wait for next async request
setTimeout(() => {
expect(component.$el.querySelectorAll('.js-child-row').length).toEqual(1);
expect(component.$el.querySelector('td.text-center > a.btn').textContent).toContain('Show all');
expect(component.$el.querySelector('.text-center > a.btn').textContent).toContain('Show all');
Vue.http.interceptors = _.without(Vue.http.interceptors, folderInterceptor);
done();

View File

@ -29,6 +29,6 @@ describe('Environment item', () => {
},
}).$mount();
expect(component.$el.tagName).toEqual('TABLE');
expect(component.$el.getAttribute('class')).toContain('ci-table');
});
});

View File

@ -1,6 +1,6 @@
%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'}
%input{id: 'utf8', name: 'utf8', value: '✓'}
%input{id: 'check_all_issues', name: 'check_all_issues'}
%input{id: 'check-all-issues', name: 'check-all-issues'}
%input{id: 'search', name: 'search'}
%input{id: 'author_id', name: 'author_id'}
%input{id: 'assignee_id', name: 'assignee_id'}

View File

@ -1,7 +1,7 @@
/* global Issuable */
/* global IssuableIndex */
import '~/lib/utils/url_utility';
import '~/issuable';
import '~/issuable_index';
(() => {
const BASE_URL = '/user/project/issues?scope=all&state=closed';
@ -24,11 +24,11 @@ import '~/issuable';
beforeEach(() => {
loadFixtures('static/issuable_filter.html.raw');
Issuable.init();
IssuableIndex.init();
});
it('should be defined', () => {
expect(window.Issuable).toBeDefined();
expect(window.IssuableIndex).toBeDefined();
});
describe('filtering', () => {
@ -43,7 +43,7 @@ import '~/issuable';
it('should contain only the default parameters', () => {
spyOn(gl.utils, 'visitUrl');
Issuable.filterResults($filtersForm);
IssuableIndex.filterResults($filtersForm);
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + DEFAULT_PARAMS);
});
@ -52,7 +52,7 @@ import '~/issuable';
spyOn(gl.utils, 'visitUrl');
updateForm({ search: 'broken' }, $filtersForm);
Issuable.filterResults($filtersForm);
IssuableIndex.filterResults($filtersForm);
const params = `${DEFAULT_PARAMS}&search=broken`;
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
@ -64,14 +64,14 @@ import '~/issuable';
// initial filter
updateForm({ milestone_title: 'v1.0' }, $filtersForm);
Issuable.filterResults($filtersForm);
IssuableIndex.filterResults($filtersForm);
let params = `${DEFAULT_PARAMS}&milestone_title=v1.0`;
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
// update filter
updateForm({ label_name: 'Frontend' }, $filtersForm);
Issuable.filterResults($filtersForm);
IssuableIndex.filterResults($filtersForm);
params = `${DEFAULT_PARAMS}&milestone_title=v1.0&label_name=Frontend`;
expect(gl.utils.visitUrl).toHaveBeenCalledWith(BASE_URL + params);
});

View File

@ -0,0 +1,70 @@
require 'spec_helper'
describe Gitlab::OtpKeyRotator do
let(:file) { Tempfile.new("otp-key-rotator-test") }
let(:filename) { file.path }
let(:old_key) { Gitlab::Application.secrets.otp_key_base }
let(:new_key) { "00" * 32 }
let!(:users) { create_list(:user, 5, :two_factor) }
after do
file.close
file.unlink
end
def data
CSV.read(filename)
end
def build_row(user, applied = false)
[user.id.to_s, encrypt_otp(user, old_key), encrypt_otp(user, new_key)]
end
def encrypt_otp(user, key)
opts = {
value: user.otp_secret,
iv: user.encrypted_otp_secret_iv.unpack("m").join,
salt: user.encrypted_otp_secret_salt.unpack("m").join,
algorithm: 'aes-256-cbc',
insecure_mode: true,
key: key
}
[Encryptor.encrypt(opts)].pack("m")
end
subject(:rotator) { described_class.new(filename) }
describe '#rotate!' do
subject(:rotation) { rotator.rotate!(old_key: old_key, new_key: new_key) }
it 'stores the calculated values in a spreadsheet' do
rotation
expect(data).to match_array(users.map {|u| build_row(u) })
end
context 'new key is too short' do
let(:new_key) { "00" * 31 }
it { expect { rotation }.to raise_error(ArgumentError) }
end
context 'new key is the same as the old key' do
let(:new_key) { old_key }
it { expect { rotation }.to raise_error(ArgumentError) }
end
end
describe '#rollback!' do
it 'updates rows to the old value' do
file.puts("#{users[0].id},old,new")
file.close
rotator.rollback!
expect(users[0].reload.encrypted_otp_secret).to eq('old')
expect(users[1].reload.encrypted_otp_secret).not_to eq('old')
end
end
end

View File

@ -6,7 +6,7 @@ describe PagesDomain, models: true do
end
describe 'validate domain' do
subject { build(:pages_domain, domain: domain) }
subject(:pages_domain) { build(:pages_domain, domain: domain) }
context 'is unique' do
let(:domain) { 'my.domain.com' }
@ -14,36 +14,25 @@ describe PagesDomain, models: true do
it { is_expected.to validate_uniqueness_of(:domain) }
end
context 'valid domain' do
let(:domain) { 'my.domain.com' }
{
'my.domain.com' => true,
'123.456.789' => true,
'0x12345.com' => true,
'0123123' => true,
'_foo.com' => false,
'reserved.com' => false,
'a.reserved.com' => false,
nil => false
}.each do |value, validity|
context "domain #{value.inspect} validity" do
before do
allow(Settings.pages).to receive(:host).and_return('reserved.com')
end
it { is_expected.to be_valid }
end
let(:domain) { value }
context 'valid hexadecimal-looking domain' do
let(:domain) { '0x12345.com'}
it { is_expected.to be_valid }
end
context 'no domain' do
let(:domain) { nil }
it { is_expected.not_to be_valid }
end
context 'invalid domain' do
let(:domain) { '0123123' }
it { is_expected.not_to be_valid }
end
context 'domain from .example.com' do
let(:domain) { 'my.domain.com' }
before { allow(Settings.pages).to receive(:host).and_return('domain.com') }
it { is_expected.not_to be_valid }
it { expect(pages_domain.valid?).to eq(validity) }
end
end
end

Some files were not shown because too many files have changed in this diff Show More