Merge branch 'master' into michel.engelen/gitlab-ce-issue/55953
This commit is contained in:
commit
b94daa35a4
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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-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
|
||||
v-if="line.hasForm"
|
||||
:diff-file-hash="diffFileHash"
|
||||
:line="line"
|
||||
:note-target-line="line"
|
||||
:help-page-path="helpPagePath"
|
||||
/>
|
||||
</template>
|
||||
</diff-discussion-reply>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
|
|
@ -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-discussion-reply
|
||||
:has-form="showLeftSideCommentForm"
|
||||
:render-reply-placeholder="shouldRenderReplyPlaceholderOnLeft"
|
||||
@showNewDiscussionForm="showNewDiscussionForm"
|
||||
>
|
||||
<template #form>
|
||||
<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"
|
||||
/>
|
||||
</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-discussion-reply
|
||||
:has-form="showRightSideCommentForm"
|
||||
:render-reply-placeholder="shouldRenderReplyPlaceholderOnRight"
|
||||
@showNewDiscussionForm="showNewDiscussionForm"
|
||||
>
|
||||
<template #form>
|
||||
<diff-line-note-form
|
||||
v-if="showRightSideCommentForm"
|
||||
:diff-file-hash="diffFileHash"
|
||||
:line="line.right"
|
||||
:note-target-line="line.right"
|
||||
line-position="right"
|
||||
/>
|
||||
</template>
|
||||
</diff-discussion-reply>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
|
|
@ -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')));
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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),
|
||||
updateLineInFile(selectedFile, lineCode, line =>
|
||||
Object.assign(line, {
|
||||
discussions: line.discussions.filter(discussion => discussion.notes.length),
|
||||
}),
|
||||
);
|
||||
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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -39,16 +39,24 @@ 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">
|
||||
<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"
|
||||
:url="resolveWithIssuePath"
|
||||
|
|
|
@ -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,11 +119,15 @@ export default {
|
|||
/>
|
||||
<slot slot="avatar-badge" name="avatar-badge"></slot>
|
||||
</component>
|
||||
<div
|
||||
:class="discussion.diff_discussion ? 'discussion-collapsible bordered-box clearfix' : ''"
|
||||
>
|
||||
<toggle-replies-widget
|
||||
v-if="hasReplies"
|
||||
:collapsed="!isExpanded"
|
||||
:replies="replies"
|
||||
@toggle="$emit('toggleDiscussion')"
|
||||
:class="{ 'discussion-toggle-replies': discussion.diff_discussion }"
|
||||
@toggle="toggleDiscussion({ discussionId: discussion.id })"
|
||||
/>
|
||||
<template v-if="isExpanded">
|
||||
<component
|
||||
|
@ -135,6 +140,8 @@ export default {
|
|||
@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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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">
|
||||
<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>
|
||||
{{
|
||||
__(`No container images stored for this project.
|
||||
Add one by following the instructions above.`)
|
||||
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>
|
||||
|
|
|
@ -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>
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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">
|
||||
•
|
||||
<span v-gl-tooltip.bottom :title="tooltipTitle(release.created_at)">
|
||||
<span v-gl-tooltip.bottom :title="tooltipTitle(release.released_at)">
|
||||
{{ releasedTimeAgo }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -2,6 +2,12 @@
|
|||
* Container Registry
|
||||
*/
|
||||
|
||||
.container-message {
|
||||
pre {
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
|
||||
.container-image {
|
||||
border-bottom: 1px solid $white-normal;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,6 +46,8 @@ module Projects
|
|||
repository.save! if repository.has_tags?
|
||||
end
|
||||
end
|
||||
rescue ContainerRegistry::Path::InvalidRegistryPathError
|
||||
@character_error = true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -293,6 +293,10 @@ class Namespace < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def aggregation_scheduled?
|
||||
aggregation_schedule.present?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parent_changed?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
def default_title
|
||||
'Bugzilla'
|
||||
end
|
||||
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
|
||||
|
|
|
@ -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
|
||||
def default_title
|
||||
'Custom Issue Tracker'
|
||||
end
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,21 +73,13 @@ 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
|
||||
def default_title
|
||||
'Jira'
|
||||
end
|
||||
end
|
||||
|
||||
def description
|
||||
if self.properties && self.properties['description'].present?
|
||||
self.properties['description']
|
||||
else
|
||||
def default_description
|
||||
s_('JiraService|Jira issue tracker')
|
||||
end
|
||||
end
|
||||
|
||||
def self.to_param
|
||||
'jira'
|
||||
|
|
|
@ -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
|
||||
def default_title
|
||||
'Redmine'
|
||||
end
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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’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’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 } }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Resolve Multiple discussions per line in merge request diffs
|
||||
merge_request: 28748
|
||||
author:
|
||||
type: added
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow custom username for deploy tokens
|
||||
merge_request: 29639
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Implement borderless discussion design with new reply field
|
||||
merge_request: 28580
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove istanbul JavaScript package
|
||||
merge_request: 30232
|
||||
author: Takuya Noguchi
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow Ingress to be uninstalled from the UI
|
||||
merge_request: 29977
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Show an Upcoming Status for Releases
|
||||
merge_request: 29577
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Don't let logged out user do manual order
|
||||
merge_request: 30264
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Cache Flipper persisted names directly to local memory storage
|
||||
merge_request: 30265
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add Redis call details in Peek performance bar
|
||||
merge_request: 30191
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace slugifyWithHyphens with improved slugify function
|
||||
merge_request: 30172
|
||||
author: Luke Ward
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Make sure UnicornSampler is started only in master process.
|
||||
merge_request: 30215
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Use PostgreSQL 9.6.11 in CI tests
|
||||
merge_request: 30270
|
||||
author: Takuya Noguchi
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix typo in updateResolvableDiscussionsCounts action
|
||||
merge_request: 30278
|
||||
author: Frank van Rest
|
||||
type: other
|
|
@ -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({})
|
||||
|
|
|
@ -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
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1 +0,0 @@
|
|||
require_relative 'limits_to_mysql'
|
|
@ -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
|
|
@ -1,6 +0,0 @@
|
|||
# rubocop:disable all
|
||||
class IndexOnCurrentSignInAt < ActiveRecord::Migration[4.2]
|
||||
def change
|
||||
add_index :users, :current_sign_in_at
|
||||
end
|
||||
end
|
|
@ -1,6 +0,0 @@
|
|||
# rubocop:disable all
|
||||
class AddNotesIndexUpdatedAt < ActiveRecord::Migration[4.2]
|
||||
def change
|
||||
add_index :notes, :updated_at
|
||||
end
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue