Commit ed6f524c authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into fix/issue-move-rewrite-uploads

* master: (27 commits)
  Fix commit comment alignment
  minor cleanup in system_hook_spec
  Pre-calculate Emoji digests
  Clear .todo listener
  Change window.location to use turbolinks
  Make entire todo row clickable
  Add 8.6.2 CHANGELOG items
  Ensure uploads dir exists when running backup specs
  Move CarrierWave test env config to separate file
  Remove console logs
  Off the event initially
  Collapsed sidebar opens over instead of pushing content.
  Sidebar collapse update issue
  User selection from collapsed sidebar
  Add json response for user avatar in merge request
  Make changed values visible in minimized sidebar.
  Fixed MergeRequestController spec
  We need `sha` reference from `diff_base_commit` to generate the diff
  Use `diff_base_commit` instead of `target_branch` to generate diffs
  Isolate CarrierWave uploads in test enviroment
  ...
parents 5ac61d7b 0af8587d
...@@ -7,15 +7,34 @@ v 8.7.0 (unreleased) ...@@ -7,15 +7,34 @@ v 8.7.0 (unreleased)
- Expose label description in API (Mariusz Jachimowicz) - Expose label description in API (Mariusz Jachimowicz)
- Allow back dating on issues when created through the API - Allow back dating on issues when created through the API
- Fix avatar stretching by providing a cropping feature - Fix avatar stretching by providing a cropping feature
- Fix raw/rendered diff producing different results on merge requests !3450
- Add links to CI setup documentation from project settings and builds pages - Add links to CI setup documentation from project settings and builds pages
- Handle nil descriptions in Slack issue messages (Stan Hu) - Handle nil descriptions in Slack issue messages (Stan Hu)
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.) - Implement 'TODOs View' as an option for dashboard preferences !3379 (Elias W.)
- Gracefully handle notes on deleted commits in merge requests (Stan Hu) - Gracefully handle notes on deleted commits in merge requests (Stan Hu)
v 8.6.2 (unreleased) v 8.6.2
- Comments on confidential issues don't show up in activity feed to non-members - Fix dropdown alignment. !3298
- Fix NoMethodError when visiting CI root path at `/ci` - Fix issuable sidebar overlaps on tablet. !3299
- Make dropdowns pixel perfect. !3337
- Fix order of steps to prevent PostgreSQL errors when running migration. !3355
- Fix bold text in issuable sidebar. !3358
- Fix error with anonymous token in applications settings. !3362
- Fix the milestone 'upcoming' filter. !3364 + !3368
- Fix comments on confidential issues showing up in activity feed to non-members. !3375
- Fix `NoMethodError` when visiting CI root path at `/ci`. !3377
- Add a tooltip to new branch button in issue page. !3380
- Fix an issue hiding the password form when signed-in with a linked account. !3381
- Add links to CI setup documentation from project settings and builds pages. !3384
- Fix an issue with width of project select dropdown. !3386
- Remove redundant `require`s from Banzai files. !3391
- Fix error 500 with cancel button on issuable edit form. !3392 + !3417
- Fix background when editing a highlighted note. !3423
- Remove tabstop from the WIP toggle links. !3426
- Ensure private project snippets are not viewable by unauthorized people.
- Gracefully handle notes on deleted commits in merge requests (Stan Hu). !3402
- Fixed issue with notification settings not saving. !3452
v 8.6.1 v 8.6.1
- Add option to reload the schema before restoring a database backup. !2807 - Add option to reload the schema before restoring a database backup. !2807
...@@ -55,6 +74,7 @@ v 8.6.0 ...@@ -55,6 +74,7 @@ v 8.6.0
- Fix wiki search results point to raw source (Hiroyuki Sato) - Fix wiki search results point to raw source (Hiroyuki Sato)
- Don't load all of GitLab in mail_room - Don't load all of GitLab in mail_room
- Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner) - Add information about `image` and `services` field at `job` level in the `.gitlab-ci.yml` documentation (Pat Turner)
- Fall back to `In-Reply-To` and `References` headers when sub-addressing is not available (David Padilla)
- HTTP error pages work independently from location and config (Artem Sidorenko) - HTTP error pages work independently from location and config (Artem Sidorenko)
- Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set - Update `omniauth-saml` to 1.5.0 to allow for custom response attributes to be set
- Memoize @group in Admin::GroupsController (Yatish Mehta) - Memoize @group in Admin::GroupsController (Yatish Mehta)
......
...@@ -126,9 +126,9 @@ GEM ...@@ -126,9 +126,9 @@ GEM
coderay (1.1.0) coderay (1.1.0)
coercible (1.0.0) coercible (1.0.0)
descendants_tracker (~> 0.0.1) descendants_tracker (~> 0.0.1)
coffee-rails (4.1.0) coffee-rails (4.1.1)
coffee-script (>= 2.2.0) coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.0) railties (>= 4.0.0, < 5.1.x)
coffee-script (2.4.1) coffee-script (2.4.1)
coffee-script-source coffee-script-source
execjs execjs
......
...@@ -195,6 +195,8 @@ class GitLabDropdown ...@@ -195,6 +195,8 @@ class GitLabDropdown
if @options.filterable if @options.filterable
@dropdown.find(".dropdown-input-field").focus() @dropdown.find(".dropdown-input-field").focus()
@dropdown.trigger('shown.gl.dropdown')
hidden: (e) => hidden: (e) =>
if @options.filterable if @options.filterable
@dropdown @dropdown
...@@ -209,6 +211,8 @@ class GitLabDropdown ...@@ -209,6 +211,8 @@ class GitLabDropdown
if @options.hidden if @options.hidden
@options.hidden.call(@,e) @options.hidden.call(@,e)
@dropdown.trigger('hidden.gl.dropdown')
# Render the full menu # Render the full menu
renderMenu: (html) -> renderMenu: (html) ->
......
...@@ -9,7 +9,7 @@ class @IssuableContext ...@@ -9,7 +9,7 @@ class @IssuableContext
$(".issuable-sidebar .inline-update").on "change", ".js-assignee", -> $(".issuable-sidebar .inline-update").on "change", ".js-assignee", ->
$(this).submit() $(this).submit()
$(document).on "click",".edit-link", (e) -> $(document).off("click", ".edit-link").on "click",".edit-link", (e) ->
$block = $(@).parents('.block') $block = $(@).parents('.block')
$selectbox = $block.find('.selectbox') $selectbox = $block.find('.selectbox')
if $selectbox.is(':visible') if $selectbox.is(':visible')
......
...@@ -16,6 +16,7 @@ class @LabelsSelect ...@@ -16,6 +16,7 @@ class @LabelsSelect
abilityName = $dropdown.data('ability-name') abilityName = $dropdown.data('ability-name')
$selectbox = $dropdown.closest('.selectbox') $selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block') $block = $selectbox.closest('.block')
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span')
$value = $block.find('.value') $value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut() $loading = $block.find('.block-loading').fadeOut()
...@@ -142,6 +143,7 @@ class @LabelsSelect ...@@ -142,6 +143,7 @@ class @LabelsSelect
if not selected.length if not selected.length
data[abilityName].label_ids = [''] data[abilityName].label_ids = ['']
$loading.fadeIn() $loading.fadeIn()
$dropdown.trigger('loading.gl.dropdown')
$.ajax( $.ajax(
type: 'PUT' type: 'PUT'
url: issueUpdateURL url: issueUpdateURL
...@@ -149,15 +151,20 @@ class @LabelsSelect ...@@ -149,15 +151,20 @@ class @LabelsSelect
data: data data: data
).done (data) -> ).done (data) ->
$loading.fadeOut() $loading.fadeOut()
$dropdown.trigger('loaded.gl.dropdown')
$selectbox.hide() $selectbox.hide()
data.issueURLSplit = issueURLSplit data.issueURLSplit = issueURLSplit
if not data.labels.length labelCount = 0
template = labelNoneHTMLTemplate() if data.labels.length
else
template = labelHTMLTemplate(data) template = labelHTMLTemplate(data)
href = $value labelCount = data.labels.length
.show() else
template = labelNoneHTMLTemplate()
$value
.removeAttr('style')
.html(template) .html(template)
$sidebarCollapsedValue.text(labelCount)
$value $value
.find('a') .find('a')
.each((i) -> .each((i) ->
...@@ -226,7 +233,8 @@ class @LabelsSelect ...@@ -226,7 +233,8 @@ class @LabelsSelect
hidden: -> hidden: ->
$selectbox.hide() $selectbox.hide()
$value.show() # display:block overrides the hide-collapse rule
$value.removeAttr('style')
if $dropdown.hasClass 'js-multiselect' if $dropdown.hasClass 'js-multiselect'
saveLabelData() saveLabelData()
......
...@@ -18,6 +18,7 @@ class @MilestoneSelect ...@@ -18,6 +18,7 @@ class @MilestoneSelect
abilityName = $dropdown.data('ability-name') abilityName = $dropdown.data('ability-name')
$selectbox = $dropdown.closest('.selectbox') $selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block') $block = $selectbox.closest('.block')
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon')
$value = $block.find('.value') $value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut() $loading = $block.find('.block-loading').fadeOut()
...@@ -80,18 +81,14 @@ class @MilestoneSelect ...@@ -80,18 +81,14 @@ class @MilestoneSelect
milestone.name is selectedMilestone milestone.name is selectedMilestone
hidden: -> hidden: ->
$selectbox.hide() $selectbox.hide()
$value.show()
clicked: (selected) -> # display:block overrides the hide-collapse rule
$value.removeAttr('style')
clicked: (e) ->
if $dropdown.hasClass 'js-filter-bulk-update' if $dropdown.hasClass 'js-filter-bulk-update'
return return
if $dropdown.hasClass('js-filter-submit') if $dropdown.hasClass 'js-filter-submit'
if selected.name?
selectedMilestone = selected.name
else if selected.title?
selectedMilestone = selected.title
else
selectedMilestone = ''
$dropdown.parents('form').submit() $dropdown.parents('form').submit()
else else
selected = $selectbox selected = $selectbox
...@@ -102,20 +99,22 @@ class @MilestoneSelect ...@@ -102,20 +99,22 @@ class @MilestoneSelect
data[abilityName].milestone_id = selected data[abilityName].milestone_id = selected
$loading $loading
.fadeIn() .fadeIn()
$dropdown.trigger('loading.gl.dropdown')
$.ajax( $.ajax(
type: 'PUT' type: 'PUT'
url: issueUpdateURL url: issueUpdateURL
data: data data: data
).done (data) -> ).done (data) ->
$dropdown.trigger('loaded.gl.dropdown')
$loading.fadeOut() $loading.fadeOut()
$selectbox.hide() $selectbox.hide()
$milestoneLink = $value $value.removeAttr('style')
.show()
.find('a')
if data.milestone? if data.milestone?
data.milestone.namespace = _this.currentProject.namespace data.milestone.namespace = _this.currentProject.namespace
data.milestone.path = _this.currentProject.path data.milestone.path = _this.currentProject.path
$value.html(milestoneLinkTemplate(data.milestone)) $value.html(milestoneLinkTemplate(data.milestone))
$sidebarCollapsedValue.find('span').text(data.milestone.title)
else else
$value.html(milestoneLinkNoneTemplate) $value.html(milestoneLinkNoneTemplate)
$sidebarCollapsedValue.find('span').text('No')
) )
\ No newline at end of file
class @Sidebar
constructor: (currentUser) ->
@addEventListeners()
addEventListeners: ->
$('aside').on('click', '.sidebar-collapsed-icon', @sidebarCollapseClicked)
$('.dropdown').on('hidden.gl.dropdown', @sidebarDropdownHidden)
$('.dropdown').on('loading.gl.dropdown', @sidebarDropdownLoading)
$('.dropdown').on('loaded.gl.dropdown', @sidebarDropdownLoaded)
sidebarDropdownLoading: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
img = $sidebarCollapsedIcon.find('img')
i = $sidebarCollapsedIcon.find('i')
$loading = $('<i class="fa fa-spinner fa-spin"></i>')
if img.length
img.before($loading)
img.hide()
else if i.length
i.before($loading)
i.hide()
sidebarDropdownLoaded: (e) ->
$sidebarCollapsedIcon = $(@).closest('.block').find('.sidebar-collapsed-icon')
img = $sidebarCollapsedIcon.find('img')
$sidebarCollapsedIcon.find('i.fa-spin').remove()
i = $sidebarCollapsedIcon.find('i')
if img.length
img.show()
else
i.show()
sidebarCollapseClicked: (e) ->
e.preventDefault()
$block = $(@).closest('.block')
$('aside')
.find('.gutter-toggle')
.trigger('click')
$editLink = $block.find('.edit-link')
if $editLink.length
$editLink.trigger('click')
$block.addClass('collapse-after-update')
$('.page-with-sidebar').addClass('with-overlay')
sidebarDropdownHidden: (e) ->
$block = $(@).closest('.block')
if $block.hasClass('collapse-after-update')
$block.removeClass('collapse-after-update')
$('.page-with-sidebar').removeClass('with-overlay')
$('aside')
.find('.gutter-toggle')
.trigger('click')
\ No newline at end of file
...@@ -6,10 +6,12 @@ class @Todos ...@@ -6,10 +6,12 @@ class @Todos
clearListeners: -> clearListeners: ->
$('.done-todo').off('click') $('.done-todo').off('click')
$('.js-todos-mark-all').off('click') $('.js-todos-mark-all').off('click')
$('.todo').off('click')
initBtnListeners: -> initBtnListeners: ->
$('.done-todo').on('click', @doneClicked) $('.done-todo').on('click', @doneClicked)
$('.js-todos-mark-all').on('click', @allDoneClicked) $('.js-todos-mark-all').on('click', @allDoneClicked)
$('.todo').on('click', @goToTodoUrl)
doneClicked: (e) => doneClicked: (e) =>
e.preventDefault() e.preventDefault()
...@@ -54,3 +56,6 @@ class @Todos ...@@ -54,3 +56,6 @@ class @Todos
updateBadges: (data) -> updateBadges: (data) ->
$('.todos-pending .badge, .todos-pending-count').text data.count $('.todos-pending .badge, .todos-pending-count').text data.count
$('.todos-done .badge').text data.done_count $('.todos-done .badge').text data.done_count
goToTodoUrl: ->
Turbolinks.visit($(this).data('url'))
...@@ -19,6 +19,7 @@ class @UsersSelect ...@@ -19,6 +19,7 @@ class @UsersSelect
$block = $selectbox.closest('.block') $block = $selectbox.closest('.block')
abilityName = $dropdown.data('ability-name') abilityName = $dropdown.data('ability-name')
$value = $block.find('.value') $value = $block.find('.value')
$collapsedSidebar = $block.find('.sidebar-collapsed-user')
$loading = $block.find('.block-loading').fadeOut() $loading = $block.find('.block-loading').fadeOut()
$block.on('click', '.js-assign-yourself', (e) => $block.on('click', '.js-assign-yourself', (e) =>
...@@ -32,12 +33,14 @@ class @UsersSelect ...@@ -32,12 +33,14 @@ class @UsersSelect
data[abilityName].assignee_id = selected data[abilityName].assignee_id = selected
$loading $loading
.fadeIn() .fadeIn()
$dropdown.trigger('loading.gl.dropdown')
$.ajax( $.ajax(
type: 'PUT' type: 'PUT'
dataType: 'json' dataType: 'json'
url: issueURL url: issueURL
data: data data: data
).done (data) -> ).done (data) ->
$dropdown.trigger('loaded.gl.dropdown')
$loading.fadeOut() $loading.fadeOut()
$selectbox.hide() $selectbox.hide()
...@@ -51,11 +54,22 @@ class @UsersSelect ...@@ -51,11 +54,22 @@ class @UsersSelect
name: 'Unassigned' name: 'Unassigned'
username: '' username: ''
avatar: '' avatar: ''
$value.html(assigneeTemplate(user))
$collapsedSidebar.html(collapsedAssigneeTemplate(user))
$value.html(noAssigneeTemplate(user))
$value.find('a').attr('href')
noAssigneeTemplate = _.template( collapsedAssigneeTemplate = _.template(
'<% if( avatar ) { %>
<a class="author_link" href="/u/<%= username %>">
<img width="24" class="avatar avatar-inline s24" alt="" src="<%= avatar %>">
<span class="author">Toni Boehm</span>
</a>
<% } else { %>
<i class="fa fa-user"></i>
<% } %>'
)
assigneeTemplate = _.template(
'<% if (username) { %> '<% if (username) { %>
<a class="author_link " href="/u/<%= username %>"> <a class="author_link " href="/u/<%= username %>">
<% if( avatar ) { %> <% if( avatar ) { %>
...@@ -131,7 +145,8 @@ class @UsersSelect ...@@ -131,7 +145,8 @@ class @UsersSelect
hidden: (e) -> hidden: (e) ->
$selectbox.hide() $selectbox.hide()
$value.show() # display:block overrides the hide-collapse rule
$value.removeAttr('style')
clicked: (user) -> clicked: (user) ->
page = $('body').data 'page' page = $('body').data 'page'
......
...@@ -288,6 +288,10 @@ ...@@ -288,6 +288,10 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
padding-right: $sidebar_collapsed_width; padding-right: $sidebar_collapsed_width;
} }
.sidebar-collapsed-icon {
cursor: pointer;
}
} }
.right-sidebar-expanded { .right-sidebar-expanded {
...@@ -300,4 +304,8 @@ ...@@ -300,4 +304,8 @@
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
padding-right: $gutter_width; padding-right: $gutter_width;
} }
&.with-overlay {
padding-right: $sidebar_collapsed_width;
}
} }
...@@ -13,6 +13,12 @@ ...@@ -13,6 +13,12 @@
} }
} }
.todo {
&:hover {
cursor: pointer;
}
}
.todo-item { .todo-item {
.todo-title { .todo-title {
@include str-truncated(calc(100% - 174px)); @include str-truncated(calc(100% - 174px));
......
...@@ -2,11 +2,12 @@ class Projects::BadgesController < Projects::ApplicationController ...@@ -2,11 +2,12 @@ class Projects::BadgesController < Projects::ApplicationController
before_action :no_cache_headers before_action :no_cache_headers
def build def build
badge = Gitlab::Badge::Build.new(project, params[:ref])
respond_to do |format| respond_to do |format|
format.html { render_404 } format.html { render_404 }
format.svg do format.svg do
image = Ci::ImageForBuildService.new.execute(project, ref: params[:ref]) send_data(badge.data, type: badge.type, disposition: 'inline')
send_file(image.path, filename: image.name, disposition: 'inline', type: 'image/svg+xml')
end end
end end
end end
......
...@@ -57,8 +57,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -57,8 +57,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json { render json: @merge_request } format.json { render json: @merge_request }
format.diff { render text: @merge_request.to_diff(current_user) } format.diff { render text: @merge_request.to_diff }
format.patch { render text: @merge_request.to_patch(current_user) } format.patch { render text: @merge_request.to_patch }
end end
end end
...@@ -154,7 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -154,7 +154,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.target_project, @merge_request]) @merge_request.target_project, @merge_request])
end end
format.json do format.json do
render json: @merge_request.to_json(include: [:milestone, :labels, :assignee]) render json: @merge_request.to_json(include: [:milestone, :labels, assignee: { methods: :avatar_url }])
end end
end end
else else
......
...@@ -138,7 +138,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -138,7 +138,7 @@ class ProjectsController < Projects::ApplicationController
participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id) participants = ::Projects::ParticipantsService.new(@project, current_user).execute(note_type, note_id)
@suggestions = { @suggestions = {
emojis: autocomplete_emojis, emojis: AwardEmoji.urls,
issues: autocomplete.issues, issues: autocomplete.issues,
mergerequests: autocomplete.merge_requests, mergerequests: autocomplete.merge_requests,
members: participants members: participants
...@@ -235,17 +235,6 @@ class ProjectsController < Projects::ApplicationController ...@@ -235,17 +235,6 @@ class ProjectsController < Projects::ApplicationController
) )
end end
def autocomplete_emojis
Rails.cache.fetch("autocomplete-emoji-#{Gemojione::VERSION}") do
Emoji.emojis.map do |name, emoji|
{
name: name,
path: view_context.image_url("#{emoji["unicode"]}.png")
}
end
end
end
def repo_exists? def repo_exists?
project.repository_exists? && !project.empty_repo? project.repository_exists? && !project.empty_repo?
end end
......
...@@ -110,6 +110,10 @@ class Notify < BaseMailer ...@@ -110,6 +110,10 @@ class Notify < BaseMailer
headers['Reply-To'] = address headers['Reply-To'] = address
fallback_reply_message_id = "<reply-#{reply_key}@#{Gitlab.config.gitlab.host}>".freeze
headers['References'] ||= ''
headers['References'] << ' ' << fallback_reply_message_id
@reply_by_email = true @reply_by_email = true
end end
......
...@@ -331,15 +331,15 @@ class MergeRequest < ActiveRecord::Base ...@@ -331,15 +331,15 @@ class MergeRequest < ActiveRecord::Base
# Returns the raw diff for this merge request # Returns the raw diff for this merge request
# #
# see "git diff" # see "git diff"
def to_diff(current_user) def to_diff
target_project.repository.diff_text(target_branch, source_sha) target_project.repository.diff_text(diff_base_commit.sha, source_sha)
end end
# Returns the commit as a series of email patches. # Returns the commit as a series of email patches.
# #
# see "git format-patch" # see "git format-patch"
def to_patch(current_user) def to_patch
target_project.repository.format_patch(target_branch, source_sha) target_project.repository.format_patch(diff_base_commit.sha, source_sha)
end end
def hook_attrs def hook_attrs
......
...@@ -335,6 +335,8 @@ class Repository ...@@ -335,6 +335,8 @@ class Repository
# Runs code just before a repository is deleted. # Runs code just before a repository is deleted.
def before_delete def before_delete
expire_exists_cache
expire_cache if exists? expire_cache if exists?
expire_root_ref_cache expire_root_ref_cache
......
%li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo) } %li{class: "todo todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data:{url: todo_target_path(todo)} }
.todo-item.todo-block .todo-item.todo-block
= image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:'' = image_tag avatar_icon(todo.author_email, 40), class: 'avatar s40', alt:''
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
Show/hide discussion Show/hide discussion
%div %div
= link_to_member(@project, note.author, avatar: false) = link_to_member(@project, note.author, avatar: false)
%p started a discussion on #{commit_description} started a discussion on #{commit_description}
- if commit - if commit
= link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace') = link_to(commit.short_id, namespace_project_commit_path(note.project.namespace, note.project, note.noteable), class: 'monospace')
.last-update.hide.js-toggle-content .last-update.hide.js-toggle-content
......
...@@ -150,3 +150,4 @@ ...@@ -150,3 +150,4 @@
new LabelsSelect(); new LabelsSelect();
new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}'); new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}');
new Subscription('.subscription') new Subscription('.subscription')
new Sidebar();
\ No newline at end of file
...@@ -5,6 +5,9 @@ class ProjectCacheWorker ...@@ -5,6 +5,9 @@ class ProjectCacheWorker
def perform(project_id) def perform(project_id)
project = Project.find(project_id) project = Project.find(project_id)
return unless project.repository.exists?
project.update_repository_size project.update_repository_size
project.update_commit_count project.update_commit_count
......
...@@ -106,7 +106,7 @@ production: &base ...@@ -106,7 +106,7 @@ production: &base
enabled: false enabled: false
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com" address: "gitlab-incoming+%{key}@gmail.com"
# Email account username # Email account username
......
...@@ -17,7 +17,7 @@ if File.exists?(config_file) ...@@ -17,7 +17,7 @@ if File.exists?(config_file)
config['start_tls'] = false if config['start_tls'].nil? config['start_tls'] = false if config['start_tls'].nil?
config['mailbox'] = "inbox" if config['mailbox'].nil? config['mailbox'] = "inbox" if config['mailbox'].nil?
if config['enabled'] && config['address'] && config['address'].include?('%{key}') if config['enabled'] && config['address']
redis_url = Gitlab::RedisConfig.new(rails_env).url redis_url = Gitlab::RedisConfig.new(rails_env).url
%> %>
- -
......
# Reply by email # Reply by email
GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails. GitLab can be set up to allow users to comment on issues and merge requests by
replying to notification emails.
## Get a mailbox ## Requirement
Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises. Reply by email requires an IMAP-enabled email account. GitLab allows you to use
three strategies for this feature:
- using email sub-addressing
- using a dedicated email address
- using a catch-all mailbox
If you want to use Gmail / Google Apps with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255). ### Email sub-addressing
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these instructions](./postfix.md). **If your provider or server supports email sub-addressing, we recommend using it.**
[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
a feature where any email to `user+some_arbitrary_tag@example.com` will end up
in the mailbox for `user@example.com`, and is supported by providers such as
Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix
mail server which you can run on-premises.
### Dedicated email address
This solution is really simple to set up: you just have to create an email
address dedicated to receive your users' replies to GitLab notifications.
### Catch-all mailbox
A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
"catch all" the emails addressed to the domain that do not exist in the mail
server.
## How it works?
### 1. GitLab sends a notification email
When GitLab sends a notification and Reply by email is enabled, the `Reply-To`
header is set to the address defined in your GitLab configuration, with the
`%{key}` placeholder (if present) replaced by a specific "reply key". In
addition, this "reply key" is also added to the `References` header.
### 2. You reply to the notification email
When you reply to the notification email, your email client will:
- send the email to the `Reply-To` address it got from the notification email
- set the `In-Reply-To` header to the value of the `Message-ID` header from the
notification email
- set the `References` header to the value of the `Message-ID` plus the value of
the notification email's `References` header.
### 3. GitLab receives your reply to the notification email
When GitLab receives your reply, it will look for the "reply key" in the
following headers, in this order:
1. the `To` header
1. the `References` header
If it finds a reply key, it will be able to leave your reply as a comment on
the entity the notification was about (issue, merge request, commit...).
For more details about the `Message-ID`, `In-Reply-To`, and `References headers`,
please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
## Set it up ## Set it up
If you want to use Gmail / Google Apps with Reply by email, make sure you have
[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
To set up a basic Postfix mail server with IMAP access on Ubuntu, follow
[these instructions](./postfix.md).
### Omnibus package installations ### Omnibus package installations
1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature and fill in the details for your specific IMAP server and email account: 1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
feature and fill in the details for your specific IMAP server and email account:
```ruby ```ruby
# Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
gitlab_rails['incoming_email_enabled'] = true gitlab_rails['incoming_email_enabled'] = true
# The email address including a placeholder for the key that references the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com" gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
# Email account username # Email account username
...@@ -49,7 +112,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -49,7 +112,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
gitlab_rails['incoming_email_enabled'] = true gitlab_rails['incoming_email_enabled'] = true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com" gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
# Email account username # Email account username
...@@ -72,8 +135,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -72,8 +135,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
gitlab_rails['incoming_email_mailbox_name'] = "inbox" gitlab_rails['incoming_email_mailbox_name'] = "inbox"
``` ```
As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
1. Reconfigure GitLab and restart mailroom for the changes to take effect: 1. Reconfigure GitLab and restart mailroom for the changes to take effect:
```sh ```sh
...@@ -97,7 +158,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -97,7 +158,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
cd /home/git/gitlab cd /home/git/gitlab
``` ```
1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account: 1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
and fill in the details for your specific IMAP server and email account:
```sh ```sh
sudo editor config/gitlab.yml sudo editor config/gitlab.yml
...@@ -109,7 +171,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -109,7 +171,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
enabled: true enabled: true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "incoming+%{key}@gitlab.example.com" address: "incoming+%{key}@gitlab.example.com"
# Email account username # Email account username
...@@ -138,7 +200,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -138,7 +200,7 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
enabled: true enabled: true
# The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, after a `+` character, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com" address: "gitlab-incoming+%{key}@gmail.com"
# Email account username # Email account username
...@@ -161,8 +223,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -161,8 +223,6 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
mailbox: "inbox" mailbox: "inbox"
``` ```
As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
1. Enable `mail_room` in the init script at `/etc/default/gitlab`: 1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
```sh ```sh
...@@ -195,8 +255,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these ...@@ -195,8 +255,8 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
incoming_email: incoming_email:
enabled: true enabled: true
# The email address including a placeholder for the key that references the item being replied to. # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
# The `%{key}` placeholder is added after the user part, before the `@`. # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
address: "gitlab-incoming+%{key}@gmail.com" address: "gitlab-incoming+%{key}@gmail.com"
# Email account username # Email account username
......
...@@ -36,3 +36,8 @@ Feature: Dashboard Todos ...@@ -36,3 +36,8 @@ Feature: Dashboard Todos
Scenario: I filter by action Scenario: I filter by action
Given I filter by "Mentioned" Given I filter by "Mentioned"
Then I should not see todos related to "Assignments" in the list Then I should not see todos related to "Assignments" in the list
@javascript
Scenario: I click on a todo row
Given I click on the todo
Then I should be directed to the corresponding page
...@@ -88,6 +88,14 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps ...@@ -88,6 +88,14 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
should_not_see_todo "John Doe assigned you issue ##{issue.iid}" should_not_see_todo "John Doe assigned you issue ##{issue.iid}"
end end
step 'I click on the todo' do
find('.todo:nth-child(1)').click
end
step 'I should be directed to the corresponding page' do
page.should have_css('.identifier', text: 'Merge Request !1')
end
def should_see_todo(position, title, body, pending = true) def should_see_todo(position, title, body, pending = true)
page.within(".todo:nth-child(#{position})") do page.within(".todo:nth-child(#{position})") do
expect(page).to have_content title expect(page).to have_content title
......
This diff is collapsed.
...@@ -48,4 +48,23 @@ class AwardEmoji ...@@ -48,4 +48,23 @@ class AwardEmoji
JSON.parse(File.read(json_path)) JSON.parse(File.read(json_path))
end end
end end
# Returns an Array of Emoji names and their asset URLs.
def self.urls
@urls ||= begin
path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
prefix = Gitlab::Application.config.assets.prefix
digest = Gitlab::Application.config.assets.digest
JSON.parse(File.read(path)).map do |hash|
if digest
fname = "#{hash['unicode']}-#{hash['digest']}"
else
fname = hash['unicode']
end
{ name: hash['name'], path: "#{prefix}/#{fname}.png" }
end
end
end
end end
module Gitlab
module Badge
##
# Build badge
#
class Build
def initialize(project, ref)
@image = ::Ci::ImageForBuildService.new.execute(project, ref: ref)
end
def to_s
@image[:name].sub(/\.svg$/, '')
end
def type
'image/svg+xml'
end
def data
File.read(@image[:path])
end
end
end
end
...@@ -63,6 +63,10 @@ module Gitlab ...@@ -63,6 +63,10 @@ module Gitlab
end end
def reply_key def reply_key
key_from_to_header || key_from_additional_headers
end
def key_from_to_header
key = nil key = nil
message.to.each do |address| message.to.each do |address|
key = Gitlab::IncomingEmail.key_from_address(address) key = Gitlab::IncomingEmail.key_from_address(address)
...@@ -72,6 +76,17 @@ module Gitlab ...@@ -72,6 +76,17 @@ module Gitlab
key key
end end
def key_from_additional_headers
reply_key = nil
Array(message.references).each do |message_id|
reply_key = Gitlab::IncomingEmail.key_from_fallback_reply_message_id(message_id)
break if reply_key
end
reply_key
end
def sent_notification def sent_notification
return nil unless reply_key return nil unless reply_key
......
module Gitlab module Gitlab
module IncomingEmail module IncomingEmail
class << self class << self
def enabled? FALLBACK_REPLY_MESSAGE_ID_REGEX = /\Areply\-(.+)@#{Gitlab.config.gitlab.host}\Z/.freeze
config.enabled && address_formatted_correctly?
end
def address_formatted_correctly? def enabled?
config.address && config.enabled && config.address
config.address.include?("%{key}")
end end
def reply_address(key) def reply_address(key)
...@@ -24,6 +21,13 @@ module Gitlab ...@@ -24,6 +21,13 @@ module Gitlab
match[1] match[1]
end end
def key_from_fallback_reply_message_id(message_id)
match = message_id.match(FALLBACK_REPLY_MESSAGE_ID_REGEX)
return unless match
match[1]
end
def config def config
Gitlab.config.incoming_email Gitlab.config.incoming_email
end end
......
# This task will generate a standard and Retina sprite of all of the current namespace :gemojione do
# Gemojione Emojis, with the accompanying SCSS map. desc 'Generates Emoji SHA256 digests'
# task digests: :environment do
# It will not appear in `rake -T` output, and the dependent gems are not require 'digest/sha2'
# included in the Gemfile by default, because this task will only be needed require 'json'
# occasionally, such as when new Emojis are added to Gemojione.
dir = Gemojione.index.images_path
begin
digests = AwardEmoji.emojis.map do |name, emoji_hash|
fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
digest = Digest::SHA256.file(fpath).hexdigest
{ name: name, unicode: emoji_hash['unicode'], digest: digest }
end
out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
File.open(out, 'w') do |handle|
handle.write(JSON.pretty_generate(digests))
end
end
# This task will generate a standard and Retina sprite of all of the current
# Gemojione Emojis, with the accompanying SCSS map.
#
# It will not appear in `rake -T` output, and the dependent gems are not
# included in the Gemfile by default, because this task will only be needed
# occasionally, such as when new Emojis are added to Gemojione.
task sprite: :environment do
begin
require 'sprite_factory' require 'sprite_factory'
require 'rmagick' require 'rmagick'
rescue LoadError rescue LoadError
# noop # noop
end end
namespace :gemojione do
task sprite: :environment do
check_requirements! check_requirements!
SIZE = 20 SIZE = 20
......
...@@ -623,7 +623,6 @@ namespace :gitlab do ...@@ -623,7 +623,6 @@ namespace :gitlab do
start_checking "Reply by email" start_checking "Reply by email"
if Gitlab.config.incoming_email.enabled if Gitlab.config.incoming_email.enabled
check_address_formatted_correctly
check_imap_authentication check_imap_authentication
if Rails.env.production? if Rails.env.production?
...@@ -643,20 +642,6 @@ namespace :gitlab do ...@@ -643,20 +642,6 @@ namespace :gitlab do
# Checks # Checks
######################## ########################
def check_address_formatted_correctly
print "Address formatted correctly? ... "
if Gitlab::IncomingEmail.address_formatted_correctly?
puts "yes".green
else
puts "no".red
try_fixing_it(
"Make sure that the address in config/gitlab.yml includes the '%{key}' placeholder."
)
fix_and_rerun
end
end
def check_initd_configured_correctly def check_initd_configured_correctly
print "Init.d configured correctly? ... " print "Init.d configured correctly? ... "
......
...@@ -63,7 +63,7 @@ describe Projects::MergeRequestsController do ...@@ -63,7 +63,7 @@ describe Projects::MergeRequestsController do
id: merge_request.iid, id: merge_request.iid,
format: format) format: format)
expect(response.body).to eq((merge_request.send(:"to_#{format}",user)).to_s) expect(response.body).to eq((merge_request.send(:"to_#{format}")).to_s)
end end
it "should not escape Html" do it "should not escape Html" do
......
Return-Path: <jake@adventuretime.ooo>
Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo>
To: reply@appmail.adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
In-Reply-To: <issue_1@localhost>
References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
Mime-Version: 1.0
Content-Type: text/plain;
charset=ISO-8859-1
Content-Transfer-Encoding: 7bit
X-Sieve: CMU Sieve 2.2
X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
13 Jun 2013 14:03:48 -0700 (PDT)
X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
I could not disagree more. I am obviously biased but adventure time is the
greatest show ever created. Everyone should watch it.
- Jake out
On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
>
>
>
> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
>
> ---
> hey guys everyone knows adventure time sucks!
>
> ---
> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
>
> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
>
...@@ -7,6 +7,8 @@ Date: Thu, 13 Jun 2013 17:03:48 -0400 ...@@ -7,6 +7,8 @@ Date: Thu, 13 Jun 2013 17:03:48 -0400
From: Jake the Dog <jake@adventuretime.ooo> From: Jake the Dog <jake@adventuretime.ooo>
To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
In-Reply-To: <issue_1@localhost>
References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux' Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
Mime-Version: 1.0 Mime-Version: 1.0
Content-Type: text/plain; Content-Type: text/plain;
......
require 'spec_helper'
describe AwardEmoji do
describe '.urls' do
subject { AwardEmoji.urls }
it { is_expected.to be_an_instance_of(Array) }
it { is_expected.to_not be_empty }
context 'every Hash in the Array' do
it 'has the correct keys and values' do
subject.each do |hash|
expect(hash[:name]).to be_an_instance_of(String)
expect(hash[:path]).to be_an_instance_of(String)
end
end
end
end
end
require 'spec_helper'
describe Gitlab::Badge::Build do
let(:project) { create(:project) }
let(:sha) { project.commit.sha }
let(:badge) { described_class.new(project, 'master') }
describe '#type' do
subject { badge.type }
it { is_expected.to eq 'image/svg+xml' }
end
context 'build exists' do
let(:ci_commit) { create(:ci_commit, project: project, sha: sha) }
let!(:build) { create(:ci_build, commit: ci_commit) }
context 'build success' do
before { build.success! }
describe '#to_s' do
subject { badge.to_s }
it { is_expected.to eq 'build-success' }
end
describe '#data' do
let(:data) { badge.data }
it 'contains infromation about success' do
expect(status_node(data, 'success')).to be_truthy
end
end
end
context 'build failed' do
before { build.drop! }
describe '#to_s' do
subject { badge.to_s }
it { is_expected.to eq 'build-failed' }
end
describe '#data' do
let(:data) { badge.data }
it 'contains infromation about failure' do
expect(status_node(data, 'failed')).to be_truthy
end
end
end
end
context 'build does not exist' do
describe '#to_s' do
subject { badge.to_s }
it { is_expected.to eq 'build-unknown' }
end
describe '#data' do
let(:data) { badge.data }
it 'contains infromation about unknown build' do
expect(status_node(data, 'unknown')).to be_truthy
end
end
end
def status_node(data, status)
xml = Nokogiri::XML.parse(data)
xml.at(%Q{text:contains("#{status}")})
end
end
...@@ -3,6 +3,7 @@ require "spec_helper" ...@@ -3,6 +3,7 @@ require "spec_helper"
describe Gitlab::Email::Receiver, lib: true do describe Gitlab::Email::Receiver, lib: true do
before do before do
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
stub_config_setting(host: 'localhost')
end end
let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
...@@ -137,5 +138,27 @@ describe Gitlab::Email::Receiver, lib: true do ...@@ -137,5 +138,27 @@ describe Gitlab::Email::Receiver, lib: true do
expect(note.note).to include(markdown) expect(note.note).to include(markdown)
end end
context 'when sub-addressing is not supported' do
before do
stub_incoming_email_setting(enabled: true, address: nil)
end
shared_examples 'an email that contains a reply key' do |header|
it "fetches the reply key from the #{header} header and creates a comment" do
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
note = noteable.notes.last
expect(note.author).to eq(sent_notification.recipient)
expect(note.note).to include('I could not disagree more.')
end
end
context 'reply key is in the References header' do
let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') }
it_behaves_like 'an email that contains a reply key', 'References'
end
end
end end
end end
...@@ -7,27 +7,11 @@ describe Gitlab::IncomingEmail, lib: true do ...@@ -7,27 +7,11 @@ describe Gitlab::IncomingEmail, lib: true do
stub_incoming_email_setting(enabled: true) stub_incoming_email_setting(enabled: true)
end end
context "when the address is valid" do it 'returns true' do
before do
stub_incoming_email_setting(address: "replies+%{key}@example.com")
end
it "returns true" do
expect(described_class.enabled?).to be_truthy expect(described_class.enabled?).to be_truthy
end end
end end
context "when the address is invalid" do
before do
stub_incoming_email_setting(address: "replies@example.com")
end
it "returns false" do
expect(described_class.enabled?).to be_falsey
end
end
end
context "when reply by email is disabled" do context "when reply by email is disabled" do
before do before do
stub_incoming_email_setting(enabled: false) stub_incoming_email_setting(enabled: false)
...@@ -58,4 +42,10 @@ describe Gitlab::IncomingEmail, lib: true do ...@@ -58,4 +42,10 @@ describe Gitlab::IncomingEmail, lib: true do
expect(described_class.key_from_address("replies+key@example.com")).to eq("key") expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
end end
end end
context 'self.key_from_fallback_reply_message_id' do
it 'returns reply key' do
expect(described_class.key_from_fallback_reply_message_id('reply-key@localhost')).to eq('key')
end
end
end end
...@@ -35,7 +35,9 @@ describe Notify do ...@@ -35,7 +35,9 @@ describe Notify do
subject { Notify.new_issue_email(issue.assignee_id, issue.id) } subject { Notify.new_issue_email(issue.assignee_id, issue.id) }
it_behaves_like 'an assignee email' it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'issue' it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
let(:model) { issue }
end
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread' it_behaves_like 'an unsubscribeable thread'
...@@ -73,9 +75,11 @@ describe Notify do ...@@ -73,9 +75,11 @@ describe Notify do
subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) } subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) }
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like "an unsubscribeable thread" it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -104,7 +108,9 @@ describe Notify do ...@@ -104,7 +108,9 @@ describe Notify do
subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) } subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) }
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'a user cannot unsubscribe through footer link' it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer' it_behaves_like 'an email with a labels subscriptions link in its footer'
...@@ -132,7 +138,9 @@ describe Notify do ...@@ -132,7 +138,9 @@ describe Notify do
let(:status) { 'closed' } let(:status) { 'closed' }
subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) } subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread' it_behaves_like 'an unsubscribeable thread'
...@@ -163,7 +171,9 @@ describe Notify do ...@@ -163,7 +171,9 @@ describe Notify do
let(:new_issue) { create(:issue) } let(:new_issue) { create(:issue) }
subject { Notify.issue_moved_email(recipient, issue, new_issue, current_user) } subject { Notify.issue_moved_email(recipient, issue, new_issue, current_user) }
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread' it_behaves_like 'an unsubscribeable thread'
...@@ -196,9 +206,11 @@ describe Notify do ...@@ -196,9 +206,11 @@ describe Notify do
subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) } subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
it_behaves_like 'an assignee email' it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'merge_request' it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread" it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
...@@ -216,10 +228,6 @@ describe Notify do ...@@ -216,10 +228,6 @@ describe Notify do
is_expected.to have_body_text /#{merge_request.target_branch}/ is_expected.to have_body_text /#{merge_request.target_branch}/
end end
it 'has the correct message-id set' do
is_expected.to have_header 'Message-ID', "<merge_request_#{merge_request.id}@#{Gitlab.config.gitlab.host}>"
end
context 'when enabled email_author_in_body' do context 'when enabled email_author_in_body' do
before do before do
allow(current_application_settings).to receive(:email_author_in_body).and_return(true) allow(current_application_settings).to receive(:email_author_in_body).and_return(true)
...@@ -247,7 +255,9 @@ describe Notify do ...@@ -247,7 +255,9 @@ describe Notify do
subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) } subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread" it_behaves_like "an unsubscribeable thread"
...@@ -278,7 +288,9 @@ describe Notify do ...@@ -278,7 +288,9 @@ describe Notify do
subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) } subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) }
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'a user cannot unsubscribe through footer link' it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer' it_behaves_like 'an email with a labels subscriptions link in its footer'
...@@ -306,9 +318,11 @@ describe Notify do ...@@ -306,9 +318,11 @@ describe Notify do
let(:status) { 'reopened' } let(:status) { 'reopened' }
subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) } subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread" it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do it 'is sent as the author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -337,9 +351,11 @@ describe Notify do ...@@ -337,9 +351,11 @@ describe Notify do
subject { Notify.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) } subject { Notify.merged_merge_request_email(recipient.id, merge_request.id, merge_author.id) }
it_behaves_like 'a multiple recipients email' it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread" it_behaves_like 'an unsubscribeable thread'
it 'is sent as the merge author' do it 'is sent as the merge author' do
sender = subject.header[:from].addrs[0] sender = subject.header[:from].addrs[0]
...@@ -456,9 +472,11 @@ describe Notify do ...@@ -456,9 +472,11 @@ describe Notify do
subject { Notify.note_commit_email(recipient.id, note.id) } subject { Notify.note_commit_email(recipient.id, note.id) }
it_behaves_like 'a note email' it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'commit' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { commit }
end
it_behaves_like 'it should show Gmail Actions View Commit link' it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like "a user cannot unsubscribe through footer link" it_behaves_like 'a user cannot unsubscribe through footer link'
it 'has the correct subject' do it 'has the correct subject' do
is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/
...@@ -477,7 +495,9 @@ describe Notify do ...@@ -477,7 +495,9 @@ describe Notify do
subject { Notify.note_merge_request_email(recipient.id, note.id) } subject { Notify.note_merge_request_email(recipient.id, note.id) }
it_behaves_like 'a note email' it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { merge_request }
end
it_behaves_like 'it should show Gmail Actions View Merge request link' it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread' it_behaves_like 'an unsubscribeable thread'
...@@ -498,7 +518,9 @@ describe Notify do ...@@ -498,7 +518,9 @@ describe Notify do
subject { Notify.note_issue_email(recipient.id, note.id) } subject { Notify.note_issue_email(recipient.id, note.id) }
it_behaves_like 'a note email' it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { issue }
end
it_behaves_like 'it should show Gmail Actions View Issue link' it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread' it_behaves_like 'an unsubscribeable thread'
......
...@@ -10,6 +10,13 @@ shared_context 'gitlab email notification' do ...@@ -10,6 +10,13 @@ shared_context 'gitlab email notification' do
ActionMailer::Base.deliveries.clear ActionMailer::Base.deliveries.clear
email = recipient.emails.create(email: "notifications@example.com") email = recipient.emails.create(email: "notifications@example.com")
recipient.update_attribute(:notification_email, email.email) recipient.update_attribute(:notification_email, email.email)
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@#{Gitlab.config.gitlab.host}")
end
end
shared_context 'reply-by-email is enabled with incoming address without %{key}' do
before do
stub_incoming_email_setting(enabled: true, address: "reply@#{Gitlab.config.gitlab.host}")
end end
end end
...@@ -46,25 +53,76 @@ shared_examples 'an email with X-GitLab headers containing project details' do ...@@ -46,25 +53,76 @@ shared_examples 'an email with X-GitLab headers containing project details' do
end end
end end
shared_examples 'an email starting a new thread' do |message_id_prefix| shared_examples 'a new thread email with reply-by-email enabled' do
include_examples 'an email with X-GitLab headers containing project details' let(:regex) { /\A<reply\-(.*)@#{Gitlab.config.gitlab.host}>\Z/ }
it 'has a Message-ID header' do
is_expected.to have_header 'Message-ID', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>"
end
it 'has a discussion identifier' do it 'has a References header' do
is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ is_expected.to have_header 'References', regex
end end
end end
shared_examples 'an answer to an existing thread' do |thread_id_prefix| shared_examples 'a thread answer email with reply-by-email enabled' do
include_examples 'an email with X-GitLab headers containing project details' include_examples 'an email with X-GitLab headers containing project details'
let(:regex) { /\A<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}> <reply\-(.*)@#{Gitlab.config.gitlab.host}>\Z/ }
it 'has a Message-ID header' do
is_expected.to have_header 'Message-ID', /\A<(.*)@#{Gitlab.config.gitlab.host}>\Z/
end
it 'has a In-Reply-To header' do
is_expected.to have_header 'In-Reply-To', "<#{model.class.model_name.singular_route_key}_#{model.id}@#{Gitlab.config.gitlab.host}>"
end
it 'has a References header' do
is_expected.to have_header 'References', regex
end
it 'has a subject that begins with Re: ' do it 'has a subject that begins with Re: ' do
is_expected.to have_subject /^Re: / is_expected.to have_subject /^Re: /
end end
end
shared_examples 'an email starting a new thread with reply-by-email enabled' do
include_examples 'an email with X-GitLab headers containing project details'
include_examples 'a new thread email with reply-by-email enabled'
it 'has headers that reference an existing thread' do context 'when reply-by-email is enabled with incoming address with %{key}' do
is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/ it 'has a Reply-To header' do
is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ end
end
context 'when reply-by-email is enabled with incoming address without %{key}' do
include_context 'reply-by-email is enabled with incoming address without %{key}'
include_examples 'a new thread email with reply-by-email enabled'
it 'has a Reply-To header' do
is_expected.to have_header 'Reply-To', /<reply@#{Gitlab.config.gitlab.host}>\Z/
end
end
end
shared_examples 'an answer to an existing thread with reply-by-email enabled' do
include_examples 'an email with X-GitLab headers containing project details'
include_examples 'a thread answer email with reply-by-email enabled'
context 'when reply-by-email is enabled with incoming address with %{key}' do
it 'has a Reply-To header' do
is_expected.to have_header 'Reply-To', /<reply+(.*)@#{Gitlab.config.gitlab.host}>\Z/
end
end
context 'when reply-by-email is enabled with incoming address without %{key}' do
include_context 'reply-by-email is enabled with incoming address without %{key}'
include_examples 'a thread answer email with reply-by-email enabled'
it 'has a Reply-To header' do
is_expected.to have_header 'Reply-To', /<reply@#{Gitlab.config.gitlab.host}>\Z/
end
end end
end end
......
...@@ -20,24 +20,27 @@ require "spec_helper" ...@@ -20,24 +20,27 @@ require "spec_helper"
describe SystemHook, models: true do describe SystemHook, models: true do
describe "execute" do describe "execute" do
before(:each) do let(:system_hook) { create(:system_hook) }
@system_hook = create(:system_hook) let(:user) { create(:user) }
WebMock.stub_request(:post, @system_hook.url) let(:project) { create(:project, namespace: user.namespace) }
let(:group) { create(:group) }
before do
WebMock.stub_request(:post, system_hook.url)
end end
it "project_create hook" do it "project_create hook" do
Projects::CreateService.new(create(:user), name: 'empty').execute Projects::CreateService.new(user, name: 'empty').execute
expect(WebMock).to have_requested(:post, @system_hook.url).with( expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /project_create/, body: /project_create/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once ).once
end end
it "project_destroy hook" do it "project_destroy hook" do
user = create(:user)
project = create(:empty_project, namespace: user.namespace)
Projects::DestroyService.new(project, user, {}).pending_delete! Projects::DestroyService.new(project, user, {}).pending_delete!
expect(WebMock).to have_requested(:post, @system_hook.url).with(
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /project_destroy/, body: /project_destroy/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once ).once
...@@ -45,37 +48,36 @@ describe SystemHook, models: true do ...@@ -45,37 +48,36 @@ describe SystemHook, models: true do
it "user_create hook" do it "user_create hook" do
create(:user) create(:user)
expect(WebMock).to have_requested(:post, @system_hook.url).with(
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_create/, body: /user_create/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once ).once
end end
it "user_destroy hook" do it "user_destroy hook" do
user = create(:user)
user.destroy user.destroy
expect(WebMock).to have_requested(:post, @system_hook.url).with(
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_destroy/, body: /user_destroy/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once ).once
end end
it "project_create hook" do it "project_create hook" do
user = create(:user)
project = create(:project)
project.team << [user, :master] project.team << [user, :master]
expect(WebMock).to have_requested(:post, @system_hook.url).with(
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_add_to_team/, body: /user_add_to_team/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once ).once
end end
it "project_destroy hook" do it "project_destroy hook" do
user = create(:user)
project = create(:project)
project.team << [user, :master] project.team << [user, :master]
project.project_members.destroy_all project.project_members.destroy_all
expect(WebMock).to have_requested(:post, @system_hook.url).with(
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_remove_from_team/, body: /user_remove_from_team/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once ).once
...@@ -83,41 +85,39 @@ describe SystemHook, models: true do ...@@ -83,41 +85,39 @@ describe SystemHook, models: true do
it 'group create hook' do it 'group create hook' do
create(:group) create(:group)
expect(WebMock).to have_requested(:post, @system_hook.url).with(
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /group_create/, body: /group_create/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once ).once
end end
it 'group destroy hook' do it 'group destroy hook' do
group = create(:group)
group.destroy group.destroy
expect(WebMock).to have_requested(:post, @system_hook.url).with(
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /group_destroy/, body: /group_destroy/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once ).once
end end
it 'group member create hook' do it 'group member create hook' do
group = create(:group)
user = create(:user)
group.add_master(user) group.add_master(user)
expect(WebMock).to have_requested(:post, @system_hook.url).with(
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_add_to_group/, body: /user_add_to_group/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once ).once
end end
it 'group member destroy hook' do it 'group member destroy hook' do
group = create(:group)
user = create(:user)
group.add_master(user) group.add_master(user)
group.group_members.destroy_all group.group_members.destroy_all
expect(WebMock).to have_requested(:post, @system_hook.url).with(
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_remove_from_group/, body: /user_remove_from_group/,
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' }
).once ).once
end end
end end
end end
...@@ -558,7 +558,7 @@ describe Repository, models: true do ...@@ -558,7 +558,7 @@ describe Repository, models: true do
end end
it 'flushes the exists cache' do it 'flushes the exists cache' do
expect(repository).to receive(:expire_exists_cache) expect(repository).to receive(:expire_exists_cache).twice
repository.before_delete repository.before_delete
end end
......
CarrierWave.root = 'tmp/tests/uploads'
RSpec.configure do |config|
config.after(:suite) do
FileUtils.rm_rf('tmp/tests/uploads')
end
end
...@@ -7,8 +7,12 @@ describe 'gitlab:app namespace rake task' do ...@@ -7,8 +7,12 @@ describe 'gitlab:app namespace rake task' do
Rake.application.rake_require 'tasks/gitlab/backup' Rake.application.rake_require 'tasks/gitlab/backup'
Rake.application.rake_require 'tasks/gitlab/shell' Rake.application.rake_require 'tasks/gitlab/shell'
Rake.application.rake_require 'tasks/gitlab/db' Rake.application.rake_require 'tasks/gitlab/db'
# empty task as env is already loaded # empty task as env is already loaded
Rake::Task.define_task :environment Rake::Task.define_task :environment
# We need this directory to run `gitlab:backup:create` task
FileUtils.mkdir_p('public/uploads')
end end
def run_rake_task(task_name) def run_rake_task(task_name)
......
require 'spec_helper'
describe ProjectCacheWorker do
let(:project) { create(:project) }
subject { described_class.new }
describe '#perform' do
it 'updates project cache data' do
expect_any_instance_of(Repository).to receive(:size)
expect_any_instance_of(Repository).to receive(:commit_count)
expect_any_instance_of(Project).to receive(:update_repository_size)
expect_any_instance_of(Project).to receive(:update_commit_count)
subject.perform(project.id)
end
it 'handles missing repository data' do
expect_any_instance_of(Repository).to receive(:exists?).and_return(false)
expect_any_instance_of(Repository).not_to receive(:size)
subject.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