Commit daccc54d authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'builds-view' into 'master'

Added builds view

![Screen_Shot_2015-10-13_at_19.02.48](https://gitlab.com/gitlab-org/gitlab-ce/uploads/95bb3a7d9d603678fdd077558637045d/Screen_Shot_2015-10-13_at_19.02.48.png)

/cc @dzaporozhets @vsizov 


See merge request !1593
parents 524b3db3 9fa209e1
...@@ -18,6 +18,7 @@ v 8.1.0 (unreleased) ...@@ -18,6 +18,7 @@ v 8.1.0 (unreleased)
- Fix cases where Markdown did not render links in activity feed (Stan Hu) - Fix cases where Markdown did not render links in activity feed (Stan Hu)
- Add first and last to pagination (Zeger-Jan van de Weg) - Add first and last to pagination (Zeger-Jan van de Weg)
- Added Commit Status API - Added Commit Status API
- Added Builds View
- Show CI status on commit page - Show CI status on commit page
- Added CI_BUILD_TAG, _STAGE, _NAME and _TRIGGERED to CI builds - Added CI_BUILD_TAG, _STAGE, _NAME and _TRIGGERED to CI builds
- Show CI status on Your projects page and Starred projects page - Show CI status on Your projects page and Starred projects page
......
...@@ -7,6 +7,7 @@ class @ShortcutsNavigation extends Shortcuts ...@@ -7,6 +7,7 @@ class @ShortcutsNavigation extends Shortcuts
Mousetrap.bind('g e', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity')) Mousetrap.bind('g e', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity'))
Mousetrap.bind('g f', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-tree')) Mousetrap.bind('g f', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-tree'))
Mousetrap.bind('g c', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-commits')) Mousetrap.bind('g c', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-commits'))
Mousetrap.bind('g b', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-builds'))
Mousetrap.bind('g n', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-network')) Mousetrap.bind('g n', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-network'))
Mousetrap.bind('g g', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs')) Mousetrap.bind('g g', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs'))
Mousetrap.bind('g i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-issues')) Mousetrap.bind('g i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-issues'))
......
class Projects::BuildsController < Projects::ApplicationController class Projects::BuildsController < Projects::ApplicationController
before_action :ci_project before_action :ci_project
before_action :build before_action :build, except: [:index, :cancel_all]
before_action :authorize_admin_project!, except: [:show, :status] before_action :authorize_admin_project!, except: [:index, :show, :status]
layout "project" layout "project"
def index
@scope = params[:scope]
@all_builds = project.ci_builds.order('created_at DESC').page(params[:page]).per(30)
@builds =
case @scope
when 'all'
@all_builds
when 'finished'
@all_builds.finished
else
@all_builds.running_or_pending
end
end
def cancel_all
@project.ci_builds.running_or_pending.each(&:cancel)
redirect_to namespace_project_builds_path(project.namespace, project)
end
def show def show
@builds = @ci_project.commits.find_by_sha(@build.sha).builds.order('id DESC') @builds = @ci_project.commits.find_by_sha(@build.sha).builds.order('id DESC')
@builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20) @builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20)
......
...@@ -25,6 +25,10 @@ module GitlabRoutingHelper ...@@ -25,6 +25,10 @@ module GitlabRoutingHelper
namespace_project_commits_path(project.namespace, project, @ref || project.repository.root_ref) namespace_project_commits_path(project.namespace, project, @ref || project.repository.root_ref)
end end
def project_builds_path(project, *args)
namespace_project_builds_path(project.namespace, project, *args)
end
def activity_project_path(project, *args) def activity_project_path(project, *args)
activity_namespace_project_path(project.namespace, project, *args) activity_namespace_project_path(project.namespace, project, *args)
end end
......
...@@ -113,6 +113,10 @@ module ProjectsHelper ...@@ -113,6 +113,10 @@ module ProjectsHelper
nav_tabs << :merge_requests nav_tabs << :merge_requests
end end
if can?(current_user, :read_build, project)
nav_tabs << :builds
end
if can?(current_user, :admin_project, project) if can?(current_user, :admin_project, project)
nav_tabs << :settings nav_tabs << :settings
end end
......
...@@ -13,4 +13,17 @@ module RunnersHelper ...@@ -13,4 +13,17 @@ module RunnersHelper
title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago" title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago"
end end
end end
def runner_link(runner)
display_name = truncate(runner.display_name, length: 20)
id = "\##{runner.id}"
if current_user && current_user.admin
link_to ci_admin_runner_path(runner) do
display_name + id
end
else
display_name + id
end
end
end end
...@@ -41,6 +41,7 @@ class Ability ...@@ -41,6 +41,7 @@ class Ability
:read_project_member, :read_project_member,
:read_merge_request, :read_merge_request,
:read_note, :read_note,
:read_build,
:download_code :download_code
] ]
...@@ -127,6 +128,7 @@ class Ability ...@@ -127,6 +128,7 @@ class Ability
:read_project_member, :read_project_member,
:read_merge_request, :read_merge_request,
:read_note, :read_note,
:read_build,
:create_project, :create_project,
:create_issue, :create_issue,
:create_note :create_note
......
...@@ -24,6 +24,8 @@ module Ci ...@@ -24,6 +24,8 @@ module Ci
has_many :builds, class_name: 'Ci::Build' has_many :builds, class_name: 'Ci::Build'
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest' has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
scope :ordered, -> { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }
validates_presence_of :sha validates_presence_of :sha
validate :valid_commit_sha validate :valid_commit_sha
......
...@@ -205,7 +205,7 @@ module Ci ...@@ -205,7 +205,7 @@ module Ci
end end
def commits def commits
gl_project.ci_commits gl_project.ci_commits.ordered
end end
def builds def builds
......
...@@ -59,7 +59,7 @@ module Ci ...@@ -59,7 +59,7 @@ module Ci
end end
def display_name def display_name
return token unless !description.blank? return short_sha unless !description.blank?
description description
end end
...@@ -95,7 +95,7 @@ module Ci ...@@ -95,7 +95,7 @@ module Ci
end end
def short_sha def short_sha
token[0...10] token[0...8] if token
end end
end end
end end
...@@ -16,6 +16,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -16,6 +16,7 @@ class CommitStatus < ActiveRecord::Base
scope :success, -> { where(status: 'success') } scope :success, -> { where(status: 'success') }
scope :failed, -> { where(status: 'failed') } scope :failed, -> { where(status: 'failed') }
scope :running_or_pending, -> { where(status:[:running, :pending]) } scope :running_or_pending, -> { where(status:[:running, :pending]) }
scope :finished, -> { where(status:[:success, :failed, :canceled]) }
scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) } scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) }
scope :ordered, -> { order(:ref, :stage_idx, :name) } scope :ordered, -> { order(:ref, :stage_idx, :name) }
scope :for_ref, ->(ref) { where(ref: ref) } scope :for_ref, ->(ref) { where(ref: ref) }
......
...@@ -119,7 +119,7 @@ class Project < ActiveRecord::Base ...@@ -119,7 +119,7 @@ class Project < ActiveRecord::Base
has_many :deploy_keys, through: :deploy_keys_projects has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy has_many :users_star_projects, dependent: :destroy
has_many :starrers, through: :users_star_projects, source: :user has_many :starrers, through: :users_star_projects, source: :user
has_many :ci_commits, ->() { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id
has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build' has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build'
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData" has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
......
...@@ -99,6 +99,12 @@ ...@@ -99,6 +99,12 @@
.key c .key c
%td %td
Go to commits Go to commits
%tr
%td.shortcut
.key g
.key b
%td
Go to builds
%tr %tr
%td.shortcut %td.shortcut
.key g .key g
......
...@@ -32,12 +32,20 @@ ...@@ -32,12 +32,20 @@
Files Files
- if project_nav_tab? :commits - if project_nav_tab? :commits
= nav_link(controller: %w(commit commits compare repositories tags branches builds)) do = nav_link(controller: %w(commit commits compare repositories tags branches)) do
= link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do
= icon('history fw') = icon('history fw')
%span %span
Commits Commits
- if project_nav_tab? :builds
= nav_link(controller: %w(builds)) do
= link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds', data: {placement: 'right'} do
= icon('cubes fw')
%span
Builds
%span.count.builds_counter= @project.ci_builds.running_or_pending.count(:all)
- if project_nav_tab? :network - if project_nav_tab? :network
= nav_link(controller: %w(network)) do = nav_link(controller: %w(network)) do
= link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do
......
%tr.build
%td.status
= ci_status_with_icon(build.status)
%td.commit_status-link
- if build.target_url
= link_to build.target_url do
%strong Build ##{build.id}
- else
%strong Build ##{build.id}
%td
= link_to build.short_sha, namespace_project_commit_path(@project.namespace, @project, build.sha)
%td
= link_to build.ref, namespace_project_commits_path(@project.namespace, @project, build.ref)
%td
- if build.runner
= runner_link(build.runner)
- else
.light none
%td
= build.name
.pull-right
- if build.tags.any?
- build.tags.each do |tag|
%span.label.label-primary
= tag
- if build.trigger_request
%span.label.label-info triggered
- if build.allow_failure
%span.label.label-danger allowed to fail
%td.duration
- if build.duration
#{duration_in_words(build.finished_at, build.started_at)}
%td.timestamp
- if build.finished_at
%span #{time_ago_in_words build.finished_at} ago
%td
.pull-right
- if current_user && can?(current_user, :manage_builds, @project)
- if build.cancel_url
= link_to build.cancel_url, title: 'Cancel' do
%i.fa.fa-remove.cred
- page_title "Builds"
- header_title project_title(@project, "Builds", project_builds_path(@project))
.project-issuable-filter
.controls
- if @ci_project && current_user && can?(current_user, :manage_builds, @project)
.pull-left.hidden-xs
- if @all_builds.running_or_pending.any?
= link_to 'Cancel all', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger'
%ul.center-top-menu
%li{class: ('active' if @scope.nil?)}
= link_to project_builds_path(@project) do
Running
%span.badge.js-running-count= @all_builds.running_or_pending.size
%li{class: ('active' if @scope == 'finished')}
= link_to project_builds_path(@project, scope: :finished) do
Finished
%span.badge.js-running-count= @all_builds.finished.size
%li{class: ('active' if @scope == 'all')}
= link_to project_builds_path(@project, scope: :all) do
All
%span.badge.js-totalbuilds-count= @all_builds.size
.gray-content-block
List of #{@scope || 'running'} builds from this project
%ul.content-list
- if @builds.blank?
%li
.nothing-here-block No builds to show
- else
%table.table.builds
%thead
%tr
%th Status
%th Build ID
%th Commit
%th Ref
%th Runner
%th Name
%th Duration
%th Finished at
%th
- @builds.each do |build|
= render 'projects/builds/build', build: build
= paginate @builds
...@@ -587,7 +587,11 @@ Gitlab::Application.routes.draw do ...@@ -587,7 +587,11 @@ Gitlab::Application.routes.draw do
end end
end end
resources :builds, only: [:show] do resources :builds, only: [:index, :show] do
collection do
get :cancel_all
end
member do member do
get :cancel get :cancel
get :status get :status
......
...@@ -113,7 +113,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps ...@@ -113,7 +113,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end end
step 'I click status link' do step 'I click status link' do
click_link "Builds" find('.commit-ci-menu').click_link "Builds"
end end
step 'I see builds list' do step 'I see builds list' do
......
...@@ -9,6 +9,54 @@ describe "Builds" do ...@@ -9,6 +9,54 @@ describe "Builds" do
@gl_project.team << [@user, :master] @gl_project.team << [@user, :master]
end end
describe "GET /:project/builds" do
context "Running scope" do
before do
@build.run!
visit namespace_project_builds_path(@gl_project.namespace, @gl_project)
end
it { expect(page).to have_content 'Running' }
it { expect(page).to have_content 'Cancel all' }
it { expect(page).to have_content @build.short_sha }
it { expect(page).to have_content @build.ref }
it { expect(page).to have_content @build.name }
end
context "Finished scope" do
before do
@build.run!
visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :finished)
end
it { expect(page).to have_content 'No builds to show' }
it { expect(page).to have_content 'Cancel all' }
end
context "All builds" do
before do
@gl_project.ci_builds.running_or_pending.each(&:success)
visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :all)
end
it { expect(page).to have_content 'All' }
it { expect(page).to have_content @build.short_sha }
it { expect(page).to have_content @build.ref }
it { expect(page).to have_content @build.name }
it { expect(page).to_not have_content 'Cancel all' }
end
end
describe "GET /:project/builds/:id/cancel_all" do
before do
@build.run!
visit cancel_all_namespace_project_builds_path(@gl_project.namespace, @gl_project)
end
it { expect(page).to have_content 'No builds to show' }
it { expect(page).to_not have_content 'Cancel all' }
end
describe "GET /:project/builds/:id" do describe "GET /:project/builds/:id" do
before do before do
visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build) visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build)
......
...@@ -32,6 +32,24 @@ describe Ci::Commit do ...@@ -32,6 +32,24 @@ describe Ci::Commit do
it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :git_author_email }
it { is_expected.to respond_to :short_sha } it { is_expected.to respond_to :short_sha }
describe :ordered do
let(:project) { FactoryGirl.create :empty_project }
it 'returns ordered list of commits' do
commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project
commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project
expect(project.ci_commits.ordered).to eq([commit2, commit1])
end
it 'returns commits ordered by committed_at and id, with nulls last' do
commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project
commit2 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project
commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project
commit4 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project
expect(project.ci_commits.ordered).to eq([commit2, commit4, commit3, commit1])
end
end
describe :last_build do describe :last_build do
subject { commit.last_build } subject { commit.last_build }
before do before do
......
...@@ -131,24 +131,6 @@ describe Ci::Project do ...@@ -131,24 +131,6 @@ describe Ci::Project do
end end
end end
describe 'ordered commits' do
let(:project) { FactoryGirl.create :empty_project }
it 'returns ordered list of commits' do
commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project
commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project
expect(project.ci_commits).to eq([commit2, commit1])
end
it 'returns commits ordered by committed_at and id, with nulls last' do
commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project
commit2 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project
commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project
commit4 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project
expect(project.ci_commits).to eq([commit2, commit4, commit3, commit1])
end
end
context :valid_project do context :valid_project do
let(:commit) { FactoryGirl.create(:ci_commit) } let(:commit) { FactoryGirl.create(:ci_commit) }
......
...@@ -32,7 +32,7 @@ describe Ci::Runner do ...@@ -32,7 +32,7 @@ describe Ci::Runner do
end end
it 'should return the token if the description is an empty string' do it 'should return the token if the description is an empty string' do
runner = FactoryGirl.build(:ci_runner, description: '') runner = FactoryGirl.build(:ci_runner, description: '', token: 'token')
expect(runner.display_name).to eq runner.token expect(runner.display_name).to eq runner.token
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