diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index dab1b220bfb..75e419b4223 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -303,12 +303,12 @@ ee_compat_check: script: - bundle exec rake db:migrate:reset -db:migrate:reset pg: +rake pg db:migrate:reset: <<: *db-migrate-reset <<: *use-pg <<: *except-docs -db:migrate:reset mysql: +rake mysql db:migrate:reset: <<: *db-migrate-reset <<: *use-mysql <<: *except-docs @@ -320,12 +320,12 @@ db:migrate:reset mysql: - bundle exec rake db:rollback STEP=120 - bundle exec rake db:migrate -db:rollback pg: +rake pg db:rollback: <<: *db-rollback <<: *use-pg <<: *except-docs -db:rollback mysql: +rake mysql db:rollback: <<: *db-rollback <<: *use-mysql <<: *except-docs @@ -347,17 +347,17 @@ db:rollback mysql: paths: - log/development.log -db:seed_fu pg: +rake pg db:seed_fu: <<: *db-seed_fu <<: *use-pg <<: *except-docs -db:seed_fu mysql: +rake mysql db:seed_fu: <<: *db-seed_fu <<: *use-mysql <<: *except-docs -gitlab:assets:compile: +rake gitlab:assets:compile: stage: test <<: *dedicated-runner <<: *except-docs @@ -377,7 +377,7 @@ gitlab:assets:compile: paths: - webpack-report/ -karma: +rake karma: cache: paths: - vendor/ruby @@ -443,11 +443,11 @@ bundler:audit: - . scripts/prepare_build.sh - bundle exec rake db:migrate -migration path pg: +migration pg paths: <<: *migration-paths <<: *use-pg -migration path mysql: +migration mysql paths: <<: *migration-paths <<: *use-mysql @@ -502,30 +502,14 @@ trigger_docs: - master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ee -# Notify slack in the end -notify:slack: - stage: post-test - <<: *dedicated-runner - variables: - SETUP_DB: "false" - USE_BUNDLE_INSTALL: "false" - script: - - ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See " - when: on_failure - only: - - master@gitlab-org/gitlab-ce - - tags@gitlab-org/gitlab-ce - - master@gitlab-org/gitlab-ee - - tags@gitlab-org/gitlab-ee - pages: before_script: [] stage: pages <<: *dedicated-runner dependencies: - coverage - - karma - - gitlab:assets:compile + - rake karma + - rake gitlab:assets:compile - lint:javascript:report script: - mv public/ .public/ diff --git a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico new file mode 100644 index 00000000000..4af3582b60d Binary files /dev/null and b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico differ diff --git a/app/assets/images/ci_favicons/dev/favicon_status_created.ico b/app/assets/images/ci_favicons/dev/favicon_status_created.ico new file mode 100644 index 00000000000..13639da2e8a Binary files /dev/null and b/app/assets/images/ci_favicons/dev/favicon_status_created.ico differ diff --git a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico new file mode 100644 index 00000000000..5f0e711b104 Binary files /dev/null and b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico differ diff --git a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico new file mode 100644 index 00000000000..8b1168a1267 Binary files /dev/null and b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico differ diff --git a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico new file mode 100644 index 00000000000..ed19b69e1c5 Binary files /dev/null and b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico differ diff --git a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico new file mode 100644 index 00000000000..5dfefd4cc5a Binary files /dev/null and b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico differ diff --git a/app/assets/images/ci_favicons/dev/favicon_status_running.ico b/app/assets/images/ci_favicons/dev/favicon_status_running.ico new file mode 100644 index 00000000000..a41539c0e3e Binary files /dev/null and b/app/assets/images/ci_favicons/dev/favicon_status_running.ico differ diff --git a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico new file mode 100644 index 00000000000..2c1ae552b93 Binary files /dev/null and b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico differ diff --git a/app/assets/images/ci_favicons/dev/favicon_status_success.ico b/app/assets/images/ci_favicons/dev/favicon_status_success.ico new file mode 100644 index 00000000000..70f0ca61eca Binary files /dev/null and b/app/assets/images/ci_favicons/dev/favicon_status_success.ico differ diff --git a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico new file mode 100644 index 00000000000..db289e03eb1 Binary files /dev/null and b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico differ diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico old mode 100755 new mode 100644 index 5a19458f2a2..23adcffff50 Binary files a/app/assets/images/ci_favicons/favicon_status_canceled.ico and b/app/assets/images/ci_favicons/favicon_status_canceled.ico differ diff --git a/app/assets/images/ci_favicons/favicon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico old mode 100755 new mode 100644 index 4dca9640cb3..f9d93b390d8 Binary files a/app/assets/images/ci_favicons/favicon_status_created.ico and b/app/assets/images/ci_favicons/favicon_status_created.ico differ diff --git a/app/assets/images/ci_favicons/favicon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico old mode 100755 new mode 100644 index c961ff9a69b..28a22ebf724 Binary files a/app/assets/images/ci_favicons/favicon_status_failed.ico and b/app/assets/images/ci_favicons/favicon_status_failed.ico differ diff --git a/app/assets/images/ci_favicons/favicon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico old mode 100755 new mode 100644 index 5fbbc99ea7c..dbbf1abf30c Binary files a/app/assets/images/ci_favicons/favicon_status_manual.ico and b/app/assets/images/ci_favicons/favicon_status_manual.ico differ diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico old mode 100755 new mode 100644 index 21afa9c72e6..49b9b232dd1 Binary files a/app/assets/images/ci_favicons/favicon_status_not_found.ico and b/app/assets/images/ci_favicons/favicon_status_not_found.ico differ diff --git a/app/assets/images/ci_favicons/favicon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico old mode 100755 new mode 100644 index 8be32dab85a..05962f3f148 Binary files a/app/assets/images/ci_favicons/favicon_status_pending.ico and b/app/assets/images/ci_favicons/favicon_status_pending.ico differ diff --git a/app/assets/images/ci_favicons/favicon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico old mode 100755 new mode 100644 index f328ff1a5ed..7fa3d4d48d4 Binary files a/app/assets/images/ci_favicons/favicon_status_running.ico and b/app/assets/images/ci_favicons/favicon_status_running.ico differ diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico old mode 100755 new mode 100644 index b4394e1b4af..b0c26b62068 Binary files a/app/assets/images/ci_favicons/favicon_status_skipped.ico and b/app/assets/images/ci_favicons/favicon_status_skipped.ico differ diff --git a/app/assets/images/ci_favicons/favicon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico old mode 100755 new mode 100644 index 4f436c95242..b150960b5be Binary files a/app/assets/images/ci_favicons/favicon_status_success.ico and b/app/assets/images/ci_favicons/favicon_status_success.ico differ diff --git a/app/assets/images/ci_favicons/favicon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico old mode 100755 new mode 100644 index 805cc20cdec..7e71d71684d Binary files a/app/assets/images/ci_favicons/favicon_status_warning.ico and b/app/assets/images/ci_favicons/favicon_status_warning.ico differ diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index cbfc4581411..a119934febc 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -1,4 +1,6 @@ class Admin::HooksController < Admin::ApplicationController + before_action :hook, only: :edit + def index @hooks = SystemHook.all @hook = SystemHook.new @@ -15,15 +17,25 @@ class Admin::HooksController < Admin::ApplicationController end end + def edit + end + + def update + if hook.update_attributes(hook_params) + flash[:notice] = 'System hook was successfully updated.' + redirect_to admin_hooks_path + else + render 'edit' + end + end + def destroy - @hook = SystemHook.find(params[:id]) - @hook.destroy + hook.destroy redirect_to admin_hooks_path end def test - @hook = SystemHook.find(params[:hook_id]) data = { event_name: "project_create", name: "Ruby", @@ -32,11 +44,17 @@ class Admin::HooksController < Admin::ApplicationController owner_name: "Someone", owner_email: "example@gitlabhq.com" } - @hook.execute(data, 'system_hooks') + hook.execute(data, 'system_hooks') redirect_back_or_default end + private + + def hook + @hook ||= SystemHook.find(params[:id]) + end + def hook_params params.require(:hook).permit( :enable_ssl_verification, diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb new file mode 100644 index 00000000000..c32038d07bf --- /dev/null +++ b/app/controllers/concerns/notes_actions.rb @@ -0,0 +1,136 @@ +module NotesActions + include RendersNotes + extend ActiveSupport::Concern + + included do + before_action :authorize_admin_note!, only: [:update, :destroy] + end + + def index + current_fetched_at = Time.now.to_i + + notes_json = { notes: [], last_fetched_at: current_fetched_at } + + @notes = notes_finder.execute.inc_relations_for_view + @notes = prepare_notes_for_rendering(@notes) + + @notes.each do |note| + next if note.cross_reference_not_visible_for?(current_user) + + notes_json[:notes] << note_json(note) + end + + render json: notes_json + end + + def create + create_params = note_params.merge( + merge_request_diff_head_sha: params[:merge_request_diff_head_sha], + in_reply_to_discussion_id: params[:in_reply_to_discussion_id] + ) + @note = Notes::CreateService.new(project, current_user, create_params).execute + + if @note.is_a?(Note) + Banzai::NoteRenderer.render([@note], @project, current_user) + end + + respond_to do |format| + format.json { render json: note_json(@note) } + format.html { redirect_back_or_default } + end + end + + def update + @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) + + if @note.is_a?(Note) + Banzai::NoteRenderer.render([@note], @project, current_user) + end + + respond_to do |format| + format.json { render json: note_json(@note) } + format.html { redirect_back_or_default } + end + end + + def destroy + if note.editable? + Notes::DestroyService.new(project, current_user).execute(note) + end + + respond_to do |format| + format.js { head :ok } + end + end + + private + + def note_json(note) + attrs = { + commands_changes: note.commands_changes + } + + if note.persisted? + attrs.merge!( + valid: true, + id: note.id, + discussion_id: note.discussion_id(noteable), + html: note_html(note), + note: note.note + ) + + discussion = note.to_discussion(noteable) + unless discussion.individual_note? + attrs.merge!( + discussion_resolvable: discussion.resolvable?, + + diff_discussion_html: diff_discussion_html(discussion), + discussion_html: discussion_html(discussion) + ) + end + else + attrs.merge!( + valid: false, + errors: note.errors + ) + end + + attrs + end + + def authorize_admin_note! + return access_denied! unless can?(current_user, :admin_note, note) + end + + def note_params + params.require(:note).permit( + :project_id, + :noteable_type, + :noteable_id, + :commit_id, + :noteable, + :type, + + :note, + :attachment, + + # LegacyDiffNote + :line_code, + + # DiffNote + :position + ) + end + + def noteable + @noteable ||= notes_finder.target + end + + def last_fetched_at + request.headers['X-Last-Fetched-At'] + end + + def notes_finder + @notes_finder ||= NotesFinder.new(project, current_user, finder_params) + end +end diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb index dd21066ac13..41c3114ad1e 100644 --- a/app/controllers/concerns/renders_notes.rb +++ b/app/controllers/concerns/renders_notes.rb @@ -10,6 +10,8 @@ module RendersNotes private def preload_max_access_for_authors(notes, project) + return nil unless project + user_ids = notes.map(&:author_id) project.team.max_member_access_for_user_ids(user_ids) end diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb index ca6dffe1cc5..ffea712a833 100644 --- a/app/controllers/concerns/snippets_actions.rb +++ b/app/controllers/concerns/snippets_actions.rb @@ -5,10 +5,12 @@ module SnippetsActions end def raw + disposition = params[:inline] == 'false' ? 'attachment' : 'inline' + send_data( convert_line_endings(@snippet.content), type: 'text/plain; charset=utf-8', - disposition: 'inline', + disposition: disposition, filename: @snippet.sanitized_file_name ) end diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index fbf9a026b10..ba5b7d33f87 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -22,7 +22,8 @@ module ToggleAwardEmoji def to_todoable(awardable) case awardable when Note - awardable.noteable + # we don't create todos for personal snippet comments for now + awardable.for_personal_snippet? ? nil : awardable.noteable when MergeRequest, Issue awardable when Snippet diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 1e41f980f31..86d13a0d222 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -1,6 +1,7 @@ class Projects::HooksController < Projects::ApplicationController # Authorize before_action :authorize_admin_project! + before_action :hook, only: :edit respond_to :html @@ -17,6 +18,18 @@ class Projects::HooksController < Projects::ApplicationController redirect_to namespace_project_settings_integrations_path(@project.namespace, @project) end + def edit + end + + def update + if hook.update_attributes(hook_params) + flash[:notice] = 'Hook was successfully updated.' + redirect_to namespace_project_settings_integrations_path(@project.namespace, @project) + else + render 'edit' + end + end + def test if !@project.empty_repo? status, message = TestHookService.new.execute(hook, current_user) diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 405ea3c0a4f..37f51b2ebe3 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -1,68 +1,22 @@ class Projects::NotesController < Projects::ApplicationController - include RendersNotes + include NotesActions include ToggleAwardEmoji - # Authorize before_action :authorize_read_note! before_action :authorize_create_note!, only: [:create] - before_action :authorize_admin_note!, only: [:update, :destroy] before_action :authorize_resolve_note!, only: [:resolve, :unresolve] - def index - current_fetched_at = Time.now.to_i - - notes_json = { notes: [], last_fetched_at: current_fetched_at } - - @notes = notes_finder.execute.inc_relations_for_view - @notes = prepare_notes_for_rendering(@notes) - - @notes.each do |note| - next if note.cross_reference_not_visible_for?(current_user) - - notes_json[:notes] << note_json(note) - end - - render json: notes_json - end - + # + # This is a fix to make spinach feature tests passing: + # Controller actions are returned from AbstractController::Base and methods of parent classes are + # excluded in order to return only specific controller related methods. + # That is ok for the app (no :create method in ancestors) + # but fails for tests because there is a :create method on FactoryGirl (one of the ancestors) + # + # see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78 + # def create - create_params = note_params.merge( - merge_request_diff_head_sha: params[:merge_request_diff_head_sha], - in_reply_to_discussion_id: params[:in_reply_to_discussion_id] - ) - @note = Notes::CreateService.new(project, current_user, create_params).execute - - if @note.is_a?(Note) - Banzai::NoteRenderer.render([@note], @project, current_user) - end - - respond_to do |format| - format.json { render json: note_json(@note) } - format.html { redirect_back_or_default } - end - end - - def update - @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) - - if @note.is_a?(Note) - Banzai::NoteRenderer.render([@note], @project, current_user) - end - - respond_to do |format| - format.json { render json: note_json(@note) } - format.html { redirect_back_or_default } - end - end - - def destroy - if note.editable? - Notes::DestroyService.new(project, current_user).execute(note) - end - - respond_to do |format| - format.js { head :ok } - end + super end def delete_attachment @@ -110,7 +64,7 @@ class Projects::NotesController < Projects::ApplicationController def note_html(note) render_to_string( - "projects/notes/_note", + "shared/notes/_note", layout: false, formats: [:html], locals: { note: note } @@ -152,76 +106,11 @@ class Projects::NotesController < Projects::ApplicationController ) end - def note_json(note) - attrs = { - commands_changes: note.commands_changes - } - - if note.persisted? - attrs.merge!( - valid: true, - id: note.id, - discussion_id: note.discussion_id(noteable), - html: note_html(note), - note: note.note - ) - - discussion = note.to_discussion(noteable) - unless discussion.individual_note? - attrs.merge!( - discussion_resolvable: discussion.resolvable?, - - diff_discussion_html: diff_discussion_html(discussion), - discussion_html: discussion_html(discussion) - ) - end - else - attrs.merge!( - valid: false, - errors: note.errors - ) - end - - attrs - end - - def authorize_admin_note! - return access_denied! unless can?(current_user, :admin_note, note) + def finder_params + params.merge(last_fetched_at: last_fetched_at) end def authorize_resolve_note! return access_denied! unless can?(current_user, :resolve_note, note) end - - def note_params - params.require(:note).permit( - :project_id, - :noteable_type, - :noteable_id, - :commit_id, - :noteable, - :type, - - :note, - :attachment, - - # LegacyDiffNote - :line_code, - - # DiffNote - :position - ) - end - - def notes_finder - @notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at)) - end - - def noteable - @noteable ||= notes_finder.target - end - - def last_fetched_at - request.headers['X-Last-Fetched-At'] - end end diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb new file mode 100644 index 00000000000..3c4ddc1680d --- /dev/null +++ b/app/controllers/snippets/notes_controller.rb @@ -0,0 +1,44 @@ +class Snippets::NotesController < ApplicationController + include NotesActions + include ToggleAwardEmoji + + skip_before_action :authenticate_user!, only: [:index] + before_action :snippet + before_action :authorize_read_snippet!, only: [:show, :index, :create] + + private + + def note + @note ||= snippet.notes.find(params[:id]) + end + alias_method :awardable, :note + + def note_html(note) + render_to_string( + "shared/notes/_note", + layout: false, + formats: [:html], + locals: { note: note } + ) + end + + def project + nil + end + + def snippet + PersonalSnippet.find_by(id: params[:snippet_id]) + end + + def note_params + super.merge(noteable_id: params[:snippet_id]) + end + + def finder_params + params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet') + end + + def authorize_read_snippet! + return render_404 unless can?(current_user, :read_personal_snippet, snippet) + end +end diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 906833505d1..da1ae9a34d9 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,14 +1,15 @@ class SnippetsController < ApplicationController + include RendersNotes include ToggleAwardEmoji include SpammableActions include SnippetsActions include MarkdownPreview include RendersBlob - before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] + before_action :snippet, only: [:show, :edit, :destroy, :update, :raw] # Allow read snippet - before_action :authorize_read_snippet!, only: [:show, :raw, :download] + before_action :authorize_read_snippet!, only: [:show, :raw] # Allow modify snippet before_action :authorize_update_snippet!, only: [:edit, :update] @@ -16,7 +17,7 @@ class SnippetsController < ApplicationController # Allow destroy snippet before_action :authorize_admin_snippet!, only: [:destroy] - skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download] + skip_before_action :authenticate_user!, only: [:index, :show, :raw] layout 'snippets' respond_to :html @@ -64,6 +65,11 @@ class SnippetsController < ApplicationController blob = @snippet.blob override_max_blob_size(blob) + @noteable = @snippet + + @discussions = @snippet.discussions + @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes)) + respond_to do |format| format.html do render 'show' @@ -83,14 +89,6 @@ class SnippetsController < ApplicationController redirect_to snippets_path end - def download - send_data( - convert_line_endings(@snippet.content), - type: 'text/plain; charset=utf-8', - filename: @snippet.sanitized_file_name - ) - end - def preview_markdown render_markdown_preview(params[:text], skip_project_check: true) end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 3c499184b41..dc6a8ad1f66 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -68,6 +68,8 @@ class NotesFinder MergeRequestsFinder.new(@current_user, project_id: @project.id).execute when "snippet", "project_snippet" SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project) + when "personal_snippet" + PersonalSnippet.all else raise 'invalid target_type' end diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb index 167b09e678f..024cf38469e 100644 --- a/app/helpers/award_emoji_helper.rb +++ b/app/helpers/award_emoji_helper.rb @@ -1,10 +1,14 @@ module AwardEmojiHelper def toggle_award_url(awardable) - return url_for([:toggle_award_emoji, awardable]) unless @project + return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note) if awardable.is_a?(Note) # We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x) - toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id) + if awardable.for_personal_snippet? + toggle_award_emoji_snippet_note_path(awardable.noteable, awardable) + else + toggle_award_emoji_namespace_project_note_path(@project.namespace, @project, awardable.id) + end else url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index e347f61fb8d..2614cdfe90e 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -1,6 +1,6 @@ module MergeRequestsHelper def new_mr_path_from_push_event(event) - target_project = event.project.forked_from_project || event.project + target_project = event.project.default_merge_request_target new_namespace_project_merge_request_path( event.project.namespace, event.project, @@ -127,6 +127,10 @@ module MergeRequestsHelper end end + def target_projects(project) + [project, project.default_merge_request_target].uniq + end + def merge_request_button_visibility(merge_request, closed) return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? end diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index 979264c9421..2fd64b3441e 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -8,6 +8,14 @@ module SnippetsHelper end end + def download_snippet_path(snippet) + if snippet.project_id + raw_namespace_project_snippet_path(@project.namespace, @project, snippet, inline: false) + else + raw_snippet_path(snippet, inline: false) + end + end + # Return the path of a snippets index for a user or for a project # # @returns String, path to snippet index diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 9d2288c311e..365fa4f1e70 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -100,6 +100,7 @@ class MergeRequest < ActiveRecord::Base validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing? validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_fork, unless: :closed_without_fork? + validate :validate_target_project, on: :create scope :by_source_or_target_branch, ->(branch_name) do where("source_branch = :branch OR target_branch = :branch", branch: branch_name) @@ -330,6 +331,12 @@ class MergeRequest < ActiveRecord::Base end end + def validate_target_project + return true if target_project.merge_requests_enabled? + + errors.add :base, 'Target project has disabled merge requests' + end + def validate_fork return true unless target_project && source_project return true if target_project == source_project diff --git a/app/models/project.rb b/app/models/project.rb index c7dc562c238..9d64e5d406d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1314,6 +1314,14 @@ class Project < ActiveRecord::Base namespace_id_changed? end + def default_merge_request_target + if forked_from_project&.merge_requests_enabled? + forked_from_project + else + self + end + end + alias_method :name_with_namespace, :full_name alias_method :human_name, :full_name alias_method :path_with_namespace, :full_path diff --git a/app/models/repository.rb b/app/models/repository.rb index feabfa111fb..ba34d570dbd 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -505,14 +505,8 @@ class Repository delegate :tag_names, to: :raw_repository cache_method :tag_names, fallback: [] - def branch_count - branches.size - end + delegate :branch_count, :tag_count, to: :raw_repository cache_method :branch_count, fallback: 0 - - def tag_count - raw_repository.rugged.tags.count - end cache_method :tag_count, fallback: 0 def avatar diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb index 944472f3e51..188c3747f18 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/status_entity.rb @@ -7,6 +7,9 @@ class StatusEntity < Grape::Entity expose :details_path expose :favicon do |status| - ActionController::Base.helpers.image_path(File.join('ci_favicons', "#{status.favicon}.ico")) + dir = 'ci_favicons' + dir = File.join(dir, 'dev') if Rails.env.development? + + ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico")) end end diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index d45da5180e1..bc0e7ad4e39 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -28,7 +28,7 @@ module MergeRequests def find_target_project return target_project if target_project.present? && can?(current_user, :read_project, target_project) - project.forked_from_project || project + project.default_merge_request_target end def find_target_branch diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index b6e88b0280f..8ae61694b50 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -281,7 +281,7 @@ class TodoService def attributes_for_target(target) attributes = { - project_id: target.project.id, + project_id: target&.project&.id, target_id: target.id, target_type: target.class.name, commit_id: nil diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 8c9fdc9ae42..53f0a1e7fde 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -73,6 +73,12 @@ = container_reg %span.light.pull-right = boolean_to_icon Gitlab.config.registry.enabled + - gitlab_pages = 'GitLab Pages' + - gitlab_pages_enabled = Gitlab.config.pages.enabled + %p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") } + = gitlab_pages + %span.light.pull-right + = boolean_to_icon gitlab_pages_enabled .col-md-4 %h4 diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml new file mode 100644 index 00000000000..6217d5fb135 --- /dev/null +++ b/app/views/admin/hooks/_form.html.haml @@ -0,0 +1,40 @@ += form_errors(hook) + +.form-group + = form.label :url, 'URL', class: 'control-label' + .col-sm-10 + = form.text_field :url, class: 'form-control' +.form-group + = form.label :token, 'Secret Token', class: 'control-label' + .col-sm-10 + = form.text_field :token, class: 'form-control' + %p.help-block + Use this token to validate received payloads +.form-group + = form.label :url, 'Trigger', class: 'control-label' + .col-sm-10.prepend-top-10 + %div + System hook will be triggered on set of events like creating project + or adding ssh key. But you can also enable extra triggers like Push events. + + .prepend-top-default + = form.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = form.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This url will be triggered by a push to the repository + %div + = form.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = form.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This url will be triggered when a new tag is pushed to the repository +.form-group + = form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox' + .col-sm-10 + .checkbox + = form.label :enable_ssl_verification do + = form.check_box :enable_ssl_verification + %strong Enable SSL verification diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml new file mode 100644 index 00000000000..0777f5e2629 --- /dev/null +++ b/app/views/admin/hooks/edit.html.haml @@ -0,0 +1,14 @@ +- page_title 'Edit System Hook' +%h3.page-title + Edit System Hook + +%p.light + #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be + used for binding events when GitLab creates a User or Project. + +%hr + += form_for @hook, as: :hook, url: admin_hook_path, html: { class: 'form-horizontal' } do |f| + = render partial: 'form', locals: { form: f, hook: @hook } + .form-actions + = f.submit 'Save changes', class: 'btn btn-create' diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index d9c7948763a..71117758921 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -1,57 +1,17 @@ -- page_title "System Hooks" +- page_title 'System Hooks' %h3.page-title System hooks %p.light - #{link_to "System hooks ", help_page_path("system_hooks/system_hooks"), class: "vlink"} can be + #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be used for binding events when GitLab creates a User or Project. %hr - = form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f| - = form_errors(@hook) - - .form-group - = f.label :url, 'URL', class: 'control-label' - .col-sm-10 - = f.text_field :url, class: 'form-control' - .form-group - = f.label :token, 'Secret Token', class: 'control-label' - .col-sm-10 - = f.text_field :token, class: 'form-control' - %p.help-block - Use this token to validate received payloads - .form-group - = f.label :url, "Trigger", class: 'control-label' - .col-sm-10.prepend-top-10 - %div - System hook will be triggered on set of events like creating project - or adding ssh key. But you can also enable extra triggers like Push events. - - .prepend-top-default - = f.check_box :push_events, class: 'pull-left' - .prepend-left-20 - = f.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This url will be triggered by a push to the repository - %div - = f.check_box :tag_push_events, class: 'pull-left' - .prepend-left-20 - = f.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This url will be triggered when a new tag is pushed to the repository - .form-group - = f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox' - .col-sm-10 - .checkbox - = f.label :enable_ssl_verification do - = f.check_box :enable_ssl_verification - %strong Enable SSL verification + = render partial: 'form', locals: { form: f, hook: @hook } .form-actions - = f.submit "Add system hook", class: "btn btn-create" + = f.submit 'Add system hook', class: 'btn btn-create' %hr - if @hooks.any? @@ -62,11 +22,12 @@ - @hooks.each do |hook| %li .controls - = link_to 'Test hook', admin_hook_test_path(hook), class: "btn btn-sm" - = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" + = link_to 'Test hook', test_admin_hook_path(hook), class: 'btn btn-sm' + = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm' + = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm' .monospace= hook.url %div - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| - if hook.send(trigger) %span.label.label-gray= trigger.titleize - %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} + %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'} diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml index 34789808f10..964473ee3e0 100644 --- a/app/views/discussions/_notes.html.haml +++ b/app/views/discussions/_notes.html.haml @@ -1,6 +1,6 @@ .discussion-notes %ul.notes{ data: { discussion_id: discussion.id } } - = render partial: "projects/notes/note", collection: discussion.notes, as: :note + = render partial: "shared/notes/note", collection: discussion.notes, as: :note - if current_user .discussion-reply-holder diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 85e442e115c..50e0bad3ccf 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -60,7 +60,7 @@ git init git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git add . - git commit + git commit -m "Initial commit" git push -u origin master %fieldset diff --git a/app/views/projects/hooks/_index.html.haml b/app/views/projects/hooks/_index.html.haml index 8faad351463..676b7c345bc 100644 --- a/app/views/projects/hooks/_index.html.haml +++ b/app/views/projects/hooks/_index.html.haml @@ -1 +1,23 @@ -= render 'shared/web_hooks/form', hook: @hook, hooks: @hooks, url_components: [@project.namespace.becomes(Namespace), @project] +.row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + = page_title + %p + #{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be + used for binding events when something is happening within the project. + + .col-lg-9.append-bottom-default + = form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f| + = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook } + = f.submit 'Add webhook', class: 'btn btn-create' + + %hr + %h5.prepend-top-default + Webhooks (#{@hooks.count}) + - if @hooks.any? + %ul.well-list + - @hooks.each do |hook| + = render 'project_hook', hook: hook + - else + %p.settings-message.text-center.append-bottom-0 + No webhooks found, add one in the form above. diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml new file mode 100644 index 00000000000..7998713be1f --- /dev/null +++ b/app/views/projects/hooks/edit.html.haml @@ -0,0 +1,14 @@ += render 'projects/settings/head' + +.row.prepend-top-default + .col-lg-3 + %h4.prepend-top-0 + = page_title + %p + #{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be + used for binding events when something is happening within the project. + .col-lg-9.append-bottom-default + = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hook_path do |f| + = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook } + = f.submit 'Save changes', class: 'btn btn-create' + diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml index 8d134aaac67..9cf24e10842 100644 --- a/app/views/projects/merge_requests/_new_compare.html.haml +++ b/app/views/projects/merge_requests/_new_compare.html.haml @@ -38,7 +38,7 @@ .panel-heading Target branch .panel-body.clearfix - - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project] + - projects = target_projects(@project) .merge-request-select.dropdown = f.hidden_field :target_project_id = dropdown_toggle f.object.target_project.path_with_namespace, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" } diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml new file mode 100644 index 00000000000..718b52dd82e --- /dev/null +++ b/app/views/projects/notes/_actions.html.haml @@ -0,0 +1,44 @@ +- access = note_max_access_for_user(note) +- if access + %span.note-role= access + +- if note.resolvable? + - can_resolve = can?(current_user, :resolve_note, note) + %resolve-btn{ "project-path" => project_path(note.project), + "discussion-id" => note.discussion_id(@noteable), + ":note-id" => note.id, + ":resolved" => note.resolved?, + ":can-resolve" => can_resolve, + ":author-name" => "'#{j(note.author.name)}'", + "author-avatar" => note.author.avatar_url, + ":note-truncated" => "'#{j(truncate(note.note, length: 17))}'", + ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'", + "v-show" => "#{can_resolve || note.resolved?}", + "inline-template" => true, + "ref" => "note_#{note.id}" } + + %button.note-action-button.line-resolve-btn{ type: "button", + class: ("is-disabled" unless can_resolve), + ":class" => "{ 'is-active': isResolved }", + ":aria-label" => "buttonText", + "@click" => "resolve", + ":title" => "buttonText", + ":ref" => "'button'" } + + = icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading') + %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg' + +- if current_user + - if note.emoji_awardable? + - user_authored = note.user_authored?(current_user) + = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do + = icon('spinner spin') + %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') + %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') + %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + + - if note_editable + = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do + = icon('pencil', class: 'link-highlight') + = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do + = icon('trash-o', class: 'danger-highlight') diff --git a/app/views/projects/notes/_edit.html.haml b/app/views/projects/notes/_edit.html.haml new file mode 100644 index 00000000000..f1e251d65b7 --- /dev/null +++ b/app/views/projects/notes/_edit.html.haml @@ -0,0 +1,3 @@ +.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } + #{note.note} +%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml deleted file mode 100644 index 7afccb3900a..00000000000 --- a/app/views/projects/notes/_note.html.haml +++ /dev/null @@ -1,101 +0,0 @@ -- return unless note.author -- return if note.cross_reference_not_visible_for?(current_user) - -- note_editable = note_editable?(note) -%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} } - .timeline-entry-inner - .timeline-icon - - if note.system - = icon_for_system_note(note) - - else - %a{ href: user_path(note.author) } - = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' - .timeline-content - .note-header - .note-header-info - %a{ href: user_path(note.author) } - %span.hidden-xs - = sanitize(note.author.name) - %span.note-headline-light - = note.author.to_reference - %span.note-headline-light - %span.note-headline-meta - - unless note.system - commented - - if note.system - %span.system-note-message - = note.redacted_note_html - %a{ href: "##{dom_id(note)}" } - = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') - - unless note.system? - .note-actions - - access = note_max_access_for_user(note) - - if access - %span.note-role= access - - - if note.resolvable? - - can_resolve = can?(current_user, :resolve_note, note) - %resolve-btn{ "project-path" => project_path(note.project), - "discussion-id" => note.discussion_id(@noteable), - ":note-id" => note.id, - ":resolved" => note.resolved?, - ":can-resolve" => can_resolve, - ":author-name" => "'#{j(note.author.name)}'", - "author-avatar" => note.author.avatar_url, - ":note-truncated" => "'#{j(truncate(note.note, length: 17))}'", - ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'", - "v-show" => "#{can_resolve || note.resolved?}", - "inline-template" => true, - "ref" => "note_#{note.id}" } - - %button.note-action-button.line-resolve-btn{ type: "button", - class: ("is-disabled" unless can_resolve), - ":class" => "{ 'is-active': isResolved }", - ":aria-label" => "buttonText", - "@click" => "resolve", - ":title" => "buttonText", - ":ref" => "'button'" } - - = icon("spin spinner", "v-show" => "loading", class: 'loading') - %div{ 'v-show' => '!loading' }= render "shared/icons/icon_status_success.svg" - - - if current_user - - if note.emoji_awardable? - - user_authored = note.user_authored?(current_user) - = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do - = icon('spinner spin') - %span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face') - %span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley') - %span{ class: "link-highlight award-control-icon-super-positive" }= custom_icon('emoji_smile') - - - if note_editable - = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do - = icon('pencil', class: 'link-highlight') - = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do - = icon('trash-o', class: 'danger-highlight') - .note-body{ class: note_editable ? 'js-task-list-container' : '' } - .note-text.md - = note.redacted_note_html - = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) - - if note_editable - .original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } - #{note.note} - %textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note - .note-awards - = render 'award_emoji/awards_block', awardable: note, inline: false - - if note.system - .system-note-commit-list-toggler - Toggle commit list - %i.fa.fa-angle-down - - if note.attachment.url - .note-attachment - - if note.attachment.image? - = link_to note.attachment.url, target: '_blank' do - = image_tag note.attachment.url, class: 'note-image-attach' - .attachment - = link_to note.attachment.url, target: '_blank' do - = icon('paperclip') - = note.attachment_identifier - = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note), - title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do - = icon('trash-o', class: 'cred') diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml index 90a150aa74c..555228623cc 100644 --- a/app/views/projects/notes/_notes_with_form.html.haml +++ b/app/views/projects/notes/_notes_with_form.html.haml @@ -1,5 +1,5 @@ %ul#notes-list.notes.main-notes-list.timeline - = render "projects/notes/notes" + = render "shared/notes/notes" = render 'projects/notes/edit_form' diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml index e50a543ffa8..5a5ade03624 100644 --- a/app/views/projects/settings/_head.html.haml +++ b/app/views/projects/settings/_head.html.haml @@ -14,7 +14,7 @@ %span Members - if can_edit - = nav_link(controller: [:integrations, :services]) do + = nav_link(controller: [:integrations, :services, :hooks]) do = link_to project_settings_integrations_path(@project), title: 'Integrations' do %span Integrations diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml index ceabe2eab3d..8dc276a3bec 100644 --- a/app/views/projects/settings/integrations/_project_hook.html.haml +++ b/app/views/projects/settings/integrations/_project_hook.html.haml @@ -9,6 +9,7 @@ .col-md-4.col-lg-5.text-right-lg.prepend-top-5 %span.append-right-10.inline SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} + = link_to "Edit", edit_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm" = link_to "Test", test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm" = link_to namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent" do %span.sr-only Remove diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml new file mode 100644 index 00000000000..731270d4127 --- /dev/null +++ b/app/views/shared/notes/_note.html.haml @@ -0,0 +1,62 @@ +- return unless note.author +- return if note.cross_reference_not_visible_for?(current_user) + +- note_editable = note_editable?(note) +%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} } + .timeline-entry-inner + .timeline-icon + - if note.system + = icon_for_system_note(note) + - else + %a{ href: user_path(note.author) } + = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40' + .timeline-content + .note-header + .note-header-info + %a{ href: user_path(note.author) } + %span.hidden-xs + = sanitize(note.author.name) + %span.note-headline-light + = note.author.to_reference + %span.note-headline-light + %span.note-headline-meta + - unless note.system + commented + - if note.system + %span.system-note-message + = note.redacted_note_html + %a{ href: "##{dom_id(note)}" } + = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') + - unless note.system? + .note-actions + - if note.for_personal_snippet? + = render 'snippets/notes/actions', note: note, note_editable: note_editable + - else + = render 'projects/notes/actions', note: note, note_editable: note_editable + .note-body{ class: note_editable ? 'js-task-list-container' : '' } + .note-text.md + = note.redacted_note_html + = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) + - if note_editable + - if note.for_personal_snippet? + = render 'snippets/notes/edit', note: note + - else + = render 'projects/notes/edit', note: note + .note-awards + = render 'award_emoji/awards_block', awardable: note, inline: false + - if note.system + .system-note-commit-list-toggler + Toggle commit list + %i.fa.fa-angle-down + - if note.attachment.url + .note-attachment + - if note.attachment.image? + = link_to note.attachment.url, target: '_blank' do + = image_tag note.attachment.url, class: 'note-image-attach' + .attachment + = link_to note.attachment.url, target: '_blank' do + = icon('paperclip') + = note.attachment_identifier + = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note), + title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do + = icon('trash-o', class: 'cred') diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/shared/notes/_notes.html.haml similarity index 53% rename from app/views/projects/notes/_notes.html.haml rename to app/views/shared/notes/_notes.html.haml index 2b2bab09c74..cfdfeeb9e97 100644 --- a/app/views/projects/notes/_notes.html.haml +++ b/app/views/shared/notes/_notes.html.haml @@ -1,8 +1,8 @@ - if defined?(@discussions) - @discussions.each do |discussion| - if discussion.individual_note? - = render partial: "projects/notes/note", collection: discussion.notes, as: :note + = render partial: "shared/notes/note", collection: discussion.notes, as: :note - else = render 'discussions/discussion', discussion: discussion - else - = render partial: "projects/notes/note", collection: @notes, as: :note + = render partial: "shared/notes/note", collection: @notes, as: :note diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml index 67d186e2874..9bcb4544b97 100644 --- a/app/views/shared/snippets/_blob.html.haml +++ b/app/views/shared/snippets/_blob.html.haml @@ -18,7 +18,6 @@ = copy_blob_source_button(blob) = open_raw_blob_button(blob) - - if defined?(download_path) && download_path - = link_to icon('download'), download_path, class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' } + = link_to icon('download'), download_snippet_path(@snippet), target: '_blank', class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' } = render 'projects/blob/content', blob: blob diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index ee3be3c789a..37c3e61912c 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -1,102 +1,82 @@ -.row.prepend-top-default - .col-lg-3 - %h4.prepend-top-0 - = page_title - %p - #{link_to "Webhooks", help_page_path("user/project/integrations/webhooks")} can be - used for binding events when something is happening within the project. - .col-lg-9.append-bottom-default - = form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f| - = form_errors(hook) += form_errors(hook) - .form-group - = f.label :url, "URL", class: 'label-light' - = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' - .form-group - = f.label :token, "Secret Token", class: 'label-light' - = f.text_field :token, class: "form-control", placeholder: '' - %p.help-block - Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. - .form-group - = f.label :url, "Trigger", class: 'label-light' - %ul.list-unstyled - %li - = f.check_box :push_events, class: 'pull-left' - .prepend-left-20 - = f.label :push_events, class: 'list-label' do - %strong Push events - %p.light - This URL will be triggered by a push to the repository - %li - = f.check_box :tag_push_events, class: 'pull-left' - .prepend-left-20 - = f.label :tag_push_events, class: 'list-label' do - %strong Tag push events - %p.light - This URL will be triggered when a new tag is pushed to the repository - %li - = f.check_box :note_events, class: 'pull-left' - .prepend-left-20 - = f.label :note_events, class: 'list-label' do - %strong Comments - %p.light - This URL will be triggered when someone adds a comment - %li - = f.check_box :issues_events, class: 'pull-left' - .prepend-left-20 - = f.label :issues_events, class: 'list-label' do - %strong Issues events - %p.light - This URL will be triggered when an issue is created/updated/merged - %li - = f.check_box :confidential_issues_events, class: 'pull-left' - .prepend-left-20 - = f.label :confidential_issues_events, class: 'list-label' do - %strong Confidential Issues events - %p.light - This URL will be triggered when a confidential issue is created/updated/merged - %li - = f.check_box :merge_requests_events, class: 'pull-left' - .prepend-left-20 - = f.label :merge_requests_events, class: 'list-label' do - %strong Merge Request events - %p.light - This URL will be triggered when a merge request is created/updated/merged - %li - = f.check_box :build_events, class: 'pull-left' - .prepend-left-20 - = f.label :build_events, class: 'list-label' do - %strong Jobs events - %p.light - This URL will be triggered when the job status changes - %li - = f.check_box :pipeline_events, class: 'pull-left' - .prepend-left-20 - = f.label :pipeline_events, class: 'list-label' do - %strong Pipeline events - %p.light - This URL will be triggered when the pipeline status changes - %li - = f.check_box :wiki_page_events, class: 'pull-left' - .prepend-left-20 - = f.label :wiki_page_events, class: 'list-label' do - %strong Wiki Page events - %p.light - This URL will be triggered when a wiki page is created/updated - .form-group - = f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox' - .checkbox - = f.label :enable_ssl_verification do - = f.check_box :enable_ssl_verification - %strong Enable SSL verification - = f.submit "Add webhook", class: "btn btn-create" - %hr - %h5.prepend-top-default - Webhooks (#{hooks.count}) - - if hooks.any? - %ul.well-list - - hooks.each do |hook| - = render "project_hook", hook: hook - - else - %p.settings-message.text-center.append-bottom-0 - No webhooks found, add one in the form above. +.form-group + = form.label :url, 'URL', class: 'label-light' + = form.text_field :url, class: 'form-control', placeholder: 'http://example.com/trigger-ci.json' +.form-group + = form.label :token, 'Secret Token', class: 'label-light' + = form.text_field :token, class: 'form-control', placeholder: '' + %p.help-block + Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. +.form-group + = form.label :url, 'Trigger', class: 'label-light' + %ul.list-unstyled + %li + = form.check_box :push_events, class: 'pull-left' + .prepend-left-20 + = form.label :push_events, class: 'list-label' do + %strong Push events + %p.light + This URL will be triggered by a push to the repository + %li + = form.check_box :tag_push_events, class: 'pull-left' + .prepend-left-20 + = form.label :tag_push_events, class: 'list-label' do + %strong Tag push events + %p.light + This URL will be triggered when a new tag is pushed to the repository + %li + = form.check_box :note_events, class: 'pull-left' + .prepend-left-20 + = form.label :note_events, class: 'list-label' do + %strong Comments + %p.light + This URL will be triggered when someone adds a comment + %li + = form.check_box :issues_events, class: 'pull-left' + .prepend-left-20 + = form.label :issues_events, class: 'list-label' do + %strong Issues events + %p.light + This URL will be triggered when an issue is created/updated/merged + %li + = form.check_box :confidential_issues_events, class: 'pull-left' + .prepend-left-20 + = form.label :confidential_issues_events, class: 'list-label' do + %strong Confidential Issues events + %p.light + This URL will be triggered when a confidential issue is created/updated/merged + %li + = form.check_box :merge_requests_events, class: 'pull-left' + .prepend-left-20 + = form.label :merge_requests_events, class: 'list-label' do + %strong Merge Request events + %p.light + This URL will be triggered when a merge request is created/updated/merged + %li + = form.check_box :build_events, class: 'pull-left' + .prepend-left-20 + = form.label :build_events, class: 'list-label' do + %strong Jobs events + %p.light + This URL will be triggered when the job status changes + %li + = form.check_box :pipeline_events, class: 'pull-left' + .prepend-left-20 + = form.label :pipeline_events, class: 'list-label' do + %strong Pipeline events + %p.light + This URL will be triggered when the pipeline status changes + %li + = form.check_box :wiki_page_events, class: 'pull-left' + .prepend-left-20 + = form.label :wiki_page_events, class: 'list-label' do + %strong Wiki Page events + %p.light + This URL will be triggered when a wiki page is created/updated +.form-group + = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox' + .checkbox + = form.label :enable_ssl_verification do + = form.check_box :enable_ssl_verification + %strong Enable SSL verification diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml new file mode 100644 index 00000000000..dace11e5474 --- /dev/null +++ b/app/views/snippets/notes/_actions.html.haml @@ -0,0 +1,13 @@ +- if current_user + - if note.emoji_awardable? + - user_authored = note.user_authored?(current_user) + = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do + = icon('spinner spin') + %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face') + %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley') + %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile') + - if note_editable + = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do + = icon('pencil', class: 'link-highlight') + = link_to snippet_note_path(note.noteable, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do + = icon('trash-o', class: 'danger-highlight') diff --git a/app/views/snippets/notes/_edit.html.haml b/app/views/snippets/notes/_edit.html.haml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/app/views/snippets/notes/_notes.html.haml b/app/views/snippets/notes/_notes.html.haml new file mode 100644 index 00000000000..f07d6b8c126 --- /dev/null +++ b/app/views/snippets/notes/_notes.html.haml @@ -0,0 +1,2 @@ +%ul#notes-list.notes.main-notes-list.timeline + = render "projects/notes/notes" diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml index 8a80013bbfd..98287cba5b4 100644 --- a/app/views/snippets/show.html.haml +++ b/app/views/snippets/show.html.haml @@ -3,7 +3,10 @@ = render 'shared/snippets/header' %article.file-holder.snippet-file-content - = render 'shared/snippets/blob', download_path: download_snippet_path(@snippet) + = render 'shared/snippets/blob' .row-content-block.top-block.content-component-block = render 'award_emoji/awards_block', awardable: @snippet, inline: true + +%ul#notes-list.notes.main-notes-list.timeline + #notes= render 'shared/notes/notes' diff --git a/changelogs/unreleased/12910-personal-snippets-notes-show.yml b/changelogs/unreleased/12910-personal-snippets-notes-show.yml new file mode 100644 index 00000000000..15c6f3c5e6a --- /dev/null +++ b/changelogs/unreleased/12910-personal-snippets-notes-show.yml @@ -0,0 +1,4 @@ +--- +title: Display comments for personal snippets +merge_request: +author: diff --git a/changelogs/unreleased/19364-webhook-edit.yml b/changelogs/unreleased/19364-webhook-edit.yml new file mode 100644 index 00000000000..60e154b8b83 --- /dev/null +++ b/changelogs/unreleased/19364-webhook-edit.yml @@ -0,0 +1,4 @@ +--- +title: Implement ability to edit hooks +merge_request: 10816 +author: Alexander Randa diff --git a/changelogs/unreleased/26488-target-disabled-mr.yml b/changelogs/unreleased/26488-target-disabled-mr.yml new file mode 100644 index 00000000000..02058481ccf --- /dev/null +++ b/changelogs/unreleased/26488-target-disabled-mr.yml @@ -0,0 +1,4 @@ +--- +title: Disallow merge requests from fork when source project have disabled merge requests +merge_request: +author: mhasbini diff --git a/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml b/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml new file mode 100644 index 00000000000..4452b13037b --- /dev/null +++ b/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml @@ -0,0 +1,4 @@ +--- +title: Display GitLab Pages status in Admin Dashboard +merge_request: +author: diff --git a/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml b/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml new file mode 100644 index 00000000000..950336ea932 --- /dev/null +++ b/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml @@ -0,0 +1,4 @@ +--- +title: Change Git commit command in Existing folder to git commit -m +merge_request: 10900 +author: TM Lee diff --git a/changelogs/unreleased/add-tanuki-ci-status-favicons.yml b/changelogs/unreleased/add-tanuki-ci-status-favicons.yml new file mode 100644 index 00000000000..b60ad81947a --- /dev/null +++ b/changelogs/unreleased/add-tanuki-ci-status-favicons.yml @@ -0,0 +1,4 @@ +--- +title: Updated CI status favicons to include the tanuki +merge_request: 10923 +author: diff --git a/changelogs/unreleased/dm-snippet-download-button.yml b/changelogs/unreleased/dm-snippet-download-button.yml new file mode 100644 index 00000000000..09ece1e7f98 --- /dev/null +++ b/changelogs/unreleased/dm-snippet-download-button.yml @@ -0,0 +1,4 @@ +--- +title: Add download button to project snippets +merge_request: +author: diff --git a/config/routes/admin.rb b/config/routes/admin.rb index 52ba10604d4..48993420ed9 100644 --- a/config/routes/admin.rb +++ b/config/routes/admin.rb @@ -50,8 +50,10 @@ namespace :admin do resources :deploy_keys, only: [:index, :new, :create, :destroy] - resources :hooks, only: [:index, :create, :destroy] do - get :test + resources :hooks, only: [:index, :create, :edit, :update, :destroy] do + member do + get :test + end end resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do diff --git a/config/routes/project.rb b/config/routes/project.rb index 115ae2324b3..aaad503eba4 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -44,7 +44,7 @@ constraints(ProjectUrlConstrainer.new) do resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do member do - get 'raw' + get :raw post :mark_as_spam end end @@ -185,7 +185,7 @@ constraints(ProjectUrlConstrainer.new) do end end - resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do + resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do member do get :test end diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb index 56534f677be..dae83734fe6 100644 --- a/config/routes/snippets.rb +++ b/config/routes/snippets.rb @@ -1,10 +1,17 @@ resources :snippets, concerns: :awardable do member do - get 'raw' - get 'download' + get :raw post :mark_as_spam post :preview_markdown end + + scope module: :snippets do + resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do + member do + delete :delete_attachment + end + end + end end get '/s/:username', to: redirect('/u/%{username}/snippets'), diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md index d9ca74ca1a3..359de0efadb 100644 --- a/doc/administration/high_availability/load_balancer.md +++ b/doc/administration/high_availability/load_balancer.md @@ -13,7 +13,7 @@ you need to use with GitLab. | LB Port | Backend Port | Protocol | | ------- | ------------ | --------------- | | 80 | 80 | HTTP [^1] | -| 443 | 443 | HTTPS [^1] [^2] | +| 443 | 443 | TCP or HTTPS [^1] [^2] | | 22 | 22 | TCP | ## GitLab Pages Ports diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md index 3b5ee86b68b..91e844c7b42 100644 --- a/doc/administration/integration/terminal.md +++ b/doc/administration/integration/terminal.md @@ -32,7 +32,7 @@ In brief: As web terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers -through to the next one in the chain. If you installed Gitlab using Omnibus, or +through to the next one in the chain. If you installed GitLab using Omnibus, or from source, starting with GitLab 8.15, this should be done by the default configuration, so there's no need for you to do anything. @@ -58,7 +58,7 @@ document for more details. If you'd like to disable web terminal support in GitLab, just stop passing the `Connection` and `Upgrade` hop-by-hop headers in the *first* HTTP reverse proxy in the chain. For most users, this will be the NGINX server bundled with -Omnibus Gitlab, in which case, you need to: +Omnibus GitLab, in which case, you need to: * Find the `nginx['proxy_set_headers']` section of your `gitlab.rb` file * Ensure the whole block is uncommented, and then comment out or remove the diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index 1c549844ee1..2513f4b420a 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -1,24 +1,28 @@ # How to create a project in GitLab -There are two ways to create a new project in GitLab. - -1. While in your dashboard, you can create a new project using the **New project** - green button or you can use the cross icon in the upper right corner next to - your avatar which is always visible. +1. In your dashboard, click the green **New project** button or use the plus + icon in the upper right corner of the navigation bar. ![Create a project](img/create_new_project_button.png) -1. From there you can see several options. +1. This opens the **New project** page. ![Project information](img/create_new_project_info.png) -1. Fill out the information: +1. Provide the following information: + - Enter the name of your project in the **Project name** field. You can't use + special characters, but you can use spaces, hyphens, underscores or even + emoji. + - If you have a project in a different repository, you can [import it] by + clicking an **Import project from** button provided this is enabled in + your GitLab instance. Ask your administrator if not. + - The **Project description (optional)** field enables you to enter a + description for your project's dashboard, which will help others + understand what your project is about. Though it's not required, it's a good + idea to fill this in. + - Changing the **Visibility Level** modifies the project's + [viewing and access rights](../public_access/public_access.md) for users. - 1. "Project name" is the name of your project (you can't use special characters, - but you can use spaces, hyphens, underscores or even emojis). - 1. The "Project description" is optional and will be shown in your project's - dashboard so others can briefly understand what your project is about. - 1. Select a [visibility level](../public_access/public_access.md). - 1. You can also [import your existing projects](../workflow/importing/README.md). +1. Click **Create project**. -1. Finally, click **Create project**. +[import it]: ../workflow/importing/README.md diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png index 8d7a69e55ed..567f104880f 100644 Binary files a/doc/gitlab-basics/img/create_new_project_button.png and b/doc/gitlab-basics/img/create_new_project_button.png differ diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md index e5e3cd395df..e538983e603 100644 --- a/doc/update/8.10-to-8.11.md +++ b/doc/update/8.10-to-8.11.md @@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. Get latest code ```bash +cd /home/git/gitlab + sudo -u git -H git fetch --all sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md index d6b3b0ffa5a..604166beb56 100644 --- a/doc/update/8.11-to-8.12.md +++ b/doc/update/8.11-to-8.12.md @@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. Get latest code ```bash +cd /home/git/gitlab + sudo -u git -H git fetch --all sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md index ed0e668d854..d83965131f5 100644 --- a/doc/update/8.12-to-8.13.md +++ b/doc/update/8.12-to-8.13.md @@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. Get latest code ```bash +cd /home/git/gitlab + sudo -u git -H git fetch --all sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md index aa1c659717e..aaadcec8ac0 100644 --- a/doc/update/8.13-to-8.14.md +++ b/doc/update/8.13-to-8.14.md @@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc ### 4. Get latest code ```bash +cd /home/git/gitlab + sudo -u git -H git fetch --all sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically ``` diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index e5793fbc5cb..710deba5ae3 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -20,6 +20,8 @@ module API error!(errors[:validate_fork], 422) elsif errors[:validate_branches].any? conflict!(errors[:validate_branches]) + elsif errors[:base].any? + error!(errors[:base], 422) end render_api_error!(errors, 400) diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb index 3077240e650..1616142a619 100644 --- a/lib/api/v3/merge_requests.rb +++ b/lib/api/v3/merge_requests.rb @@ -23,6 +23,8 @@ module API error!(errors[:validate_fork], 422) elsif errors[:validate_branches].any? conflict!(errors[:validate_branches]) + elsif errors[:base].any? + error!(errors[:base], 422) end render_api_error!(errors, 400) diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index 18eda0279f7..c3f0de76d01 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -122,13 +122,30 @@ module Gitlab # Returns the number of valid branches def branch_count - rugged.branches.count do |ref| - begin - ref.name && ref.target # ensures the branch is valid + Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled| + if is_enabled + gitaly_ref_client.count_branch_names + else + rugged.branches.count do |ref| + begin + ref.name && ref.target # ensures the branch is valid - true - rescue Rugged::ReferenceError - false + true + rescue Rugged::ReferenceError + false + end + end + end + end + end + + # Returns the number of valid tags + def tag_count + Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled| + if is_enabled + gitaly_ref_client.count_tag_names + else + rugged.tags.count end end end diff --git a/lib/gitlab/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb index d3c0743db4e..2a5e8f73e55 100644 --- a/lib/gitlab/gitaly_client/ref.rb +++ b/lib/gitlab/gitaly_client/ref.rb @@ -34,6 +34,14 @@ module Gitlab stub.find_ref_name(request).name end + def count_tag_names + tag_names.count + end + + def count_branch_names + branch_names.count + end + private def consume_refs_response(response, prefix:) diff --git a/scripts/notify_slack.sh b/scripts/notify_slack.sh deleted file mode 100755 index 6b3bc563c7a..00000000000 --- a/scripts/notify_slack.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh -# Sends Slack notification ERROR_MSG to CHANNEL -# An env. variable CI_SLACK_WEBHOOK_URL needs to be set. - -CHANNEL=$1 -ERROR_MSG=$2 - -if [ -z "$CHANNEL" ] || [ -z "$ERROR_MSG" ] || [ -z "$CI_SLACK_WEBHOOK_URL" ]; then - echo "Missing argument(s) - Use: $0 channel message" - echo "and set CI_SLACK_WEBHOOK_URL environment variable." -else - curl -X POST --data-urlencode 'payload={"channel": "'"$CHANNEL"'", "username": "gitlab-ci", "text": "'"$ERROR_MSG"'", "icon_emoji": ":gitlab:"}' "$CI_SLACK_WEBHOOK_URL" -fi \ No newline at end of file diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index f140eaef5d5..45f4cf9180d 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -167,6 +167,47 @@ describe Projects::NotesController do end end + describe 'DELETE destroy' do + let(:request_params) do + { + namespace_id: project.namespace, + project_id: project, + id: note, + format: :js + } + end + + context 'user is the author of a note' do + before do + sign_in(note.author) + project.team << [note.author, :developer] + end + + it "returns status 200 for html" do + delete :destroy, request_params + + expect(response).to have_http_status(200) + end + + it "deletes the note" do + expect { delete :destroy, request_params }.to change { Note.count }.from(1).to(0) + end + end + + context 'user is not the author of a note' do + before do + sign_in(user) + project.team << [user, :developer] + end + + it "returns status 404" do + delete :destroy, request_params + + expect(response).to have_http_status(404) + end + end + end + describe 'POST toggle_award_emoji' do before do sign_in(user) diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb new file mode 100644 index 00000000000..1c494b8c7ab --- /dev/null +++ b/spec/controllers/snippets/notes_controller_spec.rb @@ -0,0 +1,196 @@ +require 'spec_helper' + +describe Snippets::NotesController do + let(:user) { create(:user) } + + let(:private_snippet) { create(:personal_snippet, :private) } + let(:internal_snippet) { create(:personal_snippet, :internal) } + let(:public_snippet) { create(:personal_snippet, :public) } + + let(:note_on_private) { create(:note_on_personal_snippet, noteable: private_snippet) } + let(:note_on_internal) { create(:note_on_personal_snippet, noteable: internal_snippet) } + let(:note_on_public) { create(:note_on_personal_snippet, noteable: public_snippet) } + + describe 'GET index' do + context 'when a snippet is public' do + before do + note_on_public + + get :index, { snippet_id: public_snippet } + end + + it "returns status 200" do + expect(response).to have_http_status(200) + end + + it "returns not empty array of notes" do + expect(JSON.parse(response.body)["notes"].empty?).to be_falsey + end + end + + context 'when a snippet is internal' do + before do + note_on_internal + end + + context 'when user not logged in' do + it "returns status 404" do + get :index, { snippet_id: internal_snippet } + + expect(response).to have_http_status(404) + end + end + + context 'when user logged in' do + before do + sign_in(user) + end + + it "returns status 200" do + get :index, { snippet_id: internal_snippet } + + expect(response).to have_http_status(200) + end + end + end + + context 'when a snippet is private' do + before do + note_on_private + end + + context 'when user not logged in' do + it "returns status 404" do + get :index, { snippet_id: private_snippet } + + expect(response).to have_http_status(404) + end + end + + context 'when user other than author logged in' do + before do + sign_in(user) + end + + it "returns status 404" do + get :index, { snippet_id: private_snippet } + + expect(response).to have_http_status(404) + end + end + + context 'when author logged in' do + before do + note_on_private + + sign_in(private_snippet.author) + end + + it "returns status 200" do + get :index, { snippet_id: private_snippet } + + expect(response).to have_http_status(200) + end + + it "returns 1 note" do + get :index, { snippet_id: private_snippet } + + expect(JSON.parse(response.body)['notes'].count).to eq(1) + end + end + end + + context 'dont show non visible notes' do + before do + note_on_public + + sign_in(user) + + expect_any_instance_of(Note).to receive(:cross_reference_not_visible_for?).and_return(true) + end + + it "does not return any note" do + get :index, { snippet_id: public_snippet } + + expect(JSON.parse(response.body)['notes'].count).to eq(0) + end + end + end + + describe 'DELETE destroy' do + let(:request_params) do + { + snippet_id: public_snippet, + id: note_on_public, + format: :js + } + end + + context 'when user is the author of a note' do + before do + sign_in(note_on_public.author) + end + + it "returns status 200" do + delete :destroy, request_params + + expect(response).to have_http_status(200) + end + + it "deletes the note" do + expect{ delete :destroy, request_params }.to change{ Note.count }.from(1).to(0) + end + + context 'system note' do + before do + expect_any_instance_of(Note).to receive(:system?).and_return(true) + end + + it "does not delete the note" do + expect{ delete :destroy, request_params }.not_to change{ Note.count } + end + end + end + + context 'when user is not the author of a note' do + before do + sign_in(user) + + note_on_public + end + + it "returns status 404" do + delete :destroy, request_params + + expect(response).to have_http_status(404) + end + + it "does not update the note" do + expect{ delete :destroy, request_params }.not_to change{ Note.count } + end + end + end + + describe 'POST toggle_award_emoji' do + let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) } + before do + sign_in(user) + end + + subject { post(:toggle_award_emoji, snippet_id: public_snippet, id: note.id, name: "thumbsup") } + + it "toggles the award emoji" do + expect { subject }.to change { note.award_emoji.count }.by(1) + + expect(response).to have_http_status(200) + end + + it "removes the already awarded emoji when it exists" do + note.toggle_award_emoji('thumbsup', user) # create award emoji before + + expect { subject }.to change { AwardEmoji.count }.by(-1) + + expect(response).to have_http_status(200) + end + end +end diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 234f3edd3d8..41cd5bdcdd8 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -350,144 +350,138 @@ describe SnippetsController do end end - %w(raw download).each do |action| - describe "GET #{action}" do - context 'when the personal snippet is private' do - let(:personal_snippet) { create(:personal_snippet, :private, author: user) } + describe "GET #raw" do + context 'when the personal snippet is private' do + let(:personal_snippet) { create(:personal_snippet, :private, author: user) } - context 'when signed in' do - before do - sign_in(user) - end - - context 'when signed in user is not the author' do - let(:other_author) { create(:author) } - let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } - - it 'responds with status 404' do - get action, id: other_personal_snippet.to_param - - expect(response).to have_http_status(404) - end - end - - context 'when signed in user is the author' do - before { get action, id: personal_snippet.to_param } - - it 'responds with status 200' do - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) - end - - it 'has expected headers' do - expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') - - if action == :download - expect(response.header['Content-Disposition']).to match(/attachment/) - elsif action == :raw - expect(response.header['Content-Disposition']).to match(/inline/) - end - end - end + context 'when signed in' do + before do + sign_in(user) end - context 'when not signed in' do - it 'redirects to the sign in page' do - get action, id: personal_snippet.to_param - - expect(response).to redirect_to(new_user_session_path) - end - end - end - - context 'when the personal snippet is internal' do - let(:personal_snippet) { create(:personal_snippet, :internal, author: user) } - - context 'when signed in' do - before do - sign_in(user) - end - - it 'responds with status 200' do - get action, id: personal_snippet.to_param - - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) - end - end - - context 'when not signed in' do - it 'redirects to the sign in page' do - get action, id: personal_snippet.to_param - - expect(response).to redirect_to(new_user_session_path) - end - end - end - - context 'when the personal snippet is public' do - let(:personal_snippet) { create(:personal_snippet, :public, author: user) } - - context 'when signed in' do - before do - sign_in(user) - end - - it 'responds with status 200' do - get action, id: personal_snippet.to_param - - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) - end - - context 'CRLF line ending' do - let(:personal_snippet) do - create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line") - end - - it 'returns LF line endings by default' do - get action, id: personal_snippet.to_param - - expect(response.body).to eq("first line\nsecond line\nthird line") - end - - it 'does not convert line endings when parameter present' do - get action, id: personal_snippet.to_param, line_ending: :raw - - expect(response.body).to eq("first line\r\nsecond line\r\nthird line") - end - end - end - - context 'when not signed in' do - it 'responds with status 200' do - get action, id: personal_snippet.to_param - - expect(assigns(:snippet)).to eq(personal_snippet) - expect(response).to have_http_status(200) - end - end - end - - context 'when the personal snippet does not exist' do - context 'when signed in' do - before do - sign_in(user) - end + context 'when signed in user is not the author' do + let(:other_author) { create(:author) } + let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) } it 'responds with status 404' do - get action, id: 'doesntexist' + get :raw, id: other_personal_snippet.to_param expect(response).to have_http_status(404) end end - context 'when not signed in' do - it 'responds with status 404' do - get action, id: 'doesntexist' + context 'when signed in user is the author' do + before { get :raw, id: personal_snippet.to_param } - expect(response).to have_http_status(404) + it 'responds with status 200' do + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) end + + it 'has expected headers' do + expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8') + + expect(response.header['Content-Disposition']).to match(/inline/) + end + end + end + + context 'when not signed in' do + it 'redirects to the sign in page' do + get :raw, id: personal_snippet.to_param + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context 'when the personal snippet is internal' do + let(:personal_snippet) { create(:personal_snippet, :internal, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + it 'responds with status 200' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) + end + end + + context 'when not signed in' do + it 'redirects to the sign in page' do + get :raw, id: personal_snippet.to_param + + expect(response).to redirect_to(new_user_session_path) + end + end + end + + context 'when the personal snippet is public' do + let(:personal_snippet) { create(:personal_snippet, :public, author: user) } + + context 'when signed in' do + before do + sign_in(user) + end + + it 'responds with status 200' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) + end + + context 'CRLF line ending' do + let(:personal_snippet) do + create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line") + end + + it 'returns LF line endings by default' do + get :raw, id: personal_snippet.to_param + + expect(response.body).to eq("first line\nsecond line\nthird line") + end + + it 'does not convert line endings when parameter present' do + get :raw, id: personal_snippet.to_param, line_ending: :raw + + expect(response.body).to eq("first line\r\nsecond line\r\nthird line") + end + end + end + + context 'when not signed in' do + it 'responds with status 200' do + get :raw, id: personal_snippet.to_param + + expect(assigns(:snippet)).to eq(personal_snippet) + expect(response).to have_http_status(200) + end + end + end + + context 'when the personal snippet does not exist' do + context 'when signed in' do + before do + sign_in(user) + end + + it 'responds with status 404' do + get :raw, id: 'doesntexist' + + expect(response).to have_http_status(404) + end + end + + context 'when not signed in' do + it 'responds with status 404' do + get :raw, id: 'doesntexist' + + expect(response).to have_http_status(404) end end end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 93f4903119c..44c3186d813 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -5,7 +5,7 @@ include ActionDispatch::TestProcess FactoryGirl.define do factory :note do project factory: :empty_project - note "Note" + note { generate(:title) } author on_issue diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb index 39c2a9dd1fb..0210e871a63 100644 --- a/spec/factories/project_hooks.rb +++ b/spec/factories/project_hooks.rb @@ -1,6 +1,7 @@ FactoryGirl.define do factory :project_hook do url { generate(:url) } + enable_ssl_verification false trait :token do token { SecureRandom.hex(10) } @@ -11,6 +12,7 @@ FactoryGirl.define do merge_requests_events true tag_push_events true issues_events true + confidential_issues_events true note_events true build_events true pipeline_events true diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index fb519a9bf12..c5f24d412d7 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Admin::Hooks", feature: true do +describe 'Admin::Hooks', feature: true do before do @project = create(:project) login_as :admin @@ -8,24 +8,24 @@ describe "Admin::Hooks", feature: true do @system_hook = create(:system_hook) end - describe "GET /admin/hooks" do - it "is ok" do + describe 'GET /admin/hooks' do + it 'is ok' do visit admin_root_path - page.within ".layout-nav" do - click_on "Hooks" + page.within '.layout-nav' do + click_on 'Hooks' end expect(current_path).to eq(admin_hooks_path) end - it "has hooks list" do + it 'has hooks list' do visit admin_hooks_path expect(page).to have_content(@system_hook.url) end end - describe "New Hook" do + describe 'New Hook' do let(:url) { generate(:url) } it 'adds new hook' do @@ -40,11 +40,36 @@ describe "Admin::Hooks", feature: true do end end - describe "Test" do + describe 'Update existing hook' do + let(:new_url) { generate(:url) } + + it 'updates existing hook' do + visit admin_hooks_path + + click_link 'Edit' + fill_in 'hook_url', with: new_url + check 'Enable SSL verification' + click_button 'Save changes' + + expect(page).to have_content 'SSL Verification: enabled' + expect(current_path).to eq(admin_hooks_path) + expect(page).to have_content(new_url) + end + end + + describe 'Remove existing hook' do + it 'remove existing hook' do + visit admin_hooks_path + + expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1) + end + end + + describe 'Test' do before do WebMock.stub_request(:post, @system_hook.url) visit admin_hooks_path - click_link "Test hook" + click_link 'Test hook' end it { expect(current_path).to eq(admin_hooks_path) } diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 6a6f8b4f4d5..8dba2ccbafa 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -231,7 +231,7 @@ feature 'File blob', :js, feature: true do branch_name: 'master', commit_message: "Add PDF", file_path: 'files/test.pdf', - file_content: File.read(Rails.root.join('spec/javascripts/blob/pdf/test.pdf')) + file_content: project.repository.blob_at('add-pdf-file', 'files/pdf/test.pdf').data ).execute visit_blob('files/test.pdf') diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb new file mode 100644 index 00000000000..7909234556e --- /dev/null +++ b/spec/features/projects/settings/integration_settings_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +feature 'Integration settings', feature: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:role) { :developer } + let(:integrations_path) { namespace_project_settings_integrations_path(project.namespace, project) } + + background do + login_as(user) + project.team << [user, role] + end + + context 'for developer' do + given(:role) { :developer } + + scenario 'to be disallowed to view' do + visit integrations_path + + expect(page.status_code).to eq(404) + end + end + + context 'for master' do + given(:role) { :master } + + context 'Webhooks' do + let(:hook) { create(:project_hook, :all_events_enabled, enable_ssl_verification: true, project: project) } + let(:url) { generate(:url) } + + scenario 'show list of webhooks' do + hook + + visit integrations_path + + expect(page.status_code).to eq(200) + expect(page).to have_content(hook.url) + expect(page).to have_content('SSL Verification: enabled') + expect(page).to have_content('Push Events') + expect(page).to have_content('Tag Push Events') + expect(page).to have_content('Issues Events') + expect(page).to have_content('Confidential Issues Events') + expect(page).to have_content('Note Events') + expect(page).to have_content('Merge Requests Events') + expect(page).to have_content('Pipeline Events') + expect(page).to have_content('Wiki Page Events') + end + + scenario 'create webhook' do + visit integrations_path + + fill_in 'hook_url', with: url + check 'Tag push events' + check 'Enable SSL verification' + + click_button 'Add webhook' + + expect(page).to have_content(url) + expect(page).to have_content('SSL Verification: enabled') + expect(page).to have_content('Push Events') + expect(page).to have_content('Tag Push Events') + end + + scenario 'edit existing webhook' do + hook + visit integrations_path + + click_link 'Edit' + fill_in 'hook_url', with: url + check 'Enable SSL verification' + click_button 'Save changes' + + expect(page).to have_content 'SSL Verification: enabled' + expect(page).to have_content(url) + end + + scenario 'test existing webhook' do + WebMock.stub_request(:post, hook.url) + visit integrations_path + + click_link 'Test' + + expect(current_path).to eq(integrations_path) + end + + scenario 'remove existing webhook' do + hook + visit integrations_path + + expect { click_link 'Remove' }.to change(ProjectHook, :count).by(-1) + end + end + end +end diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb index 7eb1210e307..cedf3778c7e 100644 --- a/spec/features/projects/snippets/show_spec.rb +++ b/spec/features/projects/snippets/show_spec.rb @@ -30,6 +30,12 @@ feature 'Project snippet', :js, feature: true do # shows an enabled copy button expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') end end end @@ -59,6 +65,12 @@ feature 'Project snippet', :js, feature: true do # shows a disabled copy button expect(page).to have_selector('.js-copy-blob-source-btn.disabled') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') end end diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb new file mode 100644 index 00000000000..c646039e0b1 --- /dev/null +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe 'Comments on personal snippets', feature: true do + let!(:user) { create(:user) } + let!(:snippet) { create(:personal_snippet, :public) } + let!(:snippet_notes) do + [ + create(:note_on_personal_snippet, noteable: snippet, author: user), + create(:note_on_personal_snippet, noteable: snippet) + ] + end + let!(:other_note) { create(:note_on_personal_snippet) } + + before do + login_as user + visit snippet_path(snippet) + end + + subject { page } + + context 'viewing the snippet detail page' do + it 'contains notes for a snippet with correct action icons' do + expect(page).to have_selector('#notes-list li', count: 2) + + # comment authored by current user + page.within("#notes-list li#note_#{snippet_notes[0].id}") do + expect(page).to have_content(snippet_notes[0].note) + expect(page).to have_selector('.js-note-delete') + expect(page).to have_selector('.note-emoji-button') + end + + page.within("#notes-list li#note_#{snippet_notes[1].id}") do + expect(page).to have_content(snippet_notes[1].note) + expect(page).not_to have_selector('.js-note-delete') + expect(page).to have_selector('.note-emoji-button') + end + end + end +end diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb index cebcba6a230..e36cf547f80 100644 --- a/spec/features/snippets/show_spec.rb +++ b/spec/features/snippets/show_spec.rb @@ -24,6 +24,12 @@ feature 'Snippet', :js, feature: true do # shows an enabled copy button expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') end end end @@ -53,6 +59,12 @@ feature 'Snippet', :js, feature: true do # shows a disabled copy button expect(page).to have_selector('.js-copy-blob-source-btn.disabled') + + # shows a raw button + expect(page).to have_link('Open raw') + + # shows a download button + expect(page).to have_link('Download') end end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 765bf44d863..ba6bbb3bce0 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -110,6 +110,15 @@ describe NotesFinder do expect(notes.count).to eq(1) end + it 'finds notes on personal snippets' do + note = create(:note_on_personal_snippet) + params = { target_type: 'personal_snippet', target_id: note.noteable_id } + + notes = described_class.new(project, user, params).execute + + expect(notes.count).to eq(1) + end + it 'raises an exception for an invalid target_type' do params[:target_type] = 'invalid' expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type') diff --git a/spec/helpers/award_emoji_helper_spec.rb b/spec/helpers/award_emoji_helper_spec.rb new file mode 100644 index 00000000000..7dfd6a3f6b4 --- /dev/null +++ b/spec/helpers/award_emoji_helper_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe AwardEmojiHelper do + describe '.toggle_award_url' do + context 'note on personal snippet' do + let(:note) { create(:note_on_personal_snippet) } + + it 'returns correct url' do + expected_url = "/snippets/#{note.noteable.id}/notes/#{note.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(note)).to eq(expected_url) + end + end + + context 'note on project item' do + let(:note) { create(:note_on_project_snippet) } + + it 'returns correct url' do + @project = note.noteable.project + + expected_url = "/#{@project.namespace.path}/#{@project.path}/notes/#{note.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(note)).to eq(expected_url) + end + end + + context 'personal snippet' do + let(:snippet) { create(:personal_snippet) } + + it 'returns correct url' do + expected_url = "/snippets/#{snippet.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(snippet)).to eq(expected_url) + end + end + + context 'merge request' do + let(:merge_request) { create(:merge_request) } + + it 'returns correct url' do + @project = merge_request.project + + expected_url = "/#{@project.namespace.path}/#{@project.path}/merge_requests/#{merge_request.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(merge_request)).to eq(expected_url) + end + end + + context 'issue' do + let(:issue) { create(:issue) } + + it 'returns correct url' do + @project = issue.project + + expected_url = "/#{@project.namespace.path}/#{@project.path}/issues/#{issue.id}/toggle_award_emoji" + + expect(helper.toggle_award_url(issue)).to eq(expected_url) + end + end + end +end diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index e9037749ef2..10681af5f7e 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -64,7 +64,7 @@ describe MergeRequestsHelper do it do @project = project - + is_expected.to eq("#1, #2, and #{other_project.namespace.path}/#{other_project.path}#3") end end @@ -149,6 +149,50 @@ describe MergeRequestsHelper do end end + describe '#target_projects' do + let(:project) { create(:empty_project) } + let(:fork_project) { create(:empty_project, forked_from_project: project) } + + context 'when target project has enabled merge requests' do + it 'returns the forked_from project' do + expect(target_projects(fork_project)).to contain_exactly(project, fork_project) + end + end + + context 'when target project has disabled merge requests' do + it 'returns the forked project' do + project.project_feature.update(merge_requests_access_level: 0) + + expect(target_projects(fork_project)).to contain_exactly(fork_project) + end + end + end + + describe '#new_mr_path_from_push_event' do + subject(:url_params) { URI.decode_www_form(new_mr_path_from_push_event(event)).to_h } + let(:user) { create(:user) } + let(:project) { create(:empty_project, creator: user) } + let(:fork_project) { create(:project, forked_from_project: project, creator: user) } + let(:event) do + push_data = Gitlab::DataBuilder::Push.build_sample(fork_project, user) + create(:event, :pushed, project: fork_project, target: fork_project, author: user, data: push_data) + end + + context 'when target project has enabled merge requests' do + it 'returns link to create merge request on source project' do + expect(url_params['merge_request[target_project_id]'].to_i).to eq(project.id) + end + end + + context 'when target project has disabled merge requests' do + it 'returns link to create merge request on forked project' do + project.project_feature.update(merge_requests_access_level: 0) + + expect(url_params['merge_request[target_project_id]'].to_i).to eq(fork_project.id) + end + end + end + describe '#mr_issues_mentioned_but_not_closing' do let(:user_1) { create(:user) } let(:user_2) { create(:user) } diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index f88653cb1fe..1b78910fa3c 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1074,20 +1074,8 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe '#branch_count' do - before(:each) do - valid_ref = double(:ref) - invalid_ref = double(:ref) - - allow(valid_ref).to receive_messages(name: 'master', target: double(:target)) - - allow(invalid_ref).to receive_messages(name: 'bad-branch') - allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError } - - allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref]) - end - it 'returns the number of branches' do - expect(repository.branch_count).to eq(1) + expect(repository.branch_count).to eq(9) end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f6846cc1b2f..5216764a82d 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1379,12 +1379,22 @@ describe Repository, models: true do describe '#branch_count' do it 'returns the number of branches' do expect(repository.branch_count).to be_an(Integer) + + # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync + rugged_count = repository.raw_repository.rugged.branches.count + + expect(repository.branch_count).to eq(rugged_count) end end describe '#tag_count' do it 'returns the number of tags' do expect(repository.tag_count).to be_an(Integer) + + # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync + rugged_count = repository.raw_repository.rugged.tags.count + + expect(repository.tag_count).to eq(rugged_count) end end diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index c4bff1647b5..16e5efb2f5b 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -434,6 +434,19 @@ describe API::MergeRequests do expect(json_response['title']).to eq('Test merge_request') end + it 'returns 422 when target project has disabled merge requests' do + project.project_feature.update(merge_requests_access_level: 0) + + post api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test', + target_branch: 'master', + source_branch: 'markdown', + author: user2, + target_project_id: project.id + + expect(response).to have_http_status(422) + end + it "returns 400 when source_branch is missing" do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index 6c2950a6e6f..f6ff96be566 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -338,6 +338,19 @@ describe API::MergeRequests do expect(json_response['title']).to eq('Test merge_request') end + it "returns 422 when target project has disabled merge requests" do + project.project_feature.update(merge_requests_access_level: 0) + + post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + title: 'Test', + target_branch: "master", + source_branch: 'markdown', + author: user2, + target_project_id: project.id + + expect(response).to have_http_status(422) + end + it "returns 400 when source_branch is missing" do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 99c44bde151..e5fc0b676af 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -71,13 +71,15 @@ describe Admin::ProjectsController, "routing" do end end -# admin_hook_test GET /admin/hooks/:hook_id/test(.:format) admin/hooks#test +# admin_hook_test GET /admin/hooks/:id/test(.:format) admin/hooks#test # admin_hooks GET /admin/hooks(.:format) admin/hooks#index # POST /admin/hooks(.:format) admin/hooks#create # admin_hook DELETE /admin/hooks/:id(.:format) admin/hooks#destroy +# PUT /admin/hooks/:id(.:format) admin/hooks#update +# edit_admin_hook GET /admin/hooks/:id(.:format) admin/hooks#edit describe Admin::HooksController, "routing" do it "to #test" do - expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', hook_id: '1') + expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', id: '1') end it "to #index" do @@ -88,6 +90,14 @@ describe Admin::HooksController, "routing" do expect(post("/admin/hooks")).to route_to('admin/hooks#create') end + it "to #edit" do + expect(get("/admin/hooks/1/edit")).to route_to('admin/hooks#edit', id: '1') + end + + it "to #update" do + expect(put("/admin/hooks/1")).to route_to('admin/hooks#update', id: '1') + end + it "to #destroy" do expect(delete("/admin/hooks/1")).to route_to('admin/hooks#destroy', id: '1') end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index a3de022d242..163df072cf6 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -340,14 +340,16 @@ describe 'project routing' do # test_project_hook GET /:project_id/hooks/:id/test(.:format) hooks#test # project_hooks GET /:project_id/hooks(.:format) hooks#index # POST /:project_id/hooks(.:format) hooks#create - # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy + # edit_project_hook GET /:project_id/hooks/:id/edit(.:format) hooks#edit + # project_hook PUT /:project_id/hooks/:id(.:format) hooks#update + # DELETE /:project_id/hooks/:id(.:format) hooks#destroy describe Projects::HooksController, 'routing' do it 'to #test' do expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end it_behaves_like 'RESTful project resources' do - let(:actions) { [:index, :create, :destroy] } + let(:actions) { [:index, :create, :destroy, :edit, :update] } let(:controller) { 'hooks' } end end diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb index c94902dbab8..3964b998084 100644 --- a/spec/serializers/status_entity_spec.rb +++ b/spec/serializers/status_entity_spec.rb @@ -18,6 +18,12 @@ describe StatusEntity do it 'contains status details' do expect(subject).to include :text, :icon, :favicon, :label, :group expect(subject).to include :has_details, :details_path + expect(subject[:favicon]).to eq('/assets/ci_favicons/favicon_status_success.ico') + end + + it 'contains a dev namespaced favicon if dev env' do + allow(Rails.env).to receive(:development?) { true } + expect(entity.as_json[:favicon]).to eq('/assets/ci_favicons/dev/favicon_status_success.ico') end end end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index be9f9ea2dec..6f9d1208b1d 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -261,6 +261,16 @@ describe MergeRequests::BuildService, services: true do end end + context 'upstream project has disabled merge requests' do + let(:upstream_project) { create(:empty_project, :merge_requests_disabled) } + let(:project) { create(:empty_project, forked_from_project: upstream_project) } + let(:commits) { Commit.decorate([commit_1], project) } + + it 'sets target project correctly' do + expect(merge_request.target_project).to eq(project) + end + end + context 'target_project is set and accessible by current_user' do let(:target_project) { create(:project, :public, :repository)} let(:commits) { Commit.decorate([commit_1], project) } diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb index 0a4a6ed8145..df2f2ce95e6 100644 --- a/spec/tasks/gitlab/backup_rake_spec.rb +++ b/spec/tasks/gitlab/backup_rake_spec.rb @@ -230,11 +230,13 @@ describe 'gitlab:app namespace rake task' do before do FileUtils.mkdir('tmp/tests/default_storage') FileUtils.mkdir('tmp/tests/custom_storage') + gitaly_address = Gitlab.config.repositories.storages.default.gitaly_address storages = { - 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') }, - 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage') } + 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address }, + 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address } } allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + Gitlab::GitalyClient.configure_channels # Create the projects now, after mocking the settings but before doing the backup project_a