Commit c158fa8d authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent b806264d
...@@ -374,7 +374,7 @@ export default { ...@@ -374,7 +374,7 @@ export default {
<div <div
:data-can-create-note="getNoteableData.current_user.can_create_note" :data-can-create-note="getNoteableData.current_user.can_create_note"
class="files d-flex prepend-top-default" class="files d-flex"
> >
<div <div
v-show="showTreeList" v-show="showTreeList"
......
<script> <script>
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import { mapActions, mapGetters, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui'; import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
import { __ } from '~/locale'; import { __ } from '~/locale';
...@@ -63,9 +62,6 @@ export default { ...@@ -63,9 +62,6 @@ export default {
showDropdowns() { showDropdowns() {
return !this.commit && this.mergeRequestDiffs.length; return !this.commit && this.mergeRequestDiffs.length;
}, },
fileTreeIcon() {
return this.showTreeList ? 'collapse-left' : 'expand-left';
},
toggleFileBrowserTitle() { toggleFileBrowserTitle() {
return this.showTreeList ? __('Hide file browser') : __('Show file browser'); return this.showTreeList ? __('Hide file browser') : __('Show file browser');
}, },
...@@ -91,7 +87,7 @@ export default { ...@@ -91,7 +87,7 @@ export default {
</script> </script>
<template> <template>
<div class="mr-version-controls border-top border-bottom"> <div class="mr-version-controls border-top">
<div <div
class="mr-version-menus-container content-block" class="mr-version-menus-container content-block"
:class="{ :class="{
...@@ -108,17 +104,17 @@ export default { ...@@ -108,17 +104,17 @@ export default {
:title="toggleFileBrowserTitle" :title="toggleFileBrowserTitle"
@click="toggleShowTreeList" @click="toggleShowTreeList"
> >
<icon :name="fileTreeIcon" /> <icon name="file-tree" />
</button> </button>
<div v-if="showDropdowns" class="d-flex align-items-center compare-versions-container"> <div v-if="showDropdowns" class="d-flex align-items-center compare-versions-container">
Changes between {{ __('Compare') }}
<compare-versions-dropdown <compare-versions-dropdown
:other-versions="mergeRequestDiffs" :other-versions="mergeRequestDiffs"
:merge-request-version="mergeRequestDiff" :merge-request-version="mergeRequestDiff"
:show-commit-count="true" :show-commit-count="true"
class="mr-version-dropdown" class="mr-version-dropdown"
/> />
and {{ __('and') }}
<compare-versions-dropdown <compare-versions-dropdown
:other-versions="comparableDiffs" :other-versions="comparableDiffs"
:base-version-path="baseVersionPath" :base-version-path="baseVersionPath"
......
...@@ -123,6 +123,20 @@ export default { ...@@ -123,6 +123,20 @@ export default {
} }
return s__('MRDiff|Show full file'); return s__('MRDiff|Show full file');
}, },
changedFile() {
const {
new_path: changed,
deleted_file: deleted,
new_file: tempFile,
...diffFile
} = this.diffFile;
return {
...diffFile,
changed: Boolean(changed),
deleted,
tempFile,
};
},
}, },
mounted() { mounted() {
polyfillSticky(this.$refs.header); polyfillSticky(this.$refs.header);
...@@ -221,7 +235,7 @@ export default { ...@@ -221,7 +235,7 @@ export default {
<div <div
v-if="!diffFile.submodule && addMergeRequestButtons" v-if="!diffFile.submodule && addMergeRequestButtons"
class="file-actions d-none d-sm-block" class="file-actions d-none d-sm-flex align-items-center"
> >
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" /> <diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
<div class="btn-group" role="group"> <div class="btn-group" role="group">
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
export default { export default {
components: { Icon },
props: { props: {
addedLines: { addedLines: {
type: Number, type: Number,
...@@ -21,7 +19,7 @@ export default { ...@@ -21,7 +19,7 @@ export default {
}, },
computed: { computed: {
filesText() { filesText() {
return n__('File', 'Files', this.diffFilesLength); return n__('file', 'files', this.diffFilesLength);
}, },
isCompareVersionsHeader() { isCompareVersionsHeader() {
return Boolean(this.diffFilesLength); return Boolean(this.diffFilesLength);
...@@ -39,14 +37,21 @@ export default { ...@@ -39,14 +37,21 @@ export default {
}" }"
> >
<div v-if="diffFilesLength !== null" class="diff-stats-group"> <div v-if="diffFilesLength !== null" class="diff-stats-group">
<icon name="doc-code" class="diff-stats-icon text-secondary" /> <span class="text-secondary bold">{{ diffFilesLength }} {{ filesText }}</span>
<strong>{{ diffFilesLength }} {{ filesText }}</strong>
</div> </div>
<div class="diff-stats-group cgreen"> <div
<icon name="file-addition" class="diff-stats-icon" /> <strong>{{ addedLines }}</strong> class="diff-stats-group cgreen d-flex align-items-center"
:class="{ bold: isCompareVersionsHeader }"
>
<span>+</span>
<span class="js-file-addition-line">{{ addedLines }}</span>
</div> </div>
<div class="diff-stats-group cred"> <div
<icon name="file-deletion" class="diff-stats-icon" /> <strong>{{ removedLines }}</strong> class="diff-stats-group cred d-flex align-items-center"
:class="{ bold: isCompareVersionsHeader }"
>
<span>-</span>
<span class="js-file-deletion-line">{{ removedLines }}</span>
</div> </div>
</div> </div>
</template> </template>
...@@ -4,7 +4,6 @@ import { GlTooltipDirective } from '@gitlab/ui'; ...@@ -4,7 +4,6 @@ import { GlTooltipDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue'; import FileRow from '~/vue_shared/components/file_row.vue';
import FileRowStats from './file_row_stats.vue';
export default { export default {
directives: { directives: {
...@@ -48,9 +47,6 @@ export default { ...@@ -48,9 +47,6 @@ export default {
return acc; return acc;
}, []); }, []);
}, },
fileRowExtraComponent() {
return this.hideFileStats ? null : FileRowStats;
},
}, },
methods: { methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']), ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
...@@ -58,8 +54,8 @@ export default { ...@@ -58,8 +54,8 @@ export default {
this.search = ''; this.search = '';
}, },
}, },
searchPlaceholder: sprintf(s__('MergeRequest|Filter files or search with %{modifier_key}+p'), { searchPlaceholder: sprintf(s__('MergeRequest|Search files (%{modifier_key}P)'), {
modifier_key: /Mac/i.test(navigator.userAgent) ? 'cmd' : 'ctrl', modifier_key: /Mac/i.test(navigator.userAgent) ? '' : 'Ctrl+',
}), }),
}; };
</script> </script>
...@@ -97,7 +93,6 @@ export default { ...@@ -97,7 +93,6 @@ export default {
:file="file" :file="file"
:level="0" :level="0"
:hide-extra-on-tree="true" :hide-extra-on-tree="true"
:extra-component="fileRowExtraComponent"
:show-changed-icon="true" :show-changed-icon="true"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile" @clickFile="scrollToFile"
......
...@@ -6,6 +6,7 @@ import CommitMessageField from './message_field.vue'; ...@@ -6,6 +6,7 @@ import CommitMessageField from './message_field.vue';
import Actions from './actions.vue'; import Actions from './actions.vue';
import SuccessMessage from './success_message.vue'; import SuccessMessage from './success_message.vue';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants'; import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
components: { components: {
...@@ -14,6 +15,7 @@ export default { ...@@ -14,6 +15,7 @@ export default {
CommitMessageField, CommitMessageField,
SuccessMessage, SuccessMessage,
}, },
mixins: [glFeatureFlagsMixin()],
data() { data() {
return { return {
isCompact: true, isCompact: true,
...@@ -27,9 +29,13 @@ export default { ...@@ -27,9 +29,13 @@ export default {
...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']), ...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
overviewText() { overviewText() {
return sprintf( return sprintf(
__( this.glFeatures.stageAllByDefault
'<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes', ? __(
), '<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes',
)
: __(
'<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
),
{ {
stagedFilesLength: this.stagedFiles.length, stagedFilesLength: this.stagedFiles.length,
changedFilesLength: this.changedFiles.length, changedFilesLength: this.changedFiles.length,
...@@ -39,6 +45,10 @@ export default { ...@@ -39,6 +45,10 @@ export default {
commitButtonText() { commitButtonText() {
return this.stagedFiles.length ? __('Commit') : __('Stage & Commit'); return this.stagedFiles.length ? __('Commit') : __('Stage & Commit');
}, },
currentViewIsCommitView() {
return this.currentActivityView === activityBarViews.commit;
},
}, },
watch: { watch: {
currentActivityView() { currentActivityView() {
...@@ -46,27 +56,26 @@ export default { ...@@ -46,27 +56,26 @@ export default {
this.isCompact = false; this.isCompact = false;
} else { } else {
this.isCompact = !( this.isCompact = !(
this.currentActivityView === activityBarViews.commit && this.currentViewIsCommitView && window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
); );
} }
}, },
lastCommitMsg() {
this.isCompact =
this.currentActivityView !== activityBarViews.commit && this.lastCommitMsg === '';
},
}, },
methods: { methods: {
...mapActions(['updateActivityBarView']), ...mapActions(['updateActivityBarView']),
...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']), ...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
toggleIsSmall() { toggleIsCompact() {
this.updateActivityBarView(activityBarViews.commit) if (this.currentViewIsCommitView) {
.then(() => { this.isCompact = !this.isCompact;
this.isCompact = !this.isCompact; } else {
}) this.updateActivityBarView(activityBarViews.commit)
.catch(e => { .then(() => {
throw e; this.isCompact = false;
}); })
.catch(e => {
throw e;
});
}
}, },
beforeEnterTransition() { beforeEnterTransition() {
const elHeight = this.isCompact const elHeight = this.isCompact
...@@ -114,7 +123,7 @@ export default { ...@@ -114,7 +123,7 @@ export default {
:disabled="!hasChanges" :disabled="!hasChanges"
type="button" type="button"
class="btn btn-primary btn-sm btn-block qa-begin-commit-button" class="btn btn-primary btn-sm btn-block qa-begin-commit-button"
@click="toggleIsSmall" @click="toggleIsCompact"
> >
{{ __('Commit…') }} {{ __('Commit…') }}
</button> </button>
...@@ -148,7 +157,7 @@ export default { ...@@ -148,7 +157,7 @@ export default {
v-else v-else
type="button" type="button"
class="btn btn-default btn-sm float-right" class="btn btn-default btn-sm float-right"
@click="toggleIsSmall" @click="toggleIsCompact"
> >
{{ __('Collapse') }} {{ __('Collapse') }}
</button> </button>
......
...@@ -6,6 +6,7 @@ import Icon from '~/vue_shared/components/icon.vue'; ...@@ -6,6 +6,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import NewDropdown from './new_dropdown/index.vue'; import NewDropdown from './new_dropdown/index.vue';
import MrFileIcon from './mr_file_icon.vue'; import MrFileIcon from './mr_file_icon.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
name: 'FileRowExtra', name: 'FileRowExtra',
...@@ -18,6 +19,7 @@ export default { ...@@ -18,6 +19,7 @@ export default {
ChangedFileIcon, ChangedFileIcon,
MrFileIcon, MrFileIcon,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
file: { file: {
type: Object, type: Object,
...@@ -55,10 +57,15 @@ export default { ...@@ -55,10 +57,15 @@ export default {
return n__('%d staged change', '%d staged changes', this.folderStagedCount); return n__('%d staged change', '%d staged changes', this.folderStagedCount);
} }
return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), { return sprintf(
unstaged: this.folderUnstagedCount, this.glFeatures.stageAllByDefault
staged: this.folderStagedCount, ? __('%{staged} staged and %{unstaged} unstaged changes')
}); : __('%{unstaged} unstaged and %{staged} staged changes'),
{
unstaged: this.folderUnstagedCount,
staged: this.folderStagedCount,
},
);
}, },
showTreeChangesCount() { showTreeChangesCount() {
return this.isTree && this.changesCount > 0 && !this.file.opened; return this.isTree && this.changesCount > 0 && !this.file.opened;
......
...@@ -51,7 +51,7 @@ export const setResizingStatus = ({ commit }, resizing) => { ...@@ -51,7 +51,7 @@ export const setResizingStatus = ({ commit }, resizing) => {
}; };
export const createTempEntry = ( export const createTempEntry = (
{ state, commit, dispatch }, { state, commit, dispatch, getters },
{ name, type, content = '', base64 = false, binary = false, rawPath = '' }, { name, type, content = '', base64 = false, binary = false, rawPath = '' },
) => { ) => {
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name; const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
...@@ -92,7 +92,11 @@ export const createTempEntry = ( ...@@ -92,7 +92,11 @@ export const createTempEntry = (
if (type === 'blob') { if (type === 'blob') {
commit(types.TOGGLE_FILE_OPEN, file.path); commit(types.TOGGLE_FILE_OPEN, file.path);
commit(types.ADD_FILE_TO_CHANGED, file.path);
if (gon.features?.stageAllByDefault)
commit(types.STAGE_CHANGE, { path: file.path, diffInfo: getters.getDiffInfo(file.path) });
else commit(types.ADD_FILE_TO_CHANGED, file.path);
dispatch('setFileActive', file.path); dispatch('setFileActive', file.path);
dispatch('triggerFilesChange'); dispatch('triggerFilesChange');
dispatch('burstUnusedSeal'); dispatch('burstUnusedSeal');
...@@ -238,7 +242,7 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => { ...@@ -238,7 +242,7 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPath }) => { export const renameEntry = ({ dispatch, commit, state, getters }, { path, name, parentPath }) => {
const entry = state.entries[path]; const entry = state.entries[path];
const newPath = parentPath ? `${parentPath}/${name}` : name; const newPath = parentPath ? `${parentPath}/${name}` : name;
const existingParent = parentPath && state.entries[parentPath]; const existingParent = parentPath && state.entries[parentPath];
...@@ -268,7 +272,10 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPat ...@@ -268,7 +272,10 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPat
if (isReset) { if (isReset) {
commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry); commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry);
} else if (!isInChanges) { } else if (!isInChanges) {
commit(types.ADD_FILE_TO_CHANGED, newPath); if (gon.features?.stageAllByDefault)
commit(types.STAGE_CHANGE, { path: newPath, diffInfo: getters.getDiffInfo(newPath) });
else commit(types.ADD_FILE_TO_CHANGED, newPath);
dispatch('burstUnusedSeal'); dispatch('burstUnusedSeal');
} }
......
...@@ -147,7 +147,7 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) = ...@@ -147,7 +147,7 @@ export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) =
}); });
}; };
export const changeFileContent = ({ commit, dispatch, state }, { path, content }) => { export const changeFileContent = ({ commit, dispatch, state, getters }, { path, content }) => {
const file = state.entries[path]; const file = state.entries[path];
commit(types.UPDATE_FILE_CONTENT, { commit(types.UPDATE_FILE_CONTENT, {
path, path,
...@@ -157,7 +157,9 @@ export const changeFileContent = ({ commit, dispatch, state }, { path, content } ...@@ -157,7 +157,9 @@ export const changeFileContent = ({ commit, dispatch, state }, { path, content }
const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path); const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path);
if (file.changed && indexOfChangedFile === -1) { if (file.changed && indexOfChangedFile === -1) {
commit(types.ADD_FILE_TO_CHANGED, path); if (gon.features?.stageAllByDefault)
commit(types.STAGE_CHANGE, { path, diffInfo: getters.getDiffInfo(path) });
else commit(types.ADD_FILE_TO_CHANGED, path);
} else if (!file.changed && !file.tempFile && indexOfChangedFile !== -1) { } else if (!file.changed && !file.tempFile && indexOfChangedFile !== -1) {
commit(types.REMOVE_FILE_FROM_CHANGED, path); commit(types.REMOVE_FILE_FROM_CHANGED, path);
} }
......
...@@ -37,7 +37,7 @@ export default { ...@@ -37,7 +37,7 @@ export default {
}} }}
</li> </li>
</ul> </ul>
<gl-loading-icon v-if="isLoading" ref="loading-icon" /> <gl-loading-icon v-if="isLoading" ref="loading-icon" size="xl" />
<settings-form v-else ref="settings-form" /> <settings-form v-else ref="settings-form" />
</div> </div>
</template> </template>
...@@ -46,7 +46,7 @@ export default { ...@@ -46,7 +46,7 @@ export default {
regexHelpText() { regexHelpText() {
return sprintf( return sprintf(
s__( s__(
'ContainerRegistry|Wildcards such as %{codeStart}*-stable%{codeEnd} or %{codeStart}production/*%{codeEnd} are supported', 'ContainerRegistry|Wildcards such as %{codeStart}*-stable%{codeEnd} or %{codeStart}production/*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}',
), ),
{ {
codeStart: '<code>', codeStart: '<code>',
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
nameRegexState() { nameRegexState() {
return this.name_regex ? this.name_regex.length <= NAME_REGEX_LENGTH : null; return this.name_regex ? this.name_regex.length <= NAME_REGEX_LENGTH : null;
}, },
formIsValid() { formIsInvalid() {
return this.nameRegexState === false; return this.nameRegexState === false;
}, },
}, },
...@@ -124,7 +124,7 @@ export default { ...@@ -124,7 +124,7 @@ export default {
:label-cols="$options.labelsConfig.cols" :label-cols="$options.labelsConfig.cols"
:label-align="$options.labelsConfig.align" :label-align="$options.labelsConfig.align"
label-for="expiration-policy-latest" label-for="expiration-policy-latest"
:label="s__('ContainerRegistry|Expiration latest:')" :label="s__('ContainerRegistry|Number of tags to retain:')"
> >
<gl-form-select id="expiration-policy-latest" v-model="keep_n" :disabled="!enabled"> <gl-form-select id="expiration-policy-latest" v-model="keep_n" :disabled="!enabled">
<option v-for="option in formOptions.keepN" :key="option.key" :value="option.key"> <option v-for="option in formOptions.keepN" :key="option.key" :value="option.key">
...@@ -138,7 +138,7 @@ export default { ...@@ -138,7 +138,7 @@ export default {
:label-cols="$options.labelsConfig.cols" :label-cols="$options.labelsConfig.cols"
:label-align="$options.labelsConfig.align" :label-align="$options.labelsConfig.align"
label-for="expiration-policy-name-matching" label-for="expiration-policy-name-matching"
:label="s__('ContainerRegistry|Expire Docker tags with name matching:')" :label="s__('ContainerRegistry|Expire Docker tags that match this regex:')"
:state="nameRegexState" :state="nameRegexState"
:invalid-feedback=" :invalid-feedback="
s__('ContainerRegistry|The value of this input should be less than 255 characters') s__('ContainerRegistry|The value of this input should be less than 255 characters')
...@@ -165,7 +165,7 @@ export default { ...@@ -165,7 +165,7 @@ export default {
<gl-button <gl-button
ref="save-button" ref="save-button"
type="submit" type="submit"
:disabled="formIsValid" :disabled="formIsInvalid"
variant="success" variant="success"
class="d-block" class="d-block"
> >
......
...@@ -18,8 +18,8 @@ export const resetSettings = ({ commit }) => commit(types.RESET_SETTINGS); ...@@ -18,8 +18,8 @@ export const resetSettings = ({ commit }) => commit(types.RESET_SETTINGS);
export const fetchSettings = ({ dispatch, state }) => { export const fetchSettings = ({ dispatch, state }) => {
dispatch('toggleLoading'); dispatch('toggleLoading');
return Api.project(state.projectId) return Api.project(state.projectId)
.then(({ tag_expiration_policies }) => .then(({ data: { container_expiration_policy } }) =>
dispatch('receiveSettingsSuccess', tag_expiration_policies), dispatch('receiveSettingsSuccess', container_expiration_policy),
) )
.catch(() => dispatch('receiveSettingsError')) .catch(() => dispatch('receiveSettingsError'))
.finally(() => dispatch('toggleLoading')); .finally(() => dispatch('toggleLoading'));
...@@ -27,10 +27,12 @@ export const fetchSettings = ({ dispatch, state }) => { ...@@ -27,10 +27,12 @@ export const fetchSettings = ({ dispatch, state }) => {
export const saveSettings = ({ dispatch, state }) => { export const saveSettings = ({ dispatch, state }) => {
dispatch('toggleLoading'); dispatch('toggleLoading');
return Api.updateProject(state.projectId, { tag_expiration_policies: state.settings }) return Api.updateProject(state.projectId, {
.then(({ tag_expiration_policies }) => { container_expiration_policy_attributes: state.settings,
dispatch('receiveSettingsSuccess', tag_expiration_policies); })
createFlash(UPDATE_SETTINGS_SUCCESS_MESSAGE); .then(({ data: { container_expiration_policy } }) => {
dispatch('receiveSettingsSuccess', container_expiration_policy);
createFlash(UPDATE_SETTINGS_SUCCESS_MESSAGE, 'success');
}) })
.catch(() => dispatch('updateSettingsError')) .catch(() => dispatch('updateSettingsError'))
.finally(() => dispatch('toggleLoading')); .finally(() => dispatch('toggleLoading'));
......
...@@ -104,7 +104,11 @@ export default { ...@@ -104,7 +104,11 @@ export default {
</span> </span>
<div class="commit-detail flex-list"> <div class="commit-detail flex-list">
<div class="commit-content qa-commit-content"> <div class="commit-content qa-commit-content">
<gl-link :href="commit.webUrl" class="commit-row-message item-title"> <gl-link
:href="commit.webUrl"
:class="{ 'font-italic': !commit.message }"
class="commit-row-message item-title"
>
{{ commit.title }} {{ commit.title }}
</gl-link> </gl-link>
<gl-button <gl-button
......
...@@ -6,6 +6,7 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) { ...@@ -6,6 +6,7 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
sha sha
title title
description description
message
webUrl webUrl
authoredDate authoredDate
authorName authorName
......
...@@ -36,12 +36,17 @@ export default { ...@@ -36,12 +36,17 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
showChangedStatus: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
changedIcon() { changedIcon() {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
// eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
const suffix = !this.file.changed && this.file.staged && this.showStagedIcon ? '-solid' : ''; const suffix = this.showStagedIcon ? '-solid' : '';
return `${getCommitIconMap(this.file).icon}${suffix}`; return `${getCommitIconMap(this.file).icon}${suffix}`;
}, },
...@@ -86,8 +91,8 @@ export default { ...@@ -86,8 +91,8 @@ export default {
<span <span
v-gl-tooltip.right v-gl-tooltip.right
:title="tooltipTitle" :title="tooltipTitle"
:class="{ 'ml-auto': isCentered }" :class="[{ 'ml-auto': isCentered }, changedIconClass]"
class="file-changed-icon d-inline-block" class="file-changed-icon d-flex align-items-center "
> >
<icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" /> <icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" />
</span> </span>
......
<script> <script>
import Icon from '~/vue_shared/components/icon.vue';
import FileHeader from '~/vue_shared/components/file_row_header.vue'; import FileHeader from '~/vue_shared/components/file_row_header.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
...@@ -9,7 +8,6 @@ export default { ...@@ -9,7 +8,6 @@ export default {
components: { components: {
FileHeader, FileHeader,
FileIcon, FileIcon,
Icon,
ChangedFileIcon, ChangedFileIcon,
}, },
props: { props: {
...@@ -26,6 +24,7 @@ export default { ...@@ -26,6 +24,7 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
hideExtraOnTree: { hideExtraOnTree: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -143,17 +142,17 @@ export default { ...@@ -143,17 +142,17 @@ export default {
@mouseleave="toggleDropdown(false)" @mouseleave="toggleDropdown(false)"
> >
<div class="file-row-name-container"> <div class="file-row-name-container">
<span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated"> <span ref="textOutput" :style="levelIndentation" class="file-row-name str-truncated d-flex">
<file-icon <file-icon
v-if="!showChangedIcon || file.type === 'tree'" v-if="!showChangedIcon || file.type === 'tree'"
class="file-row-icon" class="file-row-icon text-secondary mr-1"
:file-name="file.name" :file-name="file.name"
:loading="file.loading" :loading="file.loading"
:folder="isTree" :folder="isTree"
:opened="file.opened" :opened="file.opened"
:size="16" :size="16"
/> />
<changed-file-icon v-else :file="file" :size="16" class="append-right-5" /> <file-icon v-else :file-name="file.name" :size="16" css-classes="top mr-1" />
{{ file.name }} {{ file.name }}
</span> </span>
<component <component
...@@ -163,6 +162,7 @@ export default { ...@@ -163,6 +162,7 @@ export default {
:dropdown-open="dropdownOpen" :dropdown-open="dropdownOpen"
@toggle="toggleDropdown($event)" @toggle="toggleDropdown($event)"
/> />
<changed-file-icon :file="file" :size="16" class="append-right-5" />
</div> </div>
</div> </div>
<template v-if="file.opened || file.isHeader"> <template v-if="file.opened || file.isHeader">
...@@ -172,7 +172,6 @@ export default { ...@@ -172,7 +172,6 @@ export default {
:file="childFile" :file="childFile"
:level="childFilesLevel" :level="childFilesLevel"
:hide-extra-on-tree="hideExtraOnTree" :hide-extra-on-tree="hideExtraOnTree"
:extra-component="extraComponent"
:show-changed-icon="showChangedIcon" :show-changed-icon="showChangedIcon"
@toggleTreeOpen="toggleTreeOpen" @toggleTreeOpen="toggleTreeOpen"
@clickFile="clickedFile" @clickFile="clickedFile"
......
...@@ -14,9 +14,9 @@ ...@@ -14,9 +14,9 @@
cursor: pointer; cursor: pointer;
@media (min-width: map-get($grid-breakpoints, md)) { @media (min-width: map-get($grid-breakpoints, md)) {
// The `-1` below is to prevent two borders from clashing up against eachother - // The `+11` is to ensure the file header border shows when scrolled -
// the bottom of the compare-versions header and the top of the file header // the bottom of the compare-versions header and the top of the file header
$mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height - 1; $mr-file-header-top: $mr-version-controls-height + $header-height + $mr-tabs-height + 11;
position: -webkit-sticky; position: -webkit-sticky;
position: sticky; position: sticky;
...@@ -552,7 +552,7 @@ table.code { ...@@ -552,7 +552,7 @@ table.code {
.diff-stats { .diff-stats {
align-items: center; align-items: center;
padding: 0 0.25rem; padding: 0 1rem;
.diff-stats-group { .diff-stats-group {
padding: 0 0.25rem; padding: 0 0.25rem;
...@@ -564,7 +564,7 @@ table.code { ...@@ -564,7 +564,7 @@ table.code {
&.is-compare-versions-header { &.is-compare-versions-header {
.diff-stats-group { .diff-stats-group {
padding: 0 0.5rem; padding: 0 0.25rem;
} }
} }
} }
...@@ -1059,8 +1059,8 @@ table.code { ...@@ -1059,8 +1059,8 @@ table.code {
.diff-tree-list { .diff-tree-list {
position: -webkit-sticky; position: -webkit-sticky;
position: sticky; position: sticky;
$top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 11px;
top: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px; top: $header-height + $mr-tabs-height + $mr-version-controls-height + 11px;
max-height: calc(100vh - #{$top-pos}); max-height: calc(100vh - #{$top-pos});
z-index: 202; z-index: 202;
...@@ -1097,10 +1097,7 @@ table.code { ...@@ -1097,10 +1097,7 @@ table.code {
.tree-list-scroll { .tree-list-scroll {
max-height: 100%; max-height: 100%;
padding-top: $grid-size;
padding-bottom: $grid-size; padding-bottom: $grid-size;
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
overflow-y: scroll; overflow-y: scroll;
overflow-x: auto; overflow-x: auto;
} }
......
...@@ -708,7 +708,7 @@ ...@@ -708,7 +708,7 @@
.mr-version-controls { .mr-version-controls {
position: relative; position: relative;
z-index: 203; z-index: 203;
background: $gray-light; background: $white-light;
color: $gl-text-color; color: $gl-text-color;
margin-top: -1px; margin-top: -1px;
...@@ -732,7 +732,7 @@ ...@@ -732,7 +732,7 @@
} }
.content-block { .content-block {
padding: $gl-padding-top $gl-padding; padding: $gl-padding;
border-bottom: 0; border-bottom: 0;
} }
......
...@@ -11,7 +11,7 @@ module SpammableActions ...@@ -11,7 +11,7 @@ module SpammableActions
end end
def mark_as_spam def mark_as_spam
if SpamService.new(spammable: spammable).mark_as_spam! if Spam::MarkAsSpamService.new(spammable: spammable).execute
redirect_to spammable_path, notice: _("%{spammable_titlecase} was submitted to Akismet successfully.") % { spammable_titlecase: spammable.spammable_entity_type.titlecase } redirect_to spammable_path, notice: _("%{spammable_titlecase} was submitted to Akismet successfully.") % { spammable_titlecase: spammable.spammable_entity_type.titlecase }
else else
redirect_to spammable_path, alert: _('Error with Akismet. Please check the logs for more info.') redirect_to spammable_path, alert: _('Error with Akismet. Please check the logs for more info.')
......
...@@ -119,7 +119,9 @@ class Groups::MilestonesController < Groups::ApplicationController ...@@ -119,7 +119,9 @@ class Groups::MilestonesController < Groups::ApplicationController
end end
def search_params def search_params
params.permit(:state, :search_title).merge(group_ids: group.id) groups = request.format.json? ? group.self_and_ancestors.select(:id) : group.id
params.permit(:state, :search_title).merge(group_ids: groups)
end end
end end
......
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
class IdeController < ApplicationController class IdeController < ApplicationController
layout 'fullscreen' layout 'fullscreen'
before_action do
push_frontend_feature_flag(:stage_all_by_default, default_enabled: true)
end
def index def index
Gitlab::UsageDataCounters::WebIdeCounter.increment_views_count Gitlab::UsageDataCounters::WebIdeCounter.increment_views_count
end end
......
...@@ -24,7 +24,7 @@ module Mutations ...@@ -24,7 +24,7 @@ module Mutations
private private
def mark_as_spam(snippet) def mark_as_spam(snippet)
SpamService.new(spammable: snippet).mark_as_spam! Spam::MarkAsSpamService.new(spammable: snippet).execute
end end
def authorized_resource?(snippet) def authorized_resource?(snippet)
......
...@@ -154,7 +154,9 @@ module MarkupHelper ...@@ -154,7 +154,9 @@ module MarkupHelper
else else
other_markup_unsafe(file_name, text, context) other_markup_unsafe(file_name, text, context)
end end
rescue RuntimeError rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, project_id: @project&.id, file_name: file_name, context: context)
simple_format(text) simple_format(text)
end end
......
# frozen_string_literal: true # frozen_string_literal: true
# The ReactiveCaching concern is used to fetch some data in the background and # The usage of the ReactiveCaching module is documented here:
# store it in the Rails cache, keeping it up-to-date for as long as it is being # https://docs.gitlab.com/ee/development/utilities.html#reactivecaching
# requested. If the data hasn't been requested for +reactive_cache_lifetime+,
# it stop being refreshed, and then be removed.
#
# Example of use:
#
# class Foo < ApplicationRecord
# include ReactiveCaching
#
# after_save :clear_reactive_cache!
#
# def calculate_reactive_cache
# # Expensive operation here. The return value of this method is cached
# end
#
# def result
# with_reactive_cache do |data|
# # ...
# end
# end
# end
#
# In this example, the first time `#result` is called, it will return `nil`.
# However, it will enqueue a background worker to call `#calculate_reactive_cache`
# and set an initial cache lifetime of ten minutes.
#
# The background worker needs to find or generate the object on which
# `with_reactive_cache` was called.
# The default behaviour can be overridden by defining a custom
# `reactive_cache_worker_finder`.
# Otherwise the background worker will use the class name and primary key to get
# the object using the ActiveRecord find_by method.
#
# class Bar
# include ReactiveCaching
#
# self.reactive_cache_key = ->() { ["bar", "thing"] }
# self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
#
# def self.from_cache(var1, var2)
# # This method will be called by the background worker with "bar1" and
# # "bar2" as arguments.
# new(var1, var2)
# end
#
# def initialize(var1, var2)
# # ...
# end
#
# def calculate_reactive_cache
# # Expensive operation here. The return value of this method is cached
# end
#
# def result
# with_reactive_cache("bar1", "bar2") do |data|
# # ...
# end
# end
# end
#
# Each time the background job completes, it stores the return value of
# `#calculate_reactive_cache`. It is also re-enqueued to run again after
# `reactive_cache_refresh_interval`, so keeping the stored value up to date.
# Calculations are never run concurrently.
#
# Calling `#result` while a value is in the cache will call the block given to
# `#with_reactive_cache`, yielding the cached value. It will also extend the
# lifetime by `reactive_cache_lifetime`.
#
# Once the lifetime has expired, no more background jobs will be enqueued and
# calling `#result` will again return `nil` - starting the process all over
# again
module ReactiveCaching module ReactiveCaching
extend ActiveSupport::Concern extend ActiveSupport::Concern
......
# frozen_string_literal: true
module AkismetMethods
def spammable_owner
@user ||= User.find(spammable_owner_id)
end
def spammable_owner_id
@owner_id ||=
if spammable.respond_to?(:author_id)
spammable.author_id
elsif spammable.respond_to?(:creator_id)
spammable.creator_id
end
end
def akismet
@akismet ||= AkismetService.new(
spammable_owner.name,
spammable_owner.email,
spammable.spammable_text,
options
)
end
end
...@@ -11,3 +11,5 @@ module Notes ...@@ -11,3 +11,5 @@ module Notes
end end
end end
end end
Notes::DestroyService.prepend_if_ee('EE::Notes::DestroyService')
# frozen_string_literal: true
module Spam
class MarkAsSpamService
include ::AkismetMethods
attr_accessor :spammable, :options
def initialize(spammable:)
@spammable = spammable
@options = {}
@options[:ip_address] = @spammable.ip_address
@options[:user_agent] = @spammable.user_agent
end
def execute
return unless spammable.submittable_as_spam?
return unless akismet.submit_spam
spammable.user_agent_detail.update_attribute(:submitted, true)
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
class SpamService class SpamService
include AkismetMethods
attr_accessor :spammable, :request, :options attr_accessor :spammable, :request, :options
attr_reader :spam_log attr_reader :spam_log
def initialize(spammable:, request: nil) def initialize(spammable:, request:)
@spammable = spammable @spammable = spammable
@request = request @request = request
@options = {} @options = {}
...@@ -19,16 +21,6 @@ class SpamService ...@@ -19,16 +21,6 @@ class SpamService
end end
end end
def mark_as_spam!
return false unless spammable.submittable_as_spam?
if akismet.submit_spam
spammable.user_agent_detail.update_attribute(:submitted, true)
else
false
end
end
def when_recaptcha_verified(recaptcha_verified, api = false) def when_recaptcha_verified(recaptcha_verified, api = false)
# In case it's a request which is already verified through recaptcha, yield # In case it's a request which is already verified through recaptcha, yield
# block. # block.
...@@ -54,28 +46,6 @@ class SpamService ...@@ -54,28 +46,6 @@ class SpamService
true true
end end
def akismet
@akismet ||= AkismetService.new(
spammable_owner.name,
spammable_owner.email,
spammable.spammable_text,
options
)
end
def spammable_owner
@user ||= User.find(spammable_owner_id)
end
def spammable_owner_id
@owner_id ||=
if spammable.respond_to?(:author_id)
spammable.author_id
elsif spammable.respond_to?(:creator_id)
spammable.creator_id
end
end
def check_for_spam? def check_for_spam?
spammable.check_for_spam? spammable.check_for_spam?
end end
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div %div
= f.label 'Two-Factor Authentication code', name: :otp_attempt = f.label 'Two-Factor Authentication code', name: :otp_attempt
= f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.', inputmode: 'numeric', pattern: '[0-9]*' = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.'
%p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. %p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20 .prepend-top-20
= f.submit "Verify code", class: "btn btn-success" = f.submit "Verify code", class: "btn btn-success"
......
...@@ -21,9 +21,9 @@ ...@@ -21,9 +21,9 @@
.commit-detail.flex-list .commit-detail.flex-list
.commit-content.qa-commit-content .commit-content.qa-commit-content
- if view_details && merge_request - if view_details && merge_request
= link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: "commit-row-message item-title js-onboarding-commit-item" = link_to commit.title, project_commit_path(project, commit.id, merge_request_iid: merge_request.iid), class: ["commit-row-message item-title js-onboarding-commit-item", ("font-italic" if commit.message.empty?)]
- else - else
= link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title js-onboarding-commit-item") = link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title js-onboarding-commit-item #{"font-italic" if commit.message.empty?}")
%span.commit-row-message.d-inline.d-sm-none %span.commit-row-message.d-inline.d-sm-none
&middot; &middot;
= commit.short_id = commit.short_id
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
= render 'projects/triggers/index' = render 'projects/triggers/index'
- if Feature.enabled?(:registry_retention_policies_settings, @project) - if Feature.enabled?(:registry_retention_policies_settings, @project)
%section.settings.no-animate#js-registry-polcies{ class: ('expanded' if expanded) } %section.settings.no-animate#js-registry-policies{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
= _("Container Registry tag expiration policy") = _("Container Registry tag expiration policy")
......
---
title: Restyle changes header & file tree
merge_request: 22364
author:
type: changed
---
title: 'Geo: Handle repositories in Docker Registry with no tags gracefully'
merge_request: 23022
author:
type: fixed
---
title: Fix group issue list and group issue board filters not showing ancestor group
milestones
merge_request: 23038
author:
type: fixed
---
title: Exposes tiller.log as artifact in Managed-Cluster-Applications GitLab CI template
merge_request: 22940
author:
type: changed
---
title: Remove storage_version column from snippets
merge_request: 23004
author:
type: changed
---
title: Stage all changes by default in Web IDE
merge_request: 21067
author:
type: added
---
title: Updated no commit verbiage
merge_request: 22765
author:
type: other
---
title: Enable redis HSET diff caching by default
merge_request: 23105
author:
type: performance
---
title: Fix Error 500 in parsing invalid CI needs and dependencies
merge_request: 22567
author:
type: fixed
# frozen_string_literal: true
class RemoveStorageVersionColumnFromSnippets < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
return unless column_exists?(:snippets, :storage_version)
remove_column :snippets, :storage_version
end
def down
add_column_with_default( # rubocop:disable Migration/AddColumnWithDefault
:snippets,
:storage_version,
:integer,
default: 2,
allow_null: false
)
end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2020_01_14_113341) do ActiveRecord::Schema.define(version: 2020_01_14_180546) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"
...@@ -3838,7 +3838,6 @@ ActiveRecord::Schema.define(version: 2020_01_14_113341) do ...@@ -3838,7 +3838,6 @@ ActiveRecord::Schema.define(version: 2020_01_14_113341) do
t.string "encrypted_secret_token_iv", limit: 255 t.string "encrypted_secret_token_iv", limit: 255
t.boolean "secret", default: false, null: false t.boolean "secret", default: false, null: false
t.string "repository_storage", limit: 255, default: "default", null: false t.string "repository_storage", limit: 255, default: "default", null: false
t.integer "storage_version", default: 2, null: false
t.index ["author_id"], name: "index_snippets_on_author_id" t.index ["author_id"], name: "index_snippets_on_author_id"
t.index ["content"], name: "index_snippets_on_content_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["content"], name: "index_snippets_on_content_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["created_at"], name: "index_snippets_on_created_at" t.index ["created_at"], name: "index_snippets_on_created_at"
......
...@@ -208,6 +208,41 @@ Now every single time on attempt to fetch a version, our client will fetch `id` ...@@ -208,6 +208,41 @@ Now every single time on attempt to fetch a version, our client will fetch `id`
Read more about local state management with Apollo in the [Vue Apollo documentation](https://vue-apollo.netlify.com/guide/local-state.html#local-state). Read more about local state management with Apollo in the [Vue Apollo documentation](https://vue-apollo.netlify.com/guide/local-state.html#local-state).
### Feature flags in queries
Sometimes it may be useful to have an entity in the GraphQL query behind a feature flag.
For example, when working on a feature where the backend has already been merged but the frontend
hasn't you might want to put the GraphQL entity behind a feature flag to allow for smaller
merge requests to be created and merged.
To do this we can use the `@include` directive to exclude an entity if the `if` statement passes.
```graphql
query getAuthorData($authorNameEnabled: Boolean = false) {
username
name @include(if: $authorNameEnabled)
}
```
Then in the Vue (or JavaScript) call to the query we can pass in our feature flag. This feature
flag will need to be already setup correctly. See the [feature flag documentation](../feature_flags/development.md)
for the correct way to do this.
```javascript
export default {
apollo: {
user: {
query: QUERY_IMPORT,
variables() {
return {
authorNameEnabled: gon?.features?.authorNameEnabled,
};
},
}
},
};
```
### Testing ### Testing
#### Mocking response as component data #### Mocking response as component data
......
...@@ -103,6 +103,10 @@ always take the latest DAST artifact available. Behind the scenes, the ...@@ -103,6 +103,10 @@ always take the latest DAST artifact available. Behind the scenes, the
[GitLab DAST Docker image](https://gitlab.com/gitlab-org/security-products/dast) [GitLab DAST Docker image](https://gitlab.com/gitlab-org/security-products/dast)
is used to run the tests on the specified URL and scan it for possible vulnerabilities. is used to run the tests on the specified URL and scan it for possible vulnerabilities.
By default, the DAST template will use the latest major version of the DAST Docker image. Using the `DAST_VERSION` variable,
you can choose to automatically update DAST with new features and fixes by pinning to a major version (e.g. 1), only update fixes by pinning to a minor version (e.g. 1.6) or prevent all updates by pinning to a specific version (e.g. 1.6.4).
Find the latest DAST versions on the [Releases](https://gitlab.com/gitlab-org/security-products/dast/-/releases) page.
### Authenticated scan ### Authenticated scan
It's also possible to authenticate the user before performing the DAST checks: It's also possible to authenticate the user before performing the DAST checks:
......
...@@ -505,7 +505,10 @@ To install applications using GitLab CI: ...@@ -505,7 +505,10 @@ To install applications using GitLab CI:
customize values for the installed application. customize values for the installed application.
A GitLab CI pipeline will then run on the `master` branch to install the A GitLab CI pipeline will then run on the `master` branch to install the
applications you have configured. applications you have configured. In case of pipeline failure, the
output of the [Helm
Tiller](https://v2.helm.sh/docs/install/#running-tiller-locally) binary
will be saved as a [CI job artifact](../project/pipelines/job_artifacts.md).
### Install Ingress using GitLab CI ### Install Ingress using GitLab CI
......
...@@ -390,8 +390,8 @@ As a reviewer, you're able to suggest code changes with a simple ...@@ -390,8 +390,8 @@ As a reviewer, you're able to suggest code changes with a simple
Markdown syntax in Merge Request Diff threads. Then, the Markdown syntax in Merge Request Diff threads. Then, the
Merge Request author (or other users with appropriate Merge Request author (or other users with appropriate
[permission](../permissions.md)) is able to apply these [permission](../permissions.md)) is able to apply these
suggestions with a click, which will generate a commit in Suggestions with a click, which will generate a commit in
the Merge Request authored by the user that applied them. the merge request authored by the user that applied them.
1. Choose a line of code to be changed, add a new comment, then click 1. Choose a line of code to be changed, add a new comment, then click
on the **Insert suggestion** icon in the toolbar: on the **Insert suggestion** icon in the toolbar:
...@@ -407,32 +407,28 @@ the Merge Request authored by the user that applied them. ...@@ -407,32 +407,28 @@ the Merge Request authored by the user that applied them.
NOTE: **Note:** NOTE: **Note:**
If you're using GitLab Premium, GitLab.com Silver, and higher tiers, the thread will display [Review](#merge-request-reviews-premium) options. Click either **Start a review**, **Add comment now**, or **Add to review** to obtain the same result. If you're using GitLab Premium, GitLab.com Silver, and higher tiers, the thread will display [Review](#merge-request-reviews-premium) options. Click either **Start a review**, **Add comment now**, or **Add to review** to obtain the same result.
The suggestions in the comment can be applied by the merge request author The Suggestion in the comment can be applied by the merge request author
directly from the merge request: directly from the merge request:
![Apply suggestions](img/apply_suggestion_v12_7.png) ![Apply suggestions](img/apply_suggestion_v12_7.png)
Once the author applies a suggestion, it will be marked with the **Applied** label, Once the author applies a Suggestion, it will be marked with the **Applied** label,
the thread will be automatically resolved, and GitLab will create a new commit the thread will be automatically resolved, and GitLab will create a new commit
and push the suggested change directly into the codebase in the merge request's and push the suggested change directly into the codebase in the merge request's
branch. [Developer permission](../permissions.md) is required to do so. branch. [Developer permission](../permissions.md) is required to do so.
> **Note:** ### Multi-line Suggestions
Custom commit messages will be introduced by
[#54404](https://gitlab.com/gitlab-org/gitlab-foss/issues/54404).
### Multi-line suggestions
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/53310) in GitLab 11.10. > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/53310) in GitLab 11.10.
Reviewers can also suggest changes to multiple lines with a single suggestion Reviewers can also suggest changes to multiple lines with a single Suggestion
within Merge Request diff threads by adjusting the range offsets. The within merge request diff threads by adjusting the range offsets. The
offsets are relative to the position of the diff thread, and specify the offsets are relative to the position of the diff thread, and specify the
range to be replaced by the suggestion when it is applied. range to be replaced by the suggestion when it is applied.
![Multi-line suggestion syntax](img/multi-line-suggestion-syntax.png) ![Multi-line suggestion syntax](img/multi-line-suggestion-syntax.png)
In the example above, the suggestion covers three lines above and four lines In the example above, the Suggestion covers three lines above and four lines
below the commented line. When applied, it would replace from 3 lines _above_ below the commented line. When applied, it would replace from 3 lines _above_
to 4 lines _below_ the commented line, with the suggested change. to 4 lines _below_ the commented line, with the suggested change.
...@@ -443,23 +439,36 @@ Suggestions covering multiple lines are limited to 100 lines _above_ and 100 ...@@ -443,23 +439,36 @@ Suggestions covering multiple lines are limited to 100 lines _above_ and 100
lines _below_ the commented diff line, allowing up to 200 changed lines per lines _below_ the commented diff line, allowing up to 200 changed lines per
suggestion. suggestion.
### Configure the commit message for applied suggestions ### Configure the commit message for applied Suggestions
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/13086) in GitLab 12.7.
GitLab will use `Apply suggestion to %{file_path}` by default as commit messages GitLab uses `Apply suggestion to %{file_path}` by default as commit messages
when applying change suggestions. This commit message can be customized to when applying Suggestions. This commit message can be customized to
follow any guidelines you might have. To do so, open the **Merge requests** tab follow any guidelines you might have. To do so, expand the **Merge requests**
within your project settings and change the **Merge suggestions** text. tab within your project's **General** settings and change the
**Merge suggestions** text:
![Suggestion Commit Message Configuration](img/suggestion-commit-message-configuration.png) ![Custom commit message for applied Suggestions](img/suggestions_custom_commit_messages_v12_7.png)
You can also use following variables besides static text: You can also use following variables besides static text:
- `%{project_path}`: The full URL safe project path. E.g: *my-group/my-project* | Variable | Description | Output example |
- `%{project_name}`: The human readable name of the project. E.g: *My Project* |---|---|---|
- `%{file_path}`: The full path of the file the suggestion is applied to. E.g: *docs/index.md* | `%{project_path}` | The project path. | `my-group/my-project` |
- `%{branch_name}`: The name of the branch the suggestion is applied on. E.g: *my-feature-branch* | `%{project_name}` | The human-readable name of the project. | **My Project** |
- `%{username}`: The username of the user applying the suggestion. E.g: *user_1* | `%{file_path}` | The path of the file the Suggestion is applied to. | `docs/index.md` |
- `%{user_full_name}`: The full name of the user applying the suggestion. E.g: *User 1* | `%{branch_name}` | The name of the branch the Suggestion is applied on. | `my-feature-branch` |
| `%{username}` | The username of the user applying the Suggestion. | `user_1` |
| `%{user_full_name}` | The full name of the user applying the Suggestion. | `**User 1** |
For example, to customize the commit message to output
**Addresses user_1's review**, set the custom text to
`Adresses %{username}'s review`.
NOTE: **Note:**
Custom commit messages for each applied Suggestion will be
introduced by [#25381](https://gitlab.com/gitlab-org/gitlab/issues/25381).
## Start a thread by replying to a standard comment ## Start a thread by replying to a standard comment
......
...@@ -46,12 +46,16 @@ Single file editing is based on the [Ace Editor](https://ace.c9.io). ...@@ -46,12 +46,16 @@ Single file editing is based on the [Ace Editor](https://ace.c9.io).
## Stage and commit changes ## Stage and commit changes
After making your changes, click the **Commit** button in the bottom left to After making your changes, click the **Commit** button in the bottom left to
review the list of changed files. Click on each file to review the changes and review the list of changed files. If you're using GitLab 12.6 or older versions,
click the tick icon to stage the file. click on each file to review the changes and tick the item to stage a file.
![Review changes](img/review_changes_v12_3.png) From [GitLab 12.7 onwards](https://gitlab.com/gitlab-org/gitlab/issues/33441),
all your files will be automatically staged. You still have the option to unstage
changes in case you want to submit them in multiple smaller commits. To unstage
a change, simply click the **Unstage** button when a staged file is open, or click
the undo icon next to **Staged changes** to unstage all changes.
Once you have staged some changes, you can add a commit message, commit the Once you have finalized your changes, you can add a commit message, commit the
staged changes and directly create a merge request. In case you don't have write staged changes and directly create a merge request. In case you don't have write
access to the selected branch, you will see a warning, but still be able to create access to the selected branch, you will see a warning, but still be able to create
a new branch and start a merge request. a new branch and start a merge request.
......
...@@ -55,11 +55,12 @@ module Gitlab ...@@ -55,11 +55,12 @@ module Gitlab
validates :start_in, duration: { limit: '1 week' }, if: :delayed? validates :start_in, duration: { limit: '1 week' }, if: :delayed?
validates :start_in, absence: true, if: -> { has_rules? || !delayed? } validates :start_in, absence: true, if: -> { has_rules? || !delayed? }
validate do validate on: :composed do
next unless dependencies.present? next unless dependencies.present?
next unless needs.present? next unless needs_value.present?
missing_needs = dependencies - needs_value[:job].pluck(:name) # rubocop:disable CodeReuse/ActiveRecord (Array#pluck)
missing_needs = dependencies - needs
if missing_needs.any? if missing_needs.any?
errors.add(:dependencies, "the #{missing_needs.join(", ")} should be part of needs") errors.add(:dependencies, "the #{missing_needs.join(", ")} should be part of needs")
end end
......
...@@ -14,3 +14,7 @@ apply: ...@@ -14,3 +14,7 @@ apply:
only: only:
refs: refs:
- master - master
artifacts:
when: on_failure
paths:
- tiller.log
...@@ -10,10 +10,13 @@ stages: ...@@ -10,10 +10,13 @@ stages:
- deploy - deploy
- dast - dast
variables:
DAST_VERSION: 1
dast: dast:
stage: dast stage: dast
image: image:
name: "registry.gitlab.com/gitlab-org/security-products/dast:$CI_SERVER_VERSION_MAJOR-$CI_SERVER_VERSION_MINOR-stable" name: "registry.gitlab.com/gitlab-org/security-products/dast:$DAST_VERSION"
variables: variables:
# URL to scan: # URL to scan:
# DAST_WEBSITE: https://example.com/ # DAST_WEBSITE: https://example.com/
......
...@@ -47,7 +47,7 @@ module Gitlab ...@@ -47,7 +47,7 @@ module Gitlab
private private
def cache def cache
@cache ||= if Feature.enabled?(:hset_redis_diff_caching, project) @cache ||= if Feature.enabled?(:hset_redis_diff_caching, project, default_enabled: true)
Gitlab::Diff::HighlightCache.new(self) Gitlab::Diff::HighlightCache.new(self)
else else
Gitlab::Diff::DeprecatedHighlightCache.new(self) Gitlab::Diff::DeprecatedHighlightCache.new(self)
......
...@@ -254,7 +254,7 @@ module Gitlab ...@@ -254,7 +254,7 @@ module Gitlab
end end
def no_commit_message def no_commit_message
"--no commit message" "No commit message"
end end
def to_hash def to_hash
......
...@@ -379,6 +379,9 @@ msgstr "" ...@@ -379,6 +379,9 @@ msgstr ""
msgid "%{spanStart}in%{spanEnd} %{errorFn}" msgid "%{spanStart}in%{spanEnd} %{errorFn}"
msgstr "" msgstr ""
msgid "%{staged} staged and %{unstaged} unstaged changes"
msgstr ""
msgid "%{start} to %{end}" msgid "%{start} to %{end}"
msgstr "" msgstr ""
...@@ -700,6 +703,9 @@ msgstr "" ...@@ -700,6 +703,9 @@ msgstr ""
msgid "<strong>%{pushes}</strong> pushes, more than <strong>%{commits}</strong> commits by <strong>%{people}</strong> contributors." msgid "<strong>%{pushes}</strong> pushes, more than <strong>%{commits}</strong> commits by <strong>%{people}</strong> contributors."
msgstr "" msgstr ""
msgid "<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes"
msgstr ""
msgid "<strong>Deletes</strong> source branch" msgid "<strong>Deletes</strong> source branch"
msgstr "" msgstr ""
...@@ -4912,9 +4918,6 @@ msgstr "" ...@@ -4912,9 +4918,6 @@ msgstr ""
msgid "ContainerRegistry|Expiration interval:" msgid "ContainerRegistry|Expiration interval:"
msgstr "" msgstr ""
msgid "ContainerRegistry|Expiration latest:"
msgstr ""
msgid "ContainerRegistry|Expiration policy successfully saved." msgid "ContainerRegistry|Expiration policy successfully saved."
msgstr "" msgstr ""
...@@ -4924,7 +4927,7 @@ msgstr "" ...@@ -4924,7 +4927,7 @@ msgstr ""
msgid "ContainerRegistry|Expiration schedule:" msgid "ContainerRegistry|Expiration schedule:"
msgstr "" msgstr ""
msgid "ContainerRegistry|Expire Docker tags with name matching:" msgid "ContainerRegistry|Expire Docker tags that match this regex:"
msgstr "" msgstr ""
msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password." msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password."
...@@ -4939,6 +4942,9 @@ msgstr "" ...@@ -4939,6 +4942,9 @@ msgstr ""
msgid "ContainerRegistry|Last Updated" msgid "ContainerRegistry|Last Updated"
msgstr "" msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
msgid "ContainerRegistry|Quick Start" msgid "ContainerRegistry|Quick Start"
msgstr "" msgstr ""
...@@ -4989,7 +4995,7 @@ msgstr "" ...@@ -4989,7 +4995,7 @@ msgstr ""
msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}" msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr "" msgstr ""
msgid "ContainerRegistry|Wildcards such as %{codeStart}*-stable%{codeEnd} or %{codeStart}production/*%{codeEnd} are supported" msgid "ContainerRegistry|Wildcards such as %{codeStart}*-stable%{codeEnd} or %{codeStart}production/*%{codeEnd} are supported. To select all tags, use %{codeStart}.*%{codeEnd}"
msgstr "" msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}" msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
...@@ -7986,11 +7992,6 @@ msgstr "" ...@@ -7986,11 +7992,6 @@ msgstr ""
msgid "Fetching licenses failed. You are not permitted to perform this action." msgid "Fetching licenses failed. You are not permitted to perform this action."
msgstr "" msgstr ""
msgid "File"
msgid_plural "Files"
msgstr[0] ""
msgstr[1] ""
msgid "File added" msgid "File added"
msgstr "" msgstr ""
...@@ -11529,10 +11530,10 @@ msgstr "" ...@@ -11529,10 +11530,10 @@ msgstr ""
msgid "MergeRequest|Error loading full diff. Please try again." msgid "MergeRequest|Error loading full diff. Please try again."
msgstr "" msgstr ""
msgid "MergeRequest|Filter files or search with %{modifier_key}+p" msgid "MergeRequest|No files found"
msgstr "" msgstr ""
msgid "MergeRequest|No files found" msgid "MergeRequest|Search files (%{modifier_key}P)"
msgstr "" msgstr ""
msgid "Merged" msgid "Merged"
...@@ -21543,6 +21544,9 @@ msgstr "" ...@@ -21543,6 +21544,9 @@ msgstr ""
msgid "among other things" msgid "among other things"
msgstr "" msgstr ""
msgid "and"
msgstr ""
msgid "archived" msgid "archived"
msgstr "" msgstr ""
...@@ -21970,6 +21974,11 @@ msgstr "" ...@@ -21970,6 +21974,11 @@ msgstr ""
msgid "failed to dismiss associated finding(id=%{finding_id}): %{message}" msgid "failed to dismiss associated finding(id=%{finding_id}): %{message}"
msgstr "" msgstr ""
msgid "file"
msgid_plural "files"
msgstr[0] ""
msgstr[1] ""
msgid "finding is not found or is already attached to a vulnerability" msgid "finding is not found or is already attached to a vulnerability"
msgstr "" msgstr ""
......
...@@ -84,16 +84,13 @@ module QA ...@@ -84,16 +84,13 @@ module QA
end end
def commit_changes def commit_changes
# Clicking :begin_commit_button the first time switches from the # Clicking :begin_commit_button switches from the
# edit to the commit view # edit to the commit view
click_element :begin_commit_button click_element :begin_commit_button
active_element? :commit_mode_tab active_element? :commit_mode_tab
# We need to click :begin_commit_button again # After clicking :begin_commit_button, there is an animation
click_element :begin_commit_button # that hides :begin_commit_button and shows :commit_button
# After clicking :begin_commit_button the 2nd time there is an
# animation that hides :begin_commit_button and shows :commit_button
# #
# Wait for the animation to complete before clicking :commit_button # Wait for the animation to complete before clicking :commit_button
# otherwise the click will quietly do nothing. # otherwise the click will quietly do nothing.
...@@ -102,9 +99,6 @@ module QA ...@@ -102,9 +99,6 @@ module QA
has_element?(:commit_button) has_element?(:commit_button)
end end
# At this point we're ready to commit and the button should be
# labelled "Stage & Commit"
#
# Click :commit_button and keep retrying just in case part of the # Click :commit_button and keep retrying just in case part of the
# animation is still in process even when the buttons have the # animation is still in process even when the buttons have the
# expected visibility. # expected visibility.
......
...@@ -148,6 +148,19 @@ describe Groups::MilestonesController do ...@@ -148,6 +148,19 @@ describe Groups::MilestonesController do
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(response.content_type).to eq 'application/json' expect(response.content_type).to eq 'application/json'
end end
context 'for a subgroup' do
let(:subgroup) { create(:group, parent: group) }
it 'includes ancestor group milestones' do
get :index, params: { group_id: subgroup.to_param }, format: :json
milestones = json_response
expect(milestones.count).to eq(1)
expect(milestones.first['title']).to eq('group milestone')
end
end
end end
context 'external authorization' do context 'external authorization' do
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Issues > User creates issue by email' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
before do
sign_in(user)
project.add_developer(user)
end
describe 'new issue by email' do
shared_examples 'show the email in the modal' do
let(:issue) { create(:issue, project: project) }
before do
project.issues << issue
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
visit project_issues_path(project)
click_button('Email a new issue')
end
it 'click the button to show modal for the new email' do
page.within '#issuable-email-modal' do
email = project.new_issuable_address(user, 'issue')
expect(page).to have_selector("input[value='#{email}']")
end
end
end
context 'with existing issues' do
let!(:issue) { create(:issue, project: project, author: user) }
it_behaves_like 'show the email in the modal'
end
context 'without existing issues' do
it_behaves_like 'show the email in the modal'
end
end
end
...@@ -3,8 +3,32 @@ ...@@ -3,8 +3,32 @@
require "spec_helper" require "spec_helper"
describe "User creates issue" do describe "User creates issue" do
let(:project) { create(:project_empty_repo, :public) } include DropzoneHelper
let(:user) { create(:user) }
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
context "when unauthenticated" do
before do
sign_out(:user)
end
it "redirects to signin then back to new issue after signin" do
create(:issue, project: project)
visit project_issues_path(project)
page.within ".nav-controls" do
click_link "New issue"
end
expect(current_path).to eq new_user_session_path
gitlab_sign_in(create(:user))
expect(current_path).to eq new_project_issue_path(project)
end
end
context "when signed in as guest" do context "when signed in as guest" do
before do before do
...@@ -92,6 +116,104 @@ describe "User creates issue" do ...@@ -92,6 +116,104 @@ describe "User creates issue" do
.and have_content(label_titles.first) .and have_content(label_titles.first)
end end
end end
context 'with due date', :js do
it 'saves with due date' do
date = Date.today.at_beginning_of_month
fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description'
find('#issuable-due-date').click
page.within '.pika-single' do
click_button date.day
end
expect(find('#issuable-due-date').value).to eq date.to_s
click_button 'Submit issue'
page.within '.issuable-sidebar' do
expect(page).to have_content date.to_s(:medium)
end
end
end
context 'dropzone upload file', :js do
before do
visit new_project_issue_path(project)
end
it 'uploads file when dragging into textarea' do
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(page.find_field("issue_description").value).to have_content 'banana_sample'
end
it "doesn't add double newline to end of a single attachment markdown" do
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(page.find_field("issue_description").value).not_to match /\n\n$/
end
it "cancels a file upload correctly" do
slow_requests do
dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false)
click_button 'Cancel'
end
expect(page).to have_button('Attach a file')
expect(page).not_to have_button('Cancel')
expect(page).not_to have_selector('.uploading-progress-container', visible: true)
end
end
context 'form filled by URL parameters' do
let(:project) { create(:project, :public, :repository) }
before do
project.repository.create_file(
user,
'.gitlab/issue_templates/bug.md',
'this is a test "bug" template',
message: 'added issue template',
branch_name: 'master')
visit new_project_issue_path(project, issuable_template: 'bug')
end
it 'fills in template' do
expect(find('.js-issuable-selector .dropdown-toggle-text')).to have_content('bug')
end
end
context 'suggestions', :js do
it 'displays list of related issues' do
issue = create(:issue, project: project)
create(:issue, project: project, title: 'test issue')
visit new_project_issue_path(project)
fill_in 'issue_title', with: issue.title
expect(page).to have_selector('.suggestion-item', count: 1)
end
end
it 'clears local storage after creating a new issue', :js do
2.times do
visit new_project_issue_path(project)
wait_for_requests
expect(page).to have_field('Title', with: '')
fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description'
click_button 'Submit issue'
end
end
end end
context "when signed in as user with special characters in their name" do context "when signed in as user with special characters in their name" do
......
...@@ -2,26 +2,283 @@ ...@@ -2,26 +2,283 @@
require "spec_helper" require "spec_helper"
describe "User edits issue", :js do describe "Issues > User edits issue", :js do
set(:project) { create(:project_empty_repo, :public) } let_it_be(:project) { create(:project_empty_repo, :public) }
set(:user) { create(:user) } let_it_be(:user) { create(:user) }
set(:issue) { create(:issue, project: project, author: user) } let_it_be(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
let_it_be(:label) { create(:label, project: project) }
let_it_be(:milestone) { create(:milestone, project: project) }
before do before do
project.add_developer(user) project.add_developer(user)
sign_in(user) sign_in(user)
end
context "from edit page" do
before do
visit edit_project_issue_path(project, issue)
end
it "previews content" do
form = first(".gfm-form")
page.within(form) do
fill_in("Description", with: "Bug fixed :smile:")
click_button("Preview")
end
expect(form).to have_button("Write")
end
it 'allows user to select unassigned' do
visit edit_project_issue_path(project, issue)
expect(page).to have_content "Assignee #{user.name}"
first('.js-user-search').click
click_link 'Unassigned'
click_button 'Save changes'
page.within('.assignee') do
expect(page).to have_content 'None - assign yourself'
end
end
context 'with due date' do
before do
visit edit_project_issue_path(project, issue)
end
it 'saves with due date' do
date = Date.today.at_beginning_of_month.tomorrow
fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description'
find('#issuable-due-date').click
page.within '.pika-single' do
click_button date.day
end
expect(find('#issuable-due-date').value).to eq date.to_s
click_button 'Save changes'
visit(edit_project_issue_path(project, issue)) page.within '.issuable-sidebar' do
expect(page).to have_content date.to_s(:medium)
end
end
it 'warns about version conflict' do
issue.update(title: "New title")
fill_in 'issue_title', with: 'bug 345'
fill_in 'issue_description', with: 'bug description'
click_button 'Save changes'
expect(page).to have_content 'Someone edited the issue the same time you did'
end
end
end end
it "previews content" do context "from issue#show" do
form = first(".gfm-form") before do
visit project_issue_path(project, issue)
end
describe 'update labels' do
it 'will not send ajax request when no data is changed' do
page.within '.labels' do
click_link 'Edit'
page.within(form) do find('.dropdown-menu-close', match: :first).click
fill_in("Description", with: "Bug fixed :smile:")
click_button("Preview") expect(page).not_to have_selector('.block-loading')
end
end
end end
expect(form).to have_button("Write") describe 'update assignee' do
context 'by authorized user' do
def close_dropdown_menu_if_visible
find('.dropdown-menu-toggle', visible: :all).tap do |toggle|
toggle.click if toggle.visible?
end
end
it 'allows user to select unassigned' do
visit project_issue_path(project, issue)
page.within('.assignee') do
expect(page).to have_content "#{user.name}"
click_link 'Edit'
click_link 'Unassigned'
first('.title').click
expect(page).to have_content 'None - assign yourself'
end
end
it 'allows user to select an assignee' do
issue2 = create(:issue, project: project, author: user)
visit project_issue_path(project, issue2)
page.within('.assignee') do
expect(page).to have_content "None"
end
page.within '.assignee' do
click_link 'Edit'
end
page.within '.dropdown-menu-user' do
click_link user.name
end
page.within('.assignee') do
expect(page).to have_content user.name
end
end
it 'allows user to unselect themselves' do
issue2 = create(:issue, project: project, author: user)
visit project_issue_path(project, issue2)
page.within '.assignee' do
click_link 'Edit'
click_link user.name
close_dropdown_menu_if_visible
page.within '.value .author' do
expect(page).to have_content user.name
end
click_link 'Edit'
click_link user.name
close_dropdown_menu_if_visible
page.within '.value .assign-yourself' do
expect(page).to have_content "None"
end
end
end
end
context 'by unauthorized user' do
let(:guest) { create(:user) }
before do
project.add_guest(guest)
end
it 'shows assignee text' do
sign_out(:user)
sign_in(guest)
visit project_issue_path(project, issue)
expect(page).to have_content issue.assignees.first.name
end
end
end
describe 'update milestone' do
context 'by authorized user' do
it 'allows user to select unassigned' do
visit project_issue_path(project, issue)
page.within('.milestone') do
expect(page).to have_content "None"
end
find('.block.milestone .edit-link').click
sleep 2 # wait for ajax stuff to complete
first('.dropdown-content li').click
sleep 2
page.within('.milestone') do
expect(page).to have_content 'None'
end
end
it 'allows user to de-select milestone' do
visit project_issue_path(project, issue)
page.within('.milestone') do
click_link 'Edit'
click_link milestone.title
page.within '.value' do
expect(page).to have_content milestone.title
end
click_link 'Edit'
click_link milestone.title
page.within '.value' do
expect(page).to have_content 'None'
end
end
end
end
context 'by unauthorized user' do
let(:guest) { create(:user) }
before do
project.add_guest(guest)
issue.milestone = milestone
issue.save
end
it 'shows milestone text' do
sign_out(:user)
sign_in(guest)
visit project_issue_path(project, issue)
expect(page).to have_content milestone.title
end
end
end
context 'update due date' do
it 'adds due date to issue' do
date = Date.today.at_beginning_of_month + 2.days
page.within '.due_date' do
click_link 'Edit'
page.within '.pika-single' do
click_button date.day
end
wait_for_requests
expect(find('.value').text).to have_content date.strftime('%b %-d, %Y')
end
end
it 'removes due date from issue' do
date = Date.today.at_beginning_of_month + 2.days
page.within '.due_date' do
click_link 'Edit'
page.within '.pika-single' do
click_button date.day
end
wait_for_requests
expect(page).to have_no_content 'None'
click_link 'remove due date'
expect(page).to have_content 'None'
end
end
end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe 'User filters issues' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project_empty_repo, :public) }
before do
%w[foobar barbaz].each do |title|
create(:issue,
author: user,
assignees: [user],
project: project,
title: title)
end
@issue = Issue.find_by(title: 'foobar')
@issue.milestone = create(:milestone, project: project)
@issue.assignees = []
@issue.save
end
let(:issue) { @issue }
it 'allows filtering by issues with no specified assignee' do
visit project_issues_path(project, assignee_id: IssuableFinder::FILTER_NONE)
expect(page).to have_content 'foobar'
expect(page).not_to have_content 'barbaz'
end
it 'allows filtering by a specified assignee' do
visit project_issues_path(project, assignee_id: user.id)
expect(page).not_to have_content 'foobar'
expect(page).to have_content 'barbaz'
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Issues > User resets their incoming email token' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, namespace: user.namespace) }
let_it_be(:issue) { create(:issue, project: project) }
before do
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
project.add_maintainer(user)
sign_in(user)
visit namespace_project_issues_path(user.namespace, project)
end
it 'changes incoming email address token', :js do
find('.issuable-email-modal-btn').click
previous_token = find('input#issuable_email').value
find('.incoming-email-token-reset').click
wait_for_requests
expect(page).to have_no_field('issuable_email', with: previous_token)
new_token = project.new_issuable_address(user.reload, 'issue')
expect(page).to have_field(
'issuable_email',
with: new_token
)
end
end
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
describe 'New issue breadcrumb' do describe 'New issue breadcrumb' do
let(:project) { create(:project) } let_it_be(:project, reload: true) { create(:project) }
let(:user) { project.creator } let(:user) { project.creator }
before do before do
...@@ -17,4 +17,22 @@ describe 'New issue breadcrumb' do ...@@ -17,4 +17,22 @@ describe 'New issue breadcrumb' do
expect(find_link('New')[:href]).to end_with(new_project_issue_path(project)) expect(find_link('New')[:href]).to end_with(new_project_issue_path(project))
end end
end end
it 'links to current issue in breadcrubs' do
issue = create(:issue, project: project)
visit project_issue_path(project, issue)
expect(find('.breadcrumbs-sub-title a')[:href]).to end_with(issue_path(issue))
end
it 'excludes award_emoji from comment count' do
issue = create(:issue, author: user, assignees: [user], project: project, title: 'foobar')
create(:award_emoji, awardable: issue)
visit project_issues_path(project, assignee_id: user.id)
expect(page).to have_content 'foobar'
expect(page.all('.no-comments').first.text).to eq "0"
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe 'Issues > User sees empty state' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { project.creator }
shared_examples_for 'empty state with filters' do
it 'user sees empty state with filters' do
create(:issue, author: user, project: project)
visit project_issues_path(project, milestone_title: "1.0")
expect(page).to have_content('Sorry, your filter produced no results')
expect(page).to have_content('To widen your search, change or remove filters above')
end
end
describe 'while user is signed out' do
describe 'empty state' do
it 'user sees empty state' do
visit project_issues_path(project)
expect(page).to have_content('Register / Sign In')
expect(page).to have_content('The Issue Tracker is the place to add things that need to be improved or solved in a project.')
expect(page).to have_content('You can register or sign in to create issues for this project.')
end
it_behaves_like 'empty state with filters'
end
end
describe 'while user is signed in' do
before do
sign_in(user)
end
describe 'empty state' do
it 'user sees empty state' do
visit project_issues_path(project)
expect(page).to have_content('The Issue Tracker is the place to add things that need to be improved or solved in a project')
expect(page).to have_content('Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.')
expect(page).to have_content('New issue')
end
it_behaves_like 'empty state with filters'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Issues > User sees live update', :js do
let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { project.creator }
before do
sign_in(user)
end
describe 'title issue#show' do
it 'updates the title' do
issue = create(:issue, author: user, assignees: [user], project: project, title: 'new title')
visit project_issue_path(project, issue)
expect(page).to have_text("new title")
issue.update(title: "updated title")
wait_for_requests
expect(page).to have_text("updated title")
end
end
describe 'confidential issue#show' do
it 'shows confidential sibebar information as confidential and can be turned off' do
issue = create(:issue, :confidential, project: project)
visit project_issue_path(project, issue)
expect(page).to have_css('.issuable-note-warning')
expect(find('.issuable-sidebar-item.confidentiality')).to have_css('.is-active')
expect(find('.issuable-sidebar-item.confidentiality')).not_to have_css('.not-active')
find('.confidential-edit').click
expect(page).to have_css('.sidebar-item-warning-message')
within('.sidebar-item-warning-message') do
find('.btn-close').click
end
wait_for_requests
visit project_issue_path(project, issue)
expect(page).not_to have_css('.is-active')
end
end
end
...@@ -3,12 +3,17 @@ ...@@ -3,12 +3,17 @@
require "spec_helper" require "spec_helper"
describe "User sorts issues" do describe "User sorts issues" do
set(:user) { create(:user) } include SortingHelper
set(:group) { create(:group) } include IssueHelpers
set(:project) { create(:project_empty_repo, :public, group: group) }
set(:issue1) { create(:issue, project: project) } let_it_be(:user) { create(:user) }
set(:issue2) { create(:issue, project: project) } let_it_be(:group) { create(:group) }
set(:issue3) { create(:issue, project: project) } let_it_be(:project) { create(:project_empty_repo, :public, group: group) }
let_it_be(:issue1, reload: true) { create(:issue, title: 'foo', created_at: Time.now, project: project) }
let_it_be(:issue2, reload: true) { create(:issue, title: 'bar', created_at: Time.now - 60, project: project) }
let_it_be(:issue3, reload: true) { create(:issue, title: 'baz', created_at: Time.now - 120, project: project) }
let_it_be(:newer_due_milestone) { create(:milestone, project: project, due_date: '2013-12-11') }
let_it_be(:later_due_milestone) { create(:milestone, project: project, due_date: '2013-12-12') }
before do before do
create_list(:award_emoji, 2, :upvote, awardable: issue1) create_list(:award_emoji, 2, :upvote, awardable: issue1)
...@@ -62,4 +67,174 @@ describe "User sorts issues" do ...@@ -62,4 +67,174 @@ describe "User sorts issues" do
end end
end end
end end
it 'sorts by newest' do
visit project_issues_path(project, sort: sort_value_created_date)
expect(first_issue).to include('foo')
expect(last_issue).to include('baz')
end
it 'sorts by most recently updated' do
issue3.updated_at = Time.now + 100
issue3.save
visit project_issues_path(project, sort: sort_value_recently_updated)
expect(first_issue).to include('baz')
end
describe 'sorting by due date' do
before do
issue1.update(due_date: 1.day.from_now)
issue2.update(due_date: 6.days.from_now)
end
it 'sorts by due date' do
visit project_issues_path(project, sort: sort_value_due_date)
expect(first_issue).to include('foo')
end
it 'sorts by due date by excluding nil due dates' do
issue2.update(due_date: nil)
visit project_issues_path(project, sort: sort_value_due_date)
expect(first_issue).to include('foo')
end
context 'with a filter on labels' do
let(:label) { create(:label, project: project) }
before do
create(:label_link, label: label, target: issue1)
end
it 'sorts by least recently due date by excluding nil due dates' do
issue2.update(due_date: nil)
visit project_issues_path(project, label_names: [label.name], sort: sort_value_due_date_later)
expect(first_issue).to include('foo')
end
end
end
describe 'filtering by due date' do
before do
issue1.update(due_date: 1.day.from_now)
issue2.update(due_date: 6.days.from_now)
end
it 'filters by none' do
visit project_issues_path(project, due_date: Issue::NoDueDate.name)
page.within '.issues-holder' do
expect(page).not_to have_content('foo')
expect(page).not_to have_content('bar')
expect(page).to have_content('baz')
end
end
it 'filters by any' do
visit project_issues_path(project, due_date: Issue::AnyDueDate.name)
page.within '.issues-holder' do
expect(page).to have_content('foo')
expect(page).to have_content('bar')
expect(page).to have_content('baz')
end
end
it 'filters by due this week' do
issue1.update(due_date: Date.today.beginning_of_week + 2.days)
issue2.update(due_date: Date.today.end_of_week)
issue3.update(due_date: Date.today - 8.days)
visit project_issues_path(project, due_date: Issue::DueThisWeek.name)
page.within '.issues-holder' do
expect(page).to have_content('foo')
expect(page).to have_content('bar')
expect(page).not_to have_content('baz')
end
end
it 'filters by due this month' do
issue1.update(due_date: Date.today.beginning_of_month + 2.days)
issue2.update(due_date: Date.today.end_of_month)
issue3.update(due_date: Date.today - 50.days)
visit project_issues_path(project, due_date: Issue::DueThisMonth.name)
page.within '.issues-holder' do
expect(page).to have_content('foo')
expect(page).to have_content('bar')
expect(page).not_to have_content('baz')
end
end
it 'filters by overdue' do
issue1.update(due_date: Date.today + 2.days)
issue2.update(due_date: Date.today + 20.days)
issue3.update(due_date: Date.yesterday)
visit project_issues_path(project, due_date: Issue::Overdue.name)
page.within '.issues-holder' do
expect(page).not_to have_content('foo')
expect(page).not_to have_content('bar')
expect(page).to have_content('baz')
end
end
it 'filters by due next month and previous two weeks' do
issue1.update(due_date: Date.today - 4.weeks)
issue2.update(due_date: (Date.today + 2.months).beginning_of_month)
issue3.update(due_date: Date.yesterday)
visit project_issues_path(project, due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name)
page.within '.issues-holder' do
expect(page).not_to have_content('foo')
expect(page).not_to have_content('bar')
expect(page).to have_content('baz')
end
end
end
describe 'sorting by milestone' do
before do
issue1.milestone = newer_due_milestone
issue1.save
issue2.milestone = later_due_milestone
issue2.save
end
it 'sorts by milestone' do
visit project_issues_path(project, sort: sort_value_milestone)
expect(first_issue).to include('foo')
expect(last_issue).to include('baz')
end
end
describe 'combine filter and sort' do
let(:user2) { create(:user) }
before do
issue1.assignees << user2
issue1.save
issue2.assignees << user2
issue2.save
end
it 'sorts with a filter applied' do
visit project_issues_path(project, sort: sort_value_created_date, assignee_id: user2.id)
expect(first_issue).to include('foo')
expect(last_issue).to include('bar')
expect(page).not_to have_content('baz')
end
end
end end
This diff is collapsed.
...@@ -50,7 +50,7 @@ describe 'Merge request > User sees versions', :js do ...@@ -50,7 +50,7 @@ describe 'Merge request > User sees versions', :js do
expect(page).to have_content 'latest version' expect(page).to have_content 'latest version'
end end
expect(page).to have_content '8 Files' expect(page).to have_content '8 files'
end end
it_behaves_like 'allows commenting', it_behaves_like 'allows commenting',
...@@ -84,7 +84,7 @@ describe 'Merge request > User sees versions', :js do ...@@ -84,7 +84,7 @@ describe 'Merge request > User sees versions', :js do
end end
it 'shows comments that were last relevant at that version' do it 'shows comments that were last relevant at that version' do
expect(page).to have_content '5 Files' expect(page).to have_content '5 files'
position = Gitlab::Diff::Position.new( position = Gitlab::Diff::Position.new(
old_path: ".gitmodules", old_path: ".gitmodules",
...@@ -128,12 +128,10 @@ describe 'Merge request > User sees versions', :js do ...@@ -128,12 +128,10 @@ describe 'Merge request > User sees versions', :js do
diff_id: merge_request_diff3.id, diff_id: merge_request_diff3.id,
start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9' start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
) )
expect(page).to have_content '4 Files' expect(page).to have_content '4 files'
additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-addition') additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-addition-line').text
.ancestor('.diff-stats-group').text deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-deletion-line').text
deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-deletion')
.ancestor('.diff-stats-group').text
expect(additions_content).to eq '15' expect(additions_content).to eq '15'
expect(deletions_content).to eq '6' expect(deletions_content).to eq '6'
...@@ -156,12 +154,10 @@ describe 'Merge request > User sees versions', :js do ...@@ -156,12 +154,10 @@ describe 'Merge request > User sees versions', :js do
end end
it 'show diff between new and old version' do it 'show diff between new and old version' do
additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-addition') additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-addition-line').text
.ancestor('.diff-stats-group').text deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-deletion-line').text
deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-deletion')
.ancestor('.diff-stats-group').text
expect(page).to have_content '4 Files' expect(page).to have_content '4 files'
expect(additions_content).to eq '15' expect(additions_content).to eq '15'
expect(deletions_content).to eq '6' expect(deletions_content).to eq '6'
end end
...@@ -171,7 +167,7 @@ describe 'Merge request > User sees versions', :js do ...@@ -171,7 +167,7 @@ describe 'Merge request > User sees versions', :js do
page.within '.mr-version-dropdown' do page.within '.mr-version-dropdown' do
expect(page).to have_content 'latest version' expect(page).to have_content 'latest version'
end end
expect(page).to have_content '8 Files' expect(page).to have_content '8 files'
end end
it_behaves_like 'allows commenting', it_behaves_like 'allows commenting',
...@@ -197,7 +193,7 @@ describe 'Merge request > User sees versions', :js do ...@@ -197,7 +193,7 @@ describe 'Merge request > User sees versions', :js do
find('.btn-default').click find('.btn-default').click
click_link 'version 1' click_link 'version 1'
end end
expect(page).to have_content '0 Files' expect(page).to have_content '0 files'
end end
end end
...@@ -223,7 +219,7 @@ describe 'Merge request > User sees versions', :js do ...@@ -223,7 +219,7 @@ describe 'Merge request > User sees versions', :js do
expect(page).to have_content 'version 1' expect(page).to have_content 'version 1'
end end
expect(page).to have_content '0 Files' expect(page).to have_content '0 files'
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Project > Settings > CI/CD > Container registry tag expiration policy', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
context 'as owner' do
before do
sign_in(user)
visit project_settings_ci_cd_path(project)
end
it 'section is available' do
settings_block = find('#js-registry-policies')
expect(settings_block).to have_text 'Container Registry tag expiration policy'
end
it 'Save expiration policy submit the form', :js do
within '#js-registry-policies' do
within '.card-body' do
click_button(class: 'gl-toggle')
select('7 days until tags are automatically removed', from: 'expiration-policy-interval')
select('Every day', from: 'expiration-policy-schedule')
select('50 tags per image name', from: 'expiration-policy-latest')
fill_in('expiration-policy-name-matching', with: '*-production')
end
submit_button = find('.card-footer .btn.btn-success')
expect(submit_button).not_to be_disabled
submit_button.click
end
flash_text = find('.flash-text')
expect(flash_text).to have_content('Expiration policy successfully saved.')
end
end
end
...@@ -46,8 +46,6 @@ describe 'Multi-file editor new directory', :js do ...@@ -46,8 +46,6 @@ describe 'Multi-file editor new directory', :js do
find('.js-ide-commit-mode').click find('.js-ide-commit-mode').click
click_button 'Stage'
fill_in('commit-message', with: 'commit message ide') fill_in('commit-message', with: 'commit message ide')
find(:css, ".js-ide-commit-new-mr input").set(false) find(:css, ".js-ide-commit-new-mr input").set(false)
......
...@@ -36,8 +36,6 @@ describe 'Multi-file editor new file', :js do ...@@ -36,8 +36,6 @@ describe 'Multi-file editor new file', :js do
find('.js-ide-commit-mode').click find('.js-ide-commit-mode').click
click_button 'Stage'
fill_in('commit-message', with: 'commit message ide') fill_in('commit-message', with: 'commit message ide')
find(:css, ".js-ide-commit-new-mr input").set(false) find(:css, ".js-ide-commit-new-mr input").set(false)
......
...@@ -50,8 +50,7 @@ describe('CompareVersions', () => { ...@@ -50,8 +50,7 @@ describe('CompareVersions', () => {
expect(treeListBtn.exists()).toBe(true); expect(treeListBtn.exists()).toBe(true);
expect(treeListBtn.attributes('title')).toBe('Hide file browser'); expect(treeListBtn.attributes('title')).toBe('Hide file browser');
expect(treeListBtn.findAll(Icon).length).not.toBe(0); expect(treeListBtn.find(Icon).props('name')).toBe('file-tree');
expect(treeListBtn.find(Icon).props('name')).toBe('collapse-left');
}); });
it('should render comparison dropdowns with correct values', () => { it('should render comparison dropdowns with correct values', () => {
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Icon from '~/vue_shared/components/icon.vue';
import DiffStats from '~/diffs/components/diff_stats.vue'; import DiffStats from '~/diffs/components/diff_stats.vue';
describe('diff_stats', () => { describe('diff_stats', () => {
...@@ -24,18 +23,11 @@ describe('diff_stats', () => { ...@@ -24,18 +23,11 @@ describe('diff_stats', () => {
}, },
}); });
const findIcon = name => const findFileLine = name => wrapper.find(name);
wrapper const additions = findFileLine('.js-file-addition-line');
.findAll(Icon) const deletions = findFileLine('.js-file-deletion-line');
.filter(c => c.attributes('name') === name)
.at(0).element.parentNode;
const additions = findIcon('file-addition'); expect(additions.text()).toBe('100');
const deletions = findIcon('file-deletion'); expect(deletions.text()).toBe('200');
const filesChanged = findIcon('doc-code');
expect(additions.textContent).toContain('100');
expect(deletions.textContent).toContain('200');
expect(filesChanged.textContent).toContain('300');
}); });
}); });
...@@ -514,6 +514,8 @@ describe('IDE store file actions', () => { ...@@ -514,6 +514,8 @@ describe('IDE store file actions', () => {
describe('changeFileContent', () => { describe('changeFileContent', () => {
let tmpFile; let tmpFile;
const callAction = (content = 'content\n') =>
store.dispatch('changeFileContent', { path: tmpFile.path, content });
beforeEach(() => { beforeEach(() => {
tmpFile = file('tmpFile'); tmpFile = file('tmpFile');
...@@ -523,11 +525,7 @@ describe('IDE store file actions', () => { ...@@ -523,11 +525,7 @@ describe('IDE store file actions', () => {
}); });
it('updates file content', done => { it('updates file content', done => {
store callAction()
.dispatch('changeFileContent', {
path: tmpFile.path,
content: 'content\n',
})
.then(() => { .then(() => {
expect(tmpFile.content).toBe('content\n'); expect(tmpFile.content).toBe('content\n');
...@@ -537,11 +535,7 @@ describe('IDE store file actions', () => { ...@@ -537,11 +535,7 @@ describe('IDE store file actions', () => {
}); });
it('adds a newline to the end of the file if it doesnt already exist', done => { it('adds a newline to the end of the file if it doesnt already exist', done => {
store callAction('content')
.dispatch('changeFileContent', {
path: tmpFile.path,
content: 'content',
})
.then(() => { .then(() => {
expect(tmpFile.content).toBe('content\n'); expect(tmpFile.content).toBe('content\n');
...@@ -551,11 +545,7 @@ describe('IDE store file actions', () => { ...@@ -551,11 +545,7 @@ describe('IDE store file actions', () => {
}); });
it('adds file into changedFiles array', done => { it('adds file into changedFiles array', done => {
store callAction()
.dispatch('changeFileContent', {
path: tmpFile.path,
content: 'content',
})
.then(() => { .then(() => {
expect(store.state.changedFiles.length).toBe(1); expect(store.state.changedFiles.length).toBe(1);
...@@ -564,7 +554,7 @@ describe('IDE store file actions', () => { ...@@ -564,7 +554,7 @@ describe('IDE store file actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('adds file once into changedFiles array', done => { it('adds file not more than once into changedFiles array', done => {
store store
.dispatch('changeFileContent', { .dispatch('changeFileContent', {
path: tmpFile.path, path: tmpFile.path,
...@@ -604,6 +594,52 @@ describe('IDE store file actions', () => { ...@@ -604,6 +594,52 @@ describe('IDE store file actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
describe('when `gon.feature.stageAllByDefault` is true', () => {
const originalGonFeatures = Object.assign({}, gon.features);
beforeAll(() => {
gon.features = { stageAllByDefault: true };
});
afterAll(() => {
gon.features = originalGonFeatures;
});
it('adds file into stagedFiles array', done => {
store
.dispatch('changeFileContent', {
path: tmpFile.path,
content: 'content',
})
.then(() => {
expect(store.state.stagedFiles.length).toBe(1);
done();
})
.catch(done.fail);
});
it('adds file not more than once into stagedFiles array', done => {
store
.dispatch('changeFileContent', {
path: tmpFile.path,
content: 'content',
})
.then(() =>
store.dispatch('changeFileContent', {
path: tmpFile.path,
content: 'content 123',
}),
)
.then(() => {
expect(store.state.stagedFiles.length).toBe(1);
done();
})
.catch(done.fail);
});
});
it('bursts unused seal', done => { it('bursts unused seal', done => {
store store
.dispatch('changeFileContent', { .dispatch('changeFileContent', {
......
...@@ -106,7 +106,7 @@ exports[`Settings Form renders 1`] = ` ...@@ -106,7 +106,7 @@ exports[`Settings Form renders 1`] = `
<glformgroup-stub <glformgroup-stub
id="expiration-policy-latest-group" id="expiration-policy-latest-group"
label="Expiration latest:" label="Number of tags to retain:"
label-align="right" label-align="right"
label-cols="3" label-cols="3"
label-for="expiration-policy-latest" label-for="expiration-policy-latest"
...@@ -136,7 +136,7 @@ exports[`Settings Form renders 1`] = ` ...@@ -136,7 +136,7 @@ exports[`Settings Form renders 1`] = `
<glformgroup-stub <glformgroup-stub
id="expiration-policy-name-matching-group" id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters" invalid-feedback="The value of this input should be less than 255 characters"
label="Expire Docker tags with name matching:" label="Expire Docker tags that match this regex:"
label-align="right" label-align="right"
label-cols="3" label-cols="3"
label-for="expiration-policy-name-matching" label-for="expiration-policy-name-matching"
......
...@@ -44,7 +44,9 @@ describe('Actions Registry Store', () => { ...@@ -44,7 +44,9 @@ describe('Actions Registry Store', () => {
}; };
const payload = { const payload = {
tag_expiration_policies: 'foo', data: {
container_expiration_policy: 'foo',
},
}; };
it('should fetch the data from the API', done => { it('should fetch the data from the API', done => {
...@@ -56,7 +58,7 @@ describe('Actions Registry Store', () => { ...@@ -56,7 +58,7 @@ describe('Actions Registry Store', () => {
[], [],
[ [
{ type: 'toggleLoading' }, { type: 'toggleLoading' },
{ type: 'receiveSettingsSuccess', payload: payload.tag_expiration_policies }, { type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy },
{ type: 'toggleLoading' }, { type: 'toggleLoading' },
], ],
done, done,
...@@ -83,7 +85,9 @@ describe('Actions Registry Store', () => { ...@@ -83,7 +85,9 @@ describe('Actions Registry Store', () => {
}; };
const payload = { const payload = {
tag_expiration_policies: 'foo', data: {
tag_expiration_policies: 'foo',
},
}; };
it('should fetch the data from the API', done => { it('should fetch the data from the API', done => {
...@@ -95,11 +99,11 @@ describe('Actions Registry Store', () => { ...@@ -95,11 +99,11 @@ describe('Actions Registry Store', () => {
[], [],
[ [
{ type: 'toggleLoading' }, { type: 'toggleLoading' },
{ type: 'receiveSettingsSuccess', payload: payload.tag_expiration_policies }, { type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy },
{ type: 'toggleLoading' }, { type: 'toggleLoading' },
], ],
() => { () => {
expect(createFlash).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE); expect(createFlash).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, 'success');
done(); done();
}, },
); );
......
...@@ -6,7 +6,7 @@ import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link ...@@ -6,7 +6,7 @@ import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link
let vm; let vm;
function createCommitData(data = {}) { function createCommitData(data = {}) {
return { const defaultData = {
sha: '123456789', sha: '123456789',
title: 'Commit title', title: 'Commit title',
message: 'Commit message', message: 'Commit message',
...@@ -26,8 +26,8 @@ function createCommitData(data = {}) { ...@@ -26,8 +26,8 @@ function createCommitData(data = {}) {
group: {}, group: {},
}, },
}, },
...data,
}; };
return Object.assign(defaultData, data);
} }
function factory(commit = createCommitData(), loading = false) { function factory(commit = createCommitData(), loading = false) {
...@@ -46,6 +46,8 @@ function factory(commit = createCommitData(), loading = false) { ...@@ -46,6 +46,8 @@ function factory(commit = createCommitData(), loading = false) {
vm.vm.$apollo.queries.commit.loading = loading; vm.vm.$apollo.queries.commit.loading = loading;
} }
const emptyMessageClass = 'font-italic';
describe('Repository last commit component', () => { describe('Repository last commit component', () => {
afterEach(() => { afterEach(() => {
vm.destroy(); vm.destroy();
...@@ -135,4 +137,12 @@ describe('Repository last commit component', () => { ...@@ -135,4 +137,12 @@ describe('Repository last commit component', () => {
expect(vm.element).toMatchSnapshot(); expect(vm.element).toMatchSnapshot();
}); });
}); });
it('sets correct CSS class if the commit message is empty', () => {
factory(createCommitData({ message: '' }));
return vm.vm.$nextTick().then(() => {
expect(vm.find('.item-title').classes()).toContain(emptyMessageClass);
});
});
}); });
...@@ -57,10 +57,10 @@ describe('Changed file icon', () => { ...@@ -57,10 +57,10 @@ describe('Changed file icon', () => {
describe.each` describe.each`
file | iconName | tooltipText | desc file | iconName | tooltipText | desc
${changedFile()} | ${'file-modified'} | ${'Unstaged modification'} | ${'with file changed'} ${changedFile()} | ${'file-modified-solid'} | ${'Unstaged modification'} | ${'with file changed'}
${stagedFile()} | ${'file-modified-solid'} | ${'Staged modification'} | ${'with file staged'} ${stagedFile()} | ${'file-modified-solid'} | ${'Staged modification'} | ${'with file staged'}
${changedAndStagedFile()} | ${'file-modified'} | ${'Unstaged and staged modification'} | ${'with file changed and staged'} ${changedAndStagedFile()} | ${'file-modified-solid'} | ${'Unstaged and staged modification'} | ${'with file changed and staged'}
${newFile()} | ${'file-addition'} | ${'Unstaged addition'} | ${'with file new'} ${newFile()} | ${'file-addition-solid'} | ${'Unstaged addition'} | ${'with file new'}
`('$desc', ({ file, iconName, tooltipText }) => { `('$desc', ({ file, iconName, tooltipText }) => {
beforeEach(() => { beforeEach(() => {
factory({ file }); factory({ file });
......
...@@ -357,10 +357,10 @@ describe MarkupHelper do ...@@ -357,10 +357,10 @@ describe MarkupHelper do
describe '#markup_unsafe' do describe '#markup_unsafe' do
subject { helper.markup_unsafe(file_name, text, context) } subject { helper.markup_unsafe(file_name, text, context) }
let_it_be(:project_base) { create(:project, :repository) }
let_it_be(:context) { { project: project_base } }
let(:file_name) { 'foo.bar' } let(:file_name) { 'foo.bar' }
let(:text) { 'Noël' } let(:text) { 'Noël' }
let(:project_base) { build(:project, :repository) }
let(:context) { { project: project_base } }
context 'when text is missing' do context 'when text is missing' do
let(:text) { nil } let(:text) { nil }
...@@ -383,12 +383,21 @@ describe MarkupHelper do ...@@ -383,12 +383,21 @@ describe MarkupHelper do
context 'when renderer returns an error' do context 'when renderer returns an error' do
before do before do
allow(Banzai).to receive(:render).and_raise("An error") allow(Banzai).to receive(:render).and_raise(StandardError, "An error")
end end
it 'returns html (rendered by ActionView:TextHelper)' do it 'returns html (rendered by ActionView:TextHelper)' do
is_expected.to eq('<p>Noël</p>') is_expected.to eq('<p>Noël</p>')
end end
it 'logs the error' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(
instance_of(StandardError),
project_id: project.id, file_name: 'foo.md', context: context
)
subject
end
end end
end end
......
...@@ -33,6 +33,12 @@ describe('IDE commit form', () => { ...@@ -33,6 +33,12 @@ describe('IDE commit form', () => {
}); });
describe('compact', () => { describe('compact', () => {
beforeEach(done => {
vm.isCompact = true;
vm.$nextTick(done);
});
it('renders commit button in compact mode', () => { it('renders commit button in compact mode', () => {
expect(vm.$el.querySelector('.btn-primary')).not.toBeNull(); expect(vm.$el.querySelector('.btn-primary')).not.toBeNull();
expect(vm.$el.querySelector('.btn-primary').textContent).toContain('Commit'); expect(vm.$el.querySelector('.btn-primary').textContent).toContain('Commit');
...@@ -61,7 +67,7 @@ describe('IDE commit form', () => { ...@@ -61,7 +67,7 @@ describe('IDE commit form', () => {
}); });
}); });
it('toggles activity bar vie when clicking commit button', done => { it('toggles activity bar view when clicking commit button', done => {
vm.$el.querySelector('.btn-primary').click(); vm.$el.querySelector('.btn-primary').click();
vm.$nextTick(() => { vm.$nextTick(() => {
...@@ -104,6 +110,17 @@ describe('IDE commit form', () => { ...@@ -104,6 +110,17 @@ describe('IDE commit form', () => {
}); });
}); });
it('always opens itself in full view current activity view is not commit view when clicking commit button', done => {
vm.$el.querySelector('.btn-primary').click();
vm.$nextTick(() => {
expect(store.state.currentActivityView).toBe(activityBarViews.commit);
expect(vm.isCompact).toBe(false);
done();
});
});
describe('discard draft button', () => { describe('discard draft button', () => {
it('hidden when commitMessage is empty', () => { it('hidden when commitMessage is empty', () => {
expect(vm.$el.querySelector('.btn-default').textContent).toContain('Collapse'); expect(vm.$el.querySelector('.btn-default').textContent).toContain('Collapse');
......
...@@ -93,13 +93,13 @@ describe('RepoTab', () => { ...@@ -93,13 +93,13 @@ describe('RepoTab', () => {
Vue.nextTick() Vue.nextTick()
.then(() => { .then(() => {
expect(vm.$el.querySelector('.file-modified')).toBeNull(); expect(vm.$el.querySelector('.file-modified-solid')).toBeNull();
vm.$el.dispatchEvent(new Event('mouseout')); vm.$el.dispatchEvent(new Event('mouseout'));
}) })
.then(Vue.nextTick) .then(Vue.nextTick)
.then(() => { .then(() => {
expect(vm.$el.querySelector('.file-modified')).not.toBeNull(); expect(vm.$el.querySelector('.file-modified-solid')).not.toBeNull();
done(); done();
}) })
......
...@@ -225,6 +225,35 @@ describe('Multi-file store actions', () => { ...@@ -225,6 +225,35 @@ describe('Multi-file store actions', () => {
.catch(done.fail); .catch(done.fail);
}); });
describe('when `gon.feature.stageAllByDefault` is true', () => {
const originalGonFeatures = Object.assign({}, gon.features);
beforeAll(() => {
gon.features = { stageAllByDefault: true };
});
afterAll(() => {
gon.features = originalGonFeatures;
});
it('adds tmp file to staged files', done => {
const name = 'test';
store
.dispatch('createTempEntry', {
name,
branchId: 'mybranch',
type: 'blob',
})
.then(() => {
expect(store.state.stagedFiles).toEqual([jasmine.objectContaining({ name })]);
done();
})
.catch(done.fail);
});
});
it('adds tmp file to open files', done => { it('adds tmp file to open files', done => {
const name = 'test'; const name = 'test';
...@@ -255,41 +284,25 @@ describe('Multi-file store actions', () => { ...@@ -255,41 +284,25 @@ describe('Multi-file store actions', () => {
type: 'blob', type: 'blob',
}) })
.then(() => { .then(() => {
const f = store.state.entries[name]; expect(store.state.changedFiles).toEqual([
jasmine.objectContaining({ name, tempFile: true }),
expect(store.state.changedFiles.length).toBe(1); ]);
expect(store.state.changedFiles[0].name).toBe(f.name);
done(); done();
}) })
.catch(done.fail); .catch(done.fail);
}); });
it('sets tmp file as active', done => { it('sets tmp file as active', () => {
testAction( const dispatch = jasmine.createSpy();
createTempEntry, const commit = jasmine.createSpy();
{
name: 'test', createTempEntry(
branchId: 'mybranch', { state: store.state, getters: store.getters, dispatch, commit },
type: 'blob', { name: 'test', branchId: 'mybranch', type: 'blob' },
},
store.state,
[
{ type: types.CREATE_TMP_ENTRY, payload: jasmine.any(Object) },
{ type: types.TOGGLE_FILE_OPEN, payload: 'test' },
{ type: types.ADD_FILE_TO_CHANGED, payload: 'test' },
],
jasmine.arrayContaining([
{
type: 'setFileActive',
payload: 'test',
},
{
type: 'triggerFilesChange',
},
]),
done,
); );
expect(dispatch).toHaveBeenCalledWith('setFileActive', 'test');
}); });
it('creates flash message if file already exists', done => { it('creates flash message if file already exists', done => {
...@@ -800,6 +813,33 @@ describe('Multi-file store actions', () => { ...@@ -800,6 +813,33 @@ describe('Multi-file store actions', () => {
}); });
}); });
describe('when `gon.feature.stageAllByDefault` is true', () => {
const originalGonFeatures = Object.assign({}, gon.features);
beforeAll(() => {
gon.features = { stageAllByDefault: true };
});
afterAll(() => {
gon.features = originalGonFeatures;
});
it('by default renames an entry and stages it', () => {
const dispatch = jasmine.createSpy();
const commit = jasmine.createSpy();
renameEntry(
{ dispatch, commit, state: store.state, getters: store.getters },
{ path: 'orig', name: 'renamed' },
);
expect(commit.calls.allArgs()).toEqual([
[types.RENAME_ENTRY, { path: 'orig', name: 'renamed', parentPath: undefined }],
[types.STAGE_CHANGE, jasmine.objectContaining({ path: 'renamed' })],
]);
});
});
it('by default renames an entry and adds to changed', done => { it('by default renames an entry and adds to changed', done => {
testAction( testAction(
renameEntry, renameEntry,
...@@ -819,12 +859,12 @@ describe('Multi-file store actions', () => { ...@@ -819,12 +859,12 @@ describe('Multi-file store actions', () => {
payload: 'renamed', payload: 'renamed',
}, },
], ],
[{ type: 'burstUnusedSeal' }, { type: 'triggerFilesChange' }], jasmine.any(Object),
done, done,
); );
}); });
it('if not changed, completely unstages entry if renamed to original', done => { it('if not changed, completely unstages and discards entry if renamed to original', done => {
testAction( testAction(
renameEntry, renameEntry,
{ path: 'renamed', name: 'orig' }, { path: 'renamed', name: 'orig' },
......
...@@ -1735,6 +1735,39 @@ module Gitlab ...@@ -1735,6 +1735,39 @@ module Gitlab
it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test1 dependencies the build2 should be part of needs') } it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test1 dependencies the build2 should be part of needs') }
end end
context 'needs with a Hash type and dependencies with a string type that are mismatching' do
let(:needs) do
[
"build1",
{ job: "build2" }
]
end
let(:dependencies) { %w(build3) }
it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test1 dependencies the build3 should be part of needs') }
end
context 'needs with an array type and dependency with a string type' do
let(:needs) { %w(build1) }
let(:dependencies) { 'deploy' }
it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test1 dependencies should be an array of strings') }
end
context 'needs with a string type and dependency with an array type' do
let(:needs) { 'build1' }
let(:dependencies) { %w(deploy) }
it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test1:needs config can only be a hash or an array') }
end
context 'needs with a Hash type and dependency with a string type' do
let(:needs) { { job: 'build1' } }
let(:dependencies) { 'deploy' }
it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'jobs:test1 dependencies should be an array of strings') }
end
end end
context 'with when/rules conflict' do context 'with when/rules conflict' do
......
...@@ -57,7 +57,7 @@ describe Gitlab::Git::Commit, :seed_helper do ...@@ -57,7 +57,7 @@ describe Gitlab::Git::Commit, :seed_helper do
it { expect(@commit.different_committer?).to be_truthy } it { expect(@commit.different_committer?).to be_truthy }
it { expect(@commit.parents).to eq(@gitlab_parents) } it { expect(@commit.parents).to eq(@gitlab_parents) }
it { expect(@commit.parent_id).to eq(@parents.first.oid) } it { expect(@commit.parent_id).to eq(@parents.first.oid) }
it { expect(@commit.no_commit_message).to eq("--no commit message") } it { expect(@commit.no_commit_message).to eq("No commit message") }
after do after do
# Erase the new commit so other tests get the original repo # Erase the new commit so other tests get the original repo
......
...@@ -277,7 +277,7 @@ describe Commit do ...@@ -277,7 +277,7 @@ describe Commit do
describe '#title' do describe '#title' do
it "returns no_commit_message when safe_message is blank" do it "returns no_commit_message when safe_message is blank" do
allow(commit).to receive(:safe_message).and_return('') allow(commit).to receive(:safe_message).and_return('')
expect(commit.title).to eq("--no commit message") expect(commit.title).to eq("No commit message")
end end
it 'truncates a message without a newline at natural break to 80 characters' do it 'truncates a message without a newline at natural break to 80 characters' do
...@@ -308,7 +308,7 @@ eos ...@@ -308,7 +308,7 @@ eos
describe '#full_title' do describe '#full_title' do
it "returns no_commit_message when safe_message is blank" do it "returns no_commit_message when safe_message is blank" do
allow(commit).to receive(:safe_message).and_return('') allow(commit).to receive(:safe_message).and_return('')
expect(commit.full_title).to eq("--no commit message") expect(commit.full_title).to eq("No commit message")
end end
it "returns entire message if there is no newline" do it "returns entire message if there is no newline" do
...@@ -330,7 +330,7 @@ eos ...@@ -330,7 +330,7 @@ eos
it 'returns no_commit_message when safe_message is blank' do it 'returns no_commit_message when safe_message is blank' do
allow(commit).to receive(:safe_message).and_return(nil) allow(commit).to receive(:safe_message).and_return(nil)
expect(commit.description).to eq('--no commit message') expect(commit.description).to eq('No commit message')
end end
it 'returns description of commit message if title less than 100 characters' do it 'returns description of commit message if title less than 100 characters' do
......
...@@ -52,8 +52,8 @@ describe 'Mark snippet as spam' do ...@@ -52,8 +52,8 @@ describe 'Mark snippet as spam' do
end end
it 'marks snippet as spam' do it 'marks snippet as spam' do
expect_next_instance_of(SpamService) do |instance| expect_next_instance_of(Spam::MarkAsSpamService) do |instance|
expect(instance).to receive(:mark_as_spam!) expect(instance).to receive(:execute)
end end
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
......
# frozen_string_literal: true
require 'spec_helper'
describe Spam::MarkAsSpamService do
let(:user_agent_detail) { build(:user_agent_detail) }
let(:spammable) { build(:issue, user_agent_detail: user_agent_detail) }
let(:fake_akismet_service) { double(:akismet_service, submit_spam: true) }
subject { described_class.new(spammable: spammable) }
describe '#execute' do
before do
allow(subject).to receive(:akismet).and_return(fake_akismet_service)
end
context 'when the spammable object is not submittable' do
before do
allow(spammable).to receive(:submittable_as_spam?).and_return false
end
it 'does not submit as spam' do
expect(subject.execute).to be_falsey
end
end
context 'spam is submitted successfully' do
before do
allow(spammable).to receive(:submittable_as_spam?).and_return true
allow(fake_akismet_service).to receive(:submit_spam).and_return true
end
it 'submits as spam' do
expect(subject.execute).to be_truthy
end
it "updates the spammable object's user agent detail as being submitted as spam" do
expect(user_agent_detail).to receive(:update_attribute)
subject.execute
end
context 'when Akismet does not consider it spam' do
it 'does not update the spammable object as spam' do
allow(fake_akismet_service).to receive(:submit_spam).and_return false
expect(subject.execute).to be_falsey
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment