Commit fe4b55d9 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee-2018-04-18' into 'master'

CE upstream - 2018-04-18 15:24 UTC

Closes gitaly#1115

See merge request gitlab-org/gitlab-ee!5408
parents e83aa920 4d53b3d6
...@@ -36,6 +36,7 @@ Set the title to: `[Security] Description of the original issue` ...@@ -36,6 +36,7 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details) - [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details) - [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
- [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details) - [ ] Add Yes/No and further details if needed to the migration and settings columns in the [details section](#details)
- [ ] Add the nickname of the external user who found the issue (and/or HackerOne profile) to the Thanks row in the [details section](#details)
### Summary ### Summary
#### Links #### Links
...@@ -61,8 +62,9 @@ Set the title to: `[Security] Description of the original issue` ...@@ -61,8 +62,9 @@ Set the title to: `[Security] Description of the original issue`
| Upgrade notes | | | | Upgrade notes | | |
| GitLab Settings updated | Yes/No| | | GitLab Settings updated | Yes/No| |
| Migration required | Yes/No | | | Migration required | Yes/No | |
| Thanks | | |
[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/process.md [security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
[RM list]: https://about.gitlab.com/release-managers/ [RM list]: https://about.gitlab.com/release-managers/
/label ~security /label ~security
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { sprintf, __ } from '~/locale'; import { sprintf, __ } from '~/locale';
import * as consts from '../../stores/modules/commit/constants'; import * as consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue'; import RadioGroup from './radio_group.vue';
export default { export default {
components: { components: {
RadioGroup, RadioGroup,
},
computed: {
...mapState(['currentBranchId']),
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
{ branchName: `<strong class="monospace">${this.currentBranchId}</strong>` },
false,
);
}, },
computed: { },
...mapState([ commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
'currentBranchId', commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
]), commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
newMergeRequestHelpText() { };
return sprintf(
__('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
{ branchName: this.currentBranchId },
);
},
commitToCurrentBranchText() {
return sprintf(
__('Commit to %{branchName} branch'),
{ branchName: `<strong>${this.currentBranchId}</strong>` },
false,
);
},
commitToNewBranchText() {
return sprintf(
__('Creates a new branch from %{branchName}'),
{ branchName: this.currentBranchId },
);
},
},
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR,
};
</script> </script>
<template> <template>
...@@ -53,13 +39,11 @@ ...@@ -53,13 +39,11 @@
:value="$options.commitToNewBranch" :value="$options.commitToNewBranch"
:label="__('Create a new branch')" :label="__('Create a new branch')"
:show-input="true" :show-input="true"
:help-text="commitToNewBranchText"
/> />
<radio-group <radio-group
:value="$options.commitToNewBranchMR" :value="$options.commitToNewBranchMR"
:label="__('Create a new branch and merge request')" :label="__('Create a new branch and merge request')"
:show-input="true" :show-input="true"
:help-text="newMergeRequestHelpText"
/> />
</div> </div>
</template> </template>
<script>
import { __, sprintf } from '../../../locale';
import Icon from '../../../vue_shared/components/icon.vue';
import popover from '../../../vue_shared/directives/popover';
import { MAX_TITLE_LENGTH, MAX_BODY_LENGTH } from '../../constants';
export default {
directives: {
popover,
},
components: {
Icon,
},
props: {
text: {
type: String,
required: true,
},
},
data() {
return {
scrollTop: 0,
isFocused: false,
};
},
computed: {
allLines() {
return this.text.split('\n').map((line, i) => ({
text: line.substr(0, this.getLineLength(i)) || ' ',
highlightedText: line.substr(this.getLineLength(i)),
}));
},
},
methods: {
handleScroll() {
if (this.$refs.textarea) {
this.$nextTick(() => {
this.scrollTop = this.$refs.textarea.scrollTop;
});
}
},
getLineLength(i) {
return i === 0 ? MAX_TITLE_LENGTH : MAX_BODY_LENGTH;
},
onInput(e) {
this.$emit('input', e.target.value);
},
updateIsFocused(isFocused) {
this.isFocused = isFocused;
},
},
popoverOptions: {
trigger: 'hover',
placement: 'top',
content: sprintf(
__(`
The character highligher helps you keep the subject line to %{titleLength} characters
and wrap the body at %{bodyLength} so they are readable in git.
`),
{ titleLength: MAX_TITLE_LENGTH, bodyLength: MAX_BODY_LENGTH },
),
},
};
</script>
<template>
<fieldset class="common-note-form ide-commit-message-field">
<div
class="md-area"
:class="{
'is-focused': isFocused
}"
>
<div
v-once
class="md-header"
>
<ul class="nav-links">
<li>
{{ __('Commit Message') }}
<span
v-popover="$options.popoverOptions"
class="help-block prepend-left-10"
>
<icon
name="question"
/>
</span>
</li>
</ul>
</div>
<div class="ide-commit-message-textarea-container">
<div class="ide-commit-message-highlights-container">
<div
class="note-textarea highlights monospace"
:style="{
transform: `translate3d(0, ${-scrollTop}px, 0)`
}"
>
<div
v-for="(line, index) in allLines"
:key="index"
>
<span
v-text="line.text"
>
</span><mark
v-show="line.highlightedText"
v-text="line.highlightedText"
>
</mark>
</div>
</div>
</div>
<textarea
class="note-textarea ide-commit-message-textarea"
name="commit-message"
:placeholder="__('Write a commit message...')"
:value="text"
@scroll="handleScroll"
@input="onInput"
@focus="updateIsFocused(true)"
@blur="updateIsFocused(false)"
ref="textarea"
>
</textarea>
</div>
</div>
</fieldset>
</template>
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
export default { export default {
directives: { directives: {
tooltip, tooltip,
},
props: {
value: {
type: String,
required: true,
}, },
props: { label: {
value: { type: String,
type: String, required: false,
required: true, default: null,
},
label: {
type: String,
required: false,
default: null,
},
checked: {
type: Boolean,
required: false,
default: false,
},
showInput: {
type: Boolean,
required: false,
default: false,
},
helpText: {
type: String,
required: false,
default: null,
},
}, },
computed: { checked: {
...mapState('commit', [ type: Boolean,
'commitAction', required: false,
]), default: false,
...mapGetters('commit', [
'newBranchName',
]),
}, },
methods: { showInput: {
...mapActions('commit', [ type: Boolean,
'updateCommitAction', required: false,
'updateBranchName', default: false,
]),
}, },
}; },
computed: {
...mapState('commit', ['commitAction']),
...mapGetters('commit', ['newBranchName']),
},
methods: {
...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
},
};
</script> </script>
<template> <template>
...@@ -65,18 +53,6 @@ ...@@ -65,18 +53,6 @@
{{ label }} {{ label }}
</template> </template>
<slot v-else></slot> <slot v-else></slot>
<span
v-if="helpText"
v-tooltip
class="help-block inline"
:title="helpText"
>
<i
class="fa fa-question-circle"
aria-hidden="true"
>
</i>
</span>
</span> </span>
</label> </label>
<div <div
...@@ -85,7 +61,7 @@ ...@@ -85,7 +61,7 @@
> >
<input <input
type="text" type="text"
class="form-control" class="form-control monospace"
:placeholder="newBranchName" :placeholder="newBranchName"
@input="updateBranchName($event.target.value)" @input="updateBranchName($event.target.value)"
/> />
......
...@@ -5,6 +5,7 @@ import icon from '~/vue_shared/components/icon.vue'; ...@@ -5,6 +5,7 @@ import icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import commitFilesList from './commit_sidebar/list.vue'; import commitFilesList from './commit_sidebar/list.vue';
import CommitMessageField from './commit_sidebar/message_field.vue';
import * as consts from '../stores/modules/commit/constants'; import * as consts from '../stores/modules/commit/constants';
import Actions from './commit_sidebar/actions.vue'; import Actions from './commit_sidebar/actions.vue';
...@@ -15,6 +16,7 @@ export default { ...@@ -15,6 +16,7 @@ export default {
commitFilesList, commitFilesList,
Actions, Actions,
LoadingButton, LoadingButton,
CommitMessageField,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -38,15 +40,9 @@ export default { ...@@ -38,15 +40,9 @@ export default {
'changedFiles', 'changedFiles',
]), ]),
...mapState('commit', ['commitMessage', 'submitCommitLoading']), ...mapState('commit', ['commitMessage', 'submitCommitLoading']),
...mapGetters('commit', [ ...mapGetters('commit', ['commitButtonDisabled', 'discardDraftButtonDisabled', 'branchName']),
'commitButtonDisabled',
'discardDraftButtonDisabled',
'branchName',
]),
statusSvg() { statusSvg() {
return this.lastCommitMsg return this.lastCommitMsg ? this.committedStateSvgPath : this.noChangesStateSvgPath;
? this.committedStateSvgPath
: this.noChangesStateSvgPath;
}, },
}, },
methods: { methods: {
...@@ -64,9 +60,7 @@ export default { ...@@ -64,9 +60,7 @@ export default {
}); });
}, },
forceCreateNewBranch() { forceCreateNewBranch() {
return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => return this.updateCommitAction(consts.COMMIT_TO_NEW_BRANCH).then(() => this.commitChanges());
this.commitChanges(),
);
}, },
}, },
}; };
...@@ -105,16 +99,10 @@ export default { ...@@ -105,16 +99,10 @@ export default {
@submit.prevent.stop="commitChanges" @submit.prevent.stop="commitChanges"
v-if="!rightPanelCollapsed" v-if="!rightPanelCollapsed"
> >
<div class="multi-file-commit-fieldset"> <commit-message-field
<textarea :text="commitMessage"
class="form-control multi-file-commit-message" @input="updateCommitMessage"
name="commit-message" />
:value="commitMessage"
:placeholder="__('Write a commit message...')"
@input="updateCommitMessage($event.target.value)"
>
</textarea>
</div>
<div class="clearfix prepend-top-15"> <div class="clearfix prepend-top-15">
<actions /> <actions />
<loading-button <loading-button
......
// Fuzzy file finder
export const MAX_TITLE_LENGTH = 50;
export const MAX_BODY_LENGTH = 72;
...@@ -5,45 +5,71 @@ import * as types from '../mutation_types'; ...@@ -5,45 +5,71 @@ import * as types from '../mutation_types';
export const getProjectData = ( export const getProjectData = (
{ commit, state, dispatch }, { commit, state, dispatch },
{ namespace, projectId, force = false } = {}, { namespace, projectId, force = false } = {},
) => new Promise((resolve, reject) => { ) =>
if (!state.projects[`${namespace}/${projectId}`] || force) { new Promise((resolve, reject) => {
commit(types.TOGGLE_LOADING, { entry: state }); if (!state.projects[`${namespace}/${projectId}`] || force) {
service.getProjectData(namespace, projectId)
.then(res => res.data)
.then((data) => {
commit(types.TOGGLE_LOADING, { entry: state }); commit(types.TOGGLE_LOADING, { entry: state });
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); service
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); .getProjectData(namespace, projectId)
resolve(data); .then(res => res.data)
}) .then(data => {
.catch(() => { commit(types.TOGGLE_LOADING, { entry: state });
flash('Error loading project data. Please try again.', 'alert', document, null, false, true); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
reject(new Error(`Project not loaded ${namespace}/${projectId}`)); if (!state.currentProjectId)
}); commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
} else { resolve(data);
resolve(state.projects[`${namespace}/${projectId}`]); })
} .catch(() => {
}); flash(
'Error loading project data. Please try again.',
'alert',
document,
null,
false,
true,
);
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
resolve(state.projects[`${namespace}/${projectId}`]);
}
});
export const getBranchData = ( export const getBranchData = (
{ commit, state, dispatch }, { commit, state, dispatch },
{ projectId, branchId, force = false } = {}, { projectId, branchId, force = false } = {},
) => new Promise((resolve, reject) => { ) =>
if ((typeof state.projects[`${projectId}`] === 'undefined' || new Promise((resolve, reject) => {
!state.projects[`${projectId}`].branches[branchId]) if (
|| force) { typeof state.projects[`${projectId}`] === 'undefined' ||
service.getBranchData(`${projectId}`, branchId) !state.projects[`${projectId}`].branches[branchId] ||
.then(({ data }) => { force
const { id } = data.commit; ) {
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data }); service
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); .getBranchData(`${projectId}`, branchId)
resolve(data); .then(({ data }) => {
}) const { id } = data.commit;
.catch(() => { commit(types.SET_BRANCH, {
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true); projectPath: `${projectId}`,
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); branchName: branchId,
}); branch: data,
} else { });
resolve(state.projects[`${projectId}`].branches[branchId]); commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
} commit(types.SET_CURRENT_BRANCH, branchId);
}); resolve(data);
})
.catch(() => {
flash(
'Error loading branch data. Please try again.',
'alert',
document,
null,
false,
true,
);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
});
} else {
resolve(state.projects[`${projectId}`].branches[branchId]);
}
});
...@@ -662,11 +662,6 @@ ...@@ -662,11 +662,6 @@
} }
} }
.multi-file-commit-message.form-control {
height: 160px;
resize: none;
}
.dirty-diff { .dirty-diff {
// !important need to override monaco inline style // !important need to override monaco inline style
width: 4px !important; width: 4px !important;
...@@ -839,3 +834,74 @@ ...@@ -839,3 +834,74 @@
align-items: center; align-items: center;
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
.ide-commit-message-field {
height: 200px;
background-color: $white-light;
.md-area {
display: flex;
flex-direction: column;
height: 100%;
}
.nav-links {
height: 30px;
}
.help-block {
margin-top: 2px;
color: $blue-500;
cursor: pointer;
}
}
.ide-commit-message-textarea-container {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
.note-textarea {
font-family: $monospace_font;
}
}
.ide-commit-message-highlights-container {
position: absolute;
left: 0;
top: 0;
right: -100px;
bottom: 0;
padding-right: 100px;
pointer-events: none;
z-index: 1;
.highlights {
white-space: pre-wrap;
word-wrap: break-word;
color: transparent;
}
mark {
margin-left: -1px;
padding: 0 2px;
border-radius: $border-radius-small;
background-color: $orange-200;
color: transparent;
opacity: 0.6;
}
}
.ide-commit-message-textarea {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: 2;
background: transparent;
resize: none;
}
...@@ -6,6 +6,12 @@ module Emails ...@@ -6,6 +6,12 @@ module Emails
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason)) mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
end end
def issue_due_email(recipient_id, issue_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id, reason))
end
def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil) def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id) setup_issue_mail(issue_id, recipient_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
......
...@@ -166,7 +166,7 @@ module Ci ...@@ -166,7 +166,7 @@ module Ci
build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies') build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
end end
before_transition pending: :running do |build| after_transition pending: :running do |build|
build.ensure_metadata.update_timeout_state build.ensure_metadata.update_timeout_state
end end
end end
......
...@@ -87,7 +87,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -87,7 +87,7 @@ class CommitStatus < ActiveRecord::Base
transition [:created, :pending, :running, :manual] => :canceled transition [:created, :pending, :running, :manual] => :canceled
end end
before_transition created: [:pending, :running] do |commit_status| before_transition [:created, :skipped, :manual] => :pending do |commit_status|
commit_status.queued_at = Time.now commit_status.queued_at = Time.now
end end
......
...@@ -11,7 +11,9 @@ module CacheMarkdownField ...@@ -11,7 +11,9 @@ module CacheMarkdownField
extend ActiveSupport::Concern extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output # Increment this number every time the renderer changes its output
CACHE_VERSION = 3 CACHE_REDCARPET_VERSION = 3
CACHE_COMMONMARK_VERSION_START = 10
CACHE_COMMONMARK_VERSION = 11
# changes to these attributes cause the cache to be invalidates # changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze INVALIDATED_BY = %w[author project].freeze
...@@ -49,12 +51,14 @@ module CacheMarkdownField ...@@ -49,12 +51,14 @@ module CacheMarkdownField
# Always include a project key, or Banzai complains # Always include a project key, or Banzai complains
project = self.project if self.respond_to?(:project) project = self.project if self.respond_to?(:project)
group = self.group if self.respond_to?(:group) group = self.group if self.respond_to?(:group)
context = cached_markdown_fields[field].merge(project: project, group: group) context = cached_markdown_fields[field].merge(project: project, group: group)
# Banzai is less strict about authors, so don't always have an author key # Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author) context[:author] = self.author if self.respond_to?(:author)
context[:markdown_engine] = markdown_engine
context context
end end
...@@ -69,7 +73,7 @@ module CacheMarkdownField ...@@ -69,7 +73,7 @@ module CacheMarkdownField
Banzai::Renderer.cacheless_render_field(self, markdown_field, options) Banzai::Renderer.cacheless_render_field(self, markdown_field, options)
] ]
end.to_h end.to_h
updates['cached_markdown_version'] = CacheMarkdownField::CACHE_VERSION updates['cached_markdown_version'] = latest_cached_markdown_version
updates.each {|html_field, data| write_attribute(html_field, data) } updates.each {|html_field, data| write_attribute(html_field, data) }
end end
...@@ -90,7 +94,7 @@ module CacheMarkdownField ...@@ -90,7 +94,7 @@ module CacheMarkdownField
markdown_changed = attribute_changed?(markdown_field) || false markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false html_changed = attribute_changed?(html_field) || false
CacheMarkdownField::CACHE_VERSION == cached_markdown_version && latest_cached_markdown_version == cached_markdown_version &&
(html_changed || markdown_changed == html_changed) (html_changed || markdown_changed == html_changed)
end end
...@@ -109,6 +113,24 @@ module CacheMarkdownField ...@@ -109,6 +113,24 @@ module CacheMarkdownField
__send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend __send__(cached_markdown_fields.html_field(markdown_field)) # rubocop:disable GitlabSecurity/PublicSend
end end
def latest_cached_markdown_version
return CacheMarkdownField::CACHE_REDCARPET_VERSION unless cached_markdown_version
if cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
CacheMarkdownField::CACHE_REDCARPET_VERSION
else
CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
end
def markdown_engine
if latest_cached_markdown_version < CacheMarkdownField::CACHE_COMMONMARK_VERSION_START
:redcarpet
else
:common_mark
end
end
included do included do
cattr_reader :cached_markdown_fields do cattr_reader :cached_markdown_fields do
FieldData.new FieldData.new
......
...@@ -56,6 +56,7 @@ class Issue < ActiveRecord::Base ...@@ -56,6 +56,7 @@ class Issue < ActiveRecord::Base
scope :without_due_date, -> { where(due_date: nil) } scope :without_due_date, -> { where(due_date: nil) }
scope :due_before, ->(date) { where('issues.due_date < ?', date) } scope :due_before, ->(date) { where('issues.due_date < ?', date) }
scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) } scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
scope :due_tomorrow, -> { where(due_date: Date.tomorrow) }
scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') } scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') } scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
......
...@@ -47,7 +47,8 @@ class NotificationSetting < ActiveRecord::Base ...@@ -47,7 +47,8 @@ class NotificationSetting < ActiveRecord::Base
].freeze ].freeze
EXCLUDED_WATCHER_EVENTS = [ EXCLUDED_WATCHER_EVENTS = [
:push_to_merge_request :push_to_merge_request,
:issue_due
].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze ].push(*EXCLUDED_PARTICIPATING_EVENTS).freeze
def self.find_or_create_for(source) def self.find_or_create_for(source)
......
...@@ -6,6 +6,9 @@ module Ci ...@@ -6,6 +6,9 @@ module Ci
attr_reader :runner attr_reader :runner
JOB_QUEUE_DURATION_SECONDS_BUCKETS = [1, 3, 10, 30].freeze
JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5.freeze
Result = Struct.new(:build, :valid?) Result = Struct.new(:build, :valid?)
def initialize(runner) def initialize(runner)
...@@ -32,7 +35,7 @@ module Ci ...@@ -32,7 +35,7 @@ module Ci
end end
end end
builds.find do |build| builds.auto_include(false).find do |build|
next unless runner.can_pick?(build) next unless runner.can_pick?(build)
begin begin
...@@ -106,10 +109,22 @@ module Ci ...@@ -106,10 +109,22 @@ module Ci
end end
def register_success(job) def register_success(job)
job_queue_duration_seconds.observe({ shared_runner: @runner.shared? }, Time.now - job.created_at) labels = { shared_runner: runner.shared?,
jobs_running_for_project: jobs_running_for_project(job) }
job_queue_duration_seconds.observe(labels, Time.now - job.queued_at) unless job.queued_at.nil?
attempt_counter.increment attempt_counter.increment
end end
def jobs_running_for_project(job)
return '+Inf' unless runner.shared?
# excluding currently started job
running_jobs_count = job.project.builds.running.where(runner: Ci::Runner.shared)
.limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1
running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+"
end
def failed_attempt_counter def failed_attempt_counter
@failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job") @failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job")
end end
...@@ -119,7 +134,7 @@ module Ci ...@@ -119,7 +134,7 @@ module Ci
end end
def job_queue_duration_seconds def job_queue_duration_seconds
@job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time') @job_queue_duration_seconds ||= Gitlab::Metrics.histogram(:job_queue_duration_seconds, 'Request handling execution time', {}, JOB_QUEUE_DURATION_SECONDS_BUCKETS)
end end
end end
end end
...@@ -203,10 +203,11 @@ module NotificationRecipientService ...@@ -203,10 +203,11 @@ module NotificationRecipientService
attr_reader :action attr_reader :action
attr_reader :previous_assignee attr_reader :previous_assignee
attr_reader :skip_current_user attr_reader :skip_current_user
def initialize(target, current_user, action:, previous_assignee: nil, skip_current_user: true) def initialize(target, current_user, action:, custom_action: nil, previous_assignee: nil, skip_current_user: true)
@target = target @target = target
@current_user = current_user @current_user = current_user
@action = action @action = action
@custom_action = custom_action
@previous_assignee = previous_assignee @previous_assignee = previous_assignee
@skip_current_user = skip_current_user @skip_current_user = skip_current_user
end end
...@@ -236,7 +237,13 @@ module NotificationRecipientService ...@@ -236,7 +237,13 @@ module NotificationRecipientService
add_mentions(current_user, target: target) add_mentions(current_user, target: target)
# Add the assigned users, if any # Add the assigned users, if any
assignees = custom_action == :new_issue ? target.assignees : target.assignee assignees = case custom_action
when :new_issue
target.assignees
else
target.assignee
end
# We use the `:participating` notification level in order to match existing legacy behavior as captured # We use the `:participating` notification level in order to match existing legacy behavior as captured
# in existing specs (notification_service_spec.rb ~ line 507) # in existing specs (notification_service_spec.rb ~ line 507)
add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees add_recipients(assignees, :participating, NotificationReason::ASSIGNED) if assignees
......
...@@ -396,6 +396,20 @@ class NotificationService ...@@ -396,6 +396,20 @@ class NotificationService
end end
end end
def issue_due(issue)
recipients = NotificationRecipientService.build_recipients(
issue,
issue.author,
action: 'due',
custom_action: :issue_due,
skip_current_user: false
)
recipients.each do |recipient|
mailer.send(:issue_due_email, recipient.user.id, issue.id, recipient.reason).deliver_later
end
end
protected protected
def new_resource_email(target, method) def new_resource_email(target, method)
......
...@@ -159,7 +159,7 @@ module SystemNoteService ...@@ -159,7 +159,7 @@ module SystemNoteService
body = if noteable.time_estimate == 0 body = if noteable.time_estimate == 0
"removed time estimate" "removed time estimate"
else else
"changed time estimate to #{parsed_time}" "changed time estimate to #{parsed_time},"
end end
create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking')) create_note(NoteSummary.new(noteable, project, author, body, action: 'time_tracking'))
......
<%= yield -%> <%= yield -%>
--- -- <%# signature marker %>
You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>. You're receiving this email because of your account on <%= Gitlab.config.gitlab.host %>.
<%# EE-specific start %><%= render 'layouts/mailer/additional_text' %><%# EE-specific end %> <%# EE-specific start %><%= render 'layouts/mailer/additional_text' %><%# EE-specific end %>
\ No newline at end of file
<%= yield -%> <%= yield -%>
--- -- <%# signature marker %>
<% if @target_url -%> <% if @target_url -%>
<% if @reply_by_email -%> <% if @reply_by_email -%>
<%= "Reply to this email directly or view it on GitLab: #{@target_url}" -%> <%= "Reply to this email directly or view it on GitLab: #{@target_url}" -%>
......
%p.details
#{link_to @issue.author_name, user_url(@issue.author)}'s issue is due soon.
- if @issue.assignees.any?
%p
Assignee: #{@issue.assignee_list}
%p
This issue is due on: #{@issue.due_date.to_s(:medium)}
- if @issue.description
%div
= markdown(@issue.description, pipeline: :email, author: @issue.author)
The following issue is due on <%= @issue.due_date %>:
Issue <%= @issue.iid %>: <%= url_for(project_issue_url(@issue.project, @issue)) %>
Author: <%= @issue.author_name %>
Assignee: <%= @issue.assignee_list %>
<%= @issue.description %>
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
= s_('Branches|Cant find HEAD commit for this branch') = s_('Branches|Cant find HEAD commit for this branch')
- if branch.name != @repository.root_ref - if branch.name != @repository.root_ref
.divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind), .divergence-graph.hidden-xs{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
default_branch: @repository.root_ref, default_branch: @repository.root_ref,
number_commits_ahead: diverging_count_label(number_commits_ahead) } } number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side .graph-side
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
- cronjob:stuck_import_jobs - cronjob:stuck_import_jobs
- cronjob:stuck_merge_jobs - cronjob:stuck_merge_jobs
- cronjob:trending_projects - cronjob:trending_projects
- cronjob:issue_due_scheduler
- gcp_cluster:cluster_install_app - gcp_cluster:cluster_install_app
- gcp_cluster:cluster_provision - gcp_cluster:cluster_provision
...@@ -39,6 +40,8 @@ ...@@ -39,6 +40,8 @@
- github_importer:github_import_stage_import_pull_requests - github_importer:github_import_stage_import_pull_requests
- github_importer:github_import_stage_import_repository - github_importer:github_import_stage_import_repository
- mail_scheduler:mail_scheduler_issue_due
- object_storage_upload - object_storage_upload
- object_storage:object_storage_background_move - object_storage:object_storage_background_move
- object_storage:object_storage_migrate_uploads - object_storage:object_storage_migrate_uploads
......
module MailSchedulerQueue
extend ActiveSupport::Concern
included do
queue_namespace :mail_scheduler
end
end
class IssueDueSchedulerWorker
include ApplicationWorker
include CronjobQueue
def perform
project_ids = Issue.opened.due_tomorrow.group(:project_id).pluck(:project_id).map { |id| [id] }
MailScheduler::IssueDueWorker.bulk_perform_async(project_ids)
end
end
module MailScheduler
class IssueDueWorker
include ApplicationWorker
include MailSchedulerQueue
def perform(project_id)
notification_service = NotificationService.new
Issue.opened.due_tomorrow.in_projects(project_id).preload(:project).find_each do |issue|
notification_service.issue_due(issue)
end
end
end
end
---
title: Add cron job to email users on issue due date
merge_request: 17985
author: Stuart Nelson
type: added
---
title: Add a comma to the time estimate system notes
merge_request: 18326
author:
type: changed
---
title: Remove ahead/behind graphs on project branches on mobile
merge_request: 18415
author: Takuya Noguchi
type: other
---
title: Use RFC 3676 mail signature delimiters
merge_request: 17979
author: Enrico Scholz
type: changed
---
title: Fix `Trace::HttpIO` can not render multi-byte chars
merge_request: 18417
author:
type: fixed
---
title: Partition job_queue_duration_seconds with jobs_running_for_project
merge_request: 17730
author:
type: changed
...@@ -536,6 +536,10 @@ Settings.cron_jobs['pages_domain_verification_cron_worker'] ||= Settingslogic.ne ...@@ -536,6 +536,10 @@ Settings.cron_jobs['pages_domain_verification_cron_worker'] ||= Settingslogic.ne
Settings.cron_jobs['pages_domain_verification_cron_worker']['cron'] ||= '*/15 * * * *' Settings.cron_jobs['pages_domain_verification_cron_worker']['cron'] ||= '*/15 * * * *'
Settings.cron_jobs['pages_domain_verification_cron_worker']['job_class'] = 'PagesDomainVerificationCronWorker' Settings.cron_jobs['pages_domain_verification_cron_worker']['job_class'] = 'PagesDomainVerificationCronWorker'
Settings.cron_jobs['issue_due_scheduler_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['issue_due_scheduler_worker']['cron'] ||= '50 00 * * *'
Settings.cron_jobs['issue_due_scheduler_worker']['job_class'] = 'IssueDueSchedulerWorker'
# #
# Sidekiq # Sidekiq
# #
......
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
- [email_receiver, 2] - [email_receiver, 2]
- [emails_on_push, 2] - [emails_on_push, 2]
- [mailers, 2] - [mailers, 2]
- [mail_scheduler, 2]
- [invalid_gpg_signature_update, 2] - [invalid_gpg_signature_update, 2]
- [create_gpg_signature, 2] - [create_gpg_signature, 2]
- [rebase, 2] - [rebase, 2]
......
class AddIssueDueToNotificationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :notification_settings, :issue_due, :boolean
end
end
...@@ -1748,6 +1748,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do ...@@ -1748,6 +1748,7 @@ ActiveRecord::Schema.define(version: 20180405142733) do
t.boolean "failed_pipeline" t.boolean "failed_pipeline"
t.boolean "success_pipeline" t.boolean "success_pipeline"
t.boolean "push_to_merge_request" t.boolean "push_to_merge_request"
t.boolean "issue_due"
end end
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
......
...@@ -23,6 +23,7 @@ new_issue ...@@ -23,6 +23,7 @@ new_issue
reopen_issue reopen_issue
close_issue close_issue
reassign_issue reassign_issue
issue_due
new_merge_request new_merge_request
push_to_merge_request push_to_merge_request
reopen_merge_request reopen_merge_request
...@@ -75,6 +76,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab ...@@ -75,6 +76,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `reopen_issue` | boolean | no | Enable/disable this notification | | `reopen_issue` | boolean | no | Enable/disable this notification |
| `close_issue` | boolean | no | Enable/disable this notification | | `close_issue` | boolean | no | Enable/disable this notification |
| `reassign_issue` | boolean | no | Enable/disable this notification | | `reassign_issue` | boolean | no | Enable/disable this notification |
| `issue_due` | boolean | no | Enable/disable this notification |
| `new_merge_request` | boolean | no | Enable/disable this notification | | `new_merge_request` | boolean | no | Enable/disable this notification |
| `push_to_merge_request` | boolean | no | Enable/disable this notification | | `push_to_merge_request` | boolean | no | Enable/disable this notification |
| `reopen_merge_request` | boolean | no | Enable/disable this notification | | `reopen_merge_request` | boolean | no | Enable/disable this notification |
...@@ -142,6 +144,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab ...@@ -142,6 +144,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab
| `reopen_issue` | boolean | no | Enable/disable this notification | | `reopen_issue` | boolean | no | Enable/disable this notification |
| `close_issue` | boolean | no | Enable/disable this notification | | `close_issue` | boolean | no | Enable/disable this notification |
| `reassign_issue` | boolean | no | Enable/disable this notification | | `reassign_issue` | boolean | no | Enable/disable this notification |
| `issue_due` | boolean | no | Enable/disable this notification |
| `new_merge_request` | boolean | no | Enable/disable this notification | | `new_merge_request` | boolean | no | Enable/disable this notification |
| `push_to_merge_request` | boolean | no | Enable/disable this notification | | `push_to_merge_request` | boolean | no | Enable/disable this notification |
| `reopen_merge_request` | boolean | no | Enable/disable this notification | | `reopen_merge_request` | boolean | no | Enable/disable this notification |
...@@ -166,6 +169,7 @@ Example responses: ...@@ -166,6 +169,7 @@ Example responses:
"reopen_issue": false, "reopen_issue": false,
"close_issue": false, "close_issue": false,
"reassign_issue": false, "reassign_issue": false,
"issue_due": false,
"new_merge_request": false, "new_merge_request": false,
"push_to_merge_request": false, "push_to_merge_request": false,
"reopen_merge_request": false, "reopen_merge_request": false,
......
...@@ -133,11 +133,19 @@ roughly be as follows: ...@@ -133,11 +133,19 @@ roughly be as follows:
1. Release B: 1. Release B:
1. Deploy code so that the application starts using the new column and stops 1. Deploy code so that the application starts using the new column and stops
scheduling jobs for newly created data. scheduling jobs for newly created data.
1. In a post-deployment migration you'll need to ensure no jobs remain. To do 1. In a post-deployment migration you'll need to ensure no jobs remain.
so you can use `Gitlab::BackgroundMigration.steal` to process any remaining 1. Use `Gitlab::BackgroundMigration.steal` to process any remaining
jobs before continuing. jobs in Sidekiq.
1. Reschedule the migration to be run directly (i.e. not through Sidekiq)
on any rows that weren't migrated by Sidekiq. This can happen if, for
instance, Sidekiq received a SIGKILL, or if a particular batch failed
enough times to be marked as dead.
1. Remove the old column. 1. Remove the old column.
This may also require a bump to the [import/export version][import-export], if
importing a project from a prior version of GitLab requires the data to be in
the new format.
## Example ## Example
To explain all this, let's use the following example: the table `services` has a To explain all this, let's use the following example: the table `services` has a
...@@ -296,3 +304,4 @@ for more details. ...@@ -296,3 +304,4 @@ for more details.
[migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md [migrations-readme]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/migrations/README.md
[issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351 [issue-rspec-hooks]: https://gitlab.com/gitlab-org/gitlab-ce/issues/35351
[reliable-sidekiq]: https://gitlab.com/gitlab-org/gitlab-ce/issues/36791 [reliable-sidekiq]: https://gitlab.com/gitlab-org/gitlab-ce/issues/36791
[import-export]: ../user/project/settings/import_export.md
...@@ -35,5 +35,9 @@ Due dates also appear in your [todos list](../../../workflow/todos.md). ...@@ -35,5 +35,9 @@ Due dates also appear in your [todos list](../../../workflow/todos.md).
![Issues with due dates in the todos](img/due_dates_todos.png) ![Issues with due dates in the todos](img/due_dates_todos.png)
The day before an open issue is due, an email will be sent to all participants
of the issue. Both the due date and the day before are calculated using the
server's timezone.
[ce-3614]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3614 [ce-3614]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3614
[permissions]: ../../permissions.md#project [permissions]: ../../permissions.md#project
...@@ -9,8 +9,7 @@ Labels allow you to categorize issues or merge requests using descriptive titles ...@@ -9,8 +9,7 @@ Labels allow you to categorize issues or merge requests using descriptive titles
In GitLab, you can create project and group labels: In GitLab, you can create project and group labels:
- **Project labels** can be assigned to issues or merge requests in that project only. - **Project labels** can be assigned to issues or merge requests in that project only.
- **Group labels** can be assigned to any issue or merge request of any project in that group or subgroup. - **Group labels** can be assigned to any issue or merge request of any project in that group or any subgroups of the group.
- In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/40915), you will be able to assign group labels to issues and merge reqeusts of projects in [subgroups](../group/subgroups/index.md).
## Creating labels ## Creating labels
......
...@@ -86,6 +86,7 @@ In most of the below cases, the notification will be sent to: ...@@ -86,6 +86,7 @@ In most of the below cases, the notification will be sent to:
| Close issue | | | Close issue | |
| Reassign issue | The above, plus the old assignee | | Reassign issue | The above, plus the old assignee |
| Reopen issue | | | Reopen issue | |
| Due issue | Participants and Custom notification level with this event selected |
| New merge request | | | New merge request | |
| Push to merge request | Participants and Custom notification level with this event selected | | Push to merge request | Participants and Custom notification level with this event selected |
| Reassign merge request | The above, plus the old assignee | | Reassign merge request | The above, plus the old assignee |
...@@ -102,10 +103,10 @@ In addition, if the title or description of an Issue or Merge Request is ...@@ -102,10 +103,10 @@ In addition, if the title or description of an Issue or Merge Request is
changed, notifications will be sent to any **new** mentions by `@username` as changed, notifications will be sent to any **new** mentions by `@username` as
if they had been mentioned in the original text. if they had been mentioned in the original text.
You won't receive notifications for Issues, Merge Requests or Milestones You won't receive notifications for Issues, Merge Requests or Milestones created
created by yourself. You will only receive automatic notifications when by yourself (except when an issue is due). You will only receive automatic
somebody else comments or adds changes to the ones that you've created or notifications when somebody else comments or adds changes to the ones that
mentions you. you've created or mentions you.
### Email Headers ### Email Headers
...@@ -123,7 +124,7 @@ Notification emails include headers that provide extra content about the notific ...@@ -123,7 +124,7 @@ Notification emails include headers that provide extra content about the notific
| X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc | | X-GitLab-NotificationReason | The reason for being notified. "mentioned", "assigned", etc |
#### X-GitLab-NotificationReason #### X-GitLab-NotificationReason
This header holds the reason for the notification to have been sent out, This header holds the reason for the notification to have been sent out,
where reason can be `mentioned`, `assigned`, `own_activity`, etc. where reason can be `mentioned`, `assigned`, `own_activity`, etc.
Only one reason is sent out according to its priority: Only one reason is sent out according to its priority:
- `own_activity` - `own_activity`
...@@ -131,7 +132,7 @@ Only one reason is sent out according to its priority: ...@@ -131,7 +132,7 @@ Only one reason is sent out according to its priority:
- `mentioned` - `mentioned`
The reason in this header will also be shown in the footer of the notification email. For example an email with the The reason in this header will also be shown in the footer of the notification email. For example an email with the
reason `assigned` will have this sentence in the footer: reason `assigned` will have this sentence in the footer:
`"You are receiving this email because you have been assigned an item on {configured GitLab hostname}"` `"You are receiving this email because you have been assigned an item on {configured GitLab hostname}"`
**Note: Only reasons listed above have been implemented so far** **Note: Only reasons listed above have been implemented so far**
......
...@@ -75,18 +75,28 @@ module Gitlab ...@@ -75,18 +75,28 @@ module Gitlab
end end
end end
def read(length = nil) def read(length = nil, outbuf = "")
out = "" out = ""
until eof? || (length && out.length >= length) length ||= size - tell
until length <= 0 || eof?
data = get_chunk data = get_chunk
break if data.empty? break if data.empty?
out << data chunk_bytes = [BUFFER_SIZE - chunk_offset, length].min
@tell += data.bytesize chunk_data = data.byteslice(0, chunk_bytes)
out << chunk_data
@tell += chunk_data.bytesize
length -= chunk_data.bytesize
end end
out = out[0, length] if length && out.length > length # If outbuf is passed, we put the output into the buffer. This supports IO.copy_stream functionality
if outbuf
outbuf.slice!(0, outbuf.bytesize)
outbuf << out
end
out out
end end
...@@ -158,7 +168,7 @@ module Gitlab ...@@ -158,7 +168,7 @@ module Gitlab
# Provider: GCS # Provider: GCS
# - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206 # - When the file size is larger than requested Content-range, the Content-range is included in responces with Net::HTTPPartialContent 206
# - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPOK 200 # - When the file size is smaller than requested Content-range, the Content-range is included in responces with Net::HTTPOK 200
@chunk_range ||= (chunk_start...(chunk_start + @chunk.length)) @chunk_range ||= (chunk_start...(chunk_start + @chunk.bytesize))
end end
@chunk[chunk_offset..BUFFER_SIZE] @chunk[chunk_offset..BUFFER_SIZE]
......
...@@ -10,7 +10,9 @@ module Gitlab ...@@ -10,7 +10,9 @@ module Gitlab
delegate :close, :tell, :seek, :size, :url, :truncate, to: :stream, allow_nil: true delegate :close, :tell, :seek, :size, :url, :truncate, to: :stream, allow_nil: true
delegate :valid?, to: :stream, as: :present?, allow_nil: true delegate :valid?, to: :stream, allow_nil: true
alias_method :present?, :valid?
def initialize def initialize
@stream = yield @stream = yield
......
...@@ -486,6 +486,8 @@ module Gitlab ...@@ -486,6 +486,8 @@ module Gitlab
end end
def tree_entry(path) def tree_entry(path)
return unless path.present?
@repository.gitaly_migrate(:commit_tree_entry) do |is_migrated| @repository.gitaly_migrate(:commit_tree_entry) do |is_migrated|
if is_migrated if is_migrated
gitaly_tree_entry(path) gitaly_tree_entry(path)
......
...@@ -62,6 +62,7 @@ FactoryBot.define do ...@@ -62,6 +62,7 @@ FactoryBot.define do
end end
trait :pending do trait :pending do
queued_at 'Di 29. Okt 09:50:59 CET 2013'
status 'pending' status 'pending'
end end
......
This diff is collapsed.
import Vue from 'vue';
import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue';
import createComponent from 'spec/helpers/vue_mount_component_helper';
describe('IDE commit message field', () => {
const Component = Vue.extend(CommitMessageField);
let vm;
beforeEach(() => {
setFixtures('<div id="app"></div>');
vm = createComponent(
Component,
{
text: '',
},
'#app',
);
});
afterEach(() => {
vm.$destroy();
});
it('adds is-focused class on focus', done => {
vm.$el.querySelector('textarea').focus();
vm.$nextTick(() => {
expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
done();
});
});
it('removed is-focused class on blur', done => {
vm.$el.querySelector('textarea').focus();
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.is-focused')).not.toBeNull();
vm.$el.querySelector('textarea').blur();
return vm.$nextTick();
})
.then(() => {
expect(vm.$el.querySelector('.is-focused')).toBeNull();
done();
})
.then(done)
.catch(done.fail);
});
it('emits input event on input', () => {
spyOn(vm, '$emit');
const textarea = vm.$el.querySelector('textarea');
textarea.value = 'testing';
textarea.dispatchEvent(new Event('input'));
expect(vm.$emit).toHaveBeenCalledWith('input', 'testing');
});
describe('highlights', () => {
describe('subject line', () => {
it('does not highlight less than 50 characters', done => {
vm.text = 'text less than 50 chars';
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.highlights span').textContent).toContain(
'text less than 50 chars',
);
expect(vm.$el.querySelector('mark').style.display).toBe('none');
})
.then(done)
.catch(done.fail);
});
it('highlights characters over 50 length', done => {
vm.text =
'text less than 50 chars that should not highlighted. text more than 50 should be highlighted';
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelector('.highlights span').textContent).toContain(
'text less than 50 chars that should not highlighte',
);
expect(vm.$el.querySelector('mark').style.display).not.toBe('none');
expect(vm.$el.querySelector('mark').textContent).toBe(
'd. text more than 50 should be highlighted',
);
})
.then(done)
.catch(done.fail);
});
});
describe('body text', () => {
it('does not highlight body text less tan 72 characters', done => {
vm.text = 'subject line\nbody content';
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
expect(vm.$el.querySelectorAll('mark')[1].style.display).toBe('none');
})
.then(done)
.catch(done.fail);
});
it('highlights body text more than 72 characters', done => {
vm.text =
'subject line\nbody content that will be highlighted when it is more than 72 characters in length';
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
expect(vm.$el.querySelectorAll('mark')[1].style.display).not.toBe('none');
expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
})
.then(done)
.catch(done.fail);
});
it('highlights body text & subject line', done => {
vm.text =
'text less than 50 chars that should not highlighted\nbody content that will be highlighted when it is more than 72 characters in length';
vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelectorAll('.highlights span').length).toBe(2);
expect(vm.$el.querySelectorAll('mark').length).toBe(2);
expect(vm.$el.querySelectorAll('mark')[0].textContent).toContain('d');
expect(vm.$el.querySelectorAll('mark')[1].textContent).toBe(' in length');
})
.then(done)
.catch(done.fail);
});
});
});
describe('scrolling textarea', () => {
it('updates transform of highlights', done => {
vm.text = 'subject line\n\n\n\n\n\n\n\n\n\n\nbody content';
vm
.$nextTick()
.then(() => {
vm.$el.querySelector('textarea').scrollTo(0, 50);
vm.handleScroll();
})
.then(vm.$nextTick)
.then(() => {
expect(vm.scrollTop).toBe(50);
expect(vm.$el.querySelector('.highlights').style.transform).toBe(
'translate3d(0px, -50px, 0px)',
);
})
.then(done)
.catch(done.fail);
});
});
});
...@@ -69,19 +69,6 @@ describe('IDE commit sidebar radio group', () => { ...@@ -69,19 +69,6 @@ describe('IDE commit sidebar radio group', () => {
}); });
}); });
it('renders helpText tooltip', done => {
vm.helpText = 'help text';
Vue.nextTick(() => {
const help = vm.$el.querySelector('.help-block');
expect(help).not.toBeNull();
expect(help.getAttribute('data-original-title')).toBe('help text');
done();
});
});
describe('with input', () => { describe('with input', () => {
beforeEach(done => { beforeEach(done => {
vm.$destroy(); vm.$destroy();
......
...@@ -11,7 +11,7 @@ describe Banzai::ObjectRenderer do ...@@ -11,7 +11,7 @@ describe Banzai::ObjectRenderer do
) )
end end
let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_VERSION) } let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
describe '#render' do describe '#render' do
context 'with cache' do context 'with cache' do
......
...@@ -2182,10 +2182,6 @@ describe Ci::Build do ...@@ -2182,10 +2182,6 @@ describe Ci::Build do
it "doesn't save timeout_source" do it "doesn't save timeout_source" do
expect { run_job_without_exception }.not_to change { job.reload.ensure_metadata.timeout_source } expect { run_job_without_exception }.not_to change { job.reload.ensure_metadata.timeout_source }
end end
it 'raises an exception' do
expect { job.run! }.to raise_error(StateMachines::InvalidTransition)
end
end end
end end
......
...@@ -450,6 +450,11 @@ eos ...@@ -450,6 +450,11 @@ eos
it "returns nil if the path doesn't exists" do it "returns nil if the path doesn't exists" do
expect(commit.uri_type('this/path/doesnt/exist')).to be_nil expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
end end
it 'is nil if the path is nil or empty' do
expect(commit.uri_type(nil)).to be_nil
expect(commit.uri_type("")).to be_nil
end
end end
context 'when Gitaly commit_tree_entry feature is enabled' do context 'when Gitaly commit_tree_entry feature is enabled' do
......
...@@ -533,4 +533,36 @@ describe CommitStatus do ...@@ -533,4 +533,36 @@ describe CommitStatus do
end end
end end
end end
describe '#enqueue' do
let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) }
before do
allow(Time).to receive(:now).and_return(current_time)
end
shared_examples 'commit status enqueued' do
it 'sets queued_at value when enqueued' do
expect { commit_status.enqueue }.to change { commit_status.reload.queued_at }.from(nil).to(current_time)
end
end
context 'when initial state is :created' do
let(:commit_status) { create(:commit_status, :created) }
it_behaves_like 'commit status enqueued'
end
context 'when initial state is :skipped' do
let(:commit_status) { create(:commit_status, :skipped) }
it_behaves_like 'commit status enqueued'
end
context 'when initial state is :manual' do
let(:commit_status) { create(:commit_status, :manual) }
it_behaves_like 'commit status enqueued'
end
end
end end
...@@ -72,7 +72,7 @@ describe CacheMarkdownField do ...@@ -72,7 +72,7 @@ describe CacheMarkdownField do
let(:updated_markdown) { '`Bar`' } let(:updated_markdown) { '`Bar`' }
let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' } let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' }
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_VERSION) } let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
describe '.attributes' do describe '.attributes' do
it 'excludes cache attributes' do it 'excludes cache attributes' do
...@@ -89,17 +89,24 @@ describe CacheMarkdownField do ...@@ -89,17 +89,24 @@ describe CacheMarkdownField do
it { expect(thing.foo).to eq(markdown) } it { expect(thing.foo).to eq(markdown) }
it { expect(thing.foo_html).to eq(html) } it { expect(thing.foo_html).to eq(html) }
it { expect(thing.foo_html_changed?).not_to be_truthy } it { expect(thing.foo_html_changed?).not_to be_truthy }
it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) } it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION) }
end end
context 'a changed markdown field' do context 'a changed markdown field' do
before do shared_examples 'with cache version' do |cache_version|
thing.foo = updated_markdown let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
thing.save
before do
thing.foo = updated_markdown
thing.save
end
it { expect(thing.foo_html).to eq(updated_html) }
it { expect(thing.cached_markdown_version).to eq(cache_version) }
end end
it { expect(thing.foo_html).to eq(updated_html) } it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) } it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end end
context 'when a markdown field is set repeatedly to an empty string' do context 'when a markdown field is set repeatedly to an empty string' do
...@@ -123,15 +130,22 @@ describe CacheMarkdownField do ...@@ -123,15 +130,22 @@ describe CacheMarkdownField do
end end
context 'a non-markdown field changed' do context 'a non-markdown field changed' do
before do shared_examples 'with cache version' do |cache_version|
thing.bar = 'OK' let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
thing.save
before do
thing.bar = 'OK'
thing.save
end
it { expect(thing.bar).to eq('OK') }
it { expect(thing.foo).to eq(markdown) }
it { expect(thing.foo_html).to eq(html) }
it { expect(thing.cached_markdown_version).to eq(cache_version) }
end end
it { expect(thing.bar).to eq('OK') } it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
it { expect(thing.foo).to eq(markdown) } it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
it { expect(thing.foo_html).to eq(html) }
it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) }
end end
context 'version is out of date' do context 'version is out of date' do
...@@ -142,59 +156,85 @@ describe CacheMarkdownField do ...@@ -142,59 +156,85 @@ describe CacheMarkdownField do
end end
it { expect(thing.foo_html).to eq(updated_html) } it { expect(thing.foo_html).to eq(updated_html) }
it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) } it { expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION) }
end end
describe '#cached_html_up_to_date?' do describe '#cached_html_up_to_date?' do
subject { thing.cached_html_up_to_date?(:foo) } shared_examples 'with cache version' do |cache_version|
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
it 'returns false when the version is absent' do subject { thing.cached_html_up_to_date?(:foo) }
thing.cached_markdown_version = nil
is_expected.to be_falsy it 'returns false when the version is absent' do
end thing.cached_markdown_version = nil
it 'returns false when the version is too early' do is_expected.to be_falsy
thing.cached_markdown_version -= 1 end
is_expected.to be_falsy it 'returns false when the version is too early' do
end thing.cached_markdown_version -= 1
it 'returns false when the version is too late' do is_expected.to be_falsy
thing.cached_markdown_version += 1 end
is_expected.to be_falsy it 'returns false when the version is too late' do
end thing.cached_markdown_version += 1
it 'returns true when the version is just right' do is_expected.to be_falsy
thing.cached_markdown_version = CacheMarkdownField::CACHE_VERSION end
is_expected.to be_truthy it 'returns true when the version is just right' do
end thing.cached_markdown_version = cache_version
it 'returns false if markdown has been changed but html has not' do is_expected.to be_truthy
thing.foo = updated_html end
is_expected.to be_falsy it 'returns false if markdown has been changed but html has not' do
end thing.foo = updated_html
it 'returns true if markdown has not been changed but html has' do is_expected.to be_falsy
thing.foo_html = updated_html end
it 'returns true if markdown has not been changed but html has' do
thing.foo_html = updated_html
is_expected.to be_truthy is_expected.to be_truthy
end
it 'returns true if markdown and html have both been changed' do
thing.foo = updated_markdown
thing.foo_html = updated_html
is_expected.to be_truthy
end
it 'returns false if the markdown field is set but the html is not' do
thing.foo_html = nil
is_expected.to be_falsy
end
end end
it 'returns true if markdown and html have both been changed' do it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
thing.foo = updated_markdown it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
thing.foo_html = updated_html end
describe '#latest_cached_markdown_version' do
subject { thing.latest_cached_markdown_version }
is_expected.to be_truthy it 'returns redcarpet version' do
thing.cached_markdown_version = CacheMarkdownField::CACHE_COMMONMARK_VERSION_START - 1
is_expected.to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end end
it 'returns false if the markdown field is set but the html is not' do it 'returns commonmark version' do
thing.foo_html = nil thing.cached_markdown_version = CacheMarkdownField::CACHE_COMMONMARK_VERSION_START + 1
is_expected.to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
end
is_expected.to be_falsy it 'returns default version when version is nil' do
thing.cached_markdown_version = nil
is_expected.to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end end
end end
...@@ -221,37 +261,44 @@ describe CacheMarkdownField do ...@@ -221,37 +261,44 @@ describe CacheMarkdownField do
thing.cached_markdown_version = nil thing.cached_markdown_version = nil
thing.refresh_markdown_cache thing.refresh_markdown_cache
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end end
end end
describe '#refresh_markdown_cache!' do describe '#refresh_markdown_cache!' do
before do shared_examples 'with cache version' do |cache_version|
thing.foo = updated_markdown let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
end
it 'fills all html fields' do before do
thing.refresh_markdown_cache! thing.foo = updated_markdown
end
expect(thing.foo_html).to eq(updated_html) it 'fills all html fields' do
expect(thing.foo_html_changed?).to be_truthy thing.refresh_markdown_cache!
expect(thing.baz_html_changed?).to be_truthy
end
it 'skips saving if not persisted' do expect(thing.foo_html).to eq(updated_html)
expect(thing).to receive(:persisted?).and_return(false) expect(thing.foo_html_changed?).to be_truthy
expect(thing).not_to receive(:update_columns) expect(thing.baz_html_changed?).to be_truthy
end
thing.refresh_markdown_cache! it 'skips saving if not persisted' do
end expect(thing).to receive(:persisted?).and_return(false)
expect(thing).not_to receive(:update_columns)
it 'saves the changes using #update_columns' do thing.refresh_markdown_cache!
expect(thing).to receive(:persisted?).and_return(true) end
expect(thing).to receive(:update_columns)
.with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION)
thing.refresh_markdown_cache! it 'saves the changes using #update_columns' do
expect(thing).to receive(:persisted?).and_return(true)
expect(thing).to receive(:update_columns)
.with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => cache_version)
thing.refresh_markdown_cache!
end
end end
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end end
describe '#banzai_render_context' do describe '#banzai_render_context' do
...@@ -299,7 +346,7 @@ describe CacheMarkdownField do ...@@ -299,7 +346,7 @@ describe CacheMarkdownField do
expect(thing.foo_html).to eq(updated_html) expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html) expect(thing.baz_html).to eq(updated_html)
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end end
end end
...@@ -319,7 +366,7 @@ describe CacheMarkdownField do ...@@ -319,7 +366,7 @@ describe CacheMarkdownField do
expect(thing.foo_html).to eq(updated_html) expect(thing.foo_html).to eq(updated_html)
expect(thing.baz_html).to eq(updated_html) expect(thing.baz_html).to eq(updated_html)
expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) expect(thing.cached_markdown_version).to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end end
end end
end end
......
...@@ -370,10 +370,111 @@ module Ci ...@@ -370,10 +370,111 @@ module Ci
it_behaves_like 'validation is not active' it_behaves_like 'validation is not active'
end end
end end
end
describe '#register_success' do
let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) }
let!(:attempt_counter) { double('Gitlab::Metrics::NullMetric') }
let!(:job_queue_duration_seconds) { double('Gitlab::Metrics::NullMetric') }
before do
allow(Time).to receive(:now).and_return(current_time)
# Stub defaults for any metrics other than the ones we're testing
allow(Gitlab::Metrics).to receive(:counter)
.with(any_args)
.and_return(Gitlab::Metrics::NullMetric.instance)
allow(Gitlab::Metrics).to receive(:histogram)
.with(any_args)
.and_return(Gitlab::Metrics::NullMetric.instance)
# Stub tested metrics
allow(Gitlab::Metrics).to receive(:counter)
.with(:job_register_attempts_total, anything)
.and_return(attempt_counter)
allow(Gitlab::Metrics).to receive(:histogram)
.with(:job_queue_duration_seconds, anything, anything, anything)
.and_return(job_queue_duration_seconds)
project.update(shared_runners_enabled: true)
pending_job.update(created_at: current_time - 3600, queued_at: current_time - 1800)
end
shared_examples 'attempt counter collector' do
it 'increments attempt counter' do
allow(job_queue_duration_seconds).to receive(:observe)
expect(attempt_counter).to receive(:increment)
execute(runner)
end
end
shared_examples 'jobs queueing time histogram collector' do
it 'counts job queuing time histogram with expected labels' do
allow(attempt_counter).to receive(:increment)
expect(job_queue_duration_seconds).to receive(:observe)
.with({ shared_runner: expected_shared_runner,
jobs_running_for_project: expected_jobs_running_for_project_first_job }, 1800)
execute(runner)
end
context 'when project already has running jobs' do
let!(:build2) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
let!(:build3) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) }
it 'counts job queuing time histogram with expected labels' do
allow(attempt_counter).to receive(:increment)
expect(job_queue_duration_seconds).to receive(:observe)
.with({ shared_runner: expected_shared_runner,
jobs_running_for_project: expected_jobs_running_for_project_third_job }, 1800)
execute(runner)
end
end
end
def execute(runner) shared_examples 'metrics collector' do
described_class.new(runner).execute.build it_behaves_like 'attempt counter collector'
it_behaves_like 'jobs queueing time histogram collector'
end end
context 'when shared runner is used' do
let(:runner) { shared_runner }
let(:expected_shared_runner) { true }
let(:expected_jobs_running_for_project_first_job) { 0 }
let(:expected_jobs_running_for_project_third_job) { 2 }
it_behaves_like 'metrics collector'
context 'when pending job with queued_at=nil is used' do
before do
pending_job.update(queued_at: nil)
end
it_behaves_like 'attempt counter collector'
it "doesn't count job queuing time histogram" do
allow(attempt_counter).to receive(:increment)
expect(job_queue_duration_seconds).not_to receive(:observe)
execute(runner)
end
end
end
context 'when specific runner is used' do
let(:runner) { specific_runner }
let(:expected_shared_runner) { false }
let(:expected_jobs_running_for_project_first_job) { '+Inf' }
let(:expected_jobs_running_for_project_third_job) { '+Inf' }
it_behaves_like 'metrics collector'
end
end
def execute(runner)
described_class.new(runner).execute.build
end end
end end
end end
...@@ -933,6 +933,46 @@ describe NotificationService, :mailer do ...@@ -933,6 +933,46 @@ describe NotificationService, :mailer do
let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) } let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) }
end end
end end
describe '#issue_due' do
before do
issue.update!(due_date: Date.today)
update_custom_notification(:issue_due, @u_guest_custom, resource: project)
update_custom_notification(:issue_due, @u_custom_global)
end
it 'sends email to issue notification recipients, excluding watchers' do
notification.issue_due(issue)
should_email(issue.assignees.first)
should_email(issue.author)
should_email(@u_guest_custom)
should_email(@u_custom_global)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_not_email(@u_watcher)
should_not_email(@u_guest_watcher)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
end
it 'sends the email from the author' do
notification.issue_due(issue)
email = find_email_for(@subscriber)
expect(email.header[:from].display_names).to eq([issue.author.name])
end
it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { issue }
let(:notification_trigger) { notification.issue_due(issue) }
end
end
end end
describe 'Merge Requests' do describe 'Merge Requests' do
......
...@@ -947,7 +947,7 @@ describe SystemNoteService do ...@@ -947,7 +947,7 @@ describe SystemNoteService do
it 'sets the note text' do it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200) noteable.update_attribute(:time_estimate, 277200)
expect(subject.note).to eq "changed time estimate to 1w 4d 5h" expect(subject.note).to eq "changed time estimate to 1w 4d 5h,"
end end
end end
...@@ -984,28 +984,6 @@ describe SystemNoteService do ...@@ -984,28 +984,6 @@ describe SystemNoteService do
end end
end end
describe '.change_time_estimate' do
subject { described_class.change_time_estimate(noteable, project, author) }
it_behaves_like 'a system note' do
let(:action) { 'time_tracking' }
end
context 'with a time estimate' do
it 'sets the note text' do
noteable.update_attribute(:time_estimate, 277200)
expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
end
end
context 'without a time estimate' do
it 'sets the note text' do
expect(subject.note).to eq "removed time estimate"
end
end
end
describe '.change_time_spent' do describe '.change_time_spent' do
# We need a custom noteable in order to the shared examples to be green. # We need a custom noteable in order to the shared examples to be green.
let(:noteable) do let(:noteable) do
......
...@@ -44,10 +44,11 @@ module HttpIOHelpers ...@@ -44,10 +44,11 @@ module HttpIOHelpers
def remote_trace_body def remote_trace_body
@remote_trace_body ||= File.read(expand_fixture_path('trace/sample_trace')) @remote_trace_body ||= File.read(expand_fixture_path('trace/sample_trace'))
.force_encoding(Encoding::BINARY)
end end
def remote_trace_size def remote_trace_size
remote_trace_body.length remote_trace_body.bytesize
end end
def set_smaller_buffer_size_than(file_size) def set_smaller_buffer_size_than(file_size)
......
require 'spec_helper'
describe IssueDueSchedulerWorker do
describe '#perform' do
it 'schedules one MailScheduler::IssueDueWorker per project with open issues due tomorrow' do
project1 = create(:project)
project2 = create(:project)
project_closed_issue = create(:project)
project_issue_due_another_day = create(:project)
create(:issue, :opened, project: project1, due_date: Date.tomorrow)
create(:issue, :opened, project: project1, due_date: Date.tomorrow)
create(:issue, :opened, project: project2, due_date: Date.tomorrow)
create(:issue, :closed, project: project_closed_issue, due_date: Date.tomorrow)
create(:issue, :opened, project: project_issue_due_another_day, due_date: Date.today)
expect(MailScheduler::IssueDueWorker).to receive(:bulk_perform_async).with([[project1.id], [project2.id]])
described_class.new.perform
end
end
end
require 'spec_helper'
describe MailScheduler::IssueDueWorker do
describe '#perform' do
let(:worker) { described_class.new }
let(:project) { create(:project) }
it 'sends emails for open issues due tomorrow in the project specified' do
issue1 = create(:issue, :opened, project: project, due_date: Date.tomorrow)
issue2 = create(:issue, :opened, project: project, due_date: Date.tomorrow)
create(:issue, :closed, project: project, due_date: Date.tomorrow) # closed
create(:issue, :opened, project: project, due_date: 2.days.from_now) # due on another day
create(:issue, :opened, due_date: Date.tomorrow) # different project
expect_any_instance_of(NotificationService).to receive(:issue_due).with(issue1)
expect_any_instance_of(NotificationService).to receive(:issue_due).with(issue2)
worker.perform(project.id)
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