diff --git a/CHANGELOG b/CHANGELOG index 09c71a1068..21a096b461 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,16 @@ Please view this file on the master branch, on stable branches it's out of date. v 7.14.0 (unreleased) + - Fix multi-line syntax highlighting (Stan Hu) + - Fix network graph when branch name has single quotes (Stan Hu) + - Upgrade gitlab_git to version 7.2.6 to fix Error 500 when creating network graphs (Stan Hu) + - Add support for Unicode filenames in relative links (Hiroyuki Sato) + - Fix URL used for refreshing notes if relative_url is present (Bartłomiej Święcki) + - Fix commit data retrieval when branch name has single quotes (Stan Hu) + - Check that project was actually created rather than just validated in import:repos task (Stan Hu) - Fix full screen mode for snippet comments (Daniel Gerhardt) - Fix 404 error in files view after deleting the last file in a repository (Stan Hu) + - Fix the "Reload with full diff" URL button (Stan Hu) - Fix label read access for unauthenticated users (Daniel Gerhardt) - Fix access to disabled features for unauthenticated users (Daniel Gerhardt) - Fix OAuth provider bug where GitLab would not go return to the redirect_uri after sign-in (Stan Hu) @@ -10,17 +18,40 @@ v 7.14.0 (unreleased) - Set OmniAuth full_host parameter to ensure redirect URIs are correct (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 + - 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. - Fix bug causing "Remove source-branch" option not to work for merge requests from the same project. - Fix approvals for forks. Now you can change approvals settings on the new merge request form - Suggested approvers are shown on the new merge request form + - Disabled autocapitalize and autocorrect on login field (Daryl Chan) + - Mention group and project name in creation, update and deletion notices (Achilleas Pipinellis) + +v 7.13.2 + - Fix randomly failed spec + - Create project services on Project creation + - Add admin_merge_request ability to Developer level and up + - Fix Error 500 when browsing projects with no HEAD (Stan Hu) + - Fix labels / assignee / milestone for the merge requests when issues are disabled + - Show the first tab automatically on MergeRequests#new + - Add rake task 'gitlab:update_commit_count' (Daniel Gerhardt) + - Fix Gmail Actions v 7.13.1 - - Revert issue caching - - Reverted cache for events + - Fix: Label modifications are not reflected in existing notes and in the issue list + - Fix: Label not shown in the Issue list, although it's set through web interface + - Fix: Group/project references are linked incorrectly + - Improve documentation + - Fix of migration: Check if session_expire_delay column exists before adding the column + - Fix: ActionView::Template::Error + - Fix: "Create Merge Request" isn't always shown in event for newly pushed branch + - Fix bug causing "Remove source-branch" option not to work for merge requests from the same project. + - Render Note field hints consistently for "new" and "edit" forms -v 7.13.0 (unreleased) +v 7.13.0 - Remove repository graph log to fix slow cache updates after push event (Stan Hu) + - Return comments in created order in merge request API (Stan Hu) - Only enable HSTS header for HTTPS and port 443 (Stan Hu) - Fix user autocomplete for unauthenticated users accessing public projects (Stan Hu) - Fix redirection to home page URL for unauthorized users (Daniel Gerhardt) @@ -74,6 +105,7 @@ v 7.12.2 - Faster automerge check and merge itself when source and target branches are in same repository - Audit log for user authentication - Fix transferring of project to another group using the API. + - Allow custom label to be set for authentication providers. v 7.12.1 - Fix error when deleting a user who has projects (Stan Hu) diff --git a/Gemfile b/Gemfile index 14ea25d627..69040d8487 100644 --- a/Gemfile +++ b/Gemfile @@ -38,7 +38,7 @@ gem "browser", '~> 0.8.0' # Extracting information from a git repository # Provide access to Gitlab::Git library -gem "gitlab_git", '~> 7.2.5' +gem "gitlab_git", '~> 7.2.6' # Ruby/Rack Git Smart-HTTP Server Handler # GitLab fork with a lot of changes (improved thread-safety, better memory usage etc) @@ -275,4 +275,3 @@ end gem "newrelic_rpm" gem 'octokit', '3.7.0' -gem "rugments", "~> 1.0.0.beta8" diff --git a/Gemfile.lock b/Gemfile.lock index 789ebba56c..4a8ef53e00 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -272,7 +272,7 @@ GEM mime-types (~> 1.19) gitlab_emoji (0.1.0) gemojione (~> 2.0) - gitlab_git (7.2.5) + gitlab_git (7.2.6) activesupport (~> 4.0) charlock_holmes (~> 0.6) gitlab-linguist (~> 3.0) @@ -289,7 +289,7 @@ GEM github-markup (~> 1.3.1) gollum-grit_adapter (~> 0.1, >= 0.1.1) nokogiri (~> 1.6.4) - rouge (~> 1.7.4) + rouge (~> 1.9) sanitize (~> 2.1.0) stringex (~> 2.5.1) gon (5.0.1) @@ -537,7 +537,7 @@ GEM netrc (~> 0.7) rinku (1.7.3) rotp (1.6.1) - rouge (1.7.7) + rouge (1.9.1) rqrcode (0.4.2) rqrcode-rails3 (0.1.7) rqrcode (>= 0.4.2) @@ -580,7 +580,6 @@ GEM rubyntlm (0.5.0) rubypants (0.2.0) rugged (0.22.2) - rugments (1.0.0.beta8) safe_yaml (1.0.4) sanitize (2.1.0) nokogiri (>= 1.4.4) @@ -786,7 +785,7 @@ DEPENDENCIES gitlab-license (~> 0.0.2) gitlab-linguist (~> 3.0.1) gitlab_emoji (~> 0.1) - gitlab_git (~> 7.2.5) + gitlab_git (~> 7.2.6) gitlab_meta (= 7.0) gitlab_omniauth-ldap (= 1.2.1) gollum-lib (~> 4.0.2) @@ -839,7 +838,6 @@ DEPENDENCIES rqrcode-rails3 rspec-rails (~> 3.3.0) rubocop (= 0.28.0) - rugments (~> 1.0.0.beta8) sanitize (~> 2.0) sass-rails (~> 4.0.5) sdoc diff --git a/app/assets/images/authbuttons/bitbucket_64.png b/app/assets/images/auth_buttons/bitbucket_64.png similarity index 100% rename from app/assets/images/authbuttons/bitbucket_64.png rename to app/assets/images/auth_buttons/bitbucket_64.png diff --git a/app/assets/images/auth_buttons/github_64.png b/app/assets/images/auth_buttons/github_64.png new file mode 100644 index 0000000000..182a1a3f73 Binary files /dev/null and b/app/assets/images/auth_buttons/github_64.png differ diff --git a/app/assets/images/auth_buttons/gitlab_64.png b/app/assets/images/auth_buttons/gitlab_64.png new file mode 100644 index 0000000000..99a40583b3 Binary files /dev/null and b/app/assets/images/auth_buttons/gitlab_64.png differ diff --git a/app/assets/images/authbuttons/google_64.png b/app/assets/images/auth_buttons/google_64.png similarity index 100% rename from app/assets/images/authbuttons/google_64.png rename to app/assets/images/auth_buttons/google_64.png diff --git a/app/assets/images/authbuttons/twitter_64.png b/app/assets/images/auth_buttons/twitter_64.png similarity index 100% rename from app/assets/images/authbuttons/twitter_64.png rename to app/assets/images/auth_buttons/twitter_64.png diff --git a/app/assets/images/authbuttons/github_64.png b/app/assets/images/authbuttons/github_64.png deleted file mode 100644 index dc7c03d100..0000000000 Binary files a/app/assets/images/authbuttons/github_64.png and /dev/null differ diff --git a/app/assets/images/authbuttons/gitlab_64.png b/app/assets/images/authbuttons/gitlab_64.png deleted file mode 100644 index 31281a1944..0000000000 Binary files a/app/assets/images/authbuttons/gitlab_64.png and /dev/null differ diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee index b0e7728b5e..2c5269050a 100644 --- a/app/assets/javascripts/application.js.coffee +++ b/app/assets/javascripts/application.js.coffee @@ -166,9 +166,10 @@ $ -> $('.account-box').hover -> $(@).toggleClass('hover') # Commit show suppressed diff - $(".diff-content").on "click", ".supp_diff_link", -> - $(@).next('table').show() - $(@).remove() + $(document).on 'click', '.diff-content .js-show-suppressed-diff', -> + $container = $(@).parent() + $container.next('table').show() + $container.remove() $('.navbar-toggle').on 'click', -> $('.header-content .title').toggle() diff --git a/app/assets/javascripts/diff.js.coffee b/app/assets/javascripts/diff.js.coffee index 069f91c30e..6d9b364cb8 100644 --- a/app/assets/javascripts/diff.js.coffee +++ b/app/assets/javascripts/diff.js.coffee @@ -31,6 +31,10 @@ class @Diff bottom: unfoldBottom offset: offset unfold: unfold + # indent is used to compensate for single space indent to fit + # '+' and '-' prepended to diff lines, + # see https://gitlab.com/gitlab-org/gitlab-ce/issues/707 + indent: 1 $.get(link, params, (response) => target.parent().replaceWith(response) diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee index a132a0a9dc..19a07b6a03 100644 --- a/app/assets/javascripts/merge_request_tabs.js.coffee +++ b/app/assets/javascripts/merge_request_tabs.js.coffee @@ -49,12 +49,6 @@ class @MergeRequestTabs # Store the `location` object, allowing for easier stubbing in tests @_location = location - switch @opts.action - when 'commits' - @commitsLoaded = true - when 'diffs' - @diffsLoaded = true - @bindEvents() @activateTab(@opts.action) @@ -102,7 +96,7 @@ class @MergeRequestTabs action = 'notes' if action == 'show' # Remove a trailing '/commits' or '/diffs' - new_state = @_location.pathname.replace(/\/(commits|diffs)\/?$/, '') + new_state = @_location.pathname.replace(/\/(commits|diffs)(\.html)?\/?$/, '') # Append the new action if we're on a tab other than 'notes' unless action == 'notes' @@ -133,7 +127,7 @@ class @MergeRequestTabs return if @diffsLoaded @_get - url: "#{source}.json" + url: "#{source}.json" + @_location.search success: (data) => document.getElementById('diffs').innerHTML = data.html @diffsLoaded = true diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee index bcff7bcc49..0021d17d85 100644 --- a/app/assets/javascripts/notes.js.coffee +++ b/app/assets/javascripts/notes.js.coffee @@ -10,7 +10,6 @@ class @Notes constructor: (notes_url, note_ids, last_fetched_at, view) -> @notes_url = notes_url - @notes_url = gon.relative_url_root + @notes_url if gon.relative_url_root? @note_ids = note_ids @last_fetched_at = last_fetched_at @view = view diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss index 9e10a6c1a9..07c288d514 100644 --- a/app/assets/stylesheets/generic/common.scss +++ b/app/assets/stylesheets/generic/common.scss @@ -184,7 +184,7 @@ li.note { } } -.supp_diff_link, +.show-suppressed-diff, .show-all-commits { cursor: pointer; } diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index af6ea58382..1557c243db 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -65,6 +65,17 @@ color: #777; } + .suppressed-container { + padding: ($padding-base-vertical + 5px) $padding-base-horizontal; + text-align: center; + + // "Changes suppressed. Click to show." link + .show-suppressed-diff { + font-size: 110%; + font-weight: bold; + } + } + table { width: 100%; font-family: $monospace_font; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index 4da65b2874..85c828ec1a 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -37,7 +37,7 @@ ul.notes { font-size: 13px; a { - @extend .cgray; + @extend .cgray; &:hover { text-decoration: underline; @@ -105,6 +105,8 @@ ul.notes { } hr { + // Darken 'whitesmoke' a bit to make it more visible in note bodies + border-color: darken(#F5F5F5, 8%); margin: 10px 0; } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 21831073f2..65f92b8caf 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -305,14 +305,14 @@ class ApplicationController < ActionController::Base end def github_import_enabled? - OauthHelper.enabled_oauth_providers.include?(:github) + Gitlab::OAuth::Provider.enabled?(:github) end def gitlab_import_enabled? - OauthHelper.enabled_oauth_providers.include?(:gitlab) + Gitlab::OAuth::Provider.enabled?(:gitlab) end def bitbucket_import_enabled? - OauthHelper.enabled_oauth_providers.include?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? + Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present? end end diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb index 4eccf49e5d..8c44f59ceb 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -3,6 +3,10 @@ class Groups::ApplicationController < ApplicationController private + def group + @group ||= Group.find_by(path: params[:group_id]) + end + def authorize_read_group! unless @group and can?(current_user, :read_group, @group) if current_user.nil? @@ -19,7 +23,9 @@ class Groups::ApplicationController < ApplicationController end end - def group - @group ||= Group.find_by(path: params[:group_id]) + def authorize_admin_group_member! + unless can?(current_user, :admin_group_member, group) + return render_403 + end end end diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index a7f174383f..3f1c298545 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -5,6 +5,7 @@ class Groups::GroupMembersController < Groups::ApplicationController # Authorize before_action :authorize_read_group! before_action :authorize_admin_group!, except: [:index, :leave] + before_action :authorize_admin_group_member!, only: [:create, :resend_invite] def index @project = @group.projects.find(params[:project_id]) if params[:project_id] @@ -36,6 +37,9 @@ class Groups::GroupMembersController < Groups::ApplicationController def update @member = @group.group_members.find(params[:id]) + + return render_403 unless can?(current_user, :update_group_member, @member) + old_access_level = @member.human_access if @member.update_attributes(member_params) diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index b500fd44a0..55b2a728bd 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -24,7 +24,7 @@ class GroupsController < Groups::ApplicationController if @group.save @group.add_owner(current_user) - redirect_to @group, notice: 'Group was successfully created.' + redirect_to @group, notice: "Group '#{@group.name}' was successfully created." else render action: "new" end @@ -77,7 +77,7 @@ class GroupsController < Groups::ApplicationController def update if @group.update_attributes(group_params) - redirect_to edit_group_path(@group), notice: 'Group was successfully updated.' + redirect_to edit_group_path(@group), notice: "Group '#{@group.name}' was successfully updated." else render action: "edit" end @@ -86,7 +86,7 @@ class GroupsController < Groups::ApplicationController def destroy DestroyGroupService.new(@group, current_user).execute - redirect_to root_path, notice: 'Group was removed.' + redirect_to root_path, alert: "Group '#{@group.name} was deleted." end protected diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index ca78a4aaa8..af0b841f0b 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -3,6 +3,7 @@ class Import::BitbucketController < Import::BaseController before_action :bitbucket_auth, except: :callback rescue_from OAuth::Error, with: :bitbucket_unauthorized + rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized def callback request_token = session.delete(:oauth_request_token) diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index b460a1b11c..95cd0e5712 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -77,10 +77,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController end end rescue Gitlab::OAuth::SignupDisabledError => e - message = "Signing in using your #{oauth['provider']} account without a pre-existing GitLab account is not allowed." + label = Gitlab::OAuth::Provider.label_for(oauth['provider']) + message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." if current_application_settings.signup_enabled? - message << " Create a GitLab account first, and then connect it to your #{oauth['provider']} account." + message << " Create a GitLab account first, and then connect it to your #{label} account." end flash[:notice] = message diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index 06aef91cad..b181c47bae 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -7,6 +7,10 @@ class Projects::NetworkController < Projects::ApplicationController before_action :authorize_download_code! def show + + @url = namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json)) + @commit_url = namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s") + respond_to do |format| format.html diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index d83561cf32..6080c849c8 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -1,5 +1,6 @@ class Projects::RefsController < Projects::ApplicationController include ExtractsPath + include TreeHelper before_action :require_non_empty_project before_action :assign_ref_vars @@ -60,6 +61,11 @@ class Projects::RefsController < Projects::ApplicationController } end + if @logs.present? + @log_url = namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/')) + @more_log_url = logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit)) + end + respond_to do |format| format.html { render_404 } format.js diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 399deb86a0..6e395e666a 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -24,7 +24,7 @@ class ProjectsController < ApplicationController if @project.saved? redirect_to( project_path(@project), - notice: 'Project was successfully created.' + notice: "Project '#{@project.name}' was successfully created." ) else render 'new' @@ -36,11 +36,11 @@ class ProjectsController < ApplicationController respond_to do |format| if status - flash[:notice] = 'Project was successfully updated.' + flash[:notice] = "Project '#{@project.name}' was successfully updated." format.html do redirect_to( edit_project_path(@project), - notice: 'Project was successfully updated.' + notice: "Project '#{@project.name}' was successfully updated." ) end format.js @@ -100,7 +100,7 @@ class ProjectsController < ApplicationController return access_denied! unless can?(current_user, :remove_project, @project) ::Projects::DestroyService.new(@project, current_user, {}).execute - flash[:alert] = 'Project deleted.' + flash[:alert] = "Project '#{@project.name}' was deleted." if request.referer.include?('/admin') redirect_to admin_namespaces_projects_path diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 89629bc058..796cbe4c58 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -90,7 +90,7 @@ class SessionsController < Devise::SessionsController # Prevent alert from popping up on the first page shown after authentication. flash[:alert] = nil - redirect_to omniauth_authorize_path(:user, provider.to_sym) + redirect_to user_omniauth_authorize_path(provider.to_sym) end def valid_otp_attempt?(user) diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb new file mode 100644 index 0000000000..9f8ac6b9d9 --- /dev/null +++ b/app/helpers/auth_helper.rb @@ -0,0 +1,54 @@ +module AuthHelper + PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze + FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos'].freeze + + def ldap_enabled? + Gitlab.config.ldap.enabled + end + + def kerberos_enabled? + enabled_oauth_providers.include?(:kerberos) + end + + def provider_has_icon?(name) + PROVIDERS_WITH_ICONS.include?(name.to_s) + end + + def auth_providers + Gitlab::OAuth::Provider.providers + end + + def label_for_provider(name) + Gitlab::OAuth::Provider.label_for(name) + end + + def form_based_provider?(name) + FORM_BASED_PROVIDERS.any? { |pattern| pattern === name.to_s } + end + + def form_based_providers + auth_providers.select { |provider| form_based_provider?(provider) } + end + + def button_based_providers + auth_providers.reject { |provider| form_based_provider?(provider) } + end + + def provider_image_tag(provider, size = 64) + label = label_for_provider(provider) + + if provider_has_icon?(provider) + file_name = "#{provider.to_s.split('_').first}_#{size}.png" + + image_tag(image_path("auth_buttons/#{file_name}"), alt: label, title: "Sign in with #{label}") + else + label + end + end + + def auth_active?(provider) + current_user.identities.exists?(provider: provider.to_s) + end + + extend self +end diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 50df380170..77d99140c4 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,6 +1,6 @@ module BlobHelper def highlight(blob_name, blob_content, nowrap: false, continue: false) - @formatter ||= Rugments::Formatters::HTML.new( + @formatter ||= Rouge::Formatters::HTMLGitlab.new( nowrap: nowrap, cssclass: 'code highlight', lineanchors: true, @@ -8,11 +8,11 @@ module BlobHelper ) begin - @lexer ||= Rugments::Lexer.guess(filename: blob_name, source: blob_content).new + @lexer ||= Rouge::Lexer.guess(filename: blob_name, source: blob_content).new result = @formatter.format(@lexer.lex(blob_content, continue: continue)).html_safe rescue - lexer = Rugments::Lexers::PlainText - result = @formatter.format(lexer.lex(blob_content)).html_safe + @lexer = Rouge::Lexers::PlainText + result = @formatter.format(@lexer.lex(blob_content)).html_safe end result diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index 128de18bc4..45788ba95a 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -31,8 +31,8 @@ module EmailsHelper end def color_email_diff(diffcontent) - formatter = Rugments::Formatters::HTML.new(cssclass: "highlight", inline_theme: :github) - lexer = Rugments::Lexers::Diff.new + formatter = Rouge::Formatters::HTML.new(css_class: 'highlight', inline_theme: 'github') + lexer = Rouge::Lexers::Diff raw formatter.format(lexer.lex(diffcontent)) end diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb deleted file mode 100644 index 51c9519ca1..0000000000 --- a/app/helpers/oauth_helper.rb +++ /dev/null @@ -1,44 +0,0 @@ -module OauthHelper - def ldap_enabled? - Gitlab.config.ldap.enabled - end - - def kerberos_enabled? - enabled_oauth_providers.include?(:kerberos) - end - - def standard_login_form_only? - ldap_enabled? || kerberos_enabled? - end - - def default_providers - [:twitter, :github, :gitlab, :bitbucket, :google_oauth2, :ldap] - end - - def enabled_oauth_providers - Devise.omniauth_providers - end - - def enabled_social_providers - enabled_oauth_providers.select do |name| - [:saml, :twitter, :gitlab, :github, :bitbucket, :google_oauth2, :kerberos].include?(name.to_sym) - end - end - - def additional_providers - enabled_oauth_providers.reject do |provider| - provider.to_s.starts_with?('ldap') || provider == :kerberos - end - end - - def oauth_image_tag(provider, size = 64) - file_name = "#{provider.to_s.split('_').first}_#{size}.png" - image_tag(image_path("authbuttons/#{file_name}"), alt: "Sign in with #{provider.to_s.titleize}") - end - - def oauth_active?(provider) - current_user.identities.exists?(provider: provider.to_s) - end - - extend self -end diff --git a/app/helpers/profile_helper.rb b/app/helpers/profile_helper.rb deleted file mode 100644 index 780c7cd513..0000000000 --- a/app/helpers/profile_helper.rb +++ /dev/null @@ -1,13 +0,0 @@ -module ProfileHelper - def show_profile_username_tab? - current_user.can_change_username? - end - - def show_profile_social_tab? - enabled_social_providers.any? - end - - def show_profile_remove_tab? - signup_enabled? - end -end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index d4cdd2ab7e..a3d5102e12 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -286,7 +286,8 @@ module ProjectsHelper end def readme_cache_key - [@project.id, @project.commit.sha, "readme"].join('-') + sha = @project.commit.try(:sha) || 'nil' + [@project.id, sha, "readme"].join('-') end def round_commit_count(project) diff --git a/app/models/ability.rb b/app/models/ability.rb index c2e89c7ac4..198b259136 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -156,12 +156,13 @@ class Ability :create_project_snippet, :update_issue, :admin_issue, - :admin_label, + :admin_label ] end def project_dev_rules project_report_rules + [ + :admin_merge_request, :create_merge_request, :create_wiki, :push_code @@ -248,7 +249,8 @@ class Ability if group.has_owner?(user) || user.admin? rules.push(*[ :admin_group, - :admin_namespace + :admin_namespace, + :admin_group_member ]) unless group.ldap_synced? diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 97846b06d7..c21e7fd0e3 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -159,6 +159,16 @@ module Issuable end end + # Convert this Issuable class name to a format usable by Ability definitions + # + # Examples: + # + # issuable.class # => MergeRequest + # issuable.to_ability_name # => "merge_request" + def to_ability_name + self.class.to_s.underscore + end + private def filter_superceded_votes(votes, notes) diff --git a/app/models/group.rb b/app/models/group.rb index 57eab28f3c..39c01f8b06 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -60,6 +60,12 @@ class Group < Namespace name end + def avatar_url(size = nil) + if avatar.present? + [gitlab_config.url, avatar.url].join + end + end + def owners @owners ||= group_members.owners.map(&:user) end diff --git a/app/models/project.rb b/app/models/project.rb index 87e47ae560..a891901210 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -36,7 +36,6 @@ class Project < ActiveRecord::Base include Gitlab::ConfigHelper include Gitlab::ShellAdapter include Gitlab::VisibilityLevel - include Rails.application.routes.url_helpers include Referable include Sortable @@ -323,7 +322,7 @@ class Project < ActiveRecord::Base end def web_url - [gitlab_config.url, path_with_namespace].join('/') + Rails.application.routes.url_helpers.namespace_project_url(self.namespace, self) end def web_url_without_protocol @@ -448,7 +447,7 @@ class Project < ActiveRecord::Base if avatar.present? [gitlab_config.url, avatar.url].join elsif avatar_in_git - [gitlab_config.url, namespace_project_avatar_path(namespace, self)].join + Rails.application.routes.url_helpers.namespace_project_avatar_url(namespace, self) end end @@ -591,7 +590,7 @@ class Project < ActiveRecord::Base end def http_url_to_repo - [gitlab_config.url, '/', path_with_namespace, '.git'].join('') + "#{web_url}.git" end # Check if current branch name is marked as protected in the system @@ -733,14 +732,14 @@ class Project < ActiveRecord::Base ensure_satellite_exists true else - errors.add(:base, 'Failed to fork repository') + errors.add(:base, 'Failed to fork repository via gitlab-shell') false end else if gitlab_shell.add_repository(path_with_namespace) true else - errors.add(:base, 'Failed to create repository') + errors.add(:base, 'Failed to create repository via gitlab-shell') false end end diff --git a/app/models/user.rb b/app/models/user.rb index af8d52e36f..e07b56ab03 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -281,6 +281,10 @@ class User < ActiveRecord::Base value: login.to_s.downcase).first end + def find_by_username!(username) + find_by!('lower(username) = ?', username.downcase) + end + def by_username_or_id(name_or_id) where('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i).first end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index f1ef5ca84f..15b3825f96 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -27,8 +27,10 @@ class IssuableBaseService < BaseService old_branch, new_branch) end - def filter_params - unless can?(current_user, :admin_issue, project) + def filter_params(issuable_ability_name = :issue) + ability = :"admin_#{issuable_ability_name}" + + unless can?(current_user, ability, project) params.delete(:milestone_id) params.delete(:label_ids) params.delete(:assignee_id) diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index c3ca04a434..770f32de94 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -10,6 +10,10 @@ module Issues private + def filter_params + super(:issue) + end + def execute_hooks(issue, action = 'open') issue_data = hook_data(issue, action) issue.project.execute_hooks(issue_data, :issue_hooks) diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index e455fe9579..7b306a8a53 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -20,5 +20,11 @@ module MergeRequests merge_request.project.execute_services(merge_data, :merge_request_hooks) end end + + private + + def filter_params + super(:merge_request) + end end end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 66d61a5ef5..36d05b1a85 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -85,6 +85,8 @@ module Projects @project.create_wiki if @project.wiki_enabled? + @project.build_missing_services + event_service.create_project(@project, current_user) system_hook_service.execute_hooks_for(@project, :create) diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 14a0d9280c..901eabeb3b 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -78,21 +78,22 @@ %span.monospace= project.path_with_namespace + ".git" .col-md-6 - .panel.panel-default - .panel-heading - Add user(s) to the group: - .panel-body.form-holder - %p.light - Read more about project permissions - %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" + - if can?(current_user, :admin_group_member, @group) + .panel.panel-default + .panel-heading + Add user(s) to the group: + .panel-body.form-holder + %p.light + Read more about project permissions + %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink" - = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do - %div - = users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all) - %div.prepend-top-10 - = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" - %hr - = button_tag 'Add users to group', class: "btn btn-create" + = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do + %div + = users_select_tag(:user_ids, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all) + %div.prepend-top-10 + = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" + %hr + = button_tag 'Add users to group', class: "btn btn-create" .panel.panel-default .panel-heading %h3.panel-title @@ -113,7 +114,8 @@ (invited) %span.pull-right.light = member.human_access - = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do - %i.fa.fa-minus.fa-inverse + - if can?(current_user, :destroy_group_member, member) + = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do + %i.fa.fa-minus.fa-inverse .panel-footer = paginate @members, param_name: 'members_page', theme: 'gitlab' diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml index 0525552ebf..3a78855822 100644 --- a/app/views/admin/identities/_form.html.haml +++ b/app/views/admin/identities/_form.html.haml @@ -8,7 +8,8 @@ .form-group = f.label :provider, class: 'control-label' .col-sm-10 - = f.select :provider, Gitlab::OAuth::Provider.names, { allow_blank: false }, class: 'form-control' + - values = Gitlab::OAuth::Provider.providers.map { |name| ["#{Gitlab::OAuth::Provider.label_for(name)} (#{name})", name] } + = f.select :provider, values, { allow_blank: false }, class: 'form-control' .form-group = f.label :extern_uid, "Identifier", class: 'control-label' .col-sm-10 diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml index 671c4fbc67..7362d904b9 100644 --- a/app/views/admin/identities/_identity.html.haml +++ b/app/views/admin/identities/_identity.html.haml @@ -1,6 +1,6 @@ %tr %td - = identity.provider + = "#{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})" %td = identity.extern_uid %td diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 54a3972677..9f5520603c 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,5 +1,5 @@ = form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| - = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus" + = f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off" = f.password_field :password, class: "form-control bottom", placeholder: "Password" - if devise_mapping.rememberable? .remember-me.checkbox diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 6ec741e488..689cd6ed66 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -6,4 +6,4 @@ %label{for: "remember_me"} = check_box_tag :remember_me, '1', false, id: 'remember_me' %span Remember me - = button_tag "#{server['label']} Sign in", class: "btn-save btn" + = button_tag "Sign in", class: "btn-save btn" diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index f8ba9d80ae..ecf680e7b2 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,10 +1,8 @@ %p %span.light Sign in with   - - providers = additional_providers + - providers = button_based_providers - providers.each do |provider| %span.light - - if default_providers.include?(provider) - = link_to oauth_image_tag(provider), omniauth_authorize_path(resource_name, provider), method: :post, class: 'oauth-image-link' - - else - = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), method: :post, class: "btn", "data-no-turbolink" => "true" + - has_icon = provider_has_icon?(provider) + = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true" diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml index 7900995dc4..c8447bf8c6 100644 --- a/app/views/devise/shared/_signin_box.html.haml +++ b/app/views/devise/shared/_signin_box.html.haml @@ -6,7 +6,7 @@ .login-heading %h3 Sign in .login-body - - if ldap_enabled? + - if form_based_providers.any? %ul.nav.nav-tabs - @ldap_servers.each_with_index do |server, i| %li{class: (:active if i.zero?)} diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml index b460e0ff59..acc7f8b28c 100644 --- a/app/views/groups/group_members/_group_member.html.haml +++ b/app/views/groups/group_members/_group_member.html.haml @@ -24,7 +24,7 @@ = link_to member.created_by.name, user_path(member.created_by) = time_ago_with_tooltip(member.created_at) - - if show_controls && can?(current_user, :admin_group, @group) + - if show_controls && can?(current_user, :admin_group_member, member) = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do Resend invite diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml index a32d81c213..43c9688361 100644 --- a/app/views/groups/group_members/index.html.haml +++ b/app/views/groups/group_members/index.html.haml @@ -17,7 +17,7 @@ = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' } = button_tag 'Search', class: 'btn' - - if current_user && current_user.can?(:admin_group, @group) + - if current_user && current_user.can?(:admin_group_member, @group) .pull-right - if ldap_enabled? && @group.ldap_synced? = link_to reset_access_group_ldap_path(@group), class: 'btn btn-grouped', data: { confirm: "Force GitLab to do LDAP permission checks for all group members? All members besides yourself will be reduced to 'Guest' access until their next interaction with GitLab." }, method: :put do diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml index ee1b57278b..c8662a15ad 100644 --- a/app/views/layouts/notify.html.haml +++ b/app/views/layouts/notify.html.haml @@ -33,7 +33,7 @@ = yield %div.footer{style: "margin-top: 10px;"} %p - \— + — %br - if @target_url #{link_to "View it on GitLab", @target_url} diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml index f76e4d57ef..499f746a5c 100644 --- a/app/views/profiles/accounts/show.html.haml +++ b/app/views/profiles/accounts/show.html.haml @@ -58,22 +58,22 @@ %div = link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success' - - if show_profile_social_tab? + - if button_based_providers.any? .panel.panel-default .panel-heading Connected Accounts .panel-body .oauth-buttons.append-bottom-10 %p Click on icon to activate signin with one of the following services - - enabled_social_providers.each do |provider| + - button_based_providers.each do |provider| .btn-group - = link_to oauth_image_tag(provider), omniauth_authorize_path(User, provider), - method: :post, class: "btn btn-lg #{'active' if oauth_active?(provider)}" - - if oauth_active?(provider) + = link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: "btn btn-lg #{'active' if auth_active?(provider)}", "data-no-turbolink" => "true" + + - if auth_active?(provider) = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do = icon('close') - - if show_profile_username_tab? + - if current_user.can_change_username? .panel.panel-warning.update-username .panel-heading Change Username @@ -93,7 +93,7 @@ %div = f.submit 'Save username', class: "btn btn-warning" - - if show_profile_remove_tab? + - if signup_enabled? .panel.panel-danger.remove-account .panel-heading Remove account diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index f905417f0e..b76a5b636a 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -6,14 +6,13 @@ - @key.errors.full_messages.each do |msg| %li= msg - .form-group - = f.label :title, class: 'control-label' - .col-sm-10= f.text_field :title, class: "form-control" .form-group = f.label :key, class: 'control-label' .col-sm-10 = f.text_area :key, class: "form-control", rows: 8 - + .form-group + = f.label :title, class: 'control-label' + .col-sm-10= f.text_field :title, class: "form-control" .form-actions = f.submit 'Add key', class: "btn btn-create" diff --git a/app/views/projects/blob/diff.html.haml b/app/views/projects/blob/diff.html.haml index 8474260898..f3b01ff328 100644 --- a/app/views/projects/blob/diff.html.haml +++ b/app/views/projects/blob/diff.html.haml @@ -11,7 +11,7 @@ %td.old_line.diff-line-num{data: {linenumber: line_old}} = link_to raw(line_old), "#" %td.new_line= link_to raw(line_new) , "#" - %td.line_content.noteable_line= line + %td.line_content.noteable_line= ' ' * @form.indent + line - if @form.unfold? && @form.bottom? && @form.to < @blob.loc %tr.line_holder{ id: @form.to } diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml index ed4c601bcd..977ca423f7 100644 --- a/app/views/projects/diffs/_text_file.html.haml +++ b/app/views/projects/diffs/_text_file.html.haml @@ -1,6 +1,7 @@ - too_big = diff_file.diff_lines.count > Commit::DIFF_SAFE_LINES - if too_big - %a.supp_diff_link Changes suppressed. Click to show + .suppressed-container + %a.show-suppressed-diff.js-show-suppressed-diff Changes suppressed. Click to show. %table.text-file{class: "#{'hide' if too_big}"} - last_line = 0 diff --git a/app/views/projects/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml index da3d4be84b..caed0e69dc 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)), class: "btn btn-sm btn-warning" + = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true, format: :html)), 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/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml index faaa85896c..c57eee1414 100644 --- a/app/views/projects/merge_requests/_show.html.haml +++ b/app/views/projects/merge_requests/_show.html.haml @@ -66,11 +66,9 @@ #notes.notes.tab-pane.voting_notes = render "projects/merge_requests/discussion" #commits.commits.tab-pane - - if current_page?(action: 'commits') - = render "projects/merge_requests/show/commits" + - # This tab is always loaded via AJAX #diffs.diffs.tab-pane - - if current_page?(action: 'diffs') - = render "projects/merge_requests/show/diffs" + - # This tab is always loaded via AJAX .mr-loading-status = spinner diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml index a88cf16751..52b5b8b877 100644 --- a/app/views/projects/network/show.html.haml +++ b/app/views/projects/network/show.html.haml @@ -17,9 +17,9 @@ :javascript network_graph = new Network({ - url: '#{namespace_project_network_path(@project.namespace, @project, @ref, @options.merge(format: :json))}', - commit_url: '#{namespace_project_commit_path(@project.namespace, @project, 'ae45ca32').gsub("ae45ca32", "%s")}', - ref: '#{@ref}', + url: "#{escape_javascript(@url)}", + commit_url: "#{escape_javascript(@commit_url)}", + ref: "#{escape_javascript(@ref)}", commit_id: '#{@commit.id}' }) new ShortcutsNetwork(network_graph.branch_graph) diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml index 7472b33bb5..8f7d2e84c7 100644 --- a/app/views/projects/notes/_edit_form.html.haml +++ b/app/views/projects/notes/_edit_form.html.haml @@ -3,10 +3,7 @@ = note_target_fields(note) = render layout: 'projects/md_preview', locals: { preview_class: 'note-text' } do = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field' - - .comment-hints.clearfix - .pull-left #{link_to 'Markdown ', help_page_path('markdown', 'markdown'),{ target: '_blank', tabindex: -1 }} - .pull-right #{link_to 'Attach a file', '#', class: 'markdown-selector', tabindex: -1 } + = render 'projects/notes/hints' .note-form-actions .buttons diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml index 64f98741d4..3be8f44b28 100644 --- a/app/views/projects/notes/_form.html.haml +++ b/app/views/projects/notes/_form.html.haml @@ -8,18 +8,8 @@ = f.hidden_field :noteable_type = render layout: 'projects/md_preview', locals: { preview_class: "note-text", referenced_users: true } do - = render 'projects/zen', f: f, attr: :note, - classes: 'note_text js-note-text' - - .comment-hints.clearfix - .pull-left - = link_to "Markdown ", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 } - tip: - = random_markdown_tip - .pull-right - = link_to '#', class: 'markdown-selector', tabindex: -1 do - Attach a file - = icon('paperclip') + = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text' + = render 'projects/notes/hints' .error-alert .note-form-actions diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml new file mode 100644 index 0000000000..6e7929bdab --- /dev/null +++ b/app/views/projects/notes/_hints.html.haml @@ -0,0 +1,9 @@ +.comment-hints.clearfix + .pull-left + = link_to 'Markdown', help_page_path('markdown', 'markdown'), target: '_blank', tabindex: -1 + tip: + = random_markdown_tip + .pull-right + = link_to '#', class: 'markdown-selector', tabindex: -1 do + = icon('paperclip') + Attach a file diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml index 35c15cf3a9..db7f244d00 100644 --- a/app/views/projects/refs/logs_tree.js.haml +++ b/app/views/projects/refs/logs_tree.js.haml @@ -11,9 +11,11 @@ - if @logs.present? :plain var current_url = location.href.replace(/\/?$/, '/'); - var log_url = '#{namespace_project_tree_url(@project.namespace, @project, tree_join(@ref, @path || '/'))}'.replace(/\/?$/, '/'); + var log_url = "#{escape_javascript(@log_url)}".replace(/\/?$/, '/'); + if(current_url == log_url) { - // Load 10 more commit log for each file in tree + // Load more commit logs for each file in tree // if we still on the same page - ajaxGet('#{logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit))}'); + var url = "#{escape_javascript(@more_log_url)}"; + ajaxGet(url); } diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml index d304690d16..5048154cb2 100644 --- a/app/views/projects/tree/_tree.html.haml +++ b/app/views/projects/tree/_tree.html.haml @@ -49,5 +49,5 @@ :javascript // Load last commit log for each file in tree $('#tree-slider').waitForImages(function() { - ajaxGet('#{@logs_path}'); + ajaxGet("#{escape_javascript(@logs_path)}"); }); diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 83cd4c6667..5c4dd7f91a 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -3,6 +3,10 @@ %h3.page-title = @page.title = render 'main_links' + +.wiki-last-edit-by + Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + - if @page.historical? .warning_message This is an old version of this page. @@ -16,6 +20,6 @@ = render_wiki_content(@page) %hr - .wiki-last-edit-by Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)} + diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml index d1bd5ef968..19e8c31975 100644 --- a/app/views/shared/issuable/_context.html.haml +++ b/app/views/shared/issuable/_context.html.haml @@ -8,7 +8,7 @@ - else none .issuable-context-selectbox - - if can?(current_user, :"admin_#{issuable.class.to_s.underscore}", @project) + - 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) %div.prepend-top-20.clearfix @@ -24,7 +24,7 @@ - else none .issuable-context-selectbox - - if can?(current_user, :"admin_#{issuable.class.to_s.underscore}", @project) + - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) = f.select(:milestone_id, milestone_options(issuable), { include_blank: 'Select milestone' }, {class: 'select2 select2-compact js-select2 js-milestone'}) = hidden_field_tag :issuable_context = f.submit class: 'btn hide' diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index 8d2d70a2df..805e51a251 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_issue, @project) +- if can?(current_user, :"admin_#{issuable.to_ability_name}", @project) .form-group .issue-assignee = f.label :assignee_id, class: 'control-label' do diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index 5988373fdd..8f042baf39 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -243,24 +243,30 @@ production: &base # arguments, followed by optional 'args' which can be either a hash or an array. # Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html providers: - # - { name: 'google_oauth2', app_id: 'YOUR_APP_ID', + # - { name: 'google_oauth2', + # label: 'Google', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { access_type: 'offline', approval_prompt: '' } } - # - { name: 'twitter', app_id: 'YOUR_APP_ID', - # app_secret: 'YOUR_APP_SECRET'} - # - { name: 'gitlab', app_id: 'YOUR_APP_ID', - # app_secret: 'YOUR_APP_SECRET', - # args: { scope: 'api' } } - # - { name: 'github', app_id: 'YOUR_APP_ID', + # - { name: 'twitter', + # app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET' } + # - { name: 'github', + # label: 'GitHub', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # url: "https://github.com/", # args: { scope: 'user:email' } } - # - { name: 'gitlab', app_id: 'YOUR_APP_ID', + # - { name: 'gitlab', + # label: 'GitLab.com', + # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', # args: { scope: 'api' } } - # - { name: 'bitbucket', app_id: 'YOUR_APP_ID', - # app_secret: 'YOUR_APP_SECRET'} - # - { name: 'saml', + # - { name: 'bitbucket', + # app_id: 'YOUR_APP_ID', + # app_secret: 'YOUR_APP_SECRET' } + # - { name: 'saml', + # label: 'Our SAML Provider', # args: { # assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback', # idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8', diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md index d6272cd591..538894f584 100644 --- a/doc/gitlab-basics/README.md +++ b/doc/gitlab-basics/README.md @@ -2,7 +2,7 @@ Step-by-step guides on the basics of working with Git and GitLab. -* [Start using Git on the commandline](start-using-git.md) +* [Start using Git on the command line](start-using-git.md) * [Create and add your SSH Keys](create-your-ssh-keys.md) @@ -17,3 +17,5 @@ Step-by-step guides on the basics of working with Git and GitLab. * [Create a branch](create-branch.md) * [Fork a project](fork-project.md) + +* [Add a file](add-file.md) diff --git a/doc/gitlab-basics/add-file.md b/doc/gitlab-basics/add-file.md new file mode 100644 index 0000000000..e7c441e769 --- /dev/null +++ b/doc/gitlab-basics/add-file.md @@ -0,0 +1,31 @@ +# How to add a file + +You can create a file in your [shell](command-line-commands.md) or in GitLab. + +To create a file in GitLab, sign in to [GitLab.com](https://gitlab.com). + +Select a project on the right side of your screen: + +![Select a project](basicsimages/select_project.png) + +It's a good idea to [create a branch](create-branch.md), but it's not necessary. + +Go to the directory where you'd like to add the file and click on the "+" sign next to the name of the project and directory: + +![Create a file](basicsimages/create_file.png) + +Name your file (you can't add spaces, so you can use hyphens or underscores). Don't forget to include the markup language you'd like to use : + +![File name](basicsimages/file_name.png) + +Add all the information that you'd like to include in your file: + +![Add information](basicsimages/white_space.png) + +Add a commit message based on what you just added and then click on "commit changes": + +![Commit changes](basicsimages/commit_changes.png) + +### Note +Besides its regular files, every directory needs a README.md or README.html file which works like an index, telling +what the directory is about. It's the first document you'll find when you open a directory. diff --git a/doc/integration/gitlab_actions.png b/doc/integration/gitlab_actions.png deleted file mode 100644 index b08f54d137..0000000000 Binary files a/doc/integration/gitlab_actions.png and /dev/null differ diff --git a/doc/integration/gitlab_buttons_in_gmail.md b/doc/integration/gitlab_buttons_in_gmail.md deleted file mode 100644 index e35bb8ba69..0000000000 --- a/doc/integration/gitlab_buttons_in_gmail.md +++ /dev/null @@ -1,28 +0,0 @@ -# GitLab buttons in Gmail - -GitLab supports [Google actions in email](https://developers.google.com/gmail/markup/actions/actions-overview). - -If correctly setup, emails that require an action will be marked in Gmail. - -![gitlab_actions](gitlab_actions.png) - -To get this functioning, you need to be registered with Google. -[See how to register with Google in this document.](https://developers.google.com/gmail/markup/registering-with-google) - -To aid the registering with Google, GitLab offers a rake task that will send an email to Google whitelisting email address from your GitLab server. - -To check what would be sent to the Google email address, run the rake task: - -```bash -bundle exec rake gitlab:mail_google_schema_whitelisting RAILS_ENV=production -``` - -**This will not send the email but give you the output of how the mail will look.** - -Copy the output of the rake task to [Google email markup tester](https://www.google.com/webmasters/markup-tester/u/0/) and press "Validate". - -If you receive "No errors detected" message from the tester you can send the email using: - -```bash -bundle exec rake gitlab:mail_google_schema_whitelisting RAILS_ENV=production SEND=true -``` diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md index 8e2a602ec3..2010cb9b8a 100644 --- a/doc/integration/omniauth.md +++ b/doc/integration/omniauth.md @@ -84,7 +84,7 @@ Existing users can enable OmniAuth for specific providers after the account is c 1. Sign in normally - whether standard sign in, LDAP, or another OmniAuth provider. 1. Go to profile settings (the silhouette icon in the top right corner). 1. Select the "Account" tab. -1. Under "Social Accounts" select the desired OmniAuth provider, such as Twitter. +1. Under "Connected Accounts" select the desired OmniAuth provider, such as Twitter. 1. The user will be redirected to the provider. Once the user authorized GitLab they will be redirected back to GitLab. The chosen OmniAuth provider is now active and can be used to sign in to GitLab from then on. diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md index fe9091ad9a..1350c8f693 100644 --- a/doc/integration/twitter.md +++ b/doc/integration/twitter.md @@ -2,9 +2,7 @@ To enable the Twitter OmniAuth provider you must register your application with Twitter. Twitter will generate a client ID and secret key for you to use. -1. Sign in to [Twitter Developers](https://dev.twitter.com/) area. - -1. Hover over the avatar in the top right corner and select "My applications." +1. Sign in to [Twitter Application Management](https://apps.twitter.com/). 1. Select "Create new app" @@ -14,18 +12,18 @@ To enable the Twitter OmniAuth provider you must register your application with - Description: Create a description. - Website: The URL to your GitLab installation. 'https://gitlab.example.com' - Callback URL: 'https://gitlab.example.com/users/auth/twitter/callback' - - Agree to the "Rules of the Road." + - Agree to the "Developer Agreement". ![Twitter App Details](twitter_app_details.png) 1. Select "Create your Twitter application." 1. Select the "Settings" tab. -1. Underneath the Callback URL check the box next to "Allow this application to be used to Sign in the Twitter." +1. Underneath the Callback URL check the box next to "Allow this application to be used to Sign in with Twitter." 1. Select "Update settings" at the bottom to save changes. -1. Select the "API Keys" tab. +1. Select the "Keys and Access Tokens" tab. 1. You should now see an API key and API secret (see screenshot). Keep this page open as you continue configuration. @@ -78,4 +76,4 @@ To enable the Twitter OmniAuth provider you must register your application with 1. Restart GitLab for the changes to take effect. -On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. +On the sign in page there should now be a Twitter icon below the regular sign in form. Click the icon to begin the authentication process. Twitter will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in. \ No newline at end of file diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md index 70b7e17795..e81432c600 100644 --- a/doc/permissions/permissions.md +++ b/doc/permissions/permissions.md @@ -17,6 +17,7 @@ If a user is a GitLab administrator they receive all permissions. | Create code snippets | | ✓ | ✓ | ✓ | ✓ | | Manage issue tracker | | ✓ | ✓ | ✓ | ✓ | | Manage labels | | ✓ | ✓ | ✓ | ✓ | +| Manage merge requests | | | ✓ | ✓ | ✓ | | Create new merge request | | | ✓ | ✓ | ✓ | | Create new branches | | | ✓ | ✓ | ✓ | | Push to non-protected branches | | | ✓ | ✓ | ✓ | diff --git a/doc/release/monthly.md b/doc/release/monthly.md index ca9696e957..552b24195b 100644 --- a/doc/release/monthly.md +++ b/doc/release/monthly.md @@ -68,7 +68,7 @@ Xth: (2 working days before the 22nd) Xth: (1 working day before the 22nd) - [ ] Merge CE stable into EE stable -- [ ] Create (hopefully final) CE, EE, CI release candidates (#LINK) +- [ ] Create CE, EE, CI release candidates (#LINK) (hopefully final ones with the same commit as the release tomorrow) - [ ] Create Omnibus tags and build packages for the latest release candidates - [ ] Update GitLab.com with the latest RC (#LINK) - [ ] Update ci.gitLab.com with the latest RC (#LINK) @@ -81,10 +81,10 @@ workday to quickly fix any issues. - [ ] Merge CE stable into EE stable (#LINK) - [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools) (#LINK) -- [ ] BEFORE 11AM CET Create and push omnibus tags for x.y.0 (will auto-release the packages) (#LINK) -- [ ] BEFORE 12AM CET Publish the release blog post (#LINK) +- [ ] Try to do before 11AM CET: Create and push omnibus tags for x.y.0 (will auto-release the packages) (#LINK) +- [ ] Try to do before 12AM CET: Publish the release blog post (#LINK) - [ ] Tweet about the release (blog post) (#LINK) -- [ ] Schedule a second tweet of the release announcement at 6PM CET / 9AM PST +- [ ] Schedule a second tweet of the release announcement with the same text at 6PM CET / 9AM PST ``` @@ -220,4 +220,4 @@ Consider creating a post on Hacker News. ## Create a WIP blogpost for the next release -Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). +Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md). \ No newline at end of file diff --git a/doc/ssh/README.md b/doc/ssh/README.md index 5f44f9351d..7cdcd11c04 100644 --- a/doc/ssh/README.md +++ b/doc/ssh/README.md @@ -105,3 +105,6 @@ IdentityFile ~/my-ssh-key-directory/company-com-private-key-filename Note in the gitlab.com example above a username was specified to override the default chosen by OpenSSH (your local username). This is only required if your local and remote usernames differ. Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document. + +Public SSH keys need to be unique, as they will bind to your account. Your SSH key is the only identifier you'll +have when pushing code via SSH. That's why it needs to uniquely map to a single user. diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md index e24ce4e405..2d9fe4ad01 100644 --- a/doc/workflow/gitlab_flow.md +++ b/doc/workflow/gitlab_flow.md @@ -31,7 +31,7 @@ We think there is still room for improvement and will detail a set of practices ## Git flow and its problems -[![Git Flow timeline by Vincent Driessen, used with permission](gitdashflow.png) +![Git Flow timeline by Vincent Driessen, used with permission](gitdashflow.png) Git flow was one of the first proposals to use git branches and it has gotten a lot of attention. It advocates a master branch and a separate develop branch as well as supporting branches for features, releases and hotfixes. @@ -54,7 +54,7 @@ And doing releases doesn't automatically mean also doing hotfixes. ![Master branch with feature branches merged in](github_flow.png) - In reaction to git flow a simpler alternative was detailed, [GitHub flow](https://guides.github.com/introduction/flow/index.html). +In reaction to git flow a simpler alternative was detailed, [GitHub flow](https://guides.github.com/introduction/flow/index.html). This flow has only feature branches and a master branch. This is very simple and clean, many organizations have adopted it with great success. Atlassian recommends [a similar strategy](http://blogs.atlassian.com/2014/01/simple-git-workflow-simple/) although they rebase feature branches. @@ -131,7 +131,7 @@ When you feel comfortable with it to be merged you assign it to the person that There is room for more feedback and after the assigned person feels comfortable with the result the branch is merged. If the assigned person does not feel comfortable they can close the merge request without merging. -In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/permissions/permissions.md). +In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://doc.gitlab.com/ce/permissions/permissions.html). So if you want to merge it into a protected branch you assign it to someone with master authorizations. ## Issues with GitLab flow @@ -216,7 +216,7 @@ This prevents creating a merge commit when merging master into your feature bran However, just like with squashing you should never rebase commits you have pushed to a remote server. This makes it impossible to rebase work in progress that you already shared with your team which is something we recommend. When using rebase to keep your feature branch updated you [need to resolve similar conflicts again and again](http://blogs.atlassian.com/2013/10/git-team-workflows-merge-or-rebase/). -You can reuse recorded resolutions (rerere) sometimes, but with without rebasing you only have to solve the conflicts one time and you’re set. +You can reuse recorded resolutions (rerere) sometimes, but without rebasing you only have to solve the conflicts one time and you’re set. There has to be a better way to avoid many merge commits. The way to prevent creating many merge commits is to not frequently merge master into the feature branch. diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md index 1939565771..cd98d1b985 100644 --- a/doc/workflow/importing/README.md +++ b/doc/workflow/importing/README.md @@ -6,4 +6,7 @@ 4. [SVN](migrating_from_svn.md) ### Note -* If you'd like to migrate from a self-hosted GitLab instance to GitLab.com, you can copy your repos by changing the remote and pushing to the new server; but issues and merge requests can't be imported. \ No newline at end of file +* If you'd like to migrate from a self-hosted GitLab instance to GitLab.com, you can copy your repos by changing the remote and pushing to the new server; but issues and merge requests can't be imported. + +* Repositories are imported to GitLab via HTTP. +If the repository is too large, it can timeout. We have a soft limit of 10GB. diff --git a/doc/workflow/labels.md b/doc/workflow/labels.md index 085b7baf5c..6e4840ca5a 100644 --- a/doc/workflow/labels.md +++ b/doc/workflow/labels.md @@ -1,6 +1,6 @@ # Labels -In GitLab, you can easily tag issues and merge requests. If you have permission level `Developer` or higher, you can manage labels. To create, edit or delete a label, go to a project and then to `Issues` and then `Labels`. +In GitLab, you can easily tag issues and Merge Requests. If you have permission level `Developer` or higher, you can manage labels. To create, edit or delete a label, go to a project and then to `Issues` and then `Labels`. Here you can create a new label. @@ -14,3 +14,5 @@ If you want to change an existing label, press edit next to the listed label. You will be presented with the same form as when creating a new label. ![edit label](labels/label3.png) + +You can add labels to Merge Requests when you create or edit them. diff --git a/features/project/network_graph.feature b/features/project/network_graph.feature index 8beb6043af..6cc89a15a7 100644 --- a/features/project/network_graph.feature +++ b/features/project/network_graph.feature @@ -10,6 +10,11 @@ Feature: Project Network Graph And page should select "master" in select box And page should have "master" on graph + @javascript + Scenario: I should see project network with 'test' branch + When I visit project network page on branch 'test' + Then page should have 'test' on graph + @javascript Scenario: I should switch "branch" and "tag" When I switch ref to "feature" diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature index af68cb96ed..d3a77466a3 100644 --- a/features/project/source/browse_files.feature +++ b/features/project/source/browse_files.feature @@ -158,3 +158,10 @@ Feature: Project Source Browse Files Given I visit project source page for "6d394385cf567f80a8fd85055db1ab4c5295806f" And I click on ".gitignore" file in repo Then I don't see the permalink link + + @javascript + Scenario: I browse code with single quotes in the ref + Given I switch ref to 'test' + And I see the ref 'test' has been selected + And I visit the 'test' tree + Then I see the commit data diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb index c55f15932b..fb9ba9d3f1 100644 --- a/features/steps/admin/users.rb +++ b/features/steps/admin/users.rb @@ -3,6 +3,14 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps include SharedPaths include SharedAdmin + before do + allow(Devise).to receive(:omniauth_providers).and_return([:twitter, :twitter_updated]) + end + + after do + allow(Devise).to receive(:omniauth_providers).and_call_original + end + step 'I should see all users' do User.all.each do |user| expect(page).to have_content user.name @@ -121,7 +129,6 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps end step 'I visit "Pete" identities page in admin' do - allow(Gitlab::OAuth::Provider).to receive(:names).and_return(%w(twitter twitter_updated)) visit admin_user_identities_path(@user) end diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb index 81494954d6..5bd6d4b9f4 100644 --- a/features/steps/project/network_graph.rb +++ b/features/steps/project/network_graph.rb @@ -11,8 +11,12 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps # Stub Graph max_size to speed up test (10 commits vs. 650) allow(Network::Graph).to receive(:max_count).and_return(10) - project = Project.find_by(name: "Shop") - visit namespace_project_network_path(project.namespace, project, "master") + @project = Project.find_by(name: "Shop") + visit namespace_project_network_path(@project.namespace, @project, "master") + end + + step "I visit project network page on branch 'test'" do + visit namespace_project_network_path(@project.namespace, @project, "'test'") end step 'page should select "master" in select box' do @@ -29,6 +33,12 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps end end + step "page should have 'test' on graph" do + page.within '.network-graph' do + expect(page).to have_content "'test'" + end + end + When 'I switch ref to "feature"' do select 'feature', from: 'ref' sleep 2 diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb index 95879b9544..5cb085db20 100644 --- a/features/steps/project/source/browse_files.rb +++ b/features/steps/project/source/browse_files.rb @@ -193,6 +193,23 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps FileUtils.rm_f(File.join(@project.repository.path, 'hooks', 'pre-receive')) end + step "I switch ref to 'test'" do + select "'test'", from: 'ref' + end + + step "I see the ref 'test' has been selected" do + expect(page).to have_selector '.select2-chosen', text: "'test'" + end + + step "I visit the 'test' tree" do + visit namespace_project_tree_path(@project.namespace, @project, "'test'") + end + + step 'I see the commit data' do + expect(page).to have_css('.tree-commit-link', visible: true) + expect(page).not_to have_content('Loading commit data...') + end + private def set_new_content diff --git a/lib/api/entities.rb b/lib/api/entities.rb index b96a122743..80bbee90c8 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -6,6 +6,10 @@ module API class UserBasic < UserSafe expose :id, :state, :avatar_url + + expose :web_url do |user, options| + Rails.application.routes.url_helpers.user_url(user) + end end class User < UserBasic @@ -64,6 +68,7 @@ module API expose :namespace expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? } expose :avatar_url + expose :star_count, :forks_count end class ProjectMember < UserBasic @@ -79,6 +84,11 @@ module API class Group < Grape::Entity expose :id, :name, :path, :ldap_cn, :ldap_access, :description expose :ldap_group_links, using: Entities::LdapGroupLink, if: lambda{ | group, options | group.ldap_group_links.any? } + expose :avatar_url + + expose :web_url do |group, options| + Rails.application.routes.url_helpers.group_url(group) + end end class GroupDetail < Group diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index aa43e1dffd..ce21c699e8 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -229,7 +229,7 @@ module API authorize! :read_merge_request, merge_request - present paginate(merge_request.notes), with: Entities::MRNote + present paginate(merge_request.notes.fresh), with: Entities::MRNote end # Post comment to merge request diff --git a/lib/backup/database.rb b/lib/backup/database.rb index c5a5396cbb..bbb230a10f 100644 --- a/lib/backup/database.rb +++ b/lib/backup/database.rb @@ -7,7 +7,11 @@ 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') - FileUtils.mkdir_p(@db_dir) unless Dir.exists?(@db_dir) + 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 @@ -25,7 +29,6 @@ module Backup abort 'Backup failed' unless success $progress.print 'Compressing database ... ' - FileUtils.rm_f db_file_name_gz success = system('gzip', db_file_name) report_success(success) abort 'Backup failed: compress error' unless success diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb index 6fa2079d1a..9ae4b34643 100644 --- a/lib/backup/manager.rb +++ b/lib/backup/manager.rb @@ -16,8 +16,6 @@ module Backup file << s.to_yaml.gsub(/^---\n/,'') end - FileUtils.chmod(0700, folders_to_backup) - # create archive $progress.print "Creating backup archive: #{tar_file} ... " orig_umask = File.umask(0077) diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb index dfb2da9f84..4d70f7883d 100644 --- a/lib/backup/repository.rb +++ b/lib/backup/repository.rb @@ -130,7 +130,10 @@ module Backup def prepare FileUtils.rm_rf(backup_repos_path) - FileUtils.mkdir_p(backup_repos_path) + # Ensure the parent dir of backup_repos_path exists + FileUtils.mkdir_p(Gitlab.config.backup.path) + # Fail if somebody raced to create backup_repos_path before us + FileUtils.mkdir(backup_repos_path, mode: 0700) end def silent diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb index bf43610acf..1f9626644e 100644 --- a/lib/backup/uploads.rb +++ b/lib/backup/uploads.rb @@ -10,7 +10,11 @@ module Backup # Copy uploads from public/uploads to backup/uploads def dump - FileUtils.mkdir_p(backup_uploads_dir) + FileUtils.rm_rf(backup_uploads_dir) + # Ensure the parent dir of backup_uploads_dir exists + FileUtils.mkdir_p(Gitlab.config.backup.path) + # Fail if somebody raced to create backup_uploads_dir before us + FileUtils.mkdir(backup_uploads_dir, mode: 0700) FileUtils.cp_r(app_uploads_dir, backup_dir) end diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb index 03cef30c97..12292f614e 100644 --- a/lib/gitlab/backend/grack_auth.rb +++ b/lib/gitlab/backend/grack_auth.rb @@ -26,7 +26,12 @@ module Grack auth! if project && authorized_request? - @app.call(env) + if ENV['GITLAB_GRACK_AUTH_ONLY'] == '1' + # Tell gitlab-git-http-server the request is OK, and what the GL_ID is + render_grack_auth_ok + else + @app.call(env) + end elsif @user.nil? && !@gitlab_ci unauthorized else @@ -174,6 +179,10 @@ module Grack end end + def render_grack_auth_ok + [200, { "Content-Type" => "application/json" }, [JSON.dump({ 'GL_ID' => Gitlab::ShellEnv.gl_id(@user) })]] + end + def render_not_found [404, { "Content-Type" => "text/plain" }, ["Not Found"]] end diff --git a/lib/gitlab/backend/shell_env.rb b/lib/gitlab/backend/shell_env.rb index 17ec029eed..9f5adee594 100644 --- a/lib/gitlab/backend/shell_env.rb +++ b/lib/gitlab/backend/shell_env.rb @@ -7,7 +7,7 @@ module Gitlab def set_env(user) # Set GL_ID env variable if user - ENV['GL_ID'] = "user-#{user.id}" + ENV['GL_ID'] = gl_id(user) end end @@ -15,5 +15,14 @@ module Gitlab # Reset GL_ID env variable ENV['GL_ID'] = nil end + + def gl_id(user) + if user.present? + "user-#{user.id}" + else + # This empty string is used in the render_grack_auth_ok method + "" + end + end end end diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb index 5b1952b967..778b76f689 100644 --- a/lib/gitlab/bitbucket_import/client.rb +++ b/lib/gitlab/bitbucket_import/client.rb @@ -1,6 +1,8 @@ module Gitlab module BitbucketImport class Client + class Unauthorized < StandardError; end + attr_reader :consumer, :api def initialize(access_token = nil, access_token_secret = nil) @@ -46,23 +48,23 @@ module Gitlab end def user - JSON.parse(api.get("/api/1.0/user").body) + JSON.parse(get("/api/1.0/user").body) end def issues(project_identifier) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues").body) + JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues").body) end def issue_comments(project_identifier, issue_id) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body) + JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body) end def project(project_identifier) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}").body) + JSON.parse(get("/api/1.0/repositories/#{project_identifier}").body) end def find_deploy_key(project_identifier, key) - JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| + JSON.parse(get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key| deploy_key["key"].chomp == key.chomp end end @@ -82,11 +84,18 @@ module Gitlab end def projects - JSON.parse(api.get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" } + JSON.parse(get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" } end private + def get(url) + response = api.get(url) + raise Unauthorized if (400..499).include?(response.code.to_i) + + response + end + def config Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket"} end diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/relative_link_filter.rb index 9de2b24a9d..30f50b8299 100644 --- a/lib/gitlab/markdown/relative_link_filter.rb +++ b/lib/gitlab/markdown/relative_link_filter.rb @@ -98,15 +98,25 @@ module Gitlab # # Returns a String def path_type(path) - if repository.tree(current_sha, path).entries.any? + unescaped_path = Addressable::URI.unescape(path) + + if tree?(unescaped_path) 'tree' - elsif repository.blob_at(current_sha, path).try(:image?) + elsif image?(unescaped_path) 'raw' else 'blob' end end + def tree?(path) + repository.tree(current_sha, path).entries.any? + end + + def image?(path) + repository.blob_at(current_sha, path).try(:image?) + end + def current_sha context[:commit].try(:id) || ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha diff --git a/lib/gitlab/o_auth/provider.rb b/lib/gitlab/o_auth/provider.rb index f986499a27..90c3fe8da3 100644 --- a/lib/gitlab/o_auth/provider.rb +++ b/lib/gitlab/o_auth/provider.rb @@ -1,18 +1,30 @@ module Gitlab module OAuth class Provider - def self.names - providers = [] + def self.providers + Devise.omniauth_providers + end - Gitlab.config.ldap.servers.values.each do |server| - providers << server['provider_name'] + def self.enabled?(name) + providers.include?(name.to_sym) + end + + def self.ldap_provider?(name) + name.to_s.start_with?('ldap') + end + + def self.config_for(name) + name = name.to_s + if ldap_provider?(name) + Gitlab::LDAP::Config.new(name).options + else + Gitlab.config.omniauth.providers.find { |provider| provider.name == name } end + end - Gitlab.config.omniauth.providers.each do |provider| - providers << provider['name'] - end - - providers + def self.label_for(name) + config = config_for(name) + (config && config['label']) || name.to_s.titleize end end end diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb index 2f7aff03c2..04440e4f68 100644 --- a/lib/redcarpet/render/gitlab_html.rb +++ b/lib/redcarpet/render/gitlab_html.rb @@ -22,10 +22,10 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML ERB::Util.html_escape_once(text) end - # Stolen from Rugments::Plugins::Redcarpet as this module is not required - # from Rugments's gem root. + # Stolen from Rouge::Plugins::Redcarpet as this module is not required + # from Rouge's gem root. def block_code(code, language) - lexer = Rugments::Lexer.find_fancy(language, code) || Rugments::Lexers::PlainText + lexer = Rouge::Lexer.find_fancy(language, code) || Rouge::Lexers::PlainText # XXX HACK: Redcarpet strips hard tabs out of code blocks, # so we assume you're not using leading spaces that aren't tabs, @@ -34,7 +34,7 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML code.gsub!(/^ /, "\t") end - formatter = Rugments::Formatters::HTML.new( + formatter = Rouge::Formatters::HTMLGitlab.new( cssclass: "code highlight #{@color_scheme} #{lexer.tag}" ) formatter.format(lexer.lex(code)) diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb new file mode 100644 index 0000000000..092a920a0c --- /dev/null +++ b/lib/rouge/formatters/html_gitlab.rb @@ -0,0 +1,176 @@ +require 'cgi' + +module Rouge + module Formatters + class HTMLGitlab < Rouge::Formatter + tag 'html_gitlab' + + # Creates a new Rouge::Formatter::HTMLGitlab instance. + # + # [+nowrap+] If set to True, don't wrap the output at all, not + # even inside a
 tag (default: false).
+      # [+cssclass+]        CSS class for the wrapping 
tag + # (default: 'highlight'). + # [+linenos+] If set to 'table', output line numbers as a table + # with two cells, one containing the line numbers, + # the other the whole code. This is copy paste friendly, + # but may cause alignment problems with some browsers + # or fonts. If set to 'inline', the line numbers will + # be integrated in the
 tag that contains
+      #                     the code (default: nil).
+      # [+linenostart+]     The line number for the first line (default: 1).
+      # [+lineanchors+]     If set to true the formatter will wrap each output
+      #                     line in an anchor tag with a name of L-linenumber.
+      #                     This allows easy linking to certain lines
+      #                     (default: false).
+      # [+lineanchorsid+]   If lineanchors is true the name of the anchors can
+      #                     be changed with lineanchorsid to e.g. foo-linenumber
+      #                     (default: 'L').
+      # [+anchorlinenos+]   If set to true, will wrap line numbers in 
+      #                     tags. Used in combination with linenos and lineanchors
+      #                     (default: false).
+      # [+inline_theme+]    Inline CSS styles for the 
 tag (default: false).
+      def initialize(
+          nowrap: false,
+          cssclass: 'highlight',
+          linenos: nil,
+          linenostart: 1,
+          lineanchors: false,
+          lineanchorsid: 'L',
+          anchorlinenos: false,
+          inline_theme: nil
+        )
+        @nowrap = nowrap
+        @cssclass = cssclass
+        @linenos = linenos
+        @linenostart = linenostart
+        @lineanchors = lineanchors
+        @lineanchorsid = lineanchorsid
+        @anchorlinenos = anchorlinenos
+        @inline_theme = Theme.find(@inline_theme).new if @inline_theme.is_a?(String)
+      end
+
+      def render(tokens)
+        case @linenos
+        when 'table'
+          render_tableized(tokens)
+        when 'inline'
+          render_untableized(tokens)
+        else
+          render_untableized(tokens)
+        end
+      end
+
+      alias_method :format, :render
+
+      private
+
+      def render_untableized(tokens)
+        data = process_tokens(tokens)
+
+        html = ''
+        html << "
" unless @nowrap
+        html << wrap_lines(data[:code])
+        html << "
\n" unless @nowrap + html + end + + def render_tableized(tokens) + data = process_tokens(tokens) + + html = '' + html << "
" unless @nowrap + html << '' + html << "' + html << "' + html << '
"
+        html << wrap_linenos(data[:numbers])
+        html << '
"
+        html << wrap_lines(data[:code])
+        html << '
' + html << '
' unless @nowrap + html + end + + def process_tokens(tokens) + num_lines = 0 + last_val = '' + rendered = '' + + tokens.each do |tok, val| + last_val = val + num_lines += val.scan(/\n/).size + rendered << span(tok, val) + end + + numbers = (@linenostart..num_lines + @linenostart - 1).to_a + + { numbers: numbers, code: rendered } + end + + def wrap_linenos(numbers) + if @anchorlinenos + numbers.map! do |number| + "
#{number}" + end + end + numbers.join("\n") + end + + def wrap_lines(rendered) + if @lineanchors + lines = rendered.split("\n") + lines = lines.each_with_index.map do |line, index| + number = index + @linenostart + + if @linenos == 'inline' + "" \ + "#{number}" \ + "#{line}" \ + '' + else + "#{line}" \ + '' + end + end + lines.join("\n") + else + if @linenos == 'inline' + lines = rendered.split("\n") + lines = lines.each_with_index.map do |line, index| + number = index + @linenostart + "#{number}#{line}" + end + lines.join("\n") + else + rendered + end + end + end + + def wrap_values(val, element) + lines = val.split("\n") + lines = lines.map{ |x| "#{x}" } + lines.join("\n") + end + + def span(tok, val) + # http://stackoverflow.com/a/1600584/2587286 + val = CGI.escapeHTML(val) + + 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(';')}\"") + else + wrap_values(val, "class=\"#{tok.shortname}\"") + end + end + end + end + end +end diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab index edb987875d..efa0898900 100644 --- a/lib/support/nginx/gitlab +++ b/lib/support/nginx/gitlab @@ -38,6 +38,11 @@ upstream gitlab { server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; } +## Experimental: gitlab-git-http-server +# upstream gitlab-git-http-server { +# server localhost:8181; +# } + ## Normal HTTP host server { ## Either remove "default_server" from the listen line below, @@ -109,6 +114,26 @@ server { proxy_pass http://gitlab; } + ## Experimental: send Git HTTP traffic to gitlab-git-http-server instead of Unicorn + # location ~ [-\/\w\.]+\.git\/ { + # ## If you use HTTPS make sure you disable gzip compression + # ## to be safe against BREACH attack. + # # gzip off; + + # ## https://github.com/gitlabhq/gitlabhq/issues/694 + # ## Some requests take more than 30 seconds. + # proxy_read_timeout 300; + # proxy_connect_timeout 300; + # proxy_redirect off; + + # proxy_set_header Host $http_host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + + # proxy_pass http://gitlab-git-http-server; + # } + ## Enable gzip compression as per rails guide: ## http://guides.rubyonrails.org/asset_pipeline.html#gzip-compression ## WARNING: If you are using relative urls remove the block below diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl index 766559b49f..314525518f 100644 --- a/lib/support/nginx/gitlab-ssl +++ b/lib/support/nginx/gitlab-ssl @@ -42,6 +42,11 @@ upstream gitlab { server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0; } +## Experimental: gitlab-git-http-server +# upstream gitlab-git-http-server { +# server localhost:8181; +# } + ## Redirects all HTTP traffic to the HTTPS host server { ## Either remove "default_server" from the listen line below, @@ -156,6 +161,26 @@ server { proxy_pass http://gitlab; } + ## Experimental: send Git HTTP traffic to gitlab-git-http-server instead of Unicorn + # location ~ [-\/\w\.]+\.git\/ { + # ## If you use HTTPS make sure you disable gzip compression + # ## to be safe against BREACH attack. + # gzip off; + + # ## https://github.com/gitlabhq/gitlabhq/issues/694 + # ## Some requests take more than 30 seconds. + # proxy_read_timeout 300; + # proxy_connect_timeout 300; + # proxy_redirect off; + + # proxy_set_header Host $http_host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-Ssl on; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # proxy_pass http://gitlab-git-http-server; + # } + ## Enable gzip compression as per rails guide: ## http://guides.rubyonrails.org/asset_pipeline.html#gzip-compression ## WARNING: If you are using relative urls remove the block below diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake index aed84226a2..badb47c677 100644 --- a/lib/tasks/gitlab/check.rake +++ b/lib/tasks/gitlab/check.rake @@ -485,7 +485,8 @@ namespace :gitlab do if project.empty_repo? puts "repository is empty".magenta - elsif File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path) + elsif File.directory?(project_hook_directory) && File.directory?(gitlab_shell_hooks_path) && + (File.realpath(project_hook_directory) == File.realpath(gitlab_shell_hooks_path)) puts 'ok'.green else puts "wrong or missing hooks".red @@ -754,7 +755,7 @@ namespace :gitlab do print "Ruby version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "yes (#{current_version})".green + puts "yes (#{current_version})".green else puts "no".red try_fixing_it( @@ -772,7 +773,7 @@ namespace :gitlab do print "Git version >= #{required_version} ? ... " if current_version.valid? && required_version <= current_version - puts "yes (#{current_version})".green + puts "yes (#{current_version})".green else puts "no".red try_fixing_it( @@ -806,4 +807,3 @@ namespace :gitlab do end end end - diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake index 5f83e5e8e7..c1ee271ae2 100644 --- a/lib/tasks/gitlab/import.rake +++ b/lib/tasks/gitlab/import.rake @@ -62,11 +62,11 @@ namespace :gitlab do project = Projects::CreateService.new(user, project_params).execute - if project.valid? + if project.persisted? puts " * Created #{project.name} (#{repo_path})".green else puts " * Failed trying to create #{project.name} (#{repo_path})".red - puts " Validation Errors: #{project.errors.messages}".red + puts " Errors: #{project.errors.messages}".red end end end diff --git a/lib/tasks/gitlab/mail_google_schema_whitelisting.rake b/lib/tasks/gitlab/mail_google_schema_whitelisting.rake deleted file mode 100644 index 102c6ae55d..0000000000 --- a/lib/tasks/gitlab/mail_google_schema_whitelisting.rake +++ /dev/null @@ -1,73 +0,0 @@ -require "#{Rails.root}/app/helpers/emails_helper" -require 'action_view/helpers' -extend ActionView::Helpers - -include ActionView::Context -include EmailsHelper - -namespace :gitlab do - desc "Email google whitelisting email with example email for actions in inbox" - task mail_google_schema_whitelisting: :environment do - subject = "Rails | Implemented feature" - url = "#{Gitlab.config.gitlab.url}/base/rails-project/issues/#{rand(1..100)}#note_#{rand(10..1000)}" - schema = email_action(url) - body = email_template(schema, url) - mail = Notify.test_email("schema.whitelisting+sample@gmail.com", subject, body.html_safe) - if send_now - mail.deliver - else - puts "WOULD SEND:" - end - puts mail - end - - def email_template(schema, url) - " - - - - GitLab - - - - - -
-
-

