Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-16 15:08:32 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-16 15:08:32 +0300
commitc158fa8d69c704663d289341a014c44c062cda88 (patch)
treed0cac82a9ac9e9ad28bb0030266eb8d5dc91fbbc
parentb806264d29b8d52ccb78a41dcc3d67f2b040700c (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/diffs/components/app.vue2
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue12
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue16
-rw-r--r--app/assets/javascripts/diffs/components/diff_stats.vue23
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue9
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue47
-rw-r--r--app/assets/javascripts/ide/components/file_row_extra.vue15
-rw-r--r--app/assets/javascripts/ide/stores/actions.js15
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js6
-rw-r--r--app/assets/javascripts/registry/settings/components/registry_settings_app.vue2
-rw-r--r--app/assets/javascripts/registry/settings/components/settings_form.vue10
-rw-r--r--app/assets/javascripts/registry/settings/store/actions.js14
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue6
-rw-r--r--app/assets/javascripts/repository/queries/pathLastCommit.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue11
-rw-r--r--app/assets/stylesheets/pages/diff.scss15
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss4
-rw-r--r--app/controllers/concerns/spammable_actions.rb2
-rw-r--r--app/controllers/groups/milestones_controller.rb4
-rw-r--r--app/controllers/ide_controller.rb4
-rw-r--r--app/graphql/mutations/snippets/mark_as_spam.rb2
-rw-r--r--app/helpers/markup_helper.rb4
-rw-r--r--app/models/concerns/reactive_caching.rb75
-rw-r--r--app/services/concerns/akismet_methods.rb25
-rw-r--r--app/services/notes/destroy_service.rb2
-rw-r--r--app/services/spam/mark_as_spam_service.rb24
-rw-r--r--app/services/spam_service.rb36
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml4
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--changelogs/unreleased/118442-restyle-changes-header-and-file-tree.yml5
-rw-r--r--changelogs/unreleased/196158-fix-tags-nil.yml5
-rw-r--r--changelogs/unreleased/27366-supergroup-milestone-not-showing-in-subgroup-board-issue-list.yml6
-rw-r--r--changelogs/unreleased/expose-tiller-log.yml5
-rw-r--r--changelogs/unreleased/fj-remove-storage-version-from-snippets.yml5
-rw-r--r--changelogs/unreleased/himkp-33441.yml5
-rw-r--r--changelogs/unreleased/issue-28822-add-style-to-no-commit-message.yml5
-rw-r--r--changelogs/unreleased/kerrizor-enable-redis-diff-caching-by-default.yml5
-rw-r--r--changelogs/unreleased/sh-fix-ci-lint-error.yml5
-rw-r--r--db/post_migrate/20200114180546_remove_storage_version_column_from_snippets.rb25
-rw-r--r--db/schema.rb3
-rw-r--r--doc/development/fe_guide/graphql.md35
-rw-r--r--doc/user/application_security/dast/index.md4
-rw-r--r--doc/user/clusters/applications.md5
-rw-r--r--doc/user/discussions/img/suggestion-commit-message-configuration.pngbin62700 -> 0 bytes
-rw-r--r--doc/user/discussions/img/suggestions_custom_commit_messages_v12_7.pngbin0 -> 21720 bytes
-rw-r--r--doc/user/discussions/index.md57
-rw-r--r--doc/user/project/web_ide/img/review_changes_v12_3.pngbin179489 -> 0 bytes
-rw-r--r--doc/user/project/web_ide/index.md12
-rw-r--r--lib/gitlab/ci/config/entry/job.rb7
-rw-r--r--lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml5
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff_base.rb2
-rw-r--r--lib/gitlab/git/commit.rb2
-rw-r--r--locale/gitlab.pot33
-rw-r--r--qa/qa/page/project/web_ide/edit.rb12
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb13
-rw-r--r--spec/features/issues/user_creates_issue_by_email_spec.rb46
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb126
-rw-r--r--spec/features/issues/user_edits_issue_spec.rb279
-rw-r--r--spec/features/issues/user_filters_issues_spec.rb39
-rw-r--r--spec/features/issues/user_resets_their_incoming_email_token_spec.rb32
-rw-r--r--spec/features/issues/user_sees_breadcrumb_links_spec.rb20
-rw-r--r--spec/features/issues/user_sees_empty_state_spec.rb51
-rw-r--r--spec/features/issues/user_sees_live_update_spec.rb52
-rw-r--r--spec/features/issues/user_sorts_issues_spec.rb187
-rw-r--r--spec/features/issues_spec.rb828
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb26
-rw-r--r--spec/features/projects/settings/registry_settings_spec.rb37
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb2
-rw-r--r--spec/features/projects/tree/create_file_spec.rb2
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js3
-rw-r--r--spec/frontend/diffs/components/diff_stats_spec.js18
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js68
-rw-r--r--spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap4
-rw-r--r--spec/frontend/registry/settings/store/actions_spec.js14
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/changed_file_icon_spec.js6
-rw-r--r--spec/helpers/markup_helper_spec.rb15
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/form_spec.js19
-rw-r--r--spec/javascripts/ide/components/repo_tab_spec.js4
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js100
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb33
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb2
-rw-r--r--spec/models/commit_spec.rb6
-rw-r--r--spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb4
-rw-r--r--spec/services/spam/mark_as_spam_service_spec.rb52
88 files changed, 1547 insertions, 1212 deletions
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 463d1427805..878b54f7d53 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -374,7 +374,7 @@ export default {
<div
:data-can-create-note="getNoteableData.current_user.can_create_note"
- class="files d-flex prepend-top-default"
+ class="files d-flex"
>
<div
v-show="showTreeList"
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index 24542126b07..63ce43a193d 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -1,5 +1,4 @@
<script>
-/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui';
import { __ } from '~/locale';
@@ -63,9 +62,6 @@ export default {
showDropdowns() {
return !this.commit && this.mergeRequestDiffs.length;
},
- fileTreeIcon() {
- return this.showTreeList ? 'collapse-left' : 'expand-left';
- },
toggleFileBrowserTitle() {
return this.showTreeList ? __('Hide file browser') : __('Show file browser');
},
@@ -91,7 +87,7 @@ export default {
</script>
<template>
- <div class="mr-version-controls border-top border-bottom">
+ <div class="mr-version-controls border-top">
<div
class="mr-version-menus-container content-block"
:class="{
@@ -108,17 +104,17 @@ export default {
:title="toggleFileBrowserTitle"
@click="toggleShowTreeList"
>
- <icon :name="fileTreeIcon" />
+ <icon name="file-tree" />
</button>
<div v-if="showDropdowns" class="d-flex align-items-center compare-versions-container">
- Changes between
+ {{ __('Compare') }}
<compare-versions-dropdown
:other-versions="mergeRequestDiffs"
:merge-request-version="mergeRequestDiff"
:show-commit-count="true"
class="mr-version-dropdown"
/>
- and
+ {{ __('and') }}
<compare-versions-dropdown
:other-versions="comparableDiffs"
:base-version-path="baseVersionPath"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 5d27c6eb865..e78bea789c3 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -123,6 +123,20 @@ export default {
}
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() {
polyfillSticky(this.$refs.header);
@@ -221,7 +235,7 @@ export default {
<div
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" />
<div class="btn-group" role="group">
diff --git a/app/assets/javascripts/diffs/components/diff_stats.vue b/app/assets/javascripts/diffs/components/diff_stats.vue
index 2e5855380af..1fa1fda7bd7 100644
--- a/app/assets/javascripts/diffs/components/diff_stats.vue
+++ b/app/assets/javascripts/diffs/components/diff_stats.vue
@@ -1,9 +1,7 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import { n__ } from '~/locale';
export default {
- components: { Icon },
props: {
addedLines: {
type: Number,
@@ -21,7 +19,7 @@ export default {
},
computed: {
filesText() {
- return n__('File', 'Files', this.diffFilesLength);
+ return n__('file', 'files', this.diffFilesLength);
},
isCompareVersionsHeader() {
return Boolean(this.diffFilesLength);
@@ -39,14 +37,21 @@ export default {
}"
>
<div v-if="diffFilesLength !== null" class="diff-stats-group">
- <icon name="doc-code" class="diff-stats-icon text-secondary" />
- <strong>{{ diffFilesLength }} {{ filesText }}</strong>
+ <span class="text-secondary bold">{{ diffFilesLength }} {{ filesText }}</span>
</div>
- <div class="diff-stats-group cgreen">
- <icon name="file-addition" class="diff-stats-icon" /> <strong>{{ addedLines }}</strong>
+ <div
+ 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 class="diff-stats-group cred">
- <icon name="file-deletion" class="diff-stats-icon" /> <strong>{{ removedLines }}</strong>
+ <div
+ 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>
</template>
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 30be2e68e76..7956d05b4f1 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -4,7 +4,6 @@ import { GlTooltipDirective } from '@gitlab/ui';
import { s__, sprintf } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
-import FileRowStats from './file_row_stats.vue';
export default {
directives: {
@@ -48,9 +47,6 @@ export default {
return acc;
}, []);
},
- fileRowExtraComponent() {
- return this.hideFileStats ? null : FileRowStats;
- },
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']),
@@ -58,8 +54,8 @@ export default {
this.search = '';
},
},
- searchPlaceholder: sprintf(s__('MergeRequest|Filter files or search with %{modifier_key}+p'), {
- modifier_key: /Mac/i.test(navigator.userAgent) ? 'cmd' : 'ctrl',
+ searchPlaceholder: sprintf(s__('MergeRequest|Search files (%{modifier_key}P)'), {
+ modifier_key: /Mac/i.test(navigator.userAgent) ? '⌘' : 'Ctrl+',
}),
};
</script>
@@ -97,7 +93,6 @@ export default {
:file="file"
:level="0"
:hide-extra-on-tree="true"
- :extra-component="fileRowExtraComponent"
:show-changed-icon="true"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="scrollToFile"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index f7ed7006874..002c00599bb 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -6,6 +6,7 @@ import CommitMessageField from './message_field.vue';
import Actions from './actions.vue';
import SuccessMessage from './success_message.vue';
import { activityBarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -14,6 +15,7 @@ export default {
CommitMessageField,
SuccessMessage,
},
+ mixins: [glFeatureFlagsMixin()],
data() {
return {
isCompact: true,
@@ -27,9 +29,13 @@ export default {
...mapGetters('commit', ['discardDraftButtonDisabled', 'preBuiltCommitMessage']),
overviewText() {
return sprintf(
- __(
- '<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
- ),
+ this.glFeatures.stageAllByDefault
+ ? __(
+ '<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes',
+ )
+ : __(
+ '<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes',
+ ),
{
stagedFilesLength: this.stagedFiles.length,
changedFilesLength: this.changedFiles.length,
@@ -39,6 +45,10 @@ export default {
commitButtonText() {
return this.stagedFiles.length ? __('Commit') : __('Stage & Commit');
},
+
+ currentViewIsCommitView() {
+ return this.currentActivityView === activityBarViews.commit;
+ },
},
watch: {
currentActivityView() {
@@ -46,27 +56,26 @@ export default {
this.isCompact = false;
} else {
this.isCompact = !(
- this.currentActivityView === activityBarViews.commit &&
- window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
+ this.currentViewIsCommitView && window.innerHeight >= MAX_WINDOW_HEIGHT_COMPACT
);
}
},
- lastCommitMsg() {
- this.isCompact =
- this.currentActivityView !== activityBarViews.commit && this.lastCommitMsg === '';
- },
},
methods: {
...mapActions(['updateActivityBarView']),
...mapActions('commit', ['updateCommitMessage', 'discardDraft', 'commitChanges']),
- toggleIsSmall() {
- this.updateActivityBarView(activityBarViews.commit)
- .then(() => {
- this.isCompact = !this.isCompact;
- })
- .catch(e => {
- throw e;
- });
+ toggleIsCompact() {
+ if (this.currentViewIsCommitView) {
+ this.isCompact = !this.isCompact;
+ } else {
+ this.updateActivityBarView(activityBarViews.commit)
+ .then(() => {
+ this.isCompact = false;
+ })
+ .catch(e => {
+ throw e;
+ });
+ }
},
beforeEnterTransition() {
const elHeight = this.isCompact
@@ -114,7 +123,7 @@ export default {
:disabled="!hasChanges"
type="button"
class="btn btn-primary btn-sm btn-block qa-begin-commit-button"
- @click="toggleIsSmall"
+ @click="toggleIsCompact"
>
{{ __('Commit…') }}
</button>
@@ -148,7 +157,7 @@ export default {
v-else
type="button"
class="btn btn-default btn-sm float-right"
- @click="toggleIsSmall"
+ @click="toggleIsCompact"
>
{{ __('Collapse') }}
</button>
diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue
index f0bedcfbd6b..33098eb1af0 100644
--- a/app/assets/javascripts/ide/components/file_row_extra.vue
+++ b/app/assets/javascripts/ide/components/file_row_extra.vue
@@ -6,6 +6,7 @@ import Icon from '~/vue_shared/components/icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
import NewDropdown from './new_dropdown/index.vue';
import MrFileIcon from './mr_file_icon.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'FileRowExtra',
@@ -18,6 +19,7 @@ export default {
ChangedFileIcon,
MrFileIcon,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
file: {
type: Object,
@@ -55,10 +57,15 @@ export default {
return n__('%d staged change', '%d staged changes', this.folderStagedCount);
}
- return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), {
- unstaged: this.folderUnstagedCount,
- staged: this.folderStagedCount,
- });
+ return sprintf(
+ this.glFeatures.stageAllByDefault
+ ? __('%{staged} staged and %{unstaged} unstaged changes')
+ : __('%{unstaged} unstaged and %{staged} staged changes'),
+ {
+ unstaged: this.folderUnstagedCount,
+ staged: this.folderStagedCount,
+ },
+ );
},
showTreeChangesCount() {
return this.isTree && this.changesCount > 0 && !this.file.opened;
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 3445ef7a75f..cb027358d46 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -51,7 +51,7 @@ export const setResizingStatus = ({ commit }, resizing) => {
};
export const createTempEntry = (
- { state, commit, dispatch },
+ { state, commit, dispatch, getters },
{ name, type, content = '', base64 = false, binary = false, rawPath = '' },
) => {
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
@@ -92,7 +92,11 @@ export const createTempEntry = (
if (type === 'blob') {
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('triggerFilesChange');
dispatch('burstUnusedSeal');
@@ -238,7 +242,7 @@ export const deleteEntry = ({ commit, dispatch, state }, path) => {
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 newPath = parentPath ? `${parentPath}/${name}` : name;
const existingParent = parentPath && state.entries[parentPath];
@@ -268,7 +272,10 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, parentPat
if (isReset) {
commit(types.REMOVE_FILE_FROM_STAGED_AND_CHANGED, newEntry);
} 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');
}
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 1bfee7b6be4..70a966afa66 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -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];
commit(types.UPDATE_FILE_CONTENT, {
path,
@@ -157,7 +157,9 @@ export const changeFileContent = ({ commit, dispatch, state }, { path, content }
const indexOfChangedFile = state.changedFiles.findIndex(f => f.path === path);
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) {
commit(types.REMOVE_FILE_FROM_CHANGED, path);
}
diff --git a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
index c770fd70260..ca495cd2eca 100644
--- a/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
+++ b/app/assets/javascripts/registry/settings/components/registry_settings_app.vue
@@ -37,7 +37,7 @@ export default {
}}
</li>
</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" />
</div>
</template>
diff --git a/app/assets/javascripts/registry/settings/components/settings_form.vue b/app/assets/javascripts/registry/settings/components/settings_form.vue
index 55a6a1ace55..457bf35daab 100644
--- a/app/assets/javascripts/registry/settings/components/settings_form.vue
+++ b/app/assets/javascripts/registry/settings/components/settings_form.vue
@@ -46,7 +46,7 @@ export default {
regexHelpText() {
return sprintf(
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>',
@@ -61,7 +61,7 @@ export default {
nameRegexState() {
return this.name_regex ? this.name_regex.length <= NAME_REGEX_LENGTH : null;
},
- formIsValid() {
+ formIsInvalid() {
return this.nameRegexState === false;
},
},
@@ -124,7 +124,7 @@ export default {
:label-cols="$options.labelsConfig.cols"
:label-align="$options.labelsConfig.align"
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">
<option v-for="option in formOptions.keepN" :key="option.key" :value="option.key">
@@ -138,7 +138,7 @@ export default {
:label-cols="$options.labelsConfig.cols"
:label-align="$options.labelsConfig.align"
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"
:invalid-feedback="
s__('ContainerRegistry|The value of this input should be less than 255 characters')
@@ -165,7 +165,7 @@ export default {
<gl-button
ref="save-button"
type="submit"
- :disabled="formIsValid"
+ :disabled="formIsInvalid"
variant="success"
class="d-block"
>
diff --git a/app/assets/javascripts/registry/settings/store/actions.js b/app/assets/javascripts/registry/settings/store/actions.js
index b161373dd0a..5e46d564121 100644
--- a/app/assets/javascripts/registry/settings/store/actions.js
+++ b/app/assets/javascripts/registry/settings/store/actions.js
@@ -18,8 +18,8 @@ export const resetSettings = ({ commit }) => commit(types.RESET_SETTINGS);
export const fetchSettings = ({ dispatch, state }) => {
dispatch('toggleLoading');
return Api.project(state.projectId)
- .then(({ tag_expiration_policies }) =>
- dispatch('receiveSettingsSuccess', tag_expiration_policies),
+ .then(({ data: { container_expiration_policy } }) =>
+ dispatch('receiveSettingsSuccess', container_expiration_policy),
)
.catch(() => dispatch('receiveSettingsError'))
.finally(() => dispatch('toggleLoading'));
@@ -27,10 +27,12 @@ export const fetchSettings = ({ dispatch, state }) => {
export const saveSettings = ({ dispatch, state }) => {
dispatch('toggleLoading');
- return Api.updateProject(state.projectId, { tag_expiration_policies: state.settings })
- .then(({ tag_expiration_policies }) => {
- dispatch('receiveSettingsSuccess', tag_expiration_policies);
- createFlash(UPDATE_SETTINGS_SUCCESS_MESSAGE);
+ return Api.updateProject(state.projectId, {
+ container_expiration_policy_attributes: state.settings,
+ })
+ .then(({ data: { container_expiration_policy } }) => {
+ dispatch('receiveSettingsSuccess', container_expiration_policy);
+ createFlash(UPDATE_SETTINGS_SUCCESS_MESSAGE, 'success');
})
.catch(() => dispatch('updateSettingsError'))
.finally(() => dispatch('toggleLoading'));
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 8e7529899c0..fe1724acf89 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -104,7 +104,11 @@ export default {
</span>
<div class="commit-detail flex-list">
<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 }}
</gl-link>
<gl-button
diff --git a/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql b/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
index 9be025afe39..c812614e94d 100644
--- a/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
+++ b/app/assets/javascripts/repository/queries/pathLastCommit.query.graphql
@@ -6,6 +6,7 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
sha
title
description
+ message
webUrl
authoredDate
authorName
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 75c3c544c77..09cffc57688 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -36,12 +36,17 @@ export default {
required: false,
default: true,
},
+ showChangedStatus: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
changedIcon() {
// 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
- const suffix = !this.file.changed && this.file.staged && this.showStagedIcon ? '-solid' : '';
+ const suffix = this.showStagedIcon ? '-solid' : '';
return `${getCommitIconMap(this.file).icon}${suffix}`;
},
@@ -86,8 +91,8 @@ export default {
<span
v-gl-tooltip.right
:title="tooltipTitle"
- :class="{ 'ml-auto': isCentered }"
- class="file-changed-icon d-inline-block"
+ :class="[{ 'ml-auto': isCentered }, changedIconClass]"
+ class="file-changed-icon d-flex align-items-center "
>
<icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" />
</span>
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 611001df32f..0c9f6ea94d5 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -1,5 +1,4 @@
<script>
-import Icon from '~/vue_shared/components/icon.vue';
import FileHeader from '~/vue_shared/components/file_row_header.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue';
@@ -9,7 +8,6 @@ export default {
components: {
FileHeader,
FileIcon,
- Icon,
ChangedFileIcon,
},
props: {
@@ -26,6 +24,7 @@ export default {
required: false,
default: null,
},
+
hideExtraOnTree: {
type: Boolean,
required: false,
@@ -143,17 +142,17 @@ export default {
@mouseleave="toggleDropdown(false)"
>
<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
v-if="!showChangedIcon || file.type === 'tree'"
- class="file-row-icon"
+ class="file-row-icon text-secondary mr-1"
:file-name="file.name"
:loading="file.loading"
:folder="isTree"
:opened="file.opened"
: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 }}
</span>
<component
@@ -163,6 +162,7 @@ export default {
:dropdown-open="dropdownOpen"
@toggle="toggleDropdown($event)"
/>
+ <changed-file-icon :file="file" :size="16" class="append-right-5" />
</div>
</div>
<template v-if="file.opened || file.isHeader">
@@ -172,7 +172,6 @@ export default {
:file="childFile"
:level="childFilesLevel"
:hide-extra-on-tree="hideExtraOnTree"
- :extra-component="extraComponent"
:show-changed-icon="showChangedIcon"
@toggleTreeOpen="toggleTreeOpen"
@clickFile="clickedFile"
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index f394e4ab58a..d1053570093 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -14,9 +14,9 @@
cursor: pointer;
@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
- $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: sticky;
@@ -552,7 +552,7 @@ table.code {
.diff-stats {
align-items: center;
- padding: 0 0.25rem;
+ padding: 0 1rem;
.diff-stats-group {
padding: 0 0.25rem;
@@ -564,7 +564,7 @@ table.code {
&.is-compare-versions-header {
.diff-stats-group {
- padding: 0 0.5rem;
+ padding: 0 0.25rem;
}
}
}
@@ -1059,8 +1059,8 @@ table.code {
.diff-tree-list {
position: -webkit-sticky;
position: sticky;
- $top-pos: $header-height + $mr-tabs-height + $mr-version-controls-height + 10px;
- top: $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 + 11px;
max-height: calc(100vh - #{$top-pos});
z-index: 202;
@@ -1097,10 +1097,7 @@ table.code {
.tree-list-scroll {
max-height: 100%;
- padding-top: $grid-size;
padding-bottom: $grid-size;
- border-top: 1px solid $border-color;
- border-bottom: 1px solid $border-color;
overflow-y: scroll;
overflow-x: auto;
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index c023c9e5cbd..84daec4fb43 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -708,7 +708,7 @@
.mr-version-controls {
position: relative;
z-index: 203;
- background: $gray-light;
+ background: $white-light;
color: $gl-text-color;
margin-top: -1px;
@@ -732,7 +732,7 @@
}
.content-block {
- padding: $gl-padding-top $gl-padding;
+ padding: $gl-padding;
border-bottom: 0;
}
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index 36fdbfd6eff..9ec8f930a78 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -11,7 +11,7 @@ module SpammableActions
end
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 }
else
redirect_to spammable_path, alert: _('Error with Akismet. Please check the logs for more info.')
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 1e9d51cf970..7eba73daa3c 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -119,7 +119,9 @@ class Groups::MilestonesController < Groups::ApplicationController
end
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
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index 4c9aac9a327..ca35b07111c 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -3,6 +3,10 @@
class IdeController < ApplicationController
layout 'fullscreen'
+ before_action do
+ push_frontend_feature_flag(:stage_all_by_default, default_enabled: true)
+ end
+
def index
Gitlab::UsageDataCounters::WebIdeCounter.increment_views_count
end
diff --git a/app/graphql/mutations/snippets/mark_as_spam.rb b/app/graphql/mutations/snippets/mark_as_spam.rb
index 96cb208651f..8cfbbae7c08 100644
--- a/app/graphql/mutations/snippets/mark_as_spam.rb
+++ b/app/graphql/mutations/snippets/mark_as_spam.rb
@@ -24,7 +24,7 @@ module Mutations
private
def mark_as_spam(snippet)
- SpamService.new(spammable: snippet).mark_as_spam!
+ Spam::MarkAsSpamService.new(spammable: snippet).execute
end
def authorized_resource?(snippet)
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 719de095faf..d6e466d4678 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -154,7 +154,9 @@ module MarkupHelper
else
other_markup_unsafe(file_name, text, context)
end
- rescue RuntimeError
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_exception(e, project_id: @project&.id, file_name: file_name, context: context)
+
simple_format(text)
end
diff --git a/app/models/concerns/reactive_caching.rb b/app/models/concerns/reactive_caching.rb
index 45fbbef9225..4b9896343c6 100644
--- a/app/models/concerns/reactive_caching.rb
+++ b/app/models/concerns/reactive_caching.rb
@@ -1,78 +1,7 @@
# frozen_string_literal: true
-# The ReactiveCaching concern is used to fetch some data in the background and
-# store it in the Rails cache, keeping it up-to-date for as long as it is being
-# 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
+# The usage of the ReactiveCaching module is documented here:
+# https://docs.gitlab.com/ee/development/utilities.html#reactivecaching
module ReactiveCaching
extend ActiveSupport::Concern
diff --git a/app/services/concerns/akismet_methods.rb b/app/services/concerns/akismet_methods.rb
new file mode 100644
index 00000000000..1cbcf0d47b9
--- /dev/null
+++ b/app/services/concerns/akismet_methods.rb
@@ -0,0 +1,25 @@
+# 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
diff --git a/app/services/notes/destroy_service.rb b/app/services/notes/destroy_service.rb
index fa0c2c5c86b..ee8a680fcb4 100644
--- a/app/services/notes/destroy_service.rb
+++ b/app/services/notes/destroy_service.rb
@@ -11,3 +11,5 @@ module Notes
end
end
end
+
+Notes::DestroyService.prepend_if_ee('EE::Notes::DestroyService')
diff --git a/app/services/spam/mark_as_spam_service.rb b/app/services/spam/mark_as_spam_service.rb
new file mode 100644
index 00000000000..0ebcf17927a
--- /dev/null
+++ b/app/services/spam/mark_as_spam_service.rb
@@ -0,0 +1,24 @@
+# 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
diff --git a/app/services/spam_service.rb b/app/services/spam_service.rb
index a546c834603..ba9b812a01c 100644
--- a/app/services/spam_service.rb
+++ b/app/services/spam_service.rb
@@ -1,10 +1,12 @@
# frozen_string_literal: true
class SpamService
+ include AkismetMethods
+
attr_accessor :spammable, :request, :options
attr_reader :spam_log
- def initialize(spammable:, request: nil)
+ def initialize(spammable:, request:)
@spammable = spammable
@request = request
@options = {}
@@ -19,16 +21,6 @@ class SpamService
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)
# In case it's a request which is already verified through recaptcha, yield
# block.
@@ -54,28 +46,6 @@ class SpamService
true
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?
spammable.check_for_spam?
end
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 08675c16124..f49cdfbf8da 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -8,7 +8,7 @@
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div
= 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.
.prepend-top-20
= f.submit "Verify code", class: "btn btn-success"
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 3a9c7a8bec5..8b659034fe6 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -21,9 +21,9 @@
.commit-detail.flex-list
.commit-content.qa-commit-content
- 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
- = 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
&middot;
= commit.short_id
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 9e7652fe663..5a6c8079543 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -60,7 +60,7 @@
= render 'projects/triggers/index'
- 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
%h4
= _("Container Registry tag expiration policy")
diff --git a/changelogs/unreleased/118442-restyle-changes-header-and-file-tree.yml b/changelogs/unreleased/118442-restyle-changes-header-and-file-tree.yml
new file mode 100644
index 00000000000..8b82d03a503
--- /dev/null
+++ b/changelogs/unreleased/118442-restyle-changes-header-and-file-tree.yml
@@ -0,0 +1,5 @@
+---
+title: Restyle changes header & file tree
+merge_request: 22364
+author:
+type: changed
diff --git a/changelogs/unreleased/196158-fix-tags-nil.yml b/changelogs/unreleased/196158-fix-tags-nil.yml
new file mode 100644
index 00000000000..5a7dd3177ea
--- /dev/null
+++ b/changelogs/unreleased/196158-fix-tags-nil.yml
@@ -0,0 +1,5 @@
+---
+title: 'Geo: Handle repositories in Docker Registry with no tags gracefully'
+merge_request: 23022
+author:
+type: fixed
diff --git a/changelogs/unreleased/27366-supergroup-milestone-not-showing-in-subgroup-board-issue-list.yml b/changelogs/unreleased/27366-supergroup-milestone-not-showing-in-subgroup-board-issue-list.yml
new file mode 100644
index 00000000000..6fc98aadc79
--- /dev/null
+++ b/changelogs/unreleased/27366-supergroup-milestone-not-showing-in-subgroup-board-issue-list.yml
@@ -0,0 +1,6 @@
+---
+title: Fix group issue list and group issue board filters not showing ancestor group
+ milestones
+merge_request: 23038
+author:
+type: fixed
diff --git a/changelogs/unreleased/expose-tiller-log.yml b/changelogs/unreleased/expose-tiller-log.yml
new file mode 100644
index 00000000000..71538f05333
--- /dev/null
+++ b/changelogs/unreleased/expose-tiller-log.yml
@@ -0,0 +1,5 @@
+---
+title: Exposes tiller.log as artifact in Managed-Cluster-Applications GitLab CI template
+merge_request: 22940
+author:
+type: changed
diff --git a/changelogs/unreleased/fj-remove-storage-version-from-snippets.yml b/changelogs/unreleased/fj-remove-storage-version-from-snippets.yml
new file mode 100644
index 00000000000..ee73b903bc6
--- /dev/null
+++ b/changelogs/unreleased/fj-remove-storage-version-from-snippets.yml
@@ -0,0 +1,5 @@
+---
+title: Remove storage_version column from snippets
+merge_request: 23004
+author:
+type: changed
diff --git a/changelogs/unreleased/himkp-33441.yml b/changelogs/unreleased/himkp-33441.yml
new file mode 100644
index 00000000000..63b7fb43f7a
--- /dev/null
+++ b/changelogs/unreleased/himkp-33441.yml
@@ -0,0 +1,5 @@
+---
+title: Stage all changes by default in Web IDE
+merge_request: 21067
+author:
+type: added
diff --git a/changelogs/unreleased/issue-28822-add-style-to-no-commit-message.yml b/changelogs/unreleased/issue-28822-add-style-to-no-commit-message.yml
new file mode 100644
index 00000000000..e2cf12299e5
--- /dev/null
+++ b/changelogs/unreleased/issue-28822-add-style-to-no-commit-message.yml
@@ -0,0 +1,5 @@
+---
+title: Updated no commit verbiage
+merge_request: 22765
+author:
+type: other
diff --git a/changelogs/unreleased/kerrizor-enable-redis-diff-caching-by-default.yml b/changelogs/unreleased/kerrizor-enable-redis-diff-caching-by-default.yml
new file mode 100644
index 00000000000..de22d282635
--- /dev/null
+++ b/changelogs/unreleased/kerrizor-enable-redis-diff-caching-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Enable redis HSET diff caching by default
+merge_request: 23105
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-ci-lint-error.yml b/changelogs/unreleased/sh-fix-ci-lint-error.yml
new file mode 100644
index 00000000000..7ab4b05f00f
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-ci-lint-error.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Error 500 in parsing invalid CI needs and dependencies
+merge_request: 22567
+author:
+type: fixed
diff --git a/db/post_migrate/20200114180546_remove_storage_version_column_from_snippets.rb b/db/post_migrate/20200114180546_remove_storage_version_column_from_snippets.rb
new file mode 100644
index 00000000000..6aa545a1787
--- /dev/null
+++ b/db/post_migrate/20200114180546_remove_storage_version_column_from_snippets.rb
@@ -0,0 +1,25 @@
+# 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
diff --git a/db/schema.rb b/db/schema.rb
index 0932993be24..a154f7fa993 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# 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
enable_extension "pg_trgm"
@@ -3838,7 +3838,6 @@ ActiveRecord::Schema.define(version: 2020_01_14_113341) do
t.string "encrypted_secret_token_iv", limit: 255
t.boolean "secret", default: false, 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 ["content"], name: "index_snippets_on_content_trigram", opclass: :gin_trgm_ops, using: :gin
t.index ["created_at"], name: "index_snippets_on_created_at"
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index 36cde1b0f75..1639029d193 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -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).
+### 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
#### Mocking response as component data
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index fd418748371..9678ff4de5a 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -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)
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
It's also possible to authenticate the user before performing the DAST checks:
diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md
index 3d7d5019c94..5c9b8b56cbd 100644
--- a/doc/user/clusters/applications.md
+++ b/doc/user/clusters/applications.md
@@ -505,7 +505,10 @@ To install applications using GitLab CI:
customize values for the installed application.
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
diff --git a/doc/user/discussions/img/suggestion-commit-message-configuration.png b/doc/user/discussions/img/suggestion-commit-message-configuration.png
deleted file mode 100644
index 962bc9b0aed..00000000000
--- a/doc/user/discussions/img/suggestion-commit-message-configuration.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/discussions/img/suggestions_custom_commit_messages_v12_7.png b/doc/user/discussions/img/suggestions_custom_commit_messages_v12_7.png
new file mode 100644
index 00000000000..8bbc0fcb99b
--- /dev/null
+++ b/doc/user/discussions/img/suggestions_custom_commit_messages_v12_7.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 0a2a9c6d2fa..f5116f22cd8 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -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
Merge Request author (or other users with appropriate
[permission](../permissions.md)) is able to apply these
-suggestions with a click, which will generate a commit in
-the Merge Request authored by the user that applied them.
+Suggestions with a click, which will generate a commit in
+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
on the **Insert suggestion** icon in the toolbar:
@@ -407,32 +407,28 @@ the Merge Request authored by the user that applied them.
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.
- 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:
![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
and push the suggested change directly into the codebase in the merge request's
branch. [Developer permission](../permissions.md) is required to do so.
-> **Note:**
-Custom commit messages will be introduced by
-[#54404](https://gitlab.com/gitlab-org/gitlab-foss/issues/54404).
-
-### Multi-line suggestions
+### Multi-line Suggestions
> [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
-within Merge Request diff threads by adjusting the range offsets. The
+Reviewers can also suggest changes to multiple lines with a single Suggestion
+within merge request diff threads by adjusting the range offsets. 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.
![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_
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
lines _below_ the commented diff line, allowing up to 200 changed lines per
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
-when applying change suggestions. This commit message can be customized to
-follow any guidelines you might have. To do so, open the **Merge requests** tab
-within your project settings and change the **Merge suggestions** text.
+GitLab uses `Apply suggestion to %{file_path}` by default as commit messages
+when applying Suggestions. This commit message can be customized to
+follow any guidelines you might have. To do so, expand the **Merge requests**
+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:
-- `%{project_path}`: The full URL safe project path. E.g: *my-group/my-project*
-- `%{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*
-- `%{branch_name}`: The name of the branch the suggestion is applied on. E.g: *my-feature-branch*
-- `%{username}`: The username of the user applying the suggestion. E.g: *user_1*
-- `%{user_full_name}`: The full name of the user applying the suggestion. E.g: *User 1*
+| Variable | Description | Output example |
+|---|---|---|
+| `%{project_path}` | The project path. | `my-group/my-project` |
+| `%{project_name}` | The human-readable name of the project. | **My Project** |
+| `%{file_path}` | The path of the file the Suggestion is applied to. | `docs/index.md` |
+| `%{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
diff --git a/doc/user/project/web_ide/img/review_changes_v12_3.png b/doc/user/project/web_ide/img/review_changes_v12_3.png
deleted file mode 100644
index cd13e5d9e8e..00000000000
--- a/doc/user/project/web_ide/img/review_changes_v12_3.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index ee96fca7fd1..8f2314bf31f 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -46,12 +46,16 @@ Single file editing is based on the [Ace Editor](https://ace.c9.io).
## Stage and commit changes
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
-click the tick icon to stage the file.
+review the list of changed files. If you're using GitLab 12.6 or older versions,
+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
access to the selected branch, you will see a warning, but still be able to create
a new branch and start a merge request.
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 1b9daa2dbc7..967db0c628c 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -55,11 +55,12 @@ module Gitlab
validates :start_in, duration: { limit: '1 week' }, if: :delayed?
validates :start_in, absence: true, if: -> { has_rules? || !delayed? }
- validate do
+ validate on: :composed do
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?
errors.add(:dependencies, "the #{missing_needs.join(", ")} should be part of needs")
end
diff --git a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
index 16a950ddda1..e5dbc672825 100644
--- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml
@@ -14,3 +14,7 @@ apply:
only:
refs:
- master
+ artifacts:
+ when: on_failure
+ paths:
+ - tiller.log
diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
index 23c65a0cb67..94b9d94fd39 100644
--- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml
@@ -10,10 +10,13 @@ stages:
- deploy
- dast
+variables:
+ DAST_VERSION: 1
+
dast:
stage: dast
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:
# URL to scan:
# DAST_WEBSITE: https://example.com/
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
index 06cf3d4d168..d27da186de0 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff_base.rb
@@ -47,7 +47,7 @@ module Gitlab
private
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)
else
Gitlab::Diff::DeprecatedHighlightCache.new(self)
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index c6f762c3ee6..48da838366f 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -254,7 +254,7 @@ module Gitlab
end
def no_commit_message
- "--no commit message"
+ "No commit message"
end
def to_hash
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a64eb2c40c2..a1b93afec2f 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -379,6 +379,9 @@ msgstr ""
msgid "%{spanStart}in%{spanEnd} %{errorFn}"
msgstr ""
+msgid "%{staged} staged and %{unstaged} unstaged changes"
+msgstr ""
+
msgid "%{start} to %{end}"
msgstr ""
@@ -700,6 +703,9 @@ msgstr ""
msgid "<strong>%{pushes}</strong> pushes, more than <strong>%{commits}</strong> commits by <strong>%{people}</strong> contributors."
msgstr ""
+msgid "<strong>%{stagedFilesLength} staged</strong> and <strong>%{changedFilesLength} unstaged</strong> changes"
+msgstr ""
+
msgid "<strong>Deletes</strong> source branch"
msgstr ""
@@ -4912,9 +4918,6 @@ msgstr ""
msgid "ContainerRegistry|Expiration interval:"
msgstr ""
-msgid "ContainerRegistry|Expiration latest:"
-msgstr ""
-
msgid "ContainerRegistry|Expiration policy successfully saved."
msgstr ""
@@ -4924,7 +4927,7 @@ msgstr ""
msgid "ContainerRegistry|Expiration schedule:"
msgstr ""
-msgid "ContainerRegistry|Expire Docker tags with name matching:"
+msgid "ContainerRegistry|Expire Docker tags that match this regex:"
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."
@@ -4939,6 +4942,9 @@ msgstr ""
msgid "ContainerRegistry|Last Updated"
msgstr ""
+msgid "ContainerRegistry|Number of tags to retain:"
+msgstr ""
+
msgid "ContainerRegistry|Quick Start"
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}"
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 ""
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 ""
msgid "Fetching licenses failed. You are not permitted to perform this action."
msgstr ""
-msgid "File"
-msgid_plural "Files"
-msgstr[0] ""
-msgstr[1] ""
-
msgid "File added"
msgstr ""
@@ -11529,10 +11530,10 @@ msgstr ""
msgid "MergeRequest|Error loading full diff. Please try again."
msgstr ""
-msgid "MergeRequest|Filter files or search with %{modifier_key}+p"
+msgid "MergeRequest|No files found"
msgstr ""
-msgid "MergeRequest|No files found"
+msgid "MergeRequest|Search files (%{modifier_key}P)"
msgstr ""
msgid "Merged"
@@ -21543,6 +21544,9 @@ msgstr ""
msgid "among other things"
msgstr ""
+msgid "and"
+msgstr ""
+
msgid "archived"
msgstr ""
@@ -21970,6 +21974,11 @@ msgstr ""
msgid "failed to dismiss associated finding(id=%{finding_id}): %{message}"
msgstr ""
+msgid "file"
+msgid_plural "files"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "finding is not found or is already attached to a vulnerability"
msgstr ""
diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb
index 0977ff0c91b..73b0856b445 100644
--- a/qa/qa/page/project/web_ide/edit.rb
+++ b/qa/qa/page/project/web_ide/edit.rb
@@ -84,16 +84,13 @@ module QA
end
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
click_element :begin_commit_button
active_element? :commit_mode_tab
- # We need to click :begin_commit_button again
- click_element :begin_commit_button
-
- # After clicking :begin_commit_button the 2nd time there is an
- # animation that hides :begin_commit_button and shows :commit_button
+ # After clicking :begin_commit_button, there is an animation
+ # that hides :begin_commit_button and shows :commit_button
#
# Wait for the animation to complete before clicking :commit_button
# otherwise the click will quietly do nothing.
@@ -102,9 +99,6 @@ module QA
has_element?(:commit_button)
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
# animation is still in process even when the buttons have the
# expected visibility.
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index 4f4f9e5143b..8fb9f0c516c 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -148,6 +148,19 @@ describe Groups::MilestonesController do
expect(response).to have_gitlab_http_status(200)
expect(response.content_type).to eq 'application/json'
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
context 'external authorization' do
diff --git a/spec/features/issues/user_creates_issue_by_email_spec.rb b/spec/features/issues/user_creates_issue_by_email_spec.rb
new file mode 100644
index 00000000000..c73a65849cc
--- /dev/null
+++ b/spec/features/issues/user_creates_issue_by_email_spec.rb
@@ -0,0 +1,46 @@
+# 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
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index 39ce3415727..b0a2a734877 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -3,8 +3,32 @@
require "spec_helper"
describe "User creates issue" do
- let(:project) { create(:project_empty_repo, :public) }
- let(:user) { create(:user) }
+ include DropzoneHelper
+
+ 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
before do
@@ -92,6 +116,104 @@ describe "User creates issue" do
.and have_content(label_titles.first)
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
context "when signed in as user with special characters in their name" do
diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb
index 0afc19d9519..ad984cf07e2 100644
--- a/spec/features/issues/user_edits_issue_spec.rb
+++ b/spec/features/issues/user_edits_issue_spec.rb
@@ -2,26 +2,283 @@
require "spec_helper"
-describe "User edits issue", :js do
- set(:project) { create(:project_empty_repo, :public) }
- set(:user) { create(:user) }
- set(:issue) { create(:issue, project: project, author: user) }
+describe "Issues > User edits issue", :js do
+ let_it_be(:project) { create(:project_empty_repo, :public) }
+ let_it_be(:user) { create(: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
project.add_developer(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
- it "previews content" do
- form = first(".gfm-form")
+ context "from issue#show" do
+ 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
- fill_in("Description", with: "Bug fixed :smile:")
- click_button("Preview")
+ find('.dropdown-menu-close', match: :first).click
+
+ expect(page).not_to have_selector('.block-loading')
+ 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
diff --git a/spec/features/issues/user_filters_issues_spec.rb b/spec/features/issues/user_filters_issues_spec.rb
new file mode 100644
index 00000000000..714bc972025
--- /dev/null
+++ b/spec/features/issues/user_filters_issues_spec.rb
@@ -0,0 +1,39 @@
+# 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
diff --git a/spec/features/issues/user_resets_their_incoming_email_token_spec.rb b/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
new file mode 100644
index 00000000000..108b6f550db
--- /dev/null
+++ b/spec/features/issues/user_resets_their_incoming_email_token_spec.rb
@@ -0,0 +1,32 @@
+# 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
diff --git a/spec/features/issues/user_sees_breadcrumb_links_spec.rb b/spec/features/issues/user_sees_breadcrumb_links_spec.rb
index f31d730c337..8a120a0a0b2 100644
--- a/spec/features/issues/user_sees_breadcrumb_links_spec.rb
+++ b/spec/features/issues/user_sees_breadcrumb_links_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
describe 'New issue breadcrumb' do
- let(:project) { create(:project) }
+ let_it_be(:project, reload: true) { create(:project) }
let(:user) { project.creator }
before do
@@ -17,4 +17,22 @@ describe 'New issue breadcrumb' do
expect(find_link('New')[:href]).to end_with(new_project_issue_path(project))
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
diff --git a/spec/features/issues/user_sees_empty_state_spec.rb b/spec/features/issues/user_sees_empty_state_spec.rb
new file mode 100644
index 00000000000..114d119aca8
--- /dev/null
+++ b/spec/features/issues/user_sees_empty_state_spec.rb
@@ -0,0 +1,51 @@
+# 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
diff --git a/spec/features/issues/user_sees_live_update_spec.rb b/spec/features/issues/user_sees_live_update_spec.rb
new file mode 100644
index 00000000000..98c7d289fb0
--- /dev/null
+++ b/spec/features/issues/user_sees_live_update_spec.rb
@@ -0,0 +1,52 @@
+# 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
diff --git a/spec/features/issues/user_sorts_issues_spec.rb b/spec/features/issues/user_sorts_issues_spec.rb
index 79938785633..66110f55435 100644
--- a/spec/features/issues/user_sorts_issues_spec.rb
+++ b/spec/features/issues/user_sorts_issues_spec.rb
@@ -3,12 +3,17 @@
require "spec_helper"
describe "User sorts issues" do
- set(:user) { create(:user) }
- set(:group) { create(:group) }
- set(:project) { create(:project_empty_repo, :public, group: group) }
- set(:issue1) { create(:issue, project: project) }
- set(:issue2) { create(:issue, project: project) }
- set(:issue3) { create(:issue, project: project) }
+ include SortingHelper
+ include IssueHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+ 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
create_list(:award_emoji, 2, :upvote, awardable: issue1)
@@ -62,4 +67,174 @@ describe "User sorts issues" do
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
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
deleted file mode 100644
index ef9daf70b0c..00000000000
--- a/spec/features/issues_spec.rb
+++ /dev/null
@@ -1,828 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe 'Issues' do
- include DropzoneHelper
- include IssueHelpers
- include SortingHelper
-
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
-
- 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)
- user2 = create(:user)
-
- project.add_developer(user)
- project.add_developer(user2)
- 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
-
- describe 'Edit issue' do
- let!(:issue) do
- create(:issue,
- author: user,
- assignees: [user],
- project: project)
- end
-
- before do
- visit edit_project_issue_path(project, issue)
- find('.js-zen-enter').click
- end
-
- it 'opens new issue popup' do
- expect(page).to have_content("Issue ##{issue.iid}")
- end
- end
-
- describe 'Editing issue assignee' do
- let!(:issue) do
- create(:issue,
- author: user,
- assignees: [user],
- project: project)
- end
-
- it 'allows user to select unassigned', :js 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
-
- expect(issue.reload.assignees).to be_empty
- end
- end
-
- describe 'due date', :js do
- context 'on new form' do
- before do
- visit new_project_issue_path(project)
- end
-
- 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 'on edit form' do
- let(:issue) { create(:issue, author: user, project: project, due_date: Date.today.at_beginning_of_month.to_s) }
-
- before do
- visit edit_project_issue_path(project, issue)
- end
-
- it 'saves with due date' do
- date = Date.today.at_beginning_of_month
-
- expect(find('#issuable-due-date').value).to eq date.to_s
-
- date = date.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'
-
- 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
-
- describe 'Issue info' do
- 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
-
- describe 'Filter issue' do
- before do
- %w(foobar barbaz gitlab).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'
- expect(page).not_to have_content 'gitlab'
- 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'
- expect(page).to have_content 'gitlab'
- end
- end
-
- describe 'filter issue' do
- titles = %w[foo bar baz]
- titles.each_with_index do |title, index|
- let!(title.to_sym) do
- create(:issue, title: title,
- project: project,
- created_at: Time.now - (index * 60))
- end
- end
- let(:newer_due_milestone) { create(:milestone, project: project, due_date: '2013-12-11') }
- let(:later_due_milestone) { create(:milestone, project: project, due_date: '2013-12-12') }
-
- 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
- baz.updated_at = Time.now + 100
- baz.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
- foo.update(due_date: 1.day.from_now)
- bar.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
- bar.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: foo)
- end
-
- it 'sorts by least recently due date by excluding nil due dates' do
- bar.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
- foo.update(due_date: 1.day.from_now)
- bar.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
- foo.update(due_date: Date.today.beginning_of_week + 2.days)
- bar.update(due_date: Date.today.end_of_week)
- baz.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
- foo.update(due_date: Date.today.beginning_of_month + 2.days)
- bar.update(due_date: Date.today.end_of_month)
- baz.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
- foo.update(due_date: Date.today + 2.days)
- bar.update(due_date: Date.today + 20.days)
- baz.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
- foo.update(due_date: Date.today - 4.weeks)
- bar.update(due_date: (Date.today + 2.months).beginning_of_month)
- baz.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
- foo.milestone = newer_due_milestone
- foo.save
- bar.milestone = later_due_milestone
- bar.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
- foo.assignees << user2
- foo.save
- bar.assignees << user2
- bar.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
-
- describe 'when I want to reset my incoming email token' do
- let(:project1) { create(:project, namespace: user.namespace) }
- let!(:issue) { create(:issue, project: project1) }
-
- before do
- stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
- project1.add_maintainer(user)
- visit namespace_project_issues_path(user.namespace, project1)
- 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 = project1.new_issuable_address(user.reload, 'issue')
- expect(page).to have_field(
- 'issuable_email',
- with: new_token
- )
- end
- end
-
- describe 'update labels from issue#show', :js do
- let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
- let!(:label) { create(:label, project: project) }
-
- before do
- visit project_issue_path(project, issue)
- end
-
- it 'will not send ajax request when no data is changed' do
- page.within '.labels' do
- click_link 'Edit'
-
- find('.dropdown-menu-close', match: :first).click
-
- expect(page).not_to have_selector('.block-loading')
- end
- end
- end
-
- describe 'update assignee from issue#show' do
- let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
-
- context 'by authorized user' do
- it 'allows user to select unassigned', :js 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'
- end
-
- wait_for_requests
-
- expect(issue.reload.assignees).to be_empty
- end
-
- it 'allows user to select an assignee', :js 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', :js do
- issue2 = create(:issue, project: project, author: user)
-
- visit project_issue_path(project, issue2)
-
- def close_dropdown_menu_if_visible
- find('.dropdown-menu-toggle', visible: :all).tap do |toggle|
- toggle.click if toggle.visible?
- end
- end
-
- 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', :js 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 from issue#show' do
- let!(:issue) { create(:issue, project: project, author: user) }
- let!(:milestone) { create(:milestone, project: project) }
-
- context 'by authorized user' do
- it 'allows user to select unassigned', :js 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
-
- expect(issue.reload.milestone).to be_nil
- end
-
- it 'allows user to de-select milestone', :js 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', :js do
- sign_out(:user)
- sign_in(guest)
-
- visit project_issue_path(project, issue)
- expect(page).to have_content milestone.title
- end
- end
- end
-
- describe 'new issue' do
- let!(:issue) { create(:issue, project: project) }
-
- context 'by unauthenticated user' do
- before do
- sign_out(:user)
- end
-
- it 'redirects to signin then back to new issue after signin' do
- 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
-
- 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
-
- 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
- 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
- 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
-
- describe 'due date' do
- context 'update due on issue#show', :js do
- let(:issue) { create(:issue, project: project, author: user, assignees: [user]) }
-
- before do
- visit project_issue_path(project, issue)
- end
-
- 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
-
- describe 'title issue#show', :js do
- it 'updates the title', :js 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', :js 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
-end
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index cab86f3fd94..cd62bab412a 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -50,7 +50,7 @@ describe 'Merge request > User sees versions', :js do
expect(page).to have_content 'latest version'
end
- expect(page).to have_content '8 Files'
+ expect(page).to have_content '8 files'
end
it_behaves_like 'allows commenting',
@@ -84,7 +84,7 @@ describe 'Merge request > User sees versions', :js do
end
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(
old_path: ".gitmodules",
@@ -128,12 +128,10 @@ describe 'Merge request > User sees versions', :js do
diff_id: merge_request_diff3.id,
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')
- .ancestor('.diff-stats-group').text
- deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-deletion')
- .ancestor('.diff-stats-group').text
+ additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-addition-line').text
+ deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-deletion-line').text
expect(additions_content).to eq '15'
expect(deletions_content).to eq '6'
@@ -156,12 +154,10 @@ describe 'Merge request > User sees versions', :js do
end
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')
- .ancestor('.diff-stats-group').text
- deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group svg.ic-file-deletion')
- .ancestor('.diff-stats-group').text
+ additions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-addition-line').text
+ deletions_content = page.find('.diff-stats.is-compare-versions-header .diff-stats-group .js-file-deletion-line').text
- expect(page).to have_content '4 Files'
+ expect(page).to have_content '4 files'
expect(additions_content).to eq '15'
expect(deletions_content).to eq '6'
end
@@ -171,7 +167,7 @@ describe 'Merge request > User sees versions', :js do
page.within '.mr-version-dropdown' do
expect(page).to have_content 'latest version'
end
- expect(page).to have_content '8 Files'
+ expect(page).to have_content '8 files'
end
it_behaves_like 'allows commenting',
@@ -197,7 +193,7 @@ describe 'Merge request > User sees versions', :js do
find('.btn-default').click
click_link 'version 1'
end
- expect(page).to have_content '0 Files'
+ expect(page).to have_content '0 files'
end
end
@@ -223,7 +219,7 @@ describe 'Merge request > User sees versions', :js do
expect(page).to have_content 'version 1'
end
- expect(page).to have_content '0 Files'
+ expect(page).to have_content '0 files'
end
end
diff --git a/spec/features/projects/settings/registry_settings_spec.rb b/spec/features/projects/settings/registry_settings_spec.rb
new file mode 100644
index 00000000000..86da866a927
--- /dev/null
+++ b/spec/features/projects/settings/registry_settings_spec.rb
@@ -0,0 +1,37 @@
+# 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
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index 99285011405..7e0ee861b18 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -46,8 +46,6 @@ describe 'Multi-file editor new directory', :js do
find('.js-ide-commit-mode').click
- click_button 'Stage'
-
fill_in('commit-message', with: 'commit message ide')
find(:css, ".js-ide-commit-new-mr input").set(false)
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index 780575a5975..eba33168006 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -36,8 +36,6 @@ describe 'Multi-file editor new file', :js do
find('.js-ide-commit-mode').click
- click_button 'Stage'
-
fill_in('commit-message', with: 'commit message ide')
find(:css, ".js-ide-commit-new-mr input").set(false)
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index 7f47677f56c..7648c39976c 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -50,8 +50,7 @@ describe('CompareVersions', () => {
expect(treeListBtn.exists()).toBe(true);
expect(treeListBtn.attributes('title')).toBe('Hide file browser');
- expect(treeListBtn.findAll(Icon).length).not.toBe(0);
- expect(treeListBtn.find(Icon).props('name')).toBe('collapse-left');
+ expect(treeListBtn.find(Icon).props('name')).toBe('file-tree');
});
it('should render comparison dropdowns with correct values', () => {
diff --git a/spec/frontend/diffs/components/diff_stats_spec.js b/spec/frontend/diffs/components/diff_stats_spec.js
index 4482abf18c1..aa5c7f6278a 100644
--- a/spec/frontend/diffs/components/diff_stats_spec.js
+++ b/spec/frontend/diffs/components/diff_stats_spec.js
@@ -1,5 +1,4 @@
import { shallowMount } from '@vue/test-utils';
-import Icon from '~/vue_shared/components/icon.vue';
import DiffStats from '~/diffs/components/diff_stats.vue';
describe('diff_stats', () => {
@@ -24,18 +23,11 @@ describe('diff_stats', () => {
},
});
- const findIcon = name =>
- wrapper
- .findAll(Icon)
- .filter(c => c.attributes('name') === name)
- .at(0).element.parentNode;
+ const findFileLine = name => wrapper.find(name);
+ const additions = findFileLine('.js-file-addition-line');
+ const deletions = findFileLine('.js-file-deletion-line');
- const additions = findIcon('file-addition');
- const deletions = findIcon('file-deletion');
- const filesChanged = findIcon('doc-code');
-
- expect(additions.textContent).toContain('100');
- expect(deletions.textContent).toContain('200');
- expect(filesChanged.textContent).toContain('300');
+ expect(additions.text()).toBe('100');
+ expect(deletions.text()).toBe('200');
});
});
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index e7b34aa3e7a..a8e48f0b85e 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -514,6 +514,8 @@ describe('IDE store file actions', () => {
describe('changeFileContent', () => {
let tmpFile;
+ const callAction = (content = 'content\n') =>
+ store.dispatch('changeFileContent', { path: tmpFile.path, content });
beforeEach(() => {
tmpFile = file('tmpFile');
@@ -523,11 +525,7 @@ describe('IDE store file actions', () => {
});
it('updates file content', done => {
- store
- .dispatch('changeFileContent', {
- path: tmpFile.path,
- content: 'content\n',
- })
+ callAction()
.then(() => {
expect(tmpFile.content).toBe('content\n');
@@ -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 => {
- store
- .dispatch('changeFileContent', {
- path: tmpFile.path,
- content: 'content',
- })
+ callAction('content')
.then(() => {
expect(tmpFile.content).toBe('content\n');
@@ -551,11 +545,7 @@ describe('IDE store file actions', () => {
});
it('adds file into changedFiles array', done => {
- store
- .dispatch('changeFileContent', {
- path: tmpFile.path,
- content: 'content',
- })
+ callAction()
.then(() => {
expect(store.state.changedFiles.length).toBe(1);
@@ -564,7 +554,7 @@ describe('IDE store file actions', () => {
.catch(done.fail);
});
- it('adds file once into changedFiles array', done => {
+ it('adds file not more than once into changedFiles array', done => {
store
.dispatch('changeFileContent', {
path: tmpFile.path,
@@ -604,6 +594,52 @@ describe('IDE store file actions', () => {
.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 => {
store
.dispatch('changeFileContent', {
diff --git a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
index bef4674bd8b..d26df308b97 100644
--- a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
+++ b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap
@@ -106,7 +106,7 @@ exports[`Settings Form renders 1`] = `
<glformgroup-stub
id="expiration-policy-latest-group"
- label="Expiration latest:"
+ label="Number of tags to retain:"
label-align="right"
label-cols="3"
label-for="expiration-policy-latest"
@@ -136,7 +136,7 @@ exports[`Settings Form renders 1`] = `
<glformgroup-stub
id="expiration-policy-name-matching-group"
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-cols="3"
label-for="expiration-policy-name-matching"
diff --git a/spec/frontend/registry/settings/store/actions_spec.js b/spec/frontend/registry/settings/store/actions_spec.js
index 71c815cd19c..80fb800ac3a 100644
--- a/spec/frontend/registry/settings/store/actions_spec.js
+++ b/spec/frontend/registry/settings/store/actions_spec.js
@@ -44,7 +44,9 @@ describe('Actions Registry Store', () => {
};
const payload = {
- tag_expiration_policies: 'foo',
+ data: {
+ container_expiration_policy: 'foo',
+ },
};
it('should fetch the data from the API', done => {
@@ -56,7 +58,7 @@ describe('Actions Registry Store', () => {
[],
[
{ type: 'toggleLoading' },
- { type: 'receiveSettingsSuccess', payload: payload.tag_expiration_policies },
+ { type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy },
{ type: 'toggleLoading' },
],
done,
@@ -83,7 +85,9 @@ describe('Actions Registry Store', () => {
};
const payload = {
- tag_expiration_policies: 'foo',
+ data: {
+ tag_expiration_policies: 'foo',
+ },
};
it('should fetch the data from the API', done => {
@@ -95,11 +99,11 @@ describe('Actions Registry Store', () => {
[],
[
{ type: 'toggleLoading' },
- { type: 'receiveSettingsSuccess', payload: payload.tag_expiration_policies },
+ { type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy },
{ type: 'toggleLoading' },
],
() => {
- expect(createFlash).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE);
+ expect(createFlash).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, 'success');
done();
},
);
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index 30f701ed77a..d2576ec26b7 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -6,7 +6,7 @@ import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link
let vm;
function createCommitData(data = {}) {
- return {
+ const defaultData = {
sha: '123456789',
title: 'Commit title',
message: 'Commit message',
@@ -26,8 +26,8 @@ function createCommitData(data = {}) {
group: {},
},
},
- ...data,
};
+ return Object.assign(defaultData, data);
}
function factory(commit = createCommitData(), loading = false) {
@@ -46,6 +46,8 @@ function factory(commit = createCommitData(), loading = false) {
vm.vm.$apollo.queries.commit.loading = loading;
}
+const emptyMessageClass = 'font-italic';
+
describe('Repository last commit component', () => {
afterEach(() => {
vm.destroy();
@@ -135,4 +137,12 @@ describe('Repository last commit component', () => {
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);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/changed_file_icon_spec.js b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
index 9197cb8bc00..3a52941a06e 100644
--- a/spec/frontend/vue_shared/components/changed_file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/changed_file_icon_spec.js
@@ -57,10 +57,10 @@ describe('Changed file icon', () => {
describe.each`
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'}
- ${changedAndStagedFile()} | ${'file-modified'} | ${'Unstaged and staged modification'} | ${'with file changed and staged'}
- ${newFile()} | ${'file-addition'} | ${'Unstaged addition'} | ${'with file new'}
+ ${changedAndStagedFile()} | ${'file-modified-solid'} | ${'Unstaged and staged modification'} | ${'with file changed and staged'}
+ ${newFile()} | ${'file-addition-solid'} | ${'Unstaged addition'} | ${'with file new'}
`('$desc', ({ file, iconName, tooltipText }) => {
beforeEach(() => {
factory({ file });
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index f415fb05b5b..a775c69335e 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -357,10 +357,10 @@ describe MarkupHelper do
describe '#markup_unsafe' do
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(:text) { 'Noël' }
- let(:project_base) { build(:project, :repository) }
- let(:context) { { project: project_base } }
context 'when text is missing' do
let(:text) { nil }
@@ -383,12 +383,21 @@ describe MarkupHelper do
context 'when renderer returns an error' do
before do
- allow(Banzai).to receive(:render).and_raise("An error")
+ allow(Banzai).to receive(:render).and_raise(StandardError, "An error")
end
it 'returns html (rendered by ActionView:TextHelper)' do
is_expected.to eq('<p>Noël</p>')
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
diff --git a/spec/javascripts/ide/components/commit_sidebar/form_spec.js b/spec/javascripts/ide/components/commit_sidebar/form_spec.js
index fdbabf84e25..2ee0b94582c 100644
--- a/spec/javascripts/ide/components/commit_sidebar/form_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/form_spec.js
@@ -33,6 +33,12 @@ describe('IDE commit form', () => {
});
describe('compact', () => {
+ beforeEach(done => {
+ vm.isCompact = true;
+
+ vm.$nextTick(done);
+ });
+
it('renders commit button in compact mode', () => {
expect(vm.$el.querySelector('.btn-primary')).not.toBeNull();
expect(vm.$el.querySelector('.btn-primary').textContent).toContain('Commit');
@@ -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.$nextTick(() => {
@@ -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', () => {
it('hidden when commitMessage is empty', () => {
expect(vm.$el.querySelector('.btn-default').textContent).toContain('Collapse');
diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js
index 3b52f279bf2..7466ed5468b 100644
--- a/spec/javascripts/ide/components/repo_tab_spec.js
+++ b/spec/javascripts/ide/components/repo_tab_spec.js
@@ -93,13 +93,13 @@ describe('RepoTab', () => {
Vue.nextTick()
.then(() => {
- expect(vm.$el.querySelector('.file-modified')).toBeNull();
+ expect(vm.$el.querySelector('.file-modified-solid')).toBeNull();
vm.$el.dispatchEvent(new Event('mouseout'));
})
.then(Vue.nextTick)
.then(() => {
- expect(vm.$el.querySelector('.file-modified')).not.toBeNull();
+ expect(vm.$el.querySelector('.file-modified-solid')).not.toBeNull();
done();
})
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 9c24f20ca9c..d582462d542 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -225,6 +225,35 @@ describe('Multi-file store actions', () => {
.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 => {
const name = 'test';
@@ -255,41 +284,25 @@ describe('Multi-file store actions', () => {
type: 'blob',
})
.then(() => {
- const f = store.state.entries[name];
-
- expect(store.state.changedFiles.length).toBe(1);
- expect(store.state.changedFiles[0].name).toBe(f.name);
+ expect(store.state.changedFiles).toEqual([
+ jasmine.objectContaining({ name, tempFile: true }),
+ ]);
done();
})
.catch(done.fail);
});
- it('sets tmp file as active', done => {
- testAction(
- createTempEntry,
- {
- 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,
+ it('sets tmp file as active', () => {
+ const dispatch = jasmine.createSpy();
+ const commit = jasmine.createSpy();
+
+ createTempEntry(
+ { state: store.state, getters: store.getters, dispatch, commit },
+ { name: 'test', branchId: 'mybranch', type: 'blob' },
);
+
+ expect(dispatch).toHaveBeenCalledWith('setFileActive', 'test');
});
it('creates flash message if file already exists', done => {
@@ -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 => {
testAction(
renameEntry,
@@ -819,12 +859,12 @@ describe('Multi-file store actions', () => {
payload: 'renamed',
},
],
- [{ type: 'burstUnusedSeal' }, { type: 'triggerFilesChange' }],
+ jasmine.any(Object),
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(
renameEntry,
{ path: 'renamed', name: 'orig' },
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 9dea74f6345..058afddd73f 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -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') }
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
context 'with when/rules conflict' do
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 7ec655eb113..c2fc228d34a 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -57,7 +57,7 @@ describe Gitlab::Git::Commit, :seed_helper do
it { expect(@commit.different_committer?).to be_truthy }
it { expect(@commit.parents).to eq(@gitlab_parents) }
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
# Erase the new commit so other tests get the original repo
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index a9d79454dd5..782d1ac4552 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -277,7 +277,7 @@ describe Commit do
describe '#title' do
it "returns no_commit_message when safe_message is blank" do
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
it 'truncates a message without a newline at natural break to 80 characters' do
@@ -308,7 +308,7 @@ eos
describe '#full_title' do
it "returns no_commit_message when safe_message is blank" do
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
it "returns entire message if there is no newline" do
@@ -330,7 +330,7 @@ eos
it 'returns no_commit_message when safe_message is blank' do
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
it 'returns description of commit message if title less than 100 characters' do
diff --git a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
index 0e8fe4987b9..f80a3401134 100644
--- a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
+++ b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
@@ -52,8 +52,8 @@ describe 'Mark snippet as spam' do
end
it 'marks snippet as spam' do
- expect_next_instance_of(SpamService) do |instance|
- expect(instance).to receive(:mark_as_spam!)
+ expect_next_instance_of(Spam::MarkAsSpamService) do |instance|
+ expect(instance).to receive(:execute)
end
post_graphql_mutation(mutation, current_user: current_user)
diff --git a/spec/services/spam/mark_as_spam_service_spec.rb b/spec/services/spam/mark_as_spam_service_spec.rb
new file mode 100644
index 00000000000..cba9d6a39cb
--- /dev/null
+++ b/spec/services/spam/mark_as_spam_service_spec.rb
@@ -0,0 +1,52 @@
+# 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