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: ...@@ -264,8 +264,17 @@ package-and-qa:
stage: build stage: build
cache: {} cache: {}
when: manual 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: script:
- scripts/trigger-build-omnibus - ./trigger-build-omnibus
only: only:
- //@gitlab-org/gitlab-ce - //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee - //@gitlab-org/gitlab-ee
......
...@@ -6,7 +6,6 @@ end ...@@ -6,7 +6,6 @@ end
gem_versions = {} gem_versions = {}
gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0' : '0.2' 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['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'] = rails5? ? '5.0.6' : '4.2.10'
gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9' gem_versions['rails-i18n'] = rails5? ? '~> 5.1' : '~> 4.0.9'
# --- The end of special code for migrating to Rails 5.0 --- # --- The end of special code for migrating to Rails 5.0 ---
...@@ -136,7 +135,7 @@ gem 'unf', '~> 0.1.4' ...@@ -136,7 +135,7 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.7' gem 'seed-fu', '~> 2.3.7'
# Markdown and HTML processing # 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 'deckar01-task_list', '2.0.0'
gem 'gitlab-markup', '~> 1.6.2' gem 'gitlab-markup', '~> 1.6.2'
gem 'redcarpet', '~> 3.4' gem 'redcarpet', '~> 3.4'
...@@ -310,7 +309,7 @@ end ...@@ -310,7 +309,7 @@ end
group :development do group :development do
gem 'foreman', '~> 0.84.0' 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 'letter_opener_web', '~> 1.3.0'
gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
......
...@@ -95,7 +95,7 @@ GEM ...@@ -95,7 +95,7 @@ GEM
autoprefixer-rails (>= 5.2.1) autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4) sass (>= 3.3.4)
bootstrap_form (2.7.0) bootstrap_form (2.7.0)
brakeman (3.6.1) brakeman (4.2.1)
browser (2.2.0) browser (2.2.0)
builder (3.2.3) builder (3.2.3)
bullet (5.5.1) bullet (5.5.1)
...@@ -120,7 +120,7 @@ GEM ...@@ -120,7 +120,7 @@ GEM
activesupport (>= 4.0.0) activesupport (>= 4.0.0)
mime-types (>= 1.16) mime-types (>= 1.16)
cause (0.1) cause (0.1)
charlock_holmes (0.7.5) charlock_holmes (0.7.6)
childprocess (0.7.0) childprocess (0.7.0)
ffi (~> 1.0, >= 1.0.11) ffi (~> 1.0, >= 1.0.11)
chronic (0.10.2) chronic (0.10.2)
...@@ -399,9 +399,9 @@ GEM ...@@ -399,9 +399,9 @@ GEM
hipchat (1.5.2) hipchat (1.5.2)
httparty httparty
mimemagic mimemagic
html-pipeline (1.11.0) html-pipeline (2.7.1)
activesupport (>= 2) activesupport (>= 2)
nokogiri (~> 1.4) nokogiri (>= 1.4)
html2text (0.2.0) html2text (0.2.0)
nokogiri (~> 1.6) nokogiri (~> 1.6)
htmlentities (4.3.4) htmlentities (4.3.4)
...@@ -1012,7 +1012,7 @@ DEPENDENCIES ...@@ -1012,7 +1012,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7.2) binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0) bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0) bootstrap_form (~> 2.7.0)
brakeman (~> 3.6.0) brakeman (~> 4.2)
browser (~> 2.2) browser (~> 2.2)
bullet (~> 5.5.0) bullet (~> 5.5.0)
bundler-audit (~> 0.5.0) bundler-audit (~> 0.5.0)
...@@ -1083,7 +1083,7 @@ DEPENDENCIES ...@@ -1083,7 +1083,7 @@ DEPENDENCIES
hashie-forbidden_attributes hashie-forbidden_attributes
health_check (~> 2.6.0) health_check (~> 2.6.0)
hipchat (~> 1.5.0) hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0) html-pipeline (~> 2.7.1)
html2text html2text
httparty (~> 0.13.3) httparty (~> 0.13.3)
influxdb (~> 0.2) influxdb (~> 0.2)
......
...@@ -10,6 +10,9 @@ const Api = { ...@@ -10,6 +10,9 @@ const Api = {
projectsPath: '/api/:version/projects.json', projectsPath: '/api/:version/projects.json',
projectPath: '/api/:version/projects/:id', projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels', 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', groupLabelsPath: '/groups/:namespace_path/-/labels',
licensePath: '/api/:version/templates/licenses/:key', licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key', gitignorePath: '/api/:version/templates/gitignores/:key',
...@@ -22,25 +25,27 @@ const Api = { ...@@ -22,25 +25,27 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches', createBranchPath: '/api/:version/projects/:id/repository/branches',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath) const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
.replace(':id', groupId); return axios.get(url).then(({ data }) => {
return axios.get(url) callback(data);
.then(({ data }) => {
callback(data);
return data; return data;
}); });
}, },
// Return groups list. Filtered by query // Return groups list. Filtered by query
groups(query, options, callback = $.noop) { groups(query, options, callback = $.noop) {
const url = Api.buildUrl(Api.groupsPath); const url = Api.buildUrl(Api.groupsPath);
return axios.get(url, { return axios
params: Object.assign({ .get(url, {
search: query, params: Object.assign(
per_page: 20, {
}, options), search: query,
}) per_page: 20,
},
options,
),
})
.then(({ data }) => { .then(({ data }) => {
callback(data); callback(data);
...@@ -51,12 +56,13 @@ const Api = { ...@@ -51,12 +56,13 @@ const Api = {
// Return namespaces list. Filtered by query // Return namespaces list. Filtered by query
namespaces(query, callback) { namespaces(query, callback) {
const url = Api.buildUrl(Api.namespacesPath); const url = Api.buildUrl(Api.namespacesPath);
return axios.get(url, { return axios
params: { .get(url, {
search: query, params: {
per_page: 20, search: query,
}, per_page: 20,
}) },
})
.then(({ data }) => callback(data)); .then(({ data }) => callback(data));
}, },
...@@ -73,9 +79,10 @@ const Api = { ...@@ -73,9 +79,10 @@ const Api = {
defaults.membership = true; defaults.membership = true;
} }
return axios.get(url, { return axios
params: Object.assign(defaults, options), .get(url, {
}) params: Object.assign(defaults, options),
})
.then(({ data }) => { .then(({ data }) => {
callback(data); callback(data);
...@@ -85,8 +92,32 @@ const Api = { ...@@ -85,8 +92,32 @@ const Api = {
// Return single project // Return single project
project(projectPath) { project(projectPath) {
const url = Api.buildUrl(Api.projectPath) const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(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); return axios.get(url);
}, },
...@@ -102,30 +133,30 @@ const Api = { ...@@ -102,30 +133,30 @@ const Api = {
url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath); url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
} }
return axios.post(url, { return axios
label: data, .post(url, {
}) label: data,
})
.then(res => callback(res.data)) .then(res => callback(res.data))
.catch(e => callback(e.response.data)); .catch(e => callback(e.response.data));
}, },
// Return group projects list. Filtered by query // Return group projects list. Filtered by query
groupProjects(groupId, query, callback) { groupProjects(groupId, query, callback) {
const url = Api.buildUrl(Api.groupProjectsPath) const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
.replace(':id', groupId); return axios
return axios.get(url, { .get(url, {
params: { params: {
search: query, search: query,
per_page: 20, per_page: 20,
}, },
}) })
.then(({ data }) => callback(data)); .then(({ data }) => callback(data));
}, },
commitMultiple(id, data) { commitMultiple(id, data) {
// see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
const url = Api.buildUrl(Api.commitPath) const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
.replace(':id', encodeURIComponent(id));
return axios.post(url, JSON.stringify(data), { return axios.post(url, JSON.stringify(data), {
headers: { headers: {
'Content-Type': 'application/json; charset=utf-8', 'Content-Type': 'application/json; charset=utf-8',
...@@ -136,39 +167,34 @@ const Api = { ...@@ -136,39 +167,34 @@ const Api = {
branchSingle(id, branch) { branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath) const url = Api.buildUrl(Api.branchSinglePath)
.replace(':id', encodeURIComponent(id)) .replace(':id', encodeURIComponent(id))
.replace(':branch', branch); .replace(':branch', encodeURIComponent(branch));
return axios.get(url); return axios.get(url);
}, },
// Return text for a specific license // Return text for a specific license
licenseText(key, data, callback) { licenseText(key, data, callback) {
const url = Api.buildUrl(Api.licensePath) const url = Api.buildUrl(Api.licensePath).replace(':key', key);
.replace(':key', key); return axios
return axios.get(url, { .get(url, {
params: data, params: data,
}) })
.then(res => callback(res.data)); .then(res => callback(res.data));
}, },
gitignoreText(key, callback) { gitignoreText(key, callback) {
const url = Api.buildUrl(Api.gitignorePath) const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
.replace(':key', key); return axios.get(url).then(({ data }) => callback(data));
return axios.get(url)
.then(({ data }) => callback(data));
}, },
gitlabCiYml(key, callback) { gitlabCiYml(key, callback) {
const url = Api.buildUrl(Api.gitlabCiYmlPath) const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
.replace(':key', key); return axios.get(url).then(({ data }) => callback(data));
return axios.get(url)
.then(({ data }) => callback(data));
}, },
dockerfileYml(key, callback) { dockerfileYml(key, callback) {
const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
return axios.get(url) return axios.get(url).then(({ data }) => callback(data));
.then(({ data }) => callback(data));
}, },
issueTemplate(namespacePath, projectPath, key, type, callback) { issueTemplate(namespacePath, projectPath, key, type, callback) {
...@@ -177,7 +203,8 @@ const Api = { ...@@ -177,7 +203,8 @@ const Api = {
.replace(':type', type) .replace(':type', type)
.replace(':project_path', projectPath) .replace(':project_path', projectPath)
.replace(':namespace_path', namespacePath); .replace(':namespace_path', namespacePath);
return axios.get(url) return axios
.get(url)
.then(({ data }) => callback(null, data)) .then(({ data }) => callback(null, data))
.catch(callback); .catch(callback);
}, },
...@@ -185,10 +212,13 @@ const Api = { ...@@ -185,10 +212,13 @@ const Api = {
users(query, options) { users(query, options) {
const url = Api.buildUrl(this.usersPath); const url = Api.buildUrl(this.usersPath);
return axios.get(url, { return axios.get(url, {
params: Object.assign({ params: Object.assign(
search: query, {
per_page: 20, search: query,
}, options), per_page: 20,
},
options,
),
}); });
}, },
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import { __, sprintf } from '~/locale';
export default { export default {
components: { components: {
Icon, Icon,
},
props: {
hasChanges: {
type: Boolean,
required: false,
default: false,
}, },
props: { mergeRequestId: {
hasChanges: { type: String,
type: Boolean, required: false,
required: false, default: '',
default: false,
},
viewer: {
type: String,
required: true,
},
showShadow: {
type: Boolean,
required: true,
},
}, },
methods: { viewer: {
changeMode(mode) { type: String,
this.$emit('click', mode); 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> </script>
<template> <template>
...@@ -43,7 +56,10 @@ ...@@ -43,7 +56,10 @@
}" }"
data-toggle="dropdown" data-toggle="dropdown"
> >
<template v-if="viewer === 'editor'"> <template v-if="viewer === 'mrdiff' && mergeRequestId">
{{ mergeReviewLine }}
</template>
<template v-else-if="viewer === 'editor'">
{{ __('Editing') }} {{ __('Editing') }}
</template> </template>
<template v-else> <template v-else>
...@@ -57,6 +73,29 @@ ...@@ -57,6 +73,29 @@
</button> </button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-open-left"> <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
<ul> <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> <li>
<a <a
href="#" href="#"
......
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue'; import ideSidebar from './ide_side_bar.vue';
import ideContextbar from './ide_context_bar.vue'; import ideContextbar from './ide_context_bar.vue';
import repoTabs from './repo_tabs.vue'; import repoTabs from './repo_tabs.vue';
import repoFileButtons from './repo_file_buttons.vue'; import repoFileButtons from './repo_file_buttons.vue';
import ideStatusBar from './ide_status_bar.vue'; import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue'; import repoEditor from './repo_editor.vue';
export default { export default {
components: { components: {
ideSidebar, ideSidebar,
ideContextbar, ideContextbar,
repoTabs, repoTabs,
repoFileButtons, repoFileButtons,
ideStatusBar, ideStatusBar,
repoEditor, repoEditor,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
}, },
props: { noChangesStateSvgPath: {
emptyStateSvgPath: { type: String,
type: String, required: true,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
}, },
computed: { committedStateSvgPath: {
...mapState(['changedFiles', 'openFiles', 'viewer']), type: String,
...mapGetters(['activeFile', 'hasChanges']), required: true,
}, },
mounted() { },
const returnValue = 'Are you sure you want to lose unsaved changes?'; computed: {
window.onbeforeunload = e => { ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
if (!this.changedFiles.length) return undefined; ...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, { Object.assign(e, {
returnValue, returnValue,
}); });
return returnValue; return returnValue;
}; };
}, },
}; };
</script> </script>
<template> <template>
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
:files="openFiles" :files="openFiles"
:viewer="viewer" :viewer="viewer"
:has-changes="hasChanges" :has-changes="hasChanges"
:merge-request-id="currentMergeRequestId"
/> />
<repo-editor <repo-editor
class="multi-file-edit-pane-content" 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> <script>
/* global monaco */ /* global monaco */
import { mapState, mapActions } from 'vuex'; import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '~/flash'; import flash from '~/flash';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
...@@ -13,12 +13,8 @@ export default { ...@@ -13,12 +13,8 @@ export default {
}, },
}, },
computed: { computed: {
...mapState([ ...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
'leftPanelCollapsed', ...mapGetters(['currentMergeRequest']),
'rightPanelCollapsed',
'viewer',
'delayViewerUpdated',
]),
shouldHideEditor() { shouldHideEditor() {
return this.file && this.file.binary && !this.file.raw; return this.file && this.file.binary && !this.file.raw;
}, },
...@@ -68,9 +64,14 @@ export default { ...@@ -68,9 +64,14 @@ export default {
this.editor.clearEditor(); this.editor.clearEditor();
this.getRawFileData(this.file) this.getRawFileData({
path: this.file.path,
baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
})
.then(() => { .then(() => {
const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve(); const viewerPromise = this.delayViewerUpdated
? this.updateViewer('editor')
: Promise.resolve();
return viewerPromise; return viewerPromise;
}) })
...@@ -78,7 +79,7 @@ export default { ...@@ -78,7 +79,7 @@ export default {
this.updateDelayViewerUpdated(false); this.updateDelayViewerUpdated(false);
this.createEditorInstance(); this.createEditorInstance();
}) })
.catch((err) => { .catch(err => {
flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true); flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
throw err; throw err;
}); });
...@@ -101,9 +102,13 @@ export default { ...@@ -101,9 +102,13 @@ export default {
this.model = this.editor.createModel(this.file); 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; const { file } = model;
if (file.active) { if (file.active) {
......
...@@ -6,6 +6,7 @@ import router from '../ide_router'; ...@@ -6,6 +6,7 @@ import router from '../ide_router';
import newDropdown from './new_dropdown/index.vue'; import newDropdown from './new_dropdown/index.vue';
import fileStatusIcon from './repo_file_status_icon.vue'; import fileStatusIcon from './repo_file_status_icon.vue';
import changedFileIcon from './changed_file_icon.vue'; import changedFileIcon from './changed_file_icon.vue';
import mrFileIcon from './mr_file_icon.vue';
export default { export default {
name: 'RepoFile', name: 'RepoFile',
...@@ -15,6 +16,7 @@ export default { ...@@ -15,6 +16,7 @@ export default {
fileStatusIcon, fileStatusIcon,
fileIcon, fileIcon,
changedFileIcon, changedFileIcon,
mrFileIcon,
}, },
props: { props: {
file: { file: {
...@@ -56,10 +58,7 @@ export default { ...@@ -56,10 +58,7 @@ export default {
...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']), ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
clickFile() { clickFile() {
// Manual Action if a tree is selected/opened // Manual Action if a tree is selected/opened
if ( if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
this.isTree &&
this.$router.currentRoute.path === `/project${this.file.url}`
) {
this.toggleTreeOpen(this.file.path); this.toggleTreeOpen(this.file.path);
} }
...@@ -102,12 +101,17 @@ export default { ...@@ -102,12 +101,17 @@ export default {
:file="file" :file="file"
/> />
</span> </span>
<changed-file-icon <span class="pull-right">
v-if="file.changed || file.tempFile || file.staged" <mr-file-icon
:file="file" v-if="file.mrChange"
:show-tooltip="true" />
class="prepend-top-5 pull-right" <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 <new-dropdown
v-if="isTree" v-if="isTree"
:project-id="file.projectId" :project-id="file.projectId"
......
<script> <script>
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility'; import '~/lib/utils/datetime_utility';
export default { export default {
components: { components: {
icon, 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> </script>
<template> <template>
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue'; import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue'; import EditorMode from './editor_mode_dropdown.vue';
export default { export default {
components: { components: {
RepoTab, RepoTab,
EditorMode, EditorMode,
},
props: {
files: {
type: Array,
required: true,
}, },
props: { viewer: {
files: { type: String,
type: Array, required: true,
required: true,
},
viewer: {
type: String,
required: true,
},
hasChanges: {
type: Boolean,
required: true,
},
}, },
data() { hasChanges: {
return { type: Boolean,
showShadow: false, required: true,
};
}, },
updated() { mergeRequestId: {
if (!this.$refs.tabsScroller) return; type: String,
required: false,
this.showShadow = default: '',
this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer']),
}, },
}; },
data() {
return {
showShadow: false,
};
},
updated() {
if (!this.$refs.tabsScroller) return;
this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
},
methods: {
...mapActions(['updateViewer']),
},
};
</script> </script>
<template> <template>
...@@ -55,6 +59,7 @@ ...@@ -55,6 +59,7 @@
:viewer="viewer" :viewer="viewer"
:show-shadow="showShadow" :show-shadow="showShadow"
:has-changes="hasChanges" :has-changes="hasChanges"
:merge-request-id="mergeRequestId"
@click="updateViewer" @click="updateViewer"
/> />
</div> </div>
......
...@@ -44,7 +44,7 @@ const router = new VueRouter({ ...@@ -44,7 +44,7 @@ const router = new VueRouter({
component: EmptyRouterComponent, component: EmptyRouterComponent,
}, },
{ {
path: 'mr/:mrid', path: 'merge_requests/:mrid',
component: EmptyRouterComponent, component: EmptyRouterComponent,
}, },
], ],
...@@ -76,9 +76,7 @@ router.beforeEach((to, from, next) => { ...@@ -76,9 +76,7 @@ router.beforeEach((to, from, next) => {
.then(() => { .then(() => {
if (to.params[0]) { if (to.params[0]) {
const path = const path =
to.params[0].slice(-1) === '/' to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
? to.params[0].slice(0, -1)
: to.params[0];
const treeEntry = store.state.entries[path]; const treeEntry = store.state.entries[path];
if (treeEntry) { if (treeEntry) {
store.dispatch('handleTreeEntryAction', treeEntry); store.dispatch('handleTreeEntryAction', treeEntry);
...@@ -96,6 +94,60 @@ router.beforeEach((to, from, next) => { ...@@ -96,6 +94,60 @@ router.beforeEach((to, from, next) => {
); );
throw e; 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 => { .catch(e => {
......
...@@ -21,6 +21,15 @@ export default class Model { ...@@ -21,6 +21,15 @@ export default class Model {
new this.monaco.Uri(null, null, this.file.path), 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(); this.events = new Map();
...@@ -28,10 +37,7 @@ export default class Model { ...@@ -28,10 +37,7 @@ export default class Model {
this.dispose = this.dispose.bind(this); this.dispose = this.dispose.bind(this);
eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose); eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
eventHub.$on( eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
`editor.update.model.content.${this.file.path}`,
this.updateContent,
);
} }
get url() { get url() {
...@@ -58,6 +64,10 @@ export default class Model { ...@@ -58,6 +64,10 @@ export default class Model {
return this.originalModel; return this.originalModel;
} }
getBaseModel() {
return this.baseModel;
}
setValue(value) { setValue(value) {
this.getModel().setValue(value); this.getModel().setValue(value);
} }
...@@ -81,13 +91,7 @@ export default class Model { ...@@ -81,13 +91,7 @@ export default class Model {
this.disposable.dispose(); this.disposable.dispose();
this.events.clear(); this.events.clear();
eventHub.$off( eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
`editor.update.model.dispose.${this.file.path}`, eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
this.dispose,
);
eventHub.$off(
`editor.update.model.content.${this.file.path}`,
this.updateContent,
);
} }
} }
...@@ -109,11 +109,19 @@ export default class Editor { ...@@ -109,11 +109,19 @@ export default class Editor {
if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model); 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() { setupMonacoTheme() {
this.monaco.editor.defineTheme( this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
gitlabTheme.themeName,
gitlabTheme.monacoTheme,
);
this.monaco.editor.setTheme('gitlab'); this.monaco.editor.setTheme('gitlab');
} }
...@@ -161,8 +169,6 @@ export default class Editor { ...@@ -161,8 +169,6 @@ export default class Editor {
onPositionChange(cb) { onPositionChange(cb) {
if (!this.instance.onDidChangeCursorPosition) return; if (!this.instance.onDidChangeCursorPosition) return;
this.disposable.add( this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
);
} }
} }
...@@ -20,12 +20,35 @@ export default { ...@@ -20,12 +20,35 @@ export default {
return Promise.resolve(file.raw); 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()); .then(res => res.text());
}, },
getProjectData(namespace, project) { getProjectData(namespace, project) {
return Api.project(`${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) { getBranchData(projectId, currentBranchId) {
return Api.branchSingle(projectId, currentBranchId); return Api.branchSingle(projectId, currentBranchId);
}, },
......
...@@ -7,8 +7,7 @@ import FilesDecoratorWorker from './workers/files_decorator_worker'; ...@@ -7,8 +7,7 @@ import FilesDecoratorWorker from './workers/files_decorator_worker';
export const redirectToUrl = (_, url) => visitUrl(url); export const redirectToUrl = (_, url) => visitUrl(url);
export const setInitialData = ({ commit }, data) => export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
commit(types.SET_INITIAL_DATA, data);
export const discardAllChanges = ({ state, commit, dispatch }) => { export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach(file => { state.changedFiles.forEach(file => {
...@@ -60,14 +59,11 @@ export const createTempEntry = ( ...@@ -60,14 +59,11 @@ export const createTempEntry = (
) => ) =>
new Promise(resolve => { new Promise(resolve => {
const worker = new FilesDecoratorWorker(); const worker = new FilesDecoratorWorker();
const fullName = const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (state.entries[name]) { if (state.entries[name]) {
flash( flash(
`The name "${name `The name "${name.split('/').pop()}" is already taken in this directory.`,
.split('/')
.pop()}" is already taken in this directory.`,
'alert', 'alert',
document, document,
null, null,
...@@ -144,3 +140,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => { ...@@ -144,3 +140,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
export * from './actions/merge_request';
...@@ -46,53 +46,63 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => { ...@@ -46,53 +46,63 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
commit(types.SET_CURRENT_BRANCH, file.branchId); 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 }); commit(types.TOGGLE_LOADING, { entry: file });
return service return service
.getFileData(file.url) .getFileData(file.url)
.then(res => { .then(res => {
const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']); const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
setPageTitle(pageTitle); setPageTitle(pageTitle);
return res.json(); return res.json();
}) })
.then(data => { .then(data => {
commit(types.SET_FILE_DATA, { data, file }); commit(types.SET_FILE_DATA, { data, file });
commit(types.TOGGLE_FILE_OPEN, file.path); commit(types.TOGGLE_FILE_OPEN, path);
dispatch('setFileActive', file.path); if (makeFileActive) dispatch('setFileActive', path);
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
}) })
.catch(() => { .catch(() => {
commit(types.TOGGLE_LOADING, { entry: file }); commit(types.TOGGLE_LOADING, { entry: file });
flash( flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
'Error loading file data. Please try again.',
'alert',
document,
null,
false,
true,
);
}); });
}; };
export const getRawFileData = ({ commit, dispatch }, file) => export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
service commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
.getRawFileData(file) };
.then(raw => {
commit(types.SET_FILE_RAW_DATA, { file, raw }); export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
}) const file = state.entries[path];
.catch(() => return new Promise((resolve, reject) => {
flash( service
'Error loading file content. Please try again.', .getRawFileData(file)
'alert', .then(raw => {
document, commit(types.SET_FILE_RAW_DATA, { file, raw });
null, if (file.mrChange && file.mrChange.new_file === false) {
false, service
true, .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 }) => { export const changeFileContent = ({ state, commit }, { path, content }) => {
const file = state.entries[path]; const file = state.entries[path];
...@@ -119,10 +129,7 @@ export const setFileEOL = ({ getters, commit }, { eol }) => { ...@@ -119,10 +129,7 @@ export const setFileEOL = ({ getters, commit }, { eol }) => {
} }
}; };
export const setEditorPosition = ( export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
{ getters, commit },
{ editorRow, editorColumn },
) => {
if (getters.activeFile) { if (getters.activeFile) {
commit(types.SET_FILE_POSITION, { commit(types.SET_FILE_POSITION, {
file: getters.activeFile, 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'; ...@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
import flash from '~/flash'; import flash from '~/flash';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import { import { findEntry } from '../utils';
findEntry,
} from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker'; import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => { export const toggleTreeOpen = ({ commit, dispatch }, path) => {
...@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => { ...@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
dispatch('setFileActive', row.path); dispatch('setFileActive', row.path);
} else { } else {
dispatch('getFileData', row); dispatch('getFileData', { path: row.path });
} }
}; };
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return; if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service.getTreeLastCommit(tree.lastCommitPath) service
.then((res) => { .getTreeLastCommit(tree.lastCommitPath)
.then(res => {
const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null; const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath }); commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
return res.json(); return res.json();
}) })
.then((data) => { .then(data => {
data.forEach((lastCommit) => { data.forEach(lastCommit => {
const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name); const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
if (entry) { if (entry) {
...@@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
}; };
export const getFiles = ( export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
{ state, commit, dispatch }, new Promise((resolve, reject) => {
{ projectId, branchId } = {}, if (!state.trees[`${projectId}/${branchId}`]) {
) => new Promise((resolve, reject) => { const selectedProject = state.projects[projectId];
if (!state.trees[`${projectId}/${branchId}`]) { commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
const selectedProject = state.projects[projectId];
commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` }); service
.getFiles(selectedProject.web_url, branchId)
service .then(res => res.json())
.getFiles(selectedProject.web_url, branchId) .then(data => {
.then(res => res.json()) const worker = new FilesDecoratorWorker();
.then((data) => { worker.addEventListener('message', e => {
const worker = new FilesDecoratorWorker(); const { entries, treeList } = e.data;
worker.addEventListener('message', (e) => { const selectedTree = state.trees[`${projectId}/${branchId}`];
const { entries, treeList } = e.data;
const selectedTree = state.trees[`${projectId}/${branchId}`]; commit(types.SET_ENTRIES, entries);
commit(types.SET_DIRECTORY_DATA, {
commit(types.SET_ENTRIES, entries); treePath: `${projectId}/${branchId}`,
commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList }); data: treeList,
commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false }); });
commit(types.TOGGLE_LOADING, {
worker.terminate(); entry: selectedTree,
forceValue: false,
resolve(); });
});
worker.terminate();
worker.postMessage({
data, resolve();
projectId, });
branchId,
worker.postMessage({
data,
projectId,
branchId,
});
})
.catch(e => {
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
reject(e);
}); });
}) } else {
.catch((e) => { resolve();
flash('Error loading tree data. Please try again.', 'alert', document, null, false, true); }
reject(e); });
});
} else {
resolve();
}
});
export const activeFile = state => export const activeFile = state => state.openFiles.find(file => file.active) || null;
state.openFiles.find(file => file.active) || null;
export const addedFiles = state => state.changedFiles.filter(f => f.tempFile); export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
export const modifiedFiles = state => export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
state.changedFiles.filter(f => !f.tempFile);
export const projectsWithTrees = state => export const projectsWithTrees = state =>
Object.keys(state.projects).map(projectId => { Object.keys(state.projects).map(projectId => {
...@@ -23,13 +21,21 @@ export const projectsWithTrees = state => ...@@ -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 // eslint-disable-next-line no-confusing-arrow
export const collapseButtonIcon = state => export const collapseButtonIcon = state =>
state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right'; state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
export const hasChanges = state => export const hasChanges = state => !!state.changedFiles.length || !!state.stagedFiles.length;
!!state.changedFiles.length || !!state.stagedFiles.length;
// eslint-disable-next-line no-confusing-arrow // eslint-disable-next-line no-confusing-arrow
export const collapseButtonTooltip = state => export const collapseButtonTooltip = state =>
state.rightPanelCollapsed ? 'Expand sidebar' : 'Collapse sidebar'; state.rightPanelCollapsed ? 'Expand sidebar' : 'Collapse sidebar';
export const hasMergeRequest = state => !!state.currentMergeRequestId;
...@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT'; ...@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT'; export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN'; 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 // Branch Mutation Types
export const SET_BRANCH = 'SET_BRANCH'; export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
...@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA'; ...@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN'; export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE'; export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA'; 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 UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE'; export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
export const SET_FILE_POSITION = 'SET_FILE_POSITION'; export const SET_FILE_POSITION = 'SET_FILE_POSITION';
...@@ -39,6 +46,7 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED'; ...@@ -39,6 +46,7 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH'; export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
export const SET_ENTRIES = 'SET_ENTRIES'; export const SET_ENTRIES = 'SET_ENTRIES';
export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY'; 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_VIEWER = 'UPDATE_VIEWER';
export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE'; export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
......
import * as types from './mutation_types'; import * as types from './mutation_types';
import projectMutations from './mutations/project'; import projectMutations from './mutations/project';
import mergeRequestMutation from './mutations/merge_request';
import fileMutations from './mutations/file'; import fileMutations from './mutations/file';
import treeMutations from './mutations/tree'; import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch'; import branchMutations from './mutations/branch';
...@@ -11,10 +12,7 @@ export default { ...@@ -11,10 +12,7 @@ export default {
[types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) { [types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
if (entry.path) { if (entry.path) {
Object.assign(state.entries[entry.path], { Object.assign(state.entries[entry.path], {
loading: loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
forceValue !== undefined
? forceValue
: !state.entries[entry.path].loading,
}); });
} else { } else {
Object.assign(entry, { Object.assign(entry, {
...@@ -88,9 +86,7 @@ export default { ...@@ -88,9 +86,7 @@ export default {
if (!foundEntry) { if (!foundEntry) {
Object.assign(state.trees[`${projectId}/${branchId}`], { Object.assign(state.trees[`${projectId}/${branchId}`], {
tree: state.trees[`${projectId}/${branchId}`].tree.concat( tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
data.treeList,
),
}); });
} }
}, },
...@@ -120,6 +116,7 @@ export default { ...@@ -120,6 +116,7 @@ export default {
}); });
}, },
...projectMutations, ...projectMutations,
...mergeRequestMutation,
...fileMutations, ...fileMutations,
...treeMutations, ...treeMutations,
...branchMutations, ...branchMutations,
......
...@@ -28,6 +28,8 @@ export default { ...@@ -28,6 +28,8 @@ export default {
rawPath: data.raw_path, rawPath: data.raw_path,
binary: data.binary, binary: data.binary,
renderError: data.render_error, renderError: data.render_error,
raw: null,
baseRaw: null,
}); });
}, },
[types.SET_FILE_RAW_DATA](state, { file, raw }) { [types.SET_FILE_RAW_DATA](state, { file, raw }) {
...@@ -35,6 +37,11 @@ export default { ...@@ -35,6 +37,11 @@ export default {
raw, raw,
}); });
}, },
[types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
Object.assign(state.entries[file.path], {
baseRaw,
});
},
[types.UPDATE_FILE_CONTENT](state, { path, content }) { [types.UPDATE_FILE_CONTENT](state, { path, content }) {
const changed = content !== state.entries[path].raw; const changed = content !== state.entries[path].raw;
...@@ -59,6 +66,11 @@ export default { ...@@ -59,6 +66,11 @@ export default {
editorColumn, editorColumn,
}); });
}, },
[types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
Object.assign(state.entries[file.path], {
mrChange,
});
},
[types.DISCARD_FILE_CHANGES](state, path) { [types.DISCARD_FILE_CHANGES](state, path) {
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
content: state.entries[path].raw, 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 { ...@@ -11,6 +11,7 @@ export default {
Object.assign(project, { Object.assign(project, {
tree: [], tree: [],
branches: {}, branches: {},
mergeRequests: {},
active: true, active: true,
}); });
......
export default () => ({ export default () => ({
currentProjectId: '', currentProjectId: '',
currentBranchId: '', currentBranchId: '',
currentMergeRequestId: '',
changedFiles: [], changedFiles: [],
stagedFiles: [], stagedFiles: [],
endpoints: {}, endpoints: {},
......
...@@ -51,7 +51,7 @@ export function removeParams(params) { ...@@ -51,7 +51,7 @@ export function removeParams(params) {
const url = document.createElement('a'); const url = document.createElement('a');
url.href = window.location.href; url.href = window.location.href;
params.forEach((param) => { params.forEach(param => {
url.search = removeParamQueryString(url.search, param); url.search = removeParamQueryString(url.search, param);
}); });
...@@ -83,3 +83,11 @@ export function refreshCurrentPage() { ...@@ -83,3 +83,11 @@ export function refreshCurrentPage() {
export function redirectTo(url) { export function redirectTo(url) {
return window.location.assign(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> <script>
import Flash from '../../../flash'; import Flash from '../../../flash';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
import { __ } from '../../../locale'; import { __ } from '../../../locale';
import eventHub from '../../event_hub';
export default { export default {
components: { components: {
editForm, editForm,
Icon, Icon,
},
props: {
isConfidential: {
required: true,
type: Boolean,
}, },
props: { isEditable: {
isConfidential: { required: true,
required: true, type: Boolean,
type: Boolean,
},
isEditable: {
required: true,
type: Boolean,
},
service: {
required: true,
type: Object,
},
}, },
data() { service: {
return { required: true,
edit: false, type: Object,
};
}, },
computed: { },
confidentialityIcon() { data() {
return this.isConfidential ? 'eye-slash' : 'eye'; return {
}, edit: false,
};
},
computed: {
confidentialityIcon() {
return this.isConfidential ? 'eye-slash' : 'eye';
}, },
methods: { },
toggleForm() { created() {
this.edit = !this.edit; eventHub.$on('closeConfidentialityForm', this.toggleForm);
}, },
updateConfidentialAttribute(confidential) { beforeDestroy() {
this.service.update('issue', { confidential }) eventHub.$off('closeConfidentialityForm', this.toggleForm);
.then(() => location.reload()) },
.catch(() => { methods: {
Flash(__('Something went wrong trying to change the confidentiality of this issue')); 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> </script>
<template> <template>
<div class="block issuable-sidebar-item confidentiality"> <div class="block issuable-sidebar-item confidentiality">
<div class="sidebar-collapsed-icon"> <div
class="sidebar-collapsed-icon"
@click="toggleForm"
>
<icon <icon
:name="confidentialityIcon" :name="confidentialityIcon"
:size="16"
aria-hidden="true" aria-hidden="true"
/> />
</div> </div>
...@@ -71,7 +85,6 @@ ...@@ -71,7 +85,6 @@
<div class="value sidebar-item-value hide-collapsed"> <div class="value sidebar-item-value hide-collapsed">
<editForm <editForm
v-if="edit" v-if="edit"
:toggle-form="toggleForm"
:is-confidential="isConfidential" :is-confidential="isConfidential"
:update-confidential-attribute="updateConfidentialAttribute" :update-confidential-attribute="updateConfidentialAttribute"
/> />
......
<script> <script>
import editFormButtons from './edit_form_buttons.vue'; import editFormButtons from './edit_form_buttons.vue';
import { s__ } from '../../../locale'; import { s__ } from '../../../locale';
export default { export default {
components: { components: {
editFormButtons, editFormButtons,
},
props: {
isConfidential: {
required: true,
type: Boolean,
}, },
props: { updateConfidentialAttribute: {
isConfidential: { required: true,
required: true, type: Function,
type: Boolean,
},
toggleForm: {
required: true,
type: Function,
},
updateConfidentialAttribute: {
required: true,
type: Function,
},
}, },
computed: { },
confidentialityOnWarning() { computed: {
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.'); confidentialityOnWarning() {
}, return s__(
confidentialityOffWarning() { '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.',
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.'); );
},
}, },
}; 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> </script>
<template> <template>
...@@ -45,7 +45,6 @@ ...@@ -45,7 +45,6 @@
</p> </p>
<edit-form-buttons <edit-form-buttons
:is-confidential="isConfidential" :is-confidential="isConfidential"
:toggle-form="toggleForm"
:update-confidential-attribute="updateConfidentialAttribute" :update-confidential-attribute="updateConfidentialAttribute"
/> />
</div> </div>
......
<script> <script>
import $ from 'jquery';
import eventHub from '../../event_hub';
export default { export default {
props: { props: {
isConfidential: { isConfidential: {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
toggleForm: {
required: true,
type: Function,
},
updateConfidentialAttribute: { updateConfidentialAttribute: {
required: true, required: true,
type: Function, type: Function,
...@@ -22,6 +21,16 @@ export default { ...@@ -22,6 +21,16 @@ export default {
return !this.isConfidential; return !this.isConfidential;
}, },
}, },
methods: {
closeForm() {
eventHub.$emit('closeConfidentialityForm');
$(this.$el).trigger('hidden.gl.dropdown');
},
submitForm() {
this.closeForm();
this.updateConfidentialAttribute(this.updateConfidentialBool);
},
},
}; };
</script> </script>
...@@ -30,14 +39,14 @@ export default { ...@@ -30,14 +39,14 @@ export default {
<button <button
type="button" type="button"
class="btn btn-default append-right-10" class="btn btn-default append-right-10"
@click="toggleForm" @click="closeForm"
> >
{{ __('Cancel') }} {{ __('Cancel') }}
</button> </button>
<button <button
type="button" type="button"
class="btn btn-close" class="btn btn-close"
@click.prevent="updateConfidentialAttribute(updateConfidentialBool)" @click.prevent="submitForm"
> >
{{ toggleButtonText }} {{ toggleButtonText }}
</button> </button>
......
<script> <script>
import editFormButtons from './edit_form_buttons.vue'; import editFormButtons from './edit_form_buttons.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable'; import issuableMixin from '../../../vue_shared/mixins/issuable';
import { __, sprintf } from '../../../locale'; import { __, sprintf } from '../../../locale';
export default { export default {
components: { components: {
editFormButtons, editFormButtons,
},
mixins: [issuableMixin],
props: {
isLocked: {
required: true,
type: Boolean,
}, },
mixins: [
issuableMixin,
],
props: {
isLocked: {
required: true,
type: Boolean,
},
toggleForm: { updateLockedAttribute: {
required: true, required: true,
type: Function, type: Function,
}, },
},
updateLockedAttribute: { computed: {
required: true, lockWarning() {
type: Function, return sprintf(
}, __(
'Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.',
),
{ issuableDisplayName: this.issuableDisplayName },
);
}, },
computed: { unlockWarning() {
lockWarning() { return sprintf(
return sprintf(__('Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName }); __(
}, 'Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.',
unlockWarning() { ),
return sprintf(__('Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName }); { issuableDisplayName: this.issuableDisplayName },
}, );
}, },
}; },
};
</script> </script>
<template> <template>
...@@ -54,7 +57,6 @@ ...@@ -54,7 +57,6 @@
<edit-form-buttons <edit-form-buttons
:is-locked="isLocked" :is-locked="isLocked"
:toggle-form="toggleForm"
:update-locked-attribute="updateLockedAttribute" :update-locked-attribute="updateLockedAttribute"
/> />
</div> </div>
......
<script> <script>
import $ from 'jquery';
import eventHub from '../../event_hub';
export default { export default {
props: { props: {
isLocked: { isLocked: {
...@@ -6,11 +9,6 @@ export default { ...@@ -6,11 +9,6 @@ export default {
type: Boolean, type: Boolean,
}, },
toggleForm: {
required: true,
type: Function,
},
updateLockedAttribute: { updateLockedAttribute: {
required: true, required: true,
type: Function, type: Function,
...@@ -26,6 +24,17 @@ export default { ...@@ -26,6 +24,17 @@ export default {
return !this.isLocked; return !this.isLocked;
}, },
}, },
methods: {
closeForm() {
eventHub.$emit('closeLockForm');
$(this.$el).trigger('hidden.gl.dropdown');
},
submitForm() {
this.closeForm();
this.updateLockedAttribute(this.toggleLock);
},
},
}; };
</script> </script>
...@@ -34,7 +43,7 @@ export default { ...@@ -34,7 +43,7 @@ export default {
<button <button
type="button" type="button"
class="btn btn-default append-right-10" class="btn btn-default append-right-10"
@click="toggleForm" @click="closeForm"
> >
{{ __('Cancel') }} {{ __('Cancel') }}
</button> </button>
...@@ -42,7 +51,7 @@ export default { ...@@ -42,7 +51,7 @@ export default {
<button <button
type="button" type="button"
class="btn btn-close" class="btn btn-close"
@click.prevent="updateLockedAttribute(toggleLock)" @click.prevent="submitForm"
> >
{{ buttonText }} {{ buttonText }}
</button> </button>
......
<script> <script>
import Flash from '~/flash'; import Flash from '~/flash';
import editForm from './edit_form.vue'; import editForm from './edit_form.vue';
import issuableMixin from '../../../vue_shared/mixins/issuable'; import issuableMixin from '../../../vue_shared/mixins/issuable';
import Icon from '../../../vue_shared/components/icon.vue'; import Icon from '../../../vue_shared/components/icon.vue';
import eventHub from '../../event_hub';
export default { export default {
components: { components: {
editForm, editForm,
Icon, Icon,
}, },
mixins: [ mixins: [issuableMixin],
issuableMixin,
],
props: { props: {
isLocked: { isLocked: {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
isEditable: { isEditable: {
required: true, required: true,
type: Boolean, type: Boolean,
}, },
mediator: { mediator: {
required: true, required: true,
type: Object, type: Object,
validator(mediatorObject) { validator(mediatorObject) {
return mediatorObject.service && mediatorObject.service.update && mediatorObject.store; return (
}, mediatorObject.service &&
mediatorObject.service.update &&
mediatorObject.store
);
}, },
}, },
},
computed: { computed: {
lockIcon() { lockIcon() {
return this.isLocked ? 'lock' : 'lock-open'; return this.isLocked ? 'lock' : 'lock-open';
}, },
isLockDialogOpen() { isLockDialogOpen() {
return this.mediator.store.isLockDialogOpen; return this.mediator.store.isLockDialogOpen;
},
}, },
},
methods: { created() {
toggleForm() { eventHub.$on('closeLockForm', this.toggleForm);
this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen; },
},
beforeDestroy() {
eventHub.$off('closeLockForm', this.toggleForm);
},
updateLockedAttribute(locked) { methods: {
this.mediator.service.update(this.issuableType, { toggleForm() {
this.mediator.store.isLockDialogOpen = !this.mediator.store
.isLockDialogOpen;
},
updateLockedAttribute(locked) {
this.mediator.service
.update(this.issuableType, {
discussion_locked: locked, discussion_locked: locked,
}) })
.then(() => location.reload()) .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> </script>
<template> <template>
<div class="block issuable-sidebar-item lock"> <div class="block issuable-sidebar-item lock">
<div class="sidebar-collapsed-icon"> <div
class="sidebar-collapsed-icon"
@click="toggleForm"
>
<icon <icon
:name="lockIcon" :name="lockIcon"
:size="16"
aria-hidden="true" aria-hidden="true"
class="sidebar-item-icon is-active" class="sidebar-item-icon is-active"
/> />
...@@ -85,7 +108,6 @@ ...@@ -85,7 +108,6 @@
<div class="value sidebar-item-value hide-collapsed"> <div class="value sidebar-item-value hide-collapsed">
<edit-form <edit-form
v-if="isLockDialogOpen" v-if="isLockDialogOpen"
:toggle-form="toggleForm"
:is-locked="isLocked" :is-locked="isLocked"
:update-locked-attribute="updateLockedAttribute" :update-locked-attribute="updateLockedAttribute"
:issuable-type="issuableType" :issuable-type="issuableType"
......
<script> <script>
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import icon from '~/vue_shared/components/icon.vue'; import { webIDEUrl } from '~/lib/utils/url_utility';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; import icon from '~/vue_shared/components/icon.vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default { export default {
name: 'MRWidgetHeader', name: 'MRWidgetHeader',
directives: { directives: {
tooltip, tooltip,
},
components: {
icon,
clipboardButton,
},
props: {
mr: {
type: Object,
required: true,
}, },
components: { },
icon, computed: {
clipboardButton, shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
}, },
props: { commitsText() {
mr: { return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
type: Object,
required: true,
},
}, },
computed: { branchNameClipboardData() {
shouldShowCommitsBehindText() { // This supports code in app/assets/javascripts/copy_to_clipboard.js that
return this.mr.divergedCommitsCount > 0; // works around ClipboardJS limitations to allow the context-specific
}, // copy/pasting of plain text or GFM.
commitsText() { return JSON.stringify({
return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount); text: this.mr.sourceBranch,
}, gfm: `\`${this.mr.sourceBranch}\``,
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);
},
}, },
methods: { isSourceBranchLong() {
isBranchTitleLong(branchTitle) { return this.isBranchTitleLong(this.mr.sourceBranch);
return branchTitle.length > 32;
},
}, },
}; isTargetBranchLong() {
return this.isBranchTitleLong(this.mr.targetBranch);
},
webIdePath() {
return webIDEUrl(this.mr.statusPath.replace('.json', ''));
},
},
methods: {
isBranchTitleLong(branchTitle) {
return branchTitle.length > 32;
},
},
};
</script> </script>
<template> <template>
<div class="mr-source-target"> <div class="mr-source-target">
...@@ -96,6 +100,13 @@ ...@@ -96,6 +100,13 @@
</div> </div>
<div v-if="mr.isOpen"> <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 <button
data-target="#modal_merge_info" data-target="#modal_merge_info"
data-toggle="modal" data-toggle="modal"
......
...@@ -88,7 +88,6 @@ ...@@ -88,7 +88,6 @@
.right-sidebar { .right-sidebar {
border-left: 1px solid $border-color; border-left: 1px solid $border-color;
height: calc(100% - #{$header-height});
} }
.with-performance-bar .right-sidebar.affix { .with-performance-bar .right-sidebar.affix {
......
...@@ -522,10 +522,6 @@ ...@@ -522,10 +522,6 @@
.with-performance-bar .right-sidebar { .with-performance-bar .right-sidebar {
top: $header-height + $performance-bar-height; top: $header-height + $performance-bar-height;
.issuable-sidebar {
height: calc(100% - #{$performance-bar-height});
}
} }
.sidebar-move-issue-confirmation-button { .sidebar-move-issue-confirmation-button {
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
.ide-view { .ide-view {
display: flex; display: flex;
height: calc(100vh - #{$header-height}); height: calc(100vh - #{$header-height});
margin-top: 40px; margin-top: 0;
border-top: 1px solid $white-dark; border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
...@@ -53,6 +53,7 @@ ...@@ -53,6 +53,7 @@
flex: 1; flex: 1;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: inherit;
svg { svg {
vertical-align: middle; vertical-align: middle;
...@@ -460,6 +461,8 @@ ...@@ -460,6 +461,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
max-height: 100%;
overflow: auto;
} }
.ide-commity-empty-state { .ide-commity-empty-state {
...@@ -474,7 +477,7 @@ ...@@ -474,7 +477,7 @@
.multi-file-commit-panel-header { .multi-file-commit-panel-header {
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 12px; margin-bottom: 0;
border-bottom: 1px solid $white-dark; border-bottom: 1px solid $white-dark;
padding: $gl-btn-padding 0; padding: $gl-btn-padding 0;
...@@ -689,8 +692,14 @@ ...@@ -689,8 +692,14 @@
overflow: hidden; overflow: hidden;
&.nav-only { &.nav-only {
padding-top: $header-height;
.with-performance-bar & {
padding-top: $header-height + $performance-bar-height;
}
.flash-container { .flash-container {
margin-top: $header-height; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
} }
...@@ -700,7 +709,7 @@ ...@@ -700,7 +709,7 @@
} }
.content-wrapper { .content-wrapper {
margin-top: $header-height; margin-top: 0;
padding-bottom: 0; padding-bottom: 0;
} }
...@@ -724,11 +733,11 @@ ...@@ -724,11 +733,11 @@
.with-performance-bar .ide.nav-only { .with-performance-bar .ide.nav-only {
.flash-container { .flash-container {
margin-top: #{$header-height + $performance-bar-height}; margin-top: 0;
} }
.content-wrapper { .content-wrapper {
margin-top: #{$header-height + $performance-bar-height}; margin-top: 0;
padding-bottom: 0; padding-bottom: 0;
} }
...@@ -737,10 +746,6 @@ ...@@ -737,10 +746,6 @@
} }
&.flash-shown { &.flash-shown {
.content-wrapper {
margin-top: 0;
}
.ide-view { .ide-view {
height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height}); height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
} }
......
This diff is collapsed.
...@@ -50,9 +50,19 @@ class Admin::AppearancesController < Admin::ApplicationController ...@@ -50,9 +50,19 @@ class Admin::AppearancesController < Admin::ApplicationController
# Only allow a trusted parameter "white list" through. # Only allow a trusted parameter "white list" through.
def appearance_params def appearance_params
params.require(:appearance).permit( params.require(:appearance).permit(allowed_appearance_params)
:title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache, end
:new_project_guidelines, :updated_by
) def allowed_appearance_params
%i[
title
description
logo
logo_cache
header_logo
header_logo_cache
new_project_guidelines
updated_by
]
end end
end end
...@@ -46,6 +46,8 @@ class Projects::ServicesController < Projects::ApplicationController ...@@ -46,6 +46,8 @@ class Projects::ServicesController < Projects::ApplicationController
else else
{ error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') } { error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') }
end end
rescue Gitlab::HTTP::BlockedUrlError => e
{ error: true, message: 'Test failed.', service_response: e.message }
end end
def success_message def success_message
......
module AppearancesHelper module AppearancesHelper
def brand_title def brand_title
brand_item&.title.presence || 'GitLab Community Edition' current_appearance&.title.presence || 'GitLab Community Edition'
end end
def brand_image def brand_image
image_tag(brand_item.logo) if brand_item&.logo? image_tag(current_appearance.logo) if current_appearance&.logo?
end end
def brand_text def brand_text
markdown_field(brand_item, :description) markdown_field(current_appearance, :description)
end end
def brand_new_project_guidelines def brand_new_project_guidelines
markdown_field(brand_item, :new_project_guidelines) markdown_field(current_appearance, :new_project_guidelines)
end end
def brand_item def current_appearance
@appearance ||= Appearance.current @appearance ||= Appearance.current
end end
def brand_header_logo def brand_header_logo
if brand_item&.header_logo? if current_appearance&.header_logo?
image_tag brand_item.header_logo image_tag current_appearance.header_logo
else else
render 'shared/logo.svg' render 'shared/logo.svg'
end end
...@@ -29,7 +29,7 @@ module AppearancesHelper ...@@ -29,7 +29,7 @@ module AppearancesHelper
# Skip the 'GitLab' type logo when custom brand logo is set # Skip the 'GitLab' type logo when custom brand logo is set
def brand_header_logo_type def brand_header_logo_type
unless brand_item&.header_logo? unless current_appearance&.header_logo?
render 'shared/logo_type.svg' render 'shared/logo_type.svg'
end end
end end
......
...@@ -285,6 +285,10 @@ module ApplicationHelper ...@@ -285,6 +285,10 @@ module ApplicationHelper
class_names class_names
end end
# EE feature: System header and footer, unavailable in CE
def system_message_class
end
# Returns active css class when condition returns true # Returns active css class when condition returns true
# otherwise returns nil. # otherwise returns nil.
# #
......
...@@ -54,9 +54,9 @@ module EmailsHelper ...@@ -54,9 +54,9 @@ module EmailsHelper
end end
def header_logo def header_logo
if brand_item && brand_item.header_logo? if current_appearance&.header_logo?
image_tag( image_tag(
brand_item.header_logo, current_appearance.header_logo,
style: 'height: 50px' style: 'height: 50px'
) )
else else
......
...@@ -36,16 +36,15 @@ module Ci ...@@ -36,16 +36,15 @@ module Ci
def external_url(project, job) def external_url(project, job)
return unless external_link?(job) return unless external_link?(job)
full_path_parts = project.full_path_components url_project_path = project.full_path.partition('/').last
top_level_group = full_path_parts.shift
artifact_path = [ artifact_path = [
'-', *full_path_parts, '-', '-', url_project_path, '-',
'jobs', job.id, 'jobs', job.id,
'artifacts', path 'artifacts', path
].join('/') ].join('/')
"#{pages_config.protocol}://#{top_level_group}.#{pages_config.host}/#{artifact_path}" "#{project.pages_group_url}/#{artifact_path}"
end end
def external_link?(job) def external_link?(job)
......
...@@ -6,6 +6,7 @@ module Ci ...@@ -6,6 +6,7 @@ module Ci
include ObjectStorage::BackgroundMove include ObjectStorage::BackgroundMove
include Presentable include Presentable
include Importable include Importable
include Gitlab::Utils::StrongMemoize
MissingDependenciesError = Class.new(StandardError) MissingDependenciesError = Class.new(StandardError)
...@@ -25,15 +26,17 @@ module Ci ...@@ -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 :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' has_one :metadata, class_name: 'Ci::BuildMetadata'
delegate :timeout, to: :metadata, prefix: true, allow_nil: true 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 def persisted_environment
@persisted_environment ||= Environment.find_by( return unless has_environment?
name: expanded_environment_name,
project: project strong_memoize(:persisted_environment) do
) Environment.find_by(name: expanded_environment_name, project: project)
end
end end
serialize :options # rubocop:disable Cop/ActiveRecordSerialize serialize :options # rubocop:disable Cop/ActiveRecordSerialize
...@@ -212,7 +215,11 @@ module Ci ...@@ -212,7 +215,11 @@ module Ci
end end
def expanded_environment_name 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 end
def has_environment? def has_environment?
...@@ -258,31 +265,52 @@ module Ci ...@@ -258,31 +265,52 @@ module Ci
Gitlab::Utils.slugify(ref.to_s) Gitlab::Utils.slugify(ref.to_s)
end end
# Variables whose value does not depend on environment ##
def simple_variables # Variables in the environment name scope.
variables(environment: nil) #
end def scoped_variables(environment: expanded_environment_name)
Gitlab::Ci::Variables::Collection.new.tap do |variables|
# 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.concat(predefined_variables) variables.concat(predefined_variables)
variables.concat(project.predefined_variables) variables.concat(project.predefined_variables)
variables.concat(pipeline.predefined_variables) variables.concat(pipeline.predefined_variables)
variables.concat(runner.predefined_variables) if runner 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(yaml_variables)
variables.concat(user_variables) variables.concat(user_variables)
variables.concat(project.group.secret_variables_for(ref, project)) if project.group variables.concat(secret_group_variables)
variables.concat(secret_variables(environment: environment)) variables.concat(secret_project_variables(environment: environment))
variables.concat(trigger_request.user_variables) if trigger_request variables.concat(trigger_request.user_variables) if trigger_request
variables.concat(pipeline.variables) variables.concat(pipeline.variables)
variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
variables.concat(persisted_environment_variables) if environment
end 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 end
def features def features
...@@ -459,9 +487,14 @@ module Ci ...@@ -459,9 +487,14 @@ module Ci
end end
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) project.secret_variables_for(ref: ref, environment: environment)
.map(&:to_runner_variable)
end end
def steps def steps
...@@ -558,6 +591,21 @@ module Ci ...@@ -558,6 +591,21 @@ module Ci
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze 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 def predefined_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables| Gitlab::Ci::Variables::Collection.new.tap do |variables|
variables.append(key: 'CI', value: 'true') variables.append(key: 'CI', value: 'true')
...@@ -566,16 +614,11 @@ module Ci ...@@ -566,16 +614,11 @@ module Ci
variables.append(key: 'CI_SERVER_NAME', value: 'GitLab') variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION) variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION) 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_NAME', value: name)
variables.append(key: 'CI_JOB_STAGE', value: stage) 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_SHA', value: sha)
variables.append(key: 'CI_COMMIT_REF_NAME', value: ref) variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug) 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_COMMIT_TAG", value: ref) if tag?
variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
variables.append(key: "CI_JOB_MANUAL", value: 'true') if action? variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
...@@ -583,23 +626,8 @@ module Ci ...@@ -583,23 +626,8 @@ module Ci
end end
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 def legacy_variables
Gitlab::Ci::Variables::Collection.new.tap do |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_REF', value: sha)
variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha) variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
variables.append(key: 'CI_BUILD_REF_NAME', value: ref) variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
...@@ -612,6 +640,19 @@ module Ci ...@@ -612,6 +640,19 @@ module Ci
end end
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 def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url options&.dig(:environment, :url) || persisted_environment&.external_url
end end
......
...@@ -51,6 +51,10 @@ module Clusters ...@@ -51,6 +51,10 @@ module Clusters
scope :enabled, -> { where(enabled: true) } scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) } 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) } scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
def status_name def status_name
......
...@@ -4,6 +4,8 @@ module Clusters ...@@ -4,6 +4,8 @@ module Clusters
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
scope :installed, -> { where(status: self.state_machines[:status].states[:installed].value) }
state_machine :status, initial: :not_installable do state_machine :status, initial: :not_installable do
state :not_installable, value: -2 state :not_installable, value: -2
state :errored, value: -1 state :errored, value: -1
......
...@@ -23,6 +23,7 @@ class Issue < ActiveRecord::Base ...@@ -23,6 +23,7 @@ class Issue < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :moved_to, class_name: 'Issue' 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) } has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
...@@ -78,6 +79,11 @@ class Issue < ActiveRecord::Base ...@@ -78,6 +79,11 @@ class Issue < ActiveRecord::Base
before_transition any => :closed do |issue| before_transition any => :closed do |issue|
issue.closed_at = Time.zone.now issue.closed_at = Time.zone.now
end end
before_transition closed: :opened do |issue|
issue.closed_at = nil
issue.closed_by = nil
end
end end
class << self class << self
......
...@@ -1346,20 +1346,19 @@ class Project < ActiveRecord::Base ...@@ -1346,20 +1346,19 @@ class Project < ActiveRecord::Base
Dir.exist?(public_pages_path) Dir.exist?(public_pages_path)
end end
def pages_url def pages_group_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
# The host in URL always needs to be downcased # The host in URL always needs to be downcased
url = Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix| Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
"#{prefix}#{subdomain}." "#{prefix}#{pages_subdomain}."
end.downcase 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 # 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}" "#{url}/#{url_path}"
end end
...@@ -1545,8 +1544,8 @@ class Project < ActiveRecord::Base ...@@ -1545,8 +1544,8 @@ class Project < ActiveRecord::Base
@errors = original_errors @errors = original_errors
end end
def add_export_job(current_user:, params: {}) def add_export_job(current_user:, after_export_strategy: nil, params: {})
job_id = ProjectExportWorker.perform_async(current_user.id, self.id, params) job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params)
if job_id if job_id
Rails.logger.info "Export job started for project ID #{self.id} with job ID #{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 ...@@ -1572,6 +1571,8 @@ class Project < ActiveRecord::Base
def export_status def export_status
if export_in_progress? if export_in_progress?
:started :started
elsif after_export_in_progress?
:after_export_action
elsif export_project_path elsif export_project_path
:finished :finished
else else
...@@ -1583,12 +1584,22 @@ class Project < ActiveRecord::Base ...@@ -1583,12 +1584,22 @@ class Project < ActiveRecord::Base
import_export_shared.active_export_count > 0 import_export_shared.active_export_count > 0
end end
def after_export_in_progress?
import_export_shared.after_export_in_progress?
end
def remove_exports def remove_exports
return nil unless export_path.present? return nil unless export_path.present?
FileUtils.rm_rf(export_path) FileUtils.rm_rf(export_path)
end end
def remove_exported_project_file
return unless export_project_path.present?
FileUtils.rm_f(export_project_path)
end
def full_path_slug def full_path_slug
Gitlab::Utils.slugify(full_path.to_s) Gitlab::Utils.slugify(full_path.to_s)
end end
......
...@@ -2,11 +2,15 @@ module Boards ...@@ -2,11 +2,15 @@ module Boards
class ListService < Boards::BaseService class ListService < Boards::BaseService
def execute def execute
create_board! if parent.boards.empty? create_board! if parent.boards.empty?
parent.boards boards
end end
private private
def boards
parent.boards
end
def create_board! def create_board!
Boards::CreateService.new(parent, current_user).execute Boards::CreateService.new(parent, current_user).execute
end end
......
...@@ -23,6 +23,7 @@ module Issues ...@@ -23,6 +23,7 @@ module Issues
end end
if project.issues_enabled? && issue.close if project.issues_enabled? && issue.close
issue.update(closed_by: current_user)
event_service.close_issue(issue, current_user) event_service.close_issue(issue, current_user)
create_note(issue, commit) if system_note create_note(issue, commit) if system_note
notification_service.close_issue(issue, current_user) if notifications notification_service.close_issue(issue, current_user) if notifications
......
module Projects module Projects
module ImportExport module ImportExport
class ExportService < BaseService class ExportService < BaseService
def execute(_options = {}) def execute(after_export_strategy = nil, options = {})
@shared = project.import_export_shared @shared = project.import_export_shared
save_all
save_all!
execute_after_export_action(after_export_strategy)
end end
private private
def save_all def execute_after_export_action(after_export_strategy)
if [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save) 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) Gitlab::ImportExport::Saver.save(project: project, shared: @shared)
notify_success notify_success
else else
cleanup_and_notify cleanup_and_notify_error!
end end
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 def version_saver
Gitlab::ImportExport::VersionSaver.new(shared: @shared) Gitlab::ImportExport::VersionSaver.new(shared: @shared)
end end
...@@ -41,19 +55,22 @@ module Projects ...@@ -41,19 +55,22 @@ module Projects
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared) Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
end 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(', ')}") Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
FileUtils.rm_rf(@shared.export_path) FileUtils.rm_rf(@shared.export_path)
notify_error notify_error
end
def cleanup_and_notify_error!
cleanup_and_notify_error
raise Gitlab::ImportExport::Error.new(@shared.errors.join(', ')) raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
end end
def notify_success def notify_success
Rails.logger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported") Rails.logger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported")
notification_service.project_exported(@project, @current_user)
end end
def notify_error def notify_error
......
...@@ -28,7 +28,11 @@ module Projects ...@@ -28,7 +28,11 @@ module Projects
def add_repository_to_project def add_repository_to_project
if project.external_import? && !unknown_url? 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 end
# We should skip the repository for a GitHub import or GitLab project import, # We should skip the repository for a GitHub import or GitLab project import,
......
...@@ -178,6 +178,9 @@ module Projects ...@@ -178,6 +178,9 @@ module Projects
def latest_sha def latest_sha
project.commit(build.ref).try(:sha).to_s project.commit(build.ref).try(:sha).to_s
ensure
# Close any file descriptors that were opened and free libgit2 buffers
project.cleanup
end end
def sha 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 @@ ...@@ -4,8 +4,8 @@
# protect against Server-side Request Forgery (SSRF). # protect against Server-side Request Forgery (SSRF).
class ImportableUrlValidator < ActiveModel::EachValidator class ImportableUrlValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value) def validate_each(record, attribute, value)
if Gitlab::UrlBlocker.blocked_url?(value, valid_ports: Project::VALID_IMPORT_PORTS) Gitlab::UrlBlocker.validate!(value, valid_ports: Project::VALID_IMPORT_PORTS)
record.errors.add(attribute, "imports are not allowed from that URL") rescue Gitlab::UrlBlocker::BlockedUrlError => e
end record.errors.add(attribute, "is blocked: #{e.message}")
end end
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 @@ ...@@ -9,111 +9,6 @@
.col-sm-10 .col-sm-10
= f.number_field :container_registry_token_expire_delay, class: 'form-control' = 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 %fieldset
%legend Abuse reports %legend Abuse reports
.form-group .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 @@ ...@@ -7,7 +7,7 @@
.settings-header .settings-header
%h4 %h4
= _('Visibility and access controls') = _('Visibility and access controls')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
= _('Set default and restrict visibility levels. Configure import sources and git access protocol.') = _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
.settings-header .settings-header
%h4 %h4
= _('Account and limit settings') = _('Account and limit settings')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
= _('Session expiration, projects limit and attachment size.') = _('Session expiration, projects limit and attachment size.')
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
.settings-header .settings-header
%h4 %h4
= _('Sign-up restrictions') = _('Sign-up restrictions')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
= _('Configure the way a user creates a new account.') = _('Configure the way a user creates a new account.')
...@@ -40,7 +40,7 @@ ...@@ -40,7 +40,7 @@
.settings-header .settings-header
%h4 %h4
= _('Sign-in restrictions') = _('Sign-in restrictions')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
= _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.') = _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.')
...@@ -51,7 +51,7 @@ ...@@ -51,7 +51,7 @@
.settings-header .settings-header
%h4 %h4
= _('Help page') = _('Help page')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
= _('Help page text and support page url.') = _('Help page text and support page url.')
...@@ -62,7 +62,7 @@ ...@@ -62,7 +62,7 @@
.settings-header .settings-header
%h4 %h4
= _('Pages') = _('Pages')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
= _('Size and domain settings for static websites') = _('Size and domain settings for static websites')
...@@ -73,7 +73,7 @@ ...@@ -73,7 +73,7 @@
.settings-header .settings-header
%h4 %h4
= _('Continuous Integration and Deployment') = _('Continuous Integration and Deployment')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
= _('Auto DevOps, runners amd job artifacts') = _('Auto DevOps, runners amd job artifacts')
...@@ -84,7 +84,7 @@ ...@@ -84,7 +84,7 @@
.settings-header .settings-header
%h4 %h4
= _('Metrics - Influx') = _('Metrics - Influx')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
= _('Enable and configure InfluxDB metrics.') = _('Enable and configure InfluxDB metrics.')
...@@ -95,12 +95,46 @@ ...@@ -95,12 +95,46 @@
.settings-header .settings-header
%h4 %h4
= _('Metrics - Prometheus') = _('Metrics - Prometheus')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
= _('Enable and configure Prometheus metrics.') = _('Enable and configure Prometheus metrics.')
.settings-content .settings-content
= render 'prometheus' = 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 .prepend-top-20
= render 'form' = render 'form'
- @no_container = true - @no_container = true
- breadcrumb_title "Details" - breadcrumb_title _("Details")
- can_create_subgroups = can?(current_user, :create_subgroup, @group) - can_create_subgroups = can?(current_user, :create_subgroup, @group)
= content_for :meta_tags do = content_for :meta_tags do
......
!!! 5 !!! 5
%html.devise-layout-html %html.devise-layout-html{ class: system_message_class }
= render "layouts/head" = render "layouts/head"
%body.ui_indigo.login-page.application.navless{ data: { page: body_data_page } } %body.ui_indigo.login-page.application.navless{ data: { page: body_data_page } }
.page-wrap .page-wrap
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
%h1 %h1
= brand_title = brand_title
= brand_image = brand_image
- if brand_item&.description? - if current_appearance&.description?
= brand_text = brand_text
- else - else
%h3 Open source software to collaborate on code %h3 Open source software to collaborate on code
......
!!! 5 !!! 5
%html{ lang: "en" } %html{ lang: "en", class: system_message_class }
= render "layouts/head" = render "layouts/head"
%body.ui_indigo.login-page.application.navless %body.ui_indigo.login-page.application.navless
= render "layouts/header/empty" = render "layouts/header/empty"
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.settings-header .settings-header
%h4 %h4
Export project Export project
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %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. 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 @@ ...@@ -31,7 +31,7 @@
%section.settings#js-cluster-details{ class: ('expanded' if expanded) } %section.settings#js-cluster-details{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4= s_('ClusterIntegration|Kubernetes cluster details') %h4= s_('ClusterIntegration|Kubernetes cluster details')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster') %p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content .settings-content
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
%section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) } %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4= _('Advanced settings') %h4= _('Advanced settings')
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration") %p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration")
.settings-content .settings-content
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
.settings-header .settings-header
%h4 %h4
Deploy Keys 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' = expanded ? 'Collapse' : 'Expand'
%p %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. 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 @@ ...@@ -8,7 +8,7 @@
.settings-header .settings-header
%h4 %h4
General project settings General project settings
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Update your project name, description, avatar, and other general settings. Update your project name, description, avatar, and other general settings.
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
.settings-header .settings-header
%h4 %h4
Permissions Permissions
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Enable or disable certain project features and choose access levels. Enable or disable certain project features and choose access levels.
...@@ -79,7 +79,7 @@ ...@@ -79,7 +79,7 @@
.settings-header .settings-header
%h4 %h4
Merge request settings Merge request settings
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Customize your merge request restrictions. Customize your merge request restrictions.
...@@ -94,7 +94,7 @@ ...@@ -94,7 +94,7 @@
.settings-header .settings-header
%h4 %h4
Advanced settings Advanced settings
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project. Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
......
- @no_container = true - @no_container = true
- breadcrumb_title "Details" - breadcrumb_title _("Details")
= render partial: 'flash_messages', locals: { project: @project } = render partial: 'flash_messages', locals: { project: @project }
......
- breadcrumb_title _("Details")
%h2 %h2
%i.fa.fa-warning %i.fa.fa-warning
#{ _('No repository') } #{ _('No repository') }
...@@ -10,7 +12,7 @@ ...@@ -10,7 +12,7 @@
.no-repo-actions .no-repo-actions
= link_to project_repository_path(@project), method: :post, class: 'btn btn-primary' do = 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 %strong.prepend-left-10.append-right-10 or
...@@ -19,4 +21,4 @@ ...@@ -19,4 +21,4 @@
- if can? current_user, :remove_project, @project - if can? current_user, :remove_project, @project
.prepend-top-20 .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 @@ ...@@ -4,7 +4,7 @@
.settings-header .settings-header
%h4 %h4
Protected Branches Protected Branches
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Keep stable branches secure and force developers to use merge requests. Keep stable branches secure and force developers to use merge requests.
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.settings-header .settings-header
%h4 %h4
Protected Tags Protected Tags
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Limit access to creating and updating tags. Limit access to creating and updating tags.
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.settings-header .settings-header
%h4 %h4
General pipelines settings General pipelines settings
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Update your CI/CD configuration, like job timeout or Auto DevOps. Update your CI/CD configuration, like job timeout or Auto DevOps.
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
.settings-header .settings-header
%h4 %h4
Runners settings Runners settings
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Register and see your runners for this project. Register and see your runners for this project.
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
%h4 %h4
= _('Secret variables') = _('Secret variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' = 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' = expanded ? 'Collapse' : 'Expand'
%p.append-bottom-0 %p.append-bottom-0
= render "ci/variables/content" = render "ci/variables/content"
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
.settings-header .settings-header
%h4 %h4
Pipeline triggers Pipeline triggers
%button.btn.js-settings-toggle %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will Triggers can force a specific branch or tag to get rebuilt with an API call. These tokens will
......
- @no_container = true - @no_container = true
- breadcrumb_title "Details" - breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- show_auto_devops_callout = show_auto_devops_callout?(@project) - show_auto_devops_callout = show_auto_devops_callout?(@project)
......
...@@ -84,7 +84,7 @@ ...@@ -84,7 +84,7 @@
= dropdown_content do = dropdown_content do
.js-due-date-calendar .js-due-date-calendar
- if @labels && @labels.any? - if @labels
- selected_labels = issuable.labels - selected_labels = issuable.labels
.block.labels .block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } } .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] - group = local_assigns[:group]
...@@ -17,7 +18,7 @@ ...@@ -17,7 +18,7 @@
Milestone #{milestone.title} Milestone #{milestone.title}
- if milestone.due_date || milestone.start_date - if milestone.due_date || milestone.start_date
%span.creator %span.creator
&middot; &nbsp;&middot;
= milestone_date_range(milestone) = milestone_date_range(milestone)
- if group - if group
.pull-right .pull-right
......
...@@ -138,21 +138,18 @@ module ObjectStorage ...@@ -138,21 +138,18 @@ module ObjectStorage
include Report include Report
def self.enqueue!(uploads, mounted_as, to_store) def self.enqueue!(uploads, model_class, mounted_as, to_store)
sanity_check!(uploads, mounted_as) 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 end
# We need to be sure all the uploads are for the same uploader and model type # We need to be sure all the uploads are for the same uploader and model type
# and that the mount point exists if provided. # 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 upload = uploads.first
uploader_class = upload.uploader.constantize uploader_class = upload.uploader.constantize
model_class = uploads.first.model_type.constantize
uploader_types = uploads.map(&:uploader).uniq uploader_types = uploads.map(&:uploader).uniq
model_types = uploads.map(&:model_type).uniq model_types = uploads.map(&:model_type).uniq
model_has_mount = mounted_as.nil? || model_class.uploaders[mounted_as] == uploader_class model_has_mount = mounted_as.nil? || model_class.uploaders[mounted_as] == uploader_class
...@@ -162,7 +159,12 @@ module ObjectStorage ...@@ -162,7 +159,12 @@ module ObjectStorage
raise(SanityCheckError, "Mount point #{mounted_as} not found in #{model_class}.") unless model_has_mount raise(SanityCheckError, "Mount point #{mounted_as} not found in #{model_class}.") unless model_has_mount
end 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 @mounted_as = mounted_as&.to_sym
@to_store = to_store @to_store = to_store
...@@ -178,7 +180,17 @@ module ObjectStorage ...@@ -178,7 +180,17 @@ module ObjectStorage
end end
def sanity_check!(uploads) 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 end
def build_uploaders(uploads) def build_uploaders(uploads)
......
...@@ -4,11 +4,19 @@ class ProjectExportWorker ...@@ -4,11 +4,19 @@ class ProjectExportWorker
sidekiq_options retry: 3 sidekiq_options retry: 3
def perform(current_user_id, project_id, params = {}) def perform(current_user_id, project_id, after_export_strategy = {}, params = {})
params = params.with_indifferent_access
current_user = User.find(current_user_id) current_user = User.find(current_user_id)
project = Project.find(project_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
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 ...@@ -932,6 +932,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
t.integer "last_edited_by_id" t.integer "last_edited_by_id"
t.boolean "discussion_locked" t.boolean "discussion_locked"
t.datetime_with_timezone "closed_at" t.datetime_with_timezone "closed_at"
t.integer "closed_by_id"
end end
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
...@@ -2095,6 +2096,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do ...@@ -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", "milestones", name: "fk_96b1dd429c", on_delete: :nullify
add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade 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: "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 "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", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", 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 ...@@ -24,11 +24,11 @@ Because Rubular doesn't understand `%{issue_ref}`, you can replace this by
**For Omnibus installations** **For Omnibus installations**
1. Open `/etc/gitlab/gitlab.rb` with your editor. 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: expression of your liking:
```ruby ```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. 1. [Reconfigure] GitLab for the changes to take effect.
......
...@@ -412,9 +412,10 @@ Example response: ...@@ -412,9 +412,10 @@ Example response:
Since GitLab 8.1, this is the new commit status API. 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 GET /projects/:id/repository/commits/:sha/statuses
......
...@@ -100,6 +100,7 @@ Example response: ...@@ -100,6 +100,7 @@ Example response:
}, },
"updated_at" : "2016-01-04T15:31:51.081Z", "updated_at" : "2016-01-04T15:31:51.081Z",
"closed_at" : null, "closed_at" : null,
"closed_by" : null,
"id" : 76, "id" : 76,
"title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.", "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
"created_at" : "2016-01-04T15:31:51.081Z", "created_at" : "2016-01-04T15:31:51.081Z",
...@@ -122,6 +123,8 @@ Example response: ...@@ -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**: `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 ## List group issues
Get a list of a group's issues. Get a list of a group's issues.
...@@ -216,6 +219,7 @@ Example response: ...@@ -216,6 +219,7 @@ Example response:
"updated_at" : "2016-01-04T15:31:46.176Z", "updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : null, "closed_at" : null,
"closed_by" : null,
"user_notes_count": 1, "user_notes_count": 1,
"due_date": null, "due_date": null,
"web_url": "http://example.com/example/example/issues/1", "web_url": "http://example.com/example/example/issues/1",
...@@ -233,6 +237,8 @@ Example response: ...@@ -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**: `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 ## List project issues
Get a list of a project's issues. Get a list of a project's issues.
...@@ -326,6 +332,14 @@ Example response: ...@@ -326,6 +332,14 @@ Example response:
"updated_at" : "2016-01-04T15:31:46.176Z", "updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : "2016-01-05T15: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, "user_notes_count": 1,
"due_date": "2016-07-22", "due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/1", "web_url": "http://example.com/example/example/issues/1",
...@@ -343,6 +357,8 @@ Example response: ...@@ -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**: `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 ## Single issue
Get a single project issue. Get a single project issue.
...@@ -409,6 +425,8 @@ Example response: ...@@ -409,6 +425,8 @@ Example response:
"title" : "Ut commodi ullam eos dolores perferendis nihil sunt.", "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
"updated_at" : "2016-01-04T15:31:46.176Z", "updated_at" : "2016-01-04T15:31:46.176Z",
"created_at" : "2016-01-04T15:31:46.176Z", "created_at" : "2016-01-04T15:31:46.176Z",
"closed_at" : null,
"closed_by" : null,
"subscribed": false, "subscribed": false,
"user_notes_count": 1, "user_notes_count": 1,
"due_date": null, "due_date": null,
...@@ -432,6 +450,8 @@ Example response: ...@@ -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**: `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 ## New issue
Creates a new project issue. Creates a new project issue.
...@@ -484,6 +504,7 @@ Example response: ...@@ -484,6 +504,7 @@ Example response:
"description" : null, "description" : null,
"updated_at" : "2016-01-07T12:44:33.959Z", "updated_at" : "2016-01-07T12:44:33.959Z",
"closed_at" : null, "closed_at" : null,
"closed_by" : null,
"milestone" : null, "milestone" : null,
"subscribed" : true, "subscribed" : true,
"user_notes_count": 0, "user_notes_count": 0,
...@@ -508,6 +529,8 @@ Example response: ...@@ -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**: `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 ## Edit issue
Updates an existing project issue. This call is also used to mark an issue as Updates an existing project issue. This call is also used to mark an issue as
...@@ -556,6 +579,14 @@ Example response: ...@@ -556,6 +579,14 @@ Example response:
"description" : null, "description" : null,
"updated_at" : "2016-01-07T12:55:16.213Z", "updated_at" : "2016-01-07T12:55:16.213Z",
"closed_at" : "2016-01-08T12: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, "iid" : 15,
"labels" : [ "labels" : [
"bug" "bug"
...@@ -587,6 +618,8 @@ Example response: ...@@ -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**: `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 ## Delete an issue
Only for admins and project owners. Soft deletes the issue in question. Only for admins and project owners. Soft deletes the issue in question.
...@@ -640,6 +673,7 @@ Example response: ...@@ -640,6 +673,7 @@ Example response:
"created_at": "2016-04-05T21:41:45.652Z", "created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z", "updated_at": "2016-04-07T12:20:17.596Z",
"closed_at": null, "closed_at": null,
"closed_by": null,
"labels": [], "labels": [],
"milestone": null, "milestone": null,
"assignees": [{ "assignees": [{
...@@ -687,6 +721,8 @@ Example response: ...@@ -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**: `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 ## Subscribe to an issue
Subscribes the authenticated user to an issue to receive notifications. Subscribes the authenticated user to an issue to receive notifications.
...@@ -719,6 +755,7 @@ Example response: ...@@ -719,6 +755,7 @@ Example response:
"created_at": "2016-04-05T21:41:45.652Z", "created_at": "2016-04-05T21:41:45.652Z",
"updated_at": "2016-04-07T12:20:17.596Z", "updated_at": "2016-04-07T12:20:17.596Z",
"closed_at": null, "closed_at": null,
"closed_by": null,
"labels": [], "labels": [],
"milestone": null, "milestone": null,
"assignees": [{ "assignees": [{
...@@ -766,6 +803,9 @@ Example response: ...@@ -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**: `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 ## Unsubscribe from an issue
Unsubscribes the authenticated user from the issue to not receive notifications Unsubscribes the authenticated user from the issue to not receive notifications
...@@ -807,6 +847,8 @@ Example response: ...@@ -807,6 +847,8 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
"web_url": "https://gitlab.example.com/keyon" "web_url": "https://gitlab.example.com/keyon"
}, },
"closed_at": null,
"closed_by": null,
"author": { "author": {
"name": "Vivian Hermann", "name": "Vivian Hermann",
"username": "orville", "username": "orville",
...@@ -927,6 +969,9 @@ Example response: ...@@ -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**: `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 ## Set a time estimate for an issue
Sets an estimated time of work for this issue. Sets an estimated time of work for this issue.
...@@ -1112,6 +1157,8 @@ Example response: ...@@ -1112,6 +1157,8 @@ Example response:
"assignee": null, "assignee": null,
"source_project_id": 1, "source_project_id": 1,
"target_project_id": 1, "target_project_id": 1,
"closed_at": null,
"closed_by": null,
"labels": [], "labels": [],
"work_in_progress": false, "work_in_progress": false,
"milestone": null, "milestone": null,
...@@ -1206,3 +1253,4 @@ Example response: ...@@ -1206,3 +1253,4 @@ Example response:
[ce-13004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13004 [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-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 ...@@ -294,9 +294,10 @@ Example of response
## Get job artifacts ## 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 GET /projects/:id/jobs/:job_id/artifacts
...@@ -307,8 +308,10 @@ 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 | | `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 | | `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: Response:
...@@ -322,7 +325,8 @@ Response: ...@@ -322,7 +325,8 @@ Response:
## Download the artifacts archive ## 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 Download the artifacts archive from the given reference name and job provided the
job finished successfully. job finished successfully.
...@@ -339,7 +343,7 @@ Parameters ...@@ -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) | | `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 | | `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" curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test"
......
...@@ -8,6 +8,14 @@ ...@@ -8,6 +8,14 @@
Start a new export. 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 ```http
POST /projects/:id/export POST /projects/:id/export
``` ```
...@@ -16,9 +24,12 @@ 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 | | `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 | | `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 ```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 ```json
...@@ -43,7 +54,11 @@ GET /projects/:id/export ...@@ -43,7 +54,11 @@ GET /projects/:id/export
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/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. `_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