I like it :+1:

-
-
- - - - " - end - - def send_now - if ENV['SEND'] == "true" - true - else - false - end - end -end diff --git a/lib/tasks/gitlab/update_commit_count.rake b/lib/tasks/gitlab/update_commit_count.rake new file mode 100644 index 0000000000..9b636f12d9 --- /dev/null +++ b/lib/tasks/gitlab/update_commit_count.rake @@ -0,0 +1,20 @@ +namespace :gitlab do + desc "GitLab | Update commit count for projects" + task update_commit_count: :environment do + projects = Project.where(commit_count: 0) + puts "#{projects.size} projects need to be updated. This might take a while." + ask_to_continue unless ENV['force'] == 'yes' + + projects.find_each(batch_size: 100) do |project| + print "#{project.name_with_namespace.yellow} ... " + + unless project.repo_exists? + puts "skipping, because the repo is empty".magenta + next + end + + project.update_commit_count + puts project.commit_count.to_s.green + end + end +end diff --git a/lib/unfold_form.rb b/lib/unfold_form.rb index 46b12beeaa..fcd01503d1 100644 --- a/lib/unfold_form.rb +++ b/lib/unfold_form.rb @@ -8,4 +8,5 @@ class UnfoldForm attribute :bottom, Boolean attribute :unfold, Boolean, default: true attribute :offset, Integer + attribute :indent, Integer, default: 0 end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index d47a37914d..9f89101d7f 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -1,25 +1,38 @@ require 'spec_helper' describe UsersController do - let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } - - before do - sign_in(user) - end + let(:user) { create(:user) } describe 'GET #show' do - render_views + it 'is case-insensitive' do + user = create(:user, username: 'CamelCaseUser') + sign_in(user) - it 'renders the show template' do - get :show, username: user.username - expect(response.status).to eq(200) - expect(response).to render_template('show') + get :show, username: user.username.downcase + + expect(response).to be_success + end + + context 'with rendered views' do + render_views + + it 'renders the show template' do + sign_in(user) + + get :show, username: user.username + + expect(response).to be_success + expect(response).to render_template('show') + end end end describe 'GET #calendar' do it 'renders calendar' do + sign_in(user) + get :calendar, username: user.username + expect(response).to render_template('calendar') end end @@ -30,6 +43,8 @@ describe UsersController do before do allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id]) + + sign_in(user) project.team << [user, :developer] end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index b8199aa5e6..859a62f740 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -17,26 +17,208 @@ require 'erb' # -> Post-process HTML # -> `gfm_with_options` helper # -> HTML::Pipeline -# -> Sanitize -# -> RelativeLink -# -> Emoji -# -> Table of Contents -# -> Autolinks -# -> Rinku (http, https, ftp) -# -> Other schemes -# -> ExternalLink -# -> References -# -> TaskList +# -> SanitizationFilter +# -> Other filters, depending on pipeline # -> `html_safe` # -> Template # # See the MarkdownFeature class for setup details. describe 'GitLab Markdown', feature: true do - include ActionView::Helpers::TagHelper - include ActionView::Helpers::UrlHelper include Capybara::Node::Matchers include GitlabMarkdownHelper + include MarkdownMatchers + + # Sometimes it can be useful to see the parsed output of the Markdown document + # for debugging. Call this method to write the output to + # `tmp/capybara/.html`. + def write_markdown(filename = 'markdown_spec') + File.open(Rails.root.join("tmp/capybara/#{filename}.html"), 'w') do |file| + file.puts @html + end + end + + def doc(html = @html) + Nokogiri::HTML::DocumentFragment.parse(html) + end + + # Shared behavior that all pipelines should exhibit + shared_examples 'all pipelines' do + describe 'Redcarpet extensions' do + it 'does not parse emphasis inside of words' do + expect(doc.to_html).not_to match('foobarbaz') + end + + it 'parses table Markdown' do + aggregate_failures do + expect(doc).to have_selector('th:contains("Header")') + expect(doc).to have_selector('th:contains("Row")') + expect(doc).to have_selector('th:contains("Example")') + end + end + + it 'allows Markdown in tables' do + expect(doc.at_css('td:contains("Baz")').children.to_html). + to eq 'Baz' + end + + it 'parses fenced code blocks' do + aggregate_failures do + expect(doc).to have_selector('pre.code.highlight.white.c') + expect(doc).to have_selector('pre.code.highlight.white.python') + end + end + + it 'parses strikethroughs' do + expect(doc).to have_selector(%{del:contains("and this text doesn't")}) + end + + it 'parses superscript' do + expect(doc).to have_selector('sup', count: 2) + end + end + + describe 'SanitizationFilter' do + it 'permits b elements' do + expect(doc).to have_selector('b:contains("b tag")') + end + + it 'permits em elements' do + expect(doc).to have_selector('em:contains("em tag")') + end + + it 'permits code elements' do + expect(doc).to have_selector('code:contains("code tag")') + end + + it 'permits kbd elements' do + expect(doc).to have_selector('kbd:contains("s")') + end + + it 'permits strike elements' do + expect(doc).to have_selector('strike:contains(Emoji)') + end + + it 'permits img elements' do + expect(doc).to have_selector('img[src*="smile.png"]') + end + + it 'permits br elements' do + expect(doc).to have_selector('br') + end + + it 'permits hr elements' do + expect(doc).to have_selector('hr') + end + + it 'permits span elements' do + expect(doc).to have_selector('span:contains("span tag")') + end + + it 'permits style attribute in th elements' do + aggregate_failures do + expect(doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center' + expect(doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right' + expect(doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left' + end + end + + it 'permits style attribute in td elements' do + aggregate_failures do + expect(doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center' + expect(doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right' + expect(doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left' + end + end + + it 'removes `rel` attribute from links' do + expect(doc).not_to have_selector('a[rel="bookmark"]') + end + + it "removes `href` from `a` elements if it's fishy" do + expect(doc).not_to have_selector('a[href*="javascript"]') + end + end + + describe 'Escaping' do + it 'escapes non-tag angle brackets' do + table = doc.css('table').last.at_css('tbody') + expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 < 3 & 5' + end + end + + describe 'Edge Cases' do + it 'allows markup inside link elements' do + aggregate_failures do + expect(doc.at_css('a[href="#link-emphasis"]').to_html). + to eq %{text} + + expect(doc.at_css('a[href="#link-strong"]').to_html). + to eq %{text} + + expect(doc.at_css('a[href="#link-code"]').to_html). + to eq %{text} + end + end + end + + describe 'ExternalLinkFilter' do + it 'adds nofollow to external link' do + link = doc.at_css('a:contains("Google")') + expect(link.attr('rel')).to match 'nofollow' + end + + it 'ignores internal link' do + link = doc.at_css('a:contains("GitLab Root")') + expect(link.attr('rel')).not_to match 'nofollow' + end + end + end + + context 'default pipeline' do + before(:all) do + @feat = MarkdownFeature.new + + # `gfm_with_options` depends on a `@project` variable + @project = @feat.project + + @html = markdown(@feat.raw_markdown) + end + + it_behaves_like 'all pipelines' + + it 'includes RelativeLinkFilter' do + expect(doc).to parse_relative_links + end + + it 'includes EmojiFilter' do + expect(doc).to parse_emoji + end + + it 'includes TableOfContentsFilter' do + expect(doc).to create_header_links + end + + it 'includes AutolinkFilter' do + expect(doc).to create_autolinks + end + + it 'includes all reference filters' do + aggregate_failures do + expect(doc).to reference_users + expect(doc).to reference_issues + expect(doc).to reference_merge_requests + expect(doc).to reference_snippets + expect(doc).to reference_commit_ranges + expect(doc).to reference_commits + expect(doc).to reference_labels + end + end + + it 'includes TaskListFilter' do + expect(doc).to parse_task_lists + end + end # `markdown` calls these two methods def current_user @@ -46,381 +228,4 @@ describe 'GitLab Markdown', feature: true do def user_color_scheme_class :white end - - # Let's only parse this thing once - before(:all) do - @feat = MarkdownFeature.new - - # `markdown` expects a `@project` variable - @project = @feat.project - - @md = markdown(@feat.raw_markdown) - @doc = Nokogiri::HTML::DocumentFragment.parse(@md) - end - - after(:all) do - @feat.teardown - end - - # Given a header ID, goes to that element's parent (the header itself), then - # its next sibling element (the body). - def get_section(id) - @doc.at_css("##{id}").parent.next_element - end - - # Sometimes it can be useful to see the parsed output of the Markdown document - # for debugging. Uncomment this block to write the output to - # tmp/capybara/markdown_spec.html. - # - # it 'writes to a file' do - # File.open(Rails.root.join('tmp/capybara/markdown_spec.html'), 'w') do |file| - # file.puts @md - # end - # end - - describe 'Markdown' do - describe 'No Intra Emphasis' do - it 'does not parse emphasis inside of words' do - body = get_section('no-intra-emphasis') - expect(body.to_html).not_to match('foobarbaz') - end - end - - describe 'Tables' do - it 'parses table Markdown' do - body = get_section('tables') - expect(body).to have_selector('th:contains("Header")') - expect(body).to have_selector('th:contains("Row")') - expect(body).to have_selector('th:contains("Example")') - end - - it 'allows Markdown in tables' do - expect(@doc.at_css('td:contains("Baz")').children.to_html). - to eq 'Baz' - end - end - - describe 'Fenced Code Blocks' do - it 'parses fenced code blocks' do - expect(@doc).to have_selector('pre.code.highlight.white.c') - expect(@doc).to have_selector('pre.code.highlight.white.python') - end - end - - describe 'Strikethrough' do - it 'parses strikethroughs' do - expect(@doc).to have_selector(%{del:contains("and this text doesn't")}) - end - end - - describe 'Superscript' do - it 'parses superscript' do - body = get_section('superscript') - expect(body.to_html).to match('1st') - expect(body.to_html).to match('2nd') - end - end - end - - describe 'HTML::Pipeline' do - describe 'SanitizationFilter' do - it 'uses a permissive whitelist' do - expect(@doc).to have_selector('b:contains("b tag")') - expect(@doc).to have_selector('em:contains("em tag")') - expect(@doc).to have_selector('code:contains("code tag")') - expect(@doc).to have_selector('kbd:contains("s")') - expect(@doc).to have_selector('strike:contains(Emoji)') - expect(@doc).to have_selector('img[src*="smile.png"]') - expect(@doc).to have_selector('br') - expect(@doc).to have_selector('hr') - end - - it 'permits span elements' do - expect(@doc).to have_selector('span:contains("span tag")') - end - - it 'permits table alignment' do - expect(@doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center' - expect(@doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right' - expect(@doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left' - - expect(@doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center' - expect(@doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right' - expect(@doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left' - end - - it 'removes `rel` attribute from links' do - body = get_section('sanitizationfilter') - expect(body).not_to have_selector('a[rel="bookmark"]') - end - - it "removes `href` from `a` elements if it's fishy" do - expect(@doc).not_to have_selector('a[href*="javascript"]') - end - end - - describe 'Escaping' do - let(:table) { @doc.css('table').last.at_css('tbody') } - - it 'escapes non-tag angle brackets' do - expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 < 3 & 5' - end - end - - describe 'Edge Cases' do - it 'allows markup inside link elements' do - expect(@doc.at_css('a[href="#link-emphasis"]').to_html). - to eq %{text} - - expect(@doc.at_css('a[href="#link-strong"]').to_html). - to eq %{text} - - expect(@doc.at_css('a[href="#link-code"]').to_html). - to eq %{text} - end - end - - describe 'EmojiFilter' do - it 'parses Emoji' do - expect(@doc).to have_selector('img.emoji', count: 10) - end - end - - describe 'TableOfContentsFilter' do - it 'creates anchors inside header elements' do - expect(@doc).to have_selector('h1 a#gitlab-markdown') - expect(@doc).to have_selector('h2 a#markdown') - expect(@doc).to have_selector('h3 a#autolinkfilter') - end - end - - describe 'AutolinkFilter' do - let(:list) { get_section('autolinkfilter').next_element } - - def item(index) - list.at_css("li:nth-child(#{index})") - end - - it 'autolinks http://' do - expect(item(1).children.first.name).to eq 'a' - expect(item(1).children.first['href']).to eq 'http://about.gitlab.com/' - end - - it 'autolinks https://' do - expect(item(2).children.first.name).to eq 'a' - expect(item(2).children.first['href']).to eq 'https://google.com/' - end - - it 'autolinks ftp://' do - expect(item(3).children.first.name).to eq 'a' - expect(item(3).children.first['href']).to eq 'ftp://ftp.us.debian.org/debian/' - end - - it 'autolinks smb://' do - expect(item(4).children.first.name).to eq 'a' - expect(item(4).children.first['href']).to eq 'smb://foo/bar/baz' - end - - it 'autolinks irc://' do - expect(item(5).children.first.name).to eq 'a' - expect(item(5).children.first['href']).to eq 'irc://irc.freenode.net/git' - end - - it 'autolinks short, invalid URLs' do - expect(item(6).children.first.name).to eq 'a' - expect(item(6).children.first['href']).to eq 'http://localhost:3000' - end - - %w(code a kbd).each do |elem| - it "ignores links inside '#{elem}' element" do - body = get_section('autolinkfilter') - expect(body).not_to have_selector("#{elem} a") - end - end - end - - describe 'ExternalLinkFilter' do - let(:links) { get_section('externallinkfilter').next_element } - - it 'adds nofollow to external link' do - expect(links.css('a').first.to_html).to match 'nofollow' - end - - it 'ignores internal link' do - expect(links.css('a').last.to_html).not_to match 'nofollow' - end - end - - describe 'ReferenceFilter' do - it 'handles references in headers' do - header = @doc.at_css('#reference-filters-eg-1').parent - - expect(header.css('a').size).to eq 2 - end - - it "handles references in Markdown" do - body = get_section('reference-filters-eg-1') - expect(body).to have_selector('em a.gfm-merge_request', count: 1) - end - - it 'parses user references' do - body = get_section('userreferencefilter') - expect(body).to have_selector('a.gfm.gfm-project_member', count: 3) - end - - it 'parses issue references' do - body = get_section('issuereferencefilter') - expect(body).to have_selector('a.gfm.gfm-issue', count: 2) - end - - it 'parses merge request references' do - body = get_section('mergerequestreferencefilter') - expect(body).to have_selector('a.gfm.gfm-merge_request', count: 2) - end - - it 'parses snippet references' do - body = get_section('snippetreferencefilter') - expect(body).to have_selector('a.gfm.gfm-snippet', count: 2) - end - - it 'parses commit range references' do - body = get_section('commitrangereferencefilter') - expect(body).to have_selector('a.gfm.gfm-commit_range', count: 2) - end - - it 'parses commit references' do - body = get_section('commitreferencefilter') - expect(body).to have_selector('a.gfm.gfm-commit', count: 2) - end - - it 'parses label references' do - body = get_section('labelreferencefilter') - expect(body).to have_selector('a.gfm.gfm-label', count: 3) - end - end - - describe 'Task Lists' do - it 'generates task lists' do - body = get_section('task-lists') - expect(body).to have_selector('ul.task-list', count: 2) - expect(body).to have_selector('li.task-list-item', count: 7) - expect(body).to have_selector('input[checked]', count: 3) - end - end - end -end - -# This is a helper class used by the GitLab Markdown feature spec -# -# Because the feature spec only cares about the output of the Markdown, and the -# test setup and teardown and parsing is fairly expensive, we only want to do it -# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)` -# block, so we fake it by encapsulating all the shared setup in this class. -# -# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for -# reference to the factory-created objects. -class MarkdownFeature - include FactoryGirl::Syntax::Methods - - def initialize - DatabaseCleaner.start - end - - def teardown - DatabaseCleaner.clean - end - - def user - @user ||= create(:user) - end - - def group - unless @group - @group = create(:group) - @group.add_user(user, Gitlab::Access::DEVELOPER) - end - - @group - end - - # Direct references ---------------------------------------------------------- - - def project - @project ||= create(:project) - end - - def issue - @issue ||= create(:issue, project: project) - end - - def merge_request - @merge_request ||= create(:merge_request, :simple, source_project: project) - end - - def snippet - @snippet ||= create(:project_snippet, project: project) - end - - def commit - @commit ||= project.commit - end - - def commit_range - unless @commit_range - commit2 = project.commit('HEAD~3') - @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project) - end - - @commit_range - end - - def simple_label - @simple_label ||= create(:label, name: 'gfm', project: project) - end - - def label - @label ||= create(:label, name: 'awaiting feedback', project: project) - end - - # Cross-references ----------------------------------------------------------- - - def xproject - unless @xproject - namespace = create(:namespace, name: 'cross-reference') - @xproject = create(:project, namespace: namespace) - @xproject.team << [user, :developer] - end - - @xproject - end - - def xissue - @xissue ||= create(:issue, project: xproject) - end - - def xmerge_request - @xmerge_request ||= create(:merge_request, :simple, source_project: xproject) - end - - def xsnippet - @xsnippet ||= create(:project_snippet, project: xproject) - end - - def xcommit - @xcommit ||= xproject.commit - end - - def xcommit_range - unless @xcommit_range - xcommit2 = xproject.commit('HEAD~2') - @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) - end - - @xcommit_range - end - - def raw_markdown - fixture = Rails.root.join('spec/fixtures/markdown.md.erb') - ERB.new(File.read(fixture)).result(binding) - end end diff --git a/spec/features/security/group_access_spec.rb b/spec/features/security/group_access_spec.rb index 0f36e47498..8ce1538860 100644 --- a/spec/features/security/group_access_spec.rb +++ b/spec/features/security/group_access_spec.rb @@ -36,7 +36,7 @@ describe 'Group access', feature: true do subject { group_path(group) } context 'with public projects' do - before(:all) { create_project(:public) } + let!(:project) { create_project(:public) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_allowed_for group_member(:master) } @@ -48,7 +48,7 @@ describe 'Group access', feature: true do end context 'with mixed projects' do - before(:all) { create_project(:mixed) } + let!(:project) { create_project(:mixed) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_allowed_for group_member(:master) } @@ -60,7 +60,7 @@ describe 'Group access', feature: true do end context 'with internal projects' do - before(:all) { create_project(:internal) } + let!(:project) { create_project(:internal) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_allowed_for group_member(:master) } @@ -86,7 +86,7 @@ describe 'Group access', feature: true do subject { issues_group_path(group) } context 'with public projects' do - before(:all) { create_project(:public) } + let!(:project) { create_project(:public) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_allowed_for group_member(:master) } @@ -98,7 +98,7 @@ describe 'Group access', feature: true do end context 'with mixed projects' do - before(:all) { create_project(:mixed) } + let!(:project) { create_project(:mixed) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_allowed_for group_member(:master) } @@ -110,7 +110,7 @@ describe 'Group access', feature: true do end context 'with internal projects' do - before(:all) { create_project(:internal) } + let!(:project) { create_project(:internal) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_allowed_for group_member(:master) } @@ -136,7 +136,7 @@ describe 'Group access', feature: true do subject { merge_requests_group_path(group) } context 'with public projects' do - before(:all) { create_project(:public) } + let!(:project) { create_project(:public) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_allowed_for group_member(:master) } @@ -148,7 +148,7 @@ describe 'Group access', feature: true do end context 'with mixed projects' do - before(:all) { create_project(:mixed) } + let!(:project) { create_project(:mixed) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_allowed_for group_member(:master) } @@ -160,7 +160,7 @@ describe 'Group access', feature: true do end context 'with internal projects' do - before(:all) { create_project(:internal) } + let!(:project) { create_project(:internal) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_allowed_for group_member(:master) } @@ -186,7 +186,7 @@ describe 'Group access', feature: true do subject { group_group_members_path(group) } context 'with public projects' do - before(:all) { create_project(:public) } + let!(:project) { create_project(:public) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_allowed_for group_member(:master) } @@ -198,7 +198,7 @@ describe 'Group access', feature: true do end context 'with mixed projects' do - before(:all) { create_project(:mixed) } + let!(:project) { create_project(:mixed) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_allowed_for group_member(:master) } @@ -210,7 +210,7 @@ describe 'Group access', feature: true do end context 'with internal projects' do - before(:all) { create_project(:internal) } + let!(:project) { create_project(:internal) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_allowed_for group_member(:master) } @@ -236,7 +236,7 @@ describe 'Group access', feature: true do subject { edit_group_path(group) } context 'with public projects' do - before(:all) { create_project(:public) } + let!(:project) { create_project(:public) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_denied_for group_member(:master) } @@ -248,7 +248,7 @@ describe 'Group access', feature: true do end context 'with mixed projects' do - before(:all) { create_project(:mixed) } + let!(:project) { create_project(:mixed) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_denied_for group_member(:master) } @@ -260,7 +260,7 @@ describe 'Group access', feature: true do end context 'with internal projects' do - before(:all) { create_project(:internal) } + let!(:project) { create_project(:internal) } it { is_expected.to be_allowed_for group_member(:owner) } it { is_expected.to be_denied_for group_member(:master) } diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index 02ab46c905..41d12afa9c 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -100,6 +100,13 @@ Markdown should be usable inside a link. Let's try! - [**text**](#link-strong) - [`text`](#link-code) +### RelativeLinkFilter + +Linking to a file relative to this project's repository should work. + +[Relative Link](doc/README.md) +![Relative Image](app/assets/images/touch-icon-ipad.png) + ### EmojiFilter Because life would be :zzz: without Emoji, right? :rocket: @@ -123,9 +130,9 @@ These are all plain text that should get turned into links: But it shouldn't autolink text inside certain tags: -- http://about.gitlab.com/ -- http://about.gitlab.com/ -- http://about.gitlab.com/ +- http://code.gitlab.com/ +- http://a.gitlab.com/ +- http://kbd.gitlab.com/ ### ExternalLinkFilter diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb new file mode 100644 index 0000000000..e47a54fdac --- /dev/null +++ b/spec/helpers/auth_helper_spec.rb @@ -0,0 +1,20 @@ +require "spec_helper" + +describe AuthHelper do + describe "button_based_providers" do + it 'returns all enabled providers' do + allow(helper).to receive(:auth_providers) { [:twitter, :github] } + expect(helper.button_based_providers).to include(*[:twitter, :github]) + end + + it 'does not return ldap provider' do + allow(helper).to receive(:auth_providers) { [:twitter, :ldapmain] } + expect(helper.button_based_providers).to include(:twitter) + end + + it 'returns empty array' do + allow(helper).to receive(:auth_providers) { [] } + expect(helper.button_based_providers).to eq([]) + end + end +end diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index e49e4e6d5d..76009c3609 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -6,6 +6,14 @@ describe BlobHelper do let(:no_context_content) { ":type \"assem\"))" } let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } let(:split_content) { blob_content.split("\n") } + let(:multiline_content) do + %q( + def test(input): + """This is line 1 of a multi-line comment. + This is line 2. + """ + ) + end it 'should return plaintext for unknown lexer context' do result = highlight(blob_name, no_context_content, nowrap: true, continue: false) @@ -29,5 +37,15 @@ describe BlobHelper do result = split_content.map{ |content| highlight(blob_name, content, nowrap: true, continue: true) } expect(result).to eq(expected) end + + it 'should highlight multi-line comments' do + result = highlight(blob_name, multiline_content, nowrap: true, continue: false) + html = Nokogiri::HTML(result) + lines = html.search('.s') + expect(lines.count).to eq(3) + expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.') + expect(lines[1].text).to eq(' This is line 2.') + expect(lines[2].text).to eq(' """') + end end end diff --git a/spec/helpers/oauth_helper_spec.rb b/spec/helpers/oauth_helper_spec.rb deleted file mode 100644 index 4b1e67178d..0000000000 --- a/spec/helpers/oauth_helper_spec.rb +++ /dev/null @@ -1,32 +0,0 @@ -require "spec_helper" - -describe OauthHelper do - describe "additional_providers" do - it 'returns all enabled providers' do - allow(helper).to receive(:enabled_oauth_providers) { [:twitter, :github] } - expect(helper.additional_providers).to include(*[:twitter, :github]) - end - - it 'does not return ldap provider' do - allow(helper).to receive(:enabled_oauth_providers) { [:twitter, :ldapmain] } - expect(helper.additional_providers).to include(:twitter) - end - - it 'returns empty array' do - allow(helper).to receive(:enabled_oauth_providers) { [] } - expect(helper.additional_providers).to eq([]) - end - end - - describe "kerberos_enabled?" do - it 'returns true' do - allow(helper).to receive(:enabled_oauth_providers) { [:twitter, :github, :kerberos] } - expect(helper).to be_kerberos_enabled - end - - it 'returns false' do - allow(helper).to receive(:enabled_oauth_providers) { [:twitter, :ldapmain] } - expect(helper).not_to be_kerberos_enabled - end - end -end diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index beb9b4e438..99abb95d90 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -22,7 +22,7 @@ describe ProjectsHelper do let(:user) { create(:user) } - it "returns false if there are no approipriate permissions" do + it "returns false if there are no appropriate permissions" do allow(helper).to receive(:can?) { false } expect(helper.can_change_visibility_level?(project, user)).to be_falsey @@ -52,4 +52,22 @@ describe ProjectsHelper do end end end + + describe "readme_cache_key" do + let(:project) { create(:project) } + + before do + helper.instance_variable_set(:@project, project) + end + + it "returns a valid cach key" do + expect(helper.send(:readme_cache_key)).to eq("#{project.id}-#{project.commit.id}-readme") + end + + it "returns a valid cache key if HEAD does not exist" do + allow(project).to receive(:commit) { nil } + + expect(helper.send(:readme_cache_key)).to eq("#{project.id}-nil-readme") + end + end end diff --git a/spec/javascripts/merge_request_tabs_spec.js.coffee b/spec/javascripts/merge_request_tabs_spec.js.coffee index 6cc96fb68a..a0cfba455e 100644 --- a/spec/javascripts/merge_request_tabs_spec.js.coffee +++ b/spec/javascripts/merge_request_tabs_spec.js.coffee @@ -51,6 +51,12 @@ describe 'MergeRequestTabs', -> expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') + it 'changes from diffs.html', -> + @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs.html') + + expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') + expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') + it 'changes from notes', -> @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1') diff --git a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb index 5ee5310825..7f4d67e403 100644 --- a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb +++ b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb @@ -1,3 +1,5 @@ +# encoding: UTF-8 + require 'spec_helper' module Gitlab::Markdown @@ -101,6 +103,20 @@ module Gitlab::Markdown expect(doc.at_css('a')['href']).to eq 'http://example.com' end + it 'supports Unicode filenames' do + path = 'files/images/한글.png' + escaped = Addressable::URI.escape(path) + + # Stub these methods so the file doesn't actually need to be in the repo + allow_any_instance_of(described_class).to receive(:file_exists?). + and_return(true) + allow_any_instance_of(described_class). + to receive(:image?).with(path).and_return(true) + + doc = filter(image(escaped)) + expect(doc.at_css('img')['src']).to match '/raw/' + end + context 'when requested path is a file in the repo' do let(:requested_path) { 'doc/api/README.md' } include_examples :relative_to_requested diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 17ae887eb1..871e9c6c0a 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -111,14 +111,20 @@ describe Project do expect(project.url_to_repo).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + 'somewhere.git') end - it 'returns the full web URL for this repo' do - project = Project.new(path: 'somewhere') - expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/somewhere") + describe "#web_url" do + let(:project) { create(:empty_project, path: "somewhere") } + + it 'returns the full web URL for this repo' do + expect(project.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.namespace.path}/somewhere") + end end - it 'returns the web URL without the protocol for this repo' do - project = Project.new(path: 'somewhere') - expect(project.web_url_without_protocol).to eq("#{Gitlab.config.gitlab.url.split('://')[1]}/somewhere") + describe "#web_url_without_protocol" do + let(:project) { create(:empty_project, path: "somewhere") } + + it 'returns the web URL without the protocol for this repo' do + expect(project.web_url_without_protocol).to eq("#{Gitlab.config.gitlab.url.split('://')[1]}/#{project.namespace.path}/somewhere") + end end describe 'last_activity methods' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2b1b116849..aac8f00260 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -455,6 +455,18 @@ describe User do end end + describe '.find_by_username!' do + it 'raises RecordNotFound' do + expect { described_class.find_by_username!('JohnDoe') }. + to raise_error(ActiveRecord::RecordNotFound) + end + + it 'is case-insensitive' do + user = create(:user, username: 'JohnDoe') + expect(described_class.find_by_username!('JOHNDOE')).to eq user + end + end + describe 'all_ssh_keys' do it { is_expected.to have_many(:keys).dependent(:destroy) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 7030c105b5..29db035b2d 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -8,6 +8,7 @@ describe API::API, api: true do let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test") } let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test") } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } + let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } before do project.team << [user, :reporters] @@ -397,13 +398,14 @@ describe API::API, api: true do end describe "GET :id/merge_request/:merge_request_id/comments" do - it "should return merge_request comments" do + it "should return merge_request comments ordered by created_at" do get api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) expect(response.status).to eq(200) expect(json_response).to be_an Array - expect(json_response.length).to eq(1) + expect(json_response.length).to eq(2) expect(json_response.first['note']).to eq("a comment on a MR") expect(json_response.first['author']['id']).to eq(user.id) + expect(json_response.last['note']).to eq("another comment on a MR") end it "should return a 404 error if merge_request_id not found" do diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index b520a1d49b..b1d362b422 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -4,13 +4,19 @@ describe Projects::CreateService do describe :create_by_user do before do @user = create :user - @admin = create :user, admin: true @opts = { name: "GitLab", namespace: @user.namespace } end + it 'creates services on Project creation' do + project = create_project(@user, @opts) + project.reload + + expect(project.services).not_to be_empty + end + context 'user namespace' do before do @project = create_project(@user, @opts) @@ -75,7 +81,9 @@ describe Projects::CreateService do end it 'should allow a restricted visibility level for admins' do - project = create_project(@admin, @opts) + admin = create(:admin) + project = create_project(admin, @opts) + expect(project.errors.any?).to be(false) expect(project.saved?).to be(true) end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index 439a492cea..c04e842c67 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -29,7 +29,7 @@ describe Projects::ForkService do it "fails due to transaction failure" do @to_project = fork_project(@from_project, @to_user, false) expect(@to_project.errors).not_to be_empty - expect(@to_project.errors[:base]).to include("Failed to fork repository") + expect(@to_project.errors[:base]).to include("Failed to fork repository via gitlab-shell") end end diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb new file mode 100644 index 0000000000..2a868aed73 --- /dev/null +++ b/spec/support/markdown_feature.rb @@ -0,0 +1,106 @@ +# This is a helper class used by the GitLab Markdown feature spec +# +# Because the feature spec only cares about the output of the Markdown, and the +# test setup and teardown and parsing is fairly expensive, we only want to do it +# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)` +# block, so we fake it by encapsulating all the shared setup in this class. +# +# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for +# reference to the factory-created objects. +class MarkdownFeature + include FactoryGirl::Syntax::Methods + + def user + @user ||= create(:user) + end + + def group + unless @group + @group = create(:group) + @group.add_user(user, Gitlab::Access::DEVELOPER) + end + + @group + end + + # Direct references ---------------------------------------------------------- + + def project + @project ||= create(:project) + end + + def issue + @issue ||= create(:issue, project: project) + end + + def merge_request + @merge_request ||= create(:merge_request, :simple, source_project: project) + end + + def snippet + @snippet ||= create(:project_snippet, project: project) + end + + def commit + @commit ||= project.commit + end + + def commit_range + unless @commit_range + commit2 = project.commit('HEAD~3') + @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}", project) + end + + @commit_range + end + + def simple_label + @simple_label ||= create(:label, name: 'gfm', project: project) + end + + def label + @label ||= create(:label, name: 'awaiting feedback', project: project) + end + + # Cross-references ----------------------------------------------------------- + + def xproject + unless @xproject + namespace = create(:namespace, name: 'cross-reference') + @xproject = create(:project, namespace: namespace) + @xproject.team << [user, :developer] + end + + @xproject + end + + def xissue + @xissue ||= create(:issue, project: xproject) + end + + def xmerge_request + @xmerge_request ||= create(:merge_request, :simple, source_project: xproject) + end + + def xsnippet + @xsnippet ||= create(:project_snippet, project: xproject) + end + + def xcommit + @xcommit ||= xproject.commit + end + + def xcommit_range + unless @xcommit_range + xcommit2 = xproject.commit('HEAD~2') + @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}", xproject) + end + + @xcommit_range + end + + def raw_markdown + fixture = Rails.root.join('spec/fixtures/markdown.md.erb') + ERB.new(File.read(fixture)).result(binding) + end +end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb new file mode 100644 index 0000000000..9df226c3af --- /dev/null +++ b/spec/support/matchers/markdown_matchers.rb @@ -0,0 +1,156 @@ +# MarkdownMatchers +# +# Custom matchers for our custom HTML::Pipeline filters. These are used to test +# that specific filters are or are not used by our defined pipelines. +# +# Must be included manually. +module MarkdownMatchers + extend RSpec::Matchers::DSL + include Capybara::Node::Matchers + + # RelativeLinkFilter + matcher :parse_relative_links do + set_default_markdown_messages + + match do |actual| + link = actual.at_css('a:contains("Relative Link")') + image = actual.at_css('img[alt="Relative Image"]') + + expect(link['href']).to end_with('master/doc/README.md') + expect(image['src']).to end_with('master/app/assets/images/touch-icon-ipad.png') + end + end + + # EmojiFilter + matcher :parse_emoji do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('img.emoji', count: 10) + end + end + + # TableOfContentsFilter + matcher :create_header_links do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('h1 a#gitlab-markdown') + expect(actual).to have_selector('h2 a#markdown') + expect(actual).to have_selector('h3 a#autolinkfilter') + end + end + + # AutolinkFilter + matcher :create_autolinks do + def have_autolink(link) + have_link(link, href: link) + end + + set_default_markdown_messages + + match do |actual| + expect(actual).to have_autolink('http://about.gitlab.com/') + expect(actual).to have_autolink('https://google.com/') + expect(actual).to have_autolink('ftp://ftp.us.debian.org/debian/') + expect(actual).to have_autolink('smb://foo/bar/baz') + expect(actual).to have_autolink('irc://irc.freenode.net/git') + expect(actual).to have_autolink('http://localhost:3000') + + %w(code a kbd).each do |elem| + expect(body).not_to have_selector("#{elem} a") + end + end + end + + # UserReferenceFilter + matcher :reference_users do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-project_member', count: 3) + end + end + + # IssueReferenceFilter + matcher :reference_issues do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-issue', count: 3) + end + end + + # MergeRequestReferenceFilter + matcher :reference_merge_requests do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 3) + expect(actual).to have_selector('em a.gfm-merge_request') + end + end + + # SnippetReferenceFilter + matcher :reference_snippets do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-snippet', count: 2) + end + end + + # CommitRangeReferenceFilter + matcher :reference_commit_ranges do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 2) + end + end + + # CommitReferenceFilter + matcher :reference_commits do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-commit', count: 2) + end + end + + # LabelReferenceFilter + matcher :reference_labels do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-label', count: 3) + end + end + + # TaskListFilter + matcher :parse_task_lists do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('ul.task-list', count: 2) + expect(actual).to have_selector('li.task-list-item', count: 7) + expect(actual).to have_selector('input[checked]', count: 3) + end + end +end + +# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for +# setting the failure messages for these matchers +module RSpec::Matchers::DSL::Macros + def set_default_markdown_messages + failure_message do + # expected to parse emoji, but didn't + "expected to #{description}, but didn't" + end + + failure_message_when_negated do + # expected not to parse task lists, but did + "expected not to #{description}, but did" + end + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index dcf2a9e2ce..dab4535e2c 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -12,7 +12,8 @@ module TestEnv 'fix' => '12d65c8', 'improve/awesome' => '5937ac0', 'markdown' => '0ed8c6c', - 'master' => '5937ac0' + 'master' => '5937ac0', + "'test'" => 'e56497b', } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily