Commit 58eae096 authored by Robert Speicher's avatar Robert Speicher

Merge branch 'route-map' into 'master'

Add 'View on [env]' link to blobs and individual files in diffs

See merge request !8867
parents d3aaa1a2 0fdf54dd
...@@ -30,6 +30,8 @@ class Projects::BlobController < Projects::ApplicationController ...@@ -30,6 +30,8 @@ class Projects::BlobController < Projects::ApplicationController
end end
def show def show
environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit }
@environment = EnvironmentsFinder.new(@project, current_user, environment_params).execute.last
end end
def edit def edit
......
...@@ -94,6 +94,8 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -94,6 +94,8 @@ class Projects::CommitController < Projects::ApplicationController
@diffs = commit.diffs(opts) @diffs = commit.diffs(opts)
@notes_count = commit.notes.count @notes_count = commit.notes.count
@environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last
end end
def define_note_vars def define_note_vars
......
...@@ -57,6 +57,9 @@ class Projects::CompareController < Projects::ApplicationController ...@@ -57,6 +57,9 @@ class Projects::CompareController < Projects::ApplicationController
@diffs = @compare.diffs(diff_options) @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 @diff_notes_disabled = true
@grouped_diff_discussions = {} @grouped_diff_discussions = {}
end end
......
...@@ -10,7 +10,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -10,7 +10,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
def index def index
@scope = params[:scope] @scope = params[:scope]
@environments = project.environments @environments = project.environments.includes(:last_deployment)
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -103,6 +103,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -103,6 +103,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
end end
@environment = @merge_request.environments_for(current_user).last
respond_to do |format| respond_to do |format|
format.html { define_discussion_vars } format.html { define_discussion_vars }
format.json do format.json do
...@@ -248,7 +250,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -248,7 +250,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
@diff_notes_disabled = true @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 end
end end
...@@ -447,9 +451,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -447,9 +451,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def ci_environments_status def ci_environments_status
environments = environments =
begin begin
@merge_request.environments.map do |environment| @merge_request.environments_for(current_user).map do |environment|
next unless can?(current_user, :read_environment, environment)
project = environment.project project = environment.project
deployment = environment.first_deployment_for(@merge_request.diff_head_commit) deployment = environment.first_deployment_for(@merge_request.diff_head_commit)
......
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
...@@ -194,7 +194,7 @@ module CommitsHelper ...@@ -194,7 +194,7 @@ module CommitsHelper
end end
end end
def view_file_btn(commit_sha, diff_new_path, project) def view_file_button(commit_sha, diff_new_path, project)
link_to( link_to(
namespace_project_blob_path(project.namespace, project, namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff_new_path)), tree_join(commit_sha, diff_new_path)),
...@@ -205,6 +205,17 @@ module CommitsHelper ...@@ -205,6 +205,17 @@ module CommitsHelper
end end
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) def truncate_sha(sha)
Commit.truncate_sha(sha) Commit.truncate_sha(sha)
end end
......
...@@ -9,6 +9,7 @@ module Ci ...@@ -9,6 +9,7 @@ module Ci
belongs_to :erased_by, class_name: 'User' belongs_to :erased_by, class_name: 'User'
has_many :deployments, as: :deployable 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 # The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment def persisted_environment
...@@ -183,10 +184,6 @@ module Ci ...@@ -183,10 +184,6 @@ module Ci
success? && !last_deployment.try(:last?) success? && !last_deployment.try(:last?)
end end
def last_deployment
deployments.last
end
def depends_on_builds def depends_on_builds
# Get builds of the same type # Get builds of the same type
latest_builds = self.pipeline.builds.latest latest_builds = self.pipeline.builds.latest
......
...@@ -283,13 +283,7 @@ module Ci ...@@ -283,13 +283,7 @@ module Ci
def ci_yaml_file def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file) return @ci_yaml_file if defined?(@ci_yaml_file)
@ci_yaml_file ||= begin @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
blob.load_all_data!(project.repository)
blob.data
rescue
nil
end
end end
def has_yaml_errors? def has_yaml_errors?
......
...@@ -6,7 +6,8 @@ class Environment < ActiveRecord::Base ...@@ -6,7 +6,8 @@ class Environment < ActiveRecord::Base
belongs_to :project, required: true, validate: true 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 :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? } before_validation :generate_slug, if: ->(env) { env.slug.blank? }
...@@ -37,6 +38,13 @@ class Environment < ActiveRecord::Base ...@@ -37,6 +38,13 @@ class Environment < ActiveRecord::Base
scope :available, -> { with_state(:available) } scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) } 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 state_machine :state, initial: :available do
event :start do event :start do
...@@ -62,10 +70,6 @@ class Environment < ActiveRecord::Base ...@@ -62,10 +70,6 @@ class Environment < ActiveRecord::Base
ref.to_s == last_deployment.try(:ref) ref.to_s == last_deployment.try(:ref)
end end
def last_deployment
deployments.last
end
def nullify_external_url def nullify_external_url
self.external_url = nil if self.external_url.blank? self.external_url = nil if self.external_url.blank?
end end
...@@ -87,6 +91,10 @@ class Environment < ActiveRecord::Base ...@@ -87,6 +91,10 @@ class Environment < ActiveRecord::Base
last_deployment.includes_commit?(commit) last_deployment.includes_commit?(commit)
end end
def last_deployed_at
last_deployment.try(:created_at)
end
def update_merge_request_metrics? def update_merge_request_metrics?
(environment_type || name) == "production" (environment_type || name) == "production"
end end
...@@ -171,6 +179,15 @@ class Environment < ActiveRecord::Base ...@@ -171,6 +179,15 @@ class Environment < ActiveRecord::Base
self.slug = slugified self.slug = slugified
end 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 private
# Slugifying a name may remove the uniqueness guarantee afforded by it being # Slugifying a name may remove the uniqueness guarantee afforded by it being
......
...@@ -715,18 +715,22 @@ class MergeRequest < ActiveRecord::Base ...@@ -715,18 +715,22 @@ class MergeRequest < ActiveRecord::Base
!head_pipeline || head_pipeline.success? || head_pipeline.skipped? !head_pipeline || head_pipeline.success? || head_pipeline.skipped?
end end
def environments def environments_for(current_user)
return [] unless diff_head_commit return [] unless diff_head_commit
@environments ||= begin @environments ||= Hash.new do |h, current_user|
target_envs = target_project.environments_for( envs = EnvironmentsFinder.new(target_project, current_user,
target_branch, commit: diff_head_commit, with_tags: true) ref: target_branch, commit: diff_head_commit, with_tags: true).execute
source_envs = source_project.environments_for( if source_project
source_branch, commit: diff_head_commit) 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 end
@environments[current_user]
end end
def state_human_name def state_human_name
......
...@@ -1306,28 +1306,26 @@ class Project < ActiveRecord::Base ...@@ -1306,28 +1306,26 @@ class Project < ActiveRecord::Base
Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) } Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) }
end end
def environments_for(ref, commit: nil, with_tags: false) def route_map_for(commit_sha)
deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?' @route_maps_by_commit ||= Hash.new do |h, sha|
h[sha] = begin
environment_ids = deployments data = repository.route_map_for(sha)
.where(deployments_query, ref.to_s) next unless data
.group(:environment_id)
.select(:environment_id) Gitlab::RouteMap.new(data)
rescue Gitlab::RouteMap::FormatError
environments_found = environments.available nil
.where(id: environment_ids).to_a
return environments_found unless commit
environments_found.select do |environment|
environment.includes_commit?(commit)
end end
end end
def environments_recently_updated_on_branch(branch) @route_maps_by_commit[commit_sha]
environments_for(branch).select do |environment|
environment.recently_updated_on_branch?(branch)
end 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 end
private private
......
...@@ -464,6 +464,8 @@ class Repository ...@@ -464,6 +464,8 @@ class Repository
unless Gitlab::Git.blank_ref?(sha) unless Gitlab::Git.blank_ref?(sha)
Blob.decorate(Gitlab::Git::Blob.find(self, sha, path)) Blob.decorate(Gitlab::Git::Blob.find(self, sha, path))
end end
rescue Gitlab::Git::Repository::NoRepository
nil
end end
def blob_by_oid(oid) def blob_by_oid(oid)
...@@ -1160,6 +1162,14 @@ class Repository ...@@ -1160,6 +1162,14 @@ class Repository
end end
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 protected
def tree_entry_at(branch_name, path) def tree_entry_at(branch_name, path)
...@@ -1186,6 +1196,14 @@ class Repository ...@@ -1186,6 +1196,14 @@ class Repository
private 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) def git_action(index, action)
path = normalize_path(action[:file_path]) path = normalize_path(action[:file_path])
......
...@@ -21,8 +21,8 @@ module Ci ...@@ -21,8 +21,8 @@ module Ci
end end
def environments def environments
@environments ||= project @environments ||=
.environments_recently_updated_on_branch(@ref) EnvironmentsFinder.new(project, current_user, ref: @ref, recently_updated: true).execute
end end
end end
end end
.btn-group
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group.tree-btn-group .btn-group.tree-btn-group
= link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id), = link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id),
class: 'btn btn-sm', target: '_blank' class: 'btn btn-sm', target: '_blank'
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= render "ci_menu" = render "ci_menu"
- else - else
.block-connector .block-connector
= render "projects/diffs/diffs", diffs: @diffs = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
= render "projects/notes/notes_with_form" = render "projects/notes/notes_with_form"
- if can_collaborate_with_project? - if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type| - %w(revert cherry-pick).each do |type|
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- if @commits.present? - if @commits.present?
= render "projects/commits/commit_list" = render "projects/commits/commit_list"
= render "projects/diffs/diffs", diffs: @diffs = render "projects/diffs/diffs", diffs: @diffs, environment: @environment
- else - else
.light-well .light-well
.center .center
......
- environment = local_assigns.fetch(:environment, nil)
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true) - show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project) - can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files - diff_files = diffs.diff_files
...@@ -30,4 +31,4 @@ ...@@ -30,4 +31,4 @@
- file_hash = hexdigest(diff_file.file_path) - file_hash = hexdigest(diff_file.file_path)
= render 'projects/diffs/file', file_hash: file_hash, project: diffs.project, = 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
- 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) } .diff-file.file-holder{ id: file_hash, data: diff_file_html_data(project, diff_file.file_path, diff_commit.id) }
.file-title .file-title
= render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}" = render "projects/diffs/file_header", diff_file: diff_file, blob: blob, diff_commit: diff_commit, project: project, url: "##{file_hash}"
...@@ -13,6 +14,7 @@ ...@@ -13,6 +14,7 @@
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path, = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts) 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 = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false = render "projects/diffs/diffs", diffs: @diffs, environment: @environment, show_whitespace_toggle: false
- if @merge_request_diff.collected? || @merge_request_diff.overflow? - if @merge_request_diff.collected? || @merge_request_diff.overflow?
= render 'projects/merge_requests/show/versions' = 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? - elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
---
title: Add 'View on [env]' link to blobs and individual files in diffs
merge_request: 8867
author:
...@@ -442,6 +442,45 @@ and/or `production`) you can see this information in the merge request itself. ...@@ -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) ![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 We now have a full development cycle, where our app is tested, built, deployed
......
...@@ -35,6 +35,20 @@ module Gitlab ...@@ -35,6 +35,20 @@ module Gitlab
order order
end 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 def self.random
Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()" Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()"
end end
......
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
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
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
...@@ -26,4 +26,23 @@ describe CommitsHelper do ...@@ -26,4 +26,23 @@ describe CommitsHelper do
not_to include('onmouseover="alert(1)"') not_to include('onmouseover="alert(1)"')
end end
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 end
...@@ -55,6 +55,22 @@ describe Gitlab::Database, lib: true do ...@@ -55,6 +55,22 @@ describe Gitlab::Database, lib: true do
end end
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 describe '#true_value' do
it 'returns correct value for PostgreSQL' do it 'returns correct value for PostgreSQL' do
expect(described_class).to receive(:postgresql?).and_return(true) expect(described_class).to receive(:postgresql?).and_return(true)
......
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
...@@ -7,8 +7,6 @@ describe Environment, models: true do ...@@ -7,8 +7,6 @@ describe Environment, models: true do
it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:deployments) } 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(:stop_action).to(:last_deployment) }
it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) } it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) }
...@@ -22,6 +20,20 @@ describe Environment, models: true do ...@@ -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_length_of(:external_url).is_at_most(255) }
it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) } 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 describe '#nullify_external_url' do
it 'replaces a blank url with nil' do it 'replaces a blank url with nil' do
env = build(:environment, external_url: "") env = build(:environment, external_url: "")
...@@ -323,4 +335,33 @@ describe Environment, models: true do ...@@ -323,4 +335,33 @@ describe Environment, models: true do
end end
end 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 end
...@@ -1005,10 +1005,16 @@ describe MergeRequest, models: true do ...@@ -1005,10 +1005,16 @@ describe MergeRequest, models: true do
end end
end end
describe "#environments" do describe "#environments_for" do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project) } 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 context 'with multiple environments' do
let(:environments) { create_list(:environment, 3, project: project) } let(:environments) { create_list(:environment, 3, project: project) }
...@@ -1018,7 +1024,7 @@ describe MergeRequest, models: true do ...@@ -1018,7 +1024,7 @@ describe MergeRequest, models: true do
end end
it 'selects deployed environments' do 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
end end
...@@ -1042,7 +1048,7 @@ describe MergeRequest, models: true do ...@@ -1042,7 +1048,7 @@ describe MergeRequest, models: true do
end end
it 'selects deployed environments' do 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 end
context 'with environments on target project' do context 'with environments on target project' do
...@@ -1053,7 +1059,7 @@ describe MergeRequest, models: true do ...@@ -1053,7 +1059,7 @@ describe MergeRequest, models: true do
end end
it 'selects deployed environments' do 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 end
end end
...@@ -1064,7 +1070,7 @@ describe MergeRequest, models: true do ...@@ -1064,7 +1070,7 @@ describe MergeRequest, models: true do
end end
it 'returns an empty array' do 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 end
end end
......
...@@ -1716,146 +1716,128 @@ describe Project, models: true do ...@@ -1716,146 +1716,128 @@ describe Project, models: true do
end end
end end
describe '#environments_for' do describe '#deployment_variables' do
let(:project) { create(:project, :repository) } context 'when project has no deployment service' do
let(:environment) { create(:environment, project: project) } let(:project) { create(:empty_project) }
context 'tagged deployment' do it 'returns an empty array' do
before do expect(project.deployment_variables).to eq []
create(:deployment, environment: environment, ref: '1.0', tag: true, sha: project.commit.id)
end 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 end
it 'does not return environment when no with_tags is set' do context 'when project has a deployment service' do
expect(project.environments_for('master', commit: project.commit)) let(:project) { create(:kubernetes_project) }
.to be_empty
end
it 'does not return environment when commit is not part of deployment' do it 'returns variables from this service' do
expect(project.environments_for('master', commit: project.commit('feature'))) expect(project.deployment_variables).to include(
.to be_empty { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
)
end end
end end
context 'branch deployment' do
before do
create(:deployment, environment: environment, ref: 'master', sha: project.commit.id)
end end
it 'returns environment when ref is set' do describe '#update_project_statistics' do
expect(project.environments_for('master', commit: project.commit)) let(:project) { create(:empty_project) }
.to contain_exactly(environment)
end
it 'does not environment when ref is different' do it "is called after creation" do
expect(project.environments_for('feature', commit: project.commit)) expect(project.statistics).to be_a ProjectStatistics
.to be_empty expect(project.statistics).to be_persisted
end end
it 'does not return environment when commit is not part of deployment' do it "copies the namespace_id" do
expect(project.environments_for('master', commit: project.commit('feature'))) expect(project.statistics.namespace_id).to eq project.namespace_id
.to be_empty
end end
it 'returns environment when commit constraint is not set' do it "updates the namespace_id when changed" do
expect(project.environments_for('master')) namespace = create(:namespace)
.to contain_exactly(environment) project.update(namespace: namespace)
end
expect(project.statistics.namespace_id).to eq namespace.id
end end
end end
describe '#environments_recently_updated_on_branch' do describe 'inside_path' do
let(:project) { create(:project, :repository) } let!(:project1) { create(:empty_project) }
let(:environment) { create(:environment, project: project) } let!(:project2) { create(:empty_project) }
let!(:path) { project1.namespace.path }
context 'when last deployment to environment is the most recent one' do it { expect(Project.inside_path(path)).to eq([project1]) }
before do
create(:deployment, environment: environment, ref: 'feature')
end end
it 'finds recently updated environment' do describe '#route_map_for' do
expect(project.environments_recently_updated_on_branch('feature')) let(:project) { create(:project) }
.to contain_exactly(environment) let(:route_map) do
end <<-MAP.strip_heredoc
- source: /source/(.*)/
public: '\\1'
MAP
end end
context 'when last deployment to environment is not the most recent' do
before do before do
create(:deployment, environment: environment, ref: 'feature') project.repository.commit_file(User.last, '.gitlab/route-map.yml', route_map, message: 'Add .gitlab/route-map.yml', branch_name: 'master', update: false)
create(:deployment, environment: environment, ref: 'master')
end end
it 'does not find environment' do context 'when there is a .gitlab/route-map.yml at the commit' do
expect(project.environments_recently_updated_on_branch('feature')) context 'when the route map is valid' do
.to be_empty 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
end end
context 'when there are two environments that deploy to the same branch' do context 'when the route map is invalid' do
let(:second_environment) { create(:environment, project: project) } let(:route_map) { 'INVALID' }
before do it 'returns nil' do
create(:deployment, environment: environment, ref: 'feature') expect(project.route_map_for(project.commit.sha)).to be_nil
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 end
end end
describe '#deployment_variables' do context 'when there is no .gitlab/route-map.yml at the commit' do
context 'when project has no deployment service' do it 'returns nil' do
let(:project) { create(:empty_project) } expect(project.route_map_for(project.commit.parent.sha)).to be_nil
end
it 'returns an empty array' do
expect(project.deployment_variables).to eq []
end end
end end
context 'when project has a deployment service' do describe '#public_path_for_source_path' do
let(:project) { create(:kubernetes_project) } 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 }
it 'returns variables from this service' do context 'when there is a route map' do
expect(project.deployment_variables).to include( before do
{ key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false } allow(project).to receive(:route_map_for).with(sha).and_return(route_map)
)
end 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
end end
describe '#update_project_statistics' do context 'when the source path is not mapped' do
let(:project) { create(:empty_project) } it 'returns nil' do
expect(project.public_path_for_source_path('file.html', sha)).to be_nil
it "is called after creation" do end
expect(project.statistics).to be_a ProjectStatistics
expect(project.statistics).to be_persisted
end end
it "copies the namespace_id" do
expect(project.statistics.namespace_id).to eq project.namespace_id
end end
it "updates the namespace_id when changed" do context 'when there is no route map' do
namespace = create(:namespace) before do
project.update(namespace: namespace) allow(project).to receive(:route_map_for).with(sha).and_return(nil)
end
expect(project.statistics.namespace_id).to eq namespace.id it 'returns nil' do
expect(project.public_path_for_source_path('source/file.html', sha)).to be_nil
end end
end end
describe 'inside_path' do
let!(:project1) { create(:empty_project) }
let!(:project2) { create(:empty_project) }
let!(:path) { project1.namespace.path }
it { expect(Project.inside_path(path)).to eq([project1]) }
end end
def enable_lfs def enable_lfs
......
...@@ -1782,4 +1782,40 @@ describe Repository, models: true do ...@@ -1782,4 +1782,40 @@ describe Repository, models: true do
repository.refresh_method_caches(%i(readme license)) repository.refresh_method_caches(%i(readme license))
end end
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 end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment