Commit 37b9a441 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into 'backstage/gb/migrate-pipeline-stages-index'

# Conflicts:
#   db/schema.rb
parents df6a2d42 5beb4dde
{ {
"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"
] ]
} }
} }
......
...@@ -483,10 +483,11 @@ GEM ...@@ -483,10 +483,11 @@ GEM
logging (2.2.2) logging (2.2.2)
little-plugger (~> 1.1) little-plugger (~> 1.1)
multi_json (~> 1.10) multi_json (~> 1.10)
lograge (0.5.1) lograge (0.10.0)
actionpack (>= 4, < 5.2) actionpack (>= 4)
activesupport (>= 4, < 5.2) activesupport (>= 4)
railties (>= 4, < 5.2) railties (>= 4)
request_store (~> 1.0)
loofah (2.2.2) loofah (2.2.2)
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
......
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import VirtualList from 'vue-virtual-scroll-list';
import Item from './item.vue';
import router from '../../ide_router';
import {
MAX_FILE_FINDER_RESULTS,
FILE_FINDER_ROW_HEIGHT,
FILE_FINDER_EMPTY_ROW_HEIGHT,
} from '../../constants';
import {
UP_KEY_CODE,
DOWN_KEY_CODE,
ENTER_KEY_CODE,
ESC_KEY_CODE,
} from '../../../lib/utils/keycodes';
export default {
components: {
Item,
VirtualList,
},
data() {
return {
focusedIndex: 0,
searchText: '',
mouseOver: false,
cancelMouseOver: false,
};
},
computed: {
...mapGetters(['allBlobs']),
...mapState(['fileFindVisible', 'loading']),
filteredBlobs() {
const searchText = this.searchText.trim();
if (searchText === '') {
return this.allBlobs.slice(0, MAX_FILE_FINDER_RESULTS);
}
return fuzzaldrinPlus
.filter(this.allBlobs, searchText, {
key: 'path',
maxResults: MAX_FILE_FINDER_RESULTS,
})
.sort((a, b) => b.lastOpenedAt - a.lastOpenedAt);
},
filteredBlobsLength() {
return this.filteredBlobs.length;
},
listShowCount() {
return this.filteredBlobsLength ? Math.min(this.filteredBlobsLength, 5) : 1;
},
listHeight() {
return this.filteredBlobsLength ? FILE_FINDER_ROW_HEIGHT : FILE_FINDER_EMPTY_ROW_HEIGHT;
},
showClearInputButton() {
return this.searchText.trim() !== '';
},
},
watch: {
fileFindVisible() {
this.$nextTick(() => {
if (!this.fileFindVisible) {
this.searchText = '';
} else {
this.focusedIndex = 0;
if (this.$refs.searchInput) {
this.$refs.searchInput.focus();
}
}
});
},
searchText() {
this.focusedIndex = 0;
},
focusedIndex() {
if (!this.mouseOver) {
this.$nextTick(() => {
const el = this.$refs.virtualScrollList.$el;
const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
if (this.focusedIndex === 0) {
// if index is the first index, scroll straight to start
el.scrollTop = 0;
} else if (this.focusedIndex === this.filteredBlobsLength - 1) {
// if index is the last index, scroll to the end
el.scrollTop = this.filteredBlobsLength * FILE_FINDER_ROW_HEIGHT;
} else if (scrollTop >= bottom + el.scrollTop) {
// if element is off the bottom of the scroll list, scroll down one item
el.scrollTop = scrollTop - bottom + FILE_FINDER_ROW_HEIGHT;
} else if (scrollTop < el.scrollTop) {
// if element is off the top of the scroll list, scroll up one item
el.scrollTop = scrollTop;
}
});
}
},
},
methods: {
...mapActions(['toggleFileFinder']),
clearSearchInput() {
this.searchText = '';
this.$nextTick(() => {
this.$refs.searchInput.focus();
});
},
onKeydown(e) {
switch (e.keyCode) {
case UP_KEY_CODE:
e.preventDefault();
this.mouseOver = false;
this.cancelMouseOver = true;
if (this.focusedIndex > 0) {
this.focusedIndex -= 1;
} else {
this.focusedIndex = this.filteredBlobsLength - 1;
}
break;
case DOWN_KEY_CODE:
e.preventDefault();
this.mouseOver = false;
this.cancelMouseOver = true;
if (this.focusedIndex < this.filteredBlobsLength - 1) {
this.focusedIndex += 1;
} else {
this.focusedIndex = 0;
}
break;
default:
break;
}
},
onKeyup(e) {
switch (e.keyCode) {
case ENTER_KEY_CODE:
this.openFile(this.filteredBlobs[this.focusedIndex]);
break;
case ESC_KEY_CODE:
this.toggleFileFinder(false);
break;
default:
break;
}
},
openFile(file) {
this.toggleFileFinder(false);
router.push(`/project${file.url}`);
},
onMouseOver(index) {
if (!this.cancelMouseOver) {
this.mouseOver = true;
this.focusedIndex = index;
}
},
onMouseMove(index) {
this.cancelMouseOver = false;
this.onMouseOver(index);
},
},
};
</script>
<template>
<div
class="ide-file-finder-overlay"
@mousedown.self="toggleFileFinder(false)"
>
<div
class="dropdown-menu diff-file-changes ide-file-finder show"
>
<div class="dropdown-input">
<input
type="search"
class="dropdown-input-field"
:placeholder="__('Search files')"
autocomplete="off"
v-model="searchText"
ref="searchInput"
@keydown="onKeydown($event)"
@keyup="onKeyup($event)"
/>
<i
aria-hidden="true"
class="fa fa-search dropdown-input-search"
:class="{
hidden: showClearInputButton
}"
></i>
<i
role="button"
:aria-label="__('Clear search input')"
class="fa fa-times dropdown-input-clear"
:class="{
show: showClearInputButton
}"
@click="clearSearchInput"
></i>
</div>
<div>
<virtual-list
:size="listHeight"
:remain="listShowCount"
wtag="ul"
ref="virtualScrollList"
>
<template v-if="filteredBlobsLength">
<li
v-for="(file, index) in filteredBlobs"
:key="file.key"
>
<item
class="disable-hover"
:file="file"
:search-text="searchText"
:focused="index === focusedIndex"
:index="index"
@click="openFile"
@mouseover="onMouseOver"
@mousemove="onMouseMove"
/>
</li>
</template>
<li
v-else
class="dropdown-menu-empty-item"
>
<div class="append-right-default prepend-left-default prepend-top-8 append-bottom-8">
<template v-if="loading">
{{ __('Loading...') }}
</template>
<template v-else>
{{ __('No files found.') }}
</template>
</div>
</li>
</virtual-list>
</div>
</div>
</div>
</template>
<script>
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import FileIcon from '../../../vue_shared/components/file_icon.vue';
import ChangedFileIcon from '../changed_file_icon.vue';
const MAX_PATH_LENGTH = 60;
export default {
components: {
ChangedFileIcon,
FileIcon,
},
props: {
file: {
type: Object,
required: true,
},
focused: {
type: Boolean,
required: true,
},
searchText: {
type: String,
required: true,
},
index: {
type: Number,
required: true,
},
},
computed: {
pathWithEllipsis() {
const path = this.file.path;
return path.length < MAX_PATH_LENGTH
? path
: `...${path.substr(path.length - MAX_PATH_LENGTH)}`;
},
nameSearchTextOccurences() {
return fuzzaldrinPlus.match(this.file.name, this.searchText);
},
pathSearchTextOccurences() {
return fuzzaldrinPlus.match(this.pathWithEllipsis, this.searchText);
},
},
methods: {
clickRow() {
this.$emit('click', this.file);
},
mouseOverRow() {
this.$emit('mouseover', this.index);
},
mouseMove() {
this.$emit('mousemove', this.index);
},
},
};
</script>
<template>
<button
type="button"
class="diff-changed-file"
:class="{
'is-focused': focused,
}"
@click.prevent="clickRow"
@mouseover="mouseOverRow"
@mousemove="mouseMove"
>
<file-icon
:file-name="file.name"
:size="16"
css-classes="diff-file-changed-icon append-right-8"
/>
<span class="diff-changed-file-content append-right-8">
<strong
class="diff-changed-file-name"
>
<span
v-for="(char, index) in file.name.split('')"
:key="index + char"
:class="{
highlighted: nameSearchTextOccurences.indexOf(index) >= 0,
}"
v-text="char"
>
</span>
</strong>
<span
class="diff-changed-file-path prepend-top-5"
>
<span
v-for="(char, index) in pathWithEllipsis.split('')"
:key="index + char"
:class="{
highlighted: pathSearchTextOccurences.indexOf(index) >= 0,
}"
v-text="char"
>
</span>
</span>
</span>
<span
v-if="file.changed || file.tempFile"
class="diff-changed-stats"
>
<changed-file-icon
:file="file"
/>
</span>
</button>
</template>
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import ideSidebar from './ide_side_bar.vue'; import Mousetrap from 'mousetrap';
import ideContextbar from './ide_context_bar.vue'; import ideSidebar from './ide_side_bar.vue';
import repoTabs from './repo_tabs.vue'; import ideContextbar from './ide_context_bar.vue';
import ideStatusBar from './ide_status_bar.vue'; import repoTabs from './repo_tabs.vue';
import repoEditor from './repo_editor.vue'; import ideStatusBar from './ide_status_bar.vue';
import repoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue';
export default { const originalStopCallback = Mousetrap.stopCallback;
components: {
ideSidebar, export default {
ideContextbar, components: {
repoTabs, ideSidebar,
ideStatusBar, ideContextbar,
repoEditor, repoTabs,
}, ideStatusBar,
props: { repoEditor,
emptyStateSvgPath: { FindFile,
type: String,
required: true,
}, },
noChangesStateSvgPath: { props: {
type: String, emptyStateSvgPath: {
required: true, type: String,
required: true,
},
noChangesStateSvgPath: {
type: String,
required: true,
},
committedStateSvgPath: {
type: String,
required: true,
},
}, },
committedStateSvgPath: { computed: {
type: String, ...mapState([
required: true, 'changedFiles',
'openFiles',
'viewer',
'currentMergeRequestId',
'fileFindVisible',
]),
...mapGetters(['activeFile', 'hasChanges']),
}, },
}, mounted() {
computed: { const returnValue = 'Are you sure you want to lose unsaved changes?';
...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']), window.onbeforeunload = e => {
...mapGetters(['activeFile', 'hasChanges']), if (!this.changedFiles.length) return undefined;
},
mounted() { Object.assign(e, {
const returnValue = 'Are you sure you want to lose unsaved changes?'; returnValue,
window.onbeforeunload = e => { });
if (!this.changedFiles.length) return undefined; return returnValue;
};
Object.assign(e, { Mousetrap.bind(['t', 'command+p', 'ctrl+p'], e => {
returnValue, if (e.preventDefault) {
e.preventDefault();
}
this.toggleFileFinder(!this.fileFindVisible);
}); });
return returnValue;
}; Mousetrap.stopCallback = (e, el, combo) => this.mousetrapStopCallback(e, el, combo);
}, },
}; methods: {
...mapActions(['toggleFileFinder']),
mousetrapStopCallback(e, el, combo) {
if (combo === 't' && el.classList.contains('dropdown-input-field')) {
return true;
} else if (combo === 'command+p' || combo === 'ctrl+p') {
return false;
}
return originalStopCallback(e, el, combo);
},
},
};
</script> </script>
<template> <template>
<div <div
class="ide-view" class="ide-view"
> >
<find-file
v-show="fileFindVisible"
/>
<ide-sidebar /> <ide-sidebar />
<div <div
class="multi-file-edit-pane" class="multi-file-edit-pane"
......
// Fuzzy file finder // Fuzzy file finder
export const MAX_FILE_FINDER_RESULTS = 40;
export const FILE_FINDER_ROW_HEIGHT = 55;
export const FILE_FINDER_EMPTY_ROW_HEIGHT = 33;
// Commit message textarea
export const MAX_TITLE_LENGTH = 50; export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72; export const MAX_BODY_LENGTH = 72;
import _ from 'underscore'; import _ from 'underscore';
import store from '../stores';
import DecorationsController from './decorations/controller'; import DecorationsController from './decorations/controller';
import DirtyDiffController from './diff/controller'; import DirtyDiffController from './diff/controller';
import Disposable from './common/disposable'; import Disposable from './common/disposable';
import ModelManager from './common/model_manager'; import ModelManager from './common/model_manager';
import editorOptions, { defaultEditorOptions } from './editor_options'; import editorOptions, { defaultEditorOptions } from './editor_options';
import gitlabTheme from './themes/gl_theme'; import gitlabTheme from './themes/gl_theme';
import keymap from './keymap.json';
export const clearDomElement = el => { export const clearDomElement = el => {
if (!el || !el.firstChild) return; if (!el || !el.firstChild) return;
...@@ -53,6 +55,8 @@ export default class Editor { ...@@ -53,6 +55,8 @@ export default class Editor {
)), )),
); );
this.addCommands();
window.addEventListener('resize', this.debouncedUpdate, false); window.addEventListener('resize', this.debouncedUpdate, false);
} }
} }
...@@ -73,6 +77,8 @@ export default class Editor { ...@@ -73,6 +77,8 @@ export default class Editor {
})), })),
); );
this.addCommands();
window.addEventListener('resize', this.debouncedUpdate, false); window.addEventListener('resize', this.debouncedUpdate, false);
} }
} }
...@@ -189,4 +195,31 @@ export default class Editor { ...@@ -189,4 +195,31 @@ export default class Editor {
static renderSideBySide(domElement) { static renderSideBySide(domElement) {
return domElement.offsetWidth >= 700; return domElement.offsetWidth >= 700;
} }
addCommands() {
const getKeyCode = key => {
const monacoKeyMod = key.indexOf('KEY_') === 0;
return monacoKeyMod ? this.monaco.KeyCode[key] : this.monaco.KeyMod[key];
};
keymap.forEach(command => {
const keybindings = command.bindings.map(binding => {
const keys = binding.split('+');
// eslint-disable-next-line no-bitwise
return keys.length > 1 ? getKeyCode(keys[0]) | getKeyCode(keys[1]) : getKeyCode(keys[0]);
});
this.instance.addAction({
id: command.id,
label: command.label,
keybindings,
run() {
store.dispatch(command.action.name, command.action.params);
return null;
},
});
});
}
} }
[
{
"id": "file-finder",
"label": "File finder",
"bindings": ["CtrlCmd+KEY_P"],
"action": {
"name": "toggleFileFinder",
"params": true
}
}
]
...@@ -137,7 +137,13 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => { ...@@ -137,7 +137,13 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay); commit(types.UPDATE_DELAY_VIEWER_CHANGE, delay);
}; };
export const toggleFileFinder = ({ commit }, fileFindVisible) =>
commit(types.TOGGLE_FILE_FINDER, fileFindVisible);
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
export * from './actions/merge_request'; export * from './actions/merge_request';
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
...@@ -42,4 +42,20 @@ export const collapseButtonTooltip = state => ...@@ -42,4 +42,20 @@ export const collapseButtonTooltip = state =>
export const hasMergeRequest = state => !!state.currentMergeRequestId; export const hasMergeRequest = state => !!state.currentMergeRequestId;
export const allBlobs = state =>
Object.keys(state.entries)
.reduce((acc, key) => {
const entry = state.entries[key];
if (entry.type === 'blob') {
acc.push(entry);
}
return acc;
}, [])
.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 () => {};
...@@ -185,3 +185,6 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState }) = ...@@ -185,3 +185,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 () => {};
...@@ -58,3 +58,5 @@ export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE'; ...@@ -58,3 +58,5 @@ export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT'; 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 TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
...@@ -100,6 +100,11 @@ export default { ...@@ -100,6 +100,11 @@ export default {
delayViewerUpdated, delayViewerUpdated,
}); });
}, },
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
Object.assign(state, {
fileFindVisible,
});
},
[types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) { [types.UPDATE_FILE_AFTER_COMMIT](state, { file, lastCommit }) {
const changedFile = state.changedFiles.find(f => f.path === file.path); const changedFile = state.changedFiles.find(f => f.path === file.path);
......
...@@ -4,6 +4,7 @@ export default { ...@@ -4,6 +4,7 @@ export default {
[types.SET_FILE_ACTIVE](state, { path, active }) { [types.SET_FILE_ACTIVE](state, { path, active }) {
Object.assign(state.entries[path], { Object.assign(state.entries[path], {
active, active,
lastOpenedAt: new Date().getTime(),
}); });
if (active && !state.entries[path].pending) { if (active && !state.entries[path].pending) {
......
...@@ -18,4 +18,5 @@ export default () => ({ ...@@ -18,4 +18,5 @@ export default () => ({
entries: {}, entries: {},
viewer: 'editor', viewer: 'editor',
delayViewerUpdated: false, delayViewerUpdated: false,
fileFindVisible: false,
}); });
...@@ -42,6 +42,7 @@ export const dataStructure = () => ({ ...@@ -42,6 +42,7 @@ export const dataStructure = () => ({
viewMode: 'edit', viewMode: 'edit',
previewMode: null, previewMode: null,
size: 0, size: 0,
lastOpenedAt: 0,
}); });
export const decorateData = entity => { export const decorateData = entity => {
......
...@@ -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';
......
export const UP_KEY_CODE = 38;
export const DOWN_KEY_CODE = 40;
export const ENTER_KEY_CODE = 13;
export const ESC_KEY_CODE = 27;
...@@ -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 () => {};
...@@ -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 () => {};
...@@ -472,6 +472,7 @@ img.emoji { ...@@ -472,6 +472,7 @@ img.emoji {
.append-right-20 { margin-right: 20px; } .append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; } .append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; } .append-bottom-5 { margin-bottom: 5px; }
.append-bottom-8 { margin-bottom: $grid-size; }
.append-bottom-10 { margin-bottom: 10px; } .append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; } .append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; } .append-bottom-20 { margin-bottom: 20px; }
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
border-color: $gray-darkest; border-color: $gray-darkest;
} }
[data-toggle="dropdown"] { [data-toggle='dropdown'] {
outline: 0; outline: 0;
} }
} }
...@@ -172,7 +172,11 @@ ...@@ -172,7 +172,11 @@
color: $brand-danger; color: $brand-danger;
} }
&:hover, &.disable-hover {
text-decoration: none;
}
&:not(.disable-hover):hover,
&:active, &:active,
&:focus, &:focus,
&.is-focused { &.is-focused {
...@@ -508,17 +512,16 @@ ...@@ -508,17 +512,16 @@
} }
&.is-indeterminate::before { &.is-indeterminate::before {
content: "\f068"; content: '\f068';
} }
&.is-active::before { &.is-active::before {
content: "\f00c"; content: '\f00c';
} }
} }
} }
} }
.dropdown-title { .dropdown-title {
position: relative; position: relative;
padding: 2px 25px 10px; padding: 2px 25px 10px;
...@@ -724,7 +727,6 @@ ...@@ -724,7 +727,6 @@
} }
} }
.dropdown-menu-due-date { .dropdown-menu-due-date {
.dropdown-content { .dropdown-content {
max-height: 230px; max-height: 230px;
...@@ -854,9 +856,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu { ...@@ -854,9 +856,13 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
} }
.projects-list-frequent-container, .projects-list-frequent-container,
.projects-list-search-container, { .projects-list-search-container {
padding: 8px 0; padding: 8px 0;
overflow-y: auto; overflow-y: auto;
li.section-empty.section-failure {
color: $callout-danger-color;
}
} }
.section-header, .section-header,
...@@ -867,13 +873,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu { ...@@ -867,13 +873,6 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
font-size: $gl-font-size; font-size: $gl-font-size;
} }
.projects-list-frequent-container,
.projects-list-search-container {
li.section-empty.section-failure {
color: $callout-danger-color;
}
}
.search-input-container { .search-input-container {
position: relative; position: relative;
padding: 4px $gl-padding; padding: 4px $gl-padding;
...@@ -905,8 +904,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu { ...@@ -905,8 +904,7 @@ header.header-content .dropdown-menu.projects-dropdown-menu {
} }
.projects-list-item-container { .projects-list-item-container {
.project-item-avatar-container .project-item-avatar-container .project-item-metadata-container {
.project-item-metadata-container {
float: left; float: left;
} }
......
...@@ -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;
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
} }
.ide-view { .ide-view {
position: relative;
display: flex; display: flex;
height: calc(100vh - #{$header-height}); height: calc(100vh - #{$header-height});
margin-top: 0; margin-top: 0;
...@@ -876,6 +877,26 @@ ...@@ -876,6 +877,26 @@
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
.ide-file-finder-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 100;
}
.ide-file-finder {
top: 10px;
left: 50%;
transform: translateX(-50%);
.highlighted {
color: $blue-500;
font-weight: $gl-font-weight-bold;
}
}
.ide-commit-message-field { .ide-commit-message-field {
height: 200px; height: 200px;
background-color: $white-light; background-color: $white-light;
......
...@@ -23,6 +23,9 @@ module AuthenticatesWithTwoFactor ...@@ -23,6 +23,9 @@ module AuthenticatesWithTwoFactor
# #
# Returns nil # Returns nil
def prompt_for_two_factor(user) def prompt_for_two_factor(user)
# Set @user for Devise views
@user = user # rubocop:disable Gitlab/ModuleWithInstanceVariables
return locked_user_redirect(user) unless user.can?(:log_in) return locked_user_redirect(user) unless user.can?(:log_in)
session[:otp_user_id] = user.id session[:otp_user_id] = user.id
......
...@@ -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
......
class Ldap::OmniauthCallbacksController < OmniauthCallbacksController
extend ::Gitlab::Utils::Override
def self.define_providers!
return unless Gitlab::Auth::LDAP::Config.enabled?
Gitlab::Auth::LDAP::Config.available_servers.each do |server|
alias_method server['provider_name'], :ldap
end
end
# We only find ourselves here
# if the authentication to LDAP was successful.
def ldap
sign_in_user_flow(Gitlab::Auth::LDAP::User)
end
define_providers!
override :set_remember_me
def set_remember_me(user)
user.remember_me = params[:remember_me] if user.persisted?
end
override :fail_login
def fail_login(user)
flash[:alert] = 'Access denied for your LDAP account.'
redirect_to new_user_session_path
end
end
...@@ -4,18 +4,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -4,18 +4,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
protect_from_forgery except: [:kerberos, :saml, :cas3] protect_from_forgery except: [:kerberos, :saml, :cas3]
Gitlab.config.omniauth.providers.each do |provider| def handle_omniauth
define_method provider['name'] do omniauth_flow(Gitlab::Auth::OAuth)
handle_omniauth
end
end end
if Gitlab::Auth::LDAP::Config.enabled? Gitlab.config.omniauth.providers.each do |provider|
Gitlab::Auth::LDAP::Config.available_servers.each do |server| alias_method provider['name'], :handle_omniauth
define_method server['provider_name'] do
ldap
end
end
end end
# Extend the standard implementation to also increment # Extend the standard implementation to also increment
...@@ -37,51 +31,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -37,51 +31,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
error ||= exception.error if exception.respond_to?(:error) error ||= exception.error if exception.respond_to?(:error)
error ||= exception.message if exception.respond_to?(:message) error ||= exception.message if exception.respond_to?(:message)
error ||= env["omniauth.error.type"].to_s error ||= env["omniauth.error.type"].to_s
error.to_s.humanize if error
end
# We only find ourselves here
# if the authentication to LDAP was successful.
def ldap
ldap_user = Gitlab::Auth::LDAP::User.new(oauth)
ldap_user.save if ldap_user.changed? # will also save new users
@user = ldap_user.gl_user
@user.remember_me = params[:remember_me] if ldap_user.persisted?
# Do additional LDAP checks for the user filter and EE features error.to_s.humanize if error
if ldap_user.allowed?
if @user.two_factor_enabled?
prompt_for_two_factor(@user)
else
log_audit_event(@user, with: oauth['provider'])
sign_in_and_redirect(@user)
end
else
fail_ldap_login
end
end end
def saml def saml
if current_user omniauth_flow(Gitlab::Auth::Saml)
log_audit_event(current_user, with: :saml)
# Update SAML identity if data has changed.
identity = current_user.identities.with_extern_uid(:saml, oauth['uid']).take
if identity.nil?
current_user.identities.create(extern_uid: oauth['uid'], provider: :saml)
redirect_to profile_account_path, notice: 'Authentication method updated'
else
redirect_to after_sign_in_path_for(current_user)
end
else
saml_user = Gitlab::Auth::Saml::User.new(oauth)
saml_user.save if saml_user.changed?
@user = saml_user.gl_user
continue_login_process
end
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error
end end
def omniauth_error def omniauth_error
...@@ -117,25 +72,36 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -117,25 +72,36 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
private private
def handle_omniauth def omniauth_flow(auth_module, identity_linker: nil)
if current_user if current_user
# Add new authentication method
current_user.identities
.with_extern_uid(oauth['provider'], oauth['uid'])
.first_or_create(extern_uid: oauth['uid'])
log_audit_event(current_user, with: oauth['provider']) log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
oauth_user = Gitlab::Auth::OAuth::User.new(oauth)
oauth_user.save
@user = oauth_user.gl_user
continue_login_process identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth)
identity_linker.link
if identity_linker.changed?
redirect_identity_linked
elsif identity_linker.error_message.present?
redirect_identity_link_failed(identity_linker.error_message)
else
redirect_identity_exists
end
else
sign_in_user_flow(auth_module::User)
end end
rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError end
handle_disabled_provider
rescue Gitlab::Auth::OAuth::User::SignupDisabledError def redirect_identity_exists
handle_signup_error redirect_to after_sign_in_path_for(current_user)
end
def redirect_identity_link_failed(error_message)
redirect_to profile_account_path, notice: "Authentication failed: #{error_message}"
end
def redirect_identity_linked
redirect_to profile_account_path, notice: 'Authentication method updated'
end end
def handle_service_ticket(provider, ticket) def handle_service_ticket(provider, ticket)
...@@ -144,21 +110,27 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -144,21 +110,27 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
session[:service_tickets][provider] = ticket session[:service_tickets][provider] = ticket
end end
def continue_login_process def sign_in_user_flow(auth_user_class)
# Only allow properly saved users to login. auth_user = auth_user_class.new(oauth)
if @user.persisted? && @user.valid? user = auth_user.find_and_update!
log_audit_event(@user, with: oauth['provider'])
if auth_user.valid_sign_in?
log_audit_event(user, with: oauth['provider'])
if @user.two_factor_enabled? set_remember_me(user)
params[:remember_me] = '1' if remember_me?
prompt_for_two_factor(@user) if user.two_factor_enabled?
prompt_for_two_factor(user)
else else
remember_me(@user) if remember_me? sign_in_and_redirect(user)
sign_in_and_redirect(@user)
end end
else else
fail_login fail_login(user)
end end
rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
handle_disabled_provider
rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error
end end
def handle_signup_error def handle_signup_error
...@@ -178,18 +150,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -178,18 +150,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
@oauth ||= request.env['omniauth.auth'] @oauth ||= request.env['omniauth.auth']
end end
def fail_login def fail_login(user)
error_message = @user.errors.full_messages.to_sentence error_message = user.errors.full_messages.to_sentence
return redirect_to omniauth_error_path(oauth['provider'], error: error_message) return redirect_to omniauth_error_path(oauth['provider'], error: error_message)
end end
def fail_ldap_login
flash[:alert] = 'Access denied for your LDAP account.'
redirect_to new_user_session_path
end
def fail_auth0_login def fail_auth0_login
flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.' flash[:alert] = 'Wrong extern UID provided. Make sure Auth0 is configured correctly.'
...@@ -208,6 +174,16 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -208,6 +174,16 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
.for_authentication.security_event .for_authentication.security_event
end end
def set_remember_me(user)
return unless remember_me?
if user.two_factor_enabled?
params[:remember_me] = '1'
else
remember_me(user)
end
end
def remember_me? def remember_me?
request_params = request.env['omniauth.params'] request_params = request.env['omniauth.params']
(request_params['remember_me'] == '1') if request_params.present? (request_params['remember_me'] == '1') if request_params.present?
......
...@@ -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
......
...@@ -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)
......
...@@ -68,6 +68,11 @@ class Project < ActiveRecord::Base ...@@ -68,6 +68,11 @@ class Project < ActiveRecord::Base
after_save :update_project_statistics, if: :namespace_id_changed? after_save :update_project_statistics, if: :namespace_id_changed?
after_create :create_project_feature, unless: :project_feature after_create :create_project_feature, unless: :project_feature
after_create :create_ci_cd_settings,
unless: :ci_cd_settings,
if: proc { ProjectCiCdSetting.available? }
after_create :set_last_activity_at after_create :set_last_activity_at
after_create :set_last_repository_updated_at after_create :set_last_repository_updated_at
after_update :update_forks_visibility_level after_update :update_forks_visibility_level
...@@ -231,6 +236,7 @@ class Project < ActiveRecord::Base ...@@ -231,6 +236,7 @@ class Project < ActiveRecord::Base
has_many :custom_attributes, class_name: 'ProjectCustomAttribute' has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
has_many :project_badges, class_name: 'ProjectBadge' has_many :project_badges, class_name: 'ProjectBadge'
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting'
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true accepts_nested_attributes_for :project_feature, update_only: true
...@@ -1041,13 +1047,6 @@ class Project < ActiveRecord::Base ...@@ -1041,13 +1047,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
...@@ -2008,10 +2007,11 @@ class Project < ActiveRecord::Base ...@@ -2008,10 +2007,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
......
class ProjectCiCdSetting < ActiveRecord::Base
belongs_to :project
# The version of the schema that first introduced this model/table.
MINIMUM_SCHEMA_VERSION = 20180403035759
def self.available?
@available ||=
ActiveRecord::Migrator.current_version >= MINIMUM_SCHEMA_VERSION
end
def self.reset_column_information
@available = nil
super
end
end
...@@ -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?
......
...@@ -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)
......
...@@ -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
......
- 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,7 +58,9 @@ ...@@ -58,7 +58,9 @@
touch README.md touch README.md
git add README.md git add README.md
git commit -m "add README" git commit -m "add README"
git push -u origin master - if @project.can_current_user_push_to_default_branch?
%span><
git push -u origin master
%fieldset %fieldset
%h5 Existing folder %h5 Existing folder
...@@ -69,7 +71,9 @@ ...@@ -69,7 +71,9 @@
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"
git push -u origin master - if @project.can_current_user_push_to_default_branch?
%span><
git push -u origin master
%fieldset %fieldset
%h5 Existing Git repository %h5 Existing Git repository
...@@ -78,8 +82,10 @@ ...@@ -78,8 +82,10 @@
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')}
git push -u origin --all - if @project.can_current_user_push_to_default_branch?
git push -u origin --tags %span><
git push -u origin --all
git push -u origin --tags
- if can? current_user, :remove_project, @project - if can? current_user, :remove_project, @project
.prepend-top-20 .prepend-top-20
......
...@@ -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
......
---
title: Introduce new ProjectCiCdSetting model with group_runners_enabled
merge_request: 18144
author:
type: performance
---
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: 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: Added Webhook SSRF prevention to documentation
merge_request: 18532
author:
type: other
---
title: Added fuzzy file finder to web IDE
merge_request:
author:
type: added
---
title: Restore label underline color
merge_request: 18407
author: George Tsiolis
type: fixed
---
title: Bump lograge to 0.10.0 and remove monkey patch
merge_request:
author:
type: other
---
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
# Monkey patch lograge until https://github.com/roidrage/lograge/pull/241 is released
module Lograge
class RequestLogSubscriber < ActiveSupport::LogSubscriber
def strip_query_string(path)
index = path.index('?')
index ? path[0, index] : path
end
def extract_location
location = Thread.current[:lograge_location]
return {} unless location
Thread.current[:lograge_location] = nil
{ location: strip_query_string(location) }
end
end
end
# Only use Lograge for Rails # Only use Lograge for Rails
unless Sidekiq.server? unless Sidekiq.server?
filename = File.join(Rails.root, 'log', "#{Rails.env}_json.log") filename = File.join(Rails.root, 'log', "#{Rails.env}_json.log")
......
...@@ -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) {
......
# Allows individual providers to be directed to a chosen controller
# Call from inside devise_scope
def override_omniauth(provider, controller, path_prefix = '/users/auth')
match "#{path_prefix}/#{provider}/callback",
to: "#{controller}##{provider}",
as: "#{provider}_omniauth_callback",
via: [:get, :post]
end
# Use custom controller for LDAP omniauth callback
if Gitlab::Auth::LDAP::Config.enabled?
devise_scope :user do
Gitlab::Auth::LDAP::Config.available_servers.each do |server|
override_omniauth(server['provider_name'], 'ldap/omniauth_callbacks')
end
end
end
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
registrations: :registrations, registrations: :registrations,
passwords: :passwords, passwords: :passwords,
......
class CreateProjectCiCdSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
unless table_exists?(:project_ci_cd_settings)
create_table(:project_ci_cd_settings) do |t|
t.integer(:project_id, null: false)
t.boolean(:group_runners_enabled, default: true, null: false)
end
end
disable_statement_timeout
# This particular INSERT will take between 10 and 20 seconds.
execute 'INSERT INTO project_ci_cd_settings (project_id) SELECT id FROM projects'
# We add the index and foreign key separately so the above INSERT statement
# takes as little time as possible.
add_concurrent_index(:project_ci_cd_settings, :project_id, unique: true)
add_foreign_key_with_retry
end
def down
drop_table :project_ci_cd_settings
end
def add_foreign_key_with_retry
if Gitlab::Database.mysql?
# When using MySQL we don't support online upgrades, thus projects can't
# be deleted while we are running this migration.
return add_project_id_foreign_key
end
# Between the initial INSERT and the addition of the foreign key some
# projects may have been removed, leaving orphaned rows in our new settings
# table.
loop do
remove_orphaned_settings
begin
add_project_id_foreign_key
break
rescue ActiveRecord::InvalidForeignKey
say 'project_ci_cd_settings contains some orphaned rows, retrying...'
end
end
end
def add_project_id_foreign_key
add_concurrent_foreign_key(:project_ci_cd_settings, :projects, column: :project_id)
end
def remove_orphaned_settings
execute <<~SQL
DELETE FROM project_ci_cd_settings
WHERE NOT EXISTS (
SELECT 1
FROM projects
WHERE projects.id = project_ci_cd_settings.project_id
)
SQL
end
end
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
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class PopulateMissingProjectCiCdSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
# MySQL does not support online upgrades, thus there can't be any missing
# rows.
return if Gitlab::Database.mysql?
# Projects created after the initial migration but before the code started
# using ProjectCiCdSetting won't have a corresponding row in
# project_ci_cd_settings, so let's fix that.
execute <<~SQL
INSERT INTO project_ci_cd_settings (project_id)
SELECT id
FROM projects
WHERE NOT EXISTS (
SELECT 1
FROM project_ci_cd_settings
WHERE project_ci_cd_settings.project_id = projects.id
)
SQL
end
def down
# There's nothing to revert for this migration.
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: 20180420080616) 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"
...@@ -1437,6 +1437,13 @@ ActiveRecord::Schema.define(version: 20180420080616) do ...@@ -1437,6 +1437,13 @@ ActiveRecord::Schema.define(version: 20180420080616) do
add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
create_table "project_ci_cd_settings", force: :cascade do |t|
t.integer "project_id", null: false
t.boolean "group_runners_enabled", default: true, null: false
end
add_index "project_ci_cd_settings", ["project_id"], name: "index_project_ci_cd_settings_on_project_id", unique: true, using: :btree
create_table "project_custom_attributes", force: :cascade do |t| create_table "project_custom_attributes", force: :cascade do |t|
t.datetime "created_at", null: false t.datetime "created_at", null: false
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
...@@ -2163,6 +2170,7 @@ ActiveRecord::Schema.define(version: 20180420080616) do ...@@ -2163,6 +2170,7 @@ ActiveRecord::Schema.define(version: 20180420080616) do
add_foreign_key "project_authorizations", "projects", on_delete: :cascade add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade add_foreign_key "project_authorizations", "users", on_delete: :cascade
add_foreign_key "project_auto_devops", "projects", on_delete: :cascade add_foreign_key "project_auto_devops", "projects", on_delete: :cascade
add_foreign_key "project_ci_cd_settings", "projects", name: "fk_24c15d2f2e", on_delete: :cascade
add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade add_foreign_key "project_custom_attributes", "projects", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade add_foreign_key "project_deploy_tokens", "deploy_tokens", on_delete: :cascade
add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade add_foreign_key "project_deploy_tokens", "projects", on_delete: :cascade
......
...@@ -75,43 +75,33 @@ Notice several options that you should consider using: ...@@ -75,43 +75,33 @@ Notice several options that you should consider using:
| `nobootwait` | Don't halt boot process waiting for this mount to become available | `nobootwait` | Don't halt boot process waiting for this mount to become available
| `lookupcache=positive` | Tells the NFS client to honor `positive` cache results but invalidates any `negative` cache results. Negative cache results cause problems with Git. Specifically, a `git push` can fail to register uniformly across all NFS clients. The negative cache causes the clients to 'remember' that the files did not exist previously. | `lookupcache=positive` | Tells the NFS client to honor `positive` cache results but invalidates any `negative` cache results. Negative cache results cause problems with Git. Specifically, a `git push` can fail to register uniformly across all NFS clients. The negative cache causes the clients to 'remember' that the files did not exist previously.
## Mount locations ## A single NFS mount
When using default Omnibus configuration you will need to share 5 data locations It's recommended to nest all gitlab data dirs within a mount, that allows automatic
between all GitLab cluster nodes. No other locations should be shared. The restore of backups without manually moving existing data.
following are the 5 locations you need to mount:
| Location | Description | Default configuration |
| -------- | ----------- | --------------------- |
| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})`
| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'`
| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'`
| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'`
| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'`
Other GitLab directories should not be shared between nodes. They contain ```
node-specific files and GitLab code that does not need to be shared. To ship mountpoint
logs to a central location consider using remote syslog. GitLab Omnibus packages └── gitlab-data
provide configuration for [UDP log shipping][udp-log-shipping]. ├── builds
├── git-data
### Consolidating mount points ├── home-git
├── shared
If you don't want to configure 5-6 different NFS mount points, you have a few └── uploads
alternative options. ```
#### Change default file locations To do so, we'll need to configure Omnibus with the paths to each directory nested
in the mount point as follows:
Omnibus allows you to configure the file locations. With custom configuration Mount `/gitlab-nfs` then use the following Omnibus
you can specify just one main mountpoint and have all of these locations
as subdirectories. Mount `/gitlab-data` then use the following Omnibus
configuration to move each data location to a subdirectory: configuration to move each data location to a subdirectory:
```ruby ```ruby
git_data_dirs({"default" => "/gitlab-data/git-data"}) git_data_dirs({"default" => "/gitlab-nfs/gitlab-data/git-data"})
user['home'] = '/gitlab-data/home' user['home'] = '/gitlab-nfs/gitlab-data/home'
gitlab_rails['uploads_directory'] = '/gitlab-data/uploads' gitlab_rails['uploads_directory'] = '/gitlab-nfs/gitlab-data/uploads'
gitlab_rails['shared_path'] = '/gitlab-data/shared' gitlab_rails['shared_path'] = '/gitlab-nfs/gitlab-data/shared'
gitlab_ci['builds_directory'] = '/gitlab-data/builds' gitlab_ci['builds_directory'] = '/gitlab-nfs/gitlab-data/builds'
``` ```
To move the `git` home directory, all GitLab services must be stopped. Run To move the `git` home directory, all GitLab services must be stopped. Run
...@@ -122,22 +112,52 @@ Run `sudo gitlab-ctl reconfigure` to start using the central location. Please ...@@ -122,22 +112,52 @@ Run `sudo gitlab-ctl reconfigure` to start using the central location. Please
be aware that if you had existing data you will need to manually copy/rsync it be aware that if you had existing data you will need to manually copy/rsync it
to these new locations and then restart GitLab. to these new locations and then restart GitLab.
#### Bind mounts ## Bind mounts
Alternatively to changing the configuration in Omnibus, bind mounts can be used
to store the data on an NFS mount.
Bind mounts provide a way to specify just one NFS mount and then Bind mounts provide a way to specify just one NFS mount and then
bind the default GitLab data locations to the NFS mount. Start by defining your bind the default GitLab data locations to the NFS mount. Start by defining your
single NFS mount point as you normally would in `/etc/fstab`. Let's assume your single NFS mount point as you normally would in `/etc/fstab`. Let's assume your
NFS mount point is `/gitlab-data`. Then, add the following bind mounts in NFS mount point is `/gitlab-nfs`. Then, add the following bind mounts in
`/etc/fstab`: `/etc/fstab`:
```bash ```bash
/gitlab-data/git-data /var/opt/gitlab/git-data none bind 0 0 /gitlab-nfs/gitlab-data/git-data /var/opt/gitlab/git-data none bind 0 0
/gitlab-data/.ssh /var/opt/gitlab/.ssh none bind 0 0 /gitlab-nfs/gitlab-data/.ssh /var/opt/gitlab/.ssh none bind 0 0
/gitlab-data/uploads /var/opt/gitlab/gitlab-rails/uploads none bind 0 0 /gitlab-nfs/gitlab-data/uploads /var/opt/gitlab/gitlab-rails/uploads none bind 0 0
/gitlab-data/shared /var/opt/gitlab/gitlab-rails/shared none bind 0 0 /gitlab-nfs/gitlab-data/shared /var/opt/gitlab/gitlab-rails/shared none bind 0 0
/gitlab-data/builds /var/opt/gitlab/gitlab-ci/builds none bind 0 0 /gitlab-nfs/gitlab-data/builds /var/opt/gitlab/gitlab-ci/builds none bind 0 0
``` ```
Using bind mounts will require manually making sure the data directories
are empty before attempting a restore. Read more about the
[restore prerequisites](../../raketasks/backup_restore.md).
## Multiple NFS mounts
When using default Omnibus configuration you will need to share 5 data locations
between all GitLab cluster nodes. No other locations should be shared. The
following are the 5 locations need to be shared:
| Location | Description | Default configuration |
| -------- | ----------- | --------------------- |
| `/var/opt/gitlab/git-data` | Git repository data. This will account for a large portion of your data | `git_data_dirs({"default" => "/var/opt/gitlab/git-data"})`
| `/var/opt/gitlab/.ssh` | SSH `authorized_keys` file and keys used to import repositories from some other Git services | `user['home'] = '/var/opt/gitlab/'`
| `/var/opt/gitlab/gitlab-rails/uploads` | User uploaded attachments | `gitlab_rails['uploads_directory'] = '/var/opt/gitlab/gitlab-rails/uploads'`
| `/var/opt/gitlab/gitlab-rails/shared` | Build artifacts, GitLab Pages, LFS objects, temp files, etc. If you're using LFS this may also account for a large portion of your data | `gitlab_rails['shared_path'] = '/var/opt/gitlab/gitlab-rails/shared'`
| `/var/opt/gitlab/gitlab-ci/builds` | GitLab CI build traces | `gitlab_ci['builds_directory'] = '/var/opt/gitlab/gitlab-ci/builds'`
Other GitLab directories should not be shared between nodes. They contain
node-specific files and GitLab code that does not need to be shared. To ship
logs to a central location consider using remote syslog. GitLab Omnibus packages
provide configuration for [UDP log shipping][udp-log-shipping].
Having multiple NFS mounts will require manually making sure the data directories
are empty before attempting a restore. Read more about the
[restore prerequisites](../../raketasks/backup_restore.md).
--- ---
Read more on high-availability configuration: Read more on high-availability configuration:
......
...@@ -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.
......
...@@ -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
......
...@@ -498,6 +498,13 @@ more of the following options: ...@@ -498,6 +498,13 @@ more of the following options:
Read what the [backup timestamp is about](#backup-timestamp). Read what the [backup timestamp is about](#backup-timestamp).
- `force=yes` - Does not ask if the authorized_keys file should get regenerated and assumes 'yes' for warning that database tables will be removed. - `force=yes` - Does not ask if the authorized_keys file should get regenerated and assumes 'yes' for warning that database tables will be removed.
If you are restoring into directories that are mountpoints you will need to make
sure these directories are empty before attempting a restore. Otherwise GitLab
will attempt to move these directories before restoring the new data and this
would cause an error.
Read more on [configuring NFS mounts](../administration/high_availability/nfs.md)
### Restore for installation from source ### Restore for installation from source
``` ```
......
...@@ -2,12 +2,19 @@ ...@@ -2,12 +2,19 @@
If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks. If you have non-GitLab web services running on your GitLab server or within its local network, these may be vulnerable to exploitation via Webhooks.
With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way. With [Webhooks](../user/project/integrations/webhooks.md), you and your project masters and owners can set up URLs to be triggered when specific things happen to projects. Normally, these requests are sent to external web services specifically set up for this purpose, that process the request and its attached data in some appropriate way.
Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent. Things get hairy, however, when a Webhook is set up with a URL that doesn't point to an external, but to an internal service, that may do something completely unintended when the webhook is triggered and the POST request is sent.
Because Webhook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world. Because Webhook requests are made by the GitLab server itself, these have complete access to everything running on the server (http://localhost:123) or within the server's local network (http://192.168.1.12:345), even if these services are otherwise protected and inaccessible from the outside world.
If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete". If a web service does not require authentication, Webhooks can be used to trigger destructive commands by getting the GitLab server to make POST requests to endpoints like "http://localhost:123/some-resource/delete".
To prevent this type of exploitation from happening, make sure that you are aware of every web service GitLab could potentially have access to, and that all of these are set up to require authentication for every potentially destructive command. Enabling authentication but leaving a default password is not enough. To prevent this type of exploitation from happening, starting with GitLab 10.6, all Webhook requests to the current GitLab instance server address and/or in a private network will be forbidden by default. That means that all requests made to 127.0.0.1, ::1 and 0.0.0.0, as well as IPv4 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 and IPv6 site-local (ffc0::/10) addresses won't be allowed.
This behavior can be overridden by enabling the option *"Allow requests to the local network from hooks and services"* in the *"Outbound requests"* section inside the Admin area under **Settings** (`/admin/application_settings`):
![Outbound requests admin settings](img/outbound_requests_section.png)
>**Note:**
*System hooks* are exempt from this protection because they are set up by admins.
...@@ -38,6 +38,7 @@ do. ...@@ -38,6 +38,7 @@ do.
| `/award :emoji:` | Toggle award for :emoji: | | `/award :emoji:` | Toggle award for :emoji: |
| `/board_move ~column` | Move issue to column on the board | | `/board_move ~column` | Move issue to column on the board |
| `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue | | `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue |
| `/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 `¯\_(ツ)_/¯` |
\ No newline at end of file | <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).
module EE
module Ldap
module OmniauthCallbacksController
extend ::Gitlab::Utils::Override
override :sign_in_and_redirect
def sign_in_and_redirect(user)
# The counter gets incremented in `sign_in_and_redirect`
show_ldap_sync_flash if user.sign_in_count == 0
super
end
private
def show_ldap_sync_flash
flash[:notice] = 'LDAP sync in progress. This could take a few minutes. '\
'Refresh the page to see the changes.'
end
end
end
end
require 'spec_helper'
describe Ldap::OmniauthCallbacksController do
include_context 'Ldap::OmniauthCallbacksController'
it "displays LDAP sync flash on first sign in" do
post provider
expect(flash[:notice]).to match(/LDAP sync in progress*/)
end
it "skips LDAP sync flash on subsequent sign ins" do
user.update!(sign_in_count: 1)
post provider
expect(flash[:notice]).to eq nil
end
context 'access denied' do
let(:valid_login?) { false }
it 'logs a failure event' do
stub_licensed_features(extended_audit_events: true)
expect { post provider }.to change(SecurityEvent, :count).by(1)
end
end
end
@dashboard
Feature: Project Find File
Background:
Given I sign in as a user
And I own a project
And I visit my project's files page
@javascript
Scenario: Navigate to find file by shortcut
Given I press "t"
Then I should see "find file" page
Scenario: Navigate to find file
Given I click Find File button
Then I should see "find file" page
@javascript
Scenario: I search file
Given I visit project find file page
And I fill in file find with "change"
Then I should not see ".gitignore" in files
And I should not see ".gitmodules" in files
And I should see "CHANGELOG" in files
And I should not see "VERSION" in files
@javascript
Scenario: I search file that not exist
Given I visit project find file page
And I fill in file find with "asdfghjklqwertyuizxcvbnm"
Then I should not see ".gitignore" in files
And I should not see ".gitmodules" in files
And I should not see "CHANGELOG" in files
And I should not see "VERSION" in files
@javascript
Scenario: I search file that partially matches
Given I visit project find file page
And I fill in file find with "git"
Then I should see ".gitignore" in files
And I should see ".gitmodules" in files
And I should not see "CHANGELOG" in files
And I should not see "VERSION" in files
class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
include SharedProjectTab
step 'I press "t"' do
find('body').native.send_key('t')
end
step 'I click Find File button' do
click_link 'Find file'
end
step 'I should see "find file" page' do
ensure_active_main_tab('Repository')
expect(page).to have_selector('.file-finder-holder', count: 1)
end
step 'I fill in Find by path with "git"' do
ensure_active_main_tab('Repository')
expect(page).to have_selector('.file-finder-holder', count: 1)
end
step 'I fill in file find with "git"' do
find_file "git"
end
step 'I fill in file find with "change"' do
find_file "change"
end
step 'I fill in file find with "asdfghjklqwertyuizxcvbnm"' do
find_file "asdfghjklqwertyuizxcvbnm"
end
step 'I should see "VERSION" in files' do
expect(page).to have_content("VERSION")
end
step 'I should not see "VERSION" in files' do
expect(page).not_to have_content("VERSION")
end
step 'I should see "CHANGELOG" in files' do
expect(page).to have_content("CHANGELOG")
end
step 'I should not see "CHANGELOG" in files' do
expect(page).not_to have_content("CHANGELOG")
end
step 'I should see ".gitmodules" in files' do
expect(page).to have_content(".gitmodules")
end
step 'I should not see ".gitmodules" in files' do
expect(page).not_to have_content(".gitmodules")
end
step 'I should see ".gitignore" in files' do
expect(page).to have_content(".gitignore")
end
step 'I should not see ".gitignore" in files' do
expect(page).not_to have_content(".gitignore")
end
def find_file(text)
fill_in 'file_find', with: text
end
end
...@@ -200,10 +200,6 @@ module SharedPaths ...@@ -200,10 +200,6 @@ module SharedPaths
visit edit_project_path(@project) visit edit_project_path(@project)
end end
step "I visit my project's files page" do
visit project_tree_path(@project, root_ref)
end
step 'I visit a binary file in the repo' do step 'I visit a binary file in the repo' do
visit project_blob_path(@project, visit project_blob_path(@project,
File.join(root_ref, 'files/images/logo-black.png')) File.join(root_ref, 'files/images/logo-black.png'))
......
...@@ -53,6 +53,8 @@ module Backup ...@@ -53,6 +53,8 @@ module Backup
FileUtils.mv(files, timestamped_files_path) FileUtils.mv(files, timestamped_files_path)
rescue Errno::EACCES rescue Errno::EACCES
access_denied_error(app_files_dir) access_denied_error(app_files_dir)
rescue Errno::EBUSY
resource_busy_error(app_files_dir)
end end
end end
end end
......
...@@ -13,5 +13,19 @@ module Backup ...@@ -13,5 +13,19 @@ module Backup
EOS EOS
raise message raise message
end end
def resource_busy_error(path)
message = <<~EOS
### NOTICE ###
As part of restore, the task tried to rename `#{path}` before restoring.
This could not be completed, perhaps `#{path}` is a mountpoint?
To complete the restore, please move the contents of `#{path}` to a
different location and run the restore task again.
EOS
raise message
end
end end
end end
...@@ -81,6 +81,8 @@ module Backup ...@@ -81,6 +81,8 @@ module Backup
FileUtils.mv(files, bk_repos_path) FileUtils.mv(files, bk_repos_path)
rescue Errno::EACCES rescue Errno::EACCES
access_denied_error(path) access_denied_error(path)
rescue Errno::EBUSY
resource_busy_error(path)
end end
end end
end end
......
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
...@@ -8,6 +8,8 @@ module Gitlab ...@@ -8,6 +8,8 @@ module Gitlab
module Auth module Auth
module LDAP module LDAP
class User < Gitlab::Auth::OAuth::User class User < Gitlab::Auth::OAuth::User
extend ::Gitlab::Utils::Override
class << self class << self
def find_by_uid_and_provider(uid, provider) def find_by_uid_and_provider(uid, provider)
identity = ::Identity.with_extern_uid(provider, uid).take identity = ::Identity.with_extern_uid(provider, uid).take
...@@ -29,7 +31,8 @@ module Gitlab ...@@ -29,7 +31,8 @@ module Gitlab
self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider) self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
end end
def changed? override :should_save?
def should_save?
gl_user.changed? || gl_user.identities.any?(&:changed?) gl_user.changed? || gl_user.identities.any?(&:changed?)
end end
...@@ -41,6 +44,10 @@ module Gitlab ...@@ -41,6 +44,10 @@ module Gitlab
Gitlab::Auth::LDAP::Access.allowed?(gl_user) Gitlab::Auth::LDAP::Access.allowed?(gl_user)
end end
def valid_sign_in?
allowed? && super
end
def ldap_config def ldap_config
Gitlab::Auth::LDAP::Config.new(auth_hash.provider) Gitlab::Auth::LDAP::Config.new(auth_hash.provider)
end end
......
module Gitlab
module Auth
module OAuth
class IdentityLinker < OmniauthIdentityLinkerBase
end
end
end
end
...@@ -30,6 +30,10 @@ module Gitlab ...@@ -30,6 +30,10 @@ module Gitlab
gl_user.try(:valid?) gl_user.try(:valid?)
end end
def valid_sign_in?
valid? && persisted?
end
def save(provider = 'OAuth') def save(provider = 'OAuth')
raise SigninDisabledForProviderError if oauth_provider_disabled? raise SigninDisabledForProviderError if oauth_provider_disabled?
raise SignupDisabledError unless gl_user raise SignupDisabledError unless gl_user
...@@ -64,8 +68,18 @@ module Gitlab ...@@ -64,8 +68,18 @@ module Gitlab
user user
end end
def find_and_update!
save if should_save?
gl_user
end
protected protected
def should_save?
true
end
def add_or_update_user_identities def add_or_update_user_identities
return unless gl_user return unless gl_user
......
module Gitlab
module Auth
class OmniauthIdentityLinkerBase
attr_reader :current_user, :oauth
def initialize(current_user, oauth)
@current_user = current_user
@oauth = oauth
@changed = false
end
def link
save if identity.new_record?
end
def changed?
@changed
end
def error_message
identity.validate
identity.errors.full_messages.join(', ')
end
private
def save
@changed = identity.save
end
def identity
@identity ||= current_user.identities
.with_extern_uid(provider, uid)
.first_or_initialize(extern_uid: uid)
end
def provider
oauth['provider']
end
def uid
oauth['uid']
end
end
end
end
module Gitlab
module Auth
module Saml
class IdentityLinker < OmniauthIdentityLinkerBase
end
end
end
end
...@@ -7,6 +7,8 @@ module Gitlab ...@@ -7,6 +7,8 @@ module Gitlab
module Auth module Auth
module Saml module Saml
class User < Gitlab::Auth::OAuth::User class User < Gitlab::Auth::OAuth::User
extend ::Gitlab::Utils::Override
def save def save
super('SAML') super('SAML')
end end
...@@ -21,13 +23,14 @@ module Gitlab ...@@ -21,13 +23,14 @@ module Gitlab
if external_users_enabled? && user if external_users_enabled? && user
# Check if there is overlap between the user's groups and the external groups # Check if there is overlap between the user's groups and the external groups
# setting then set user as external or internal. # setting then set user as external or internal.
user.external = !(auth_hash.groups & Gitlab::Auth::Saml::Config.external_groups).empty? user.external = !(auth_hash.groups & saml_config.external_groups).empty?
end end
user user
end end
def changed? override :should_save?
def should_save?
return true unless gl_user return true unless gl_user
gl_user.changed? || gl_user.identities.any?(&:changed?) gl_user.changed? || gl_user.identities.any?(&:changed?)
...@@ -35,12 +38,16 @@ module Gitlab ...@@ -35,12 +38,16 @@ module Gitlab
protected protected
def saml_config
Gitlab::Auth::Saml::Config
end
def auto_link_saml_user? def auto_link_saml_user?
Gitlab.config.omniauth.auto_link_saml_user Gitlab.config.omniauth.auto_link_saml_user
end end
def external_users_enabled? def external_users_enabled?
!Gitlab::Auth::Saml::Config.external_groups.nil? !saml_config.external_groups.nil?
end end
def auth_hash=(auth_hash) def auth_hash=(auth_hash)
......
...@@ -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:)
......
...@@ -64,6 +64,7 @@ project_tree: ...@@ -64,6 +64,7 @@ project_tree:
- :project_feature - :project_feature
- :custom_attributes - :custom_attributes
- :project_badges - :project_badges
- :ci_cd_settings
# Only include the following attributes for the models specified. # Only include the following attributes for the models specified.
included_attributes: included_attributes:
...@@ -73,6 +74,8 @@ included_attributes: ...@@ -73,6 +74,8 @@ included_attributes:
- :username - :username
author: author:
- :name - :name
ci_cd_settings:
- :group_runners_enabled
# Do not include the following attributes for the models specified. # Do not include the following attributes for the models specified.
excluded_attributes: excluded_attributes:
......
...@@ -17,7 +17,8 @@ module Gitlab ...@@ -17,7 +17,8 @@ module Gitlab
auto_devops: :project_auto_devops, auto_devops: :project_auto_devops,
label: :project_label, label: :project_label,
custom_attributes: 'ProjectCustomAttribute', custom_attributes: 'ProjectCustomAttribute',
project_badges: 'Badge' }.freeze project_badges: 'Badge',
ci_cd_settings: 'ProjectCiCdSetting' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze
......
...@@ -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
......
...@@ -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",
...@@ -88,6 +88,7 @@ ...@@ -88,6 +88,7 @@
"vue-resource": "^1.3.5", "vue-resource": "^1.3.5",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.13", "vue-template-compiler": "^2.5.13",
"vue-virtual-scroll-list": "^1.2.5",
"vuex": "^3.0.1", "vuex": "^3.0.1",
"webpack": "^3.11.0", "webpack": "^3.11.0",
"webpack-bundle-analyzer": "^2.10.0", "webpack-bundle-analyzer": "^2.10.0",
...@@ -98,6 +99,9 @@ ...@@ -98,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",
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment