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 = {
(jqXHR, textStatus, errorThrown) => {
const error = new Error(`${options.url}: ${errorThrown}`);
error.textStatus = textStatus;
if (jqXHR && jqXHR.responseJSON) error.responseJSON = jqXHR.responseJSON;
reject(error);
},
);
......
......@@ -65,7 +65,17 @@ export default class CreateItemDropdown {
getData(term, callback) {
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 */
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 LineHighlighter from './line_highlighter';
import MergeRequest from './merge_request';
import Sidebar from './right_sidebar';
import IssuableTemplateSelectors from './templates/issuable_template_selectors';
import Flash from './flash';
import UserCallout from './user_callout';
import BlobViewer from './blob/viewer/index';
......@@ -216,14 +212,16 @@ import SearchAutocomplete from './search_autocomplete';
.then(callDefault)
.catch(fail);
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':
new Diff();
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'), true);
new IssuableForm($('.merge-request-form'));
new LabelsSelect();
new MilestoneSelect();
new IssuableTemplateSelectors();
import('./pages/projects/merge_requests/edit')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
break;
case 'projects:tags:new':
import('./pages/projects/tags/new')
......
......@@ -10,6 +10,7 @@ const hideFlash = (flashEl, fadeTransition = true) => {
flashEl.addEventListener('transitionend', () => {
flashEl.remove();
if (document.body.classList.contains('flash-shown')) document.body.classList.remove('flash-shown');
}, {
once: true,
passive: true,
......@@ -64,6 +65,7 @@ const createFlash = function createFlash(
parent = document,
actionConfig = null,
fadeTransition = true,
addBodyClass = false,
) {
const flashContainer = parent.querySelector('.flash-container');
......@@ -86,6 +88,8 @@ const createFlash = function createFlash(
flashContainer.style.display = 'block';
if (addBodyClass) document.body.classList.add('flash-shown');
return flashContainer;
};
......
......@@ -68,12 +68,8 @@ export default {
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.submitCommitsLoading = false;
this.$store.dispatch('getTreeData', {
projectId: this.currentProjectId,
branch: this.currentBranchId,
endpoint: `/tree/${this.currentBranchId}`,
force: true,
});
this.commitMessage = '';
this.startNewMR = false;
})
.catch(() => {
this.submitCommitsLoading = false;
......@@ -153,6 +149,7 @@ you started editing. Would you like to create a new branch?`)"
type="submit"
:disabled="commitButtonDisabled"
class="btn btn-default btn-sm append-right-10 prepend-left-10"
:class="{ disabled: submitCommitsLoading }"
>
<i
v-if="submitCommitsLoading"
......
......@@ -70,7 +70,10 @@ export default {
this.editor.createInstance(this.$refs.editor);
})
.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() {
if (!this.activeFile) return;
......
......@@ -35,9 +35,12 @@
return this.file.type === 'tree';
},
levelIndentation() {
if (this.file.level > 0) {
return {
marginLeft: `${this.file.level * 16}px`,
};
}
return {};
},
shortId() {
return this.file.id.substr(0, 8);
......@@ -111,7 +114,7 @@
/>
<i
class="fa"
v-if="changedClass"
v-if="file.changed || file.tempFile"
:class="changedClass"
aria-hidden="true"
>
......
......@@ -84,13 +84,13 @@ router.beforeEach((to, from, next) => {
}
})
.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;
});
}
})
.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;
});
}
......
......@@ -55,7 +55,7 @@ export default class Editor {
attachModel(model) {
this.instance.setModel(model.getModel());
this.dirtyDiffController.attachModel(model);
if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
this.currentModel = model;
......@@ -68,7 +68,7 @@ export default class Editor {
return acc;
}, {}));
this.dirtyDiffController.reDecorate(model);
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
clearEditor() {
......
......@@ -3,6 +3,7 @@ import { visitUrl } from '../../lib/utils/url_utility';
import flash from '../../flash';
import service from '../services';
import * as types from './mutation_types';
import { stripHtml } from '../../lib/utils/text_utility';
export const redirectToUrl = (_, url) => visitUrl(url);
......@@ -81,7 +82,7 @@ export const checkCommitStatus = ({ state }) =>
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 = (
{ commit, state, dispatch, getters },
......@@ -92,7 +93,7 @@ export const commitChanges = (
.then((data) => {
const { branch } = payload;
if (!data.short_id) {
flash(data.message);
flash(data.message, 'alert', document, null, false, true);
return;
}
......@@ -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(
`Your changes have been committed. Commit ${data.short_id} with ${
data.stats.additions
} additions, ${data.stats.deletions} deletions.`,
commitMsg,
'notice',
);
document,
null,
false,
true);
window.dispatchEvent(new Event('resize'));
if (newMr) {
dispatch('discardAllChanges');
dispatch(
'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 {
commit(types.SET_BRANCH_WORKING_REFERENCE, {
......@@ -134,12 +141,18 @@ export const commitChanges = (
});
dispatch('discardAllChanges');
dispatch('closeAllFiles');
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 = (
{ state, dispatch },
......
......@@ -17,7 +17,7 @@ export const getBranchData = (
resolve(data);
})
.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}`));
});
} else {
......
......@@ -69,7 +69,7 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
})
.catch(() => {
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
.then((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 }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
};
export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
if (state.selectedFile) {
commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
}
};
export const setFileEOL = ({ state, commit }, { eol }) => {
if (state.selectedFile) {
commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
}
};
export const setEditorPosition = ({ state, commit }, { 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 = '' }) => {
......@@ -112,7 +118,7 @@ export const createTempFile = ({ state, commit, dispatch }, { projectId, branchI
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, {
parent,
......
......@@ -18,7 +18,7 @@ export const getProjectData = (
resolve(data);
})
.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}`));
});
} else {
......
......@@ -52,7 +52,7 @@ export const getTreeData = (
resolve(data);
})
.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);
reject(e);
});
......@@ -151,7 +151,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
dispatch('getLastCommitData', tree);
})
.catch(() => flash('Error fetching log data.'));
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
export const updateDirectoryData = (
......
......@@ -64,7 +64,7 @@ export default {
},
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: '',
content: file.raw,
changed: false,
});
},
......
......@@ -152,6 +152,13 @@
hasUpdated() {
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() {
this.service = new Service(this.endpoint);
......@@ -176,6 +183,8 @@
}
});
window.addEventListener('beforeunload', this.handleBeforeUnloadEvent);
eventHub.$on('delete.issuable', this.deleteIssuable);
eventHub.$on('update.issuable', this.updateIssuable);
eventHub.$on('close.form', this.closeForm);
......@@ -186,8 +195,17 @@
eventHub.$off('update.issuable', this.updateIssuable);
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
},
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() {
if (!this.showForm) {
this.showForm = true;
......
......@@ -72,4 +72,4 @@ export function capitalizeFirstCharacter(text) {
* @param {*} replace
* @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
export { default as ClosedState } from './components/states/mr_widget_closed';
export { default as MergingState } from './components/states/mr_widget_merging';
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 NothingToMergeState } from './components/states/mr_widget_nothing_to_merge';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch';
......
......@@ -2,6 +2,9 @@ import {
Vue,
mrWidgetOptions,
} from './dependencies';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
......
......@@ -33,7 +33,7 @@
<template>
<component
:is="rootElementType"
class="text-center">
class="loading-container text-center">
<i
class="fa fa-spin fa-spinner"
:class="cssClass"
......
......@@ -174,12 +174,13 @@
&.user-authored {
cursor: default;
opacity: 0.65;
background-color: $gray-light;
border-color: $theme-gray-200;
color: $gl-text-color-disabled;
&:hover,
&:active {
background-color: $white-light;
border-color: $border-color;
gl-emoji {
opacity: 0.4;
filter: grayscale(100%);
}
}
......
......@@ -220,14 +220,6 @@
@include btn-with-margin;
}
&.disabled {
pointer-events: auto !important;
}
&[disabled] {
pointer-events: none !important;
}
.fa-caret-down,
.fa-chevron-down {
margin-left: 5px;
......@@ -450,3 +442,28 @@
.btn-svg 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 @@
border-radius: $border-radius-base;
white-space: nowrap;
&[disabled] {
opacity: .65;
cursor: not-allowed;
}
&.no-outline {
outline: 0;
}
......
......@@ -17,6 +17,8 @@
*/
@mixin markdown-table {
width: auto;
display: block;
overflow-x: auto;
}
/*
......
......@@ -164,6 +164,7 @@ $gl-text-color-tertiary: #949494;
$gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, .85);
$gl-text-color-disabled: #919191;
$gl-text-green: $green-600;
$gl-text-green-hover: $green-700;
$gl-text-red: $red-500;
......@@ -258,6 +259,8 @@ $general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
$performance-bar-height: 35px;
$flash-height: 52px;
$context-header-height: 60px;
/*
* Common component specific colors
......
......@@ -391,11 +391,17 @@
.dropdown-toggle {
float: right;
.toggle-icon {
i {
color: $white-light;
padding-right: 2px;
margin-top: 2px;
}
&[disabled] {
i {
color: $gl-text-color-disabled;
}
}
}
.dropdown-menu {
......
......@@ -107,6 +107,11 @@ table.table tr td.multi-file-table-name {
vertical-align: middle;
margin-right: 2px;
}
.loading-container {
margin-right: 4px;
display: inline-block;
}
}
.multi-file-table-col-commit-message {
......@@ -247,7 +252,6 @@ table.table tr td.multi-file-table-name {
display: flex;
position: relative;
flex-direction: column;
height: 100%;
width: 290px;
padding: 0;
background-color: $gray-light;
......@@ -256,6 +260,11 @@ table.table tr td.multi-file-table-name {
.projects-sidebar {
display: flex;
flex-direction: column;
.context-header {
width: auto;
margin-right: 0;
}
}
.multi-file-commit-panel-inner {
......@@ -496,19 +505,70 @@ table.table tr td.multi-file-table-name {
}
}
.ide-flash-container.flash-container {
.ide.nav-only {
.flash-container {
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 {
.ide-flash-container.flash-container {
margin-top: $header-height + $performance-bar-height;
.with-performance-bar .ide.nav-only {
.flash-container {
margin-top: #{$header-height + $performance-bar-height};
}
.content {
margin-top: #{$header-height + $performance-bar-height};
}
.ide-view {
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
'text-align:center'
].join(';')
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
require 'webpack/rails/manifest'
module WebpackHelper
def webpack_bundle_tag(bundle)
javascript_include_tag(*gitlab_webpack_asset_paths(bundle))
def webpack_bundle_tag(bundle, force_same_domain: false)
javascript_include_tag(*gitlab_webpack_asset_paths(bundle, force_same_domain: true))
end
# 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?
paths = Webpack::Rails::Manifest.asset_paths(source)
......@@ -14,10 +14,12 @@ module WebpackHelper
paths.select! { |p| p.ends_with? ".#{extension}" }
end
unless force_same_domain
force_host = webpack_public_host
if force_host
paths.map! { |p| "#{force_host}#{p}" }
end
end
paths
end
......
module Emails
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)
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
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)
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
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)
@previous_assignees = []
@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
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)
@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
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)
@label_names = label_names
@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
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)
@issue_status = status
@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
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)
@new_issue = new_issue
@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
private
......@@ -61,11 +61,12 @@ module Emails
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end
def issue_thread_options(sender_id, recipient_id)
def issue_thread_options(sender_id, recipient_id, reason)
{
from: sender(sender_id),
to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})")
subject: subject("#{@issue.title} (##{@issue.iid})"),
'X-GitLab-NotificationReason' => reason
}
end
end
......
module Emails
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)
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
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)
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
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)
@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
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)
@label_names = label_names
@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
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)
@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
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)
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
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)
@mr_status = status
@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
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)
@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
private
......@@ -64,11 +64,12 @@ module Emails
@sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
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),
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
......
......@@ -112,6 +112,8 @@ class Notify < BaseMailer
headers["X-GitLab-#{model.class.name}-ID"] = model.id
headers['X-GitLab-Reply-Key'] = reply_key
@reason = headers['X-GitLab-NotificationReason']
if Gitlab::IncomingEmail.enabled? && @sent_notification
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
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
attr_reader :user, :type
def initialize(
user, type,
custom_action: nil,
target: nil,
acting_user: nil,
project: nil,
group: nil,
skip_read_ability: false
)
attr_reader :user, :type, :reason
def initialize(user, type, **opts)
unless NotificationSetting.levels.key?(type) || type == :subscription
raise ArgumentError, "invalid type: #{type.inspect}"
end
@custom_action = custom_action
@acting_user = acting_user
@target = target
@project = project || default_project
@group = group || @project&.group
@custom_action = opts[:custom_action]
@acting_user = opts[:acting_user]
@target = opts[:target]
@project = opts[:project] || default_project
@group = opts[:group] || @project&.group
@user = user
@type = type
@skip_read_ability = skip_read_ability
@reason = opts[:reason]
@skip_read_ability = opts[:skip_read_ability]
end
def notification_setting
......@@ -77,9 +69,15 @@ class NotificationRecipient
def own_activity?
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
def has_access?
......
......@@ -314,6 +314,7 @@ class Project < ActiveRecord::Base
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
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) }
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
......
......@@ -11,11 +11,11 @@ module NotificationRecipientService
end
def self.build_recipients(*a)
Builder::Default.new(*a).recipient_users
Builder::Default.new(*a).notification_recipients
end
def self.build_new_note_recipients(*a)
Builder::NewNote.new(*a).recipient_users
Builder::NewNote.new(*a).notification_recipients
end
module Builder
......@@ -49,25 +49,24 @@ module NotificationRecipientService
@recipients ||= []
end
def <<(pair)
users, type = pair
def add_recipients(users, type, reason)
if users.is_a?(ActiveRecord::Relation)
users = users.includes(:notification_settings)
end
users = Array(users)
users.compact!
recipients.concat(users.map { |u| make_recipient(u, type) })
recipients.concat(users.map { |u| make_recipient(u, type, reason) })
end
def user_scope
User.includes(:notification_settings)
end
def make_recipient(user, type)
def make_recipient(user, type, reason)
NotificationRecipient.new(
user, type,
reason: reason,
project: project,
custom_action: custom_action,
target: target,
......@@ -75,14 +74,13 @@ module NotificationRecipientService
)
end
def recipient_users
@recipient_users ||=
def notification_recipients
@notification_recipients ||=
begin
build!
filter!
users = recipients.map(&:user)
users.uniq!
users.freeze
recipients = self.recipients.sort_by { |r| NotificationReason.priority(r.reason) }.uniq(&:user)
recipients.freeze
end
end
......@@ -95,13 +93,13 @@ module NotificationRecipientService
def add_participants(user)
return unless target.respond_to?(:participants)
self << [target.participants(user), :participating]
add_recipients(target.participants(user), :participating, nil)
end
def add_mentions(user, target:)
return unless target.respond_to?(:mentioned_users)
self << [target.mentioned_users(user), :mention]
add_recipients(target.mentioned_users(user), :mention, NotificationReason::MENTIONED)
end
# Get project/group users with CUSTOM notification level
......@@ -119,11 +117,11 @@ module NotificationRecipientService
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)
self << [user_scope.where(id: user_ids), :watch]
add_recipients(user_scope.where(id: user_ids), :watch, nil)
end
def add_project_watchers
self << [project_watchers, :watch]
add_recipients(project_watchers, :watch, nil)
end
# Get project users with WATCH notification level
......@@ -144,7 +142,7 @@ module NotificationRecipientService
def add_subscribed_users
return unless target.respond_to? :subscribers
self << [target.subscribers(project), :subscription]
add_recipients(target.subscribers(project), :subscription, nil)
end
def user_ids_notifiable_on(resource, notification_level = nil)
......@@ -195,7 +193,7 @@ module NotificationRecipientService
return unless target.respond_to? :labels
(labels || target.labels).each do |label|
self << [label.subscribers(project), :subscription]
add_recipients(label.subscribers(project), :subscription, nil)
end
end
end
......@@ -222,12 +220,12 @@ module NotificationRecipientService
# Re-assign is considered as a mention of the new assignee
case custom_action
when :reassign_merge_request
self << [previous_assignee, :mention]
self << [target.assignee, :mention]
add_recipients(previous_assignee, :mention, nil)
add_recipients(target.assignee, :mention, NotificationReason::ASSIGNED)
when :reassign_issue
previous_assignees = Array(previous_assignee)
self << [previous_assignees, :mention]
self << [target.assignees, :mention]
add_recipients(previous_assignees, :mention, nil)
add_recipients(target.assignees, :mention, NotificationReason::ASSIGNED)
end
add_subscribed_users
......@@ -238,6 +236,12 @@ module NotificationRecipientService
# receive them, too.
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
end
end
......
......@@ -85,10 +85,11 @@ class NotificationService
recipients.each do |recipient|
mailer.send(
:reassigned_issue_email,
recipient.id,
recipient.user.id,
issue.id,
previous_assignee_ids,
current_user.id
current_user.id,
recipient.reason
).deliver_later
end
end
......@@ -176,7 +177,7 @@ class NotificationService
action: "resolve_all_discussions")
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
......@@ -199,7 +200,7 @@ class NotificationService
recipients = NotificationRecipientService.build_new_note_recipients(note)
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
......@@ -299,7 +300,7 @@ class NotificationService
recipients = NotificationRecipientService.build_recipients(issue, current_user, action: 'moved')
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
end
......@@ -339,16 +340,16 @@ class NotificationService
recipients = NotificationRecipientService.build_recipients(target, target.author, action: "new")
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
def new_mentions_in_resource_email(target, new_mentioned_users, current_user, method)
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|
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
......@@ -363,7 +364,7 @@ class NotificationService
)
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
......@@ -381,10 +382,11 @@ class NotificationService
recipients.each do |recipient|
mailer.send(
method,
recipient.id,
recipient.user.id,
target.id,
previous_assignee_id,
current_user.id
current_user.id,
recipient.reason
).deliver_later
end
end
......@@ -408,7 +410,7 @@ class NotificationService
recipients = NotificationRecipientService.build_recipients(target, current_user, action: "reopen")
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
......
- @body_class = 'ide'
- page_title 'IDE'
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
= webpack_bundle_tag 'ide'
.ide-flash-container.flash-container
= webpack_bundle_tag 'ide', force_same_domain: true
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
.text-center
......
!!! 5
%html{ lang: I18n.locale, class: page_class }
= 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 "layouts/header/default"
= render 'shared/outdated_browser'
......@@ -10,4 +10,5 @@
= render "layouts/broadcast"
= yield :flash_message
= render "layouts/flash"
.content{ id: "content-body" }
= yield
......@@ -20,7 +20,7 @@
#{link_to "View it on GitLab", @target_url}.
%br
-# 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 @labels_url
adjust your #{link_to 'label subscriptions', @labels_url}.
......
......@@ -9,4 +9,4 @@
<% 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 = {
{
test: /\_worker\.js$/,
use: [
{ loader: 'worker-loader' },
{
loader: 'worker-loader',
options: {
inline: true
}
},
{ 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 @@
#
# 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
enable_extension "plpgsql"
......@@ -1538,8 +1538,6 @@ ActiveRecord::Schema.define(version: 20180105212544) do
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_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
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.
1. [Reconfigure][reconf] or [restart] GitLab for Omnibus and installations
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
......
......@@ -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
communicate securely. By default omnibus-gitlab will generate one keypair,
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
`/etc/gitlab/gitlab.rb`
......
......@@ -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
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.
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
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
```
......
......@@ -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 |
| `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
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:
| --------- | ---- | -------- | ----------- |
| `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 |
| `note_id` | integer | yes | The ID of an note |
| `note_id` | integer | yes | The ID of a note |
```bash
......@@ -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 |
| `issue_iid` | integer | yes | The internal ID of an issue |
| `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
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
Parameters:
- `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
- `due_date` (optional) - The due 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
| `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. |
| `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 |
| `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 |
......
......@@ -70,7 +70,7 @@ POST /projects/:id/milestones
Parameters:
- `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
- `due_date` (optional) - The due date of the milestone
- `start_date` (optional) - The start date of the milestone
......
......@@ -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
- `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
{
......
......@@ -982,7 +982,7 @@ Example response:
## Unarchive a project
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
......
......@@ -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 `\.`.
- `public`
- 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.
......
......@@ -167,7 +167,7 @@ Finally, push to GitLab and let the tests begin!
### Test against different PHP versions in Shell builds
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.
You will have to install it on your build machine under the `gitlab-runner`
......@@ -227,7 +227,7 @@ following in your `.gitlab-ci.yml`:
...
# 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.
cache:
paths:
......
......@@ -42,7 +42,7 @@ production:
This project has three jobs:
1. `test` - used to test Django application,
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
......
......@@ -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)
Notice that there are two jobs pending which are named after what we wrote in
`.gitlab-ci.yml`. The red triangle indicates that there is no Runner configured
yet for these jobs.
Notice that there is a pending job which is named after what we wrote in
`.gitlab-ci.yml`. "stuck" indicates that there is no Runner configured
yet for this job.
The next step is to configure a Runner so that it picks the pending jobs.
......
......@@ -194,7 +194,7 @@ before_script:
##
## 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
## you will overwrite your user's SSH config.
##
......
......@@ -87,7 +87,7 @@ future GitLab releases.**
## 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
release.
......
......@@ -61,7 +61,7 @@ against EE.
1. Tries to apply it to current EE `master`
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
`master`.
At this point if you retry the failing job in your CE merge request, it should
......
......@@ -123,7 +123,7 @@ roughly be as follows:
scheduling jobs for newly created data.
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
jobs before continueing.
jobs before continuing.
1. Remove the old column.
## Example
......
......@@ -12,9 +12,9 @@ following format:
```yaml
---
title: "Going through change[log]s"
title: "Change[log]s"
merge_request: 1972
author: Ozzy Osbourne
author: Black Sabbath
type: added
```
......
......@@ -420,7 +420,7 @@ the style below as a guide:
In this case:
- 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
- the code blocks are indented one or more spaces under the list item to render
correctly
......
......@@ -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
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
mock.onGet('/users').reply(200, { foo: 'bar' }, {});
......
......@@ -456,7 +456,7 @@ describe('Todos App', () => {
});
```
#### `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
import Vue from 'vue';
......
......@@ -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
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
migrations are written carefully, can be applied online and adhere to the style
guide below.
......
......@@ -8,7 +8,7 @@ Note that if your db user does not have advanced privileges you must create the
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.
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:
- Use `.method` to describe class methods and `#method` to describe instance
methods.
- 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 follow the [Four-Phase Test][four-phase-test] pattern, using newlines
to separate phases.
......
......@@ -27,7 +27,7 @@ View the [interactive example](http://codepen.io/awhildy/full/GNyEvM/) here.
### 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.
......
......@@ -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)
#### 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)
......@@ -181,7 +181,7 @@ A count element is used in navigation contexts where it is helpful to indicate t
## 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
......@@ -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.
* **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.
* **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.
* 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
>**Example:**
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
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
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
has been assigned to. If you followed the process above, then Azure will have automatically created
......
......@@ -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:
* 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)
### Install gitlab-workhorse
......@@ -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
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
......@@ -486,7 +486,7 @@ Make GitLab start on boot:
# 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
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
......@@ -656,7 +656,7 @@ Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-
### 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's IP address.
......
......@@ -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
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
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
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
### 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
......
......@@ -147,7 +147,7 @@ change which will be helpful is the database name for which we can use
## ElastiCache
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
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
public subnets instead of the private ones where our instances will
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
from anywhere.
......
......@@ -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`:
- 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 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.
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
+ **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.
......
......@@ -316,7 +316,7 @@ X-Gitlab-Event: Issue Hook
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
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:
1. `commit`
......
......@@ -40,7 +40,7 @@ issues around that same idea.
You do that as explained above, when
[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
issues in merge requests.
......
......@@ -52,7 +52,7 @@ special options available when filtering by milestone:
The milestone sidebar shows percentage complete, start date and due date,
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.
![Milestone sidebar](img/sidebar.png)
......
......@@ -91,7 +91,7 @@ to steal the tokens of other jobs.
Since 9.0 [pipeline triggers][triggers] do support the new permission model.
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**.
## Before GitLab 8.12
......
......@@ -373,7 +373,7 @@ configuration.
If the case of `404.html`, there are different scenarios. For example:
- 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`.
- If you use user/group Pages (served under `/`) and try to access
`/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
created by yourself. You will only receive automatic notifications when
somebody else comments or adds changes to the ones that you've created or
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
def present_projects(projects, options = {})
projects = reorder_projects(projects)
projects = projects.with_statistics if params[:statistics]
projects = projects.with_issues_enabled if params[:with_issues_enabled]
projects = projects.with_issues_available_for_user(current_user) if params[:with_issues_enabled]
projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
projects = projects.with_statistics if params[:statistics]
projects = paginate(projects)
if current_user
......
......@@ -50,7 +50,7 @@ module Banzai
end
def process_link_to_upload_attr(html_attr)
path_parts = [html_attr.value]
path_parts = [Addressable::URI.unescape(html_attr.value)]
if group
path_parts.unshift(relative_url_root, 'groups', group.full_path, '-')
......@@ -58,13 +58,13 @@ module Banzai
path_parts.unshift(relative_url_root, project.full_path)
end
path = File.join(*path_parts)
path = Addressable::URI.escape(File.join(*path_parts))
html_attr.value =
if context[:only_path]
path
else
URI.join(Gitlab.config.gitlab.base_url, path).to_s
Addressable::URI.join(Gitlab.config.gitlab.base_url, path).to_s
end
end
......
......@@ -33,9 +33,9 @@ module Gitlab
object
end
def initialize(repository, name, target, derefenced_target)
def initialize(repository, name, target, dereferenced_target)
@name = Gitlab::Git.ref_name(name)
@dereferenced_target = derefenced_target
@dereferenced_target = dereferenced_target
@target = if target.respond_to?(:oid)
target.oid
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