Commit 8f4d45cd authored by Felipe Artur's avatar Felipe Artur

Merge CE/master into EE/Master

parents 1d5c7429 0ec91454
......@@ -163,6 +163,11 @@ const ShortcutsBlob = require('./shortcuts_blob');
new ZenMode();
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:commit:pipelines':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
}).bindEvents();
break;
case 'projects:commits:show':
case 'projects:activity':
shortcut_handler = new ShortcutsNavigation();
......
......@@ -78,8 +78,8 @@ require('../window')(function(w){
},
destroy: function() {
if (this.listTemplate) {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate;
}
}
......
......@@ -50,6 +50,8 @@ require('./smart_interval');
this.getCIStatus(false);
this.retrieveSuccessIcon();
this.initMiniPipelineGraph();
this.ciStatusInterval = new global.SmartInterval({
callback: this.getCIStatus.bind(this, true),
startingInterval: 10000,
......@@ -65,6 +67,7 @@ require('./smart_interval');
incrementByFactorOf: 15000,
immediateExecution: true,
});
notifyPermissions();
}
......@@ -253,17 +256,20 @@ require('./smart_interval');
case "failed":
case "canceled":
case "not_found":
return this.setMergeButtonClass('btn-danger');
this.setMergeButtonClass('btn-danger');
break;
case "running":
return this.setMergeButtonClass('btn-info');
this.setMergeButtonClass('btn-info');
break;
case "success":
case "success_with_warnings":
return this.setMergeButtonClass('btn-create');
this.setMergeButtonClass('btn-create');
}
} else {
$('.ci_widget.ci-error').show();
return this.setMergeButtonClass('btn-danger');
this.setMergeButtonClass('btn-danger');
}
this.initMiniPipelineGraph();
};
MergeRequestWidget.prototype.showCICoverage = function(coverage) {
......@@ -286,6 +292,12 @@ require('./smart_interval');
$('.js-commit-link').text(`#${id}`).attr('href', [commitsUrl, id].join('/'));
};
MergeRequestWidget.prototype.initMiniPipelineGraph = function() {
new gl.MiniPipelineGraph({
container: '.js-pipeline-inline-mr-widget-graph:visible',
}).bindEvents();
};
return MergeRequestWidget;
})();
})(window.gl || (window.gl = {}));
......@@ -21,8 +21,6 @@
this.container = opts.container || '';
this.dropdownListSelector = '.js-builds-dropdown-container';
this.getBuildsList = this.getBuildsList.bind(this);
this.bindEvents();
}
/**
......
......@@ -80,6 +80,10 @@
.ci_widget {
border-bottom: 1px solid $well-inner-border;
color: $gl-text-color;
display: -webkit-flex;
display: flex;
-webkit-align-items: center;
align-items: center;
svg {
margin-right: 4px;
......@@ -88,12 +92,20 @@
overflow: visible;
}
&> span {
padding-right: 4px;
}
&.ci-success_with_warnings {
i {
color: $gl-warning;
}
}
@media (max-width: $screen-xs-max) {
flex-wrap: wrap;
}
}
.mr-widget-body,
......@@ -102,6 +114,37 @@
padding: $gl-padding;
}
.mr-widget-pipeline-graph {
flex-shrink: 0;
.dropdown-menu {
margin-top: 11px;
}
.ci-action-icon-wrapper {
line-height: 16px;
}
@media (max-width: $screen-xs-max) {
order: 1;
margin-top: $gl-padding-top;
border-radius: 3px;
background-color: $white-light;
border: 1px solid $gray-darker;
width: 100%;
text-align: center;
.dropdown-menu {
margin-left: -97.5px;
}
.arrow-up::before,
.arrow-up::after, {
margin-left: 97.5px;
}
}
}
.normal {
color: $gl-text-color;
}
......
......@@ -183,48 +183,6 @@
}
}
.stage-cell {
font-size: 0;
padding: 10px 4px;
> .stage-container > div > button > span > svg,
> .stage-container > button > svg {
height: 22px;
width: 22px;
position: absolute;
top: -1px;
left: -1px;
z-index: 2;
overflow: visible;
}
.stage-container {
display: inline-block;
position: relative;
height: 22px;
margin: 3px 6px 3px 0;
.tooltip {
white-space: nowrap;
}
.tooltip-inner {
padding: 3px 4px;
}
&:not(:last-child) {
&::after {
content: '';
width: 7px;
position: absolute;
right: -7px;
top: 10px;
border-bottom: 2px solid $border-color;
}
}
}
}
.duration,
.finished-at {
color: $gl-text-color-secondary;
......@@ -312,6 +270,48 @@
}
}
.stage-cell {
font-size: 0;
padding: 10px 4px;
> .stage-container > div > button > span > svg,
> .stage-container > button > svg {
height: 22px;
width: 22px;
position: absolute;
top: -1px;
left: -1px;
z-index: 2;
overflow: visible;
}
.stage-container {
display: inline-block;
position: relative;
height: 22px;
margin: 3px 6px 3px 0;
.tooltip {
white-space: nowrap;
}
.tooltip-inner {
padding: 3px 4px;
}
&:not(:last-child) {
&::after {
content: '';
width: 7px;
position: absolute;
right: -7px;
top: 10px;
border-bottom: 2px solid $border-color;
}
}
}
}
.admin-builds-table {
.ci-table td:last-child {
min-width: 120px;
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -106,6 +106,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
......@@ -230,9 +232,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format|
format.html { define_new_vars }
format.json do
render json: { pipelines: PipelineSerializer
define_pipelines_vars
render json: PipelineSerializer
.new(project: @project, user: @current_user)
.represent(@pipelines) }
.represent(@pipelines)
end
end
end
......@@ -251,7 +255,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
......@@ -473,9 +479,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)
......
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
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
......
......@@ -10,6 +10,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
......@@ -184,10 +185,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
......
......@@ -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?
......
......@@ -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
......
......@@ -738,18 +738,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
......
......@@ -1566,10 +1566,26 @@ class Project < ActiveRecord::Base
end
end
def environments_recently_updated_on_branch(branch)
environments_for(branch).select do |environment|
environment.recently_updated_on_branch?(branch)
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 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
......
......@@ -516,6 +516,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)
......@@ -1313,6 +1315,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)
......@@ -1339,6 +1349,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])
......
......@@ -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
.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'
......
......@@ -40,25 +40,8 @@
- else
Cant find HEAD commit for this branch
%td.stage-cell
- pipeline.stages.each do |stage|
- if stage.status
- detailed_status = stage.detailed_status(current_user)
- icon_status = "#{detailed_status.icon}_borderless"
- status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
.stage-container.dropdown.js-mini-pipeline-graph
%button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
= custom_icon(icon_status)
= icon('caret-down')
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
.arrow-up
.js-builds-dropdown-list.scrollable-menu
.js-builds-dropdown-loading.builds-dropdown-loading.hidden
%span.fa.fa-spinner.fa-spin
%td
= render 'shared/mini_pipeline_graph', pipeline: pipeline, klass: 'js-mini-pipeline-graph'
%td
- if pipeline.duration
......
......@@ -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|
......
......@@ -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
......
- 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
- 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
= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment, show_whitespace_toggle: false
......@@ -46,7 +46,7 @@
-# This tab is always loaded via AJAX
- if @pipelines.any?
#pipelines.pipelines.tab-pane
= render "projects/merge_requests/show/pipelines", endpoint: link_to(url_for(params))
= render 'projects/merge_requests/show/pipelines', endpoint: url_for(params.merge(format: :json))
.mr-loading-status
= spinner
......
- 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}
= render 'projects/commit/pipelines_list', endpoint: pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
- endpoint_path = local_assigns[:endpoint] || pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, format: :json)
= render 'projects/commit/pipelines_list', endpoint: endpoint_path
- if @pipeline
.mr-widget-heading
- %w[success success_with_warnings skipped canceled failed running pending].each do |status|
.ci_widget{ class: "ci-#{status} ci-status-icon-#{status}", style: ("display:none" unless @pipeline.status == status) }
= link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
= ci_icon_for_status(status)
.ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
%div{ class: "ci-status-icon-#{status}" }
= link_to namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'icon-link' do
= ci_icon_for_status(status)
%span
Pipeline
= link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status)
for
= succeed "." do
= link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
.mr-widget-pipeline-graph
= render 'shared/mini_pipeline_graph', pipeline: @pipeline, klass: 'js-pipeline-inline-mr-widget-graph'
%span
for
= succeed "." do
= link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace js-commit-link"
%span.ci-coverage
- elsif @merge_request.has_ci?
......
.stage-cell
- pipeline.stages.each do |stage|
- if stage.status
- detailed_status = stage.detailed_status(current_user)
- icon_status = "#{detailed_status.icon}_borderless"
- status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}"
.stage-container.dropdown{ class: klass }
%button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline, stage: stage.name) } }
= custom_icon(icon_status)
= icon('caret-down')
%ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
.arrow-up
.js-builds-dropdown-list.scrollable-menu
.js-builds-dropdown-loading.builds-dropdown-loading.hidden
%span.fa.fa-spinner.fa-spin
---
title: Improve gl.utils.handleLocationHash tests
merge_request:
author:
---
title: Add 'View on [env]' link to blobs and individual files in diffs
merge_request: 8867
author:
......@@ -442,6 +442,57 @@ 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
# Team data
- source: 'data/team.yml' # data/team.yml
public: 'team/' # team/
# 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 string, starting and ending with `'`, for an exact match
- a regular expression, starting and ending with `/`, for a pattern match
- The regular expression needs to match the entire source path - `^` and `$` anchors are implied.
- 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 `\/`.
- Literal periods (`.`) should 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 if appropriate.
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
......
......@@ -12,49 +12,48 @@ need to be configured in a Bamboo build plan before GitLab can integrate.
## Setup
### Complete these steps in Bamboo:
### Complete these steps in Bamboo
1. Navigate to a Bamboo build plan and choose 'Configure plan' from the 'Actions'
dropdown.
dropdown.
1. Select the 'Triggers' tab.
1. Click 'Add trigger'.
1. Enter a description such as 'GitLab trigger'
1. Choose 'Repository triggers the build when changes are committed'
1. Check one or more repositories checkboxes
1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a
whitelist of IP addresses that are allowed to trigger Bamboo builds.
1. Enter the GitLab IP address in the 'Trigger IP addresses' box. This is a
whitelist of IP addresses that are allowed to trigger Bamboo builds.
1. Save the trigger.
1. In the left pane, select a build stage. If you have multiple build stages
you want to select the last stage that contains the git checkout task.
1. In the left pane, select a build stage. If you have multiple build stages
you want to select the last stage that contains the git checkout task.
1. Select the 'Miscellaneous' tab.
1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}'
in the 'Labels' box.
1. Under 'Pattern Match Labelling' put '${bamboo.repository.revision.number}'
in the 'Labels' box.
1. Save
Bamboo is now ready to accept triggers from GitLab. Next, set up the Bamboo
service in GitLab
service in GitLab.
### Complete these steps in GitLab:
### Complete these steps in GitLab
1. Navigate to the project you want to configure to trigger builds.
1. Select 'Settings' in the top navigation.
1. Select 'Services' in the left navigation.
1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
1. Click 'Atlassian Bamboo CI'
1. Select the 'Active' checkbox.
1. Enter the base URL of your Bamboo server. 'https://bamboo.example.com'
1. Enter the build key from your Bamboo build plan. Build keys are a short,
all capital letter, identifier that is unique. It will be something like PR-BLD
1. If necessary, enter username and password for a Bamboo user that has
access to trigger the build plan. Leave these fields blank if you do not require
authentication.
1. Enter the build key from your Bamboo build plan. Build keys are a short,
all capital letter, identifier that is unique. It will be something like PR-BLD
1. If necessary, enter username and password for a Bamboo user that has
access to trigger the build plan. Leave these fields blank if you do not require
authentication.
1. Save or optionally click 'Test Settings'. Please note that 'Test Settings'
will actually trigger a build in Bamboo.
will actually trigger a build in Bamboo.
## Troubleshooting
If builds are not triggered, these are a couple of things to keep in mind.
1. Ensure you entered the right GitLab IP address in Bamboo under 'Trigger
IP addresses'.
IP addresses'.
1. Remember that GitLab only triggers builds on push events. A commit via the
web interface will not trigger CI currently.
web interface will not trigger CI currently.
# Bugzilla Service
Go to your project's **Settings > Services > Bugzilla** and fill in the required
details as described in the table below.
Navigate to the [Integrations page](project_services.md#accessing-the-project-services),
select the **Bugzilla** service and fill in the required details as described
in the table below.
| Field | Description |
| ----- | ----------- |
......
## Enabling build emails
# Enabling build emails
To receive e-mail notifications about the result status of your builds, visit
your project's **Settings > Services > Builds emails** and activate the service.
By enabling this service, you will be able to receive e-mail notifications about
the result status of your builds.
Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
and select the **Builds emails** service to configure it.
In the _Recipients_ area, provide a list of e-mails separated by comma.
......@@ -10,7 +13,3 @@ e-mail notifications about each build's status.
If you enable the _Notify only broken builds_ option, e-mail notifications will
be sent only for failed builds.
---
![Builds emails service settings](img/builds_emails_service.png)
## Enabling emails on push
# Enabling emails on push
To receive email notifications for every change that is pushed to the project, visit
your project's **Settings > Services > Emails on push** and activate the service.
By enabling this service, you will be able to receive email notifications for
every change that is pushed to your project.
Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
and select the **Emails on push** service to configure it.
In the _Recipients_ area, provide a list of emails separated by commas.
......
......@@ -16,7 +16,7 @@ HipChat v2 API has tokens that are can be created using the Integrations tab
in the Group or Room admin page. By design, these are lightweight tokens that
allow GitLab to send messages only to *one* room.
### Complete these steps in HipChat:
### Complete these steps in HipChat
1. Go to: https://admin.hipchat.com/admin
1. Click on "Group Admin" -> "Integrations".
......@@ -26,17 +26,16 @@ allow GitLab to send messages only to *one* room.
see a URL in the format:
```
https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token>
https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token>
```
HipChat is now ready to accept messages from GitLab. Next, set up the HipChat
service in GitLab.
### Complete these steps in GitLab:
### Complete these steps in GitLab
1. Navigate to the project you want to configure for notifications.
1. Select "Settings" in the top navigation.
1. Select "Services" in the left navigation.
1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
1. Click "HipChat".
1. Select the "Active" checkbox.
1. Insert the `token` field from the URL into the `Token` field on the Web page.
......
# Project integrations
You can find the available integrations under the **Integrations** page by
navigating to the cog icon in the upper right corner of your project. You need
to have at least [master permission][permissions] on the project.
![Accessing the integrations](img/accessing_integrations.png)
## Project services
Project services allow you to integrate GitLab with other applications.
......@@ -8,7 +14,7 @@ adding functionality to GitLab.
[Learn more about project services.](project_services.md)
## Webhooks
## Project webhooks
Project webhooks allow you to trigger a URL if for example new code is pushed or
a new issue is created. You can configure webhooks to listen for specific events
......@@ -16,3 +22,5 @@ like pushes, issues or merge requests. GitLab will send a POST request with data
to the webhook URL.
[Learn more about webhooks.](webhooks.md)
[permissions]: ../../permissions.md
......@@ -23,11 +23,10 @@ from the GitLab service.
If the Irker server runs on the same machine, you are done. If not, you will
need to follow the firsts steps of the next section.
## Complete these steps in GitLab:
## Complete these steps in GitLab
1. Navigate to the project you want to configure for notifications.
1. Select "Settings" in the top navigation.
1. Select "Services" in the left navigation.
1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
1. Click "Irker".
1. Select the "Active" checkbox.
1. Enter the server host address where `irkerd` runs (defaults to `localhost`)
......
......@@ -90,8 +90,9 @@ password as they will be needed when configuring GitLab in the next section.
the configuration options you have to enter. If you are using an older version,
[follow this documentation][jira-repo-old-docs].
To enable JIRA integration in a project, navigate to your project's
**Services ➔ JIRA** and fill in the required details on the page as described
To enable JIRA integration in a project, navigate to the
[Integrations page](project_services.md#accessing-the-project-services), click
the **JIRA** service, and fill in the required details on the page as described
in the table below.
| Field | Description |
......
......@@ -12,6 +12,9 @@ template, see the [Services Templates](services_templates.md) document.
## Configuration
Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
of your project and select the **Kubernetes** service to configure it.
![Kubernetes configuration settings](img/kubernetes_configuration.png)
The Kubernetes service takes the following arguments:
......
......@@ -18,8 +18,9 @@ Display name override is not enabled by default, you need to ask your admin to e
After you set up Mattermost, it's time to set up GitLab.
Go to your project's **Settings > Services > Mattermost Notifications** and you will see a
checkbox with the following events that can be triggered:
Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
and select the **Mattermost notifications** service to configure it.
There, you will see a checkbox with the following events that can be triggered:
- Push
- Issue
......
......@@ -53,9 +53,11 @@ the administrator console.
### Step 2. Open the Mattermost slash commands service in GitLab
1. Open a new tab for GitLab and go to your project's settings
**Services ➔ Mattermost command**. A screen will appear with all the values you
need to copy in Mattermost as described in the next step. Leave the window open.
1. Open a new tab for GitLab, go to your project's
[Integrations page](project_services.md#accessing-the-project-services)
and select the **Mattermost command** service to configure it.
A screen will appear with all the values you need to copy in Mattermost as
described in the next step. Leave the window open.
>**Note:**
GitLab will propose some values for the Mattermost settings. The only one
......
# Project Services
# Project services
Project services allow you to integrate GitLab with other applications. Below
is list of the currently supported ones.
Project services allow you to integrate GitLab with other applications. They
are a bit like plugins in that they allow a lot of freedom in adding
functionality to GitLab.
You can find these within GitLab in the Services page under Project Settings if
you are at least a master on the project.
Project Services are a bit like plugins in that they allow a lot of freedom in
adding functionality to GitLab. For example there is also a service that can
send an email every time someone pushes new commits.
## Accessing the project services
Because GitLab is open source we can ship with the code and tests for all
plugins. This allows the community to keep the plugins up to date so that they
always work in newer GitLab versions.
You can find the available services under the **Integrations** page in your
project's settings.
For an overview of what projects services are available without logging in,
please see the [project_services directory][projects-code].
1. Navigate to the cog icon in the upper right corner of your project. You need
to have at least [master permission][permissions] on the project.
[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
![Accessing the services](img/accessing_integrations.png)
Click on the service links to see
further configuration instructions and details. Contributions are welcome.
1. There are more than 20 services to integrate with. Click on the one that you
want to configure.
![Project services list](img/project_services.png)
Below, you will find a list of the currently supported ones accompanied with
comprehensive documentation.
## Services
Click on the service links to see further configuration instructions and details.
| Service | Description |
| ------- | ----------- |
| Asana | Asana - Teamwork without email |
......@@ -51,9 +54,23 @@ further configuration instructions and details. Contributions are welcome.
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
| [Redmine](redmine.md) | Redmine issue tracker |
## Services Templates
## Services templates
Services templates is a way to set some predefined values in the Service of
your liking which will then be pre-filled on each project's Service.
Read more about [Services Templates in this document](services_templates.md).
Read more about [Services templates in this document](services_templates.md).
## Contributing to project services
Because GitLab is open source we can ship with the code and tests for all
plugins. This allows the community to keep the plugins up to date so that they
always work in newer GitLab versions.
For an overview of what projects services are available, please see the
[project_services source directory][projects-code].
Contributions are welcome!
[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
[permissions]: ../../permissions.md
# Redmine Service
Go to your project's **Settings > Services > Redmine** and fill in the required
details as described in the table below.
To enable the Redmine integration in a project, navigate to the
[Integrations page](project_services.md#accessing-the-project-services), click
the **Redmine** service, and fill in the required details on the page as described
in the table below.
| Field | Description |
| ----- | ----------- |
......
......@@ -15,8 +15,9 @@ Slack:
After you set up Slack, it's time to set up GitLab.
Go to your project's **Settings > Integrations > Slack Notifications** and you will see a
checkbox with the following events that can be triggered:
Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
and select the **Slack notifications** service to configure it.
There, you will see a checkbox with the following events that can be triggered:
- Push
- Issue
......
......@@ -5,19 +5,20 @@
Slack commands give users an extra interface to perform common operations
from the chat environment. This allows one to, for example, create an issue as
soon as the idea was discussed in chat.
For all available commands try the help subcommand, for example: `/gitlab help`,
For all available commands try the help subcommand, for example: `/gitlab help`,
all review the [full list of commands](../../../integration/chat_commands.md).
## Prerequisites
A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in Slack should be created beforehand, GitLab cannot create it for you.
A [team](https://get.slack.help/hc/en-us/articles/217608418-Creating-a-team) in
Slack should be created beforehand, GitLab cannot create it for you.
## Configuration
First, navigate to the Slack Slash commands service page, found at your project's
**Settings** > **Services**, and you find the instructions there:
Go to your project's [Integrations page](project_services.md#accessing-the-project-services)
and select the **Slack slash commands** service to configure it.
![Slack setup instructions](img/slack_setup.png)
![Slack setup instructions](img/slack_setup.png)
Once you've followed the instructions, mark the service as active and insert the token
you've received from Slack. After saving the service you are good to go!
......@@ -14,8 +14,11 @@ to the webhook URL.
Webhooks can be used to update an external issue tracker, trigger CI builds,
update a backup mirror, or even deploy to your production server.
Navigate to the webhooks page by choosing **Webhooks** from your project's
settings which can be found under the wheel icon in the upper right corner.
Navigate to the webhooks page by going to the **Integrations** page from your
project's settings which can be found under the wheel icon in the upper right
corner.
![Accessing the integrations](img/accessing_integrations.png)
## Webhook endpoint tips
......
......@@ -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
......
module Gitlab
class RouteMap
class FormatError < StandardError; end
def initialize(data)
begin
entries = YAML.safe_load(data)
rescue
raise FormatError, 'Route map is not valid YAML'
end
raise FormatError, 'Route map is not 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| mapping[:source] === path }
return unless mapping
path.sub(mapping[:source], mapping[:public])
end
private
def parse_entry(entry)
raise FormatError, 'Route map entry is not a hash' unless entry.is_a?(Hash)
raise FormatError, 'Route map entry does not have a source key' unless entry.has_key?('source')
raise FormatError, 'Route map entry does not have a public key' unless entry.has_key?('public')
source_pattern = entry['source']
public_path = entry['public']
if source_pattern.start_with?('/') && source_pattern.end_with?('/')
source_pattern = source_pattern[1...-1].gsub('\/', '/')
begin
source_pattern = /\A#{source_pattern}\z/
rescue RegexpError => e
raise FormatError, "Route map entry source is not a valid regular expression: #{e}"
end
end
{
source: source_pattern,
public: public_path
}
end
end
end
......@@ -33,11 +33,17 @@ describe Projects::MergeRequestsController do
end
context 'when rendering JSON response' do
before do
create(:ci_pipeline, sha: fork_project.commit('remove-submodule').id,
ref: 'remove-submodule',
project: fork_project)
end
it 'renders JSON including serialized pipelines' do
submit_new_merge_request(format: :json)
expect(json_response).to have_key('pipelines')
expect(response).to be_ok
expect(json_response).not_to be_empty
end
end
end
......
......@@ -113,4 +113,24 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).not_to have_selector('#error_explanation')
expect(page).not_to have_content('The form contains the following error')
end
context 'when a new merge request has a pipeline' do
let!(:pipeline) do
create(:ci_pipeline, sha: project.commit('fix').id,
ref: 'fix',
project: project)
end
it 'shows pipelines for a new merge request' do
visit new_namespace_project_merge_request_path(
project.namespace, project,
merge_request: { target_branch: 'master', source_branch: 'fix' })
page.within('.merge-request') do
click_link 'Pipelines'
expect(page).to have_content "##{pipeline.id}"
end
end
end
end
require 'rails_helper'
feature 'Mini Pipeline Graph', :js, :feature do
include WaitForAjax
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:merge_request) { create(:merge_request, source_project: project) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) }
let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', commands: 'test') }
before do
build.run
login_as(user)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
it 'should display a mini pipeline graph' do
expect(page).to have_selector('.mr-widget-pipeline-graph')
end
describe 'build list toggle' do
let(:toggle) do
find('.mini-pipeline-graph-dropdown-toggle')
first('.mini-pipeline-graph-dropdown-toggle')
end
it 'should expand when hovered' do
before_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();")
toggle.hover
after_width = evaluate_script("$('.mini-pipeline-graph-dropdown-toggle:visible').outerWidth();")
expect(before_width).to be < after_width
end
it 'should show dropdown caret when hovered' do
toggle.hover
expect(toggle).to have_selector('.fa-caret-down')
end
it 'should show tooltip when hovered' do
toggle.hover
expect(toggle.find(:xpath, '..')).to have_selector('.tooltip')
end
end
describe 'builds list menu' do
let(:toggle) do
find('.mini-pipeline-graph-dropdown-toggle')
first('.mini-pipeline-graph-dropdown-toggle')
end
before do
toggle.click
wait_for_ajax
end
it 'should open when toggle is clicked' do
expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu')
end
it 'should close when toggle is clicked again' do
toggle.click
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
end
it 'should close when clicking somewhere else' do
find('body').click
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
end
describe 'build list build item' do
let(:build_item) do
find('.mini-pipeline-graph-dropdown-item')
first('.mini-pipeline-graph-dropdown-item')
end
it 'should visit the build page when clicked' do
build_item.click
find('.build-page')
expect(current_path).to eql(namespace_project_build_path(project.namespace, project, build))
end
it 'should show tooltip when hovered' do
build_item.hover
expect(build_item.find(:xpath, '..')).to have_selector('.tooltip')
end
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
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
......@@ -88,3 +88,5 @@ const pipeline = {
created_at: '2017-01-16T17:13:59.800Z',
updated_at: '2017-01-25T00:00:17.132Z',
};
module.exports = pipeline;
/* global pipeline, Vue */
require('vue-resource');
require('flash');
require('~/flash');
require('~/commit/pipelines/pipelines_store');
require('~/commit/pipelines/pipelines_service');
require('~/commit/pipelines/pipelines_table');
require('~vue_shared/vue_resource_interceptor');
require('./mock_data');
require('~/vue_shared/vue_resource_interceptor');
const pipeline = require('./mock_data');
describe('Pipelines table in Commits and Merge requests', () => {
preloadFixtures('pipelines_table');
preloadFixtures('static/pipelines_table.html.raw');
beforeEach(() => {
loadFixtures('pipelines_table');
loadFixtures('static/pipelines_table.html.raw');
});
describe('successfull request', () => {
......
require('~commit/pipelines/pipelines_store');
require('~/commit/pipelines/pipelines_store');
describe('Store', () => {
const store = gl.commits.pipelines.PipelinesStore;
let store;
beforeEach(() => {
store.create();
store = new gl.commits.pipelines.PipelinesStore();
});
it('should start with a blank state', () => {
......@@ -23,7 +23,7 @@ describe('Store', () => {
},
];
store.store(pipelines);
store.storePipelines(pipelines);
expect(store.state.pipelines.length).toBe(pipelines.length);
});
......
......@@ -43,14 +43,35 @@ require('~/lib/utils/common_utils');
describe('gl.utils.handleLocationHash', () => {
beforeEach(() => {
window.history.pushState({}, null, '#definição');
spyOn(window.document, 'getElementById').and.callThrough();
});
function expectGetElementIdToHaveBeenCalledWith(elementId) {
expect(window.document.getElementById).toHaveBeenCalledWith(elementId);
}
it('decodes hash parameter', () => {
spyOn(window.document, 'getElementById').and.callThrough();
window.history.pushState({}, null, '#random-hash');
gl.utils.handleLocationHash();
expectGetElementIdToHaveBeenCalledWith('random-hash');
expectGetElementIdToHaveBeenCalledWith('user-content-random-hash');
});
it('decodes cyrillic hash parameter', () => {
window.history.pushState({}, null, '#definição');
gl.utils.handleLocationHash();
expect(window.document.getElementById).toHaveBeenCalledWith('definição');
expect(window.document.getElementById).toHaveBeenCalledWith('user-content-definição');
expectGetElementIdToHaveBeenCalledWith('definição');
expectGetElementIdToHaveBeenCalledWith('user-content-definição');
});
it('decodes encoded cyrillic hash parameter', () => {
window.history.pushState({}, null, '#defini%C3%A7%C3%A3o');
gl.utils.handleLocationHash();
expectGetElementIdToHaveBeenCalledWith('definição');
expectGetElementIdToHaveBeenCalledWith('user-content-definição');
});
});
......
......@@ -31,7 +31,7 @@ require('~/mini_pipeline_graph_dropdown');
it('should call getBuildsList', () => {
const getBuildsListSpy = spyOn(gl.MiniPipelineGraph.prototype, 'getBuildsList').and.callFake(function () {});
new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' });
new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
......@@ -41,7 +41,7 @@ require('~/mini_pipeline_graph_dropdown');
it('should make a request to the endpoint provided in the html', () => {
const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' });
new gl.MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
......
require('~/vue_shared/components/committ');
require('~/vue_shared/components/commit');
describe('Commit component', () => {
let props;
......
/* global pipeline */
require('~vue_shared/components/pipelines_table_row');
require('./mock_data');
require('~/vue_shared/components/pipelines_table_row');
const pipeline = require('../../commit/pipelines/mock_data');
describe('Pipelines Table Row', () => {
let component;
......
/* global pipeline */
require('~vue_shared/components/pipelines_table');
require('~lib/utils/datetime_utility');
require('./mock_data');
require('~/vue_shared/components/pipelines_table');
require('~/lib/utils/datetime_utility');
const pipeline = require('../../commit/pipelines/mock_data');
describe('Pipelines Table', () => {
preloadFixtures('static/environments/element.html.raw');
......
......@@ -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)
......
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 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.haml', 'public' => 'index.html' }, { 'source' => '/(.*)\.md/', 'public' => '\1.html' }]))
expect(route_map.public_path_for_source_path('index.haml')).to eq('index.html')
expect(route_map.public_path_for_source_path('foo.md')).to eq('foo.html')
end
end
end
describe '#public_path_for_source_path' do
subject do
described_class.new(<<-'MAP'.strip_heredoc)
# Team data
- source: 'data/team.yml'
public: 'team/'
# 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('data/team.yml')).to eq('team/')
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
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
......@@ -1262,10 +1262,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) }
......@@ -1275,7 +1281,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
......@@ -1299,7 +1305,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
......@@ -1310,7 +1316,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
......@@ -1321,7 +1327,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
......
......@@ -2067,100 +2067,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) }
......@@ -2209,6 +2115,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
......
......@@ -1926,6 +1926,42 @@ describe Repository, models: true do
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
def create_remote_branch(remote_name, branch_name, target)
rugged = repository.rugged
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id)
......
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