Commit 8730f69e authored by Clement Ho's avatar Clement Ho

Merge branch 'master' into projects-r-s

parents f57cde14 7fa0a3e7
...@@ -218,6 +218,7 @@ const Api = { ...@@ -218,6 +218,7 @@ const Api = {
(jqXHR, textStatus, errorThrown) => { (jqXHR, textStatus, errorThrown) => {
const error = new Error(`${options.url}: ${errorThrown}`); const error = new Error(`${options.url}: ${errorThrown}`);
error.textStatus = textStatus; error.textStatus = textStatus;
if (jqXHR && jqXHR.responseJSON) error.responseJSON = jqXHR.responseJSON;
reject(error); reject(error);
}, },
); );
......
...@@ -65,7 +65,17 @@ export default class CreateItemDropdown { ...@@ -65,7 +65,17 @@ export default class CreateItemDropdown {
getData(term, callback) { getData(term, callback) {
this.getDataOption(term, (data = []) => { this.getDataOption(term, (data = []) => {
callback(data.concat(this.selectedItem || [])); // Ensure the selected item isn't already in the data to avoid duplicates
const alreadyHasSelectedItem = this.selectedItem && data.some(item =>
item.id === this.selectedItem.id,
);
let uniqueData = data;
if (!alreadyHasSelectedItem) {
uniqueData = data.concat(this.selectedItem || []);
}
callback(uniqueData);
}); });
} }
......
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
import Milestone from './milestone'; import Milestone from './milestone';
import IssuableForm from './issuable_form';
import LabelsSelect from './labels_select';
import MilestoneSelect from './milestone_select';
import notificationsDropdown from './notifications_dropdown'; import notificationsDropdown from './notifications_dropdown';
import LineHighlighter from './line_highlighter'; import LineHighlighter from './line_highlighter';
import MergeRequest from './merge_request'; import MergeRequest from './merge_request';
import Sidebar from './right_sidebar'; import Sidebar from './right_sidebar';
import IssuableTemplateSelectors from './templates/issuable_template_selectors';
import Flash from './flash'; import Flash from './flash';
import UserCallout from './user_callout'; import UserCallout from './user_callout';
import BlobViewer from './blob/viewer/index'; import BlobViewer from './blob/viewer/index';
...@@ -216,14 +212,16 @@ import SearchAutocomplete from './search_autocomplete'; ...@@ -216,14 +212,16 @@ import SearchAutocomplete from './search_autocomplete';
.then(callDefault) .then(callDefault)
.catch(fail); .catch(fail);
case 'projects:merge_requests:creations:diffs': case 'projects:merge_requests:creations:diffs':
import('./pages/projects/merge_requests/creations/diffs')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
new Diff(); import('./pages/projects/merge_requests/edit')
shortcut_handler = new ShortcutsNavigation(); .then(callDefault)
new GLForm($('.merge-request-form'), true); .catch(fail);
new IssuableForm($('.merge-request-form')); shortcut_handler = true;
new LabelsSelect();
new MilestoneSelect();
new IssuableTemplateSelectors();
break; break;
case 'projects:tags:new': case 'projects:tags:new':
import('./pages/projects/tags/new') import('./pages/projects/tags/new')
......
...@@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => { ...@@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => {
flashEl.addEventListener('transitionend', () => { flashEl.addEventListener('transitionend', () => {
flashEl.remove(); flashEl.remove();
if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown');
}, { }, {
once: true, once: true,
passive: true, passive: true,
...@@ -64,6 +65,7 @@ const createFlash = function createFlash( ...@@ -64,6 +65,7 @@ const createFlash = function createFlash(
parent = document, parent = document,
actionConfig = null, actionConfig = null,
fadeTransition = true, fadeTransition = true,
addBodyClass = false,
) { ) {
const flashContainer = parent.querySelector('.flash-container'); const flashContainer = parent.querySelector('.flash-container');
...@@ -86,6 +88,8 @@ const createFlash = function createFlash( ...@@ -86,6 +88,8 @@ const createFlash = function createFlash(
flashContainer.style.display = 'block'; flashContainer.style.display = 'block';
if (addBodyClass) document.body.classList.add('flash-shown');
return flashContainer; return flashContainer;
}; };
......
...@@ -68,12 +68,8 @@ export default { ...@@ -68,12 +68,8 @@ export default {
this.commitChanges({ payload, newMr: this.startNewMR }) this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => { .then(() => {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
this.$store.dispatch('getTreeData', { this.commitMessage = '';
projectId: this.currentProjectId, this.startNewMR = false;
branch: this.currentBranchId,
endpoint: `/tree/${this.currentBranchId}`,
force: true,
});
}) })
.catch(() => { .catch(() => {
this.submitCommitsLoading = false; this.submitCommitsLoading = false;
...@@ -153,6 +149,7 @@ you started editing. Would you like to create a new branch?`)" ...@@ -153,6 +149,7 @@ you started editing. Would you like to create a new branch?`)"
type="submit" type="submit"
:disabled="commitButtonDisabled" :disabled="commitButtonDisabled"
class="btn btn-default btn-sm append-right-10 prepend-left-10" class="btn btn-default btn-sm append-right-10 prepend-left-10"
:class="{ disabled: submitCommitsLoading }"
> >
<i <i
v-if="submitCommitsLoading" v-if="submitCommitsLoading"
......
...@@ -70,7 +70,10 @@ export default { ...@@ -70,7 +70,10 @@ export default {
this.editor.createInstance(this.$refs.editor); this.editor.createInstance(this.$refs.editor);
}) })
.then(() => this.setupEditor()) .then(() => this.setupEditor())
.catch(() => flash('Error setting up monaco. Please try again.')); .catch((err) => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
}, },
setupEditor() { setupEditor() {
if (!this.activeFile) return; if (!this.activeFile) return;
......
...@@ -35,9 +35,12 @@ ...@@ -35,9 +35,12 @@
return this.file.type === 'tree'; return this.file.type === 'tree';
}, },
levelIndentation() { levelIndentation() {
return { if (this.file.level > 0) {
marginLeft: `${this.file.level * 16}px`, return {
}; marginLeft: `${this.file.level * 16}px`,
};
}
return {};
}, },
shortId() { shortId() {
return this.file.id.substr(0, 8); return this.file.id.substr(0, 8);
...@@ -111,7 +114,7 @@ ...@@ -111,7 +114,7 @@
/> />
<i <i
class="fa" class="fa"
v-if="changedClass" v-if="file.changed || file.tempFile"
:class="changedClass" :class="changedClass"
aria-hidden="true" aria-hidden="true"
> >
......
...@@ -84,13 +84,13 @@ router.beforeEach((to, from, next) => { ...@@ -84,13 +84,13 @@ router.beforeEach((to, from, next) => {
} }
}) })
.catch((e) => { .catch((e) => {
flash('Error while loading the branch files. Please try again.'); flash('Error while loading the branch files. Please try again.', 'alert', document, null, false, true);
throw e; throw e;
}); });
} }
}) })
.catch((e) => { .catch((e) => {
flash('Error while loading the project data. Please try again.'); flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true);
throw e; throw e;
}); });
} }
......
...@@ -55,7 +55,7 @@ export default class Editor { ...@@ -55,7 +55,7 @@ export default class Editor {
attachModel(model) { attachModel(model) {
this.instance.setModel(model.getModel()); this.instance.setModel(model.getModel());
this.dirtyDiffController.attachModel(model); if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
this.currentModel = model; this.currentModel = model;
...@@ -68,7 +68,7 @@ export default class Editor { ...@@ -68,7 +68,7 @@ export default class Editor {
return acc; return acc;
}, {})); }, {}));
this.dirtyDiffController.reDecorate(model); if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
} }
clearEditor() { clearEditor() {
......
...@@ -3,6 +3,7 @@ import { visitUrl } from '../../lib/utils/url_utility'; ...@@ -3,6 +3,7 @@ import { visitUrl } from '../../lib/utils/url_utility';
import flash from '../../flash'; import flash from '../../flash';
import service from '../services'; import service from '../services';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { stripHtml } from '../../lib/utils/text_utility';
export const redirectToUrl = (_, url) => visitUrl(url); export const redirectToUrl = (_, url) => visitUrl(url);
...@@ -81,7 +82,7 @@ export const checkCommitStatus = ({ state }) => ...@@ -81,7 +82,7 @@ export const checkCommitStatus = ({ state }) =>
return false; return false;
}) })
.catch(() => flash('Error checking branch data. Please try again.')); .catch(() => flash('Error checking branch data. Please try again.', 'alert', document, null, false, true));
export const commitChanges = ( export const commitChanges = (
{ commit, state, dispatch, getters }, { commit, state, dispatch, getters },
...@@ -92,7 +93,7 @@ export const commitChanges = ( ...@@ -92,7 +93,7 @@ export const commitChanges = (
.then((data) => { .then((data) => {
const { branch } = payload; const { branch } = payload;
if (!data.short_id) { if (!data.short_id) {
flash(data.message); flash(data.message, 'alert', document, null, false, true);
return; return;
} }
...@@ -105,19 +106,25 @@ export const commitChanges = ( ...@@ -105,19 +106,25 @@ export const commitChanges = (
}, },
}; };
let commitMsg = `Your changes have been committed. Commit ${data.short_id}`;
if (data.stats) {
commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`;
}
flash( flash(
`Your changes have been committed. Commit ${data.short_id} with ${ commitMsg,
data.stats.additions
} additions, ${data.stats.deletions} deletions.`,
'notice', 'notice',
); document,
null,
false,
true);
window.dispatchEvent(new Event('resize'));
if (newMr) { if (newMr) {
dispatch('discardAllChanges');
dispatch( dispatch(
'redirectToUrl', 'redirectToUrl',
`${ `${selectedProject.web_url}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
selectedProject.web_url
}/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
); );
} else { } else {
commit(types.SET_BRANCH_WORKING_REFERENCE, { commit(types.SET_BRANCH_WORKING_REFERENCE, {
...@@ -134,12 +141,18 @@ export const commitChanges = ( ...@@ -134,12 +141,18 @@ export const commitChanges = (
}); });
dispatch('discardAllChanges'); dispatch('discardAllChanges');
dispatch('closeAllFiles');
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
}) })
.catch(() => flash('Error committing changes. Please try again.')); .catch((err) => {
let errMsg = 'Error committing changes. Please try again.';
if (err.responseJSON && err.responseJSON.message) {
errMsg += ` (${stripHtml(err.responseJSON.message)})`;
}
flash(errMsg, 'alert', document, null, false, true);
window.dispatchEvent(new Event('resize'));
});
export const createTempEntry = ( export const createTempEntry = (
{ state, dispatch }, { state, dispatch },
......
...@@ -17,7 +17,7 @@ export const getBranchData = ( ...@@ -17,7 +17,7 @@ export const getBranchData = (
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
flash('Error loading branch data. Please try again.'); flash('Error loading branch data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
}); });
} else { } else {
......
...@@ -69,7 +69,7 @@ export const getFileData = ({ state, commit, dispatch }, file) => { ...@@ -69,7 +69,7 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
}) })
.catch(() => { .catch(() => {
commit(types.TOGGLE_LOADING, file); commit(types.TOGGLE_LOADING, file);
flash('Error loading file data. Please try again.'); flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
}); });
}; };
...@@ -77,22 +77,28 @@ export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFile ...@@ -77,22 +77,28 @@ export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFile
.then((raw) => { .then((raw) => {
commit(types.SET_FILE_RAW_DATA, { file, raw }); commit(types.SET_FILE_RAW_DATA, { file, raw });
}) })
.catch(() => flash('Error loading file content. Please try again.')); .catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
export const changeFileContent = ({ commit }, { file, content }) => { export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content }); commit(types.UPDATE_FILE_CONTENT, { file, content });
}; };
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => { export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage }); if (state.selectedFile) {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
}
}; };
export const setFileEOL = ({ state, commit }, { eol }) => { export const setFileEOL = ({ state, commit }, { eol }) => {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol }); if (state.selectedFile) {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
}
}; };
export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => { export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn }); if (state.selectedFile) {
commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
}
}; };
export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => { export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
...@@ -112,7 +118,7 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI ...@@ -112,7 +118,7 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI
url: newUrl, url: newUrl,
}); });
if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`); if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
commit(types.CREATE_TMP_FILE, { commit(types.CREATE_TMP_FILE, {
parent, parent,
......
...@@ -18,7 +18,7 @@ export const getProjectData = ( ...@@ -18,7 +18,7 @@ export const getProjectData = (
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
flash('Error loading project data. Please try again.'); flash('Error loading project data. Please try again.', 'alert', document, null, false, true);
reject(new Error(`Project not loaded ${namespace}/${projectId}`)); reject(new Error(`Project not loaded ${namespace}/${projectId}`));
}); });
} else { } else {
......
...@@ -52,7 +52,7 @@ export const getTreeData = ( ...@@ -52,7 +52,7 @@ export const getTreeData = (
resolve(data); resolve(data);
}) })
.catch((e) => { .catch((e) => {
flash('Error loading tree data. Please try again.'); flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
if (tree) commit(types.TOGGLE_LOADING, tree); if (tree) commit(types.TOGGLE_LOADING, tree);
reject(e); reject(e);
}); });
...@@ -151,7 +151,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -151,7 +151,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
dispatch('getLastCommitData', tree); dispatch('getLastCommitData', tree);
}) })
.catch(() => flash('Error fetching log data.')); .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
}; };
export const updateDirectoryData = ( export const updateDirectoryData = (
......
...@@ -64,7 +64,7 @@ export default { ...@@ -64,7 +64,7 @@ export default {
}, },
[types.DISCARD_FILE_CHANGES](state, file) { [types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, { Object.assign(file, {
content: '', content: file.raw,
changed: false, changed: false,
}); });
}, },
......
...@@ -152,6 +152,13 @@ ...@@ -152,6 +152,13 @@
hasUpdated() { hasUpdated() {
return !!this.state.updatedAt; return !!this.state.updatedAt;
}, },
issueChanged() {
const descriptionChanged =
this.initialDescriptionText !== this.store.formState.description;
const titleChanged =
this.initialTitleText !== this.store.formState.title;
return descriptionChanged || titleChanged;
},
}, },
created() { created() {
this.service = new Service(this.endpoint); this.service = new Service(this.endpoint);
...@@ -176,6 +183,8 @@ ...@@ -176,6 +183,8 @@
} }
}); });
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
eventHub.$on('delete.issuable', this.deleteIssuable); eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable); eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm); eventHub.$on('close.form', this.closeForm);
...@@ -186,8 +195,17 @@ ...@@ -186,8 +195,17 @@
eventHub.$off('update.issuable', this.updateIssuable); eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm); eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm); eventHub.$off('open.form', this.openForm);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
}, },
methods: { methods: {
handleBeforeUnloadEvent(e) {
const event = e;
if (this.showForm && this.issueChanged) {
event.returnValue = 'Are you sure you want to lose your issue information?';
}
return undefined;
},
openForm() { openForm() {
if (!this.showForm) { if (!this.showForm) {
this.showForm = true; this.showForm = true;
......
...@@ -72,4 +72,4 @@ export function capitalizeFirstCharacter(text) { ...@@ -72,4 +72,4 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace * @param {*} replace
* @returns {String} * @returns {String}
*/ */
export const stripeHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace); export const stripHtml = (string, replace = '') => string.replace(/<[^>]*>/g, replace);
import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request';
export default initMergeRequest;
import initMergeRequest from '~/pages/projects/merge_requests/init_merge_request';
export default initMergeRequest;
/* eslint-disable no-new */
import Diff from '~/diff';
import ShortcutsNavigation from '~/shortcuts_navigation';
import GLForm from '~/gl_form';
import IssuableForm from '~/issuable_form';
import LabelsSelect from '~/labels_select';
import MilestoneSelect from '~/milestone_select';
import IssuableTemplateSelectors from '~/templates/issuable_template_selectors';
export default () => {
new Diff();
new ShortcutsNavigation();
new GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
new IssuableTemplateSelectors();
};
import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetArchived',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<div class="space-children">
<status-icon status="failed" />
<button
type="button"
class="btn btn-success btn-sm"
disabled="true">
Merge
</button>
</div>
<div class="media-body">
<span class="bold">
This project is archived, write access has been disabled
</span>
</div>
</div>
`,
};
<script>
import statusIcon from '../mr_widget_status_icon';
export default {
name: 'MRWidgetArchived',
components: {
statusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<div class="space-children">
<status-icon
status="warning"
/>
<button
type="button"
class="btn btn-success btn-sm"
disabled="true"
>
{{ s__("mrWidget|Merge") }}
</button>
</div>
<div class="media-body">
<span class="bold">
{{ s__("mrWidget|This project is archived, write access has been disabled") }}
</span>
</div>
</div>
</template>
...@@ -21,7 +21,7 @@ export { default as FailedToMerge } from './components/states/mr_widget_failed_t ...@@ -21,7 +21,7 @@ export { default as FailedToMerge } from './components/states/mr_widget_failed_t
export { default as ClosedState } from './components/states/mr_widget_closed'; export { default as ClosedState } from './components/states/mr_widget_closed';
export { default as MergingState } from './components/states/mr_widget_merging'; export { default as MergingState } from './components/states/mr_widget_merging';
export { default as WipState } from './components/states/mr_widget_wip'; export { default as WipState } from './components/states/mr_widget_wip';
export { default as ArchivedState } from './components/states/mr_widget_archived'; export { default as ArchivedState } from './components/states/mr_widget_archived.vue';
export { default as ConflictsState } from './components/states/mr_widget_conflicts'; export { default as ConflictsState } from './components/states/mr_widget_conflicts';
export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge'; export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch'; export { default as MissingBranchState } from './components/states/mr_widget_missing_branch';
......
...@@ -2,6 +2,9 @@ import { ...@@ -2,6 +2,9 @@ import {
Vue, Vue,
mrWidgetOptions, mrWidgetOptions,
} from './dependencies'; } from './dependencies';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo; gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
<template> <template>
<component <component
:is="rootElementType" :is="rootElementType"
class="text-center"> class="loading-container text-center">
<i <i
class="fa fa-spin fa-spinner" class="fa fa-spin fa-spinner"
:class="cssClass" :class="cssClass"
......
...@@ -174,12 +174,13 @@ ...@@ -174,12 +174,13 @@
&.user-authored { &.user-authored {
cursor: default; cursor: default;
opacity: 0.65; background-color: $gray-light;
border-color: $theme-gray-200;
color: $gl-text-color-disabled;
&:hover, gl-emoji {
&:active { opacity: 0.4;
background-color: $white-light; filter: grayscale(100%);
border-color: $border-color;
} }
} }
......
...@@ -220,14 +220,6 @@ ...@@ -220,14 +220,6 @@
@include btn-with-margin; @include btn-with-margin;
} }
&.disabled {
pointer-events: auto !important;
}
&[disabled] {
pointer-events: none !important;
}
.fa-caret-down, .fa-caret-down,
.fa-chevron-down { .fa-chevron-down {
margin-left: 5px; margin-left: 5px;
...@@ -450,3 +442,28 @@ ...@@ -450,3 +442,28 @@
.btn-svg svg { .btn-svg svg {
@include btn-svg; @include btn-svg;
} }
// All disabled buttons, regardless of color, type, etc
%disabled {
background-color: $gray-light !important;
border-color: $theme-gray-200 !important;
color: $gl-text-color-disabled !important;
opacity: 1 !important;
cursor: default !important;
i {
color: $gl-text-color-disabled !important;
}
}
.btn.disabled,
.btn[disabled],
fieldset[disabled] .btn,
.dropdown-toggle[disabled],
[disabled].dropdown-menu-toggle {
@extend %disabled;
&:hover {
@extend %disabled;
}
}
...@@ -63,11 +63,6 @@ ...@@ -63,11 +63,6 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
white-space: nowrap; white-space: nowrap;
&[disabled] {
opacity: .65;
cursor: not-allowed;
}
&.no-outline { &.no-outline {
outline: 0; outline: 0;
} }
......
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
*/ */
@mixin markdown-table { @mixin markdown-table {
width: auto; width: auto;
display: block;
overflow-x: auto;
} }
/* /*
......
...@@ -164,6 +164,7 @@ $gl-text-color-tertiary: #949494; ...@@ -164,6 +164,7 @@ $gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6; $gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1); $gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85); $gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-color-disabled: #919191;
$gl-text-green: $green-600; $gl-text-green: $green-600;
$gl-text-green-hover: $green-700; $gl-text-green-hover: $green-700;
$gl-text-red: $red-500; $gl-text-red: $red-500;
...@@ -258,6 +259,8 @@ $general-hover-transition-duration: 100ms; ...@@ -258,6 +259,8 @@ $general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear; $general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232); $highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px; $performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
/* /*
* Common component specific colors * Common component specific colors
......
...@@ -391,11 +391,17 @@ ...@@ -391,11 +391,17 @@
.dropdown-toggle { .dropdown-toggle {
float: right; float: right;
.toggle-icon { i {
color: $white-light; color: $white-light;
padding-right: 2px; padding-right: 2px;
margin-top: 2px; margin-top: 2px;
} }
&[disabled] {
i {
color: $gl-text-color-disabled;
}
}
} }
.dropdown-menu { .dropdown-menu {
......
...@@ -107,6 +107,11 @@ table.table tr td.multi-file-table-name { ...@@ -107,6 +107,11 @@ table.table tr td.multi-file-table-name {
vertical-align: middle; vertical-align: middle;
margin-right: 2px; margin-right: 2px;
} }
.loading-container {
margin-right: 4px;
display: inline-block;
}
} }
.multi-file-table-col-commit-message { .multi-file-table-col-commit-message {
...@@ -247,7 +252,6 @@ table.table tr td.multi-file-table-name { ...@@ -247,7 +252,6 @@ table.table tr td.multi-file-table-name {
display: flex; display: flex;
position: relative; position: relative;
flex-direction: column; flex-direction: column;
height: 100%;
width: 290px; width: 290px;
padding: 0; padding: 0;
background-color: $gray-light; background-color: $gray-light;
...@@ -256,6 +260,11 @@ table.table tr td.multi-file-table-name { ...@@ -256,6 +260,11 @@ table.table tr td.multi-file-table-name {
.projects-sidebar { .projects-sidebar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
.context-header {
width: auto;
margin-right: 0;
}
} }
.multi-file-commit-panel-inner { .multi-file-commit-panel-inner {
...@@ -496,19 +505,70 @@ table.table tr td.multi-file-table-name { ...@@ -496,19 +505,70 @@ table.table tr td.multi-file-table-name {
} }
} }
.ide-flash-container.flash-container { .ide.nav-only {
margin-top: $header-height; .flash-container {
margin-bottom: 0; margin-top: $header-height;
margin-bottom: 0;
}
.alert-wrapper .flash-container .flash-alert:last-child,
.alert-wrapper .flash-container .flash-notice:last-child {
margin-bottom: 0;
}
.content {
margin-top: $header-height;
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $context-header-height});
}
&.flash-shown {
.content {
margin-top: 0;
}
.ide-view {
height: calc(100vh - #{$header-height + $flash-height});
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $flash-height + $context-header-height});
}
}
} }
.with-performance-bar { .with-performance-bar .ide.nav-only {
.ide-flash-container.flash-container { .flash-container {
margin-top: $header-height + $performance-bar-height; margin-top: #{$header-height + $performance-bar-height};
}
.content {
margin-top: #{$header-height + $performance-bar-height};
} }
.ide-view { .ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height}); height: calc(100vh - #{$header-height + $performance-bar-height});
} }
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + 60});
}
&.flash-shown {
.content {
margin-top: 0;
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
}
.multi-file-commit-panel .multi-file-commit-panel-inner-scroll {
max-height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height + $context-header-height});
}
}
} }
......
...@@ -80,4 +80,20 @@ module EmailsHelper ...@@ -80,4 +80,20 @@ module EmailsHelper
'text-align:center' 'text-align:center'
].join(';') ].join(';')
end end
# "You are receiving this email because #{reason}"
def notification_reason_text(reason)
string = case reason
when NotificationReason::OWN_ACTIVITY
'of your activity'
when NotificationReason::ASSIGNED
'you have been assigned an item'
when NotificationReason::MENTIONED
'you have been mentioned'
else
'of your account'
end
"#{string} on #{Gitlab.config.gitlab.host}"
end
end end
require 'webpack/rails/manifest' require 'webpack/rails/manifest'
module WebpackHelper module WebpackHelper
def webpack_bundle_tag(bundle) def webpack_bundle_tag(bundle, force_same_domain: false)
javascript_include_tag(*gitlab_webpack_asset_paths(bundle)) javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: true))
end end
# override webpack-rails gem helper until changes can make it upstream # override webpack-rails gem helper until changes can make it upstream
def gitlab_webpack_asset_paths(source, extension: nil) def gitlab_webpack_asset_paths(source, extension: nil, force_same_domain: false)
return "" unless source.present? return "" unless source.present?
paths = Webpack::Rails::Manifest.asset_paths(source) paths = Webpack::Rails::Manifest.asset_paths(source)
...@@ -14,9 +14,11 @@ module WebpackHelper ...@@ -14,9 +14,11 @@ module WebpackHelper
paths.select! { |p| p.ends_with? ".#{extension}" } paths.select! { |p| p.ends_with? ".#{extension}" }
end end
force_host = webpack_public_host unless force_same_domain
if force_host force_host = webpack_public_host
paths.map! { |p| "#{force_host}#{p}" } if force_host
paths.map! { |p| "#{force_host}#{p}" }
end
end end
paths paths
......
module Emails module Emails
module Issues module Issues
def new_issue_email(recipient_id, issue_id) def new_issue_email(recipient_id, issue_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id)) mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
end end
def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id) def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end end
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_ids, updated_by_user_id) def reassigned_issue_email(recipient_id, issue_id, previous_assignee_ids, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
@previous_assignees = [] @previous_assignees = []
@previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any? @previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any?
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end end
def closed_issue_email(recipient_id, issue_id, updated_by_user_id) def closed_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
@updated_by = User.find(updated_by_user_id) @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end end
def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id) def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
@label_names = label_names @label_names = label_names
@labels_url = project_labels_url(@project) @labels_url = project_labels_url(@project)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id) def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
@issue_status = status @issue_status = status
@updated_by = User.find(updated_by_user_id) @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
end end
def issue_moved_email(recipient, issue, new_issue, updated_by_user) def issue_moved_email(recipient, issue, new_issue, updated_by_user, reason = nil)
setup_issue_mail(issue.id, recipient.id) setup_issue_mail(issue.id, recipient.id)
@new_issue = new_issue @new_issue = new_issue
@new_project = new_issue.project @new_project = new_issue.project
mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id)) mail_answer_thread(issue, issue_thread_options(updated_by_user.id, recipient.id, reason))
end end
private private
...@@ -61,11 +61,12 @@ module Emails ...@@ -61,11 +61,12 @@ module Emails
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key) @sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end end
def issue_thread_options(sender_id, recipient_id) def issue_thread_options(sender_id, recipient_id, reason)
{ {
from: sender(sender_id), from: sender(sender_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})") subject: subject("#{@issue.title} (##{@issue.iid})"),
'X-GitLab-NotificationReason' => reason
} }
end end
end end
......
module Emails module Emails
module MergeRequests module MergeRequests
def new_merge_request_email(recipient_id, merge_request_id) def new_merge_request_email(recipient_id, merge_request_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id)) mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id, reason))
end end
def new_mention_in_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) def new_mention_in_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id) def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id) def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@label_names = label_names @label_names = label_names
@labels_url = project_labels_url(@project) @labels_url = project_labels_url(@project)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@updated_by = User.find(updated_by_user_id) @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id) def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@mr_status = status @mr_status = status
@updated_by = User.find(updated_by_user_id) @updated_by = User.find(updated_by_user_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end end
def resolved_all_discussions_email(recipient_id, merge_request_id, resolved_by_user_id) def resolved_all_discussions_email(recipient_id, merge_request_id, resolved_by_user_id, reason = nil)
setup_merge_request_mail(merge_request_id, recipient_id) setup_merge_request_mail(merge_request_id, recipient_id)
@resolved_by = User.find(resolved_by_user_id) @resolved_by = User.find(resolved_by_user_id)
mail_answer_thread(@merge_request, merge_request_thread_options(resolved_by_user_id, recipient_id)) mail_answer_thread(@merge_request, merge_request_thread_options(resolved_by_user_id, recipient_id, reason))
end end
private private
...@@ -64,11 +64,12 @@ module Emails ...@@ -64,11 +64,12 @@ module Emails
@sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key) @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
end end
def merge_request_thread_options(sender_id, recipient_id) def merge_request_thread_options(sender_id, recipient_id, reason = nil)
{ {
from: sender(sender_id), from: sender(sender_id),
to: recipient(recipient_id), to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (#{@merge_request.to_reference})") subject: subject("#{@merge_request.title} (#{@merge_request.to_reference})"),
'X-GitLab-NotificationReason' => reason
} }
end end
end end
......
...@@ -112,6 +112,8 @@ class Notify < BaseMailer ...@@ -112,6 +112,8 @@ class Notify < BaseMailer
headers["X-GitLab-#{model.class.name}-ID"] = model.id headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key headers['X-GitLab-Reply-Key'] = reply_key
@reason = headers['X-GitLab-NotificationReason']
if Gitlab::IncomingEmail.enabled? && @sent_notification if Gitlab::IncomingEmail.enabled? && @sent_notification
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key)) address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace address.display_name = @project.name_with_namespace
......
# Holds reasons for a notification to have been sent as well as a priority list to select which reason to use
# above the rest
class NotificationReason
OWN_ACTIVITY = 'own_activity'.freeze
ASSIGNED = 'assigned'.freeze
MENTIONED = 'mentioned'.freeze
# Priority list for selecting which reason to return in the notification
REASON_PRIORITY = [
OWN_ACTIVITY,
ASSIGNED,
MENTIONED
].freeze
# returns the priority of a reason as an integer
def self.priority(reason)
REASON_PRIORITY.index(reason) || REASON_PRIORITY.length + 1
end
end
class NotificationRecipient class NotificationRecipient
attr_reader :user, :type attr_reader :user, :type, :reason
def initialize( def initialize(user, type, **opts)
user, type,
custom_action: nil,
target: nil,
acting_user: nil,
project: nil,
group: nil,
skip_read_ability: false
)
unless NotificationSetting.levels.key?(type) || type == :subscription unless NotificationSetting.levels.key?(type) || type == :subscription
raise ArgumentError, "invalid type: #{type.inspect}" raise ArgumentError, "invalid type: #{type.inspect}"
end end
@custom_action = custom_action @custom_action = opts[:custom_action]
@acting_user = acting_user @acting_user = opts[:acting_user]
@target = target @target = opts[:target]
@project = project || default_project @project = opts[:project] || default_project
@group = group || @project&.group @group = opts[:group] || @project&.group
@user = user @user = user
@type = type @type = type
@skip_read_ability = skip_read_ability @reason = opts[:reason]
@skip_read_ability = opts[:skip_read_ability]
end end
def notification_setting def notification_setting
...@@ -77,9 +69,15 @@ class NotificationRecipient ...@@ -77,9 +69,15 @@ class NotificationRecipient
def own_activity? def own_activity?
return false unless @acting_user return false unless @acting_user
return false if @acting_user.notified_of_own_activity?
user == @acting_user if user == @acting_user
# if activity was generated by the same user, change reason to :own_activity
@reason = NotificationReason::OWN_ACTIVITY
# If the user wants to be notified, we must return `false`
!@acting_user.notified_of_own_activity?
else
false
end
end end
def has_access? def has_access?
......
...@@ -314,6 +314,7 @@ class Project < ActiveRecord::Base ...@@ -314,6 +314,7 @@ class Project < ActiveRecord::Base
scope :with_builds_enabled, -> { with_feature_enabled(:builds) } scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) } scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
scope :with_issues_available_for_user, ->(current_user) { with_feature_available_for_user(:issues, current_user) }
scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) } scope :with_merge_requests_enabled, -> { with_feature_enabled(:merge_requests) }
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 } enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
......
...@@ -11,11 +11,11 @@ module NotificationRecipientService ...@@ -11,11 +11,11 @@ module NotificationRecipientService
end end
def self.build_recipients(*a) def self.build_recipients(*a)
Builder::Default.new(*a).recipient_users Builder::Default.new(*a).notification_recipients
end end
def self.build_new_note_recipients(*a) def self.build_new_note_recipients(*a)
Builder::NewNote.new(*a).recipient_users Builder::NewNote.new(*a).notification_recipients
end end
module Builder module Builder
...@@ -49,25 +49,24 @@ module NotificationRecipientService ...@@ -49,25 +49,24 @@ module NotificationRecipientService
@recipients ||= [] @recipients ||= []
end end
def <<(pair) def add_recipients(users, type, reason)
users, type = pair
if users.is_a?(ActiveRecord::Relation) if users.is_a?(ActiveRecord::Relation)
users = users.includes(:notification_settings) users = users.includes(:notification_settings)
end end
users = Array(users) users = Array(users)
users.compact! users.compact!
recipients.concat(users.map { |u| make_recipient(u, type) }) recipients.concat(users.map { |u| make_recipient(u, type, reason) })
end end
def user_scope def user_scope
User.includes(:notification_settings) User.includes(:notification_settings)
end end
def make_recipient(user, type) def make_recipient(user, type, reason)
NotificationRecipient.new( NotificationRecipient.new(
user, type, user, type,
reason: reason,
project: project, project: project,
custom_action: custom_action, custom_action: custom_action,
target: target, target: target,
...@@ -75,14 +74,13 @@ module NotificationRecipientService ...@@ -75,14 +74,13 @@ module NotificationRecipientService
) )
end end
def recipient_users def notification_recipients
@recipient_users ||= @notification_recipients ||=
begin begin
build! build!
filter! filter!
users = recipients.map(&:user) recipients = self.recipients.sort_by { |r| NotificationReason.priority(r.reason) }.uniq(&:user)
users.uniq! recipients.freeze
users.freeze
end end
end end
...@@ -95,13 +93,13 @@ module NotificationRecipientService ...@@ -95,13 +93,13 @@ module NotificationRecipientService
def add_participants(user) def add_participants(user)
return unless target.respond_to?(:participants) return unless target.respond_to?(:participants)
self << [target.participants(user), :participating] add_recipients(target.participants(user), :participating, nil)
end end
def add_mentions(user, target:) def add_mentions(user, target:)
return unless target.respond_to?(:mentioned_users) return unless target.respond_to?(:mentioned_users)
self << [target.mentioned_users(user), :mention] add_recipients(target.mentioned_users(user), :mention, NotificationReason::MENTIONED)
end end
# Get project/group users with CUSTOM notification level # Get project/group users with CUSTOM notification level
...@@ -119,11 +117,11 @@ module NotificationRecipientService ...@@ -119,11 +117,11 @@ module NotificationRecipientService
global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global) global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
user_ids += user_ids_with_global_level_custom(global_users_ids, custom_action) user_ids += user_ids_with_global_level_custom(global_users_ids, custom_action)
self << [user_scope.where(id: user_ids), :watch] add_recipients(user_scope.where(id: user_ids), :watch, nil)
end end
def add_project_watchers def add_project_watchers
self << [project_watchers, :watch] add_recipients(project_watchers, :watch, nil)
end end
# Get project users with WATCH notification level # Get project users with WATCH notification level
...@@ -144,7 +142,7 @@ module NotificationRecipientService ...@@ -144,7 +142,7 @@ module NotificationRecipientService
def add_subscribed_users def add_subscribed_users
return unless target.respond_to? :subscribers return unless target.respond_to? :subscribers
self << [target.subscribers(project), :subscription] add_recipients(target.subscribers(project), :subscription, nil)
end end
def user_ids_notifiable_on(resource, notification_level = nil) def user_ids_notifiable_on(resource, notification_level = nil)
...@@ -195,7 +193,7 @@ module NotificationRecipientService ...@@ -195,7 +193,7 @@ module NotificationRecipientService
return unless target.respond_to? :labels return unless target.respond_to? :labels
(labels || target.labels).each do |label| (labels || target.labels).each do |label|
self << [label.subscribers(project), :subscription] add_recipients(label.subscribers(project), :subscription, nil)
end end
end end
end end
...@@ -222,12 +220,12 @@ module NotificationRecipientService ...@@ -222,12 +220,12 @@ module NotificationRecipientService
# Re-assign is considered as a mention of the new assignee # Re-assign is considered as a mention of the new assignee
case custom_action case custom_action
when :reassign_merge_request when :reassign_merge_request
self << [previous_assignee, :mention] add_recipients(previous_assignee, :mention, nil)
self << [target.assignee, :mention] add_recipients(target.assignee, :mention, NotificationReason::ASSIGNED)
when :reassign_issue when :reassign_issue
previous_assignees = Array(previous_assignee) previous_assignees = Array(previous_assignee)
self << [previous_assignees, :mention] add_recipients(previous_assignees, :mention, nil)
self << [target.assignees, :mention] add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED)
end end
add_subscribed_users add_subscribed_users
...@@ -238,6 +236,12 @@ module NotificationRecipientService ...@@ -238,6 +236,12 @@ module NotificationRecipientService
# receive them, too. # receive them, too.
add_mentions(current_user, target: target) add_mentions(current_user, target: target)
# Add the assigned users, if any
assignees = custom_action == :new_issue ? target.assignees : target.assignee
# We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507)
add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees
add_labels_subscribers add_labels_subscribers
end end
end end
......
...@@ -85,10 +85,11 @@ class NotificationService ...@@ -85,10 +85,11 @@ class NotificationService
recipients.each do |recipient| recipients.each do |recipient|
mailer.send( mailer.send(
:reassigned_issue_email, :reassigned_issue_email,
recipient.id, recipient.user.id,
issue.id, issue.id,
previous_assignee_ids, previous_assignee_ids,
current_user.id current_user.id,
recipient.reason
).deliver_later ).deliver_later
end end
end end
...@@ -176,7 +177,7 @@ class NotificationService ...@@ -176,7 +177,7 @@ class NotificationService
action: "resolve_all_discussions") action: "resolve_all_discussions")
recipients.each do |recipient| recipients.each do |recipient|
mailer.resolved_all_discussions_email(recipient.id, merge_request.id, current_user.id).deliver_later mailer.resolved_all_discussions_email(recipient.user.id, merge_request.id, current_user.id, recipient.reason).deliver_later
end end
end end
...@@ -199,7 +200,7 @@ class NotificationService ...@@ -199,7 +200,7 @@ class NotificationService
recipients = NotificationRecipientService.build_new_note_recipients(note) recipients = NotificationRecipientService.build_new_note_recipients(note)
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(notify_method, recipient.id, note.id).deliver_later mailer.send(notify_method, recipient.user.id, note.id).deliver_later
end end
end end
...@@ -299,7 +300,7 @@ class NotificationService ...@@ -299,7 +300,7 @@ class NotificationService
recipients = NotificationRecipientService.build_recipients(issue, current_user, action: 'moved') recipients = NotificationRecipientService.build_recipients(issue, current_user, action: 'moved')
recipients.map do |recipient| recipients.map do |recipient|
email = mailer.issue_moved_email(recipient, issue, new_issue, current_user) email = mailer.issue_moved_email(recipient.user, issue, new_issue, current_user, recipient.reason)
email.deliver_later email.deliver_later
email email
end end
...@@ -339,16 +340,16 @@ class NotificationService ...@@ -339,16 +340,16 @@ class NotificationService
recipients = NotificationRecipientService.build_recipients(target, target.author, action: "new") recipients = NotificationRecipientService.build_recipients(target, target.author, action: "new")
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later mailer.send(method, recipient.user.id, target.id, recipient.reason).deliver_later
end end
end end
def new_mentions_in_resource_email(target, new_mentioned_users, current_user, method) def new_mentions_in_resource_email(target, new_mentioned_users, current_user, method)
recipients = NotificationRecipientService.build_recipients(target, current_user, action: "new") recipients = NotificationRecipientService.build_recipients(target, current_user, action: "new")
recipients = recipients & new_mentioned_users recipients = recipients.select {|r| new_mentioned_users.include?(r.user) }
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, current_user.id).deliver_later mailer.send(method, recipient.user.id, target.id, current_user.id, recipient.reason).deliver_later
end end
end end
...@@ -363,7 +364,7 @@ class NotificationService ...@@ -363,7 +364,7 @@ class NotificationService
) )
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, current_user.id).deliver_later mailer.send(method, recipient.user.id, target.id, current_user.id, recipient.reason).deliver_later
end end
end end
...@@ -381,10 +382,11 @@ class NotificationService ...@@ -381,10 +382,11 @@ class NotificationService
recipients.each do |recipient| recipients.each do |recipient|
mailer.send( mailer.send(
method, method,
recipient.id, recipient.user.id,
target.id, target.id,
previous_assignee_id, previous_assignee_id,
current_user.id current_user.id,
recipient.reason
).deliver_later ).deliver_later
end end
end end
...@@ -408,7 +410,7 @@ class NotificationService ...@@ -408,7 +410,7 @@ class NotificationService
recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen") recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen")
recipients.each do |recipient| recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later mailer.send(method, recipient.user.id, target.id, status, current_user.id, recipient.reason).deliver_later
end end
end end
......
- @body_class = 'ide'
- page_title 'IDE' - page_title 'IDE'
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue' = webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'ide' = webpack_bundle_tag 'ide', force_same_domain: true
.ide-flash-container.flash-container
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} } #ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
.text-center .text-center
......
!!! 5 !!! 5
%html{ lang: I18n.locale, class: page_class } %html{ lang: I18n.locale, class: page_class }
= render "layouts/head" = render "layouts/head"
%body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page } } %body{ class: "#{user_application_theme} #{@body_class} nav-only", data: { page: body_data_page } }
= render 'peek/bar' = render 'peek/bar'
= render "layouts/header/default" = render "layouts/header/default"
= render 'shared/outdated_browser' = render 'shared/outdated_browser'
...@@ -10,4 +10,5 @@ ...@@ -10,4 +10,5 @@
= render "layouts/broadcast" = render "layouts/broadcast"
= yield :flash_message = yield :flash_message
= render "layouts/flash" = render "layouts/flash"
= yield .content{ id: "content-body" }
= yield
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
#{link_to "View it on GitLab", @target_url}. #{link_to "View it on GitLab", @target_url}.
%br %br
-# Don't link the host in the line below, one link in the email is easier to quickly click than two. -# Don't link the host in the line below, one link in the email is easier to quickly click than two.
You're receiving this email because of your account on #{Gitlab.config.gitlab.host}. You're receiving this email because #{notification_reason_text(@reason)}.
If you'd like to receive fewer emails, you can If you'd like to receive fewer emails, you can
- if @labels_url - if @labels_url
adjust your #{link_to 'label subscriptions', @labels_url}. adjust your #{link_to 'label subscriptions', @labels_url}.
......
...@@ -9,4 +9,4 @@ ...@@ -9,4 +9,4 @@
<% end -%> <% end -%>
<% end -%> <% end -%>
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. <%= "You're receiving this email because #{notification_reason_text(@reason)}." %>
---
title: Set standard disabled state for all buttons
merge_request:
author:
type: other
---
title: Fix the Projects API with_issues_enabled filter behaving incorrectly
any user
merge_request: 12724
author: Jan Christophersen
type: fixed
---
title: Initial work to add notification reason to emails
merge_request: 16160
author: Mario de la Ossa
type: added
---
title: Fix duplicate item in protected branch/tag dropdown
merge_request:
author:
type: fixed
---
title: Correctly escape UTF-8 path elements for uploads
merge_request: 16560
author:
type: fixed
---
title: Add horizontal scroll to wiki tables
merge_request: 16527
author: George Tsiolis
type: fixed
---
title: rework indexes on redirect_routes
merge_request:
author:
type: performance
...@@ -119,7 +119,12 @@ var config = { ...@@ -119,7 +119,12 @@ var config = {
{ {
test: /\_worker\.js$/, test: /\_worker\.js$/,
use: [ use: [
{ loader: 'worker-loader' }, {
loader: 'worker-loader',
options: {
inline: true
}
},
{ loader: 'babel-loader' }, { loader: 'babel-loader' },
], ],
}, },
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class ReworkRedirectRoutesIndexes < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME_UNIQUE = "index_redirect_routes_on_path_unique_text_pattern_ops"
INDEX_NAME_PERM = "index_redirect_routes_on_path_text_pattern_ops_where_permanent"
INDEX_NAME_TEMP = "index_redirect_routes_on_path_text_pattern_ops_where_temporary"
OLD_INDEX_NAME_PATH_TPOPS = "index_redirect_routes_on_path_text_pattern_ops"
OLD_INDEX_NAME_PATH_LOWER = "index_on_redirect_routes_lower_path"
def up
disable_statement_timeout
# this is a plain btree on a single boolean column. It'll never be
# selective enough to be valuable. This class is called by
# setup_postgresql.rake so it needs to be able to handle this
# index not existing.
if index_exists?(:redirect_routes, :permanent)
remove_concurrent_index(:redirect_routes, :permanent)
end
# If we're on MySQL then the existing index on path is ok. But on
# Postgres we need to clean things up:
return unless Gitlab::Database.postgresql?
if_not_exists = Gitlab::Database.version.to_f >= 9.5 ? "IF NOT EXISTS" : ""
# Unique index on lower(path) across both types of redirect_routes:
execute("CREATE UNIQUE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_UNIQUE} ON redirect_routes (lower(path) varchar_pattern_ops);")
# Make two indexes on path -- one for permanent and one for temporary routes:
execute("CREATE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_PERM} ON redirect_routes (lower(path) varchar_pattern_ops) where (permanent);")
execute("CREATE INDEX CONCURRENTLY #{if_not_exists} #{INDEX_NAME_TEMP} ON redirect_routes (lower(path) varchar_pattern_ops) where (not permanent or permanent is null) ;")
# Remove the old indexes:
# This one needed to be on lower(path) but wasn't so it's replaced with the two above
execute "DROP INDEX CONCURRENTLY IF EXISTS #{OLD_INDEX_NAME_PATH_TPOPS};"
# This one isn't needed because we only ever do = and LIKE on this
# column so the varchar_pattern_ops index is sufficient
execute "DROP INDEX CONCURRENTLY IF EXISTS #{OLD_INDEX_NAME_PATH_LOWER};"
end
def down
disable_statement_timeout
add_concurrent_index(:redirect_routes, :permanent)
return unless Gitlab::Database.postgresql?
execute("CREATE INDEX CONCURRENTLY #{OLD_INDEX_NAME_PATH_TPOPS} ON redirect_routes (path varchar_pattern_ops);")
execute("CREATE INDEX CONCURRENTLY #{OLD_INDEX_NAME_PATH_LOWER} ON redirect_routes (LOWER(path));")
execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_UNIQUE};")
execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_PERM};")
execute("DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME_TEMP};")
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180105212544) do ActiveRecord::Schema.define(version: 20180113220114) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -1538,8 +1538,6 @@ ActiveRecord::Schema.define(version: 20180105212544) do ...@@ -1538,8 +1538,6 @@ ActiveRecord::Schema.define(version: 20180105212544) do
end end
add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path", unique: true, using: :btree
add_index "redirect_routes", ["path"], name: "index_redirect_routes_on_path_text_pattern_ops", using: :btree, opclasses: {"path"=>"varchar_pattern_ops"}
add_index "redirect_routes", ["permanent"], name: "index_redirect_routes_on_permanent", using: :btree
add_index "redirect_routes", ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree add_index "redirect_routes", ["source_type", "source_id"], name: "index_redirect_routes_on_source_type_and_source_id", using: :btree
create_table "releases", force: :cascade do |t| create_table "releases", force: :cascade do |t|
......
...@@ -144,7 +144,7 @@ Now that the Okta app is configured, it's time to enable it in GitLab. ...@@ -144,7 +144,7 @@ Now that the Okta app is configured, it's time to enable it in GitLab.
1. [Reconfigure][reconf] or [restart] GitLab for Omnibus and installations 1. [Reconfigure][reconf] or [restart] GitLab for Omnibus and installations
from source respectively for the changes to take effect. from source respectively for the changes to take effect.
You might want to try this out on a incognito browser window. You might want to try this out on an incognito browser window.
## Configuring groups ## Configuring groups
......
...@@ -483,7 +483,7 @@ You can use GitLab as an auth endpoint and use a non-bundled Container Registry. ...@@ -483,7 +483,7 @@ You can use GitLab as an auth endpoint and use a non-bundled Container Registry.
1. A certificate keypair is required for GitLab and the Container Registry to 1. A certificate keypair is required for GitLab and the Container Registry to
communicate securely. By default omnibus-gitlab will generate one keypair, communicate securely. By default omnibus-gitlab will generate one keypair,
which is saved to `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key`. which is saved to `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key`.
When using an non-bundled Container Registry, you will need to supply a When using a non-bundled Container Registry, you will need to supply a
custom certificate key. To do that, add the following to custom certificate key. To do that, add the following to
`/etc/gitlab/gitlab.rb` `/etc/gitlab/gitlab.rb`
......
...@@ -154,7 +154,7 @@ who will take all the decisions to restore the service availability by: ...@@ -154,7 +154,7 @@ who will take all the decisions to restore the service availability by:
- Reconfigure the old **Master** and demote to **Slave** when it comes back online - Reconfigure the old **Master** and demote to **Slave** when it comes back online
You must have at least `3` Redis Sentinel servers, and they need to You must have at least `3` Redis Sentinel servers, and they need to
be each in a independent machine (that are believed to fail independently), be each in an independent machine (that are believed to fail independently),
ideally in different geographical areas. ideally in different geographical areas.
You can configure them in the same machines where you've configured the other You can configure them in the same machines where you've configured the other
......
...@@ -36,7 +36,7 @@ Add the following to your `sshd_config` file. This is usuaully located at ...@@ -36,7 +36,7 @@ Add the following to your `sshd_config` file. This is usuaully located at
Omnibus Docker: Omnibus Docker:
``` ```
AuthorizedKeysCommand /opt/embedded/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k AuthorizedKeysCommand /opt/gitlab/embedded/service/gitlab-shell/bin/gitlab-shell-authorized-keys-check git %u %k
AuthorizedKeysCommandUser git AuthorizedKeysCommandUser git
``` ```
......
...@@ -172,7 +172,7 @@ Parameters: ...@@ -172,7 +172,7 @@ Parameters:
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of an issue | | `issue_iid` | integer | yes | The internal ID of an issue |
| `award_id` | integer | yes | The ID of a award_emoji | | `award_id` | integer | yes | The ID of an award_emoji |
```bash ```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/344 curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/344
...@@ -197,7 +197,7 @@ Parameters: ...@@ -197,7 +197,7 @@ Parameters:
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of an issue | | `issue_iid` | integer | yes | The internal ID of an issue |
| `note_id` | integer | yes | The ID of an note | | `note_id` | integer | yes | The ID of a note |
```bash ```bash
...@@ -323,7 +323,7 @@ Parameters: ...@@ -323,7 +323,7 @@ Parameters:
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `issue_iid` | integer | yes | The internal ID of an issue | | `issue_iid` | integer | yes | The internal ID of an issue |
| `note_id` | integer | yes | The ID of a note | | `note_id` | integer | yes | The ID of a note |
| `award_id` | integer | yes | The ID of a award_emoji | | `award_id` | integer | yes | The ID of an award_emoji |
```bash ```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/345 curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/issues/80/award_emoji/345
......
...@@ -73,7 +73,7 @@ POST /groups/:id/milestones ...@@ -73,7 +73,7 @@ POST /groups/:id/milestones
Parameters: Parameters:
- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user - `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
- `title` (required) - The title of an milestone - `title` (required) - The title of a milestone
- `description` (optional) - The description of the milestone - `description` (optional) - The description of the milestone
- `due_date` (optional) - The due date of the milestone - `due_date` (optional) - The due date of the milestone
- `start_date` (optional) - The start date of the milestone - `start_date` (optional) - The start date of the milestone
......
...@@ -591,7 +591,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid ...@@ -591,7 +591,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid
| `title` | string | no | Title of MR | | `title` | string | no | Title of MR |
| `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. | | `assignee_id` | integer | no | The ID of the user to assign the merge request to. Set to `0` or provide an empty value to unassign all assignees. |
| `milestone_id` | integer | no | The ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.| | `milestone_id` | integer | no | The ID of a milestone to assign the merge request to. Set to `0` or provide an empty value to unassign a milestone.|
| `labels` | string | no | Comma-separated label names for an merge request. Set to an empty string to unassign all labels. | | `labels` | string | no | Comma-separated label names for a merge request. Set to an empty string to unassign all labels. |
| `description` | string | no | Description of MR | | `description` | string | no | Description of MR |
| `state_event` | string | no | New state (close/reopen) | | `state_event` | string | no | New state (close/reopen) |
| `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging |
......
...@@ -70,7 +70,7 @@ POST /projects/:id/milestones ...@@ -70,7 +70,7 @@ POST /projects/:id/milestones
Parameters: Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `title` (required) - The title of an milestone - `title` (required) - The title of a milestone
- `description` (optional) - The description of the milestone - `description` (optional) - The description of the milestone
- `due_date` (optional) - The due date of the milestone - `due_date` (optional) - The due date of the milestone
- `start_date` (optional) - The start date of the milestone - `start_date` (optional) - The start date of the milestone
......
...@@ -158,7 +158,7 @@ Parameters: ...@@ -158,7 +158,7 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user - `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
- `snippet_id` (required) - The ID of a project snippet - `snippet_id` (required) - The ID of a project snippet
- `note_id` (required) - The ID of an snippet note - `note_id` (required) - The ID of a snippet note
```json ```json
{ {
......
...@@ -982,7 +982,7 @@ Example response: ...@@ -982,7 +982,7 @@ Example response:
## Unarchive a project ## Unarchive a project
Unarchives the project if the user is either admin or the project owner of this project. This action is Unarchives the project if the user is either admin or the project owner of this project. This action is
idempotent, thus unarchiving an non-archived project will not change the project. idempotent, thus unarchiving a non-archived project will not change the project.
``` ```
POST /projects/:id/unarchive POST /projects/:id/unarchive
......
...@@ -455,7 +455,7 @@ Mappings are defined as entries in the root YAML array, and are identified by a ...@@ -455,7 +455,7 @@ Mappings are defined as entries in the root YAML array, and are identified by a
- Literal periods (`.`) should be escaped as `\.`. - Literal periods (`.`) should be escaped as `\.`.
- `public` - `public`
- a string, starting and ending with `'`. - a string, starting and ending with `'`.
- Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurence, starting with `\1`. - Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurrence, starting with `\1`.
The public path for a source path is determined by finding the first `source` expression that matches it, and returning the corresponding `public` path, replacing the `\N` expressions with the values of the `()` capture groups if appropriate. The public path for a source path is determined by finding the first `source` expression that matches it, and returning the corresponding `public` path, replacing the `\N` expressions with the values of the `()` capture groups if appropriate.
......
...@@ -167,7 +167,7 @@ Finally, push to GitLab and let the tests begin! ...@@ -167,7 +167,7 @@ Finally, push to GitLab and let the tests begin!
### Test against different PHP versions in Shell builds ### Test against different PHP versions in Shell builds
The [phpenv][] project allows you to easily manage different versions of PHP The [phpenv][] project allows you to easily manage different versions of PHP
each with its own config. This is specially usefull when testing PHP projects each with its own config. This is especially useful when testing PHP projects
with the Shell executor. with the Shell executor.
You will have to install it on your build machine under the `gitlab-runner` You will have to install it on your build machine under the `gitlab-runner`
...@@ -227,7 +227,7 @@ following in your `.gitlab-ci.yml`: ...@@ -227,7 +227,7 @@ following in your `.gitlab-ci.yml`:
... ...
# Composer stores all downloaded packages in the vendor/ directory. # Composer stores all downloaded packages in the vendor/ directory.
# Do not use the following if the vendor/ directory is commited to # Do not use the following if the vendor/ directory is committed to
# your git repository. # your git repository.
cache: cache:
paths: paths:
......
...@@ -42,7 +42,7 @@ production: ...@@ -42,7 +42,7 @@ production:
This project has three jobs: This project has three jobs:
1. `test` - used to test Django application, 1. `test` - used to test Django application,
2. `staging` - used to automatically deploy staging environment every push to `master` branch 2. `staging` - used to automatically deploy staging environment every push to `master` branch
3. `production` - used to automatically deploy production environmnet for every created tag 3. `production` - used to automatically deploy production environment for every created tag
## Store API keys ## Store API keys
......
...@@ -135,9 +135,9 @@ Clicking on it you will be directed to the jobs page for that specific commit. ...@@ -135,9 +135,9 @@ Clicking on it you will be directed to the jobs page for that specific commit.
![Single commit jobs page](img/single_commit_status_pending.png) ![Single commit jobs page](img/single_commit_status_pending.png)
Notice that there are two jobs pending which are named after what we wrote in Notice that there is a pending job which is named after what we wrote in
`.gitlab-ci.yml`. The red triangle indicates that there is no Runner configured `.gitlab-ci.yml`. "stuck" indicates that there is no Runner configured
yet for these jobs. yet for this job.
The next step is to configure a Runner so that it picks the pending jobs. The next step is to configure a Runner so that it picks the pending jobs.
......
...@@ -194,7 +194,7 @@ before_script: ...@@ -194,7 +194,7 @@ before_script:
## ##
## You can optionally disable host key checking. Be aware that by adding that ## You can optionally disable host key checking. Be aware that by adding that
## you are suspectible to man-in-the-middle attacks. ## you are susceptible to man-in-the-middle attacks.
## WARNING: Use this only with the Docker executor, if you use it with shell ## WARNING: Use this only with the Docker executor, if you use it with shell
## you will overwrite your user's SSH config. ## you will overwrite your user's SSH config.
## ##
......
...@@ -87,7 +87,7 @@ future GitLab releases.** ...@@ -87,7 +87,7 @@ future GitLab releases.**
## 9.0 Renaming ## 9.0 Renaming
To follow conventions of naming across GitLab, and to futher move away from the To follow conventions of naming across GitLab, and to further move away from the
`build` term and toward `job` CI variables have been renamed for the 9.0 `build` term and toward `job` CI variables have been renamed for the 9.0
release. release.
......
...@@ -61,7 +61,7 @@ against EE. ...@@ -61,7 +61,7 @@ against EE.
1. Tries to apply it to current EE `master` 1. Tries to apply it to current EE `master`
1. If it applies cleanly, the job succeeds 1. If it applies cleanly, the job succeeds
In the case where the job fails, it means you should create a `ee-<ce_branch>` In the case where the job fails, it means you should create an `ee-<ce_branch>`
or `<ce_branch>-ee` branch, push it to EE and open a merge request against EE or `<ce_branch>-ee` branch, push it to EE and open a merge request against EE
`master`. `master`.
At this point if you retry the failing job in your CE merge request, it should At this point if you retry the failing job in your CE merge request, it should
......
...@@ -123,7 +123,7 @@ roughly be as follows: ...@@ -123,7 +123,7 @@ roughly be as follows:
scheduling jobs for newly created data. scheduling jobs for newly created data.
1. In a post-deployment migration you'll need to ensure no jobs remain. To do 1. In a post-deployment migration you'll need to ensure no jobs remain. To do
so you can use `Gitlab::BackgroundMigration.steal` to process any remaining so you can use `Gitlab::BackgroundMigration.steal` to process any remaining
jobs before continueing. jobs before continuing.
1. Remove the old column. 1. Remove the old column.
## Example ## Example
......
...@@ -12,9 +12,9 @@ following format: ...@@ -12,9 +12,9 @@ following format:
```yaml ```yaml
--- ---
title: "Going through change[log]s" title: "Change[log]s"
merge_request: 1972 merge_request: 1972
author: Ozzy Osbourne author: Black Sabbath
type: added type: added
``` ```
......
...@@ -420,7 +420,7 @@ the style below as a guide: ...@@ -420,7 +420,7 @@ the style below as a guide:
In this case: In this case:
- before each step list the installation method is declared in bold - before each step list the installation method is declared in bold
- three dashes (`---`) are used to create an horizontal line and separate the - three dashes (`---`) are used to create a horizontal line and separate the
two methods two methods
- the code blocks are indented one or more spaces under the list item to render - the code blocks are indented one or more spaces under the list item to render
correctly correctly
......
...@@ -56,7 +56,7 @@ To help us mock the responses we need we use [axios-mock-adapter][axios-mock-ada ...@@ -56,7 +56,7 @@ To help us mock the responses we need we use [axios-mock-adapter][axios-mock-ada
### Mock poll requests on tests with axios ### Mock poll requests on tests with axios
Because polling function requires an header object, we need to always include an object as the third argument: Because polling function requires a header object, we need to always include an object as the third argument:
```javascript ```javascript
mock.onGet('/users').reply(200, { foo: 'bar' }, {}); mock.onGet('/users').reply(200, { foo: 'bar' }, {});
......
...@@ -456,7 +456,7 @@ describe('Todos App', () => { ...@@ -456,7 +456,7 @@ describe('Todos App', () => {
}); });
``` ```
#### `mountComponent` helper #### `mountComponent` helper
There is an helper in `spec/javascripts/helpers/vue_mount_component_helper.js` that allows you to mount a component with the given props: There is a helper in `spec/javascripts/helpers/vue_mount_component_helper.js` that allows you to mount a component with the given props:
```javascript ```javascript
import Vue from 'vue'; import Vue from 'vue';
......
...@@ -4,7 +4,7 @@ When writing migrations for GitLab, you have to take into account that ...@@ -4,7 +4,7 @@ When writing migrations for GitLab, you have to take into account that
these will be ran by hundreds of thousands of organizations of all sizes, some with these will be ran by hundreds of thousands of organizations of all sizes, some with
many years of data in their database. many years of data in their database.
In addition, having to take a server offline for a a upgrade small or big is a In addition, having to take a server offline for an upgrade small or big is a
big burden for most organizations. For this reason it is important that your big burden for most organizations. For this reason it is important that your
migrations are written carefully, can be applied online and adhere to the style migrations are written carefully, can be applied online and adhere to the style
guide below. guide below.
......
...@@ -8,7 +8,7 @@ Note that if your db user does not have advanced privileges you must create the ...@@ -8,7 +8,7 @@ Note that if your db user does not have advanced privileges you must create the
bundle exec rake setup bundle exec rake setup
``` ```
The `setup` task is a alias for `gitlab:setup`. The `setup` task is an alias for `gitlab:setup`.
This tasks calls `db:reset` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database. This tasks calls `db:reset` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database.
Note: `db:setup` calls `db:seed` but this does nothing. Note: `db:setup` calls `db:seed` but this does nothing.
......
...@@ -26,7 +26,6 @@ Here are some things to keep in mind regarding test performance: ...@@ -26,7 +26,6 @@ Here are some things to keep in mind regarding test performance:
- Use `.method` to describe class methods and `#method` to describe instance - Use `.method` to describe class methods and `#method` to describe instance
methods. methods.
- Use `context` to test branching logic. - Use `context` to test branching logic.
- Don't assert against the absolute value of a sequence-generated attribute (see [Gotchas](../gotchas.md#dont-assert-against-the-absolute-value-of-a-sequence-generated-attribute)).
- Try to match the ordering of tests to the ordering within the class. - Try to match the ordering of tests to the ordering within the class.
- Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines - Try to follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases. to separate phases.
......
...@@ -27,7 +27,7 @@ View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here. ...@@ -27,7 +27,7 @@ View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here.
### Dropdowns ### Dropdowns
The dropdown menu should feel like it is appearing from the triggering element. Combining a position shift `400ms cubic-bezier(0.23, 1, 0.32, 1)` with a opacity animation `200ms linear` on the second half of the motion achieves this affect. The dropdown menu should feel like it is appearing from the triggering element. Combining a position shift `400ms cubic-bezier(0.23, 1, 0.32, 1)` with an opacity animation `200ms linear` on the second half of the motion achieves this affect.
View the [interactive example](http://codepen.io/awhildy/full/jVLJpb/) here. View the [interactive example](http://codepen.io/awhildy/full/jVLJpb/) here.
......
...@@ -108,7 +108,7 @@ Primary buttons communicate the main call to action. There should only be one ca ...@@ -108,7 +108,7 @@ Primary buttons communicate the main call to action. There should only be one ca
![Primary button example](img/button-primary.png) ![Primary button example](img/button-primary.png)
#### Secondary #### Secondary
Secondary buttons are for alternative commands. They should be conveyed by a button with an stroke, and no background fill. Secondary buttons are for alternative commands. They should be conveyed by a button with a stroke, and no background fill.
![Secondary button example](img/button-secondary.png) ![Secondary button example](img/button-secondary.png)
...@@ -181,7 +181,7 @@ A count element is used in navigation contexts where it is helpful to indicate t ...@@ -181,7 +181,7 @@ A count element is used in navigation contexts where it is helpful to indicate t
## Lists ## Lists
Lists are used where ever there is a single column of information to display. Ths [issues list](https://gitlab.com/gitlab-org/gitlab-ce/issues) is an example of a important list in the GitLab UI. Lists are used where ever there is a single column of information to display. Ths [issues list](https://gitlab.com/gitlab-org/gitlab-ce/issues) is an example of an important list in the GitLab UI.
### Types ### Types
...@@ -269,7 +269,7 @@ Modals are only used for having a conversation and confirmation with the user. T ...@@ -269,7 +269,7 @@ Modals are only used for having a conversation and confirmation with the user. T
* Modals contain the header, body, and actions. * Modals contain the header, body, and actions.
* **Header(1):** The header title is a question instead of a descriptive phrase. * **Header(1):** The header title is a question instead of a descriptive phrase.
* **Body(2):** The content in body should never be ambiguous and unclear. It provides specific information. * **Body(2):** The content in body should never be ambiguous and unclear. It provides specific information.
* **Actions(3):** Contains a affirmative action, a dismissive action, and an extra action. The order of actions from left to right: Dismissive action → Extra action → Affirmative action * **Actions(3):** Contains an affirmative action, a dismissive action, and an extra action. The order of actions from left to right: Dismissive action → Extra action → Affirmative action
* Confirmations regarding labels should keep labeling styling. * Confirmations regarding labels should keep labeling styling.
* References to commits, branches, and tags should be **monospaced**. * References to commits, branches, and tags should be **monospaced**.
......
...@@ -27,7 +27,7 @@ This means that, as a rule, copy should be very short. A long message or label i ...@@ -27,7 +27,7 @@ This means that, as a rule, copy should be very short. A long message or label i
>**Example:** >**Example:**
Use `Add` instead of `Add issue` as a button label. Use `Add` instead of `Add issue` as a button label.
Preferrably use context and placement of controls to make it obvious what clicking on them will do. Preferably use context and placement of controls to make it obvious what clicking on them will do.
--- ---
......
...@@ -178,7 +178,7 @@ address. Read [IP address types and allocation methods in Azure][Azure-IP-Addres ...@@ -178,7 +178,7 @@ address. Read [IP address types and allocation methods in Azure][Azure-IP-Addres
At this stage you should have a running and fully operational VM. However, none of the services on At this stage you should have a running and fully operational VM. However, none of the services on
your VM (e.g. GitLab) will be publicly accessible via the internet until you have opened up the your VM (e.g. GitLab) will be publicly accessible via the internet until you have opened up the
neccessary ports to enable access to those services. necessary ports to enable access to those services.
Ports are opened by adding _security rules_ to the **"Network security group"** (NSG) which our VM Ports are opened by adding _security rules_ to the **"Network security group"** (NSG) which our VM
has been assigned to. If you followed the process above, then Azure will have automatically created has been assigned to. If you followed the process above, then Azure will have automatically created
...@@ -436,4 +436,4 @@ Check out our other [Technical Articles][GitLab-Technical-Articles] or browse th ...@@ -436,4 +436,4 @@ Check out our other [Technical Articles][GitLab-Technical-Articles] or browse th
[SSH]: https://en.wikipedia.org/wiki/Secure_Shell [SSH]: https://en.wikipedia.org/wiki/Secure_Shell
[PuTTY]: http://www.putty.org/ [PuTTY]: http://www.putty.org/
[Using-SSH-In-Putty]: https://mediatemple.net/community/products/dv/204404604/using-ssh-in-putty- [Using-SSH-In-Putty]: https://mediatemple.net/community/products/dv/204404604/using-ssh-in-putty-
\ No newline at end of file
...@@ -431,7 +431,7 @@ GitLab Shell is an SSH access and repository management software developed speci ...@@ -431,7 +431,7 @@ GitLab Shell is an SSH access and repository management software developed speci
**Note:** GitLab Shell application startup time can be greatly reduced by disabling RubyGems. This can be done in several manners: **Note:** GitLab Shell application startup time can be greatly reduced by disabling RubyGems. This can be done in several manners:
* Export `RUBYOPT=--disable-gems` environment variable for the processes * Export `RUBYOPT=--disable-gems` environment variable for the processes
* Compile Ruby with `configure --disable-rubygems` to disable RubyGems by default. Not recommened for system-wide Ruby. * Compile Ruby with `configure --disable-rubygems` to disable RubyGems by default. Not recommended for system-wide Ruby.
* Omnibus GitLab [replaces the *shebang* line of the `gitlab-shell/bin/*` scripts](https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1707) * Omnibus GitLab [replaces the *shebang* line of the `gitlab-shell/bin/*` scripts](https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1707)
### Install gitlab-workhorse ### Install gitlab-workhorse
...@@ -442,7 +442,7 @@ which is the recommended location. ...@@ -442,7 +442,7 @@ which is the recommended location.
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
You can specify a different Git repository by providing it as an extra paramter: You can specify a different Git repository by providing it as an extra parameter:
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production
...@@ -486,7 +486,7 @@ Make GitLab start on boot: ...@@ -486,7 +486,7 @@ Make GitLab start on boot:
# Fetch Gitaly source with Git and compile with Go # Fetch Gitaly source with Git and compile with Go
sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly]" RAILS_ENV=production sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly]" RAILS_ENV=production
You can specify a different Git repository by providing it as an extra paramter: You can specify a different Git repository by providing it as an extra parameter:
sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,https://example.com/gitaly.git]" RAILS_ENV=production sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,https://example.com/gitaly.git]" RAILS_ENV=production
...@@ -656,7 +656,7 @@ Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab- ...@@ -656,7 +656,7 @@ Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-
### Adding your Trusted Proxies ### Adding your Trusted Proxies
If you are using a reverse proxy on an separate machine, you may want to add the If you are using a reverse proxy on a separate machine, you may want to add the
proxy to the trusted proxies list. Otherwise users will appear signed in from the proxy to the trusted proxies list. Otherwise users will appear signed in from the
proxy's IP address. proxy's IP address.
......
...@@ -14,7 +14,7 @@ This article will show you how to install Git on macOS, Ubuntu Linux and Windows ...@@ -14,7 +14,7 @@ This article will show you how to install Git on macOS, Ubuntu Linux and Windows
Although it is easy to use the version of Git shipped with macOS Although it is easy to use the version of Git shipped with macOS
or install the latest version of Git on macOS by downloading it from the project website, or install the latest version of Git on macOS by downloading it from the project website,
we recommend installing it via Homebrew to get access to we recommend installing it via Homebrew to get access to
an extensive selection of dependancy managed libraries and applications. an extensive selection of dependency managed libraries and applications.
If you are sure you don't need access to any additional development libraries If you are sure you don't need access to any additional development libraries
or don't have approximately 15gb of available disk space for Xcode and Homebrew or don't have approximately 15gb of available disk space for Xcode and Homebrew
......
...@@ -229,7 +229,7 @@ Our free on Premise solution with >100,000 users ...@@ -229,7 +229,7 @@ Our free on Premise solution with >100,000 users
### GitLab CI ### GitLab CI
Our own Continuos Integration [feature](https://about.gitlab.com/gitlab-ci/) that is shipped with each instance Our own Continuous Integration [feature](https://about.gitlab.com/gitlab-ci/) that is shipped with each instance
### GitLab EE ### GitLab EE
......
...@@ -147,7 +147,7 @@ change which will be helpful is the database name for which we can use ...@@ -147,7 +147,7 @@ change which will be helpful is the database name for which we can use
## ElastiCache ## ElastiCache
EC is an in-memory hosted caching solution. Redis maintains its own EC is an in-memory hosted caching solution. Redis maintains its own
persistance and is used for certain types of application. persistence and is used for certain types of application.
Let's choose the ElastiCache service in the Database section from our Let's choose the ElastiCache service in the Database section from our
AWS console. Now lets create a cache subnet group which will be very AWS console. Now lets create a cache subnet group which will be very
...@@ -311,7 +311,7 @@ Here is a tricky part though, when adding subnets we need to associate ...@@ -311,7 +311,7 @@ Here is a tricky part though, when adding subnets we need to associate
public subnets instead of the private ones where our instances will public subnets instead of the private ones where our instances will
actually live. actually live.
On the secruity group section let's create a new one named On the security group section let's create a new one named
`gitlab-loadbalancer-sec-group` and allow both HTTP ad HTTPS traffic `gitlab-loadbalancer-sec-group` and allow both HTTP ad HTTPS traffic
from anywhere. from anywhere.
......
...@@ -212,7 +212,7 @@ Sign in and re-enable two-factor authentication as soon as possible. ...@@ -212,7 +212,7 @@ Sign in and re-enable two-factor authentication as soon as possible.
For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`: For example, if a user is trying to access a GitLab instance from `first.host.xyz` and `second.host.xyz`:
- The user logs in via `first.host.xyz` and registers their U2F key. - The user logs in via `first.host.xyz` and registers their U2F key.
- The user logs out and attempts to log in via `first.host.xyz` - U2F authentication suceeds. - The user logs out and attempts to log in via `first.host.xyz` - U2F authentication succeeds.
- The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because - The user logs out and attempts to log in via `second.host.xyz` - U2F authentication fails, because
the U2F key has only been registered on `first.host.xyz`. the U2F key has only been registered on `first.host.xyz`.
......
...@@ -10,7 +10,7 @@ In the _Recipients_ area, provide a list of emails separated by commas. ...@@ -10,7 +10,7 @@ In the _Recipients_ area, provide a list of emails separated by commas.
You can configure any of the following settings depending on your preference. You can configure any of the following settings depending on your preference.
+ **Push events** - Email will be triggered when a push event is recieved + **Push events** - Email will be triggered when a push event is received
+ **Tag push events** - Email will be triggered when a tag is created and pushed + **Tag push events** - Email will be triggered when a tag is created and pushed
+ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`). + **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`).
+ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body. + **Disable code diffs** - Don't include possibly sensitive code diffs in notification body.
......
...@@ -316,7 +316,7 @@ X-Gitlab-Event: Issue Hook ...@@ -316,7 +316,7 @@ X-Gitlab-Event: Issue Hook
Triggered when a new comment is made on commits, merge requests, issues, and code snippets. Triggered when a new comment is made on commits, merge requests, issues, and code snippets.
The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The The note data will be stored in `object_attributes` (e.g. `note`, `noteable_type`). The
payload will also include information about the target of the comment. For example, payload will also include information about the target of the comment. For example,
a comment on a issue will include the specific issue information under the `issue` key. a comment on an issue will include the specific issue information under the `issue` key.
Valid target types: Valid target types:
1. `commit` 1. `commit`
......
...@@ -40,7 +40,7 @@ issues around that same idea. ...@@ -40,7 +40,7 @@ issues around that same idea.
You do that as explained above, when You do that as explained above, when
[mentioning an issue from a commit message](#from-commit-messages). [mentioning an issue from a commit message](#from-commit-messages).
When mentioning the issue "A" in a issue "B", the issue "A" will also When mentioning the issue "A" in issue "B", the issue "A" will also
display a notification in its tracker. The same is valid for mentioning display a notification in its tracker. The same is valid for mentioning
issues in merge requests. issues in merge requests.
......
...@@ -52,7 +52,7 @@ special options available when filtering by milestone: ...@@ -52,7 +52,7 @@ special options available when filtering by milestone:
The milestone sidebar shows percentage complete, start date and due date, The milestone sidebar shows percentage complete, start date and due date,
issues, total issue weight, total issue time spent, and merge requests. issues, total issue weight, total issue time spent, and merge requests.
The percentage complete is calcualted as: Closed and merged merge requests plus all closed issues divided by The percentage complete is calculated as: Closed and merged merge requests plus all closed issues divided by
total merge requests and issues. total merge requests and issues.
![Milestone sidebar](img/sidebar.png) ![Milestone sidebar](img/sidebar.png)
......
...@@ -91,7 +91,7 @@ to steal the tokens of other jobs. ...@@ -91,7 +91,7 @@ to steal the tokens of other jobs.
Since 9.0 [pipeline triggers][triggers] do support the new permission model. Since 9.0 [pipeline triggers][triggers] do support the new permission model.
The new triggers do impersonate their associated user including their access The new triggers do impersonate their associated user including their access
to projects and their project permissions. To migrate trigger to use new permisison to projects and their project permissions. To migrate trigger to use new permission
model use **Take ownership**. model use **Take ownership**.
## Before GitLab 8.12 ## Before GitLab 8.12
......
...@@ -373,7 +373,7 @@ configuration. ...@@ -373,7 +373,7 @@ configuration.
If the case of `404.html`, there are different scenarios. For example: If the case of `404.html`, there are different scenarios. For example:
- If you use project Pages (served under `/projectname/`) and try to access - If you use project Pages (served under `/projectname/`) and try to access
`/projectname/non/exsiting_file`, GitLab Pages will try to serve first `/projectname/non/existing_file`, GitLab Pages will try to serve first
`/projectname/404.html`, and then `/404.html`. `/projectname/404.html`, and then `/404.html`.
- If you use user/group Pages (served under `/`) and try to access - If you use user/group Pages (served under `/`) and try to access
`/non/existing_file` GitLab Pages will try to serve `/404.html`. `/non/existing_file` GitLab Pages will try to serve `/404.html`.
......
...@@ -104,3 +104,33 @@ You won't receive notifications for Issues, Merge Requests or Milestones ...@@ -104,3 +104,33 @@ You won't receive notifications for Issues, Merge Requests or Milestones
created by yourself. You will only receive automatic notifications when created by yourself. You will only receive automatic notifications when
somebody else comments or adds changes to the ones that you've created or somebody else comments or adds changes to the ones that you've created or
mentions you. mentions you.
### Email Headers
Notification emails include headers that provide extra content about the notification received:
| Header | Description |
|-----------------------------|-------------------------------------------------------------------------|
| X-GitLab-Project | The name of the project the notification belongs to |
| X-GitLab-Project-Id | The ID of the project |
| X-GitLab-Project-Path | The path of the project |
| X-GitLab-(Resource)-ID | The ID of the resource the notification is for, where resource is `Issue`, `MergeRequest`, `Commit`, etc|
| X-GitLab-Discussion-ID | Only in comment emails, the ID of the discussion the comment is from |
| X-GitLab-Pipeline-Id | Only in pipeline emails, the ID of the pipeline the notification is for |
| X-GitLab-Reply-Key | A unique token to support reply by email |
| X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc |
#### X-GitLab-NotificationReason
This header holds the reason for the notification to have been sent out,
where reason can be `mentioned`, `assigned`, `own_activity`, etc.
Only one reason is sent out according to its priority:
- `own_activity`
- `assigned`
- `mentioned`
The reason in this header will also be shown in the footer of the notification email. For example an email with the
reason `assigned` will have this sentence in the footer:
`"You are receiving this email because you have been assigned an item on {configured GitLab hostname}"`
**Note: Only reasons listed above have been implemented so far**
Further implementation is [being discussed here](https://gitlab.com/gitlab-org/gitlab-ce/issues/42062)
...@@ -76,9 +76,9 @@ module API ...@@ -76,9 +76,9 @@ module API
def present_projects(projects, options = {}) def present_projects(projects, options = {})
projects = reorder_projects(projects) projects = reorder_projects(projects)
projects = projects.with_statistics if params[:statistics] projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
projects = projects.with_issues_enabled if params[:with_issues_enabled]
projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled] projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
projects = projects.with_statistics if params[:statistics]
projects = paginate(projects) projects = paginate(projects)
if current_user if current_user
......
...@@ -50,7 +50,7 @@ module Banzai ...@@ -50,7 +50,7 @@ module Banzai
end end
def process_link_to_upload_attr(html_attr) def process_link_to_upload_attr(html_attr)
path_parts = [html_attr.value] path_parts = [Addressable::URI.unescape(html_attr.value)]
if group if group
path_parts.unshift(relative_url_root, 'groups', group.full_path, '-') path_parts.unshift(relative_url_root, 'groups', group.full_path, '-')
...@@ -58,13 +58,13 @@ module Banzai ...@@ -58,13 +58,13 @@ module Banzai
path_parts.unshift(relative_url_root, project.full_path) path_parts.unshift(relative_url_root, project.full_path)
end end
path = File.join(*path_parts) path = Addressable::URI.escape(File.join(*path_parts))
html_attr.value = html_attr.value =
if context[:only_path] if context[:only_path]
path path
else else
URI.join(Gitlab.config.gitlab.base_url, path).to_s Addressable::URI.join(Gitlab.config.gitlab.base_url, path).to_s
end end
end end
......
...@@ -33,9 +33,9 @@ module Gitlab ...@@ -33,9 +33,9 @@ module Gitlab
object object
end end
def initialize(repository, name, target, derefenced_target) def initialize(repository, name, target, dereferenced_target)
@name = Gitlab::Git.ref_name(name) @name = Gitlab::Git.ref_name(name)
@dereferenced_target = derefenced_target @dereferenced_target = dereferenced_target
@target = if target.respond_to?(:oid) @target = if target.respond_to?(:oid)
target.oid target.oid
elsif target.respond_to?(:name) elsif target.respond_to?(:name)
......
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