Commit c158fa8d authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent b806264d
......@@ -374,7 +374,7 @@ export default {
<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"
......
<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"
......
......@@ -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">
......
<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>
......@@ -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"
......
......@@ -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>
......
......@@ -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;
......
......@@ -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');
}
......
......@@ -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);
}
......
......@@ -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>
......@@ -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"
>
......
......@@ -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'));
......
......@@ -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
......
......@@ -6,6 +6,7 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
sha
title
description
message
webUrl
authoredDate
authorName
......
......@@ -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>
......
<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"
......
......@@ -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;
}
......
......@@ -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;
}
......
......@@ -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.')
......
......@@ -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
......
......@@ -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
......
......@@ -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)
......
......@@ -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
......
# 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
......
# frozen_string_literal: true
module AkismetMethods
def spammable_owner
@user ||= User.find(spammable_owner_id)
end
def spammable_owner_id
@owner_id ||=
if spammable.respond_to?(:author_id)
spammable.author_id
elsif spammable.respond_to?(:creator_id)
spammable.creator_id
end
end
def akismet
@akismet ||= AkismetService.new(
spammable_owner.name,
spammable_owner.email,
spammable.spammable_text,
options
)
end
end
......@@ -11,3 +11,5 @@ module Notes
end
end
end
Notes::DestroyService.prepend_if_ee('EE::Notes::DestroyService')
# frozen_string_literal: true
module Spam
class MarkAsSpamService
include ::AkismetMethods
attr_accessor :spammable, :options
def initialize(spammable:)
@spammable = spammable
@options = {}
@options[:ip_address] = @spammable.ip_address
@options[:user_agent] = @spammable.user_agent
end
def execute
return unless spammable.submittable_as_spam?
return unless akismet.submit_spam
spammable.user_agent_detail.update_attribute(:submitted, true)
end
end
end
# frozen_string_literal: true
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
......
......@@ -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"
......
......@@ -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
......
......@@ -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")
......
---
title: Restyle changes header & file tree
merge_request: 22364
author:
type: changed
---
title: 'Geo: Handle repositories in Docker Registry with no tags gracefully'
merge_request: 23022
author:
type: fixed
---
title: Fix group issue list and group issue board filters not showing ancestor group
milestones
merge_request: 23038
author:
type: fixed
---
title: Exposes tiller.log as artifact in Managed-Cluster-Applications GitLab CI template
merge_request: 22940
author:
type: changed
---
title: Remove storage_version column from snippets
merge_request: 23004
author:
type: changed
---
title: Stage all changes by default in Web IDE
merge_request: 21067
author:
type: added
---
title: Updated no commit verbiage
merge_request: 22765
author:
type: other
---
title: Enable redis HSET diff caching by default
merge_request: 23105
author:
type: performance
---
title: Fix Error 500 in parsing invalid CI needs and dependencies
merge_request: 22567
author:
type: fixed
# frozen_string_literal: true
class RemoveStorageVersionColumnFromSnippets < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
return unless column_exists?(:snippets, :storage_version)
remove_column :snippets, :storage_version
end
def down
add_column_with_default( # rubocop:disable Migration/AddColumnWithDefault
:snippets,
:storage_version,
:integer,
default: 2,
allow_null: false
)
end
end
......@@ -10,7 +10,7 @@
#
# 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"
......
......@@ -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
......
......@@ -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:
......
......@@ -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
......
......@@ -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
......
......@@ -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.
......
......@@ -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
......
......@@ -14,3 +14,7 @@ apply:
only:
refs:
- master
artifacts:
when: on_failure
paths:
- tiller.log
......@@ -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/
......
......@@ -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)
......
......@@ -254,7 +254,7 @@ module Gitlab
end
def no_commit_message
"--no commit message"
"No commit message"
end
def to_hash
......
......@@ -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 ""
......
......@@ -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.
......
......@@ -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
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Issues > User creates issue by email' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
before do
sign_in(user)
project.add_developer(user)
end
describe 'new issue by email' do
shared_examples 'show the email in the modal' do
let(:issue) { create(:issue, project: project) }
before do
project.issues << issue
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
visit project_issues_path(project)
click_button('Email a new issue')
end
it 'click the button to show modal for the new email' do
page.within '#issuable-email-modal' do
email = project.new_issuable_address(user, 'issue')
expect(page).to have_selector("input[value='#{email}']")
end
end
end
context 'with existing issues' do
let!(:issue) { create(:issue, project: project, author: user) }
it_behaves_like 'show the email in the modal'
end
context 'without existing issues' do
it_behaves_like 'show the email in the modal'
end
end
end
......@@ -3,8 +3,32 @@
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
......
......@@ -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
# frozen_string_literal: true
require 'spec_helper'
describe 'User filters issues' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project_empty_repo, :public) }
before do
%w[foobar barbaz].each do |title|
create(:issue,
author: user,
assignees: [user],
project: project,
title: title)
end
@issue = Issue.find_by(title: 'foobar')
@issue.milestone = create(:milestone, project: project)
@issue.assignees = []
@issue.save
end
let(:issue) { @issue }
it 'allows filtering by issues with no specified assignee' do
visit project_issues_path(project, assignee_id: IssuableFinder::FILTER_NONE)
expect(page).to have_content 'foobar'
expect(page).not_to have_content 'barbaz'
end
it 'allows filtering by a specified assignee' do
visit project_issues_path(project, assignee_id: user.id)
expect(page).not_to have_content 'foobar'
expect(page).to have_content 'barbaz'
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Issues > User resets their incoming email token' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, namespace: user.namespace) }
let_it_be(:issue) { create(:issue, project: project) }
before do
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
project.add_maintainer(user)
sign_in(user)
visit namespace_project_issues_path(user.namespace, project)
end
it 'changes incoming email address token', :js do
find('.issuable-email-modal-btn').click
previous_token = find('input#issuable_email').value
find('.incoming-email-token-reset').click
wait_for_requests
expect(page).to have_no_field('issuable_email', with: previous_token)
new_token = project.new_issuable_address(user.reload, 'issue')
expect(page).to have_field(
'issuable_email',
with: new_token
)
end
end
......@@ -3,7 +3,7 @@
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
# frozen_string_literal: true
require 'spec_helper'
describe 'Issues > User sees empty state' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { project.creator }
shared_examples_for 'empty state with filters' do
it 'user sees empty state with filters' do
create(:issue, author: user, project: project)
visit project_issues_path(project, milestone_title: "1.0")
expect(page).to have_content('Sorry, your filter produced no results')
expect(page).to have_content('To widen your search, change or remove filters above')
end
end
describe 'while user is signed out' do
describe 'empty state' do
it 'user sees empty state' do
visit project_issues_path(project)
expect(page).to have_content('Register / Sign In')
expect(page).to have_content('The Issue Tracker is the place to add things that need to be improved or solved in a project.')
expect(page).to have_content('You can register or sign in to create issues for this project.')
end
it_behaves_like 'empty state with filters'
end
end
describe 'while user is signed in' do
before do
sign_in(user)
end
describe 'empty state' do
it 'user sees empty state' do
visit project_issues_path(project)
expect(page).to have_content('The Issue Tracker is the place to add things that need to be improved or solved in a project')
expect(page).to have_content('Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable.')
expect(page).to have_content('New issue')
end
it_behaves_like 'empty state with filters'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Issues > User sees live update', :js do
let_it_be(:project) { create(:project, :public) }
let_it_be(:user) { project.creator }
before do
sign_in(user)
end
describe 'title issue#show' do
it 'updates the title' do
issue = create(:issue, author: user, assignees: [user], project: project, title: 'new title')
visit project_issue_path(project, issue)
expect(page).to have_text("new title")
issue.update(title: "updated title")
wait_for_requests
expect(page).to have_text("updated title")
end
end
describe 'confidential issue#show' do
it 'shows confidential sibebar information as confidential and can be turned off' do
issue = create(:issue, :confidential, project: project)
visit project_issue_path(project, issue)
expect(page).to have_css('.issuable-note-warning')
expect(find('.issuable-sidebar-item.confidentiality')).to have_css('.is-active')
expect(find('.issuable-sidebar-item.confidentiality')).not_to have_css('.not-active')
find('.confidential-edit').click
expect(page).to have_css('.sidebar-item-warning-message')
within('.sidebar-item-warning-message') do
find('.btn-close').click
end
wait_for_requests
visit project_issue_path(project, issue)
expect(page).not_to have_css('.is-active')
end
end
end
......@@ -3,12 +3,17 @@
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
This diff is collapsed.
......@@ -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
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Project > Settings > CI/CD > Container registry tag expiration policy', :js do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
context 'as owner' do
before do
sign_in(user)
visit project_settings_ci_cd_path(project)
end
it 'section is available' do
settings_block = find('#js-registry-policies')
expect(settings_block).to have_text 'Container Registry tag expiration policy'
end
it 'Save expiration policy submit the form', :js do
within '#js-registry-policies' do
within '.card-body' do
click_button(class: 'gl-toggle')
select('7 days until tags are automatically removed', from: 'expiration-policy-interval')
select('Every day', from: 'expiration-policy-schedule')
select('50 tags per image name', from: 'expiration-policy-latest')
fill_in('expiration-policy-name-matching', with: '*-production')
end
submit_button = find('.card-footer .btn.btn-success')
expect(submit_button).not_to be_disabled
submit_button.click
end
flash_text = find('.flash-text')
expect(flash_text).to have_content('Expiration policy successfully saved.')
end
end
end
......@@ -46,8 +46,6 @@ describe 'Multi-file editor new directory', :js do
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)
......
......@@ -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)
......
......@@ -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', () => {
......
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');
});
});
......@@ -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', {
......
......@@ -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"
......
......@@ -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();
},
);
......
......@@ -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);
});
});
});
......@@ -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 });
......
......@@ -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
......
......@@ -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');
......
......@@ -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();
})
......
......@@ -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' },
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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)
......
# frozen_string_literal: true
require 'spec_helper'
describe Spam::MarkAsSpamService do
let(:user_agent_detail) { build(:user_agent_detail) }
let(:spammable) { build(:issue, user_agent_detail: user_agent_detail) }
let(:fake_akismet_service) { double(:akismet_service, submit_spam: true) }
subject { described_class.new(spammable: spammable) }
describe '#execute' do
before do
allow(subject).to receive(:akismet).and_return(fake_akismet_service)
end
context 'when the spammable object is not submittable' do
before do
allow(spammable).to receive(:submittable_as_spam?).and_return false
end
it 'does not submit as spam' do
expect(subject.execute).to be_falsey
end
end
context 'spam is submitted successfully' do
before do
allow(spammable).to receive(:submittable_as_spam?).and_return true
allow(fake_akismet_service).to receive(:submit_spam).and_return true
end
it 'submits as spam' do
expect(subject.execute).to be_truthy
end
it "updates the spammable object's user agent detail as being submitted as spam" do
expect(user_agent_detail).to receive(:update_attribute)
subject.execute
end
context 'when Akismet does not consider it spam' do
it 'does not update the spammable object as spam' do
allow(fake_akismet_service).to receive(:submit_spam).and_return false
expect(subject.execute).to be_falsey
end
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment