Commit 4de3bc5f authored by Nick Thomas's avatar Nick Thomas

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents c2834665 25c17102
{ {
"presets": [["latest", { "es2015": { "modules": false } }], "stage-2"], "presets": [["latest", { "es2015": { "modules": false } }], "stage-2"],
"env": { "env": {
"karma": {
"plugins": ["rewire"]
},
"coverage": { "coverage": {
"plugins": [ "plugins": [
[ [
...@@ -14,7 +17,8 @@ ...@@ -14,7 +17,8 @@
{ {
"process.env.BABEL_ENV": "coverage" "process.env.BABEL_ENV": "coverage"
} }
] ],
"rewire"
] ]
} }
} }
......
...@@ -289,7 +289,6 @@ stages: ...@@ -289,7 +289,6 @@ stages:
# Trigger a package build in omnibus-gitlab repository # Trigger a package build in omnibus-gitlab repository
# #
package-and-qa: package-and-qa:
<<: *dedicated-runner
image: ruby:2.4-alpine image: ruby:2.4-alpine
before_script: [] before_script: []
stage: build stage: build
......
...@@ -26,11 +26,18 @@ export default { ...@@ -26,11 +26,18 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
forceModifiedIcon: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
changedIcon() { changedIcon() {
const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : ''; const suffix = this.file.staged && !this.showStagedIcon ? '-solid' : '';
return this.file.tempFile ? `file-addition${suffix}` : `file-modified${suffix}`; return this.file.tempFile && !this.forceModifiedIcon
? `file-addition${suffix}`
: `file-modified${suffix}`;
}, },
stagedIcon() { stagedIcon() {
return `${this.changedIcon}-solid`; return `${this.changedIcon}-solid`;
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import icon from '~/vue_shared/components/icon.vue'; import icon from '~/vue_shared/components/icon.vue';
import newModal from './modal.vue'; import newModal from './modal.vue';
import upload from './upload.vue'; import upload from './upload.vue';
export default { export default {
components: { components: {
icon, icon,
newModal, newModal,
...@@ -27,10 +27,15 @@ ...@@ -27,10 +27,15 @@
dropdownOpen: false, dropdownOpen: false,
}; };
}, },
watch: {
dropdownOpen() {
this.$nextTick(() => {
this.$refs.dropdownMenu.scrollIntoView();
});
},
},
methods: { methods: {
...mapActions([ ...mapActions(['createTempEntry']),
'createTempEntry',
]),
createNewItem(type) { createNewItem(type) {
this.modalType = type; this.modalType = type;
this.openModal = true; this.openModal = true;
...@@ -43,7 +48,7 @@ ...@@ -43,7 +48,7 @@
this.dropdownOpen = !this.dropdownOpen; this.dropdownOpen = !this.dropdownOpen;
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -71,7 +76,10 @@ ...@@ -71,7 +76,10 @@
css-classes="pull-left" css-classes="pull-left"
/> />
</button> </button>
<ul class="dropdown-menu dropdown-menu-right"> <ul
class="dropdown-menu dropdown-menu-right"
ref="dropdownMenu"
>
<li> <li>
<a <a
href="#" href="#"
......
...@@ -40,13 +40,6 @@ export default { ...@@ -40,13 +40,6 @@ export default {
return __('Create file'); return __('Create file');
}, },
formLabelName() {
if (this.type === 'tree') {
return __('Directory name');
}
return __('File name');
},
}, },
mounted() { mounted() {
this.$refs.fieldName.focus(); this.$refs.fieldName.focus();
...@@ -82,8 +75,8 @@ export default { ...@@ -82,8 +75,8 @@ export default {
@submit.prevent="createEntryInStore" @submit.prevent="createEntryInStore"
> >
<fieldset class="form-group append-bottom-0"> <fieldset class="form-group append-bottom-0">
<label class="label-light col-sm-3"> <label class="label-light col-sm-3 ide-new-modal-label">
{{ formLabelName }} {{ __('Name') }}
</label> </label>
<div class="col-sm-9"> <div class="col-sm-9">
<input <input
......
...@@ -97,7 +97,7 @@ export default { ...@@ -97,7 +97,7 @@ export default {
:file="file" :file="file"
/> />
</span> </span>
<span class="pull-right"> <span class="pull-right ide-file-icon-holder">
<mr-file-icon <mr-file-icon
v-if="file.mrChange" v-if="file.mrChange"
/> />
...@@ -106,7 +106,8 @@ export default { ...@@ -106,7 +106,8 @@ export default {
:file="file" :file="file"
:show-tooltip="true" :show-tooltip="true"
:show-staged-icon="true" :show-staged-icon="true"
class="prepend-top-5 pull-right" :force-modified-icon="true"
class="pull-right"
/> />
</span> </span>
<new-dropdown <new-dropdown
......
...@@ -84,6 +84,7 @@ export default { ...@@ -84,6 +84,7 @@ export default {
<changed-file-icon <changed-file-icon
v-else v-else
:file="tab" :file="tab"
:force-modified-icon="true"
/> />
</button> </button>
......
...@@ -33,10 +33,7 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => { ...@@ -33,10 +33,7 @@ export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
} }
}; };
export const toggleRightPanelCollapsed = ( export const toggleRightPanelCollapsed = ({ dispatch, state }, e = undefined) => {
{ dispatch, state },
e = undefined,
) => {
if (e) { if (e) {
$(e.currentTarget) $(e.currentTarget)
.tooltip('hide') .tooltip('hide')
...@@ -77,7 +74,7 @@ export const createTempEntry = ( ...@@ -77,7 +74,7 @@ export const createTempEntry = (
} }
worker.addEventListener('message', ({ data }) => { worker.addEventListener('message', ({ data }) => {
const { file } = data; const { file, parentPath } = data;
worker.terminate(); worker.terminate();
...@@ -93,6 +90,10 @@ export const createTempEntry = ( ...@@ -93,6 +90,10 @@ export const createTempEntry = (
dispatch('setFileActive', file.path); dispatch('setFileActive', file.path);
} }
if (parentPath && !state.entries[parentPath].opened) {
commit(types.TOGGLE_TREE_OPEN, parentPath);
}
resolve(file); resolve(file);
}); });
...@@ -137,6 +138,14 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => { ...@@ -137,6 +138,14 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay); commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
}; };
export const updateTempFlagForEntry = ({ commit, dispatch, state }, { file, tempFile }) => {
commit(types.UPDATE_TEMP_FLAG, { path: file.path, tempFile });
if (file.parentPath) {
dispatch('updateTempFlagForEntry', { file: state.entries[file.parentPath], tempFile });
}
};
export const toggleFileFinder = ({ commit }, fileFindVisible) => export const toggleFileFinder = ({ commit }, fileFindVisible) =>
commit(types.TOGGLE_FILE_FINDER, fileFindVisible); commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
...@@ -144,3 +153,6 @@ export * from './actions/tree'; ...@@ -144,3 +153,6 @@ 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'; export * from './actions/merge_request';
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
...@@ -56,3 +56,6 @@ export const allBlobs = state => ...@@ -56,3 +56,6 @@ export const allBlobs = state =>
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path); export const getStagedFile = state => path => state.stagedFiles.find(f => f.path === path);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
...@@ -110,6 +110,17 @@ export const updateFilesAfterCommit = ( ...@@ -110,6 +110,17 @@ export const updateFilesAfterCommit = (
{ root: true }, { root: true },
); );
commit(
rootTypes.TOGGLE_FILE_CHANGED,
{
file,
changed: false,
},
{ root: true },
);
dispatch('updateTempFlagForEntry', { file, tempFile: false }, { root: true });
eventHub.$emit(`editor.update.model.content.${file.key}`, { eventHub.$emit(`editor.update.model.content.${file.key}`, {
content: file.content, content: file.content,
changed: !!changedFile, changed: !!changedFile,
...@@ -185,3 +196,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) = ...@@ -185,3 +196,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) =
commit(types.UPDATE_LOADING, false); commit(types.UPDATE_LOADING, false);
}); });
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
...@@ -27,3 +27,6 @@ export const branchName = (state, getters, rootState) => { ...@@ -27,3 +27,6 @@ export const branchName = (state, getters, rootState) => {
return rootState.currentBranchId; return rootState.currentBranchId;
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
...@@ -59,4 +59,5 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT'; ...@@ -59,4 +59,5 @@ export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
export const ADD_PENDING_TAB = 'ADD_PENDING_TAB'; export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB'; export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
...@@ -4,6 +4,7 @@ import mergeRequestMutation from './mutations/merge_request'; ...@@ -4,6 +4,7 @@ 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';
import { sortTree } from './utils';
export default { export default {
[types.SET_INITIAL_DATA](state, data) { [types.SET_INITIAL_DATA](state, data) {
...@@ -73,7 +74,7 @@ export default { ...@@ -73,7 +74,7 @@ export default {
f => foundEntry.tree.find(e => e.path === f.path) === undefined, f => foundEntry.tree.find(e => e.path === f.path) === undefined,
); );
Object.assign(foundEntry, { Object.assign(foundEntry, {
tree: foundEntry.tree.concat(tree), tree: sortTree(foundEntry.tree.concat(tree)),
}); });
} }
...@@ -86,10 +87,16 @@ export default { ...@@ -86,10 +87,16 @@ 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(data.treeList), tree: sortTree(state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList)),
}); });
} }
}, },
[types.UPDATE_TEMP_FLAG](state, { path, tempFile }) {
Object.assign(state.entries[path], {
tempFile,
changed: tempFile,
});
},
[types.UPDATE_VIEWER](state, viewer) { [types.UPDATE_VIEWER](state, viewer) {
Object.assign(state, { Object.assign(state, {
viewer, viewer,
......
...@@ -33,6 +33,7 @@ export const dataStructure = () => ({ ...@@ -33,6 +33,7 @@ export const dataStructure = () => ({
raw: '', raw: '',
content: '', content: '',
parentTreeUrl: '', parentTreeUrl: '',
parentPath: '',
renderError: false, renderError: false,
base64: false, base64: false,
editorRow: 1, editorRow: 1,
...@@ -65,6 +66,7 @@ export const decorateData = entity => { ...@@ -65,6 +66,7 @@ export const decorateData = entity => {
previewMode, previewMode,
file_lock, file_lock,
html, html,
parentPath = '',
} = entity; } = entity;
return { return {
...@@ -81,6 +83,7 @@ export const decorateData = entity => { ...@@ -81,6 +83,7 @@ export const decorateData = entity => {
opened, opened,
active, active,
parentTreeUrl, parentTreeUrl,
parentPath,
changed, changed,
renderError, renderError,
content, content,
...@@ -121,8 +124,8 @@ const sortTreesByTypeAndName = (a, b) => { ...@@ -121,8 +124,8 @@ const sortTreesByTypeAndName = (a, b) => {
} else if (a.type === 'blob' && b.type === 'tree') { } else if (a.type === 'blob' && b.type === 'tree') {
return 1; return 1;
} }
if (a.name.toLowerCase() < b.name.toLowerCase()) return -1; if (a.name < b.name) return -1;
if (a.name.toLowerCase() > b.name.toLowerCase()) return 1; if (a.name > b.name) return 1;
return 0; return 0;
}; };
......
...@@ -6,6 +6,7 @@ self.addEventListener('message', e => { ...@@ -6,6 +6,7 @@ self.addEventListener('message', e => {
const treeList = []; const treeList = [];
let file; let file;
let parentPath;
const entries = data.reduce((acc, path) => { const entries = data.reduce((acc, path) => {
const pathSplit = path.split('/'); const pathSplit = path.split('/');
const blobName = pathSplit.pop().trim(); const blobName = pathSplit.pop().trim();
...@@ -17,6 +18,8 @@ self.addEventListener('message', e => { ...@@ -17,6 +18,8 @@ self.addEventListener('message', e => {
const foundEntry = acc[folderPath]; const foundEntry = acc[folderPath];
if (!foundEntry) { if (!foundEntry) {
parentPath = parentFolder ? parentFolder.path : null;
const tree = decorateData({ const tree = decorateData({
projectId, projectId,
branchId, branchId,
...@@ -29,6 +32,7 @@ self.addEventListener('message', e => { ...@@ -29,6 +32,7 @@ self.addEventListener('message', e => {
tempFile, tempFile,
changed: tempFile, changed: tempFile,
opened: tempFile, opened: tempFile,
parentPath,
}); });
Object.assign(acc, { Object.assign(acc, {
...@@ -52,6 +56,8 @@ self.addEventListener('message', e => { ...@@ -52,6 +56,8 @@ self.addEventListener('message', e => {
if (blobName !== '') { if (blobName !== '') {
const fileFolder = acc[pathSplit.join('/')]; const fileFolder = acc[pathSplit.join('/')];
parentPath = fileFolder ? fileFolder.path : null;
file = decorateData({ file = decorateData({
projectId, projectId,
branchId, branchId,
...@@ -66,6 +72,7 @@ self.addEventListener('message', e => { ...@@ -66,6 +72,7 @@ self.addEventListener('message', e => {
content, content,
base64, base64,
previewMode: viewerInformationForPath(blobName), previewMode: viewerInformationForPath(blobName),
parentPath,
}); });
Object.assign(acc, { Object.assign(acc, {
...@@ -86,5 +93,6 @@ self.addEventListener('message', e => { ...@@ -86,5 +93,6 @@ self.addEventListener('message', e => {
entries, entries,
treeList: sortTree(treeList), treeList: sortTree(treeList),
file, file,
parentPath,
}); });
}); });
...@@ -45,7 +45,7 @@ export default { ...@@ -45,7 +45,7 @@ export default {
return timeIntervalInWords(this.job.queued); return timeIntervalInWords(this.job.queued);
}, },
runnerId() { runnerId() {
return `#${this.job.runner.id}`; return `${this.job.runner.description} (#${this.job.runner.id})`;
}, },
retryButtonClass() { retryButtonClass() {
let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block'; let className = 'js-retry-button pull-right btn btn-retry visible-md-block visible-lg-block';
......
...@@ -315,3 +315,6 @@ export const scrollToNoteIfNeeded = (context, el) => { ...@@ -315,3 +315,6 @@ export const scrollToNoteIfNeeded = (context, el) => {
scrollToElement(el); scrollToElement(el);
} }
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
...@@ -68,3 +68,6 @@ export const resolvedDiscussionCount = (state, getters) => { ...@@ -68,3 +68,6 @@ export const resolvedDiscussionCount = (state, getters) => {
return Object.keys(resolvedMap).length; return Object.keys(resolvedMap).length;
}; };
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
...@@ -52,16 +52,15 @@ ...@@ -52,16 +52,15 @@
text() { text() {
const keepContributionsText = s__(`AdminArea| const keepContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}. You are about to permanently delete the user %{username}.
This will delete all of the issues, merge requests, and groups linked to them. Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user".
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
const deleteContributionsText = s__(`AdminArea| const deleteContributionsText = s__(`AdminArea|
You are about to permanently delete the user %{username}. You are about to permanently delete the user %{username}.
Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user". This will delete all of the issues, merge requests, and groups linked to them.
To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead.
Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`);
return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText, return sprintf(this.deleteContributions ? deleteContributionsText : keepContributionsText,
{ {
username: `<strong>${_.escape(this.username)}</strong>`, username: `<strong>${_.escape(this.username)}</strong>`,
......
...@@ -35,3 +35,6 @@ export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destr ...@@ -35,3 +35,6 @@ export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destr
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING); export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
export const isLoading = state => state.isLoading; export const isLoading = state => state.isLoading;
export const repos = state => state.repos; export const repos = state => state.repos;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
<script> <script>
import getIconForFile from './file_icon/file_icon_map'; import getIconForFile from './file_icon/file_icon_map';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import icon from '../../vue_shared/components/icon.vue'; import icon from '../../vue_shared/components/icon.vue';
/* This is a re-usable vue component for rendering a svg sprite /* This is a re-usable vue component for rendering a svg sprite
icon icon
Sample configuration: Sample configuration:
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
/> />
*/ */
export default { export default {
components: { components: {
loadingIcon, loadingIcon,
icon, icon,
...@@ -68,7 +68,7 @@ ...@@ -68,7 +68,7 @@
return this.size ? `s${this.size}` : ''; return this.size ? `s${this.size}` : '';
}, },
}, },
}; };
</script> </script>
<template> <template>
<span> <span>
...@@ -82,6 +82,7 @@ ...@@ -82,6 +82,7 @@
v-if="!loading && folder" v-if="!loading && folder"
:name="folderIconName" :name="folderIconName"
:size="size" :size="size"
css-classes="folder-icon"
/> />
<loading-icon <loading-icon
v-if="loading" v-if="loading"
......
...@@ -40,10 +40,6 @@ ...@@ -40,10 +40,6 @@
.project-home-panel { .project-home-panel {
padding-left: 0 !important; padding-left: 0 !important;
.project-avatar {
display: block;
}
.project-repo-buttons, .project-repo-buttons,
.git-clone-holder { .git-clone-holder {
display: none; display: none;
......
...@@ -241,8 +241,6 @@ ...@@ -241,8 +241,6 @@
} }
.scrolling-tabs-container { .scrolling-tabs-container {
position: relative;
.merge-request-tabs-container & { .merge-request-tabs-container & {
overflow: hidden; overflow: hidden;
} }
...@@ -272,8 +270,6 @@ ...@@ -272,8 +270,6 @@
} }
.inner-page-scroll-tabs { .inner-page-scroll-tabs {
position: relative;
.fade-right { .fade-right {
@include fade(left, $white-light); @include fade(left, $white-light);
right: 0; right: 0;
......
...@@ -314,6 +314,10 @@ ...@@ -314,6 +314,10 @@
display: inline-flex; display: inline-flex;
vertical-align: top; vertical-align: top;
&:hover .color-label {
text-decoration: underline;
}
.label { .label {
vertical-align: inherit; vertical-align: inherit;
font-size: $label-font-size; font-size: $label-font-size;
......
...@@ -55,6 +55,7 @@ ...@@ -55,6 +55,7 @@
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: inherit; max-width: inherit;
line-height: 22px;
svg { svg {
vertical-align: middle; vertical-align: middle;
...@@ -67,6 +68,11 @@ ...@@ -67,6 +68,11 @@
} }
} }
.ide-file-icon-holder {
display: flex;
align-items: center;
}
.ide-file-changed-icon { .ide-file-changed-icon {
margin-left: auto; margin-left: auto;
...@@ -77,7 +83,6 @@ ...@@ -77,7 +83,6 @@
.ide-new-btn { .ide-new-btn {
display: none; display: none;
margin-bottom: -4px;
margin-right: -8px; margin-right: -8px;
} }
...@@ -90,12 +95,10 @@ ...@@ -90,12 +95,10 @@
} }
} }
&.folder { .folder-icon {
svg {
fill: $gl-text-color-secondary; fill: $gl-text-color-secondary;
} }
} }
}
a { a {
color: $gl-text-color; color: $gl-text-color;
...@@ -111,6 +114,7 @@ ...@@ -111,6 +114,7 @@
.file-col-commit-message { .file-col-commit-message {
display: flex; display: flex;
overflow: visible; overflow: visible;
align-items: center;
padding: 6px 12px; padding: 6px 12px;
} }
...@@ -438,7 +442,7 @@ ...@@ -438,7 +442,7 @@
.projects-sidebar { .projects-sidebar {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; flex: 1;
.context-header { .context-header {
width: auto; width: auto;
...@@ -967,3 +971,7 @@ ...@@ -967,3 +971,7 @@
background: transparent; background: transparent;
resize: none; resize: none;
} }
.ide-new-modal-label {
line-height: 34px;
}
...@@ -165,8 +165,8 @@ module IssuableCollections ...@@ -165,8 +165,8 @@ module IssuableCollections
[:project, :author, :assignees, :labels, :milestone, project: :namespace] [:project, :author, :assignees, :labels, :milestone, project: :namespace]
when 'MergeRequest' when 'MergeRequest'
[ [
:source_project, :target_project, :author, :assignee, :labels, :milestone, :target_project, :author, :assignee, :labels, :milestone,
head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits source_project: :route, head_pipeline: :project, target_project: :namespace, latest_merge_request_diff: :merge_request_diff_commits
] ]
end end
end end
......
...@@ -400,7 +400,8 @@ module ProjectsHelper ...@@ -400,7 +400,8 @@ module ProjectsHelper
exports_path = File.join(Settings.shared['path'], 'tmp/project_exports') exports_path = File.join(Settings.shared['path'], 'tmp/project_exports')
filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]") filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]")
filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") disk_path = Gitlab.config.repositories.storages[project.repository_storage].legacy_disk_path
filtered_message.gsub(disk_path.chomp('/'), "[REPOS PATH]")
end end
def project_child_container_class(view_path) def project_child_container_class(view_path)
......
...@@ -31,7 +31,7 @@ module ProtectedRef ...@@ -31,7 +31,7 @@ module ProtectedRef
end end
end end
def protected_ref_accessible_to?(ref, user, action:, protected_refs: nil) def protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level| access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
access_level.check_access(user) access_level.check_access(user)
end end
......
...@@ -45,25 +45,25 @@ module Storage ...@@ -45,25 +45,25 @@ module Storage
# Hooks # Hooks
# Save the storage paths before the projects are destroyed to use them on after destroy # Save the storages before the projects are destroyed to use them on after destroy
def prepare_for_destroy def prepare_for_destroy
old_repository_storage_paths old_repository_storages
end end
private private
def move_repositories def move_repositories
# Move the namespace directory in all storage paths used by member projects # Move the namespace directory in all storages used by member projects
repository_storage_paths.each do |repository_storage_path| repository_storages.each do |repository_storage|
# Ensure old directory exists before moving it # Ensure old directory exists before moving it
gitlab_shell.add_namespace(repository_storage_path, full_path_was) gitlab_shell.add_namespace(repository_storage, full_path_was)
# Ensure new directory exists before moving it (if there's a parent) # Ensure new directory exists before moving it (if there's a parent)
gitlab_shell.add_namespace(repository_storage_path, parent.full_path) if parent gitlab_shell.add_namespace(repository_storage, parent.full_path) if parent
unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path) unless gitlab_shell.mv_namespace(repository_storage, full_path_was, full_path)
Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}" Rails.logger.error "Exception moving path #{repository_storage} from #{full_path_was} to #{full_path}"
# if we cannot move namespace directory we should rollback # if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs # db changes in order to prevent out of sync between db and fs
...@@ -72,33 +72,33 @@ module Storage ...@@ -72,33 +72,33 @@ module Storage
end end
end end
def old_repository_storage_paths def old_repository_storages
@old_repository_storage_paths ||= repository_storage_paths @old_repository_storage_paths ||= repository_storages
end end
def repository_storage_paths def repository_storages
# We need to get the storage paths for all the projects, even the ones that are # We need to get the storage paths for all the projects, even the ones that are
# pending delete. Unscoping also get rids of the default order, which causes # pending delete. Unscoping also get rids of the default order, which causes
# problems with SELECT DISTINCT. # problems with SELECT DISTINCT.
Project.unscoped do Project.unscoped do
all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path) all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage)
end end
end end
def rm_dir def rm_dir
# Remove the namespace directory in all storages paths used by member projects # Remove the namespace directory in all storages paths used by member projects
old_repository_storage_paths.each do |repository_storage_path| old_repository_storages.each do |repository_storage|
# Move namespace directory into trash. # Move namespace directory into trash.
# We will remove it later async # We will remove it later async
new_path = "#{full_path}+#{id}+deleted" new_path = "#{full_path}+#{id}+deleted"
if gitlab_shell.mv_namespace(repository_storage_path, full_path, new_path) if gitlab_shell.mv_namespace(repository_storage, full_path, new_path)
Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}") Gitlab::AppLogger.info %Q(Namespace directory "#{full_path}" moved to "#{new_path}")
# Remove namespace directroy async with delay so # Remove namespace directroy async with delay so
# GitLab has time to remove all projects first # GitLab has time to remove all projects first
run_after_commit do run_after_commit do
GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage_path, new_path) GitlabShellWorker.perform_in(5.minutes, :rm_namespace, repository_storage, new_path)
end end
end end
end end
......
...@@ -197,10 +197,6 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -197,10 +197,6 @@ class MergeRequestDiff < ActiveRecord::Base
CompareService.new(project, head_commit_sha).execute(project, sha, straight: true) CompareService.new(project, head_commit_sha).execute(project, sha, straight: true)
end end
def commits_count
super || merge_request_diff_commits.size
end
private private
def create_merge_request_diff_files(diffs) def create_merge_request_diff_files(diffs)
......
...@@ -518,10 +518,6 @@ class Project < ActiveRecord::Base ...@@ -518,10 +518,6 @@ class Project < ActiveRecord::Base
repository.empty? repository.empty?
end end
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage]&.legacy_disk_path
end
def team def team
@team ||= ProjectTeam.new(self) @team ||= ProjectTeam.new(self)
end end
...@@ -1047,13 +1043,6 @@ class Project < ActiveRecord::Base ...@@ -1047,13 +1043,6 @@ class Project < ActiveRecord::Base
"#{web_url}.git" "#{web_url}.git"
end end
def user_can_push_to_empty_repo?(user)
return false unless empty_repo?
return false unless Ability.allowed?(user, :push_code, self)
!ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
end
def forked? def forked?
return true if fork_network && fork_network.root_project != self return true if fork_network && fork_network.root_project != self
...@@ -1112,7 +1101,7 @@ class Project < ActiveRecord::Base ...@@ -1112,7 +1101,7 @@ class Project < ActiveRecord::Base
# Check if repository already exists on disk # Check if repository already exists on disk
def check_repository_path_availability def check_repository_path_availability
return true if skip_disk_validation return true if skip_disk_validation
return false unless repository_storage_path return false unless repository_storage
expires_full_path_cache # we need to clear cache to validate renames correctly expires_full_path_cache # we need to clear cache to validate renames correctly
...@@ -1917,14 +1906,14 @@ class Project < ActiveRecord::Base ...@@ -1917,14 +1906,14 @@ class Project < ActiveRecord::Base
def check_repository_absence! def check_repository_absence!
return if skip_disk_validation return if skip_disk_validation
if repository_storage_path.blank? || repository_with_same_path_already_exists? if repository_storage.blank? || repository_with_same_path_already_exists?
errors.add(:base, 'There is already a repository with that name on disk') errors.add(:base, 'There is already a repository with that name on disk')
throw :abort throw :abort
end end
end end
def repository_with_same_path_already_exists? def repository_with_same_path_already_exists?
gitlab_shell.exists?(repository_storage_path, "#{disk_path}.git") gitlab_shell.exists?(repository_storage, "#{disk_path}.git")
end end
# set last_activity_at to the same as created_at # set last_activity_at to the same as created_at
...@@ -2014,10 +2003,11 @@ class Project < ActiveRecord::Base ...@@ -2014,10 +2003,11 @@ class Project < ActiveRecord::Base
def fetch_branch_allows_maintainer_push?(user, branch_name) def fetch_branch_allows_maintainer_push?(user, branch_name)
check_access = -> do check_access = -> do
next false if empty_repo?
merge_request = source_of_merge_requests.opened merge_request = source_of_merge_requests.opened
.where(allow_maintainer_to_push: true) .where(allow_maintainer_to_push: true)
.find_by(source_branch: branch_name) .find_by(source_branch: branch_name)
merge_request&.can_be_merged_by?(user) merge_request&.can_be_merged_by?(user)
end end
......
...@@ -21,7 +21,7 @@ class ProjectWiki ...@@ -21,7 +21,7 @@ class ProjectWiki
end end
delegate :empty?, to: :pages delegate :empty?, to: :pages
delegate :repository_storage_path, :hashed_storage?, to: :project delegate :repository_storage, :hashed_storage?, to: :project
def path def path
@project.path + '.wiki' @project.path + '.wiki'
......
...@@ -4,6 +4,15 @@ class ProtectedBranch < ActiveRecord::Base ...@@ -4,6 +4,15 @@ class ProtectedBranch < ActiveRecord::Base
protected_ref_access_levels :merge, :push protected_ref_access_levels :merge, :push
def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
# Masters, owners and admins are allowed to create the default branch
if default_branch_protected? && project.empty_repo?
return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER
end
super
end
# Check if branch name is marked as protected in the system # Check if branch name is marked as protected in the system
def self.protected?(project, ref_name) def self.protected?(project, ref_name)
return true if project.empty_repo? && default_branch_protected? return true if project.empty_repo? && default_branch_protected?
......
...@@ -84,10 +84,15 @@ class Repository ...@@ -84,10 +84,15 @@ class Repository
# Return absolute path to repository # Return absolute path to repository
def path_to_repo def path_to_repo
@path_to_repo ||= File.expand_path( @path_to_repo ||=
File.join(repository_storage_path, disk_path + '.git') begin
storage = Gitlab.config.repositories.storages[@project.repository_storage]
File.expand_path(
File.join(storage.legacy_disk_path, disk_path + '.git')
) )
end end
end
def inspect def inspect
"#<#{self.class.name}:#{@disk_path}>" "#<#{self.class.name}:#{@disk_path}>"
...@@ -915,10 +920,6 @@ class Repository ...@@ -915,10 +920,6 @@ class Repository
raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref) raw_repository.fetch_ref(source_repository.raw_repository, source_ref: source_ref, target_ref: target_ref)
end end
def repository_storage_path
@project.repository_storage_path
end
def rebase(user, merge_request) def rebase(user, merge_request)
raw.rebase(user, merge_request.id, branch: merge_request.source_branch, raw.rebase(user, merge_request.id, branch: merge_request.source_branch,
branch_sha: merge_request.source_branch_sha, branch_sha: merge_request.source_branch_sha,
......
module Storage module Storage
class HashedProject class HashedProject
attr_accessor :project attr_accessor :project
delegate :gitlab_shell, :repository_storage_path, to: :project delegate :gitlab_shell, :repository_storage, to: :project
ROOT_PATH_PREFIX = '@hashed'.freeze ROOT_PATH_PREFIX = '@hashed'.freeze
...@@ -24,7 +24,7 @@ module Storage ...@@ -24,7 +24,7 @@ module Storage
end end
def ensure_storage_path_exists def ensure_storage_path_exists
gitlab_shell.add_namespace(repository_storage_path, base_dir) gitlab_shell.add_namespace(repository_storage, base_dir)
end end
def rename_repo def rename_repo
......
module Storage module Storage
class LegacyProject class LegacyProject
attr_accessor :project attr_accessor :project
delegate :namespace, :gitlab_shell, :repository_storage_path, to: :project delegate :namespace, :gitlab_shell, :repository_storage, to: :project
def initialize(project) def initialize(project)
@project = project @project = project
...@@ -24,18 +24,18 @@ module Storage ...@@ -24,18 +24,18 @@ module Storage
def ensure_storage_path_exists def ensure_storage_path_exists
return unless namespace return unless namespace
gitlab_shell.add_namespace(repository_storage_path, base_dir) gitlab_shell.add_namespace(repository_storage, base_dir)
end end
def rename_repo def rename_repo
new_full_path = project.build_full_path new_full_path = project.build_full_path
if gitlab_shell.mv_repository(repository_storage_path, project.full_path_was, new_full_path) if gitlab_shell.mv_repository(repository_storage, project.full_path_was, new_full_path)
# If repository moved successfully we need to send update instructions to users. # If repository moved successfully we need to send update instructions to users.
# However we cannot allow rollback since we moved repository # However we cannot allow rollback since we moved repository
# So we basically we mute exceptions in next actions # So we basically we mute exceptions in next actions
begin begin
gitlab_shell.mv_repository(repository_storage_path, "#{project.full_path_was}.wiki", "#{new_full_path}.wiki") gitlab_shell.mv_repository(repository_storage, "#{project.full_path_was}.wiki", "#{new_full_path}.wiki")
return true return true
rescue => e rescue => e
Rails.logger.error "Exception renaming #{project.full_path_was} -> #{new_full_path}: #{e}" Rails.logger.error "Exception renaming #{project.full_path_was} -> #{new_full_path}: #{e}"
......
...@@ -4,6 +4,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -4,6 +4,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include GitlabRoutingHelper include GitlabRoutingHelper
include StorageHelper include StorageHelper
include TreeHelper include TreeHelper
include ChecksCollaboration
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
presents :project presents :project
...@@ -170,9 +171,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -170,9 +171,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def can_current_user_push_to_branch?(branch) def can_current_user_push_to_branch?(branch)
return false unless repository.branch_exists?(branch) user_access(project).can_push_to_branch?(branch)
end
::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch) def can_current_user_push_to_default_branch?
can_current_user_push_to_branch?(default_branch)
end end
def files_anchor_data def files_anchor_data
...@@ -200,7 +203,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -200,7 +203,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def new_file_anchor_data def new_file_anchor_data
if current_user && can_current_user_push_code? if current_user && can_current_user_push_to_default_branch?
OpenStruct.new(enabled: false, OpenStruct.new(enabled: false,
label: _('New file'), label: _('New file'),
link: project_new_blob_path(project, default_branch || 'master'), link: project_new_blob_path(project, default_branch || 'master'),
...@@ -209,7 +212,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -209,7 +212,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def readme_anchor_data def readme_anchor_data
if current_user && can_current_user_push_code? && repository.readme.blank? if current_user && can_current_user_push_to_default_branch? && repository.readme.blank?
OpenStruct.new(enabled: false, OpenStruct.new(enabled: false,
label: _('Add Readme'), label: _('Add Readme'),
link: add_readme_path) link: add_readme_path)
...@@ -221,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -221,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def changelog_anchor_data def changelog_anchor_data
if current_user && can_current_user_push_code? && repository.changelog.blank? if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank?
OpenStruct.new(enabled: false, OpenStruct.new(enabled: false,
label: _('Add Changelog'), label: _('Add Changelog'),
link: add_changelog_path) link: add_changelog_path)
...@@ -233,7 +236,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -233,7 +236,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def license_anchor_data def license_anchor_data
if current_user && can_current_user_push_code? && repository.license_blob.blank? if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank?
OpenStruct.new(enabled: false, OpenStruct.new(enabled: false,
label: _('Add License'), label: _('Add License'),
link: add_license_path) link: add_license_path)
...@@ -245,7 +248,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -245,7 +248,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end end
def contribution_guide_anchor_data def contribution_guide_anchor_data
if current_user && can_current_user_push_code? && repository.contribution_guide.blank? if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank?
OpenStruct.new(enabled: false, OpenStruct.new(enabled: false,
label: _('Add Contribution guide'), label: _('Add Contribution guide'),
link: add_contribution_guide_path) link: add_contribution_guide_path)
......
...@@ -91,7 +91,7 @@ module Projects ...@@ -91,7 +91,7 @@ module Projects
project.run_after_commit do project.run_after_commit do
# self is now project # self is now project
GitlabShellWorker.perform_in(5.minutes, :remove_repository, self.repository_storage_path, new_path) GitlabShellWorker.perform_in(5.minutes, :remove_repository, self.repository_storage, new_path)
end end
else else
false false
...@@ -100,9 +100,9 @@ module Projects ...@@ -100,9 +100,9 @@ module Projects
def mv_repository(from_path, to_path) def mv_repository(from_path, to_path)
# There is a possibility project does not have repository or wiki # There is a possibility project does not have repository or wiki
return true unless gitlab_shell.exists?(project.repository_storage_path, from_path + '.git') return true unless gitlab_shell.exists?(project.repository_storage, from_path + '.git')
gitlab_shell.mv_repository(project.repository_storage_path, from_path, to_path) gitlab_shell.mv_repository(project.repository_storage, from_path, to_path)
end end
def attempt_rollback(project, message) def attempt_rollback(project, message)
......
...@@ -47,8 +47,8 @@ module Projects ...@@ -47,8 +47,8 @@ module Projects
private private
def move_repository(from_name, to_name) def move_repository(from_name, to_name)
from_exists = gitlab_shell.exists?(project.repository_storage_path, "#{from_name}.git") from_exists = gitlab_shell.exists?(project.repository_storage, "#{from_name}.git")
to_exists = gitlab_shell.exists?(project.repository_storage_path, "#{to_name}.git") to_exists = gitlab_shell.exists?(project.repository_storage, "#{to_name}.git")
# If we don't find the repository on either original or target we should log that as it could be an issue if the # If we don't find the repository on either original or target we should log that as it could be an issue if the
# project was not originally empty. # project was not originally empty.
...@@ -60,7 +60,7 @@ module Projects ...@@ -60,7 +60,7 @@ module Projects
return true return true
end end
gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name) gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
end end
def rollback_folder_move def rollback_folder_move
......
...@@ -127,7 +127,7 @@ module Projects ...@@ -127,7 +127,7 @@ module Projects
end end
def move_repo_folder(from_name, to_name) def move_repo_folder(from_name, to_name)
gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name) gitlab_shell.mv_repository(project.repository_storage, from_name, to_name)
end end
def execute_system_hooks def execute_system_hooks
......
...@@ -138,8 +138,10 @@ module QuickActions ...@@ -138,8 +138,10 @@ module QuickActions
'Remove assignee' 'Remove assignee'
end end
end end
explanation do explanation do |users = nil|
"Removes #{'assignee'.pluralize(issuable.assignees.size)} #{issuable.assignees.map(&:to_reference).to_sentence}." assignees = issuable.assignees
assignees &= users if users.present? && issuable.allows_multiple_assignees?
"Removes #{'assignee'.pluralize(assignees.size)} #{assignees.map(&:to_reference).to_sentence}."
end end
params do params do
issuable.allows_multiple_assignees? ? '@user1 @user2' : '' issuable.allows_multiple_assignees? ? '@user1 @user2' : ''
...@@ -268,6 +270,26 @@ module QuickActions ...@@ -268,6 +270,26 @@ module QuickActions
end end
end end
desc 'Copy labels and milestone from other issue or merge request'
explanation do |source_issuable|
"Copy labels and milestone from #{source_issuable.to_reference}."
end
params '#issue | !merge_request'
condition do
issuable.persisted? &&
current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
end
parse_params do |issuable_param|
extract_references(issuable_param, :issue).first ||
extract_references(issuable_param, :merge_request).first
end
command :copy_metadata do |source_issuable|
if source_issuable.present? && source_issuable.project.id == issuable.project.id
@updates[:add_label_ids] = source_issuable.labels.map(&:id)
@updates[:milestone_id] = source_issuable.milestone.id if source_issuable.milestone
end
end
desc 'Add a todo' desc 'Add a todo'
explanation 'Adds a todo.' explanation 'Adds a todo.'
condition do condition do
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
delete_user_url: admin_user_path(user), delete_user_url: admin_user_path(user),
block_user_url: block_admin_user_path(user), block_user_url: block_admin_user_path(user),
username: user.name, username: user.name,
delete_contributions: 'false' }, type: 'button' } delete_contributions: false }, type: 'button' }
= s_('AdminUsers|Delete user') = s_('AdminUsers|Delete user')
%li %li
...@@ -52,5 +52,5 @@ ...@@ -52,5 +52,5 @@
delete_user_url: admin_user_path(user, hard_delete: true), delete_user_url: admin_user_path(user, hard_delete: true),
block_user_url: block_admin_user_path(user), block_user_url: block_admin_user_path(user),
username: user.name, username: user.name,
delete_contributions: 'true' }, type: 'button' } delete_contributions: true }, type: 'button' }
= s_('AdminUsers|Delete user and contributions') = s_('AdminUsers|Delete user and contributions')
...@@ -183,7 +183,7 @@ ...@@ -183,7 +183,7 @@
delete_user_url: admin_user_path(@user), delete_user_url: admin_user_path(@user),
block_user_url: block_admin_user_path(@user), block_user_url: block_admin_user_path(@user),
username: @user.name, username: @user.name,
delete_contributions: 'false' }, type: 'button' } delete_contributions: false }, type: 'button' }
= s_('AdminUsers|Delete user') = s_('AdminUsers|Delete user')
- else - else
- if @user.solo_owned_groups.present? - if @user.solo_owned_groups.present?
...@@ -215,7 +215,7 @@ ...@@ -215,7 +215,7 @@
delete_user_url: admin_user_path(@user, hard_delete: true), delete_user_url: admin_user_path(@user, hard_delete: true),
block_user_url: block_admin_user_path(@user), block_user_url: block_admin_user_path(@user),
username: @user.name, username: @user.name,
delete_contributions: 'true' }, type: 'button' } delete_contributions: true }, type: 'button' }
= s_('AdminUsers|Delete user and contributions') = s_('AdminUsers|Delete user and contributions')
- else - else
%p %p
......
- breadcrumb_title "General Settings" - breadcrumb_title "General Settings"
- @content_class = "limit-container-width" unless fluid_layout
.panel.panel-default.prepend-top-default .panel.panel-default.prepend-top-default
.panel-heading .panel-heading
Group settings Group settings
......
...@@ -58,6 +58,8 @@ ...@@ -58,6 +58,8 @@
touch README.md touch README.md
git add README.md git add README.md
git commit -m "add README" git commit -m "add README"
- if @project.can_current_user_push_to_default_branch?
%span><
git push -u origin master git push -u origin master
%fieldset %fieldset
...@@ -69,6 +71,8 @@ ...@@ -69,6 +71,8 @@
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git add . git add .
git commit -m "Initial commit" git commit -m "Initial commit"
- if @project.can_current_user_push_to_default_branch?
%span><
git push -u origin master git push -u origin master
%fieldset %fieldset
...@@ -78,6 +82,8 @@ ...@@ -78,6 +82,8 @@
cd existing_repo cd existing_repo
git remote rename origin old-origin git remote rename origin old-origin
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- if @project.can_current_user_push_to_default_branch?
%span><
git push -u origin --all git push -u origin --all
git push -u origin --tags git push -u origin --tags
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
- if @namespaces.present? - if @namespaces.present?
.fork-thumbnail-container.js-fork-content .fork-thumbnail-container.js-fork-content
%h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default %h5.prepend-top-0.append-bottom-0.prepend-left-default.append-right-default
Click to fork the project = _("Select a namespace to fork the project")
- @namespaces.each do |namespace| - @namespaces.each do |namespace|
= render 'fork_button', namespace: namespace = render 'fork_button', namespace: namespace
- else - else
......
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
docker login #{Gitlab.config.registry.host_port} docker login #{Gitlab.config.registry.host_port}
%br %br
%p %p
- deploy_token = link_to(_('deploy token'), help_page_path('user/projects/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank') - deploy_token = link_to(_('deploy token'), help_page_path('user/project/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank')
= s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token } = s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
%br %br
%p %p
......
...@@ -32,6 +32,13 @@ ...@@ -32,6 +32,13 @@
required: true, required: true,
title: 'You can choose a descriptive name different from the path.' title: 'You can choose a descriptive name different from the path.'
- if @group.persisted?
.form-group.group-name-holder
= f.label :id, class: 'control-label' do
= _("Group ID")
.col-sm-10
= f.text_field :id, class: 'form-control', readonly: true
.form-group.group-description-holder .form-group.group-description-holder
= f.label :description, class: 'control-label' = f.label :description, class: 'control-label'
.col-sm-10 .col-sm-10
......
...@@ -13,7 +13,9 @@ class RepositoryForkWorker ...@@ -13,7 +13,9 @@ class RepositoryForkWorker
# See https://gitlab.com/gitlab-org/gitaly/issues/1110 # See https://gitlab.com/gitlab-org/gitaly/issues/1110
if args.empty? if args.empty?
source_project = target_project.forked_from_project source_project = target_project.forked_from_project
return target_project.mark_import_as_failed('Source project cannot be found.') unless source_project unless source_project
return target_project.mark_import_as_failed('Source project cannot be found.')
end
fork_repository(target_project, source_project.repository_storage, source_project.disk_path) fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
else else
......
---
title: Reduce queries on merge requests list page for merge requests from forks
merge_request: 18561
author:
type: performance
---
title: Fix tabs container styles to make RSS button clickable
merge_request: 18559
author:
type: fixed
---
title: Correct text and functionality for delete user / delete user and contributions
modal.
merge_request: 18463
author: Marc Schwede
type: fixed
---
title: Fix unassign slash command preview
merge_request: 18447
author:
type: fixed
---
title: Replace "Click" with "Select" to be more inclusive of people with accessibility
requirements
merge_request: 18386
author: Mark Lapierre
type: other
---
title: Add Copy metadata quick action
merge_request: 16473
author: Mateusz Bajorski
type: added
---
title: Align project avatar on small viewports
merge_request: 18513
author: George Tsiolis
type: changed
---
title: Fix errors on pushing to an empty repository
merge_request: 18462
author:
type: fixed
---
title: Improve performance of a service responsible for creating a pipeline
merge_request: 18582
author:
type: performance
---
title: Restore label underline color
merge_request: 18407
author: George Tsiolis
type: fixed
---
title: Show group id in group settings
merge_request: 18482
author: George Tsiolis
type: added
---
title: Show Runner's description on job's page
merge_request: 17321
author:
type: added
require_dependency File.expand_path('../../lib/gitlab', __dir__) # Load Gitlab as soon as possible require_relative '../settings'
# Default settings # Default settings
Settings['ldap'] ||= Settingslogic.new({}) Settings['ldap'] ||= Settingslogic.new({})
......
require_relative '../../lib/gitlab'
deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab') deprecator = ActiveSupport::Deprecation.new('11.0', 'GitLab')
if Gitlab.com? || Rails.env.development? if Gitlab.dev_env_or_com?
ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator) ActiveSupport::Deprecation.deprecate_methods(Gitlab::GitalyClient::StorageSettings, :legacy_disk_path, deprecator: deprecator)
end end
...@@ -33,7 +33,7 @@ webpackConfig.plugins.push( ...@@ -33,7 +33,7 @@ webpackConfig.plugins.push(
}) })
); );
webpackConfig.devtool = 'cheap-inline-source-map'; webpackConfig.devtool = process.env.BABEL_ENV !== 'coverage' && 'cheap-inline-source-map';
// Karma configuration // Karma configuration
module.exports = function(config) { module.exports = function(config) {
......
...@@ -59,17 +59,17 @@ class RemoveDotGitFromGroupNames < ActiveRecord::Migration ...@@ -59,17 +59,17 @@ class RemoveDotGitFromGroupNames < ActiveRecord::Migration
end end
def move_namespace(group_id, path_was, path) def move_namespace(group_id, path_was, path)
repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row| repository_storages = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{group_id}").map do |row|
Gitlab.config.repositories.storages[row['repository_storage']].legacy_disk_path row['repository_storage']
end.compact end.compact
# Move the namespace directory in all storages paths used by member projects # Move the namespace directory in all storages paths used by member projects
repository_storage_paths.each do |repository_storage_path| repository_storages.each do |repository_storage|
# Ensure old directory exists before moving it # Ensure old directory exists before moving it
gitlab_shell.add_namespace(repository_storage_path, path_was) gitlab_shell.add_namespace(repository_storage, path_was)
unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) unless gitlab_shell.mv_namespace(repository_storage, path_was, path)
Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" Rails.logger.error "Exception moving on shard #{repository_storage} from #{path_was} to #{path}"
# if we cannot move namespace directory we should rollback # if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs # db changes in order to prevent out of sync between db and fs
......
...@@ -53,8 +53,8 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration ...@@ -53,8 +53,8 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present? select_all("SELECT id, path FROM routes WHERE path = '#{quote_string(path)}'").present?
end end
def path_exists?(path, repository_storage_path) def path_exists?(shard, repository_storage_path)
repository_storage_path && gitlab_shell.exists?(repository_storage_path, path) repository_storage_path && gitlab_shell.exists?(shard, repository_storage_path)
end end
# Accepts invalid path like test.git and returns test_git or # Accepts invalid path like test.git and returns test_git or
...@@ -70,8 +70,8 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration ...@@ -70,8 +70,8 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
def check_routes(base, counter, path) def check_routes(base, counter, path)
route_exists = route_exists?(path) route_exists = route_exists?(path)
Gitlab.config.repositories.storages.each_value do |storage| Gitlab.config.repositories.storages.each do |shard, storage|
if route_exists || path_exists?(path, storage.legacy_disk_path) if route_exists || path_exists?(shard, storage.legacy_disk_path)
counter += 1 counter += 1
path = "#{base}#{counter}" path = "#{base}#{counter}"
...@@ -83,17 +83,17 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration ...@@ -83,17 +83,17 @@ class RemoveDotGitFromUsernames < ActiveRecord::Migration
end end
def move_namespace(namespace_id, path_was, path) def move_namespace(namespace_id, path_was, path)
repository_storage_paths = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row| repository_storages = select_all("SELECT distinct(repository_storage) FROM projects WHERE namespace_id = #{namespace_id}").map do |row|
Gitlab.config.repositories.storages[row['repository_storage']].legacy_disk_path row['repository_storage']
end.compact end.compact
# Move the namespace directory in all storages paths used by member projects # Move the namespace directory in all storages used by member projects
repository_storage_paths.each do |repository_storage_path| repository_storages.each do |repository_storage|
# Ensure old directory exists before moving it # Ensure old directory exists before moving it
gitlab_shell.add_namespace(repository_storage_path, path_was) gitlab_shell.add_namespace(repository_storage, path_was)
unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path) unless gitlab_shell.mv_namespace(repository_storage, path_was, path)
Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}" Rails.logger.error "Exception moving on shard #{repository_storage} from #{path_was} to #{path}"
# if we cannot move namespace directory we should rollback # if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs # db changes in order to prevent out of sync between db and fs
......
class AssureCommitsCountForMergeRequestDiff < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
class MergeRequestDiff < ActiveRecord::Base
self.table_name = 'merge_request_diffs'
include ::EachBatch
end
def up
Gitlab::BackgroundMigration.steal('AddMergeRequestDiffCommitsCount')
MergeRequestDiff.where(commits_count: nil).each_batch(of: 50) do |batch|
range = batch.pluck('MIN(id)', 'MAX(id)').first
Gitlab::BackgroundMigration::AddMergeRequestDiffCommitsCount.new.perform(*range)
end
end
def down
# noop
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180418053107) do ActiveRecord::Schema.define(version: 20180425131009) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
......
...@@ -13,6 +13,7 @@ following locations: ...@@ -13,6 +13,7 @@ following locations:
- [Broadcast Messages](broadcast_messages.md) - [Broadcast Messages](broadcast_messages.md)
- [Project-level Variables](project_level_variables.md) - [Project-level Variables](project_level_variables.md)
- [Group-level Variables](group_level_variables.md) - [Group-level Variables](group_level_variables.md)
- [Code Snippets](snippets.md)
- [Commits](commits.md) - [Commits](commits.md)
- [Custom Attributes](custom_attributes.md) - [Custom Attributes](custom_attributes.md)
- [Deployments](deployments.md) - [Deployments](deployments.md)
...@@ -85,6 +86,29 @@ have been resolved to our satisfaction by the relicensing of the reference ...@@ -85,6 +86,29 @@ have been resolved to our satisfaction by the relicensing of the reference
implementations under MIT, and the use of the OWF license for the GraphQL implementations under MIT, and the use of the OWF license for the GraphQL
specification. specification.
## Compatibility Guidelines
The HTTP API is versioned using a single number, the current one being 4. This
number symbolises the same as the major version number as described by
[SemVer](https://semver.org/). This mean that backward incompatible changes
will require this version number to change. However, the minor version is
not explicit. This allows for a stable API endpoint, but also means new
features can be added to the API in the same version number.
New features and bug fixes are released in tandem with a new GitLab, and apart
from incidental patch and security releases, are released on the 22nd each
month. Backward incompatible changes (e.g. endpoints removal, parameters
removal etc.), as well as removal of entire API versions are done in tandem
with a major point release of GitLab itself. All deprecations and changes
between two versions should be listed in the documentation. For the changes
between v3 and v4; please read the [v3 to v4 documentation](v3_to_v4.md)
#### Current status
Currently two API versions are available, v3 and v4. v3 is deprecated and
will soon be removed. Deletion is scheduled for
[GitLab 11.0](https://gitlab.com/gitlab-org/gitlab-ce/issues/36819).
## Basic usage ## Basic usage
API requests should be prefixed with `api` and the API version. The API version API requests should be prefixed with `api` and the API version. The API version
......
...@@ -298,6 +298,28 @@ Mentioned briefly earlier, but the following things of Runners can be exploited. ...@@ -298,6 +298,28 @@ Mentioned briefly earlier, but the following things of Runners can be exploited.
We're always looking for contributions that can mitigate these We're always looking for contributions that can mitigate these
[Security Considerations](https://docs.gitlab.com/runner/security/). [Security Considerations](https://docs.gitlab.com/runner/security/).
### Resetting the registration token for a Project
If you think that registration token for a Project was revealed, you should
reset them. It's recommended because such token can be used to register another
Runner to thi Project. It may be next used to obtain the values of secret
variables or clone the project code, that normally may be unavailable for the
attacker.
To reset the token:
1. Go to **Settings > CI/CD** for a specified Project
1. Expand the **General pipelines settings** section
1. Find the **Runner token** form field and click the **Reveal value** button
1. Delete the value and save the form
1. After the page is refreshed, expand the **Runners settings** section
and check the registration token - it should be changed
From now on the old token is not valid anymore and will not allow to register
a new Runner to the project. If you are using any tools to provision and
register new Runners, you should now update the token that is used to the
new value.
## Determining the IP address of a Runner ## Determining the IP address of a Runner
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17286) in GitLab 10.6. > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17286) in GitLab 10.6.
......
...@@ -61,7 +61,7 @@ future GitLab releases.** ...@@ -61,7 +61,7 @@ future GitLab releases.**
| **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) | | **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) |
| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally | | **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] | | **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
| **CI_PIPELINE_SOURCE** | 10.0 | all | The source for this pipeline, one of: push, web, trigger, schedule, api, external. Pipelines created before 9.5 will have unknown as source | | **CI_PIPELINE_SOURCE** | 10.0 | all | Indicates how the pipeline was triggered. Possible options are: `push`, `web`, `trigger`, `schedule`, `api`, and `pipeline`. For pipelines created before GitLab 9.5, this will show as `unknown` |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run | | **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the job is run |
| **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally | | **CI_PROJECT_ID** | all | all | The unique id of the current project that GitLab CI uses internally |
| **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) | | **CI_PROJECT_NAME** | 8.10 | 0.5 | The project name that is currently being built (actually it is project folder name) |
......
...@@ -126,13 +126,51 @@ it('tests a promise rejection', (done) => { ...@@ -126,13 +126,51 @@ it('tests a promise rejection', (done) => {
}); });
``` ```
#### Stubbing #### Stubbing and Mocking
For unit tests, you should stub methods that are unrelated to the current unit you are testing. Jasmine provides useful helpers `spyOn`, `spyOnProperty`, `jasmine.createSpy`,
If you need to use a prototype method, instantiate an instance of the class and call it there instead of mocking the instance completely. and `jasmine.createSpyObject` to facilitate replacing methods with dummy
placeholders, and recalling when they are called and the arguments that are
passed to them. These tools should be used liberally, to test for expected
behavior, to mock responses, and to block unwanted side effects (such as a
method that would generate a network request or alter `window.location`). The
documentation for these methods can be found in the [jasmine introduction page](https://jasmine.github.io/2.0/introduction.html#section-Spies).
For integration tests, you should stub methods that will effect the stability of the test if they Sometimes you may need to spy on a method that is directly imported by another
execute their original behaviour. i.e. Network requests. module. GitLab has a custom `spyOnDependency` method which utilizes
[babel-plugin-rewire](https://github.com/speedskater/babel-plugin-rewire) to
achieve this. It can be used like so:
```javascript
// my_module.js
import { visitUrl } from '~/lib/utils/url_utility';
export default function doSomething() {
visitUrl('/foo/bar');
}
// my_module_spec.js
import doSomething from '~/my_module';
describe('my_module', () => {
it('does something', () => {
const visitUrl = spyOnDependency(doSomething, 'visitUrl');
doSomething();
expect(visitUrl).toHaveBeenCalledWith('/foo/bar');
});
});
```
Unlike `spyOn`, `spyOnDependency` expects its first parameter to be the default
export of a module who's import you want to stub, rather than an object which
contains a method you wish to stub (if the module does not have a default
export, one is be generated by the babel plugin). The second parameter is the
name of the import you wish to change. The result of the function is a Spy
object which can be treated like any other jasmine spy object.
Further documentation on the babel rewire pluign API can be found on
[its repository Readme doc](https://github.com/speedskater/babel-plugin-rewire#babel-plugin-rewire).
### Vue.js unit tests ### Vue.js unit tests
......
This diff is collapsed.
...@@ -10,10 +10,9 @@ should be deployed, upgraded, and configured. ...@@ -10,10 +10,9 @@ should be deployed, upgraded, and configured.
## Chart Overview ## Chart Overview
* **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today, suited for small deployments. The chart is in beta and will be deprecated by the [cloud native GitLab chart](#cloud-native-gitlab-chart). * **[GitLab-Omnibus](gitlab_omnibus.md)**: The best way to run GitLab on Kubernetes today, suited for small deployments. The chart is in beta and will be deprecated by the [cloud native GitLab chart](#cloud-native-gitlab-chart).
* **[Cloud Native GitLab Chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md)**: The next generation GitLab chart, currently in alpha. Will support large deployments with horizontal scaling of individual GitLab components. * **[Cloud Native GitLab Chart](https://gitlab.com/charts/gitlab/blob/master/README.md)**: The next generation GitLab chart, currently in alpha. Will support large deployments with horizontal scaling of individual GitLab components.
* Other Charts * Other Charts
* [GitLab Runner Chart](gitlab_runner_chart.md): For deploying just the GitLab Runner. * [GitLab Runner Chart](gitlab_runner_chart.md): For deploying just the GitLab Runner.
* [Advanced GitLab Installation](gitlab_chart.md): Deprecated, being replaced by the [cloud native GitLab chart](#cloud-native-gitlab-chart). Provides additional deployment options, but provides less functionality out-of-the-box.
* [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab chart. * [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab chart.
## GitLab-Omnibus Chart (Recommended) ## GitLab-Omnibus Chart (Recommended)
...@@ -27,7 +26,7 @@ Learn more about the [gitlab-omnibus chart](gitlab_omnibus.md). ...@@ -27,7 +26,7 @@ Learn more about the [gitlab-omnibus chart](gitlab_omnibus.md).
## Cloud Native GitLab Chart ## Cloud Native GitLab Chart
GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current chart](#gitlab-omnibus-chart-recommended). GitLab is working towards building a [cloud native GitLab chart](https://gitlab.com/charts/gitlab/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current chart](#gitlab-omnibus-chart-recommended).
By offering individual containers and charts, we will be able to provide a number of benefits: By offering individual containers and charts, we will be able to provide a number of benefits:
* Easier horizontal scaling of each service, * Easier horizontal scaling of each service,
...@@ -37,7 +36,7 @@ By offering individual containers and charts, we will be able to provide a numbe ...@@ -37,7 +36,7 @@ By offering individual containers and charts, we will be able to provide a numbe
Presently this chart is available in alpha for testing, and not recommended for production use. Presently this chart is available in alpha for testing, and not recommended for production use.
Learn more about the [cloud native GitLab chart here ](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) and [here [Video]](https://youtu.be/Z6jWR8Z8dv8). Learn more about the [cloud native GitLab chart here ](https://gitlab.com/charts/gitlab/blob/master/README.md) and [here [Video]](https://youtu.be/Z6jWR8Z8dv8).
## Other Charts ## Other Charts
......
...@@ -69,7 +69,7 @@ GitHub will generate an application ID and secret key for you to use. ...@@ -69,7 +69,7 @@ GitHub will generate an application ID and secret key for you to use.
"name" => "github", "name" => "github",
"app_id" => "YOUR_APP_ID", "app_id" => "YOUR_APP_ID",
"app_secret" => "YOUR_APP_SECRET", "app_secret" => "YOUR_APP_SECRET",
"url" => "https://github.com/", "url" => "https://github.example.com/",
"args" => { "scope" => "user:email" } "args" => { "scope" => "user:email" }
} }
] ]
...@@ -125,7 +125,7 @@ For omnibus package: ...@@ -125,7 +125,7 @@ For omnibus package:
"name" => "github", "name" => "github",
"app_id" => "YOUR_APP_ID", "app_id" => "YOUR_APP_ID",
"app_secret" => "YOUR_APP_SECRET", "app_secret" => "YOUR_APP_SECRET",
"url" => "https://github.com/", "url" => "https://github.example.com/",
"verify_ssl" => false, "verify_ssl" => false,
"args" => { "scope" => "user:email" } "args" => { "scope" => "user:email" }
} }
......
...@@ -10,8 +10,30 @@ applications. ...@@ -10,8 +10,30 @@ applications.
## Overview ## Overview
With Auto DevOps, the software development process becomes easier to set up With Auto DevOps, the software development process becomes easier to set up
as every project can have a complete workflow from build to deploy and monitoring, as every project can have a complete workflow from verification to monitoring
with minimal to zero configuration. without needing to configure anything. Just push your code and GitLab takes
care of everything else. This makes it easier to start new projects and brings
consistency to how applications are set up throughout a company.
## Comparison to application platforms and PaaS
Auto DevOps provides functionality described by others as an application
platform or as a Platform as a Service (PaaS). It takes inspiration from the
innovative work done by [Heroku](https://www.heroku.com/) and goes beyond it
in a couple of ways:
1. Auto DevOps works with any Kubernetes cluster, you're not limited to running
on GitLab's infrastructure (note that many features also work without Kubernetes).
1. There is no additional cost (no markup on the infrastructure costs), and you
can use a self-hosted Kubernetes cluster or Containers as a Service on any
public cloud (for example [Google Kubernetes Engine](https://cloud.google.com/kubernetes-engine/)).
1. Auto DevOps has more features including security testing, performance testing,
and code quality testing.
1. It offers an incremental graduation path. If you need advanced customizations
you can start modifying the templates without having to start over on a
completely different platform.
## Features
Comprised of a set of stages, Auto DevOps brings these best practices to your Comprised of a set of stages, Auto DevOps brings these best practices to your
project in an easy and automatic way: project in an easy and automatic way:
......
doc/user/group/img/groups.png

198 KB | W: | H:

doc/user/group/img/groups.png

244 KB | W: | H:

doc/user/group/img/groups.png
doc/user/group/img/groups.png
doc/user/group/img/groups.png
doc/user/group/img/groups.png
  • 2-up
  • Swipe
  • Onion skin
...@@ -41,3 +41,4 @@ do. ...@@ -41,3 +41,4 @@ do.
| `/move path/to/project` | Moves issue to another project | | `/move path/to/project` | Moves issue to another project |
| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` | | `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` |
| `/shrug` | Append the comment with `¯\_(ツ)_/¯` | | `/shrug` | Append the comment with `¯\_(ツ)_/¯` |
| <code>/copy_metadata #issue &#124; !merge_request</code> | Copy labels and milestone from other issue or merge request |
...@@ -31,7 +31,8 @@ with all their related data and be moved into a new GitLab instance. ...@@ -31,7 +31,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version | | GitLab version | Import/Export version |
| ---------------- | --------------------- | | ---------------- | --------------------- |
| 10.4 to current | 0.2.2 | | 10.8 to current | 0.2.3 |
| 10.4 | 0.2.2 |
| 10.3 | 0.2.1 | | 10.3 | 0.2.1 |
| 10.0 | 0.2.0 | | 10.0 | 0.2.0 |
| 9.4.0 | 0.1.8 | | 9.4.0 | 0.1.8 |
......
...@@ -244,3 +244,20 @@ GitLab checks files to detect LFS pointers on push. If LFS pointers are detected ...@@ -244,3 +244,20 @@ GitLab checks files to detect LFS pointers on push. If LFS pointers are detected
Verify that LFS in installed locally and consider a manual push with `git lfs push --all`. Verify that LFS in installed locally and consider a manual push with `git lfs push --all`.
If you are storing LFS files outside of GitLab you can disable LFS on the project by settting `lfs_enabled: false` with the [projects api](../../api/projects.md#edit-project). If you are storing LFS files outside of GitLab you can disable LFS on the project by settting `lfs_enabled: false` with the [projects api](../../api/projects.md#edit-project).
### Hosting LFS objects externally
It is possible to host LFS objects externally by setting a custom LFS url with `git config -f .lfsconfig lfs.url https://example.com/<project>.git/info/lfs`.
Because GitLab verifies the existence of objects referenced by LFS pointers, push will fail when LFS is enabled for the project.
LFS can be disabled for a project by Owners and Masters using the [Project API](../../api/projects.md#edit-project).
```bash
curl --request PUT \
--url https://example.com/api/v4/projects/<PROJECT_ID> \
--header 'Private-Token: <YOUR_PRIVATE_TOKEN>' \
--data 'lfs_enabled=false'
```
Note, `<PROJECT_ID>` can also be substituted with a [namespaced path](../../api/README.md#namespaced-path-encoding).
require_dependency 'settings'
require_dependency 'gitlab/popen' require_dependency 'gitlab/popen'
module Gitlab module Gitlab
...@@ -30,6 +29,6 @@ module Gitlab ...@@ -30,6 +29,6 @@ module Gitlab
end end
def self.dev_env_or_com? def self.dev_env_or_com?
Rails.env.test? || Rails.env.development? || org? || com? Rails.env.development? || org? || com?
end end
end end
...@@ -75,10 +75,11 @@ module Gitlab ...@@ -75,10 +75,11 @@ module Gitlab
end end
def mv_repo(project) def mv_repo(project)
FileUtils.mv(repo_path, File.join(project.repository_storage_path, project.disk_path + '.git')) storage_path = storage_path_for_shard(project.repository_storage)
FileUtils.mv(repo_path, project.repository.path_to_repo)
if bare_repo.wiki_exists? if bare_repo.wiki_exists?
FileUtils.mv(wiki_path, File.join(project.repository_storage_path, project.disk_path + '.wiki.git')) FileUtils.mv(wiki_path, File.join(storage_path, project.disk_path + '.wiki.git'))
end end
true true
...@@ -88,6 +89,10 @@ module Gitlab ...@@ -88,6 +89,10 @@ module Gitlab
false false
end end
def storage_path_for_shard(shard)
Gitlab.config.repositories.storages[shard].legacy_disk_path
end
def find_or_create_groups def find_or_create_groups
return nil unless group_path.present? return nil unless group_path.present?
......
...@@ -14,14 +14,10 @@ module Gitlab ...@@ -14,14 +14,10 @@ module Gitlab
@command.seeds_block&.call(pipeline) @command.seeds_block&.call(pipeline)
## ##
# Populate pipeline with all stages and builds from pipeline seeds. # Populate pipeline with all stages, and stages with builds.
# #
pipeline.stage_seeds.each do |stage| pipeline.stage_seeds.each do |stage|
pipeline.stages << stage.to_resource pipeline.stages << stage.to_resource
stage.seeds.each do |build|
pipeline.builds << build.to_resource
end
end end
if pipeline.stages.none? if pipeline.stages.none?
......
...@@ -62,21 +62,20 @@ module Gitlab ...@@ -62,21 +62,20 @@ module Gitlab
end end
def move_repositories(namespace, old_full_path, new_full_path) def move_repositories(namespace, old_full_path, new_full_path)
repo_paths_for_namespace(namespace).each do |repository_storage_path| repo_shards_for_namespace(namespace).each do |repository_storage|
# Ensure old directory exists before moving it # Ensure old directory exists before moving it
gitlab_shell.add_namespace(repository_storage_path, old_full_path) gitlab_shell.add_namespace(repository_storage, old_full_path)
unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path) unless gitlab_shell.mv_namespace(repository_storage, old_full_path, new_full_path)
message = "Exception moving path #{repository_storage_path} \ message = "Exception moving on shard #{repository_storage} from #{old_full_path} to #{new_full_path}"
from #{old_full_path} to #{new_full_path}"
Rails.logger.error message Rails.logger.error message
end end
end end
end end
def repo_paths_for_namespace(namespace) def repo_shards_for_namespace(namespace)
projects_for_namespace(namespace).distinct.select(:repository_storage) projects_for_namespace(namespace).distinct.select(:repository_storage)
.map(&:repository_storage_path) .map(&:repository_storage)
end end
def projects_for_namespace(namespace) def projects_for_namespace(namespace)
......
...@@ -51,7 +51,7 @@ module Gitlab ...@@ -51,7 +51,7 @@ module Gitlab
end end
def move_repository(project, old_path, new_path) def move_repository(project, old_path, new_path)
unless gitlab_shell.mv_repository(project.repository_storage_path, unless gitlab_shell.mv_repository(project.repository_storage,
old_path, old_path,
new_path) new_path)
Rails.logger.error "Error moving #{old_path} to #{new_path}" Rails.logger.error "Error moving #{old_path} to #{new_path}"
......
...@@ -3,7 +3,7 @@ module Gitlab ...@@ -3,7 +3,7 @@ module Gitlab
extend self extend self
# For every version update, the version history in import_export.md has to be kept up to date. # For every version update, the version history in import_export.md has to be kept up to date.
VERSION = '0.2.2'.freeze VERSION = '0.2.3'.freeze
FILENAME_LIMIT = 50 FILENAME_LIMIT = 50
def export_path(relative_path:) def export_path(relative_path:)
......
...@@ -65,11 +65,11 @@ module Gitlab ...@@ -65,11 +65,11 @@ module Gitlab
# Init new repository # Init new repository
# #
# storage - project's storage name # storage - the shard key
# name - project disk path # name - project disk path
# #
# Ex. # Ex.
# create_repository("/path/to/storage", "gitlab/gitlab-ci") # create_repository("default", "gitlab/gitlab-ci")
# #
def create_repository(storage, name) def create_repository(storage, name)
relative_path = name.dup relative_path = name.dup
...@@ -291,13 +291,13 @@ module Gitlab ...@@ -291,13 +291,13 @@ module Gitlab
# Add empty directory for storing repositories # Add empty directory for storing repositories
# #
# Ex. # Ex.
# add_namespace("/path/to/storage", "gitlab") # add_namespace("default", "gitlab")
# #
def add_namespace(storage, name) def add_namespace(storage, name)
Gitlab::GitalyClient.migrate(:add_namespace, Gitlab::GitalyClient.migrate(:add_namespace,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled| status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled if enabled
gitaly_namespace_client(storage).add(name) Gitlab::GitalyClient::NamespaceService.new(storage).add(name)
else else
path = full_path(storage, name) path = full_path(storage, name)
FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name) FileUtils.mkdir_p(path, mode: 0770) unless exists?(storage, name)
...@@ -313,13 +313,13 @@ module Gitlab ...@@ -313,13 +313,13 @@ module Gitlab
# Every repository inside this directory will be removed too # Every repository inside this directory will be removed too
# #
# Ex. # Ex.
# rm_namespace("/path/to/storage", "gitlab") # rm_namespace("default", "gitlab")
# #
def rm_namespace(storage, name) def rm_namespace(storage, name)
Gitlab::GitalyClient.migrate(:remove_namespace, Gitlab::GitalyClient.migrate(:remove_namespace,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled| status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled if enabled
gitaly_namespace_client(storage).remove(name) Gitlab::GitalyClient::NamespaceService.new(storage).remove(name)
else else
FileUtils.rm_r(full_path(storage, name), force: true) FileUtils.rm_r(full_path(storage, name), force: true)
end end
...@@ -338,7 +338,8 @@ module Gitlab ...@@ -338,7 +338,8 @@ module Gitlab
Gitlab::GitalyClient.migrate(:rename_namespace, Gitlab::GitalyClient.migrate(:rename_namespace,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled| status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled if enabled
gitaly_namespace_client(storage).rename(old_name, new_name) Gitlab::GitalyClient::NamespaceService.new(storage)
.rename(old_name, new_name)
else else
break false if exists?(storage, new_name) || !exists?(storage, old_name) break false if exists?(storage, new_name) || !exists?(storage, old_name)
...@@ -374,7 +375,8 @@ module Gitlab ...@@ -374,7 +375,8 @@ module Gitlab
Gitlab::GitalyClient.migrate(:namespace_exists, Gitlab::GitalyClient.migrate(:namespace_exists,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled| status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |enabled|
if enabled if enabled
gitaly_namespace_client(storage).exists?(dir_name) Gitlab::GitalyClient::NamespaceService.new(storage)
.exists?(dir_name)
else else
File.exist?(full_path(storage, dir_name)) File.exist?(full_path(storage, dir_name))
end end
...@@ -398,7 +400,7 @@ module Gitlab ...@@ -398,7 +400,7 @@ module Gitlab
def full_path(storage, dir_name) def full_path(storage, dir_name)
raise ArgumentError.new("Directory name can't be blank") if dir_name.blank? raise ArgumentError.new("Directory name can't be blank") if dir_name.blank?
File.join(storage, dir_name) File.join(Gitlab.config.repositories.storages[storage].legacy_disk_path, dir_name)
end end
def gitlab_shell_projects_path def gitlab_shell_projects_path
...@@ -475,14 +477,6 @@ module Gitlab ...@@ -475,14 +477,6 @@ module Gitlab
Bundler.with_original_env { Popen.popen(cmd, nil, vars) } Bundler.with_original_env { Popen.popen(cmd, nil, vars) }
end end
def gitaly_namespace_client(storage_path)
storage, _value = Gitlab.config.repositories.storages.find do |storage, value|
value.legacy_disk_path == storage_path
end
Gitlab::GitalyClient::NamespaceService.new(storage)
end
def git_timeout def git_timeout
Gitlab.config.gitlab_shell.git_timeout Gitlab.config.gitlab_shell.git_timeout
end end
......
...@@ -63,10 +63,12 @@ module Gitlab ...@@ -63,10 +63,12 @@ module Gitlab
request_cache def can_push_to_branch?(ref) request_cache def can_push_to_branch?(ref)
return false unless can_access_git? return false unless can_access_git?
return false unless user.can?(:push_code, project) || project.branch_allows_maintainer_push?(user, ref) return false unless project
return false if !user.can?(:push_code, project) && !project.branch_allows_maintainer_push?(user, ref)
if protected?(ProtectedBranch, project, ref) if protected?(ProtectedBranch, project, ref)
project.user_can_push_to_empty_repo?(user) || protected_branch_accessible_to?(ref, action: :push) protected_branch_accessible_to?(ref, action: :push)
else else
true true
end end
...@@ -101,6 +103,7 @@ module Gitlab ...@@ -101,6 +103,7 @@ module Gitlab
def protected_branch_accessible_to?(ref, action:) def protected_branch_accessible_to?(ref, action:)
ProtectedBranch.protected_ref_accessible_to?( ProtectedBranch.protected_ref_accessible_to?(
ref, user, ref, user,
project: project,
action: action, action: action,
protected_refs: project.protected_branches) protected_refs: project.protected_branches)
end end
...@@ -108,6 +111,7 @@ module Gitlab ...@@ -108,6 +111,7 @@ module Gitlab
def protected_tag_accessible_to?(ref, action:) def protected_tag_accessible_to?(ref, action:)
ProtectedTag.protected_ref_accessible_to?( ProtectedTag.protected_ref_accessible_to?(
ref, user, ref, user,
project: project,
action: action, action: action,
protected_refs: project.protected_tags) protected_refs: project.protected_tags)
end end
......
...@@ -427,10 +427,7 @@ namespace :gitlab do ...@@ -427,10 +427,7 @@ namespace :gitlab do
user = User.find_by(username: username) user = User.find_by(username: username)
if user if user
repo_dirs = user.authorized_projects.map do |p| repo_dirs = user.authorized_projects.map do |p|
File.join( p.repository.path_to_repo
p.repository_storage_path,
"#{p.disk_path}.git"
)
end end
repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) } repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
......
...@@ -10,9 +10,8 @@ namespace :gitlab do ...@@ -10,9 +10,8 @@ namespace :gitlab do
end end
scope.find_each do |project| scope.find_each do |project|
base = File.join(project.repository_storage_path, project.disk_path) puts project.repository.path_to_repo
puts base + '.git' puts project.wiki.repository.path_to_repo
puts base + '.wiki.git'
end end
end end
end end
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-04-17 11:44+0200\n" "POT-Creation-Date: 2018-04-24 13:19+0000\n"
"PO-Revision-Date: 2018-04-17 11:44+0200\n" "PO-Revision-Date: 2018-04-24 13:19+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -3136,6 +3136,9 @@ msgstr "" ...@@ -3136,6 +3136,9 @@ msgstr ""
msgid "Select Archive Format" msgid "Select Archive Format"
msgstr "" msgstr ""
msgid "Select a namespace to fork the project"
msgstr ""
msgid "Select a timezone" msgid "Select a timezone"
msgstr "" msgstr ""
......
...@@ -5,9 +5,9 @@ ...@@ -5,9 +5,9 @@
"eslint": "eslint --max-warnings 0 --ext .js,.vue .", "eslint": "eslint --max-warnings 0 --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .", "eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .", "eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .",
"karma": "karma start --single-run true config/karma.config.js", "karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
"karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js", "karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js",
"karma-start": "karma start config/karma.config.js", "karma-start": "BABEL_ENV=karma karma start config/karma.config.js",
"prettier-staged": "node ./scripts/frontend/prettier.js", "prettier-staged": "node ./scripts/frontend/prettier.js",
"prettier-staged-save": "node ./scripts/frontend/prettier.js save", "prettier-staged-save": "node ./scripts/frontend/prettier.js save",
"prettier-all": "node ./scripts/frontend/prettier.js check-all", "prettier-all": "node ./scripts/frontend/prettier.js check-all",
...@@ -83,11 +83,11 @@ ...@@ -83,11 +83,11 @@
"underscore": "^1.8.3", "underscore": "^1.8.3",
"url-loader": "^0.6.2", "url-loader": "^0.6.2",
"visibilityjs": "^1.2.4", "visibilityjs": "^1.2.4",
"vue": "^2.5.13", "vue": "^2.5.16",
"vue-loader": "^14.1.1", "vue-loader": "^14.1.1",
"vue-resource": "^1.3.5", "vue-resource": "^1.5.0",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.13", "vue-template-compiler": "^2.5.16",
"vue-virtual-scroll-list": "^1.2.5", "vue-virtual-scroll-list": "^1.2.5",
"vuex": "^3.0.1", "vuex": "^3.0.1",
"webpack": "^3.11.0", "webpack": "^3.11.0",
...@@ -99,6 +99,9 @@ ...@@ -99,6 +99,9 @@
"axios-mock-adapter": "^1.10.0", "axios-mock-adapter": "^1.10.0",
"babel-eslint": "^8.0.2", "babel-eslint": "^8.0.2",
"babel-plugin-istanbul": "^4.1.5", "babel-plugin-istanbul": "^4.1.5",
"babel-plugin-rewire": "^1.1.0",
"babel-template": "^6.26.0",
"babel-types": "^6.26.0",
"commander": "^2.15.1", "commander": "^2.15.1",
"eslint": "^3.18.0", "eslint": "^3.18.0",
"eslint-config-airbnb-base": "^10.0.1", "eslint-config-airbnb-base": "^10.0.1",
......
...@@ -125,7 +125,7 @@ describe ProfilesController, :request_store do ...@@ -125,7 +125,7 @@ describe ProfilesController, :request_store do
user.reload user.reload
expect(response.status).to eq(302) expect(response.status).to eq(302)
expect(gitlab_shell.exists?(project.repository_storage_path, "#{new_username}/#{project.path}.git")).to be_truthy expect(gitlab_shell.exists?(project.repository_storage, "#{new_username}/#{project.path}.git")).to be_truthy
end end
end end
...@@ -143,7 +143,7 @@ describe ProfilesController, :request_store do ...@@ -143,7 +143,7 @@ describe ProfilesController, :request_store do
user.reload user.reload
expect(response.status).to eq(302) expect(response.status).to eq(302)
expect(gitlab_shell.exists?(project.repository_storage_path, "#{project.disk_path}.git")).to be_truthy expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_truthy
expect(before_disk_path).to eq(project.disk_path) expect(before_disk_path).to eq(project.disk_path)
end end
end end
......
...@@ -147,7 +147,15 @@ FactoryBot.define do ...@@ -147,7 +147,15 @@ FactoryBot.define do
# We delete hooks so that gitlab-shell will not try to authenticate with # We delete hooks so that gitlab-shell will not try to authenticate with
# an API that isn't running # an API that isn't running
FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.disk_path}.git", 'hooks')) project.gitlab_shell.rm_directory(project.repository_storage,
File.join("#{project.disk_path}.git", 'hooks'))
end
end
trait :stubbed_repository do
after(:build) do |project|
allow(project).to receive(:empty_repo?).and_return(false)
allow(project.repository).to receive(:empty?).and_return(false)
end end
end end
...@@ -165,7 +173,8 @@ FactoryBot.define do ...@@ -165,7 +173,8 @@ FactoryBot.define do
after(:create) do |project| after(:create) do |project|
raise "Failed to create repository!" unless project.create_repository raise "Failed to create repository!" unless project.create_repository
FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.disk_path}.git", 'refs')) project.gitlab_shell.rm_directory(project.repository_storage,
File.join("#{project.disk_path}.git", 'refs'))
end end
end end
......
...@@ -9,7 +9,8 @@ unless Object.respond_to?(:require_dependency) ...@@ -9,7 +9,8 @@ unless Object.respond_to?(:require_dependency)
end end
end end
# Defines Gitlab and Gitlab.config which are at the center of the app # Defines Settings and Gitlab.config which are at the center of the app
require_relative '../config/settings'
require_relative '../lib/gitlab' unless defined?(Gitlab.config) require_relative '../lib/gitlab' unless defined?(Gitlab.config)
require_relative 'support/rspec' require_relative 'support/rspec'
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