Commit b3ec93e2 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge remote-tracking branch 'ce-com/master' into ce-to-ee

Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parents ba01638b a3b5381e
......@@ -206,7 +206,36 @@ rake config_lint: *exec
rake brakeman: *exec
rake flay: *exec
license_finder: *exec
<<<<<<< HEAD
rake downtime_check: *exec
=======
rake downtime_check:
<<: *exec
except:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
rake ee_compat_check:
<<: *exec
only:
- branches@gitlab-org/gitlab-ce
except:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
allow_failure: yes
cache:
key: "ee_compat_check_repo"
paths:
- ee_compat_check/ee-repo/
artifacts:
name: "${CI_JOB_NAME}_${CI_COMIT_REF_NAME}_${CI_COMMIT_SHA}"
when: on_failure
expire_in: 10d
paths:
- ee_compat_check/patches/*.patch
>>>>>>> ce-com/master
rake db:migrate:reset:
stage: test
......
......@@ -139,7 +139,10 @@ entry.
- Fix API group/issues default state filter. (Alexander Randa)
- Prevent builds dropdown to close when the user clicks in a build.
- Display all closed issues in “done” board list.
<<<<<<< HEAD
- added focus mode button to issue boards.
=======
>>>>>>> ce-com/master
- Remove no-new annotation from file_template_mediator.js.
- Changed dropdown style slightly.
- Change gfm textarea to use monospace font.
......
9.1.0-pre
9.2.0-pre
function BlobForkSuggestion(openButton, cancelButton, suggestionSection) {
if (openButton) {
openButton.addEventListener('click', () => {
const defaults = {
// Buttons that will show the `suggestionSections`
// has `data-fork-path`, and `data-action`
openButtons: [],
// Update the href(from `openButton` -> `data-fork-path`)
// whenever a `openButton` is clicked
forkButtons: [],
// Buttons to hide the `suggestionSections`
cancelButtons: [],
// Section to show/hide
suggestionSections: [],
// Pieces of text that need updating depending on the action, `edit`, `replace`, `delete`
actionTextPieces: [],
};
class BlobForkSuggestion {
constructor(options) {
this.elementMap = Object.assign({}, defaults, options);
this.onClickWrapper = this.onClick.bind(this);
document.addEventListener('click', this.onClickWrapper);
}
showSuggestionSection(forkPath, action = 'edit') {
[].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => {
suggestionSection.classList.remove('hidden');
});
[].forEach.call(this.elementMap.forkButtons, (forkButton) => {
forkButton.setAttribute('href', forkPath);
});
[].forEach.call(this.elementMap.actionTextPieces, (actionTextPiece) => {
// eslint-disable-next-line no-param-reassign
actionTextPiece.textContent = action;
});
}
if (cancelButton) {
cancelButton.addEventListener('click', () => {
hideSuggestionSection() {
[].forEach.call(this.elementMap.suggestionSections, (suggestionSection) => {
suggestionSection.classList.add('hidden');
});
}
onClick(e) {
const el = e.target;
if ([].includes.call(this.elementMap.openButtons, el)) {
const { forkPath, action } = el.dataset;
this.showSuggestionSection(forkPath, action);
}
if ([].includes.call(this.elementMap.cancelButtons, el)) {
this.hideSuggestionSection();
}
}
destroy() {
document.removeEventListener('click', this.onClickWrapper);
}
}
export default BlobForkSuggestion;
// ECMAScript polyfills
import 'core-js/fn/array/find';
import 'core-js/fn/array/from';
import 'core-js/fn/array/includes';
import 'core-js/fn/object/assign';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
......
......@@ -97,11 +97,13 @@ const ShortcutsBlob = require('./shortcuts_blob');
fileBlobPermalinkUrl,
});
new BlobForkSuggestion(
document.querySelector('.js-edit-blob-link-fork-toggler'),
document.querySelector('.js-cancel-fork-suggestion'),
document.querySelector('.js-file-fork-suggestion-section'),
);
new BlobForkSuggestion({
openButtons: document.querySelectorAll('.js-edit-blob-link-fork-toggler'),
forkButtons: document.querySelectorAll('.js-fork-suggestion-button'),
cancelButtons: document.querySelectorAll('.js-cancel-fork-suggestion-button'),
suggestionSections: document.querySelectorAll('.js-file-fork-suggestion-section'),
actionTextPieces: document.querySelectorAll('.js-file-fork-suggestion-section-action'),
});
}
switch (page) {
......
......@@ -170,8 +170,9 @@ class DueDateSelectors {
const $datePicker = $(this);
const calendar = new Pikaday({
field: $datePicker.get(0),
theme: 'gitlab-theme',
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: $datePicker.parent().get(0),
onSelect(dateText) {
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
......
......@@ -4,9 +4,9 @@ import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
import RollbackComponent from './environment_rollback';
import RollbackComponent from './environment_rollback.vue';
import TerminalButtonComponent from './environment_terminal_button.vue';
import MonitoringButtonComponent from './environment_monitoring';
import MonitoringButtonComponent from './environment_monitoring.vue';
import CommitComponent from '../../vue_shared/components/commit';
import eventHub from '../event_hub';
......@@ -441,6 +441,7 @@ export default {
<template>
<tr :class="{ 'js-child-row': model.isChildren }">
<td>
<<<<<<< HEAD:app/assets/javascripts/environments/components/environment_item.vue
<span
class="deploy-board-icon"
v-if="model.hasDeployBoard"
......@@ -457,6 +458,8 @@ export default {
aria-hidden="true" />
</span>
=======
>>>>>>> ce-com/master:app/assets/javascripts/environments/components/environment_item.vue
<a
v-if="!model.isFolder"
class="environment-name"
......
<script>
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
......@@ -5,7 +6,6 @@ export default {
props: {
monitoringUrl: {
type: String,
default: '',
required: true,
},
},
......@@ -15,16 +15,19 @@ export default {
return 'Monitoring';
},
},
template: `
<a
class="btn monitoring-url has-tooltip"
data-container="body"
:href="monitoringUrl"
rel="noopener noreferrer nofollow"
:title="title"
:aria-label="title">
<i class="fa fa-area-chart" aria-hidden="true"></i>
</a>
`,
};
</script>
<template>
<a
class="btn monitoring-url has-tooltip"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
:title="title"
:aria-label="title">
<i
class="fa fa-area-chart"
aria-hidden="true" />
</a>
</template>
<script>
/* global Flash */
/* eslint-disable no-new */
/**
......@@ -50,21 +51,25 @@ export default {
});
},
},
};
</script>
<template>
<button
type="button"
class="btn"
@click="onClick"
:disabled="isLoading">
template: `
<button type="button"
class="btn"
@click="onClick"
:disabled="isLoading">
<span v-if="isLastDeployment">
Re-deploy
</span>
<span v-else>
Rollback
</span>
<span v-if="isLastDeployment">
Re-deploy
</span>
<span v-else>
Rollback
</span>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
`,
};
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</button>
</template>
......@@ -3,12 +3,18 @@
* Render environments table.
*/
import EnvironmentTableRowComponent from './environment_item.vue';
<<<<<<< HEAD
import DeployBoard from './deploy_board_component';
=======
>>>>>>> ce-com/master
export default {
components: {
'environment-item': EnvironmentTableRowComponent,
<<<<<<< HEAD
DeployBoard,
=======
>>>>>>> ce-com/master
},
props: {
......@@ -40,6 +46,7 @@ export default {
required: false,
default: false,
},
<<<<<<< HEAD
toggleDeployBoard: {
type: Function,
......@@ -52,6 +59,8 @@ export default {
required: false,
default: () => ({}),
},
=======
>>>>>>> ce-com/master
},
methods: {
......@@ -92,6 +101,7 @@ export default {
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
<<<<<<< HEAD
:service="service"
:toggleDeployBoard="toggleDeployBoard"
/>
......@@ -107,6 +117,9 @@ export default {
/>
</td>
</tr>
=======
:service="service" />
>>>>>>> ce-com/master
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<tr v-if="isLoadingFolderContent">
......
......@@ -41,8 +41,9 @@
if ($issuableDueDate.length) {
calendar = new Pikaday({
field: $issuableDueDate.get(0),
theme: 'gitlab-theme',
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
container: $issuableDueDate.parent().get(0),
onSelect: function(dateText) {
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
......
......@@ -34,17 +34,6 @@ export default {
};
},
methods: {
fetch() {
this.poll.makeRequest();
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
renderResponse(res) {
const body = JSON.parse(res.body);
this.triggerAnimation(body);
......@@ -71,7 +60,17 @@ export default {
},
},
created() {
this.fetch();
if (!Visibility.hidden()) {
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
};
</script>
......
......@@ -169,7 +169,10 @@
w.gl.utils.getSelectedFragment = () => {
const selection = window.getSelection();
if (selection.rangeCount === 0) return null;
const documentFragment = selection.getRangeAt(0).cloneContents();
const documentFragment = document.createDocumentFragment();
for (let i = 0; i < selection.rangeCount; i += 1) {
documentFragment.appendChild(selection.getRangeAt(i).cloneContents());
}
if (documentFragment.textContent.length === 0) return null;
return documentFragment;
......
......@@ -18,9 +18,10 @@
const calendar = new Pikaday({
field: $input.get(0),
theme: 'gitlab-theme',
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
minDate: new Date(),
container: $input.parent().get(0),
onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
......
......@@ -57,8 +57,11 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
Shortcuts.prototype.toggleMarkdownPreview = function(e) {
// Check if short-cut was triggered while in Write Mode
if ($(e.target).hasClass('js-note-text')) {
$('.js-md-preview-button').focus();
const $target = $(e.target);
const $form = $target.closest('form');
if ($target.hasClass('js-note-text')) {
$('.js-md-preview-button', $form).focus();
}
return $(document).triggerHandler('markdown-preview:toggle', [e]);
};
......
......@@ -94,15 +94,17 @@ content on the Users#show page.
e.preventDefault();
$('.tab-pane.active').empty();
this.loadTab($(e.target).attr('href'), this.getCurrentAction());
const endpoint = $(e.target).attr('href');
this.loadTab(this.getCurrentAction(), endpoint);
}
tabShown(event) {
const $target = $(event.target);
const action = $target.data('action');
const source = $target.attr('href');
this.setTab(source, action);
return this.setCurrentAction(source, action);
const endpoint = $target.data('endpoint');
this.setTab(action, endpoint);
return this.setCurrentAction(source);
}
activateTab(action) {
......@@ -110,27 +112,27 @@ content on the Users#show page.
.tab('show');
}
setTab(source, action) {
setTab(action, endpoint) {
if (this.loaded[action]) {
return;
}
if (action === 'activity') {
this.loadActivities(source);
this.loadActivities();
}
const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
if (loadableActions.indexOf(action) > -1) {
return this.loadTab(source, action);
return this.loadTab(action, endpoint);
}
}
loadTab(source, action) {
loadTab(action, endpoint) {
return $.ajax({
beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
url: source,
url: endpoint,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
......@@ -140,7 +142,7 @@ content on the Users#show page.
});
}
loadActivities(source) {
loadActivities() {
if (this.loaded['activity']) {
return;
}
......@@ -155,7 +157,7 @@ content on the Users#show page.
.toggle(status);
}
setCurrentAction(source, action) {
setCurrentAction(source) {
let new_state = source;
new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
......
......@@ -230,7 +230,6 @@
float: right;
margin-top: 8px;
padding-bottom: 8px;
border-bottom: 1px solid $border-color;
}
}
......
......@@ -14,14 +14,32 @@
}
}
@mixin set-visible {
transform: translateY(0);
visibility: visible;
opacity: 1;
transition-duration: 100ms, 150ms, 25ms;
transition-delay: 35ms, 50ms, 25ms;
}
@mixin set-invisible {
transform: translateY(-10px);
visibility: hidden;
opacity: 0;
transition-property: opacity, transform, visibility;
transition-duration: 70ms, 250ms, 250ms;
transition-timing-function: linear, $dropdown-animation-timing;
transition-delay: 25ms, 50ms, 0ms;
}
.open {
.dropdown-menu,
.dropdown-menu-nav {
display: block;
@include set-visible;
@media (max-width: $screen-xs-max) {
width: 100%;
min-width: 240px;
}
}
......@@ -161,8 +179,9 @@
.dropdown-menu,
.dropdown-menu-nav {
display: none;
display: block;
position: absolute;
width: 100%;
top: 100%;
left: 0;
z-index: 9;
......@@ -176,6 +195,12 @@
border: 1px solid $dropdown-border-color;
border-radius: $border-radius-base;
box-shadow: 0 2px 4px $dropdown-shadow-color;
overflow: hidden;
@include set-invisible;
@media (max-width: $screen-sm-min) {
width: 100%;
}
&.is-loading {
.dropdown-content {
......@@ -252,6 +277,23 @@
}
}
.filtered-search-box-input-container .dropdown-menu,
.filtered-search-box-input-container .dropdown-menu-nav,
.comment-type-dropdown .dropdown-menu {
display: none;
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.filtered-search-box-input-container {
.dropdown-menu,
.dropdown-menu-nav {
max-width: 280px;
width: auto;
}
}
.dropdown-menu-drop-up {
top: auto;
bottom: 100%;
......@@ -326,6 +368,10 @@
.dropdown-select {
width: $dropdown-width;
@media (max-width: $screen-sm-min) {
width: 100%;
}
}
.dropdown-menu-align-right {
......@@ -568,3 +614,24 @@
.droplab-item-ignore {
pointer-events: none;
}
.pika-single.animate-picker.is-bound,
.pika-single.animate-picker.is-bound.is-hidden {
/*
* Having `!important` is not recommended but
* since `pikaday` sets positioning inline
* there's no way it can be gracefully overridden
* using config options.
*/
position: absolute !important;
display: block;
}
.pika-single.animate-picker.is-bound {
@include set-visible;
}
.pika-single.animate-picker.is-bound.is-hidden {
@include set-invisible;
overflow: hidden;
}
......@@ -329,6 +329,7 @@ header {
.header-user {
.dropdown-menu-nav {
width: auto;
min-width: 140px;
margin-top: -5px;
......
......@@ -110,7 +110,7 @@
.top-area {
@include clearfix;
border-bottom: 1px solid $white-normal;
border-bottom: 1px solid $border-color;
.nav-text {
padding-top: 16px;
......
......@@ -338,3 +338,32 @@ h4 {
.idiff.addition {
background: $line-added-dark;
}
/**
* form text input i.e. search bar, comments, forms, etc.
*/
input,
textarea {
&::-webkit-input-placeholder {
color: $placeholder-text-color;
}
// support firefox 19+ vendor prefix
&::-moz-placeholder {
color: $placeholder-text-color;
opacity: 1; // FF defaults to 0.54
}
// scss-lint:disable PseudoElement
// support Edge vendor prefix
&::-ms-input-placeholder {
color: $placeholder-text-color;
}
// scss-lint:disable PseudoElement
// support IE vendor prefix
&:-ms-input-placeholder {
color: $placeholder-text-color;
}
}
......@@ -111,6 +111,7 @@ $gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
$gl-header-color: #4c4e54;
$gl-header-nav-hover-color: #434343;
$placeholder-text-color: rgba(0, 0, 0, .42);
/*
* Lists
......@@ -564,3 +565,8 @@ $filter-name-text-color: rgba(0, 0, 0, 0.55);
$filter-value-text-color: rgba(0, 0, 0, 0.85);
$filter-name-selected-color: #ebebeb;
$filter-value-selected-color: #d7d7d7;
/*
Animation Functions
*/
$dropdown-animation-timing: cubic-bezier(0.23, 1, 0.32, 1);
......@@ -124,7 +124,13 @@ input[type="checkbox"]:hover {
// Custom dropdown positioning
.dropdown-menu {
top: 37px;
transition-property: opacity, transform;
transition-duration: 250ms, 250ms;
transition-delay: 0ms, 25ms;
transition-timing-function: $dropdown-animation-timing;
transform: translateY(0);
opacity: 0;
display: block;
left: -5px;
padding: 0;
......@@ -156,6 +162,13 @@ input[type="checkbox"]:hover {
color: $layout-link-gray;
}
}
.dropdown-menu {
transition-duration: 100ms, 75ms;
transition-delay: 75ms, 100ms;
transform: translateY(13px);
opacity: 1;
}
}
&.has-value {
......
......@@ -43,9 +43,13 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
@group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user)
status = Members::CreateService.new(@group, current_user, params).execute
redirect_to [:admin, @group], notice: 'Users were successfully added.'
if status
redirect_to [:admin, @group], notice: 'Users were successfully added.'
else
redirect_to [:admin, @group], alert: 'No users specified.'
end
end
def destroy
......
module MembershipActions
extend ActiveSupport::Concern
def create
status = Members::CreateService.new(membershipable, current_user, params).execute
redirect_url = members_page_url
if status
redirect_to redirect_url, notice: 'Users were successfully added.'
else
redirect_to redirect_url, alert: 'No users specified.'
end
end
def destroy
Members::DestroyService.new(membershipable, current_user, params).
execute(:all)
respond_to do |format|
format.html do
message = "User was successfully removed from #{source_type}."
redirect_to members_page_url, notice: message
end
format.js { head :ok }
end
end
def request_access
membershipable.request_access(current_user)
......@@ -13,14 +39,13 @@ module MembershipActions
log_audit_event(member, action: :create)
redirect_to polymorphic_url([membershipable, :members])
redirect_to members_page_url
end
def leave
member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
execute(:all)
source_type = membershipable.class.to_s.humanize(capitalize: false)
notice =
if member.request?
"Your access request to the #{source_type} has been withdrawn."
......@@ -28,8 +53,11 @@ module MembershipActions
"You left the \"#{membershipable.human_name}\" #{source_type}."
end
<<<<<<< HEAD
log_audit_event(member, action: :destroy) unless member.request?
=======
>>>>>>> ce-com/master
redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice
......@@ -41,8 +69,21 @@ module MembershipActions
raise NotImplementedError
end
<<<<<<< HEAD
def log_audit_event(member, options = {})
AuditEventService.new(current_user, membershipable, options)
.for_member(member).security_event
=======
def members_page_url
if membershipable.is_a?(Project)
project_settings_members_path(membershipable)
else
polymorphic_url([membershipable, :members])
end
end
def source_type
@source_type ||= membershipable.class.to_s.humanize(capitalize: false)
>>>>>>> ce-com/master
end
end
......@@ -24,6 +24,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member = @group.group_members.new
end
<<<<<<< HEAD
def create
if params[:user_ids].blank?
return redirect_to(group_group_members_path(@group), alert: 'No users specified.')
......@@ -45,6 +46,8 @@ class Groups::GroupMembersController < Groups::ApplicationController
redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end
=======
>>>>>>> ce-com/master
def update
@group_member = @group.group_members.find(params[:id])
......@@ -57,6 +60,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
end
<<<<<<< HEAD
def destroy
member = Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
......@@ -68,6 +72,8 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
end
=======
>>>>>>> ce-com/master
def resend_invite
redirect_path = group_group_members_path(@group)
......
......@@ -10,6 +10,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
redirect_to namespace_project_settings_members_path(@project.namespace, @project, sort: sort)
end
<<<<<<< HEAD
def create
status = Members::CreateService.new(@project, current_user, params).execute
......@@ -28,6 +29,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
end
=======
>>>>>>> ce-com/master
def update
@project_member = @project.project_members.find(params[:id])
......@@ -40,6 +43,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
end
<<<<<<< HEAD
def destroy
member = Members::DestroyService.new(@project, current_user, params).
execute(:all)
......@@ -54,6 +58,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
end
=======
>>>>>>> ce-com/master
def resend_invite
redirect_path = namespace_project_settings_members_path(@project.namespace, @project)
......
......@@ -14,15 +14,6 @@ module BlobHelper
options[:link_opts])
end
def fork_path(project = @project, ref = @ref, path = @path, options = {})
continue_params = {
to: edit_path,
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
end
def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
blob = options.delete(:blob)
blob ||= project.repository.blob_at(ref, path) rescue nil
......@@ -37,7 +28,16 @@ module BlobHelper
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
elsif current_user && can?(current_user, :fork_project, project)
button_tag 'Edit', class: "#{common_classes} js-edit-blob-link-fork-toggler"
continue_params = {
to: edit_path,
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
button_tag 'Edit',
class: "#{common_classes} js-edit-blob-link-fork-toggler",
data: { action: 'edit', fork_path: fork_path }
end
end
......@@ -48,21 +48,25 @@ module BlobHelper
return unless blob
common_classes = "btn btn-#{btn_class}"
if !on_top_of_branch?(project, ref)
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
elsif blob.lfs_pointer?
button_tag label, class: "btn btn-#{btn_class} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
button_tag label, class: "#{common_classes} disabled has-tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: request.fullpath,
to: request.fullpath,
notice: edit_in_new_fork_notice + " Try to #{action} this file again.",
notice_now: edit_in_new_fork_notice_now
}
fork_path = namespace_project_forks_path(project.namespace, project, namespace_key: current_user.namespace.id, continue: continue_params)
link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post
button_tag label,
class: "#{common_classes} js-edit-blob-link-fork-toggler",
data: { action: action, fork_path: fork_path }
end
end
......
......@@ -162,6 +162,7 @@ module IssuablesHelper
html.html_safe
end
<<<<<<< HEAD
def cached_issuables_count_for_state(issuable_type, state)
Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do
issuables_count_for_state(issuable_type, state)
......@@ -173,6 +174,10 @@ module IssuablesHelper
Rails.cache.fetch(cache_key, expires_in: 2.minutes) do
assigned_issuables_count(assignee, issuable_type, state)
end
=======
def assigned_issuables_count(issuable_type)
current_user.public_send("assigned_open_#{issuable_type}_count")
>>>>>>> ce-com/master
end
def issuable_filter_params
......@@ -196,10 +201,6 @@ module IssuablesHelper
private
def assigned_issuables_count(assignee, issuable_type, state)
assignee.public_send("assigned_#{issuable_type}").public_send(state).count
end
def sidebar_gutter_collapsed?
cookies[:collapsed_gutter] == 'true'
end
......
......@@ -5,7 +5,7 @@ module SubmoduleHelper
def submodule_links(submodule_item, ref = nil, repository = @repository)
url = repository.submodule_url_for(ref, submodule_item.path)
return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/
return url, nil unless url =~ /([^\/:]+)\/([^\/]+(?:\.git)?)\Z/
namespace = $1
project = $2
......@@ -37,14 +37,16 @@ module SubmoduleHelper
end
def self_url?(url, namespace, project)
return true if url == [Gitlab.config.gitlab.url, '/', namespace, '/',
project, '.git'].join('')
url == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
url_no_dotgit = url.chomp('.git')
return true if url_no_dotgit == [Gitlab.config.gitlab.url, '/', namespace, '/',
project].join('')
url_with_dotgit = url_no_dotgit + '.git'
url_with_dotgit == gitlab_shell.url_to_repo([namespace, '/', project].join(''))
end
def relative_self_url?(url)
# (./)?(../repo.git) || (./)?(../../project/repo.git) )
url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/
url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*(\.git)?\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*(\.git)?\z/
end
def standard_links(host, namespace, project, commit)
......
......@@ -8,6 +8,14 @@
#
# Corresponding foo_html, bar_html and baz_html fields should exist.
module CacheMarkdownField
extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output
CACHE_VERSION = 1
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
# Knows about the relationship between markdown and html field names, and
# stores the rendering contexts for the latter
class FieldData
......@@ -30,60 +38,71 @@ module CacheMarkdownField
end
end
# Dynamic registries don't really work in Rails as it's not guaranteed that
# every class will be loaded, so hardcode the list.
CACHING_CLASSES = %w[
AbuseReport
Appearance
ApplicationSetting
BroadcastMessage
Issue
Label
MergeRequest
Milestone
Namespace
Note
Project
Release
Snippet
].freeze
def self.caching_classes
CACHING_CLASSES.map(&:constantize)
end
def skip_project_check?
false
end
extend ActiveSupport::Concern
# Returns the default Banzai render context for the cached markdown field.
def banzai_render_context(field)
raise ArgumentError.new("Unknown field: #{field.inspect}") unless
cached_markdown_fields.markdown_fields.include?(field)
included do
cattr_reader :cached_markdown_fields do
FieldData.new
end
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
context = cached_markdown_fields[field].merge(project: project)
# Returns the default Banzai render context for the cached markdown field.
def banzai_render_context(field)
raise ArgumentError.new("Unknown field: #{field.inspect}") unless
cached_markdown_fields.markdown_fields.include?(field)
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
# Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project)
context = cached_markdown_fields[field].merge(project: project)
context
end
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
# Update every column in a row if any one is invalidated, as we only store
# one version per row
def refresh_markdown_cache!(do_update: false)
options = { skip_project_check: skip_project_check? }
context
end
updates = cached_markdown_fields.markdown_fields.map do |markdown_field|
[
cached_markdown_fields.html_field(markdown_field),
Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
]
end.to_h
updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION
# Allow callers to look up the cache field name, rather than hardcoding it
def markdown_cache_field_for(field)
raise ArgumentError.new("Unknown field: #{field}") unless
cached_markdown_fields.markdown_fields.include?(field)
updates.each {|html_field, data| write_attribute(html_field, data) }
cached_markdown_fields.html_field(field)
update_columns(updates) if persisted? && do_update
end
def cached_html_up_to_date?(markdown_field)
html_field = cached_markdown_fields.html_field(markdown_field)
markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false
CacheMarkdownField::CACHE_VERSION == cached_markdown_version &&
(html_changed || markdown_changed == html_changed)
end
def invalidated_markdown_cache?
cached_markdown_fields.html_fields.any? {|html_field| attribute_invalidated?(html_field) }
end
def attribute_invalidated?(attr)
__send__("#{attr}_invalidated?")
end
def cached_html_for(markdown_field)
raise ArgumentError.new("Unknown field: #{field}") unless
cached_markdown_fields.markdown_fields.include?(markdown_field)
__send__(cached_markdown_fields.html_field(markdown_field))
end
included do
cattr_reader :cached_markdown_fields do
FieldData.new
end
# Always exclude _html fields from attributes (including serialization).
......@@ -92,12 +111,16 @@ module CacheMarkdownField
def attributes
attrs = attributes_before_markdown_cache
attrs.delete('cached_markdown_version')
cached_markdown_fields.html_fields.each do |field|
attrs.delete(field)
end
attrs
end
before_save :refresh_markdown_cache!, if: :invalidated_markdown_cache?
end
class_methods do
......@@ -107,31 +130,18 @@ module CacheMarkdownField
# a corresponding _html field. Any custom rendering options may be provided
# as a context.
def cache_markdown_field(markdown_field, context = {})
raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless
CacheMarkdownField::CACHING_CLASSES.include?(self.to_s)
cached_markdown_fields[markdown_field] = context
html_field = cached_markdown_fields.html_field(markdown_field)
cache_method = "#{markdown_field}_cache_refresh".to_sym
invalidation_method = "#{html_field}_invalidated?".to_sym
define_method(cache_method) do
options = { skip_project_check: skip_project_check? }
html = Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
__send__("#{html_field}=", html)
true
end
# The HTML becomes invalid if any dependent fields change. For now, assume
# author and project invalidate the cache in all circumstances.
define_method(invalidation_method) do
changed_fields = changed_attributes.keys
invalidations = changed_fields & [markdown_field.to_s, "author", "project"]
!invalidations.empty?
invalidations = changed_fields & [markdown_field.to_s, *INVALIDATED_BY]
!invalidations.empty? || !cached_html_up_to_date?(markdown_field)
end
before_save cache_method, if: invalidation_method
end
end
end
......@@ -141,7 +141,7 @@ class Group < Namespace
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users_to_group(
GroupMember.add_users(
self,
users,
access_level,
......
......@@ -219,7 +219,7 @@ class Issue < ActiveRecord::Base
# Returns `true` if the current issue can be viewed by either a logged in User
# or an anonymous user.
def visible_to_user?(user = nil)
return false unless project.feature_available?(:issues, user)
return false unless project && project.feature_available?(:issues, user)
user ? readable_by?(user) : publicly_visible?
end
......
......@@ -154,6 +154,22 @@ class Member < ActiveRecord::Base
member
end
def add_users(source, users, access_level, current_user: nil, expires_at: nil)
return [] unless users.present?
self.transaction do
users.map do |user|
add_user(
source,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
end
def access_levels
Gitlab::Access.sym_options
end
......@@ -176,18 +192,6 @@ class Member < ActiveRecord::Base
# There is no current user for bulk actions, in which case anything is allowed
!current_user || current_user.can?(:"update_#{member.type.underscore}", member)
end
def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil)
users.each do |user|
add_user(
source,
user,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
end
def real_source_type
......
......@@ -26,18 +26,6 @@ class GroupMember < Member
Gitlab::Access.sym_options_with_owner
end
def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil)
self.transaction do
add_users_to_source(
group,
users,
access_level,
current_user: current_user,
expires_at: expires_at
)
end
end
def group
source
end
......
......@@ -17,7 +17,7 @@ class ProjectMember < Member
before_destroy :delete_member_branch_protection
class << self
# Add users to project teams with passed access option
# Add users to projects with passed access option
#
# access can be an integer representing a access code
# or symbol like :master representing role
......@@ -40,7 +40,7 @@ class ProjectMember < Member
project_ids.each do |project_id|
project = Project.find(project_id)
add_users_to_source(
add_users(
project,
users,
access_level,
......
......@@ -190,7 +190,7 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :count, to: :forks, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, to: :team
delegate :add_user, :add_users, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
delegate :empty_repo?, to: :repository
......
......@@ -50,10 +50,15 @@ class ProjectTeam
end
def add_users(users, access_level, current_user: nil, expires_at: nil)
<<<<<<< HEAD
return false if group_member_lock
ProjectMember.add_users_to_projects(
[project.id],
=======
ProjectMember.add_users(
project,
>>>>>>> ce-com/master
users,
access_level,
current_user: current_user,
......
......@@ -109,9 +109,6 @@ class User < ActiveRecord::Base
has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ProtectedBranch::PushAccessLevel
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id
has_many :assigned_issues, dependent: :nullify, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
# Issues that a user owns are expected to be moved to the "ghost" user before
# the user is destroyed. If the user owns any issues during deletion, this
# should be treated as an exceptional condition.
......@@ -921,20 +918,20 @@ class User < ActiveRecord::Base
@global_notification_setting
end
def assigned_open_merge_request_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_merge_request_count'], force: force) do
assigned_merge_requests.opened.count
def assigned_open_merge_requests_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force) do
MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
def assigned_open_issues_count(force: false)
Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force) do
assigned_issues.opened.count
IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count
end
end
def update_cache_counts
assigned_open_merge_request_count(force: true)
assigned_open_merge_requests_count(force: true)
assigned_open_issues_count(force: true)
end
......
......@@ -9,7 +9,11 @@ module Members
def execute
return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
member.destroy
Member.transaction do
unassign_issues_and_merge_requests(member)
member.destroy
end
if member.request? && member.user != user
notification_service.decline_access_request(member)
......@@ -17,5 +21,23 @@ module Members
member
end
private
def unassign_issues_and_merge_requests(member)
if member.is_a?(GroupMember)
IssuesFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
execute.
update_all(assignee_id: nil)
MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id).
execute.
update_all(assignee_id: nil)
else
project = member.source
project.issues.opened.assigned_to(member.user).update_all(assignee_id: nil)
project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
member.user.update_cache_counts
end
end
end
end
module Members
class CreateService < BaseService
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
@params = params
end
def execute
return false if params[:user_ids].blank?
project.team.add_users(
@source.add_users(
params[:user_ids].split(','),
params[:access_level],
expires_at: params[:expires_at],
......
......@@ -47,13 +47,13 @@
%li
= link_to assigned_issues_dashboard_path, title: 'Issues', aria: { label: "Issues" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('hashtag fw')
- issues_count = cached_assigned_issuables_count(current_user, :issues, :opened)
- issues_count = assigned_issuables_count(:issues)
%span.badge.issues-count{ class: ('hidden' if issues_count.zero?) }
= number_with_delimiter(issues_count)
%li
= link_to assigned_mrs_dashboard_path, title: 'Merge requests', aria: { label: "Merge requests" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= custom_icon('mr_bold')
- merge_requests_count = cached_assigned_issuables_count(current_user, :merge_requests, :opened)
- merge_requests_count = assigned_issuables_count(:merge_requests)
%span.badge.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) }
= number_with_delimiter(merge_requests_count)
%li
......
......@@ -44,7 +44,7 @@
I
%span
Issues
.badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :issues, :opened))
.badge= number_with_delimiter(assigned_issuables_count(:issues))
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
.shortcut-mappings
......@@ -53,7 +53,7 @@
M
%span
Merge Requests
.badge= number_with_delimiter(cached_assigned_issuables_count(current_user, :merge_requests, :opened))
.badge= number_with_delimiter(assigned_issuables_count(:merge_requests))
= nav_link(controller: 'dashboard/snippets') do
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
.shortcut-mappings
......
......@@ -40,12 +40,17 @@
- if current_user
= replace_blob_link
= delete_blob_link
- if current_user
.js-file-fork-suggestion-section.file-fork-suggestion.hidden
%span.file-fork-suggestion-note
You don't have permission to edit this file. Try forking this project to edit the file.
= link_to 'Fork', fork_path, method: :post, class: 'btn btn-grouped btn-inverted btn-new'
%button.js-cancel-fork-suggestion.btn.btn-grouped{ type: 'button' }
You're not allowed to
%span.js-file-fork-suggestion-section-action
edit
files in this project directly. Please fork this project,
make your changes there, and submit a merge request.
= link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button btn btn-grouped btn-inverted btn-new'
%button.js-cancel-fork-suggestion-button.btn.btn-grouped{ type: 'button' }
Cancel
- if license_allows_file_locks?
......
......@@ -5,7 +5,7 @@
%div{ class: container_class }
%h3.page-title
Edit Milestone #{@milestone.to_reference}
Edit Milestone
%hr
......
......@@ -17,7 +17,7 @@
.header-text-content
%span.identifier
%strong
Milestone #{@milestone.to_reference}
Milestone
- if @milestone.due_date || @milestone.start_date
= milestone_date_range(@milestone)
.milestone-buttons
......
......@@ -145,10 +145,16 @@
}
});
$('#project_import_url').disable();
$('.import_git').click(function( event ) {
<<<<<<< HEAD
$projectImportUrl = $('#project_import_url')
$projectMirror = $('#project_mirror')
$projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'))
$projectMirror.attr('disabled', !$projectMirror.attr('disabled'))
=======
$projectImportUrl = $('#project_import_url');
$projectImportUrl.attr('disabled', !$projectImportUrl.attr('disabled'));
>>>>>>> ce-com/master
});
......@@ -19,7 +19,7 @@
%strong Schedule trigger (experimental)
.help-block
If checked, this trigger will be executed periodically according to cron and timezone.
= link_to icon('question-circle'), help_page_path('ci/triggers', anchor: 'schedule')
= link_to icon('question-circle'), help_page_path('ci/triggers/README', anchor: 'using-scheduled-triggers')
.form-group
= schedule_fields.label :cron, "Cron", class: "label-light"
= schedule_fields.text_field :cron, class: "form-control", title: 'Cron specification is required.', placeholder: "0 1 * * *"
......
......@@ -30,9 +30,10 @@
new Pikaday({
field: $dateField.get(0),
theme: 'gitlab-theme',
theme: 'gitlab-theme animate-picker',
format: 'yyyy-mm-dd',
minDate: new Date(),
container: $dateField.parent().get(0),
onSelect: function(dateText) {
$dateField.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
......
<svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1280 896q0 14-9 23l-320 320q-9 9-23 9-13 0-22.5-9.5t-9.5-22.5v-192h-352q-13 0-22.5-9.5t-9.5-22.5v-192q0-13 9.5-22.5t22.5-9.5h352v-192q0-14 9-23t23-9q12 0 24 10l319 319q9 9 9 23zm160 0q0-148-73-273t-198-198-273-73-273 73-198 198-73 273 73 273 198 198 273 73 273-73 198-198 73-273zm224 0q0 209-103 385.5t-279.5 279.5-385.5 103-385.5-103-279.5-279.5-103-385.5 103-385.5 279.5-279.5 385.5-103 385.5 103 279.5 279.5 103 385.5z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14"><g fill-rule="evenodd"><path fill-rule="nonzero" d="m0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7m1 0c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6"/><path d="m7 6h-2.702c-.154 0-.298.132-.298.295v1.41c0 .164.133.295.298.295h2.702v1.694c0 .18.095.209.213.09l2.539-2.568c.115-.116.118-.312 0-.432l-2.539-2.568c-.115-.116-.213-.079-.213.09v1.694"/></g></svg>
......@@ -3,10 +3,10 @@
= f.label :start_date, "Start Date", class: "control-label"
.col-sm-10
= f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date"
%a.inline.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
%a.inline.pull-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
.col-md-6
.form-group
= f.label :due_date, "Due Date", class: "control-label"
.col-sm-10
= f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date"
%a.inline.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
%a.inline.pull-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
......@@ -84,19 +84,19 @@
.fade-right= icon('angle-right')
%ul.nav-links.center.user-profile-nav.scrolling-tabs
%li.js-activity-tab
= link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
= link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do
Activity
%li.js-groups-tab
= link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
= link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do
Groups
%li.js-contributed-tab
= link_to user_contributed_projects_path, data: {target: 'div#contributed', action: 'contributed', toggle: 'tab'} do
= link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do
Contributed projects
%li.js-projects-tab
= link_to user_projects_path, data: {target: 'div#projects', action: 'projects', toggle: 'tab'} do
= link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do
Personal projects
%li.js-snippets-tab
= link_to user_snippets_path, data: {target: 'div#snippets', action: 'snippets', toggle: 'tab'} do
= link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do
Snippets
%div{ class: container_class }
......
# This worker clears all cache fields in the database, working in batches.
class ClearDatabaseCacheWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
BATCH_SIZE = 1000
def perform
CacheMarkdownField.caching_classes.each do |kls|
fields = kls.cached_markdown_fields.html_fields
clear_cache_fields = fields.each_with_object({}) do |field, memo|
memo[field] = nil
end
Rails.logger.debug("Clearing Markdown cache for #{kls}: #{fields.inspect}")
kls.unscoped.in_batches(of: BATCH_SIZE) do |relation|
relation.update_all(clear_cache_fields)
end
end
nil
end
end
---
title: Add animations to all the dropdowns
merge_request: 8419
author:
---
title: Replace rake cache:clear:db with an automatic mechanism
merge_request: 10597
author:
---
title: fix inline diff copy in firefox
merge_request:
author:
---
title: Refactor add_users method for project and group
merge_request: 10850
author:
---
title: Refactor Admin::GroupsController#members_update method and add some specs
merge_request: 10735
author:
---
title: Refactor code that creates project/group members
merge_request: 10735
author:
---
title: Prevent user profile tabs to display raw json when going back and forward in
browser history
merge_request:
author:
---
title: Fixued preview shortcut focusing wrong preview tab
merge_request:
author:
---
title: Removed the milestone references from the milestone views
merge_request:
author:
---
title: 'repository browser: handle submodule urls that don''t end with .git'
merge_request:
author: David Turner
---
title: Unassign all Issues and Merge Requests when member leaves a team
merge_request:
author:
......@@ -34,7 +34,6 @@
- [repository_fork, 1]
- [repository_import, 1]
- [project_service, 1]
- [clear_database_cache, 1]
- [delete_user, 1]
- [delete_merged_branches, 1]
- [authorized_projects, 1]
......
class AddVersionFieldToMarkdownCache < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
%i[
abuse_reports
appearances
application_settings
broadcast_messages
issues
labels
merge_requests
milestones
namespaces
notes
projects
releases
snippets
].each do |table|
add_column table, :cached_markdown_version, :integer, limit: 4
end
end
end
......@@ -24,6 +24,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.datetime "created_at"
t.datetime "updated_at"
t.text "message_html"
t.integer "cached_markdown_version"
end
create_table "appearances", force: :cascade do |t|
......@@ -35,6 +36,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.datetime "updated_at"
t.string "header_logo"
t.text "description_html"
t.integer "cached_markdown_version"
end
create_table "application_settings", force: :cascade do |t|
......@@ -121,6 +123,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.integer "unique_ips_limit_per_user"
t.integer "unique_ips_limit_time_window"
t.boolean "unique_ips_limit_enabled", default: false, null: false
<<<<<<< HEAD
t.integer "minimum_mirror_sync_time", default: 15, null: false
t.string "default_artifacts_expire_in", default: "0", null: false
t.string "elasticsearch_url", default: "http://localhost:9200"
......@@ -129,6 +132,11 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.string "elasticsearch_aws_access_key"
t.string "elasticsearch_aws_secret_access_key"
t.integer "geo_status_timeout", default: 10
=======
t.decimal "polling_interval_multiplier", default: 1.0, null: false
t.integer "cached_markdown_version"
t.boolean "usage_ping_enabled", default: true, null: false
>>>>>>> ce-com/master
t.string "uuid"
t.decimal "polling_interval_multiplier", default: 1.0, null: false
t.boolean "elasticsearch_experimental_indexer"
......@@ -209,6 +217,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.string "color"
t.string "font"
t.text "message_html"
t.integer "cached_markdown_version"
end
create_table "chat_names", force: :cascade do |t|
......@@ -566,7 +575,11 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.integer "time_estimate"
t.integer "relative_position"
t.datetime "closed_at"
<<<<<<< HEAD
t.string "service_desk_reply_to"
=======
t.integer "cached_markdown_version"
>>>>>>> ce-com/master
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
......@@ -631,6 +644,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.text "description_html"
t.string "type"
t.integer "group_id"
t.integer "cached_markdown_version"
end
add_index "labels", ["group_id", "project_id", "title"], name: "index_labels_on_group_id_and_project_id_and_title", unique: true, using: :btree
......@@ -770,7 +784,11 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.text "title_html"
t.text "description_html"
t.integer "time_estimate"
<<<<<<< HEAD
t.boolean "squash", default: false, null: false
=======
t.integer "cached_markdown_version"
>>>>>>> ce-com/master
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
......@@ -808,6 +826,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.text "title_html"
t.text "description_html"
t.date "start_date"
t.integer "cached_markdown_version"
end
add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
......@@ -848,8 +867,12 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.integer "parent_id"
t.boolean "require_two_factor_authentication", default: false, null: false
t.integer "two_factor_grace_period", default: 48, null: false
<<<<<<< HEAD
t.integer "shared_runners_minutes_limit"
t.integer "repository_size_limit", limit: 8
=======
t.integer "cached_markdown_version"
>>>>>>> ce-com/master
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
......@@ -886,6 +909,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.integer "resolved_by_id"
t.string "discussion_id"
t.text "note_html"
t.integer "cached_markdown_version"
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
......@@ -1110,7 +1134,11 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.integer "sync_time", default: 60, null: false
t.boolean "printing_merge_request_link_enabled", default: true, null: false
t.string "import_jid"
<<<<<<< HEAD
t.boolean "service_desk_enabled"
=======
t.integer "cached_markdown_version"
>>>>>>> ce-com/master
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......@@ -1209,6 +1237,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.datetime "created_at"
t.datetime "updated_at"
t.text "description_html"
t.integer "cached_markdown_version"
end
add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
......@@ -1300,6 +1329,7 @@ ActiveRecord::Schema.define(version: 20170421113144) do
t.integer "visibility_level", default: 0, null: false
t.text "title_html"
t.text "content_html"
t.integer "cached_markdown_version"
end
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
......
# PlantUML & GitLab
> [Introduced][ce-7810] in GitLab 8.16.
> [Introduced][ce-8537] in GitLab 8.16.
When [PlantUML](http://plantuml.com) integration is enabled and configured in
GitLab we are able to create simple diagrams in AsciiDoc and Markdown documents
......@@ -28,7 +28,7 @@ using Tomcat:
sudo apt-get install tomcat7
sudo cp target/plantuml.war /var/lib/tomcat7/webapps/plantuml.war
sudo chown tomcat7:tomcat7 /var/lib/tomcat7/webapps/plantuml.war
sudo service restart tomcat7
sudo service tomcat7 restart
```
Once the Tomcat service restarts the PlantUML service will be ready and
......@@ -93,3 +93,5 @@ Some parameters can be added to the AsciiDoc block definition:
- *height*: Height attribute added to the img tag.
Markdown does not support any parameters and will always use PNG format.
[ce-8537]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8537
\ No newline at end of file
......@@ -1029,7 +1029,7 @@ Parameters:
| `from` | string | no | Date string in the format YEAR-MONTH-DAY, e.g. `2016-03-11`. Defaults to 6 months ago. |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/user/activities
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/user/activities
```
Example response:
......
# Technical Articles
[Technical Articles](../development/writing_documentation.md#technical-articles) are
topic-related documentation, written with an user-friendly approach and language, aiming
to provide the community with guidance on specific processes to achieve certain objectives.
They are written by members of the GitLab Team and by
[Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
## GitLab Pages
- **GitLab Pages from A to Z**
- [Part 1: Static sites and GitLab Pages domains](../user/project/pages/getting_started_part_one.md)
- [Part 2: Quick start guide - Setting up GitLab Pages](../user/project/pages/getting_started_part_two.md)
- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](../user/project/pages/getting_started_part_three.md)
- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](../user/project/pages/getting_started_part_four.md)
......@@ -35,6 +35,7 @@ enable [Kubernetes service][kubernetes-service].
1. Test your deployment configuration using a [Review App][review-app] that was
created automatically for you.
<<<<<<< HEAD
## Using the Kubernetes deploy example project with Nginx
The Autodeploy templates are based on the [kubernetes-deploy][kube-deploy]
......@@ -114,6 +115,8 @@ Next, we replace `__CI_ENVIRONMENT_SLUG__` with the content of the
Finally, the Nginx pod is created from the definition of the
`nginx-deployment.yaml` file.
=======
>>>>>>> ce-com/master
## Private Project Support
> Experimental support [introduced][mr-2] in GitLab 9.1.
......@@ -146,7 +149,10 @@ PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRE
[kubernetes-service]: ../../user/project/integrations/kubernetes.md
[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
[review-app]: ../review_apps/index.md
<<<<<<< HEAD
[kube-image]: https://gitlab.com/gitlab-examples/kubernetes-deploy/container_registry "Kubernetes deploy Container Registry"
[kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project"
=======
>>>>>>> ce-com/master
[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html
[postgresql]: https://www.postgresql.org/
......@@ -227,3 +227,31 @@ branch of project with ID `9` every night at `00:30`:
```
[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
## Using scheduled triggers
> [Introduced][ci-10533] in GitLab CE 9.1 as experimental.
In order to schedule a trigger, navigate to your project's **Settings ➔ CI/CD Pipelines ➔ Triggers** and edit an existing trigger token.
![Triggers Schedule edit](img/trigger_schedule_edit.png)
To set up a scheduled trigger:
1. Check the **Schedule trigger (experimental)** checkbox
1. Enter a cron value for the frequency of the trigger ([learn more about cron notation](http://www.nncron.ru/help/EN/working/cron-format.htm))
1. Enter the timezone of the cron trigger ([see a list of timezones](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones))
1. Enter the branch or tag that the trigger will target
1. Hit **Save trigger** for the changes to take effect
![Triggers Schedule create](img/trigger_schedule_create.png)
You can check a next execution date of the scheduled trigger, which is automatically calculated by a server.
![Triggers Schedule create](img/trigger_schedule_updated_next_run_at.png)
> **Notes**:
- Those triggers won't be executed precicely. Because scheduled triggers are handled by Sidekiq, which runs according to its interval. For exmaple, if you set a trigger to be executed every minute (`* * * * *`) and the Sidekiq worker performs 00:00 and 12:00 o'clock every day (`0 */12 * * *`), then your trigger will be executed only 00:00 and 12:00 o'clock every day. To change the Sidekiq worker's frequency, you have to edit the `trigger_schedule_worker` value in `config/gitlab.yml` and restart GitLab. The Sidekiq worker's configuration on GiLab.com is able to be looked up at [here](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example#L185).
- Cron notation is parsed by [Rufus-Scheduler](https://github.com/jmettraux/rufus-scheduler).
[ci-10533]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533
......@@ -29,7 +29,8 @@ The table below shows what kind of documentation goes where.
| `doc/legal/` | Legal documents about contributing to GitLab. |
| `doc/install/`| Probably the most visited directory, since `installation.md` is there. Ideally this should go under `doc/administration/`, but it's best to leave it as-is in order to avoid confusion (still debated though). |
| `doc/update/` | Same with `doc/install/`. Should be under `administration/`, but this is a well known location, better leave as-is, at least for now. |
| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`); Technical Articles: user guides, admin guides, technical overviews, tutorials (`doc/topics/topic-name/`). |
| `doc/topics/` | Indexes per Topic (`doc/topics/topic-name/index.md`): all resources for that topic (user and admin documentation, articles, and third-party docs) |
| `doc/articles/` | [Technical Articles](writing_documentation.md#technical-articles): user guides, admin guides, technical overviews, tutorials (`doc/articles/article-title/index.md`). |
---
......@@ -61,8 +62,8 @@ The table below shows what kind of documentation goes where.
located at `doc/user/admin_area/settings/visibility_and_access_controls.md`.
1. The `doc/topics/` directory holds topic-related technical content. Create
`doc/topics/topic-name/subtopic-name/index.md` when subtopics become necessary.
Note that `topics` holds the index page per topic, and technical articles. General
user- and admin- related documentation, should be placed accordingly.
General user- and admin- related documentation, should be placed accordingly.
1. For technical articles, place their images under `doc/articles/article-title/img/`.
---
......
......@@ -25,6 +25,59 @@ Working with our frontend assets requires Node (v4.3 or greater) and Yarn
For our currently-supported browsers, see our [requirements][requirements].
---
## Development Process
When you are assigned an issue please follow the next steps:
### Divide a big feature into small Merge Requests
1. Big Merge Request are painful to review. In order to make this process easier we
must break a big feature into smaller ones and create a Merge Request for each step.
1. First step is to create a branch from `master`, let's call it `new-feature`. This branch
will be the recipient of all the smaller Merge Requests. Only this one will be merged to master.
1. Don't do any work on this one, let's keep it synced with master.
1. Create a new branch from `new-feature`, let's call it `new-feature-step-1`. We advise you
to clearly identify which step the branch represents.
1. Do the first part of the modifications in this branch. The target branch of this Merge Request
should be `new-feature`.
1. Once `new-feature-step-1` gets merged into `new-feature` we can continue our work. Create a new
branch from `new-feature`, let's call it `new-feature-step-2` and repeat the process done before.
```shell
* master
|\
| * new-feature
| |\
| | * new-feature-step-1
| |\
| | * new-feature-step-2
| |\
| | * new-feature-step-3
```
**Tips**
- Make sure `new-feature` branch is always synced with `master`: merge master frequently.
- Do the same for the feature branch you have opened. This can be accomplished by merging `master` into `new-feature` and `new-feature` into `new-feature-step-*`
- Avoid rewriting history.
### Share your work early
1. Before writing code guarantee your vision of the architecture is aligned with
GitLab's architecture.
1. Add a diagram to the issue and ask a Frontend Architecture about it.
![Diagram of Issue Boards Architecture](img/boards_diagram.png)
1. Don't take more than one week between starting work on a feature and
sharing a Merge Request with a reviewer or a maintainer.
### Vue features
1. Follow the steps in [Vue.js Best Practices](vue.md)
1. Follow the style guide.
1. Only a handful of people are allowed to merge Vue related features.
Reach out to @jschatz, @iamphill, @fatihacet or @filipa early in this process.
---
## [Architecture](architecture.md)
......
......@@ -58,7 +58,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
import Bar from './bar';
```
- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
- **Never** disable the `no-undef` rule. Declare globals with `/* global Foo */` instead.
- When declaring multiple globals, always use one `/* global [name] */` line per variable.
......@@ -71,6 +71,16 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
/* global Cookies */
/* global jQuery */
```
- Use up to 3 parameters for a function or class. If you need more accept an Object instead.
```javascript
// bad
fn(p1, p2, p3, p4) {}
// good
fn(options) {}
```
#### Modules, Imports, and Exports
- Use ES module syntax to import modules
......@@ -168,6 +178,23 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
- Avoid constructors with side-effects
- Prefer `.map`, `.reduce` or `.filter` over `.forEach`
A forEach will cause side effects, it will be mutating the array being iterated. Prefer using `.map`,
`.reduce` or `.filter`
```javascript
const users = [ { name: 'Foo' }, { name: 'Bar' } ];
// bad
users.forEach((user, index) => {
user.id = index;
});
// good
const usersWithId = users.map((user, index) => {
return Object.assign({}, user, { id: index });
});
```
#### Parse Strings into Numbers
- `parseInt()` is preferable over `Number()` or `+`
......@@ -183,6 +210,19 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
parseInt('10', 10);
```
#### CSS classes used for JavaScript
- If the class is being used in Javascript it needs to be prepend with `js-`
```html
// bad
<button class="add-user">
Add User
</button>
// good
<button class="js-add-user">
Add User
</button>
```
### Vue.js
......@@ -200,6 +240,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
#### Naming
- **Extensions**: Use `.vue` extension for Vue components.
- **Reference Naming**: Use PascalCase for Vue components and camelCase for their instances:
```javascript
// bad
import cardBoard from 'cardBoard';
......@@ -217,6 +258,7 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
cardBoard: CardBoard
};
```
- **Props Naming:**
- Avoid using DOM component prop names.
- Use kebab-case instead of camelCase to provide props in templates.
......@@ -243,12 +285,18 @@ See [our current .eslintrc][eslintrc] for specific rules and patterns.
<component v-if="bar"
param="baz" />
<button class="btn">Click me</button>
// good
<component
v-if="bar"
param="baz"
/>
<button class="btn">
Click me
</button>
// if props fit in one line then keep it on the same line
<component bar="bar" />
```
......
......@@ -26,6 +26,10 @@ browser and you will not have access to certain APIs, such as
[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
which will have to be stubbed.
### Writing tests
### Vue.js unit tests
See this [section][vue-test].
### Running frontend tests
`rake karma` runs the frontend-only (JavaScript) tests.
......@@ -134,3 +138,4 @@ Scenario: Developer can approve merge request
[jasmine-focus]: https://jasmine.github.io/2.5/focused_specs.html
[jasmine-jquery]: https://github.com/velesin/jasmine-jquery
[karma]: http://karma-runner.github.io/
[vue-test]:https://docs.gitlab.com/ce/development/fe_guide/vue.html#testing-vue-components
This diff is collapsed.
......@@ -2,7 +2,7 @@
- **General Documentation**: written by the developers responsible by creating features. Should be submitted in the same merge request containing code. Feature proposals (by GitLab contributors) should also be accompanied by its respective documentation. They can be later improved by PMs and Technical Writers.
- **Technical Articles**: written by any [GitLab Team](https://about.gitlab.com/team/) member, GitLab contributors, or [Community Writers](https://about.gitlab.com/handbook/product/technical-writing/community-writers/).
- **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs, in the same merge request containing code.
- **Indexes per topic**: initially prepared by the Technical Writing Team, and kept up-to-date by developers and PMs in the same merge request containing code. They gather all resources for that topic in a single page (user and admin documentation, articles, and third-party docs).
## Distinction between General Documentation and Technical Articles
......@@ -18,7 +18,7 @@ They are topic-related documentation, written with an user-friendly approach and
A technical article guides users and/or admins to achieve certain objectives (within guides and tutorials), or provide an overview of that particular topic or feature (within technical overviews). It can also describe the use, implementation, or integration of third-party tools with GitLab.
They live under `doc/topics/topic-name/`, and can be searched per topic, within "Indexes per Topic" pages. The topics are listed on the main [Indexes per Topic](../topics/index.md) page.
They live under `doc/articles/article-title/index.md`, and their images should be placed under `doc/articles/article-title/img/`. Find a list of existing [technical articles](../articles/index.md) here.
#### Types of Technical Articles
......
......@@ -18,10 +18,12 @@ another is through backup restore.
To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json`
(for omnibus packages) or `/home/git/gitlab/.secret` (for installations
from source). This file contains the database encryption key and CI secret
variables used for two-factor authentication. If you fail to restore this
encryption key file along with the application data backup, users with two-factor
authentication enabled will lose access to your GitLab server.
from source). This file contains the database encryption key,
[CI secret variables](../ci/variables/README.md#secret-variables), and
secret variables used for [two-factor authentication](../security/two_factor_authentication.md).
If you fail to restore this encryption key file along with the application data
backup, users with two-factor authentication enabled and GitLab Runners will
lose access to your GitLab server.
## Create a backup of the GitLab system
......
......@@ -333,7 +333,11 @@ A [platform](https://www.meteor.com) for building javascript apps.
### Milestones
<<<<<<< HEAD
Allow you to [organize issues](https://docs.gitlab.com/ce/user/project/milestones/) and merge requests in GitLab into a cohesive group, optionally setting a due date. A common use is keeping track of an upcoming software version. Milestones are created per-project.
=======
Allow you to [organize issues](../../user/project/milestones/index.md) and merge requests in GitLab into a cohesive group, optionally setting a due date. A common use is keeping track of an upcoming software version. Milestones are created per-project.
>>>>>>> ce-com/master
### Mirror Repositories
......
# Cohorts
> **Notes:**
- [Introduced][ce-23361] in GitLab 9.1.
> [Introduced][ce-23361] in GitLab 9.1.
As a benefit of having the [usage ping active](settings/usage_statistics.md),
GitLab lets you analyze the users' activities of your GitLab installation.
Under `/admin/cohorts`, when the usage ping is active, GitLab will show the
monthly cohorts of new users and their activities over time.
## Overview
How do we read the user cohorts table? Let's take an example with the following
user cohorts.
![User cohort example](img/cohorts.png)
For the cohort of June 2016, 163 users have been created on this server. One
month later, in July 2016, 155 users (or 95% of the June cohort) are still
active. Two months later, 139 users (or 85%) are still active. 9 months later,
we can see that only 6% of this cohort are still active.
For the cohort of June 2016, 163 users have been added on this server and have
been active since this month. One month later, in July 2016, out of
these 163 users, 155 users (or 95% of the June cohort) are still active. Two
months later, 139 users (or 85%) are still active. 9 months later, we can see
that only 6% of this cohort are still active.
The Inactive users column shows the number of users who have been added during
the month, but who have never actually had any activity in the instance.
How do we measure the activity of users? GitLab considers a user active if:
* the user signs in
* the user has Git activity (whether push or pull).
### Setup
## Setup
1. Activate the usage ping as defined [in the documentation](settings/usage_statistics.md)
1. [Activate the usage ping](settings/usage_statistics.md)
2. Go to `/admin/cohorts` to see the user cohorts of the server
[ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361
......@@ -7,6 +7,9 @@ project itself, the highest permission level is used.
On public and internal projects the Guest role is not enforced. All users will
be able to create issues, leave comments, and pull or download the project code.
When a member leaves the team the all assigned Issues and Merge Requests
will be unassigned automatically.
GitLab administrators receive all permissions.
To add or import a user, you can follow the [project users and members
......
......@@ -56,8 +56,12 @@ GitLab CI build environment:
- `KUBE_URL` - equal to the API URL
- `KUBE_TOKEN`
- `KUBE_NAMESPACE`
- `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path to a file containing PEM data.
- `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified.
The default value is `<project_name>-<project_id>`. You can overwrite it to
use different one if needed, otherwise the `KUBE_NAMESPACE` variable will
receive the default value.
- `KUBE_CA_PEM_FILE` - only present if a custom CA bundle was specified. Path
to a file containing PEM data.
- `KUBE_CA_PEM` (deprecated)- only if a custom CA bundle was specified. Raw PEM data.
## Web terminals
......
# Microsoft Teams Service
# Microsoft Teams service
## On Microsoft Teams
To enable Microsoft Teams integration you must create an incoming webhook integration on Microsoft Teams by following the steps described in this [document](https://msdn.microsoft.com/en-us/microsoft-teams/connectors)
To enable Microsoft Teams integration you must create an incoming webhook integration on Microsoft Teams by following the steps described in this [document](https://msdn.microsoft.com/en-us/microsoft-teams/connectors).
## On GitLab
......@@ -30,4 +30,4 @@ At the end fill in your Microsoft Teams details:
After you are all done, click **Save changes** for the changes to take effect.
![Microsoft Teams configuration](img/microsoft_teams_configuration.png)
\ No newline at end of file
![Microsoft Teams configuration](img/microsoft_teams_configuration.png)
......@@ -47,6 +47,7 @@ Click on the service links to see further configuration instructions and details
| [Kubernetes](kubernetes.md) | A containerized deployment service |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
| [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors |
| Pipelines emails | Email the pipeline status to a list of recipients |
| [Slack Notifications](slack.md) | Receive event notifications in Slack |
| [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
......
......@@ -35,6 +35,7 @@ Resolving comments prevents you from forgetting to address feedback and lets
you hide discussions that are no longer relevant.
[Read more about resolving discussion comments in merge requests reviews.](../../discussions/index.md)
<<<<<<< HEAD
## Squash and merge
......@@ -42,6 +43,8 @@ GitLab allows you to squash all changes present in a merge request into a single
commit when merging, to allow for a neater commit history.
[Learn more about squash and merge.](squash_and_merge.md)
=======
>>>>>>> ce-com/master
## Resolve conflicts
......
# Milestones
<<<<<<< HEAD
Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
A common use is keeping track of an upcoming software version. Milestones are created per-project.
......@@ -7,16 +8,37 @@ You can find the milestones page under your project's **Issues ➔ Milestones**.
## Creating a milestone
=======
Milestones allow you to organize issues and merge requests into a cohesive group,
optionally setting a due date. A common use is keeping track of an upcoming
software version. Milestones can be created per-project or per-group.
## Creating a project milestone
>**Note:**
You need [Master permissions](../../permissions.md) in order to create a milestone.
You can find the milestones page under your project's **Issues ➔ Milestones**.
>>>>>>> ce-com/master
To create a new milestone, simply click the **New milestone** button when in the
milestones page. A milestone can have a title, a description and start/due dates.
Once you fill in all the details, hit the **Create milestone** button.
<<<<<<< HEAD
>**Note:**
The start/due dates are required if you intend to use [Burndown charts](#burndown-charts).
![Creating a milestone](img/milestone_create.png)
## Groups and milestones
=======
![Creating a milestone](img/milestone_create.png)
## Creating a group milestone
>**Note:**
You need [Master permissions](../../permissions.md) in order to create a milestone.
>>>>>>> ce-com/master
You can create a milestone for several projects in the same group simultaneously.
On the group's **Issues ➔ Milestones** page, you will be able to see the status
......@@ -41,6 +63,7 @@ special options available when filtering by milestone:
* **Started** - show issues or merge requests from any milestone with a start
date less than today. Note that this can return results from several
milestones in the same project.
<<<<<<< HEAD
## Burndown charts
......@@ -70,3 +93,5 @@ cumulative value.
[ee-1540]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1540
[ee]: https://about.gitlab.com/gitlab-ee
=======
>>>>>>> ce-com/master
......@@ -4,40 +4,6 @@ Feature: Group Members
And "John Doe" is owner of group "Owned"
And "John Doe" is guest of group "Guest"
@javascript
Scenario: I should add user to group "Owned"
Given User "Mary Jane" exists
When I visit group "Owned" members page
And I select user "Mary Jane" from list with role "Reporter"
Then I should see user "Mary Jane" in team list
@javascript
Scenario: Add user to group
Given gitlab user "Mike"
When I visit group "Owned" members page
When I select "Mike" as "Reporter"
Then I should see "Mike" in team list as "Reporter"
@javascript
Scenario: Ignore add user to group when is already Owner
Given gitlab user "Mike"
When I visit group "Owned" members page
When I select "Mike" as "Reporter"
Then I should see "Mike" in team list as "Owner"
@javascript
Scenario: Invite user to group
When I visit group "Owned" members page
When I select "sjobs@apple.com" as "Reporter"
Then I should see "sjobs@apple.com" in team list as invited "Reporter"
@javascript
Scenario: Edit group member permissions
Given "Mary Jane" is guest of group "Owned"
And I visit group "Owned" members page
When I change the "Mary Jane" role to "Developer"
Then I should see "Mary Jane" as "Developer"
# Leave
@javascript
......
......@@ -117,6 +117,8 @@ Feature: Project Source Browse Files
And I click on ".gitignore" file in repo
And I see the ".gitignore"
And I click on "Replace"
Then I should see a Fork/Cancel combo
And I click button "Fork"
Then I should see a notice about a new fork having been created
When I click on "Replace"
And I replace it with a text file
......@@ -265,6 +267,8 @@ Feature: Project Source Browse Files
And I click on ".gitignore" file in repo
And I see the ".gitignore"
And I click on "Delete"
Then I should see a Fork/Cancel combo
And I click button "Fork"
Then I should see a notice about a new fork having been created
When I click on "Delete"
And I fill the commit message
......
......@@ -7,26 +7,6 @@ Feature: Project Team Management
And "Dmitriy" is "Shop" developer
And I visit project "Shop" team page
Scenario: See all team members
Then I should be able to see myself in team
And I should see "Dmitriy" in team list
@javascript
Scenario: Add user to project
When I select "Mike" as "Reporter"
Then I should see "Mike" in team list as "Reporter"
@javascript
Scenario: Invite user to project
When I select "sjobs@apple.com" as "Reporter"
Then I should see "sjobs@apple.com" in team list as invited "Reporter"
@javascript
Scenario: Update user access
Given I should see "Dmitriy" in team list as "Developer"
And I change "Dmitriy" role to "Reporter"
And I should see "Dmitriy" in team list as "Reporter"
Scenario: Cancel team member
Given I click cancel link for "Dmitriy"
Then I visit project "Shop" team page
......
......@@ -4,71 +4,6 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
include SharedPaths
include SharedGroup
include SharedUser
include Select2Helper
step 'I select "Mike" as "Reporter"' do
user = User.find_by(name: "Mike")
page.within ".users-group-form" do
select2(user.id, from: "#user_ids", multiple: true)
select "Reporter", from: "access_level"
end
click_button "Add to group"
end
step 'I select "Mike" as "Master"' do
user = User.find_by(name: "Mike")
page.within ".users-group-form" do
select2(user.id, from: "#user_ids", multiple: true)
select "Master", from: "access_level"
end
click_button "Add to group"
end
step 'I should see "Mike" in team list as "Reporter"' do
page.within '.content-list' do
expect(page).to have_content('Mike')
expect(page).to have_content('Reporter')
end
end
step 'I should see "Mike" in team list as "Owner"' do
page.within '.content-list' do
expect(page).to have_content('Mike')
expect(page).to have_content('Owner')
end
end
step 'I select "sjobs@apple.com" as "Reporter"' do
page.within ".users-group-form" do
select2("sjobs@apple.com", from: "#user_ids", multiple: true)
select "Reporter", from: "access_level"
end
click_button "Add to group"
end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
page.within '.content-list' do
expect(page).to have_content('sjobs@apple.com')
expect(page).to have_content('Invited')
expect(page).to have_content('Reporter')
end
end
step 'I select user "Mary Jane" from list with role "Reporter"' do
user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane")
page.within ".users-group-form" do
select2(user.id, from: "#user_ids", multiple: true)
select "Reporter", from: "access_level"
end
click_button "Add to group"
end
step 'I should see user "John Doe" in team list' do
expect(group_members_list).to have_content("John Doe")
......
......@@ -178,11 +178,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
def select_using_dropdown(dropdown_type, selection, is_commit = false)
dropdown = find(".js-compare-#{dropdown_type}-dropdown")
dropdown.find(".compare-dropdown-toggle").click
dropdown.find('.dropdown-menu', visible: true)
dropdown.fill_in("Filter by Git revision", with: selection)
if is_commit
dropdown.find('input[type="search"]').send_keys(:return)
else
find_link(selection, visible: true).click
end
dropdown.find('.dropdown-menu', visible: false)
end
end
......@@ -87,9 +87,9 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I fill the new branch name' do
first('button.js-target-branch', visible: true).click
first('.create-new-branch', visible: true).click
first('#new_branch_name', visible: true).set('new_branch_name')
first('.js-new-branch-btn', visible: true).click
find('.create-new-branch', visible: true).click
find('#new_branch_name', visible: true).set('new_branch_name')
find('.js-new-branch-btn', visible: true).click
end
step 'I fill the new file name with an illegal name' do
......@@ -377,7 +377,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I should see a Fork/Cancel combo' do
expect(page).to have_link 'Fork'
expect(page).to have_button 'Cancel'
expect(page).to have_content 'You don\'t have permission to edit this file. Try forking this project to edit the file.'
end
step 'I should see a notice about a new fork having been created' do
......
......@@ -4,25 +4,10 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
include SharedPaths
include Select2Helper
step 'I should be able to see myself in team' do
expect(page).to have_content(@user.name)
expect(page).to have_content(@user.username)
end
step 'I should see "Dmitriy" in team list' do
step 'I should not see "Dmitriy" in team list' do
user = User.find_by(name: "Dmitriy")
expect(page).to have_content(user.name)
expect(page).to have_content(user.username)
end
step 'I select "Mike" as "Reporter"' do
user = User.find_by(name: "Mike")
page.within ".users-project-form" do
select2(user.id, from: "#user_ids", multiple: true)
select "Reporter", from: "access_level"
end
click_button "Add to project"
expect(page).not_to have_content(user.name)
expect(page).not_to have_content(user.username)
end
step 'I should see "Mike" in team list as "Reporter"' do
......@@ -34,60 +19,6 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
end
step 'I select "sjobs@apple.com" as "Reporter"' do
page.within ".users-project-form" do
find('#user_ids', visible: false).set('sjobs@apple.com')
select "Reporter", from: "access_level"
end
click_button "Add to project"
end
step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
project_member = project.project_members.find_by(invite_email: 'sjobs@apple.com')
page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('sjobs@apple.com')
expect(page).to have_content('Invited')
expect(page).to have_content('Reporter')
end
end
step 'I should see "Dmitriy" in team list as "Developer"' do
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Dmitriy')
expect(page).to have_content('Developer')
end
end
step 'I change "Dmitriy" role to "Reporter"' do
project = Project.find_by(name: "Shop")
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
click_button project_member.human_access
page.within '.dropdown-menu' do
click_link 'Reporter'
end
end
end
step 'I should see "Dmitriy" in team list as "Reporter"' do
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
expect(page).to have_content('Dmitriy')
expect(page).to have_content('Reporter')
end
end
step 'I should not see "Dmitriy" in team list' do
user = User.find_by(name: "Dmitriy")
expect(page).not_to have_content(user.name)
expect(page).not_to have_content(user.username)
end
step 'gitlab user "Mike"' do
create(:user, name: "Mike")
end
......@@ -113,7 +44,7 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
project.team << [user, :reporter]
end
step 'I click link "Import team from another project"' do
step 'I click link "Import team from another project"' do
page.within '.users-project-form' do
click_link "Import"
end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment