Commit 49f198e6 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-staged-changes

parents d6dd2f5f 8dca091f
......@@ -264,8 +264,17 @@ package-and-qa:
stage: build
cache: {}
when: manual
variables:
GIT_STRATEGY: none
before_script:
# We need to download the script rather than clone the repo since the
# package-and-qa job will not be able to run when the branch gets
# deleted (when merging the MR).
- apk add --update openssl
- wget https://gitlab.com/gitlab-org/gitlab-ce/raw/$CI_COMMIT_SHA/scripts/trigger-build-omnibus
- chmod 755 trigger-build-omnibus
script:
- scripts/trigger-build-omnibus
- ./trigger-build-omnibus
only:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
......
......@@ -6,7 +6,6 @@ end
gem_versions = {}
gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2'
gem_versions['default_value_for'] = rails5? ? '~> 3.0.5' : '~> 3.0.0'
gem_versions['html-pipeline'] = rails5? ? '~> 2.6.0' : '~> 1.11.0'
gem_versions['rails'] = rails5? ? '5.0.6' : '4.2.10'
gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
# --- The end of special code for migrating to Rails 5.0 ---
......@@ -136,7 +135,7 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.7'
# Markdown and HTML processing
gem 'html-pipeline', gem_versions['html-pipeline']
gem 'html-pipeline', '~> 2.7.1'
gem 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.6.2'
gem 'redcarpet', '~> 3.4'
......@@ -310,7 +309,7 @@ end
group :development do
gem 'foreman', '~> 0.84.0'
gem 'brakeman', '~> 3.6.0', require: false
gem 'brakeman', '~> 4.2', require: false
gem 'letter_opener_web', '~> 1.3.0'
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
......
......@@ -95,7 +95,7 @@ GEM
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
bootstrap_form (2.7.0)
brakeman (3.6.1)
brakeman (4.2.1)
browser (2.2.0)
builder (3.2.3)
bullet (5.5.1)
......@@ -120,7 +120,7 @@ GEM
activesupport (>= 4.0.0)
mime-types (>= 1.16)
cause (0.1)
charlock_holmes (0.7.5)
charlock_holmes (0.7.6)
childprocess (0.7.0)
ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2)
......@@ -399,9 +399,9 @@ GEM
hipchat (1.5.2)
httparty
mimemagic
html-pipeline (1.11.0)
html-pipeline (2.7.1)
activesupport (>= 2)
nokogiri (~> 1.4)
nokogiri (>= 1.4)
html2text (0.2.0)
nokogiri (~> 1.6)
htmlentities (4.3.4)
......@@ -1012,7 +1012,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0)
brakeman (~> 3.6.0)
brakeman (~> 4.2)
browser (~> 2.2)
bullet (~> 5.5.0)
bundler-audit (~> 0.5.0)
......@@ -1083,7 +1083,7 @@ DEPENDENCIES
hashie-forbidden_attributes
health_check (~> 2.6.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
html-pipeline (~> 2.7.1)
html2text
httparty (~> 0.13.3)
influxdb (~> 0.2)
......
......@@ -10,6 +10,9 @@ const Api = {
projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key',
......@@ -22,25 +25,27 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath)
.replace(':id', groupId);
return axios.get(url)
.then(({ data }) => {
callback(data);
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
return axios.get(url).then(({ data }) => {
callback(data);
return data;
});
return data;
});
},
// Return groups list. Filtered by query
groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath);
return axios.get(url, {
params: Object.assign({
search: query,
per_page: 20,
}, options),
})
return axios
.get(url, {
params: Object.assign(
{
search: query,
per_page: 20,
},
options,
),
})
.then(({ data }) => {
callback(data);
......@@ -51,12 +56,13 @@ const Api = {
// Return namespaces list. Filtered by query
namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath);
return axios.get(url, {
params: {
search: query,
per_page: 20,
},
})
return axios
.get(url, {
params: {
search: query,
per_page: 20,
},
})
.then(({ data }) => callback(data));
},
......@@ -73,9 +79,10 @@ const Api = {
defaults.membership = true;
}
return axios.get(url, {
params: Object.assign(defaults, options),
})
return axios
.get(url, {
params: Object.assign(defaults, options),
})
.then(({ data }) => {
callback(data);
......@@ -85,8 +92,32 @@ const Api = {
// Return single project
project(projectPath) {
const url = Api.buildUrl(Api.projectPath)
.replace(':id', encodeURIComponent(projectPath));
const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
return axios.get(url);
},
// Return Merge Request for project
mergeRequest(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url);
},
mergeRequestChanges(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestChangesPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url);
},
mergeRequestVersions(projectPath, mergeRequestId) {
const url = Api.buildUrl(Api.mergeRequestVersionsPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':mrid', mergeRequestId);
return axios.get(url);
},
......@@ -102,30 +133,30 @@ const Api = {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
}
return axios.post(url, {
label: data,
})
return axios
.post(url, {
label: data,
})
.then(res => callback(res.data))
.catch(e => callback(e.response.data));
},
// Return group projects list. Filtered by query
groupProjects(groupId, query, callback) {
const url = Api.buildUrl(Api.groupProjectsPath)
.replace(':id', groupId);
return axios.get(url, {
params: {
search: query,
per_page: 20,
},
})
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
return axios
.get(url, {
params: {
search: query,
per_page: 20,
},
})
.then(({ data }) => callback(data));
},
commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath)
.replace(':id', encodeURIComponent(id));
const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
return axios.post(url, JSON.stringify(data), {
headers: {
'Content-Type': 'application/json; charset=utf-8',
......@@ -136,39 +167,34 @@ const Api = {
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id))
.replace(':branch', branch);
.replace(':branch', encodeURIComponent(branch));
return axios.get(url);
},
// Return text for a specific license
licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath)
.replace(':key', key);
return axios.get(url, {
params: data,
})
const url = Api.buildUrl(Api.licensePath).replace(':key', key);
return axios
.get(url, {
params: data,
})
.then(res => callback(res.data));
},
gitignoreText(key, callback) {
const url = Api.buildUrl(Api.gitignorePath)
.replace(':key', key);
return axios.get(url)
.then(({ data }) => callback(data));
const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
return axios.get(url).then(({ data }) => callback(data));
},
gitlabCiYml(key, callback) {
const url = Api.buildUrl(Api.gitlabCiYmlPath)
.replace(':key', key);
return axios.get(url)
.then(({ data }) => callback(data));
const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
return axios.get(url).then(({ data }) => callback(data));
},
dockerfileYml(key, callback) {
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
return axios.get(url)
.then(({ data }) => callback(data));
return axios.get(url).then(({ data }) => callback(data));
},
issueTemplate(namespacePath, projectPath, key, type, callback) {
......@@ -177,7 +203,8 @@ const Api = {
.replace(':type', type)
.replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath);
return axios.get(url)
return axios
.get(url)
.then(({ data }) => callback(null, data))
.catch(callback);
},
......@@ -185,10 +212,13 @@ const Api = {
users(query, options) {
const url = Api.buildUrl(this.usersPath);
return axios.get(url, {
params: Object.assign({
search: query,
per_page: 20,
}, options),
params: Object.assign(
{
search: query,
per_page: 20,
},
options,
),
});
},
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale';
export default {
components: {
Icon,
export default {
components: {
Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
required: true,
},
mergeRequestId: {
type: String,
required: false,
default: '',
},
methods: {
changeMode(mode) {
this.$emit('click', mode);
},
viewer: {
type: String,
required: true,
},
};
showShadow: {
type: Boolean,
required: true,
},
},
computed: {
mergeReviewLine() {
return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
mergeRequestId: this.mergeRequestId,
});
},
},
methods: {
changeMode(mode) {
this.$emit('click', mode);
},
},
};
</script>
<template>
......@@ -43,7 +56,10 @@
}"
data-toggle="dropdown"
>
<template v-if="viewer === 'editor'">
<template v-if="viewer === 'mrdiff' && mergeRequestId">
{{ mergeReviewLine }}
</template>
<template v-else-if="viewer === 'editor'">
{{ __('Editing') }}
</template>
<template v-else>
......@@ -57,6 +73,29 @@
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul>
<template v-if="mergeRequestId">
<li>
<a
href="#"
@click.prevent="changeMode('mrdiff')"
:class="{
'is-active': viewer === 'mrdiff',
}"
>
<strong class="dropdown-menu-inner-title">
{{ mergeReviewLine }}
</strong>
<span class="dropdown-menu-inner-content">
{{ __('Compare changes with the merge request target branch') }}
</span>
</a>
</li>
<li
role="separator"
class="divider"
>
</li>
</template>
<li>
<a
href="#"
......
<script>
import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
export default {
components: {
ideSidebar,
ideContextbar,
repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor,
export default {
components: {
ideSidebar,
ideContextbar,
repoTabs,
repoFileButtons,
ideStatusBar,
repoEditor,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
computed: {
...mapState(['changedFiles', 'openFiles', 'viewer']),
...mapGetters(['activeFile', 'hasChanges']),
committedStateSvgPath: {
type: String,
required: true,
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
},
computed: {
...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
...mapGetters(['activeFile', 'hasChanges']),
},
mounted() {
const returnValue = 'Are you sure you want to lose unsaved changes?';
window.onbeforeunload = e => {
if (!this.changedFiles.length) return undefined;
Object.assign(e, {
returnValue,
});
return returnValue;
};
},
};
Object.assign(e, {
returnValue,
});
return returnValue;
};
},
};
</script>
<template>
......@@ -63,6 +63,7 @@
:files="openFiles"
:viewer="viewer"
:has-changes="hasChanges"
:merge-request-id="currentMergeRequestId"
/>
<repo-editor
class="multi-file-edit-pane-content"
......
<script>
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
components: {
icon,
},
directives: {
tooltip,
},
};
</script>
<template>
<icon
name="git-merge"
v-tooltip
title="__('Part of merge request changes')"
css-classes="ide-file-changed-icon"
:size="12"
/>
</template>
<script>
/* global monaco */
import { mapState, mapActions } from 'vuex';
import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
......@@ -13,12 +13,8 @@ export default {
},
},
computed: {
...mapState([
'leftPanelCollapsed',
'rightPanelCollapsed',
'viewer',
'delayViewerUpdated',
]),
...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
...mapGetters(['currentMergeRequest']),
shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw;
},
......@@ -68,9 +64,14 @@ export default {
this.editor.clearEditor();
this.getRawFileData(this.file)
this.getRawFileData({
path: this.file.path,
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
})
.then(() => {
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
const viewerPromise = this.delayViewerUpdated
? this.updateViewer('editor')
: Promise.resolve();
return viewerPromise;
})
......@@ -78,7 +79,7 @@ export default {
this.updateDelayViewerUpdated(false);
this.createEditorInstance();
})
.catch((err) => {
.catch(err => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err;
});
......@@ -101,9 +102,13 @@ export default {
this.model = this.editor.createModel(this.file);
this.editor.attachModel(this.model);
if (this.viewer === 'mrdiff') {
this.editor.attachMergeRequestModel(this.model);
} else {
this.editor.attachModel(this.model);
}
this.model.onChange((model) => {
this.model.onChange(model => {
const { file } = model;
if (file.active) {
......
......@@ -6,6 +6,7 @@ import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue';
import mrFileIcon from './mr_file_icon.vue';
export default {
name: 'RepoFile',
......@@ -15,6 +16,7 @@ export default {
fileStatusIcon,
fileIcon,
changedFileIcon,
mrFileIcon,
},
props: {
file: {
......@@ -56,10 +58,7 @@ export default {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() {
// Manual Action if a tree is selected/opened
if (
this.isTree &&
this.$router.currentRoute.path === `/project${this.file.url}`
) {
if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.toggleTreeOpen(this.file.path);
}
......@@ -102,12 +101,17 @@ export default {
:file="file"
/>
</span>
<changed-file-icon
v-if="file.changed || file.tempFile || file.staged"
:file="file"
:show-tooltip="true"
class="prepend-top-5 pull-right"
/>
<span class="pull-right">
<mr-file-icon
v-if="file.mrChange"
/>
<changed-file-icon
v-if="file.changed || file.tempFile || file.staged"
:file="file"
:show-tooltip="true"
class="prepend-top-5 pull-right"
/>
</span>
<new-dropdown
v-if="isTree"
:project-id="file.projectId"
......
<script>
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility';
export default {
components: {
icon,
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
file: {
type: Object,
required: true,
},
directives: {
tooltip,
},
computed: {
lockTooltip() {
return `Locked by ${this.file.file_lock.user.name}`;
},
props: {
file: {
type: Object,
required: true,
},
},
computed: {
lockTooltip() {
return `Locked by ${this.file.file_lock.user.name}`;
},
},
};
},
};
</script>
<template>
......
<script>
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
export default {
components: {
RepoTab,
EditorMode,
export default {
components: {
RepoTab,
EditorMode,
},
props: {
files: {
type: Array,
required: true,
},
props: {
files: {
type: Array,
required: true,
},
viewer: {
type: String,
required: true,
},
hasChanges: {
type: Boolean,
required: true,
},
viewer: {
type: String,
required: true,
},
data() {
return {
showShadow: false,
};
hasChanges: {
type: Boolean,
required: true,
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow =
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer']),
mergeRequestId: {
type: String,
required: false,
default: '',
},
};
},
data() {
return {
showShadow: false,
};
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer']),
},
};
</script>
<template>
......@@ -55,6 +59,7 @@
:viewer="viewer"
:show-shadow="showShadow"
:has-changes="hasChanges"
:merge-request-id="mergeRequestId"
@click="updateViewer"
/>
</div>
......
......@@ -44,7 +44,7 @@ const router = new VueRouter({
component: EmptyRouterComponent,
},
{
path: 'mr/:mrid',
path: 'merge_requests/:mrid',
component: EmptyRouterComponent,
},
],
......@@ -76,9 +76,7 @@ router.beforeEach((to, from, next) => {
.then(() => {
if (to.params[0]) {
const path =
to.params[0].slice(-1) === '/'
? to.params[0].slice(0, -1)
: to.params[0];
to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
const treeEntry = store.state.entries[path];
if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry);
......@@ -96,6 +94,60 @@ router.beforeEach((to, from, next) => {
);
throw e;
});
} else if (to.params.mrid) {
store.dispatch('updateViewer', 'mrdiff');
store
.dispatch('getMergeRequestData', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
})
.then(mr => {
store.dispatch('getBranchData', {
projectId: fullProjectId,
branchId: mr.source_branch,
});
return store.dispatch('getFiles', {
projectId: fullProjectId,
branchId: mr.source_branch,
});
})
.then(() =>
store.dispatch('getMergeRequestVersions', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
}),
)
.then(() =>
store.dispatch('getMergeRequestChanges', {
projectId: fullProjectId,
mergeRequestId: to.params.mrid,
}),
)
.then(mrChanges => {
mrChanges.changes.forEach((change, ind) => {
const changeTreeEntry = store.state.entries[change.new_path];
if (changeTreeEntry) {
store.dispatch('setFileMrChange', {
file: changeTreeEntry,
mrChange: change,
});
if (ind < 10) {
store.dispatch('getFileData', {
path: change.new_path,
makeFileActive: ind === 0,
});
}
}
});
})
.catch(e => {
flash('Error while loading the merge request. Please try again.');
throw e;
});
}
})
.catch(e => {
......
......@@ -21,6 +21,15 @@ export default class Model {
new this.monaco.Uri(null, null, this.file.path),
)),
);
if (this.file.mrChange) {
this.disposable.add(
(this.baseModel = this.monaco.editor.createModel(
this.file.baseRaw,
undefined,
new this.monaco.Uri(null, null, `target/${this.file.path}`),
)),
);
}
this.events = new Map();
......@@ -28,10 +37,7 @@ export default class Model {
this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$on(
`editor.update.model.content.${this.file.path}`,
this.updateContent,
);
eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
get url() {
......@@ -58,6 +64,10 @@ export default class Model {
return this.originalModel;
}
getBaseModel() {
return this.baseModel;
}
setValue(value) {
this.getModel().setValue(value);
}
......@@ -81,13 +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.content.${this.file.path}`,
this.updateContent,
);
eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
}
}
......@@ -109,11 +109,19 @@ export default class Editor {
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
}
attachMergeRequestModel(model) {
this.instance.setModel({
original: model.getBaseModel(),
modified: model.getModel(),
});
this.monaco.editor.createDiffNavigator(this.instance, {
alwaysRevealFirst: true,
});
}
setupMonacoTheme() {
this.monaco.editor.defineTheme(
gitlabTheme.themeName,
gitlabTheme.monacoTheme,
);
this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
this.monaco.editor.setTheme('gitlab');
}
......@@ -161,8 +169,6 @@ export default class Editor {
onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return;
this.disposable.add(
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
);
this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
}
}
......@@ -20,12 +20,35 @@ export default {
return Promise.resolve(file.raw);
}
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
},
getBaseRawFileData(file, sha) {
if (file.tempFile) {
return Promise.resolve(file.baseRaw);
}
if (file.baseRaw) {
return Promise.resolve(file.baseRaw);
}
return Vue.http
.get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
params: { format: 'json' },
})
.then(res => res.text());
},
getProjectData(namespace, project) {
return Api.project(`${namespace}/${project}`);
},
getProjectMergeRequestData(projectId, mergeRequestId) {
return Api.mergeRequest(projectId, mergeRequestId);
},
getProjectMergeRequestChanges(projectId, mergeRequestId) {
return Api.mergeRequestChanges(projectId, mergeRequestId);
},
getProjectMergeRequestVersions(projectId, mergeRequestId) {
return Api.mergeRequestVersions(projectId, mergeRequestId);
},
getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId);
},
......
......@@ -7,8 +7,7 @@ import FilesDecoratorWorker from './workers/files_decorator_worker';
export const redirectToUrl = (_, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) =>
commit(types.SET_INITIAL_DATA, data);
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach(file => {
......@@ -60,14 +59,11 @@ export const createTempEntry = (
) =>
new Promise(resolve => {
const worker = new FilesDecoratorWorker();
const fullName =
name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) {
flash(
`The name "${name
.split('/')
.pop()}" is already taken in this directory.`,
`The name "${name.split('/').pop()}" is already taken in this directory.`,
'alert',
document,
null,
......@@ -144,3 +140,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/merge_request';
......@@ -46,53 +46,63 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
commit(types.SET_CURRENT_BRANCH, file.branchId);
};
export const getFileData = ({ state, commit, dispatch }, file) => {
export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
const file = state.entries[path];
commit(types.TOGGLE_LOADING, { entry: file });
return service
.getFileData(file.url)
.then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle);
return res.json();
})
.then(data => {
commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file.path);
dispatch('setFileActive', file.path);
commit(types.TOGGLE_FILE_OPEN, path);
if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file });
})
.catch(() => {
commit(types.TOGGLE_LOADING, { entry: file });
flash(
'Error loading file data. Please try again.',
'alert',
document,
null,
false,
true,
);
flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
});
};
export const getRawFileData = ({ commit, dispatch }, file) =>
service
.getRawFileData(file)
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
})
.catch(() =>
flash(
'Error loading file content. Please try again.',
'alert',
document,
null,
false,
true,
),
);
export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
};
export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
const file = state.entries[path];
return new Promise((resolve, reject) => {
service
.getRawFileData(file)
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw });
if (file.mrChange && file.mrChange.new_file === false) {
service
.getBaseRawFileData(file, baseSha)
.then(baseRaw => {
commit(types.SET_FILE_BASE_RAW_DATA, {
file,
baseRaw,
});
resolve(raw);
})
.catch(e => {
reject(e);
});
} else {
resolve(raw);
}
})
.catch(() => {
flash('Error loading file content. Please try again.');
reject();
});
});
};
export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path];
......@@ -119,10 +129,7 @@ export const setFileEOL = ({ getters, commit }, { eol }) => {
}
};
export const setEditorPosition = (
{ getters, commit },
{ editorRow, editorColumn },
) => {
export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
if (getters.activeFile) {
commit(types.SET_FILE_POSITION, {
file: getters.activeFile,
......
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
export const getMergeRequestData = (
{ commit, state, dispatch },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
service
.getProjectMergeRequestData(projectId, mergeRequestId)
.then(res => res.data)
.then(data => {
commit(types.SET_MERGE_REQUEST, {
projectPath: projectId,
mergeRequestId,
mergeRequest: data,
});
if (!state.currentMergeRequestId) {
commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
}
resolve(data);
})
.catch(() => {
flash('Error loading merge request data. Please try again.');
reject(new Error(`Merge Request not loaded ${projectId}`));
});
} else {
resolve(state.projects[projectId].mergeRequests[mergeRequestId]);
}
});
export const getMergeRequestChanges = (
{ commit, state, dispatch },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
service
.getProjectMergeRequestChanges(projectId, mergeRequestId)
.then(res => res.data)
.then(data => {
commit(types.SET_MERGE_REQUEST_CHANGES, {
projectPath: projectId,
mergeRequestId,
changes: data,
});
resolve(data);
})
.catch(() => {
flash('Error loading merge request changes. Please try again.');
reject(new Error(`Merge Request Changes not loaded ${projectId}`));
});
} else {
resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes);
}
});
export const getMergeRequestVersions = (
{ commit, state, dispatch },
{ projectId, mergeRequestId, force = false } = {},
) =>
new Promise((resolve, reject) => {
if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) {
service
.getProjectMergeRequestVersions(projectId, mergeRequestId)
.then(res => res.data)
.then(data => {
commit(types.SET_MERGE_REQUEST_VERSIONS, {
projectPath: projectId,
mergeRequestId,
versions: data,
});
resolve(data);
})
.catch(() => {
flash('Error loading merge request versions. Please try again.');
reject(new Error(`Merge Request Versions not loaded ${projectId}`));
});
} else {
resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
}
});
......@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash';
import service from '../../services';
import * as types from '../mutation_types';
import {
findEntry,
} from '../utils';
import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
......@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('setFileActive', row.path);
} else {
dispatch('getFileData', row);
dispatch('getFileData', { path: row.path });
}
};
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service.getTreeLastCommit(tree.lastCommitPath)
.then((res) => {
service
.getTreeLastCommit(tree.lastCommitPath)
.then(res => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json();
})
.then((data) => {
data.forEach((lastCommit) => {
.then(data => {
data.forEach(lastCommit => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) {
......@@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
};
export const getFiles = (
{ state, commit, dispatch },
{ projectId, branchId } = {},
) => new Promise((resolve, reject) => {
if (!state.trees[`${projectId}/${branchId}`]) {
const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service
.getFiles(selectedProject.web_url, branchId)
.then(res => res.json())
.then((data) => {
const worker = new FilesDecoratorWorker();
worker.addEventListener('message', (e) => {
const { entries, treeList } = e.data;
const selectedTree = state.trees[`${projectId}/${branchId}`];
commit(types.SET_ENTRIES, entries);
commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
worker.terminate();
resolve();
});
worker.postMessage({
data,
projectId,
branchId,
export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
new Promise((resolve, reject) => {
if (!state.trees[`${projectId}/${branchId}`]) {
const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
service
.getFiles(selectedProject.web_url, branchId)
.then(res => res.json())
.then(data => {
const worker = new FilesDecoratorWorker();
worker.addEventListener('message', e => {
const { entries, treeList } = e.data;
const selectedTree = state.trees[`${projectId}/${branchId}`];
commit(types.SET_ENTRIES, entries);
commit(types.SET_DIRECTORY_DATA, {
treePath: `${projectId}/${branchId}`,
data: treeList,
});
commit(types.TOGGLE_LOADING, {
entry: selectedTree,
forceValue: false,
});
worker.terminate();
resolve();
});
worker.postMessage({
data,
projectId,
branchId,
});
})
.catch(e => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
reject(e);
});
})
.catch((e) => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
reject(e);
});
} else {
resolve();
}
});
} else {
resolve();
}
});
export const activeFile = state =>
state.openFiles.find(file => file.active) || null;
export const activeFile = state => state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const modifiedFiles = state =>
state.changedFiles.filter(f => !f.tempFile);
export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => {
......@@ -23,13 +21,21 @@ export const projectsWithTrees = state =>
};
});
export const currentMergeRequest = state => {
if (state.projects[state.currentProjectId]) {
return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId];
}
return null;
};
// eslint-disable-next-line no-confusing-arrow
export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state =>
!!state.changedFiles.length || !!state.stagedFiles.length;
export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
// eslint-disable-next-line no-confusing-arrow
export const collapseButtonTooltip = state =>
state.rightPanelCollapsed ? 'Expand sidebar' : 'Collapse sidebar';
export const hasMergeRequest = state => !!state.currentMergeRequestId;
......@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
// Merge Request Mutation Types
export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST';
export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES';
export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
// Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
......@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION';
......@@ -39,6 +46,7 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
export const UPDATE_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
......
import * as types from './mutation_types';
import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
......@@ -11,10 +12,7 @@ export default {
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
if (entry.path) {
Object.assign(state.entries[entry.path], {
loading:
forceValue !== undefined
? forceValue
: !state.entries[entry.path].loading,
loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
});
} else {
Object.assign(entry, {
......@@ -88,9 +86,7 @@ export default {
if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], {
tree: state.trees[`${projectId}/${branchId}`].tree.concat(
data.treeList,
),
tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
});
}
},
......@@ -120,6 +116,7 @@ export default {
});
},
...projectMutations,
...mergeRequestMutation,
...fileMutations,
...treeMutations,
...branchMutations,
......
......@@ -28,6 +28,8 @@ export default {
rawPath: data.raw_path,
binary: data.binary,
renderError: data.render_error,
raw: null,
baseRaw: null,
});
},
[types.SET_FILE_RAW_DATA](state, { file, raw }) {
......@@ -35,6 +37,11 @@ export default {
raw,
});
},
[types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
Object.assign(state.entries[file.path], {
baseRaw,
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw;
......@@ -59,6 +66,11 @@ export default {
editorColumn,
});
},
[types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
Object.assign(state.entries[file.path], {
mrChange,
});
},
[types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], {
content: state.entries[path].raw,
......
import * as types from '../mutation_types';
export default {
[types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) {
Object.assign(state, {
currentMergeRequestId,
});
},
[types.SET_MERGE_REQUEST](state, { projectPath, mergeRequestId, mergeRequest }) {
Object.assign(state.projects[projectPath], {
mergeRequests: {
[mergeRequestId]: {
...mergeRequest,
active: true,
changes: [],
versions: [],
baseCommitSha: null,
},
},
});
},
[types.SET_MERGE_REQUEST_CHANGES](state, { projectPath, mergeRequestId, changes }) {
Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
changes,
});
},
[types.SET_MERGE_REQUEST_VERSIONS](state, { projectPath, mergeRequestId, versions }) {
Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
versions,
baseCommitSha: versions.length ? versions[0].base_commit_sha : null,
});
},
};
......@@ -11,6 +11,7 @@ export default {
Object.assign(project, {
tree: [],
branches: {},
mergeRequests: {},
active: true,
});
......
export default () => ({
currentProjectId: '',
currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [],
stagedFiles: [],
endpoints: {},
......
......@@ -51,7 +51,7 @@ export function removeParams(params) {
const url = document.createElement('a');
url.href = window.location.href;
params.forEach((param) => {
params.forEach(param => {
url.search = removeParamQueryString(url.search, param);
});
......@@ -83,3 +83,11 @@ export function refreshCurrentPage() {
export function redirectTo(url) {
return window.location.assign(url);
}
export function webIDEUrl(route = undefined) {
let returnUrl = `${gon.relative_url_root}/-/ide/`;
if (route) {
returnUrl += `project${route}`;
}
return returnUrl;
}
<script>
import Flash from '../../../flash';
import editForm from './edit_form.vue';
import Icon from '../../../vue_shared/components/icon.vue';
import { __ } from '../../../locale';
import Flash from '../../../flash';
import editForm from './edit_form.vue';
import Icon from '../../../vue_shared/components/icon.vue';
import { __ } from '../../../locale';
import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
export default {
components: {
editForm,
Icon,
},
props: {
isConfidential: {
required: true,
type: Boolean,
},
props: {
isConfidential: {
required: true,
type: Boolean,
},
isEditable: {
required: true,
type: Boolean,
},
service: {
required: true,
type: Object,
},
isEditable: {
required: true,
type: Boolean,
},
data() {
return {
edit: false,
};
service: {
required: true,
type: Object,
},
computed: {
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
},
},
data() {
return {
edit: false,
};
},
computed: {
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
},
methods: {
toggleForm() {
this.edit = !this.edit;
},
updateConfidentialAttribute(confidential) {
this.service.update('issue', { confidential })
.then(() => location.reload())
.catch(() => {
Flash(__('Something went wrong trying to change the confidentiality of this issue'));
});
},
},
created() {
eventHub.$on('closeConfidentialityForm', this.toggleForm);
},
beforeDestroy() {
eventHub.$off('closeConfidentialityForm', this.toggleForm);
},
methods: {
toggleForm() {
this.edit = !this.edit;
},
};
updateConfidentialAttribute(confidential) {
this.service
.update('issue', { confidential })
.then(() => location.reload())
.catch(() => {
Flash(
__(
'Something went wrong trying to change the confidentiality of this issue',
),
);
});
},
},
};
</script>
<template>
<div class="block issuable-sidebar-item confidentiality">
<div class="sidebar-collapsed-icon">
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
>
<icon
:name="confidentialityIcon"
:size="16"
aria-hidden="true"
/>
</div>
......@@ -71,7 +85,6 @@
<div class="value sidebar-item-value hide-collapsed">
<editForm
v-if="edit"
:toggle-form="toggleForm"
:is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute"
/>
......
<script>
import editFormButtons from './edit_form_buttons.vue';
import { s__ } from '../../../locale';
import editFormButtons from './edit_form_buttons.vue';
import { s__ } from '../../../locale';
export default {
components: {
editFormButtons,
export default {
components: {
editFormButtons,
},
props: {
isConfidential: {
required: true,
type: Boolean,
},
props: {
isConfidential: {
required: true,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateConfidentialAttribute: {
required: true,
type: Function,
},
updateConfidentialAttribute: {
required: true,
type: Function,
},
computed: {
confidentialityOnWarning() {
return s__('confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.');
},
confidentialityOffWarning() {
return s__('confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.');
},
},
computed: {
confidentialityOnWarning() {
return s__(
'confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.',
);
},
};
confidentialityOffWarning() {
return s__(
'confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.',
);
},
},
};
</script>
<template>
......@@ -45,7 +45,6 @@
</p>
<edit-form-buttons
:is-confidential="isConfidential"
:toggle-form="toggleForm"
:update-confidential-attribute="updateConfidentialAttribute"
/>
</div>
......
<script>
import $ from 'jquery';
import eventHub from '../../event_hub';
export default {
props: {
isConfidential: {
required: true,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateConfidentialAttribute: {
required: true,
type: Function,
......@@ -22,6 +21,16 @@ export default {
return !this.isConfidential;
},
},
methods: {
closeForm() {
eventHub.$emit('closeConfidentialityForm');
$(this.$el).trigger('hidden.gl.dropdown');
},
submitForm() {
this.closeForm();
this.updateConfidentialAttribute(this.updateConfidentialBool);
},
},
};
</script>
......@@ -30,14 +39,14 @@ export default {
<button
type="button"
class="btn btn-default append-right-10"
@click="toggleForm"
@click="closeForm"
>
{{ __('Cancel') }}
</button>
<button
type="button"
class="btn btn-close"
@click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
@click.prevent="submitForm"
>
{{ toggleButtonText }}
</button>
......
<script>
import editFormButtons from './edit_form_buttons.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
import { __, sprintf } from '../../../locale';
import editFormButtons from './edit_form_buttons.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
import { __, sprintf } from '../../../locale';
export default {
components: {
editFormButtons,
export default {
components: {
editFormButtons,
},
mixins: [issuableMixin],
props: {
isLocked: {
required: true,
type: Boolean,
},
mixins: [
issuableMixin,
],
props: {
isLocked: {
required: true,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateLockedAttribute: {
required: true,
type: Function,
},
updateLockedAttribute: {
required: true,
type: Function,
},
},
computed: {
lockWarning() {
return sprintf(
__(
'Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.',
),
{ issuableDisplayName: this.issuableDisplayName },
);
},
computed: {
lockWarning() {
return sprintf(__('Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
},
unlockWarning() {
return sprintf(__('Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
},
unlockWarning() {
return sprintf(
__(
'Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.',
),
{ issuableDisplayName: this.issuableDisplayName },
);
},
};
},
};
</script>
<template>
......@@ -54,7 +57,6 @@
<edit-form-buttons
:is-locked="isLocked"
:toggle-form="toggleForm"
:update-locked-attribute="updateLockedAttribute"
/>
</div>
......
<script>
import $ from 'jquery';
import eventHub from '../../event_hub';
export default {
props: {
isLocked: {
......@@ -6,11 +9,6 @@ export default {
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateLockedAttribute: {
required: true,
type: Function,
......@@ -26,6 +24,17 @@ export default {
return !this.isLocked;
},
},
methods: {
closeForm() {
eventHub.$emit('closeLockForm');
$(this.$el).trigger('hidden.gl.dropdown');
},
submitForm() {
this.closeForm();
this.updateLockedAttribute(this.toggleLock);
},
},
};
</script>
......@@ -34,7 +43,7 @@ export default {
<button
type="button"
class="btn btn-default append-right-10"
@click="toggleForm"
@click="closeForm"
>
{{ __('Cancel') }}
</button>
......@@ -42,7 +51,7 @@ export default {
<button
type="button"
class="btn btn-close"
@click.prevent="updateLockedAttribute(toggleLock)"
@click.prevent="submitForm"
>
{{ buttonText }}
</button>
......
<script>
import Flash from '~/flash';
import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
import Icon from '../../../vue_shared/components/icon.vue';
import Flash from '~/flash';
import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable';
import Icon from '../../../vue_shared/components/icon.vue';
import eventHub from '../../event_hub';
export default {
components: {
editForm,
Icon,
},
mixins: [
issuableMixin,
],
export default {
components: {
editForm,
Icon,
},
mixins: [issuableMixin],
props: {
isLocked: {
required: true,
type: Boolean,
},
props: {
isLocked: {
required: true,
type: Boolean,
},
isEditable: {
required: true,
type: Boolean,
},
isEditable: {
required: true,
type: Boolean,
},
mediator: {
required: true,
type: Object,
validator(mediatorObject) {
return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
},
mediator: {
required: true,
type: Object,
validator(mediatorObject) {
return (
mediatorObject.service &&
mediatorObject.service.update &&
mediatorObject.store
);
},
},
},
computed: {
lockIcon() {
return this.isLocked ? 'lock' : 'lock-open';
},
computed: {
lockIcon() {
return this.isLocked ? 'lock' : 'lock-open';
},
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
},
isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen;
},
},
methods: {
toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
},
created() {
eventHub.$on('closeLockForm', this.toggleForm);
},
beforeDestroy() {
eventHub.$off('closeLockForm', this.toggleForm);
},
updateLockedAttribute(locked) {
this.mediator.service.update(this.issuableType, {
methods: {
toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store
.isLockDialogOpen;
},
updateLockedAttribute(locked) {
this.mediator.service
.update(this.issuableType, {
discussion_locked: locked,
})
.then(() => location.reload())
.catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`)));
},
.catch(() =>
Flash(
this.__(
`Something went wrong trying to change the locked state of this ${
this.issuableDisplayName
}`,
),
),
);
},
};
},
};
</script>
<template>
<div class="block issuable-sidebar-item lock">
<div class="sidebar-collapsed-icon">
<div
class="sidebar-collapsed-icon"
@click="toggleForm"
>
<icon
:name="lockIcon"
:size="16"
aria-hidden="true"
class="sidebar-item-icon is-active"
/>
......@@ -85,7 +108,6 @@
<div class="value sidebar-item-value hide-collapsed">
<edit-form
v-if="isLockDialogOpen"
:toggle-form="toggleForm"
:is-locked="isLocked"
:update-locked-attribute="updateLockedAttribute"
:issuable-type="issuableType"
......
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale';
import { webIDEUrl } from '~/lib/utils/url_utility';
import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
name: 'MRWidgetHeader',
directives: {
tooltip,
export default {
name: 'MRWidgetHeader',
directives: {
tooltip,
},
components: {
icon,
clipboardButton,
},
props: {
mr: {
type: Object,
required: true,
},
components: {
icon,
clipboardButton,
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
props: {
mr: {
type: Object,
required: true,
},
commitsText() {
return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
},
computed: {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
commitsText() {
return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
// copy/pasting of plain text or GFM.
return JSON.stringify({
text: this.mr.sourceBranch,
gfm: `\`${this.mr.sourceBranch}\``,
});
},
isSourceBranchLong() {
return this.isBranchTitleLong(this.mr.sourceBranch);
},
isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
// copy/pasting of plain text or GFM.
return JSON.stringify({
text: this.mr.sourceBranch,
gfm: `\`${this.mr.sourceBranch}\``,
});
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
isSourceBranchLong() {
return this.isBranchTitleLong(this.mr.sourceBranch);
},
};
isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
webIdePath() {
return webIDEUrl(this.mr.statusPath.replace('.json', ''));
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
};
</script>
<template>
<div class="mr-source-target">
......@@ -96,6 +100,13 @@
</div>
<div v-if="mr.isOpen">
<a
v-if="!mr.sourceBranchRemoved"
:href="webIdePath"
class="btn btn-sm btn-default inline js-web-ide"
>
{{ s__("mrWidget|Web IDE") }}
</a>
<button
data-target="#modal_merge_info"
data-toggle="modal"
......
......@@ -88,7 +88,6 @@
.right-sidebar {
border-left: 1px solid $border-color;
height: calc(100% - #{$header-height});
}
.with-performance-bar .right-sidebar.affix {
......
......@@ -522,10 +522,6 @@
.with-performance-bar .right-sidebar {
top: $header-height + $performance-bar-height;
.issuable-sidebar {
height: calc(100% - #{$performance-bar-height});
}
}
.sidebar-move-issue-confirmation-button {
......
......@@ -19,7 +19,7 @@
.ide-view {
display: flex;
height: calc(100vh - #{$header-height});
margin-top: 40px;
margin-top: 0;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
......@@ -53,6 +53,7 @@
flex: 1;
white-space: nowrap;
text-overflow: ellipsis;
max-width: inherit;
svg {
vertical-align: middle;
......@@ -460,6 +461,8 @@
display: flex;
flex-direction: column;
flex: 1;
max-height: 100%;
overflow: auto;
}
.ide-commity-empty-state {
......@@ -474,7 +477,7 @@
.multi-file-commit-panel-header {
display: flex;
align-items: center;
margin-bottom: 12px;
margin-bottom: 0;
border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0;
......@@ -689,8 +692,14 @@
overflow: hidden;
&.nav-only {
padding-top: $header-height;
.with-performance-bar & {
padding-top: $header-height + $performance-bar-height;
}
.flash-container {
margin-top: $header-height;
margin-top: 0;
margin-bottom: 0;
}
......@@ -700,7 +709,7 @@
}
.content-wrapper {
margin-top: $header-height;
margin-top: 0;
padding-bottom: 0;
}
......@@ -724,11 +733,11 @@
.with-performance-bar .ide.nav-only {
.flash-container {
margin-top: #{$header-height + $performance-bar-height};
margin-top: 0;
}
.content-wrapper {
margin-top: #{$header-height + $performance-bar-height};
margin-top: 0;
padding-bottom: 0;
}
......@@ -737,10 +746,6 @@
}
&.flash-shown {
.content-wrapper {
margin-top: 0;
}
.ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
}
......
This diff is collapsed.
......@@ -50,9 +50,19 @@ class Admin::AppearancesController < Admin::ApplicationController
# Only allow a trusted parameter "white list" through.
def appearance_params
params.require(:appearance).permit(
:title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache,
:new_project_guidelines, :updated_by
)
params.require(:appearance).permit(allowed_appearance_params)
end
def allowed_appearance_params
%i[
title
description
logo
logo_cache
header_logo
header_logo_cache
new_project_guidelines
updated_by
]
end
end
......@@ -46,6 +46,8 @@ class Projects::ServicesController < Projects::ApplicationController
else
{ error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') }
end
rescue Gitlab::HTTP::BlockedUrlError => e
{ error: true, message: 'Test failed.', service_response: e.message }
end
def success_message
......
module AppearancesHelper
def brand_title
brand_item&.title.presence || 'GitLab Community Edition'
current_appearance&.title.presence || 'GitLab Community Edition'
end
def brand_image
image_tag(brand_item.logo) if brand_item&.logo?
image_tag(current_appearance.logo) if current_appearance&.logo?
end
def brand_text
markdown_field(brand_item, :description)
markdown_field(current_appearance, :description)
end
def brand_new_project_guidelines
markdown_field(brand_item, :new_project_guidelines)
markdown_field(current_appearance, :new_project_guidelines)
end
def brand_item
def current_appearance
@appearance ||= Appearance.current
end
def brand_header_logo
if brand_item&.header_logo?
image_tag brand_item.header_logo
if current_appearance&.header_logo?
image_tag current_appearance.header_logo
else
render 'shared/logo.svg'
end
......@@ -29,7 +29,7 @@ module AppearancesHelper
# Skip the 'GitLab' type logo when custom brand logo is set
def brand_header_logo_type
unless brand_item&.header_logo?
unless current_appearance&.header_logo?
render 'shared/logo_type.svg'
end
end
......
......@@ -285,6 +285,10 @@ module ApplicationHelper
class_names
end
# EE feature: System header and footer, unavailable in CE
def system_message_class
end
# Returns active css class when condition returns true
# otherwise returns nil.
#
......
......@@ -54,9 +54,9 @@ module EmailsHelper
end
def header_logo
if brand_item && brand_item.header_logo?
if current_appearance&.header_logo?
image_tag(
brand_item.header_logo,
current_appearance.header_logo,
style: 'height: 50px'
)
else
......
......@@ -36,16 +36,15 @@ module Ci
def external_url(project, job)
return unless external_link?(job)
full_path_parts = project.full_path_components
top_level_group = full_path_parts.shift
url_project_path = project.full_path.partition('/').last
artifact_path = [
'-', *full_path_parts, '-',
'-', url_project_path, '-',
'jobs', job.id,
'artifacts', path
].join('/')
"#{pages_config.protocol}://#{top_level_group}.#{pages_config.host}/#{artifact_path}"
"#{project.pages_group_url}/#{artifact_path}"
end
def external_link?(job)
......
......@@ -6,6 +6,7 @@ module Ci
include ObjectStorage::BackgroundMove
include Presentable
include Importable
include Gitlab::Utils::StrongMemoize
MissingDependenciesError = Class.new(StandardError)
......@@ -25,15 +26,17 @@ module Ci
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :metadata, class_name: 'Ci::BuildMetadata'
delegate :timeout, to: :metadata, prefix: true, allow_nil: true
# The "environment" field for builds is a String, and is the unexpanded name
##
# The "environment" field for builds is a String, and is the unexpanded name!
#
def persisted_environment
@persisted_environment ||= Environment.find_by(
name: expanded_environment_name,
project: project
)
return unless has_environment?
strong_memoize(:persisted_environment) do
Environment.find_by(name: expanded_environment_name, project: project)
end
end
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
......@@ -212,7 +215,11 @@ module Ci
end
def expanded_environment_name
ExpandVariables.expand(environment, simple_variables) if environment
return unless has_environment?
strong_memoize(:expanded_environment_name) do
ExpandVariables.expand(environment, simple_variables)
end
end
def has_environment?
......@@ -258,31 +265,52 @@ module Ci
Gitlab::Utils.slugify(ref.to_s)
end
# Variables whose value does not depend on environment
def simple_variables
variables(environment: nil)
end
# All variables, including those dependent on environment, which could
# contain unexpanded variables.
def variables(environment: persisted_environment)
collection = Gitlab::Ci::Variables::Collection.new.tap do |variables|
##
# Variables in the environment name scope.
#
def scoped_variables(environment: expanded_environment_name)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.concat(predefined_variables)
variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runner
variables.concat(project.deployment_variables(environment: environment)) if has_environment?
variables.concat(project.deployment_variables(environment: environment)) if environment
variables.concat(yaml_variables)
variables.concat(user_variables)
variables.concat(project.group.secret_variables_for(ref, project)) if project.group
variables.concat(secret_variables(environment: environment))
variables.concat(secret_group_variables)
variables.concat(secret_project_variables(environment: environment))
variables.concat(trigger_request.user_variables) if trigger_request
variables.concat(pipeline.variables)
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
variables.concat(persisted_environment_variables) if environment
end
end
##
# Variables that do not depend on the environment name.
#
def simple_variables
strong_memoize(:simple_variables) do
scoped_variables(environment: nil).to_runner_variables
end
end
collection.to_runner_variables
##
# All variables, including persisted environment variables.
#
def variables
Gitlab::Ci::Variables::Collection.new
.concat(persisted_variables)
.concat(scoped_variables)
.concat(persisted_environment_variables)
.to_runner_variables
end
##
# Regular Ruby hash of scoped variables, without duplicates that are
# possible to be present in an array of hashes returned from `variables`.
#
def scoped_variables_hash
scoped_variables.to_hash
end
def features
......@@ -459,9 +487,14 @@ module Ci
end
end
def secret_variables(environment: persisted_environment)
def secret_group_variables
return [] unless project.group
project.group.secret_variables_for(ref, project)
end
def secret_project_variables(environment: persisted_environment)
project.secret_variables_for(ref: ref, environment: environment)
.map(&:to_runner_variable)
end
def steps
......@@ -558,6 +591,21 @@ module Ci
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
def persisted_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables unless persisted?
variables
.append(key: 'CI_JOB_ID', value: id.to_s)
.append(key: 'CI_JOB_TOKEN', value: token, public: false)
.append(key: 'CI_BUILD_ID', value: id.to_s)
.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
end
end
def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true')
......@@ -566,16 +614,11 @@ module Ci
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
variables.append(key: 'CI_JOB_ID', value: id.to_s)
variables.append(key: 'CI_JOB_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage)
variables.append(key: 'CI_JOB_TOKEN', value: token, public: false)
variables.append(key: 'CI_COMMIT_SHA', value: sha)
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
variables.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
variables.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
variables.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
......@@ -583,23 +626,8 @@ module Ci
end
end
def persisted_environment_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables unless persisted_environment
variables.concat(persisted_environment.predefined_variables)
# Here we're passing unexpanded environment_url for runner to expand,
# and we need to make sure that CI_ENVIRONMENT_NAME and
# CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
end
end
def legacy_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI_BUILD_ID', value: id.to_s)
variables.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
variables.append(key: 'CI_BUILD_REF', value: sha)
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
......@@ -612,6 +640,19 @@ module Ci
end
end
def persisted_environment_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
return variables unless persisted? && persisted_environment.present?
variables.concat(persisted_environment.predefined_variables)
# Here we're passing unexpanded environment_url for runner to expand,
# and we need to make sure that CI_ENVIRONMENT_NAME and
# CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
end
end
def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url
end
......
......@@ -51,6 +51,10 @@ module Clusters
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
scope :user_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:user]) }
scope :gcp_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:gcp]) }
scope :gcp_installed, -> { gcp_provided.includes(:provider_gcp).where(cluster_providers_gcp: { status: ::Clusters::Providers::Gcp.state_machines[:status].states[:created].value }) }
scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
def status_name
......
......@@ -4,6 +4,8 @@ module Clusters
extend ActiveSupport::Concern
included do
scope :installed, -> { where(status: self.state_machines[:status].states[:installed].value) }
state_machine :status, initial: :not_installable do
state :not_installable, value: -2
state :errored, value: -1
......
......@@ -23,6 +23,7 @@ class Issue < ActiveRecord::Base
belongs_to :project
belongs_to :moved_to, class_name: 'Issue'
belongs_to :closed_by, class_name: 'User'
has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
......@@ -78,6 +79,11 @@ class Issue < ActiveRecord::Base
before_transition any => :closed do |issue|
issue.closed_at = Time.zone.now
end
before_transition closed: :opened do |issue|
issue.closed_at = nil
issue.closed_by = nil
end
end
class << self
......
......@@ -1346,20 +1346,19 @@ class Project < ActiveRecord::Base
Dir.exist?(public_pages_path)
end
def pages_url
subdomain, _, url_path = full_path.partition('/')
# The hostname always needs to be in downcased
# All web servers convert hostname to lowercase
host = "#{subdomain}.#{Settings.pages.host}".downcase
def pages_group_url
# The host in URL always needs to be downcased
url = Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
"#{prefix}#{subdomain}."
Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
"#{prefix}#{pages_subdomain}."
end.downcase
end
def pages_url
url = pages_group_url
url_path = full_path.partition('/').last
# If the project path is the same as host, we serve it as group page
return url if host == url_path
return url if url == "#{Settings.pages.protocol}://#{url_path}"
"#{url}/#{url_path}"
end
......@@ -1545,8 +1544,8 @@ class Project < ActiveRecord::Base
@errors = original_errors
end
def add_export_job(current_user:, params: {})
job_id = ProjectExportWorker.perform_async(current_user.id, self.id, params)
def add_export_job(current_user:, after_export_strategy: nil, params: {})
job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params)
if job_id
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
......@@ -1572,6 +1571,8 @@ class Project < ActiveRecord::Base
def export_status
if export_in_progress?
:started
elsif after_export_in_progress?
:after_export_action
elsif export_project_path
:finished
else
......@@ -1583,12 +1584,22 @@ class Project < ActiveRecord::Base
import_export_shared.active_export_count > 0
end
def after_export_in_progress?
import_export_shared.after_export_in_progress?
end
def remove_exports
return nil unless export_path.present?
FileUtils.rm_rf(export_path)
end
def remove_exported_project_file
return unless export_project_path.present?
FileUtils.rm_f(export_project_path)
end
def full_path_slug
Gitlab::Utils.slugify(full_path.to_s)
end
......
......@@ -2,11 +2,15 @@ module Boards
class ListService < Boards::BaseService
def execute
create_board! if parent.boards.empty?
parent.boards
boards
end
private
def boards
parent.boards
end
def create_board!
Boards::CreateService.new(parent, current_user).execute
end
......
......@@ -23,6 +23,7 @@ module Issues
end
if project.issues_enabled? && issue.close
issue.update(closed_by: current_user)
event_service.close_issue(issue, current_user)
create_note(issue, commit) if system_note
notification_service.close_issue(issue, current_user) if notifications
......
module Projects
module ImportExport
class ExportService < BaseService
def execute(_options = {})
def execute(after_export_strategy = nil, options = {})
@shared = project.import_export_shared
save_all
save_all!
execute_after_export_action(after_export_strategy)
end
private
def save_all
if [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
def execute_after_export_action(after_export_strategy)
return unless after_export_strategy
unless after_export_strategy.execute(current_user, project)
cleanup_and_notify_error
end
end
def save_all!
if save_services
Gitlab::ImportExport::Saver.save(project: project, shared: @shared)
notify_success
else
cleanup_and_notify
cleanup_and_notify_error!
end
end
def save_services
[version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
end
def version_saver
Gitlab::ImportExport::VersionSaver.new(shared: @shared)
end
......@@ -41,19 +55,22 @@ module Projects
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
end
def cleanup_and_notify
def cleanup_and_notify_error
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
FileUtils.rm_rf(@shared.export_path)
notify_error
end
def cleanup_and_notify_error!
cleanup_and_notify_error
raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
end
def notify_success
Rails.logger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported")
notification_service.project_exported(@project, @current_user)
end
def notify_error
......
......@@ -28,7 +28,11 @@ module Projects
def add_repository_to_project
if project.external_import? && !unknown_url?
raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
begin
Gitlab::UrlBlocker.validate!(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
rescue Gitlab::UrlBlocker::BlockedUrlError => e
raise Error, "Blocked import URL: #{e.message}"
end
end
# We should skip the repository for a GitHub import or GitLab project import,
......
......@@ -178,6 +178,9 @@ module Projects
def latest_sha
project.commit(build.ref).try(:sha).to_s
ensure
# Close any file descriptors that were opened and free libgit2 buffers
project.cleanup
end
def sha
......
class CertificateFingerprintValidator < ActiveModel::EachValidator
FINGERPRINT_PATTERN = /\A([a-zA-Z0-9]{2}[\s\-:]?){16,}\z/.freeze
def validate_each(record, attribute, value)
unless value.try(:match, FINGERPRINT_PATTERN)
record.errors.add(attribute, "must be a hash containing only letters, numbers, spaces, : and -")
end
end
end
......@@ -4,8 +4,8 @@
# protect against Server-side Request Forgery (SSRF).
class ImportableUrlValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if Gitlab::UrlBlocker.blocked_url?(value, valid_ports: Project::VALID_IMPORT_PORTS)
record.errors.add(attribute, "imports are not allowed from that URL")
end
Gitlab::UrlBlocker.validate!(value, valid_ports: Project::VALID_IMPORT_PORTS)
rescue Gitlab::UrlBlocker::BlockedUrlError => e
record.errors.add(attribute, "is blocked: #{e.message}")
end
end
class TopLevelGroupValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if value&.subgroup?
record.errors.add(attribute, "must be a top level Group")
end
end
end
= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
%p
These settings require a
= link_to 'restart', help_page_path('administration/restart_gitlab')
to take effect.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :sidekiq_throttling_enabled do
= f.check_box :sidekiq_throttling_enabled
Enable Sidekiq Job Throttling
.help-block
Limit the amount of resources slow running jobs are assigned.
.form-group
= f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
.col-sm-10
= f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
.help-block
Choose which queues you wish to throttle.
.form-group
= f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
.help-block
The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
= f.submit 'Save changes', class: "btn btn-success"
......@@ -9,111 +9,6 @@
.col-sm-10
= f.number_field :container_registry_token_expire_delay, class: 'form-control'
%fieldset
%legend Profiling - Performance Bar
%p
Enable the Performance Bar for a given group.
= link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :performance_bar_enabled do
= f.check_box :performance_bar_enabled
Enable the Performance Bar
.form-group
= f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
%fieldset
%legend Background Jobs
%p
These settings require a
= link_to 'restart', help_page_path('administration/restart_gitlab')
to take effect.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :sidekiq_throttling_enabled do
= f.check_box :sidekiq_throttling_enabled
Enable Sidekiq Job Throttling
.help-block
Limit the amount of resources slow running jobs are assigned.
.form-group
= f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
.col-sm-10
= f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
.help-block
Choose which queues you wish to throttle.
.form-group
= f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
.help-block
The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
%fieldset
%legend Spam and Anti-bot Protection
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :recaptcha_enabled do
= f.check_box :recaptcha_enabled
Enable reCAPTCHA
%span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
.form-group
= f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :recaptcha_site_key, class: 'form-control'
.help-block
Generate site and private keys at
%a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
.form-group
= f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :recaptcha_private_key, class: 'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :akismet_enabled do
= f.check_box :akismet_enabled
Enable Akismet
%span.help-block#akismet_help_block Helps prevent bots from creating issues
.form-group
= f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :akismet_api_key, class: 'form-control'
.help-block
Generate API key at
%a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :unique_ips_limit_enabled do
= f.check_box :unique_ips_limit_enabled
Limit sign in from multiple ips
%span.help-block#unique_ip_help_block
Helps prevent malicious users hide their activity
.form-group
= f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :unique_ips_limit_per_user, class: 'form-control'
.help-block
Maximum number of unique IPs per user
.form-group
= f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :unique_ips_limit_time_window, class: 'form-control'
.help-block
How many seconds an IP will be counted towards the limit
%fieldset
%legend Abuse reports
.form-group
......
= 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 :performance_bar_enabled do
= f.check_box :performance_bar_enabled
Enable the Performance Bar
.form-group
= f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
= f.submit 'Save changes', class: "btn btn-success"
= 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 :recaptcha_enabled do
= f.check_box :recaptcha_enabled
Enable reCAPTCHA
%span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
.form-group
= f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :recaptcha_site_key, class: 'form-control'
.help-block
Generate site and private keys at
%a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
.form-group
= f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :recaptcha_private_key, class: 'form-control'
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :akismet_enabled do
= f.check_box :akismet_enabled
Enable Akismet
%span.help-block#akismet_help_block Helps prevent bots from creating issues
.form-group
= f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
.col-sm-10
= f.text_field :akismet_api_key, class: 'form-control'
.help-block
Generate API key at
%a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :unique_ips_limit_enabled do
= f.check_box :unique_ips_limit_enabled
Limit sign in from multiple ips
%span.help-block#unique_ip_help_block
Helps prevent malicious users hide their activity
.form-group
= f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :unique_ips_limit_per_user, class: 'form-control'
.help-block
Maximum number of unique IPs per user
.form-group
= f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :unique_ips_limit_time_window, class: 'form-control'
.help-block
How many seconds an IP will be counted towards the limit
= f.submit 'Save changes', class: "btn btn-success"
......@@ -7,7 +7,7 @@
.settings-header
%h4
= _('Visibility and access controls')
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
......@@ -18,7 +18,7 @@
.settings-header
%h4
= _('Account and limit settings')
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Session expiration, projects limit and attachment size.')
......@@ -29,7 +29,7 @@
.settings-header
%h4
= _('Sign-up restrictions')
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Configure the way a user creates a new account.')
......@@ -40,7 +40,7 @@
.settings-header
%h4
= _('Sign-in restrictions')
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.')
......@@ -51,7 +51,7 @@
.settings-header
%h4
= _('Help page')
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Help page text and support page url.')
......@@ -62,7 +62,7 @@
.settings-header
%h4
= _('Pages')
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Size and domain settings for static websites')
......@@ -73,7 +73,7 @@
.settings-header
%h4
= _('Continuous Integration and Deployment')
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Auto DevOps, runners amd job artifacts')
......@@ -84,7 +84,7 @@
.settings-header
%h4
= _('Metrics - Influx')
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Enable and configure InfluxDB metrics.')
......@@ -95,12 +95,46 @@
.settings-header
%h4
= _('Metrics - Prometheus')
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Enable and configure Prometheus metrics.')
.settings-content
= render 'prometheus'
%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Profiling - Performance bar')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Enable the Performance Bar for a given group.')
= link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
.settings-content
= render 'performance_bar'
%section.settings.as-background.no-animate#js-background-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Background jobs')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Configure Sidekiq job throttling.')
.settings-content
= render 'background_jobs'
%section.settings.as-spam.no-animate#js-spam-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Spam and Anti-bot Protection')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
= _('Enable reCAPTCHA or Akismet and set IP limits.')
.settings-content
= render 'spam'
.prepend-top-20
= render 'form'
- @no_container = true
- breadcrumb_title "Details"
- breadcrumb_title _("Details")
- can_create_subgroups = can?(current_user, :create_subgroup, @group)
= content_for :meta_tags do
......
!!! 5
%html.devise-layout-html
%html.devise-layout-html{ class: system_message_class }
= render "layouts/head"
%body.ui_indigo.login-page.application.navless{ data: { page: body_data_page } }
.page-wrap
......@@ -16,7 +16,7 @@
%h1
= brand_title
= brand_image
- if brand_item&.description?
- if current_appearance&.description?
= brand_text
- else
%h3 Open source software to collaborate on code
......
!!! 5
%html{ lang: "en" }
%html{ lang: "en", class: system_message_class }
= render "layouts/head"
%body.ui_indigo.login-page.application.navless
= render "layouts/header/empty"
......
......@@ -7,7 +7,7 @@
.settings-header
%h4
Export project
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
......
......@@ -31,7 +31,7 @@
%section.settings#js-cluster-details{ class: ('expanded' if expanded) }
.settings-header
%h4= s_('ClusterIntegration|Kubernetes cluster details')
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content
......@@ -43,7 +43,7 @@
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Advanced settings')
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration")
.settings-content
......
......@@ -3,7 +3,7 @@
.settings-header
%h4
Deploy Keys
%button.btn.js-settings-toggle.qa-expand-deploy-keys
%button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
......
......@@ -8,7 +8,7 @@
.settings-header
%h4
General project settings
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Update your project name, description, avatar, and other general settings.
......@@ -64,7 +64,7 @@
.settings-header
%h4
Permissions
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Enable or disable certain project features and choose access levels.
......@@ -79,7 +79,7 @@
.settings-header
%h4
Merge request settings
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Customize your merge request restrictions.
......@@ -94,7 +94,7 @@
.settings-header
%h4
Advanced settings
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
......
- @no_container = true
- breadcrumb_title "Details"
- breadcrumb_title _("Details")
= render partial: 'flash_messages', locals: { project: @project }
......
- breadcrumb_title _("Details")
%h2
%i.fa.fa-warning
#{ _('No repository') }
......@@ -10,7 +12,7 @@
.no-repo-actions
= link_to project_repository_path(@project), method: :post, class: 'btn btn-primary' do
#{ _('Create empty bare repository') }
#{ _('Create empty repository') }
%strong.prepend-left-10.append-right-10 or
......@@ -19,4 +21,4 @@
- if can? current_user, :remove_project, @project
.prepend-top-20
= link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
= link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right"
......@@ -4,7 +4,7 @@
.settings-header
%h4
Protected Branches
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
......
......@@ -4,7 +4,7 @@
.settings-header
%h4
Protected Tags
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Limit access to creating and updating tags.
......
......@@ -8,7 +8,7 @@
.settings-header
%h4
General pipelines settings
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Update your CI/CD configuration, like job timeout or Auto DevOps.
......@@ -19,7 +19,7 @@
.settings-header
%h4
Runners settings
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Register and see your runners for this project.
......@@ -31,7 +31,7 @@
%h4
= _('Secret variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p.append-bottom-0
= render "ci/variables/content"
......@@ -42,7 +42,7 @@
.settings-header
%h4
Pipeline triggers
%button.btn.js-settings-toggle
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
%p
Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
......
- @no_container = true
- breadcrumb_title "Details"
- breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout
- show_auto_devops_callout = show_auto_devops_callout?(@project)
......
......@@ -84,7 +84,7 @@
= dropdown_content do
.js-due-date-calendar
- if @labels && @labels.any?
- if @labels
- selected_labels = issuable.labels
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
......
- page_title milestone.title, "Milestones"
- page_title @milestone.title
- @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- group = local_assigns[:group]
......@@ -17,7 +18,7 @@
Milestone #{milestone.title}
- if milestone.due_date || milestone.start_date
%span.creator
&middot;
&nbsp;&middot;
= milestone_date_range(milestone)
- if group
.pull-right
......
......@@ -138,21 +138,18 @@ module ObjectStorage
include Report
def self.enqueue!(uploads, mounted_as, to_store)
sanity_check!(uploads, mounted_as)
def self.enqueue!(uploads, model_class, mounted_as, to_store)
sanity_check!(uploads, model_class, mounted_as)
perform_async(uploads.ids, mounted_as, to_store)
perform_async(uploads.ids, model_class.to_s, mounted_as, to_store)
end
# We need to be sure all the uploads are for the same uploader and model type
# and that the mount point exists if provided.
#
def self.sanity_check!(uploads, mounted_as)
def self.sanity_check!(uploads, model_class, mounted_as)
upload = uploads.first
uploader_class = upload.uploader.constantize
model_class = uploads.first.model_type.constantize
uploader_types = uploads.map(&:uploader).uniq
model_types = uploads.map(&:model_type).uniq
model_has_mount = mounted_as.nil? || model_class.uploaders[mounted_as] == uploader_class
......@@ -162,7 +159,12 @@ module ObjectStorage
raise(SanityCheckError, "Mount point #{mounted_as} not found in #{model_class}.") unless model_has_mount
end
def perform(ids, mounted_as, to_store)
def perform(*args)
args_check!(args)
(ids, model_type, mounted_as, to_store) = args
@model_class = model_type.constantize
@mounted_as = mounted_as&.to_sym
@to_store = to_store
......@@ -178,7 +180,17 @@ module ObjectStorage
end
def sanity_check!(uploads)
self.class.sanity_check!(uploads, @mounted_as)
self.class.sanity_check!(uploads, @model_class, @mounted_as)
end
def args_check!(args)
return if args.count == 4
case args.count
when 3 then raise SanityCheckError, "Job is missing the `model_type` argument."
else
raise SanityCheckError, "Job has wrong arguments format."
end
end
def build_uploaders(uploads)
......
......@@ -4,11 +4,19 @@ class ProjectExportWorker
sidekiq_options retry: 3
def perform(current_user_id, project_id, params = {})
params = params.with_indifferent_access
def perform(current_user_id, project_id, after_export_strategy = {}, params = {})
current_user = User.find(current_user_id)
project = Project.find(project_id)
after_export = build!(after_export_strategy)
::Projects::ImportExport::ExportService.new(project, current_user, params).execute
::Projects::ImportExport::ExportService.new(project, current_user, params).execute(after_export)
end
private
def build!(after_export_strategy)
strategy_klass = after_export_strategy&.delete('klass')
Gitlab::ImportExport::AfterExportStrategyBuilder.build!(strategy_klass, after_export_strategy)
end
end
---
title: Enable restore rake task to handle nested storage directories
merge_request: 17516
author: Balasankar C
type: fixed
---
title: adds closed by informations in issue api
merge_request: 17042
author: haseebeqx
type: added
---
title: Add additional cluster usage metrics to usage ping.
merge_request: 17922
author:
type: changed
---
title: Fixed gitlab:uploads:migrate task ignoring some uploads.
merge_request: 18082
author:
type: fixed
---
title: Fixed gitlab:uploads:migrate task failing for Groups' avatar.
merge_request: 18088
author:
type: fixed
---
title: Update brakeman 3.6.1 to 4.2.1
merge_request: 18122
author: Takuya Noguchi
type: other
---
title: Always display Labels section in issuable sidebar, even when the project has no labels
merge_request: 18081
author: Branka Martinovic
type: fixed
---
title: Add missing port to artifact links
merge_request:
author:
type: fixed
---
title: Update dashboard milestones breadcrumb link
merge_request: 17933
author: George Tsiolis
type: fixed
---
title: Bump html-pipeline to 2.7.1
merge_request: 18132
author: "@blackst0ne"
type: other
---
title: Add support for pipeline variables expressions in only/except
merge_request: 17316
author:
type: added
---
title: Update no repository placeholder
merge_request: 17964
author: George Tsiolis
type: fixed
---
title: Extend API for exporting a project with direct upload URL
merge_request: 17686
author:
type: added
---
title: Free open file descriptors and libgit2 buffers in UpdatePagesService
merge_request:
author:
type: performance
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddClosedByToIssues < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
def up
add_column :issues, :closed_by_id, :integer
add_concurrent_foreign_key :issues, :users, column: :closed_by_id, on_delete: :nullify
end
def down
remove_foreign_key :issues, column: :closed_by_id
remove_column :issues, :closed_by_id
end
end
......@@ -932,6 +932,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
t.integer "last_edited_by_id"
t.boolean "discussion_locked"
t.datetime_with_timezone "closed_at"
t.integer "closed_by_id"
end
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
......@@ -2095,6 +2096,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
add_foreign_key "issues", "milestones", name: "fk_96b1dd429c", on_delete: :nullify
add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade
add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify
add_foreign_key "issues", "users", column: "closed_by_id", name: "fk_c63cbf6c25", on_delete: :nullify
add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
......
......@@ -24,11 +24,11 @@ Because Rubular doesn't understand `%{issue_ref}`, you can replace this by
**For Omnibus installations**
1. Open `/etc/gitlab/gitlab.rb` with your editor.
1. Change the value of `gitlab_rails['issue_closing_pattern']` to a regular
1. Change the value of `gitlab_rails['gitlab_issue_closing_pattern']` to a regular
expression of your liking:
```ruby
gitlab_rails['issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
gitlab_rails['gitlab_issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
```
1. [Reconfigure] GitLab for the changes to take effect.
......
......@@ -412,9 +412,10 @@ Example response:
Since GitLab 8.1, this is the new commit status API.
### Get the status of a commit
### List the statuses of a commit
Get the statuses of a commit in a project.
List the statuses of a commit in a project.
The pagination parameters `page` and `per_page` can be used to restrict the list of references.
```
GET /projects/:id/repository/commits/:sha/statuses
......
......@@ -100,6 +100,7 @@ Example response:
},
"updated_at" : "2016-01-04T15:31:51.081Z",
"closed_at" : null,
"closed_by" : null,
"id" : 76,
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
"created_at" : "2016-01-04T15:31:51.081Z",
......@@ -122,6 +123,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## List group issues
Get a list of a group's issues.
......@@ -216,6 +219,7 @@ Example response:
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : null,
"closed_by" : null,
"user_notes_count": 1,
"due_date": null,
"web_url": "http://example.com/example/example/issues/1",
......@@ -233,6 +237,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## List project issues
Get a list of a project's issues.
......@@ -326,6 +332,14 @@ Example response:
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : "2016-01-05T15:31:46.176Z",
"closed_by" : {
"state" : "active",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
"name" : "Administrator"
},
"user_notes_count": 1,
"due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/1",
......@@ -343,6 +357,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## Single issue
Get a single project issue.
......@@ -409,6 +425,8 @@ Example response:
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : null,
"closed_by" : null,
"subscribed": false,
"user_notes_count": 1,
"due_date": null,
......@@ -432,6 +450,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## New issue
Creates a new project issue.
......@@ -484,6 +504,7 @@ Example response:
"description" : null,
"updated_at" : "2016-01-07T12:44:33.959Z",
"closed_at" : null,
"closed_by" : null,
"milestone" : null,
"subscribed" : true,
"user_notes_count": 0,
......@@ -508,6 +529,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## Edit issue
Updates an existing project issue. This call is also used to mark an issue as
......@@ -556,6 +579,14 @@ Example response:
"description" : null,
"updated_at" : "2016-01-07T12:55:16.213Z",
"closed_at" : "2016-01-08T12:55:16.213Z",
"closed_by" : {
"state" : "active",
"web_url" : "https://gitlab.example.com/root",
"avatar_url" : null,
"username" : "root",
"id" : 1,
"name" : "Administrator"
},
"iid" : 15,
"labels" : [
"bug"
......@@ -587,6 +618,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## Delete an issue
Only for admins and project owners. Soft deletes the issue in question.
......@@ -640,6 +673,7 @@ Example response:
"created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z",
"closed_at": null,
"closed_by": null,
"labels": [],
"milestone": null,
"assignees": [{
......@@ -687,6 +721,8 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## Subscribe to an issue
Subscribes the authenticated user to an issue to receive notifications.
......@@ -719,6 +755,7 @@ Example response:
"created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z",
"closed_at": null,
"closed_by": null,
"labels": [],
"milestone": null,
"assignees": [{
......@@ -766,6 +803,9 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## Unsubscribe from an issue
Unsubscribes the authenticated user from the issue to not receive notifications
......@@ -807,6 +847,8 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
"web_url": "https://gitlab.example.com/keyon"
},
"closed_at": null,
"closed_by": null,
"author": {
"name": "Vivian Hermann",
"username": "orville",
......@@ -927,6 +969,9 @@ Example response:
**Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
## Set a time estimate for an issue
Sets an estimated time of work for this issue.
......@@ -1112,6 +1157,8 @@ Example response:
"assignee": null,
"source_project_id": 1,
"target_project_id": 1,
"closed_at": null,
"closed_by": null,
"labels": [],
"work_in_progress": false,
"milestone": null,
......@@ -1206,3 +1253,4 @@ Example response:
[ce-13004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13004
[ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016
[ce-17042]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17042
......@@ -294,9 +294,10 @@ Example of response
## Get job artifacts
> [Introduced][ce-2893] in GitLab 8.5
> **Notes**:
- [Introduced][ce-2893] in GitLab 8.5.
Get job artifacts of a project
Get job artifacts of a project.
```
GET /projects/:id/jobs/:job_id/artifacts
......@@ -307,8 +308,10 @@ GET /projects/:id/jobs/:job_id/artifacts
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `job_id` | integer | yes | The ID of a job |
Example requests:
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/artifacts"
curl --location --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/artifacts"
```
Response:
......@@ -322,7 +325,8 @@ Response:
## Download the artifacts archive
> [Introduced][ce-5347] in GitLab 8.10.
> **Notes**:
- [Introduced][ce-5347] in GitLab 8.10.
Download the artifacts archive from the given reference name and job provided the
job finished successfully.
......@@ -339,7 +343,7 @@ Parameters
| `ref_name` | string | yes | The ref from a repository (can only be branch or tag name, not HEAD or SHA) |
| `job` | string | yes | The name of the job |
Example request:
Example requests:
```
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test"
......
......@@ -8,6 +8,14 @@
Start a new export.
The endpoint also accepts an `upload` param. This param is a hash that contains
all the necessary information to upload the exported project to a web server or
to any S3-compatible platform. At the moment we only support binary
data file uploads to the final server.
If the `upload` params is present, `upload[url]` param is required.
(**Note:** This feature was introduced in GitLab 10.7)
```http
POST /projects/:id/export
```
......@@ -16,9 +24,12 @@ POST /projects/:id/export
| --------- | -------------- | -------- | ---------------------------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `description` | string | no | Overrides the project description |
| `upload` | hash | no | Hash that contains the information to upload the exported project to a web server |
| `upload[url]` | string | yes | The URL to upload the project |
| `upload[http_method]` | string | no | The HTTP method to upload the exported project. Only `PUT` and `POST` methods allowed. Default is `PUT` |
```console
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "description=Foo Bar" https://gitlab.example.com/api/v4/projects/1/export
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/export --data "description=FooBar&upload[http_method]=PUT&upload[url]=https://example-bucket.s3.eu-west-3.amazonaws.com/backup?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIMBJHN2O62W8IELQ%2F20180312%2Feu-west-3%2Fs3%2Faws4_request&X-Amz-Date=20180312T110328Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=8413facb20ff33a49a147a0b4abcff4c8487cc33ee1f7e450c46e8f695569dbd"
```
```json
......@@ -43,7 +54,11 @@ GET /projects/:id/export
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/export
```
Status can be one of `none`, `started`, or `finished`.
Status can be one of `none`, `started`, `after_export_action` or `finished`. The
`after_export_action` state represents that the export process has been completed successfully and
the platform is performing some actions on the resulted file. For example, sending
an email notifying the user to download the file, uploading the exported file
to a web server, etc.
`_links` are only present when export has finished.
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment