diff options
268 files changed, 5982 insertions, 1004 deletions
diff --git a/.gitignore b/.gitignore index 3e30fb8cf77..8a68bb3e4f0 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 d228ef616d4..a93558fb10d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,40 @@ Please view this file on the master branch, on stable branches it's out of date. -v 7.14.0 (unreleased) +v 8.0.0 (unreleased) + - 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) + - Fix "Reload with full diff" URL button in compare branch view (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) + - Faster merge + - Ability to fetch merge requests from refs/merge-requests/:id + - 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) + - Improve abuse reports management from admin area + - Move dashboard activity to separate page + +v 7.14.1 (unreleased) + - 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) + +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) + - Clear cache to prevent listing deleted branches after MR removes source branch (Stan Hu) + - Provide more feedback what went wrong if HipChat service failed test (Stan Hu) + - Fix bug where backslashes in inline diffs could be dropped (Stan Hu) + - Disable turbolinks when linking to Bitbucket import status (Stan Hu) + - Fix broken code import and display error messages if something went wrong with creating project (Stan Hu) + - Fix corrupted binary files when using API files endpoint (Stan Hu) + - Bump Haml to 4.0.7 to speed up textarea rendering (Stan Hu) - Show incompatible projects in Bitbucket import status (Stan Hu) - Fix coloring of diffs on MR Discussion-tab (Gert Goet) - Fix "Network" and "Graphs" pages for branches with encoded slashes (Stan Hu) @@ -23,14 +57,11 @@ v 7.14.0 (unreleased) - Fix file upload dialog for comment editing (Daniel Gerhardt) - Set OmniAuth full_host parameter to ensure redirect URIs are correct (Stan Hu) - Return comments in created order in merge request API (Stan Hu) + - Disable internal issue tracker controller if external tracker is used (Stan Hu) - Expire Rails cache entries after two weeks to prevent endless Redis growth - Add support for destroying project milestones (Stan Hu) - - Add fetch command to the MR page. - Allow custom backup archive permissions - - Add fetch command to the MR page - Add project star and fork count, group avatar URL and user/group web URL attributes to API - - Fix bug causing Bitbucket importer to crash when OAuth application had been removed. - - Add fetch command to the MR page. - Show who last edited a comment if it wasn't the original author - Send notification to all participants when MR is merged. - Add ability to manage user email addresses via the API. @@ -42,15 +73,34 @@ v 7.14.0 (unreleased) - Remove redis-store TTL monkey patch - Add support for CI skipped status - Fetch code from forks to refs/merge-requests/:id/head when merge request created - - Remove satellites - Remove comments and email addresses when publicly exposing ssh keys (Zeger-Jan van de Weg) + - Add "Check out branch" button to the MR page. - Improve MR merge widget text and UI consistency. - Improve text in MR "How To Merge" modal. - Cache all events + - Order commits by date when comparing branches + - Fix bug causing error when the target branch of a symbolic ref was deleted + - Include branch/tag name in archive file and directory name + - Add dropzone upload progress + - Add a label for merged branches on branches page (Florent Baldino) + - Detect .mkd and .mkdn files as markdown (Ben Boeckel) + - Fix: User search feature in admin area does not respect filters + - Set max-width for README, issue and merge request description for easier read on big screens + - Update Flowdock integration to support new Flowdock API (Boyan Tabakov) + - Remove author from files view (Sven Strickroth) + - Fix infinite loop when SAML was incorrectly configured. + +v 7.13.5 + - Satellites reverted + +v 7.13.4 + - Allow users to send abuse reports v 7.13.3 - Fix bug causing Bitbucket importer to crash when OAuth application had been removed. - Allow users to send abuse reports + - Remove satellites + - Link username to profile on Group Members page (Tom Webster) v 7.13.2 - Fix randomly failed spec @@ -192,7 +242,6 @@ v 7.12.0 - Add SAML support as an omniauth provider - Allow to configure a URL to show after sign out - Add an option to automatically sign-in with an Omniauth provider - - Better performance for web editor (switched from satellites to rugged) - GitLab CI service sends .gitlab-ci.yml in each push call - When remove project - move repository and schedule it removal - Improve group removing logic @@ -24,7 +24,7 @@ gem 'omniauth-shibboleth' gem 'omniauth-kerberos', group: :kerberos gem 'omniauth-gitlab' gem 'omniauth-bitbucket' -gem 'omniauth-saml' +gem 'omniauth-saml', '~> 1.4.0' gem 'doorkeeper', '2.1.3' gem "rack-oauth2", "~> 1.0.5" @@ -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.6' +gem "gitlab_git", '~> 7.2.14' # Ruby/Rack Git Smart-HTTP Server Handler # GitLab fork with a lot of changes (improved thread-safety, better memory usage etc) @@ -150,7 +150,7 @@ gem 'tinder', '~> 1.9.2' gem 'hipchat', '~> 1.5.0' # Flowdock integration -gem "gitlab-flowdock-git-hook", "~> 0.4.2" +gem "gitlab-flowdock-git-hook", "~> 1.0.1" # Gemnasium integration gem "gemnasium-gitlab-service", "~> 0.2" @@ -272,3 +272,7 @@ end gem "newrelic_rpm" gem 'octokit', '3.7.0' + +gem "mail_room", "~> 0.4.0" + +gem 'email_reply_parser' diff --git a/Gemfile.lock b/Gemfile.lock index 643c513161f..a7cfe6b7511 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) @@ -183,6 +184,9 @@ GEM ffi (1.9.8) fission (0.5.0) CFPropertyList (~> 2.2) + flowdock (0.7.0) + httparty (~> 0.7) + multi_json fog (1.25.0) fog-brightbox (~> 0.4) fog-core (~> 1.25) @@ -255,7 +259,8 @@ GEM racc github-markup (1.3.1) posix-spawn (~> 0.3.8) - gitlab-flowdock-git-hook (0.4.2.2) + gitlab-flowdock-git-hook (1.0.1) + flowdock (~> 0.7) gitlab-grit (>= 2.4.1) multi_json gitlab-grack (2.0.2) @@ -271,7 +276,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.0) gemojione (~> 2.0) - gitlab_git (7.2.6) + gitlab_git (7.2.14) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -307,7 +312,7 @@ GEM grape-entity (0.4.2) activesupport multi_json (>= 1.3.2) - haml (4.0.5) + haml (4.0.7) tilt haml-rails (0.5.3) actionpack (>= 4.0.1) @@ -367,6 +372,7 @@ GEM systemu (~> 2.6.2) mail (2.6.3) mime-types (>= 1.16, < 3) + mail_room (0.4.0) method_source (0.8.2) mime-types (1.25.1) mimemagic (0.3.0) @@ -422,9 +428,9 @@ GEM omniauth-oauth2 (1.1.1) oauth2 (~> 0.8.0) omniauth (~> 1.0) - omniauth-saml (1.3.1) + omniauth-saml (1.4.1) omniauth (~> 1.1) - ruby-saml (~> 0.8.1) + ruby-saml (~> 1.0.0) omniauth-shibboleth (1.1.1) omniauth (>= 1.0.0) omniauth-twitter (1.0.1) @@ -568,8 +574,8 @@ GEM rainbow (>= 1.99.1, < 3.0) ruby-progressbar (~> 1.4) ruby-progressbar (1.7.1) - ruby-saml (0.8.2) - nokogiri (>= 1.5.0) + ruby-saml (1.0.0) + nokogiri (>= 1.5.10) uuid (~> 2.3) ruby2ruby (2.1.3) ruby_parser (~> 3.1) @@ -709,7 +715,7 @@ GEM raindrops (~> 0.7) unicorn-worker-killer (0.4.2) unicorn (~> 4) - uuid (2.3.7) + uuid (2.3.8) macaddr (~> 1.0) version_sorter (2.0.0) virtus (1.0.1) @@ -749,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) @@ -769,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 @@ -779,11 +786,11 @@ DEPENDENCIES fuubar (~> 2.0.0) gemnasium-gitlab-service (~> 0.2) github-markup - gitlab-flowdock-git-hook (~> 0.4.2) + gitlab-flowdock-git-hook (~> 1.0.1) gitlab-grack (~> 2.0.2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.2.6) + gitlab_git (~> 7.2.14) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.1) gollum-lib (~> 4.0.2) @@ -801,6 +808,7 @@ DEPENDENCIES jquery-ui-rails kaminari (~> 0.15.1) letter_opener + mail_room (~> 0.4.0) minitest (~> 5.3.0) mousetrap-rails mysql2 @@ -813,7 +821,7 @@ DEPENDENCIES omniauth-gitlab omniauth-google-oauth2 omniauth-kerberos - omniauth-saml + omniauth-saml (~> 1.4.0) omniauth-shibboleth omniauth-twitter org-ruby (= 0.9.12) @@ -875,4 +883,4 @@ DEPENDENCIES wikicloth (= 0.8.1) BUNDLED WITH - 1.10.4 + 1.10.6 @@ -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 @@ -1 +1 @@ -7.13.0.pre +8.0.0.pre diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index bb0a0c51fd4..c263912b7ea 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/commit/image-file.js.coffee b/app/assets/javascripts/commit/image-file.js.coffee index 9e5f49b1f69..9c723f51e54 100644 --- a/app/assets/javascripts/commit/image-file.js.coffee +++ b/app/assets/javascripts/commit/image-file.js.coffee @@ -119,8 +119,9 @@ class @ImageFile requestImageInfo: (img, callback) -> domImg = img.get(0) - if domImg.complete - callback.call(this, domImg.naturalWidth, domImg.naturalHeight) - else - img.on 'load', => + if domImg + if domImg.complete callback.call(this, domImg.naturalWidth, domImg.naturalHeight) + else + img.on 'load', => + callback.call(this, domImg.naturalWidth, domImg.naturalHeight) diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee index 81e73799271..539041c2862 100644 --- a/app/assets/javascripts/dispatcher.js.coffee +++ b/app/assets/javascripts/dispatcher.js.coffee @@ -51,6 +51,7 @@ class Dispatcher MergeRequests.init() when 'dashboard:show', 'root:show' new Dashboard() + when 'dashboard:activity' new Activities() when 'dashboard:projects:starred' new Activities() diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee index 87c1b67a772..39a433dfc91 100644 --- a/app/assets/javascripts/project.js.coffee +++ b/app/assets/javascripts/project.js.coffee @@ -24,8 +24,3 @@ class @Project $.cookie('hide_no_password_message', 'false', { path: path }) $(@).parents('.no-password-message').remove() e.preventDefault() - - $('.js-toggle-clone-holder').on 'click', (e) -> - cloneHolder.toggle() - - cloneHolder.hide() unless $('.empty-project').length diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee index c0e36d1ccc5..db5faf71faf 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/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee index aeeed9ca3cc..9157562a5c5 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/variables.scss b/app/assets/stylesheets/base/variables.scss index 08f153dfbc9..cb439a0e0bf 100644 --- a/app/assets/stylesheets/base/variables.scss +++ b/app/assets/stylesheets/base/variables.scss @@ -13,6 +13,7 @@ $code_line_height: 1.5; $border-color: #E5E5E5; $background-color: #f5f5f5; $header-height: 50px; +$readable-width: 1100px; /* diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index d36530169a9..bf5c7a8d75e 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -373,3 +373,23 @@ table { border-color: #EEE !important; } } + +.center-top-menu { + border-bottom: 1px solid #EEE; + list-style: none; + text-align: center; + padding-bottom: 15px; + margin-bottom: 15px; + + li { + display: inline-block; + + a { + padding: 10px; + } + + &.active a { + color: #666; + } + } +} diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/generic/files.scss index 8014dcb165b..f845342c67b 100644 --- a/app/assets/stylesheets/generic/files.scss +++ b/app/assets/stylesheets/generic/files.scss @@ -90,12 +90,7 @@ border-right: none; } background: #fff; - padding: 5px; - } - .author, - .blame_commit { - background: $background-color; - vertical-align: top; + padding: 8px; } .lines { pre { diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss index 9a3b543ad10..c1103a1c2e6 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/explore.scss b/app/assets/stylesheets/pages/explore.scss index 9b92128624c..da06fe9954e 100644 --- a/app/assets/stylesheets/pages/explore.scss +++ b/app/assets/stylesheets/pages/explore.scss @@ -6,3 +6,11 @@ font-size: 30px; } } + +.explore-trending-block { + .lead { + line-height: 32px; + font-size: 18px; + margin-top: 10px; + } +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 586e7b5f8da..3f617e72b02 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -45,3 +45,9 @@ .btn { font-size: 13px; } } + +.issuable-details { + .description { + max-width: $readable-width; + } +} diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 30bbec7b795..10fce5b3daa 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -186,3 +186,7 @@ #modal_merge_info .modal-dialog { width: 600px; } + +.mr-source-target { + line-height: 31px; +} diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 29d3dbc25eb..4d065c3bdf6 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; } } @@ -316,3 +323,34 @@ 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; + } +} + +.project-row { + .project-full-name { + font-weight: bold; + font-size: 15px; + } + + .project-description { + color: #888; + font-size: 13px; + + p { + margin-bottom: 0; + color: #888; + } + } +} + +.my-projects .project-row { + padding: 10px 0; +} diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss index 34ee4d7b31e..5f1a3db4fb6 100644 --- a/app/assets/stylesheets/pages/tree.scss +++ b/app/assets/stylesheets/pages/tree.scss @@ -89,6 +89,10 @@ td.blame-commit { background: #f9f9f9; min-width: 350px; + + .commit-author-link { + color: #888; + } } td.blame-numbers { pre { @@ -112,6 +116,9 @@ } .readme-holder { + margin: 0 auto; + max-width: $readable-width; + .readme-file-title { font-size: 14px; font-weight: bold; diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb index 34f37bca4ad..38a5a9fca08 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/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index c7c643db401..f38e07af84b 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -29,6 +29,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end end + import_sources = params[:application_setting][:import_sources] + if import_sources.nil? + params[:application_setting][:import_sources] = [] + else + import_sources.map! do |source| + source.to_str + end + end + params.require(:application_setting).permit( :default_projects_limit, :default_branch_protection, @@ -47,6 +56,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController :version_check_enabled, :user_oauth_applications, restricted_visibility_levels: [], + import_sources: [] ) end end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index da5f5bb83fa..ae1de06b983 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -9,6 +9,7 @@ class Admin::ProjectsController < Admin::ApplicationController @projects = @projects.where("visibility_level IN (?)", params[:visibility_levels]) if params[:visibility_levels].present? @projects = @projects.with_push if params[:with_push].present? @projects = @projects.abandoned if params[:abandoned].present? + @projects = @projects.non_archived unless params[:with_archived].present? @projects = @projects.search(params[:name]) if params[:name].present? @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(PER_PAGE) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 3ce8dbc9407..12d439b0b31 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -20,7 +20,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception helper_method :abilities, :can?, :current_application_settings - helper_method :github_import_enabled?, :gitlab_import_enabled?, :bitbucket_import_enabled? + helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :git_import_enabled? rescue_from Encoding::CompatibilityError do |exception| log_exception(exception) @@ -298,15 +298,43 @@ class ApplicationController < ActionController::Base @issuable_finder.execute end + def import_sources_enabled? + !current_application_settings.import_sources.empty? + end + def github_import_enabled? + current_application_settings.import_sources.include?('github') + end + + def github_import_configured? Gitlab::OAuth::Provider.enabled?(:github) end def gitlab_import_enabled? + request.host != 'gitlab.com' && current_application_settings.import_sources.include?('gitlab') + end + + def gitlab_import_configured? Gitlab::OAuth::Provider.enabled?(:gitlab) end def bitbucket_import_enabled? + current_application_settings.import_sources.include?('bitbucket') + end + + def bitbucket_import_configured? Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? end + + def gitorious_import_enabled? + current_application_settings.import_sources.include?('gitorious') + end + + def google_code_import_enabled? + current_application_settings.import_sources.include?('google_code') + end + + def git_import_enabled? + current_application_settings.import_sources.include?('git') + end end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 5c3ca8e23c9..904d26a39f4 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 d2f0c43929f..d745131694b 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/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index e9bcb44f6b3..6c733c1ae4d 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -7,6 +7,7 @@ class Explore::ProjectsController < Explore::ApplicationController @tags = @projects.tags_on(:tags) @projects = @projects.tagged_with(params[:tag]) if params[:tag].present? @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present? + @projects = @projects.non_archived @projects = @projects.search(params[:search]) if params[:search].present? @projects = @projects.sort(@sort = params[:sort]) @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE) @@ -14,6 +15,7 @@ class Explore::ProjectsController < Explore::ApplicationController def trending @trending_projects = TrendingProjectsFinder.new.execute(current_user) + @trending_projects = @trending_projects.non_archived @trending_projects = @trending_projects.page(params[:page]).per(PER_PAGE) end diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index 8a45dc8860d..71831c5380d 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -10,7 +10,8 @@ class HelpController < ApplicationController respond_to do |format| format.any(:markdown, :md, :html) do - path = Rails.root.join('doc', @category, "#{@file}.md") + # Note: We are purposefully NOT using `Rails.root.join` + path = File.join(Rails.root, 'doc', @category, "#{@file}.md") if File.exist?(path) @markdown = File.read(path) @@ -24,7 +25,8 @@ class HelpController < ApplicationController # Allow access to images in the doc folder format.any(:png, :gif, :jpeg) do - path = Rails.root.join('doc', @category, "#{@file}.#{params[:format]}") + # Note: We are purposefully NOT using `Rails.root.join` + path = File.join(Rails.root, 'doc', @category, "#{@file}.#{params[:format]}") if File.exist?(path) send_file(path, disposition: 'inline') diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index 4e6c0b66634..f84f85a7df8 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 b9f99c1b88a..f21fbd9ecca 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 1b8962d8924..27af19f5f61 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/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb index c121d2de7cb..f24cdb3709a 100644 --- a/app/controllers/import/gitorious_controller.rb +++ b/app/controllers/import/gitorious_controller.rb @@ -1,4 +1,5 @@ class Import::GitoriousController < Import::BaseController + before_action :verify_gitorious_import_enabled def new redirect_to client.authorize_url(callback_import_gitorious_url) @@ -40,4 +41,8 @@ class Import::GitoriousController < Import::BaseController @client ||= Gitlab::GitoriousImport::Client.new(session[:gitorious_repos]) end + def verify_gitorious_import_enabled + not_found! unless gitorious_import_enabled? + end + end diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb index 4aa6d28c9a8..82fadeb7e83 100644 --- a/app/controllers/import/google_code_controller.rb +++ b/app/controllers/import/google_code_controller.rb @@ -1,4 +1,5 @@ class Import::GoogleCodeController < Import::BaseController + before_action :verify_google_code_import_enabled before_action :user_map, only: [:new_user_map, :create_user_map] def new @@ -104,6 +105,10 @@ class Import::GoogleCodeController < Import::BaseController @client ||= Gitlab::GoogleCodeImport::Client.new(session[:google_code_dump]) end + def verify_google_code_import_enabled + not_found! unless google_code_import_enabled? + end + def user_map @user_map ||= begin user_map = client.user_map diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index 3362264dcce..9ea518e6c85 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -7,7 +7,29 @@ class Projects::BlameController < Projects::ApplicationController before_action :authorize_download_code! def show - @blame = Gitlab::Git::Blame.new(@repository, @commit.id, @path) - @blob = @blame.blob + @blob = @repository.blob_at(@commit.id, @path) + @blame = group_blame_lines + end + + def group_blame_lines + blame = Gitlab::Git::Blame.new(@repository, @commit.id, @path) + + prev_sha = nil + groups = [] + current_group = nil + + blame.each do |commit, line| + if prev_sha && prev_sha == commit.sha + current_group[:lines] << line + else + groups << current_group if current_group.present? + current_group = { commit: commit, lines: [line] } + end + + prev_sha = commit.sha + end + + groups << current_group if current_group.present? + groups end end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index bfafdeeb1fb..0f89f2e88cc 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -131,7 +131,7 @@ class Projects::IssuesController < Projects::ApplicationController end def module_enabled - return render_404 unless @project.issues_enabled + return render_404 unless @project.issues_enabled && @project.default_issues_tracker? end # Since iids are implemented only in 6.1 diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 1e435be8275..01105532479 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -39,10 +39,13 @@ class Projects::ServicesController < Projects::ApplicationController def test data = Gitlab::PushDataBuilder.build_sample(project, current_user) - if @service.execute(data) + outcome = @service.test(data) + if outcome[:success] message = { notice: 'We sent a request to the provided URL' } else - message = { alert: 'We tried to send a request to the provided URL but an error occured' } + error_message = "We tried to send a request to the provided URL but an error occurred" + error_message << ": #{outcome[:result]}" if outcome[:result].present? + message = { alert: error_message } end redirect_to :back, message diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 4e2ea6c5710..eb0408a95e5 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/sessions_controller.rb b/app/controllers/sessions_controller.rb index 796cbe4c58c..8389f07a3bd 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -2,27 +2,10 @@ class SessionsController < Devise::SessionsController include AuthenticatesWithTwoFactor prepend_before_action :authenticate_with_two_factor, only: [:create] + prepend_before_action :store_redirect_path, only: [:new] before_action :auto_sign_in_with_provider, only: [:new] def new - redirect_path = - if request.referer.present? && (params['redirect_to_referer'] == 'yes') - referer_uri = URI(request.referer) - if referer_uri.host == Gitlab.config.gitlab.host - referer_uri.path - else - request.fullpath - end - else - request.fullpath - end - - # Prevent a 'you are already signed in' message directly after signing: - # we should never redirect to '/users/sign_in' after signing in successfully. - unless redirect_path == new_user_session_path - store_location_for(:redirect, redirect_path) - end - if Gitlab.config.ldap.enabled @ldap_servers = Gitlab::LDAP::Config.servers end @@ -55,6 +38,26 @@ class SessionsController < Devise::SessionsController User.find(session[:otp_user_id]) end end + + def store_redirect_path + redirect_path = + if request.referer.present? && (params['redirect_to_referer'] == 'yes') + referer_uri = URI(request.referer) + if referer_uri.host == Gitlab.config.gitlab.host + referer_uri.path + else + request.fullpath + end + else + request.fullpath + end + + # Prevent a 'you are already signed in' message directly after signing: + # we should never redirect to '/users/sign_in' after signing in successfully. + unless redirect_path == new_user_session_path + store_location_for(:redirect, redirect_path) + end + end def authenticate_with_two_factor user = self.resource = find_user diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 61d14383945..7d6b58ee21a 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -39,4 +39,21 @@ module ApplicationSettingsHelper end end end + + # Return a group of checkboxes that use Bootstrap's button plugin for a + # toggle button effect. + def import_sources_checkboxes(help_block_id) + Gitlab::ImportSources.options.map do |name, source| + checked = current_application_settings.import_sources.include?(source) + css_class = 'btn' + css_class += ' active' if checked + checkbox_name = 'application_setting[import_sources][]' + + label_tag(checkbox_name, class: css_class) do + check_box_tag(checkbox_name, source, checked, + autocomplete: 'off', + 'aria-describedby' => help_block_id) + name + end + end + end end diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index 7616fe6bad8..0d291f9a87e 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -10,7 +10,7 @@ module ExploreHelper options = exist_opts.merge(options) - path = request.path + path = explore_projects_path path << "?#{options.to_param}" path end diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index 30b17a736a7..1cf5b96481a 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/projects_helper.rb b/app/helpers/projects_helper.rb index 525a46291f6..ab9b068de05 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -231,37 +231,20 @@ module ProjectsHelper end end + def readme_path(project) + filename_path(project, :readme) + end + def changelog_path(project) - if project && changelog = project.repository.changelog - namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, - changelog.name) - ) - end + filename_path(project, :changelog) end def license_path(project) - if project && license = project.repository.license - namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, - license.name) - ) - end + filename_path(project, :license) end def version_path(project) - if project && version = project.repository.version - namespace_project_blob_path( - project.namespace, - project, - tree_join(project.default_branch, - version.name) - ) - end + filename_path(project, :version) end def hidden_pass_url(original_url) @@ -331,4 +314,17 @@ module ProjectsHelper count end end + + private + + def filename_path(project, filename) + if project && blob = project.repository.send(filename) + namespace_project_blob_path( + project.namespace, + project, + tree_join(project.default_branch, + blob.name) + ) + end + end end diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 2b99a398049..12fce8db701 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/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index 77727337f07..0e7d8065ac7 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -67,6 +67,14 @@ module TabHelper path.any? do |single_path| current_path?(single_path) end + elsif page = options.delete(:page) + unless page.respond_to?(:each) + page = [page] + end + + page.any? do |single_page| + current_page?(single_page) + end else c = options.delete(:controller) a = options.delete(:action) diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb new file mode 100644 index 00000000000..aedb0889185 --- /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 00000000000..883f1c73ad4 --- /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 687bac3aa31..2c035fbb70b 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 512a8f7ea6b..7923fb770d0 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}")) - end - end + subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) - # 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 => <Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...> - # => target project => <Project id: 2, name: "My Ror", path: "ruby_on_rails", ...> - # => source branch => source - # => target branch => target - # >> subject('Lorem ipsum') - # => "GitLab Merge Request | Ruby on Rails:source >> My Ror:target | Lorem ipsum " - # - # Non Forked MR - # => source project => <Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...> - # => target project => <Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...> - # => source branch => source - # => target branch => target - # >> subject('Lorem ipsum') - # => "GitLab Merge Request | Ruby on Rails | source >> target | Lorem ipsum " - # # Accepts multiple arguments - # >> subject('Lorem ipsum', 'Dolor sit amet') - # => "GitLab Merge Request | Lorem ipsum | Dolor sit amet" - def subject(*extra) - subject = "Merge Request | " - if @merge_request.for_fork? - subject << "#{@merge_request.source_project.name_with_namespace}:#{merge_request.source_branch} >> #{@merge_request.target_project.name_with_namespace}:#{merge_request.target_branch}" - else - subject << "#{@merge_request.source_project.name_with_namespace} | #{merge_request.source_branch} >> #{merge_request.target_branch}" + SentNotification.record(@merge_request, recipient_id, reply_key) end - subject << " | " + extra.join(' | ') if extra.present? - subject end - end diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb index ff251209e01..63d4aca61af 100644 --- a/app/mailers/emails/notes.rb +++ b/app/mailers/emails/notes.rb @@ -11,6 +11,8 @@ module Emails from: sender(@note.author_id), to: recipient(recipient_id), subject: subject("#{@commit.title} (#{@commit.short_id})")) + + SentNotification.record(@commit, recipient_id, reply_key) end def note_issue_email(recipient_id, note_id) @@ -24,6 +26,8 @@ module Emails from: sender(@note.author_id), to: recipient(recipient_id), subject: subject("#{@issue.title} (##{@issue.iid})")) + + SentNotification.record(@issue, recipient_id, reply_key) end def note_merge_request_email(recipient_id, note_id) @@ -38,6 +42,8 @@ module Emails from: sender(@note.author_id), to: recipient(recipient_id), subject: subject("#{@merge_request.title} (##{@merge_request.iid})")) + + SentNotification.record(@merge_request, recipient_id, reply_key) end end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index 79fb48b00d3..5717c89e61d 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -1,4 +1,4 @@ -class Notify < ActionMailer::Base +class Notify < BaseMailer include ActionDispatch::Routing::PolymorphicRoutes include Emails::Issues @@ -8,22 +8,9 @@ class Notify < ActionMailer::Base include Emails::Profile include Emails::Groups - add_template_helper ApplicationHelper - add_template_helper GitlabMarkdownHelper add_template_helper MergeRequestsHelper add_template_helper EmailsHelper - attr_accessor :current_user - helper_method :current_user, :can? - - default from: Proc.new { default_sender_address.format } - default reply_to: Gitlab.config.gitlab.email_reply_to - - # Just send email with 2 seconds delay - def self.delay - delay_for(2.seconds) - end - def test_email(recipient_email, subject, body) mail(to: recipient_email, subject: subject, @@ -48,13 +35,6 @@ class Notify < ActionMailer::Base private - # The default email address to send emails from - 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 can_send_from_user_email?(sender) sender_domain = sender.email.split("@").last self.class.allowed_email_domains.include?(sender_domain) @@ -85,14 +65,6 @@ class Notify < ActionMailer::Base @current_user.notification_email end - # Set the References header field - # - # local_part - The local part of the referenced message ID - # - def set_reference(local_part) - headers["References"] = "<#{local_part}@#{Gitlab.config.gitlab.host}>" - end - # Formats arguments into a String suitable for use as an email subject # # extra - Extra Strings to be inserted into the subject @@ -126,14 +98,37 @@ class Notify < ActionMailer::Base "<#{model_name}_#{model.id}@#{Gitlab.config.gitlab.host}>" end + def mail_thread(model, headers = {}) + if @project + headers['X-GitLab-Project'] = @project.name + headers['X-GitLab-Project-Id'] = @project.id + headers['X-GitLab-Project-Path'] = @project.path_with_namespace + end + + headers["X-GitLab-#{model.class.name}-ID"] = model.id + + if reply_key + headers['X-GitLab-Reply-Key'] = reply_key + + address = Mail::Address.new(Gitlab::ReplyByEmail.reply_address(reply_key)) + address.display_name = @project.name_with_namespace + + headers['Reply-To'] = address + + @reply_by_email = true + end + + mail(headers) + end + # Send an email that starts a new conversation thread, # with headers suitable for grouping by thread in email clients. # # See: mail_answer_thread - def mail_new_thread(model, headers = {}, &block) + def mail_new_thread(model, headers = {}) headers['Message-ID'] = message_id(model) - headers['X-GitLab-Project'] = "#{@project.name} | " if @project - mail(headers, &block) + + mail_thread(model, headers) end # Send an email that responds to an existing conversation thread, @@ -144,19 +139,17 @@ class Notify < ActionMailer::Base # * have a subject that begin by 'Re: ' # * have a 'In-Reply-To' or 'References' header that references the original 'Message-ID' # - def mail_answer_thread(model, headers = {}, &block) + def mail_answer_thread(model, headers = {}) + headers['Message-ID'] = SecureRandom.hex headers['In-Reply-To'] = message_id(model) headers['References'] = message_id(model) - headers['X-GitLab-Project'] = "#{@project.name} | " if @project - if headers[:subject] - headers[:subject].prepend('Re: ') - end + headers[:subject].prepend('Re: ') if headers[:subject] - mail(headers, &block) + mail_thread(model, headers) end - def can? - Ability.abilities.allowed?(user, action, subject) + def reply_key + @reply_key ||= Gitlab::ReplyByEmail.reply_key end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 6d1ad82a262..8f27e35d723 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -22,10 +22,12 @@ # user_oauth_applications :boolean default(TRUE) # after_sign_out_path :string(255) # session_expire_delay :integer default(10080), not null +# import_sources :text # class ApplicationSetting < ActiveRecord::Base serialize :restricted_visibility_levels + serialize :import_sources serialize :restricted_signup_domains, Array attr_accessor :restricted_signup_domains_raw @@ -52,6 +54,16 @@ class ApplicationSetting < ActiveRecord::Base end end + validates_each :import_sources do |record, attr, value| + unless value.nil? + value.each do |source| + unless Gitlab::ImportSources.options.has_value?(source) + record.errors.add(attr, "'#{source}' is not a import source") + end + end + end + end + def self.current ApplicationSetting.last end @@ -70,7 +82,8 @@ class ApplicationSetting < ActiveRecord::Base session_expire_delay: Settings.gitlab['session_expire_delay'], default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'], default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'], - restricted_signup_domains: Settings.gitlab['restricted_signup_domains'] + restricted_signup_domains: Settings.gitlab['restricted_signup_domains'], + import_sources: ['github','bitbucket','gitlab','gitorious','google_code','git'] ) end diff --git a/app/models/group.rb b/app/models/group.rb index 4ff610f8e9d..9cd146bb73b 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -17,6 +17,7 @@ require 'carrierwave/orm/activerecord' require 'file_size_validator' class Group < Namespace + include Gitlab::ConfigHelper include Referable has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember' diff --git a/app/models/milestone.rb b/app/models/milestone.rb index d28f3c8d3f9..c6aff6f709f 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -47,6 +47,13 @@ class Milestone < ActiveRecord::Base state :active end + class << self + def search(query) + query = "%#{query}%" + where("title like ? or description like ?", query, query) + end + end + def expired? if due_date due_date.past? @@ -54,7 +61,7 @@ class Milestone < ActiveRecord::Base false end end - + def open_items_count self.issues.opened.count + self.merge_requests.opened.count end diff --git a/app/models/note.rb b/app/models/note.rb index a99d428b02d..36cad8f583d 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -90,7 +90,7 @@ class Note < ActiveRecord::Base end def search(query) - where("note like :query", query: "%#{query}%") + where("LOWER(note) like :query", query: "%#{query.downcase}%") end end @@ -360,6 +360,10 @@ class Note < ActiveRecord::Base create_new_cross_references!(project, author) end + def system? + read_attribute(:system) + end + def editable? !read_attribute(:system) end diff --git a/app/models/project.rb b/app/models/project.rb index 4628f478ca6..69f9af91c51 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -215,7 +215,7 @@ class Project < ActiveRecord::Base end def search(query) - joins(:namespace).where('projects.archived = ?', false). + joins(:namespace). where('LOWER(projects.name) LIKE :query OR LOWER(projects.path) LIKE :query OR LOWER(namespaces.name) LIKE :query OR diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb index bf801ba61ad..27fc19379f1 100644 --- a/app/models/project_services/flowdock_service.rb +++ b/app/models/project_services/flowdock_service.rb @@ -38,7 +38,7 @@ class FlowdockService < Service def fields [ - { type: 'text', name: 'token', placeholder: '' } + { type: 'text', name: 'token', placeholder: 'Flowdock Git source token' } ] end diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 6761f00183e..7a15a861abc 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -60,6 +60,16 @@ class HipchatService < Service gate[room].send('GitLab', message, message_options) end + def test(data) + begin + result = execute(data) + rescue StandardError => error + return { success: false, result: error } + end + + { success: true, result: result } + end + private def gate diff --git a/app/models/repository.rb b/app/models/repository.rb index 46efbede2a2..79b48ebfedf 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -1,4 +1,9 @@ +require 'securerandom' + class Repository + class PreReceiveError < StandardError; end + class CommitError < StandardError; end + include Gitlab::ShellAdapter attr_accessor :raw_repository, :path_with_namespace, :project @@ -148,6 +153,10 @@ class Repository @lookup_cache ||= {} end + def expire_branch_names + cache.expire(:branch_names) + end + def method_missing(m, *args, &block) if m == :lookup && !block_given? lookup_cache[m] ||= {} @@ -364,43 +373,47 @@ class Repository @root_ref ||= raw_repository.root_ref end - def commit_file(user, path, content, message, ref) - path[0] = '' if path[0] == '/' + def commit_file(user, path, content, message, branch) + commit_with_hooks(user, branch) do |ref| + path[0] = '' if path[0] == '/' - committer = user_to_comitter(user) - options = {} - options[:committer] = committer - options[:author] = committer - options[:commit] = { - message: message, - branch: ref - } + committer = user_to_comitter(user) + options = {} + options[:committer] = committer + options[:author] = committer + options[:commit] = { + message: message, + branch: ref, + } - options[:file] = { - content: content, - path: path - } + options[:file] = { + content: content, + path: path + } - Gitlab::Git::Blob.commit(raw_repository, options) + Gitlab::Git::Blob.commit(raw_repository, options) + end end - def remove_file(user, path, message, ref) - path[0] = '' if path[0] == '/' + def remove_file(user, path, message, branch) + commit_with_hooks(user, branch) do |ref| + path[0] = '' if path[0] == '/' - committer = user_to_comitter(user) - options = {} - options[:committer] = committer - options[:author] = committer - options[:commit] = { - message: message, - branch: ref - } + committer = user_to_comitter(user) + options = {} + options[:committer] = committer + options[:author] = committer + options[:commit] = { + message: message, + branch: ref + } - options[:file] = { - path: path - } + options[:file] = { + path: path + } - Gitlab::Git::Blob.remove(raw_repository, options) + Gitlab::Git::Blob.remove(raw_repository, options) + end end def user_to_comitter(user) @@ -422,7 +435,7 @@ class Repository end end - def merge(source_sha, target_branch, options = {}) + def merge(user, source_sha, target_branch, options = {}) our_commit = rugged.branches[target_branch].target their_commit = rugged.lookup(source_sha) @@ -432,13 +445,26 @@ class Repository merge_index = rugged.merge_commits(our_commit, their_commit) return false if merge_index.conflicts? - actual_options = options.merge( - parents: [our_commit, their_commit], - tree: merge_index.write_tree(rugged), - update_ref: "refs/heads/#{target_branch}" - ) + commit_with_hooks(user, target_branch) do |ref| + actual_options = options.merge( + parents: [our_commit, their_commit], + tree: merge_index.write_tree(rugged), + update_ref: ref + ) - Rugged::Commit.create(rugged, actual_options) + Rugged::Commit.create(rugged, actual_options) + end + end + + def merged_to_root_ref?(branch_name) + branch_commit = commit(branch_name) + root_ref_commit = commit(root_ref) + + if branch_commit + rugged.merge_base(root_ref_commit.id, branch_commit.id) == branch_commit.id + else + nil + end end def search_files(query, ref) @@ -479,6 +505,59 @@ class Repository Gitlab::Popen.popen(args, path_to_repo) end + def commit_with_hooks(current_user, branch) + oldrev = Gitlab::Git::BLANK_SHA + ref = Gitlab::Git::BRANCH_REF_PREFIX + branch + gl_id = Gitlab::ShellEnv.gl_id(current_user) + was_empty = empty? + + # Create temporary ref + random_string = SecureRandom.hex + tmp_ref = "refs/tmp/#{random_string}/head" + + unless was_empty + oldrev = find_branch(branch).target + rugged.references.create(tmp_ref, oldrev) + end + + # Make commit in tmp ref + newrev = yield(tmp_ref) + + unless newrev + raise CommitError.new('Failed to create commit') + end + + # Run GitLab pre-receive hook + pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', path_to_repo) + status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref) + + if status + if was_empty + # Create branch + rugged.references.create(ref, newrev) + else + # Update head + current_head = find_branch(branch).target + + # Make sure target branch was not changed during pre-receive hook + if current_head == oldrev + rugged.references.update(ref, newrev) + else + raise CommitError.new('Commit was rejected because branch received new push') + end + end + + # Run GitLab post receive hook + post_receive_hook = Gitlab::Git::Hook.new('post-receive', path_to_repo) + status = post_receive_hook.trigger(gl_id, oldrev, newrev, ref) + else + # Remove tmp ref and return error to user + rugged.references.delete(tmp_ref) + + raise PreReceiveError.new('Commit was rejected by pre-receive hook') + end + end + private def cache diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb new file mode 100644 index 00000000000..460ca40be3f --- /dev/null +++ b/app/models/sent_notification.rb @@ -0,0 +1,50 @@ +class SentNotification < ActiveRecord::Base + belongs_to :project + belongs_to :noteable, polymorphic: true + belongs_to :recipient, class_name: "User" + + validate :project, :recipient, :reply_key, presence: true + validate :reply_key, uniqueness: true + + validates :noteable_id, presence: true, unless: :for_commit? + validates :commit_id, presence: true, if: :for_commit? + + class << self + def for(reply_key) + find_by(reply_key: reply_key) + end + + def record(noteable, recipient_id, reply_key) + return unless reply_key + + noteable_id = nil + commit_id = nil + if noteable.is_a?(Commit) + commit_id = noteable.id + else + noteable_id = noteable.id + end + + create( + project: noteable.project, + noteable_type: noteable.class.name, + noteable_id: noteable_id, + commit_id: commit_id, + recipient_id: recipient_id, + reply_key: reply_key + ) + end + end + + def for_commit? + noteable_type == "Commit" + end + + def noteable + if for_commit? + project.commit(commit_id) rescue nil + else + super + end + end +end diff --git a/app/models/service.rb b/app/models/service.rb index 818a6808db5..dcef2866c3b 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -87,10 +87,16 @@ class Service < ActiveRecord::Base %w(push tag_push issue merge_request) end - def execute + def execute(data) # implement inside child end + def test(data) + # default implementation + result = execute(data) + { success: result.present?, result: result } + end + def can_test? !project.empty_repo? end diff --git a/app/models/user.rb b/app/models/user.rb index 57145cc6b6e..f70761074c5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -471,8 +471,19 @@ class User < ActiveRecord::Base events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours) events = events.where(project_id: project_id) if project_id - # Take only latest one - events = events.recent.limit(1).first + # Use the latest event that has not been pushed or merged recently + events.recent.find do |event| + project = Project.find_by_id(event.project_id) + next unless project + repo = project.repository + + if repo.branch_names.include?(event.branch_name) + merge_requests = MergeRequest.where("created_at >= ?", event.created_at). + where(source_project_id: project.id, + source_branch: event.branch_name) + merge_requests.empty? + end + end end def projects_sorted_by_activity diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index d7b40ee8906..7aecee217d8 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -22,21 +22,16 @@ module Files end if sha = commit - after_commit(sha, @target_branch) success else error("Something went wrong. Your changes were not committed") end - rescue ValidationError => ex + rescue Repository::CommitError, Repository::PreReceiveError, ValidationError => ex error(ex.message) end private - def after_commit(sha, branch) - PostCommitService.new(project, current_user).execute(sha, branch) - end - def current_branch @current_branch ||= params[:current_branch] end diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 81535450ac1..0a73244774a 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -78,24 +78,29 @@ class GitPushService # For push with 1k commits it prevents 900+ requests in database author = nil + # Keep track of the issues that will be actually closed because they are on a default branch. + # Hence, when creating cross-reference notes, the not-closed issues (on non-default branches) + # will also have cross-reference. + actually_closed_issues = [] + if issues_to_close.present? && is_default_branch author ||= commit_user(commit) - + actually_closed_issues = issues_to_close issues_to_close.each do |issue| Issues::CloseService.new(project, author, {}).execute(issue, commit) end end if project.default_issues_tracker? - create_cross_reference_notes(commit, issues_to_close) + create_cross_reference_notes(commit, actually_closed_issues) end end end def create_cross_reference_notes(commit, issues_to_close) - # Create cross-reference notes for any other references. Omit any issues that were referenced in an - # issue-closing phrase, or have already been mentioned from this commit (probably from this commit - # being pushed to a different branch). + # Create cross-reference notes for any other references than those given in issues_to_close. + # Omit any issues that were referenced in an issue-closing phrase, or have already been + # mentioned from this commit (probably from this commit being pushed to a different branch). refs = commit.references(project, user) - issues_to_close refs.reject! { |r| commit.has_mentioned?(r) } diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index f431c5d5534..9651b16462c 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -1,11 +1,17 @@ module MergeRequests class CreateService < MergeRequests::BaseService def execute + # @project is used to determine whether the user can set the merge request's + # assignee, milestone and labels. Whether they can depends on their + # permissions on the target project. + source_project = @project + @project = Project.find(params[:target_project_id]) if params[:target_project_id] + filter_params label_params = params[:label_ids] merge_request = MergeRequest.new(params.except(:label_ids)) - merge_request.source_project = project - merge_request.target_project ||= project + merge_request.source_project = source_project + merge_request.target_project ||= source_project merge_request.author = current_user if merge_request.save diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb index 2107529a21a..98a67c0bc99 100644 --- a/app/services/merge_requests/merge_service.rb +++ b/app/services/merge_requests/merge_service.rb @@ -17,7 +17,7 @@ module MergeRequests end merge_request.in_locked_state do - if merge_changes + if commit after_merge success else @@ -28,12 +28,6 @@ module MergeRequests private - def merge_changes - if sha = commit - after_commit(sha, merge_request.target_branch) - end - end - def commit committer = repository.user_to_comitter(current_user) @@ -43,11 +37,7 @@ module MergeRequests committer: committer } - repository.merge(merge_request.source_sha, merge_request.target_branch, options) - end - - def after_commit(sha, branch) - PostCommitService.new(project, current_user).execute(sha, branch) + repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options) end def after_merge diff --git a/app/services/post_commit_service.rb b/app/services/post_commit_service.rb deleted file mode 100644 index 8592c8d238b..00000000000 --- a/app/services/post_commit_service.rb +++ /dev/null @@ -1,71 +0,0 @@ -class PostCommitService < BaseService - include Gitlab::Popen - - attr_reader :changes, :repo_path - - def execute(sha, branch) - commit = repository.commit(sha) - full_ref = Gitlab::Git::BRANCH_REF_PREFIX + branch - old_sha = commit.parent_id || Gitlab::Git::BLANK_SHA - @changes = "#{old_sha} #{sha} #{full_ref}" - @repo_path = repository.path_to_repo - - post_receive - end - - private - - def post_receive - hook = hook_file('post-receive', repo_path) - return true if hook.nil? - call_receive_hook(hook) - end - - def call_receive_hook(hook) - # function will return true if succesful - exit_status = false - - vars = { - 'GL_ID' => Gitlab::ShellEnv.gl_id(current_user), - 'PWD' => repo_path - } - - options = { - chdir: repo_path - } - - # we combine both stdout and stderr as we don't know what stream - # will be used by the custom hook - Open3.popen2e(vars, hook, options) do |stdin, stdout_stderr, wait_thr| - exit_status = true - stdin.sync = true - - # in git, pre- and post- receive hooks may just exit without - # reading stdin. We catch the exception to avoid a broken pipe - # warning - begin - # inject all the changes as stdin to the hook - changes.lines do |line| - stdin.puts line - end - rescue Errno::EPIPE - end - - # need to close stdin before reading stdout - stdin.close - - # only output stdut_stderr if scripts doesn't return 0 - unless wait_thr.value == 0 - exit_status = false - end - end - - exit_status - end - - def hook_file(hook_type, repo_path) - hook_path = File.join(repo_path.strip, 'hooks') - hook_file = "#{hook_path}/#{hook_type}" - hook_file if File.exist?(hook_file) - end -end diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb index 992a7a7a1dc..279550d6f4a 100644 --- a/app/services/projects/upload_service.rb +++ b/app/services/projects/upload_service.rb @@ -13,9 +13,9 @@ module Projects filename = uploader.image? ? uploader.file.basename : uploader.file.filename { - 'alt' => filename, - 'url' => uploader.secure_url, - 'is_image' => uploader.image? + alt: filename, + url: uploader.secure_url, + is_image: uploader.image? } end diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml index 4449721ae38..d3afc658cd6 100644 --- a/app/views/admin/abuse_reports/_abuse_report.html.haml +++ b/app/views/admin/abuse_reports/_abuse_report.html.haml @@ -3,7 +3,7 @@ %tr %td - if reporter - = link_to reporter.name, [:admin, reporter] + = link_to reporter.name, reporter - else (removed) %td @@ -12,12 +12,15 @@ = abuse_report.message %td - if user - = link_to user.name, [:admin, user] + = link_to user.name, user - else (removed) %td - if user - = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs btn-warning" - = link_to 'Remove user', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove" + = link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true), + data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-xs btn-remove js-remove-tr" + %td - = link_to 'Remove report', [:admin, abuse_report], method: :delete, class: "btn btn-xs btn-close" + - if user + = link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs" + = link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr" diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml index 4a25848f156..2e8746146d1 100644 --- a/app/views/admin/abuse_reports/index.html.haml +++ b/app/views/admin/abuse_reports/index.html.haml @@ -9,7 +9,7 @@ %th Reported at %th Message %th User - %th + %th Primary action %th = render @abuse_reports = paginate @abuse_reports diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml index 6bef33c6d7a..330b8bbf9d2 100644 --- a/app/views/admin/application_settings/_form.html.haml +++ b/app/views/admin/application_settings/_form.html.haml @@ -28,6 +28,20 @@ = level %span.help-block#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets .form-group + = f.label :import_sources, class: 'control-label col-sm-2' + .col-sm-10 + - data_attrs = { toggle: 'buttons' } + .btn-group{ data: data_attrs } + - import_sources_checkboxes('import-sources-help').each do |source| + = source + %span.help-block#import-sources-help + Enabled sources for code import during project creation. OmniAuth must be configured for GitHub + = link_to "(?)", help_page_path("integration", "github") + , Bitbucket + = link_to "(?)", help_page_path("integration", "bitbucket") + and GitLab.com + = link_to "(?)", help_page_path("integration", "gitlab") + .form-group .col-sm-offset-2.col-sm-10 .checkbox = f.label :version_check_enabled do @@ -90,7 +104,7 @@ = f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control' .help-block Only users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com .form-group - = f.label :home_page_url, class: 'control-label col-sm-2' + = f.label :home_page_url, 'Home page URL', class: 'control-label col-sm-2' .col-sm-10 = f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block' %span.help-block#home_help_block We will redirect non-logged in users to this page diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 3732ff847b9..54191aadda6 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -55,6 +55,10 @@ OmniAuth %span.light.pull-right = boolean_to_icon Gitlab.config.omniauth.enabled + %p + Reply by email + %span.light.pull-right + = boolean_to_icon Gitlab::ReplyByEmail.enabled? .col-md-4 %h4 Components diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index f43d46356fa..d9b481404f7 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -23,6 +23,10 @@ = label_tag :abandoned do = check_box_tag :abandoned, 1, params[:abandoned] %span No activity over 6 month + .checkbox + = label_tag :with_archived do + = check_box_tag :with_archived, 1, params[:with_archived] + %span Show archived projects %fieldset %strong Visibility level: @@ -73,6 +77,8 @@ = visibility_level_icon(project.visibility_level) = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project] .pull-right + - if project.archived + %span.label.label-warning archived %span.label.label-gray = repository_size(project) = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index b0d31170704..5e40d95d1c5 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -33,6 +33,7 @@ = form_tag admin_users_path, method: :get, class: 'form-inline' do .form-group = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control' + = hidden_field_tag "filter", params[:filter] = button_tag class: 'btn btn-primary' do %i.fa.fa-search %hr diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml new file mode 100644 index 00000000000..8a397a84e0e --- /dev/null +++ b/app/views/dashboard/_groups_head.html.haml @@ -0,0 +1,7 @@ +%ul.center-top-menu + = nav_link(page: [dashboard_groups_path]) do + = link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do + Your Groups + = nav_link(page: [explore_groups_path]) do + = link_to explore_groups_path, title: 'Explore groups', data: {toggle: 'tooltip', placement: 'bottom'} do + Explore Groups diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/_projects.html.haml index d676576067c..dc83d5343f2 100644 --- a/app/views/dashboard/_projects.html.haml +++ b/app/views/dashboard/_projects.html.haml @@ -1,5 +1,5 @@ -.panel.panel-default - .panel-heading.clearfix +.projects-list-holder + .projects-search-form .input-group = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' - if current_user.can_create_project? @@ -7,4 +7,7 @@ = link_to new_project_path, class: 'btn btn-success' do New project - = render 'shared/projects_list', projects: @projects, projects_limit: 20 + %ul.projects-list.bordered-list.my-projects + - @projects.each do |project| + %li.project-row + = render partial: 'shared/project', locals: { project: project, avatar: true, stars: true } diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml new file mode 100644 index 00000000000..f7be194c696 --- /dev/null +++ b/app/views/dashboard/_projects_head.html.haml @@ -0,0 +1,10 @@ +%ul.center-top-menu + = nav_link(path: ['dashboard#show', 'root#show']) do + = link_to dashboard_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do + Your Projects + = nav_link(page: starred_dashboard_projects_path) do + = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do + Starred Projects + = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do + = link_to explore_root_path, title: 'Explore', data: {toggle: 'tooltip', placement: 'bottom'} do + Explore Projects diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml new file mode 100644 index 00000000000..7a5a093add5 --- /dev/null +++ b/app/views/dashboard/activity.html.haml @@ -0,0 +1,6 @@ += content_for :meta_tags do + - if current_user + = auto_discovery_link_tag(:atom, dashboard_url(format: :atom, private_token: current_user.private_token), title: "All activity") + +%section.activities + = render 'activities' diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 0a354373b9b..0860fe3c761 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -1,14 +1,13 @@ - page_title "Groups" -%h3.page-title - Group Membership += render 'dashboard/groups_head' + +.slead + Group members have access to all group projects. - if current_user.can_create_group? %span.pull-right.hidden-xs - = link_to new_group_path, class: "btn btn-new" do + = link_to new_group_path, class: "btn btn-new btn-sm" do %i.fa.fa-plus New Group -%p.light - Group members have access to all group projects. -%hr .panel.panel-default .panel-heading %strong Groups diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index 8aaa0a7f071..6dcfd497ed2 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -1,12 +1,14 @@ - page_title "Starred Projects" += render 'dashboard/projects_head' + - if @projects.any? = render 'shared/show_aside' .dashboard.row - %section.activities.col-md-8 + %section.activities.col-md-7 = render 'dashboard/activities' - %aside.col-md-4 - .panel.panel-default + %aside.col-md-5 + .panel.panel-default.projects-list-holder .panel-heading.clearfix .input-group = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/show.html.haml index 5001c2101e1..4cf2feb9aa6 100644 --- a/app/views/dashboard/show.html.haml +++ b/app/views/dashboard/show.html.haml @@ -2,14 +2,12 @@ - if current_user = auto_discovery_link_tag(:atom, dashboard_url(format: :atom, private_token: current_user.private_token), title: "All activity") -- if @projects.any? - = render 'shared/show_aside' += render 'dashboard/projects_head' - .dashboard.row - %section.activities.col-md-8 - = render 'activities' - %aside.col-md-4 - = render 'sidebar' +- if @last_push + = render "events/event_last_push", event: @last_push +- if @projects.any? + = render 'projects' - else = render "zero_authorized_projects" diff --git a/app/views/email_rejection_mailer/rejection.html.haml b/app/views/email_rejection_mailer/rejection.html.haml new file mode 100644 index 00000000000..7f7d841fe21 --- /dev/null +++ b/app/views/email_rejection_mailer/rejection.html.haml @@ -0,0 +1,4 @@ +%p + Unfortunately, your email message to GitLab could not be processed. + += markdown @reason diff --git a/app/views/email_rejection_mailer/rejection.text.haml b/app/views/email_rejection_mailer/rejection.text.haml new file mode 100644 index 00000000000..6693e6f90e8 --- /dev/null +++ b/app/views/email_rejection_mailer/rejection.text.haml @@ -0,0 +1,4 @@ +Unfortunately, your email message to GitLab could not be processed. + + += @reason diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml index 501412642db..6a0c6cba41b 100644 --- a/app/views/events/_event_last_push.html.haml +++ b/app/views/events/_event_last_push.html.haml @@ -9,6 +9,6 @@ #{time_ago_with_tooltip(event.created_at)} .pull-right - = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-create btn-sm" do + = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do Create Merge Request %hr diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index f3f0b778539..7dcefd330a1 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -1,5 +1,7 @@ - page_title "Groups" -.clearfix +- if current_user + = render 'dashboard/groups_head' +.clearfix.append-bottom-10 .pull-left = form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f| = hidden_field_tag :sort, @sort @@ -28,15 +30,12 @@ = link_to explore_groups_path(sort: sort_value_oldest_updated) do = sort_title_oldest_updated -%hr - %ul.bordered-list - @groups.each do |group| %li .clearfix %h4 = link_to group_path(id: group.path) do - %i.fa.fa-users = group.name .clearfix %p diff --git a/app/views/explore/projects/_dropdown.html.haml b/app/views/explore/projects/_dropdown.html.haml new file mode 100644 index 00000000000..b23a3c1e5c1 --- /dev/null +++ b/app/views/explore/projects/_dropdown.html.haml @@ -0,0 +1,27 @@ +.dropdown.inline + %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} + %span.light sort: + - if @sort.present? + = sort_options_hash[@sort] + - elsif current_page?(trending_explore_projects_path) || current_page?(explore_root_path) + Trending projects + - elsif current_page?(starred_explore_projects_path) + Most stars + - else + = sort_title_recently_created + %b.caret + %ul.dropdown-menu + %li + = link_to trending_explore_projects_path do + Trending projects + = link_to starred_explore_projects_path do + Most stars + = link_to explore_projects_filter_path(sort: sort_value_recently_created) do + = sort_title_recently_created + = link_to explore_projects_filter_path(sort: sort_value_oldest_created) do + = sort_title_oldest_created + = link_to explore_projects_filter_path(sort: sort_value_recently_updated) do + = sort_title_recently_updated + = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do + = sort_title_oldest_updated + diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml index 82622a58ed2..4b91291caf4 100644 --- a/app/views/explore/projects/_filter.html.haml +++ b/app/views/explore/projects/_filter.html.haml @@ -46,22 +46,4 @@ = link_to explore_projects_filter_path(tag: tag.name) do %i.fa.fa-tag = tag.name - - .dropdown.inline - %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'} - %span.light sort: - - if @sort.present? - = sort_options_hash[@sort] - - else - = sort_title_recently_created - %b.caret - %ul.dropdown-menu - %li - = link_to explore_projects_filter_path(sort: sort_value_recently_created) do - = sort_title_recently_created - = link_to explore_projects_filter_path(sort: sort_value_oldest_created) do - = sort_title_oldest_created - = link_to explore_projects_filter_path(sort: sort_value_recently_updated) do - = sort_title_recently_updated - = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do - = sort_title_oldest_updated + = render 'explore/projects/dropdown' diff --git a/app/views/explore/projects/_project.html.haml b/app/views/explore/projects/_project.html.haml deleted file mode 100644 index 1e8a89e3661..00000000000 --- a/app/views/explore/projects/_project.html.haml +++ /dev/null @@ -1,24 +0,0 @@ -%li - %h4.project-title - .project-access-icon - = visibility_level_icon(project.visibility_level) - = link_to project.name_with_namespace, [project.namespace.becomes(Namespace), project] - %span.pull-right - %i.fa.fa-star - = project.star_count - - .project-info - - if project.description.present? - .project-description.str-truncated - = markdown(project.description, pipeline: :description) - - .repo-info - - unless project.empty_repo? - = link_to pluralize(round_commit_count(project), 'commit'), namespace_project_commits_path(project.namespace, project, project.default_branch) - · - = link_to pluralize(project.repository.branch_names.count, 'branch'), namespace_project_branches_path(project.namespace, project) - · - = link_to pluralize(project.repository.tag_names.count, 'tag'), namespace_project_tags_path(project.namespace, project) - - else - %i.fa.fa-exclamation-triangle - Empty repository diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml new file mode 100644 index 00000000000..22cc541115c --- /dev/null +++ b/app/views/explore/projects/_projects.html.haml @@ -0,0 +1,6 @@ +%ul.projects-list.bordered-list.my-projects.public-projects + - projects.each do |project| + %li.project-row + = render partial: 'shared/project', locals: { project: project, avatar: true, stars: true } +- unless projects.present? + .nothing-here-block No such projects diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index ba2276f51ce..0cfdf5cfd15 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -1,12 +1,8 @@ - page_title "Projects" +- if current_user + = render 'dashboard/projects_head' .clearfix = render 'filter' - -%hr -.public-projects - %ul.bordered-list.top-list - = render @projects - - unless @projects.present? - .nothing-here-block No public projects - - = paginate @projects, theme: "gitlab" +%br += render 'projects', projects: @projects += paginate @projects, theme: "gitlab" diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index b5d146b1f2f..4a9fcae4bed 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -1,11 +1,11 @@ - page_title "Starred Projects" +- if current_user + = render 'dashboard/projects_head' .explore-trending-block - %p.lead + .lead %i.fa.fa-star See most starred projects - %hr - .public-projects - %ul.bordered-list - = render @starred_projects - + .pull-right + = render 'explore/projects/dropdown' + = render 'projects', projects: @starred_projects = paginate @starred_projects, theme: 'gitlab' diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index 5e24df76a63..4c7e7d44733 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -1,4 +1,6 @@ - page_title "Trending Projects" +- if current_user + = render 'dashboard/projects_head' .explore-title %h3 Explore GitLab @@ -6,10 +8,9 @@ Discover projects and groups. Share your projects with others %hr .explore-trending-block - %p.lead + .lead %i.fa.fa-comments-o See most discussed projects for last month - %hr - .public-projects - %ul.bordered-list - = render @trending_projects + .pull-right + = render 'explore/projects/dropdown' + = render 'projects', projects: @trending_projects diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml index 4f8aec1c67e..2ae51a1c8c0 100644 --- a/app/views/groups/_projects.html.haml +++ b/app/views/groups/_projects.html.haml @@ -1,4 +1,4 @@ -.panel.panel-default +.panel.panel-default.projects-list-holder .panel-heading.clearfix .input-group = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control' diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index acc7f8b28c2..b5f359279d5 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -6,7 +6,8 @@ %span{class: ("list-item-name" if show_controls)} - if member.user = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: '' - %strong= user.name + %strong + = link_to user.name, user_path(user) %span.cgray= user.username - if user == current_user %span.label.label-success It's you diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml index 6b7efa83dea..d06cfa7ff9f 100644 --- a/app/views/groups/projects.html.haml +++ b/app/views/groups/projects.html.haml @@ -12,11 +12,14 @@ - @projects.each do |project| %li .list-item-name - = visibility_level_icon(project.visibility_level) + %span{ class: visibility_level_color(project.visibility_level) } + = visibility_level_icon(project.visibility_level) %strong= link_to project.name_with_namespace, project + .pull-right + - if project.archived + %span.label.label-warning archived %span.label.label-gray = repository_size(project) - .pull-right = link_to 'Members', namespace_project_project_members_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm" = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove" diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index d31dae7d648..0577f4ec142 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -17,7 +17,7 @@ = render 'shared/show_aside' .row - %section.activities.col-md-8 + %section.activities.col-md-7 .hidden-xs - if current_user = render "events/event_last_push", event: @last_push @@ -33,5 +33,5 @@ .content_list = spinner - %aside.side.col-md-4 + %aside.side.col-md-5 = render "projects", projects: @projects diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml index 90a6f5f9d2d..d8af0295b2d 100644 --- a/app/views/import/base/create.js.haml +++ b/app/views/import/base/create.js.haml @@ -14,12 +14,16 @@ :plain job = $("tr#repo_#{@repo_id}") job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>") -- else +- elsif @project.persisted? :plain job = $("tr#repo_#{@repo_id}") job.attr("id", "project_#{@project.id}") target_field = job.find(".import-target") target_field.empty() - target_field.append('<strong>#{link_to @project.path_with_namespace, [@project.namespace.becomes(Namespace), @project]}</strong>') + target_field.append('<strong>#{link_to @project.path_with_namespace, namespace_project_path(@project.namespace, @project)}</strong>') $("table.import-jobs tbody").prepend(job) job.addClass("active").find(".import-actions").html("<i class='fa fa-spinner fa-spin'></i> started") +- else + :plain + job = $("tr#repo_#{@repo_id}") + job.find(".import-actions").html("<i class='fa fa-exclamation-circle'> Error saving project: #{escape_javascript(@project.errors.full_messages.join(','))}</i>") diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml index 98ae509096e..777eb482714 100644 --- a/app/views/import/bitbucket/status.html.haml +++ b/app/views/import/bitbucket/status.html.haml @@ -61,7 +61,7 @@ rather than Git. Please convert = link_to "them to Git,", "https://www.atlassian.com/git/tutorials/migrating-overview" and go through the - = link_to "import flow", status_import_bitbucket_path + = link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true" again. diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml index 56bb92a536e..17fee9c510d 100644 --- a/app/views/layouts/explore.html.haml +++ b/app/views/layouts/explore.html.haml @@ -1,5 +1,8 @@ - page_title "Explore" -- header_title "Explore GitLab", explore_root_path -- sidebar "explore" +- if current_user + - header_title "Dashboard", root_path +- else + - header_title "Explore GitLab", explore_root_path +- sidebar "dashboard" = render template: "layouts/application" diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index b3cd7b0e37b..12ddbe6f1b7 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -17,13 +17,13 @@ %li.visible-sm.visible-xs = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do = icon('search') - %li.hidden-xs + -#%li.hidden-xs = link_to help_path, title: 'Help', data: {toggle: 'tooltip', placement: 'bottom'} do = icon('question-circle fw') - %li + -#%li = link_to explore_root_path, title: 'Explore', data: {toggle: 'tooltip', placement: 'bottom'} do = icon('globe fw') - %li + -#%li = link_to user_snippets_path(current_user), title: 'Your snippets', data: {toggle: 'tooltip', placement: 'bottom'} do = icon('clipboard fw') - if current_user.is_admin? @@ -34,7 +34,7 @@ %li.hidden-xs = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do = icon('plus fw') - %li + -#%li = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do = icon('cog fw') %li diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 687c1fc3dd2..d620c022273 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -1,36 +1,48 @@ %ul.nav.nav-sidebar - = nav_link(path: ['dashboard#show', 'root#show'], html_options: {class: 'home'}) do - = link_to dashboard_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do - = icon('dashboard fw') + = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do + = link_to (current_user ? root_path : explore_root_path), title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do + = icon('home fw') %span - Your Projects - = nav_link(path: 'projects#starred') do - = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do - = icon('star fw') + Projects + = nav_link(path: 'dashboard#activity') do + = link_to activity_dashboard_path, title: 'Activity', data: {placement: 'right'} do + = icon('dashboard fw') %span - Starred Projects + Activity = nav_link(controller: :groups) do - = link_to dashboard_groups_path, title: 'Groups', data: {placement: 'right'} do + = link_to (current_user ? dashboard_groups_path : explore_groups_path), title: 'Groups', data: {placement: 'right'} do = icon('group fw') %span Groups - = nav_link(controller: :milestones) do - = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do - = icon('clock-o fw') - %span - Milestones - = nav_link(path: 'dashboard#issues') do - = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do - = icon('exclamation-circle fw') - %span - Issues - %span.count= current_user.assigned_issues.opened.count - = nav_link(path: 'dashboard#merge_requests') do - = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do - = icon('tasks fw') + - if current_user + = nav_link(controller: :milestones) do + = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do + = icon('clock-o fw') + %span + Milestones + = nav_link(path: 'dashboard#issues') do + = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do + = icon('exclamation-circle fw') + %span + Issues + %span.count= current_user.assigned_issues.opened.count + = nav_link(path: 'dashboard#merge_requests') do + = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do + = icon('tasks fw') + %span + Merge Requests + %span.count= current_user.assigned_merge_requests.opened.count + = nav_link(controller: :snippets) do + = link_to (current_user ? user_snippets_path(current_user) : snippets_path), title: 'Your snippets', data: {placement: 'right'} do + = icon('clipboard fw') %span - Merge Requests - %span.count= current_user.assigned_merge_requests.opened.count + Snippets + - if current_user + = nav_link(controller: :profile) do + = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do + = icon('user fw') + %span + Profile = nav_link(controller: :help) do = link_to help_path, title: 'Help', data: {placement: 'right'} do = icon('question-circle fw') diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml deleted file mode 100644 index 66870e84ceb..00000000000 --- a/app/views/layouts/nav/_explore.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -%ul.nav.nav-sidebar - = nav_link(path: 'projects#trending') do - = link_to explore_root_path, title: 'Trending Projects', data: {placement: 'right'} do - = icon('comments fw') - %span Trending Projects - = nav_link(path: 'projects#starred') do - = link_to starred_explore_projects_path, title: 'Most-starred Projects', data: {placement: 'right'} do - = icon('star fw') - %span Most-starred Projects - = nav_link(path: 'projects#index') do - = link_to explore_projects_path, title: 'All Projects', data: {placement: 'right'} do - = icon('bookmark fw') - %span All Projects - = nav_link(controller: :groups) do - = link_to explore_groups_path, title: 'All Groups', data: {placement: 'right'} do - = icon('group fw') - %span All Groups - diff --git a/app/views/layouts/nav/_snippets.html.haml b/app/views/layouts/nav/_snippets.html.haml deleted file mode 100644 index 458b76a2c99..00000000000 --- a/app/views/layouts/nav/_snippets.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -%ul.nav.nav-sidebar - - if current_user - = nav_link(path: user_snippets_path(current_user), html_options: {class: 'home'}) do - = link_to user_snippets_path(current_user), title: 'Your snippets', data: {placement: 'right'} do - = icon('dashboard fw') - %span - Your Snippets - = nav_link(path: snippets_path) do - = link_to snippets_path, title: 'Discover snippets', data: {placement: 'right'} do - = icon('globe fw') - %span - Discover Snippets diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index c8662a15adb..ec209c38eed 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -36,7 +36,11 @@ — %br - if @target_url - #{link_to "View it on GitLab", @target_url} + - if @reply_by_email + Reply to this email directly or + #{link_to "view it on GitLab", @target_url}. + - else + #{link_to "View it on GitLab", @target_url} = email_action @target_url - if @project && !@disable_footer You're receiving this notification because you are a member of the #{link_to_unless @target_url, @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} project team. diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml index 9b0f40073ab..b77fe09fc2a 100644 --- a/app/views/layouts/snippets.html.haml +++ b/app/views/layouts/snippets.html.haml @@ -1,5 +1,8 @@ - page_title 'Snippets' -- header_title 'Snippets', snippets_path -- sidebar "snippets" +- if current_user + - header_title "Dashboard", root_path +- else + - header_title 'Snippets', snippets_path +- sidebar "dashboard" = render template: "layouts/application" diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml index 1b1395eaa17..43b58be7f98 100644 --- a/app/views/profiles/two_factor_auths/_codes.html.haml +++ b/app/views/profiles/two_factor_auths/_codes.html.haml @@ -1,6 +1,8 @@ %p.slead Should you ever lose your phone, each of these recovery codes can be used one - time each to regain access to your account. Please save them in a safe place. + time each to regain access to your account. Please save them in a safe place, or you + %b will + lose access to your account. .codes.well %ul diff --git a/app/views/projects/_bitbucket_import_modal.html.haml b/app/views/projects/_bitbucket_import_modal.html.haml index 745163e79a7..2987f6b5b22 100644 --- a/app/views/projects/_bitbucket_import_modal.html.haml +++ b/app/views/projects/_bitbucket_import_modal.html.haml @@ -5,9 +5,9 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3 Import projects from Bitbucket .modal-body - To enable importing projects from Bitbucket, + To enable importing projects from Bitbucket, - if current_user.admin? - you need to + as administrator you need to configure - else - your GitLab administrator needs to - == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/bitbucket.md'}. + ask your GitLab administrator to configure + == #{link_to 'OAuth integration', help_page_path("integration", "bitbucket")}. diff --git a/app/views/projects/_github_import_modal.html.haml b/app/views/projects/_github_import_modal.html.haml index de58b27df23..46ad1559356 100644 --- a/app/views/projects/_github_import_modal.html.haml +++ b/app/views/projects/_github_import_modal.html.haml @@ -5,9 +5,9 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3 Import projects from GitHub .modal-body - To enable importing projects from GitHub, + To enable importing projects from GitHub, - if current_user.admin? - you need to + as administrator you need to configure - else - your GitLab administrator needs to - == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/github.md'}.
\ No newline at end of file + ask your Gitlab administrator to configure + == #{link_to 'OAuth integration', help_page_path("integration", "github")}. diff --git a/app/views/projects/_gitlab_import_modal.html.haml b/app/views/projects/_gitlab_import_modal.html.haml index ae6c25f9371..377cf0187b8 100644 --- a/app/views/projects/_gitlab_import_modal.html.haml +++ b/app/views/projects/_gitlab_import_modal.html.haml @@ -5,9 +5,9 @@ %a.close{href: "#", "data-dismiss" => "modal"} × %h3 Import projects from GitLab.com .modal-body - To enable importing projects from GitLab.com, + To enable importing projects from GitLab.com, - if current_user.admin? - you need to + as administrator you need to configure - else - your GitLab administrator needs to - == #{link_to 'setup OAuth integration', 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/gitlab.md'}.
\ No newline at end of file + ask your GitLab administrator to configure + == #{link_to 'OAuth integration', help_page_path("integration", "gitlab")}. diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 71e92970305..b93036e78e6 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -2,7 +2,7 @@ .project-home-panel.clearfix{:class => ("empty-project" if empty_repo)} .project-identicon-holder = project_icon(@project, alt: '', class: 'project-avatar avatar s90') - .project-home-desc.lead + .project-home-desc %h1= @project.name - if @project.description.present? = markdown(@project.description, pipeline: :description) @@ -21,11 +21,6 @@ = forked_from_project.namespace.try(:name) - if can? current_user, :download_code, @project - = link_to "#", class: 'btn js-toggle-clone-holder' do - = icon('cloud-download fw') - Clone - - - if can? current_user, :download_code, @project = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do = icon('download fw') Download diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml index 8019c7f4569..a3ff7ce2f1f 100644 --- a/app/views/projects/blame/show.html.haml +++ b/app/views/projects/blame/show.html.haml @@ -12,25 +12,33 @@ = render "projects/blob/actions" .file-content.blame.highlight %table - - @blame.each do |commit, lines, since| - - commit = Commit.new(commit, @project) + - current_line = 1 + - @blame.each do |blame_group| %tr %td.blame-commit - %span.commit - = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "commit_short_id" - - = commit_author_link(commit, avatar: true, size: 16) - - = link_to_gfm truncate(commit.title, length: 20), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "row_title" + .commit + - commit = Commit.new(blame_group[:commit], @project) + .commit-row-title + %strong + = link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark" + .pull-right + = link_to commit.short_id, namespace_project_commit_path(@project.namespace, @project, commit), class: "monospace" + + .light + = commit_author_link(commit, avatar: false) + authored + #{time_ago_with_tooltip(commit.committed_date)} %td.lines.blame-numbers %pre - - (since...(since + lines.count)).each do |i| + - line_count = blame_group[:lines].count + - (current_line...(current_line + line_count)).each do |i| = i \ + - current_line += line_count %td.lines %pre{class: 'code highlight white'} %code - :erb - <% lines.each do |line| %> + - blame_group[:lines].each do |line| + :erb <%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %> - <% end %> + diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 43412624da6..a693c4b282f 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -5,6 +5,11 @@ %strong.str-truncated= branch.name - if branch.name == @repository.root_ref %span.label.label-info default + - elsif @repository.merged_to_root_ref? branch.name + %span.label.label-primary.has_tooltip(title="Merged into #{@repository.root_ref}") + %i.fa.fa-check + merged + - if @project.protected_branch? branch.name %span.label.label-success %i.fa.fa-lock diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml index cade930c8cc..bc7625e8989 100644 --- a/app/views/projects/buttons/_dropdown.html.haml +++ b/app/views/projects/buttons/_dropdown.html.haml @@ -2,7 +2,7 @@ %span.dropdown %a.dropdown-toggle.btn.btn-new{href: '#', "data-toggle" => "dropdown"} = icon('plus') - %ul.dropdown-menu + %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown - if can?(current_user, :create_issue, @project) %li = link_to url_for_new_issue do diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index caed0e69dc8..f99bc9a85eb 100644 --- a/app/views/projects/diffs/_warning.html.haml +++ b/app/views/projects/diffs/_warning.html.haml @@ -3,7 +3,7 @@ Too many changes to show. .pull-right - unless diff_hard_limit_enabled? - = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: :html)), class: "btn btn-sm btn-warning" + = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: nil)), class: "btn btn-sm btn-warning" - if current_controller?(:commit) or current_controller?(:merge_requests) - if current_controller?(:commit) diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index dfe45a3802d..e577d35d560 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -43,6 +43,8 @@ cd existing_folder git init git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git add . + git commit git push -u origin master - if can? current_user, :remove_project, @project diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index 2662e3aff6b..007f6c6a787 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -5,30 +5,36 @@ %hr = render "projects/merge_requests/show/mr_box" %hr - .append-bottom-20 + .append-bottom-20.mr-source-target - if @merge_request.open? - .btn-group.btn-group-sm.pull-right - %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } - = icon('download') - Download as - %span.caret - %ul.dropdown-menu - %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) - %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) + .pull-right + - if @merge_request.source_branch_exists? + = link_to "#modal_merge_info", class: "btn btn-sm", "data-toggle" => "modal" do + = icon('cloud-download fw') + Check out branch + + %span.dropdown + %a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} } + = icon('download') + Download as + %span.caret + %ul.dropdown-menu + %li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch) + %li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff) .light - %div - %span From - %span.label-branch #{source_branch_with_namespace(@merge_request)} - %span into - %span.label-branch #{@merge_request.target_branch} - - if @merge_request.open? && !@merge_request.branch_missing? - %div - If you want to try or merge this request manually, you can use the - = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" + %span Request to merge + %span.label-branch #{source_branch_with_namespace(@merge_request)} + %span into + %span.label-branch #{@merge_request.target_branch} = render "projects/merge_requests/show/how_to_merge" = render "projects/merge_requests/widget/show.html.haml" + - if @merge_request.open? && @merge_request.can_be_merged? + .light + You can also accept this merge request manually using the + = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal" + - if @commits.present? %ul.nav.nav-tabs.merge-request-tabs %li.notes-tab diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml index e1525f6aeb7..b61e193fc42 100644 --- a/app/views/projects/merge_requests/widget/open/_accept.html.haml +++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml @@ -23,4 +23,3 @@ btn = $('.accept_merge_request') btn.disable() btn.html("<i class='fa fa-spinner fa-spin'></i> Merge in progress") - diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml index d49eacb30dd..636218368cc 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -22,70 +22,75 @@ .col-sm-10 = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2} - %hr + - if import_sources_enabled? + %hr - .project-import.js-toggle-container - .form-group - %label.control-label Import project from - .col-sm-10 - - if github_import_enabled? - = link_to status_import_github_path, class: 'btn' do - %i.fa.fa-github - GitHub - - else - = link_to '#', class: 'how_to_import_link light btn' do - %i.fa.fa-github - GitHub - = render 'github_import_modal' - - - - if bitbucket_import_enabled? - = link_to status_import_bitbucket_path, class: 'btn' do - %i.fa.fa-bitbucket - Bitbucket - - else - = link_to '#', class: 'how_to_import_link light btn' do - %i.fa.fa-bitbucket - Bitbucket - = render 'bitbucket_import_modal' - - - unless request.host == 'gitlab.com' - - if gitlab_import_enabled? - = link_to status_import_gitlab_path, class: 'btn' do - %i.fa.fa-heart - GitLab.com - - else - = link_to '#', class: 'how_to_import_link light btn' do - %i.fa.fa-heart - GitLab.com - = render 'gitlab_import_modal' - - = link_to new_import_gitorious_path, class: 'btn' do - %i.icon-gitorious.icon-gitorious-small - Gitorious.org - - = link_to new_import_google_code_path, class: 'btn' do - %i.fa.fa-google - Google Code - - = link_to "#", class: 'btn js-toggle-button' do - %i.fa.fa-git - %span Any repo by URL - - .js-toggle-content.hide - .form-group.import-url-data - = f.label :import_url, class: 'control-label' do - %span Git repository URL + .project-import.js-toggle-container + .form-group + %label.control-label Import project from .col-sm-10 - = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' - .well.prepend-top-20 - %ul - %li - The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. - %li - The import will time out after 4 minutes. For big repositories, use a clone/push combination. - %li - To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. + - if github_import_enabled? + - if github_import_configured? + = link_to status_import_github_path, class: 'btn import_github' do + %i.fa.fa-github + GitHub + - else + = link_to '#', class: 'how_to_import_link light btn import_github' do + %i.fa.fa-github + GitHub + = render 'github_import_modal' + + - if bitbucket_import_enabled? + - if bitbucket_import_configured? + = link_to status_import_bitbucket_path, class: 'btn import_bitbucket', "data-no-turbolink" => "true" do + %i.fa.fa-bitbucket + Bitbucket + - else + = link_to status_import_bitbucket_path, class: 'how_to_import_link light btn import_bitbucket', "data-no-turbolink" => "true" do + %i.fa.fa-bitbucket + Bitbucket + = render 'bitbucket_import_modal' + + - if gitlab_import_enabled? + - if gitlab_import_configured? + = link_to status_import_gitlab_path, class: 'btn import_gitlab' do + %i.fa.fa-heart + GitLab.com + - else + = link_to status_import_gitlab_path, class: 'how_to_import_link light btn import_gitlab' do + %i.fa.fa-heart + GitLab.com + = render 'gitlab_import_modal' + + - if gitorious_import_enabled? + = link_to new_import_gitorious_path, class: 'btn import_gitorious' do + %i.icon-gitorious.icon-gitorious-small + Gitorious.org + + - if google_code_import_enabled? + = link_to new_import_google_code_path, class: 'btn import_google_code' do + %i.fa.fa-google + Google Code + + - if git_import_enabled? + = link_to "#", class: 'btn js-toggle-button import_git' do + %i.fa.fa-git + %span Any repo by URL + + .js-toggle-content.hide + .form-group.import-url-data + = f.label :import_url, class: 'control-label' do + %span Git repository URL + .col-sm-10 + = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git' + .well.prepend-top-20 + %ul + %li + The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>. + %li + The import will time out after 4 minutes. For big repositories, use a clone/push combination. + %li + To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}. %hr.prepend-botton-10 diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index 4577b84ab89..507f2c7beb0 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -23,18 +23,25 @@ = link_to namespace_project_tags_path(@project.namespace, @project) do = pluralize(number_with_delimiter(@repository.tag_names.count), 'tag') + %li + = link_to namespace_project_path(@project.namespace, @project) do + = repository_size + + - if !prefer_readme? && @repository.readme + %li + = link_to 'Readme', readme_path(@project) + - if @repository.changelog %li - = link_to changelog_path(@project) do - Changelog + = link_to 'Changelog', changelog_path(@project) + - if @repository.license %li - = link_to license_path(@project) do - License + = link_to 'License', license_path(@project) + - if @repository.contribution_guide %li - = link_to contribution_guide_path(@project) do - Contribution guide + = link_to 'Contribution guide', contribution_guide_path(@project) - if current_user && can_push_branch?(@project, @project.default_branch) - unless @repository.changelog @@ -73,4 +80,4 @@ - if @project.project_member_by_id(current_user) = link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project', class: 'cred' do - Leave this project + Leave this project
\ No newline at end of file diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index 50521264a61..a3a4bd4f752 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,3 +1,2 @@ %span.str-truncated - %span.tree_author= commit_author_link(commit, avatar: true, size: 16) = link_to_gfm commit.title, namespace_project_commit_path(@project.namespace, @project, commit.id), class: "tree-commit-link" diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml index 154332cb9a9..a75cd7bd809 100644 --- a/app/views/search/_category.html.haml +++ b/app/views/search/_category.html.haml @@ -21,6 +21,13 @@ Merge requests %span.badge = @search_results.merge_requests_count + %li{class: ("active" if @scope == 'milestones')} + = link_to search_filter_path(scope: 'milestones') do + = icon('clock-o fw') + %span + Milestones + %span.badge + = @search_results.milestones_count %li{class: ("active" if @scope == 'notes')} = link_to search_filter_path(scope: 'notes') do = icon('comments fw') @@ -74,4 +81,11 @@ Merge requests %span.badge = @search_results.merge_requests_count + %li{class: ("active" if @scope == 'milestones')} + = link_to search_filter_path(scope: 'milestones') do + = icon('clock-o fw') + %span + Milestones + %span.badge + = @search_results.milestones_count diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml new file mode 100644 index 00000000000..e0b18733d74 --- /dev/null +++ b/app/views/search/results/_milestone.html.haml @@ -0,0 +1,9 @@ +.search-result-row + %h4 + = link_to [milestone.project.namespace.becomes(Namespace), milestone.project, milestone] do + %span.term.str-truncated= milestone.title + + - if milestone.description.present? + .description.term + = preserve do + = search_md_sanitize(markdown(milestone.description))
\ No newline at end of file diff --git a/app/views/shared/_project.html.haml b/app/views/shared/_project.html.haml index 6bd61455d21..15df97b1333 100644 --- a/app/views/shared/_project.html.haml +++ b/app/views/shared/_project.html.haml @@ -3,7 +3,7 @@ - if avatar .dash-project-avatar = project_icon(project, alt: '', class: 'avatar project-avatar s40') - %span.str-truncated + %span.str-truncated.project-full-name %span.namespace-name - if project.namespace = project.namespace.human_name @@ -14,3 +14,7 @@ %span.pull-right.light %i.fa.fa-star = project.star_count + - if project.description.present? + .project-description + .str-truncated + = markdown(project.description, pipeline: :description) diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml index 19e8c31975b..cba18c14568 100644 --- a/app/views/shared/issuable/_context.html.haml +++ b/app/views/shared/issuable/_context.html.haml @@ -9,7 +9,7 @@ none .issuable-context-selectbox - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) - = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true) + = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true) %div.prepend-top-20.clearfix .issuable-context-title diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 0e8da8de723..bcaa48c7a12 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -36,11 +36,11 @@ .issues-other-filters .filter-item.inline = users_select_tag(:assignee_id, selected: params[:assignee_id], - placeholder: 'Assignee', class: 'trigger-submit', any_user: true, null_user: true, first_user: true) + placeholder: 'Assignee', class: 'trigger-submit', any_user: true, null_user: true, first_user: true, current_user: true) .filter-item.inline = users_select_tag(:author_id, selected: params[:author_id], - placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true) + placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true, current_user: true) .filter-item.inline.milestone-filter = select_tag('milestone_title', projects_milestones_options, @@ -60,7 +60,7 @@ .issues_bulk_update.hide = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control') - = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true) + = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true) = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone") = hidden_field_tag 'update[issues_ids]', [] = hidden_field_tag :state_event, params[:state_event] diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 3489bf3f191..09327d645f3 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -38,7 +38,7 @@ .clearfix .error-alert %hr -- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) +- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) .form-group .issue-assignee = f.label :assignee_id, class: 'control-label' do @@ -47,7 +47,8 @@ .col-sm-10 = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]", placeholder: 'Select a user', class: 'custom-form-control', null_user: true, - selected: issuable.assignee_id, project: @target_project || @project) + selected: issuable.assignee_id, project: @target_project || @project, + first_user: true, current_user: true) = link_to 'Assign to me', '#', class: 'btn assign-to-me-link' .form-group diff --git a/app/views/snippets/_head.html.haml b/app/views/snippets/_head.html.haml new file mode 100644 index 00000000000..0adf6b91f2c --- /dev/null +++ b/app/views/snippets/_head.html.haml @@ -0,0 +1,7 @@ +%ul.center-top-menu + = nav_link(page: user_snippets_path(current_user), html_options: {class: 'home'}) do + = link_to user_snippets_path(current_user), title: 'Your snippets', data: {placement: 'right'} do + Your Snippets + = nav_link(page: snippets_path) do + = link_to snippets_path, title: 'Explore snippets', data: {placement: 'right'} do + Explore Snippets diff --git a/app/views/snippets/current_user_index.html.haml b/app/views/snippets/current_user_index.html.haml index 0718f743828..62a967a2e06 100644 --- a/app/views/snippets/current_user_index.html.haml +++ b/app/views/snippets/current_user_index.html.haml @@ -1,13 +1,13 @@ - page_title "Your Snippets" -%h3.page-title - Your Snippets - .pull-right - = link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do - Add new snippet += render 'head' -%p.light +.slead Share code pastes with others out of git repository + .pull-right + = link_to new_snippet_path, class: "btn btn-new btn-sm", title: "New Snippet" do + Add new snippet + %ul.nav.nav-tabs = nav_tab :scope, nil do = link_to user_snippets_path(@user) do diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml index e9bb6a908d3..8608815e0a6 100644 --- a/app/views/snippets/index.html.haml +++ b/app/views/snippets/index.html.haml @@ -1,17 +1,9 @@ - page_title "Public Snippets" -%h3.page-title - Public snippets +- if current_user + = render 'head' - .pull-right - - if current_user - = link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do - Add new snippet - = link_to user_snippets_path(current_user), class: "btn btn-grouped" do - Your snippets - -%p.light +.slead Public snippets created by you and other users are listed here -%hr = render 'snippets' diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 089e8122918..aed00f9caeb 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -1,5 +1,5 @@ - page_title @snippet.title, "Snippets" -%h3.page-title +%h4.page-title = @snippet.title - if @snippet.private? @@ -8,17 +8,14 @@ private .pull-right - = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do + = link_to new_snippet_path, class: "btn btn-new btn-sm", title: "New Snippet" do Add new snippet -%hr -.append-bottom-20 +.append-bottom-10.prepend-top-10 .pull-right - = "##{@snippet.id}" %span.light - by + created by = link_to user_snippets_path(@snippet.author) do - = image_tag avatar_icon(@snippet.author_email), class: "avatar avatar-inline s16", alt: '' = @snippet.author_name .back-link @@ -27,7 +24,7 @@ ← your snippets - else = link_to snippets_path do - ← discover snippets + ← explore snippets .file-holder .file-title diff --git a/app/views/snippets/user_index.html.haml b/app/views/snippets/user_index.html.haml index 23700eb39da..7af5352da34 100644 --- a/app/views/snippets/user_index.html.haml +++ b/app/views/snippets/user_index.html.haml @@ -1,14 +1,13 @@ - page_title "Snippets", @user.name -%h3.page-title - = image_tag avatar_icon(@user.email), class: "avatar s24" - = @user.name - %span - \/ - Snippets - - if current_user - = link_to new_snippet_path, class: "btn btn-sm add_new pull-right", title: "New Snippet" do - Add new snippet -%hr +%ol.breadcrumb + %li + = link_to snippets_path do + Snippets + %li + = @user.name + .pull-right.hidden-xs + = link_to user_path(@user) do + #{@user.name} profile page = render 'snippets' diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml index 64b7f25ad37..aa4e8722fb1 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -7,7 +7,7 @@ = render 'shared/show_aside' .row - %section.col-md-8 + %section.col-md-7 .header-with-avatar = link_to avatar_icon(@user.email, 400), target: '_blank' do = image_tag avatar_icon(@user.email, 90), class: "avatar avatar-tile s90", alt: '' @@ -59,7 +59,7 @@ .content_list = spinner - %aside.col-md-4 + %aside.col-md-5 = render 'profile', user: @user = render 'projects', projects: @projects, contributed_projects: @contributed_projects diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb new file mode 100644 index 00000000000..8cfb96ef376 --- /dev/null +++ b/app/workers/email_receiver_worker.rb @@ -0,0 +1,51 @@ +class EmailReceiverWorker + include Sidekiq::Worker + + sidekiq_options queue: :incoming_email + + def perform(raw) + return unless Gitlab::ReplyByEmail.enabled? + + begin + Gitlab::Email::Receiver.new(raw).execute + rescue => e + handle_failure(raw, e) + end + end + + private + + def handle_failure(raw, e) + Rails.logger.warn("Email can not be processed: #{e}\n\n#{raw}") + + return unless raw.present? + + can_retry = false + reason = nil + + case e + when Gitlab::Email::Receiver::SentNotificationNotFoundError + reason = "We couldn't figure out what the email is in reply to. Please create your comment through the web interface." + when Gitlab::Email::Receiver::EmptyEmailError + can_retry = true + reason = "It appears that the email is blank. Make sure your reply is at the top of the email, we can't process inline replies." + when Gitlab::Email::Receiver::AutoGeneratedEmailError + reason = "The email was marked as 'auto generated', which we can't accept. Please create your comment through the web interface." + when Gitlab::Email::Receiver::UserNotFoundError + reason = "We couldn't figure out what user corresponds to the email. Please create your comment through the web interface." + when Gitlab::Email::Receiver::UserBlockedError + reason = "Your account has been blocked. If you believe this is in error, contact a staff member." + when Gitlab::Email::Receiver::UserNotAuthorizedError + reason = "You are not allowed to respond to the thread you are replying to. If you believe this is in error, contact a staff member." + when Gitlab::Email::Receiver::NoteableNotFoundError + reason = "The thread you are replying to no longer exists, perhaps it was deleted? If you believe this is in error, contact a staff member." + when Gitlab::Email::Receiver::InvalidNoteError + can_retry = true + reason = e.message + else + return + end + + EmailRejectionMailer.delay.rejection(reason, raw, can_retry) + end +end diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb index 1d21addece6..916a99bb273 100644 --- a/app/workers/emails_on_push_worker.rb +++ b/app/workers/emails_on_push_worker.rb @@ -4,7 +4,7 @@ class EmailsOnPushWorker def perform(project_id, recipients, push_data, options = {}) options.symbolize_keys! options.reverse_merge!( - send_from_committer_email: false, + send_from_committer_email: false, disable_diffs: false ) send_from_committer_email = options[:send_from_committer_email] @@ -16,9 +16,9 @@ class EmailsOnPushWorker ref = push_data["ref"] author_id = push_data["user_id"] - action = + action = if Gitlab::Git.blank_ref?(before_sha) - :create + :create elsif Gitlab::Git.blank_ref?(after_sha) :delete else @@ -42,17 +42,22 @@ class EmailsOnPushWorker end recipients.split(" ").each do |recipient| - Notify.repository_push_email( - project_id, - recipient, - author_id: author_id, - ref: ref, - action: action, - compare: compare, - reverse_compare: reverse_compare, - send_from_committer_email: send_from_committer_email, - disable_diffs: disable_diffs - ).deliver + begin + Notify.repository_push_email( + project_id, + recipient, + author_id: author_id, + ref: ref, + action: action, + compare: compare, + reverse_compare: reverse_compare, + send_from_committer_email: send_from_committer_email, + disable_diffs: disable_diffs + ).deliver + # These are input errors and won't be corrected even if Sidekiq retries + rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e + logger.info("Failed to send e-mail for project '#{project.name_with_namespace}' to #{recipient}: #{e}") + end end ensure compare = nil diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb index b546f8777e1..f2ba2e15e7b 100644 --- a/app/workers/repository_import_worker.rb +++ b/app/workers/repository_import_worker.rb @@ -25,9 +25,10 @@ class RepositoryImportWorker end return project.import_fail unless data_import_result + Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket' + project.import_finish project.save ProjectCacheWorker.perform_async(project.id) - Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket' end end diff --git a/bin/background_jobs b/bin/background_jobs index a041a4b0433..a4895cf6586 100755 --- a/bin/background_jobs +++ b/bin/background_jobs @@ -37,7 +37,7 @@ start_no_deamonize() start_sidekiq() { - 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 -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 + 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 -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1 } load_ok() diff --git a/bin/mail_room b/bin/mail_room new file mode 100755 index 00000000000..f4f1a170c04 --- /dev/null +++ b/bin/mail_room @@ -0,0 +1,52 @@ +#!/bin/sh + +cd $(dirname $0)/.. +app_root=$(pwd) + +mail_room_pidfile="$app_root/tmp/pids/mail_room.pid" +mail_room_logfile="$app_root/log/mail_room.log" +mail_room_config="$app_root/config/mail_room.yml" + +get_mail_room_pid() +{ + local pid=$(cat $mail_room_pidfile) + if [ -z "$pid" ] ; then + echo "Could not find a PID in $mail_room_pidfile" + exit 1 + fi + mail_room_pid=$pid +} + +start() +{ + bundle exec mail_room -q -c $mail_room_config >> $mail_room_logfile 2>&1 & + PID=$! + echo $PID > $mail_room_pidfile +} + +stop() +{ + get_mail_room_pid + kill -TERM $mail_room_pid +} + +restart() +{ + stop + start +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + restart + ;; + *) + echo "Usage: $0 {start|stop|restart}" + ;; +esac diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 56770335ddc..c7b60a1d4b1 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -94,6 +94,13 @@ production: &base # The default is 'tmp/repositories' relative to the root of the Rails app. # repository_downloads_path: tmp/repositories + ## Reply by email + # Allow users to comment on issues and merge requests by replying to notification emails. + # For documentation on how to set this up, see http://doc.gitlab.com/ce/reply_by_email/README.md + reply_by_email: + enabled: false + address: "replies+%{reply_key}@gitlab.example.com" + ## Gravatar ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html gravatar: diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 026c1a5792c..c47e5dab27c 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -8,7 +8,7 @@ class Settings < Settingslogic def gitlab_on_standard_port? gitlab.port.to_i == (gitlab.https ? 443 : 80) end - + # get host without www, thanks to http://stackoverflow.com/a/6674363/1233435 def get_host_without_www(url) url = URI.encode(url) @@ -32,14 +32,12 @@ class Settings < Settingslogic end end + def build_base_gitlab_url + base_gitlab_url.join('') + end + def build_gitlab_url - custom_port = gitlab_on_standard_port? ? nil : ":#{gitlab.port}" - [ gitlab.protocol, - "://", - gitlab.host, - custom_port, - gitlab.relative_url_root - ].join('') + (base_gitlab_url + [gitlab.relative_url_root]).join('') end # check that values in `current` (string or integer) is a contant in `modul`. @@ -64,6 +62,17 @@ class Settings < Settingslogic end value end + + private + + def base_gitlab_url + custom_port = gitlab_on_standard_port? ? nil : ":#{gitlab.port}" + [ gitlab.protocol, + "://", + gitlab.host, + custom_port + ] + end end end @@ -123,6 +132,7 @@ Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].ni Settings.gitlab['email_from'] ||= "gitlab@#{Settings.gitlab.host}" Settings.gitlab['email_display_name'] ||= "GitLab" Settings.gitlab['email_reply_to'] ||= "noreply@#{Settings.gitlab.host}" +Settings.gitlab['base_url'] ||= Settings.send(:build_base_gitlab_url) Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url) Settings.gitlab['user'] ||= 'git' Settings.gitlab['user_home'] ||= begin @@ -148,6 +158,13 @@ Settings.gitlab.default_projects_features['snippets'] = false if Settings. Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE) Settings.gitlab['repository_downloads_path'] = File.absolute_path(Settings.gitlab['repository_downloads_path'] || 'tmp/repositories', Rails.root) Settings.gitlab['restricted_signup_domains'] ||= [] +Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','git'] + +# +# Reply by email +# +Settings['reply_by_email'] ||= Settingslogic.new({}) +Settings.reply_by_email['enabled'] = false if Settings.reply_by_email['enabled'].nil? # # Gravatar diff --git a/config/initializers/7_omniauth.rb b/config/initializers/7_omniauth.rb index 7f73546ac89..70ed10e8275 100644 --- a/config/initializers/7_omniauth.rb +++ b/config/initializers/7_omniauth.rb @@ -11,7 +11,7 @@ if Gitlab::LDAP::Config.enabled? end end -OmniAuth.config.full_host = Settings.gitlab['url'] +OmniAuth.config.full_host = Settings.gitlab['base_url'] OmniAuth.config.allowed_request_methods = [:post] #In case of auto sign-in, the GET method is used (users don't get to click on a button) OmniAuth.config.allowed_request_methods << :get if Gitlab.config.omniauth.auto_sign_in_with_provider.present? diff --git a/config/mail_room.yml.example b/config/mail_room.yml.example new file mode 100644 index 00000000000..dd8edfc42eb --- /dev/null +++ b/config/mail_room.yml.example @@ -0,0 +1,27 @@ +:mailboxes: + - + # # IMAP server host + # :host: "imap.gmail.com" + # # IMAP server port + # :port: 993 + # # Whether the IMAP server uses SSL + # :ssl: true + # # Email account username. Usually the full email address. + # :email: "replies@gitlab.example.com" + # # Email account password + # :password: "password" + # # The name of the mailbox where incoming mail will end up. Usually "inbox". + # :name: "inbox" + # # Always "sidekiq". + # :delivery_method: sidekiq + # # Always true. + # :delete_after_delivery: true + # :delivery_options: + # # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml. + # :redis_url: redis://localhost:6379 + # # Always "resque:gitlab". + # :namespace: resque:gitlab + # # Always "incoming_email". + # :queue: incoming_email + # # Always "EmailReceiverWorker" + # :worker: EmailReceiverWorker diff --git a/config/routes.rb b/config/routes.rb index d7307a61ede..8ba439f08b8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -261,6 +261,7 @@ Gitlab::Application.routes.draw do member do get :issues get :merge_requests + get :activity end scope module: :dashboard do diff --git a/db/migrate/20150812080800_add_settings_import_sources.rb b/db/migrate/20150812080800_add_settings_import_sources.rb new file mode 100644 index 00000000000..276d2fdb2b1 --- /dev/null +++ b/db/migrate/20150812080800_add_settings_import_sources.rb @@ -0,0 +1,11 @@ +require 'yaml' + +class AddSettingsImportSources < ActiveRecord::Migration + def change + unless column_exists?(:application_settings, :import_sources) + add_column :application_settings, :import_sources, :text + import_sources = YAML::dump(Settings.gitlab['import_sources']) + execute("update application_settings set import_sources = '#{import_sources}'") + end + end +end diff --git a/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb b/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb new file mode 100644 index 00000000000..de2078a9268 --- /dev/null +++ b/db/migrate/20150814065925_remove_oauth_tokens_from_users.rb @@ -0,0 +1,8 @@ +class RemoveOauthTokensFromUsers < ActiveRecord::Migration + def change + remove_column :users, :github_access_token, :string + remove_column :users, :gitlab_access_token, :string + remove_column :users, :bitbucket_access_token, :string + remove_column :users, :bitbucket_access_token_secret, :string + end +end diff --git a/db/migrate/20150818213832_add_sent_notifications.rb b/db/migrate/20150818213832_add_sent_notifications.rb new file mode 100644 index 00000000000..43e8d6a1a82 --- /dev/null +++ b/db/migrate/20150818213832_add_sent_notifications.rb @@ -0,0 +1,13 @@ +class AddSentNotifications < ActiveRecord::Migration + def change + create_table :sent_notifications do |t| + t.references :project + t.references :noteable, polymorphic: true + t.references :recipient + t.string :commit_id + t.string :reply_key, null: false + end + + add_index :sent_notifications, :reply_key, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 6e919f2883b..108c48bf321 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20150806104937) do +ActiveRecord::Schema.define(version: 20150818213832) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -44,6 +44,7 @@ ActiveRecord::Schema.define(version: 20150806104937) do t.boolean "user_oauth_applications", default: true t.string "after_sign_out_path" t.integer "session_expire_delay", default: 10080, null: false + t.text "import_sources" end create_table "audit_events", force: true do |t| @@ -404,6 +405,17 @@ ActiveRecord::Schema.define(version: 20150806104937) do add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree + create_table "sent_notifications", force: true do |t| + t.integer "project_id" + t.integer "noteable_id" + t.string "noteable_type" + t.integer "recipient_id" + t.string "commit_id" + t.string "reply_key", null: false + end + + add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree + create_table "services", force: true do |t| t.string "type" t.string "title" @@ -514,13 +526,9 @@ ActiveRecord::Schema.define(version: 20150806104937) do t.string "unconfirmed_email" t.boolean "hide_no_ssh_key", default: false t.string "website_url", default: "", null: false - t.string "github_access_token" - t.string "gitlab_access_token" t.string "notification_email" t.boolean "hide_no_password", default: false t.boolean "password_automatically_set", default: false - t.string "bitbucket_access_token" - t.string "bitbucket_access_token_secret" t.string "location" t.string "encrypted_otp_secret" t.string "encrypted_otp_secret_iv" diff --git a/doc/README.md b/doc/README.md index 0524fda3ed6..337c4e6a62d 100644 --- a/doc/README.md +++ b/doc/README.md @@ -29,6 +29,7 @@ - [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed. - [Update](update/README.md) Update guides to upgrade your installation. - [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page. +- [Reply by email](reply_by_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails. ## Contributor documentation diff --git a/doc/api/notes.md b/doc/api/notes.md index ee2f9fa0eac..c683cb883d4 100644 --- a/doc/api/notes.md +++ b/doc/api/notes.md @@ -31,7 +31,10 @@ Parameters: "state": "active", "created_at": "2013-09-30T13:46:01Z" }, - "created_at": "2013-10-02T09:22:45Z" + "created_at": "2013-10-02T09:22:45Z", + "system": true, + "upvote": false, + "downvote": false }, { "id": 305, @@ -45,7 +48,10 @@ Parameters: "state": "active", "created_at": "2013-09-30T13:46:01Z" }, - "created_at": "2013-10-02T09:56:03Z" + "created_at": "2013-10-02T09:56:03Z", + "system": false, + "upvote": false, + "downvote": false } ] ``` diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md index ee57fdc6590..54c1780c3ab 100644 --- a/doc/customization/libravatar.md +++ b/doc/customization/libravatar.md @@ -1,6 +1,6 @@ # Use Libravatar service with GitLab -GitLab by default supports [Gravatar](gravatar.com) avatar service. +GitLab by default supports [Gravatar](https://gravatar.com) avatar service. Libravatar is a service which delivers your avatar (profile picture) to other websites and their API is [heavily based on gravatar](http://wiki.libravatar.org/api/). diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md index 9491f8c91fe..b904c70e980 100644 --- a/doc/gitlab-basics/README.md +++ b/doc/gitlab-basics/README.md @@ -20,4 +20,6 @@ Step-by-step guides on the basics of working with Git and GitLab. * [Add a file](add-file.md) +* [Add an image](add-image.md) + * [Create a Merge Request](add-merge-request.md) diff --git a/doc/gitlab-basics/add-image.md b/doc/gitlab-basics/add-image.md new file mode 100644 index 00000000000..476b48a217c --- /dev/null +++ b/doc/gitlab-basics/add-image.md @@ -0,0 +1,62 @@ +# How to add an image + +The following are the steps to add images to your repository in +GitLab: + +Find the image that you’d like to add. + +In your computer files, find the GitLab project to which you'd like to add the image +(you'll find it as a regular file). Click on every file until you find exactly where you'd +like to add the image. There, paste the image. + +Go to your [shell](command-line-commands.md), and add the following commands: + +Add this command for every directory that you'd like to open: +``` +cd NAME-OF-FILE-YOU'D-LIKE-TO-OPEN +``` + +Create a new branch: +``` +git checkout -b NAME-OF-BRANCH +``` + +Check if your image was correctly added to the directory: +``` +ls +``` + +You should see the name of the image in the list shown. + +Move up the hierarchy through directories: +``` +cd ../ +``` + +Check the status and you should see your image’s name in red: +``` +git status +``` + +Add your changes: +``` +git add NAME-OF-YOUR-IMAGE +``` + +Check the status and you should see your image’s name in green: +``` +git status +``` + +Add the commit: +``` +git commit -m “DESCRIBE COMMIT IN A FEW WORDS” +``` + +Now you can push (send) your changes (in the branch NAME-OF-BRANCH) to GitLab (the git remote named 'origin'): +``` +git push origin NAME-OF-BRANCH +``` + +Your image will be added to your branch in your repository in GitLab. Create a [Merge Request](add-merge-request.md) +to integrate your changes to your project. diff --git a/doc/install/installation.md b/doc/install/installation.md index 55b6f216dde..73e36fa7e51 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -195,9 +195,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da ### Clone the Source # Clone GitLab repository - sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-13-stable gitlab + sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-14-stable gitlab -**Note:** You can change `7-13-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! +**Note:** You can change `7-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server! ### Configure It @@ -366,7 +366,7 @@ Make sure to edit the config file to match your setup: # domain name of your host serving GitLab. # If using Ubuntu default nginx install: # either remove the default_server from the listen line - # or else rm -f /etc/sites-enabled/default + # or else sudo rm -f /etc/nginx/sites-enabled/default sudo editor /etc/nginx/sites-available/gitlab **Note:** If you want to use HTTPS, replace the `gitlab` Nginx config with `gitlab-ssl`. See [Using HTTPS](#using-https) for HTTPS configuration details. diff --git a/doc/install/requirements.md b/doc/install/requirements.md index e44ce25b6b4..aa0d03b75bc 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -93,7 +93,7 @@ To change the Unicorn workers when you have the Omnibus package please see [the ## Database -If you want to run the database separately, the **recommended** database size is **1 MB per user**. +If you want to run the database separately expect a size of about 1 MB per user. ## Redis and Sidekiq @@ -113,4 +113,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o ### Common UI problems with IE -If you experience UI issues with Internet Explorer, please make sure that you have the `Compatibility View` mode disabled. +If you experience UI issues with Internet Explorer, please make sure that you have the `Compatibility View` mode disabled.
\ No newline at end of file diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md index f60ce35d3e2..a0e23c1586c 100644 --- a/doc/profile/two_factor_authentication.md +++ b/doc/profile/two_factor_authentication.md @@ -8,6 +8,10 @@ your phone. By enabling 2FA, the only way someone other than you can log into your account is to know your username and password *and* have access to your phone. +#### Note +When you enable 2FA, don't forget to back up your recovery codes. For your safety, if you +lose your codes for GitLab.com, we can't disable or recover them. + ## Enabling 2FA **In GitLab:** diff --git a/doc/release/monthly.md b/doc/release/monthly.md index b10e7420675..c1ed9e3b80e 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -1,8 +1,9 @@ # Monthly Release -NOTE: This is a guide used by the GitLab B.V. developers. +NOTE: This is a guide used by the GitLab the company to release GitLab. +As an end user you do not need to use this guide. -It starts 7 working days before the release. +The process starts 7 working days before the release. The release manager doesn't have to perform all the work but must ensure someone is assigned. The current release manager must schedule the appointment of the next release manager. The new release manager should create overall issue to track the progress. @@ -156,6 +157,7 @@ Tweet about the RC release: 1. Also check the CI changelog 1. Add a proposed tweet text to the blog post WIP MR description. 1. Create a WIP MR for the blog post +1. Make sure merge request title starts with `WIP` so it can not be accidently merged until ready. 1. Ask Dmitriy (or a team member with OS X) to add screenshots to the WIP MR. 1. Decide with core team who will be the MVP user. 1. Create WIP MR for adding MVP to MVP page on website @@ -163,7 +165,7 @@ Tweet about the RC release: 1. Create a merge request on [GitLab.com](https://gitlab.com/gitlab-com/www-gitlab-com/tree/master) 1. Assign to one reviewer who will fix spelling issues by editing the branch (either with a git client or by using the online editor) 1. Comment to the reviewer: '@person Please mention the whole team as soon as you are done (3 workdays before release at the latest)' -1. Create a complete copy of the [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md) for the release after this. +1. Create a new merge request with complete copy of the [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md) for the next release using the branch name `release-x-x-x`. ## Create CE, EE, CI stable versions diff --git a/doc/reply_by_email/README.md b/doc/reply_by_email/README.md new file mode 100644 index 00000000000..5d36f5121d1 --- /dev/null +++ b/doc/reply_by_email/README.md @@ -0,0 +1,180 @@ +# Reply by email + +GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails. + +In order to do this, you need access to 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, Yahoo! Mail, Outlook.com and iCloud, as well as the [Postfix](http://www.postfix.org/) mail server which you can run on-premises. + +## Set it up + +In this example, we'll use the Gmail address `gitlab-replies@gmail.com`. If you're actually using Gmail 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). + +### Installations from source + +1. Go to the GitLab installation directory: + + ```sh + cd /home/git/gitlab + ``` + +1. Find the `reply_by_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `reply_key`: + + ```sh + sudo editor config/gitlab.yml + ``` + + ```yaml + reply_by_email: + enabled: true + address: "gitlab-replies+%{reply_key}@gmail.com" + ``` + + As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-replies@gmail.com`. + +2. Find `config/mail_room.yml.example` and copy it to `config/mail_room.yml`: + + ```sh + sudo cp config/mail_room.yml.example config/mail_room.yml + ``` + +3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account: + + ```sh + sudo editor config/mail_room.yml + ``` + + ```yaml + :mailboxes: + - + # IMAP server host + :host: "imap.gmail.com" + # IMAP server port + :port: 993 + # Whether the IMAP server uses SSL + :ssl: true + # Email account username. Usually the full email address. + :email: "gitlab-replies@gmail.com" + # Email account password + :password: "[REDACTED]" + # The name of the mailbox where incoming mail will end up. Usually "inbox". + :name: "inbox" + # Always "sidekiq". + :delivery_method: sidekiq + # Always true. + :delete_after_delivery: true + :delivery_options: + # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml. + :redis_url: redis://localhost:6379 + # Always "resque:gitlab". + :namespace: resque:gitlab + # Always "incoming_email". + :queue: incoming_email + # Always "EmailReceiverWorker" + :worker: EmailReceiverWorker + ``` + + +4. Find `lib/support/init.d/gitlab.default.example` and copy it to `/etc/default/gitlab`: + + ```sh + sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab + ``` + +5. Edit `/etc/default/gitlab` to enable `mail_room`: + + ```sh + sudo editor /etc/default/gitlab + ``` + + ```sh + mail_room_enabled=true + ``` + +6. Restart GitLab: + + ```sh + sudo service gitlab restart + ``` + +7. Check if everything is configured correctly: + + ```sh + sudo bundle exec rake gitlab:reply_by_email:check RAILS_ENV=production + ``` + +8. Reply by email should now be working. + +### Omnibus package installations + +TODO + +### Development + +1. Go to the GitLab installation directory. + +1. Find the `reply_by_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `reply_key`: + + ```yaml + reply_by_email: + enabled: true + address: "gitlab-replies+%{reply_key}@gmail.com" + ``` + + As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-replies@gmail.com`. + +2. Find `config/mail_room.yml.example` and copy it to `config/mail_room.yml`: + + ```sh + sudo cp config/mail_room.yml.example config/mail_room.yml + ``` + +3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account: + + ```yaml + :mailboxes: + - + # IMAP server host + :host: "imap.gmail.com" + # IMAP server port + :port: 993 + # Whether the IMAP server uses SSL + :ssl: true + # Email account username. Usually the full email address. + :email: "gitlab-replies@gmail.com" + # Email account password + :password: "[REDACTED]" + # The name of the mailbox where incoming mail will end up. Usually "inbox". + :name: "inbox" + # Always "sidekiq". + :delivery_method: sidekiq + # Always true. + :delete_after_delivery: true + :delivery_options: + # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml. + :redis_url: redis://localhost:6379 + # Always "resque:gitlab". + :namespace: resque:gitlab + # Always "incoming_email". + :queue: incoming_email + # Always "EmailReceiverWorker" + :worker: EmailReceiverWorker + ``` + +4. Uncomment the `mail_room` line in your `Procfile`: + + ```yaml + mail_room: bundle exec mail_room -q -c config/mail_room.yml + ``` + +6. Restart GitLab: + + ```sh + bundle exec foreman start + ``` + +7. Check if everything is configured correctly: + + ```sh + bundle exec rake gitlab:reply_by_email:check RAILS_ENV=development + ``` + +8. Reply by email should now be working. diff --git a/doc/update/6.x-or-7.x-to-7.13.md b/doc/update/6.x-or-7.x-to-7.14.md index 3e3ceb1194c..a1488474f60 100644 --- a/doc/update/6.x-or-7.x-to-7.13.md +++ b/doc/update/6.x-or-7.x-to-7.14.md @@ -1,7 +1,7 @@ -# From 6.x or 7.x to 7.13 -*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.13.md) for the most up to date instructions.* +# From 6.x or 7.x to 7.14 +*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.14.md) for the most up to date instructions.* -This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.13. +This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.14. ## Global issue numbers @@ -71,7 +71,7 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut For GitLab Community Edition: ```bash -sudo -u git -H git checkout 7-13-stable +sudo -u git -H git checkout 7-14-stable ``` OR @@ -79,7 +79,7 @@ OR For GitLab Enterprise Edition: ```bash -sudo -u git -H git checkout 7-13-stable-ee +sudo -u git -H git checkout 7-14-stable-ee ``` ## 4. Install additional packages @@ -127,7 +127,7 @@ sudo apt-get install nodejs ```bash cd /home/git/gitlab-shell sudo -u git -H git fetch -sudo -u git -H git checkout v2.6.3 +sudo -u git -H git checkout v2.6.4 ``` ## 7. Install libs, migrations, etc. @@ -162,11 +162,11 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab TIP: to see what changed in `gitlab.yml.example` in this release use next command: ``` -git diff 6-0-stable:config/gitlab.yml.example 7-13-stable:config/gitlab.yml.example +git diff 6-0-stable:config/gitlab.yml.example 7.14-stable:config/gitlab.yml.example ``` -* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-13-stable/config/gitlab.yml.example but with your settings. -* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-13-stable/config/unicorn.rb.example but with your settings. +* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/config/gitlab.yml.example but with your settings. +* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/config/unicorn.rb.example but with your settings. * Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.0/config.yml.example but with your settings. * Copy rack attack middleware config @@ -182,14 +182,14 @@ sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab ### Change Nginx settings -* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-13-stable/lib/support/nginx/gitlab but with your settings. -* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-13-stable/lib/support/nginx/gitlab-ssl but with your settings. +* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/lib/support/nginx/gitlab but with your settings. +* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/lib/support/nginx/gitlab-ssl but with your settings. * A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section. ### Check the version of /usr/local/bin/git If you installed Git from source into /usr/local/bin/git then please [check -your version](7.12-to-7.13.md). +your version](7.13-to-7.14.md). ## 9. Start application diff --git a/doc/update/7.13-to-7.14.md b/doc/update/7.13-to-7.14.md new file mode 100644 index 00000000000..7c2d3f4498a --- /dev/null +++ b/doc/update/7.13-to-7.14.md @@ -0,0 +1,129 @@ +# From 7.13 to 7.14 + +### 0. Double-check your Git version + +**This notice applies only to /usr/local/bin/git** + +If you compiled Git from source on your GitLab server then please double-check +that you are using a version that protects against CVE-2014-9390. For six +months after this vulnerability became known the GitLab installation guide +still contained instructions that would install the outdated, 'vulnerable' Git +version 2.1.2. + +Run the following command to get your current Git version. + +``` +/usr/local/bin/git --version +``` + +If you see 'No such file or directory' then you did not install Git according +to the outdated instructions from the GitLab installation guide and you can go +to the next step 'Stop server' below. + +If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4, +v2.2.1 or newer. You can use the [instructions in the GitLab source +installation +guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies) +to install a newer version of Git. + +### 1. Stop server + + sudo service gitlab stop + +### 2. Backup + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production +``` + +### 3. Get latest code + +```bash +sudo -u git -H git fetch --all +sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically +``` + +For GitLab Community Edition: + +```bash +sudo -u git -H git checkout 7-14-stable +``` + +OR + +For GitLab Enterprise Edition: + +```bash +sudo -u git -H git checkout 7-14-stable-ee +``` + +### 4. Update gitlab-shell + +```bash +cd /home/git/gitlab-shell +sudo -u git -H git fetch +sudo -u git -H git checkout v2.6.4 +``` + +### 5. Install libs, migrations, etc. + +```bash +cd /home/git/gitlab + +# MySQL installations (note: the line below states '--without ... postgres') +sudo -u git -H bundle install --without development test postgres --deployment + +# PostgreSQL installations (note: the line below states '--without ... mysql') +sudo -u git -H bundle install --without development test mysql --deployment + +# Run database migrations +sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production + +# Clean up assets and cache +sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production + +# Update init.d script +sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab +``` + +### 6. Update config files + +#### New configuration options for `gitlab.yml` + +There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`. + +``` +git diff origin/7-13-stable:config/gitlab.yml.example origin/7-14-stable:config/gitlab.yml.example +`````` + +### 7. Start application + + sudo service gitlab start + sudo service nginx restart + +### 8. Check application status + +Check if GitLab and its environment are configured correctly: + + sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production + +To make sure you didn't miss anything run a more thorough check with: + + sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production + +If all items are green, then congratulations, the upgrade is complete! + +## Things went south? Revert to previous version (7.13) + +### 1. Revert the code to the previous version +Follow the [upgrade guide from 7.12 to 7.13](7.12-to-7.13.md), except for the database migration +(The backup is already migrated to the previous version) + +### 2. Restore from the backup: + +```bash +cd /home/git/gitlab +sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production +``` +If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above. diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md index 025992deece..80817c98d22 100644 --- a/doc/workflow/notifications.md +++ b/doc/workflow/notifications.md @@ -55,7 +55,7 @@ Below is the table of events users can be notified of: | User added to project | User | Sent when user is added to project | | Project access level changed | User | Sent when user project access level is changed | | User added to group | User | Sent when user is added to group | -| Group access level changed | User | Sent when user group access level is changed | +| Group access level changed | User | Sent when user group access level is changed | | Project moved | Project members [1] | [1] not disabled | ### Issue / Merge Request events @@ -84,3 +84,8 @@ In all of the below cases, the notification will be sent to: | Reopen merge request | | | Merge merge request | | | New comment | The above, plus anyone mentioned by `@username` in the comment, with notification level "Mention" or higher | + +You won't receive notifications for Issues, Merge Requests or Milestones +created by yourself. You will only receive automatic notifications when +somebody else comments or adds changes to the ones that you've created or +mentions you. diff --git a/features/admin/groups.feature b/features/admin/groups.feature index aa365a6ea1a..973918086a3 100644 --- a/features/admin/groups.feature +++ b/features/admin/groups.feature @@ -27,3 +27,9 @@ Feature: Admin Groups When I visit admin group page And I remove user "John Doe" from group Then I should not see "John Doe" in team list + + @javascript + Scenario: Invite user to a group by e-mail + When I visit admin group page + When I select user "johndoe@gitlab.com" from user list as "Reporter" + Then I should see "johndoe@gitlab.com" in team list in every project as "Reporter" diff --git a/features/admin/projects.feature b/features/admin/projects.feature index a6c3d6b7822..f7cec04eb75 100644 --- a/features/admin/projects.feature +++ b/features/admin/projects.feature @@ -4,9 +4,18 @@ Feature: Admin Projects Given I sign in as an admin And there are projects in system - Scenario: Projects list + Scenario: I should see non-archived projects in the list + Given archived project "Archive" When I visit admin projects page + Then I should see all non-archived projects + And I should not see project "Archive" + + Scenario: I should see all projects in the list + Given archived project "Archive" + When I visit admin projects page + And I check "Show archived projects" Then I should see all projects + And I should see "archived" label Scenario: Projects show When I visit admin projects page diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature index 1959d327082..392d4235eff 100644 --- a/features/dashboard/dashboard.feature +++ b/features/dashboard/dashboard.feature @@ -10,6 +10,10 @@ Feature: Dashboard Scenario: I should see projects list Then I should see "New Project" link Then I should see "Shop" project link + + @javascript + Scenario: I should see activity list + And I visit dashboard activity page Then I should see project "Shop" activity feed Scenario: I should see groups list @@ -26,12 +30,12 @@ Feature: Dashboard @javascript Scenario: I should see User joined Project event Given user with name "John Doe" joined project "Shop" - When I visit dashboard page + When I visit dashboard activity page Then I should see "John Doe joined project Shop" event @javascript Scenario: I should see User left Project event Given user with name "John Doe" joined project "Shop" And user with name "John Doe" left project "Shop" - When I visit dashboard page + When I visit dashboard activity page Then I should see "John Doe left project Shop" event diff --git a/features/dashboard/event_filters.feature b/features/dashboard/event_filters.feature index ec5680caba6..96399ea21a6 100644 --- a/features/dashboard/event_filters.feature +++ b/features/dashboard/event_filters.feature @@ -6,7 +6,7 @@ Feature: Event Filters And this project has push event And this project has new member event And this project has merge request event - And I visit dashboard page + And I visit dashboard activity page @javascript Scenario: I should see all events @@ -16,7 +16,7 @@ Feature: Event Filters @javascript Scenario: I should see only pushed events - When I click "push" event filter + When I click "push" event filter Then I should see push event And I should not see new member event And I should not see merge request event @@ -38,11 +38,11 @@ Feature: Event Filters @javascript Scenario: I should see only selected events while page reloaded When I click "push" event filter - And I visit dashboard page + And I visit dashboard activity page Then I should see push event And I should not see new member event When I click "team" event filter - And I visit dashboard page + And I visit dashboard activity page Then I should see push event And I should see new member event And I should not see merge request event diff --git a/features/dashboard/new_project.feature b/features/dashboard/new_project.feature index 431dc4ccfcb..bbd82a85e3a 100644 --- a/features/dashboard/new_project.feature +++ b/features/dashboard/new_project.feature @@ -4,10 +4,27 @@ Background: Given I sign in as a user And I own project "Shop" And I visit dashboard page + And I click "New project" link @javascript Scenario: I should see New projects page - Given I click "New project" link Then I see "New project" page + Then I see all possible import optios + + @javascript + Scenario: I should see instructions on how to import from Git URL + Given I see "New project" page + When I click on "Any repo by URL" + Then I see instructions on how to import from Git URL + + @javascript + Scenario: I should see instructions on how to import from GitHub + Given I see "New project" page When I click on "Import project from GitHub" Then I see instructions on how to import from GitHub + + @javascript + Scenario: I should see Google Code import page + Given I see "New project" page + When I click on "Google Code" + Then I redirected to Google Code import page diff --git a/features/explore/projects.feature b/features/explore/projects.feature index a1b29722678..5d3870827f5 100644 --- a/features/explore/projects.feature +++ b/features/explore/projects.feature @@ -6,10 +6,12 @@ Feature: Explore Projects And private project "Enterprise" Scenario: I visit public area + Given archived project "Archive" When I visit the public projects area Then I should see project "Community" And I should not see project "Internal" And I should not see project "Enterprise" + And I should not see project "Archive" Scenario: I visit public project page When I visit project "Community" page @@ -37,11 +39,13 @@ Feature: Explore Projects And I should see empty public project details with ssh clone info Scenario: I visit public area as user - Given I sign in as a user + Given archived project "Archive" + And I sign in as a user When I visit the public projects area Then I should see project "Community" And I should see project "Internal" And I should not see project "Enterprise" + And I should not see project "Archive" Scenario: I visit internal project page as user Given I sign in as a user @@ -102,15 +106,20 @@ Feature: Explore Projects Then I should see list of merge requests for "Internal" project Scenario: Trending page - Given I sign in as a user + Given archived project "Archive" + And project "Archive" has comments + And I sign in as a user And project "Community" has comments When I visit the explore trending projects Then I should see project "Community" And I should not see project "Internal" And I should not see project "Enterprise" + And I should not see project "Archive" Scenario: Most starred page - Given I sign in as a user + Given archived project "Archive" + And I sign in as a user When I visit the explore starred projects Then I should see project "Community" And I should see project "Internal" + And I should see project "Archive" diff --git a/features/groups.feature b/features/groups.feature index 299e846edb0..d5272fdddcf 100644 --- a/features/groups.feature +++ b/features/groups.feature @@ -152,3 +152,10 @@ Feature: Groups And I click on one group milestone Then I should see group milestone with descriptions and expiry date And I should see group milestone with all issues and MRs assigned to that milestone + + # Group projects in settings + Scenario: I should see all projects in the project list in settings + Given Group "Owned" has archived project + When I visit group "Owned" projects page + Then I should see group "Owned" projects list + And I should see "archived" label diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature index c4b206edc95..3ebc8a39aae 100644 --- a/features/project/commits/commits.feature +++ b/features/project/commits/commits.feature @@ -41,6 +41,7 @@ Feature: Project Commits Scenario: I browse big commit Given I visit big commit page Then I see big commit warning + And I see "Reload with full diff" link Scenario: I browse a commit with an image Given I visit a commit with an image that changed diff --git a/features/search.feature b/features/search.feature index 1608e824671..a9234c1a611 100644 --- a/features/search.feature +++ b/features/search.feature @@ -23,6 +23,13 @@ Feature: Search Then I should see "Foo" link in the search results And I should not see "Bar" link in the search results + Scenario: I should see milestones I am looking for + And project has milestones + When I search for "Foo" + When I click "Milestones" link + Then I should see "Foo" link in the search results + And I should not see "Bar" link in the search results + Scenario: I should see project code I am looking for When I click project "Shop" link And I search for "rspec" @@ -44,6 +51,14 @@ Feature: Search Then I should see "Foo" link in the search results And I should not see "Bar" link in the search results + Scenario: I should see project milestones + And project has milestones + When I click project "Shop" link + And I search for "Foo" + And I click "Milestones" link + Then I should see "Foo" link in the search results + And I should not see "Bar" link in the search results + Scenario: I should see Wiki blobs And project has Wiki content When I click project "Shop" link diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb index 83a3f48abe3..d27634858a2 100644 --- a/features/steps/admin/groups.rb +++ b/features/steps/admin/groups.rb @@ -44,6 +44,14 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps click_button "Add users to group" end + When 'I select user "johndoe@gitlab.com" from user list as "Reporter"' do + select2('johndoe@gitlab.com', from: "#user_ids", multiple: true) + page.within "#new_project_member" do + select "Reporter", from: "access_level" + end + click_button "Add users to group" + end + step 'I should see "John Doe" in team list in every project as "Reporter"' do page.within ".group-users-list" do expect(page).to have_content "John Doe" @@ -51,6 +59,13 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps end end + step 'I should see "johndoe@gitlab.com" in team list in every project as "Reporter"' do + page.within ".group-users-list" do + expect(page).to have_content "johndoe@gitlab.com (invited)" + expect(page).to have_content "Reporter" + end + end + step 'I should be all groups' do Group.all.each do |group| expect(page).to have_content group.name diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb index 655f1895279..17233f89f38 100644 --- a/features/steps/admin/projects.rb +++ b/features/steps/admin/projects.rb @@ -2,6 +2,13 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps include SharedAuthentication include SharedPaths include SharedAdmin + include SharedProject + + step 'I should see all non-archived projects' do + Project.non_archived.each do |p| + expect(page).to have_content p.name_with_namespace + end + end step 'I should see all projects' do Project.all.each do |p| @@ -9,6 +16,15 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps end end + step 'I check "Show archived projects"' do + page.check 'Show archived projects' + click_button "Search" + end + + step 'I should see "archived" label' do + expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') + end + step 'I click on first project' do click_link Project.first.name_with_namespace end diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb index 147a4bd7486..7a6aec23af8 100644 --- a/features/steps/admin/settings.rb +++ b/features/steps/admin/settings.rb @@ -6,7 +6,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps step 'I modify settings and save form' do uncheck 'Gravatar enabled' - fill_in 'Home page url', with: 'https://about.gitlab.com/' + fill_in 'Home page URL', with: 'https://about.gitlab.com/' click_button 'Save' end diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb index d4440c1fb4d..1e09162a5b5 100644 --- a/features/steps/dashboard/new_project.rb +++ b/features/steps/dashboard/new_project.rb @@ -13,8 +13,17 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps expect(page).to have_content('Project path') end + step 'I see all possible import optios' do + expect(page).to have_link('GitHub') + expect(page).to have_link('Bitbucket') + expect(page).to have_link('GitLab.com') + expect(page).to have_link('Gitorious.org') + expect(page).to have_link('Google Code') + expect(page).to have_link('Any repo by URL') + end + step 'I click on "Import project from GitHub"' do - first('.how_to_import_link').click + first('.import_github').click end step 'I see instructions on how to import from GitHub' do @@ -26,4 +35,24 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps expect(element).not_to be_visible unless element == github_modal end end + + step 'I click on "Any repo by URL"' do + first('.import_git').click + end + + step 'I see instructions on how to import from Git URL' do + git_import_instructions = first('.js-toggle-content') + expect(git_import_instructions).to be_visible + expect(git_import_instructions).to have_content "Git repository URL" + expect(git_import_instructions).to have_content "The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL:" + end + + step 'I click on "Google Code"' do + first('.import_google_code').click + end + + step 'I redirected to Google Code import page' do + expect(current_path).to eq new_import_google_code_path + end + end diff --git a/features/steps/groups.rb b/features/steps/groups.rb index 46e1f4d0990..18a1c4d32ce 100644 --- a/features/steps/groups.rb +++ b/features/steps/groups.rb @@ -226,6 +226,15 @@ class Spinach::Features::Groups < Spinach::FeatureSteps expect(page).to have_link(@mr3.title, href: namespace_project_merge_request_path(@project3.namespace, @project3, @mr3)) end + step 'Group "Owned" has archived project' do + group = Group.find_by(name: 'Owned') + create(:project, namespace: group, archived: true, path: "archived-project") + end + + step 'I should see "archived" label' do + expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') + end + protected def assigned_to_me(key) diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb index e6330ec457e..a8532cc18d8 100644 --- a/features/steps/project/commits/commits.rb +++ b/features/steps/project/commits/commits.rb @@ -79,6 +79,12 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps expect(page).to have_content "Too many changes" end + step 'I see "Reload with full diff" link' do + link = find_link('Reload with full diff') + expect(link[:href]).to end_with('?force_show_diff=true') + expect(link[:href]).not_to include('.html') + end + step 'I visit a commit with an image that changed' do visit namespace_project_commit_path(@project.namespace, @project, sample_image_commit.id) end diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb index f2198f58c13..778dce06359 100644 --- a/features/steps/project/merge_requests.rb +++ b/features/steps/project/merge_requests.rb @@ -321,7 +321,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps end step 'I should see new target branch changes' do - expect(page).to have_content 'From fix into feature' + expect(page).to have_content 'Request to merge fix into feature' expect(page).to have_content 'Target branch changed from master to feature' end diff --git a/features/steps/search.rb b/features/steps/search.rb index 87893aa0205..79273cbad9a 100644 --- a/features/steps/search.rb +++ b/features/steps/search.rb @@ -41,6 +41,12 @@ class Spinach::Features::Search < Spinach::FeatureSteps end end + step 'I click "Milestones" link' do + page.within '.search-filter' do + click_link 'Milestones' + end + end + step 'I click "Wiki" link' do page.within '.search-filter' do click_link 'Wiki' @@ -72,6 +78,11 @@ class Spinach::Features::Search < Spinach::FeatureSteps create(:merge_request, :simple, title: "Bar", source_project: project, target_project: project) end + step 'project has milestones' do + create(:milestone, title: "Foo", project: project) + create(:milestone, title: "Bar", project: project) + end + step 'I should see "Foo" link in the search results' do page.within('.results') do find(:css, '.search-results').should have_link 'Foo' diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb index 72d873caa57..92e099315d8 100644 --- a/features/steps/shared/active_tab.rb +++ b/features/steps/shared/active_tab.rb @@ -26,7 +26,7 @@ module SharedActiveTab end step 'the active main tab should be Home' do - ensure_active_main_tab('Your Projects') + ensure_active_main_tab('Projects') end step 'the active main tab should be Projects' do diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb index bb0cd9ac105..b4deccb6520 100644 --- a/features/steps/shared/paths.rb +++ b/features/steps/shared/paths.rb @@ -39,6 +39,10 @@ module SharedPaths visit edit_group_path(Group.find_by(name: "Owned")) end + step 'I visit group "Owned" projects page' do + visit projects_group_path(Group.find_by(name: "Owned")) + end + step 'I visit group "Guest" page' do visit group_path(Group.find_by(name: "Guest")) end @@ -67,6 +71,10 @@ module SharedPaths visit dashboard_path end + step 'I visit dashboard activity page' do + visit activity_dashboard_path + end + step 'I visit dashboard projects page' do visit projects_dashboard_path end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb index 9ee2e5dfbed..ccbe8f96a4c 100644 --- a/features/steps/shared/project.rb +++ b/features/steps/shared/project.rb @@ -93,6 +93,29 @@ module SharedProject end # ---------------------------------------- + # Visibility of archived project + # ---------------------------------------- + + step 'archived project "Archive"' do + create :project, :public, archived: true, name: 'Archive' + end + + step 'I should not see project "Archive"' do + project = Project.find_by(name: "Archive") + expect(page).not_to have_content project.name_with_namespace + end + + step 'I should see project "Archive"' do + project = Project.find_by(name: "Archive") + expect(page).to have_content project.name_with_namespace + end + + step 'project "Archive" has comments' do + project = Project.find_by(name: "Archive") + 2.times { create(:note_on_issue, project: project) } + end + + # ---------------------------------------- # Visibility level # ---------------------------------------- diff --git a/features/support/env.rb b/features/support/env.rb index 672251af084..62c80b9c948 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -28,5 +28,9 @@ Spinach.hooks.before_run do RSpec::Mocks.setup TestEnv.init(mailer: false) + # skip pre-receive hook check so we can use + # web editor and merge + TestEnv.disable_pre_receive + include FactoryGirl::Syntax::Methods end diff --git a/lib/api/entities.rb b/lib/api/entities.rb index b5556682449..1f9dd6bc152 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -205,6 +205,9 @@ module API expose :attachment_identifier, as: :attachment expose :author, using: Entities::UserBasic expose :created_at + expose :system?, as: :system + expose :upvote?, as: :upvote + expose :downvote?, as: :downvote end class MRNote < Grape::Entity @@ -218,6 +221,7 @@ module API expose(:line) { |note| note.diff_new_line } expose(:line_type) { |note| note.diff_line_type } expose :author, using: Entities::UserBasic + expose :created_at end class Event < Grape::Entity diff --git a/lib/api/files.rb b/lib/api/files.rb index c7b30cf2f07..308c84dd135 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -67,7 +67,7 @@ module API file_path: blob.path, size: blob.size, encoding: "base64", - content: Base64.encode64(blob.data), + content: Base64.strict_encode64(blob.data), ref: ref, blob_id: blob.id, commit_id: commit.id, diff --git a/lib/backup/database.rb b/lib/backup/database.rb index bbb230a10f0..ce75476a09b 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -7,17 +7,20 @@ module Backup def initialize @config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env] @db_dir = File.join(Gitlab.config.backup.path, 'db') + end + + def dump FileUtils.rm_rf(@db_dir) # Ensure the parent dir of @db_dir exists FileUtils.mkdir_p(Gitlab.config.backup.path) # Fail if somebody raced to create @db_dir before us FileUtils.mkdir(@db_dir, mode: 0700) - end - def dump success = case config["adapter"] when /^mysql/ then $progress.print "Dumping MySQL database #{config['database']} ... " + # Workaround warnings from MySQL 5.6 about passwords on cmd line + ENV['MYSQL_PWD'] = config["password"].to_s if config["password"] system('mysqldump', *mysql_args, config['database'], out: db_file_name) when "postgresql" then $progress.print "Dumping PostgreSQL database #{config['database']} ... " @@ -43,6 +46,8 @@ module Backup success = case config["adapter"] when /^mysql/ then $progress.print "Restoring MySQL database #{config['database']} ... " + # Workaround warnings from MySQL 5.6 about passwords on cmd line + ENV['MYSQL_PWD'] = config["password"].to_s if config["password"] system('mysql', *mysql_args, config['database'], in: db_file_name) when "postgresql" then $progress.print "Restoring PostgreSQL database #{config['database']} ... " @@ -69,8 +74,7 @@ module Backup 'port' => '--port', 'socket' => '--socket', 'username' => '--user', - 'encoding' => '--default-character-set', - 'password' => '--password' + 'encoding' => '--default-character-set' } args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact end diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb index 42c93707caa..d8a7d29f1bf 100644 --- a/lib/gitlab/bitbucket_import/importer.rb +++ b/lib/gitlab/bitbucket_import/importer.rb @@ -5,7 +5,10 @@ module Gitlab def initialize(project) @project = project - @client = Client.new(project.creator.bitbucket_access_token, project.creator.bitbucket_access_token_secret) + import_data = project.import_data.try(:data) + bb_session = import_data["bb_session"] if import_data + @client = Client.new(bb_session["bitbucket_access_token"], + bb_session["bitbucket_access_token_secret"]) @formatter = Gitlab::ImportFormatter.new end @@ -16,12 +19,12 @@ module Gitlab #Issues && Comments issues = client.issues(project_identifier) - + issues["issues"].each do |issue| body = @formatter.author_line(issue["reported_by"]["username"], issue["content"]) - + comments = client.issue_comments(project_identifier, issue["local_id"]) - + if comments.any? body += @formatter.comments_header end @@ -31,13 +34,13 @@ module Gitlab end project.issues.create!( - description: body, + description: body, title: issue["title"], state: %w(resolved invalid duplicate wontfix).include?(issue["status"]) ? 'closed' : 'opened', author_id: gl_user_id(project, issue["reported_by"]["username"]) ) end - + true end diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb index 9931aa7e029..0b63f025d0a 100644 --- a/lib/gitlab/bitbucket_import/key_adder.rb +++ b/lib/gitlab/bitbucket_import/key_adder.rb @@ -3,14 +3,15 @@ module Gitlab class KeyAdder attr_reader :repo, :current_user, :client - def initialize(repo, current_user) + def initialize(repo, current_user, access_params) @repo, @current_user = repo, current_user - @client = Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret) + @client = Client.new(access_params[:bitbucket_access_token], + access_params[:bitbucket_access_token_secret]) end def execute return false unless BitbucketImport.public_key.present? - + project_identifier = "#{repo["owner"]}/#{repo["slug"]}" client.add_deploy_key(project_identifier, BitbucketImport.public_key) diff --git a/lib/gitlab/bitbucket_import/key_deleter.rb b/lib/gitlab/bitbucket_import/key_deleter.rb index 1a24a86fc37..f4dd393ad29 100644 --- a/lib/gitlab/bitbucket_import/key_deleter.rb +++ b/lib/gitlab/bitbucket_import/key_deleter.rb @@ -6,12 +6,15 @@ module Gitlab def initialize(project) @project = project @current_user = project.creator - @client = Client.new(current_user.bitbucket_access_token, current_user.bitbucket_access_token_secret) + import_data = project.import_data.try(:data) + bb_session = import_data["bb_session"] if import_data + @client = Client.new(bb_session["bitbucket_access_token"], + bb_session["bitbucket_access_token_secret"]) end def execute return false unless BitbucketImport.public_key.present? - + client.delete_deploy_key(project.import_source, BitbucketImport.public_key) true diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb index 54420e62c90..35e34d033e0 100644 --- a/lib/gitlab/bitbucket_import/project_creator.rb +++ b/lib/gitlab/bitbucket_import/project_creator.rb @@ -1,16 +1,17 @@ module Gitlab module BitbucketImport class ProjectCreator - attr_reader :repo, :namespace, :current_user + attr_reader :repo, :namespace, :current_user, :session_data - def initialize(repo, namespace, current_user) + def initialize(repo, namespace, current_user, session_data) @repo = repo @namespace = namespace @current_user = current_user + @session_data = session_data end def execute - ::Projects::CreateService.new(current_user, + project = ::Projects::CreateService.new(current_user, name: repo["name"], path: repo["slug"], description: repo["description"], @@ -18,8 +19,11 @@ module Gitlab visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, import_type: "bitbucket", import_source: "#{repo["owner"]}/#{repo["slug"]}", - import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git" + import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git", ).execute + + project.create_import_data(data: { "bb_session" => session_data } ) + project end end end diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb index 931d51c55d3..1a2a50a14d0 100644 --- a/lib/gitlab/current_settings.rb +++ b/lib/gitlab/current_settings.rb @@ -22,7 +22,8 @@ module Gitlab sign_in_text: Settings.extra['sign_in_text'], restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'], max_attachment_size: Settings.gitlab['max_attachment_size'], - session_expire_delay: Settings.gitlab['session_expire_delay'] + session_expire_delay: Settings.gitlab['session_expire_delay'], + import_sources: Settings.gitlab['import_sources'] ) end end diff --git a/lib/gitlab/email/attachment_uploader.rb b/lib/gitlab/email/attachment_uploader.rb new file mode 100644 index 00000000000..32cece8316b --- /dev/null +++ b/lib/gitlab/email/attachment_uploader.rb @@ -0,0 +1,35 @@ +module Gitlab + module Email + class AttachmentUploader + attr_accessor :message + + def initialize(message) + @message = message + end + + def execute(project) + attachments = [] + + message.attachments.each do |attachment| + tmp = Tempfile.new("gitlab-email-attachment") + begin + File.open(tmp.path, "w+b") { |f| f.write attachment.body.decoded } + + file = { + tempfile: tmp, + filename: attachment.filename, + content_type: attachment.content_type + } + + link = ::Projects::UploadService.new(project, file).execute + attachments << link if link + ensure + tmp.close! + end + end + + attachments + end + end + end +end diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb new file mode 100644 index 00000000000..355fbd27898 --- /dev/null +++ b/lib/gitlab/email/receiver.rb @@ -0,0 +1,106 @@ +# Inspired in great part by Discourse's Email::Receiver +module Gitlab + module Email + class Receiver + class ProcessingError < StandardError; end + class EmailUnparsableError < ProcessingError; end + class SentNotificationNotFoundError < ProcessingError; end + class EmptyEmailError < ProcessingError; end + class AutoGeneratedEmailError < ProcessingError; end + class UserNotFoundError < ProcessingError; end + class UserBlockedError < ProcessingError; end + class UserNotAuthorizedError < ProcessingError; end + class NoteableNotFoundError < ProcessingError; end + class InvalidNoteError < ProcessingError; end + + def initialize(raw) + @raw = raw + end + + def execute + raise EmptyEmailError if @raw.blank? + + raise SentNotificationNotFoundError unless sent_notification + + raise AutoGeneratedEmailError if message.header.to_s =~ /auto-(generated|replied)/ + + author = sent_notification.recipient + + raise UserNotFoundError unless author + + raise UserBlockedError if author.blocked? + + project = sent_notification.project + + raise UserNotAuthorizedError unless project && author.can?(:create_note, project) + + raise NoteableNotFoundError unless sent_notification.noteable + + reply = ReplyParser.new(message).execute.strip + + raise EmptyEmailError if reply.blank? + + reply = add_attachments(reply) + + note = create_note(reply) + + unless note.persisted? + message = "The comment could not be created for the following reasons:" + note.errors.full_messages.each do |error| + message << "\n\n- #{error}" + end + + raise InvalidNoteError, message + end + end + + private + + def message + @message ||= Mail::Message.new(@raw) + rescue Encoding::UndefinedConversionError, Encoding::InvalidByteSequenceError => e + raise EmailUnparsableError, e + end + + def reply_key + reply_key = nil + message.to.each do |address| + reply_key = Gitlab::ReplyByEmail.reply_key_from_address(address) + break if reply_key + end + + reply_key + end + + def sent_notification + return nil unless reply_key + + SentNotification.for(reply_key) + end + + def add_attachments(reply) + attachments = Email::AttachmentUploader.new(message).execute(sent_notification.project) + + attachments.each do |link| + text = "[#{link[:alt]}](#{link[:url]})" + text.prepend("!") if link[:is_image] + + reply << "\n\n#{text}" + end + + reply + end + + def create_note(reply) + Notes::CreateService.new( + sent_notification.project, + sent_notification.recipient, + note: reply, + noteable_type: sent_notification.noteable_type, + noteable_id: sent_notification.noteable_id, + commit_id: sent_notification.commit_id + ).execute + end + end + end +end diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb new file mode 100644 index 00000000000..6ed36b51f12 --- /dev/null +++ b/lib/gitlab/email/reply_parser.rb @@ -0,0 +1,79 @@ +# Inspired in great part by Discourse's Email::Receiver +module Gitlab + module Email + class ReplyParser + attr_accessor :message + + def initialize(message) + @message = message + end + + def execute + body = select_body(message) + + encoding = body.encoding + + body = discourse_email_trimmer(body) + + body = EmailReplyParser.parse_reply(body) + + body.force_encoding(encoding).encode("UTF-8") + end + + private + + def select_body(message) + text = message.text_part if message.multipart? + text ||= message if message.content_type !~ /text\/html/ + + return "" unless text + + text = fix_charset(text) + + # Certain trigger phrases that means we didn't parse correctly + if text =~ /(Content\-Type\:|multipart\/alternative|text\/plain)/ + return "" + end + + text + end + + # Force encoding to UTF-8 on a Mail::Message or Mail::Part + def fix_charset(object) + return nil if object.nil? + + if object.charset + object.body.decoded.force_encoding(object.charset.gsub(/utf8/i, "UTF-8")).encode("UTF-8").to_s + else + object.body.to_s + end + rescue + nil + end + + REPLYING_HEADER_LABELS = %w(From Sent To Subject Reply To Cc Bcc Date) + REPLYING_HEADER_REGEX = Regexp.union(REPLYING_HEADER_LABELS.map { |label| "#{label}:" }) + + def discourse_email_trimmer(body) + lines = body.scrub.lines.to_a + range_end = 0 + + lines.each_with_index do |l, idx| + # This one might be controversial but so many reply lines have years, times and end with a colon. + # Let's try it and see how well it works. + break if (l =~ /\d{4}/ && l =~ /\d:\d\d/ && l =~ /\:$/) || + (l =~ /On \w+ \d+,? \d+,?.*wrote:/) + + # Headers on subsequent lines + break if (0..2).all? { |off| lines[idx+off] =~ REPLYING_HEADER_REGEX } + # Headers on the same line + break if REPLYING_HEADER_LABELS.count { |label| l.include?(label) } >= 3 + + range_end = idx + end + + lines[0..range_end].join.strip + end + end + end +end diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb new file mode 100644 index 00000000000..dd393fe09d2 --- /dev/null +++ b/lib/gitlab/git/hook.rb @@ -0,0 +1,59 @@ +module Gitlab + module Git + class Hook + attr_reader :name, :repo_path, :path + + def initialize(name, repo_path) + @name = name + @repo_path = repo_path + @path = File.join(repo_path.strip, 'hooks', name) + end + + def exists? + File.exist?(path) + end + + def trigger(gl_id, oldrev, newrev, ref) + return true unless exists? + + changes = [oldrev, newrev, ref].join(" ") + + # function will return true if succesful + exit_status = false + + vars = { + 'GL_ID' => gl_id, + 'PWD' => repo_path + } + + options = { + chdir: repo_path + } + + Open3.popen2(vars, path, options) do |stdin, _, wait_thr| + exit_status = true + stdin.sync = true + + # in git, pre- and post- receive hooks may just exit without + # reading stdin. We catch the exception to avoid a broken pipe + # warning + begin + # inject all the changes as stdin to the hook + changes.lines do |line| + stdin.puts line + end + rescue Errno::EPIPE + end + + stdin.close + + unless wait_thr.value == 0 + exit_status = false + end + end + + exit_status + end + end + end +end diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb index 98039a76dcd..8c106a61735 100644 --- a/lib/gitlab/github_import/importer.rb +++ b/lib/gitlab/github_import/importer.rb @@ -5,7 +5,9 @@ module Gitlab def initialize(project) @project = project - @client = Client.new(project.creator.github_access_token) + import_data = project.import_data.try(:data) + github_session = import_data["github_session"] if import_data + @client = Client.new(github_session["github_access_token"]) @formatter = Gitlab::ImportFormatter.new end diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb index 2723eec933e..8c27ebd1ce8 100644 --- a/lib/gitlab/github_import/project_creator.rb +++ b/lib/gitlab/github_import/project_creator.rb @@ -1,16 +1,18 @@ module Gitlab module GithubImport class ProjectCreator - attr_reader :repo, :namespace, :current_user + attr_reader :repo, :namespace, :current_user, :session_data - def initialize(repo, namespace, current_user) + def initialize(repo, namespace, current_user, session_data) @repo = repo @namespace = namespace @current_user = current_user + @session_data = session_data end def execute - ::Projects::CreateService.new(current_user, + project = ::Projects::CreateService.new( + current_user, name: repo.name, path: repo.name, description: repo.description, @@ -18,8 +20,11 @@ module Gitlab visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC, import_type: "github", import_source: repo.full_name, - import_url: repo.clone_url.sub("https://", "https://#{current_user.github_access_token}@") + import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@") ).execute + + project.create_import_data(data: { "github_session" => session_data } ) + project end end end diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb index c5304a0699b..50594d2b24f 100644 --- a/lib/gitlab/gitlab_import/importer.rb +++ b/lib/gitlab/gitlab_import/importer.rb @@ -5,7 +5,9 @@ module Gitlab def initialize(project) @project = project - @client = Client.new(project.creator.gitlab_access_token) + import_data = project.import_data.try(:data) + gitlab_session = import_data["gitlab_session"] if import_data + @client = Client.new(gitlab_session["gitlab_access_token"]) @formatter = Gitlab::ImportFormatter.new end @@ -14,12 +16,12 @@ module Gitlab #Issues && Comments issues = client.issues(project_identifier) - + issues.each do |issue| body = @formatter.author_line(issue["author"]["name"], issue["description"]) - + comments = client.issue_comments(project_identifier, issue["id"]) - + if comments.any? body += @formatter.comments_header end @@ -29,13 +31,13 @@ module Gitlab end project.issues.create!( - description: body, + description: body, title: issue["title"], state: issue["state"], author_id: gl_user_id(project, issue["author"]["id"]) ) end - + true end diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb index f0d7141bf56..d9452de6a50 100644 --- a/lib/gitlab/gitlab_import/project_creator.rb +++ b/lib/gitlab/gitlab_import/project_creator.rb @@ -1,16 +1,17 @@ module Gitlab module GitlabImport class ProjectCreator - attr_reader :repo, :namespace, :current_user + attr_reader :repo, :namespace, :current_user, :session_data - def initialize(repo, namespace, current_user) + def initialize(repo, namespace, current_user, session_data) @repo = repo @namespace = namespace @current_user = current_user + @session_data = session_data end def execute - ::Projects::CreateService.new(current_user, + project = ::Projects::CreateService.new(current_user, name: repo["name"], path: repo["path"], description: repo["description"], @@ -18,8 +19,11 @@ module Gitlab visibility_level: repo["visibility_level"], import_type: "gitlab", import_source: repo["path_with_namespace"], - import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{current_user.gitlab_access_token}@") + import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{@session_data[:gitlab_access_token]}@") ).execute + + project.create_import_data(data: { "gitlab_session" => session_data } ) + project end end end diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb new file mode 100644 index 00000000000..991b70aab6a --- /dev/null +++ b/lib/gitlab/import_sources.rb @@ -0,0 +1,29 @@ +# Gitlab::ImportSources module +# +# Define import sources that can be used +# during the creation of new project +# +module Gitlab + module ImportSources + extend CurrentSettings + + class << self + def values + options.values + end + + def options + { + 'GitHub' => 'github', + 'Bitbucket' => 'bitbucket', + 'GitLab.com' => 'gitlab', + 'Gitorious.org' => 'gitorious', + 'Google Code' => 'google_code', + 'Any repo by URL' => 'git', + } + end + + end + + end +end diff --git a/lib/gitlab/inline_diff.rb b/lib/gitlab/inline_diff.rb index 3517ecdf5cf..99e7b529ba9 100644 --- a/lib/gitlab/inline_diff.rb +++ b/lib/gitlab/inline_diff.rb @@ -46,8 +46,11 @@ module Gitlab end last_the_same_symbols += 1 last_token = first_line[last_the_same_symbols..-1] - diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, FINISH + last_token) - diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, FINISH + last_token) + # This is tricky: escape backslashes so that `sub` doesn't interpret them + # as backreferences. Regexp.escape does NOT do the right thing. + replace_token = FINISH + last_token.gsub(/\\/, '\&\&') + diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, replace_token) + diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, replace_token) end diff_arr end diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb index 889decc9b48..9f6e19a09fd 100644 --- a/lib/gitlab/markdown.rb +++ b/lib/gitlab/markdown.rb @@ -25,21 +25,11 @@ module Gitlab # Public: Parse the provided text with GitLab-Flavored Markdown # # text - the source text - # options - options - # html_options - extra options for the reference links as given to link_to - def gfm(text, options = {}, html_options = {}) - gfm_with_options(text, options, html_options) - end - - # Public: Parse the provided text with GitLab-Flavored Markdown - # - # text - the source text # options - A Hash of options used to customize output (default: {}): # :xhtml - output XHTML instead of HTML # :reference_only_path - Use relative path for reference links - # project - the project # html_options - extra options for the reference links as given to link_to - def gfm_with_options(text, options = {}, html_options = {}) + def gfm(text, options = {}, html_options = {}) return text if text.nil? # Duplicate the string so we don't alter the original, then call to_str diff --git a/lib/gitlab/markdown/autolink_filter.rb b/lib/gitlab/markdown/autolink_filter.rb index 4e14a048cfb..541f1d88ffc 100644 --- a/lib/gitlab/markdown/autolink_filter.rb +++ b/lib/gitlab/markdown/autolink_filter.rb @@ -87,8 +87,14 @@ module Gitlab def autolink_filter(text) text.gsub(LINK_PATTERN) do |match| + # Remove any trailing HTML entities and store them for appending + # outside the link element. The entity must be marked HTML safe in + # order to be output literally rather than escaped. + match.gsub!(/((?:&[\w#]+;)+)\z/, '') + dropped = ($1 || '').html_safe + options = link_options.merge(href: match) - content_tag(:a, match, options) + content_tag(:a, match, options) + dropped end end diff --git a/lib/gitlab/markup_helper.rb b/lib/gitlab/markup_helper.rb index b1991e2e285..a5f767b134d 100644 --- a/lib/gitlab/markup_helper.rb +++ b/lib/gitlab/markup_helper.rb @@ -21,7 +21,7 @@ module Gitlab # # Returns boolean def gitlab_markdown?(filename) - filename.downcase.end_with?(*%w(.mdown .md .markdown)) + filename.downcase.end_with?(*%w(.mdown .mkd .mkdn .md .markdown)) end # Public: Determines if the given filename has AsciiDoc extension. diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb index 0f16c925900..9b8e783d16c 100644 --- a/lib/gitlab/o_auth/auth_hash.rb +++ b/lib/gitlab/o_auth/auth_hash.rb @@ -9,49 +9,63 @@ module Gitlab end def uid - Gitlab::Utils.force_utf8(auth_hash.uid.to_s) + @uid ||= Gitlab::Utils.force_utf8(auth_hash.uid.to_s) end def provider - Gitlab::Utils.force_utf8(auth_hash.provider.to_s) + @provider ||= Gitlab::Utils.force_utf8(auth_hash.provider.to_s) end def info auth_hash.info end - def name - Gitlab::Utils.force_utf8((info.try(:name) || full_name).to_s) + def get_info(key) + value = info.try(key) + Gitlab::Utils.force_utf8(value) if value + value end - def full_name - Gitlab::Utils.force_utf8("#{info.first_name} #{info.last_name}") + def name + @name ||= get_info(:name) || "#{get_info(:first_name)} #{get_info(:last_name)}" end def username - Gitlab::Utils.force_utf8( - (info.try(:nickname) || generate_username).to_s - ) + @username ||= username_and_email[:username].to_s end def email - Gitlab::Utils.force_utf8( - (info.try(:email) || generate_temporarily_email).downcase - ) + @email ||= username_and_email[:email].to_s end def password - devise_friendly_token = Devise.friendly_token[0, 8].downcase - @password ||= Gitlab::Utils.force_utf8(devise_friendly_token) + @password ||= Gitlab::Utils.force_utf8(Devise.friendly_token[0, 8].downcase) + end + + private + + def username_and_email + @username_and_email ||= begin + username = get_info(:nickname) || get_info(:username) + email = get_info(:email) + + username ||= generate_username(email) if email + email ||= generate_temporarily_email(username) if username + + { + username: username, + email: email + } + end end # Get the first part of the email address (before @) # In addtion in removes illegal characters - def generate_username + def generate_username(email) email.match(/^[^@]*/)[0].parameterize end - def generate_temporarily_email + def generate_temporarily_email(username) "temp-email-for-oauth-#{username}@gitlab.localhost" end end diff --git a/lib/gitlab/reply_by_email.rb b/lib/gitlab/reply_by_email.rb new file mode 100644 index 00000000000..c3fe6778f06 --- /dev/null +++ b/lib/gitlab/reply_by_email.rb @@ -0,0 +1,49 @@ +module Gitlab + module ReplyByEmail + class << self + def enabled? + config.enabled && address_formatted_correctly? + end + + def address_formatted_correctly? + config.address && + config.address.include?("%{reply_key}") + end + + def reply_key + return nil unless enabled? + + SecureRandom.hex(16) + end + + def reply_address(reply_key) + config.address.gsub('%{reply_key}', reply_key) + end + + def reply_key_from_address(address) + regex = address_regex + return unless regex + + match = address.match(regex) + return unless match + + match[1] + end + + private + + def config + Gitlab.config.reply_by_email + end + + def address_regex + wildcard_address = config.address + return nil unless wildcard_address + + regex = Regexp.escape(wildcard_address) + regex = regex.gsub(Regexp.escape('%{reply_key}'), "(.+)") + Regexp.new(regex).freeze + end + end + end +end diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb index 06245374bc8..2ab2d4af797 100644 --- a/lib/gitlab/search_results.rb +++ b/lib/gitlab/search_results.rb @@ -19,13 +19,15 @@ module Gitlab issues.page(page).per(per_page) when 'merge_requests' merge_requests.page(page).per(per_page) + when 'milestones' + milestones.page(page).per(per_page) else Kaminari.paginate_array([]).page(page).per(per_page) end end def total_count - @total_count ||= projects_count + issues_count + merge_requests_count + @total_count ||= projects_count + issues_count + merge_requests_count + milestones_count end def projects_count @@ -40,6 +42,10 @@ module Gitlab @merge_requests_count ||= merge_requests.count end + def milestones_count + @milestones_count ||= milestones.count + end + def empty? total_count.zero? end @@ -60,6 +66,12 @@ module Gitlab issues.order('updated_at DESC') end + def milestones + milestones = Milestone.where(project_id: limit_project_ids) + milestones = milestones.search(query) + milestones.order('updated_at DESC') + end + def merge_requests merge_requests = MergeRequest.in_projects(limit_project_ids) if query =~ /[#!](\d+)\z/ diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 04440e4f68d..f57b56cbdf0 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -41,6 +41,6 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML end def postprocess(full_document) - h.gfm_with_options(full_document, @options) + h.gfm(full_document, @options) end end diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb index 3f92212243d..6762ca47c32 100644 --- a/lib/rouge/formatters/html_gitlab.rb +++ b/lib/rouge/formatters/html_gitlab.rb @@ -93,16 +93,27 @@ module Rouge end def process_tokens(tokens) - num_lines = 0 - last_val = '' - rendered = '' + rendered = [] + current_line = '' tokens.each do |tok, val| - last_val = val - num_lines += val.scan(/\n/).size - rendered << span(tok, val) + # In the case of multi-line values (e.g. comments), we need to apply + # styling to each line since span elements are inline. + val.lines.each do |line| + stripped = line.chomp + current_line << span(tok, stripped) + + if line.end_with?("\n") + rendered << current_line + current_line = '' + end + end end + # Add leftover text + rendered << current_line if current_line.present? + + num_lines = rendered.size numbers = (@linenostart..num_lines + @linenostart - 1).to_a { numbers: numbers, code: rendered } @@ -117,9 +128,8 @@ module Rouge numbers.join("\n") end - def wrap_lines(rendered) + def wrap_lines(lines) if @lineanchors - lines = rendered.split("\n") lines = lines.each_with_index.map do |line, index| number = index + @linenostart @@ -136,24 +146,17 @@ module Rouge lines.join("\n") else if @linenos == 'inline' - lines = rendered.split("\n") lines = lines.each_with_index.map do |line, index| number = index + @linenostart "<span class=\"linenos\">#{number}</span>#{line}" end lines.join("\n") else - rendered + lines.join("\n") end end end - def wrap_values(val, element) - lines = val.split("\n") - lines = lines.map{ |x| "<span #{element}>#{x}</span>" } - lines.join("\n") - end - def span(tok, val) # http://stackoverflow.com/a/1600584/2587286 val = CGI.escapeHTML(val) @@ -161,13 +164,11 @@ module Rouge if tok.shortname.empty? val else - # In the case of multi-line values (e.g. comments), we need to apply - # styling to each line since span elements are inline. if @inline_theme rules = @inline_theme.style_for(tok).rendered_rules - wrap_values(val, "style=\"#{rules.to_a.join(';')}\"") + "<span style=\"#{rules.to_a.join(';')}\"#{val}</span>" else - wrap_values(val, "class=\"#{tok.shortname}\"") + "<span class=\"#{tok.shortname}\">#{val}</span>" end end end diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab index a3455728a94..457bd31e23b 100755 --- a/lib/support/init.d/gitlab +++ b/lib/support/init.d/gitlab @@ -35,6 +35,8 @@ pid_path="$app_root/tmp/pids" socket_path="$app_root/tmp/sockets" web_server_pid_path="$pid_path/unicorn.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" +mail_room_enabled=false +mail_room_pid_path="$pid_path/mail_room.pid" shell_path="/bin/bash" # Read configuration variable file if it is present @@ -70,13 +72,20 @@ check_pids(){ else spid=0 fi + if [ "$mail_room_enabled" = true ]; then + if [ -f "$mail_room_pid_path" ]; then + mpid=$(cat "$mail_room_pid_path") + else + mpid=0 + fi + fi } ## Called when we have started the two processes and are waiting for their pid files. wait_for_pids(){ # We are sleeping a bit here mostly because sidekiq is slow at writing it's pid i=0; - while [ ! -f $web_server_pid_path -o ! -f $sidekiq_pid_path ]; do + while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do sleep 0.1; i=$((i+1)) if [ $((i%10)) = 0 ]; then @@ -111,7 +120,15 @@ check_status(){ else sidekiq_status="-1" fi - if [ $web_status = 0 -a $sidekiq_status = 0 ]; then + if [ "$mail_room_enabled" = true ]; then + if [ $mpid -ne 0 ]; then + kill -0 "$mpid" 2>/dev/null + mail_room_status="$?" + else + mail_room_status="-1" + fi + fi + if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then gitlab_status=0 else # http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html @@ -125,26 +142,33 @@ check_stale_pids(){ check_status # If there is a pid it is something else than 0, the service is running if # *_status is == 0. - if [ "$wpid" != "0" -a "$web_status" != "0" ]; then + if [ "$wpid" != "0" ] && [ "$web_status" != "0" ]; then echo "Removing stale Unicorn web server pid. This is most likely caused by the web server crashing the last time it ran." if ! rm "$web_server_pid_path"; then echo "Unable to remove stale pid, exiting." exit 1 fi fi - if [ "$spid" != "0" -a "$sidekiq_status" != "0" ]; then + if [ "$spid" != "0" ] && [ "$sidekiq_status" != "0" ]; then echo "Removing stale Sidekiq job dispatcher pid. This is most likely caused by Sidekiq crashing the last time it ran." if ! rm "$sidekiq_pid_path"; then echo "Unable to remove stale pid, exiting" exit 1 fi fi + if [ "$mail_room_enabled" = true ] && [ "$mpid" != "0" ] && [ "$mail_room_status" != "0" ]; then + echo "Removing stale MailRoom job dispatcher pid. This is most likely caused by MailRoom crashing the last time it ran." + if ! rm "$mail_room_pid_path"; then + echo "Unable to remove stale pid, exiting" + exit 1 + fi + fi } ## If no parts of the service is running, bail out. exit_if_not_running(){ check_stale_pids - if [ "$web_status" != "0" -a "$sidekiq_status" != "0" ]; then + if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then echo "GitLab is not running." exit fi @@ -154,12 +178,14 @@ exit_if_not_running(){ start_gitlab() { check_stale_pids - if [ "$web_status" != "0" -a "$sidekiq_status" != "0" ]; then - echo -n "Starting both the GitLab Unicorn and Sidekiq" - elif [ "$web_status" != "0" ]; then - echo -n "Starting GitLab Unicorn" - elif [ "$sidekiq_status" != "0" ]; then - echo -n "Starting GitLab Sidekiq" + if [ "$web_status" != "0" ]; then + echo "Starting GitLab Unicorn" + fi + if [ "$sidekiq_status" != "0" ]; then + echo "Starting GitLab Sidekiq" + fi + if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then + echo "Starting GitLab MailRoom" fi # Then check if the service is running. If it is: don't start again. @@ -179,22 +205,33 @@ start_gitlab() { RAILS_ENV=$RAILS_ENV bin/background_jobs start & fi + if [ "$mail_room_enabled" = true ]; then + # If MailRoom is already running, don't start it again. + if [ "$mail_room_status" = "0" ]; then + echo "The MailRoom email processor is already running with pid $mpid, not restarting" + else + RAILS_ENV=$RAILS_ENV bin/mail_room start & + fi + fi + # Wait for the pids to be planted wait_for_pids # Finally check the status to tell wether or not GitLab is running print_status } -## Asks the Unicorn and the Sidekiq if they would be so kind as to stop, if not kills them. +## Asks Unicorn, Sidekiq and MailRoom if they would be so kind as to stop, if not kills them. stop_gitlab() { exit_if_not_running - if [ "$web_status" = "0" -a "$sidekiq_status" = "0" ]; then - echo -n "Shutting down both Unicorn and Sidekiq" - elif [ "$web_status" = "0" ]; then - echo -n "Shutting down Unicorn" - elif [ "$sidekiq_status" = "0" ]; then - echo -n "Shutting down Sidekiq" + if [ "$web_status" = "0" ]; then + echo "Shutting down GitLab Unicorn" + fi + if [ "$sidekiq_status" = "0" ]; then + echo "Shutting down GitLab Sidekiq" + fi + if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; then + echo "Shutting down GitLab MailRoom" fi # If the Unicorn web server is running, tell it to stop; @@ -205,13 +242,17 @@ stop_gitlab() { if [ "$sidekiq_status" = "0" ]; then RAILS_ENV=$RAILS_ENV bin/background_jobs stop fi + # And do the same thing for the MailRoom. + if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; then + RAILS_ENV=$RAILS_ENV bin/mail_room stop + fi # If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script. - while [ "$web_status" = "0" -o "$sidekiq_status" = "0" ]; do + while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do sleep 1 check_status printf "." - if [ "$web_status" != "0" -a "$sidekiq_status" != "0" ]; then + if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then printf "\n" break fi @@ -220,7 +261,10 @@ stop_gitlab() { sleep 1 # Cleaning up unused pids rm "$web_server_pid_path" 2>/dev/null - # rm "$sidekiq_pid_path" # Sidekiq seems to be cleaning up it's own pid. + # rm "$sidekiq_pid_path" 2>/dev/null # Sidekiq seems to be cleaning up it's own pid. + if [ "$mail_room_enabled" = true ]; then + rm "$mail_room_pid_path" 2>/dev/null + fi print_status } @@ -228,7 +272,7 @@ stop_gitlab() { ## Prints the status of GitLab and it's components. print_status() { check_status - if [ "$web_status" != "0" -a "$sidekiq_status" != "0" ]; then + if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then echo "GitLab is not running." return fi @@ -242,7 +286,14 @@ print_status() { else printf "The GitLab Sidekiq job dispatcher is \033[31mnot running\033[0m.\n" fi - if [ "$web_status" = "0" -a "$sidekiq_status" = "0" ]; then + if [ "$mail_room_enabled" = true ]; then + if [ "$mail_room_status" = "0" ]; then + echo "The GitLab MailRoom email processor with pid $mpid is running." + else + printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n" + fi + fi + if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then printf "GitLab and all its components are \033[32mup and running\033[0m.\n" fi } @@ -257,9 +308,15 @@ reload_gitlab(){ printf "Reloading GitLab Unicorn configuration... " RAILS_ENV=$RAILS_ENV bin/web reload echo "Done." + echo "Restarting GitLab Sidekiq since it isn't capable of reloading its config..." RAILS_ENV=$RAILS_ENV bin/background_jobs restart + if [ "$mail_room_enabled" != true ]; then + echo "Restarting GitLab MailRoom since it isn't capable of reloading its config..." + RAILS_ENV=$RAILS_ENV bin/mail_room restart + fi + wait_for_pids print_status } @@ -267,7 +324,7 @@ reload_gitlab(){ ## Restarts Sidekiq and Unicorn. restart_gitlab(){ check_status - if [ "$web_status" = "0" -o "$sidekiq_status" = "0" ]; then + if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then stop_gitlab fi start_gitlab diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example index cf7f4198cbf..fd70cb7cc74 100755 --- a/lib/support/init.d/gitlab.default.example +++ b/lib/support/init.d/gitlab.default.example @@ -30,6 +30,15 @@ web_server_pid_path="$pid_path/unicorn.pid" # The default is "$pid_path/sidekiq.pid" sidekiq_pid_path="$pid_path/sidekiq.pid" +# mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled. +# This is required for the Reply by email feature. +# The default is "false" +mail_room_enabled=false + +# mail_room_pid_path defines the path in which to create the pid file for mail_room +# The default is "$pid_path/mail_room.pid" +mail_room_pid_path="$pid_path/mail_room.pid" + # shell_path defines the path of shell for "$app_user" in case you are using # shell other than "bash" # The default is "/bin/bash" diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index 8acb6a7fd19..2b9688c1b40 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -2,6 +2,7 @@ namespace :gitlab do desc "GitLab | Check the configuration of GitLab and its environment" task check: %w{gitlab:gitlab_shell:check gitlab:sidekiq:check + gitlab:reply_by_email:check gitlab:ldap:check gitlab:app:check} @@ -22,6 +23,7 @@ namespace :gitlab do check_gitlab_config_not_outdated check_log_writable check_tmp_writable + check_uploads check_init_script_exists check_init_script_up_to_date check_projects_have_namespace @@ -276,6 +278,57 @@ namespace :gitlab do fix_and_rerun end end + + def check_uploads + print "Uploads directory setup correctly? ... " + + unless File.directory?(Rails.root.join('public/uploads')) + puts "no".red + try_fixing_it( + "sudo -u #{gitlab_user} mkdir -m 750 #{Rails.root}/public/uploads" + ) + for_more_information( + see_installation_guide_section "GitLab" + ) + fix_and_rerun + return + end + + upload_path = File.realpath(Rails.root.join('public/uploads')) + upload_path_tmp = File.join(upload_path, 'tmp') + + if File.stat(upload_path).mode == 040750 + unless Dir.exists?(upload_path_tmp) + puts 'skipped (no tmp uploads folder yet)'.magenta + return + end + + # if tmp upload dir has incorrect permissions, assume others do as well + if File.stat(upload_path_tmp).mode == 040755 && File.owned?(upload_path_tmp) # verify drwxr-xr-x permissions + puts "yes".green + else + puts "no".red + try_fixing_it( + "sudo chown -R #{gitlab_user} #{upload_path}", + "sudo find #{upload_path} -type f -exec chmod 0644 {} \\;", + "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0755 {} \\;" + ) + for_more_information( + see_installation_guide_section "GitLab" + ) + fix_and_rerun + end + else + puts "no".red + try_fixing_it( + "sudo chmod 0750 #{upload_path}", + ) + for_more_information( + see_installation_guide_section "GitLab" + ) + fix_and_rerun + end + end def check_redis_version print "Redis version >= 2.0.0? ... " @@ -577,6 +630,174 @@ namespace :gitlab do end end + + namespace :reply_by_email do + desc "GitLab | Check the configuration of Reply by email" + task check: :environment do + warn_user_is_not_gitlab + start_checking "Reply by email" + + if Gitlab.config.reply_by_email.enabled + check_address_formatted_correctly + check_mail_room_config_exists + check_imap_authentication + + if Rails.env.production? + check_initd_configured_correctly + check_mail_room_running + else + check_foreman_configured_correctly + end + else + puts 'Reply by email is disabled in config/gitlab.yml' + end + + finished_checking "Reply by email" + end + + + # Checks + ######################## + + def check_address_formatted_correctly + print "Address formatted correctly? ... " + + if Gitlab::ReplyByEmail.address_formatted_correctly? + puts "yes".green + else + puts "no".red + try_fixing_it( + "Make sure that the address in config/gitlab.yml includes the '%{reply_key}' placeholder." + ) + fix_and_rerun + end + end + + def check_initd_configured_correctly + print "Init.d configured correctly? ... " + + path = "/etc/default/gitlab" + + if File.exist?(path) && File.read(path).include?("mail_room_enabled=true") + puts "yes".green + else + puts "no".red + try_fixing_it( + "Enable mail_room in the init.d configuration." + ) + for_more_information( + "doc/reply_by_email/README.md" + ) + fix_and_rerun + end + end + + def check_foreman_configured_correctly + print "Foreman configured correctly? ... " + + path = Rails.root.join("Procfile") + + if File.exist?(path) && File.read(path) =~ /^mail_room:/ + puts "yes".green + else + puts "no".red + try_fixing_it( + "Enable mail_room in your Procfile." + ) + for_more_information( + "doc/reply_by_email/README.md" + ) + fix_and_rerun + end + end + + def check_mail_room_running + print "MailRoom running? ... " + + path = "/etc/default/gitlab" + + unless File.exist?(path) && File.read(path).include?("mail_room_enabled=true") + puts "can't check because of previous errors".magenta + return + end + + if mail_room_running? + puts "yes".green + else + puts "no".red + try_fixing_it( + sudo_gitlab("RAILS_ENV=production bin/mail_room start") + ) + for_more_information( + see_installation_guide_section("Install Init Script"), + "see log/mail_room.log for possible errors" + ) + fix_and_rerun + end + end + + def check_mail_room_config_exists + print "MailRoom config exists? ... " + + mail_room_config_file = Rails.root.join("config", "mail_room.yml") + + if File.exists?(mail_room_config_file) + puts "yes".green + else + puts "no".red + try_fixing_it( + "Copy config/mail_room.yml.example to config/mail_room.yml", + "Check that the information in config/mail_room.yml is correct" + ) + for_more_information( + "doc/reply_by_email/README.md" + ) + fix_and_rerun + end + end + + def check_imap_authentication + print "IMAP server credentials are correct? ... " + + mail_room_config_file = Rails.root.join("config", "mail_room.yml") + + unless File.exists?(mail_room_config_file) + puts "can't check because of previous errors".magenta + return + end + + config = YAML.load_file(mail_room_config_file)[:mailboxes].first rescue nil + + if config + begin + imap = Net::IMAP.new(config[:host], port: config[:port], ssl: config[:ssl]) + imap.login(config[:email], config[:password]) + connected = true + rescue + connected = false + end + end + + if connected + puts "yes".green + else + puts "no".red + try_fixing_it( + "Check that the information in config/mail_room.yml is correct" + ) + for_more_information( + "doc/reply_by_email/README.md" + ) + fix_and_rerun + end + end + + def mail_room_running? + ps_ux, _ = Gitlab::Popen.popen(%W(ps ux)) + ps_ux.include?("mail_room") + end + end + namespace :ldap do task :check, [:limit] => :environment do |t, args| # Only show up to 100 results because LDAP directories can be very big. @@ -693,7 +914,7 @@ namespace :gitlab do end def check_ruby_version - required_version = Gitlab::VersionInfo.new(2, 0, 0) + required_version = Gitlab::VersionInfo.new(2, 1, 0) current_version = Gitlab::VersionInfo.parse(run(%W(ruby --version))) print "Ruby version >= #{required_version} ? ... " diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake index bf221f06d3b..d6883a563ee 100644 --- a/lib/tasks/gitlab/info.rake +++ b/lib/tasks/gitlab/info.rake @@ -30,8 +30,7 @@ namespace :gitlab do # check database adapter database_adapter = ActiveRecord::Base.connection.adapter_name.downcase - project = Project.new(path: "some-project") - project.path = "some-project" + project = Group.new(path: "some-group").projects.build(path: "some-project") # construct clone URLs http_clone_url = project.http_url_to_repo ssh_clone_url = project.ssh_url_to_repo diff --git a/public/robots.txt b/public/robots.txt index 085187fa58b..528f421083e 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -1,5 +1,66 @@ -# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file # # To ban all spiders from the entire site uncomment the next two lines: # User-Agent: * # Disallow: / + +User-Agent: * + +# Add a 1 second delay between successive requests to the same server, limits resources used by crawler +# Only some crawlers respect this setting, e.g. Googlebot does not +# Crawl-delay: 1 + +# Based on details in https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/routes.rb, https://gitlab.com/gitlab-org/gitlab-ce/blob/master/spec/routing, and using application +Disallow: /autocomplete/users +Disallow: /search +Disallow: /api +Disallow: /admin +Disallow: /profile +Disallow: /dashboard +Disallow: /projects/new +Disallow: /groups/new +Disallow: /groups/*/edit +Disallow: /users + +# Global snippets +Disallow: /s +Disallow: /snippets/new +Disallow: /snippets/*/edit +Disallow: /snippets/*/raw + +# Project details +Disallow: /*/*.git +Disallow: /*/*/fork/new +Disallow: /*/*/repository/archive* +Disallow: /*/*/activity +Disallow: /*/*/new +Disallow: /*/*/edit +Disallow: /*/*/raw +Disallow: /*/*/blame +Disallow: /*/*/commits/*/* +Disallow: /*/*/commit +Disallow: /*/*/compare +Disallow: /*/*/branches/new +Disallow: /*/*/tags/new +Disallow: /*/*/network +Disallow: /*/*/graphs +Disallow: /*/*/milestones/new +Disallow: /*/*/milestones/*/edit +Disallow: /*/*/issues/new +Disallow: /*/*/issues/*/edit +Disallow: /*/*/merge_requests/new +Disallow: /*/*/merge_requests/*.patch +Disallow: /*/*/merge_requests/*.diff +Disallow: /*/*/merge_requests/*/edit +Disallow: /*/*/merge_requests/*/diffs +Disallow: /*/*/project_members/import +Disallow: /*/*/labels/new +Disallow: /*/*/labels/*/edit +Disallow: /*/*/wikis/*/edit +Disallow: /*/*/snippets/new +Disallow: /*/*/snippets/*/edit +Disallow: /*/*/snippets/*/raw +Disallow: /*/*/deploy_keys +Disallow: /*/*/hooks +Disallow: /*/*/services +Disallow: /*/*/protected_branches diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 3521d690259..aa8d6cb807f 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -74,7 +74,7 @@ describe AutocompleteController do describe 'GET #users with project ID' do before do - get(:users, project_id: project.id) + get(:users, project_id: project.id, current_user: true) end it { expect(body).to be_kind_of(Array) } diff --git a/spec/controllers/blame_controller_spec.rb b/spec/controllers/blame_controller_spec.rb new file mode 100644 index 00000000000..3ad4d5fc0a8 --- /dev/null +++ b/spec/controllers/blame_controller_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Projects::BlameController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + sign_in(user) + + project.team << [user, :master] + controller.instance_variable_set(:@project, project) + end + + describe "GET show" do + render_views + + before do + get(:show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id) + end + + context "valid file" do + let(:id) { 'master/files/ruby/popen.rb' } + it { is_expected.to respond_with(:success) } + + it 'groups blames properly' do + blame = assigns(:blame) + # Sanity check a few items + expect(blame.count).to eq(18) + expect(blame[0][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e') + expect(blame[0][:lines]).to eq(["require 'fileutils'", "require 'open3'", ""]) + + expect(blame[1][:commit].sha).to eq('874797c3a73b60d2187ed6e2fcabd289ff75171e') + expect(blame[1][:lines]).to eq(["module Popen", " extend self"]) + + expect(blame[-1][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e') + expect(blame[-1][:lines]).to eq([" end", "end"]) + end + end + end +end diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 89e595121a7..81c03c9059b 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -4,7 +4,15 @@ require_relative 'import_spec_helper' describe Import::BitbucketController do include ImportSpecHelper - let(:user) { create(:user, bitbucket_access_token: 'asd123', bitbucket_access_token_secret: "sekret") } + let(:user) { create(:user) } + let(:token) { "asdasd12345" } + let(:secret) { "sekrettt" } + let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } } + + def assign_session_tokens + session[:bitbucket_access_token] = token + session[:bitbucket_access_token_secret] = secret + end before do sign_in(user) @@ -17,8 +25,6 @@ describe Import::BitbucketController do end it "updates access token" do - token = "asdasd12345" - secret = "sekrettt" access_token = double(token: token, secret: secret) allow_any_instance_of(Gitlab::BitbucketImport::Client). to receive(:get_token).and_return(access_token) @@ -26,8 +32,8 @@ describe Import::BitbucketController do get :callback - expect(user.reload.bitbucket_access_token).to eq(token) - expect(user.reload.bitbucket_access_token_secret).to eq(secret) + expect(session[:bitbucket_access_token]).to eq(token) + expect(session[:bitbucket_access_token_secret]).to eq(secret) expect(controller).to redirect_to(status_import_bitbucket_url) end end @@ -35,6 +41,7 @@ describe Import::BitbucketController do describe "GET status" do before do @repo = OpenStruct.new(slug: 'vim', owner: 'asd') + assign_session_tokens end it "assigns variables" do @@ -73,17 +80,18 @@ describe Import::BitbucketController do before do allow(Gitlab::BitbucketImport::KeyAdder). - to receive(:new).with(bitbucket_repo, user). + to receive(:new).with(bitbucket_repo, user, access_params). and_return(double(execute: true)) stub_client(user: bitbucket_user, project: bitbucket_repo) + assign_session_tokens end context "when the repository owner is the Bitbucket user" do context "when the Bitbucket user and GitLab user's usernames match" do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, user.namespace, user). + to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -95,7 +103,7 @@ describe Import::BitbucketController do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, user.namespace, user). + to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -116,7 +124,7 @@ describe Import::BitbucketController do context "when the namespace is owned by the GitLab user" do it "takes the existing namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, existing_namespace, user). + to receive(:new).with(bitbucket_repo, existing_namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -150,7 +158,7 @@ describe Import::BitbucketController do it "takes the new namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, an_instance_of(Group), user). + to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params). and_return(double(execute: true)) post :create, format: :js diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb index 0bc14059a35..766be578f7f 100644 --- a/spec/controllers/import/github_controller_spec.rb +++ b/spec/controllers/import/github_controller_spec.rb @@ -4,7 +4,13 @@ require_relative 'import_spec_helper' describe Import::GithubController do include ImportSpecHelper - let(:user) { create(:user, github_access_token: 'asd123') } + let(:user) { create(:user) } + let(:token) { "asdasd12345" } + let(:access_params) { { github_access_token: token } } + + def assign_session_token + session[:github_access_token] = token + end before do sign_in(user) @@ -20,7 +26,7 @@ describe Import::GithubController do get :callback - expect(user.reload.github_access_token).to eq(token) + expect(session[:github_access_token]).to eq(token) expect(controller).to redirect_to(status_import_github_url) end end @@ -30,6 +36,7 @@ describe Import::GithubController do @repo = OpenStruct.new(login: 'vim', full_name: 'asd/vim') @org = OpenStruct.new(login: 'company') @org_repo = OpenStruct.new(login: 'company', full_name: 'company/repo') + assign_session_token end it "assigns variables" do @@ -66,13 +73,14 @@ describe Import::GithubController do before do stub_client(user: github_user, repo: github_repo) + assign_session_token end context "when the repository owner is the GitHub user" do context "when the GitHub user and GitLab user's usernames match" do it "takes the current user's namespace" do expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, user.namespace, user). + to receive(:new).with(github_repo, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -84,7 +92,7 @@ describe Import::GithubController do it "takes the current user's namespace" do expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, user.namespace, user). + to receive(:new).with(github_repo, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -97,6 +105,7 @@ describe Import::GithubController do before do github_repo.owner = OpenStruct.new(login: other_username) + assign_session_token end context "when a namespace with the GitHub user's username already exists" do @@ -105,7 +114,7 @@ describe Import::GithubController do context "when the namespace is owned by the GitLab user" do it "takes the existing namespace" do expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, existing_namespace, user). + to receive(:new).with(github_repo, existing_namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -139,7 +148,7 @@ describe Import::GithubController do it "takes the new namespace" do expect(Gitlab::GithubImport::ProjectCreator). - to receive(:new).with(github_repo, an_instance_of(Group), user). + to receive(:new).with(github_repo, an_instance_of(Group), user, access_params). and_return(double(execute: true)) post :create, format: :js diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb index 4bc67c86703..198d006af76 100644 --- a/spec/controllers/import/gitlab_controller_spec.rb +++ b/spec/controllers/import/gitlab_controller_spec.rb @@ -4,7 +4,13 @@ require_relative 'import_spec_helper' describe Import::GitlabController do include ImportSpecHelper - let(:user) { create(:user, gitlab_access_token: 'asd123') } + let(:user) { create(:user) } + let(:token) { "asdasd12345" } + let(:access_params) { { gitlab_access_token: token } } + + def assign_session_token + session[:gitlab_access_token] = token + end before do sign_in(user) @@ -13,14 +19,13 @@ describe Import::GitlabController do describe "GET callback" do it "updates access token" do - token = "asdasd12345" allow_any_instance_of(Gitlab::GitlabImport::Client). to receive(:get_token).and_return(token) stub_omniauth_provider('gitlab') get :callback - expect(user.reload.gitlab_access_token).to eq(token) + expect(session[:gitlab_access_token]).to eq(token) expect(controller).to redirect_to(status_import_gitlab_url) end end @@ -28,6 +33,7 @@ describe Import::GitlabController do describe "GET status" do before do @repo = OpenStruct.new(path: 'vim', path_with_namespace: 'asd/vim') + assign_session_token end it "assigns variables" do @@ -67,13 +73,14 @@ describe Import::GitlabController do before do stub_client(user: gitlab_user, project: gitlab_repo) + assign_session_token end context "when the repository owner is the GitLab.com user" do context "when the GitLab.com user and GitLab server user's usernames match" do it "takes the current user's namespace" do expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, user.namespace, user). + to receive(:new).with(gitlab_repo, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -85,7 +92,7 @@ describe Import::GitlabController do it "takes the current user's namespace" do expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, user.namespace, user). + to receive(:new).with(gitlab_repo, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -98,6 +105,7 @@ describe Import::GitlabController do before do gitlab_repo["namespace"]["path"] = other_username + assign_session_token end context "when a namespace with the GitLab.com user's username already exists" do @@ -106,7 +114,7 @@ describe Import::GitlabController do context "when the namespace is owned by the GitLab server user" do it "takes the existing namespace" do expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, existing_namespace, user). + to receive(:new).with(gitlab_repo, existing_namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -140,7 +148,7 @@ describe Import::GitlabController do it "takes the new namespace" do expect(Gitlab::GitlabImport::ProjectCreator). - to receive(:new).with(gitlab_repo, an_instance_of(Group), user). + to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params). and_return(double(execute: true)) post :create, format: :js diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb new file mode 100644 index 00000000000..871b9219ca9 --- /dev/null +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -0,0 +1,37 @@ +require('spec_helper') + +describe Projects::IssuesController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } + + before do + sign_in(user) + project.team << [user, :developer] + controller.instance_variable_set(:@project, project) + end + + describe "GET #index" do + it "returns index" do + get :index, namespace_id: project.namespace.id, project_id: project.id + + expect(response.status).to eq(200) + end + + it "returns 404 when issues are disabled" do + project.issues_enabled = false + project.save + + get :index, namespace_id: project.namespace.id, project_id: project.id + expect(response.status).to eq(404) + end + + it "returns 404 when external issue tracker is enabled" do + allow(project).to receive(:default_issues_tracker?).and_return(false) + + get :index, namespace_id: project.namespace.id, project_id: project.id + expect(response.status).to eq(404) + end + + end +end diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb new file mode 100644 index 00000000000..d4ecd98e12d --- /dev/null +++ b/spec/controllers/projects/services_controller_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Projects::ServicesController do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:service) { create(:service, project: project) } + + before do + sign_in(user) + project.team << [user, :master] + controller.instance_variable_set(:@project, project) + controller.instance_variable_set(:@service, service) + request.env["HTTP_REFERER"] = "/" + end + + describe "#test" do + context 'success' do + it "should redirect and show success message" do + expect(service).to receive(:test).and_return({ success: true, result: 'done' }) + get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + expect(response.status).to redirect_to('/') + expect(flash[:notice]).to eq('We sent a request to the provided URL') + end + end + + context 'failure' do + it "should redirect and show failure message" do + expect(service).to receive(:test).and_return({ success: false, result: 'Bad test' }) + get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html + expect(response.status).to redirect_to('/') + expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test') + end + end + end +end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 859a62f740f..3da4dfc2b23 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -15,7 +15,7 @@ require 'erb' # -> `markdown` helper # -> Redcarpet::Render::GitlabHTML converts Markdown to HTML # -> Post-process HTML -# -> `gfm_with_options` helper +# -> `gfm` helper # -> HTML::Pipeline # -> SanitizationFilter # -> Other filters, depending on pipeline @@ -179,7 +179,7 @@ describe 'GitLab Markdown', feature: true do before(:all) do @feat = MarkdownFeature.new - # `gfm_with_options` depends on a `@project` variable + # `gfm` helper depends on a `@project` variable @project = @feat.project @html = markdown(@feat.raw_markdown) diff --git a/spec/fixtures/emails/android_gmail.eml b/spec/fixtures/emails/android_gmail.eml new file mode 100644 index 00000000000..21c5dde2346 --- /dev/null +++ b/spec/fixtures/emails/android_gmail.eml @@ -0,0 +1,177 @@ +Delivered-To: reply@discourse.org +Return-Path: <walter.white@googlemail.com> +MIME-Version: 1.0 +In-Reply-To: <topic/22638/86406@meta.discourse.org> +References: <topic/22638@meta.discourse.org> + <topic/22638/86406@meta.discourse.org> +Date: Fri, 28 Nov 2014 12:53:21 -0800 +Subject: Re: [Discourse Meta] [Lounge] Testing default email replies +From: Walter White <walter.white@googlemail.com> +To: Discourse Meta <reply@discourse.org> +Content-Type: multipart/alternative; boundary=089e0149cfa485c6630508f173df + +--089e0149cfa485c6630508f173df +Content-Type: text/plain; charset=UTF-8 + +### 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 +On Nov 28, 2014 12:36 PM, "Arpit Jalan" <info@discourse.org> wrote: + +> techAPJ <https://meta.discourse.org/users/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 +> <https://meta.discourse.org/users/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 +> <https://meta.discourse.org/my/preferences>. +> + +--089e0149cfa485c6630508f173df +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +<p dir=3D"ltr">### this is a reply from Android 5 gmail</p> +<p dir=3D"ltr">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. </p> +<p dir=3D"ltr">This is **bold** in Markdown.</p> +<p dir=3D"ltr">This is a link to <a href=3D"http://example.com">http://exam= +ple.com</a></p> +<div class=3D"gmail_quote">On Nov 28, 2014 12:36 PM, "Arpit Jalan"= +; <<a href=3D"mailto:info@discourse.org">info@discourse.org</a>> wrot= +e:<br type=3D"attribution"><blockquote class=3D"gmail_quote" style=3D"margi= +n:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex"><div> + +<table style=3D"margin-bottom:25px" cellspacing=3D"0" cellpadding=3D"0" bor= +der=3D"0"> + <tbody> + <tr> + <td style=3D"vertical-align:top;width:55px"> + <img src=3D"https://meta-discourse.global.ssl.fastly.net/user_avata= +r/meta.discourse.org/techapj/45/3281.png" title=3D"techAPJ" style=3D"max-wi= +dth:100%" width=3D"45" height=3D"45"> + </td> + <td> + <a href=3D"https://meta.discourse.org/users/techapj" style=3D"text-= +decoration:none;font-weight:bold;color:#006699;font-size:13px;font-family:&= +#39;lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;text-d= +ecoration:none;font-weight:bold" target=3D"_blank">techAPJ</a><br> + <span style=3D"text-align:right;color:#999999;padding-right:5px;fon= +t-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:= +11px">November 28</span> + </td> + </tr> + <tr> + <td style=3D"padding-top:5px" colspan=3D"2"> +<p style=3D"margin-top:0;border:0">Test reply.</p> + +<p style=3D"margin-top:0;border:0">First paragraph.</p> + +<p style=3D"margin-top:0;border:0">Second paragraph.</p> +</td> + </tr> + </tbody> +</table> + + + <div style=3D"color:#666"> + <p>To respond, reply to this email or visit <a href=3D"https://meta.dis= +course.org/t/testing-default-email-replies/22638/3" style=3D"text-decoratio= +n:none;font-weight:bold;color:#006699;color:#666" target=3D"_blank">https:/= +/meta.discourse.org/t/testing-default-email-replies/22638/3</a> in your bro= +wser.</p> + </div> + <hr style=3D"background-color:#ddd;min-height:1px;border:1px;background-c= +olor:#ddd;min-height:1px;border:1px"> + <h4>Previous Replies</h4> + + <table style=3D"margin-bottom:25px" cellspacing=3D"0" cellpadding=3D"0" b= +order=3D"0"> + <tbody> + <tr> + <td style=3D"vertical-align:top;width:55px"> + <img src=3D"https://meta-discourse.global.ssl.fastly.net/user_avata= +r/meta.discourse.org/codinghorror/45/5297.png" title=3D"codinghorror" style= +=3D"max-width:100%" width=3D"45" height=3D"45"> + </td> + <td> + <a href=3D"https://meta.discourse.org/users/codinghorror" style=3D"= +text-decoration:none;font-weight:bold;color:#006699;font-size:13px;font-fam= +ily:'lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;t= +ext-decoration:none;font-weight:bold" target=3D"_blank">codinghorror</a><br= +> + <span style=3D"text-align:right;color:#999999;padding-right:5px;fon= +t-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:= +11px">November 28</span> + </td> + </tr> + <tr> + <td style=3D"padding-top:5px" colspan=3D"2"> +<p style=3D"margin-top:0;border:0">We're testing the latest GitHub emai= +l processing library which we are integrating now.</p> + +<p style=3D"margin-top:0;border:0"><a href=3D"https://github.com/github/ema= +il_reply_parser" style=3D"text-decoration:none;font-weight:bold;color:#0066= +99" target=3D"_blank">https://github.com/github/email_reply_parser</a></p> + +<p style=3D"margin-top:0;border:0">Go ahead and reply to this topic and I&#= +39;ll reply from various email clients for testing.</p> +</td> + </tr> + </tbody> +</table> + + +<hr style=3D"background-color:#ddd;min-height:1px;border:1px;background-col= +or:#ddd;min-height:1px;border:1px"> + +<div style=3D"color:#666"> +<p>To respond, reply to this email or visit <a href=3D"https://meta.discour= +se.org/t/testing-default-email-replies/22638/3" style=3D"text-decoration:no= +ne;font-weight:bold;color:#006699;color:#666" target=3D"_blank">https://met= +a.discourse.org/t/testing-default-email-replies/22638/3</a> in your browser= +.</p> +</div> +<div style=3D"color:#666"> +<p>To unsubscribe from these emails, visit your <a href=3D"https://meta.dis= +course.org/my/preferences" style=3D"text-decoration:none;font-weight:bold;c= +olor:#006699;color:#666" target=3D"_blank">user preferences</a>.</p> +</div> +</div> +</blockquote></div> + +--089e0149cfa485c6630508f173df-- diff --git a/spec/fixtures/emails/attachment.eml b/spec/fixtures/emails/attachment.eml new file mode 100644 index 00000000000..f25c3d1a449 --- /dev/null +++ b/spec/fixtures/emails/attachment.eml @@ -0,0 +1,351 @@ +Message-ID: <51C22E52.1030509@darthvader.ca> +Date: Wed, 19 Jun 2013 18:18:58 -0400 +From: Anakin Skywalker <FROM> +User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:17.0) Gecko/20130510 Thunderbird/17.0.6 +MIME-Version: 1.0 +To: Han Solo via Death Star <TO> +Subject: Re: [Death Star] [PM] re: Regarding your post in "Site Customization + not working" +References: <51d23d33f41fb_5f4e4b35d7d60798@xwing.mail> +In-Reply-To: <51d23d33f41fb_5f4e4b35d7d60798@xwing.mail> +Content-Type: multipart/mixed; boundary=047d7b45041e19c68004eb9f3de8 + +--047d7b45041e19c68004eb9f3de8 +Content-Type: multipart/alternative; boundary=047d7b45041e19c67b04eb9f3de6 + +--047d7b45041e19c67b04eb9f3de6 +Content-Type: text/plain; charset=ISO-8859-1 + +here is an image attachment + + +On Tue, Nov 19, 2013 at 5:11 PM, Neil <info@discourse.org> wrote: + +> Neil <http://meta.discourse.org/users/neil> +> November 19 +> +> Actually, deleting a spammer does what it's supposed to. It does mark the +> topic as deleted. +> +> That topic has id 11002, and you're right that the user was deleted. +> +> @eviltrout <http://users/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/5in your browser. +> ------------------------------ +> Previous Replies Neil <http://meta.discourse.org/users/neil> +> November 19 +> +> Looks like a bug when deleting a spammer. I'll look at it. +> riking <http://meta.discourse.org/users/riking> +> November 19 +> +> codinghorror: +> +> I can't even find that topic by name. +> +> In that case, I'm fairly certain someone used the 'Delete Spammer' +> function on the user, which would explain your 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 should be invisible to me, and not +> showing up in Suggested Topics. +> codinghorror <http://meta.discourse.org/users/codinghorror> +> November 19 +> +> Hmm, that's interesting -- can you have a look @eviltrout<http://users/eviltrout>? +> I can't even find that topic by name. +> riking <http://meta.discourse.org/users/riking> +> November 19 +> +> I'm one of the users who flagged this particular spam post, and it was +> promptly deleted/hidden, but it just popped up in the Suggested Topics box: +> +> Pasted image1125x220 27.7 KB +> <//cdn.discourse.org/uploads/meta_discourse/2158/50b8b49557cb249e.png> +> +> We may want to recheck the suppression on these. +> ------------------------------ +> +> To respond, reply to this email or visit +> http://meta.discourse.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5in your browser. +> +> To unsubscribe from these emails, visit your user preferences<http://meta.discourse.org/user_preferences> +> . +> + +--047d7b45041e19c67b04eb9f3de6 +Content-Type: text/html; charset=ISO-8859-1 +Content-Transfer-Encoding: quoted-printable + +<div dir=3D"ltr">here is an image attachment</div><div class=3D"gmail_extra= +"><br><br><div class=3D"gmail_quote">On Tue, Nov 19, 2013 at 5:11 PM, Neil = +<span dir=3D"ltr"><<a href=3D"mailto:info@discourse.org" target=3D"_blan= +k">info@discourse.org</a>></span> wrote:<br> +<blockquote class=3D"gmail_quote" style=3D"margin:0 0 0 .8ex;border-left:1p= +x #ccc solid;padding-left:1ex"><div> +<table style=3D"margin-bottom:25px;max-width:761px" cellspacing=3D"0" cellp= +adding=3D"0" border=3D"0"><tbody> +<tr> +<td style=3D"vertical-align:top;width:55px"> + <img src=3D"http://www.gravatar.com/avatar/42776c4982dff1fa45ee8248= +532f8ad0.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"Neil" style=3D"m= +ax-width:694px" width=3D"45" height=3D"45"> +</td> + <td> + <a href=3D"http://meta.discourse.org/users/neil" style=3D"font-size= +:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;c= +olor:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">Neil<= +/a><br> +<span style=3D"text-align:right;color:#999999;padding-right:5px;font-family= +:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px">No= +vember 19</span> + </td> + </tr> +<tr> +<td style=3D"padding-top:5px" colspan=3D"2"> +<p style=3D"margin-top:0">Actually, deleting a spammer does what it's s= +upposed to. It does mark the topic as deleted.</p> + +<p style=3D"margin-top:0">That topic has id 11002, and you're right tha= +t the user was deleted.</p> + +<p style=3D"margin-top:0"><a href=3D"http://users/eviltrout" target=3D"_bla= +nk">@eviltrout</a> Any idea why it showed up in suggested topics? </p> +</td> + </tr> +</tbody></table> +<div style=3D"color:#666"> + <p>To respond, reply to this email or visit <a href=3D"http://meta.disc= +ourse.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"co= +lor:#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back= +-up-in-suggested-topics/11005/5</a> in your browser.</p> + + </div> + <hr style=3D"background-color:#ddd;min-height:1px;border:1px"> +<h4>Previous Replies</h4> + + <table style=3D"margin-bottom:25px;max-width:761px" cellspacing=3D"0" cel= +lpadding=3D"0" border=3D"0"><tbody> +<tr> +<td style=3D"vertical-align:top;width:55px"> + <img src=3D"http://www.gravatar.com/avatar/42776c4982dff1fa45ee8248= +532f8ad0.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"Neil" style=3D"m= +ax-width:694px" width=3D"45" height=3D"45"> +</td> + <td> + <a href=3D"http://meta.discourse.org/users/neil" style=3D"font-size= +:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;c= +olor:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">Neil<= +/a><br> +<span style=3D"text-align:right;color:#999999;padding-right:5px;font-family= +:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px">No= +vember 19</span> + </td> + </tr> +<tr> +<td style=3D"padding-top:5px" colspan=3D"2"><p style=3D"margin-top:0">Looks= + like a bug when deleting a spammer. I'll look at it.</p></td> + </tr> +</tbody></table> +<table style=3D"margin-bottom:25px;max-width:761px" cellspacing=3D"0" cellp= +adding=3D"0" border=3D"0"><tbody> +<tr> +<td style=3D"vertical-align:top;width:55px"> + <img src=3D"http://www.gravatar.com/avatar/5120fc4e345db0d1a9648882= +72073819.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"riking" style=3D= +"max-width:694px" width=3D"45" height=3D"45"> +</td> + <td> + <a href=3D"http://meta.discourse.org/users/riking" style=3D"font-si= +ze:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif= +;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik= +ing</a><br> +<span style=3D"text-align:right;color:#999999;padding-right:5px;font-family= +:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px">No= +vember 19</span> + </td> + </tr> +<tr> +<td style=3D"padding-top:5px" colspan=3D"2"> +<p style=3D"margin-top:0"><u></u></p><div> +<div></div> +<img width=3D"20" height=3D"20" src=3D"http://www.gravatar.com/avatar/51d62= +3f33f8b83095db84ff35e15dbe8.png?s=3D40&r=3Dpg&d=3Didenticon" style= +=3D"max-width:694px">codinghorror:</div> +<blockquote><p style=3D"margin-top:0">I can't even find that topic by n= +ame.</p></blockquote><u></u><p></p> + +<p style=3D"margin-top:0">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.</p> + +<p style=3D"margin-top:0">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.</p> +</td> + </tr> +</tbody></table> +<table style=3D"margin-bottom:25px;max-width:761px" cellspacing=3D"0" cellp= +adding=3D"0" border=3D"0"><tbody> +<tr> +<td style=3D"vertical-align:top;width:55px"> + <img src=3D"http://www.gravatar.com/avatar/51d623f33f8b83095db84ff3= +5e15dbe8.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"codinghorror" st= +yle=3D"max-width:694px" width=3D"45" height=3D"45"> +</td> + <td> + <a href=3D"http://meta.discourse.org/users/codinghorror" style=3D"f= +ont-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans= +-serif;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blan= +k">codinghorror</a><br> +<span style=3D"text-align:right;color:#999999;padding-right:5px;font-family= +:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px">No= +vember 19</span> + </td> + </tr> +<tr> +<td style=3D"padding-top:5px" colspan=3D"2"><p style=3D"margin-top:0">Hmm, = +that's interesting -- can you have a look <a href=3D"http://users/evilt= +rout" target=3D"_blank">@eviltrout</a>? I can't even find that topic by= + name. </p> +</td> + </tr> +</tbody></table> +<table style=3D"margin-bottom:25px;max-width:761px" cellspacing=3D"0" cellp= +adding=3D"0" border=3D"0"><tbody> +<tr> +<td style=3D"vertical-align:top;width:55px"> + <img src=3D"http://www.gravatar.com/avatar/5120fc4e345db0d1a9648882= +72073819.png?s=3D45&r=3Dpg&d=3Didenticon" title=3D"riking" style=3D= +"max-width:694px" width=3D"45" height=3D"45"> +</td> + <td> + <a href=3D"http://meta.discourse.org/users/riking" style=3D"font-si= +ze:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif= +;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik= +ing</a><br> +<span style=3D"text-align:right;color:#999999;padding-right:5px;font-family= +:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px">No= +vember 19</span> + </td> + </tr> +<tr> +<td style=3D"padding-top:5px" colspan=3D"2"> +<p style=3D"margin-top:0">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:</p> + +<p style=3D"margin-top:0"></p> +<div><a href=3D"//cdn.discourse.org/uploads/meta_discourse/2158/50b8b49557c= +b249e.png" target=3D"_blank"><img src=3D"http://cdn.discourse.org/uploads/m= +eta_discourse/_optimized/ab1/c92/acd2c33402_584x134.png" width=3D"584" heig= +ht=3D"134" style=3D"max-width:694px"><div> + +<span>Pasted image</span><span>1125x220 27.7 KB</span><span></span> +</div></a></div> + +<p style=3D"margin-top:0">We may want to recheck the suppression on these.<= +/p> +</td> + </tr> +</tbody></table> +<hr style=3D"background-color:#ddd;min-height:1px;border:1px"> +<div style=3D"color:#666"> +<p>To respond, reply to this email or visit <a href=3D"http://meta.discours= +e.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"color:= +#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back-up-= +in-suggested-topics/11005/5</a> in your browser.</p> + +</div> +<div style=3D"color:#666"> +<p>To unsubscribe from these emails, visit your <a href=3D"http://meta.disc= +ourse.org/user_preferences" style=3D"color:#666" target=3D"_blank">user pre= +ferences</a>.</p> +</div> +</div></blockquote></div><br></div> + +--047d7b45041e19c67b04eb9f3de6-- +--047d7b45041e19c68004eb9f3de8 +Content-Type: image/png; name="bricks.png" +Content-Disposition: attachment; filename="bricks.png" +Content-Transfer-Encoding: base64 +X-Attachment-Id: f_ho8uteve0 + +iVBORw0KGgoAAAANSUhEUgAAASEAAAB+CAIAAADk0DDaAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ +bWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdp +bj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6 +eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEz +NDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJo +dHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlw +dGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAv +IiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RS +ZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpD +cmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNl +SUQ9InhtcC5paWQ6MDYxQjcyOUUzMDM1MTFFM0JFRTFBOTQ1RUY4QUU4MDIiIHhtcE1NOkRvY3Vt +ZW50SUQ9InhtcC5kaWQ6MDYxQjcyOUYzMDM1MTFFM0JFRTFBOTQ1RUY4QUU4MDIiPiA8eG1wTU06 +RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowNjFCNzI5QzMwMzUxMUUzQkVF +MUE5NDVFRjhBRTgwMiIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowNjFCNzI5RDMwMzUxMUUz +QkVFMUE5NDVFRjhBRTgwMiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1w +bWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pm2fyz0AAAyISURBVHja7F2/i11FFL6rL12aBdlGRDCF +EQmEbVJtChfSJJDGRkgZBBsVUhgQ7NSkCKiFVUr/AUGbhW1MlWaJBAkWVsFmG0HshMT7duJk9szc +uefOjzPn3vd9xfL2/bh35rtnznfOuXNnth7c/6ID2Lh261vO13669wm4SsZ7H3396gmePXu2OkH/ +Yr4Mv4IrCgAYY8Am4vnz51sn8EVsXth68P7eYq7Kj4cP3H+v79fq2tWDX/u/d25/7n/08/3PzIvb +u3vLs3sxhh/vXrOvb9/50v1o77W/X340B5IXMsbsta931eN24I6uRQ4wd3SJkUwYnqkLQ6wIAHWx +gn/Nx3ff3Ov/njvbWFcXFibESdZw3aFjAKBDx46Ofk/42e7u2/3f4G8jH5XF07+O7es3tnfSThps +beRNA/PRmd1rxrlGkMNDf8a2DLskJzOcRrJ5/7czb/Z/fzk8qESyjBlDxwBAZT4WGd/1/CtxLcaz +ZiLYWvOmezpXxMQwxKQYwzIkK2S4LMnQMQCorGMm4C7irhp6nUzPHfSs7un6176jffT4cULSuGkM ++1mWq5b2jDlqRpJGdWNsFqNLxqrstfejxEzjA8l+LBpkm+DihQucmodyhhErAoCOmkcvx4t3xsG4 +RaZEbgOeZZNMwu9u+P7EkkiGjgGADh2LDH21Ehd0Wvz82E/VqiLOsE6JizM8iWSZ2n0TM4aOAYAO +HUvzDW0RbNhoa8ld0Ui2cPHCBU7JCwz7DDPzMc7dEf0krzqAESsCIBmxIgAsN1YUSKMlU/9N8KxD ++b02hvn3oDWbMXQMADZMxyIOtUnqn1lTVluuWAzD+kmGjgGAeh2rcfMu7YDCd8PFKss10qRkhiV1 +Q7J2X8+Mpe+PuRcpOCEgp59lOWry1GCRfgVJdg+STFRxK4yTLFnzSCCZacaIFQGgcqworP5FvKlM +YFBwvuIGkszscny+Ij9WlJ/SyY+8oGMAUFnHZIa+tpnjRVrCn68o0PFFMqztdGkkQ8cAQCQfI87A +X0lGlZtJW4gmx9Mnr5lDGuyenawko82RJ5OczLCflfHriprNGDoGAOL5WD/63QX7tU1USV7oq2FH +yKmNf7Ukq2V4RiRrNuOVf+3LLsSrYXTlI7l2TwLUSgvxahhdNRhmRuMkQNVmxogVAUBEx9yh7zoz +STc2quwFHVKTdX7sc/WtGB4NUMsynH/AqXOpdJoxdAwAKuuYGwc3SXj0TL2NIFi7n+pfWyU8c2E4 +p6mazRg6BgAi+ZgbRIpF2yRDKIhRuRhdMJTTu8v7VyY9dpFAcr4nJhlCDZKTGS4uNTrNOLBXLeeU +beuhVefm8Q8bma/4ZLt756+XRyMkM0+xVJL5x4zU7nuGe1iSNZsxYkUAqBwrBoXbf1Os2F3E/cg0 +NeJle//qPyRLGkZiLcmJ83MhOVK7d8OEIZKDZizTcjwHDQCCOubGtfHbdpNSVc6+UuYL1/f33JRx +RttwRfKxvv2mI4Ze63pHb4zySWZuj9Z/gTDczWc3uUik4OqSJZljxt2UslYRM4aOAUBlHTNDPxJN +EmEx/wbfPBUcHy2fu4iXPeiOu22aPAyR7Eu3JTl4ITaH4QjWDPfYZjHc1oxXoxHL0DtumyIJJWl3 +8CHF0QZkJqxFbsj4ExE4aw0Er32wj3GG48Unsg4Zh2T/dHb05iy9mBnnE5KZ8xWHSK5nxt3Ak6DB +IyNWBADBmkca+P6YfPr08JS8vFD/kGc69au8+dTJP89xz5kkT2J4iGTTfkuy35jgNZJkOIdkYTM+ +RdeYGUPHAKCyjpV1BqXwZHs8nxGG8VsHR+u/r1+6sX7rdM3jj3/WPvjc2eNgR9QyrJPkqzfvBqtK +PcmEYf0kQ8cAoC62rr4FEibAKJipd333zb2hr/m+FphKskWwrjgjhrfO7+zgonLw8ae3bPRirrp5 +Jz7YgEm4vH/F/df4srmTjFgRAKBjOkQsqGAG7kdAvoL18jU0h2aOJEPHAKCyjn34wY2hz9xomIC4 +GfPNtJ1FyW8jJ423Ie7/cnpnvmzyAZIw1OtdPsnkXO4P7Uf1Llm9CxE5sqtywdSulJlN6iB0DAAq +69j3X92ND8rgqHXdwNBvR4e7+4W4L0xug+/5gv5s9Mi9g/QVLO5TM3vHVJtI++OdCrJX8JKNXohS +ZjYaZYiZ2dChoGMAUFnHvn1LS13xzM1bHH/z7kOU79Lx26XxLOXf+7jdl8uwa8Ar5sqsZPk482R1 +WRyZS3vSxKAo//nwh/Xfrru9u7e8a+Mv0FeD5O7EQ5GRZvHz/c/s600guR7Dj1DzAIDGsaIrbmlb +0dnFRsh+oaOyaX5lHa3RXNe/Xul2hprK34+UNM9/TY5vWz70acdexMZvedpWdP6pO/aq8f3X/Mjc +kkwY7pK21Q0yk8Yh+UICwzkkB814lGTXjKFjANBOx0aH/qjX4bwZdADGy3b/zwR1J1nb54KC25O6 +p+AIy1TxKQjOhmCZDEdIdlMyc+vWkuw+eRXcZdeehcleK5KVmDF0DABa6FiRhZzS3K3rAOzjDEwd +S0gXJ31UFkUWckpzt1bH3MlHHB3LbJiwrNUz4yE7CZrxKnigIovIBkkcqjQII3KB6117clXESN4o +hmXM2C/hRPaMR6wIAHWxqudaguVO88I9XbKaNdn3tZJrzyfZDxDs6XLihSb7vupk2Cd51IxNKA4d +AwARHavtVIJ3ISO5L//hnFn4VwGSh4gdKuEwl7kGyUN3g4LTGAjMcwDQMQCYrY4FnUHatKNMzGhP +syLulkNy2hPQINl9zTTjVUFC+UUIX3+rItIYzZYxtEULM34jYSRB8cVn5kiyjBkjVgQAlbFicHzz +d/4cFVmxJb40xzYJJPOfAzDL18ksDKqW5GQznhQrQscAYBE1j0ggG4QpemJV0KokAzIMQ8cAQETH +ZCo/m+BZI0wG64StGC5eu1fCsCozNjf6Vw2z0syqveZyRXA4geTaI00bw5h3DwAS4I6xzH24p6IX +2UlLw+e4wxpdS3ColVqiRHOKd61neC4kQ8cAoC64tfuykW6TJ3OL9MtNA4LTmpKJKp5LzJpkwrB7 +kByimpgxdAwAdOhYmqcfQsE5wcLzTYtIlgDD3dic4EnPjwncdQiS3LCqWcSM8Rw0AMxTx4r4Hm3P +QQs7coEuT5oNLNB3bc+/FGmJmRK4GurtpNPkEBRcbIQfyQTT4rRF8MWMLG21n2SSgwxPmncfNImE +RfAlh7EeM0asCACCsWLyQl8NJT64IHvaJh1imfTCSNY230qPGWMuFQAI6ljD9UAjixhPjZ5rLHat +wb+2YtgnucZi10rChBokG0DHAEBExwoO+iJF2KlPQFv/2mRaLTMJLEVykzK3q2AaSK7KcCmSTz0/ +1hCZlWX3h/LBmJ45gVMZnjTPw/62STA2X5IRKwKAYKxYMK0cXcuS4wKnPgnvxmnMXuS74d5pTT1v +keoIh+FRkgUYztc6PwgXq44UNGPoGACI61i9uXlFDvtk+8VmquZoZCIP8xRti871ihalGO66XJKb +l/U1mDHmUgFAIx2LD9Pm/qn3r/5DsqRtJNqWLHYX8fFtSXYVjJDsNoykJWIkFwlVZBg2+dhK59CP +VJbtmDEhjWsW8fs2/HoAcx3z/gvX9/dIUj6XLYLiDNuOWJI5DE+qB3BINp8Skme3CRNiRQAQjxWV +46A77jwFi0QCJPc1XjD45kv/fbT8Cx+p3a8Z7sEmmQiLZXjoQrzQug0gGTUPABDUsZwYt8gMJrub +06iXjTtXP/UayiLcLkeydvtmcJo/swH2+JkM55BMJvsw51KVJTnOcDcwzX8Sw6rMGPkYAIjomOsP +MudT5/ycOA/+jFX3hmmRNkf8Mfn06eEpz/cijQm5/+DPhUkmDE+aS2Xv+xdpc5zhU3QdUgG3JA8x +rMqMMZcKAATzseboncfB0dp/XL151//0j3/W7uHc2WNfwQq624Igt5WUMLzWgf9Jvnjyphsp9CQT +hn2SM6OGGgxrI9kw/PqlGy/HmG+prRAcXaMjjTDepDtPumOOKeghuY9hgtvicBgGyXGY0WXoRawI +ABLYOr+jYk6KWVGV1Dy6icvZAqMMu/7VAnvbFzdjN0yAjgHA0mseZukO4lnNv70zMI4BjrZgjOA7 +WqhZcZJde4aOAUDlfOz7r+6SYdd7OPJv51Si3AQp6CD9Hw65TytW/tCPwz9y/FyRb7r/Tu3pEFHx +/g7pCbOR8SP7Le/DBNI7v+Uckl2VC2YdkQMmXAi/zfGm+t8hJ2U2tdQldr/5nwADACLM1IGrPYuL +AAAAAElFTkSuQmCC +--047d7b45041e19c68004eb9f3de8-- diff --git a/spec/fixtures/emails/auto_reply.eml b/spec/fixtures/emails/auto_reply.eml new file mode 100644 index 00000000000..7999c8d78b7 --- /dev/null +++ b/spec/fixtures/emails/auto_reply.eml @@ -0,0 +1,21 @@ +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@discourse.example.com>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@discourse.example.com>; 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+636ca428858779856c226bb145ef4fad@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +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 +Auto-Submitted: auto-generated +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 + +Test reply to Discourse email digest diff --git a/spec/fixtures/emails/dutch.eml b/spec/fixtures/emails/dutch.eml new file mode 100644 index 00000000000..3142bf30c3b --- /dev/null +++ b/spec/fixtures/emails/dutch.eml @@ -0,0 +1,20 @@ + +Delivered-To: discourse-reply+cd480e301683c9902891f15968bf07a5@discourse.org +Received: by 10.194.216.104 with SMTP id op8csp80593wjc; + Wed, 24 Jul 2013 07:59:14 -0700 (PDT) +Return-Path: <walter.white@googlemail.com> +References: <topic/5043@discourse.org> <51efeb9b36c34_66dc2dfce6811866@discourse.mail> +From: Walter White <walter.white@googlemail.com> +In-Reply-To: <51efeb9b36c34_66dc2dfce6811866@discourse.mail> +Mime-Version: 1.0 (1.0) +Date: Wed, 24 Jul 2013 15:59:10 +0100 +Message-ID: <4597127794206131679@unknownmsgid> +Subject: Re: [Discourse] new reply to your post in 'Crystal Blue' +To: walter via Discourse <reply+cd480e301683c9902891f15968bf07a5@appmail.adventuretime.ooo> +Content-Type: multipart/alternative; boundary=001a11c20edc15a39304e2432790 + +Dit is een antwoord in het Nederlands. + +Op 18 juli 2013 10:23 schreef Sander Datema het volgende: + +Dit is de originele post. diff --git a/spec/fixtures/emails/gmail_web.eml b/spec/fixtures/emails/gmail_web.eml new file mode 100644 index 00000000000..8bb83835711 --- /dev/null +++ b/spec/fixtures/emails/gmail_web.eml @@ -0,0 +1,181 @@ +Delivered-To: reply@discourse.org +Return-Path: <walter.white@googlemail.com> +MIME-Version: 1.0 +In-Reply-To: <topic/22638/86406@meta.discourse.org> +References: <topic/22638@meta.discourse.org> + <topic/22638/86406@meta.discourse.org> +Date: Fri, 28 Nov 2014 12:36:49 -0800 +Subject: Re: [Discourse Meta] [Lounge] Testing default email replies +From: Walter White <walter.white@googlemail.com> +To: Discourse Meta <reply@discourse.org> +Content-Type: multipart/alternative; boundary=001a11c2e04e6544f30508f138ba + +--001a11c2e04e6544f30508f138ba +Content-Type: text/plain; charset=UTF-8 + +### 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 + +On Fri, Nov 28, 2014 at 12:35 PM, Arpit Jalan <info@discourse.org> wrote: + +> techAPJ <https://meta.discourse.org/users/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 +> <https://meta.discourse.org/users/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 +> <https://meta.discourse.org/my/preferences>. +> + +--001a11c2e04e6544f30508f138ba +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +<div dir=3D"ltr"><div>### This is a reply from standard GMail in Google Chr= +ome.</div><div><br></div><div>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.=C2=A0</div><div><br></div><div>Here's some **bold** text= + in Markdown.</div><div><br></div><div>Here's a link <a href=3D"http://= +example.com">http://example.com</a></div></div><div class=3D"gmail_extra"><= +br><div class=3D"gmail_quote">On Fri, Nov 28, 2014 at 12:35 PM, Arpit Jalan= + <span dir=3D"ltr"><<a href=3D"mailto:info@discourse.org" target=3D"_bla= +nk">info@discourse.org</a>></span> wrote:<br><blockquote class=3D"gmail_= +quote" style=3D"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1= +ex"><div> + +<table style=3D"margin-bottom:25px" cellspacing=3D"0" cellpadding=3D"0" bor= +der=3D"0"> + <tbody> + <tr> + <td style=3D"vertical-align:top;width:55px"> + <img src=3D"https://meta-discourse.global.ssl.fastly.net/user_avata= +r/meta.discourse.org/techapj/45/3281.png" title=3D"techAPJ" style=3D"max-wi= +dth:100%" width=3D"45" height=3D"45"> + </td> + <td> + <a href=3D"https://meta.discourse.org/users/techapj" style=3D"text-= +decoration:none;font-weight:bold;color:#006699;font-size:13px;font-family:&= +#39;lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;text-d= +ecoration:none;font-weight:bold" target=3D"_blank">techAPJ</a><br> + <span style=3D"text-align:right;color:#999999;padding-right:5px;fon= +t-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:= +11px">November 28</span> + </td> + </tr> + <tr> + <td style=3D"padding-top:5px" colspan=3D"2"> +<p style=3D"margin-top:0;border:0">Test reply.</p> + +<p style=3D"margin-top:0;border:0">First paragraph.</p> + +<p style=3D"margin-top:0;border:0">Second paragraph.</p> +</td> + </tr> + </tbody> +</table> + + + <div style=3D"color:#666"> + <p>To respond, reply to this email or visit <a href=3D"https://meta.dis= +course.org/t/testing-default-email-replies/22638/3" style=3D"text-decoratio= +n:none;font-weight:bold;color:#006699;color:#666" target=3D"_blank">https:/= +/meta.discourse.org/t/testing-default-email-replies/22638/3</a> in your bro= +wser.</p> + </div> + <hr style=3D"background-color:#ddd;min-height:1px;border:1px;background-c= +olor:#ddd;min-height:1px;border:1px"> + <h4>Previous Replies</h4> + + <table style=3D"margin-bottom:25px" cellspacing=3D"0" cellpadding=3D"0" b= +order=3D"0"> + <tbody> + <tr> + <td style=3D"vertical-align:top;width:55px"> + <img src=3D"https://meta-discourse.global.ssl.fastly.net/user_avata= +r/meta.discourse.org/codinghorror/45/5297.png" title=3D"codinghorror" style= +=3D"max-width:100%" width=3D"45" height=3D"45"> + </td> + <td> + <a href=3D"https://meta.discourse.org/users/codinghorror" style=3D"= +text-decoration:none;font-weight:bold;color:#006699;font-size:13px;font-fam= +ily:'lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;t= +ext-decoration:none;font-weight:bold" target=3D"_blank">codinghorror</a><br= +> + <span style=3D"text-align:right;color:#999999;padding-right:5px;fon= +t-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:= +11px">November 28</span> + </td> + </tr> + <tr> + <td style=3D"padding-top:5px" colspan=3D"2"> +<p style=3D"margin-top:0;border:0">We're testing the latest GitHub emai= +l processing library which we are integrating now.</p> + +<p style=3D"margin-top:0;border:0"><a href=3D"https://github.com/github/ema= +il_reply_parser" style=3D"text-decoration:none;font-weight:bold;color:#0066= +99" target=3D"_blank">https://github.com/github/email_reply_parser</a></p> + +<p style=3D"margin-top:0;border:0">Go ahead and reply to this topic and I&#= +39;ll reply from various email clients for testing.</p> +</td> + </tr> + </tbody> +</table> + + +<hr style=3D"background-color:#ddd;min-height:1px;border:1px;background-col= +or:#ddd;min-height:1px;border:1px"> + +<div style=3D"color:#666"> +<p>To respond, reply to this email or visit <a href=3D"https://meta.discour= +se.org/t/testing-default-email-replies/22638/3" style=3D"text-decoration:no= +ne;font-weight:bold;color:#006699;color:#666" target=3D"_blank">https://met= +a.discourse.org/t/testing-default-email-replies/22638/3</a> in your browser= +.</p> +</div> +<div style=3D"color:#666"> +<p>To unsubscribe from these emails, visit your <a href=3D"https://meta.dis= +course.org/my/preferences" style=3D"text-decoration:none;font-weight:bold;c= +olor:#006699;color:#666" target=3D"_blank">user preferences</a>.</p> +</div> +</div> +</blockquote></div><br></div> + +--001a11c2e04e6544f30508f138ba-- diff --git a/spec/fixtures/emails/html_paragraphs.eml b/spec/fixtures/emails/html_paragraphs.eml new file mode 100644 index 00000000000..3fe37fb8b17 --- /dev/null +++ b/spec/fixtures/emails/html_paragraphs.eml @@ -0,0 +1,205 @@ + +MIME-Version: 1.0 +Received: by 10.25.161.144 with HTTP; Tue, 7 Oct 2014 22:17:17 -0700 (PDT) +X-Originating-IP: [117.207.85.84] +In-Reply-To: <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail> +References: <topic/35@discourse.techapj.com> + <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail> +Date: Wed, 8 Oct 2014 10:47:17 +0530 +Delivered-To: arpit@techapj.com +Message-ID: <CAOJeqne=SJ_LwN4sb-0Y95ejc2OpreVhdmcPn0TnmwSvTCYzzQ@mail.gmail.com> +Subject: Re: [Discourse] [Meta] Welcome to techAPJ's Discourse! +From: Arpit Jalan <arpit@techapj.com> +To: Discourse <mail+e1c7f2a380e33840aeb654f075490bad@arpitjalan.com> +Content-Type: multipart/alternative; boundary=001a114119d8f4e46e0504e26d5b + +--001a114119d8f4e46e0504e26d5b +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +Awesome! + +Pleasure to have you here! + +:boom: + +On Wed, Oct 8, 2014 at 10:46 AM, ajalan <info@unconfigured.discourse.org> +wrote: + +> ajalan +> <http://mandrillapp.com/track/click/30081177/discourse.techapj.com?p=3Dey= +JzIjoiVXgxTTZ3eHpuRWF2QXVoZGRJZVN5MWI0WnhrIiwidiI6MSwicCI6IntcInVcIjozMDA4M= +TE3NyxcInZcIjoxLFwidXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29t= +XFxcL3VzZXJzXFxcL2FqYWxhblwiLFwiaWRcIjpcIjgyNWI5MDYzZWNmMDRkMjk5OTE4Nzk1MmU= +5YjY2YjE3XCIsXCJ1cmxfaWRzXCI6W1wiNzA3MTNjNTg4MDI3YWQyM2RiM2QwOTVhOGQwYmY4ZT= +YyMzNjYThiMFwiXX0ifQ> +> October 8 +> +> Nice to be here! Thanks! [image: smile] +> +> To respond, reply to this email or visit +> http://discourse.techapj.com/t/welcome-to-techapjs-discourse/35/2 +> <http://mandrillapp.com/track/click/30081177/discourse.techapj.com?p=3Dey= +JzIjoid1IyWnVqVGRPU2RwLUlFR0Q5QnI1a203eVNjIiwidiI6MSwicCI6IntcInVcIjozMDA4M= +TE3NyxcInZcIjoxLFwidXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29t= +XFxcL3RcXFwvd2VsY29tZS10by10ZWNoYXBqcy1kaXNjb3Vyc2VcXFwvMzVcXFwvMlwiLFwiaWR= +cIjpcIjgyNWI5MDYzZWNmMDRkMjk5OTE4Nzk1MmU5YjY2YjE3XCIsXCJ1cmxfaWRzXCI6W1wiY2= +RkYzFlZjc5OThhNzE1ODA4Yjg0MGFlNzVlZmNiYmYzYmViODk4Y1wiXX0ifQ> +> in your browser. +> ------------------------------ +> Previous Replies techAPJ +> <http://mandrillapp.com/track/click/30081177/discourse.techapj.com?p=3Dey= +JzIjoia2x3LUxac2RSX25uWEFYYWcwVDVha3pIY3RjIiwidiI6MSwicCI6IntcInVcIjozMDA4M= +TE3NyxcInZcIjoxLFwidXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29t= +XFxcL3VzZXJzXFxcL3RlY2hhcGpcIixcImlkXCI6XCI4MjViOTA2M2VjZjA0ZDI5OTkxODc5NTJ= +lOWI2NmIxN1wiLFwidXJsX2lkc1wiOltcIjk2ZjAyMzVhNmM2NzIyNmU1NjhhMzU1NDE1OTAxNz= +AyYTkxNjM1NzJcIl19In0> +> October 8 +> +> Welcome to techAPJ's Discourse! +> ------------------------------ +> +> To respond, reply to this email or visit +> http://discourse.techapj.com/t/welcome-to-techapjs-discourse/35/2 +> <http://mandrillapp.com/track/click/30081177/discourse.techapj.com?p=3Dey= +JzIjoid1IyWnVqVGRPU2RwLUlFR0Q5QnI1a203eVNjIiwidiI6MSwicCI6IntcInVcIjozMDA4M= +TE3NyxcInZcIjoxLFwidXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29t= +XFxcL3RcXFwvd2VsY29tZS10by10ZWNoYXBqcy1kaXNjb3Vyc2VcXFwvMzVcXFwvMlwiLFwiaWR= +cIjpcIjgyNWI5MDYzZWNmMDRkMjk5OTE4Nzk1MmU5YjY2YjE3XCIsXCJ1cmxfaWRzXCI6W1wiY2= +RkYzFlZjc5OThhNzE1ODA4Yjg0MGFlNzVlZmNiYmYzYmViODk4Y1wiXX0ifQ> +> in your browser. +> +> To unsubscribe from these emails, visit your user preferences +> <http://mandrillapp.com/track/click/30081177/discourse.techapj.com?p=3Dey= +JzIjoiVTNudkpobl9lUUl0cmdsVVRrcm5iaHpyN0JZIiwidiI6MSwicCI6IntcInVcIjozMDA4M= +TE3NyxcInZcIjoxLFwidXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29t= +XFxcL215XFxcL3ByZWZlcmVuY2VzXCIsXCJpZFwiOlwiODI1YjkwNjNlY2YwNGQyOTk5MTg3OTU= +yZTliNjZiMTdcIixcInVybF9pZHNcIjpbXCI0OTIyMmMyZDgyNzUwMmQyMGZjYzU4MTZkNjhmYT= +k3NzFkY2YzZDllXCJdfSJ9> +> . +> + +--001a114119d8f4e46e0504e26d5b +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +<div dir=3D"ltr">Awesome!<div><br></div><div>Pleasure to have you here!</di= +v><div><br></div><div>:boom:</div></div><div class=3D"gmail_extra"><br><div= + class=3D"gmail_quote">On Wed, Oct 8, 2014 at 10:46 AM, ajalan <span dir=3D= +"ltr"><<a href=3D"mailto:info@unconfigured.discourse.org" target=3D"_bla= +nk">info@unconfigured.discourse.org</a>></span> wrote:<br><blockquote cl= +ass=3D"gmail_quote" style=3D"margin:0 0 0 .8ex;border-left:1px #ccc solid;p= +adding-left:1ex"><div> + +<table style=3D"margin-bottom:25px;max-width:761px" cellspacing=3D"0" cellp= +adding=3D"0" border=3D"0"> + <tbody> + <tr> + <td style=3D"vertical-align:top;width:55px"> + <img src=3D"http://discourse.techapj.com/user_avatar/discourse.tech= +apj.com/ajalan/45/35.png" title=3D"ajalan" style=3D"max-width:694px" width= +=3D"45" height=3D"45"> + </td> + <td> + <a href=3D"http://mandrillapp.com/track/click/30081177/discourse.te= +chapj.com?p=3DeyJzIjoiVXgxTTZ3eHpuRWF2QXVoZGRJZVN5MWI0WnhrIiwidiI6MSwicCI6I= +ntcInVcIjozMDA4MTE3NyxcInZcIjoxLFwidXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNl= +LnRlY2hhcGouY29tXFxcL3VzZXJzXFxcL2FqYWxhblwiLFwiaWRcIjpcIjgyNWI5MDYzZWNmMDR= +kMjk5OTE4Nzk1MmU5YjY2YjE3XCIsXCJ1cmxfaWRzXCI6W1wiNzA3MTNjNTg4MDI3YWQyM2RiM2= +QwOTVhOGQwYmY4ZTYyMzNjYThiMFwiXX0ifQ" style=3D"font-size:13px;font-family:&= +#39;lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;text-d= +ecoration:none;font-weight:bold;text-decoration:none;font-weight:bold;color= +:#006699" target=3D"_blank">ajalan</a><br> + <span style=3D"text-align:right;color:#999999;padding-right:5px;fon= +t-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:= +11px">October 8</span> + </td> + </tr> + <tr> + <td style=3D"padding-top:5px" colspan=3D"2"><p style=3D"margin-top:0;= +border:0">Nice to be here! Thanks! <img src=3D"http://discourse.techapj.com= +/plugins/emoji/images/smile.png" title=3D":smile:" alt=3D"smile" width=3D"2= +0" height=3D"20"></p></td> + </tr> + </tbody> +</table> + + + <div style=3D"color:#666"> + <p>To respond, reply to this email or visit <a href=3D"http://mandrilla= +pp.com/track/click/30081177/discourse.techapj.com?p=3DeyJzIjoid1IyWnVqVGRPU= +2RwLUlFR0Q5QnI1a203eVNjIiwidiI6MSwicCI6IntcInVcIjozMDA4MTE3NyxcInZcIjoxLFwi= +dXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29tXFxcL3RcXFwvd2VsY29= +tZS10by10ZWNoYXBqcy1kaXNjb3Vyc2VcXFwvMzVcXFwvMlwiLFwiaWRcIjpcIjgyNWI5MDYzZW= +NmMDRkMjk5OTE4Nzk1MmU5YjY2YjE3XCIsXCJ1cmxfaWRzXCI6W1wiY2RkYzFlZjc5OThhNzE1O= +DA4Yjg0MGFlNzVlZmNiYmYzYmViODk4Y1wiXX0ifQ" style=3D"color:#666;text-decorat= +ion:none;font-weight:bold;color:#006699" target=3D"_blank">http://discourse= +.techapj.com/t/welcome-to-techapjs-discourse/35/2</a> in your browser.</p> + </div> + <hr style=3D"background-color:#ddd;min-height:1px;border:1px;background-c= +olor:#ddd;min-height:1px;border:1px"> + <h4>Previous Replies</h4> + + <table style=3D"margin-bottom:25px;max-width:761px" cellspacing=3D"0" cel= +lpadding=3D"0" border=3D"0"> + <tbody> + <tr> + <td style=3D"vertical-align:top;width:55px"> + <img src=3D"http://discourse.techapj.com/user_avatar/discourse.tech= +apj.com/techapj/45/34.png" title=3D"techAPJ" style=3D"max-width:694px" widt= +h=3D"45" height=3D"45"> + </td> + <td> + <a href=3D"http://mandrillapp.com/track/click/30081177/discourse.te= +chapj.com?p=3DeyJzIjoia2x3LUxac2RSX25uWEFYYWcwVDVha3pIY3RjIiwidiI6MSwicCI6I= +ntcInVcIjozMDA4MTE3NyxcInZcIjoxLFwidXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNl= +LnRlY2hhcGouY29tXFxcL3VzZXJzXFxcL3RlY2hhcGpcIixcImlkXCI6XCI4MjViOTA2M2VjZjA= +0ZDI5OTkxODc5NTJlOWI2NmIxN1wiLFwidXJsX2lkc1wiOltcIjk2ZjAyMzVhNmM2NzIyNmU1Nj= +hhMzU1NDE1OTAxNzAyYTkxNjM1NzJcIl19In0" style=3D"font-size:13px;font-family:= +'lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;text-= +decoration:none;font-weight:bold;text-decoration:none;font-weight:bold;colo= +r:#006699" target=3D"_blank">techAPJ</a><br> + <span style=3D"text-align:right;color:#999999;padding-right:5px;fon= +t-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:= +11px">October 8</span> + </td> + </tr> + <tr> + <td style=3D"padding-top:5px" colspan=3D"2"><p style=3D"margin-top:0;= +border:0">Welcome to techAPJ's Discourse!</p></td> + </tr> + </tbody> +</table> + + +<hr style=3D"background-color:#ddd;min-height:1px;border:1px;background-col= +or:#ddd;min-height:1px;border:1px"> + +<div style=3D"color:#666"> +<p>To respond, reply to this email or visit <a href=3D"http://mandrillapp.c= +om/track/click/30081177/discourse.techapj.com?p=3DeyJzIjoid1IyWnVqVGRPU2RwL= +UlFR0Q5QnI1a203eVNjIiwidiI6MSwicCI6IntcInVcIjozMDA4MTE3NyxcInZcIjoxLFwidXJs= +XCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29tXFxcL3RcXFwvd2VsY29tZS1= +0by10ZWNoYXBqcy1kaXNjb3Vyc2VcXFwvMzVcXFwvMlwiLFwiaWRcIjpcIjgyNWI5MDYzZWNmMD= +RkMjk5OTE4Nzk1MmU5YjY2YjE3XCIsXCJ1cmxfaWRzXCI6W1wiY2RkYzFlZjc5OThhNzE1ODA4Y= +jg0MGFlNzVlZmNiYmYzYmViODk4Y1wiXX0ifQ" style=3D"color:#666;text-decoration:= +none;font-weight:bold;color:#006699" target=3D"_blank">http://discourse.tec= +hapj.com/t/welcome-to-techapjs-discourse/35/2</a> in your browser.</p> +</div><span class=3D""> +<div style=3D"color:#666"> +<p>To unsubscribe from these emails, visit your <a href=3D"http://mandrilla= +pp.com/track/click/30081177/discourse.techapj.com?p=3DeyJzIjoiVTNudkpobl9lU= +Ul0cmdsVVRrcm5iaHpyN0JZIiwidiI6MSwicCI6IntcInVcIjozMDA4MTE3NyxcInZcIjoxLFwi= +dXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29tXFxcL215XFxcL3ByZWZ= +lcmVuY2VzXCIsXCJpZFwiOlwiODI1YjkwNjNlY2YwNGQyOTk5MTg3OTUyZTliNjZiMTdcIixcIn= +VybF9pZHNcIjpbXCI0OTIyMmMyZDgyNzUwMmQyMGZjYzU4MTZkNjhmYTk3NzFkY2YzZDllXCJdf= +SJ9" style=3D"color:#666;text-decoration:none;font-weight:bold;color:#00669= +9" target=3D"_blank">user preferences</a>.</p> +</div> +</span></div> + +<img src=3D"http://mandrillapp.com/track/open.php?u=3D30081177&id=3D825= +b9063ecf04d2999187952e9b66b17" height=3D"1" width=3D"1"></blockquote></div>= +<br></div> + +--001a114119d8f4e46e0504e26d5b-- diff --git a/spec/fixtures/emails/inline_reply.eml b/spec/fixtures/emails/inline_reply.eml new file mode 100644 index 00000000000..39625a225da --- /dev/null +++ b/spec/fixtures/emails/inline_reply.eml @@ -0,0 +1,60 @@ + +MIME-Version: 1.0 +In-Reply-To: <reply@discourse-app.mail> +References: <topic/36@discourse.techapj.com> + <5434ced4ee0f9_663fb0b5f76070593b@discourse-app.mail> +Date: Mon, 1 Dec 2014 20:48:40 +0530 +Delivered-To: someone@googlemail.com +Subject: Re: [Discourse] [Meta] Testing reply via email +From: Walter White <walter.white@googlemail.com> +To: Discourse <reply@mail.com> +Content-Type: multipart/alternative; boundary=20cf30363f8522466905092920a6 + +--20cf30363f8522466905092920a6 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +On Wed, Oct 8, 2014 at 11:12 AM, techAPJ <info@unconfigured.discourse.org> +wrote: + +> techAPJ <https://meta.discourse.org/users/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 +> <https://meta.discourse.org/users/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 +> <https://meta.discourse.org/my/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. + +--20cf30363f8522466905092920a6-- diff --git a/spec/fixtures/emails/ios_default.eml b/spec/fixtures/emails/ios_default.eml new file mode 100644 index 00000000000..8d4d58feb16 --- /dev/null +++ b/spec/fixtures/emails/ios_default.eml @@ -0,0 +1,136 @@ +Delivered-To: reply@discourse.org +Return-Path: <walter.white@googlemail.com> +From: Walter White <walter.white@googlemail.com> +Content-Type: multipart/alternative; + boundary=Apple-Mail-B41C7F8E-3639-49B0-A5D5-440E125A7105 +Content-Transfer-Encoding: 7bit +Mime-Version: 1.0 (1.0) +Subject: Re: [Discourse Meta] [Lounge] Testing default email replies +Date: Fri, 28 Nov 2014 12:41:41 -0800 +References: <topic/22638@meta.discourse.org> <topic/22638/86406@meta.discourse.org> +In-Reply-To: <topic/22638/86406@meta.discourse.org> +To: Discourse Meta <reply@discourse.org> +X-Mailer: iPhone Mail (12B436) + + +--Apple-Mail-B41C7F8E-3639-49B0-A5D5-440E125A7105 +Content-Type: text/plain; + charset=us-ascii +Content-Transfer-Encoding: quoted-printable + +### this is a reply from iOS default mail + +The quick brown fox jumps over the lazy dog. The quick brown fox jumps over t= +he lazy dog. The quick brown fox jumps over the lazy dog. The quick brown fo= +x jumps over the lazy dog. The quick brown fox jumps over the lazy dog. The q= +uick brown fox jumps over the lazy dog. The quick brown fox jumps over the l= +azy dog.=20 + +Here's some **bold** markdown text. + +Here's a link http://example.com + + +> On Nov 28, 2014, at 12:35 PM, Arpit Jalan <info@discourse.org> wrote: +>=20 +>=20 +> techAPJ +> November 28 +> Test reply. +>=20 +> First paragraph. +>=20 +> Second paragraph. +>=20 +> To respond, reply to this email or visit https://meta.discourse.org/t/test= +ing-default-email-replies/22638/3 in your browser. +>=20 +> Previous Replies +>=20 +> codinghorror +> November 28 +> We're testing the latest GitHub email processing library which we are inte= +grating now. +>=20 +> https://github.com/github/email_reply_parser +>=20 +> Go ahead and reply to this topic and I'll reply from various email clients= + for testing. +>=20 +> To respond, reply to this email or visit https://meta.discourse.org/t/test= +ing-default-email-replies/22638/3 in your browser. +>=20 +> To unsubscribe from these emails, visit your user preferences. + +--Apple-Mail-B41C7F8E-3639-49B0-A5D5-440E125A7105 +Content-Type: text/html; + charset=utf-8 +Content-Transfer-Encoding: 7bit + +<html><head><meta http-equiv="content-type" content="text/html; charset=utf-8"></head><body dir="auto"><div>### this is a reply from iOS default mail</div><div><br></div><div>The quick brown fox jumps over the lazy dog. <span style="background-color: rgba(255, 255, 255, 0);">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. </span></div><div><br></div><div>Here's some **bold** markdown text.</div><div><br></div><div>Here's a link <a href="http://example.com">http://example.com</a><br><br></div><div><br>On Nov 28, 2014, at 12:35 PM, Arpit Jalan <<a href="mailto:info@discourse.org">info@discourse.org</a>> wrote:<br><br></div><blockquote type="cite"><div><div> + +<table style="margin-bottom:25px;" cellspacing="0" cellpadding="0" border="0"> + <tbody> + <tr> + <td style="vertical-align:top;width:55px;"> + <img src="https://meta-discourse.global.ssl.fastly.net/user_avatar/meta.discourse.org/techapj/45/3281.png" title="techAPJ" style="max-width:100%;" width="45" height="45"> + </td> + <td> + <a href="https://meta.discourse.org/users/techapj" target="_blank" style="text-decoration: none; font-weight: bold; color: #006699;; font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;text-decoration:none;font-weight:bold">techAPJ</a><br> + <span style="text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px">November 28</span> + </td> + </tr> + <tr> + <td style="padding-top:5px;" colspan="2"> +<p style="margin-top:0; border: 0;">Test reply.</p> + +<p style="margin-top:0; border: 0;">First paragraph.</p> + +<p style="margin-top:0; border: 0;">Second paragraph.</p> +</td> + </tr> + </tbody> +</table> + + + <div style="color:#666;"> + <p>To respond, reply to this email or visit <a href="https://meta.discourse.org/t/testing-default-email-replies/22638/3" style="text-decoration: none; font-weight: bold; color: #006699;; color:#666;">https://meta.discourse.org/t/testing-default-email-replies/22638/3</a> in your browser.</p> + </div> + <hr style="background-color: #ddd; height: 1px; border: 1px;; background-color: #ddd; height: 1px; border: 1px;"> + <h4>Previous Replies</h4> + + <table style="margin-bottom:25px;" cellspacing="0" cellpadding="0" border="0"> + <tbody> + <tr> + <td style="vertical-align:top;width:55px;"> + <img src="https://meta-discourse.global.ssl.fastly.net/user_avatar/meta.discourse.org/codinghorror/45/5297.png" title="codinghorror" style="max-width:100%;" width="45" height="45"> + </td> + <td> + <a href="https://meta.discourse.org/users/codinghorror" target="_blank" style="text-decoration: none; font-weight: bold; color: #006699;; font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;text-decoration:none;font-weight:bold">codinghorror</a><br> + <span style="text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px">November 28</span> + </td> + </tr> + <tr> + <td style="padding-top:5px;" colspan="2"> +<p style="margin-top:0; border: 0;">We're testing the latest GitHub email processing library which we are integrating now.</p> + +<p style="margin-top:0; border: 0;"><a href="https://github.com/github/email_reply_parser" target="_blank" style="text-decoration: none; font-weight: bold; color: #006699;">https://github.com/github/email_reply_parser</a></p> + +<p style="margin-top:0; border: 0;">Go ahead and reply to this topic and I'll reply from various email clients for testing.</p> +</td> + </tr> + </tbody> +</table> + + +<hr style="background-color: #ddd; height: 1px; border: 1px;; background-color: #ddd; height: 1px; border: 1px;"> + +<div style="color:#666;"> +<p>To respond, reply to this email or visit <a href="https://meta.discourse.org/t/testing-default-email-replies/22638/3" style="text-decoration: none; font-weight: bold; color: #006699;; color:#666;">https://meta.discourse.org/t/testing-default-email-replies/22638/3</a> in your browser.</p> +</div> +<div style="color:#666;"> +<p>To unsubscribe from these emails, visit your <a href="https://meta.discourse.org/my/preferences" style="text-decoration: none; font-weight: bold; color: #006699;; color:#666;">user preferences</a>.</p> +</div> +</div> +</div></blockquote></body></html> +--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 00000000000..cf03b9d18bc --- /dev/null +++ b/spec/fixtures/emails/newlines.eml @@ -0,0 +1,84 @@ +In-Reply-To: <test@discourse-app.mail> +Date: Wed, 8 Oct 2014 10:36:19 +0530 +Delivered-To: walter.white@googlemail.com +Subject: Re: [Discourse] Welcome to Discourse +From: Walter White <walter.white@googlemail.com> +To: Discourse <mail@arpitjalan.com> +Content-Type: multipart/alternative; boundary=bcaec554078cc3d0c10504e24661 + +--bcaec554078cc3d0c10504e24661 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +This is my reply. +It is my best reply. +It will also be my *only* reply. + +On Wed, Oct 8, 2014 at 10:33 AM, ajalan <info@unconfigured.discourse.org> +wrote: + +> ajalan +> <http://mandrillapp.com/track/click/30081177/discourse.techapj.com?p=3Dey= +JzIjoiMGM3a1pGT250VG5sb242RVNTdFdjS1FUSHdzIiwidiI6MSwicCI6IntcInVcIjozMDA4M= +TE3NyxcInZcIjoxLFwidXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29t= +XFxcL3VzZXJzXFxcL2FqYWxhblwiLFwiaWRcIjpcImQxOWYxYjQ5NTdkODRkMGNhZWY1NDEzZGN= +hODA4YTRhXCIsXCJ1cmxfaWRzXCI6W1wiNzA3MTNjNTg4MDI3YWQyM2RiM2QwOTVhOGQwYmY4ZT= +YyMzNjYThiMFwiXX0ifQ> +> October 8 +> +> Awesome! Thank You! [image: +1] +> +> To respond, reply to this email or visit +> http://discourse.techapj.com/t/welcome-to-discourse/8/2 +> <http://mandrillapp.com/track/click/30081177/discourse.techapj.com?p=3Dey= +JzIjoibzNWaXFDRDdxSFNCbVRkUmdONlRJVW1ENU8wIiwidiI6MSwicCI6IntcInVcIjozMDA4M= +TE3NyxcInZcIjoxLFwidXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29t= +XFxcL3RcXFwvd2VsY29tZS10by1kaXNjb3Vyc2VcXFwvOFxcXC8yXCIsXCJpZFwiOlwiZDE5ZjF= +iNDk1N2Q4NGQwY2FlZjU0MTNkY2E4MDhhNGFcIixcInVybF9pZHNcIjpbXCIwYmFkNjE2NDJkNm= +M2NzJhNGU0ZjYzMGU2ZDA5M2I3MzU3NzQ4MzYxXCJdfSJ9> +> in your browser. +> ------------------------------ +> Previous Replies system +> <http://mandrillapp.com/track/click/30081177/discourse.techapj.com?p=3Dey= +JzIjoicjFZQm8ySTJjUEtNclpvekZ5ZmFqYmdpTVFNIiwidiI6MSwicCI6IntcInVcIjozMDA4M= +TE3NyxcInZcIjoxLFwidXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29t= +XFxcL3VzZXJzXFxcL3N5c3RlbVwiLFwiaWRcIjpcImQxOWYxYjQ5NTdkODRkMGNhZWY1NDEzZGN= +hODA4YTRhXCIsXCJ1cmxfaWRzXCI6W1wiMTcxNWU2OTE1M2UzMjk4YmM2Y2NhMWEyM2E5N2ViMW= +U5N2IwMWYyNFwiXX0ifQ> +> October 8 +> +> The first paragraph of this pinned topic will be visible as a welcome +> message to all new visitors on your homepage. It's important! +> +> *Edit this* into a brief description of your community: +> +> - Who is it for? +> - What can they find here? +> - Why should they come here? +> - Where can they read more (links, resources, etc)? +> +> You may want to close this topic via the wrench icon at the upper right, +> so that replies don't pile up on an announcement. +> ------------------------------ +> +> To respond, reply to this email or visit +> http://discourse.techapj.com/t/welcome-to-discourse/8/2 +> <http://mandrillapp.com/track/click/30081177/discourse.techapj.com?p=3Dey= +JzIjoibzNWaXFDRDdxSFNCbVRkUmdONlRJVW1ENU8wIiwidiI6MSwicCI6IntcInVcIjozMDA4M= +TE3NyxcInZcIjoxLFwidXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29t= +XFxcL3RcXFwvd2VsY29tZS10by1kaXNjb3Vyc2VcXFwvOFxcXC8yXCIsXCJpZFwiOlwiZDE5ZjF= +iNDk1N2Q4NGQwY2FlZjU0MTNkY2E4MDhhNGFcIixcInVybF9pZHNcIjpbXCIwYmFkNjE2NDJkNm= +M2NzJhNGU0ZjYzMGU2ZDA5M2I3MzU3NzQ4MzYxXCJdfSJ9> +> in your browser. +> +> To unsubscribe from these emails, visit your user preferences +> <http://mandrillapp.com/track/click/30081177/discourse.techapj.com?p=3Dey= +JzIjoiaFdWSWtiRGIybjJOeWc0VHRrenAzbnhraU93IiwidiI6MSwicCI6IntcInVcIjozMDA4M= +TE3NyxcInZcIjoxLFwidXJsXCI6XCJodHRwOlxcXC9cXFwvZGlzY291cnNlLnRlY2hhcGouY29t= +XFxcL215XFxcL3ByZWZlcmVuY2VzXCIsXCJpZFwiOlwiZDE5ZjFiNDk1N2Q4NGQwY2FlZjU0MTN= +kY2E4MDhhNGFcIixcInVybF9pZHNcIjpbXCI0OTIyMmMyZDgyNzUwMmQyMGZjYzU4MTZkNjhmYT= +k3NzFkY2YzZDllXCJdfSJ9> +> . +> + +--bcaec554078cc3d0c10504e24661 diff --git a/spec/fixtures/emails/no_content_reply.eml b/spec/fixtures/emails/no_content_reply.eml new file mode 100644 index 00000000000..95eb2055ce6 --- /dev/null +++ b/spec/fixtures/emails/no_content_reply.eml @@ -0,0 +1,34 @@ +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+59d8df8370b7e95c5a49fbf86aeb2c93@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+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +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 + +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). +>
\ No newline at end of file diff --git a/spec/fixtures/emails/on_wrote.eml b/spec/fixtures/emails/on_wrote.eml new file mode 100644 index 00000000000..feb59bd27bb --- /dev/null +++ b/spec/fixtures/emails/on_wrote.eml @@ -0,0 +1,277 @@ + +MIME-Version: 1.0 +Received: by 10.107.9.17 with HTTP; Tue, 9 Sep 2014 16:18:19 -0700 (PDT) +In-Reply-To: <540f16d4c08d9_4a3f9ff6d61890391c@tiefighter4-meta.mail> +References: <topic/18058@meta.discourse.org> + <540f16d4c08d9_4a3f9ff6d61890391c@tiefighter4-meta.mail> +Date: Tue, 9 Sep 2014 16:18:19 -0700 +Delivered-To: kanepyork@gmail.com +Message-ID: <CABeNrKXxfb8YJUWxO5L_oPTGrFsiZfQOpWudk+44Mh=yuUEHNQ@mail.gmail.com> +Subject: Re: [Discourse Meta] Badge icons - where to find them? +From: Kane York <jake@adventuretime.ooo> +To: Discourse Meta <reply+8305e3604ae4d1485dc12b6af6a8446c@appmail.adventuretime.ooo> +Content-Type: multipart/alternative; boundary=001a11c34c389e728f0502aa26a0 + +--001a11c34c389e728f0502aa26a0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +Sure, all you need to do is frobnicate the foobar and you'll be all set! + +On Tue, Sep 9, 2014 at 8:03 AM, gordon_ryan <info@discourse.org> wrote: + +> gordon_ryan <https://meta.discourse.org/users/gordon_ryan> +> September 9 +> +> @riking <https://meta.discourse.org/users/riking>- willing to step by +> step of the custom icon method for an admittedly ignorant admin? Seriousl= +y +> confused. +> +> Or anyone else who knows how to do this [image: smiley] +> +> To respond, reply to this email or visit +> https://meta.discourse.org/t/badge-icons-where-to-find-them/18058/9 in +> your browser. +> ------------------------------ +> Previous Replies riking <https://meta.discourse.org/users/riking> +> July 25 +> +> Check out the "HTML Head" section in the "Content" tab of the admin panel= +. +> meglio <https://meta.discourse.org/users/meglio> +> July 25 +> +> How will it load the related custom font? +> riking <https://meta.discourse.org/users/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 example. +> +> .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 stylesheet: +> +> .fa-custom-burger:before { +> content: "\01f354"; +> font-family: inherit; +> } +> +> And get =F0=9F=8D=94 as your badge icon when you enter custom-burger. +> ------------------------------ +> +> To respond, reply to this email or visit +> https://meta.discourse.org/t/badge-icons-where-to-find-them/18058/9 in +> your browser. +> +> To unsubscribe from these emails, visit your user preferences +> <https://meta.discourse.org/my/preferences>. +> + +--001a11c34c389e728f0502aa26a0 +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: quoted-printable + +<div dir=3D"ltr"><span style=3D"font-family:arial,sans-serif;font-size:13px= +">Sure, all you need to do is frobnicate the foobar and you'll be all s= +et!</span><br><div class=3D"gmail_extra"><br clear=3D"all"><div><br>= +<br><div class=3D"gmail_quote">On Tue, Sep 9, 2014 at 8:03 AM, gordon_ryan = +<span dir=3D"ltr"><<a href=3D"mailto:info@discourse.org" target=3D"_blan= +k">info@discourse.org</a>></span> wrote:<br><blockquote class=3D"gmail_q= +uote" style=3D"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1e= +x"><div> + + +<table style=3D"margin-bottom:25px;max-width:761px" cellspacing=3D"0" cellp= +adding=3D"0" border=3D"0"> + <tbody> + <tr> + <td style=3D"vertical-align:top;width:55px"> + <img src=3D"https://meta-discourse.global.ssl.fastly.net/user_avata= +r/meta.discourse.org/gordon_ryan/45/34017.png" title=3D"gordon_ryan" style= +=3D"max-width:694px" width=3D"45" height=3D"45"> + </td> + <td> + <a href=3D"https://meta.discourse.org/users/gordon_ryan" style=3D"f= +ont-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans= +-serif;color:#3b5998;text-decoration:none;font-weight:bold;text-decoration:= +none;font-weight:bold;color:#006699" target=3D"_blank">gordon_ryan</a><br> + <span style=3D"text-align:right;color:#999999;padding-right:5px;fon= +t-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:= +11px">September 9</span> + </td> + </tr> + <tr> + <td style=3D"padding-top:5px" colspan=3D"2"> +<p style=3D"margin-top:0;border:0"><a href=3D"https://meta.discourse.org/us= +ers/riking" style=3D"text-decoration:none;font-weight:bold;color:#006699" t= +arget=3D"_blank">@riking</a>- willing to step by step of the custom icon me= +thod for an admittedly ignorant admin? Seriously confused.</p> + +<p style=3D"margin-top:0;border:0">Or anyone else who knows how to do this = +<img src=3D"https://meta-discourse.global.ssl.fastly.net/plugins/emoji/imag= +es/smiley.png" title=3D":smiley:" alt=3D"smiley" width=3D"20" height=3D"20"= +></p> +</td> + </tr> + </tbody> +</table> + + + <div style=3D"color:#666"> + <p>To respond, reply to this email or visit <a href=3D"https://meta.dis= +course.org/t/badge-icons-where-to-find-them/18058/9" style=3D"color:#666;te= +xt-decoration:none;font-weight:bold;color:#006699" target=3D"_blank">https:= +//meta.discourse.org/t/badge-icons-where-to-find-them/18058/9</a> in your b= +rowser.</p> + </div> + <hr style=3D"background-color:#ddd;min-height:1px;border:1px;background-c= +olor:#ddd;min-height:1px;border:1px"> + <h4>Previous Replies</h4> + + <table style=3D"margin-bottom:25px;max-width:761px" cellspacing=3D"0" cel= +lpadding=3D"0" border=3D"0"> + <tbody> + <tr> + <td style=3D"vertical-align:top;width:55px"> + <img src=3D"https://meta-discourse.global.ssl.fastly.net/user_avata= +r/meta.discourse.org/riking/45/9779.png" title=3D"riking" style=3D"max-widt= +h:694px" width=3D"45" height=3D"45"> + </td> + <td> + <a href=3D"https://meta.discourse.org/users/riking" style=3D"font-s= +ize:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-seri= +f;color:#3b5998;text-decoration:none;font-weight:bold;text-decoration:none;= +font-weight:bold;color:#006699" target=3D"_blank">riking</a><br> + <span style=3D"text-align:right;color:#999999;padding-right:5px;fon= +t-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:= +11px">July 25</span> + </td> + </tr> + <tr> + <td style=3D"padding-top:5px" colspan=3D"2"><p style=3D"margin-top:0;= +border:0">Check out the "HTML Head" section in the "Content&= +quot; tab of the admin panel.</p></td> + </tr> + </tbody> +</table> + + <table style=3D"margin-bottom:25px;max-width:761px" cellspacing=3D"0" cel= +lpadding=3D"0" border=3D"0"> + <tbody> + <tr> + <td style=3D"vertical-align:top;width:55px"> + <img src=3D"https://meta-discourse.global.ssl.fastly.net/user_avata= +r/meta.discourse.org/meglio/45/33480.png" title=3D"meglio" style=3D"max-wid= +th:694px" width=3D"45" height=3D"45"> + </td> + <td> + <a href=3D"https://meta.discourse.org/users/meglio" style=3D"font-s= +ize:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-seri= +f;color:#3b5998;text-decoration:none;font-weight:bold;text-decoration:none;= +font-weight:bold;color:#006699" target=3D"_blank">meglio</a><br> + <span style=3D"text-align:right;color:#999999;padding-right:5px;fon= +t-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:= +11px">July 25</span> + </td> + </tr> + <tr> + <td style=3D"padding-top:5px" colspan=3D"2"><p style=3D"margin-top:0;= +border:0">How will it load the related custom font?</p></td> + </tr> + </tbody> +</table> + + <table style=3D"margin-bottom:25px;max-width:761px" cellspacing=3D"0" cel= +lpadding=3D"0" border=3D"0"> + <tbody> + <tr> + <td style=3D"vertical-align:top;width:55px"> + <img src=3D"https://meta-discourse.global.ssl.fastly.net/user_avata= +r/meta.discourse.org/riking/45/9779.png" title=3D"riking" style=3D"max-widt= +h:694px" width=3D"45" height=3D"45"> + </td> + <td> + <a href=3D"https://meta.discourse.org/users/riking" style=3D"font-s= +ize:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-seri= +f;color:#3b5998;text-decoration:none;font-weight:bold;text-decoration:none;= +font-weight:bold;color:#006699" target=3D"_blank">riking</a><br> + <span style=3D"text-align:right;color:#999999;padding-right:5px;fon= +t-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:= +11px">July 25</span> + </td> + </tr> + <tr> + <td style=3D"padding-top:5px" colspan=3D"2"> +<p style=3D"margin-top:0;border:0">Here's an example of the styles that= + FA applies. I'll use <code style=3D"background-color:#f1f1ff;padding:2= +px 5px"><i class=3D"fa fa-heart"></i></code> as the e= +xample.</p> + +<p style=3D"margin-top:0;border:0"></p> +<pre style=3D"word-wrap:break-word;max-width:694px"><code style=3D"backgrou= +nd-color:#f1f1ff;padding:2px 5px;display:block;background-color:#f1f1ff;pad= +ding:5px">.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"; +}</code></pre> + +<p style=3D"margin-top:0;border:0">So you could do this in your site styles= +heet:</p> + +<p style=3D"margin-top:0;border:0"></p> +<pre style=3D"word-wrap:break-word;max-width:694px"><code style=3D"backgrou= +nd-color:#f1f1ff;padding:2px 5px;display:block;background-color:#f1f1ff;pad= +ding:5px">.fa-custom-burger:before { + content: "\01f354"; + font-family: inherit; +}</code></pre> + +<p style=3D"margin-top:0;border:0">And get =F0=9F=8D=94 as your badge icon = +when you enter <code style=3D"background-color:#f1f1ff;padding:2px 5px">cus= +tom-burger</code>.</p> +</td> + </tr> + </tbody> +</table> + + +<hr style=3D"background-color:#ddd;min-height:1px;border:1px;background-col= +or:#ddd;min-height:1px;border:1px"> + +<div style=3D"color:#666"> +<p>To respond, reply to this email or visit <a href=3D"https://meta.discour= +se.org/t/badge-icons-where-to-find-them/18058/9" style=3D"color:#666;text-d= +ecoration:none;font-weight:bold;color:#006699" target=3D"_blank">https://me= +ta.discourse.org/t/badge-icons-where-to-find-them/18058/9</a> in your brows= +er.</p> +</div> +<div style=3D"color:#666"> +<p>To unsubscribe from these emails, visit your <a href=3D"https://meta.dis= +course.org/my/preferences" style=3D"color:#666;text-decoration:none;font-we= +ight:bold;color:#006699" target=3D"_blank">user preferences</a>.</p> +</div> +</div> +</blockquote></div><br></div></div> + +--001a11c34c389e728f0502aa26a0--
\ No newline at end of file diff --git a/spec/fixtures/emails/outlook.eml b/spec/fixtures/emails/outlook.eml new file mode 100644 index 00000000000..fb1f590a30e --- /dev/null +++ b/spec/fixtures/emails/outlook.eml @@ -0,0 +1,188 @@ + +MIME-Version: 1.0 +Received: by 10.25.161.144 with HTTP; Tue, 7 Oct 2014 22:17:17 -0700 (PDT) +X-Originating-IP: [117.207.85.84] +In-Reply-To: <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail> +References: <topic/35@discourse.techapj.com> + <5434c8b52bb3a_623ff09fec70f049749@discourse-app.mail> +Date: Wed, 8 Oct 2014 10:47:17 +0530 +Delivered-To: arpit@techapj.com +Message-ID: <CAOJeqne=SJ_LwN4sb-0Y95ejc2OpreVhdmcPn0TnmwSvTCYzzQ@mail.gmail.com> +Subject: Re: [Discourse] [Meta] Welcome to techAPJ's Discourse! +From: Arpit Jalan <arpit@techapj.com> +To: Discourse <mail+e1c7f2a380e33840aeb654f075490bad@arpitjalan.com>Accept-Language: en-US +Content-Language: en-US +X-MS-Has-Attach: +X-MS-TNEF-Correlator: +x-originating-ip: [134.68.31.227] +Content-Type: multipart/alternative; + boundary="_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_" +MIME-Version: 1.0 + +--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_ +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: base64 + +TWljcm9zb2Z0IE91dGxvb2sgMjAxMA0KDQpGcm9tOiBtaWNoYWVsIFttYWlsdG86dGFsa0BvcGVu +bXJzLm9yZ10NClNlbnQ6IE1vbmRheSwgT2N0b2JlciAxMywgMjAxNCA5OjM4IEFNDQpUbzogUG93 +ZXIsIENocmlzDQpTdWJqZWN0OiBbUE1dIFlvdXIgcG9zdCBpbiAiQnVyZ2VyaGF1czogTmV3IHJl +c3RhdXJhbnQgLyBsdW5jaCB2ZW51ZSINCg0KDQptaWNoYWVsPGh0dHA6Ly9jbC5vcGVubXJzLm9y +Zy90cmFjay9jbGljay8zMDAzOTkwNS90YWxrLm9wZW5tcnMub3JnP3A9ZXlKeklqb2liR2xaYTFW +MGVYaENZMDFNUlRGc1VESm1ZelZRTTBabGVqRTRJaXdpZGlJNk1Td2ljQ0k2SW50Y0luVmNJam96 +TURBek9Ua3dOU3hjSW5aY0lqb3hMRndpZFhKc1hDSTZYQ0pvZEhSd2N6cGNYRnd2WEZ4Y0wzUmhi +R3N1YjNCbGJtMXljeTV2Y21kY1hGd3ZkWE5sY25OY1hGd3ZiV2xqYUdGbGJGd2lMRndpYVdSY0lq +cGNJbVExWW1Nd04yTmtORFJqWkRRNE1HTTRZVGcyTXpsalpXSTFOemd6WW1ZMlhDSXNYQ0oxY214 +ZmFXUnpYQ0k2VzF3aVlqaGtPRGcxTWprNU56ZG1aalkxWldZeU5URTNPV1JpTkdZeU1XSTNOekZq +TnpoalpqaGtPRndpWFgwaWZRPg0KT2N0b2JlciAxMw0KDQpodHRwczovL3RhbGsub3Blbm1ycy5v +cmcvdC9idXJnZXJoYXVzLW5ldy1yZXN0YXVyYW50LWx1bmNoLXZlbnVlLzY3Mi8zPGh0dHA6Ly9j +bC5vcGVubXJzLm9yZy90cmFjay9jbGljay8zMDAzOTkwNS90YWxrLm9wZW5tcnMub3JnP3A9ZXlK +eklqb2lVRVJJU1VOeVIzbFZNRGRCVlZocFduUjNXV3g0TVdOc1RXNVpJaXdpZGlJNk1Td2ljQ0k2 +SW50Y0luVmNJam96TURBek9Ua3dOU3hjSW5aY0lqb3hMRndpZFhKc1hDSTZYQ0pvZEhSd2N6cGNY +Rnd2WEZ4Y0wzUmhiR3N1YjNCbGJtMXljeTV2Y21kY1hGd3ZkRnhjWEM5aWRYSm5aWEpvWVhWekxX +NWxkeTF5WlhOMFlYVnlZVzUwTFd4MWJtTm9MWFpsYm5WbFhGeGNMelkzTWx4Y1hDOHpYQ0lzWENK +cFpGd2lPbHdpWkRWaVl6QTNZMlEwTkdOa05EZ3dZemhoT0RZek9XTmxZalUzT0ROaVpqWmNJaXhj +SW5WeWJGOXBaSE5jSWpwYlhDSmlOelppWWprMFpURmlOekk1WlRrMlpUUmxaV000TkdSbU1qUTRN +RE13WWpZeVlXWXlNR00wWENKZGZTSjk+DQoNCkxvb2tzIGxpa2UgeW91ciByZXBseS1ieS1lbWFp +bCB3YXNuJ3QgcHJvY2Vzc2VkIGNvcnJlY3RseSBieSBvdXIgc29mdHdhcmUuIENhbiB5b3UgbGV0 +IG1lIGtub3cgd2hhdCB2ZXJzaW9uL09TIG9mIHdoYXQgZW1haWwgcHJvZ3JhbSB5b3UncmUgdXNp +bmc/IFdlIHdpbGwgd2FudCB0byB0cnkgdG8gZml4IHRoZSBidWcuIDpzbWlsZToNCg0KVGhhbmtz +IQ0KDQoNCl9fX19fX19fX19fX19fX19fX19fX19fX19fX19fX19fDQoNClRvIHJlc3BvbmQsIHJl +cGx5IHRvIHRoaXMgZW1haWwgb3IgdmlzaXQgaHR0cHM6Ly90YWxrLm9wZW5tcnMub3JnL3QveW91 +ci1wb3N0LWluLWJ1cmdlcmhhdXMtbmV3LXJlc3RhdXJhbnQtbHVuY2gtdmVudWUvNjc0LzE8aHR0 +cDovL2NsLm9wZW5tcnMub3JnL3RyYWNrL2NsaWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5vcmc/ +cD1leUp6SWpvaWVYaDJWbnBGTUhSMU1uRm5aRWR1TlhFd01GcFFPVlp0VFZvNElpd2lkaUk2TVN3 +aWNDSTZJbnRjSW5WY0lqb3pNREF6T1Rrd05TeGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9kSFJ3 +Y3pwY1hGd3ZYRnhjTDNSaGJHc3ViM0JsYm0xeWN5NXZjbWRjWEZ3dmRGeGNYQzk1YjNWeUxYQnZj +M1F0YVc0dFluVnlaMlZ5YUdGMWN5MXVaWGN0Y21WemRHRjFjbUZ1ZEMxc2RXNWphQzEyWlc1MVpW +eGNYQzgyTnpSY1hGd3ZNVndpTEZ3aWFXUmNJanBjSW1RMVltTXdOMk5rTkRSalpEUTRNR000WVRn +Mk16bGpaV0kxTnpnelltWTJYQ0lzWENKMWNteGZhV1J6WENJNlcxd2lZamMyWW1JNU5HVXhZamN5 +T1dVNU5tVTBaV1ZqT0RSa1pqSTBPREF6TUdJMk1tRm1NakJqTkZ3aVhYMGlmUT4gaW4geW91ciBi +cm93c2VyLg0KDQpUbyB1bnN1YnNjcmliZSBmcm9tIHRoZXNlIGVtYWlscywgdmlzaXQgeW91ciB1 +c2VyIHByZWZlcmVuY2VzPGh0dHA6Ly9jbC5vcGVubXJzLm9yZy90cmFjay9jbGljay8zMDAzOTkw +NS90YWxrLm9wZW5tcnMub3JnP3A9ZXlKeklqb2lkVXh1V2xnNVZGYzBPV1pXUzBZNGJGZExkbWx5 +V0dzeFRWOXpJaXdpZGlJNk1Td2ljQ0k2SW50Y0luVmNJam96TURBek9Ua3dOU3hjSW5aY0lqb3hM +RndpZFhKc1hDSTZYQ0pvZEhSd2N6cGNYRnd2WEZ4Y0wzUmhiR3N1YjNCbGJtMXljeTV2Y21kY1hG +d3ZiWGxjWEZ3dmNISmxabVZ5Wlc1alpYTmNJaXhjSW1sa1hDSTZYQ0prTldKak1EZGpaRFEwWTJR +ME9EQmpPR0U0TmpNNVkyVmlOVGM0TTJKbU5sd2lMRndpZFhKc1gybGtjMXdpT2x0Y0ltSTRNV1V3 +WmpBMU5EWTVORE0wTnpneU0yRm1NakEyTmpGalpqYzNaR05pTjJOaFl6ZG1NakpjSWwxOUluMD4u +DQoNCg== + +--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_ +Content-Type: text/html; charset="utf-8" +Content-Transfer-Encoding: base64 + +PGh0bWwgeG1sbnM6dj0idXJuOnNjaGVtYXMtbWljcm9zb2Z0LWNvbTp2bWwiIHhtbG5zOm89InVy +bjpzY2hlbWFzLW1pY3Jvc29mdC1jb206b2ZmaWNlOm9mZmljZSIgeG1sbnM6dz0idXJuOnNjaGVt +YXMtbWljcm9zb2Z0LWNvbTpvZmZpY2U6d29yZCIgeG1sbnM6bT0iaHR0cDovL3NjaGVtYXMubWlj +cm9zb2Z0LmNvbS9vZmZpY2UvMjAwNC8xMi9vbW1sIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv +VFIvUkVDLWh0bWw0MCI+DQo8aGVhZD4NCjxtZXRhIGh0dHAtZXF1aXY9IkNvbnRlbnQtVHlwZSIg +Y29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PXV0Zi04Ij4NCjxtZXRhIG5hbWU9IkdlbmVyYXRv +ciIgY29udGVudD0iTWljcm9zb2Z0IFdvcmQgMTQgKGZpbHRlcmVkIG1lZGl1bSkiPg0KPCEtLVtp +ZiAhbXNvXT48c3R5bGU+dlw6KiB7YmVoYXZpb3I6dXJsKCNkZWZhdWx0I1ZNTCk7fQ0Kb1w6KiB7 +YmVoYXZpb3I6dXJsKCNkZWZhdWx0I1ZNTCk7fQ0Kd1w6KiB7YmVoYXZpb3I6dXJsKCNkZWZhdWx0 +I1ZNTCk7fQ0KLnNoYXBlIHtiZWhhdmlvcjp1cmwoI2RlZmF1bHQjVk1MKTt9DQo8L3N0eWxlPjwh +W2VuZGlmXS0tPjxzdHlsZT48IS0tDQovKiBGb250IERlZmluaXRpb25zICovDQpAZm9udC1mYWNl +DQoJe2ZvbnQtZmFtaWx5OkNhbGlicmk7DQoJcGFub3NlLTE6MiAxNSA1IDIgMiAyIDQgMyAyIDQ7 +fQ0KQGZvbnQtZmFjZQ0KCXtmb250LWZhbWlseTpUYWhvbWE7DQoJcGFub3NlLTE6MiAxMSA2IDQg +MyA1IDQgNCAyIDQ7fQ0KLyogU3R5bGUgRGVmaW5pdGlvbnMgKi8NCnAuTXNvTm9ybWFsLCBsaS5N +c29Ob3JtYWwsIGRpdi5Nc29Ob3JtYWwNCgl7bWFyZ2luOjBpbjsNCgltYXJnaW4tYm90dG9tOi4w +MDAxcHQ7DQoJZm9udC1zaXplOjEyLjBwdDsNCglmb250LWZhbWlseToiVGltZXMgTmV3IFJvbWFu +Iiwic2VyaWYiO30NCmE6bGluaywgc3Bhbi5Nc29IeXBlcmxpbmsNCgl7bXNvLXN0eWxlLXByaW9y +aXR5Ojk5Ow0KCWNvbG9yOmJsdWU7DQoJdGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZTt9DQphOnZp +c2l0ZWQsIHNwYW4uTXNvSHlwZXJsaW5rRm9sbG93ZWQNCgl7bXNvLXN0eWxlLXByaW9yaXR5Ojk5 +Ow0KCWNvbG9yOnB1cnBsZTsNCgl0ZXh0LWRlY29yYXRpb246dW5kZXJsaW5lO30NCnANCgl7bXNv +LXN0eWxlLXByaW9yaXR5Ojk5Ow0KCW1zby1tYXJnaW4tdG9wLWFsdDphdXRvOw0KCW1hcmdpbi1y +aWdodDowaW47DQoJbXNvLW1hcmdpbi1ib3R0b20tYWx0OmF1dG87DQoJbWFyZ2luLWxlZnQ6MGlu +Ow0KCWZvbnQtc2l6ZToxMi4wcHQ7DQoJZm9udC1mYW1pbHk6IlRpbWVzIE5ldyBSb21hbiIsInNl +cmlmIjt9DQpzcGFuLkVtYWlsU3R5bGUxOA0KCXttc28tc3R5bGUtdHlwZTpwZXJzb25hbC1yZXBs +eTsNCglmb250LWZhbWlseToiQ2FsaWJyaSIsInNhbnMtc2VyaWYiOw0KCWNvbG9yOiMxRjQ5N0Q7 +fQ0KLk1zb0NocERlZmF1bHQNCgl7bXNvLXN0eWxlLXR5cGU6ZXhwb3J0LW9ubHk7DQoJZm9udC1m +YW1pbHk6IkNhbGlicmkiLCJzYW5zLXNlcmlmIjt9DQpAcGFnZSBXb3JkU2VjdGlvbjENCgl7c2l6 +ZTo4LjVpbiAxMS4waW47DQoJbWFyZ2luOjEuMGluIDEuMGluIDEuMGluIDEuMGluO30NCmRpdi5X +b3JkU2VjdGlvbjENCgl7cGFnZTpXb3JkU2VjdGlvbjE7fQ0KLS0+PC9zdHlsZT48IS0tW2lmIGd0 +ZSBtc28gOV0+PHhtbD4NCjxvOnNoYXBlZGVmYXVsdHMgdjpleHQ9ImVkaXQiIHNwaWRtYXg9IjEw +MjYiIC8+DQo8L3htbD48IVtlbmRpZl0tLT48IS0tW2lmIGd0ZSBtc28gOV0+PHhtbD4NCjxvOnNo +YXBlbGF5b3V0IHY6ZXh0PSJlZGl0Ij4NCjxvOmlkbWFwIHY6ZXh0PSJlZGl0IiBkYXRhPSIxIiAv +Pg0KPC9vOnNoYXBlbGF5b3V0PjwveG1sPjwhW2VuZGlmXS0tPg0KPC9oZWFkPg0KPGJvZHkgbGFu +Zz0iRU4tVVMiIGxpbms9ImJsdWUiIHZsaW5rPSJwdXJwbGUiPg0KPGRpdiBjbGFzcz0iV29yZFNl +Y3Rpb24xIj4NCjxwIGNsYXNzPSJNc29Ob3JtYWwiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTEu +MHB0O2ZvbnQtZmFtaWx5OiZxdW90O0NhbGlicmkmcXVvdDssJnF1b3Q7c2Fucy1zZXJpZiZxdW90 +Oztjb2xvcjojMUY0OTdEIj5NaWNyb3NvZnQgT3V0bG9vayAyMDEwPG86cD48L286cD48L3NwYW4+ +PC9wPg0KPHAgY2xhc3M9Ik1zb05vcm1hbCI+PHNwYW4gc3R5bGU9ImZvbnQtc2l6ZToxMS4wcHQ7 +Zm9udC1mYW1pbHk6JnF1b3Q7Q2FsaWJyaSZxdW90OywmcXVvdDtzYW5zLXNlcmlmJnF1b3Q7O2Nv +bG9yOiMxRjQ5N0QiPjxvOnA+Jm5ic3A7PC9vOnA+PC9zcGFuPjwvcD4NCjxwIGNsYXNzPSJNc29O +b3JtYWwiPjxiPjxzcGFuIHN0eWxlPSJmb250LXNpemU6MTAuMHB0O2ZvbnQtZmFtaWx5OiZxdW90 +O1RhaG9tYSZxdW90OywmcXVvdDtzYW5zLXNlcmlmJnF1b3Q7Ij5Gcm9tOjwvc3Bhbj48L2I+PHNw +YW4gc3R5bGU9ImZvbnQtc2l6ZToxMC4wcHQ7Zm9udC1mYW1pbHk6JnF1b3Q7VGFob21hJnF1b3Q7 +LCZxdW90O3NhbnMtc2VyaWYmcXVvdDsiPiBtaWNoYWVsIFttYWlsdG86dGFsa0BvcGVubXJzLm9y +Z10NCjxicj4NCjxiPlNlbnQ6PC9iPiBNb25kYXksIE9jdG9iZXIgMTMsIDIwMTQgOTozOCBBTTxi +cj4NCjxiPlRvOjwvYj4gUG93ZXIsIENocmlzPGJyPg0KPGI+U3ViamVjdDo8L2I+IFtQTV0gWW91 +ciBwb3N0IGluICZxdW90O0J1cmdlcmhhdXM6IE5ldyByZXN0YXVyYW50IC8gbHVuY2ggdmVudWUm +cXVvdDs8bzpwPjwvbzpwPjwvc3Bhbj48L3A+DQo8cCBjbGFzcz0iTXNvTm9ybWFsIj48bzpwPiZu +YnNwOzwvbzpwPjwvcD4NCjxkaXY+DQo8dGFibGUgY2xhc3M9Ik1zb05vcm1hbFRhYmxlIiBib3Jk +ZXI9IjAiIGNlbGxzcGFjaW5nPSIwIiBjZWxscGFkZGluZz0iMCI+DQo8dGJvZHk+DQo8dHI+DQo8 +dGQgdmFsaWduPSJ0b3AiIHN0eWxlPSJwYWRkaW5nOjBpbiAwaW4gMGluIDBpbiI+PC90ZD4NCjx0 +ZCBzdHlsZT0icGFkZGluZzowaW4gMGluIDBpbiAwaW4iPg0KPHAgY2xhc3M9Ik1zb05vcm1hbCIg +c3R5bGU9Im1hcmdpbi1ib3R0b206MTguNzVwdCI+PGEgaHJlZj0iaHR0cDovL2NsLm9wZW5tcnMu +b3JnL3RyYWNrL2NsaWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5vcmc/cD1leUp6SWpvaWJHbFph +MVYwZVhoQ1kwMU1SVEZzVURKbVl6VlFNMFpsZWpFNElpd2lkaUk2TVN3aWNDSTZJbnRjSW5WY0lq +b3pNREF6T1Rrd05TeGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9kSFJ3Y3pwY1hGd3ZYRnhjTDNS +aGJHc3ViM0JsYm0xeWN5NXZjbWRjWEZ3dmRYTmxjbk5jWEZ3dmJXbGphR0ZsYkZ3aUxGd2lhV1Jj +SWpwY0ltUTFZbU13TjJOa05EUmpaRFE0TUdNNFlUZzJNemxqWldJMU56Z3pZbVkyWENJc1hDSjFj +bXhmYVdSelhDSTZXMXdpWWpoa09EZzFNams1TnpkbVpqWTFaV1l5TlRFM09XUmlOR1l5TVdJM056 +RmpOemhqWmpoa09Gd2lYWDBpZlEiIHRhcmdldD0iX2JsYW5rIj48Yj48c3BhbiBzdHlsZT0iZm9u +dC1zaXplOjEwLjBwdDtmb250LWZhbWlseTomcXVvdDtUYWhvbWEmcXVvdDssJnF1b3Q7c2Fucy1z +ZXJpZiZxdW90Oztjb2xvcjojMDA2Njk5O3RleHQtZGVjb3JhdGlvbjpub25lIj5taWNoYWVsPC9z +cGFuPjwvYj48L2E+PGJyPg0KPHNwYW4gc3R5bGU9ImZvbnQtc2l6ZTo4LjVwdDtmb250LWZhbWls +eTomcXVvdDtUYWhvbWEmcXVvdDssJnF1b3Q7c2Fucy1zZXJpZiZxdW90Oztjb2xvcjojOTk5OTk5 +Ij5PY3RvYmVyIDEzPC9zcGFuPg0KPG86cD48L286cD48L3A+DQo8L3RkPg0KPC90cj4NCjx0cj4N +Cjx0ZCBjb2xzcGFuPSIyIiBzdHlsZT0icGFkZGluZzozLjc1cHQgMGluIDBpbiAwaW4iPg0KPHAg +Y2xhc3M9Ik1zb05vcm1hbCIgc3R5bGU9Im1hcmdpbi1ib3R0b206MTguNzVwdCI+PGEgaHJlZj0i +aHR0cDovL2NsLm9wZW5tcnMub3JnL3RyYWNrL2NsaWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5v +cmc/cD1leUp6SWpvaVVFUklTVU55UjNsVk1EZEJWVmhwV25SM1dXeDRNV05zVFc1Wklpd2lkaUk2 +TVN3aWNDSTZJbnRjSW5WY0lqb3pNREF6T1Rrd05TeGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9k +SFJ3Y3pwY1hGd3ZYRnhjTDNSaGJHc3ViM0JsYm0xeWN5NXZjbWRjWEZ3dmRGeGNYQzlpZFhKblpY +Sm9ZWFZ6TFc1bGR5MXlaWE4wWVhWeVlXNTBMV3gxYm1Ob0xYWmxiblZsWEZ4Y0x6WTNNbHhjWEM4 +elhDSXNYQ0pwWkZ3aU9sd2laRFZpWXpBM1kyUTBOR05rTkRnd1l6aGhPRFl6T1dObFlqVTNPRE5p +WmpaY0lpeGNJblZ5YkY5cFpITmNJanBiWENKaU56WmlZamswWlRGaU56STVaVGsyWlRSbFpXTTRO +R1JtTWpRNE1ETXdZall5WVdZeU1HTTBYQ0pkZlNKOSI+PGI+PHNwYW4gc3R5bGU9ImNvbG9yOiMw +MDY2OTk7dGV4dC1kZWNvcmF0aW9uOm5vbmUiPmh0dHBzOi8vdGFsay5vcGVubXJzLm9yZy90L2J1 +cmdlcmhhdXMtbmV3LXJlc3RhdXJhbnQtbHVuY2gtdmVudWUvNjcyLzM8L3NwYW4+PC9iPjwvYT4N +CjxvOnA+PC9vOnA+PC9wPg0KPHAgc3R5bGU9Im1hcmdpbi10b3A6MGluIj5Mb29rcyBsaWtlIHlv +dXIgcmVwbHktYnktZW1haWwgd2Fzbid0IHByb2Nlc3NlZCBjb3JyZWN0bHkgYnkgb3VyIHNvZnR3 +YXJlLiBDYW4geW91IGxldCBtZSBrbm93IHdoYXQgdmVyc2lvbi9PUyBvZiB3aGF0IGVtYWlsIHBy +b2dyYW0geW91J3JlIHVzaW5nPyBXZSB3aWxsIHdhbnQgdG8gdHJ5IHRvIGZpeCB0aGUgYnVnLiA6 +c21pbGU6PG86cD48L286cD48L3A+DQo8cCBzdHlsZT0ibWFyZ2luLXRvcDowaW4iPlRoYW5rcyE8 +bzpwPjwvbzpwPjwvcD4NCjwvdGQ+DQo8L3RyPg0KPC90Ym9keT4NCjwvdGFibGU+DQo8ZGl2IGNs +YXNzPSJNc29Ob3JtYWwiIGFsaWduPSJjZW50ZXIiIHN0eWxlPSJ0ZXh0LWFsaWduOmNlbnRlciI+ +DQo8aHIgc2l6ZT0iMSIgd2lkdGg9IjEwMCUiIGFsaWduPSJjZW50ZXIiPg0KPC9kaXY+DQo8ZGl2 +Pg0KPHA+PHNwYW4gc3R5bGU9ImNvbG9yOiM2NjY2NjYiPlRvIHJlc3BvbmQsIHJlcGx5IHRvIHRo +aXMgZW1haWwgb3IgdmlzaXQgPGEgaHJlZj0iaHR0cDovL2NsLm9wZW5tcnMub3JnL3RyYWNrL2Ns +aWNrLzMwMDM5OTA1L3RhbGsub3Blbm1ycy5vcmc/cD1leUp6SWpvaWVYaDJWbnBGTUhSMU1uRm5a +RWR1TlhFd01GcFFPVlp0VFZvNElpd2lkaUk2TVN3aWNDSTZJbnRjSW5WY0lqb3pNREF6T1Rrd05T +eGNJblpjSWpveExGd2lkWEpzWENJNlhDSm9kSFJ3Y3pwY1hGd3ZYRnhjTDNSaGJHc3ViM0JsYm0x +eWN5NXZjbWRjWEZ3dmRGeGNYQzk1YjNWeUxYQnZjM1F0YVc0dFluVnlaMlZ5YUdGMWN5MXVaWGN0 +Y21WemRHRjFjbUZ1ZEMxc2RXNWphQzEyWlc1MVpWeGNYQzgyTnpSY1hGd3ZNVndpTEZ3aWFXUmNJ +anBjSW1RMVltTXdOMk5rTkRSalpEUTRNR000WVRnMk16bGpaV0kxTnpnelltWTJYQ0lzWENKMWNt +eGZhV1J6WENJNlcxd2lZamMyWW1JNU5HVXhZamN5T1dVNU5tVTBaV1ZqT0RSa1pqSTBPREF6TUdJ +Mk1tRm1NakJqTkZ3aVhYMGlmUSI+DQo8Yj48c3BhbiBzdHlsZT0iY29sb3I6IzAwNjY5OTt0ZXh0 +LWRlY29yYXRpb246bm9uZSI+aHR0cHM6Ly90YWxrLm9wZW5tcnMub3JnL3QveW91ci1wb3N0LWlu +LWJ1cmdlcmhhdXMtbmV3LXJlc3RhdXJhbnQtbHVuY2gtdmVudWUvNjc0LzE8L3NwYW4+PC9iPjwv +YT4gaW4geW91ciBicm93c2VyLjxvOnA+PC9vOnA+PC9zcGFuPjwvcD4NCjwvZGl2Pg0KPGRpdj4N +CjxwPjxzcGFuIHN0eWxlPSJjb2xvcjojNjY2NjY2Ij5UbyB1bnN1YnNjcmliZSBmcm9tIHRoZXNl +IGVtYWlscywgdmlzaXQgeW91ciA8YSBocmVmPSJodHRwOi8vY2wub3Blbm1ycy5vcmcvdHJhY2sv +Y2xpY2svMzAwMzk5MDUvdGFsay5vcGVubXJzLm9yZz9wPWV5SnpJam9pZFV4dVdsZzVWRmMwT1da +V1MwWTRiRmRMZG1seVdHc3hUVjl6SWl3aWRpSTZNU3dpY0NJNkludGNJblZjSWpvek1EQXpPVGt3 +TlN4Y0luWmNJam94TEZ3aWRYSnNYQ0k2WENKb2RIUndjenBjWEZ3dlhGeGNMM1JoYkdzdWIzQmxi +bTF5Y3k1dmNtZGNYRnd2YlhsY1hGd3ZjSEpsWm1WeVpXNWpaWE5jSWl4Y0ltbGtYQ0k2WENKa05X +SmpNRGRqWkRRMFkyUTBPREJqT0dFNE5qTTVZMlZpTlRjNE0ySm1ObHdpTEZ3aWRYSnNYMmxrYzF3 +aU9sdGNJbUk0TVdVd1pqQTFORFk1TkRNME56Z3lNMkZtTWpBMk5qRmpaamMzWkdOaU4yTmhZemRt +TWpKY0lsMTlJbjAiPg0KPGI+PHNwYW4gc3R5bGU9ImNvbG9yOiMwMDY2OTk7dGV4dC1kZWNvcmF0 +aW9uOm5vbmUiPnVzZXIgcHJlZmVyZW5jZXM8L3NwYW4+PC9iPjwvYT4uPG86cD48L286cD48L3Nw +YW4+PC9wPg0KPC9kaXY+DQo8L2Rpdj4NCjxwIGNsYXNzPSJNc29Ob3JtYWwiPjxpbWcgYm9yZGVy +PSIwIiB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBpZD0iX3gwMDAwX2kxMDI2IiBzcmM9Imh0dHA6Ly9j +bC5vcGVubXJzLm9yZy90cmFjay9vcGVuLnBocD91PTMwMDM5OTA1JmFtcDtpZD1kNWJjMDdjZDQ0 +Y2Q0ODBjOGE4NjM5Y2ViNTc4M2JmNiI+PG86cD48L286cD48L3A+DQo8L2Rpdj4NCjwvYm9keT4N +CjwvaHRtbD4NCg== + +--_000_B0DFE1BEB3739743BC9B639D0E6BC8FF217A6341IUMSSGMBX104ads_-- diff --git a/spec/fixtures/emails/paragraphs.eml b/spec/fixtures/emails/paragraphs.eml new file mode 100644 index 00000000000..2d5b5283f7e --- /dev/null +++ b/spec/fixtures/emails/paragraphs.eml @@ -0,0 +1,42 @@ +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+59d8df8370b7e95c5a49fbf86aeb2c93@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+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +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 + +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. + +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). +> diff --git a/spec/fixtures/emails/plaintext_only.eml b/spec/fixtures/emails/plaintext_only.eml new file mode 100644 index 00000000000..1bfaec771dc --- /dev/null +++ b/spec/fixtures/emails/plaintext_only.eml @@ -0,0 +1,42 @@ +Delivered-To: reply@discourse.org +Return-Path: <walter.white@googlemail.com> +MIME-Version: 1.0 +From: <walter.white@googlemail.com> +To: + =?utf-8?Q?Discourse_Meta?= + <reply@discourse.org> +Subject: + =?utf-8?Q?Re:_[Discourse_Meta]_[Lounge]_Testing_default_email_replies?= +Importance: Normal +Date: Fri, 28 Nov 2014 21:29:10 +0000 +In-Reply-To: <topic/22638/86406@meta.discourse.org> +References: + <topic/22638@meta.discourse.org>,<topic/22638/86406@meta.discourse.org> +Content-Type: text/plain; charset="utf-8" +Content-Transfer-Encoding: base64 + +IyMjIHJlcGx5IGZyb20gZGVmYXVsdCBtYWlsIGNsaWVudCBpbiBXaW5kb3dzIDguMSBNZXRybw0K +DQoNClRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWlj +ayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gg +anVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0 +aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cu +IFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBi +cm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVt +cHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUg +bGF6eSBkb2cuDQoNCg0KVGhpcyBpcyBhICoqYm9sZCoqIHdvcmQgaW4gTWFya2Rvd24NCg0KDQpU +aGlzIGlzIGEgbGluayBodHRwOi8vZXhhbXBsZS5jb20NCiANCg0KDQoNCg0KDQpGcm9tOiBBcnBp +dCBKYWxhbg0KU2VudDog4oCORnJpZGF54oCOLCDigI5Ob3ZlbWJlcuKAjiDigI4yOOKAjiwg4oCO +MjAxNCDigI4xMuKAjjrigI4zNeKAjiDigI5QTQ0KVG86IGplZmYgYXR3b29kDQoNCg0KDQoNCg0K +DQogdGVjaEFQSg0KTm92ZW1iZXIgMjggDQoNClRlc3QgcmVwbHkuDQoNCkZpcnN0IHBhcmFncmFw +aC4NCg0KU2Vjb25kIHBhcmFncmFwaC4NCg0KDQoNClRvIHJlc3BvbmQsIHJlcGx5IHRvIHRoaXMg +ZW1haWwgb3IgdmlzaXQgaHR0cHM6Ly9tZXRhLmRpc2NvdXJzZS5vcmcvdC90ZXN0aW5nLWRlZmF1 +bHQtZW1haWwtcmVwbGllcy8yMjYzOC8zIGluIHlvdXIgYnJvd3Nlci4NCg0KDQoNClByZXZpb3Vz +IFJlcGxpZXMNCg0KIGNvZGluZ2hvcnJvcg0KTm92ZW1iZXIgMjggDQoNCldlJ3JlIHRlc3Rpbmcg +dGhlIGxhdGVzdCBHaXRIdWIgZW1haWwgcHJvY2Vzc2luZyBsaWJyYXJ5IHdoaWNoIHdlIGFyZSBp +bnRlZ3JhdGluZyBub3cuDQoNCmh0dHBzOi8vZ2l0aHViLmNvbS9naXRodWIvZW1haWxfcmVwbHlf +cGFyc2VyDQoNCkdvIGFoZWFkIGFuZCByZXBseSB0byB0aGlzIHRvcGljIGFuZCBJJ2xsIHJlcGx5 +IGZyb20gdmFyaW91cyBlbWFpbCBjbGllbnRzIGZvciB0ZXN0aW5nLg0KDQoNCg0KDQoNClRvIHJl +c3BvbmQsIHJlcGx5IHRvIHRoaXMgZW1haWwgb3IgdmlzaXQgaHR0cHM6Ly9tZXRhLmRpc2NvdXJz +ZS5vcmcvdC90ZXN0aW5nLWRlZmF1bHQtZW1haWwtcmVwbGllcy8yMjYzOC8zIGluIHlvdXIgYnJv +d3Nlci4NCg0KDQpUbyB1bnN1YnNjcmliZSBmcm9tIHRoZXNlIGVtYWlscywgdmlzaXQgeW91ciB1 +c2VyIHByZWZlcmVuY2VzLg== diff --git a/spec/fixtures/emails/valid_reply.eml b/spec/fixtures/emails/valid_reply.eml new file mode 100644 index 00000000000..1e696389954 --- /dev/null +++ b/spec/fixtures/emails/valid_reply.eml @@ -0,0 +1,40 @@ +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+59d8df8370b7e95c5a49fbf86aeb2c93@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+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +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). +>
\ No newline at end of file diff --git a/spec/fixtures/emails/windows_8_metro.eml b/spec/fixtures/emails/windows_8_metro.eml new file mode 100644 index 00000000000..67d204af562 --- /dev/null +++ b/spec/fixtures/emails/windows_8_metro.eml @@ -0,0 +1,173 @@ +Delivered-To: reply@discourse.org +Return-Path: <walter.white@googlemail.com> +MIME-Version: 1.0 +From: <walter.white@googlemail.com> +To: + =?utf-8?Q?Discourse_Meta?= + <reply@discourse.org> +Subject: + =?utf-8?Q?Re:_[Discourse_Meta]_[Lounge]_Testing_default_email_replies?= +Importance: Normal +Date: Fri, 28 Nov 2014 21:29:10 +0000 +In-Reply-To: <topic/22638/86406@meta.discourse.org> +References: + <topic/22638@meta.discourse.org>,<topic/22638/86406@meta.discourse.org> +Content-Type: multipart/alternative; + boundary="_866E2678-BB4F-4DD8-BE18-81B04AD8D1BC_" + +--_866E2678-BB4F-4DD8-BE18-81B04AD8D1BC_ +Content-Transfer-Encoding: base64 +Content-Type: text/plain; charset="utf-8" + +IyMjIHJlcGx5IGZyb20gZGVmYXVsdCBtYWlsIGNsaWVudCBpbiBXaW5kb3dzIDguMSBNZXRybw0K +DQoNClRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWlj +ayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gg +anVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0 +aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cu +IFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBi +cm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVt +cHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUg +bGF6eSBkb2cuDQoNCg0KVGhpcyBpcyBhICoqYm9sZCoqIHdvcmQgaW4gTWFya2Rvd24NCg0KDQpU +aGlzIGlzIGEgbGluayBodHRwOi8vZXhhbXBsZS5jb20NCiANCg0KDQoNCg0KDQpGcm9tOiBBcnBp +dCBKYWxhbg0KU2VudDog4oCORnJpZGF54oCOLCDigI5Ob3ZlbWJlcuKAjiDigI4yOOKAjiwg4oCO +MjAxNCDigI4xMuKAjjrigI4zNeKAjiDigI5QTQ0KVG86IGplZmYgYXR3b29kDQoNCg0KDQoNCg0K +DQogdGVjaEFQSg0KTm92ZW1iZXIgMjggDQoNClRlc3QgcmVwbHkuDQoNCkZpcnN0IHBhcmFncmFw +aC4NCg0KU2Vjb25kIHBhcmFncmFwaC4NCg0KDQoNClRvIHJlc3BvbmQsIHJlcGx5IHRvIHRoaXMg +ZW1haWwgb3IgdmlzaXQgaHR0cHM6Ly9tZXRhLmRpc2NvdXJzZS5vcmcvdC90ZXN0aW5nLWRlZmF1 +bHQtZW1haWwtcmVwbGllcy8yMjYzOC8zIGluIHlvdXIgYnJvd3Nlci4NCg0KDQoNClByZXZpb3Vz +IFJlcGxpZXMNCg0KIGNvZGluZ2hvcnJvcg0KTm92ZW1iZXIgMjggDQoNCldlJ3JlIHRlc3Rpbmcg +dGhlIGxhdGVzdCBHaXRIdWIgZW1haWwgcHJvY2Vzc2luZyBsaWJyYXJ5IHdoaWNoIHdlIGFyZSBp +bnRlZ3JhdGluZyBub3cuDQoNCmh0dHBzOi8vZ2l0aHViLmNvbS9naXRodWIvZW1haWxfcmVwbHlf +cGFyc2VyDQoNCkdvIGFoZWFkIGFuZCByZXBseSB0byB0aGlzIHRvcGljIGFuZCBJJ2xsIHJlcGx5 +IGZyb20gdmFyaW91cyBlbWFpbCBjbGllbnRzIGZvciB0ZXN0aW5nLg0KDQoNCg0KDQoNClRvIHJl +c3BvbmQsIHJlcGx5IHRvIHRoaXMgZW1haWwgb3IgdmlzaXQgaHR0cHM6Ly9tZXRhLmRpc2NvdXJz +ZS5vcmcvdC90ZXN0aW5nLWRlZmF1bHQtZW1haWwtcmVwbGllcy8yMjYzOC8zIGluIHlvdXIgYnJv +d3Nlci4NCg0KDQpUbyB1bnN1YnNjcmliZSBmcm9tIHRoZXNlIGVtYWlscywgdmlzaXQgeW91ciB1 +c2VyIHByZWZlcmVuY2VzLg== + +--_866E2678-BB4F-4DD8-BE18-81B04AD8D1BC_ +Content-Transfer-Encoding: base64 +Content-Type: text/html; charset="utf-8" + +CjxodG1sPgo8aGVhZD4KPG1ldGEgbmFtZT0iZ2VuZXJhdG9yIiBjb250ZW50PSJXaW5kb3dzIE1h +aWwgMTcuNS45NjAwLjIwNjA1Ij4KPHN0eWxlIGRhdGEtZXh0ZXJuYWxzdHlsZT0idHJ1ZSI+PCEt +LQpwLk1zb0xpc3RQYXJhZ3JhcGgsIGxpLk1zb0xpc3RQYXJhZ3JhcGgsIGRpdi5Nc29MaXN0UGFy +YWdyYXBoIHsKbWFyZ2luLXRvcDowaW47Cm1hcmdpbi1yaWdodDowaW47Cm1hcmdpbi1ib3R0b206 +MGluOwptYXJnaW4tbGVmdDouNWluOwptYXJnaW4tYm90dG9tOi4wMDAxcHQ7Cn0KcC5Nc29Ob3Jt +YWwsIGxpLk1zb05vcm1hbCwgZGl2Lk1zb05vcm1hbCB7Cm1hcmdpbjowaW47Cm1hcmdpbi1ib3R0 +b206LjAwMDFwdDsKfQpwLk1zb0xpc3RQYXJhZ3JhcGhDeFNwRmlyc3QsIGxpLk1zb0xpc3RQYXJh +Z3JhcGhDeFNwRmlyc3QsIGRpdi5Nc29MaXN0UGFyYWdyYXBoQ3hTcEZpcnN0LCAKcC5Nc29MaXN0 +UGFyYWdyYXBoQ3hTcE1pZGRsZSwgbGkuTXNvTGlzdFBhcmFncmFwaEN4U3BNaWRkbGUsIGRpdi5N +c29MaXN0UGFyYWdyYXBoQ3hTcE1pZGRsZSwgCnAuTXNvTGlzdFBhcmFncmFwaEN4U3BMYXN0LCBs +aS5Nc29MaXN0UGFyYWdyYXBoQ3hTcExhc3QsIGRpdi5Nc29MaXN0UGFyYWdyYXBoQ3hTcExhc3Qg +ewptYXJnaW4tdG9wOjBpbjsKbWFyZ2luLXJpZ2h0OjBpbjsKbWFyZ2luLWJvdHRvbTowaW47Cm1h +cmdpbi1sZWZ0Oi41aW47Cm1hcmdpbi1ib3R0b206LjAwMDFwdDsKbGluZS1oZWlnaHQ6MTE1JTsK +fQotLT48L3N0eWxlPjwvaGVhZD4KPGJvZHkgZGlyPSJsdHIiPgo8ZGl2IGRhdGEtZXh0ZXJuYWxz +dHlsZT0iZmFsc2UiIGRpcj0ibHRyIiBzdHlsZT0iZm9udC1mYW1pbHk6ICdDYWxpYnJpJywgJ1Nl +Z29lIFVJJywgJ01laXJ5bycsICdNaWNyb3NvZnQgWWFIZWkgVUknLCAnTWljcm9zb2Z0IEpoZW5n +SGVpIFVJJywgJ01hbGd1biBHb3RoaWMnLCAnc2Fucy1zZXJpZic7Zm9udC1zaXplOjEycHQ7Ij48 +ZGl2IHN0eWxlPSJmb250LXNpemU6IDE0cHQ7Ij4jIyMgcmVwbHkgZnJvbSBkZWZhdWx0IG1haWwg +Y2xpZW50IGluIFdpbmRvd3MgOC4xIE1ldHJvPC9kaXY+PGRpdiBzdHlsZT0iZm9udC1zaXplOiAx +NHB0OyI+PGJyPjwvZGl2PjxkaXYgc3R5bGU9ImZvbnQtc2l6ZTogMTRwdDsiPlRoZSBxdWljayBi +cm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVt +cHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUg +bGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRo +ZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93 +biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVtcHMg +b3ZlciB0aGUgbGF6eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6 +eSBkb2cuIFRoZSBxdWljayBicm93biBmb3gganVtcHMgb3ZlciB0aGUgbGF6eSBkb2cuPC9kaXY+ +PGRpdiBzdHlsZT0iZm9udC1zaXplOiAxNHB0OyI+PGJyPjwvZGl2PjxkaXYgc3R5bGU9ImZvbnQt +c2l6ZTogMTRwdDsiPlRoaXMgaXMgYSAqKmJvbGQqKiB3b3JkIGluIE1hcmtkb3duPC9kaXY+PGRp +diBzdHlsZT0iZm9udC1zaXplOiAxNHB0OyI+PGJyPjwvZGl2PjxkaXYgc3R5bGU9ImZvbnQtc2l6 +ZTogMTRwdDsiPlRoaXMgaXMgYSBsaW5rIDxhIGhyZWY9Imh0dHA6Ly9leGFtcGxlLmNvbSI+aHR0 +cDovL2V4YW1wbGUuY29tPC9hPjxicj4mbmJzcDs8L2Rpdj48ZGl2IHN0eWxlPSJmb250LXNpemU6 +IDE0cHQ7Ij48YnI+PC9kaXY+PGRpdiBzdHlsZT0icGFkZGluZy10b3A6IDVweDsgYm9yZGVyLXRv +cC1jb2xvcjogcmdiKDIyOSwgMjI5LCAyMjkpOyBib3JkZXItdG9wLXdpZHRoOiAxcHg7IGJvcmRl +ci10b3Atc3R5bGU6IHNvbGlkOyI+PGRpdj48Zm9udCBmYWNlPSIgJ0NhbGlicmknLCAnU2Vnb2Ug +VUknLCAnTWVpcnlvJywgJ01pY3Jvc29mdCBZYUhlaSBVSScsICdNaWNyb3NvZnQgSmhlbmdIZWkg +VUknLCAnTWFsZ3VuIEdvdGhpYycsICdzYW5zLXNlcmlmJyIgc3R5bGU9J2xpbmUtaGVpZ2h0OiAx +NXB0OyBsZXR0ZXItc3BhY2luZzogMC4wMmVtOyBmb250LWZhbWlseTogIkNhbGlicmkiLCAiU2Vn +b2UgVUkiLCAiTWVpcnlvIiwgIk1pY3Jvc29mdCBZYUhlaSBVSSIsICJNaWNyb3NvZnQgSmhlbmdI +ZWkgVUkiLCAiTWFsZ3VuIEdvdGhpYyIsICJzYW5zLXNlcmlmIjsgZm9udC1zaXplOiAxMnB0Oyc+ +PGI+RnJvbTo8L2I+Jm5ic3A7PGEgaHJlZj0ibWFpbHRvOmluZm9AZGlzY291cnNlLm9yZyIgdGFy +Z2V0PSJfcGFyZW50Ij5BcnBpdCBKYWxhbjwvYT48YnI+PGI+U2VudDo8L2I+Jm5ic3A74oCORnJp +ZGF54oCOLCDigI5Ob3ZlbWJlcuKAjiDigI4yOOKAjiwg4oCOMjAxNCDigI4xMuKAjjrigI4zNeKA +jiDigI5QTTxicj48Yj5Ubzo8L2I+Jm5ic3A7PGEgaHJlZj0ibWFpbHRvOmphdHdvb2RAY29kaW5n +aG9ycm9yLmNvbSIgdGFyZ2V0PSJfcGFyZW50Ij5qZWZmIGF0d29vZDwvYT48L2ZvbnQ+PC9kaXY+ +PC9kaXY+PGRpdj48YnI+PC9kaXY+PGRpdiBkaXI9IiI+PGRpdj4KCjx0YWJsZSB0YWJpbmRleD0i +LTEiIHN0eWxlPSJtYXJnaW4tYm90dG9tOiAyNXB4OyIgYm9yZGVyPSIwIiBjZWxsc3BhY2luZz0i +MCIgY2VsbHBhZGRpbmc9IjAiPgogIDx0Ym9keT4KICAgIDx0cj4KICAgICAgPHRkIHN0eWxlPSJ3 +aWR0aDogNTVweDsgdmVydGljYWwtYWxpZ246IHRvcDsiPgogICAgICAgIDxpbWcgd2lkdGg9IjQ1 +IiBoZWlnaHQ9IjQ1IiB0YWJpbmRleD0iLTEiIHN0eWxlPSJtYXgtd2lkdGg6IDEwMCU7IiBzcmM9 +Imh0dHBzOi8vbWV0YS1kaXNjb3Vyc2UuZ2xvYmFsLnNzbC5mYXN0bHkubmV0L3VzZXJfYXZhdGFy +L21ldGEuZGlzY291cnNlLm9yZy90ZWNoYXBqLzQ1LzMyODEucG5nIiBkYXRhLW1zLWltZ3NyYz0i +aHR0cHM6Ly9tZXRhLWRpc2NvdXJzZS5nbG9iYWwuc3NsLmZhc3RseS5uZXQvdXNlcl9hdmF0YXIv +bWV0YS5kaXNjb3Vyc2Uub3JnL3RlY2hhcGovNDUvMzI4MS5wbmciPgogICAgICA8L3RkPgogICAg +ICA8dGQ+CiAgICAgICAgPGEgc3R5bGU9J2NvbG9yOiByZ2IoNTksIDg5LCAxNTIpOyBmb250LWZh +bWlseTogImx1Y2lkYSBncmFuZGUiLHRhaG9tYSx2ZXJkYW5hLGFyaWFsLHNhbnMtc2VyaWY7IGZv +bnQtc2l6ZTogMTNweDsgZm9udC13ZWlnaHQ6IGJvbGQ7IHRleHQtZGVjb3JhdGlvbjogbm9uZTsn +IGhyZWY9Imh0dHBzOi8vbWV0YS5kaXNjb3Vyc2Uub3JnL3VzZXJzL3RlY2hhcGoiIHRhcmdldD0i +X3BhcmVudCI+dGVjaEFQSjwvYT48YnI+CiAgICAgICAgPHNwYW4gc3R5bGU9J3RleHQtYWxpZ246 +IHJpZ2h0OyBjb2xvcjogcmdiKDE1MywgMTUzLCAxNTMpOyBwYWRkaW5nLXJpZ2h0OiA1cHg7IGZv +bnQtZmFtaWx5OiAibHVjaWRhIGdyYW5kZSIsdGFob21hLHZlcmRhbmEsYXJpYWwsc2Fucy1zZXJp +ZjsgZm9udC1zaXplOiAxMXB4Oyc+Tm92ZW1iZXIgMjg8L3NwYW4+CiAgICAgIDwvdGQ+CiAgICA8 +L3RyPgogICAgPHRyPgogICAgICA8dGQgc3R5bGU9InBhZGRpbmctdG9wOiA1cHg7IiBjb2xzcGFu +PSIyIj4KPHAgc3R5bGU9ImJvcmRlcjogMHB4IGJsYWNrOyBib3JkZXItaW1hZ2U6IG5vbmU7IG1h +cmdpbi10b3A6IDBweDsiPlRlc3QgcmVwbHkuPC9wPgoKPHAgc3R5bGU9ImJvcmRlcjogMHB4IGJs +YWNrOyBib3JkZXItaW1hZ2U6IG5vbmU7IG1hcmdpbi10b3A6IDBweDsiPkZpcnN0IHBhcmFncmFw +aC48L3A+Cgo8cCBzdHlsZT0iYm9yZGVyOiAwcHggYmxhY2s7IGJvcmRlci1pbWFnZTogbm9uZTsg +bWFyZ2luLXRvcDogMHB4OyI+U2Vjb25kIHBhcmFncmFwaC48L3A+CjwvdGQ+CiAgICA8L3RyPgog +IDwvdGJvZHk+CjwvdGFibGU+CgoKICA8ZGl2IHN0eWxlPSJjb2xvcjogcmdiKDEwMiwgMTAyLCAx +MDIpOyI+CiAgICA8cD5UbyByZXNwb25kLCByZXBseSB0byB0aGlzIGVtYWlsIG9yIHZpc2l0IDxh +IHN0eWxlPSJjb2xvcjogcmdiKDEwMiwgMTAyLCAxMDIpOyBmb250LXdlaWdodDogYm9sZDsgdGV4 +dC1kZWNvcmF0aW9uOiBub25lOyIgaHJlZj0iaHR0cHM6Ly9tZXRhLmRpc2NvdXJzZS5vcmcvdC90 +ZXN0aW5nLWRlZmF1bHQtZW1haWwtcmVwbGllcy8yMjYzOC8zIiB0YXJnZXQ9Il9wYXJlbnQiPmh0 +dHBzOi8vbWV0YS5kaXNjb3Vyc2Uub3JnL3QvdGVzdGluZy1kZWZhdWx0LWVtYWlsLXJlcGxpZXMv +MjI2MzgvMzwvYT4gaW4geW91ciBicm93c2VyLjwvcD4KICA8L2Rpdj4KICA8aHIgc3R5bGU9ImJv +cmRlcjogMXB4IGJsYWNrOyBib3JkZXItaW1hZ2U6IG5vbmU7IGhlaWdodDogMXB4OyBiYWNrZ3Jv +dW5kLWNvbG9yOiByZ2IoMjIxLCAyMjEsIDIyMSk7Ij4KICA8aDQ+UHJldmlvdXMgUmVwbGllczwv +aDQ+CgogIDx0YWJsZSB0YWJpbmRleD0iLTEiIHN0eWxlPSJtYXJnaW4tYm90dG9tOiAyNXB4OyIg +Ym9yZGVyPSIwIiBjZWxsc3BhY2luZz0iMCIgY2VsbHBhZGRpbmc9IjAiPgogIDx0Ym9keT4KICAg +IDx0cj4KICAgICAgPHRkIHN0eWxlPSJ3aWR0aDogNTVweDsgdmVydGljYWwtYWxpZ246IHRvcDsi +PgogICAgICAgIDxpbWcgd2lkdGg9IjQ1IiBoZWlnaHQ9IjQ1IiB0YWJpbmRleD0iLTEiIHN0eWxl +PSJtYXgtd2lkdGg6IDEwMCU7IiBzcmM9Imh0dHBzOi8vbWV0YS1kaXNjb3Vyc2UuZ2xvYmFsLnNz +bC5mYXN0bHkubmV0L3VzZXJfYXZhdGFyL21ldGEuZGlzY291cnNlLm9yZy9jb2Rpbmdob3Jyb3Iv +NDUvNTI5Ny5wbmciIGRhdGEtbXMtaW1nc3JjPSJodHRwczovL21ldGEtZGlzY291cnNlLmdsb2Jh +bC5zc2wuZmFzdGx5Lm5ldC91c2VyX2F2YXRhci9tZXRhLmRpc2NvdXJzZS5vcmcvY29kaW5naG9y +cm9yLzQ1LzUyOTcucG5nIj4KICAgICAgPC90ZD4KICAgICAgPHRkPgogICAgICAgIDxhIHN0eWxl +PSdjb2xvcjogcmdiKDU5LCA4OSwgMTUyKTsgZm9udC1mYW1pbHk6ICJsdWNpZGEgZ3JhbmRlIix0 +YWhvbWEsdmVyZGFuYSxhcmlhbCxzYW5zLXNlcmlmOyBmb250LXNpemU6IDEzcHg7IGZvbnQtd2Vp +Z2h0OiBib2xkOyB0ZXh0LWRlY29yYXRpb246IG5vbmU7JyBocmVmPSJodHRwczovL21ldGEuZGlz +Y291cnNlLm9yZy91c2Vycy9jb2Rpbmdob3Jyb3IiIHRhcmdldD0iX3BhcmVudCI+Y29kaW5naG9y +cm9yPC9hPjxicj4KICAgICAgICA8c3BhbiBzdHlsZT0ndGV4dC1hbGlnbjogcmlnaHQ7IGNvbG9y +OiByZ2IoMTUzLCAxNTMsIDE1Myk7IHBhZGRpbmctcmlnaHQ6IDVweDsgZm9udC1mYW1pbHk6ICJs +dWNpZGEgZ3JhbmRlIix0YWhvbWEsdmVyZGFuYSxhcmlhbCxzYW5zLXNlcmlmOyBmb250LXNpemU6 +IDExcHg7Jz5Ob3ZlbWJlciAyODwvc3Bhbj4KICAgICAgPC90ZD4KICAgIDwvdHI+CiAgICA8dHI+ +CiAgICAgIDx0ZCBzdHlsZT0icGFkZGluZy10b3A6IDVweDsiIGNvbHNwYW49IjIiPgo8cCBzdHls +ZT0iYm9yZGVyOiAwcHggYmxhY2s7IGJvcmRlci1pbWFnZTogbm9uZTsgbWFyZ2luLXRvcDogMHB4 +OyI+V2UncmUgdGVzdGluZyB0aGUgbGF0ZXN0IEdpdEh1YiBlbWFpbCBwcm9jZXNzaW5nIGxpYnJh +cnkgd2hpY2ggd2UgYXJlIGludGVncmF0aW5nIG5vdy48L3A+Cgo8cCBzdHlsZT0iYm9yZGVyOiAw +cHggYmxhY2s7IGJvcmRlci1pbWFnZTogbm9uZTsgbWFyZ2luLXRvcDogMHB4OyI+PGEgc3R5bGU9 +ImNvbG9yOiByZ2IoMCwgMTAyLCAxNTMpOyBmb250LXdlaWdodDogYm9sZDsgdGV4dC1kZWNvcmF0 +aW9uOiBub25lOyIgaHJlZj0iaHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9lbWFpbF9yZXBseV9w +YXJzZXIiIHRhcmdldD0iX3BhcmVudCI+aHR0cHM6Ly9naXRodWIuY29tL2dpdGh1Yi9lbWFpbF9y +ZXBseV9wYXJzZXI8L2E+PC9wPgoKPHAgc3R5bGU9ImJvcmRlcjogMHB4IGJsYWNrOyBib3JkZXIt +aW1hZ2U6IG5vbmU7IG1hcmdpbi10b3A6IDBweDsiPkdvIGFoZWFkIGFuZCByZXBseSB0byB0aGlz +IHRvcGljIGFuZCBJJ2xsIHJlcGx5IGZyb20gdmFyaW91cyBlbWFpbCBjbGllbnRzIGZvciB0ZXN0 +aW5nLjwvcD4KPC90ZD4KICAgIDwvdHI+CiAgPC90Ym9keT4KPC90YWJsZT4KCgo8aHIgc3R5bGU9 +ImJvcmRlcjogMXB4IGJsYWNrOyBib3JkZXItaW1hZ2U6IG5vbmU7IGhlaWdodDogMXB4OyBiYWNr +Z3JvdW5kLWNvbG9yOiByZ2IoMjIxLCAyMjEsIDIyMSk7Ij4KCjxkaXYgc3R5bGU9ImNvbG9yOiBy +Z2IoMTAyLCAxMDIsIDEwMik7Ij4KPHA+VG8gcmVzcG9uZCwgcmVwbHkgdG8gdGhpcyBlbWFpbCBv +ciB2aXNpdCA8YSBzdHlsZT0iY29sb3I6IHJnYigxMDIsIDEwMiwgMTAyKTsgZm9udC13ZWlnaHQ6 +IGJvbGQ7IHRleHQtZGVjb3JhdGlvbjogbm9uZTsiIGhyZWY9Imh0dHBzOi8vbWV0YS5kaXNjb3Vy +c2Uub3JnL3QvdGVzdGluZy1kZWZhdWx0LWVtYWlsLXJlcGxpZXMvMjI2MzgvMyIgdGFyZ2V0PSJf +cGFyZW50Ij5odHRwczovL21ldGEuZGlzY291cnNlLm9yZy90L3Rlc3RpbmctZGVmYXVsdC1lbWFp +bC1yZXBsaWVzLzIyNjM4LzM8L2E+IGluIHlvdXIgYnJvd3Nlci48L3A+CjwvZGl2Pgo8ZGl2IHN0 +eWxlPSJjb2xvcjogcmdiKDEwMiwgMTAyLCAxMDIpOyI+CjxwPlRvIHVuc3Vic2NyaWJlIGZyb20g +dGhlc2UgZW1haWxzLCB2aXNpdCB5b3VyIDxhIHN0eWxlPSJjb2xvcjogcmdiKDEwMiwgMTAyLCAx +MDIpOyBmb250LXdlaWdodDogYm9sZDsgdGV4dC1kZWNvcmF0aW9uOiBub25lOyIgaHJlZj0iaHR0 +cHM6Ly9tZXRhLmRpc2NvdXJzZS5vcmcvbXkvcHJlZmVyZW5jZXMiIHRhcmdldD0iX3BhcmVudCI+ +dXNlciBwcmVmZXJlbmNlczwvYT4uPC9wPgo8L2Rpdj4KPC9kaXY+CjwvZGl2PjxkaXYgc3R5bGU9 +ImZvbnQtc2l6ZTogMTRwdDsiPjxicj48L2Rpdj48L2Rpdj4KPC9ib2R5Pgo8L2h0bWw+Cg== + +--_866E2678-BB4F-4DD8-BE18-81B04AD8D1BC_-- diff --git a/spec/fixtures/emails/wrong_reply_key.eml b/spec/fixtures/emails/wrong_reply_key.eml new file mode 100644 index 00000000000..491e078fb5b --- /dev/null +++ b/spec/fixtures/emails/wrong_reply_key.eml @@ -0,0 +1,40 @@ +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@discourse.example.com>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@discourse.example.com>; 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+QQd8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +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@discourse.example.com> 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). +>
\ No newline at end of file diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index 76009c36099..b8bba36439a 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -47,5 +47,21 @@ describe BlobHelper do expect(lines[1].text).to eq(' This is line 2.') expect(lines[2].text).to eq(' """') end + + context 'diff highlighting' do + let(:blob_name) { 'test.diff' } + let(:blob_content) { "+aaa\n+bbb\n- ccc\n ddd\n"} + let(:expected) do + %q(<span id="LC1" class="line"><span class="gi">+aaa</span></span> +<span id="LC2" class="line"><span class="gi">+bbb</span></span> +<span id="LC3" class="line"><span class="gd">- ccc</span></span> +<span id="LC4" class="line"> ddd</span>) + end + + it 'should highlight each line properly' do + result = highlight(blob_name, blob_content, nowrap: true, continue: false) + expect(result).to eq(expected) + end + end end end diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb index b392371deb4..da58ab98462 100644 --- a/spec/helpers/events_helper_spec.rb +++ b/spec/helpers/events_helper_spec.rb @@ -58,7 +58,7 @@ describe EventsHelper do expected = '<pre class="code highlight white ruby">' \ "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \ " <span class=\"s1\">\'hello world\'</span>\n" \ - "<span class=\"k\">end</span>\n" \ + "<span class=\"k\">end</span>" \ '</code></pre>' expect(event_note(input)).to eq(expected) end diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb index f8958c9bab8..0e826a319e0 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/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb new file mode 100644 index 00000000000..2e0a05088cc --- /dev/null +++ b/spec/lib/gitlab/diff/inline_diff_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::InlineDiff do + describe '#processing' do + let(:diff) do + <<eos +--- a/test.rb ++++ b/test.rb +@@ -1,6 +1,6 @@ + class Test + def cleanup_string(input) + return nil if input.nil? +- input.sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip ++ input.to_s.sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip + end + end +eos + end + + let(:expected) do + ["--- a/test.rb\n", + "+++ b/test.rb\n", + "@@ -1,6 +1,6 @@\n", + " class Test\n", + " def cleanup_string(input)\n", + " return nil if input.nil?\n", + "- input.#!idiff-start!##!idiff-finish!#sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip\n", + "+ input.#!idiff-start!#to_s.#!idiff-finish!#sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip\n", + " end\n", + " end\n"] + end + + let(:subject) { Gitlab::InlineDiff.processing(diff.lines) } + + it 'should retain backslashes' do + expect(subject).to eq(expected) + 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 00000000000..e8208e15e29 --- /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 00000000000..1cc80f35f98 --- /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 00000000000..7cae1da8050 --- /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 <info@unconfigured.discourse.org> wrote: + + > techAPJ <https://meta.discourse.org/users/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 + > <https://meta.discourse.org/users/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 + > <https://meta.discourse.org/my/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 4fe7bd3b77d..ca61d3c5234 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 938d08396fd..2d8923d14bb 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 6aa4428f367..37985c062b4 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 f49cbb7f532..65ad7524cc2 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 982be0782c9..26332ba5217 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 a5405e14a73..02d923b036c 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(/\(<a.+>#{reference}<\/a>\.\)/) end diff --git a/spec/lib/gitlab/markup_helper_spec.rb b/spec/lib/gitlab/markup_helper_spec.rb index 7e716e866b1..e610fab05da 100644 --- a/spec/lib/gitlab/markup_helper_spec.rb +++ b/spec/lib/gitlab/markup_helper_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::MarkupHelper do end describe '#gitlab_markdown?' do - %w(mdown md markdown).each do |type| + %w(mdown mkd mkdn md markdown).each do |type| it "returns true for #{type} files" do expect(Gitlab::MarkupHelper.gitlab_markdown?("README.#{type}")).to be_truthy end diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb index 4c0a4a49d2a..e4a6cd954cc 100644 --- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb +++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb @@ -91,10 +91,6 @@ describe Gitlab::OAuth::AuthHash do expect(auth_hash.name.encoding).to eql Encoding::UTF_8 end - it 'forces utf8 encoding on full_name' do - expect(auth_hash.full_name.encoding).to eql Encoding::UTF_8 - end - it 'forces utf8 encoding on username' do expect(auth_hash.username.encoding).to eql Encoding::UTF_8 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 00000000000..a678c7e1a76 --- /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/models/note_spec.rb b/spec/models/note_spec.rb index 250d1e2da80..331505a01b3 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -198,4 +198,10 @@ describe Note do let(:backref_text) { issue.gfm_reference } let(:set_mentionable_text) { ->(txt) { subject.note = txt } } end + + describe :search do + let!(:note) { create(:note, note: "WoW") } + + it { expect(Note.search('wow')).to include(note) } + end end diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb index 7e5b15cb09e..16296607a94 100644 --- a/spec/models/project_services/flowdock_service_spec.rb +++ b/spec/models/project_services/flowdock_service_spec.rb @@ -39,15 +39,19 @@ describe FlowdockService do token: 'verySecret' ) @sample_data = Gitlab::PushDataBuilder.build_sample(project, user) - @api_url = 'https://api.flowdock.com/v1/git/verySecret' + @api_url = 'https://api.flowdock.com/v1/messages' WebMock.stub_request(:post, @api_url) end it "should call FlowDock API" do @flowdock_service.execute(@sample_data) - expect(WebMock).to have_requested(:post, @api_url).with( - body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/ - ).once + @sample_data[:commits].each do |commit| + # One request to Flowdock per new commit + next if commit[:id] == @sample_data[:before] + expect(WebMock).to have_requested(:post, @api_url).with( + body: /#{commit[:id]}.*#{project.path}/ + ).once + end end end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 4707673269a..65d16beef91 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -47,6 +47,14 @@ describe HipchatService do WebMock.stub_request(:post, api_url) end + it 'should test and return errors' do + allow(hipchat).to receive(:execute).and_raise(StandardError, 'no such room') + result = hipchat.test(push_sample_data) + + expect(result[:success]).to be_falsey + expect(result[:result].to_s).to eq('no such room') + end + it 'should use v1 if version is provided' do allow(hipchat).to receive(:api_version).and_return('v1') expect(HipChat::Client).to receive(:new). diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index d96244f23e0..05e51532eb8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -34,6 +34,14 @@ describe Repository do end end + describe :merged_to_root_ref? do + context 'merged branch' do + subject { repository.merged_to_root_ref?('improve/awesome') } + + it { is_expected.to be_truthy } + end + end + describe :can_be_merged? do context 'mergeable branches' do subject { repository.can_be_merged?('0b4bc9a49b562e85de7cc9e834518ea6828729b9', 'master') } @@ -46,6 +54,18 @@ describe Repository do it { is_expected.to be_falsey } end + + context 'non merged branch' do + subject { repository.merged_to_root_ref?('fix') } + + it { is_expected.to be_falsey } + end + + context 'non existent branch' do + subject { repository.merged_to_root_ref?('non_existent_branch') } + + it { is_expected.to be_nil } + end end describe "search_files" do diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index ca11758ee06..a213ffe6c4b 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -46,6 +46,16 @@ describe Service do describe :can_test do it { expect(@testable).to eq(true) } end + + describe :test do + let(:data) { 'test' } + + it 'test runs execute' do + expect(@service).to receive(:execute).with(data) + + @service.test(data) + end + end end describe "With commits" do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 876cfb1204a..a46e789eab4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -710,4 +710,33 @@ describe User do it { expect(subject.can_be_removed?).to be_falsey } end end + + describe "#recent_push" do + subject { create(:user) } + let!(:project1) { create(:project) } + let!(:project2) { create(:project, forked_from_project: project1) } + let!(:push_data) { Gitlab::PushDataBuilder.build_sample(project2, subject) } + let!(:push_event) { create(:event, action: Event::PUSHED, project: project2, target: project1, author: subject, data: push_data) } + + before do + project1.team << [subject, :master] + project2.team << [subject, :master] + end + + it "includes push event" do + expect(subject.recent_push).to eq(push_event) + end + + it "excludes push event if branch has been deleted" do + allow_any_instance_of(Repository).to receive(:branch_names).and_return(['foo']) + + expect(subject.recent_push).to eq(nil) + end + + it "excludes push event if MR is opened for it" do + create(:merge_request, source_project: project2, target_project: project1, source_branch: project2.default_branch, target_branch: 'fix', author: subject) + + expect(subject.recent_push).to eq(nil) + end + end end diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb index 8cb8790c339..042e6352567 100644 --- a/spec/requests/api/files_spec.rb +++ b/spec/requests/api/files_spec.rb @@ -117,4 +117,35 @@ describe API::API, api: true do expect(response.status).to eq(400) end end + + describe "POST /projects/:id/repository/files with binary file" do + let(:file_path) { 'test.bin' } + let(:put_params) do + { + file_path: file_path, + branch_name: 'master', + content: 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUAAACnej3aAAAAAXRSTlMAQObYZgAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII=', + commit_message: 'Binary file with a \n should not be touched', + encoding: 'base64' + } + end + let(:get_params) do + { + file_path: file_path, + ref: 'master', + } + end + + before do + post api("/projects/#{project.id}/repository/files", user), put_params + end + + it "remains unchanged" do + get api("/projects/#{project.id}/repository/files", user), get_params + expect(response.status).to eq(200) + expect(json_response['file_path']).to eq(file_path) + expect(json_response['file_name']).to eq(file_path) + expect(json_response['content']).to eq(put_params[:content]) + end + end end diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index 1d5b4f6f36b..13cced81875 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 62cef9db534..c483060fd73 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 + context "to default branches" do + it "closes issues" 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 + 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.not_to change { Note.where(project_id: project.id, system: true, commit_id: closing_commit.id).count } - end + end - it "doesn't close issues when pushed to non-default branches" do - allow(project).to receive(:default_branch).and_return('durf') + it "doesn't create additional cross-reference notes" do + expect(SystemNoteService).not_to receive(:cross_reference) + service.execute(project, user, @oldrev, @newrev, @ref) + 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 "doesn't close issues when external issue tracker is in use" do + allow(project).to receive(:default_issues_tracker?).and_return(false) - expect(Issue.find(issue.id)).to be_opened + # 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 external issue tracker is in use" do - allow(project).to receive(:default_issues_tracker?).and_return(false) + 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 + + 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 - # 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 7aa26857649..fa4ff6b01ad 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 00000000000..a05c9d18002 --- /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 c59df4e84d6..39a64391460 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 e4004ec8f79..ef3a120d44a 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/support/test_env.rb b/spec/support/test_env.rb index 8dc687c3580..3eab74ba986 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -57,6 +57,10 @@ module TestEnv and_call_original end + def disable_pre_receive + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(true) + end + # Clean /tmp/tests # # Keeps gitlab-shell and gitlab-test diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb new file mode 100644 index 00000000000..e8f1bd2fa2f --- /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 00000000000..3600c771075 --- /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 |