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: { computed: {
...mapState([ ...mapState(['currentBranchId']),
'currentBranchId',
]),
newMergeRequestHelpText() {
return sprintf(
__('Creates a new branch from %{branchName} and re-directs to create a new merge request'),
{ branchName: this.currentBranchId },
);
},
commitToCurrentBranchText() { commitToCurrentBranchText() {
return sprintf( return sprintf(
__('Commit to %{branchName} branch'), __('Commit to %{branchName} branch'),
{ branchName: `<strong>${this.currentBranchId}</strong>` }, { branchName: `<strong class="monospace">${this.currentBranchId}</strong>` },
false, false,
); );
}, },
commitToNewBranchText() {
return sprintf(
__('Creates a new branch from %{branchName}'),
{ branchName: this.currentBranchId },
);
},
}, },
commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH, commitToCurrentBranch: consts.COMMIT_TO_CURRENT_BRANCH,
commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH, commitToNewBranch: consts.COMMIT_TO_NEW_BRANCH,
commitToNewBranchMR: consts.COMMIT_TO_NEW_BRANCH_MR, 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,
}, },
...@@ -26,27 +26,15 @@ ...@@ -26,27 +26,15 @@
required: false, required: false,
default: false, default: false,
}, },
helpText: {
type: String,
required: false,
default: null,
},
}, },
computed: { computed: {
...mapState('commit', [ ...mapState('commit', ['commitAction']),
'commitAction', ...mapGetters('commit', ['newBranchName']),
]),
...mapGetters('commit', [
'newBranchName',
]),
}, },
methods: { methods: {
...mapActions('commit', [ ...mapActions('commit', ['updateCommitAction', 'updateBranchName']),
'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) => { ) =>
new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) { if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state }); commit(types.TOGGLE_LOADING, { entry: state });
service.getProjectData(namespace, projectId) service
.getProjectData(namespace, projectId)
.then(res => res.data) .then(res => res.data)
.then((data) => { .then(data => {
commit(types.TOGGLE_LOADING, { entry: state }); commit(types.TOGGLE_LOADING, { entry: state });
commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data }); commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`); if (!state.currentProjectId)
commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
flash('Error loading project data. Please try again.', 'alert', document, null, false, true); flash(
'Error loading project data. Please try again.',
'alert',
document,
null,
false,
true,
);
reject(new Error(`Project not loaded ${namespace}/${projectId}`)); reject(new Error(`Project not loaded ${namespace}/${projectId}`));
}); });
} else { } else {
resolve(state.projects[`${namespace}/${projectId}`]); 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] ||
force
) {
service
.getBranchData(`${projectId}`, branchId)
.then(({ data }) => { .then(({ data }) => {
const { id } = data.commit; const { id } = data.commit;
commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data }); commit(types.SET_BRANCH, {
projectPath: `${projectId}`,
branchName: branchId,
branch: data,
});
commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id }); commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
commit(types.SET_CURRENT_BRANCH, branchId);
resolve(data); resolve(data);
}) })
.catch(() => { .catch(() => {
flash('Error loading branch data. Please try again.', 'alert', document, null, false, true); flash(
'Error loading branch data. Please try again.',
'alert',
document,
null,
false,
true,
);
reject(new Error(`Branch not loaded - ${projectId}/${branchId}`)); reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
}); });
} else { } else {
resolve(state.projects[`${projectId}`].branches[branchId]); 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
...@@ -55,6 +57,8 @@ module CacheMarkdownField ...@@ -55,6 +57,8 @@ module CacheMarkdownField
# 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
......
...@@ -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 source diff could not be displayed because it is too large. You can view the blob instead.
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
shared_examples 'with cache version' do |cache_version|
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
before do before do
thing.foo = updated_markdown thing.foo = updated_markdown
thing.save thing.save
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(cache_version) }
end
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_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,6 +130,9 @@ describe CacheMarkdownField do ...@@ -123,6 +130,9 @@ describe CacheMarkdownField do
end end
context 'a non-markdown field changed' do context 'a non-markdown field changed' do
shared_examples 'with cache version' do |cache_version|
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
before do before do
thing.bar = 'OK' thing.bar = 'OK'
thing.save thing.save
...@@ -131,7 +141,11 @@ describe CacheMarkdownField do ...@@ -131,7 +141,11 @@ describe CacheMarkdownField do
it { expect(thing.bar).to eq('OK') } it { expect(thing.bar).to eq('OK') }
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.cached_markdown_version).to eq(CacheMarkdownField::CACHE_VERSION) } it { expect(thing.cached_markdown_version).to eq(cache_version) }
end
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end end
context 'version is out of date' do context 'version is out of date' do
...@@ -142,10 +156,13 @@ describe CacheMarkdownField do ...@@ -142,10 +156,13 @@ 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
shared_examples 'with cache version' do |cache_version|
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
subject { thing.cached_html_up_to_date?(:foo) } subject { thing.cached_html_up_to_date?(:foo) }
it 'returns false when the version is absent' do it 'returns false when the version is absent' do
...@@ -167,7 +184,7 @@ describe CacheMarkdownField do ...@@ -167,7 +184,7 @@ describe CacheMarkdownField do
end end
it 'returns true when the version is just right' do it 'returns true when the version is just right' do
thing.cached_markdown_version = CacheMarkdownField::CACHE_VERSION thing.cached_markdown_version = cache_version
is_expected.to be_truthy is_expected.to be_truthy
end end
...@@ -198,6 +215,29 @@ describe CacheMarkdownField do ...@@ -198,6 +215,29 @@ describe CacheMarkdownField do
end end
end end
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
describe '#latest_cached_markdown_version' do
subject { thing.latest_cached_markdown_version }
it 'returns redcarpet version' do
thing.cached_markdown_version = CacheMarkdownField::CACHE_COMMONMARK_VERSION_START - 1
is_expected.to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
it 'returns commonmark version' do
thing.cached_markdown_version = CacheMarkdownField::CACHE_COMMONMARK_VERSION_START + 1
is_expected.to eq(CacheMarkdownField::CACHE_COMMONMARK_VERSION)
end
it 'returns default version when version is nil' do
thing.cached_markdown_version = nil
is_expected.to eq(CacheMarkdownField::CACHE_REDCARPET_VERSION)
end
end
describe '#refresh_markdown_cache' do describe '#refresh_markdown_cache' do
before do before do
thing.foo = updated_markdown thing.foo = updated_markdown
...@@ -221,11 +261,14 @@ describe CacheMarkdownField do ...@@ -221,11 +261,14 @@ 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
shared_examples 'with cache version' do |cache_version|
let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
before do before do
thing.foo = updated_markdown thing.foo = updated_markdown
end end
...@@ -248,12 +291,16 @@ describe CacheMarkdownField do ...@@ -248,12 +291,16 @@ describe CacheMarkdownField do
it 'saves the changes using #update_columns' do it 'saves the changes using #update_columns' do
expect(thing).to receive(:persisted?).and_return(true) expect(thing).to receive(:persisted?).and_return(true)
expect(thing).to receive(:update_columns) expect(thing).to receive(:update_columns)
.with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => CacheMarkdownField::CACHE_VERSION) .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => cache_version)
thing.refresh_markdown_cache! thing.refresh_markdown_cache!
end end
end end
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_REDCARPET_VERSION
it_behaves_like 'with cache version', CacheMarkdownField::CACHE_COMMONMARK_VERSION
end
describe '#banzai_render_context' do describe '#banzai_render_context' do
subject(:context) { thing.banzai_render_context(:foo) } subject(:context) { thing.banzai_render_context(:foo) }
...@@ -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
shared_examples 'metrics collector' do
it_behaves_like 'attempt counter collector'
it_behaves_like 'jobs queueing time histogram collector'
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) def execute(runner)
described_class.new(runner).execute.build described_class.new(runner).execute.build
end 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