diff --git a/.gitignore b/.gitignore index 3e30fb8cf7..8a68bb3e4f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ config/initializers/rack_attack.rb config/initializers/smtp_settings.rb config/resque.yml config/unicorn.rb +config/mail_room.yml coverage/* db/*.sqlite3 db/*.sqlite3-journal diff --git a/CHANGELOG b/CHANGELOG index 53c7463ae7..4946c68390 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,11 @@ Please view this file on the master branch, on stable branches it's out of date. v 8.0.0 (unreleased) + - Upgrade gitlab_git to 7.2.15 to fix `git blame` errors with ISO-encoded files (Stan Hu) + - Prevent too many redirects upon login when home page URL is set to external_url (Stan Hu) + - Improve dropdown positioning on the project home page (Hannes Rosenögger) + - Upgrade browser gem to 1.0.0 to avoid warning in IE11 compatibilty mode (Stan Hu) + - Remove user OAuth tokens from the database and request new tokens each session (Stan Hu) - Only show recent push event if the branch still exists or a recent merge request has not been created (Stan Hu) - Remove satellites - Better performance for web editor (switched from satellites to rugged) @@ -9,8 +14,26 @@ v 8.0.0 (unreleased) - Allow displaying of archived projects in the admin interface (Artem Sidorenko) - Allow configuration of import sources for new projects (Artem Sidorenko) - Search for comments should be case insensetive + - Create cross-reference for closing references on commits pushed to non-default branches (Maël Valais) + - Ability to search milestones + - Gracefully handle SMTP user input errors (e.g. incorrect email addresses) to prevent Sidekiq retries (Stan Hu) + - Move dashboard activity to separate page + - Improve performance of git blame + - Limit content width to 1200px for most of pages to improve readability on big screens + - Fix 500 error when submit project snippet without body + - Improve search page usability + - Bring more UI consistency in way how projects, snippets and groups lists are rendered + - Make all profiles public -v 7.14.0 (unreleased) +v 7.14.1 + - Improve abuse reports management from admin area + - Fix "Reload with full diff" URL button in compare branch view (Stan Hu) + - Only include base URL in OmniAuth full_host parameter (Stan Hu) + - Fix Error 500 in API when accessing a group that has an avatar (Stan Hu) + - Ability to enable SSL verification for Webhooks + +v 7.14.0 + - Fix bug where non-project members of the target project could set labels on new merge requests. - Update default robots.txt rules to disallow crawling of irrelevant pages (Ben Bodenmiller) - Fix redirection after sign in when using auto_sign_in_with_provider - Upgrade gitlab_git to 7.2.14 to ignore CRLFs in .gitmodules (Stan Hu) @@ -294,6 +317,7 @@ v 7.11.0 - Protect OmniAuth request phase against CSRF. - Don't send notifications to mentioned users that don't have access to the project in question. - Add search issues/MR by number + - Change plots to bar graphs in commit statistics screen - Move snippets UI to fluid layout - Improve UI for sidebar. Increase separation between navigation and content - Improve new project command options (Ben Bodenmiller) diff --git a/Gemfile b/Gemfile index 8f65a274ba..e1b9ede17b 100644 --- a/Gemfile +++ b/Gemfile @@ -34,11 +34,11 @@ gem 'rqrcode-rails3' gem 'attr_encrypted', '1.3.4' # Browser detection -gem "browser", '~> 0.8.0' +gem "browser", '~> 1.0.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.2.14' +gem "gitlab_git", '~> 7.2.15' # Ruby/Rack Git Smart-HTTP Server Handler # GitLab fork with a lot of changes (improved thread-safety, better memory usage etc) @@ -272,3 +272,7 @@ end gem "newrelic_rpm" gem 'octokit', '3.7.0' + +gem "mail_room", "~> 0.4.1" + +gem 'email_reply_parser' diff --git a/Gemfile.lock b/Gemfile.lock index f0c661fa9c..ff01ad1014 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -76,7 +76,7 @@ GEM ruby_parser (~> 3.5.0) sass (~> 3.0) terminal-table (~> 1.4) - browser (0.8.0) + browser (1.0.0) builder (3.2.2) byebug (3.2.0) columnize (~> 0.8) @@ -156,6 +156,7 @@ GEM dotenv (0.9.0) dropzonejs-rails (0.7.1) rails (> 3.1) + email_reply_parser (0.5.8) email_spec (1.6.0) launchy (~> 2.1) mail (~> 2.2) @@ -275,7 +276,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.0) gemojione (~> 2.0) - gitlab_git (7.2.14) + gitlab_git (7.2.15) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -371,6 +372,7 @@ GEM systemu (~> 2.6.2) mail (2.6.3) mime-types (>= 1.16, < 3) + mail_room (0.4.1) method_source (0.8.2) mime-types (1.25.1) mimemagic (0.3.0) @@ -753,7 +755,7 @@ DEPENDENCIES binding_of_caller bootstrap-sass (~> 3.0) brakeman - browser (~> 0.8.0) + browser (~> 1.0.0) byebug cal-heatmap-rails (~> 0.0.1) capybara (~> 2.4.0) @@ -773,6 +775,7 @@ DEPENDENCIES diffy (~> 3.0.3) doorkeeper (= 2.1.3) dropzonejs-rails + email_reply_parser email_spec (~> 1.6.0) enumerize factory_girl_rails @@ -787,7 +790,7 @@ DEPENDENCIES gitlab-grack (~> 2.0.2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.2.14) + gitlab_git (~> 7.2.15) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.1) gollum-lib (~> 4.0.2) @@ -805,6 +808,7 @@ DEPENDENCIES jquery-ui-rails kaminari (~> 0.15.1) letter_opener + mail_room (~> 0.4.1) minitest (~> 5.3.0) mousetrap-rails mysql2 diff --git a/Procfile b/Procfile index 799b92729f..18fd9eb3d9 100644 --- a/Procfile +++ b/Procfile @@ -1,2 +1,3 @@ web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"} -worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q common -q default +worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q common -q default +# mail_room: bundle exec mail_room -q -c config/mail_room.yml diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index bb0a0c51fd..c263912b7e 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -116,6 +116,12 @@ $ -> $('.remove-row').bind 'ajax:success', -> $(this).closest('li').fadeOut() + $('.js-remove-tr').bind 'ajax:before', -> + $(this).hide() + + $('.js-remove-tr').bind 'ajax:success', -> + $(this).closest('tr').fadeOut() + # Initialize select2 selects $('select.select2').select2(width: 'resolve', dropdownAutoWidth: true) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 81e7379927..5bf0b30217 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -51,10 +51,10 @@ class Dispatcher MergeRequests.init() when 'dashboard:show', 'root:show' new Dashboard() + when 'dashboard:activity' new Activities() when 'dashboard:projects:starred' new Activities() - new ProjectsList() when 'projects:commit:show' new Commit() new Diff() @@ -69,7 +69,6 @@ class Dispatcher when 'groups:show' new Activities() shortcut_handler = new ShortcutsNavigation() - new ProjectsList() when 'groups:group_members:index' new GroupMembers() new UsersSelect() @@ -95,8 +94,6 @@ class Dispatcher when 'users:show' new User() new Activities() - when 'admin:users:show' - new ProjectsList() switch path.first() when 'admin' diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee index c0e36d1ccc..db5faf71fa 100644 --- a/app/assets/javascripts/projects_list.js.coffee +++ b/app/assets/javascripts/projects_list.js.coffee @@ -8,7 +8,7 @@ class @ProjectsList $(".projects-list-filter").keyup -> terms = $(this).val() - uiBox = $(this).closest('.panel') + uiBox = $(this).closest('.projects-list-holder') if terms == "" || terms == undefined uiBox.find(".projects-list li").show() else diff --git a/app/assets/javascripts/syntax_highlight.coffee b/app/assets/javascripts/syntax_highlight.coffee new file mode 100644 index 0000000000..510f15d1b4 --- /dev/null +++ b/app/assets/javascripts/syntax_highlight.coffee @@ -0,0 +1,9 @@ +# Applies a syntax highlighting color scheme CSS class to any element with the +# `js-syntax-highlight` class +# +# ### Example Markup +# +#
+# +$(document).on 'ready page:load', -> + $('.js-syntax-highlight').addClass(gon.user_color_scheme) diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index aeeed9ca3c..9157562a5c 100644 --- a/app/assets/javascripts/users_select.js.coffee +++ b/app/assets/javascripts/users_select.js.coffee @@ -6,6 +6,7 @@ class @UsersSelect $('.ajax-users-select').each (i, select) => @projectId = $(select).data('project-id') @groupId = $(select).data('group-id') + @showCurrentUser = $(select).data('current-user') showNullUser = $(select).data('null-user') showAnyUser = $(select).data('any-user') showEmailUser = $(select).data('email-user') @@ -108,6 +109,7 @@ class @UsersSelect active: true project_id: @projectId group_id: @groupId + current_user: @showCurrentUser dataType: "json" ).done (users) -> callback(users) diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/base/layout.scss index 690d89a5c1..734b95e26c 100644 --- a/app/assets/stylesheets/base/layout.scss +++ b/app/assets/stylesheets/base/layout.scss @@ -20,3 +20,8 @@ html { .navless-container { margin-top: 30px; } + + +.container-limited { + max-width: $fixed-layout-width; +} diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss index 7beef1845e..05f5bd79f9 100644 --- a/app/assets/stylesheets/base/mixins.scss +++ b/app/assets/stylesheets/base/mixins.scss @@ -157,3 +157,41 @@ white-space: nowrap; max-width: $max_width; } + +/* + * Base mixin for lists in GitLab + */ +@mixin basic-list { + margin: 5px 0px; + padding: 0px; + list-style: none; + + li { + padding: 10px 0; + border-bottom: 1px solid #EEE; + overflow: hidden; + display: block; + margin: 0px; + + &:last-child { + border:none + } + + &.active { + background: #f9f9f9; + a { + font-weight: bold; + } + } + + &.hide { + display: none; + } + + &.light { + a { + color: #777; + } + } + } +} diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss index cb439a0e0b..26d0a1e536 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -13,7 +13,7 @@ $code_line_height: 1.5; $border-color: #E5E5E5; $background-color: #f5f5f5; $header-height: 50px; -$readable-width: 1100px; +$fixed-layout-width: 1200px; /* diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index bf5c7a8d75..e5902597c4 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -132,10 +132,6 @@ p.time { text-shadow: none; } -.highlight_word { - background: #fafe3d; -} - .thin_area{ height: 150px; } @@ -375,9 +371,9 @@ table { } .center-top-menu { - border-bottom: 1px solid #EEE; list-style: none; text-align: center; + margin-top: 5px; padding-bottom: 15px; margin-bottom: 15px; @@ -385,7 +381,7 @@ table { display: inline-block; a { - padding: 10px; + padding: 15px; } &.active a { diff --git a/app/assets/stylesheets/generic/header.scss b/app/assets/stylesheets/generic/header.scss index 31e2ad8669..6a29b32e19 100644 --- a/app/assets/stylesheets/generic/header.scss +++ b/app/assets/stylesheets/generic/header.scss @@ -20,16 +20,16 @@ header { } &.navbar-gitlab { + padding: 0 20px; z-index: 100; margin-bottom: 0; min-height: $header-height; border: none; - width: 100%; + border-bottom: 1px solid #EEE; - .container { + .container-fluid { background: #FFF; width: 100% !important; - padding: 0; filter: none; .nav > li > a { @@ -64,55 +64,11 @@ header { } } - .header-logo { - border-bottom: 1px solid transparent; - float: left; - height: $header-height; - width: $sidebar_width; - overflow: hidden; - transition-duration: .3s; - - a { - float: left; - height: $header-height; - width: 100%; - padding: ($header-height - 36 ) / 2 8px; - overflow: hidden; - - img { - width: 36px; - height: 36px; - float: left; - } - - .gitlab-text-container { - width: 230px; - - h3 { - width: 158px; - float: left; - margin: 0; - margin-left: 14px; - font-size: 18px; - line-height: $header-height - 14; - font-weight: normal; - } - } - } - - &:hover { - background-color: #EEE; - } - } - .header-content { - border-bottom: 1px solid #EEE; - padding-right: 35px; height: $header-height; .title { margin: 0; - padding: 0 15px 0 35px; overflow: hidden; font-size: 18px; line-height: $header-height; @@ -168,15 +124,7 @@ header { } @mixin collapsed-header { - .header-logo { - width: $sidebar_collapsed_width; - } - - .header-content { - .title { - margin-left: 30px; - } - } + margin-left: $sidebar_collapsed_width; } @media (max-width: $screen-md-max) { @@ -191,16 +139,14 @@ header { } .header-expanded { + margin-left: $sidebar_width; } } @media (max-width: $screen-xs-max) { - header .container { + header .container-fluid { font-size: 18px; - .title { - } - .navbar-nav { margin: 0px; float: none !important; diff --git a/app/assets/stylesheets/generic/lists.scss b/app/assets/stylesheets/generic/lists.scss index c502d953c7..4b7ff84de2 100644 --- a/app/assets/stylesheets/generic/lists.scss +++ b/app/assets/stylesheets/generic/lists.scss @@ -93,28 +93,12 @@ ol, ul { /** light list with border-bottom between li **/ ul.bordered-list { - margin: 5px 0px; - padding: 0px; - li { - padding: 5px 0; - border-bottom: 1px solid #EEE; - overflow: hidden; - display: block; - margin: 0px; - &:last-child { border:none } - &.active { - background: #f9f9f9; - a { font-weight: bold; } - } - - &.light { - a { color: #777; } - } - } + @include basic-list; &.top-list { li:first-child { padding-top: 0; + h4, h5 { margin-top: 0; } diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss index b96664d30d..320bdb1c76 100644 --- a/app/assets/stylesheets/generic/sidebar.scss +++ b/app/assets/stylesheets/generic/sidebar.scss @@ -188,3 +188,46 @@ width: $sidebar_width - 2 * 10px; } } + +.sidebar-wrapper { + .header-logo { + border-bottom: 1px solid transparent; + float: left; + height: $header-height; + width: $sidebar_width; + overflow: hidden; + transition-duration: .3s; + + a { + float: left; + height: $header-height; + width: 100%; + padding: ($header-height - 36 ) / 2 8px; + overflow: hidden; + + img { + width: 36px; + height: 36px; + float: left; + } + + .gitlab-text-container { + width: 230px; + + h3 { + width: 158px; + float: left; + margin: 0; + margin-left: 14px; + font-size: 18px; + line-height: $header-height - 14; + font-weight: normal; + } + } + } + + &:hover { + background-color: #EEE; + } + } +} diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss index c8cb18ec35..8323a8598e 100644 --- a/app/assets/stylesheets/highlight/dark.scss +++ b/app/assets/stylesheets/highlight/dark.scss @@ -21,6 +21,12 @@ pre.code.highlight.dark, background-color: #557 !important; } + // Search result highlight + span.highlight_word { + background: #ffe792; + color: #000000; + } + .hll { background-color: #373b41 } .c { color: #969896 } /* Comment */ .err { color: #cc6666 } /* Error */ diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss index 001e8b3102..e838167433 100644 --- a/app/assets/stylesheets/highlight/monokai.scss +++ b/app/assets/stylesheets/highlight/monokai.scss @@ -21,6 +21,12 @@ pre.code.monokai, background-color: #49483e !important; } + // Search result highlight + span.highlight_word { + background: #ffe792; + color: #000000; + } + .hll { background-color: #49483e } .c { color: #75715e } /* Comment */ .err { color: #960050; background-color: #1e0010 } /* Error */ diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss index f5b827e7c0..bd41480aef 100644 --- a/app/assets/stylesheets/highlight/solarized_dark.scss +++ b/app/assets/stylesheets/highlight/solarized_dark.scss @@ -21,6 +21,11 @@ pre.code.highlight.solarized-dark, background-color: #174652 !important; } + // Search result highlight + span.highlight_word { + background: #094554; + } + /* Solarized Dark For use with Jekyll and Pygments diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss index 6b44c00c30..4cc6286387 100644 --- a/app/assets/stylesheets/highlight/solarized_light.scss +++ b/app/assets/stylesheets/highlight/solarized_light.scss @@ -21,6 +21,11 @@ pre.code.highlight.solarized-light, background-color: #ddd8c5 !important; } + // Search result highlight + span.highlight_word { + background: #eee8d5; + } + /* Solarized Light For use with Jekyll and Pygments diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss index a52ffc971d..e0edfb80b4 100644 --- a/app/assets/stylesheets/highlight/white.scss +++ b/app/assets/stylesheets/highlight/white.scss @@ -21,6 +21,11 @@ pre.code.highlight.white, background-color: #f8eec7 !important; } + // Search result highlight + span.highlight_word { + background: #fafe3d; + } + .hll { background-color: #f8f8f8 } .c { color: #999988; font-style: italic; } .err { color: #a61717; background-color: #e3d2d2; } diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index 9a3b543ad1..c1103a1c2e 100644 --- a/app/assets/stylesheets/pages/dashboard.scss +++ b/app/assets/stylesheets/pages/dashboard.scss @@ -23,41 +23,6 @@ } } -.project-row, .group-row { - padding: 0 !important; - font-size: 14px; - line-height: 24px; - - a { - display: block; - padding: 8px 15px; - } - - .project-name, .group-name { - font-weight: 500; - } - - .arrow { - float: right; - margin: 0; - font-size: 20px; - } - - .last-activity { - float: right; - font-size: 12px; - color: #AAA; - display: block; - .date { - color: #777; - } - } -} - -.project-description { - overflow: hidden; -} - .project-access-icon { margin-left: 10px; float: left; @@ -73,10 +38,9 @@ float: left; .avatar { - margin-top: -8px; - margin-left: -15px; @include border-radius(0px); } + .identicon { line-height: 40px; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 3f617e72b0..586e7b5f8d 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -45,9 +45,3 @@ .btn { font-size: 13px; } } - -.issuable-details { - .description { - max-width: $readable-width; - } -} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 29d3dbc25e..488dded549 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -30,14 +30,21 @@ } } + .project-home-dropdown { + margin: 11px 3px 0; + } + .project-home-desc { h1 { margin: 0; margin-bottom: 10px; font-size: 26px; + font-weight: bold; } p { + font-size: 18px; + color: #666; display: inline; } } @@ -155,78 +162,6 @@ ul.nav.nav-projects-tabs { margin: 0px; } -.my-projects, -.public-projects { - li { - .project-info { - margin-bottom: 10px; - overflow: hidden; - } - - .access-icon { - color: #AAA; - margin-left: 10px; - i { - color: #AAA; - } - } - } -} - -.public-clone { - background: #EEE; - color: #777; - padding: 6px 10px; - margin: 1px; - font-weight: normal; -} - -.public-projects .repo-info { - color: #777; - - a { - color: #777; - } -} - -.project-side { - .project-fork-icon { - float: left; - font-size: 26px; - margin-right: 10px; - line-height: 1.5; - } - - .panel { - @include border-radius(3px); - - .panel-heading, .panel-footer { - font-weight: normal; - background-color: transparent; - color: #666; - border-color: #EEE; - } - - .actions { - margin-top: 10px; - } - - .nav-pills a { - padding: 10px; - font-weight: bold; - color: $gl-link-color; - } - - .nav { - margin-bottom: 15px; - } - } - - .ci-status-image { - max-height: 22px; - } -} - .transfer-project .select2-container { min-width: 200px; } @@ -316,3 +251,43 @@ table.table.protected-branches-list tr.no-border { pre.light-well { border-color: #f1f1f1; } + +.projects-search-form { + max-width: 600px; + margin: 0 auto; + margin-bottom: 20px; + + input { + border-color: #BBB; + } +} + +/* + * Projects list rendered on dashboard and user page + */ +.projects-list { + @include basic-list; + + .project-row { + .project-full-name { + @include str-truncated; + font-weight: bold; + font-size: 15px; + } + + .project-description { + color: #888; + font-size: 13px; + + p { + @include str-truncated; + margin-bottom: 0; + color: #888; + } + } + } +} + +.panel .projects-list li { + padding: 10px 15px; +} diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index bdaa17ac33..3aaa96da60 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -1,7 +1,19 @@ .search-results { .search-result-row { - border-bottom: 1px solid #EEE; - padding-bottom: 10px; - margin-bottom: 10px; + border-bottom: 1px solid #DDD; + padding-bottom: 15px; + margin-bottom: 15px; } } + +.search-holder { + max-width: 600px; + margin: 0 auto; + margin-bottom: 20px; + + input { + border-color: #BBB; + font-weight: bold; + } +} + diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss index d79591d991..a3d7aba054 100644 --- a/app/assets/stylesheets/pages/snippets.scss +++ b/app/assets/stylesheets/pages/snippets.scss @@ -6,3 +6,27 @@ .snippet-form-holder .file-holder .file-title { padding: 2px; } + + +.snippet-row { + .snippet-title { + font-size: 15px; + font-weight: bold; + line-height: 20px; + margin-bottom: 2px; + + .monospace { + font-weight: normal; + } + } + + .snippet-info { + color: #888; + font-size: 13px; + line-height: 24px; + + a { + color: #888; + } + } +} diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 5f1a3db4fb..81e2aa7bb9 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -117,7 +117,6 @@ .readme-holder { margin: 0 auto; - max-width: $readable-width; .readme-file-title { font-size: 14px; diff --git a/app/assets/stylesheets/themes/gitlab-theme.scss b/app/assets/stylesheets/themes/gitlab-theme.scss index 3589cb88d0..77b62c3153 100644 --- a/app/assets/stylesheets/themes/gitlab-theme.scss +++ b/app/assets/stylesheets/themes/gitlab-theme.scss @@ -7,27 +7,23 @@ * $color-dark - */ @mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) { - header { - &.navbar-gitlab { - .header-logo { - background-color: $color-darker; - border-color: $color-darker; + .page-with-sidebar { + .header-logo { + background-color: $color-darker; + border-color: $color-darker; + a { + color: $color-light; + } + + &:hover { + background-color: $color-dark; a { - color: $color-light; - } - - &:hover { - background-color: $color-dark; - a { - color: #FFF; - } + color: #FFF; } } } - } - .page-with-sidebar { .collapse-nav a { color: #FFF; background: $color; diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb index 34f37bca4a..38a5a9fca0 100644 --- a/app/controllers/admin/abuse_reports_controller.rb +++ b/app/controllers/admin/abuse_reports_controller.rb @@ -4,8 +4,13 @@ class Admin::AbuseReportsController < Admin::ApplicationController end def destroy - AbuseReport.find(params[:id]).destroy + abuse_report = AbuseReport.find(params[:id]) - redirect_to admin_abuse_reports_path, notice: 'Report was removed' + if params[:remove_user] + abuse_report.user.destroy + end + + abuse_report.destroy + render nothing: true end end diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index 690096bdbc..d670386f8c 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -39,6 +39,6 @@ class Admin::HooksController < Admin::ApplicationController end def hook_params - params.require(:hook).permit(:url) + params.require(:hook).permit(:url, :enable_ssl_verification) end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 12d439b0b3..cb1cf13d34 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -55,7 +55,9 @@ class ApplicationController < ActionController::Base def authenticate_user!(*args) # If user is not signed-in and tries to access root_path - redirect him to landing page - if current_application_settings.home_page_url.present? + # Don't redirect to the default URL to prevent endless redirections + if current_application_settings.home_page_url.present? && + current_application_settings.home_page_url.chomp('/') != Gitlab.config.gitlab['url'].chomp('/') if current_user.nil? && root_path == request.path redirect_to current_application_settings.home_page_url and return end @@ -190,11 +192,12 @@ class ApplicationController < ActionController::Base end def add_gon_variables + gon.api_version = API::API.version + gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s gon.default_issues_tracker = Project.new.default_issue_tracker.to_param - gon.api_version = API::API.version - gon.relative_url_root = Gitlab.config.gitlab.relative_url_root - gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s - gon.max_file_size = current_application_settings.max_attachment_size; + gon.max_file_size = current_application_settings.max_attachment_size + gon.relative_url_root = Gitlab.config.gitlab.relative_url_root + gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class if current_user gon.current_user_id = current_user.id diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 5c3ca8e23c..904d26a39f 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -33,8 +33,14 @@ class AutocompleteController < ApplicationController @users = @users.search(params[:search]) if params[:search].present? @users = @users.active @users = @users.page(params[:page]).per(PER_PAGE) - # Always include current user if available to filter by "Me" - @users = User.find(@users.pluck(:id) + [current_user.id]).uniq if current_user + + unless params[:search].present? + # Include current user if available to filter by "Me" + if params[:current_user] && current_user + @users = [*@users, current_user].uniq + end + end + render json: @users, only: [:name, :username, :id], methods: [:avatar_url] end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index d2f0c43929..d745131694 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,6 +1,6 @@ class DashboardController < Dashboard::ApplicationController before_action :load_projects - before_action :event_filter, only: :show + before_action :event_filter, only: :activity respond_to :html @@ -10,13 +10,8 @@ class DashboardController < Dashboard::ApplicationController respond_to do |format| format.html - - format.json do - load_events - pager_json("events/_events", @events.count) - end - format.atom do + event_filter load_events render layout: false end @@ -40,6 +35,19 @@ class DashboardController < Dashboard::ApplicationController end end + def activity + @last_push = current_user.recent_push + + respond_to do |format| + format.html + + format.json do + load_events + pager_json("events/_events", @events.count) + end + end + end + protected def load_projects diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 4e6c0b6663..f84f85a7df 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -13,10 +13,9 @@ class Import::BitbucketController < Import::BaseController access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url) - current_user.bitbucket_access_token = access_token.token - current_user.bitbucket_access_token_secret = access_token.secret + session[:bitbucket_access_token] = access_token.token + session[:bitbucket_access_token_secret] = access_token.secret - current_user.save redirect_to status_import_bitbucket_url end @@ -46,19 +45,20 @@ class Import::BitbucketController < Import::BaseController namespace = get_or_create_namespace || (render and return) - unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user).execute + unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute @access_denied = true render return end - @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, current_user).execute + @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute end private def client - @client ||= Gitlab::BitbucketImport::Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret) + @client ||= Gitlab::BitbucketImport::Client.new(session[:bitbucket_access_token], + session[:bitbucket_access_token_secret]) end def verify_bitbucket_import_enabled @@ -66,7 +66,7 @@ class Import::BitbucketController < Import::BaseController end def bitbucket_auth - if current_user.bitbucket_access_token.blank? + if session[:bitbucket_access_token].blank? go_to_bitbucket_for_permissions end end @@ -81,4 +81,13 @@ class Import::BitbucketController < Import::BaseController def bitbucket_unauthorized go_to_bitbucket_for_permissions end + + private + + def access_params + { + bitbucket_access_token: session[:bitbucket_access_token], + bitbucket_access_token_secret: session[:bitbucket_access_token_secret] + } + end end diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index b9f99c1b88..f21fbd9ecc 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -5,9 +5,7 @@ class Import::GithubController < Import::BaseController rescue_from Octokit::Unauthorized, with: :github_unauthorized def callback - token = client.get_token(params[:code]) - current_user.github_access_token = token - current_user.save + session[:github_access_token] = client.get_token(params[:code]) redirect_to status_import_github_url end @@ -39,13 +37,13 @@ class Import::GithubController < Import::BaseController namespace = get_or_create_namespace || (render and return) - @project = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, current_user).execute + @project = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute end private def client - @client ||= Gitlab::GithubImport::Client.new(current_user.github_access_token) + @client ||= Gitlab::GithubImport::Client.new(session[:github_access_token]) end def verify_github_import_enabled @@ -53,7 +51,7 @@ class Import::GithubController < Import::BaseController end def github_auth - if current_user.github_access_token.blank? + if session[:github_access_token].blank? go_to_github_for_permissions end end @@ -65,4 +63,10 @@ class Import::GithubController < Import::BaseController def github_unauthorized go_to_github_for_permissions end + + private + + def access_params + { github_access_token: session[:github_access_token] } + end end diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 1b8962d892..27af19f5f6 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -5,9 +5,7 @@ class Import::GitlabController < Import::BaseController rescue_from OAuth2::Error, with: :gitlab_unauthorized def callback - token = client.get_token(params[:code], callback_import_gitlab_url) - current_user.gitlab_access_token = token - current_user.save + session[:gitlab_access_token] = client.get_token(params[:code], callback_import_gitlab_url) redirect_to status_import_gitlab_url end @@ -36,13 +34,13 @@ class Import::GitlabController < Import::BaseController namespace = get_or_create_namespace || (render and return) - @project = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, current_user).execute + @project = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute end private def client - @client ||= Gitlab::GitlabImport::Client.new(current_user.gitlab_access_token) + @client ||= Gitlab::GitlabImport::Client.new(session[:gitlab_access_token]) end def verify_gitlab_import_enabled @@ -50,7 +48,7 @@ class Import::GitlabController < Import::BaseController end def gitlab_auth - if current_user.gitlab_access_token.blank? + if session[:gitlab_access_token].blank? go_to_gitlab_for_permissions end end @@ -62,4 +60,10 @@ class Import::GitlabController < Import::BaseController def gitlab_unauthorized go_to_gitlab_for_permissions end + + private + + def access_params + { gitlab_access_token: session[:gitlab_access_token] } + end end diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 76062446c9..4e5b4125f5 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -53,6 +53,7 @@ class Projects::HooksController < Projects::ApplicationController end def hook_params - params.require(:hook).permit(:url, :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events) + params.require(:hook).permit(:url, :push_events, :issues_events, + :merge_requests_events, :tag_push_events, :note_events, :enable_ssl_verification) end end diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 0110553247..b0cf5866d4 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -8,7 +8,7 @@ class Projects::ServicesController < Projects::ApplicationController :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url, :notify, :color, - :server_host, :server_port, :default_irc_uri] + :server_host, :server_port, :default_irc_uri, :enable_ssl_verification] # Authorize before_action :authorize_admin_project! before_action :service, only: [:edit, :update, :test] diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 6430663742..b07a2a8db2 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -30,9 +30,14 @@ class Projects::SnippetsController < Projects::ApplicationController def create @snippet = CreateSnippetService.new(@project, current_user, snippet_params).execute - respond_with(@snippet, - location: namespace_project_snippet_path(@project.namespace, - @project, @snippet)) + + if @snippet.valid? + respond_with(@snippet, + location: namespace_project_snippet_path(@project.namespace, + @project, @snippet)) + else + render :new + end end def edit diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 4e2ea6c571..eb0408a95e 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -23,7 +23,7 @@ class SearchController < ApplicationController @search_results = if @project - unless %w(blobs notes issues merge_requests wiki_blobs). + unless %w(blobs notes issues merge_requests milestones wiki_blobs). include?(@scope) @scope = 'blobs' end @@ -36,7 +36,7 @@ class SearchController < ApplicationController Search::SnippetService.new(current_user, params).execute else - unless %w(projects issues merge_requests).include?(@scope) + unless %w(projects issues merge_requests milestones).include?(@scope) @scope = 'projects' end Search::GlobalService.new(current_user, params).execute diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2bb5c338cf..1484356a7f 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -51,10 +51,6 @@ class UsersController < ApplicationController def set_user @user = User.find_by_username!(params[:username]) - - unless current_user || @user.public_profile? - return authenticate_user! - end end def authorized_projects_ids diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb index a79bd47d98..f3f4d461ef 100644 --- a/app/finders/trending_projects_finder.rb +++ b/app/finders/trending_projects_finder.rb @@ -2,13 +2,21 @@ class TrendingProjectsFinder def execute(current_user, start_date = nil) start_date ||= Date.today - 1.month - projects = projects_for(current_user) - # Determine trending projects based on comments count # for period of time - ex. month - projects.joins(:notes).where('notes.created_at > ?', start_date). - select("projects.*, count(notes.id) as ncount"). - group("projects.id").reorder("ncount DESC") + trending_project_ids = Note. + select("notes.project_id, count(notes.project_id) as pcount"). + where('notes.created_at > ?', start_date). + group("project_id"). + reorder("pcount DESC"). + map(&:project_id) + + sql_order_ids = trending_project_ids.reverse. + map { |project_id| "id = #{project_id}" }.join(", ") + + # Get list of projects that user allowed to see + projects = projects_for(current_user) + projects.where(id: trending_project_ids).reorder(sql_order_ids) end private diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb index eb3f72a307..114730eb94 100644 --- a/app/helpers/gitlab_markdown_helper.rb +++ b/app/helpers/gitlab_markdown_helper.rb @@ -58,7 +58,7 @@ module GitlabMarkdownHelper @options = options # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch - rend = Redcarpet::Render::GitlabHTML.new(self, user_color_scheme_class, options) + rend = Redcarpet::Render::GitlabHTML.new(self, options) # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use @markdown = Redcarpet::Markdown.new(rend, MARKDOWN_OPTIONS) diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 30b17a736a..1cf5b96481 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -20,7 +20,7 @@ module IconsHelper end def boolean_to_icon(value) - if value.to_s == "true" + if value icon('circle', class: 'cgreen') else icon('power-off', class: 'clgray') diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 01b6a63552..8473d6d75d 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -23,4 +23,12 @@ module PageLayoutHelper @sidebar end end + + def fluid_layout(enabled = false) + if @fluid_layout.nil? + @fluid_layout = enabled + else + @fluid_layout + end + end end diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index ea774e28ec..7f1b6a6992 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -1,25 +1,5 @@ # Helper methods for per-User preferences module PreferencesHelper - COLOR_SCHEMES = { - 1 => 'white', - 2 => 'dark', - 3 => 'solarized-light', - 4 => 'solarized-dark', - 5 => 'monokai', - } - COLOR_SCHEMES.default = 'white' - - # Helper method to access the COLOR_SCHEMES - # - # The keys are the `color_scheme_ids` - # The values are the `name` of the scheme. - # - # The preview images are `name-scheme-preview.png` - # The stylesheets should use the css class `.name` - def color_schemes - COLOR_SCHEMES.freeze - end - # Maps `dashboard` values to more user-friendly option text DASHBOARD_CHOICES = { projects: 'Your Projects (default)', @@ -50,12 +30,11 @@ module PreferencesHelper end def user_application_theme - theme = Gitlab::Themes.by_id(current_user.try(:theme_id)) - theme.css_class + Gitlab::Themes.for_user(current_user).css_class end - def user_color_scheme_class - COLOR_SCHEMES[current_user.try(:color_scheme_id)] if defined?(current_user) + def user_color_scheme + Gitlab::ColorSchemes.for_user(current_user).css_class end def prefer_readme? diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 2b99a39804..12fce8db70 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -10,6 +10,7 @@ module SelectsHelper any_user = opts[:any_user] || false email_user = opts[:email_user] || false first_user = opts[:first_user] && current_user ? current_user.username : false + current_user = opts[:current_user] || false project = opts[:project] || @project html = { @@ -18,7 +19,8 @@ module SelectsHelper 'data-null-user' => null_user, 'data-any-user' => any_user, 'data-email-user' => email_user, - 'data-first-user' => first_user + 'data-first-user' => first_user, + 'data-current-user' => current_user } unless opts[:scope] == :all diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb new file mode 100644 index 0000000000..aedb088918 --- /dev/null +++ b/app/mailers/base_mailer.rb @@ -0,0 +1,32 @@ +class BaseMailer < ActionMailer::Base + add_template_helper ApplicationHelper + add_template_helper GitlabMarkdownHelper + + attr_accessor :current_user + helper_method :current_user, :can? + + default from: Proc.new { default_sender_address.format } + default reply_to: Proc.new { default_reply_to_address.format } + + def self.delay + delay_for(2.seconds) + end + + def can? + Ability.abilities.allowed?(current_user, action, subject) + end + + private + + def default_sender_address + address = Mail::Address.new(Gitlab.config.gitlab.email_from) + address.display_name = Gitlab.config.gitlab.email_display_name + address + end + + def default_reply_to_address + address = Mail::Address.new(Gitlab.config.gitlab.email_reply_to) + address.display_name = Gitlab.config.gitlab.email_display_name + address + end +end diff --git a/app/mailers/email_rejection_mailer.rb b/app/mailers/email_rejection_mailer.rb new file mode 100644 index 0000000000..883f1c73ad --- /dev/null +++ b/app/mailers/email_rejection_mailer.rb @@ -0,0 +1,21 @@ +class EmailRejectionMailer < BaseMailer + def rejection(reason, original_raw, can_retry = false) + @reason = reason + @original_message = Mail::Message.new(original_raw) + + return unless @original_message.from + + headers = { + to: @original_message.from, + subject: "[Rejected] #{@original_message.subject}" + } + + headers['Message-ID'] = SecureRandom.hex + headers['In-Reply-To'] = @original_message.message_id + headers['References'] = @original_message.message_id + + headers['Reply-To'] = @original_message.to.first if can_retry + + mail(headers) + end +end diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index 687bac3aa3..2c035fbb70 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -8,6 +8,8 @@ module Emails from: sender(@issue.author_id), to: recipient(recipient_id), subject: subject("#{@issue.title} (##{@issue.iid})")) + + SentNotification.record(@issue, recipient_id, reply_key) end def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id) @@ -19,6 +21,8 @@ module Emails from: sender(updated_by_user_id), to: recipient(recipient_id), subject: subject("#{@issue.title} (##{@issue.iid})")) + + SentNotification.record(@issue, recipient_id, reply_key) end def closed_issue_email(recipient_id, issue_id, updated_by_user_id) @@ -30,6 +34,8 @@ module Emails from: sender(updated_by_user_id), to: recipient(recipient_id), subject: subject("#{@issue.title} (##{@issue.iid})")) + + SentNotification.record(@issue, recipient_id, reply_key) end def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id) @@ -42,6 +48,8 @@ module Emails from: sender(updated_by_user_id), to: recipient(recipient_id), subject: subject("#{@issue.title} (##{@issue.iid})")) + + SentNotification.record(@issue, recipient_id, reply_key) end end end diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 512a8f7ea6..7923fb770d 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -10,6 +10,8 @@ module Emails from: sender(@merge_request.author_id), to: recipient(recipient_id), subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + + SentNotification.record(@merge_request, recipient_id, reply_key) end def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id) @@ -23,6 +25,8 @@ module Emails from: sender(updated_by_user_id), to: recipient(recipient_id), subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + + SentNotification.record(@merge_request, recipient_id, reply_key) end def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) @@ -36,6 +40,8 @@ module Emails from: sender(updated_by_user_id), to: recipient(recipient_id), subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + + SentNotification.record(@merge_request, recipient_id, reply_key) end def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id) @@ -48,6 +54,8 @@ module Emails from: sender(updated_by_user_id), to: recipient(recipient_id), subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + + SentNotification.record(@merge_request, recipient_id, reply_key) end def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id) @@ -58,52 +66,12 @@ module Emails @target_url = namespace_project_merge_request_url(@project.namespace, @project, @merge_request) - set_reference("merge_request_#{merge_request_id}") mail_answer_thread(@merge_request, from: sender(updated_by_user_id), to: recipient(recipient_id), - subject: subject("#{@merge_request.title} (##{@merge_request.iid}) #{@mr_status}")) + subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + + SentNotification.record(@merge_request, recipient_id, reply_key) end end - - # Over rides default behaviour to show source/target - # Formats arguments into a String suitable for use as an email subject - # - # extra - Extra Strings to be inserted into the subject - # - # Examples - # - # >> subject('Lorem ipsum') - # => "GitLab Merge Request | Lorem ipsum" - # - # # Automatically inserts Project name: - # Forked MR - # => source project =>### this is a reply from Android 5 gmail
+The quick brown fox jumps over the lazy dog. The quick brown= + fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. = +The quick brown fox jumps over the lazy dog. The quick brown fox jumps over= + the lazy dog. The quick brown fox jumps over the lazy dog.
+This is **bold** in Markdown.
+This is a link to http://exam= +ple.com
++ +++ +
+ + ++ ++ ++
+ techAPJ +
+ November 28 ++ + ++ +Test reply.
+ +First paragraph.
+ +Second paragraph.
+++To respond, reply to this email or visit https:/= +/meta.discourse.org/t/testing-default-email-replies/22638/3 in your bro= +wser.
+
+Previous Replies
+ ++ +
+ + ++ ++ ++
+ codinghorror +
+ November 28 ++ + ++ +We're testing the latest GitHub emai= +l processing library which we are integrating now.
+ +https://github.com/github/email_reply_parser
+ +Go ahead and reply to this topic and I= +39;ll reply from various email clients for testing.
+
+ +++To respond, reply to this email or visit https://met= +a.discourse.org/t/testing-default-email-replies/22638/3 in your browser= +.
+++To unsubscribe from these emails, visit your user preferences.
+
++
++ ++ ++
+ Neil<= +/a> +
+No= +vember 19 ++ ++ +Actually, deleting a spammer does what it's s= +upposed to. It does mark the topic as deleted.
+ +That topic has id 11002, and you're right tha= +t the user was deleted.
+ +@eviltrout Any idea why it showed up in suggested topics?
+++To respond, reply to this email or visit http://meta.discourse.org/t/spam-post-pops-back= +-up-in-suggested-topics/11005/5 in your browser.
+ +
+Previous Replies
+ ++
++ ++ ++
+ Neil<= +/a> +
+No= +vember 19 ++ ++ Looks= + like a bug when deleting a spammer. I'll look at it.
+
++ ++ ++
+ rik= +ing +
+No= +vember 19 ++ ++ ++ ++codinghorror:
+ +I can't even find that topic by n= +ame.
In that case, I'm fairly certain someone used= + the 'Delete Spammer' function on the user, which would explain you= +r inability to find it - it's gone.
+ +I'm raising this because, well, it's gone= + and shouldn't be showing up. And even if it was hanging around, it sho= +uld be invisible to me, and not showing up in Suggested Topics.
++
++ ++ ++
+ codinghorror +
+No= +vember 19 ++ ++ Hmm, = +that's interesting -- can you have a look @eviltrout? I can't even find that topic by= + name.
++
++ ++ ++
+ rik= +ing +
+No= +vember 19 ++ ++ +I'm one of the users who flagged this particu= +lar spam post, and it was promptly deleted/hidden, but it just popped up in= + the Suggested Topics box:
+ + + + +We may want to recheck the suppression on these.<= +/p> +
+++To respond, reply to this email or visit http://meta.discourse.org/t/spam-post-pops-back-up-= +in-suggested-topics/11005/5 in your browser.
+ +++To unsubscribe from these emails, visit your user pre= +ferences.
+
+ +++ +
+ + ++ ++ ++
+ techAPJ +
+ November 28 ++ + ++ +Test reply.
+ +First paragraph.
+ +Second paragraph.
+++To respond, reply to this email or visit https:/= +/meta.discourse.org/t/testing-default-email-replies/22638/3 in your bro= +wser.
+
+Previous Replies
+ ++ +
+ + ++ ++ ++
+ codinghorror +
+ November 28 ++ + ++ +We're testing the latest GitHub emai= +l processing library which we are integrating now.
+ +https://github.com/github/email_reply_parser
+ +Go ahead and reply to this topic and I= +39;ll reply from various email clients for testing.
+
+ +++To respond, reply to this email or visit https://met= +a.discourse.org/t/testing-default-email-replies/22638/3 in your browser= +.
+++To unsubscribe from these emails, visit your user preferences.
+
+ ++ ++ +
+ + ++ ++ ++
+ ajalan +
+ October 8 ++ + ++ Nice to be here! Thanks!
++To respond, reply to this email or visit http://discourse= +.techapj.com/t/welcome-to-techapjs-discourse/35/2 in your browser.
+
+Previous Replies
+ ++ +
+ + ++ ++ ++
+ techAPJ +
+ October 8 ++ + ++ Welcome to techAPJ's Discourse!
+ +++To respond, reply to this email or visit http://discourse.tec= +hapj.com/t/welcome-to-techapjs-discourse/35/2 in your browser.
+++To unsubscribe from these emails, visit your user preferences.
+
+--Apple-Mail-B41C7F8E-3639-49B0-A5D5-440E125A7105-- diff --git a/spec/fixtures/emails/newlines.eml b/spec/fixtures/emails/newlines.eml new file mode 100644 index 0000000000..cf03b9d18b --- /dev/null +++ b/spec/fixtures/emails/newlines.eml @@ -0,0 +1,84 @@ +In-Reply-To:+ +++ +
+ + ++ ++ ++
+ techAPJ +
+ November 28 ++ + ++ +Test reply.
+ +First paragraph.
+ +Second paragraph.
+++To respond, reply to this email or visit https://meta.discourse.org/t/testing-default-email-replies/22638/3 in your browser.
+
+Previous Replies
+ ++ +
+ + ++ ++ ++
+ codinghorror +
+ November 28 ++ + ++ +We're testing the latest GitHub email processing library which we are integrating now.
+ +https://github.com/github/email_reply_parser
+ +Go ahead and reply to this topic and I'll reply from various email clients for testing.
+
+ +++To respond, reply to this email or visit https://meta.discourse.org/t/testing-default-email-replies/22638/3 in your browser.
+++To unsubscribe from these emails, visit your user preferences.
+
+ + +++ +
+ + ++ ++ ++
+ gordon_ryan +
+ September 9 ++ + ++ +@riking- willing to step by step of the custom icon me= +thod for an admittedly ignorant admin? Seriously confused.
+ +Or anyone else who knows how to do this = +
+++To respond, reply to this email or visit https:= +//meta.discourse.org/t/badge-icons-where-to-find-them/18058/9 in your b= +rowser.
+
+Previous Replies
+ ++ +
+ ++ ++ ++
+ riking +
+ July 25 ++ + ++ Check out the "HTML Head" section in the "Content&= +quot; tab of the admin panel.
+ +
+ ++ ++ ++
+ meglio +
+ July 25 ++ + ++ How will it load the related custom font?
+ +
+ + ++ ++ ++
+ riking +
+ July 25 ++ + ++ +Here's an example of the styles that= + FA applies. I'll use
+ + +<i class=3D"fa fa-heart"></i>as the e= +xample.+ +.fa { + display: inline-block; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.fa-heart:before { + content: "\f004"; +}So you could do this in your site styles= +heet:
+ + ++ +.fa-custom-burger:before { + content: "\01f354"; + font-family: inherit; +}And get =F0=9F=8D=94 as your badge icon = +when you enter
+cus= +tom-burger.
+ +++To respond, reply to this email or visit https://me= +ta.discourse.org/t/badge-icons-where-to-find-them/18058/9 in your brows= +er.
+++To unsubscribe from these emails, visit your user preferences.
+
' \
- 'Code block...'
+ expected = %r{Code block\.\.\.}
expect(event_note(input)).to match(expected)
end
@@ -55,7 +54,7 @@ describe EventsHelper do
it 'should preserve code color scheme' do
input = "```ruby\ndef test\n 'hello world'\nend\n```"
- expected = '' \
+ expected = '' \
"def test\n" \
" \'hello world\'\n" \
"end" \
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index d814b56211..06f69262b7 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -1,72 +1,82 @@
require 'spec_helper'
describe PreferencesHelper do
- describe 'user_application_theme' do
- context 'with a user' do
- it "returns user's theme's css_class" do
- user = double('user', theme_id: 3)
- allow(self).to receive(:current_user).and_return(user)
- expect(user_application_theme).to eq 'ui_green'
- end
-
- it 'returns the default when id is invalid' do
- user = double('user', theme_id: Gitlab::Themes::THEMES.size + 5)
-
- allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(2)
- allow(self).to receive(:current_user).and_return(user)
-
- expect(user_application_theme).to eq 'ui_charcoal'
- end
- end
-
- context 'without a user' do
- before do
- allow(self).to receive(:current_user).and_return(nil)
- end
-
- it 'returns the default theme' do
- expect(user_application_theme).to eq Gitlab::Themes.default.css_class
- end
- end
- end
-
describe 'dashboard_choices' do
it 'raises an exception when defined choices may be missing' do
expect(User).to receive(:dashboards).and_return(foo: 'foo')
- expect { dashboard_choices }.to raise_error(RuntimeError)
+ expect { helper.dashboard_choices }.to raise_error(RuntimeError)
end
it 'raises an exception when defined choices may be using the wrong key' do
expect(User).to receive(:dashboards).and_return(foo: 'foo', bar: 'bar')
- expect { dashboard_choices }.to raise_error(KeyError)
+ expect { helper.dashboard_choices }.to raise_error(KeyError)
end
it 'provides better option descriptions' do
- expect(dashboard_choices).to match_array [
+ expect(helper.dashboard_choices).to match_array [
['Your Projects (default)', 'projects'],
['Starred Projects', 'stars']
]
end
end
- describe 'user_color_scheme_class' do
- context 'with current_user is nil' do
- it 'should return a string' do
- allow(self).to receive(:current_user).and_return(nil)
- expect(user_color_scheme_class).to be_kind_of(String)
+ describe 'user_application_theme' do
+ context 'with a user' do
+ it "returns user's theme's css_class" do
+ stub_user(theme_id: 3)
+
+ expect(helper.user_application_theme).to eq 'ui_green'
+ end
+
+ it 'returns the default when id is invalid' do
+ stub_user(theme_id: Gitlab::Themes.count + 5)
+
+ allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(2)
+
+ expect(helper.user_application_theme).to eq 'ui_charcoal'
end
end
- context 'with a current_user' do
- (1..5).each do |color_scheme_id|
- context "with color_scheme_id == #{color_scheme_id}" do
- it 'should return a string' do
- current_user = double(color_scheme_id: color_scheme_id)
- allow(self).to receive(:current_user).and_return(current_user)
- expect(user_color_scheme_class).to be_kind_of(String)
- end
- end
+ context 'without a user' do
+ it 'returns the default theme' do
+ stub_user
+
+ expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class
end
end
end
+
+ describe 'user_color_scheme' do
+ context 'with a user' do
+ it "returns user's scheme's css_class" do
+ allow(helper).to receive(:current_user).
+ and_return(double(color_scheme_id: 3))
+
+ expect(helper.user_color_scheme).to eq 'solarized-light'
+ end
+
+ it 'returns the default when id is invalid' do
+ allow(helper).to receive(:current_user).
+ and_return(double(color_scheme_id: Gitlab::ColorSchemes.count + 5))
+ end
+ end
+
+ context 'without a user' do
+ it 'returns the default theme' do
+ stub_user
+
+ expect(helper.user_color_scheme).
+ to eq Gitlab::ColorSchemes.default.css_class
+ end
+ end
+ end
+
+ def stub_user(messages = {})
+ if messages.empty?
+ allow(helper).to receive(:current_user).and_return(nil)
+ else
+ allow(helper).to receive(:current_user).
+ and_return(double('user', messages))
+ end
+ end
end
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
index f8958c9bab..0e826a319e 100644
--- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::BitbucketImport::ProjectCreator do
- let(:user) { create(:user, bitbucket_access_token: "asdffg", bitbucket_access_token_secret: "sekret") }
+ let(:user) { create(:user) }
let(:repo) do
{
name: 'Vim',
@@ -11,6 +11,9 @@ describe Gitlab::BitbucketImport::ProjectCreator do
}.with_indifferent_access
end
let(:namespace){ create(:group, owner: user) }
+ let(:token) { "asdasd12345" }
+ let(:secret) { "sekrettt" }
+ let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } }
before do
namespace.add_owner(user)
@@ -19,7 +22,7 @@ describe Gitlab::BitbucketImport::ProjectCreator do
it 'creates project' do
allow_any_instance_of(Project).to receive(:add_import_job)
- project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user)
+ project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user, access_params)
project = project_creator.execute
expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git")
diff --git a/spec/lib/gitlab/color_schemes_spec.rb b/spec/lib/gitlab/color_schemes_spec.rb
new file mode 100644
index 0000000000..c7be45dbcd
--- /dev/null
+++ b/spec/lib/gitlab/color_schemes_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::ColorSchemes do
+ describe '.body_classes' do
+ it 'returns a space-separated list of class names' do
+ css = described_class.body_classes
+
+ expect(css).to include('white')
+ expect(css).to include(' solarized-light ')
+ expect(css).to include(' monokai')
+ end
+ end
+
+ describe '.by_id' do
+ it 'returns a scheme by its ID' do
+ expect(described_class.by_id(1).name).to eq 'White'
+ expect(described_class.by_id(4).name).to eq 'Solarized Dark'
+ end
+ end
+
+ describe '.default' do
+ it 'returns the default scheme' do
+ expect(described_class.default.id).to eq 1
+ end
+ end
+
+ describe '.each' do
+ it 'passes the block to the SCHEMES Array' do
+ ids = []
+ described_class.each { |scheme| ids << scheme.id }
+ expect(ids).not_to be_empty
+ end
+ end
+
+ describe '.for_user' do
+ it 'returns default when user is nil' do
+ expect(described_class.for_user(nil).id).to eq 1
+ end
+
+ it "returns user's preferred color scheme" do
+ user = double(color_scheme_id: 5)
+ expect(described_class.for_user(user).id).to eq 5
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/attachment_uploader_spec.rb b/spec/lib/gitlab/email/attachment_uploader_spec.rb
new file mode 100644
index 0000000000..e8208e15e2
--- /dev/null
+++ b/spec/lib/gitlab/email/attachment_uploader_spec.rb
@@ -0,0 +1,20 @@
+require "spec_helper"
+
+describe Gitlab::Email::AttachmentUploader do
+ describe "#execute" do
+ let(:project) { build(:project) }
+ let(:message_raw) { fixture_file("emails/attachment.eml") }
+ let(:message) { Mail::Message.new(message_raw) }
+
+ it "uploads all attachments and returns their links" do
+ links = described_class.new(message).execute(project)
+ link = links.first
+
+ expect(link).not_to be_nil
+ expect(link[:is_image]).to be_truthy
+ expect(link[:alt]).to eq("bricks")
+ expect(link[:url]).to include("/#{project.path_with_namespace}")
+ expect(link[:url]).to include("bricks.png")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
new file mode 100644
index 0000000000..1cc80f35f9
--- /dev/null
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -0,0 +1,138 @@
+require "spec_helper"
+
+describe Gitlab::Email::Receiver do
+ before do
+ stub_reply_by_email_setting(enabled: true, address: "reply+%{reply_key}@appmail.adventuretime.ooo")
+ end
+
+ let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
+ let(:email_raw) { fixture_file('emails/valid_reply.eml') }
+
+ let(:project) { create(:project, :public) }
+ let(:noteable) { create(:issue, project: project) }
+ let(:user) { create(:user) }
+ let!(:sent_notification) { SentNotification.record(noteable, user.id, reply_key) }
+
+ let(:receiver) { described_class.new(email_raw) }
+
+ context "when the recipient address doesn't include a reply key" do
+ let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(reply_key, "") }
+
+ it "raises a SentNotificationNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::SentNotificationNotFoundError)
+ end
+ end
+
+ context "when no sent notificiation for the reply key could be found" do
+ let(:email_raw) { fixture_file('emails/wrong_reply_key.eml') }
+
+ it "raises a SentNotificationNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::SentNotificationNotFoundError)
+ end
+ end
+
+ context "when the email is blank" do
+ let(:email_raw) { "" }
+
+ it "raises an EmptyEmailError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError)
+ end
+ end
+
+ context "when the email was auto generated" do
+ let!(:reply_key) { '636ca428858779856c226bb145ef4fad' }
+ let!(:email_raw) { fixture_file("emails/auto_reply.eml") }
+
+ it "raises an AutoGeneratedEmailError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::AutoGeneratedEmailError)
+ end
+ end
+
+ context "when the user could not be found" do
+ before do
+ user.destroy
+ end
+
+ it "raises a UserNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserNotFoundError)
+ end
+ end
+
+ context "when the user has been blocked" do
+ before do
+ user.block
+ end
+
+ it "raises a UserBlockedError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserBlockedError)
+ end
+ end
+
+ context "when the user is not authorized to create a note" do
+ before do
+ project.update_attribute(:visibility_level, Project::PRIVATE)
+ end
+
+ it "raises a UserNotAuthorizedError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserNotAuthorizedError)
+ end
+ end
+
+ context "when the noteable could not be found" do
+ before do
+ noteable.destroy
+ end
+
+ it "raises a NoteableNotFoundError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::NoteableNotFoundError)
+ end
+ end
+
+ context "when the reply is blank" do
+ let!(:email_raw) { fixture_file("emails/no_content_reply.eml") }
+
+ it "raises an EmptyEmailError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError)
+ end
+ end
+
+ context "when the note could not be saved" do
+ before do
+ allow_any_instance_of(Note).to receive(:persisted?).and_return(false)
+ end
+
+ it "raises an InvalidNoteError" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::InvalidNoteError)
+ end
+ end
+
+ context "when everything is fine" do
+ before do
+ allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return(
+ [
+ {
+ url: "uploads/image.png",
+ is_image: true,
+ alt: "image"
+ }
+ ]
+ )
+ end
+
+ it "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
+
+ it "adds all attachments" do
+ receiver.execute
+
+ note = noteable.notes.last
+
+ expect(note.note).to include("")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
new file mode 100644
index 0000000000..7cae1da805
--- /dev/null
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -0,0 +1,210 @@
+require "spec_helper"
+
+# Inspired in great part by Discourse's Email::Receiver
+describe Gitlab::Email::ReplyParser do
+ describe '#execute' do
+ def test_parse_body(mail_string)
+ described_class.new(Mail::Message.new(mail_string)).execute
+ end
+
+ it "returns an empty string if the message is blank" do
+ expect(test_parse_body("")).to eq("")
+ end
+
+ it "returns an empty string if the message is not an email" do
+ expect(test_parse_body("asdf" * 30)).to eq("")
+ end
+
+ it "returns an empty string if there is no reply content" do
+ expect(test_parse_body(fixture_file("emails/no_content_reply.eml"))).to eq("")
+ end
+
+ it "properly renders plaintext-only email" do
+ expect(test_parse_body(fixture_file("emails/plaintext_only.eml"))).
+ to eq(
+ <<-BODY.strip_heredoc.chomp
+ ### reply from default mail client in Windows 8.1 Metro
+
+
+ The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
+
+
+ This is a **bold** word in Markdown
+
+
+ This is a link http://example.com
+ BODY
+ )
+ end
+
+ it "supports a Dutch reply" do
+ expect(test_parse_body(fixture_file("emails/dutch.eml"))).to eq("Dit is een antwoord in het Nederlands.")
+ end
+
+ it "removes an 'on date wrote' quoting line" do
+ expect(test_parse_body(fixture_file("emails/on_wrote.eml"))).to eq("Sure, all you need to do is frobnicate the foobar and you'll be all set!")
+ end
+
+ it "handles multiple paragraphs" do
+ expect(test_parse_body(fixture_file("emails/paragraphs.eml"))).
+ to eq(
+ <<-BODY.strip_heredoc.chomp
+ Is there any reason the *old* candy can't be be kept in silos while the new candy
+ is imported into *new* silos?
+
+ The thing about candy is it stays delicious for a long time -- we can just keep
+ it there without worrying about it too much, imo.
+
+ Thanks for listening.
+ BODY
+ )
+ end
+
+ it "handles multiple paragraphs when parsing html" do
+ expect(test_parse_body(fixture_file("emails/html_paragraphs.eml"))).
+ to eq(
+ <<-BODY.strip_heredoc.chomp
+ Awesome!
+
+ Pleasure to have you here!
+
+ :boom:
+ BODY
+ )
+ end
+
+ it "handles newlines" do
+ expect(test_parse_body(fixture_file("emails/newlines.eml"))).
+ to eq(
+ <<-BODY.strip_heredoc.chomp
+ This is my reply.
+ It is my best reply.
+ It will also be my *only* reply.
+ BODY
+ )
+ end
+
+ it "handles inline reply" do
+ expect(test_parse_body(fixture_file("emails/inline_reply.eml"))).
+ to eq(
+ <<-BODY.strip_heredoc.chomp
+ On Wed, Oct 8, 2014 at 11:12 AM, techAPJ wrote:
+
+ > techAPJ
+ > November 28
+ >
+ > Test reply.
+ >
+ > First paragraph.
+ >
+ > Second paragraph.
+ >
+ > To respond, reply to this email or visit
+ > https://meta.discourse.org/t/testing-default-email-replies/22638/3 in
+ > your browser.
+ > ------------------------------
+ > Previous Replies codinghorror
+ >
+ > November 28
+ >
+ > We're testing the latest GitHub email processing library which we are
+ > integrating now.
+ >
+ > https://github.com/github/email_reply_parser
+ >
+ > Go ahead and reply to this topic and I'll reply from various email clients
+ > for testing.
+ > ------------------------------
+ >
+ > To respond, reply to this email or visit
+ > https://meta.discourse.org/t/testing-default-email-replies/22638/3 in
+ > your browser.
+ >
+ > To unsubscribe from these emails, visit your user preferences
+ > .
+ >
+
+ The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
+ the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown
+ fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
+ The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
+ the lazy dog. The quick brown fox jumps over the lazy dog.
+ BODY
+ )
+ end
+
+ it "properly renders email reply from gmail web client" do
+ expect(test_parse_body(fixture_file("emails/gmail_web.eml"))).
+ to eq(
+ <<-BODY.strip_heredoc.chomp
+ ### This is a reply from standard GMail in Google Chrome.
+
+ The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
+ the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown
+ fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
+ The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
+ the lazy dog. The quick brown fox jumps over the lazy dog.
+
+ Here's some **bold** text in Markdown.
+
+ Here's a link http://example.com
+ BODY
+ )
+ end
+
+ it "properly renders email reply from iOS default mail client" do
+ expect(test_parse_body(fixture_file("emails/ios_default.eml"))).
+ to eq(
+ <<-BODY.strip_heredoc.chomp
+ ### this is a reply from iOS default mail
+
+ The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
+
+ Here's some **bold** markdown text.
+
+ Here's a link http://example.com
+ BODY
+ )
+ end
+
+ it "properly renders email reply from Android 5 gmail client" do
+ expect(test_parse_body(fixture_file("emails/android_gmail.eml"))).
+ to eq(
+ <<-BODY.strip_heredoc.chomp
+ ### this is a reply from Android 5 gmail
+
+ The quick brown fox jumps over the lazy dog. The quick brown fox jumps over
+ the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown
+ fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
+ The quick brown fox jumps over the lazy dog.
+
+ This is **bold** in Markdown.
+
+ This is a link to http://example.com
+ BODY
+ )
+ end
+
+ it "properly renders email reply from Windows 8.1 Metro default mail client" do
+ expect(test_parse_body(fixture_file("emails/windows_8_metro.eml"))).
+ to eq(
+ <<-BODY.strip_heredoc.chomp
+ ### reply from default mail client in Windows 8.1 Metro
+
+
+ The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fox jumps over the lazy dog.
+
+
+ This is a **bold** word in Markdown
+
+
+ This is a link http://example.com
+ BODY
+ )
+ end
+
+ it "properly renders email reply from MS Outlook client" do
+ expect(test_parse_body(fixture_file("emails/outlook.eml"))).to eq("Microsoft Outlook 2010")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
index 4fe7bd3b77..ca61d3c523 100644
--- a/spec/lib/gitlab/github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/github_import/project_creator_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::GithubImport::ProjectCreator do
- let(:user) { create(:user, github_access_token: "asdffg") }
+ let(:user) { create(:user) }
let(:repo) do
OpenStruct.new(
login: 'vim',
@@ -13,6 +13,8 @@ describe Gitlab::GithubImport::ProjectCreator do
)
end
let(:namespace){ create(:group, owner: user) }
+ let(:token) { "asdffg" }
+ let(:access_params) { { github_access_token: token } }
before do
namespace.add_owner(user)
@@ -21,7 +23,7 @@ describe Gitlab::GithubImport::ProjectCreator do
it 'creates project' do
allow_any_instance_of(Project).to receive(:add_import_job)
- project_creator = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, user)
+ project_creator = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, user, access_params)
project = project_creator.execute
expect(project.import_url).to eq("https://asdffg@gitlab.com/asd/vim.git")
diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
index 938d08396f..2d8923d14b 100644
--- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::GitlabImport::ProjectCreator do
- let(:user) { create(:user, gitlab_access_token: "asdffg") }
+ let(:user) { create(:user) }
let(:repo) do
{
name: 'vim',
@@ -13,6 +13,8 @@ describe Gitlab::GitlabImport::ProjectCreator do
}.with_indifferent_access
end
let(:namespace){ create(:group, owner: user) }
+ let(:token) { "asdffg" }
+ let(:access_params) { { gitlab_access_token: token } }
before do
namespace.add_owner(user)
@@ -21,7 +23,7 @@ describe Gitlab::GitlabImport::ProjectCreator do
it 'creates project' do
allow_any_instance_of(Project).to receive(:add_import_job)
- project_creator = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, user)
+ project_creator = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, user, access_params)
project = project_creator.execute
expect(project.import_url).to eq("https://oauth2:asdffg@gitlab.com/asd/vim.git")
diff --git a/spec/lib/gitlab/google_code_import/client_spec.rb b/spec/lib/gitlab/google_code_import/client_spec.rb
index 6aa4428f36..37985c062b 100644
--- a/spec/lib/gitlab/google_code_import/client_spec.rb
+++ b/spec/lib/gitlab/google_code_import/client_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::GoogleCodeImport::Client do
- let(:raw_data) { JSON.parse(File.read(Rails.root.join("spec/fixtures/GoogleCodeProjectHosting.json"))) }
+ let(:raw_data) { JSON.parse(fixture_file("GoogleCodeProjectHosting.json")) }
subject { described_class.new(raw_data) }
describe "#valid?" do
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
index f49cbb7f53..65ad7524cc 100644
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -2,7 +2,7 @@ require "spec_helper"
describe Gitlab::GoogleCodeImport::Importer do
let(:mapped_user) { create(:user, username: "thilo123") }
- let(:raw_data) { JSON.parse(File.read(Rails.root.join("spec/fixtures/GoogleCodeProjectHosting.json"))) }
+ let(:raw_data) { JSON.parse(fixture_file("GoogleCodeProjectHosting.json")) }
let(:client) { Gitlab::GoogleCodeImport::Client.new(raw_data) }
let(:import_data) do
{
diff --git a/spec/lib/gitlab/markdown/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/autolink_filter_spec.rb
index 982be0782c..26332ba521 100644
--- a/spec/lib/gitlab/markdown/autolink_filter_spec.rb
+++ b/spec/lib/gitlab/markdown/autolink_filter_spec.rb
@@ -86,6 +86,16 @@ module Gitlab::Markdown
doc = filter("See #{link}, ok?")
expect(doc.at_css('a').text).to eq link
+
+ doc = filter("See #{link}...")
+ expect(doc.at_css('a').text).to eq link
+ end
+
+ it 'does not include trailing HTML entities' do
+ doc = filter("See <<<#{link}>>>")
+
+ expect(doc.at_css('a')['href']).to eq link
+ expect(doc.text).to eq "See <<<#{link}>>>"
end
it 'accepts link_attr options' do
diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
index a5405e14a7..02d923b036 100644
--- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
+++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
@@ -121,7 +121,6 @@ module Gitlab::Markdown
end
it 'links with adjacent text' do
- skip "TODO (rspeicher): Re-enable when usernames can't end in periods."
doc = filter("Mention me (#{reference}.)")
expect(doc.to_html).to match(/\(#{reference}<\/a>\.\)/)
end
diff --git a/spec/lib/gitlab/reply_by_email_spec.rb b/spec/lib/gitlab/reply_by_email_spec.rb
new file mode 100644
index 0000000000..a678c7e1a7
--- /dev/null
+++ b/spec/lib/gitlab/reply_by_email_spec.rb
@@ -0,0 +1,86 @@
+require "spec_helper"
+
+describe Gitlab::ReplyByEmail do
+ describe "self.enabled?" do
+ context "when reply by email is enabled" do
+ before do
+ stub_reply_by_email_setting(enabled: true)
+ end
+
+ context "when the address is valid" do
+ before do
+ stub_reply_by_email_setting(address: "replies+%{reply_key}@example.com")
+ end
+
+ it "returns true" do
+ expect(described_class.enabled?).to be_truthy
+ end
+ end
+
+ context "when the address is invalid" do
+ before do
+ stub_reply_by_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
+ before do
+ stub_reply_by_email_setting(enabled: false)
+ end
+
+ it "returns false" do
+ expect(described_class.enabled?).to be_falsey
+ end
+ end
+ end
+
+ describe "self.reply_key" do
+ context "when enabled" do
+ before do
+ allow(described_class).to receive(:enabled?).and_return(true)
+ end
+
+ it "returns a random hex" do
+ key = described_class.reply_key
+ key2 = described_class.reply_key
+
+ expect(key).not_to eq(key2)
+ end
+ end
+
+ context "when disabled" do
+ before do
+ allow(described_class).to receive(:enabled?).and_return(false)
+ end
+
+ it "returns nil" do
+ expect(described_class.reply_key).to be_nil
+ end
+ end
+ end
+
+ context "self.reply_address" do
+ before do
+ stub_reply_by_email_setting(address: "replies+%{reply_key}@example.com")
+ end
+
+ it "returns the address with an interpolated reply key" do
+ expect(described_class.reply_address("key")).to eq("replies+key@example.com")
+ end
+ end
+
+ context "self.reply_key_from_address" do
+ before do
+ stub_reply_by_email_setting(address: "replies+%{reply_key}@example.com")
+ end
+
+ it "returns reply key" do
+ expect(described_class.reply_key_from_address("replies+key@example.com")).to eq("key")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb
index 9c6c3fd810..e554458e41 100644
--- a/spec/lib/gitlab/themes_spec.rb
+++ b/spec/lib/gitlab/themes_spec.rb
@@ -43,9 +43,6 @@ describe Gitlab::Themes do
ids = []
described_class.each { |theme| ids << theme.id }
expect(ids).not_to be_empty
-
- # TODO (rspeicher): RSpec 3.x
- # expect(described_class.each).to yield_with_arg(described_class::Theme)
end
end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 1d5b4f6f36..13cced8187 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -7,7 +7,8 @@ describe API::API, api: true do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
- let!(:group1) { create(:group) }
+ let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
+ let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) }
let!(:group2) { create(:group) }
before do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 62cef9db53..c483060fd7 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -197,7 +197,7 @@ describe GitPushService do
end
end
- describe "closing issues from pushed commits" do
+ describe "closing issues from pushed commits containing a closing reference" do
let(:issue) { create :issue, project: project }
let(:other_issue) { create :issue, project: project }
let(:commit_author) { create :user }
@@ -215,36 +215,47 @@ describe GitPushService do
and_return([closing_commit])
end
- it "closes issues with commit messages" do
- service.execute(project, user, @oldrev, @newrev, @ref)
-
- expect(Issue.find(issue.id)).to be_closed
- end
-
- it "doesn't create cross-reference notes for a closing reference" do
- expect do
+ context "to default branches" do
+ it "closes issues" do
service.execute(project, user, @oldrev, @newrev, @ref)
- end.not_to change { Note.where(project_id: project.id, system: true, commit_id: closing_commit.id).count }
+ expect(Issue.find(issue.id)).to be_closed
+ end
+
+ it "adds a note indicating that the issue is now closed" do
+ expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit)
+ service.execute(project, user, @oldrev, @newrev, @ref)
+ end
+
+ it "doesn't create additional cross-reference notes" do
+ expect(SystemNoteService).not_to receive(:cross_reference)
+ service.execute(project, user, @oldrev, @newrev, @ref)
+ end
+
+ it "doesn't close issues when external issue tracker is in use" do
+ allow(project).to receive(:default_issues_tracker?).and_return(false)
+
+ # The push still shouldn't create cross-reference notes.
+ expect do
+ service.execute(project, user, @oldrev, @newrev, 'refs/heads/hurf')
+ end.not_to change { Note.where(project_id: project.id, system: true).count }
+ end
end
- it "doesn't close issues when pushed to non-default branches" do
- allow(project).to receive(:default_branch).and_return('durf')
+ context "to non-default branches" do
+ before do
+ # Make sure the "default" branch is different
+ allow(project).to receive(:default_branch).and_return('not-master')
+ end
- # The push still shouldn't create cross-reference notes.
- expect do
- service.execute(project, user, @oldrev, @newrev, 'refs/heads/hurf')
- end.not_to change { Note.where(project_id: project.id, system: true).count }
+ it "creates cross-reference notes" do
+ expect(SystemNoteService).to receive(:cross_reference).with(issue, closing_commit, commit_author)
+ service.execute(project, user, @oldrev, @newrev, @ref)
+ end
- expect(Issue.find(issue.id)).to be_opened
- end
-
- it "doesn't close issues when external issue tracker is in use" do
- allow(project).to receive(:default_issues_tracker?).and_return(false)
-
- # The push still shouldn't create cross-reference notes.
- expect do
- service.execute(project, user, @oldrev, @newrev, 'refs/heads/hurf')
- end.not_to change { Note.where(project_id: project.id, system: true).count }
+ it "doesn't close issues" do
+ service.execute(project, user, @oldrev, @newrev, @ref)
+ expect(Issue.find(issue.id)).to be_opened
+ end
end
end
diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb
index 7aa2685764..fa4ff6b01a 100644
--- a/spec/services/projects/upload_service_spec.rb
+++ b/spec/services/projects/upload_service_spec.rb
@@ -13,13 +13,13 @@ describe Projects::UploadService do
@link_to_file = upload_file(@project.repository, gif)
end
- it { expect(@link_to_file).to have_key('alt') }
- it { expect(@link_to_file).to have_key('url') }
- it { expect(@link_to_file).to have_key('is_image') }
+ it { expect(@link_to_file).to have_key(:alt) }
+ it { expect(@link_to_file).to have_key(:url) }
+ it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file).to have_value('banana_sample') }
- it { expect(@link_to_file['is_image']).to equal(true) }
- it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
- it { expect(@link_to_file['url']).to match('banana_sample.gif') }
+ it { expect(@link_to_file[:is_image]).to equal(true) }
+ it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file[:url]).to match('banana_sample.gif') }
end
context 'for valid png file' do
@@ -29,13 +29,13 @@ describe Projects::UploadService do
@link_to_file = upload_file(@project.repository, png)
end
- it { expect(@link_to_file).to have_key('alt') }
- it { expect(@link_to_file).to have_key('url') }
+ it { expect(@link_to_file).to have_key(:alt) }
+ it { expect(@link_to_file).to have_key(:url) }
it { expect(@link_to_file).to have_value('dk') }
- it { expect(@link_to_file).to have_key('is_image') }
- it { expect(@link_to_file['is_image']).to equal(true) }
- it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
- it { expect(@link_to_file['url']).to match('dk.png') }
+ it { expect(@link_to_file).to have_key(:is_image) }
+ it { expect(@link_to_file[:is_image]).to equal(true) }
+ it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file[:url]).to match('dk.png') }
end
context 'for valid jpg file' do
@@ -44,13 +44,13 @@ describe Projects::UploadService do
@link_to_file = upload_file(@project.repository, jpg)
end
- it { expect(@link_to_file).to have_key('alt') }
- it { expect(@link_to_file).to have_key('url') }
- it { expect(@link_to_file).to have_key('is_image') }
+ it { expect(@link_to_file).to have_key(:alt) }
+ it { expect(@link_to_file).to have_key(:url) }
+ it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file).to have_value('rails_sample') }
- it { expect(@link_to_file['is_image']).to equal(true) }
- it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
- it { expect(@link_to_file['url']).to match('rails_sample.jpg') }
+ it { expect(@link_to_file[:is_image]).to equal(true) }
+ it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file[:url]).to match('rails_sample.jpg') }
end
context 'for txt file' do
@@ -59,13 +59,13 @@ describe Projects::UploadService do
@link_to_file = upload_file(@project.repository, txt)
end
- it { expect(@link_to_file).to have_key('alt') }
- it { expect(@link_to_file).to have_key('url') }
- it { expect(@link_to_file).to have_key('is_image') }
+ it { expect(@link_to_file).to have_key(:alt) }
+ it { expect(@link_to_file).to have_key(:url) }
+ it { expect(@link_to_file).to have_key(:is_image) }
it { expect(@link_to_file).to have_value('doc_sample.txt') }
- it { expect(@link_to_file['is_image']).to equal(false) }
- it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
- it { expect(@link_to_file['url']).to match('doc_sample.txt') }
+ it { expect(@link_to_file[:is_image]).to equal(false) }
+ it { expect(@link_to_file[:url]).to match("/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file[:url]).to match('doc_sample.txt') }
end
context 'for too large a file' do
diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb
new file mode 100644
index 0000000000..a05c9d1800
--- /dev/null
+++ b/spec/support/fixture_helpers.rb
@@ -0,0 +1,11 @@
+module FixtureHelpers
+ def fixture_file(filename)
+ return '' if filename.blank?
+ file_path = File.expand_path(Rails.root.join('spec/fixtures/', filename))
+ File.read(file_path)
+ end
+end
+
+RSpec.configure do |config|
+ config.include FixtureHelpers
+end
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index c59df4e84d..39a6439146 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -100,7 +100,7 @@ class MarkdownFeature
end
def raw_markdown
- fixture = Rails.root.join('spec/fixtures/markdown.md.erb')
- ERB.new(File.read(fixture)).result(binding)
+ markdown = File.read(Rails.root.join('spec/fixtures/markdown.md.erb'))
+ ERB.new(markdown).result(binding)
end
end
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index e4004ec8f7..ef3a120d44 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -17,6 +17,10 @@ module StubConfiguration
allow(Gitlab.config.gravatar).to receive_messages(messages)
end
+ def stub_reply_by_email_setting(messages)
+ allow(Gitlab.config.reply_by_email).to receive_messages(messages)
+ end
+
private
# Modifies stubbed messages to also stub possible predicate versions
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
new file mode 100644
index 0000000000..e8f1bd2fa2
--- /dev/null
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -0,0 +1,45 @@
+require "spec_helper"
+
+describe EmailReceiverWorker do
+ let(:raw_message) { fixture_file('emails/valid_reply.eml') }
+
+ context "when reply by email is enabled" do
+ before do
+ allow(Gitlab::ReplyByEmail).to receive(:enabled?).and_return(true)
+ end
+
+ it "calls the email receiver" do
+ expect(Gitlab::Email::Receiver).to receive(:new).with(raw_message).and_call_original
+ expect_any_instance_of(Gitlab::Email::Receiver).to receive(:execute)
+
+ described_class.new.perform(raw_message)
+ end
+
+ context "when an error occurs" do
+ before do
+ allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(Gitlab::Email::Receiver::EmptyEmailError)
+ end
+
+ it "sends out a rejection email" do
+ described_class.new.perform(raw_message)
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email).not_to be_nil
+ expect(email.to).to eq(["jake@adventuretime.ooo"])
+ expect(email.subject).to include("Rejected")
+ end
+ end
+ end
+
+ context "when reply by email is disabled" do
+ before do
+ allow(Gitlab::ReplyByEmail).to receive(:enabled?).and_return(false)
+ end
+
+ it "doesn't call the email receiver" do
+ expect(Gitlab::Email::Receiver).not_to receive(:new)
+
+ described_class.new.perform(raw_message)
+ end
+ end
+end
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
new file mode 100644
index 0000000000..3600c77107
--- /dev/null
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe EmailsOnPushWorker do
+ include RepoHelpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+
+ subject { EmailsOnPushWorker.new }
+
+ before do
+ allow(Project).to receive(:find).and_return(project)
+ end
+
+ describe "#perform" do
+ it "sends mail" do
+ subject.perform(project.id, user.email, data.stringify_keys)
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email.subject).to include('Change some files')
+ expect(email.to).to eq([user.email])
+ end
+
+ it "gracefully handles an input SMTP error" do
+ ActionMailer::Base.deliveries.clear
+ allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
+
+ subject.perform(project.id, user.email, data.stringify_keys)
+
+ expect(ActionMailer::Base.deliveries.count).to eq(0)
+ end
+ end
+end