diff options
73 files changed, 1207 insertions, 231 deletions
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index 1f735e13391..5becf688652 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -137,6 +137,14 @@ return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; }; + gl.utils.isMetaClick = function(e) { + // Identify following special clicks + // 1) Cmd + Click on Mac (e.metaKey) + // 2) Ctrl + Click on PC (e.ctrlKey) + // 3) Middle-click or Mouse Wheel Click (e.which is 2) + return e.metaKey || e.ctrlKey || e.which === 2; + }; + gl.utils.scrollToElement = function($el) { var top = $el.offset().top; gl.navBarHeight = gl.navBarHeight || $('.navbar-gitlab').height(); diff --git a/app/assets/javascripts/merge_request_tabs.js.es6 b/app/assets/javascripts/merge_request_tabs.js.es6 index 107e85f1225..af1ba9ecaf3 100644 --- a/app/assets/javascripts/merge_request_tabs.js.es6 +++ b/app/assets/javascripts/merge_request_tabs.js.es6 @@ -82,12 +82,18 @@ require('./flash'); $(document) .on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .on('click', '.js-show-tab', this.showTab); + + $('.merge-request-tabs a[data-toggle="tab"]') + .on('click', this.clickTab); } unbindEvents() { $(document) .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .off('click', '.js-show-tab', this.showTab); + + $('.merge-request-tabs a[data-toggle="tab"]') + .off('click', this.clickTab); } showTab(e) { @@ -95,6 +101,14 @@ require('./flash'); this.activateTab($(e.target).data('action')); } + clickTab(e) { + if (e.target && gl.utils.isMetaClick(e)) { + const targetLink = e.target.getAttribute('href'); + e.stopImmediatePropagation(); + window.open(targetLink, '_blank'); + } + } + tabShown(e) { const $target = $(e.target); const action = $target.data('action'); diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6 index 96c7d927509..b07e62a8c30 100644 --- a/app/assets/javascripts/todos.js.es6 +++ b/app/assets/javascripts/todos.js.es6 @@ -146,14 +146,26 @@ } goToTodoUrl(e) { - const todoLink = $(this).data('url'); + const todoLink = this.dataset.url; + let targetLink = e.target.getAttribute('href'); + + if (e.target.tagName === 'IMG') { // See if clicked target was Avatar + targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link + } + if (!todoLink) { return; } - // Allow Meta-Click or Mouse3-click to open in a new tab - if (e.metaKey || e.which === 2) { + + if (gl.utils.isMetaClick(e)) { e.preventDefault(); - return window.open(todoLink, '_blank'); + // Meta-Click on username leads to different URL than todoLink. + // Turbolinks can resolve that URL, but window.open requires URL manually. + if (targetLink !== todoLink) { + return window.open(targetLink, '_blank'); + } else { + return window.open(todoLink, '_blank'); + } } else { return gl.utils.visitUrl(todoLink); } diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 426596027de..2bfdb9f9601 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -307,3 +307,7 @@ ul.controls { } } } + +ul.indent-list { + padding: 10px 0 0 30px; +} diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 4ef95d27f4f..9174976c4c6 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -193,7 +193,7 @@ top: $header-height; bottom: 0; right: 0; - z-index: 10; + z-index: 8; transition: width .3s; background: $gray-light; padding: 10px 20px; diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 367a468e1ba..86d4c21d7ff 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -229,6 +229,7 @@ .finished-at { color: $gl-text-color-secondary; margin: 4px 0; + white-space: nowrap; .fa { font-size: 12px; @@ -666,7 +667,7 @@ vertical-align: bottom; display: inline-block; position: relative; - font-weight: 200; + font-weight: normal; } // Dropdown button in mini pipeline graph diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 9940263ae24..4c39fe98028 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -30,6 +30,8 @@ class Projects::BlobController < Projects::ApplicationController end def show + environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } + @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last end def edit diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index f880a9862c6..e10d7992db7 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -94,6 +94,8 @@ class Projects::CommitController < Projects::ApplicationController @diffs = commit.diffs(opts) @notes_count = commit.notes.count + + @environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last end def define_note_vars diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index 321cde255c3..c6651254d70 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -57,6 +57,9 @@ class Projects::CompareController < Projects::ApplicationController @diffs = @compare.diffs(diff_options) + environment_params = @repository.branch_exists?(@head_ref) ? { ref: @head_ref } : { commit: @commit } + @environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last + @diff_notes_disabled = true @grouped_diff_discussions = {} end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 77877cd262d..0ec8f5bd64a 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -10,7 +10,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController def index @scope = params[:scope] - @environments = project.environments + @environments = project.environments.includes(:last_deployment) respond_to do |format| format.html diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 368bd27c91a..3be6e8e1772 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -103,6 +103,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController end end + @environment = @merge_request.environments_for(current_user).last + respond_to do |format| format.html { define_discussion_vars } format.json do @@ -248,7 +250,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController end @diff_notes_disabled = true - render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) } + @environment = @merge_request.environments_for(current_user).last + + render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs, environment: @environment) } end end end @@ -447,9 +451,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController def ci_environments_status environments = begin - @merge_request.environments.map do |environment| - next unless can?(current_user, :read_environment, environment) - + @merge_request.environments_for(current_user).map do |environment| project = environment.project deployment = environment.first_deployment_for(@merge_request.diff_head_commit) diff --git a/app/finders/environments_finder.rb b/app/finders/environments_finder.rb new file mode 100644 index 00000000000..a59f8c1efa3 --- /dev/null +++ b/app/finders/environments_finder.rb @@ -0,0 +1,55 @@ +class EnvironmentsFinder + attr_reader :project, :current_user, :params + + def initialize(project, current_user, params = {}) + @project, @current_user, @params = project, current_user, params + end + + def execute + deployments = project.deployments + deployments = + if ref + deployments_query = params[:with_tags] ? 'ref = :ref OR tag IS TRUE' : 'ref = :ref' + deployments.where(deployments_query, ref: ref.to_s) + elsif commit + deployments.where(sha: commit.sha) + else + deployments.none + end + + environment_ids = deployments + .group(:environment_id) + .select(:environment_id) + + environments = project.environments.available + .where(id: environment_ids).order_by_last_deployed_at.to_a + + environments.select! do |environment| + Ability.allowed?(current_user, :read_environment, environment) + end + + if ref && commit + environments.select! do |environment| + environment.includes_commit?(commit) + end + end + + if ref && params[:recently_updated] + environments.select! do |environment| + environment.recently_updated_on_branch?(ref) + end + end + + environments + end + + private + + def ref + params[:ref].try(:to_s) + end + + def commit + params[:commit] + end +end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 6dcb624c4da..8aad39e148b 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -194,7 +194,7 @@ module CommitsHelper end end - def view_file_btn(commit_sha, diff_new_path, project) + def view_file_button(commit_sha, diff_new_path, project) link_to( namespace_project_blob_path(project.namespace, project, tree_join(commit_sha, diff_new_path)), @@ -205,6 +205,17 @@ module CommitsHelper end end + def view_on_environment_button(commit_sha, diff_new_path, environment) + return unless environment && commit_sha + + external_url = environment.external_url_for(diff_new_path, commit_sha) + return unless external_url + + link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do + icon('external-link') + end + end + def truncate_sha(sha) Commit.truncate_sha(sha) end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 3c1a1ae5933..5213ea9d02b 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -9,6 +9,7 @@ module Ci belongs_to :erased_by, class_name: 'User' has_many :deployments, as: :deployable + has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment' # The "environment" field for builds is a String, and is the unexpanded name def persisted_environment @@ -183,10 +184,6 @@ module Ci success? && !last_deployment.try(:last?) end - def last_deployment - deployments.last - end - def depends_on_builds # Get builds of the same type latest_builds = self.pipeline.builds.latest diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index fab8497ec7d..bbc358adb83 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -283,13 +283,7 @@ module Ci def ci_yaml_file return @ci_yaml_file if defined?(@ci_yaml_file) - @ci_yaml_file ||= begin - blob = project.repository.blob_at(sha, '.gitlab-ci.yml') - blob.load_all_data!(project.repository) - blob.data - rescue - nil - end + @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil end def has_yaml_errors? diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index 040e3a2884e..9cf83440784 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -18,7 +18,7 @@ module TimeTrackable validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false validate :check_negative_time_spent - has_many :timelogs, as: :trackable, dependent: :destroy + has_many :timelogs, dependent: :destroy end def spend_time(options) diff --git a/app/models/environment.rb b/app/models/environment.rb index 13c4630c565..803060b3979 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -6,7 +6,8 @@ class Environment < ActiveRecord::Base belongs_to :project, required: true, validate: true - has_many :deployments + has_many :deployments, dependent: :destroy + has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment' before_validation :nullify_external_url before_validation :generate_slug, if: ->(env) { env.slug.blank? } @@ -37,6 +38,13 @@ class Environment < ActiveRecord::Base scope :available, -> { with_state(:available) } scope :stopped, -> { with_state(:stopped) } + scope :order_by_last_deployed_at, -> do + max_deployment_id_sql = + Deployment.select(Deployment.arel_table[:id].maximum). + where(Deployment.arel_table[:environment_id].eq(arel_table[:id])). + to_sql + order(Gitlab::Database.nulls_first_order("(#{max_deployment_id_sql})", 'ASC')) + end state_machine :state, initial: :available do event :start do @@ -62,10 +70,6 @@ class Environment < ActiveRecord::Base ref.to_s == last_deployment.try(:ref) end - def last_deployment - deployments.last - end - def nullify_external_url self.external_url = nil if self.external_url.blank? end @@ -87,6 +91,10 @@ class Environment < ActiveRecord::Base last_deployment.includes_commit?(commit) end + def last_deployed_at + last_deployment.try(:created_at) + end + def update_merge_request_metrics? (environment_type || name) == "production" end @@ -171,6 +179,15 @@ class Environment < ActiveRecord::Base self.slug = slugified end + def external_url_for(path, commit_sha) + return unless self.external_url + + public_path = project.public_path_for_source_path(path, commit_sha) + return unless public_path + + [external_url, public_path].join('/') + end + private # Slugifying a name may remove the uniqueness guarantee afforded by it being diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 082adcafcc8..43085f69105 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -715,18 +715,22 @@ class MergeRequest < ActiveRecord::Base !head_pipeline || head_pipeline.success? || head_pipeline.skipped? end - def environments + def environments_for(current_user) return [] unless diff_head_commit - @environments ||= begin - target_envs = target_project.environments_for( - target_branch, commit: diff_head_commit, with_tags: true) + @environments ||= Hash.new do |h, current_user| + envs = EnvironmentsFinder.new(target_project, current_user, + ref: target_branch, commit: diff_head_commit, with_tags: true).execute - source_envs = source_project.environments_for( - source_branch, commit: diff_head_commit) if source_project + if source_project + envs.concat EnvironmentsFinder.new(source_project, current_user, + ref: source_branch, commit: diff_head_commit).execute + end - (target_envs.to_a + source_envs.to_a).uniq + h[current_user] = envs.uniq end + + @environments[current_user] end def state_human_name diff --git a/app/models/project.rb b/app/models/project.rb index 7c5fdad5122..b45f22d94d9 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1306,28 +1306,26 @@ class Project < ActiveRecord::Base Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) } end - def environments_for(ref, commit: nil, with_tags: false) - deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?' - - environment_ids = deployments - .where(deployments_query, ref.to_s) - .group(:environment_id) - .select(:environment_id) - - environments_found = environments.available - .where(id: environment_ids).to_a - - return environments_found unless commit - - environments_found.select do |environment| - environment.includes_commit?(commit) + def route_map_for(commit_sha) + @route_maps_by_commit ||= Hash.new do |h, sha| + h[sha] = begin + data = repository.route_map_for(sha) + next unless data + + Gitlab::RouteMap.new(data) + rescue Gitlab::RouteMap::FormatError + nil + end end + + @route_maps_by_commit[commit_sha] end - def environments_recently_updated_on_branch(branch) - environments_for(branch).select do |environment| - environment.recently_updated_on_branch?(branch) - end + def public_path_for_source_path(path, commit_sha) + map = route_map_for(commit_sha) + return unless map + + map.public_path_for_source_path(path) end private diff --git a/app/models/project_services/chat_slash_commands_service.rb b/app/models/project_services/chat_slash_commands_service.rb index 5eb1bd86e9d..8b5bc24fd3c 100644 --- a/app/models/project_services/chat_slash_commands_service.rb +++ b/app/models/project_services/chat_slash_commands_service.rb @@ -23,7 +23,7 @@ class ChatSlashCommandsService < Service def fields [ - { type: 'text', name: 'token', placeholder: '' } + { type: 'text', name: 'token', placeholder: 'XXxxXXxxXXxxXXxxXXxxXXxx' } ] end diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb index b0f7a42f9a3..56f42d63b2d 100644 --- a/app/models/project_services/mattermost_slash_commands_service.rb +++ b/app/models/project_services/mattermost_slash_commands_service.rb @@ -8,11 +8,11 @@ class MattermostSlashCommandsService < ChatSlashCommandsService end def title - 'Mattermost Command' + 'Mattermost slash commands' end def description - "Perform common operations on GitLab in Mattermost" + "Perform common operations in Mattermost" end def self.to_param diff --git a/app/models/project_services/slack_slash_commands_service.rb b/app/models/project_services/slack_slash_commands_service.rb index c34991e4262..2182c1c7e4b 100644 --- a/app/models/project_services/slack_slash_commands_service.rb +++ b/app/models/project_services/slack_slash_commands_service.rb @@ -2,11 +2,11 @@ class SlackSlashCommandsService < ChatSlashCommandsService include TriggersHelper def title - 'Slack Command' + 'Slack slash commands' end def description - "Perform common operations on GitLab in Slack" + "Perform common operations in Slack" end def self.to_param diff --git a/app/models/repository.rb b/app/models/repository.rb index 7cf09c52bf4..d2d92a064a4 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -464,6 +464,8 @@ class Repository unless Gitlab::Git.blank_ref?(sha) Blob.decorate(Gitlab::Git::Blob.find(self, sha, path)) end + rescue Gitlab::Git::Repository::NoRepository + nil end def blob_by_oid(oid) @@ -1160,6 +1162,14 @@ class Repository end end + def route_map_for(sha) + blob_data_at(sha, '.gitlab/route-map.yml') + end + + def gitlab_ci_yml_for(sha) + blob_data_at(sha, '.gitlab-ci.yml') + end + protected def tree_entry_at(branch_name, path) @@ -1186,6 +1196,14 @@ class Repository private + def blob_data_at(sha, path) + blob = blob_at(sha, path) + return unless blob + + blob.load_all_data!(self) + blob.data + end + def git_action(index, action) path = normalize_path(action[:file_path]) diff --git a/app/models/timelog.rb b/app/models/timelog.rb index f768c4e3da5..e166cf69703 100644 --- a/app/models/timelog.rb +++ b/app/models/timelog.rb @@ -1,6 +1,22 @@ class Timelog < ActiveRecord::Base validates :time_spent, :user, presence: true + validate :issuable_id_is_present - belongs_to :trackable, polymorphic: true + belongs_to :issue + belongs_to :merge_request belongs_to :user + + def issuable + issue || merge_request + end + + private + + def issuable_id_is_present + if issue_id && merge_request_id + errors.add(:base, 'Only Issue ID or Merge Request ID is required') + elsif issuable.nil? + errors.add(:base, 'Issue or Merge Request ID is required') + end + end end diff --git a/app/services/ci/stop_environments_service.rb b/app/services/ci/stop_environments_service.rb index a51310c3967..42c72aba7dd 100644 --- a/app/services/ci/stop_environments_service.rb +++ b/app/services/ci/stop_environments_service.rb @@ -21,8 +21,8 @@ module Ci end def environments - @environments ||= project - .environments_recently_updated_on_branch(@ref) + @environments ||= + EnvironmentsFinder.new(project, current_user, ref: @ref, recently_updated: true).execute end end end diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml index 26531f0b1a6..7b9cfbbd067 100644 --- a/app/views/projects/blob/_actions.html.haml +++ b/app/views/projects/blob/_actions.html.haml @@ -1,3 +1,6 @@ +.btn-group + = view_on_environment_button(@commit.sha, @path, @environment) if @environment + .btn-group.tree-btn-group = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), class: 'btn btn-sm', target: '_blank' diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml index 7afd3d80ef5..d5fc283aa8d 100644 --- a/app/views/projects/commit/show.html.haml +++ b/app/views/projects/commit/show.html.haml @@ -9,7 +9,7 @@ = render "ci_menu" - else .block-connector - = render "projects/diffs/diffs", diffs: @diffs + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment = render "projects/notes/notes_with_form" - if can_collaborate_with_project? - %w(revert cherry-pick).each do |type| diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml index 9c8f58d4aea..0dfc9fe20ed 100644 --- a/app/views/projects/compare/show.html.haml +++ b/app/views/projects/compare/show.html.haml @@ -8,7 +8,7 @@ - if @commits.present? = render "projects/commits/commit_list" - = render "projects/diffs/diffs", diffs: @diffs + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment - else .light-well .center diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml index 58c20e225c6..4b49bed835f 100644 --- a/app/views/projects/diffs/_diffs.html.haml +++ b/app/views/projects/diffs/_diffs.html.haml @@ -1,3 +1,4 @@ +- environment = local_assigns.fetch(:environment, nil) - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project) - diff_files = diffs.diff_files @@ -30,4 +31,4 @@ - file_hash = hexdigest(diff_file.file_path) = render 'projects/diffs/file', file_hash: file_hash, project: diffs.project, - diff_file: diff_file, diff_commit: diff_commit, blob: blob + diff_file: diff_file, diff_commit: diff_commit, blob: blob, environment: environment diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml index fc478ccc995..75885badac9 100644 --- a/app/views/projects/diffs/_file.html.haml +++ b/app/views/projects/diffs/_file.html.haml @@ -1,3 +1,4 @@ +- environment = local_assigns.fetch(:environment, nil) .diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) } .file-title = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}" @@ -13,6 +14,7 @@ = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, blob: blob, link_opts: link_opts) - = view_file_btn(diff_commit.id, diff_file.new_path, project) + = view_file_button(diff_commit.id, diff_file.new_path, project) + = view_on_environment_button(diff_commit.id, diff_file.new_path, environment) if environment = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml index 74367ab9b7b..627fc4e9671 100644 --- a/app/views/projects/merge_requests/_new_diffs.html.haml +++ b/app/views/projects/merge_requests/_new_diffs.html.haml @@ -1 +1 @@ -= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false += render "projects/diffs/diffs", diffs: @diffs, environment: @environment, show_whitespace_toggle: false diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml index 5f048d04b27..7f0913ea516 100644 --- a/app/views/projects/merge_requests/show/_diffs.html.haml +++ b/app/views/projects/merge_requests/show/_diffs.html.haml @@ -1,5 +1,5 @@ - if @merge_request_diff.collected? || @merge_request_diff.overflow? = render 'projects/merge_requests/show/versions' - = render "projects/diffs/diffs", diffs: @diffs + = render "projects/diffs/diffs", diffs: @diffs, environment: @environment - elsif @merge_request_diff.empty? .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml index 8ca4c51a064..3a323d94cc2 100644 --- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -1,16 +1,19 @@ -- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}" +- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}" -To setup this service: -%ul.list-unstyled +%p To setup this service: +%ul.list-unstyled.indent-list %li 1. - = link_to 'Enable custom slash commands', 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands' + = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do + Enable custom slash commands + = icon('external-link') on your Mattermost installation %li 2. - = link_to 'Add a slash command', 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command' - in Mattermost with these options: - + = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do + Add a slash command + = icon('external-link') + in your Mattermost team with these options: %hr .help-form @@ -83,9 +86,14 @@ To setup this service: %hr -%ul.list-unstyled +%ul.list-unstyled.indent-list %li - 3. After adding the slash command, paste the - - %strong token + 3. Paste the + %strong Token into the field below + %li + 4. Select the + %strong Active + checkbox, press + %strong Save changes + and start using GitLab inside Mattermost! diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml index c1e576b42fc..a04fd5035a6 100644 --- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml @@ -1,13 +1,16 @@ - enabled = Gitlab.config.mattermost.enabled .well - This service allows GitLab users to perform common operations on this - project by entering slash commands in Mattermost. - %br - See list of available commands in Mattermost after setting up this service, - by entering - %code /<command_trigger_word> help - + %p + This service allows users to perform common operations on this + project by entering slash commands in Mattermost. + = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do + View documentation + = icon('external-link') + %p.inline + See list of available commands in Mattermost after setting up this service, + by entering + %kbd.inline /<trigger> help - unless enabled || @service.template? = render 'projects/services/mattermost_slash_commands/detailed_help', subject: @service diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index 04b9100acc6..0d973a20d4c 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -1,21 +1,25 @@ -- pretty_name = defined?(@project) ? @project.name_with_namespace : "namespace / path" -- run_actions_text = "Perform common operations on this project: #{pretty_name}" +- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path' +- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}" .well - This service allows GitLab users to perform common operations on this - project by entering slash commands in Slack. - %br - See list of available commands in Slack after setting up this service, - by entering - %code /<command> help - %br - %br + %p + This service allows users to perform common operations on this + project by entering slash commands in Slack. + = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do + View documentation + = icon('external-link') + %p.inline + See list of available commands in Slack after setting up this service, + by entering + %kbd.inline /<command> help - unless @service.template? - To setup this service: - %ul.list-unstyled + %p To setup this service: + %ul.list-unstyled.indent-list %li 1. - = link_to 'Add a slash command', 'https://my.slack.com/services/new/slash-commands' + = link_to 'https://my.slack.com/services/new/slash-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do + Add a slash command + = icon('external-link') in your Slack team with these options: %hr @@ -82,7 +86,7 @@ %hr - %ul.list-unstyled + %ul.list-unstyled.indent-list %li 2. Paste the %strong Token diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 2ad06dcf25b..aa459f8ffb1 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -66,7 +66,7 @@ .filter-item.inline = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) .filter-item.inline.labels-filter - = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } .filter-item.inline = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do %ul diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 55360dadbc4..173fa922f56 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -101,7 +101,7 @@ .filter-item.inline = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) .filter-item.inline.labels-filter - = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } + = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } .filter-item.inline = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do %ul diff --git a/changelogs/unreleased/24716-fix-ctrl-click-links.yml b/changelogs/unreleased/24716-fix-ctrl-click-links.yml new file mode 100644 index 00000000000..13de5db5e41 --- /dev/null +++ b/changelogs/unreleased/24716-fix-ctrl-click-links.yml @@ -0,0 +1,4 @@ +--- +title: Fix Ctrl+Click support for Todos and Merge Request page tabs +merge_request: 8898 +author: diff --git a/changelogs/unreleased/26908-make-timelogs-use-foreign-keys b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys new file mode 100644 index 00000000000..0e8f7093b34 --- /dev/null +++ b/changelogs/unreleased/26908-make-timelogs-use-foreign-keys @@ -0,0 +1,4 @@ +--- +title: Refactor Timelogs structure to use foreign keys. +merge_request: 8769 +author: diff --git a/changelogs/unreleased/27352-search-label-filter-header.yml b/changelogs/unreleased/27352-search-label-filter-header.yml new file mode 100644 index 00000000000..191b530aee8 --- /dev/null +++ b/changelogs/unreleased/27352-search-label-filter-header.yml @@ -0,0 +1,4 @@ +--- +title: 27352-search-label-filter-header +merge_request: +author: diff --git a/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml new file mode 100644 index 00000000000..0531ef2c038 --- /dev/null +++ b/changelogs/unreleased/27639-emoji-panel-under-the-side-panel-in-the-merge-request.yml @@ -0,0 +1,4 @@ +--- +title: Layer award emoji dropdown over the right sidebar +merge_request: 9004 +author: diff --git a/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml new file mode 100644 index 00000000000..aa89d9f9850 --- /dev/null +++ b/changelogs/unreleased/27774-text-color-contrast-is-barely-readable-for-pipelines-visualization-graph-page-with-roboto-fonts.yml @@ -0,0 +1,4 @@ +--- +title: Give ci status text on pipeline graph a better font-weight +merge_request: +author: diff --git a/changelogs/unreleased/route-map.yml b/changelogs/unreleased/route-map.yml new file mode 100644 index 00000000000..9b6df0c54af --- /dev/null +++ b/changelogs/unreleased/route-map.yml @@ -0,0 +1,4 @@ +--- +title: Add 'View on [env]' link to blobs and individual files in diffs +merge_request: 8867 +author: diff --git a/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb new file mode 100644 index 00000000000..69bfa2d3fc4 --- /dev/null +++ b/db/migrate/20170124174637_add_foreign_keys_to_timelogs.rb @@ -0,0 +1,54 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddForeignKeysToTimelogs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + # When a migration requires downtime you **must** uncomment the following + # constant and define a short and easy to understand explanation as to why the + # migration requires downtime. + DOWNTIME_REASON = '' + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + disable_ddl_transaction! + + def up + change_table :timelogs do |t| + t.column :issue_id, :integer + t.column :merge_request_id, :integer + end + + add_concurrent_index :timelogs, :issue_id + add_concurrent_index :timelogs, :merge_request_id + + if Gitlab::Database.postgresql? + execute <<-EOF + ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_issues_issue_id" FOREIGN KEY (issue_id) REFERENCES "issues" (id) ON DELETE CASCADE NOT VALID; + ALTER TABLE timelogs ADD CONSTRAINT "fk_timelogs_merge_requests_merge_request_id" FOREIGN KEY (merge_request_id) REFERENCES "merge_requests" (id) ON DELETE CASCADE NOT VALID; + EOF + else + execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_issues_issue_id FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;" + execute "ALTER TABLE timelogs ADD CONSTRAINT fk_timelogs_merge_requests_merge_request_id FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;" + end + + Timelog.where(trackable_type: 'Issue').update_all("issue_id = trackable_id") + Timelog.where(trackable_type: 'MergeRequest').update_all("merge_request_id = trackable_id") + end + + def down + Timelog.where('issue_id IS NOT NULL').update_all("trackable_id = issue_id, trackable_type = 'Issue'") + Timelog.where('merge_request_id IS NOT NULL').update_all("trackable_id = merge_request_id, trackable_type = 'MergeRequest'") + + remove_columns :timelogs, :issue_id, :merge_request_id + end +end diff --git a/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb new file mode 100644 index 00000000000..89aa753646c --- /dev/null +++ b/db/post_migrate/20170206101007_remove_trackable_columns_from_timelogs.rb @@ -0,0 +1,23 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class RemoveTrackableColumnsFromTimelogs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + # disable_ddl_transaction! + + def change + remove_columns :timelogs, :trackable_id, :trackable_type + end +end diff --git a/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb b/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb new file mode 100644 index 00000000000..f397ef919cc --- /dev/null +++ b/db/post_migrate/20170206101030_validate_foreign_keys_on_timelogs.rb @@ -0,0 +1,32 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class ValidateForeignKeysOnTimelogs < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + # When using the methods "add_concurrent_index" or "add_column_with_default" + # you must disable the use of transactions as these methods can not run in an + # existing transaction. When using "add_concurrent_index" make sure that this + # method is the _only_ method called in the migration, any other changes + # should go in a separate migration. This ensures that upon failure _only_ the + # index creation fails and can be retried or reverted easily. + # + # To disable transactions uncomment the following line and remove these + # comments: + disable_ddl_transaction! + + def up + if Gitlab::Database.postgresql? + execute <<-EOF + ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_issues_issue_id"; + ALTER TABLE timelogs VALIDATE CONSTRAINT "fk_timelogs_merge_requests_merge_request_id"; + EOF + end + end + + def down + # noop + end +end diff --git a/db/schema.rb b/db/schema.rb index aeb0d8210f0..850772ba356 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20170206071414) do +ActiveRecord::Schema.define(version: 20170206101030) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -1151,14 +1151,15 @@ ActiveRecord::Schema.define(version: 20170206071414) do create_table "timelogs", force: :cascade do |t| t.integer "time_spent", null: false - t.integer "trackable_id" - t.string "trackable_type" t.integer "user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.integer "issue_id" + t.integer "merge_request_id" end - add_index "timelogs", ["trackable_type", "trackable_id"], name: "index_timelogs_on_trackable_type_and_trackable_id", using: :btree + add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree + add_index "timelogs", ["merge_request_id"], name: "index_timelogs_on_merge_request_id", using: :btree add_index "timelogs", ["user_id"], name: "index_timelogs_on_user_id", using: :btree create_table "todos", force: :cascade do |t| @@ -1340,6 +1341,8 @@ ActiveRecord::Schema.define(version: 20170206071414) do add_foreign_key "protected_branch_merge_access_levels", "protected_branches" add_foreign_key "protected_branch_push_access_levels", "protected_branches" add_foreign_key "subscriptions", "projects", on_delete: :cascade + add_foreign_key "timelogs", "issues", name: "fk_timelogs_issues_issue_id", on_delete: :cascade + add_foreign_key "timelogs", "merge_requests", name: "fk_timelogs_merge_requests_merge_request_id", on_delete: :cascade add_foreign_key "trending_projects", "projects", on_delete: :cascade add_foreign_key "u2f_registrations", "users" end diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 579135c2052..f00cc854b05 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -442,6 +442,45 @@ and/or `production`) you can see this information in the merge request itself. ![Environment URLs in merge request](img/environments_link_url_mr.png) +### Go directly from source files to public pages on the environment + +> Introduced in GitLab 8.17. + +To go one step further, we can specify a Route Map to get GitLab to show us "View on [environment URL]" buttons to go directly from a file to that file's representation on the deployed website. It will be exposed in a few places: + +| In the diff for a merge request, comparison or commit | In the file view | +| ------ | ------ | +| !["View on env" button in merge request diff](img/view_on_env_mr.png) | !["View on env" button in file view](img/view_on_env_blob.png) | + +To get this to work, you need to tell GitLab how the paths of files in your repository map to paths of pages on your website, using a Route Map. + +A Route Map is a file inside the repository at `.gitlab/route-map.yml`, which contains a YAML array that maps `source` paths (in the repository) to `public` paths (on the website). + +This is an example of a route map for [Middleman](https://middlemanapp.com) static websites like [http://about.gitlab.com](https://gitlab.com/gitlab-com/www-gitlab-com): + +```yaml +# Blogposts +- source: /source\/posts\/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb + public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/ + +# HTML files +- source: /source\/(.+?\.html).*/ # source/index.html.haml + public: '\1' # index.html + +# Other files +- source: /source\/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png + public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png +``` + +Mappings are defined as entries in the root YAML array, and are identified by a `-` prefix. Within an entry, we have a hash map with two keys: + +- `source`: a regular expression, starting and ending with `/`. Can include capture groups denoted by `()` that can be referred to in the `public` path. Slashes (`/`) can, but don't have to be, escaped as `\/`. +- `public`: a string, starting and ending with `'`. Can include `\N` expressions to refer to capture groups in the `source` regular expression in order of their occurence, starting with `\1`. + +The public path for a source path is determined by finding the first `source` expression that matches it, and returning the corresponding `public` path, replacing the `\N` expressions with the values of the `()` capture groups. + +In the example above, the fact that mappings are evaluated in order of their definition is used to ensure that `source/index.html.haml` will match `/source\/(.+?\.html).*/` instead of `/source\/(.*)/`, and will result in a public path of `index.html`, instead of `index.html.haml`. + --- We now have a full development cycle, where our app is tested, built, deployed diff --git a/doc/ci/img/view_on_env_blob.png b/doc/ci/img/view_on_env_blob.png Binary files differnew file mode 100644 index 00000000000..f4fe99046f0 --- /dev/null +++ b/doc/ci/img/view_on_env_blob.png diff --git a/doc/ci/img/view_on_env_mr.png b/doc/ci/img/view_on_env_mr.png Binary files differnew file mode 100644 index 00000000000..47ddb40bdc1 --- /dev/null +++ b/doc/ci/img/view_on_env_mr.png diff --git a/doc/user/project/integrations/img/mattermost_config_help.png b/doc/user/project/integrations/img/mattermost_config_help.png Binary files differindex a62e4b792f9..dd3481bc1f6 100644 --- a/doc/user/project/integrations/img/mattermost_config_help.png +++ b/doc/user/project/integrations/img/mattermost_config_help.png diff --git a/doc/user/project/integrations/img/slack_setup.png b/doc/user/project/integrations/img/slack_setup.png Binary files differindex f69817f2b78..7928fb7d495 100644 --- a/doc/user/project/integrations/img/slack_setup.png +++ b/doc/user/project/integrations/img/slack_setup.png diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb index 55b8f888d53..dc2537d36aa 100644 --- a/lib/gitlab/database.rb +++ b/lib/gitlab/database.rb @@ -35,6 +35,20 @@ module Gitlab order end + def self.nulls_first_order(field, direction = 'ASC') + order = "#{field} #{direction}" + + if Gitlab::Database.postgresql? + order << ' NULLS FIRST' + else + # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL + # columns. In the (default) ascending order, `0` comes first. + order.prepend("#{field} IS NULL, ") if direction == 'DESC' + end + + order + end + def self.random Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()" end diff --git a/lib/gitlab/route_map.rb b/lib/gitlab/route_map.rb new file mode 100644 index 00000000000..89985d90c10 --- /dev/null +++ b/lib/gitlab/route_map.rb @@ -0,0 +1,52 @@ +module Gitlab + class RouteMap + class FormatError < StandardError; end + + def initialize(data) + begin + entries = YAML.safe_load(data) + rescue + raise FormatError, 'Route map needs to be valid YAML' + end + + raise FormatError, 'Route map needs to be an array' unless entries.is_a?(Array) + + @map = entries.map { |entry| parse_entry(entry) } + end + + def public_path_for_source_path(path) + mapping = @map.find { |mapping| path =~ mapping[:source] } + return unless mapping + + path.sub(mapping[:source], mapping[:public]) + end + + private + + def parse_entry(entry) + raise FormatError, 'Route map entry needs to be a hash' unless entry.is_a?(Hash) + raise FormatError, 'Route map entry requires a source key' unless entry.has_key?('source') + raise FormatError, 'Route map entry requires a public key' unless entry.has_key?('public') + + source_regexp = entry['source'] + public_path = entry['public'] + + unless source_regexp.start_with?('/') && source_regexp.end_with?('/') + raise FormatError, 'Route map entry source needs to start and end in a slash (/)' + end + + source_regexp = source_regexp[1...-1].gsub('\/', '/') + + begin + source_regexp = Regexp.new("^#{source_regexp}$") + rescue RegexpError => e + raise FormatError, "Route map entry source needs to be a valid regular expression: #{e}" + end + + { + source: source_regexp, + public: public_path + } + end + end +end diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb index 12fc4ec4486..6f1545418eb 100644 --- a/spec/factories/timelogs.rb +++ b/spec/factories/timelogs.rb @@ -4,6 +4,6 @@ FactoryGirl.define do factory :timelog do time_spent 3600 user - association :trackable, factory: :issue + issue end end diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index 7baf7913424..7baf7913424 100644 --- a/spec/features/projects/commits/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb diff --git a/spec/features/compare_spec.rb b/spec/features/projects/compare_spec.rb index 43eb4000e58..43eb4000e58 100644 --- a/spec/features/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb index 042a1ccab51..f5adb53a2dc 100644 --- a/spec/features/projects/services/mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/mattermost_slash_command_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'Setup Mattermost slash commands', feature: true do - include WaitForAjax - let(:user) { create(:user) } let(:project) { create(:empty_project) } let(:service) { project.create_mattermost_slash_commands_service } @@ -15,11 +13,15 @@ feature 'Setup Mattermost slash commands', feature: true do visit edit_namespace_project_service_path(project.namespace, project, service) end - describe 'user visits the mattermost slash command config page', js: true do + describe 'user visits the mattermost slash command config page' do it 'shows a help message' do - wait_for_ajax + expect(page).to have_content("This service allows users to perform common") + end + + it 'shows a token placeholder' do + token_placeholder = find_field('service_token')['placeholder'] - expect(page).to have_content("This service allows GitLab users to perform common") + expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx') end it 'shows the token after saving' do @@ -64,7 +66,7 @@ feature 'Setup Mattermost slash commands', feature: true do select_element = find('select#mattermost_team_id') selected_option = select_element.find('option[selected]') - expect(select_element['disabled']).to be(true) + expect(select_element['disabled']).to eq('disabled') expect(selected_option).to have_content(team_name.to_s) end @@ -93,7 +95,7 @@ feature 'Setup Mattermost slash commands', feature: true do select_element = find('select#mattermost_team_id') selected_option = select_element.find('option[selected]') - expect(select_element['disabled']).to be(false) + expect(select_element['disabled']).to be(nil) expect(selected_option).to have_content('Select team...') # The 'Select team...' placeholder is item `0`. expect(select_element.all('option').count).to eq(3) @@ -135,6 +137,12 @@ feature 'Setup Mattermost slash commands', feature: true do expect(value).to match("api/v3/projects/#{project.id}/services/mattermost_slash_commands/trigger") end + + it 'shows a token placeholder' do + token_placeholder = find_field('service_token')['placeholder'] + + expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx') + end end end diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb index 32b32f7ae8e..db903a0c8f0 100644 --- a/spec/features/projects/services/slack_slash_command_spec.rb +++ b/spec/features/projects/services/slack_slash_command_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' feature 'Slack slash commands', feature: true do - include WaitForAjax - given(:user) { create(:user) } given(:project) { create(:project) } given(:service) { project.create_slack_slash_commands_service } @@ -10,19 +8,20 @@ feature 'Slack slash commands', feature: true do background do project.team << [user, :master] login_as(user) - end - - scenario 'user visits the slack slash command config page and shows a help message', js: true do visit edit_namespace_project_service_path(project.namespace, project, service) + end - wait_for_ajax + it 'shows a token placeholder' do + token_placeholder = find_field('service_token')['placeholder'] - expect(page).to have_content('This service allows GitLab users to perform common') + expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx') end - scenario 'shows the token after saving' do - visit edit_namespace_project_service_path(project.namespace, project, service) + it 'shows a help message' do + expect(page).to have_content('This service allows users to perform common') + end + it 'shows the token after saving' do fill_in 'service_token', with: 'token' click_on 'Save' @@ -31,9 +30,7 @@ feature 'Slack slash commands', feature: true do expect(value).to eq('token') end - scenario 'shows the correct trigger url' do - visit edit_namespace_project_service_path(project.namespace, project, service) - + it 'shows the correct trigger url' do value = find_field('url').value expect(value).to match("api/v3/projects/#{project.id}/services/slack_slash_commands/trigger") end diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb new file mode 100644 index 00000000000..ce5c5f21167 --- /dev/null +++ b/spec/features/projects/view_on_env_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +describe 'View on environment', js: true do + include WaitForAjax + + let(:branch_name) { 'feature' } + let(:file_path) { 'files/ruby/feature.rb' } + let(:project) { create(:project, :repository) } + let(:user) { project.creator } + + before do + project.add_master(user) + end + + context 'when the branch has a route map' do + let(:route_map) do + <<-MAP.strip_heredoc + - source: /files/(.*)\\..*/ + public: '\\1' + MAP + end + + before do + Files::CreateService.new( + project, + user, + start_branch: branch_name, + target_branch: branch_name, + commit_message: "Add .gitlab/route-map.yml", + file_path: '.gitlab/route-map.yml', + file_content: route_map + ).execute + + # Update the file so that we still have a commit that will have a file on the environment + Files::UpdateService.new( + project, + user, + start_branch: branch_name, + target_branch: branch_name, + commit_message: "Update feature", + file_path: file_path, + file_content: "# Noop" + ).execute + end + + context 'and an active deployment' do + let(:sha) { project.commit(branch_name).sha } + let(:environment) { create(:environment, project: project, name: 'review/feature', external_url: 'http://feature.review.example.com') } + let!(:deployment) { create(:deployment, environment: environment, ref: branch_name, sha: sha) } + + context 'when visiting the diff of a merge request for the branch' do + let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: branch_name) } + + before do + login_as(user) + + visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request) + + wait_for_ajax + end + + it 'has a "View on env" button' do + within '.diffs' do + expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + end + end + end + + context 'when visiting a comparison for the branch' do + before do + login_as(user) + + visit namespace_project_compare_path(project.namespace, project, from: 'master', to: branch_name) + + wait_for_ajax + end + + it 'has a "View on env" button' do + expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + end + end + + context 'when visiting a comparison for the commit' do + before do + login_as(user) + + visit namespace_project_compare_path(project.namespace, project, from: 'master', to: sha) + + wait_for_ajax + end + + it 'has a "View on env" button' do + expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + end + end + + context 'when visiting a blob on the branch' do + before do + login_as(user) + + visit namespace_project_blob_path(project.namespace, project, File.join(branch_name, file_path)) + + wait_for_ajax + end + + it 'has a "View on env" button' do + expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + end + end + + context 'when visiting a blob on the commit' do + before do + login_as(user) + + visit namespace_project_blob_path(project.namespace, project, File.join(sha, file_path)) + + wait_for_ajax + end + + it 'has a "View on env" button' do + expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + end + end + + context 'when visiting the commit' do + before do + login_as(user) + + visit namespace_project_commit_path(project.namespace, project, sha) + + wait_for_ajax + end + + it 'has a "View on env" button' do + expect(page).to have_link('View on feature.review.example.com', href: 'http://feature.review.example.com/ruby/feature') + end + end + end + end +end diff --git a/spec/finders/environments_finder_spec.rb b/spec/finders/environments_finder_spec.rb new file mode 100644 index 00000000000..0c063f6d5ee --- /dev/null +++ b/spec/finders/environments_finder_spec.rb @@ -0,0 +1,110 @@ +require 'spec_helper' + +describe EnvironmentsFinder do + describe '#execute' do + let(:project) { create(:project, :repository) } + let(:user) { project.creator } + let(:environment) { create(:environment, project: project) } + + before do + project.add_master(user) + end + + context 'tagged deployment' do + before do + create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id) + end + + it 'returns environment when with_tags is set' do + expect(described_class.new(project, user, ref: 'master', commit: project.commit, with_tags: true).execute) + .to contain_exactly(environment) + end + + it 'does not return environment when no with_tags is set' do + expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute) + .to be_empty + end + + it 'does not return environment when commit is not part of deployment' do + expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute) + .to be_empty + end + end + + context 'branch deployment' do + before do + create(:deployment, environment: environment, ref: 'master', sha: project.commit.id) + end + + it 'returns environment when ref is set' do + expect(described_class.new(project, user, ref: 'master', commit: project.commit).execute) + .to contain_exactly(environment) + end + + it 'does not environment when ref is different' do + expect(described_class.new(project, user, ref: 'feature', commit: project.commit).execute) + .to be_empty + end + + it 'does not return environment when commit is not part of deployment' do + expect(described_class.new(project, user, ref: 'master', commit: project.commit('feature')).execute) + .to be_empty + end + + it 'returns environment when commit constraint is not set' do + expect(described_class.new(project, user, ref: 'master').execute) + .to contain_exactly(environment) + end + end + + context 'commit deployment' do + before do + create(:deployment, environment: environment, ref: 'master', sha: project.commit.id) + end + + it 'returns environment' do + expect(described_class.new(project, user, commit: project.commit).execute) + .to contain_exactly(environment) + end + end + + context 'recently updated' do + context 'when last deployment to environment is the most recent one' do + before do + create(:deployment, environment: environment, ref: 'feature') + end + + it 'finds recently updated environment' do + expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute) + .to contain_exactly(environment) + end + end + + context 'when last deployment to environment is not the most recent' do + before do + create(:deployment, environment: environment, ref: 'feature') + create(:deployment, environment: environment, ref: 'master') + end + + it 'does not find environment' do + expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute) + .to be_empty + end + end + + context 'when there are two environments that deploy to the same branch' do + let(:second_environment) { create(:environment, project: project) } + + before do + create(:deployment, environment: environment, ref: 'feature') + create(:deployment, environment: second_environment, ref: 'feature') + end + + it 'finds both environments' do + expect(described_class.new(project, user, ref: 'feature', recently_updated: true).execute) + .to contain_exactly(environment, second_environment) + end + end + end + end +end diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb index 727c25ff529..a2c008790f9 100644 --- a/spec/helpers/commits_helper_spec.rb +++ b/spec/helpers/commits_helper_spec.rb @@ -26,4 +26,23 @@ describe CommitsHelper do not_to include('onmouseover="alert(1)"') end end + + describe '#view_on_environment_button' do + let(:project) { create(:empty_project) } + let(:environment) { create(:environment, external_url: 'http://example.com') } + let(:path) { 'source/file.html' } + let(:sha) { RepoHelpers.sample_commit.id } + + before do + allow(environment).to receive(:external_url_for).with(path, sha).and_return('http://example.com/file.html') + end + + it 'returns a link tag linking to the file in the environment' do + html = helper.view_on_environment_button(sha, path, environment) + node = Nokogiri::HTML.parse(html).at_css('a') + + expect(node[:title]).to eq('View on example.com') + expect(node[:href]).to eq('http://example.com/file.html') + end + end end diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index ff70664546d..61e83d73afb 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -86,5 +86,37 @@ require('~/lib/utils/common_utils'); expect(normalized[NGINX].nginx).toBe('ok'); }); }); + + describe('gl.utils.isMetaClick', () => { + it('should identify meta click on Windows/Linux', () => { + const e = { + metaKey: false, + ctrlKey: true, + which: 1, + }; + + expect(gl.utils.isMetaClick(e)).toBe(true); + }); + + it('should identify meta click on macOS', () => { + const e = { + metaKey: true, + ctrlKey: false, + which: 1, + }; + + expect(gl.utils.isMetaClick(e)).toBe(true); + }); + + it('should identify as meta click on middle-click or Mouse-wheel click', () => { + const e = { + metaKey: false, + ctrlKey: false, + which: 2, + }; + + expect(gl.utils.isMetaClick(e)).toBe(true); + }); + }); }); })(); diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js index d20a59df041..92a0f1c05f7 100644 --- a/spec/javascripts/merge_request_tabs_spec.js +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -61,6 +61,56 @@ require('vendor/jquery.scrollTo'); expect($('#diffs')).toHaveClass('active'); }); }); + describe('#opensInNewTab', function () { + var commitsLink; + var tabUrl; + + beforeEach(function () { + commitsLink = '.commits-tab li a'; + tabUrl = $(commitsLink).attr('href'); + + spyOn($.fn, 'attr').and.returnValue(tabUrl); + }); + it('opens page tab in a new browser tab with Ctrl+Click - Windows/Linux', function () { + spyOn(window, 'open').and.callFake(function (url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual('_blank'); + }); + + this.class.clickTab({ + metaKey: false, + ctrlKey: true, + which: 1, + stopImmediatePropagation: function () {} + }); + }); + it('opens page tab in a new browser tab with Cmd+Click - Mac', function () { + spyOn(window, 'open').and.callFake(function (url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual('_blank'); + }); + + this.class.clickTab({ + metaKey: true, + ctrlKey: false, + which: 1, + stopImmediatePropagation: function () {} + }); + }); + it('opens page tab in a new browser tab with Middle-click - Mac/PC', function () { + spyOn(window, 'open').and.callFake(function (url, name) { + expect(url).toEqual(tabUrl); + expect(name).toEqual('_blank'); + }); + + this.class.clickTab({ + metaKey: false, + ctrlKey: false, + which: 2, + stopImmediatePropagation: function () {} + }); + }); + }); describe('#setCurrentAction', function () { beforeEach(function () { diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 3031559c613..b142b3a2781 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -55,6 +55,22 @@ describe Gitlab::Database, lib: true do end end + describe '.nulls_first_order' do + context 'when using PostgreSQL' do + before { expect(described_class).to receive(:postgresql?).and_return(true) } + + it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC NULLS FIRST'} + it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column DESC NULLS FIRST'} + end + + context 'when using MySQL' do + before { expect(described_class).to receive(:postgresql?).and_return(false) } + + it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC'} + it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column IS NULL, column DESC'} + end + end + describe '#true_value' do it 'returns correct value for PostgreSQL' do expect(described_class).to receive(:postgresql?).and_return(true) diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 5231ab0ba3f..06617f3b007 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -203,5 +203,6 @@ award_emoji: priorities: - label timelogs: -- trackable +- issue +- merge_request - user diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 95b230e4f5c..c5ac702d831 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -350,8 +350,8 @@ LabelPriority: Timelog: - id - time_spent -- trackable_id -- trackable_type +- merge_request_id +- issue_id - user_id - created_at - updated_at diff --git a/spec/lib/gitlab/route_map_spec.rb b/spec/lib/gitlab/route_map_spec.rb new file mode 100644 index 00000000000..459fa8a63a9 --- /dev/null +++ b/spec/lib/gitlab/route_map_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +describe Gitlab::RouteMap, lib: true do + describe '#initialize' do + context 'when the data is not YAML' do + it 'raises an error' do + expect { described_class.new('"') }. + to raise_error(Gitlab::RouteMap::FormatError, /valid YAML/) + end + end + + context 'when the data is not a YAML array' do + it 'raises an error' do + expect { described_class.new(YAML.dump('foo')) }. + to raise_error(Gitlab::RouteMap::FormatError, /an array/) + end + end + + context 'when an entry is not a hash' do + it 'raises an error' do + expect { described_class.new(YAML.dump(['foo'])) }. + to raise_error(Gitlab::RouteMap::FormatError, /a hash/) + end + end + + context 'when an entry does not have a source key' do + it 'raises an error' do + expect { described_class.new(YAML.dump([{ 'public' => 'index.html' }])) }. + to raise_error(Gitlab::RouteMap::FormatError, /source key/) + end + end + + context 'when an entry does not have a public key' do + it 'raises an error' do + expect { described_class.new(YAML.dump([{ 'source' => '/index\.html/' }])) }. + to raise_error(Gitlab::RouteMap::FormatError, /public key/) + end + end + + context 'when an entry source does not start and end with a slash' do + it 'raises an error' do + expect { described_class.new(YAML.dump([{ 'source' => 'index.html', 'public' => 'index.html' }])) }. + to raise_error(Gitlab::RouteMap::FormatError, /a slash/) + end + end + + context 'when an entry source is not a valid regex' do + it 'raises an error' do + expect { described_class.new(YAML.dump([{ 'source' => '/[/', 'public' => 'index.html' }])) }. + to raise_error(Gitlab::RouteMap::FormatError, /regular expression/) + end + end + + context 'when all is good' do + it 'returns a route map' do + route_map = described_class.new(YAML.dump([{ 'source' => '/index\.html/', 'public' => 'index.html' }])) + + expect(route_map.public_path_for_source_path('index.html')).to eq('index.html') + end + end + end + + describe '#public_path_for_source_path' do + subject do + described_class.new(<<-'MAP'.strip_heredoc) + # Blogposts + - source: /source/posts/([0-9]{4})-([0-9]{2})-([0-9]{2})-(.+?)\..*/ # source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb + public: '\1/\2/\3/\4/' # 2017/01/30/around-the-world-in-6-releases/ + + # HTML files + - source: /source/(.+?\.html).*/ # source/index.html.haml + public: '\1' # index.html + + # Other files + - source: /source/(.*)/ # source/images/blogimages/around-the-world-in-6-releases-cover.png + public: '\1' # images/blogimages/around-the-world-in-6-releases-cover.png + MAP + end + + it 'returns the public path for a provided source path' do + expect(subject.public_path_for_source_path('source/posts/2017-01-30-around-the-world-in-6-releases.html.md.erb')).to eq('2017/01/30/around-the-world-in-6-releases/') + + expect(subject.public_path_for_source_path('source/index.html.haml')).to eq('index.html') + + expect(subject.public_path_for_source_path('source/images/blogimages/around-the-world-in-6-releases-cover.png')).to eq('images/blogimages/around-the-world-in-6-releases-cover.png') + + expect(subject.public_path_for_source_path('.gitlab/route-map.yml')).to be_nil + end + end +end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 8b57d8600fe..960f29f3805 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -7,8 +7,6 @@ describe Environment, models: true do it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:deployments) } - it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) } - it { is_expected.to delegate_method(:stop_action).to(:last_deployment) } it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) } @@ -22,6 +20,20 @@ describe Environment, models: true do it { is_expected.to validate_length_of(:external_url).is_at_most(255) } it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) } + describe '.order_by_last_deployed_at' do + let(:project) { create(:project) } + let!(:environment1) { create(:environment, project: project) } + let!(:environment2) { create(:environment, project: project) } + let!(:environment3) { create(:environment, project: project) } + let!(:deployment1) { create(:deployment, environment: environment1) } + let!(:deployment2) { create(:deployment, environment: environment2) } + let!(:deployment3) { create(:deployment, environment: environment1) } + + it 'returns the environments in order of having been last deployed' do + expect(project.environments.order_by_last_deployed_at.to_a).to eq([environment3, environment2, environment1]) + end + end + describe '#nullify_external_url' do it 'replaces a blank url with nil' do env = build(:environment, external_url: "") @@ -323,4 +335,33 @@ describe Environment, models: true do end end end + + describe '#external_url_for' do + let(:source_path) { 'source/file.html' } + let(:sha) { RepoHelpers.sample_commit.id } + + before do + environment.external_url = 'http://example.com' + end + + context 'when the public path is not known' do + before do + allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return(nil) + end + + it 'returns nil' do + expect(environment.external_url_for(source_path, sha)).to be_nil + end + end + + context 'when the public path is known' do + before do + allow(project).to receive(:public_path_for_source_path).with(source_path, sha).and_return('file.html') + end + + it 'returns the full external URL' do + expect(environment.external_url_for(source_path, sha)).to eq('http://example.com/file.html') + end + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 32ed1e96749..e1e99300489 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -1005,10 +1005,16 @@ describe MergeRequest, models: true do end end - describe "#environments" do + describe "#environments_for" do let(:project) { create(:project, :repository) } + let(:user) { project.creator } let(:merge_request) { create(:merge_request, source_project: project) } + before do + merge_request.source_project.add_master(user) + merge_request.target_project.add_master(user) + end + context 'with multiple environments' do let(:environments) { create_list(:environment, 3, project: project) } @@ -1018,7 +1024,7 @@ describe MergeRequest, models: true do end it 'selects deployed environments' do - expect(merge_request.environments).to contain_exactly(environments.first) + expect(merge_request.environments_for(user)).to contain_exactly(environments.first) end end @@ -1042,7 +1048,7 @@ describe MergeRequest, models: true do end it 'selects deployed environments' do - expect(merge_request.environments).to contain_exactly(source_environment) + expect(merge_request.environments_for(user)).to contain_exactly(source_environment) end context 'with environments on target project' do @@ -1053,7 +1059,7 @@ describe MergeRequest, models: true do end it 'selects deployed environments' do - expect(merge_request.environments).to contain_exactly(source_environment, target_environment) + expect(merge_request.environments_for(user)).to contain_exactly(source_environment, target_environment) end end end @@ -1064,7 +1070,7 @@ describe MergeRequest, models: true do end it 'returns an empty array' do - expect(merge_request.environments).to be_empty + expect(merge_request.environments_for(user)).to be_empty end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index d7e6da02261..2129bcbd74d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1716,100 +1716,6 @@ describe Project, models: true do end end - describe '#environments_for' do - let(:project) { create(:project, :repository) } - let(:environment) { create(:environment, project: project) } - - context 'tagged deployment' do - before do - create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id) - end - - it 'returns environment when with_tags is set' do - expect(project.environments_for('master', commit: project.commit, with_tags: true)) - .to contain_exactly(environment) - end - - it 'does not return environment when no with_tags is set' do - expect(project.environments_for('master', commit: project.commit)) - .to be_empty - end - - it 'does not return environment when commit is not part of deployment' do - expect(project.environments_for('master', commit: project.commit('feature'))) - .to be_empty - end - end - - context 'branch deployment' do - before do - create(:deployment, environment: environment, ref: 'master', sha: project.commit.id) - end - - it 'returns environment when ref is set' do - expect(project.environments_for('master', commit: project.commit)) - .to contain_exactly(environment) - end - - it 'does not environment when ref is different' do - expect(project.environments_for('feature', commit: project.commit)) - .to be_empty - end - - it 'does not return environment when commit is not part of deployment' do - expect(project.environments_for('master', commit: project.commit('feature'))) - .to be_empty - end - - it 'returns environment when commit constraint is not set' do - expect(project.environments_for('master')) - .to contain_exactly(environment) - end - end - end - - describe '#environments_recently_updated_on_branch' do - let(:project) { create(:project, :repository) } - let(:environment) { create(:environment, project: project) } - - context 'when last deployment to environment is the most recent one' do - before do - create(:deployment, environment: environment, ref: 'feature') - end - - it 'finds recently updated environment' do - expect(project.environments_recently_updated_on_branch('feature')) - .to contain_exactly(environment) - end - end - - context 'when last deployment to environment is not the most recent' do - before do - create(:deployment, environment: environment, ref: 'feature') - create(:deployment, environment: environment, ref: 'master') - end - - it 'does not find environment' do - expect(project.environments_recently_updated_on_branch('feature')) - .to be_empty - end - end - - context 'when there are two environments that deploy to the same branch' do - let(:second_environment) { create(:environment, project: project) } - - before do - create(:deployment, environment: environment, ref: 'feature') - create(:deployment, environment: second_environment, ref: 'feature') - end - - it 'finds both environments' do - expect(project.environments_recently_updated_on_branch('feature')) - .to contain_exactly(environment, second_environment) - end - end - end - describe '#deployment_variables' do context 'when project has no deployment service' do let(:project) { create(:empty_project) } @@ -1858,6 +1764,82 @@ describe Project, models: true do it { expect(Project.inside_path(path)).to eq([project1]) } end + describe '#route_map_for' do + let(:project) { create(:project) } + let(:route_map) do + <<-MAP.strip_heredoc + - source: /source/(.*)/ + public: '\\1' + MAP + end + + before do + project.repository.commit_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false) + end + + context 'when there is a .gitlab/route-map.yml at the commit' do + context 'when the route map is valid' do + it 'returns a route map' do + map = project.route_map_for(project.commit.sha) + expect(map).to be_a_kind_of(Gitlab::RouteMap) + end + end + + context 'when the route map is invalid' do + let(:route_map) { 'INVALID' } + + it 'returns nil' do + expect(project.route_map_for(project.commit.sha)).to be_nil + end + end + end + + context 'when there is no .gitlab/route-map.yml at the commit' do + it 'returns nil' do + expect(project.route_map_for(project.commit.parent.sha)).to be_nil + end + end + end + + describe '#public_path_for_source_path' do + let(:project) { create(:project) } + let(:route_map) do + Gitlab::RouteMap.new(<<-MAP.strip_heredoc) + - source: /source/(.*)/ + public: '\\1' + MAP + end + let(:sha) { project.commit.id } + + context 'when there is a route map' do + before do + allow(project).to receive(:route_map_for).with(sha).and_return(route_map) + end + + context 'when the source path is mapped' do + it 'returns the public path' do + expect(project.public_path_for_source_path('source/file.html', sha)).to eq('file.html') + end + end + + context 'when the source path is not mapped' do + it 'returns nil' do + expect(project.public_path_for_source_path('file.html', sha)).to be_nil + end + end + end + + context 'when there is no route map' do + before do + allow(project).to receive(:route_map_for).with(sha).and_return(nil) + end + + it 'returns nil' do + expect(project.public_path_for_source_path('source/file.html', sha)).to be_nil + end + end + end + def enable_lfs allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 53b98ba05f8..9bfa6409607 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1782,4 +1782,40 @@ describe Repository, models: true do repository.refresh_method_caches(%i(readme license)) end end + + describe '#gitlab_ci_yml_for' do + before do + repository.commit_file(User.last, '.gitlab-ci.yml', 'CONTENT', message: 'Add .gitlab-ci.yml', branch_name: 'master', update: false) + end + + context 'when there is a .gitlab-ci.yml at the commit' do + it 'returns the content' do + expect(repository.gitlab_ci_yml_for(repository.commit.sha)).to eq('CONTENT') + end + end + + context 'when there is no .gitlab-ci.yml at the commit' do + it 'returns nil' do + expect(repository.gitlab_ci_yml_for(repository.commit.parent.sha)).to be_nil + end + end + end + + describe '#route_map_for' do + before do + repository.commit_file(User.last, '.gitlab/route-map.yml', 'CONTENT', message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false) + end + + context 'when there is a .gitlab/route-map.yml at the commit' do + it 'returns the content' do + expect(repository.route_map_for(repository.commit.sha)).to eq('CONTENT') + end + end + + context 'when there is no .gitlab/route-map.yml at the commit' do + it 'returns nil' do + expect(repository.route_map_for(repository.commit.parent.sha)).to be_nil + end + end + end end diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb index f08935b6425..ebc694213b6 100644 --- a/spec/models/timelog_spec.rb +++ b/spec/models/timelog_spec.rb @@ -2,9 +2,37 @@ require 'rails_helper' RSpec.describe Timelog, type: :model do subject { build(:timelog) } + let(:issue) { create(:issue) } + let(:merge_request) { create(:merge_request) } it { is_expected.to be_valid } it { is_expected.to validate_presence_of(:time_spent) } it { is_expected.to validate_presence_of(:user) } + + describe 'Issuable validation' do + it 'is invalid if issue_id and merge_request_id are missing' do + subject.attributes = { issue: nil, merge_request: nil } + + expect(subject).to be_invalid + end + + it 'is invalid if issue_id and merge_request_id are set' do + subject.attributes = { issue: issue, merge_request: merge_request } + + expect(subject).to be_invalid + end + + it 'is valid if only issue_id is set' do + subject.attributes = { issue: issue, merge_request: nil } + + expect(subject).to be_valid + end + + it 'is valid if only merge_request_id is set' do + subject.attributes = { merge_request: merge_request, issue: nil } + + expect(subject).to be_valid + end + end end |