diff options
author | Phil Hughes <me@iamphill.com> | 2018-04-05 10:46:19 +0100 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-04-05 10:46:19 +0100 |
commit | c02d344c4d13938e9e44c2c9051ddc73f26c0086 (patch) | |
tree | bedb5635131b78992e22435690ff0bc7e7918a42 /app | |
parent | 49f198e6c142c0bf6983187b164599388cd67197 (diff) | |
parent | 32d2206b01b97cdbd6cdc13b25d98c3d3db048c4 (diff) | |
download | gitlab-ce-c02d344c4d13938e9e44c2c9051ddc73f26c0086.tar.gz |
Merge branch 'master' into ide-staged-changes
Diffstat (limited to 'app')
107 files changed, 1039 insertions, 614 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 6da33a26e58..0e1ca7fe883 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -4,7 +4,7 @@ import $ from 'jquery'; import _ from 'underscore'; import Cookies from 'js-cookie'; import { __ } from './locale'; -import { isInIssuePage, isInMRPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils'; +import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils'; import flash from './flash'; import axios from './lib/utils/axios_utils'; @@ -300,7 +300,7 @@ class AwardsHandler { } isInVueNoteablePage() { - return isInIssuePage() || this.isVueMRDiscussions(); + return isInIssuePage() || isInEpicPage() || this.isVueMRDiscussions(); } getVotesBlock() { diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js index d78d4701974..7c90597f77c 100644 --- a/app/assets/javascripts/boards/services/board_service.js +++ b/app/assets/javascripts/boards/services/board_service.js @@ -19,7 +19,7 @@ export default class BoardService { } static generateIssuePath(boardId, id) { - return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`; + return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`; } all() { diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index e6390f0855b..d7e1de18d09 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager { this.filteredSearchInput = this.container.querySelector('.filtered-search'); this.page = page; this.groupsOnly = isGroup; - this.groupAncestor = isGroupAncestor; - this.isGroupDecendent = isGroupDecendent; + this.includeAncestorGroups = isGroupAncestor; + this.includeDescendantGroups = isGroupDecendent; this.setupMapping(); @@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager { } getLabelsEndpoint() { - const endpoint = `${this.baseEndpoint}/labels.json`; + let endpoint = `${this.baseEndpoint}/labels.json?`; + + if (this.groupsOnly) { + endpoint = `${endpoint}only_group_labels=true&`; + } + + if (this.includeAncestorGroups) { + endpoint = `${endpoint}include_ancestor_groups=true&`; + } + + if (this.includeDescendantGroups) { + endpoint = `${endpoint}include_descendant_groups=true`; + } return endpoint; } diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 71b7e80335b..cf5ba1e1771 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -21,7 +21,7 @@ export default class FilteredSearchManager { constructor({ page, isGroup = false, - isGroupAncestor = false, + isGroupAncestor = true, isGroupDecendent = false, filteredSearchTokenKeys = FilteredSearchTokenKeys, stateFiltersSelector = '.issues-state-filters', @@ -86,6 +86,7 @@ export default class FilteredSearchManager { page: this.page, isGroup: this.isGroup, isGroupAncestor: this.isGroupAncestor, + isGroupDecendent: this.isGroupDecendent, filteredSearchTokenKeys: this.filteredSearchTokenKeys, }); diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index 8f09d0c971a..ab88055fc36 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -3,7 +3,6 @@ import { mapActions } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import StageButton from './stage_button.vue'; import UnstageButton from './unstage_button.vue'; -import router from '../../ide_router'; export default { components: { @@ -26,17 +25,17 @@ export default { return this.file.tempFile ? 'file-addition' : 'file-modified'; }, iconClass() { - return `multi-file-${ - this.file.tempFile ? 'addition' : 'modified' - } append-right-8`; + return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`; }, }, methods: { - ...mapActions(['updateViewer', 'stageChange', 'unstageChange']), + ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']), openFileInEditor() { - this.updateViewer('diff'); - - router.push(`/project${this.file.url}`); + return this.openPendingTab(this.file).then(changeViewer => { + if (changeViewer) { + this.updateViewer('diff'); + } + }); }, fileAction() { if (this.file.staged) { diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 5e44af01241..d22869466c9 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -60,6 +60,7 @@ export default { v-if="activeFile" > <repo-tabs + :active-file="activeFile" :files="openFiles" :viewer="viewer" :has-changes="hasChanges" diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index b6f8f8a1c99..b1a16350c19 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -21,7 +21,8 @@ export default { }, watch: { file(oldVal, newVal) { - if (newVal.path !== this.file.path) { + // Compare key to allow for files opened in review mode to be cached differently + if (newVal.key !== this.file.key) { this.initMonaco(); } }, @@ -70,7 +71,7 @@ export default { }) .then(() => { const viewerPromise = this.delayViewerUpdated - ? this.updateViewer('editor') + ? this.updateViewer(this.file.pending ? 'diff' : 'editor') : Promise.resolve(); return viewerPromise; diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue index fab53592e4e..b7b8a8e989d 100644 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ b/app/assets/javascripts/ide/components/repo_file.vue @@ -62,11 +62,7 @@ export default { this.toggleTreeOpen(this.file.path); } - const delayPromise = this.file.changed - ? Promise.resolve() - : this.updateDelayViewerUpdated(true); - - return delayPromise.then(() => { + return this.updateDelayViewerUpdated(true).then(() => { router.push(`/project${this.file.url}`); }); }, diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index 7bb02bb5e26..729be292cc0 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -1,17 +1,17 @@ <script> import { mapActions } from 'vuex'; -import fileIcon from '~/vue_shared/components/file_icon.vue'; -import icon from '~/vue_shared/components/icon.vue'; -import fileStatusIcon from './repo_file_status_icon.vue'; -import changedFileIcon from './changed_file_icon.vue'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import FileStatusIcon from './repo_file_status_icon.vue'; +import ChangedFileIcon from './changed_file_icon.vue'; export default { components: { - fileStatusIcon, - fileIcon, - icon, - changedFileIcon, + FileStatusIcon, + FileIcon, + Icon, + ChangedFileIcon, }, props: { tab: { @@ -32,7 +32,7 @@ export default { return `Close ${this.tab.name}`; }, showChangedIcon() { - return this.fileHasChanged ? !this.tabMouseOver : false; + return this.tab.changed ? !this.tabMouseOver : false; }, fileHasChanged() { return this.tab.changed || this.tab.tempFile || this.tab.staged; @@ -40,9 +40,15 @@ export default { }, methods: { - ...mapActions(['closeFile']), + ...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']), clickFile(tab) { - this.$router.push(`/project${tab.url}`); + this.updateDelayViewerUpdated(true); + + if (tab.pending) { + this.openPendingTab(tab); + } else { + this.$router.push(`/project${tab.url}`); + } }, mouseOverTab() { if (this.fileHasChanged) { @@ -67,7 +73,7 @@ export default { <button type="button" class="multi-file-tab-close" - @click.stop.prevent="closeFile(tab.path)" + @click.stop.prevent="closeFile(tab)" :aria-label="closeLabel" > <icon @@ -83,7 +89,9 @@ export default { <div class="multi-file-tab" - :class="{active : tab.active }" + :class="{ + active: tab.active + }" :title="tab.url" > <file-icon diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue index a44e418b2eb..7bd646ba9b0 100644 --- a/app/assets/javascripts/ide/components/repo_tabs.vue +++ b/app/assets/javascripts/ide/components/repo_tabs.vue @@ -2,6 +2,7 @@ import { mapActions } from 'vuex'; import RepoTab from './repo_tab.vue'; import EditorMode from './editor_mode_dropdown.vue'; +import router from '../ide_router'; export default { components: { @@ -9,6 +10,10 @@ export default { EditorMode, }, props: { + activeFile: { + type: Object, + required: true, + }, files: { type: Array, required: true, @@ -38,7 +43,18 @@ export default { this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth; }, methods: { - ...mapActions(['updateViewer']), + ...mapActions(['updateViewer', 'removePendingTab']), + openFileViewer(viewer) { + this.updateViewer(viewer); + + if (this.activeFile.pending) { + return this.removePendingTab(this.activeFile).then(() => { + router.push(`/project${this.activeFile.url}`); + }); + } + + return null; + }, }, }; </script> @@ -60,7 +76,7 @@ export default { :show-shadow="showShadow" :has-changes="hasChanges" :merge-request-id="mergeRequestId" - @click="updateViewer" + @click="openFileViewer" /> </div> </template> diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js index be2c12c0487..20983666b4a 100644 --- a/app/assets/javascripts/ide/ide_router.js +++ b/app/assets/javascripts/ide/ide_router.js @@ -77,7 +77,11 @@ router.beforeEach((to, from, next) => { if (to.params[0]) { const path = to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0]; - const treeEntry = store.state.entries[path]; + const treeEntryKey = Object.keys(store.state.entries).find( + key => key === path && !store.state.entries[key].pending, + ); + const treeEntry = store.state.entries[treeEntryKey]; + if (treeEntry) { store.dispatch('handleTreeEntryAction', treeEntry); } diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js index f3c052d6903..da467cbd476 100644 --- a/app/assets/javascripts/ide/lib/common/model.js +++ b/app/assets/javascripts/ide/lib/common/model.js @@ -13,12 +13,12 @@ export default class Model { (this.originalModel = this.monaco.editor.createModel( this.file.raw, undefined, - new this.monaco.Uri(null, null, `original/${this.file.path}`), + new this.monaco.Uri(null, null, `original/${this.file.key}`), )), (this.model = this.monaco.editor.createModel( this.content, undefined, - new this.monaco.Uri(null, null, this.file.path), + new this.monaco.Uri(null, null, this.file.key), )), ); if (this.file.mrChange) { @@ -36,7 +36,7 @@ export default class Model { this.updateContent = this.updateContent.bind(this); this.dispose = this.dispose.bind(this); - eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose); + eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose); eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent); } @@ -53,7 +53,7 @@ export default class Model { } get path() { - return this.file.path; + return this.file.key; } getModel() { @@ -91,7 +91,7 @@ export default class Model { this.disposable.dispose(); this.events.clear(); - eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose); + eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose); eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent); } } diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js index 57d5e59a88b..0e7b563b5d6 100644 --- a/app/assets/javascripts/ide/lib/common/model_manager.js +++ b/app/assets/javascripts/ide/lib/common/model_manager.js @@ -9,17 +9,17 @@ export default class ModelManager { this.models = new Map(); } - hasCachedModel(path) { - return this.models.has(path); + hasCachedModel(key) { + return this.models.has(key); } - getModel(path) { - return this.models.get(path); + getModel(key) { + return this.models.get(key); } addModel(file) { - if (this.hasCachedModel(file.path)) { - return this.getModel(file.path); + if (this.hasCachedModel(file.key)) { + return this.getModel(file.key); } const model = new Model(this.monaco, file); @@ -27,7 +27,7 @@ export default class ModelManager { this.disposable.add(model); eventHub.$on( - `editor.update.model.dispose.${file.path}`, + `editor.update.model.dispose.${file.key}`, this.removeCachedModel.bind(this, file), ); @@ -35,12 +35,9 @@ export default class ModelManager { } removeCachedModel(file) { - this.models.delete(file.path); + this.models.delete(file.key); - eventHub.$off( - `editor.update.model.dispose.${file.path}`, - this.removeCachedModel, - ); + eventHub.$off(`editor.update.model.dispose.${file.key}`, this.removeCachedModel); } dispose() { diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index 3e512e79bba..cecb4d215ba 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -22,7 +22,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => { }; export const closeAllFiles = ({ state, dispatch }) => { - state.openFiles.forEach(file => dispatch('closeFile', file.path)); + state.openFiles.forEach(file => dispatch('closeFile', file)); }; export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => { diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 470d168e3e3..ef018935c7a 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -6,24 +6,34 @@ import * as types from '../mutation_types'; import router from '../../ide_router'; import { setPageTitle } from '../utils'; -export const closeFile = ({ commit, state, getters, dispatch }, path) => { - const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path); - const file = state.entries[path]; +export const closeFile = ({ commit, state, dispatch }, file) => { + const path = file.path; + const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key); const fileWasActive = file.active; - commit(types.TOGGLE_FILE_OPEN, path); - commit(types.SET_FILE_ACTIVE, { path, active: false }); + if (file.pending) { + commit(types.REMOVE_PENDING_TAB, file); + } else { + commit(types.TOGGLE_FILE_OPEN, path); + commit(types.SET_FILE_ACTIVE, { path, active: false }); + } if (state.openFiles.length > 0 && fileWasActive) { const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1; - const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path]; - - router.push(`/project${nextFileToOpen.url}`); + const nextFileToOpen = state.openFiles[nextIndexToOpen]; + + if (nextFileToOpen.pending) { + dispatch('updateViewer', 'diff'); + dispatch('openPendingTab', nextFileToOpen); + } else { + dispatch('updateDelayViewerUpdated', true); + router.push(`/project${nextFileToOpen.url}`); + } } else if (!state.openFiles.length) { router.push(`/project/${file.projectId}/tree/${file.branchId}/`); } - eventHub.$emit(`editor.update.model.dispose.${file.path}`); + eventHub.$emit(`editor.update.model.dispose.${file.key}`); }; export const setFileActive = ({ commit, state, getters, dispatch }, path) => { @@ -162,3 +172,23 @@ export const stageChange = ({ commit }, path) => { export const unstageChange = ({ commit }, path) => { commit(types.UNSTAGE_CHANGE, path); }; + +export const openPendingTab = ({ commit, getters, dispatch, state }, file) => { + if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') { + return false; + } + + commit(types.ADD_PENDING_TAB, { file }); + + dispatch('scrollToTab'); + + router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`); + + return true; +}; + +export const removePendingTab = ({ commit }, file) => { + commit(types.REMOVE_PENDING_TAB, file); + + eventHub.$emit(`editor.update.model.dispose.${file.key}`); +}; diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 118b4df33f8..e834ff5b598 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -55,3 +55,5 @@ export const STAGE_CHANGE = 'STAGE_CHANGE'; export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE'; export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT'; +export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; +export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index f4a9354b024..b52d1f96e28 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -5,6 +5,14 @@ export default { Object.assign(state.entries[path], { active, }); + + if (active && !state.entries[path].pending) { + Object.assign(state, { + openFiles: state.openFiles.map(f => + Object.assign(f, { active: f.pending ? false : f.active }), + ), + }); + } }, [types.TOGGLE_FILE_OPEN](state, path) { Object.assign(state.entries[path], { @@ -12,10 +20,14 @@ export default { }); if (state.entries[path].opened) { - state.openFiles.push(state.entries[path]); + Object.assign(state, { + openFiles: state.openFiles.filter(f => f.path !== path).concat(state.entries[path]), + }); } else { + const file = state.entries[path]; + Object.assign(state, { - openFiles: state.openFiles.filter(f => f.path !== path), + openFiles: state.openFiles.filter(f => f.key !== file.key), }); } }, @@ -141,4 +153,37 @@ export default { changed, }); }, + [types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) { + const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending); + let openFiles = state.openFiles.map(f => + Object.assign(f, { active: f.path === file.path, opened: false }), + ); + + if (!pendingTab) { + const openFile = openFiles.find(f => f.path === file.path); + + openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => { + if (!f) return acc; + + if (f.path === file.path) { + return acc.concat({ + ...f, + active: true, + pending: true, + opened: true, + key: `${keyPrefix}-${f.key}`, + }); + } + + return acc.concat(f); + }, []); + } + + Object.assign(state, { openFiles }); + }, + [types.REMOVE_PENDING_TAB](state, file) { + Object.assign(state, { + openFiles: state.openFiles.filter(f => f.key !== file.key), + }); + }, }; diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js index da0069b63a8..597fa3a1c10 100644 --- a/app/assets/javascripts/ide/stores/utils.js +++ b/app/assets/javascripts/ide/stores/utils.js @@ -1,5 +1,7 @@ export const dataStructure = () => ({ id: '', + // Key will contain a mixture of ID and path + // it can also contain a prefix `pending-` for files opened in review mode key: '', type: '', projectId: '', diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index 172de6b3679..af47056d98f 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -45,7 +45,7 @@ return `#${this.job.runner.id}`; }, hasTimeout() { - return this.job.metadata != null && this.job.metadata.timeout_human_readable !== ''; + return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null; }, timeout() { if (this.job.metadata == null) { diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 0830ebe9e4e..9ff2042475b 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -33,6 +33,7 @@ export const checkPageAndAction = (page, action) => { export const isInIssuePage = () => checkPageAndAction('issues', 'show'); export const isInMRPage = () => checkPageAndAction('merge_requests', 'show'); +export const isInEpicPage = () => checkPageAndAction('epics', 'show'); export const isInNoteablePage = () => isInIssuePage() || isInMRPage(); export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions'); diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 90dcafd75b7..648fa6ff804 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -99,6 +99,10 @@ export default { 'js-note-target-reopen': !this.isOpen, }; }, + supportQuickActions() { + // Disable quick actions support for Epics + return this.noteableType !== constants.EPIC_NOTEABLE_TYPE; + }, markdownDocsPath() { return this.getNotesData.markdownDocsPath; }, @@ -355,7 +359,7 @@ Please check your network connection and try again.`; name="note[note]" class="note-textarea js-vue-comment-form js-gfm-input js-autosize markdown-area js-vue-textarea" - data-supports-quick-actions="true" + :data-supports-quick-actions="supportQuickActions" aria-label="Description" v-model="note" ref="textarea" diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index a90c6d6381d..5bd81c7cad6 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -50,7 +50,11 @@ export default { ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']), noteableType() { // FIXME -- @fatihacet Get this from JSON data. - const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants; + const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants; + + if (this.noteableData.noteableType === EPIC_NOTEABLE_TYPE) { + return EPIC_NOTEABLE_TYPE; + } return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js index f4f407ffd8a..68f8cb1cf1e 100644 --- a/app/assets/javascripts/notes/constants.js +++ b/app/assets/javascripts/notes/constants.js @@ -10,6 +10,7 @@ export const CLOSED = 'closed'; export const EMOJI_THUMBSUP = 'thumbsup'; export const EMOJI_THUMBSDOWN = 'thumbsdown'; export const ISSUE_NOTEABLE_TYPE = 'issue'; +export const EPIC_NOTEABLE_TYPE = 'epic'; export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request'; export const UNRESOLVE_NOTE_METHOD_NAME = 'delete'; export const RESOLVE_NOTE_METHOD_NAME = 'post'; diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js index f90775d0157..e4121f151db 100644 --- a/app/assets/javascripts/notes/index.js +++ b/app/assets/javascripts/notes/index.js @@ -12,8 +12,11 @@ document.addEventListener( data() { const notesDataset = document.getElementById('js-vue-notes').dataset; const parsedUserData = JSON.parse(notesDataset.currentUserData); + const noteableData = JSON.parse(notesDataset.noteableData); let currentUserData = {}; + noteableData.noteableType = notesDataset.noteableType; + if (parsedUserData) { currentUserData = { id: parsedUserData.id, @@ -25,7 +28,7 @@ document.addEventListener( } return { - noteableData: JSON.parse(notesDataset.noteableData), + noteableData, currentUserData, notesData: JSON.parse(notesDataset.notesData), }; diff --git a/app/assets/javascripts/notes/mixins/noteable.js b/app/assets/javascripts/notes/mixins/noteable.js index 0da4ff49f08..5bf8216a1f3 100644 --- a/app/assets/javascripts/notes/mixins/noteable.js +++ b/app/assets/javascripts/notes/mixins/noteable.js @@ -14,6 +14,8 @@ export default { return constants.MERGE_REQUEST_NOTEABLE_TYPE; case 'Issue': return constants.ISSUE_NOTEABLE_TYPE; + case 'Epic': + return constants.EPIC_NOTEABLE_TYPE; default: return ''; } diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js index d149b307e7f..914f804fdd3 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants'; document.addEventListener('DOMContentLoaded', () => { initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, + isGroupDecendent: true, }); projectSelect(); }); diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index a5cc1f34b63..1600faa3611 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants'; document.addEventListener('DOMContentLoaded', () => { initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, + isGroupDecendent: true, }); projectSelect(); }); diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 42772f13155..ce2f1482456 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -706,8 +706,8 @@ button.mini-pipeline-graph-dropdown-toggle { // dropdown content for big and mini pipeline .big-pipeline-graph-dropdown-menu, .mini-pipeline-graph-dropdown-menu { - width: 195px; - max-width: 195px; + width: 240px; + max-width: 240px; .scrollable-menu { padding: 0; @@ -750,7 +750,7 @@ button.mini-pipeline-graph-dropdown-toggle { height: #{$ci-action-icon-size - 6}; left: -3px; position: relative; - top: -2px; + top: -1px; &.icon-action-stop, &.icon-action-cancel { @@ -931,13 +931,11 @@ button.mini-pipeline-graph-dropdown-toggle { */ &.dropdown-menu { transform: translate(-80%, 0); - min-width: 150px; @media(min-width: $screen-md-min) { transform: translate(-50%, 0); right: auto; left: 50%; - min-width: 240px; } } } diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index cc38608eda5..001f6520093 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -5,7 +5,7 @@ class Admin::GroupsController < Admin::ApplicationController def index @groups = Group.with_statistics.with_route - @groups = @groups.sort(@sort = params[:sort]) + @groups = @groups.sort_by_attribute(@sort = params[:sort]) @groups = @groups.search(params[:name]) if params[:name].present? @groups = @groups.page(params[:page]) end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 156a8e2c515..bfeb5a2d097 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -4,7 +4,7 @@ class Admin::UsersController < Admin::ApplicationController def index @users = User.order_name_asc.filter(params[:filter]) @users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present? - @users = @users.sort(@sort = params[:sort]) + @users = @users.sort_by_attribute(@sort = params[:sort]) @users = @users.page(params[:page]) end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7f83bd10e93..24651dd392c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -229,10 +229,6 @@ class ApplicationController < ActionController::Base @event_filter ||= EventFilter.new(filters) end - def gitlab_ldap_access(&block) - Gitlab::Auth::LDAP::Access.open { |access| yield(access) } - end - # JSON for infinite scroll via Pager object def pager_json(partial, count, locals = {}) html = render_to_string( diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb index fafb10090ca..56770a17406 100644 --- a/app/controllers/concerns/group_tree.rb +++ b/app/controllers/concerns/group_tree.rb @@ -14,7 +14,7 @@ module GroupTree end @groups = @groups.with_selects_for_list(archived: params[:archived]) - .sort(@sort = params[:sort]) + .sort_by_attribute(@sort = params[:sort]) .page(params[:page]) respond_to do |format| diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index a21e658fda1..0379f76fc3d 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -88,11 +88,15 @@ module IssuableActions discussions = Discussion.build_collection(notes, issuable) - render json: DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user).represent(discussions, context: self) + render json: discussion_serializer.represent(discussions, context: self) end private + def discussion_serializer + DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity) + end + def recaptcha_check_if_spammable(should_redirect = true, &block) return yield unless issuable.is_a? Spammable diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 03ed5b5310b..839cac3687c 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -212,7 +212,7 @@ module NotesActions end def note_serializer - NoteSerializer.new(project: project, noteable: noteable, current_user: current_user) + ProjectNoteSerializer.new(project: project, noteable: noteable, current_user: current_user) end def note_project diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index f210434b2d7..134b0dfc0db 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -17,7 +17,7 @@ class Groups::GroupMembersController < Groups::ApplicationController @members = GroupMembersFinder.new(@group).execute @members = @members.non_invite unless can?(current_user, :admin_group, @group) @members = @members.search(params[:search]) if params[:search].present? - @members = @members.sort(@sort) + @members = @members.sort_by_attribute(@sort) @members = @members.page(params[:page]).per(50) @members = present_members(@members.includes(:user)) diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index dbf61a17724..3d27ae18b17 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -51,7 +51,7 @@ class ProfilesController < Profiles::ApplicationController end def update_username - result = Users::UpdateService.new(current_user, user: @user, username: user_params[:username]).execute + result = Users::UpdateService.new(current_user, user: @user, username: username_param).execute options = if result[:status] == :success { notice: "Username successfully changed" } @@ -72,6 +72,10 @@ class ProfilesController < Profiles::ApplicationController return render_404 unless @user.can_change_username? end + def username_param + @username_param ||= user_params.require(:username) + end + def user_params @user_params ||= params.require(:user).permit( :avatar, diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index 176679f0849..b7b36f770f5 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -22,9 +22,13 @@ class Projects::BranchesController < Projects::ApplicationController @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name)) @merged_branch_names = repository.merged_branch_names(@branches.map(&:name)) - @max_commits = @branches.reduce(0) do |memo, branch| - diverging_commit_counts = repository.diverging_commit_counts(branch) - [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max + + # n+1: https://gitlab.com/gitlab-org/gitaly/issues/992 + Gitlab::GitalyClient.allow_n_plus_1_calls do + @max_commits = @branches.reduce(0) do |memo, branch| + diverging_commit_counts = repository.diverging_commit_counts(branch) + [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max + end end render diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index cba9a53dc4b..7bc16214010 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -43,7 +43,7 @@ class Projects::DiscussionsController < Projects::ApplicationController def render_json_with_discussions_serializer render json: - DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user) + DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user, note_entity: ProjectNoteEntity) .represent(discussion, context: self) end diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 516198b1b8a..91016f6494e 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -150,7 +150,8 @@ class Projects::LabelsController < Projects::ApplicationController end def find_labels - @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute + @available_labels ||= + LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: params[:include_ancestor_groups]).execute end def authorize_admin_labels! diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index c77f10ef1dd..ee4ed674110 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -41,7 +41,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController def existing_oids @existing_oids ||= begin - storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid) + project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid) end end diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index e898136d203..c5a044541f1 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -14,7 +14,7 @@ class Projects::MilestonesController < Projects::ApplicationController def index @sort = params[:sort] || 'due_date_asc' - @milestones = milestones.sort(@sort) + @milestones = milestones.sort_by_attribute(@sort) respond_to do |format| format.html do diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index e9b4679f94c..cfa5e72af64 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -21,7 +21,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id)) end - @project_members = present_members(@project_members.sort(@sort).page(params[:page])) + @project_members = present_members(@project_members.sort_by_attribute(@sort).page(params[:page])) @requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user)) @project_member = @project.project_members.new end diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index d06d18c498b..dd9e4a2af3e 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -16,6 +16,10 @@ module Projects @protected_tags = @project.protected_tags.order(:name).page(params[:page]) @protected_branch = @project.protected_branches.new @protected_tag = @project.protected_tags.new + + @protected_branches_count = @protected_branches.reduce(0) { |sum, branch| sum + branch.matching(@project.repository.branches).size } + @protected_tags_count = @protected_tags.reduce(0) { |sum, tag| sum + tag.matching(@project.repository.tags).size } + load_gon_index end diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb index 2c8f21c2400..53b77f5fed9 100644 --- a/app/finders/admin/projects_finder.rb +++ b/app/finders/admin/projects_finder.rb @@ -62,6 +62,6 @@ class Admin::ProjectsFinder def sort(items) sort = params.fetch(:sort) { 'latest_activity_desc' } - items.sort(sort) + items.sort_by_attribute(sort) end end diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index b2d4f9938ff..61c72aa22a8 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -337,7 +337,7 @@ class IssuableFinder def sort(items) # Ensure we always have an explicit sort order (instead of inheriting # multiple orders when combining ActiveRecord::Relation objects). - params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc) + params[:sort] ? items.sort_by_attribute(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc) end def by_assignee(items) diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 780c0fdb03e..afd1f824b32 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -28,9 +28,10 @@ class LabelsFinder < UnionFinder if project if project.group.present? labels_table = Label.arel_table + group_ids = group_ids_for(project.group) label_ids << Label.where( - labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or( + labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].in(group_ids)).or( labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id)) ) ) @@ -38,11 +39,14 @@ class LabelsFinder < UnionFinder label_ids << project.labels end end - elsif only_group_labels? - label_ids << Label.where(group_id: group_ids) else + if group? + group = Group.find(params[:group_id]) + label_ids << Label.where(group_id: group_ids_for(group)) + end + label_ids << Label.where(group_id: projects.group_ids) - label_ids << Label.where(project_id: projects.select(:id)) + label_ids << Label.where(project_id: projects.select(:id)) unless only_group_labels? end label_ids @@ -59,22 +63,33 @@ class LabelsFinder < UnionFinder items.where(title: title) end - def group_ids + # Gets redacted array of group ids + # which can include the ancestors and descendants of the requested group. + def group_ids_for(group) strong_memoize(:group_ids) do - groups_user_can_read_labels(groups_to_include).map(&:id) + groups = groups_to_include(group) + + groups_user_can_read_labels(groups).map(&:id) end end - def groups_to_include - group = Group.find(params[:group_id]) + def groups_to_include(group) groups = [group] - groups += group.ancestors if params[:include_ancestor_groups].present? - groups += group.descendants if params[:include_descendant_groups].present? + groups += group.ancestors if include_ancestor_groups? + groups += group.descendants if include_descendant_groups? groups end + def include_ancestor_groups? + params[:include_ancestor_groups] + end + + def include_descendant_groups? + params[:include_descendant_groups] + end + def group? params[:group_id].present? end diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index 005612ededc..c7d6bc6cfdc 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -124,7 +124,7 @@ class ProjectsFinder < UnionFinder end def sort(items) - params[:sort].present? ? items.sort(params[:sort]) : items.order_id_desc + params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.order_id_desc end def by_archived(projects) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 150f4c7688b..09e2c586f2a 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -119,7 +119,7 @@ class TodosFinder end def sort(items) - params[:sort] ? items.sort(params[:sort]) : items.order_id_desc + params[:sort] ? items.sort_by_attribute(params[:sort]) : items.order_id_desc end def by_action(items) diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index 275e892b2e6..af878bcf9a0 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -53,10 +53,12 @@ module BoardsHelper end def board_list_data + include_descendant_groups = @group&.present? + { toggle: "dropdown", - list_labels_path: labels_filter_path(true), - labels: labels_filter_path(true), + list_labels_path: labels_filter_path(true, include_ancestor_groups: true), + labels: labels_filter_path(true, include_descendant_groups: include_descendant_groups), labels_endpoint: @labels_endpoint, namespace_path: @namespace_path, project_path: @project&.path, diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index 87ff607dc3f..c4a6a1e4bb3 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -129,13 +129,17 @@ module LabelsHelper end end - def labels_filter_path(only_group_labels = false) + def labels_filter_path(only_group_labels = false, include_ancestor_groups: true, include_descendant_groups: false) project = @target_project || @project + options = {} + options[:include_ancestor_groups] = include_ancestor_groups if include_ancestor_groups + options[:include_descendant_groups] = include_descendant_groups if include_descendant_groups + if project - project_labels_path(project, :json) + project_labels_path(project, :json, options) elsif @group - options = { only_group_labels: only_group_labels } if only_group_labels + options[:only_group_labels] = only_group_labels if only_group_labels group_labels_path(@group, :json, options) else dashboard_labels_path(:json) diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 20aed60cb7a..27ed48fdbc7 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -151,16 +151,17 @@ module NotesHelper } end - def notes_data(issuable) - discussions_path = - if issuable.is_a?(Issue) - discussions_project_issue_path(@project, issuable, format: :json) - else - discussions_project_merge_request_path(@project, issuable, format: :json) - end + def discussions_path(issuable) + if issuable.is_a?(Issue) + discussions_project_issue_path(@project, issuable, format: :json) + else + discussions_project_merge_request_path(@project, issuable, format: :json) + end + end + def notes_data(issuable) { - discussionsPath: discussions_path, + discussionsPath: discussions_path(issuable), registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'), newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'), markdownDocsPath: help_page_path('user/markdown'), @@ -170,7 +171,6 @@ module NotesHelper notesPath: notes_url, totalNotes: issuable.discussions.length, lastFetchedAt: Time.now.to_i - }.to_json end diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index b64be89c181..5e7c20ef51e 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -123,7 +123,7 @@ module TreeHelper # returns the relative path of the first subdir that doesn't have only one directory descendant def flatten_tree(root_path, tree) - return tree.flat_path.sub(%r{\A#{root_path}/}, '') if tree.flat_path.present? + return tree.flat_path.sub(%r{\A#{Regexp.escape(root_path)}/}, '') if tree.flat_path.present? subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path) if subtree.count == 1 && subtree.first.dir? diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index be99f3780cc..b3f2aeb08ca 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -15,6 +15,7 @@ module Emails setup_merge_request_mail(merge_request_id, recipient_id) @new_commits = new_commits @existing_commits = existing_commits + @updated_by_user = User.find(updated_by_user_id) mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 18e96389199..4aa65bf4273 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -90,6 +90,7 @@ module Ci before_save :ensure_token before_destroy { unscoped_project } + before_create :ensure_metadata after_create unless: :importing? do |build| run_after_commit { BuildHooksWorker.perform_async(build.id) } end diff --git a/app/models/commit.rb b/app/models/commit.rb index b64462fb768..3f7f36e83c0 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -32,7 +32,8 @@ class Commit COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze def banzai_render_context(field) - context = { pipeline: :single_line, project: self.project } + pipeline = field == :description ? :commit_description : :single_line + context = { pipeline: pipeline, project: self.project } context[:author] = self.author if self.author context diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 9fb5b7efec6..3469d5d795c 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base end def group_name - name.to_s.gsub(%r{\d+[\.\s:/\\]+\d+\s*}, '').strip + name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip end def failed_but_allowed? diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 5a566f3ac02..b45395343cc 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -137,7 +137,7 @@ module Issuable fuzzy_search(query, [:title, :description]) end - def sort(method, excluded_labels: []) + def sort_by_attribute(method, excluded_labels: []) sorted = case method.to_s when 'downvotes_desc' then order_downvotes_desc diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb index caf8afa97f9..5130ecec472 100644 --- a/app/models/concerns/milestoneish.rb +++ b/app/models/concerns/milestoneish.rb @@ -45,11 +45,11 @@ module Milestoneish end def sorted_issues(user) - issues_visible_to_user(user).preload_associations.sort('label_priority') + issues_visible_to_user(user).preload_associations.sort_by_attribute('label_priority') end def sorted_merge_requests - merge_requests.sort('label_priority') + merge_requests.sort_by_attribute('label_priority') end def upcoming? diff --git a/app/models/group.rb b/app/models/group.rb index d99af79b5fe..3cfe21ac93b 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -53,7 +53,7 @@ class Group < Namespace Gitlab::Database.postgresql? end - def sort(method) + def sort_by_attribute(method) if method == 'storage_size_desc' # storage_size is a virtual column so we need to # pass a string to avoid AR adding the table name diff --git a/app/models/issue.rb b/app/models/issue.rb index 6a94d60c828..13abc6c1a0d 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -116,7 +116,7 @@ class Issue < ActiveRecord::Base 'project_id' end - def self.sort(method, excluded_labels: []) + def self.sort_by_attribute(method, excluded_labels: []) case method.to_s when 'due_date' then order_due_date_asc when 'due_date_asc' then order_due_date_asc diff --git a/app/models/member.rb b/app/models/member.rb index e1a32148538..eac4a22a03f 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -96,7 +96,7 @@ class Member < ActiveRecord::Base joins(:user).merge(User.search(query)) end - def sort(method) + def sort_by_attribute(method) case method.to_s when 'access_level_asc' then reorder(access_level: :asc) when 'access_level_desc' then reorder(access_level: :desc) diff --git a/app/models/milestone.rb b/app/models/milestone.rb index e7d397f40f5..dafae58d121 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -138,7 +138,7 @@ class Milestone < ActiveRecord::Base User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq end - def self.sort(method) + def self.sort_by_attribute(method) case method.to_s when 'due_date_asc' reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) diff --git a/app/models/note.rb b/app/models/note.rb index 787a80f0196..0f5fb529a87 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -379,12 +379,15 @@ class Note < ActiveRecord::Base def expire_etag_cache return unless noteable&.discussions_rendered_on_frontend? - key = Gitlab::Routing.url_helpers.project_noteable_notes_path( + Gitlab::EtagCaching::Store.new.touch(etag_key) + end + + def etag_key + Gitlab::Routing.url_helpers.project_noteable_notes_path( project, target_type: noteable_type.underscore, target_id: noteable_id ) - Gitlab::EtagCaching::Store.new.touch(key) end def touch(*args) diff --git a/app/models/project.rb b/app/models/project.rb index b343786d2c9..32289106f28 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -436,7 +436,7 @@ class Project < ActiveRecord::Base Gitlab::VisibilityLevel.options end - def sort(method) + def sort_by_attribute(method) case method.to_s when 'storage_size_desc' # storage_size is a joined column so we need to @@ -566,9 +566,7 @@ class Project < ActiveRecord::Base def add_import_job job_id = if forked? - RepositoryForkWorker.perform_async(id, - forked_from_project.repository_storage_path, - forked_from_project.disk_path) + RepositoryForkWorker.perform_async(id) elsif gitlab_project_import? # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved. RepositoryImportWorker.set(retry: false).perform_async(self.id) @@ -1068,6 +1066,16 @@ class Project < ActiveRecord::Base end end + # This will return all `lfs_objects` that are accessible to the project. + # So this might be `self.lfs_objects` if the project is not part of a fork + # network, or it is the base of the fork network. + # + # TODO: refactor this to get the correct lfs objects when implementing + # https://gitlab.com/gitlab-org/gitlab-ce/issues/39769 + def all_lfs_objects + lfs_storage_project.lfs_objects + end + def personal? !group end diff --git a/app/models/todo.rb b/app/models/todo.rb index 8afacd188e0..a2ab405fdbe 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -50,7 +50,7 @@ class Todo < ActiveRecord::Base # Priority sorting isn't displayed in the dropdown, because we don't show # milestones, but still show something if the user has a URL with that # selected. - def sort(method) + def sort_by_attribute(method) sorted = case method.to_s when 'priority', 'label_priority' then order_by_labels_priority diff --git a/app/models/user.rb b/app/models/user.rb index f934b654225..ba51595e6a3 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -256,7 +256,7 @@ class User < ActiveRecord::Base end end - def sort(method) + def sort_by_attribute(method) order_method = method || 'id_desc' case order_method.to_s diff --git a/app/serializers/build_metadata_entity.rb b/app/serializers/build_metadata_entity.rb index 39f429aa6c3..f16f3badffa 100644 --- a/app/serializers/build_metadata_entity.rb +++ b/app/serializers/build_metadata_entity.rb @@ -1,8 +1,5 @@ class BuildMetadataEntity < Grape::Entity - expose :timeout_human_readable do |metadata| - metadata.timeout_human_readable unless metadata.timeout.nil? - end - + expose :timeout_human_readable expose :timeout_source do |metadata| metadata.present.timeout_source end diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb index bbbcf6a97c1..718fb35e62d 100644 --- a/app/serializers/discussion_entity.rb +++ b/app/serializers/discussion_entity.rb @@ -4,7 +4,9 @@ class DiscussionEntity < Grape::Entity expose :id, :reply_id expose :expanded?, as: :expanded - expose :notes, using: NoteEntity + expose :notes do |discussion, opts| + request.note_entity.represent(discussion.notes, opts) + end expose :individual_note?, as: :individual_note expose :resolvable?, as: :resolvable @@ -12,7 +14,7 @@ class DiscussionEntity < Grape::Entity expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion| resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id) end - expose :resolve_with_issue_path do |discussion| + expose :resolve_with_issue_path, if: -> (d, _) { d.resolvable? } do |discussion| new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id) end diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index 4ccf0bca476..c964aa9c99b 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -5,10 +5,6 @@ class NoteEntity < API::Entities::Note expose :author, using: NoteUserEntity - expose :human_access do |note| - note.project.team.human_max_access(note.author_id) - end - unexpose :note, as: :body expose :note @@ -37,36 +33,10 @@ class NoteEntity < API::Entities::Note expose :emoji_awardable?, as: :emoji_awardable expose :award_emoji, if: -> (note, _) { note.emoji_awardable? }, using: AwardEmojiEntity - expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note| - if note.for_personal_snippet? - toggle_award_emoji_snippet_note_path(note.noteable, note) - else - toggle_award_emoji_project_note_path(note.project, note.id) - end - end expose :report_abuse_path do |note| new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note)) end - expose :path do |note| - if note.for_personal_snippet? - snippet_note_path(note.noteable, note) - else - project_note_path(note.project, note) - end - end - - expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note| - resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id) - end - - expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note| - new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id) - end - expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? } - expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note| - delete_attachment_project_note_path(note.project, note) - end end diff --git a/app/serializers/note_serializer.rb b/app/serializers/note_serializer.rb deleted file mode 100644 index 2afe40d7a34..00000000000 --- a/app/serializers/note_serializer.rb +++ /dev/null @@ -1,3 +0,0 @@ -class NoteSerializer < BaseSerializer - entity NoteEntity -end diff --git a/app/serializers/project_note_entity.rb b/app/serializers/project_note_entity.rb new file mode 100644 index 00000000000..e541bfbee8d --- /dev/null +++ b/app/serializers/project_note_entity.rb @@ -0,0 +1,25 @@ +class ProjectNoteEntity < NoteEntity + expose :human_access do |note| + note.project.team.human_max_access(note.author_id) + end + + expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note| + toggle_award_emoji_project_note_path(note.project, note.id) + end + + expose :path do |note| + project_note_path(note.project, note) + end + + expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note| + resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id) + end + + expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note| + new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id) + end + + expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note| + delete_attachment_project_note_path(note.project, note) + end +end diff --git a/app/serializers/project_note_serializer.rb b/app/serializers/project_note_serializer.rb new file mode 100644 index 00000000000..763ad0bdb3f --- /dev/null +++ b/app/serializers/project_note_serializer.rb @@ -0,0 +1,3 @@ +class ProjectNoteSerializer < BaseSerializer + entity ProjectNoteEntity +end diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 15fed7d17c1..3ceab209f3f 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -42,7 +42,10 @@ module Boards ) end - attrs[:move_between_ids] = move_between_ids if move_between_ids + if move_between_ids + attrs[:move_between_ids] = move_between_ids + attrs[:board_group_id] = board.group&.id + end attrs end diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb index bebc90c7a8d..02f1c709374 100644 --- a/app/services/boards/lists/create_service.rb +++ b/app/services/boards/lists/create_service.rb @@ -12,11 +12,15 @@ module Boards private def available_labels_for(board) + options = { include_ancestor_groups: true } + if board.group_board? - parent.labels + options.merge!(group_id: parent.id, only_group_labels: true) else - LabelsFinder.new(current_user, project_id: parent.id).execute + options[:project_id] = parent.id end + + LabelsFinder.new(current_user, options).execute end def next_position(board) diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 02fb48108fb..91ec702fbc6 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -106,7 +106,7 @@ class IssuableBaseService < BaseService end def available_labels - @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute + @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: true).execute end def handle_quick_actions_on_create(issuable) diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index d7aa7e2347e..4161932ad2a 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -55,9 +55,10 @@ module Issues return unless params[:move_between_ids] after_id, before_id = params.delete(:move_between_ids) + board_group_id = params.delete(:board_group_id) - issue_before = get_issue_if_allowed(issue.project, before_id) if before_id - issue_after = get_issue_if_allowed(issue.project, after_id) if after_id + issue_before = get_issue_if_allowed(before_id, board_group_id) + issue_after = get_issue_if_allowed(after_id, board_group_id) issue.move_between(issue_before, issue_after) end @@ -84,8 +85,16 @@ module Issues private - def get_issue_if_allowed(project, id) - issue = project.issues.find(id) + def get_issue_if_allowed(id, board_group_id = nil) + return unless id + + issue = + if board_group_id + IssuesFinder.new(current_user, group_id: board_group_id).find_by(id: id) + else + project.issues.find(id) + end + issue if can?(current_user, :update_issue, issue) end diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index e61ecb696d0..346971138b1 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -21,7 +21,8 @@ module Projects end def labels(target = nil) - labels = LabelsFinder.new(current_user, project_id: project.id).execute.select([:color, :title]) + labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true) + .execute.select([:color, :title]) return labels unless target&.respond_to?(:labels) diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb index 402cddd3ec1..7bf0b90b491 100644 --- a/app/services/projects/import_export/export_service.rb +++ b/app/services/projects/import_export/export_service.rb @@ -28,7 +28,7 @@ module Projects end def save_services - [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save) + [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver].all?(&:save) end def version_saver @@ -55,6 +55,10 @@ module Projects Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared) end + def lfs_saver + Gitlab::ImportExport::LfsSaver.new(project: project, shared: @shared) + end + def cleanup_and_notify_error Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}") diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb index a3828acc50b..bdd9598f85a 100644 --- a/app/services/projects/import_service.rb +++ b/app/services/projects/import_service.rb @@ -61,7 +61,7 @@ module Projects project.ensure_repository project.repository.fetch_as_mirror(project.import_url, refmap: refmap) else - gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, project.import_url) + gitlab_shell.import_repository(project.repository_storage, project.disk_path, project.import_url) end rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e # Expire cache to prevent scenarios such as: diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb index 9c8877be14e..7e228d1833d 100644 --- a/app/services/projects/update_pages_service.rb +++ b/app/services/projects/update_pages_service.rb @@ -31,15 +31,17 @@ module Projects # Check if we did extract public directory archive_public_path = File.join(archive_path, 'public') - raise FailedToExtractError, 'pages miss the public folder' unless Dir.exist?(archive_public_path) + raise InvaildStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path) raise InvaildStateError, 'pages are outdated' unless latest? deploy_page!(archive_public_path) success end - rescue InvaildStateError, FailedToExtractError => e - register_failure + rescue InvaildStateError => e error(e.message) + rescue => e + error(e.message, false) + raise e end private @@ -50,12 +52,13 @@ module Projects super end - def error(message, http_status = nil) + def error(message, allow_delete_artifact = true) + register_failure log_error("Projects::UpdatePagesService: #{message}") @status.allow_failure = !latest? @status.description = message @status.drop(:script_failure) - delete_artifact! + delete_artifact! if allow_delete_artifact super end @@ -76,7 +79,7 @@ module Projects elsif artifacts.ends_with?('.zip') extract_zip_archive!(temp_path) else - raise FailedToExtractError, 'unsupported artifacts format' + raise InvaildStateError, 'unsupported artifacts format' end end @@ -91,13 +94,13 @@ module Projects end def extract_zip_archive!(temp_path) - raise FailedToExtractError, 'missing artifacts metadata' unless build.artifacts_metadata? + raise InvaildStateError, 'missing artifacts metadata' unless build.artifacts_metadata? # Calculate page size after extract public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true) if public_entry.total_size > max_size - raise FailedToExtractError, "artifacts for pages are too large: #{public_entry.total_size}" + raise InvaildStateError, "artifacts for pages are too large: #{public_entry.total_size}" end # Requires UnZip at least 6.00 Info-ZIP. diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index cba49faac31..6cc51b6ee1b 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -200,7 +200,7 @@ module QuickActions end params '~label1 ~"label 2"' condition do - available_labels = LabelsFinder.new(current_user, project_id: project.id).execute + available_labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true).execute current_user.can?(:"admin_#{issuable.to_ability_name}", project) && available_labels.any? @@ -562,7 +562,7 @@ module QuickActions def find_labels(labels_param) extract_references(labels_param, :label) | - LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute + LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split, include_ancestor_groups: true).execute end def find_label_references(labels_param) @@ -593,6 +593,7 @@ module QuickActions def extract_references(arg, type) ext = Gitlab::ReferenceExtractor.new(project, current_user) + ext.analyze(arg, author: current_user) ext.references(type) diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml new file mode 100644 index 00000000000..bb3fa26a33e --- /dev/null +++ b/app/views/admin/application_settings/_abuse.html.haml @@ -0,0 +1,12 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :admin_notification_email, class: 'form-control' + .help-block + Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml new file mode 100644 index 00000000000..6c89f1c4e98 --- /dev/null +++ b/app/views/admin/application_settings/_email.html.haml @@ -0,0 +1,26 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :email_author_in_body do + = f.check_box :email_author_in_body + Include author name in notification email body + .help-block + Some email servers do not support overriding the email sender name. + Enable this option to include the name of the author of the issue, + merge request or comment in the email body instead. + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :html_emails_enabled do + = f.check_box :html_emails_enabled + Enable HTML emails + .help-block + By default GitLab sends emails in HTML and plain text formats so mail + clients can choose what format to use. Disable this option if you only + want to send emails in plain text format. + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml deleted file mode 100644 index 309c7ed5dfa..00000000000 --- a/app/views/admin/application_settings/_form.html.haml +++ /dev/null @@ -1,386 +0,0 @@ -= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| - = form_errors(@application_setting) - - - if Gitlab.config.registry.enabled - %fieldset - %legend Container Registry - .form-group - = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :container_registry_token_expire_delay, class: 'form-control' - - %fieldset - %legend Abuse reports - .form-group - = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2' - .col-sm-10 - = f.text_field :admin_notification_email, class: 'form-control' - .help-block - Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area. - - %fieldset - %legend Error Reporting and Logging - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :sentry_enabled do - = f.check_box :sentry_enabled - Enable Sentry - .help-block - %p This setting requires a restart to take effect. - Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: - %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com - - .form-group - = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2' - .col-sm-10 - = f.text_field :sentry_dsn, class: 'form-control' - - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :clientside_sentry_enabled do - = f.check_box :clientside_sentry_enabled - Enable Clientside Sentry - .help-block - Sentry can also be used for reporting and logging clientside exceptions. - %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/ - - .form-group - = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'control-label col-sm-2' - .col-sm-10 - = f.text_field :clientside_sentry_dsn, class: 'form-control' - - %fieldset - %legend Repository Storage - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :hashed_storage_enabled do - = f.check_box :hashed_storage_enabled - Create new projects using hashed storage paths - .help-block - Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents - repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. - %em (EXPERIMENTAL) - .form-group - = f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2' - .col-sm-10 - = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages), - {include_hidden: false}, multiple: true, class: 'form-control' - .help-block - Manage repository storage paths. Learn more in the - = succeed "." do - = link_to "repository storages documentation", help_page_path("administration/repository_storages") - - %fieldset - %legend Git Storage Circuitbreaker settings - .form-group - = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_check_interval, class: 'form-control' - .help-block - = circuitbreaker_check_interval_help_text - .form-group - = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_access_retries, class: 'form-control' - .help-block - = circuitbreaker_access_retries_help_text - .form-group - = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_storage_timeout, class: 'form-control' - .help-block - = circuitbreaker_storage_timeout_help_text - .form-group - = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' - .help-block - = circuitbreaker_failure_count_help_text - .form-group - = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control' - .help-block - = circuitbreaker_failure_reset_time_help_text - - %fieldset - %legend Repository Checks - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :repository_checks_enabled do - = f.check_box :repository_checks_enabled - Enable Repository Checks - .help-block - GitLab will periodically run - %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck' - in all project and wiki repositories to look for silent disk corruption issues. - .form-group - .col-sm-offset-2.col-sm-10 - = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" - .help-block - If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. - - - if koding_enabled? - %fieldset - %legend Koding - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :koding_enabled do - = f.check_box :koding_enabled - Enable Koding - .help-block - Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again. - .form-group - = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2' - .col-sm-10 - = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090' - .help-block - Koding has integration enabled out of the box for the - %strong gitlab - team, and you need to provide that team's URL here. Learn more in the - = succeed "." do - = link_to "Koding administration documentation", help_page_path("administration/integration/koding") - - %fieldset - %legend PlantUML - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :plantuml_enabled do - = f.check_box :plantuml_enabled - Enable PlantUML - .form-group - = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2' - .col-sm-10 - = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080' - .help-block - Allow rendering of - = link_to "PlantUML", "http://plantuml.com" - diagrams in Asciidoc documents using an external PlantUML service. - - %fieldset - %legend#usage-statistics Usage statistics - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :version_check_enabled do - = f.check_box :version_check_enabled - Enable version check - .help-block - GitLab will inform you if a new version is available. - = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check") - about what information is shared with GitLab Inc. - .form-group - .col-sm-offset-2.col-sm-10 - - can_be_configured = @application_setting.usage_ping_can_be_configured? - .checkbox - = f.label :usage_ping_enabled do - = f.check_box :usage_ping_enabled, disabled: !can_be_configured - Enable usage ping - .help-block - - if can_be_configured - To help improve GitLab and its user experience, GitLab will - periodically collect usage information. - = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") - about what information is shared with GitLab Inc. Visit - = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping') - to see the JSON payload sent. - - else - The usage ping is disabled, and cannot be configured through this - form. For more information, see the documentation on - = succeed '.' do - = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') - - %fieldset - %legend Email - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :email_author_in_body do - = f.check_box :email_author_in_body - Include author name in notification email body - .help-block - Some email servers do not support overriding the email sender name. - Enable this option to include the name of the author of the issue, - merge request or comment in the email body instead. - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :html_emails_enabled do - = f.check_box :html_emails_enabled - Enable HTML emails - .help-block - By default GitLab sends emails in HTML and plain text formats so mail - clients can choose what format to use. Disable this option if you only - want to send emails in plain text format. - %fieldset - %legend Automatic Git repository housekeeping - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :housekeeping_enabled do - = f.check_box :housekeeping_enabled - Enable automatic repository housekeeping (git repack, git gc) - .help-block - If you keep automatic housekeeping disabled for a long time Git - repository access on your GitLab server will become slower and your - repositories will use more disk space. We recommend to always leave - this enabled. - .checkbox - = f.label :housekeeping_bitmaps_enabled do - = f.check_box :housekeeping_bitmaps_enabled - Enable Git pack file bitmap creation - .help-block - Creating pack file bitmaps makes housekeeping take a little longer but - bitmaps should accelerate 'git clone' performance. - .form-group - = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :housekeeping_incremental_repack_period, class: 'form-control' - .help-block - Number of Git pushes after which an incremental 'git repack' is run. - .form-group - = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :housekeeping_full_repack_period, class: 'form-control' - .help-block - Number of Git pushes after which a full 'git repack' is run. - .form-group - = f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :housekeeping_gc_period, class: 'form-control' - .help-block - Number of Git pushes after which 'git gc' is run. - - %fieldset - %legend Gitaly Timeouts - .form-group - = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :gitaly_timeout_default, class: 'form-control' - .help-block - Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced - for git fetch/push operations or Sidekiq jobs. - .form-group - = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :gitaly_timeout_fast, class: 'form-control' - .help-block - Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast. - If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' - can help maintain the stability of the GitLab instance. - .form-group - = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :gitaly_timeout_medium, class: 'form-control' - .help-block - Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout. - - %fieldset - %legend Web terminal - .form-group - = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :terminal_max_session_time, class: 'form-control' - .help-block - Maximum time for web terminal websocket connection (in seconds). - 0 for unlimited. - - %fieldset - %legend Real-time features - .form-group - = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2' - .col-sm-10 - = f.text_field :polling_interval_multiplier, class: 'form-control' - .help-block - Change this value to influence how frequently the GitLab UI polls for updates. - If you set the value to 2 all polling intervals are multiplied - by 2, which means that polling happens half as frequently. - The multiplier can also have a decimal value. - The default value (1) is a reasonable choice for the majority of GitLab - installations. Set to 0 to completely disable polling. - = link_to icon('question-circle'), help_page_path('administration/polling') - - %fieldset - %legend Performance optimization - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :authorized_keys_enabled do - = f.check_box :authorized_keys_enabled - Write to "authorized_keys" file - .help-block - By default, we write to the "authorized_keys" file to support Git - over SSH without additional configuration. GitLab can be optimized - to authenticate SSH keys via the database file. Only uncheck this - if you have configured your OpenSSH server to use the - AuthorizedKeysCommand. Click on the help icon for more details. - = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup') - - %fieldset - %legend User and IP Rate Limits - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :throttle_unauthenticated_enabled do - = f.check_box :throttle_unauthenticated_enabled - Enable unauthenticated request rate limit - %span.help-block - Helps reduce request volume (e.g. from crawlers or abusive bots) - .form-group - = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control' - .form-group - = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control' - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :throttle_authenticated_api_enabled do - = f.check_box :throttle_authenticated_api_enabled - Enable authenticated API request rate limit - %span.help-block - Helps reduce request volume (e.g. from crawlers or abusive bots) - .form-group - = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control' - .form-group - = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control' - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :throttle_authenticated_web_enabled do - = f.check_box :throttle_authenticated_web_enabled - Enable authenticated web request rate limit - %span.help-block - Helps reduce request volume (e.g. from crawlers or abusive bots) - .form-group - = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control' - .form-group - = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' - .col-sm-10 - = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' - - %fieldset - %legend Outbound requests - .form-group - .col-sm-offset-2.col-sm-10 - .checkbox - = f.label :allow_local_requests_from_hooks_and_services do - = f.check_box :allow_local_requests_from_hooks_and_services - Allow requests to the local network from hooks and services - - .form-actions - = f.submit 'Save', class: 'btn btn-save' diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml new file mode 100644 index 00000000000..4acc5b3a0c5 --- /dev/null +++ b/app/views/admin/application_settings/_gitaly.html.haml @@ -0,0 +1,27 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :gitaly_timeout_default, class: 'form-control' + .help-block + Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced + for git fetch/push operations or Sidekiq jobs. + .form-group + = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :gitaly_timeout_fast, class: 'form-control' + .help-block + Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast. + If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' + can help maintain the stability of the GitLab instance. + .form-group + = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :gitaly_timeout_medium, class: 'form-control' + .help-block + Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout. + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml new file mode 100644 index 00000000000..b83ffc375d9 --- /dev/null +++ b/app/views/admin/application_settings/_ip_limits.html.haml @@ -0,0 +1,54 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :throttle_unauthenticated_enabled do + = f.check_box :throttle_unauthenticated_enabled + Enable unauthenticated request rate limit + %span.help-block + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control' + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :throttle_authenticated_api_enabled do + = f.check_box :throttle_authenticated_api_enabled + Enable authenticated API request rate limit + %span.help-block + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control' + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :throttle_authenticated_web_enabled do + = f.check_box :throttle_authenticated_web_enabled + Enable authenticated web request rate limit + %span.help-block + Helps reduce request volume (e.g. from crawlers or abusive bots) + .form-group + = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control' + .form-group + = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control' + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml new file mode 100644 index 00000000000..17358cf775b --- /dev/null +++ b/app/views/admin/application_settings/_koding.html.haml @@ -0,0 +1,24 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :koding_enabled do + = f.check_box :koding_enabled + Enable Koding + .help-block + Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again. + .form-group + = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090' + .help-block + Koding has integration enabled out of the box for the + %strong gitlab + team, and you need to provide that team's URL here. Learn more in the + = succeed "." do + = link_to "Koding administration documentation", help_page_path("administration/integration/koding") + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml new file mode 100644 index 00000000000..44a11ddc120 --- /dev/null +++ b/app/views/admin/application_settings/_logging.html.haml @@ -0,0 +1,36 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :sentry_enabled do + = f.check_box :sentry_enabled + Enable Sentry + .help-block + %p This setting requires a restart to take effect. + Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here: + %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com + + .form-group + = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :sentry_dsn, class: 'form-control' + + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :clientside_sentry_enabled do + = f.check_box :clientside_sentry_enabled + Enable Clientside Sentry + .help-block + Sentry can also be used for reporting and logging clientside exceptions. + %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/ + + .form-group + = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :clientside_sentry_dsn, class: 'form-control' + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml new file mode 100644 index 00000000000..d10f609006d --- /dev/null +++ b/app/views/admin/application_settings/_outbound.html.haml @@ -0,0 +1,12 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :allow_local_requests_from_hooks_and_services do + = f.check_box :allow_local_requests_from_hooks_and_services + Allow requests to the local network from hooks and services + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml new file mode 100644 index 00000000000..01d5a31aa9f --- /dev/null +++ b/app/views/admin/application_settings/_performance.html.haml @@ -0,0 +1,19 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :authorized_keys_enabled do + = f.check_box :authorized_keys_enabled + Write to "authorized_keys" file + .help-block + By default, we write to the "authorized_keys" file to support Git + over SSH without additional configuration. GitLab can be optimized + to authenticate SSH keys via the database file. Only uncheck this + if you have configured your OpenSSH server to use the + AuthorizedKeysCommand. Click on the help icon for more details. + = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup') + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml new file mode 100644 index 00000000000..56764b3fb81 --- /dev/null +++ b/app/views/admin/application_settings/_plantuml.html.haml @@ -0,0 +1,20 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :plantuml_enabled do + = f.check_box :plantuml_enabled + Enable PlantUML + .form-group + = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080' + .help-block + Allow rendering of + = link_to "PlantUML", "http://plantuml.com" + diagrams in Asciidoc documents using an external PlantUML service. + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml new file mode 100644 index 00000000000..0a53a75119e --- /dev/null +++ b/app/views/admin/application_settings/_realtime.html.haml @@ -0,0 +1,19 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2' + .col-sm-10 + = f.text_field :polling_interval_multiplier, class: 'form-control' + .help-block + Change this value to influence how frequently the GitLab UI polls for updates. + If you set the value to 2 all polling intervals are multiplied + by 2, which means that polling happens half as frequently. + The multiplier can also have a decimal value. + The default value (1) is a reasonable choice for the majority of GitLab + installations. Set to 0 to completely disable polling. + = link_to icon('question-circle'), help_page_path('administration/polling') + + = f.submit 'Save changes', class: "btn btn-success" + diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml new file mode 100644 index 00000000000..3451ef62458 --- /dev/null +++ b/app/views/admin/application_settings/_registry.html.haml @@ -0,0 +1,10 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :container_registry_token_expire_delay, class: 'form-control' + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml new file mode 100644 index 00000000000..f33769b23c2 --- /dev/null +++ b/app/views/admin/application_settings/_repository_check.html.haml @@ -0,0 +1,62 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .sub-section + %h4 Repository checks + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :repository_checks_enabled do + = f.check_box :repository_checks_enabled + Enable Repository Checks + .help-block + GitLab will periodically run + %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck' + in all project and wiki repositories to look for silent disk corruption issues. + .form-group + .col-sm-offset-2.col-sm-10 + = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove" + .help-block + If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database. + + .sub-section + %h4 Housekeeping + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :housekeeping_enabled do + = f.check_box :housekeeping_enabled + Enable automatic repository housekeeping (git repack, git gc) + .help-block + If you keep automatic housekeeping disabled for a long time Git + repository access on your GitLab server will become slower and your + repositories will use more disk space. We recommend to always leave + this enabled. + .checkbox + = f.label :housekeeping_bitmaps_enabled do + = f.check_box :housekeeping_bitmaps_enabled + Enable Git pack file bitmap creation + .help-block + Creating pack file bitmaps makes housekeeping take a little longer but + bitmaps should accelerate 'git clone' performance. + .form-group + = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :housekeeping_incremental_repack_period, class: 'form-control' + .help-block + Number of Git pushes after which an incremental 'git repack' is run. + .form-group + = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :housekeeping_full_repack_period, class: 'form-control' + .help-block + Number of Git pushes after which a full 'git repack' is run. + .form-group + = f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :housekeeping_gc_period, class: 'form-control' + .help-block + Number of Git pushes after which 'git gc' is run. + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml new file mode 100644 index 00000000000..ac31977e1a9 --- /dev/null +++ b/app/views/admin/application_settings/_repository_storage.html.haml @@ -0,0 +1,58 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .sub-section + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :hashed_storage_enabled do + = f.check_box :hashed_storage_enabled + Create new projects using hashed storage paths + .help-block + Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents + repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance. + %em (EXPERIMENTAL) + .form-group + = f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2' + .col-sm-10 + = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages), + {include_hidden: false}, multiple: true, class: 'form-control' + .help-block + Manage repository storage paths. Learn more in the + = succeed "." do + = link_to "repository storages documentation", help_page_path("administration/repository_storages") + .sub-section + %h4 Circuit breaker + .form-group + = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :circuitbreaker_check_interval, class: 'form-control' + .help-block + = circuitbreaker_check_interval_help_text + .form-group + = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :circuitbreaker_access_retries, class: 'form-control' + .help-block + = circuitbreaker_access_retries_help_text + .form-group + = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :circuitbreaker_storage_timeout, class: 'form-control' + .help-block + = circuitbreaker_storage_timeout_help_text + .form-group + = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control' + .help-block + = circuitbreaker_failure_count_help_text + .form-group + = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control' + .help-block + = circuitbreaker_failure_reset_time_help_text + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml new file mode 100644 index 00000000000..36d8838803f --- /dev/null +++ b/app/views/admin/application_settings/_terminal.html.haml @@ -0,0 +1,13 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2' + .col-sm-10 + = f.number_field :terminal_max_session_time, class: 'form-control' + .help-block + Maximum time for web terminal websocket connection (in seconds). + 0 for unlimited. + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml new file mode 100644 index 00000000000..7684e2cfdd1 --- /dev/null +++ b/app/views/admin/application_settings/_usage.html.haml @@ -0,0 +1,37 @@ += form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .col-sm-offset-2.col-sm-10 + .checkbox + = f.label :version_check_enabled do + = f.check_box :version_check_enabled + Enable version check + .help-block + GitLab will inform you if a new version is available. + = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check") + about what information is shared with GitLab Inc. + .form-group + .col-sm-offset-2.col-sm-10 + - can_be_configured = @application_setting.usage_ping_can_be_configured? + .checkbox + = f.label :usage_ping_enabled do + = f.check_box :usage_ping_enabled, disabled: !can_be_configured + Enable usage ping + .help-block + - if can_be_configured + To help improve GitLab and its user experience, GitLab will + periodically collect usage information. + = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") + about what information is shared with GitLab Inc. Visit + = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping') + to see the JSON payload sent. + - else + The usage ping is disabled, and cannot be configured through this + form. For more information, see the documentation on + = succeed '.' do + = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') + + = f.submit 'Save changes', class: "btn btn-success" + diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index d0e612e62e5..caaa93aa1e2 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -76,7 +76,7 @@ %button.btn.js-settings-toggle{ type: 'button' } = expanded ? 'Collapse' : 'Expand' %p - = _('Auto DevOps, runners amd job artifacts') + = _('Auto DevOps, runners and job artifacts') .settings-content = render 'ci_cd' @@ -102,7 +102,7 @@ .settings-content = render 'prometheus' -%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) } +%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded) } .settings-header %h4 = _('Profiling - Performance bar') @@ -136,5 +136,169 @@ .settings-content = render 'spam' -.prepend-top-20 - = render 'form' +%section.settings.as-abuse.no-animate#js-abuse-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Abuse reports') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Set notification email for abuse reports.') + .settings-content + = render 'abuse' + +%section.settings.as-logging.no-animate#js-logging-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Error Reporting and Logging') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Enable Sentry for error reporting and logging.') + .settings-content + = render 'logging' + +%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Repository storage') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Configure storage path and circuit breaker settings.') + .settings-content + = render 'repository_storage' + +%section.settings.as-repository-check.no-animate#js-repository-check-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Repository maintenance') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Configure automatic git checks and housekeeping on repositories.') + .settings-content + = render 'repository_check' + +- if Gitlab.config.registry.enabled + %section.settings.as-registry.no-animate#js-registry-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Container Registry') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Various container registry settings.') + .settings-content + = render 'registry' + +- if koding_enabled? + %section.settings.as-koding.no-animate#js-koding-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Koding') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Online IDE integration settings.') + .settings-content + = render 'koding' + +%section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('PlantUML') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Allow rendering of PlantUML diagrams in Asciidoc documents.') + .settings-content + = render 'plantuml' + +%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded) } + .settings-header#usage-statistics + %h4 + = _('Usage statistics') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Enable or disable version check and usage ping.') + .settings-content + = render 'usage' + +%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Email') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Various email settings.') + .settings-content + = render 'email' + +%section.settings.as-gitaly.no-animate#js-gitaly-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Gitaly') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Configure Gitaly timeouts.') + .settings-content + = render 'gitaly' + +%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Web terminal') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Set max session time for web terminal.') + .settings-content + = render 'terminal' + +%section.settings.as-realtime.no-animate#js-realtime-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Real-time features') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Change this value to influence how frequently the GitLab UI polls for updates.') + .settings-content + = render 'realtime' + +%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Performance optimization') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Various settings that affect GitLab performance.') + .settings-content + = render 'performance' + +%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('User and IP Rate Limits') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Configure limits for web and API requests.') + .settings-content + = render 'ip_limits' + +%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Outbound requests') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = _('Allow requests to the local network from hooks and services.') + .settings-content + = render 'outbound' diff --git a/app/views/notify/push_to_merge_request_email.html.haml b/app/views/notify/push_to_merge_request_email.html.haml index 5cc6f21c0f3..4c507c08ed7 100644 --- a/app/views/notify/push_to_merge_request_email.html.haml +++ b/app/views/notify/push_to_merge_request_email.html.haml @@ -1,7 +1,7 @@ %h3 - New commits were pushed to the merge request + = @updated_by_user.name + pushed new commits to merge request = link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request)) - by #{@current_user.name} - if @existing_commits.any? - count = @existing_commits.size diff --git a/app/views/notify/push_to_merge_request_email.text.haml b/app/views/notify/push_to_merge_request_email.text.haml index d7722e5f41f..553f771f1a6 100644 --- a/app/views/notify/push_to_merge_request_email.text.haml +++ b/app/views/notify/push_to_merge_request_email.text.haml @@ -1,4 +1,4 @@ -New commits were pushed to the merge request #{@merge_request.to_reference} by #{@current_user.name} +#{@updated_by_user.name} pushed new commits to merge request #{@merge_request.to_reference} \ #{url_for(project_merge_request_url(@merge_request.target_project, @merge_request))} \ diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml index 825bfd0707f..1e7d9444986 100644 --- a/app/views/projects/_export.html.haml +++ b/app/views/projects/_export.html.haml @@ -21,11 +21,11 @@ %li Project uploads %li Project configuration including web hooks and services %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities + %li LFS objects %p The following items will NOT be exported: %ul %li Job traces and artifacts - %li LFS objects %li Container registry images %li CI variables %li Any encrypted tokens diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 461129a3e0e..74c5317428c 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -49,10 +49,10 @@ .commit-box{ data: { project_path: project_path(@project) } } %h3.commit-title - = markdown(@commit.title, pipeline: :single_line, author: @commit.author) + = markdown_field(@commit, :title) - if @commit.description.present? %pre.commit-description - = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author)) + = preserve(markdown_field(@commit, :description)) .info-well .well-segment.branch-info diff --git a/app/views/projects/commits/_commit.atom.builder b/app/views/projects/commits/_commit.atom.builder index 50f7e7a3a33..640b5ecf99e 100644 --- a/app/views/projects/commits/_commit.atom.builder +++ b/app/views/projects/commits/_commit.atom.builder @@ -10,5 +10,5 @@ xml.entry do xml.email commit.author_email end - xml.summary markdown(commit.description, pipeline: :single_line), type: 'html' + xml.summary markdown_field(commit, :description), type: 'html' end diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml index 2a0704bc7af..a09c13176c3 100644 --- a/app/views/projects/protected_branches/shared/_branches_list.html.haml +++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml @@ -2,7 +2,7 @@ - if @protected_branches.empty? .panel-heading %h3.panel-title - Protected branch (#{@protected_branches.size}) + Protected branch (#{@protected_branches_count}) %p.settings-message.text-center There are currently no protected branches, protect a branch with the form above. - else @@ -16,7 +16,7 @@ %col %thead %tr - %th Protected branch (#{@protected_branches.size}) + %th Protected branch (#{@protected_branches_count}) %th Last commit %th Allowed to merge %th Allowed to push diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml index 3f42ae58438..02908e16dc5 100644 --- a/app/views/projects/protected_tags/shared/_tags_list.html.haml +++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml @@ -2,7 +2,7 @@ - if @protected_tags.empty? .panel-heading %h3.panel-title - Protected tag (#{@protected_tags.size}) + Protected tag (#{@protected_tags_count}) %p.settings-message.text-center There are currently no protected tags, protect a tag with the form above. - else @@ -17,7 +17,7 @@ %col %thead %tr - %th Protected tag (#{@protected_tags.size}) + %th Protected tag (#{@protected_tags_count}) %th Last commit %th Allowed to create - if can_admin_project diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 6afcd447f28..975b9cb4729 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -107,7 +107,7 @@ - selected_labels.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (project_labels_path(@project, :json) if @project) } } + %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project) } } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } = multi_label_name(selected_labels, "Labels") = icon('chevron-down', 'aria-hidden': 'true') diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb index 712a63af532..51fad4faf36 100644 --- a/app/workers/repository_fork_worker.rb +++ b/app/workers/repository_fork_worker.rb @@ -1,28 +1,50 @@ -# Gitaly issue: https://gitlab.com/gitlab-org/gitaly/issues/1110 class RepositoryForkWorker include ApplicationWorker include Gitlab::ShellAdapter include ProjectStartImport include ProjectImportOptions - def perform(project_id, forked_from_repository_storage_path, source_disk_path) - project = Project.find(project_id) + def perform(*args) + target_project_id = args.shift + target_project = Project.find(target_project_id) - return unless start_fork(project) + # By v10.8, we should've drained the queue of all jobs using the old arguments. + # We can remove the else clause if we're no longer logging the message in that clause. + # See https://gitlab.com/gitlab-org/gitaly/issues/1110 + if args.empty? + source_project = target_project.forked_from_project + return target_project.mark_import_as_failed('Source project cannot be found.') unless source_project - Gitlab::Metrics.add_event(:fork_repository, - source_path: source_disk_path, - target_path: project.disk_path) + fork_repository(target_project, source_project.repository_storage, source_project.disk_path) + else + Rails.logger.info("Project #{target_project.id} is being forked using old-style arguments.") + + source_repository_storage_path, source_disk_path = *args - result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_disk_path, - project.repository_storage_path, project.disk_path) - raise "Unable to fork project #{project_id} for repository #{source_disk_path} -> #{project.disk_path}" unless result + source_repository_storage_name = Gitlab.config.repositories.storages.find do |_, info| + info.legacy_disk_path == source_repository_storage_path + end&.first || raise("no shard found for path '#{source_repository_storage_path}'") - project.after_import + fork_repository(target_project, source_repository_storage_name, source_disk_path) + end end private + def fork_repository(target_project, source_repository_storage_name, source_disk_path) + return unless start_fork(target_project) + + Gitlab::Metrics.add_event(:fork_repository, + source_path: source_disk_path, + target_path: target_project.disk_path) + + result = gitlab_shell.fork_repository(source_repository_storage_name, source_disk_path, + target_project.repository_storage, target_project.disk_path) + raise "Unable to fork project #{target_project.id} for repository #{source_disk_path} -> #{target_project.disk_path}" unless result + + target_project.after_import + end + def start_fork(project) return true if start(project) |