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
|
/doc/ @axil @marcia @eread @mikelewis
|
||||||
|
|
||||||
# Frontend maintainers should see everything in `app/assets/`
|
# Frontend maintainers should see everything in `app/assets/`
|
||||||
app/assets/ @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
|
*.scss @annabeldunstone @ClemMakesApps @fatihacet @filipa @iamphill @mikegreiling @timzallmann @kushalpandya @pslaughter
|
||||||
|
|
||||||
# Someone from the database team should review changes in `db/`
|
# Someone from the database team should review changes in `db/`
|
||||||
db/ @abrandl @NikolayS
|
db/ @abrandl @NikolayS
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
.use-pg: &use-pg
|
.use-pg: &use-pg
|
||||||
services:
|
services:
|
||||||
- name: postgres:9.6
|
- name: postgres:9.6.11
|
||||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||||
- name: redis:alpine
|
- name: redis:alpine
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
DOCKER_HOST: tcp://docker:2375
|
DOCKER_HOST: tcp://docker:2375
|
||||||
script:
|
script:
|
||||||
- node --version
|
- 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
|
- free -m
|
||||||
- retry bundle exec rake gitlab:assets:compile
|
- retry bundle exec rake gitlab:assets:compile
|
||||||
- time scripts/build_assets_image
|
- time scripts/build_assets_image
|
||||||
|
@ -82,7 +82,7 @@ gitlab:assets:compile pull-cache:
|
||||||
stage: prepare
|
stage: prepare
|
||||||
script:
|
script:
|
||||||
- node --version
|
- node --version
|
||||||
- retry yarn install --frozen-lockfile --cache-folder .yarn-cache
|
- retry yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
|
||||||
- free -m
|
- free -m
|
||||||
- retry bundle exec rake gitlab:assets:compile
|
- retry bundle exec rake gitlab:assets:compile
|
||||||
- scripts/clean-old-cached-assets
|
- scripts/clean-old-cached-assets
|
||||||
|
@ -231,7 +231,7 @@ qa:selectors:
|
||||||
before_script: []
|
before_script: []
|
||||||
script:
|
script:
|
||||||
- date
|
- date
|
||||||
- yarn install --frozen-lockfile --cache-folder .yarn-cache
|
- yarn install --frozen-lockfile --cache-folder .yarn-cache --prefer-offline
|
||||||
- date
|
- date
|
||||||
- yarn run webpack-prod
|
- yarn run webpack-prod
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.use-pg: &use-pg
|
.use-pg: &use-pg
|
||||||
services:
|
services:
|
||||||
- name: postgres:9.6
|
- name: postgres:9.6.11
|
||||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||||
- name: redis:alpine
|
- name: redis:alpine
|
||||||
|
|
||||||
|
@ -245,7 +245,7 @@ migration:path-pg:
|
||||||
.db-rollback: &db-rollback
|
.db-rollback: &db-rollback
|
||||||
extends: .dedicated-no-docs-and-no-qa-pull-cache-job
|
extends: .dedicated-no-docs-and-no-qa-pull-cache-job
|
||||||
script:
|
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
|
- bundle exec rake db:migrate SKIP_SCHEMA_VERSION_CHECK=true
|
||||||
dependencies:
|
dependencies:
|
||||||
- setup-test-env
|
- setup-test-env
|
||||||
|
|
|
@ -236,5 +236,5 @@ danger-review:
|
||||||
script:
|
script:
|
||||||
- git version
|
- git version
|
||||||
- node --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
|
- 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)"
|
v-show="isExpanded(discussion)"
|
||||||
:discussion="discussion"
|
:discussion="discussion"
|
||||||
:render-diff-file="false"
|
:render-diff-file="false"
|
||||||
:always-expanded="true"
|
|
||||||
:discussions-by-diff-order="true"
|
:discussions-by-diff-order="true"
|
||||||
:line="line"
|
:line="line"
|
||||||
:help-page-path="helpPagePath"
|
:help-page-path="helpPagePath"
|
||||||
|
|
|
@ -151,7 +151,11 @@ export default {
|
||||||
stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false);
|
stickyMonitor(this.$refs.header, contentTop() - fileHeaderHeight - 1, false);
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('diffs', ['toggleFileDiscussions', 'toggleFullDiff']),
|
...mapActions('diffs', [
|
||||||
|
'toggleFileDiscussions',
|
||||||
|
'toggleFileDiscussionWrappers',
|
||||||
|
'toggleFullDiff',
|
||||||
|
]),
|
||||||
handleToggleFile(e, checkTarget) {
|
handleToggleFile(e, checkTarget) {
|
||||||
if (
|
if (
|
||||||
!checkTarget ||
|
!checkTarget ||
|
||||||
|
@ -165,7 +169,7 @@ export default {
|
||||||
this.$emit('showForkMessage');
|
this.$emit('showForkMessage');
|
||||||
},
|
},
|
||||||
handleToggleDiscussions() {
|
handleToggleDiscussions() {
|
||||||
this.toggleFileDiscussions(this.diffFile);
|
this.toggleFileDiscussionWrappers(this.diffFile);
|
||||||
},
|
},
|
||||||
handleFileNameClick(e) {
|
handleFileNameClick(e) {
|
||||||
const isLinkToOtherPage =
|
const isLinkToOtherPage =
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapActions } from 'vuex';
|
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
import { pluralize, truncate } from '~/lib/utils/text_utility';
|
import { pluralize, truncate } from '~/lib/utils/text_utility';
|
||||||
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
|
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
|
||||||
|
@ -19,11 +18,13 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
discussionsExpanded: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
discussionsExpanded() {
|
|
||||||
return this.discussions.every(discussion => discussion.expanded);
|
|
||||||
},
|
|
||||||
allDiscussions() {
|
allDiscussions() {
|
||||||
return this.discussions.reduce((acc, note) => acc.concat(note.notes), []);
|
return this.discussions.reduce((acc, note) => acc.concat(note.notes), []);
|
||||||
},
|
},
|
||||||
|
@ -45,26 +46,14 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['toggleDiscussion']),
|
|
||||||
getTooltipText(noteData) {
|
getTooltipText(noteData) {
|
||||||
let { note } = noteData;
|
let { note } = noteData;
|
||||||
|
|
||||||
if (note.length > LENGTH_OF_AVATAR_TOOLTIP) {
|
if (note.length > LENGTH_OF_AVATAR_TOOLTIP) {
|
||||||
note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP);
|
note = truncate(note, LENGTH_OF_AVATAR_TOOLTIP);
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${noteData.author.name}: ${note}`;
|
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>
|
</script>
|
||||||
|
@ -76,7 +65,7 @@ export default {
|
||||||
type="button"
|
type="button"
|
||||||
:aria-label="__('Show comments')"
|
:aria-label="__('Show comments')"
|
||||||
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
|
class="diff-notes-collapse js-diff-comment-avatar js-diff-comment-button"
|
||||||
@click="toggleDiscussions"
|
@click="$emit('toggleLineDiscussions')"
|
||||||
>
|
>
|
||||||
<icon :size="12" name="collapse" />
|
<icon :size="12" name="collapse" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -87,7 +76,7 @@ export default {
|
||||||
:img-src="note.author.avatar_url"
|
:img-src="note.author.avatar_url"
|
||||||
:tooltip-text="getTooltipText(note)"
|
:tooltip-text="getTooltipText(note)"
|
||||||
class="diff-comment-avatar js-diff-comment-avatar"
|
class="diff-comment-avatar js-diff-comment-avatar"
|
||||||
@click.native="toggleDiscussions"
|
@click.native="$emit('toggleLineDiscussions')"
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="moreText"
|
v-if="moreText"
|
||||||
|
@ -97,7 +86,7 @@ export default {
|
||||||
data-container="body"
|
data-container="body"
|
||||||
data-placement="top"
|
data-placement="top"
|
||||||
role="button"
|
role="button"
|
||||||
@click="toggleDiscussions"
|
@click="$emit('toggleLineDiscussions')"
|
||||||
>+{{ moreCount }}</span
|
>+{{ moreCount }}</span
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -105,7 +105,13 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('diffs', ['loadMoreLines', 'showCommentForm', 'setHighlightedRow']),
|
...mapActions('diffs', [
|
||||||
|
'loadMoreLines',
|
||||||
|
'showCommentForm',
|
||||||
|
'setHighlightedRow',
|
||||||
|
'toggleLineDiscussions',
|
||||||
|
'toggleLineDiscussionWrappers',
|
||||||
|
]),
|
||||||
handleCommentButton() {
|
handleCommentButton() {
|
||||||
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
|
this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
|
||||||
},
|
},
|
||||||
|
@ -184,7 +190,14 @@ export default {
|
||||||
@click="setHighlightedRow(lineCode)"
|
@click="setHighlightedRow(lineCode)"
|
||||||
>
|
>
|
||||||
</a>
|
</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>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import diffDiscussions from './diff_discussions.vue';
|
import { mapActions } from 'vuex';
|
||||||
import diffLineNoteForm from './diff_line_note_form.vue';
|
import DiffDiscussions from './diff_discussions.vue';
|
||||||
|
import DiffLineNoteForm from './diff_line_note_form.vue';
|
||||||
|
import DiffDiscussionReply from './diff_discussion_reply.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
diffDiscussions,
|
DiffDiscussions,
|
||||||
diffLineNoteForm,
|
DiffLineNoteForm,
|
||||||
|
DiffDiscussionReply,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
line: {
|
line: {
|
||||||
|
@ -32,10 +35,12 @@ export default {
|
||||||
if (!this.line.discussions || !this.line.discussions.length) {
|
if (!this.line.discussions || !this.line.discussions.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
return this.line.discussionsExpanded;
|
||||||
return this.line.discussions.every(discussion => discussion.expanded);
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions('diffs', ['showCommentForm']),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -49,13 +54,22 @@ export default {
|
||||||
:discussions="line.discussions"
|
:discussions="line.discussions"
|
||||||
:help-page-path="helpPagePath"
|
:help-page-path="helpPagePath"
|
||||||
/>
|
/>
|
||||||
<diff-line-note-form
|
<diff-discussion-reply
|
||||||
v-if="line.hasForm"
|
:has-form="line.hasForm"
|
||||||
:diff-file-hash="diffFileHash"
|
:render-reply-placeholder="Boolean(line.discussions.length)"
|
||||||
:line="line"
|
@showNewDiscussionForm="
|
||||||
:note-target-line="line"
|
showCommentForm({ lineCode: line.line_code, fileHash: diffFileHash })
|
||||||
:help-page-path="helpPagePath"
|
"
|
||||||
/>
|
>
|
||||||
|
<template #form>
|
||||||
|
<diff-line-note-form
|
||||||
|
:diff-file-hash="diffFileHash"
|
||||||
|
:line="line"
|
||||||
|
:note-target-line="line"
|
||||||
|
:help-page-path="helpPagePath"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</diff-discussion-reply>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import diffDiscussions from './diff_discussions.vue';
|
import { mapActions } from 'vuex';
|
||||||
import diffLineNoteForm from './diff_line_note_form.vue';
|
import DiffDiscussions from './diff_discussions.vue';
|
||||||
|
import DiffLineNoteForm from './diff_line_note_form.vue';
|
||||||
|
import DiffDiscussionReply from './diff_discussion_reply.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
diffDiscussions,
|
DiffDiscussions,
|
||||||
diffLineNoteForm,
|
DiffLineNoteForm,
|
||||||
|
DiffDiscussionReply,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
line: {
|
line: {
|
||||||
|
@ -29,24 +32,30 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
hasExpandedDiscussionOnLeft() {
|
hasExpandedDiscussionOnLeft() {
|
||||||
return this.line.left && this.line.left.discussions.length
|
return this.line.left && this.line.left.discussions.length
|
||||||
? this.line.left.discussions.every(discussion => discussion.expanded)
|
? this.line.left.discussionsExpanded
|
||||||
: false;
|
: false;
|
||||||
},
|
},
|
||||||
hasExpandedDiscussionOnRight() {
|
hasExpandedDiscussionOnRight() {
|
||||||
return this.line.right && this.line.right.discussions.length
|
return this.line.right && this.line.right.discussions.length
|
||||||
? this.line.right.discussions.every(discussion => discussion.expanded)
|
? this.line.right.discussionsExpanded
|
||||||
: false;
|
: false;
|
||||||
},
|
},
|
||||||
hasAnyExpandedDiscussion() {
|
hasAnyExpandedDiscussion() {
|
||||||
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
|
return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight;
|
||||||
},
|
},
|
||||||
shouldRenderDiscussionsOnLeft() {
|
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() {
|
shouldRenderDiscussionsOnRight() {
|
||||||
return (
|
return (
|
||||||
this.line.right &&
|
this.line.right &&
|
||||||
this.line.right.discussions &&
|
this.line.right.discussions &&
|
||||||
|
this.line.right.discussions.length &&
|
||||||
this.hasExpandedDiscussionOnRight &&
|
this.hasExpandedDiscussionOnRight &&
|
||||||
this.line.right.type
|
this.line.right.type
|
||||||
);
|
);
|
||||||
|
@ -81,6 +90,22 @@ export default {
|
||||||
|
|
||||||
return hasCommentFormOnLeft || hasCommentFormOnRight;
|
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>
|
</script>
|
||||||
|
@ -90,37 +115,49 @@ export default {
|
||||||
<td class="notes-content parallel old" colspan="2">
|
<td class="notes-content parallel old" colspan="2">
|
||||||
<div v-if="shouldRenderDiscussionsOnLeft" class="content">
|
<div v-if="shouldRenderDiscussionsOnLeft" class="content">
|
||||||
<diff-discussions
|
<diff-discussions
|
||||||
v-if="line.left.discussions.length"
|
|
||||||
:discussions="line.left.discussions"
|
:discussions="line.left.discussions"
|
||||||
:line="line.left"
|
:line="line.left"
|
||||||
:help-page-path="helpPagePath"
|
:help-page-path="helpPagePath"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<diff-line-note-form
|
<diff-discussion-reply
|
||||||
v-if="showLeftSideCommentForm"
|
:has-form="showLeftSideCommentForm"
|
||||||
:diff-file-hash="diffFileHash"
|
:render-reply-placeholder="shouldRenderReplyPlaceholderOnLeft"
|
||||||
:line="line.left"
|
@showNewDiscussionForm="showNewDiscussionForm"
|
||||||
:note-target-line="line.left"
|
>
|
||||||
:help-page-path="helpPagePath"
|
<template #form>
|
||||||
line-position="left"
|
<diff-line-note-form
|
||||||
/>
|
:diff-file-hash="diffFileHash"
|
||||||
|
:line="line.left"
|
||||||
|
:note-target-line="line.left"
|
||||||
|
:help-page-path="helpPagePath"
|
||||||
|
line-position="left"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</diff-discussion-reply>
|
||||||
</td>
|
</td>
|
||||||
<td class="notes-content parallel new" colspan="2">
|
<td class="notes-content parallel new" colspan="2">
|
||||||
<div v-if="shouldRenderDiscussionsOnRight" class="content">
|
<div v-if="shouldRenderDiscussionsOnRight" class="content">
|
||||||
<diff-discussions
|
<diff-discussions
|
||||||
v-if="line.right.discussions.length"
|
|
||||||
:discussions="line.right.discussions"
|
:discussions="line.right.discussions"
|
||||||
:line="line.right"
|
:line="line.right"
|
||||||
:help-page-path="helpPagePath"
|
:help-page-path="helpPagePath"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<diff-line-note-form
|
<diff-discussion-reply
|
||||||
v-if="showRightSideCommentForm"
|
:has-form="showRightSideCommentForm"
|
||||||
:diff-file-hash="diffFileHash"
|
:render-reply-placeholder="shouldRenderReplyPlaceholderOnRight"
|
||||||
:line="line.right"
|
@showNewDiscussionForm="showNewDiscussionForm"
|
||||||
:note-target-line="line.right"
|
>
|
||||||
line-position="right"
|
<template #form>
|
||||||
/>
|
<diff-line-note-form
|
||||||
|
:diff-file-hash="diffFileHash"
|
||||||
|
:line="line.right"
|
||||||
|
:note-target-line="line.right"
|
||||||
|
line-position="right"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</diff-discussion-reply>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
getNoteFormData,
|
getNoteFormData,
|
||||||
convertExpandLines,
|
convertExpandLines,
|
||||||
idleCallback,
|
idleCallback,
|
||||||
|
allDiscussionWrappersExpanded,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
import {
|
import {
|
||||||
|
@ -79,6 +80,7 @@ export const assignDiscussionsToDiff = (
|
||||||
discussions = rootState.notes.discussions,
|
discussions = rootState.notes.discussions,
|
||||||
) => {
|
) => {
|
||||||
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
|
const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles);
|
||||||
|
const hash = getLocationHash();
|
||||||
|
|
||||||
discussions
|
discussions
|
||||||
.filter(discussion => discussion.diff_discussion)
|
.filter(discussion => discussion.diff_discussion)
|
||||||
|
@ -86,6 +88,7 @@ export const assignDiscussionsToDiff = (
|
||||||
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
|
commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, {
|
||||||
discussion,
|
discussion,
|
||||||
diffPositionByLineCode,
|
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 });
|
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) => {
|
export const renderFileForDiscussionId = ({ commit, rootState, state }, discussionId) => {
|
||||||
const discussion = rootState.notes.discussions.find(d => d.id === 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 }) => {
|
export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
|
||||||
const postData = getNoteFormData({
|
const postData = getNoteFormData({
|
||||||
commit: state.commit,
|
commit: state.commit,
|
||||||
|
@ -267,7 +299,7 @@ export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
|
||||||
return dispatch('saveNote', postData, { root: true })
|
return dispatch('saveNote', postData, { root: true })
|
||||||
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
|
.then(result => dispatch('updateDiscussion', result.discussion, { root: true }))
|
||||||
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
|
.then(discussion => dispatch('assignDiscussionsToDiff', [discussion]))
|
||||||
.then(() => dispatch('updateResolvableDiscussonsCounts', null, { root: true }))
|
.then(() => dispatch('updateResolvableDiscussionsCounts', null, { root: true }))
|
||||||
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
|
.then(() => dispatch('closeDiffFileCommentForm', formData.diffFile.file_hash))
|
||||||
.catch(() => createFlash(s__('MergeRequests|Saving the comment failed')));
|
.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 TOGGLE_DIFF_FILE_RENDERING_MORE = 'TOGGLE_DIFF_FILE_RENDERING_MORE';
|
||||||
|
|
||||||
export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER';
|
export const SET_SHOW_SUGGEST_POPOVER = 'SET_SHOW_SUGGEST_POPOVER';
|
||||||
|
|
||||||
|
export const TOGGLE_LINE_DISCUSSIONS = 'TOGGLE_LINE_DISCUSSIONS';
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {
|
||||||
addContextLines,
|
addContextLines,
|
||||||
prepareDiffData,
|
prepareDiffData,
|
||||||
isDiscussionApplicableToLine,
|
isDiscussionApplicableToLine,
|
||||||
|
updateLineInFile,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import * as types from './mutation_types';
|
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 { latestDiff } = state;
|
||||||
|
|
||||||
const discussionLineCode = discussion.line_code;
|
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 => {
|
state.diffFiles = state.diffFiles.map(diffFile => {
|
||||||
if (diffFile.file_hash === fileHash) {
|
if (diffFile.file_hash === fileHash) {
|
||||||
const file = { ...diffFile };
|
const file = { ...diffFile };
|
||||||
|
|
||||||
if (file.highlighted_diff_lines) {
|
if (file.highlighted_diff_lines) {
|
||||||
file.highlighted_diff_lines = file.highlighted_diff_lines.map(line =>
|
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) {
|
if (left || right) {
|
||||||
return {
|
return {
|
||||||
...line,
|
...line,
|
||||||
left: line.left ? mapDiscussions(line.left) : null,
|
left: line.left ? setDiscussionsExpanded(mapDiscussions(line.left)) : null,
|
||||||
right: line.right ? mapDiscussions(line.right, () => !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 }) {
|
[types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) {
|
||||||
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
|
const selectedFile = state.diffFiles.find(f => f.file_hash === fileHash);
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
if (selectedFile.parallel_diff_lines) {
|
updateLineInFile(selectedFile, lineCode, line =>
|
||||||
const targetLine = selectedFile.parallel_diff_lines.find(
|
Object.assign(line, {
|
||||||
line =>
|
discussions: line.discussions.filter(discussion => discussion.notes.length),
|
||||||
(line.left && line.left.line_code === lineCode) ||
|
}),
|
||||||
(line.right && line.right.line_code === lineCode),
|
);
|
||||||
);
|
|
||||||
if (targetLine) {
|
|
||||||
const side = targetLine.left && targetLine.left.line_code === lineCode ? 'left' : 'right';
|
|
||||||
|
|
||||||
Object.assign(targetLine[side], {
|
|
||||||
discussions: targetLine[side].discussions.filter(discussion => discussion.notes.length),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedFile.highlighted_diff_lines) {
|
|
||||||
const targetInlineLine = selectedFile.highlighted_diff_lines.find(
|
|
||||||
line => line.line_code === lineCode,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (targetInlineLine) {
|
|
||||||
Object.assign(targetInlineLine, {
|
|
||||||
discussions: targetInlineLine.discussions.filter(discussion => discussion.notes.length),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedFile.discussions && selectedFile.discussions.length) {
|
if (selectedFile.discussions && selectedFile.discussions.length) {
|
||||||
selectedFile.discussions = selectedFile.discussions.filter(
|
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) {
|
[types.TOGGLE_FOLDER_OPEN](state, path) {
|
||||||
state.treeEntries[path].opened = !state.treeEntries[path].opened;
|
state.treeEntries[path].opened = !state.treeEntries[path].opened;
|
||||||
},
|
},
|
||||||
|
|
|
@ -454,3 +454,48 @@ export const convertExpandLines = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
export const idleCallback = cb => requestIdleCallback(cb);
|
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 $ from 'jquery';
|
||||||
import { slugifyWithHyphens } from './lib/utils/text_utility';
|
import { slugify } from './lib/utils/text_utility';
|
||||||
|
|
||||||
export default class Group {
|
export default class Group {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -14,7 +14,7 @@ export default class Group {
|
||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
const slug = slugifyWithHyphens(this.groupName.val());
|
const slug = slugify(this.groupName.val());
|
||||||
this.groupPath.val(slug);
|
this.groupPath.val(slug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,8 @@ export default {
|
||||||
@click="openFileInEditor"
|
@click="openFileInEditor"
|
||||||
>
|
>
|
||||||
<span class="multi-file-commit-list-file-path d-flex align-items-center">
|
<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>
|
</span>
|
||||||
<div class="ml-auto d-flex align-items-center">
|
<div class="ml-auto d-flex align-items-center">
|
||||||
<div class="d-flex align-items-center ide-commit-list-changed-icon">
|
<div class="d-flex align-items-center ide-commit-list-changed-icon">
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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" />
|
<icon :size="16" name="external-link" css-classes="vertical-align-middle space-right" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { activityBarViews, viewerTypes } from '../constants';
|
||||||
import Editor from '../lib/editor';
|
import Editor from '../lib/editor';
|
||||||
import ExternalLink from './external_link.vue';
|
import ExternalLink from './external_link.vue';
|
||||||
import FileTemplatesBar from './file_templates/bar.vue';
|
import FileTemplatesBar from './file_templates/bar.vue';
|
||||||
|
import { __ } from '~/locale';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -160,7 +161,14 @@ export default {
|
||||||
this.createEditorInstance();
|
this.createEditorInstance();
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.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;
|
throw err;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -247,12 +255,8 @@ export default {
|
||||||
role="button"
|
role="button"
|
||||||
@click.prevent="setFileViewMode({ file, viewMode: 'editor' })"
|
@click.prevent="setFileViewMode({ file, viewMode: 'editor' })"
|
||||||
>
|
>
|
||||||
<template v-if="viewer === $options.viewerTypes.edit">
|
<template v-if="viewer === $options.viewerTypes.edit">{{ __('Edit') }}</template>
|
||||||
{{ __('Edit') }}
|
<template v-else>{{ __('Review') }}</template>
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ __('Review') }}
|
|
||||||
</template>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="file.previewMode" :class="previewTabCSS">
|
<li v-if="file.previewMode" :class="previewTabCSS">
|
||||||
|
@ -260,9 +264,8 @@ export default {
|
||||||
href="javascript:void(0);"
|
href="javascript:void(0);"
|
||||||
role="button"
|
role="button"
|
||||||
@click.prevent="setFileViewMode({ file, viewMode: 'preview' })"
|
@click.prevent="setFileViewMode({ file, viewMode: 'preview' })"
|
||||||
|
>{{ file.previewMode.previewTitle }}</a
|
||||||
>
|
>
|
||||||
{{ file.previewMode.previewTitle }}
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<external-link :file="file" />
|
<external-link :file="file" />
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { __, sprintf } from '~/locale';
|
||||||
import icon from '~/vue_shared/components/icon.vue';
|
import icon from '~/vue_shared/components/icon.vue';
|
||||||
import tooltip from '~/vue_shared/directives/tooltip';
|
import tooltip from '~/vue_shared/directives/tooltip';
|
||||||
import '~/lib/utils/datetime_utility';
|
import '~/lib/utils/datetime_utility';
|
||||||
|
@ -18,7 +19,9 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
lockTooltip() {
|
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>
|
<script>
|
||||||
|
import { __, sprintf } from '~/locale';
|
||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
|
|
||||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||||
|
@ -27,9 +28,9 @@ export default {
|
||||||
computed: {
|
computed: {
|
||||||
closeLabel() {
|
closeLabel() {
|
||||||
if (this.fileHasChanged) {
|
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() {
|
showChangedIcon() {
|
||||||
if (this.tab.pending) return true;
|
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, '-');
|
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
|
* @param {String} str
|
||||||
* @returns {String}
|
* @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
|
* 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 initManualOrdering = () => {
|
||||||
const issueList = document.querySelector('.manual-ordering');
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,15 +39,23 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="discussion-with-resolve-btn clearfix">
|
<div class="discussion-with-resolve-btn">
|
||||||
<reply-placeholder class="qa-discussion-reply" @onClick="$emit('showReplyForm')" />
|
<reply-placeholder
|
||||||
|
:button-text="s__('MergeRequests|Reply...')"
|
||||||
<div class="btn-group discussion-actions" role="group">
|
class="qa-discussion-reply"
|
||||||
<resolve-discussion-button
|
@onClick="$emit('showReplyForm')"
|
||||||
v-if="discussion.resolvable"
|
/>
|
||||||
:is-resolving="isResolving"
|
<resolve-discussion-button
|
||||||
:button-title="resolveButtonTitle"
|
v-if="discussion.resolvable"
|
||||||
@onClick="$emit('resolve')"
|
: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
|
<resolve-with-issue-button
|
||||||
v-if="discussion.resolvable && resolveWithIssuePath"
|
v-if="discussion.resolvable && resolveWithIssuePath"
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
import { SYSTEM_NOTE } from '../constants';
|
import { SYSTEM_NOTE } from '../constants';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import NoteableNote from './noteable_note.vue';
|
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
|
||||||
import PlaceholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
|
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
|
||||||
import PlaceholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
|
|
||||||
import SystemNote from '~/vue_shared/components/notes/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 ToggleRepliesWidget from './toggle_replies_widget.vue';
|
||||||
import NoteEditedText from './note_edited_text.vue';
|
import NoteEditedText from './note_edited_text.vue';
|
||||||
|
|
||||||
|
@ -72,6 +72,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
...mapActions(['toggleDiscussion']),
|
||||||
componentName(note) {
|
componentName(note) {
|
||||||
if (note.isPlaceholderNote) {
|
if (note.isPlaceholderNote) {
|
||||||
if (note.placeholderType === SYSTEM_NOTE) {
|
if (note.placeholderType === SYSTEM_NOTE) {
|
||||||
|
@ -101,7 +102,7 @@ export default {
|
||||||
<component
|
<component
|
||||||
:is="componentName(firstNote)"
|
:is="componentName(firstNote)"
|
||||||
:note="componentData(firstNote)"
|
:note="componentData(firstNote)"
|
||||||
:line="line"
|
:line="line || diffLine"
|
||||||
:commit="commit"
|
:commit="commit"
|
||||||
:help-page-path="helpPagePath"
|
:help-page-path="helpPagePath"
|
||||||
:show-reply-button="userCanReply"
|
:show-reply-button="userCanReply"
|
||||||
|
@ -118,23 +119,29 @@ export default {
|
||||||
/>
|
/>
|
||||||
<slot slot="avatar-badge" name="avatar-badge"></slot>
|
<slot slot="avatar-badge" name="avatar-badge"></slot>
|
||||||
</component>
|
</component>
|
||||||
<toggle-replies-widget
|
<div
|
||||||
v-if="hasReplies"
|
:class="discussion.diff_discussion ? 'discussion-collapsible bordered-box clearfix' : ''"
|
||||||
:collapsed="!isExpanded"
|
>
|
||||||
:replies="replies"
|
<toggle-replies-widget
|
||||||
@toggle="$emit('toggleDiscussion')"
|
v-if="hasReplies"
|
||||||
/>
|
:collapsed="!isExpanded"
|
||||||
<template v-if="isExpanded">
|
:replies="replies"
|
||||||
<component
|
:class="{ 'discussion-toggle-replies': discussion.diff_discussion }"
|
||||||
:is="componentName(note)"
|
@toggle="toggleDiscussion({ discussionId: discussion.id })"
|
||||||
v-for="note in replies"
|
|
||||||
:key="note.id"
|
|
||||||
:note="componentData(note)"
|
|
||||||
:help-page-path="helpPagePath"
|
|
||||||
:line="line"
|
|
||||||
@handleDeleteNote="$emit('deleteNote')"
|
|
||||||
/>
|
/>
|
||||||
</template>
|
<template v-if="isExpanded">
|
||||||
|
<component
|
||||||
|
:is="componentName(note)"
|
||||||
|
v-for="note in replies"
|
||||||
|
:key="note.id"
|
||||||
|
:note="componentData(note)"
|
||||||
|
:help-page-path="helpPagePath"
|
||||||
|
:line="line"
|
||||||
|
@handleDeleteNote="$emit('deleteNote')"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<component
|
<component
|
||||||
|
@ -148,8 +155,8 @@ export default {
|
||||||
>
|
>
|
||||||
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
|
<slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
|
||||||
</component>
|
</component>
|
||||||
|
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'ReplyPlaceholder',
|
name: 'ReplyPlaceholder',
|
||||||
|
props: {
|
||||||
|
buttonText: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -12,6 +18,6 @@ export default {
|
||||||
:title="s__('MergeRequests|Add a reply')"
|
:title="s__('MergeRequests|Add a reply')"
|
||||||
@click="$emit('onClick')"
|
@click="$emit('onClick')"
|
||||||
>
|
>
|
||||||
{{ s__('MergeRequests|Reply...') }}
|
{{ buttonText }}
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -132,7 +132,7 @@ export default {
|
||||||
return this.discussion.diff_discussion && this.renderDiffFile;
|
return this.discussion.diff_discussion && this.renderDiffFile;
|
||||||
},
|
},
|
||||||
shouldGroupReplies() {
|
shouldGroupReplies() {
|
||||||
return !this.shouldRenderDiffs && !this.discussion.diff_discussion;
|
return !this.shouldRenderDiffs;
|
||||||
},
|
},
|
||||||
wrapperComponent() {
|
wrapperComponent() {
|
||||||
return this.shouldRenderDiffs ? diffWithNote : 'div';
|
return this.shouldRenderDiffs ? diffWithNote : 'div';
|
||||||
|
@ -248,6 +248,11 @@ export default {
|
||||||
clearDraft(this.autosaveKey);
|
clearDraft(this.autosaveKey);
|
||||||
},
|
},
|
||||||
saveReply(noteText, form, callback) {
|
saveReply(noteText, form, callback) {
|
||||||
|
if (!noteText) {
|
||||||
|
this.cancelReplyForm();
|
||||||
|
callback();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const postData = {
|
const postData = {
|
||||||
in_reply_to_discussion_id: this.discussion.reply_id,
|
in_reply_to_discussion_id: this.discussion.reply_id,
|
||||||
target_type: this.getNoteableData.targetType,
|
target_type: this.getNoteableData.targetType,
|
||||||
|
@ -361,7 +366,6 @@ Please check your network connection and try again.`;
|
||||||
:line="line"
|
:line="line"
|
||||||
:should-group-replies="shouldGroupReplies"
|
:should-group-replies="shouldGroupReplies"
|
||||||
@startReplying="showReplyForm"
|
@startReplying="showReplyForm"
|
||||||
@toggleDiscussion="toggleDiscussionHandler"
|
|
||||||
@deleteNote="deleteNoteHandler"
|
@deleteNote="deleteNoteHandler"
|
||||||
>
|
>
|
||||||
<slot slot="avatar-badge" name="avatar-badge"></slot>
|
<slot slot="avatar-badge" name="avatar-badge"></slot>
|
||||||
|
@ -374,7 +378,7 @@ Please check your network connection and try again.`;
|
||||||
<div
|
<div
|
||||||
v-else-if="showReplies"
|
v-else-if="showReplies"
|
||||||
:class="{ 'is-replying': isReplying }"
|
:class="{ 'is-replying': isReplying }"
|
||||||
class="discussion-reply-holder"
|
class="discussion-reply-holder clearfix"
|
||||||
>
|
>
|
||||||
<user-avatar-link
|
<user-avatar-link
|
||||||
v-if="!isReplying && userCanReply"
|
v-if="!isReplying && userCanReply"
|
||||||
|
|
|
@ -51,7 +51,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter }) =>
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(discussions => {
|
.then(discussions => {
|
||||||
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
|
commit(types.SET_INITIAL_DISCUSSIONS, discussions);
|
||||||
dispatch('updateResolvableDiscussonsCounts');
|
dispatch('updateResolvableDiscussionsCounts');
|
||||||
});
|
});
|
||||||
|
|
||||||
export const updateDiscussion = ({ commit, state }, discussion) => {
|
export const updateDiscussion = ({ commit, state }, discussion) => {
|
||||||
|
@ -67,7 +67,7 @@ export const deleteNote = ({ commit, dispatch, state }, note) =>
|
||||||
commit(types.DELETE_NOTE, note);
|
commit(types.DELETE_NOTE, note);
|
||||||
|
|
||||||
dispatch('updateMergeRequestWidget');
|
dispatch('updateMergeRequestWidget');
|
||||||
dispatch('updateResolvableDiscussonsCounts');
|
dispatch('updateResolvableDiscussionsCounts');
|
||||||
|
|
||||||
if (isInMRPage()) {
|
if (isInMRPage()) {
|
||||||
dispatch('diffs/removeDiscussionsFromDiff', discussion);
|
dispatch('diffs/removeDiscussionsFromDiff', discussion);
|
||||||
|
@ -117,7 +117,7 @@ export const replyToDiscussion = ({ commit, state, getters, dispatch }, { endpoi
|
||||||
|
|
||||||
dispatch('updateMergeRequestWidget');
|
dispatch('updateMergeRequestWidget');
|
||||||
dispatch('startTaskList');
|
dispatch('startTaskList');
|
||||||
dispatch('updateResolvableDiscussonsCounts');
|
dispatch('updateResolvableDiscussionsCounts');
|
||||||
} else {
|
} else {
|
||||||
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
|
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, res);
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,7 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
|
||||||
|
|
||||||
dispatch('updateMergeRequestWidget');
|
dispatch('updateMergeRequestWidget');
|
||||||
dispatch('startTaskList');
|
dispatch('startTaskList');
|
||||||
dispatch('updateResolvableDiscussonsCounts');
|
dispatch('updateResolvableDiscussionsCounts');
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
@ -168,7 +168,7 @@ export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved,
|
||||||
|
|
||||||
commit(mutationType, res);
|
commit(mutationType, res);
|
||||||
|
|
||||||
dispatch('updateResolvableDiscussonsCounts');
|
dispatch('updateResolvableDiscussionsCounts');
|
||||||
|
|
||||||
dispatch('updateMergeRequestWidget');
|
dispatch('updateMergeRequestWidget');
|
||||||
});
|
});
|
||||||
|
@ -442,7 +442,7 @@ export const startTaskList = ({ dispatch }) =>
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
export const updateResolvableDiscussonsCounts = ({ commit }) =>
|
export const updateResolvableDiscussionsCounts = ({ commit }) =>
|
||||||
commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS);
|
commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS);
|
||||||
|
|
||||||
export const submitSuggestion = (
|
export const submitSuggestion = (
|
||||||
|
|
|
@ -4,14 +4,12 @@ import { glEmojiTag } from '~/emoji';
|
||||||
|
|
||||||
import detailedMetric from './detailed_metric.vue';
|
import detailedMetric from './detailed_metric.vue';
|
||||||
import requestSelector from './request_selector.vue';
|
import requestSelector from './request_selector.vue';
|
||||||
import simpleMetric from './simple_metric.vue';
|
|
||||||
import { s__ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
detailedMetric,
|
detailedMetric,
|
||||||
requestSelector,
|
requestSelector,
|
||||||
simpleMetric,
|
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
store: {
|
store: {
|
||||||
|
@ -43,8 +41,13 @@ export default {
|
||||||
details: 'details',
|
details: 'details',
|
||||||
keys: ['feature', 'request'],
|
keys: ['feature', 'request'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
metric: 'redis',
|
||||||
|
header: 'Redis calls',
|
||||||
|
details: 'details',
|
||||||
|
keys: ['cmd'],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
simpleMetrics: ['redis'],
|
|
||||||
data() {
|
data() {
|
||||||
return { currentRequestId: '' };
|
return { currentRequestId: '' };
|
||||||
},
|
},
|
||||||
|
@ -124,12 +127,6 @@ export default {
|
||||||
</button>
|
</button>
|
||||||
<a v-else :href="profileUrl">{{ s__('PerformanceBar|profile') }}</a>
|
<a v-else :href="profileUrl">{{ s__('PerformanceBar|profile') }}</a>
|
||||||
</div>
|
</div>
|
||||||
<simple-metric
|
|
||||||
v-for="metric in $options.simpleMetrics"
|
|
||||||
:key="metric"
|
|
||||||
:current-request="currentRequest"
|
|
||||||
:metric="metric"
|
|
||||||
/>
|
|
||||||
<div id="peek-view-gc" class="view">
|
<div id="peek-view-gc" class="view">
|
||||||
<span v-if="currentRequest.details" class="bold">
|
<span v-if="currentRequest.details" class="bold">
|
||||||
<span title="Invoke Time">{{ currentRequest.details.gc.gc_time }}</span
|
<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 $ from 'jquery';
|
||||||
import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils';
|
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';
|
import { s__ } from '~/locale';
|
||||||
|
|
||||||
let hasUserDefinedProjectPath = false;
|
let hasUserDefinedProjectPath = false;
|
||||||
|
@ -34,7 +34,7 @@ const deriveProjectPathFromUrl = $projectImportUrl => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
|
const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
|
||||||
const slug = slugifyWithHyphens($projectNameInput.val());
|
const slug = slugify($projectNameInput.val());
|
||||||
$projectPathInput.val(slug);
|
$projectPathInput.val(slug);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3,22 +3,81 @@ import { mapGetters, mapActions } from 'vuex';
|
||||||
import { GlLoadingIcon } from '@gitlab/ui';
|
import { GlLoadingIcon } from '@gitlab/ui';
|
||||||
import store from '../stores';
|
import store from '../stores';
|
||||||
import CollapsibleContainer from './collapsible_container.vue';
|
import CollapsibleContainer from './collapsible_container.vue';
|
||||||
|
import SvgMessage from './svg_message.vue';
|
||||||
|
import { s__, sprintf } from '../../locale';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RegistryListApp',
|
name: 'RegistryListApp',
|
||||||
components: {
|
components: {
|
||||||
CollapsibleContainer,
|
CollapsibleContainer,
|
||||||
GlLoadingIcon,
|
GlLoadingIcon,
|
||||||
|
SvgMessage,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
endpoint: {
|
endpoint: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
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,
|
store,
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(['isLoading', 'repos']),
|
...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() {
|
created() {
|
||||||
this.setMainEndpoint(this.endpoint);
|
this.setMainEndpoint(this.endpoint);
|
||||||
|
@ -33,20 +92,44 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<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
|
<gl-loading-icon v-else-if="isLoading" size="md" class="prepend-top-16" />
|
||||||
v-for="item in repos"
|
|
||||||
v-else-if="!isLoading && repos.length"
|
|
||||||
:key="item.id"
|
|
||||||
:repo="item"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<p v-else-if="!isLoading && !repos.length">
|
<div v-else-if="!isLoading && !characterError && repos.length">
|
||||||
{{
|
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
|
||||||
__(`No container images stored for this project.
|
<p v-html="introText"></p>
|
||||||
Add one by following the instructions above.`)
|
<collapsible-container v-for="item in repos" :key="item.id" :repo="item" />
|
||||||
}}
|
</div>
|
||||||
</p>
|
|
||||||
|
<svg-message
|
||||||
|
v-else-if="!isLoading && !characterError && !repos.length"
|
||||||
|
id="no-container-images"
|
||||||
|
:svg-path="noContainersImage"
|
||||||
|
>
|
||||||
|
<h4>
|
||||||
|
{{ s__('ContainerRegistry|There are no container images stored for this project') }}
|
||||||
|
</h4>
|
||||||
|
<p v-html="noContainerImagesText"></p>
|
||||||
|
|
||||||
|
<h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
s__(
|
||||||
|
'ContainerRegistry|You can add an image to this registry with the following commands:',
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre>
|
||||||
|
docker build -t {{ repositoryUrl }} .
|
||||||
|
docker push {{ repositoryUrl }}
|
||||||
|
</pre>
|
||||||
|
</svg-message>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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);
|
const { dataset } = document.querySelector(this.$options.el);
|
||||||
return {
|
return {
|
||||||
endpoint: dataset.endpoint,
|
endpoint: dataset.endpoint,
|
||||||
|
characterError: Boolean(dataset.characterError),
|
||||||
|
helpPagePath: dataset.helpPagePath,
|
||||||
|
noContainersImage: dataset.noContainersImage,
|
||||||
|
containersErrorImage: dataset.containersErrorImage,
|
||||||
|
repositoryUrl: dataset.repositoryUrl,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement('registry-app', {
|
return createElement('registry-app', {
|
||||||
props: {
|
props: {
|
||||||
endpoint: this.endpoint,
|
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: {
|
computed: {
|
||||||
releasedTimeAgo() {
|
releasedTimeAgo() {
|
||||||
return sprintf(__('released %{time}'), {
|
return sprintf(__('released %{time}'), {
|
||||||
time: this.timeFormated(this.release.created_at),
|
time: this.timeFormated(this.release.released_at),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
userImageAltDescription() {
|
userImageAltDescription() {
|
||||||
|
@ -56,8 +56,8 @@ export default {
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title mt-0">
|
<h2 class="card-title mt-0">
|
||||||
{{ release.name }}
|
{{ release.name }}
|
||||||
<gl-badge v-if="release.pre_release" variant="warning" class="align-middle">{{
|
<gl-badge v-if="release.upcoming_release" variant="warning" class="align-middle">{{
|
||||||
__('Pre-release')
|
__('Upcoming Release')
|
||||||
}}</gl-badge>
|
}}</gl-badge>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ export default {
|
||||||
|
|
||||||
<div class="append-right-4">
|
<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 }}
|
{{ releasedTimeAgo }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,7 +39,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<div class="timeline-icon">
|
||||||
<user-avatar-link
|
<user-avatar-link
|
||||||
:link-href="getUserData.path"
|
:link-href="getUserData.path"
|
||||||
|
|
|
@ -2,6 +2,12 @@
|
||||||
* Container Registry
|
* Container Registry
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.container-message {
|
||||||
|
pre {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.container-image {
|
.container-image {
|
||||||
border-bottom: 1px solid $white-normal;
|
border-bottom: 1px solid $white-normal;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1093,6 +1093,17 @@ table.code {
|
||||||
line-height: 0;
|
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) {
|
@media (max-width: map-get($grid-breakpoints, md)-1) {
|
||||||
.diffs .files {
|
.diffs .files {
|
||||||
@include fixed-width-container;
|
@include fixed-width-container;
|
||||||
|
@ -1110,6 +1121,11 @@ table.code {
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.discussion-collapsible {
|
||||||
|
margin: $gl-padding;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-diff-overlay,
|
.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-created-ago,
|
||||||
.note-updated-at {
|
.note-updated-at {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
|
@ -462,6 +472,14 @@ $note-form-margin-left: 72px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notes-content .discussion-notes.diff-discussions {
|
||||||
|
border-bottom: 1px solid $border-color;
|
||||||
|
|
||||||
|
&:nth-last-child(1) {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.notes_holder {
|
.notes_holder {
|
||||||
font-family: $regular-font;
|
font-family: $regular-font;
|
||||||
|
|
||||||
|
@ -517,6 +535,17 @@ $note-form-margin-left: 72px;
|
||||||
.discussion-reply-holder {
|
.discussion-reply-holder {
|
||||||
border-radius: 0 0 $border-radius-default $border-radius-default;
|
border-radius: 0 0 $border-radius-default $border-radius-default;
|
||||||
position: relative;
|
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?
|
repository.save! if repository.has_tags?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
rescue ContainerRegistry::Path::InvalidRegistryPathError
|
||||||
|
@character_error = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -99,7 +99,7 @@ module Projects
|
||||||
end
|
end
|
||||||
|
|
||||||
def deploy_token_params
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,11 +35,8 @@ module Clusters
|
||||||
'stable/nginx-ingress'
|
'stable/nginx-ingress'
|
||||||
end
|
end
|
||||||
|
|
||||||
# We will implement this in future MRs.
|
|
||||||
# Basically we need to check all dependent applications are not installed
|
|
||||||
# first.
|
|
||||||
def allowed_to_uninstall?
|
def allowed_to_uninstall?
|
||||||
false
|
external_ip_or_hostname? && application_jupyter_nil_or_installable?
|
||||||
end
|
end
|
||||||
|
|
||||||
def install_command
|
def install_command
|
||||||
|
@ -52,6 +49,10 @@ module Clusters
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def external_ip_or_hostname?
|
||||||
|
external_ip.present? || external_hostname.present?
|
||||||
|
end
|
||||||
|
|
||||||
def schedule_status_update
|
def schedule_status_update
|
||||||
return unless installed?
|
return unless installed?
|
||||||
return if external_ip
|
return if external_ip
|
||||||
|
@ -63,6 +64,12 @@ module Clusters
|
||||||
def ingress_service
|
def ingress_service
|
||||||
cluster.kubeclient.get_service('ingress-nginx-ingress-controller', Gitlab::Kubernetes::Helm::NAMESPACE)
|
cluster.kubeclient.get_service('ingress-nginx-ingress-controller', Gitlab::Kubernetes::Helm::NAMESPACE)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def application_jupyter_nil_or_installable?
|
||||||
|
cluster.application_jupyter.nil? || cluster.application_jupyter&.installable?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,9 +23,7 @@ module Clusters
|
||||||
return unless cluster&.application_ingress_available?
|
return unless cluster&.application_ingress_available?
|
||||||
|
|
||||||
ingress = cluster.application_ingress
|
ingress = cluster.application_ingress
|
||||||
if ingress.external_ip || ingress.external_hostname
|
self.status = 'installable' if ingress.external_ip_or_hostname?
|
||||||
self.status = 'installable'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def chart
|
def chart
|
||||||
|
|
|
@ -49,14 +49,6 @@ module Clusters
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def uninstall_command
|
|
||||||
Gitlab::Kubernetes::Helm::DeleteCommand.new(
|
|
||||||
name: name,
|
|
||||||
rbac: cluster.platform_kubernetes_rbac?,
|
|
||||||
files: files
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
def upgrade_command(values)
|
def upgrade_command(values)
|
||||||
::Gitlab::Kubernetes::Helm::InstallCommand.new(
|
::Gitlab::Kubernetes::Helm::InstallCommand.new(
|
||||||
name: name,
|
name: name,
|
||||||
|
|
|
@ -19,9 +19,9 @@
|
||||||
#
|
#
|
||||||
# - `statistic_attribute` must be an ActiveRecord attribute
|
# - `statistic_attribute` must be an ActiveRecord attribute
|
||||||
# - The model must implement `project` and `project_id`. i.e. direct Project relationship or delegation
|
# - The model must implement `project` and `project_id`. i.e. direct Project relationship or delegation
|
||||||
#
|
|
||||||
module UpdateProjectStatistics
|
module UpdateProjectStatistics
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
include AfterCommitQueue
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
attr_reader :project_statistics_name, :statistic_attribute
|
attr_reader :project_statistics_name, :statistic_attribute
|
||||||
|
@ -31,7 +31,6 @@ module UpdateProjectStatistics
|
||||||
#
|
#
|
||||||
# - project_statistics_name: A column of `ProjectStatistics` to update
|
# - project_statistics_name: A column of `ProjectStatistics` to update
|
||||||
# - statistic_attribute: An attribute of the current model, default to `size`
|
# - statistic_attribute: An attribute of the current model, default to `size`
|
||||||
#
|
|
||||||
def update_project_statistics(project_statistics_name:, statistic_attribute: :size)
|
def update_project_statistics(project_statistics_name:, statistic_attribute: :size)
|
||||||
@project_statistics_name = project_statistics_name
|
@project_statistics_name = project_statistics_name
|
||||||
@statistic_attribute = statistic_attribute
|
@statistic_attribute = statistic_attribute
|
||||||
|
@ -51,6 +50,7 @@ module UpdateProjectStatistics
|
||||||
delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i
|
delta = read_attribute(attr).to_i - attribute_before_last_save(attr).to_i
|
||||||
|
|
||||||
update_project_statistics(delta)
|
update_project_statistics(delta)
|
||||||
|
schedule_namespace_aggregation_worker
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_project_statistics_attribute_changed?
|
def update_project_statistics_attribute_changed?
|
||||||
|
@ -59,6 +59,8 @@ module UpdateProjectStatistics
|
||||||
|
|
||||||
def update_project_statistics_after_destroy
|
def update_project_statistics_after_destroy
|
||||||
update_project_statistics(-read_attribute(self.class.statistic_attribute).to_i)
|
update_project_statistics(-read_attribute(self.class.statistic_attribute).to_i)
|
||||||
|
|
||||||
|
schedule_namespace_aggregation_worker
|
||||||
end
|
end
|
||||||
|
|
||||||
def project_destroyed?
|
def project_destroyed?
|
||||||
|
@ -68,5 +70,18 @@ module UpdateProjectStatistics
|
||||||
def update_project_statistics(delta)
|
def update_project_statistics(delta)
|
||||||
ProjectStatistics.increment_statistic(project_id, self.class.project_statistics_name, delta)
|
ProjectStatistics.increment_statistic(project_id, self.class.project_statistics_name, delta)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,6 +16,14 @@ class DeployToken < ApplicationRecord
|
||||||
has_many :projects, through: :project_deploy_tokens
|
has_many :projects, through: :project_deploy_tokens
|
||||||
|
|
||||||
validate :ensure_at_least_one_scope
|
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
|
before_save :ensure_token
|
||||||
|
|
||||||
accepts_nested_attributes_for :project_deploy_tokens
|
accepts_nested_attributes_for :project_deploy_tokens
|
||||||
|
@ -39,7 +47,7 @@ class DeployToken < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def username
|
def username
|
||||||
"gitlab+deploy-token-#{id}"
|
super || default_username
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_access_to?(requested_project)
|
def has_access_to?(requested_project)
|
||||||
|
@ -75,4 +83,8 @@ class DeployToken < ApplicationRecord
|
||||||
def ensure_at_least_one_scope
|
def ensure_at_least_one_scope
|
||||||
errors.add(:base, "Scopes can't be blank") unless read_repository || read_registry
|
errors.add(:base, "Scopes can't be blank") unless read_repository || read_registry
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def default_username
|
||||||
|
"gitlab+deploy-token-#{id}" if persisted?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -293,6 +293,10 @@ class Namespace < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def aggregation_scheduled?
|
||||||
|
aggregation_schedule.present?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def parent_changed?
|
def parent_changed?
|
||||||
|
|
|
@ -1,7 +1,47 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Namespace::AggregationSchedule < ApplicationRecord
|
class Namespace::AggregationSchedule < ApplicationRecord
|
||||||
|
include AfterCommitQueue
|
||||||
|
include ExclusiveLeaseGuard
|
||||||
|
|
||||||
self.primary_key = :namespace_id
|
self.primary_key = :namespace_id
|
||||||
|
|
||||||
|
DEFAULT_LEASE_TIMEOUT = 3.hours
|
||||||
|
REDIS_SHARED_KEY = 'gitlab:update_namespace_statistics_delay'.freeze
|
||||||
|
|
||||||
belongs_to :namespace
|
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
|
end
|
||||||
|
|
|
@ -1,10 +1,38 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Namespace::RootStorageStatistics < ApplicationRecord
|
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
|
self.primary_key = :namespace_id
|
||||||
|
|
||||||
belongs_to :namespace
|
belongs_to :namespace
|
||||||
has_one :route, through: :namespace
|
has_one :route, through: :namespace
|
||||||
|
|
||||||
delegate :all_projects, to: :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
|
end
|
||||||
|
|
|
@ -3,22 +3,14 @@
|
||||||
class BugzillaService < IssueTrackerService
|
class BugzillaService < IssueTrackerService
|
||||||
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
|
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
|
def default_title
|
||||||
if self.properties && self.properties['title'].present?
|
'Bugzilla'
|
||||||
self.properties['title']
|
|
||||||
else
|
|
||||||
'Bugzilla'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def description
|
def default_description
|
||||||
if self.properties && self.properties['description'].present?
|
s_('IssueTracker|Bugzilla issue tracker')
|
||||||
self.properties['description']
|
|
||||||
else
|
|
||||||
'Bugzilla issue tracker'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.to_param
|
def self.to_param
|
||||||
|
|
|
@ -5,24 +5,12 @@ class CustomIssueTrackerService < IssueTrackerService
|
||||||
|
|
||||||
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
|
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
|
||||||
|
|
||||||
def title
|
def default_title
|
||||||
if self.properties && self.properties['title'].present?
|
'Custom Issue Tracker'
|
||||||
self.properties['title']
|
|
||||||
else
|
|
||||||
'Custom Issue Tracker'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def title=(value)
|
def default_description
|
||||||
self.properties['title'] = value if self.properties
|
s_('IssueTracker|Custom issue tracker')
|
||||||
end
|
|
||||||
|
|
||||||
def description
|
|
||||||
if self.properties && self.properties['description'].present?
|
|
||||||
self.properties['description']
|
|
||||||
else
|
|
||||||
'Custom issue tracker'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.to_param
|
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?
|
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
|
default_value_for :default, true
|
||||||
|
|
||||||
|
def default_title
|
||||||
|
'GitLab'
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_description
|
||||||
|
s_('IssueTracker|GitLab issue tracker')
|
||||||
|
end
|
||||||
|
|
||||||
def self.to_param
|
def self.to_param
|
||||||
'gitlab'
|
'gitlab'
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,6 +5,8 @@ class IssueTrackerService < Service
|
||||||
|
|
||||||
default_value_for :category, 'issue_tracker'
|
default_value_for :category, 'issue_tracker'
|
||||||
|
|
||||||
|
before_save :handle_properties
|
||||||
|
|
||||||
# Pattern used to extract links from comments
|
# Pattern used to extract links from comments
|
||||||
# Override this method on services that uses different patterns
|
# Override this method on services that uses different patterns
|
||||||
# This pattern does not support cross-project references
|
# This pattern does not support cross-project references
|
||||||
|
@ -18,6 +20,37 @@ class IssueTrackerService < Service
|
||||||
end
|
end
|
||||||
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?
|
def default?
|
||||||
default
|
default
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,7 +17,7 @@ class JiraService < IssueTrackerService
|
||||||
# Jira Cloud version is deprecating authentication via username and password.
|
# 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,
|
# 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.
|
# 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
|
before_update :reset_password
|
||||||
|
|
||||||
|
@ -37,7 +37,6 @@ class JiraService < IssueTrackerService
|
||||||
def initialize_properties
|
def initialize_properties
|
||||||
super do
|
super do
|
||||||
self.properties = {
|
self.properties = {
|
||||||
title: issues_tracker['title'],
|
|
||||||
url: issues_tracker['url'],
|
url: issues_tracker['url'],
|
||||||
api_url: issues_tracker['api_url']
|
api_url: issues_tracker['api_url']
|
||||||
}
|
}
|
||||||
|
@ -74,20 +73,12 @@ class JiraService < IssueTrackerService
|
||||||
[Jira service documentation](#{help_page_url('user/project/integrations/jira')})."
|
[Jira service documentation](#{help_page_url('user/project/integrations/jira')})."
|
||||||
end
|
end
|
||||||
|
|
||||||
def title
|
def default_title
|
||||||
if self.properties && self.properties['title'].present?
|
'Jira'
|
||||||
self.properties['title']
|
|
||||||
else
|
|
||||||
'Jira'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def description
|
def default_description
|
||||||
if self.properties && self.properties['description'].present?
|
s_('JiraService|Jira issue tracker')
|
||||||
self.properties['description']
|
|
||||||
else
|
|
||||||
s_('JiraService|Jira issue tracker')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.to_param
|
def self.to_param
|
||||||
|
|
|
@ -3,22 +3,14 @@
|
||||||
class RedmineService < IssueTrackerService
|
class RedmineService < IssueTrackerService
|
||||||
validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
|
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
|
def default_title
|
||||||
if self.properties && self.properties['title'].present?
|
'Redmine'
|
||||||
self.properties['title']
|
|
||||||
else
|
|
||||||
'Redmine'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def description
|
def default_description
|
||||||
if self.properties && self.properties['description'].present?
|
s_('IssueTracker|Redmine issue tracker')
|
||||||
self.properties['description']
|
|
||||||
else
|
|
||||||
'Redmine issue tracker'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.to_param
|
def self.to_param
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
class YoutrackService < IssueTrackerService
|
class YoutrackService < IssueTrackerService
|
||||||
validates :project_url, :issues_url, presence: true, public_url: true, if: :activated?
|
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
|
# {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030
|
||||||
def self.reference_pattern(only_long: false)
|
def self.reference_pattern(only_long: false)
|
||||||
|
@ -14,16 +14,12 @@ class YoutrackService < IssueTrackerService
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def title
|
def default_title
|
||||||
'YouTrack'
|
'YouTrack'
|
||||||
end
|
end
|
||||||
|
|
||||||
def description
|
def default_description
|
||||||
if self.properties && self.properties['description'].present?
|
s_('IssueTracker|YouTrack issue tracker')
|
||||||
self.properties['description']
|
|
||||||
else
|
|
||||||
'YouTrack issue tracker'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.to_param
|
def self.to_param
|
||||||
|
|
|
@ -12,12 +12,16 @@ class Release < ApplicationRecord
|
||||||
|
|
||||||
has_many :links, class_name: 'Releases::Link'
|
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
|
accepts_nested_attributes_for :links, allow_destroy: true
|
||||||
|
|
||||||
validates :description, :project, :tag, presence: true
|
validates :description, :project, :tag, presence: true
|
||||||
validates :name, presence: true, on: :create
|
validates :name, presence: true, on: :create
|
||||||
|
|
||||||
scope :sorted, -> { order(created_at: :desc) }
|
scope :sorted, -> { order(released_at: :desc) }
|
||||||
|
|
||||||
delegate :repository, to: :project
|
delegate :repository, to: :project
|
||||||
|
|
||||||
|
@ -44,6 +48,10 @@ class Release < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def upcoming_release?
|
||||||
|
released_at.present? && released_at > Time.zone.now
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def actual_sha
|
def actual_sha
|
||||||
|
|
|
@ -129,7 +129,7 @@ class Service < ApplicationRecord
|
||||||
|
|
||||||
def api_field_names
|
def api_field_names
|
||||||
fields.map { |field| field[:name] }
|
fields.map { |field| field[:name] }
|
||||||
.reject { |field_name| field_name =~ /(password|token|key)/ }
|
.reject { |field_name| field_name =~ /(password|token|key|title|description)/ }
|
||||||
end
|
end
|
||||||
|
|
||||||
def global_fields
|
def global_fields
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
module DeployTokens
|
module DeployTokens
|
||||||
class CreateService < BaseService
|
class CreateService < BaseService
|
||||||
def execute
|
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
|
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]
|
params[:description]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def released_at
|
||||||
|
params[:released_at]
|
||||||
|
end
|
||||||
|
|
||||||
def release
|
def release
|
||||||
strong_memoize(:release) do
|
strong_memoize(:release) do
|
||||||
project.releases.find_by_tag(tag_name)
|
project.releases.find_by_tag(tag_name)
|
||||||
|
|
|
@ -58,6 +58,7 @@ module Releases
|
||||||
author: current_user,
|
author: current_user,
|
||||||
tag: tag.name,
|
tag: tag.name,
|
||||||
sha: tag.dereferenced_target.sha,
|
sha: tag.dereferenced_target.sha,
|
||||||
|
released_at: released_at,
|
||||||
links_attributes: params.dig(:assets, 'links') || []
|
links_attributes: params.dig(:assets, 'links') || []
|
||||||
)
|
)
|
||||||
end
|
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_errors(@cluster)
|
||||||
.form-group
|
.form-group
|
||||||
%h5= s_('ClusterIntegration|Integration status')
|
%h5= s_('ClusterIntegration|Integration status')
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
= f.text_field :id, class: 'form-control w-auto', readonly: true
|
= f.text_field :id, class: 'form-control w-auto', readonly: true
|
||||||
|
|
||||||
.row.prepend-top-8
|
.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.label :description, _('Group description (optional)'), class: 'label-bold'
|
||||||
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
|
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,11 @@
|
||||||
= f.label :expires_at, class: 'label-bold'
|
= 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
|
= 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
|
.form-group
|
||||||
= f.label :scopes, class: 'label-bold'
|
= f.label :scopes, class: 'label-bold'
|
||||||
%fieldset.form-group.form-check
|
%fieldset.form-group.form-check
|
||||||
|
|
|
@ -1,49 +1,9 @@
|
||||||
- page_title "Container Registry"
|
|
||||||
|
|
||||||
%section
|
%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
|
.row.registry-placeholder.prepend-bottom-10
|
||||||
.col-lg-12
|
.col-12
|
||||||
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } }
|
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json),
|
||||||
|
"help_page_path" => help_page_path('user/project/container_registry'),
|
||||||
.row.prepend-top-10
|
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
|
||||||
.col-lg-12
|
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
|
||||||
.card
|
"repository_url" => escape_once(@project.container_registry_url),
|
||||||
.card-header
|
character_error: @character_error.to_s } }
|
||||||
= 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
|
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
- cronjob:issue_due_scheduler
|
- cronjob:issue_due_scheduler
|
||||||
- cronjob:prune_web_hook_logs
|
- cronjob:prune_web_hook_logs
|
||||||
- cronjob:schedule_migrate_external_diffs
|
- cronjob:schedule_migrate_external_diffs
|
||||||
|
- cronjob:namespaces_prune_aggregation_schedules
|
||||||
|
|
||||||
- gcp_cluster:cluster_install_app
|
- gcp_cluster:cluster_install_app
|
||||||
- gcp_cluster:cluster_patch_app
|
- gcp_cluster:cluster_patch_app
|
||||||
|
@ -101,6 +102,9 @@
|
||||||
- todos_destroyer:todos_destroyer_project_private
|
- todos_destroyer:todos_destroyer_project_private
|
||||||
- todos_destroyer:todos_destroyer_private_features
|
- 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_create
|
||||||
- object_pool:object_pool_schedule_join
|
- object_pool:object_pool_schedule_join
|
||||||
- object_pool:object_pool_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'] ||= Settingslogic.new({})
|
||||||
Settings.cron_jobs['schedule_migrate_external_diffs_worker']['cron'] ||= '15 * * * *'
|
Settings.cron_jobs['schedule_migrate_external_diffs_worker']['cron'] ||= '15 * * * *'
|
||||||
Settings.cron_jobs['schedule_migrate_external_diffs_worker']['job_class'] = 'ScheduleMigrateExternalDiffsWorker'
|
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
|
Gitlab.ee do
|
||||||
Settings.cron_jobs['clear_shared_runners_minutes_worker'] ||= Settingslogic.new({})
|
Settings.cron_jobs['clear_shared_runners_minutes_worker'] ||= Settingslogic.new({})
|
||||||
|
|
|
@ -1,15 +1,27 @@
|
||||||
require 'prometheus/client'
|
require 'prometheus/client'
|
||||||
require 'prometheus/client/support/unicorn'
|
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|
|
Prometheus::Client.configure do |config|
|
||||||
config.logger = Rails.logger
|
config.logger = Rails.logger
|
||||||
|
|
||||||
config.initial_mmap_file_size = 4 * 1024
|
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 = ENV['prometheus_multiproc_dir'] || prometheus_default_multiproc_dir
|
||||||
config.multiprocess_files_dir ||= Rails.root.join('tmp/prometheus_multiproc_dir')
|
|
||||||
end
|
|
||||||
|
|
||||||
config.pid_provider = Prometheus::Client::Support::Unicorn.method(:worker_pid_provider)
|
config.pid_provider = Prometheus::Client::Support::Unicorn.method(:worker_pid_provider)
|
||||||
end
|
end
|
||||||
|
@ -29,15 +41,13 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
|
||||||
Gitlab::Cluster::LifecycleEvents.on_worker_start do
|
Gitlab::Cluster::LifecycleEvents.on_worker_start do
|
||||||
defined?(::Prometheus::Client.reinitialize_on_pid_change) && Prometheus::Client.reinitialize_on_pid_change
|
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
|
Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start
|
||||||
end
|
end
|
||||||
|
|
||||||
if defined?(::Puma)
|
Gitlab::Cluster::LifecycleEvents.on_master_start do
|
||||||
Gitlab::Cluster::LifecycleEvents.on_master_start do
|
if defined?(::Unicorn)
|
||||||
|
Gitlab::Metrics::Samplers::UnicornSampler.initialize_instance(Settings.monitoring.unicorn_sampler_interval).start
|
||||||
|
elsif defined?(::Puma)
|
||||||
Gitlab::Metrics::Samplers::PumaSampler.initialize_instance(Settings.monitoring.puma_sampler_interval).start
|
Gitlab::Metrics::Samplers::PumaSampler.initialize_instance(Settings.monitoring.puma_sampler_interval).start
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,12 +4,22 @@
|
||||||
|
|
||||||
ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
|
ActiveSupport::Notifications.subscribe('rack.attack') do |name, start, finish, request_id, req|
|
||||||
if [:throttle, :blacklist].include? req.env['rack.attack.match_type']
|
if [:throttle, :blacklist].include? req.env['rack.attack.match_type']
|
||||||
Gitlab::AuthLogger.error(
|
rack_attack_info = {
|
||||||
message: 'Rack_Attack',
|
message: 'Rack_Attack',
|
||||||
env: req.env['rack.attack.match_type'],
|
env: req.env['rack.attack.match_type'],
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
request_method: req.request_method,
|
request_method: req.request_method,
|
||||||
fullpath: req.fullpath
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -94,6 +94,7 @@
|
||||||
- [migrate_external_diffs, 1]
|
- [migrate_external_diffs, 1]
|
||||||
- [update_project_statistics, 1]
|
- [update_project_statistics, 1]
|
||||||
- [phabricator_import_import_tasks, 1]
|
- [phabricator_import_import_tasks, 1]
|
||||||
|
- [update_namespace_statistics, 1]
|
||||||
|
|
||||||
# EE-specific queues
|
# EE-specific queues
|
||||||
- [ldap_group_sync, 2]
|
- [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