Merge branch 'master' into michel.engelen/gitlab-ce-issue/55953

This commit is contained in:
Sean McGivern 2019-07-03 10:28:13 +01:00
commit b94daa35a4
1083 changed files with 6386 additions and 24338 deletions

View File

@ -6,8 +6,8 @@
/doc/ @axil @marcia @eread @mikelewis
# Frontend maintainers should see everything in `app/assets/`
app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya
*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya
app/assets/ @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya @pslaughter
*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya @pslaughter
# Someone from the database team should review changes in `db/`
db/ @abrandl @NikolayS

View File

@ -8,7 +8,7 @@
.use-pg: &use-pg
services:
- name: postgres:9.6
- name: postgres:9.6.11
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
@ -32,7 +32,7 @@
DOCKER_HOST: tcp://docker:2375
script:
- node --version
- retry yarn install --frozen-lockfile --production --cache-folder .yarn-cache
- retry yarn install --frozen-lockfile --production --cache-folder .yarn-cache --prefer-offline
- free -m
- retry bundle exec rake gitlab:assets:compile
- time scripts/build_assets_image
@ -82,7 +82,7 @@ gitlab:assets:compile pull-cache:
stage: prepare
script:
- node --version
- retry yarn install --frozen-lockfile --cache-folder .yarn-cache
- retry yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- free -m
- retry bundle exec rake gitlab:assets:compile
- scripts/clean-old-cached-assets
@ -231,7 +231,7 @@ qa:selectors:
before_script: []
script:
- date
- yarn install --frozen-lockfile --cache-folder .yarn-cache
- yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- date
- yarn run webpack-prod

View File

@ -1,6 +1,6 @@
.use-pg: &use-pg
services:
- name: postgres:9.6
- name: postgres:9.6.11
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
- name: redis:alpine
@ -245,7 +245,7 @@ migration:path-pg:
.db-rollback: &db-rollback
extends: .dedicated-no-docs-and-no-qa-pull-cache-job
script:
- bundle exec rake db:migrate VERSION=20170523121229
- bundle exec rake db:migrate VERSION=20180101160629
- bundle exec rake db:migrate SKIP_SCHEMA_VERSION_CHECK=true
dependencies:
- setup-test-env

View File

@ -236,5 +236,5 @@ danger-review:
script:
- git version
- node --version
- yarn install --frozen-lockfile --cache-folder .yarn-cache
- yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
- danger --fail-on-errors=true

View File

@ -0,0 +1,55 @@
<script>
import { mapGetters } from 'vuex';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
export default {
name: 'DiffDiscussionReply',
components: {
NoteSignedOutWidget,
ReplyPlaceholder,
UserAvatarLink,
},
props: {
hasForm: {
type: Boolean,
required: false,
default: false,
},
renderReplyPlaceholder: {
type: Boolean,
required: true,
},
},
computed: {
...mapGetters({
currentUser: 'getUserData',
userCanReply: 'userCanReply',
}),
},
};
</script>
<template>
<div class="discussion-reply-holder d-flex clearfix">
<template v-if="userCanReply">
<slot v-if="hasForm" name="form"></slot>
<template v-else-if="renderReplyPlaceholder">
<user-avatar-link
:link-href="currentUser.path"
:img-src="currentUser.avatar_url"
:img-alt="currentUser.name"
:img-size="40"
class="d-none d-sm-block"
/>
<reply-placeholder
class="qa-discussion-reply"
:button-text="__('Start a new discussion...')"
@onClick="$emit('showNewDiscussionForm')"
/>
</template>
</template>
<note-signed-out-widget v-else />
</div>
</template>

View File

@ -80,7 +80,6 @@ export default {
v-show="isExpanded(discussion)"
:discussion="discussion"
:render-diff-file="false"
:always-expanded="true"
:discussions-by-diff-order="true"
:line="line"
:help-page-path="helpPagePath"

View File

@ -151,7 +151,11 @@ export default {
stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false);
},
methods: {
...mapActions('diffs', ['toggleFileDiscussions', 'toggleFullDiff']),
...mapActions('diffs', [
'toggleFileDiscussions',
'toggleFileDiscussionWrappers',
'toggleFullDiff',
]),
handleToggleFile(e, checkTarget) {
if (
!checkTarget ||
@ -165,7 +169,7 @@ export default {
this.$emit('showForkMessage');
},
handleToggleDiscussions() {
this.toggleFileDiscussions(this.diffFile);
this.toggleFileDiscussionWrappers(this.diffFile);
},
handleFileNameClick(e) {
const isLinkToOtherPage =

View File

@ -1,5 +1,4 @@
<script>
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import { pluralize, truncate } from '~/lib/utils/text_utility';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
@ -19,11 +18,13 @@ export default {
type: Array,
required: true,
},
discussionsExpanded: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
discussionsExpanded() {
return this.discussions.every(discussion => discussion.expanded);
},
allDiscussions() {
return this.discussions.reduce((acc, note) => acc.concat(note.notes), []);
},
@ -45,26 +46,14 @@ export default {
},
},
methods: {
...mapActions(['toggleDiscussion']),
getTooltipText(noteData) {
let { note } = noteData;
if (note.length > LENGTH_OF_AVATAR_TOOLTIP) {
note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP);
}
return `${noteData.author.name}: ${note}`;
},
toggleDiscussions() {
const forceExpanded = this.discussions.some(discussion => !discussion.expanded);
this.discussions.forEach(discussion => {
this.toggleDiscussion({
discussionId: discussion.id,
forceExpanded,
});
});
},
},
};
</script>
@ -76,7 +65,7 @@ export default {
type="button"
:aria-label="__('Show comments')"
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
@click="toggleDiscussions"
@click="$emit('toggleLineDiscussions')"
>
<icon :size="12" name="collapse" />
</button>
@ -87,7 +76,7 @@ export default {
:img-src="note.author.avatar_url"
:tooltip-text="getTooltipText(note)"
class="diff-comment-avatar js-diff-comment-avatar"
@click.native="toggleDiscussions"
@click.native="$emit('toggleLineDiscussions')"
/>
<span
v-if="moreText"
@ -97,7 +86,7 @@ export default {
data-container="body"
data-placement="top"
role="button"
@click="toggleDiscussions"
@click="$emit('toggleLineDiscussions')"
>+{{ moreCount }}</span
>
</template>

View File

@ -105,7 +105,13 @@ export default {
},
},
methods: {
...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']),
...mapActions('diffs', [
'loadMoreLines',
'showCommentForm',
'setHighlightedRow',
'toggleLineDiscussions',
'toggleLineDiscussionWrappers',
]),
handleCommentButton() {
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
},
@ -184,7 +190,14 @@ export default {
@click="setHighlightedRow(lineCode)"
>
</a>
<diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" :discussions="line.discussions" />
<diff-gutter-avatars
v-if="shouldShowAvatarsOnGutter"
:discussions="line.discussions"
:discussions-expanded="line.discussionsExpanded"
@toggleLineDiscussions="
toggleLineDiscussions({ lineCode, fileHash, expanded: !line.discussionsExpanded })
"
/>
</template>
</div>
</template>

View File

@ -1,11 +1,14 @@
<script>
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
import { mapActions } from 'vuex';
import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
import DiffDiscussionReply from './diff_discussion_reply.vue';
export default {
components: {
diffDiscussions,
diffLineNoteForm,
DiffDiscussions,
DiffLineNoteForm,
DiffDiscussionReply,
},
props: {
line: {
@ -32,10 +35,12 @@ export default {
if (!this.line.discussions || !this.line.discussions.length) {
return false;
}
return this.line.discussions.every(discussion => discussion.expanded);
return this.line.discussionsExpanded;
},
},
methods: {
...mapActions('diffs', ['showCommentForm']),
},
};
</script>
@ -49,13 +54,22 @@ export default {
:discussions="line.discussions"
:help-page-path="helpPagePath"
/>
<diff-line-note-form
v-if="line.hasForm"
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="line"
:help-page-path="helpPagePath"
/>
<diff-discussion-reply
:has-form="line.hasForm"
:render-reply-placeholder="Boolean(line.discussions.length)"
@showNewDiscussionForm="
showCommentForm({ lineCode: line.line_code, fileHash: diffFileHash })
"
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line"
:note-target-line="line"
:help-page-path="helpPagePath"
/>
</template>
</diff-discussion-reply>
</div>
</td>
</tr>

View File

@ -1,11 +1,14 @@
<script>
import diffDiscussions from './diff_discussions.vue';
import diffLineNoteForm from './diff_line_note_form.vue';
import { mapActions } from 'vuex';
import DiffDiscussions from './diff_discussions.vue';
import DiffLineNoteForm from './diff_line_note_form.vue';
import DiffDiscussionReply from './diff_discussion_reply.vue';
export default {
components: {
diffDiscussions,
diffLineNoteForm,
DiffDiscussions,
DiffLineNoteForm,
DiffDiscussionReply,
},
props: {
line: {
@ -29,24 +32,30 @@ export default {
computed: {
hasExpandedDiscussionOnLeft() {
return this.line.left && this.line.left.discussions.length
? this.line.left.discussions.every(discussion => discussion.expanded)
? this.line.left.discussionsExpanded
: false;
},
hasExpandedDiscussionOnRight() {
return this.line.right && this.line.right.discussions.length
? this.line.right.discussions.every(discussion => discussion.expanded)
? this.line.right.discussionsExpanded
: false;
},
hasAnyExpandedDiscussion() {
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
},
shouldRenderDiscussionsOnLeft() {
return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft;
return (
this.line.left &&
this.line.left.discussions &&
this.line.left.discussions.length &&
this.hasExpandedDiscussionOnLeft
);
},
shouldRenderDiscussionsOnRight() {
return (
this.line.right &&
this.line.right.discussions &&
this.line.right.discussions.length &&
this.hasExpandedDiscussionOnRight &&
this.line.right.type
);
@ -81,6 +90,22 @@ export default {
return hasCommentFormOnLeft || hasCommentFormOnRight;
},
shouldRenderReplyPlaceholderOnLeft() {
return Boolean(
this.line.left && this.line.left.discussions && this.line.left.discussions.length,
);
},
shouldRenderReplyPlaceholderOnRight() {
return Boolean(
this.line.right && this.line.right.discussions && this.line.right.discussions.length,
);
},
},
methods: {
...mapActions('diffs', ['showCommentForm']),
showNewDiscussionForm() {
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.diffFileHash });
},
},
};
</script>
@ -90,37 +115,49 @@ export default {
<td class="notes-content parallel old" colspan="2">
<div v-if="shouldRenderDiscussionsOnLeft" class="content">
<diff-discussions
v-if="line.left.discussions.length"
:discussions="line.left.discussions"
:line="line.left"
:help-page-path="helpPagePath"
/>
</div>
<diff-line-note-form
v-if="showLeftSideCommentForm"
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
:help-page-path="helpPagePath"
line-position="left"
/>
<diff-discussion-reply
:has-form="showLeftSideCommentForm"
:render-reply-placeholder="shouldRenderReplyPlaceholderOnLeft"
@showNewDiscussionForm="showNewDiscussionForm"
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line.left"
:note-target-line="line.left"
:help-page-path="helpPagePath"
line-position="left"
/>
</template>
</diff-discussion-reply>
</td>
<td class="notes-content parallel new" colspan="2">
<div v-if="shouldRenderDiscussionsOnRight" class="content">
<diff-discussions
v-if="line.right.discussions.length"
:discussions="line.right.discussions"
:line="line.right"
:help-page-path="helpPagePath"
/>
</div>
<diff-line-note-form
v-if="showRightSideCommentForm"
:diff-file-hash="diffFileHash"
:line="line.right"
:note-target-line="line.right"
line-position="right"
/>
<diff-discussion-reply
:has-form="showRightSideCommentForm"
:render-reply-placeholder="shouldRenderReplyPlaceholderOnRight"
@showNewDiscussionForm="showNewDiscussionForm"
>
<template #form>
<diff-line-note-form
:diff-file-hash="diffFileHash"
:line="line.right"
:note-target-line="line.right"
line-position="right"
/>
</template>
</diff-discussion-reply>
</td>
</tr>
</template>

View File

@ -12,6 +12,7 @@ import {
getNoteFormData,
convertExpandLines,
idleCallback,
allDiscussionWrappersExpanded,
} from './utils';
import * as types from './mutation_types';
import {
@ -79,6 +80,7 @@ export const assignDiscussionsToDiff = (
discussions = rootState.notes.discussions,
) => {
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
const hash = getLocationHash();
discussions
.filter(discussion => discussion.diff_discussion)
@ -86,6 +88,7 @@ export const assignDiscussionsToDiff = (
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
discussion,
diffPositionByLineCode,
hash,
});
});
@ -99,6 +102,10 @@ export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => {
commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash: file_hash, lineCode: line_code, id });
};
export const toggleLineDiscussions = ({ commit }, options) => {
commit(types.TOGGLE_LINE_DISCUSSIONS, options);
};
export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => {
const discussion = rootState.notes.discussions.find(d => d.id === discussionId);
@ -257,6 +264,31 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
});
};
export const toggleFileDiscussionWrappers = ({ commit }, diff) => {
const discussionWrappersExpanded = allDiscussionWrappersExpanded(diff);
let linesWithDiscussions;
if (diff.highlighted_diff_lines) {
linesWithDiscussions = diff.highlighted_diff_lines.filter(line => line.discussions.length);
}
if (diff.parallel_diff_lines) {
linesWithDiscussions = diff.parallel_diff_lines.filter(
line =>
(line.left && line.left.discussions.length) ||
(line.right && line.right.discussions.length),
);
}
if (linesWithDiscussions.length) {
linesWithDiscussions.forEach(line => {
commit(types.TOGGLE_LINE_DISCUSSIONS, {
fileHash: diff.file_hash,
lineCode: line.line_code,
expanded: !discussionWrappersExpanded,
});
});
}
};
export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
const postData = getNoteFormData({
commit: state.commit,
@ -267,7 +299,7 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
return dispatch('saveNote', postData, { root: true })
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
.then(() => dispatch('updateResolvableDiscussonsCounts', null, { root: true }))
.then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true }))
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
};

View File

@ -35,3 +35,5 @@ export const ADD_CURRENT_VIEW_DIFF_FILE_LINES = 'ADD_CURRENT_VIEW_DIFF_FILE_LINE
export const TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE';
export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER';
export const TOGGLE_LINE_DISCUSSIONS = 'TOGGLE_LINE_DISCUSSIONS';

View File

@ -6,6 +6,7 @@ import {
addContextLines,
prepareDiffData,
isDiscussionApplicableToLine,
updateLineInFile,
} from './utils';
import * as types from './mutation_types';
@ -109,7 +110,7 @@ export default {
}));
},
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode }) {
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) {
const { latestDiff } = state;
const discussionLineCode = discussion.line_code;
@ -130,13 +131,27 @@ export default {
: [],
});
const setDiscussionsExpanded = line => {
const isLineNoteTargeted = line.discussions.some(
disc => disc.notes && disc.notes.find(note => hash === `note_${note.id}`),
);
return {
...line,
discussionsExpanded:
line.discussions && line.discussions.length
? line.discussions.some(disc => !disc.resolved) || isLineNoteTargeted
: false,
};
};
state.diffFiles = state.diffFiles.map(diffFile => {
if (diffFile.file_hash === fileHash) {
const file = { ...diffFile };
if (file.highlighted_diff_lines) {
file.highlighted_diff_lines = file.highlighted_diff_lines.map(line =>
lineCheck(line) ? mapDiscussions(line) : line,
setDiscussionsExpanded(lineCheck(line) ? mapDiscussions(line) : line),
);
}
@ -148,8 +163,10 @@ export default {
if (left || right) {
return {
...line,
left: line.left ? mapDiscussions(line.left) : null,
right: line.right ? mapDiscussions(line.right, () => !left) : null,
left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null,
right: line.right
? setDiscussionsExpanded(mapDiscussions(line.right, () => !left))
: null,
};
}
@ -173,32 +190,11 @@ export default {
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
if (selectedFile) {
if (selectedFile.parallel_diff_lines) {
const targetLine = selectedFile.parallel_diff_lines.find(
line =>
(line.left && line.left.line_code === lineCode) ||
(line.right && line.right.line_code === lineCode),
);
if (targetLine) {
const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
Object.assign(targetLine[side], {
discussions: targetLine[side].discussions.filter(discussion => discussion.notes.length),
});
}
}
if (selectedFile.highlighted_diff_lines) {
const targetInlineLine = selectedFile.highlighted_diff_lines.find(
line => line.line_code === lineCode,
);
if (targetInlineLine) {
Object.assign(targetInlineLine, {
discussions: targetInlineLine.discussions.filter(discussion => discussion.notes.length),
});
}
}
updateLineInFile(selectedFile, lineCode, line =>
Object.assign(line, {
discussions: line.discussions.filter(discussion => discussion.notes.length),
}),
);
if (selectedFile.discussions && selectedFile.discussions.length) {
selectedFile.discussions = selectedFile.discussions.filter(
@ -207,6 +203,15 @@ export default {
}
}
},
[types.TOGGLE_LINE_DISCUSSIONS](state, { fileHash, lineCode, expanded }) {
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
updateLineInFile(selectedFile, lineCode, line =>
Object.assign(line, { discussionsExpanded: expanded }),
);
},
[types.TOGGLE_FOLDER_OPEN](state, path) {
state.treeEntries[path].opened = !state.treeEntries[path].opened;
},

View File

@ -454,3 +454,48 @@ export const convertExpandLines = ({
};
export const idleCallback = cb => requestIdleCallback(cb);
export const updateLineInFile = (selectedFile, lineCode, updateFn) => {
if (selectedFile.parallel_diff_lines) {
const targetLine = selectedFile.parallel_diff_lines.find(
line =>
(line.left && line.left.line_code === lineCode) ||
(line.right && line.right.line_code === lineCode),
);
if (targetLine) {
const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
updateFn(targetLine[side]);
}
}
if (selectedFile.highlighted_diff_lines) {
const targetInlineLine = selectedFile.highlighted_diff_lines.find(
line => line.line_code === lineCode,
);
if (targetInlineLine) {
updateFn(targetInlineLine);
}
}
};
export const allDiscussionWrappersExpanded = diff => {
const discussionsExpandedArray = [];
if (diff.parallel_diff_lines) {
diff.parallel_diff_lines.forEach(line => {
if (line.left && line.left.discussions.length) {
discussionsExpandedArray.push(line.left.discussionsExpanded);
}
if (line.right && line.right.discussions.length) {
discussionsExpandedArray.push(line.right.discussionsExpanded);
}
});
} else if (diff.highlighted_diff_lines) {
diff.parallel_diff_lines.forEach(line => {
if (line.discussions.length) {
discussionsExpandedArray.push(line.discussionsExpanded);
}
});
}
return discussionsExpandedArray.every(el => el);
};

View File

@ -1,5 +1,5 @@
import $ from 'jquery';
import { slugifyWithHyphens } from './lib/utils/text_utility';
import { slugify } from './lib/utils/text_utility';
export default class Group {
constructor() {
@ -14,7 +14,7 @@ export default class Group {
}
update() {
const slug = slugifyWithHyphens(this.groupName.val());
const slug = slugify(this.groupName.val());
this.groupPath.val(slug);
}

View File

@ -107,7 +107,8 @@ export default {
@click="openFileInEditor"
>
<span class="multi-file-commit-list-file-path d-flex align-items-center">
<file-icon :file-name="file.name" class="append-right-8" />{{ file.name }}
<file-icon :file-name="file.name" class="append-right-8" />
{{ file.name }}
</span>
<div class="ml-auto d-flex align-items-center">
<div class="d-flex align-items-center ide-commit-list-changed-icon">

View File

@ -27,7 +27,7 @@ export default {
target="_blank"
rel="noopener noreferrer"
>
<span class="vertical-align-middle">Open in file view</span>
<span class="vertical-align-middle">{{ __('Open in file view') }}</span>
<icon :size="16" name="external-link" css-classes="vertical-align-middle space-right" />
</a>
</div>

View File

@ -8,6 +8,7 @@ import { activityBarViews, viewerTypes } from '../constants';
import Editor from '../lib/editor';
import ExternalLink from './external_link.vue';
import FileTemplatesBar from './file_templates/bar.vue';
import { __ } from '~/locale';
export default {
components: {
@ -160,7 +161,14 @@ export default {
this.createEditorInstance();
})
.catch(err => {
flash('Error setting up editor. Please try again.', 'alert', document, null, false, true);
flash(
__('Error setting up editor. Please try again.'),
'alert',
document,
null,
false,
true,
);
throw err;
});
},
@ -247,12 +255,8 @@ export default {
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'editor' })"
>
<template v-if="viewer === $options.viewerTypes.edit">
{{ __('Edit') }}
</template>
<template v-else>
{{ __('Review') }}
</template>
<template v-if="viewer === $options.viewerTypes.edit">{{ __('Edit') }}</template>
<template v-else>{{ __('Review') }}</template>
</a>
</li>
<li v-if="file.previewMode" :class="previewTabCSS">
@ -260,9 +264,8 @@ export default {
href="javascript:void(0);"
role="button"
@click.prevent="setFileViewMode({ file, viewMode: 'preview' })"
>{{ file.previewMode.previewTitle }}</a
>
{{ file.previewMode.previewTitle }}
</a>
</li>
</ul>
<external-link :file="file" />

View File

@ -1,4 +1,5 @@
<script>
import { __, sprintf } from '~/locale';
import icon from '~/vue_shared/components/icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
import '~/lib/utils/datetime_utility';
@ -18,7 +19,9 @@ export default {
},
computed: {
lockTooltip() {
return `Locked by ${this.file.file_lock.user.name}`;
return sprintf(__(`Locked by %{fileLockUserName}`), {
fileLockUserName: this.file.file_lock.user.name,
});
},
},
};

View File

@ -1,4 +1,5 @@
<script>
import { __, sprintf } from '~/locale';
import { mapActions } from 'vuex';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@ -27,9 +28,9 @@ export default {
computed: {
closeLabel() {
if (this.fileHasChanged) {
return `${this.tab.name} changed`;
return sprintf(__(`%{tabname} changed`), { tabname: this.tab.name });
}
return `Close ${this.tab.name}`;
return sprintf(__(`Close %{tabname}`, { tabname: this.tab.name }));
},
showChangedIcon() {
if (this.tab.pending) return true;

View File

@ -44,11 +44,18 @@ export const pluralize = (str, count) => str + (count > 1 || count === 0 ? 's' :
export const dasherize = str => str.replace(/[_\s]+/g, '-');
/**
* Replaces whitespaces with hyphens and converts to lower case
* Replaces whitespaces with hyphens, convert to lower case and remove non-allowed special characters
* @param {String} str
* @returns {String}
*/
export const slugifyWithHyphens = str => str.toLowerCase().replace(/\s+/g, '-');
export const slugify = str => {
const slug = str
.trim()
.toLowerCase()
.replace(/[^a-zA-Z0-9_.-]+/g, '-');
return slug === '-' ? '' : slug;
};
/**
* Replaces whitespaces with underscore and converts to lower case

View File

@ -21,7 +21,7 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
const initManualOrdering = () => {
const issueList = document.querySelector('.manual-ordering');
if (!issueList || !(gon.features && gon.features.manualSorting)) {
if (!issueList || !(gon.features && gon.features.manualSorting) || !(gon.current_user_id > 0)) {
return;
}

View File

@ -39,15 +39,23 @@ export default {
</script>
<template>
<div class="discussion-with-resolve-btn clearfix">
<reply-placeholder class="qa-discussion-reply" @onClick="$emit('showReplyForm')" />
<div class="btn-group discussion-actions" role="group">
<resolve-discussion-button
v-if="discussion.resolvable"
:is-resolving="isResolving"
:button-title="resolveButtonTitle"
@onClick="$emit('resolve')"
<div class="discussion-with-resolve-btn">
<reply-placeholder
:button-text="s__('MergeRequests|Reply...')"
class="qa-discussion-reply"
@onClick="$emit('showReplyForm')"
/>
<resolve-discussion-button
v-if="discussion.resolvable"
:is-resolving="isResolving"
:button-title="resolveButtonTitle"
@onClick="$emit('resolve')"
/>
<div v-if="discussion.resolvable" class="btn-group discussion-actions ml-sm-2" role="group">
<resolve-with-issue-button v-if="resolveWithIssuePath" :url="resolveWithIssuePath" />
<jump-to-next-discussion-button
v-if="shouldShowJumpToNextDiscussion"
@onClick="$emit('jumpToNextDiscussion')"
/>
<resolve-with-issue-button
v-if="discussion.resolvable && resolveWithIssuePath"

View File

@ -1,11 +1,11 @@
<script>
import { mapGetters } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import { SYSTEM_NOTE } from '../constants';
import { __ } from '~/locale';
import NoteableNote from './noteable_note.vue';
import PlaceholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import SystemNote from '~/vue_shared/components/notes/system_note.vue';
import NoteableNote from './noteable_note.vue';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
import NoteEditedText from './note_edited_text.vue';
@ -72,6 +72,7 @@ export default {
},
},
methods: {
...mapActions(['toggleDiscussion']),
componentName(note) {
if (note.isPlaceholderNote) {
if (note.placeholderType === SYSTEM_NOTE) {
@ -101,7 +102,7 @@ export default {
<component
:is="componentName(firstNote)"
:note="componentData(firstNote)"
:line="line"
:line="line || diffLine"
:commit="commit"
:help-page-path="helpPagePath"
:show-reply-button="userCanReply"
@ -118,23 +119,29 @@ export default {
/>
<slot slot="avatar-badge" name="avatar-badge"></slot>
</component>
<toggle-replies-widget
v-if="hasReplies"
:collapsed="!isExpanded"
:replies="replies"
@toggle="$emit('toggleDiscussion')"
/>
<template v-if="isExpanded">
<component
:is="componentName(note)"
v-for="note in replies"
:key="note.id"
:note="componentData(note)"
:help-page-path="helpPagePath"
:line="line"
@handleDeleteNote="$emit('deleteNote')"
<div
:class="discussion.diff_discussion ? 'discussion-collapsible bordered-box clearfix' : ''"
>
<toggle-replies-widget
v-if="hasReplies"
:collapsed="!isExpanded"
:replies="replies"
:class="{ 'discussion-toggle-replies': discussion.diff_discussion }"
@toggle="toggleDiscussion({ discussionId: discussion.id })"
/>
</template>
<template v-if="isExpanded">
<component
:is="componentName(note)"
v-for="note in replies"
:key="note.id"
:note="componentData(note)"
:help-page-path="helpPagePath"
:line="line"
@handleDeleteNote="$emit('deleteNote')"
/>
</template>
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</div>
</template>
<template v-else>
<component
@ -148,8 +155,8 @@ export default {
>
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
</component>
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</template>
</ul>
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</div>
</template>

View File

@ -1,6 +1,12 @@
<script>
export default {
name: 'ReplyPlaceholder',
props: {
buttonText: {
type: String,
required: true,
},
},
};
</script>
@ -12,6 +18,6 @@ export default {
:title="s__('MergeRequests|Add a reply')"
@click="$emit('onClick')"
>
{{ s__('MergeRequests|Reply...') }}
{{ buttonText }}
</button>
</template>

View File

@ -132,7 +132,7 @@ export default {
return this.discussion.diff_discussion && this.renderDiffFile;
},
shouldGroupReplies() {
return !this.shouldRenderDiffs && !this.discussion.diff_discussion;
return !this.shouldRenderDiffs;
},
wrapperComponent() {
return this.shouldRenderDiffs ? diffWithNote : 'div';
@ -248,6 +248,11 @@ export default {
clearDraft(this.autosaveKey);
},
saveReply(noteText, form, callback) {
if (!noteText) {
this.cancelReplyForm();
callback();
return;
}
const postData = {
in_reply_to_discussion_id: this.discussion.reply_id,
target_type: this.getNoteableData.targetType,
@ -361,7 +366,6 @@ Please check your network connection and try again.`;
:line="line"
:should-group-replies="shouldGroupReplies"
@startReplying="showReplyForm"
@toggleDiscussion="toggleDiscussionHandler"
@deleteNote="deleteNoteHandler"
>
<slot slot="avatar-badge" name="avatar-badge"></slot>
@ -374,7 +378,7 @@ Please check your network connection and try again.`;
<div
v-else-if="showReplies"
:class="{ 'is-replying': isReplying }"
class="discussion-reply-holder"
class="discussion-reply-holder clearfix"
>
<user-avatar-link
v-if="!isReplying && userCanReply"

View File

@ -51,7 +51,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter }) =>
.then(res => res.json())
.then(discussions => {
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
dispatch('updateResolvableDiscussonsCounts');
dispatch('updateResolvableDiscussionsCounts');
});
export const updateDiscussion = ({ commit, state }, discussion) => {
@ -67,7 +67,7 @@ export const deleteNote = ({ commit, dispatch, state }, note) =>
commit(types.DELETE_NOTE, note);
dispatch('updateMergeRequestWidget');
dispatch('updateResolvableDiscussonsCounts');
dispatch('updateResolvableDiscussionsCounts');
if (isInMRPage()) {
dispatch('diffs/removeDiscussionsFromDiff', discussion);
@ -117,7 +117,7 @@ export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoi
dispatch('updateMergeRequestWidget');
dispatch('startTaskList');
dispatch('updateResolvableDiscussonsCounts');
dispatch('updateResolvableDiscussionsCounts');
} else {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
}
@ -135,7 +135,7 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
dispatch('updateMergeRequestWidget');
dispatch('startTaskList');
dispatch('updateResolvableDiscussonsCounts');
dispatch('updateResolvableDiscussionsCounts');
}
return res;
});
@ -168,7 +168,7 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved,
commit(mutationType, res);
dispatch('updateResolvableDiscussonsCounts');
dispatch('updateResolvableDiscussionsCounts');
dispatch('updateMergeRequestWidget');
});
@ -442,7 +442,7 @@ export const startTaskList = ({ dispatch }) =>
}),
);
export const updateResolvableDiscussonsCounts = ({ commit }) =>
export const updateResolvableDiscussionsCounts = ({ commit }) =>
commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS);
export const submitSuggestion = (

View File

@ -4,14 +4,12 @@ import { glEmojiTag } from '~/emoji';
import detailedMetric from './detailed_metric.vue';
import requestSelector from './request_selector.vue';
import simpleMetric from './simple_metric.vue';
import { s__ } from '~/locale';
export default {
components: {
detailedMetric,
requestSelector,
simpleMetric,
},
props: {
store: {
@ -43,8 +41,13 @@ export default {
details: 'details',
keys: ['feature', 'request'],
},
{
metric: 'redis',
header: 'Redis calls',
details: 'details',
keys: ['cmd'],
},
],
simpleMetrics: ['redis'],
data() {
return { currentRequestId: '' };
},
@ -124,12 +127,6 @@ export default {
</button>
<a v-else :href="profileUrl">{{ s__('PerformanceBar|profile') }}</a>
</div>
<simple-metric
v-for="metric in $options.simpleMetrics"
:key="metric"
:current-request="currentRequest"
:metric="metric"
/>
<div id="peek-view-gc" class="view">
<span v-if="currentRequest.details" class="bold">
<span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span

View File

@ -1,33 +0,0 @@
<script>
export default {
props: {
currentRequest: {
type: Object,
required: true,
},
metric: {
type: String,
required: true,
},
},
computed: {
duration() {
return (
this.currentRequest.details[this.metric] &&
this.currentRequest.details[this.metric].duration
);
},
calls() {
return (
this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls
);
},
},
};
</script>
<template>
<div :id="`peek-view-${metric}`" class="view">
<span v-if="currentRequest.details" class="bold"> {{ duration }} / {{ calls }} </span>
{{ metric }}
</div>
</template>

View File

@ -1,6 +1,6 @@
import $ from 'jquery';
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
import { slugifyWithHyphens } from '../lib/utils/text_utility';
import { slugify } from '../lib/utils/text_utility';
import { s__ } from '~/locale';
let hasUserDefinedProjectPath = false;
@ -34,7 +34,7 @@ const deriveProjectPathFromUrl = $projectImportUrl => {
};
const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
const slug = slugifyWithHyphens($projectNameInput.val());
const slug = slugify($projectNameInput.val());
$projectPathInput.val(slug);
};

View File

@ -3,22 +3,81 @@ import { mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import store from '../stores';
import CollapsibleContainer from './collapsible_container.vue';
import SvgMessage from './svg_message.vue';
import { s__, sprintf } from '../../locale';
export default {
name: 'RegistryListApp',
components: {
CollapsibleContainer,
GlLoadingIcon,
SvgMessage,
},
props: {
endpoint: {
type: String,
required: true,
},
characterError: {
type: Boolean,
required: false,
default: false,
},
helpPagePath: {
type: String,
required: true,
},
noContainersImage: {
type: String,
required: true,
},
containersErrorImage: {
type: String,
required: true,
},
repositoryUrl: {
type: String,
required: true,
},
},
store,
computed: {
...mapGetters(['isLoading', 'repos']),
dockerConnectionErrorText() {
return sprintf(
s__(`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an
issue with your project name or path. For more information, please review the
%{docLinkStart}Container Registry documentation%{docLinkEnd}.`),
{
docLinkStart: `<a href="${this.helpPagePath}#docker-connection-error">`,
docLinkEnd: '</a>',
},
false,
);
},
introText() {
return sprintf(
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
project can have its own space to store its Docker images. Learn more about the
%{docLinkStart}Container Registry%{docLinkEnd}.`),
{
docLinkStart: `<a href="${this.helpPagePath}">`,
docLinkEnd: '</a>',
},
false,
);
},
noContainerImagesText() {
return sprintf(
s__(`ContainerRegistry|With the Container Registry, every project can have its own space to
store its Docker images. Learn more about the %{docLinkStart}Container Registry%{docLinkEnd}.`),
{
docLinkStart: `<a href="${this.helpPagePath}">`,
docLinkEnd: '</a>',
},
false,
);
},
},
created() {
this.setMainEndpoint(this.endpoint);
@ -33,20 +92,44 @@ export default {
</script>
<template>
<div>
<gl-loading-icon v-if="isLoading" size="md" />
<svg-message v-if="characterError" id="invalid-characters" :svg-path="containersErrorImage">
<h4>
{{ s__('ContainerRegistry|Docker connection error') }}
</h4>
<p v-html="dockerConnectionErrorText"></p>
</svg-message>
<collapsible-container
v-for="item in repos"
v-else-if="!isLoading && repos.length"
:key="item.id"
:repo="item"
/>
<gl-loading-icon v-else-if="isLoading" size="md" class="prepend-top-16" />
<p v-else-if="!isLoading && !repos.length">
{{
__(`No container images stored for this project.
Add one by following the instructions above.`)
}}
</p>
<div v-else-if="!isLoading && !characterError && repos.length">
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<p v-html="introText"></p>
<collapsible-container v-for="item in repos" :key="item.id" :repo="item" />
</div>
<svg-message
v-else-if="!isLoading && !characterError && !repos.length"
id="no-container-images"
:svg-path="noContainersImage"
>
<h4>
{{ s__('ContainerRegistry|There are no container images stored for this project') }}
</h4>
<p v-html="noContainerImagesText"></p>
<h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
<p>
{{
s__(
'ContainerRegistry|You can add an image to this registry with the following commands:',
)
}}
</p>
<pre>
docker build -t {{ repositoryUrl }} .
docker push {{ repositoryUrl }}
</pre>
</svg-message>
</div>
</template>

View File

@ -0,0 +1,24 @@
<script>
export default {
name: 'RegistrySvgMessage',
props: {
id: {
type: String,
required: true,
},
svgPath: {
type: String,
required: true,
},
},
};
</script>
<template>
<div :id="id" class="empty-state container-message mw-70p">
<div class="svg-content">
<img :src="svgPath" class="flex-align-self-center" />
</div>
<slot></slot>
</div>
</template>

View File

@ -14,12 +14,22 @@ export default () =>
const { dataset } = document.querySelector(this.$options.el);
return {
endpoint: dataset.endpoint,
characterError: Boolean(dataset.characterError),
helpPagePath: dataset.helpPagePath,
noContainersImage: dataset.noContainersImage,
containersErrorImage: dataset.containersErrorImage,
repositoryUrl: dataset.repositoryUrl,
};
},
render(createElement) {
return createElement('registry-app', {
props: {
endpoint: this.endpoint,
characterError: this.characterError,
helpPagePath: this.helpPagePath,
noContainersImage: this.noContainersImage,
containersErrorImage: this.containersErrorImage,
repositoryUrl: this.repositoryUrl,
},
});
},

View File

@ -28,7 +28,7 @@ export default {
computed: {
releasedTimeAgo() {
return sprintf(__('released %{time}'), {
time: this.timeFormated(this.release.created_at),
time: this.timeFormated(this.release.released_at),
});
},
userImageAltDescription() {
@ -56,8 +56,8 @@ export default {
<div class="card-body">
<h2 class="card-title mt-0">
{{ release.name }}
<gl-badge v-if="release.pre_release" variant="warning" class="align-middle">{{
__('Pre-release')
<gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
__('Upcoming Release')
}}</gl-badge>
</h2>
@ -74,7 +74,7 @@ export default {
<div class="append-right-4">
&bull;
<span v-gl-tooltip.bottom :title="tooltipTitle(release.created_at)">
<span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)">
{{ releasedTimeAgo }}
</span>
</div>

View File

@ -39,7 +39,7 @@ export default {
</script>
<template>
<timeline-entry-item class="note being-posted fade-in-half">
<timeline-entry-item class="note note-wrapper being-posted fade-in-half">
<div class="timeline-icon">
<user-avatar-link
:link-href="getUserData.path"

View File

@ -2,6 +2,12 @@
* Container Registry
*/
.container-message {
pre {
white-space: pre-line;
}
}
.container-image {
border-bottom: 1px solid $white-normal;
}

View File

@ -1093,6 +1093,17 @@ table.code {
line-height: 0;
}
.discussion-collapsible {
margin: 0 $gl-padding $gl-padding 71px;
}
.parallel {
.discussion-collapsible {
margin: $gl-padding;
margin-top: 0;
}
}
@media (max-width: map-get($grid-breakpoints, md)-1) {
.diffs .files {
@include fixed-width-container;
@ -1110,6 +1121,11 @@ table.code {
padding-right: 0;
}
}
.discussion-collapsible {
margin: $gl-padding;
margin-top: 0;
}
}
.image-diff-overlay,

View File

@ -134,6 +134,16 @@ $note-form-margin-left: 72px;
}
}
.discussion-toggle-replies {
border-top: 0;
border-radius: 4px 4px 0 0;
&.collapsed {
border: 0;
border-radius: 4px;
}
}
.note-created-ago,
.note-updated-at {
white-space: normal;
@ -462,6 +472,14 @@ $note-form-margin-left: 72px;
position: relative;
}
.notes-content .discussion-notes.diff-discussions {
border-bottom: 1px solid $border-color;
&:nth-last-child(1) {
border-bottom: 0;
}
}
.notes_holder {
font-family: $regular-font;
@ -517,6 +535,17 @@ $note-form-margin-left: 72px;
.discussion-reply-holder {
border-radius: 0 0 $border-radius-default $border-radius-default;
position: relative;
.discussion-form {
width: 100%;
background-color: $gray-light;
padding: 0;
}
.disabled-comment {
padding: $gl-vert-padding 0;
width: 100%;
}
}
}

View File

@ -46,6 +46,8 @@ module Projects
repository.save! if repository.has_tags?
end
end
rescue ContainerRegistry::Path::InvalidRegistryPathError
@character_error = true
end
end
end

View File

@ -99,7 +99,7 @@ module Projects
end
def deploy_token_params
params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry)
params.require(:deploy_token).permit(:name, :expires_at, :read_repository, :read_registry, :username)
end
end
end

View File

@ -35,11 +35,8 @@ module Clusters
'stable/nginx-ingress'
end
# We will implement this in future MRs.
# Basically we need to check all dependent applications are not installed
# first.
def allowed_to_uninstall?
false
external_ip_or_hostname? && application_jupyter_nil_or_installable?
end
def install_command
@ -52,6 +49,10 @@ module Clusters
)
end
def external_ip_or_hostname?
external_ip.present? || external_hostname.present?
end
def schedule_status_update
return unless installed?
return if external_ip
@ -63,6 +64,12 @@ module Clusters
def ingress_service
cluster.kubeclient.get_service('ingress-nginx-ingress-controller', Gitlab::Kubernetes::Helm::NAMESPACE)
end
private
def application_jupyter_nil_or_installable?
cluster.application_jupyter.nil? || cluster.application_jupyter&.installable?
end
end
end
end

View File

@ -23,9 +23,7 @@ module Clusters
return unless cluster&.application_ingress_available?
ingress = cluster.application_ingress
if ingress.external_ip || ingress.external_hostname
self.status = 'installable'
end
self.status = 'installable' if ingress.external_ip_or_hostname?
end
def chart

View File

@ -49,14 +49,6 @@ module Clusters
)
end
def uninstall_command
Gitlab::Kubernetes::Helm::DeleteCommand.new(
name: name,
rbac: cluster.platform_kubernetes_rbac?,
files: files
)
end
def upgrade_command(values)
::Gitlab::Kubernetes::Helm::InstallCommand.new(
name: name,

View File

@ -19,9 +19,9 @@
#
# - `statistic_attribute` must be an ActiveRecord attribute
# - The model must implement `project` and `project_id`. i.e. direct Project relationship or delegation
#
module UpdateProjectStatistics
extend ActiveSupport::Concern
include AfterCommitQueue
class_methods do
attr_reader :project_statistics_name, :statistic_attribute
@ -31,7 +31,6 @@ module UpdateProjectStatistics
#
# - project_statistics_name: A column of `ProjectStatistics` to update
# - statistic_attribute: An attribute of the current model, default to `size`
#
def update_project_statistics(project_statistics_name:, statistic_attribute: :size)
@project_statistics_name = project_statistics_name
@statistic_attribute = statistic_attribute
@ -51,6 +50,7 @@ module UpdateProjectStatistics
delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i
update_project_statistics(delta)
schedule_namespace_aggregation_worker
end
def update_project_statistics_attribute_changed?
@ -59,6 +59,8 @@ module UpdateProjectStatistics
def update_project_statistics_after_destroy
update_project_statistics(-read_attribute(self.class.statistic_attribute).to_i)
schedule_namespace_aggregation_worker
end
def project_destroyed?
@ -68,5 +70,18 @@ module UpdateProjectStatistics
def update_project_statistics(delta)
ProjectStatistics.increment_statistic(project_id, self.class.project_statistics_name, delta)
end
def schedule_namespace_aggregation_worker
run_after_commit do
next unless schedule_aggregation_worker?
Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id)
end
end
def schedule_aggregation_worker?
!project.nil? &&
Feature.enabled?(:update_statistics_namespace, project.root_ancestor)
end
end
end

View File

@ -16,6 +16,14 @@ class DeployToken < ApplicationRecord
has_many :projects, through: :project_deploy_tokens
validate :ensure_at_least_one_scope
validates :username,
length: { maximum: 255 },
allow_nil: true,
format: {
with: /\A[a-zA-Z0-9\.\+_-]+\z/,
message: "can contain only letters, digits, '_', '-', '+', and '.'"
}
before_save :ensure_token
accepts_nested_attributes_for :project_deploy_tokens
@ -39,7 +47,7 @@ class DeployToken < ApplicationRecord
end
def username
"gitlab+deploy-token-#{id}"
super || default_username
end
def has_access_to?(requested_project)
@ -75,4 +83,8 @@ class DeployToken < ApplicationRecord
def ensure_at_least_one_scope
errors.add(:base, "Scopes can't be blank") unless read_repository || read_registry
end
def default_username
"gitlab+deploy-token-#{id}" if persisted?
end
end

View File

@ -293,6 +293,10 @@ class Namespace < ApplicationRecord
end
end
def aggregation_scheduled?
aggregation_schedule.present?
end
private
def parent_changed?

View File

@ -1,7 +1,47 @@
# frozen_string_literal: true
class Namespace::AggregationSchedule < ApplicationRecord
include AfterCommitQueue
include ExclusiveLeaseGuard
self.primary_key = :namespace_id
DEFAULT_LEASE_TIMEOUT = 3.hours
REDIS_SHARED_KEY = 'gitlab:update_namespace_statistics_delay'.freeze
belongs_to :namespace
after_create :schedule_root_storage_statistics
def self.delay_timeout
redis_timeout = Gitlab::Redis::SharedState.with do |redis|
redis.get(REDIS_SHARED_KEY)
end
redis_timeout.nil? ? DEFAULT_LEASE_TIMEOUT : redis_timeout.to_i
end
def schedule_root_storage_statistics
run_after_commit_or_now do
try_obtain_lease do
Namespaces::RootStatisticsWorker
.perform_async(namespace_id)
Namespaces::RootStatisticsWorker
.perform_in(self.class.delay_timeout, namespace_id)
end
end
end
private
# Used by ExclusiveLeaseGuard
def lease_timeout
self.class.delay_timeout
end
# Used by ExclusiveLeaseGuard
def lease_key
"namespace:namespaces_root_statistics:#{namespace_id}"
end
end

View File

@ -1,10 +1,38 @@
# frozen_string_literal: true
class Namespace::RootStorageStatistics < ApplicationRecord
STATISTICS_ATTRIBUTES = %w(storage_size repository_size wiki_size lfs_objects_size build_artifacts_size packages_size).freeze
self.primary_key = :namespace_id
belongs_to :namespace
has_one :route, through: :namespace
delegate :all_projects, to: :namespace
def recalculate!
update!(attributes_from_project_statistics)
end
private
def attributes_from_project_statistics
from_project_statistics
.take
.attributes
.slice(*STATISTICS_ATTRIBUTES)
end
def from_project_statistics
all_projects
.joins('INNER JOIN project_statistics ps ON ps.project_id = projects.id')
.select(
'COALESCE(SUM(ps.storage_size), 0) AS storage_size',
'COALESCE(SUM(ps.repository_size), 0) AS repository_size',
'COALESCE(SUM(ps.wiki_size), 0) AS wiki_size',
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
'COALESCE(SUM(ps.packages_size), 0) AS packages_size'
)
end
end

View File

@ -3,22 +3,14 @@
class BugzillaService < IssueTrackerService
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
prop_accessor :project_url, :issues_url, :new_issue_url
def title
if self.properties && self.properties['title'].present?
self.properties['title']
else
'Bugzilla'
end
def default_title
'Bugzilla'
end
def description
if self.properties && self.properties['description'].present?
self.properties['description']
else
'Bugzilla issue tracker'
end
def default_description
s_('IssueTracker|Bugzilla issue tracker')
end
def self.to_param

View File

@ -5,24 +5,12 @@ class CustomIssueTrackerService < IssueTrackerService
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
def title
if self.properties && self.properties['title'].present?
self.properties['title']
else
'Custom Issue Tracker'
end
def default_title
'Custom Issue Tracker'
end
def title=(value)
self.properties['title'] = value if self.properties
end
def description
if self.properties && self.properties['description'].present?
self.properties['description']
else
'Custom issue tracker'
end
def default_description
s_('IssueTracker|Custom issue tracker')
end
def self.to_param

View File

@ -5,10 +5,18 @@ class GitlabIssueTrackerService < IssueTrackerService
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
prop_accessor :project_url, :issues_url, :new_issue_url
default_value_for :default, true
def default_title
'GitLab'
end
def default_description
s_('IssueTracker|GitLab issue tracker')
end
def self.to_param
'gitlab'
end

View File

@ -5,6 +5,8 @@ class IssueTrackerService < Service
default_value_for :category, 'issue_tracker'
before_save :handle_properties
# Pattern used to extract links from comments
# Override this method on services that uses different patterns
# This pattern does not support cross-project references
@ -18,6 +20,37 @@ class IssueTrackerService < Service
end
end
# this will be removed as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
def title
if title_attribute = read_attribute(:title)
title_attribute
elsif self.properties && self.properties['title'].present?
self.properties['title']
else
default_title
end
end
# this will be removed as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
def description
if description_attribute = read_attribute(:description)
description_attribute
elsif self.properties && self.properties['description'].present?
self.properties['description']
else
default_description
end
end
def handle_properties
properties.slice('title', 'description').each do |key, _|
current_value = self.properties.delete(key)
value = attribute_changed?(key) ? attribute_change(key).last : current_value
write_attribute(key, value)
end
end
def default?
default
end

View File

@ -17,7 +17,7 @@ class JiraService < IssueTrackerService
# Jira Cloud version is deprecating authentication via username and password.
# We should use username/password for Jira Server and email/api_token for Jira Cloud,
# for more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/49936.
prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description
prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id
before_update :reset_password
@ -37,7 +37,6 @@ class JiraService < IssueTrackerService
def initialize_properties
super do
self.properties = {
title: issues_tracker['title'],
url: issues_tracker['url'],
api_url: issues_tracker['api_url']
}
@ -74,20 +73,12 @@ class JiraService < IssueTrackerService
[Jira service documentation](#{help_page_url('user/project/integrations/jira')})."
end
def title
if self.properties && self.properties['title'].present?
self.properties['title']
else
'Jira'
end
def default_title
'Jira'
end
def description
if self.properties && self.properties['description'].present?
self.properties['description']
else
s_('JiraService|Jira issue tracker')
end
def default_description
s_('JiraService|Jira issue tracker')
end
def self.to_param

View File

@ -3,22 +3,14 @@
class RedmineService < IssueTrackerService
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
prop_accessor :project_url, :issues_url, :new_issue_url
def title
if self.properties && self.properties['title'].present?
self.properties['title']
else
'Redmine'
end
def default_title
'Redmine'
end
def description
if self.properties && self.properties['description'].present?
self.properties['description']
else
'Redmine issue tracker'
end
def default_description
s_('IssueTracker|Redmine issue tracker')
end
def self.to_param

View File

@ -3,7 +3,7 @@
class YoutrackService < IssueTrackerService
validates :project_url, :issues_url, presence: true, public_url: true, if: :activated?
prop_accessor :description, :project_url, :issues_url
prop_accessor :project_url, :issues_url
# {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030
def self.reference_pattern(only_long: false)
@ -14,16 +14,12 @@ class YoutrackService < IssueTrackerService
end
end
def title
def default_title
'YouTrack'
end
def description
if self.properties && self.properties['description'].present?
self.properties['description']
else
'YouTrack issue tracker'
end
def default_description
s_('IssueTracker|YouTrack issue tracker')
end
def self.to_param

View File

@ -12,12 +12,16 @@ class Release < ApplicationRecord
has_many :links, class_name: 'Releases::Link'
default_value_for :released_at, allows_nil: false do
Time.zone.now
end
accepts_nested_attributes_for :links, allow_destroy: true
validates :description, :project, :tag, presence: true
validates :name, presence: true, on: :create
scope :sorted, -> { order(created_at: :desc) }
scope :sorted, -> { order(released_at: :desc) }
delegate :repository, to: :project
@ -44,6 +48,10 @@ class Release < ApplicationRecord
end
end
def upcoming_release?
released_at.present? && released_at > Time.zone.now
end
private
def actual_sha

View File

@ -129,7 +129,7 @@ class Service < ApplicationRecord
def api_field_names
fields.map { |field| field[:name] }
.reject { |field_name| field_name =~ /(password|token|key)/ }
.reject { |field_name| field_name =~ /(password|token|key|title|description)/ }
end
def global_fields

View File

@ -3,7 +3,9 @@
module DeployTokens
class CreateService < BaseService
def execute
@project.deploy_tokens.create(params)
@project.deploy_tokens.create(params) do |deploy_token|
deploy_token.username = params[:username].presence
end
end
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Namespaces
class StatisticsRefresherService
RefresherError = Class.new(StandardError)
def execute(root_namespace)
root_storage_statistics = find_or_create_root_storage_statistics(root_namespace.id)
root_storage_statistics.recalculate!
rescue ActiveRecord::ActiveRecordError => e
raise RefresherError.new(e.message)
end
private
def find_or_create_root_storage_statistics(root_namespace_id)
Namespace::RootStorageStatistics
.safe_find_or_create_by!(namespace_id: root_namespace_id)
end
end
end

View File

@ -22,6 +22,10 @@ module Releases
params[:description]
end
def released_at
params[:released_at]
end
def release
strong_memoize(:release) do
project.releases.find_by_tag(tag_name)

View File

@ -58,6 +58,7 @@ module Releases
author: current_user,
tag: tag.name,
sha: tag.dereferenced_target.sha,
released_at: released_at,
links_attributes: params.dig(:assets, 'links') || []
)
end

View File

@ -1,4 +1,4 @@
= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster do |field|
= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster, html: { class: 'cluster_integration_form' } do |field|
= form_errors(@cluster)
.form-group
%h5= s_('ClusterIntegration|Integration status')

View File

@ -13,7 +13,7 @@
= f.text_field :id, class: 'form-control w-auto', readonly: true
.row.prepend-top-8
.form-group.col-md-9.append-bottom-0
.form-group.col-md-9
= f.label :description, _('Group description (optional)'), class: 'label-bold'
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250

View File

@ -12,6 +12,11 @@
= f.label :expires_at, class: 'label-bold'
= f.text_field :expires_at, class: 'datepicker form-control qa-deploy-token-expires-at', value: f.object.expires_at
.form-group
= f.label :username, class: 'label-bold'
= f.text_field :username, class: 'form-control qa-deploy-token-username'
.text-secondary= s_('DeployTokens|Default format is "gitlab+deploy-token-{n}". Enter custom username if you want to change it.')
.form-group
= f.label :scopes, class: 'label-bold'
%fieldset.form-group.form-check

View File

@ -1,49 +1,9 @@
- page_title "Container Registry"
%section
.settings-header
%h4
= page_title
%p
= s_('ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images.')
%p.append-bottom-0
= succeed '.' do
= s_('ContainerRegistry|Learn more about')
= link_to _('Container Registry'), help_page_path('user/project/container_registry'), target: '_blank'
.row.registry-placeholder.prepend-bottom-10
.col-lg-12
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } }
.row.prepend-top-10
.col-lg-12
.card
.card-header
= s_('ContainerRegistry|How to use the Container Registry')
.card-body
%p
- link_token = link_to(_('personal access token'), help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank')
- link_2fa = link_to(_('2FA enabled'), help_page_path('user/profile/account/two_factor_authentication'), target: '_blank')
= s_('ContainerRegistry|First log in to GitLab&rsquo;s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:').html_safe % { link_2fa: link_2fa, link_token: link_token }
%pre
docker login #{Gitlab.config.registry.host_port}
%br
%p
- 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 use a %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token }
%br
%p
= s_('ContainerRegistry|Once you log in, you&rsquo;re free to create and upload a container image using the common %{build} and %{push} commands').html_safe % { build: "<code>build</code>".html_safe, push: "<code>push</code>".html_safe }
%pre
:plain
docker build -t #{escape_once(@project.container_registry_url)} .
docker push #{escape_once(@project.container_registry_url)}
%hr
%h5.prepend-top-default
= s_('ContainerRegistry|Use different image names')
%p.light
= s_('ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:')
%pre
:plain
#{escape_once(@project.container_registry_url)}:tag
#{escape_once(@project.container_registry_url)}/optional-image-name:tag
#{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
.col-12
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json),
"help_page_path" => help_page_path('user/project/container_registry'),
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"repository_url" => escape_once(@project.container_registry_url),
character_error: @character_error.to_s } }

View File

@ -26,6 +26,7 @@
- cronjob:issue_due_scheduler
- cronjob:prune_web_hook_logs
- cronjob:schedule_migrate_external_diffs
- cronjob:namespaces_prune_aggregation_schedules
- gcp_cluster:cluster_install_app
- gcp_cluster:cluster_patch_app
@ -101,6 +102,9 @@
- todos_destroyer:todos_destroyer_project_private
- todos_destroyer:todos_destroyer_private_features
- update_namespace_statistics:namespaces_schedule_aggregation
- update_namespace_statistics:namespaces_root_statistics
- object_pool:object_pool_create
- object_pool:object_pool_schedule_join
- object_pool:object_pool_join

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Namespaces
class PruneAggregationSchedulesWorker
include ApplicationWorker
include CronjobQueue
# Worker to prune pending rows on Namespace::AggregationSchedule
# It's scheduled to run once a day at 1:05am.
def perform
aggregation_schedules.find_each do |aggregation_schedule|
aggregation_schedule.schedule_root_storage_statistics
end
end
private
def aggregation_schedules
Namespace::AggregationSchedule.all
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module Namespaces
class RootStatisticsWorker
include ApplicationWorker
queue_namespace :update_namespace_statistics
def perform(namespace_id)
namespace = Namespace.find(namespace_id)
return unless update_statistics_enabled_for?(namespace) && namespace.aggregation_scheduled?
Namespaces::StatisticsRefresherService.new.execute(namespace)
namespace.aggregation_schedule.destroy
rescue ::Namespaces::StatisticsRefresherService::RefresherError, ActiveRecord::RecordNotFound => ex
log_error(namespace.full_path, ex.message) if namespace
end
private
def log_error(namespace_path, error_message)
Gitlab::SidekiqLogger.error("Namespace statistics can't be updated for #{namespace_path}: #{error_message}")
end
def update_statistics_enabled_for?(namespace)
Feature.enabled?(:update_statistics_namespace, namespace)
end
end
end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
module Namespaces
class ScheduleAggregationWorker
include ApplicationWorker
queue_namespace :update_namespace_statistics
def perform(namespace_id)
return unless aggregation_schedules_table_exists?
namespace = Namespace.find(namespace_id)
root_ancestor = namespace.root_ancestor
return unless update_statistics_enabled_for?(root_ancestor) && !root_ancestor.aggregation_scheduled?
Namespace::AggregationSchedule.safe_find_or_create_by!(namespace_id: root_ancestor.id)
rescue ActiveRecord::RecordNotFound
log_error(namespace_id)
end
private
# On db/post_migrate/20180529152628_schedule_to_archive_legacy_traces.rb
# traces are archived through build.trace.archive, which in consequence
# calls UpdateProjectStatistics#schedule_namespace_statistics_worker.
#
# The migration and specs fails since NamespaceAggregationSchedule table
# does not exist at that point.
# https://gitlab.com/gitlab-org/gitlab-ce/issues/50712
def aggregation_schedules_table_exists?
return true unless Rails.env.test?
Namespace::AggregationSchedule.table_exists?
end
def log_error(root_ancestor_id)
Gitlab::SidekiqLogger.error("Namespace can't be scheduled for aggregation: #{root_ancestor_id} does not exist")
end
def update_statistics_enabled_for?(root_ancestor)
Feature.enabled?(:update_statistics_namespace, root_ancestor)
end
end
end

View File

@ -0,0 +1,5 @@
---
title: Resolve Multiple discussions per line in merge request diffs
merge_request: 28748
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Updated container registry to display error message when special characters in path. Documentation has also been updated.
merge_request: 29616
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Allow custom username for deploy tokens
merge_request: 29639
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Implement borderless discussion design with new reply field
merge_request: 28580
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Remove istanbul JavaScript package
merge_request: 30232
author: Takuya Noguchi
type: other

View File

@ -0,0 +1,5 @@
---
title: Allow Ingress to be uninstalled from the UI
merge_request: 29977
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Show an Upcoming Status for Releases
merge_request: 29577
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Don't let logged out user do manual order
merge_request: 30264
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Cache Flipper persisted names directly to local memory storage
merge_request: 30265
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Add Redis call details in Peek performance bar
merge_request: 30191
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Replace slugifyWithHyphens with improved slugify function
merge_request: 30172
author: Luke Ward
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Make sure UnicornSampler is started only in master process.
merge_request: 30215
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Use PostgreSQL 9.6.11 in CI tests
merge_request: 30270
author: Takuya Noguchi
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix typo in updateResolvableDiscussionsCounts action
merge_request: 30278
author: Frank van Rest
type: other

View File

@ -441,6 +441,9 @@ Settings.cron_jobs['prune_web_hook_logs_worker']['job_class'] = 'PruneWebHookLog
Settings.cron_jobs['schedule_migrate_external_diffs_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['schedule_migrate_external_diffs_worker']['cron'] ||= '15 * * * *'
Settings.cron_jobs['schedule_migrate_external_diffs_worker']['job_class'] = 'ScheduleMigrateExternalDiffsWorker'
Settings.cron_jobs['namespaces_prune_aggregation_schedules_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['namespaces_prune_aggregation_schedules_worker']['cron'] ||= '5 1 * * *'
Settings.cron_jobs['namespaces_prune_aggregation_schedules_worker']['job_class'] = 'Namespaces::PruneAggregationSchedulesWorker'
Gitlab.ee do
Settings.cron_jobs['clear_shared_runners_minutes_worker'] ||= Settingslogic.new({})

View File

@ -1,15 +1,27 @@
require 'prometheus/client'
require 'prometheus/client/support/unicorn'
# Keep separate directories for separate processes
def prometheus_default_multiproc_dir
return unless Rails.env.development? || Rails.env.test?
if Sidekiq.server?
Rails.root.join('tmp/prometheus_multiproc_dir/sidekiq')
elsif defined?(Unicorn::Worker)
Rails.root.join('tmp/prometheus_multiproc_dir/unicorn')
elsif defined?(::Puma)
Rails.root.join('tmp/prometheus_multiproc_dir/puma')
else
Rails.root.join('tmp/prometheus_multiproc_dir')
end
end
Prometheus::Client.configure do |config|
config.logger = Rails.logger
config.initial_mmap_file_size = 4 * 1024
config.multiprocess_files_dir = ENV['prometheus_multiproc_dir']
if Rails.env.development? || Rails.env.test?
config.multiprocess_files_dir ||= Rails.root.join('tmp/prometheus_multiproc_dir')
end
config.multiprocess_files_dir = ENV['prometheus_multiproc_dir'] || prometheus_default_multiproc_dir
config.pid_provider = Prometheus::Client::Support::Unicorn.method(:worker_pid_provider)
end
@ -29,15 +41,13 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
Gitlab::Cluster::LifecycleEvents.on_worker_start do
defined?(::Prometheus::Client.reinitialize_on_pid_change) && Prometheus::Client.reinitialize_on_pid_change
if defined?(::Unicorn)
Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
end
Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start
end
if defined?(::Puma)
Gitlab::Cluster::LifecycleEvents.on_master_start do
Gitlab::Cluster::LifecycleEvents.on_master_start do
if defined?(::Unicorn)
Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
elsif defined?(::Puma)
Gitlab::Metrics::Samplers::PumaSampler.initialize_instance(Settings.monitoring.puma_sampler_interval).start
end
end

View File

@ -4,12 +4,22 @@
ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
if [:throttle, :blacklist].include? req.env['rack.attack.match_type']
Gitlab::AuthLogger.error(
rack_attack_info = {
message: 'Rack_Attack',
env: req.env['rack.attack.match_type'],
ip: req.ip,
request_method: req.request_method,
fullpath: req.fullpath
)
}
if req.env['rack.attack.matched'] != 'throttle_unauthenticated'
user_id = req.env['rack.attack.match_discriminator']
user = User.find_by(id: user_id)
rack_attack_info[:user_id] = user_id
rack_attack_info[:username] = user.username unless user.nil?
end
Gitlab::AuthLogger.error(rack_attack_info)
end
end

View File

@ -94,6 +94,7 @@
- [migrate_external_diffs, 1]
- [update_project_statistics, 1]
- [phabricator_import_import_tasks, 1]
- [update_namespace_statistics, 1]
# EE-specific queues
- [ldap_group_sync, 2]

View File

@ -1,338 +0,0 @@
class InitSchema < ActiveRecord::Migration[4.2]
DOWNTIME = true
# rubocop:disable Metrics/AbcSize
def up
create_table "broadcast_messages", force: :cascade do |t|
t.text "message", null: false
t.datetime "starts_at"
t.datetime "ends_at"
t.integer "alert_type"
t.datetime "created_at"
t.datetime "updated_at"
t.string "color"
t.string "font"
end
create_table "deploy_keys_projects", force: :cascade do |t|
t.integer "deploy_key_id", null: false
t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
create_table "emails", force: :cascade do |t|
t.integer "user_id", null: false
t.string "email", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "emails", ["email"], name: "index_emails_on_email", unique: true, using: :btree
add_index "emails", ["user_id"], name: "index_emails_on_user_id", using: :btree
create_table "events", force: :cascade do |t|
t.string "target_type"
t.integer "target_id"
t.string "title"
t.text "data"
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "action"
t.integer "author_id"
end
add_index "events", ["action"], name: "index_events_on_action", using: :btree
add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree
add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree
add_index "events", ["project_id"], name: "index_events_on_project_id", using: :btree
add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree
add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree
create_table "forked_project_links", force: :cascade do |t|
t.integer "forked_to_project_id", null: false
t.integer "forked_from_project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
create_table "issues", force: :cascade do |t|
t.string "title"
t.integer "assignee_id"
t.integer "author_id"
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "position", default: 0
t.string "branch_name"
t.text "description"
t.integer "milestone_id"
t.string "state"
t.integer "iid"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
create_table "keys", force: :cascade do |t|
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
t.text "key"
t.string "title"
t.string "type"
t.string "fingerprint"
end
add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
create_table "merge_request_diffs", force: :cascade do |t|
t.string "state", default: "collected", null: false
t.text "st_commits"
t.text "st_diffs"
t.integer "merge_request_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
t.string "source_branch", null: false
t.integer "source_project_id", null: false
t.integer "author_id"
t.integer "assignee_id"
t.string "title"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "milestone_id"
t.string "state"
t.string "merge_status"
t.integer "target_project_id", null: false
t.integer "iid"
t.text "description"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
add_index "merge_requests", ["created_at"], name: "index_merge_requests_on_created_at", using: :btree
add_index "merge_requests", ["milestone_id"], name: "index_merge_requests_on_milestone_id", using: :btree
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
add_index "merge_requests", ["source_project_id"], name: "index_merge_requests_on_source_project_id", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
create_table "milestones", force: :cascade do |t|
t.string "title", null: false
t.integer "project_id", null: false
t.text "description"
t.date "due_date"
t.datetime "created_at"
t.datetime "updated_at"
t.string "state"
t.integer "iid"
end
add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
create_table "namespaces", force: :cascade do |t|
t.string "name", null: false
t.string "path", null: false
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "type"
t.string "description", default: "", null: false
t.string "avatar"
end
add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: :cascade do |t|
t.text "note"
t.string "noteable_type"
t.integer "author_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "project_id"
t.string "attachment"
t.string "line_code"
t.string "commit_id"
t.integer "noteable_id"
t.boolean "system", default: false, null: false
t.text "st_diff"
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
add_index "notes", %w[noteable_id noteable_type], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
add_index "notes", %w[project_id noteable_type], name: "index_notes_on_project_id_and_noteable_type", using: :btree
add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
create_table "project_group_links", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "group_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.integer "group_access", default: 30, null: false
end
create_table "projects", force: :cascade do |t|
t.string "name"
t.string "path"
t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "creator_id"
t.boolean "issues_enabled", default: true, null: false
t.boolean "wall_enabled", default: true, null: false
t.boolean "merge_requests_enabled", default: true, null: false
t.boolean "wiki_enabled", default: true, null: false
t.integer "namespace_id"
t.string "issues_tracker", default: "gitlab", null: false
t.string "issues_tracker_id"
t.boolean "snippets_enabled", default: true, null: false
t.datetime "last_activity_at"
t.string "import_url"
t.integer "visibility_level", default: 0, null: false
t.boolean "archived", default: false, null: false
t.string "avatar"
t.string "import_status"
end
add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
create_table "protected_branches", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
create_table "services", force: :cascade do |t|
t.string "type"
t.string "title"
t.string "token"
t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.boolean "active", default: false, null: false
t.string "project_url"
t.string "subdomain"
t.string "room"
t.text "recipients"
t.string "api_key"
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
create_table "snippets", force: :cascade do |t|
t.string "title"
t.text "content"
t.integer "author_id", null: false
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "file_name"
t.datetime "expires_at"
t.boolean "private", default: true, null: false
t.string "type"
end
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree
add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
create_table "taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
t.string "taggable_type"
t.integer "tagger_id"
t.string "tagger_type"
t.string "context"
t.datetime "created_at"
end
add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree
add_index "taggings", %w[taggable_id taggable_type context], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
create_table "tags", force: :cascade do |t|
t.string "name"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.datetime "created_at"
t.datetime "updated_at"
t.string "name"
t.boolean "admin", default: false, null: false
t.integer "projects_limit", default: 10
t.string "skype", default: "", null: false
t.string "linkedin", default: "", null: false
t.string "twitter", default: "", null: false
t.string "authentication_token"
t.integer "theme_id", default: 1, null: false
t.string "bio"
t.integer "failed_attempts", default: 0
t.datetime "locked_at"
t.string "extern_uid"
t.string "provider"
t.string "username"
t.boolean "can_create_group", default: true, null: false
t.boolean "can_create_team", default: true, null: false
t.string "state"
t.integer "color_scheme_id", default: 1, null: false
t.integer "notification_level", default: 1, null: false
t.datetime "password_expires_at"
t.integer "created_by_id"
t.datetime "last_credential_check_at"
t.string "avatar"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.boolean "hide_no_ssh_key", default: false
t.string "website_url", default: "", null: false
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
add_index "users", ["authentication_token"], name: "index_users_on_authentication_token", unique: true, using: :btree
add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", %w[extern_uid provider], name: "index_users_on_extern_uid_and_provider", unique: true, using: :btree
add_index "users", ["name"], name: "index_users_on_name", using: :btree
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
add_index "users", ["username"], name: "index_users_on_username", using: :btree
create_table "users_groups", force: :cascade do |t|
t.integer "group_access", null: false
t.integer "group_id", null: false
t.integer "user_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.integer "notification_level", default: 3, null: false
end
add_index "users_groups", ["user_id"], name: "index_users_groups_on_user_id", using: :btree
create_table "users_projects", force: :cascade do |t|
t.integer "user_id", null: false
t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.integer "project_access", default: 0, null: false
t.integer "notification_level", default: 3, null: false
end
add_index "users_projects", ["project_access"], name: "index_users_projects_on_project_access", using: :btree
add_index "users_projects", ["project_id"], name: "index_users_projects_on_project_id", using: :btree
add_index "users_projects", ["user_id"], name: "index_users_projects_on_user_id", using: :btree
create_table "web_hooks", force: :cascade do |t|
t.string "url"
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "type", default: "ProjectHook"
t.integer "service_id"
t.boolean "push_events", default: true, null: false
t.boolean "issues_events", default: false, null: false
t.boolean "merge_requests_events", default: false, null: false
t.boolean "tag_push_events", default: false
end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
end
def down
raise ActiveRecord::IrreversibleMigration, "The initial migration is not revertable"
end
end

View File

@ -1,16 +0,0 @@
class FixNamespaces < ActiveRecord::Migration[4.2]
DOWNTIME = false
def up
namespaces = exec_query('SELECT id, path FROM namespaces WHERE name <> path and type is null')
namespaces.each do |row|
id = row['id']
path = row['path']
exec_query("UPDATE namespaces SET name = '#{path}' WHERE id = #{id}")
end
end
def down
end
end

View File

@ -1,11 +0,0 @@
class ChangeStateToAllowEmptyMergeRequestDiffs < ActiveRecord::Migration[4.2]
def up
change_column :merge_request_diffs, :state, :string, null: true,
default: nil
end
def down
change_column :merge_request_diffs, :state, :string, null: false,
default: 'collected'
end
end

View File

@ -1 +0,0 @@
require_relative 'limits_to_mysql'

View File

@ -1,33 +0,0 @@
# rubocop:disable all
class AddIndexOnIid < ActiveRecord::Migration[4.2]
def change
RemoveDuplicateIid.clean(Issue)
RemoveDuplicateIid.clean(MergeRequest, 'target_project_id')
RemoveDuplicateIid.clean(Milestone)
add_index :issues, [:project_id, :iid], unique: true
add_index :merge_requests, [:target_project_id, :iid], unique: true
add_index :milestones, [:project_id, :iid], unique: true
end
end
class RemoveDuplicateIid
def self.clean(klass, project_field = 'project_id')
duplicates = klass.find_by_sql("SELECT iid, #{project_field} FROM #{klass.table_name} GROUP BY #{project_field}, iid HAVING COUNT(*) > 1")
duplicates.each do |duplicate|
project_id = duplicate.send(project_field)
iid = duplicate.iid
items = klass.of_projects(project_id).where(iid: iid)
if items.size > 1
puts "Remove #{klass.name} duplicates for iid: #{iid} and project_id: #{project_id}"
items.shift
items.each do |item|
item.destroy
puts '.'
end
end
end
end
end

View File

@ -1,6 +0,0 @@
# rubocop:disable all
class IndexOnCurrentSignInAt < ActiveRecord::Migration[4.2]
def change
add_index :users, :current_sign_in_at
end
end

View File

@ -1,6 +0,0 @@
# rubocop:disable all
class AddNotesIndexUpdatedAt < ActiveRecord::Migration[4.2]
def change
add_index :notes, :updated_at
end
end

View File

@ -1,6 +0,0 @@
# rubocop:disable all
class AddRepoSizeToDb < ActiveRecord::Migration[4.2]
def change
add_column :projects, :repository_size, :float, default: 0
end
end

View File

@ -1,31 +0,0 @@
# rubocop:disable all
class MigrateRepoSize < ActiveRecord::Migration[4.2]
DOWNTIME = false
def up
project_data = execute('SELECT projects.id, namespaces.path AS namespace_path, projects.path AS project_path FROM projects LEFT JOIN namespaces ON projects.namespace_id = namespaces.id')
project_data.each do |project|
id = project['id']
namespace_path = project['namespace_path'] || ''
path = File.join(namespace_path, project['project_path'] + '.git')
begin
repo = Gitlab::Git::Repository.new('default', path, '', '')
if repo.empty?
print '-'
else
size = repo.size
print '.'
execute("UPDATE projects SET repository_size = #{size} WHERE id = #{id}")
end
rescue => e
puts "\nFailed to update project #{id}: #{e}"
end
end
puts "\nDone"
end
def down
end
end

View File

@ -1,6 +0,0 @@
# rubocop:disable all
class AddPositionToMergeRequest < ActiveRecord::Migration[4.2]
def change
add_column :merge_requests, :position, :integer, default: 0
end
end

View File

@ -1,18 +0,0 @@
# rubocop:disable all
class CreateUsersStarProjects < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
create_table :users_star_projects do |t|
t.integer :project_id, null: false
t.integer :user_id, null: false
t.timestamps null: true
end
add_index :users_star_projects, :user_id
add_index :users_star_projects, :project_id
add_index :users_star_projects, [:user_id, :project_id], unique: true
add_column :projects, :star_count, :integer, default: 0, null: false
add_index :projects, :star_count, using: :btree
end
end

View File

@ -1,14 +0,0 @@
# rubocop:disable all
class CreateLabels < ActiveRecord::Migration[4.2]
DOWNTIME = false
def change
create_table :labels do |t|
t.string :title
t.string :color
t.integer :project_id
t.timestamps null: true
end
end
end

Some files were not shown because too many files have changed in this diff Show More