houston-core 0.5.4 → 0.5.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +20 -22
- data/README.md +1 -1
- data/app/adapters/houston/adapters/version_control/git_adapter/repo.rb +6 -3
- data/app/assets/javascripts/app/boot.coffee +9 -0
- data/app/assets/javascripts/app/infinite_scroll.coffee +6 -3
- data/app/assets/javascripts/app/models/ticket.coffee +1 -1
- data/app/assets/javascripts/core/app.coffee +4 -1
- data/app/assets/javascripts/core/core_ext/array.coffee +11 -0
- data/app/assets/javascripts/core/core_ext/date.coffee +8 -0
- data/app/assets/javascripts/core/handlebars_helpers.coffee +12 -8
- data/app/assets/javascripts/vendor.js +2 -2
- data/app/assets/stylesheets/application/mobile.scss +96 -0
- data/app/assets/stylesheets/application/test.scss +58 -0
- data/app/assets/stylesheets/application/test_run.scss +14 -5
- data/app/assets/stylesheets/application/timeline.scss +2 -4
- data/app/concerns/commit_synchronizer.rb +38 -2
- data/app/controllers/application_controller.rb +3 -0
- data/app/controllers/hooks_controller.rb +18 -0
- data/app/controllers/project_tests_controller.rb +46 -0
- data/app/helpers/commit_helper.rb +7 -0
- data/app/helpers/test_run_helper.rb +16 -0
- data/app/models/commit.rb +4 -0
- data/app/models/github/pull_request.rb +7 -7
- data/app/models/milestone.rb +1 -1
- data/app/models/run_tests_on_post_receive.rb +2 -0
- data/app/models/test.rb +4 -0
- data/app/models/test_result.rb +1 -1
- data/app/models/test_run.rb +25 -2
- data/app/views/layouts/_mobile_navigation.html.erb +100 -0
- data/app/views/layouts/application.html.erb +20 -10
- data/app/views/layouts/dashboard.html.erb +1 -1
- data/app/views/layouts/minimal.html.erb +1 -1
- data/app/views/layouts/naked_dashboard.html.erb +1 -1
- data/app/views/project_notification/test_run.html.erb +97 -120
- data/app/views/project_tests/_commits.html.erb +14 -0
- data/app/views/project_tests/index.html.erb +39 -0
- data/app/views/projects/_form.html.erb +6 -2
- data/config/application.rb +1 -2
- data/config/routes.rb +2 -0
- data/db/migrate/20151108221505_convert_pull_request_labels_to_array.rb +22 -0
- data/db/migrate/20151108223154_sync_body_also_for_pull_requests.rb +5 -0
- data/db/migrate/20151108233510_add_props_to_pull_requests.rb +5 -0
- data/db/structure.sql +10 -1
- data/houston.gemspec +4 -5
- data/lib/houston/version.rb +1 -1
- data/test/integration/web_hook_test.rb +7 -1
- data/test/unit/concerns/commit_synchronizer_test.rb +13 -0
- data/test/unit/models/pull_request_test.rb +17 -0
- data/vendor/assets/javascripts/showdown.js +2489 -0
- data/vendor/assets/javascripts/slideout.js +493 -0
- metadata +25 -29
- data/lib/tasks/config.rake +0 -255
- data/vendor/assets/javascripts/Markdown.Converter.js +0 -1412
@@ -44,6 +44,7 @@ h2.test-result-banner {
|
|
44
44
|
&.pass { @include badge-style(#5DB64C); letter-spacing: 0.088em; } // grass
|
45
45
|
&.error { @include badge-style(#DDC522); letter-spacing: 0.088em; } // mustard
|
46
46
|
&.aborted { @include badge-style(#888888); letter-spacing: 0.088em; } // gray
|
47
|
+
&.pending { @include badge-style(#888888); letter-spacing: 0.088em; } // gray
|
47
48
|
&.fail { @include badge-style(#E24E32); letter-spacing: 0.160em; } // tomato
|
48
49
|
|
49
50
|
a {
|
@@ -75,12 +76,20 @@ h2.test-result-banner {
|
|
75
76
|
.test {
|
76
77
|
display: block;
|
77
78
|
margin: 0;
|
78
|
-
padding: 0.18em 0 0.18em 5.25em;
|
79
|
-
text-indent: -4em;
|
80
79
|
|
81
|
-
|
82
|
-
|
83
|
-
|
80
|
+
& > a {
|
81
|
+
display: block;
|
82
|
+
padding: 0.18em 0 0.18em 5.25em;
|
83
|
+
text-indent: -4em;
|
84
|
+
|
85
|
+
color: inherit;
|
86
|
+
text-decoration: none !important;
|
87
|
+
|
88
|
+
&:hover { background-color: #edf9ff; }
|
89
|
+
}
|
90
|
+
|
91
|
+
&.fail > a { background-color: rgba(226, 78, 50, 0.15); }
|
92
|
+
&.fail > a:hover { background-color: rgba(226, 78, 50, 0.33); }
|
84
93
|
}
|
85
94
|
|
86
95
|
.test-suite-name {
|
@@ -39,11 +39,11 @@ $lightGray: #eee;
|
|
39
39
|
|
40
40
|
.timeline-event {
|
41
41
|
margin-left: 50px;
|
42
|
-
padding: 4px 4px 4px
|
42
|
+
padding: 4px 4px 4px 70px;
|
43
43
|
border-left: solid 2px $lightGray;
|
44
44
|
position: relative;
|
45
45
|
font-size: 0.92em;
|
46
|
-
text-indent: -
|
46
|
+
text-indent: -57px;
|
47
47
|
line-height: 1.25em;
|
48
48
|
|
49
49
|
.timeline-event-time {
|
@@ -131,8 +131,6 @@ $lightGray: #eee;
|
|
131
131
|
&.timeline-event-ticket-created,
|
132
132
|
&.timeline-event-ticket-closed {
|
133
133
|
padding-right: 100px;
|
134
|
-
text-indent: -57px;
|
135
|
-
padding-left: 70px;
|
136
134
|
}
|
137
135
|
|
138
136
|
&.timeline-event-release {
|
@@ -8,7 +8,12 @@ module CommitSynchronizer
|
|
8
8
|
expected_commits = repo.all_commits
|
9
9
|
|
10
10
|
create_missing_commits! expected_commits - existing_commits
|
11
|
-
|
11
|
+
|
12
|
+
reachable_commits = project.commits.reachable.pluck(:sha)
|
13
|
+
flag_unreachable_commits! reachable_commits - expected_commits
|
14
|
+
|
15
|
+
unreachable_commits = project.commits.unreachable.pluck(:sha)
|
16
|
+
flag_reachable_commits! unreachable_commits & expected_commits
|
12
17
|
end
|
13
18
|
end
|
14
19
|
|
@@ -29,7 +34,13 @@ module CommitSynchronizer
|
|
29
34
|
end
|
30
35
|
|
31
36
|
|
32
|
-
def synchronize(native_commits)
|
37
|
+
def synchronize(native_commits=[])
|
38
|
+
if block_given?
|
39
|
+
native_commits = Houston.benchmark("[commits.synchonize] reading commits") do
|
40
|
+
yield repo
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
33
44
|
native_commits = native_commits.reject(&:nil?)
|
34
45
|
return [] if native_commits.empty?
|
35
46
|
|
@@ -100,6 +111,31 @@ private
|
|
100
111
|
Houston.report_exception $!
|
101
112
|
end
|
102
113
|
|
114
|
+
def flag_reachable_commits!(reachable_commits)
|
115
|
+
return if reachable_commits.none?
|
116
|
+
|
117
|
+
# Inserting this to help troubleshoot a scenario where PG::TRDeadlockDetected
|
118
|
+
# is raised from this method. This is a recoverable scenario, so we report
|
119
|
+
# the exception (with additional context) but do not re-raise it.
|
120
|
+
query = <<-SQL
|
121
|
+
SELECT query, state, waiting, pid
|
122
|
+
FROM pg_stat_activity
|
123
|
+
WHERE state <> 'idle' AND waiting='t'
|
124
|
+
SQL
|
125
|
+
waiting_queries = connection.select_all(query).to_hash
|
126
|
+
.reject { |result| result["query"] == query }
|
127
|
+
|
128
|
+
project.commits.where(sha: reachable_commits).update_all(unreachable: false)
|
129
|
+
|
130
|
+
Rails.logger.info "[commits:sync] #{reachable_commits.length} reachable commits for #{project.name}"
|
131
|
+
|
132
|
+
rescue exceptions_wrapping(PG::TRDeadlockDetected)
|
133
|
+
$!.additional_information["project"] = project.slug
|
134
|
+
$!.additional_information["reachable_commits"] = reachable_commits.join("\n")
|
135
|
+
$!.additional_information["waiting_queries"] = MultiJson.dump(waiting_queries)
|
136
|
+
Houston.report_exception $!
|
137
|
+
end
|
138
|
+
|
103
139
|
def repo
|
104
140
|
project.repo
|
105
141
|
end
|
@@ -28,4 +28,22 @@ class HooksController < ApplicationController
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
+
def trigger
|
32
|
+
event = "hooks:#{params[:hook]}"
|
33
|
+
unless Houston.observer.observed?(event)
|
34
|
+
render text: "A hook with the slug '#{params[:hook]}' is not defined", status: 404
|
35
|
+
return
|
36
|
+
end
|
37
|
+
|
38
|
+
payload = params.except(:action, :controller).merge({
|
39
|
+
sender: {
|
40
|
+
ip: request.remote_ip,
|
41
|
+
agent: request.user_agent
|
42
|
+
}
|
43
|
+
})
|
44
|
+
|
45
|
+
Houston.observer.fire event, payload
|
46
|
+
head 200
|
47
|
+
end
|
48
|
+
|
31
49
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class ProjectTestsController < ApplicationController
|
2
|
+
|
3
|
+
def index
|
4
|
+
@project = Project.find_by_slug! params[:slug]
|
5
|
+
@test = @project.tests.find params[:id]
|
6
|
+
@totals = Hash[@test.test_results.group(:status).pluck(:status, "COUNT(*)")]
|
7
|
+
|
8
|
+
begin
|
9
|
+
head = params.fetch :at, @project.repo.branch("master")
|
10
|
+
stop_shas = @test.introduced_in_shas
|
11
|
+
@commits = Houston.benchmark("[project_tests#index] fetch commits") {
|
12
|
+
@project.repo.ancestors(head, including_self: true, limit: 100, hide: stop_shas) }
|
13
|
+
|
14
|
+
if @commits.any?
|
15
|
+
@runs = @project.test_runs.where(sha: @commits.map(&:sha))
|
16
|
+
|
17
|
+
@commits.each do |commit|
|
18
|
+
def commit.date
|
19
|
+
@date ||= committed_at.to_date
|
20
|
+
end
|
21
|
+
def commit.time
|
22
|
+
committed_at
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
@results = @test.test_results.where(test_run_id: @runs.map(&:id))
|
27
|
+
.joins(:test_run)
|
28
|
+
.select("test_runs.sha", :*)
|
29
|
+
.index_by { |result| result[:sha] }
|
30
|
+
@runs = @runs.index_by(&:sha)
|
31
|
+
end
|
32
|
+
rescue Houston::Adapters::VersionControl::CommitNotFound
|
33
|
+
@commits = []
|
34
|
+
@exception = $!
|
35
|
+
end
|
36
|
+
|
37
|
+
if request.xhr?
|
38
|
+
if @commits.any?
|
39
|
+
render partial: "project_tests/commits"
|
40
|
+
else
|
41
|
+
head 204
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -61,4 +61,11 @@ module CommitHelper
|
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
64
|
+
def commit_test_message(commit)
|
65
|
+
message = commit.message[/^.*$/]
|
66
|
+
return message unless @project
|
67
|
+
return message unless @project.repo.respond_to? :commit_url
|
68
|
+
link_to message, @project.repo.commit_url(commit), target: "_blank"
|
69
|
+
end
|
70
|
+
|
64
71
|
end
|
@@ -33,4 +33,20 @@ module TestRunHelper
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
+
def commit_test_status(test_run, test_result)
|
37
|
+
status = test_result.status if test_result
|
38
|
+
status = "untested" if test_run.nil?
|
39
|
+
status = "pending" if test_run && test_run.pending?
|
40
|
+
status = "aborted" if test_run && test_run.aborted?
|
41
|
+
status ||= "unknown"
|
42
|
+
|
43
|
+
css = "project-test-status project-test-status-#{status}"
|
44
|
+
|
45
|
+
if test_run
|
46
|
+
link_to status, test_run_url(slug: test_run.project.slug, commit: test_run.sha), class: css
|
47
|
+
else
|
48
|
+
"<span class=\"#{css}\">#{status}</span>".html_safe
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
36
52
|
end
|
data/app/models/commit.rb
CHANGED
@@ -17,8 +17,7 @@ module Github
|
|
17
17
|
after_create { Houston.observer.fire "github:pull:opened", self }
|
18
18
|
after_update { Houston.observer.fire "github:pull:updated", self, changes }
|
19
19
|
|
20
|
-
validates :project_id, :title, :number, :repo, :url, :base_ref, :base_sha, :head_ref, :head_sha,
|
21
|
-
presence: true
|
20
|
+
validates :project_id, :title, :number, :repo, :url, :base_ref, :base_sha, :head_ref, :head_sha, :username, presence: true
|
22
21
|
validates :number, uniqueness: { scope: :project_id }
|
23
22
|
|
24
23
|
class << self
|
@@ -92,16 +91,16 @@ module Github
|
|
92
91
|
number: github_pr["number"])
|
93
92
|
.merge_attributes(github_pr)
|
94
93
|
end
|
95
|
-
end
|
96
94
|
|
95
|
+
def labeled(*labels)
|
96
|
+
where(["labels && ARRAY[?]", labels])
|
97
|
+
end
|
98
|
+
end
|
97
99
|
|
98
100
|
|
99
|
-
def labels
|
100
|
-
super.split(/\n/)
|
101
|
-
end
|
102
101
|
|
103
102
|
def labels=(value)
|
104
|
-
super Array(value).uniq
|
103
|
+
super Array(value).uniq
|
105
104
|
end
|
106
105
|
|
107
106
|
def add_label!(label, options={})
|
@@ -131,6 +130,7 @@ module Github
|
|
131
130
|
end
|
132
131
|
|
133
132
|
self.title = pr["title"]
|
133
|
+
self.body = pr["body"]
|
134
134
|
self.head_sha = pr["head"]["sha"]
|
135
135
|
self.head_ref = pr["head"]["ref"]
|
136
136
|
self.labels = pr["labels"] if pr.key?("labels")
|
data/app/models/milestone.rb
CHANGED
@@ -4,7 +4,7 @@ class Milestone < ActiveRecord::Base
|
|
4
4
|
belongs_to :project
|
5
5
|
has_many :tickets, -> { reorder("NULLIF(tickets.extended_attributes->'milestoneSequence', '')::int") }
|
6
6
|
|
7
|
-
versioned only: [:name, :start_date, :end_date, :band, :lanes], class_name: "MilestoneVersion", initial_version: true
|
7
|
+
versioned only: [:name, :start_date, :end_date, :band, :lanes, :destroyed_at], class_name: "MilestoneVersion", initial_version: true
|
8
8
|
|
9
9
|
default_scope { where(destroyed_at: nil).order(:start_date) }
|
10
10
|
|
@@ -148,6 +148,8 @@ class RunTestsOnPostReceive
|
|
148
148
|
return if test_run.project.code_climate_repo_token.blank?
|
149
149
|
CodeClimate::CoverageReport.publish!(test_run)
|
150
150
|
test_run.project.feature_working! :publish_coverage_to_code_climate
|
151
|
+
rescue Houston::Adapters::VersionControl::CommitNotFound
|
152
|
+
# Got a bad Test Run, nothing we can do about it.
|
151
153
|
rescue Net::OpenTimeout, Net::ReadTimeout
|
152
154
|
test_run.project.feature_broken! :publish_coverage_to_code_climate
|
153
155
|
Rails.logger.warn "\e[31m[push:publish:codeclimate] #{$!.class}: #{$!.message}\e[0m"
|
data/app/models/test.rb
CHANGED
data/app/models/test_result.rb
CHANGED
data/app/models/test_run.rb
CHANGED
@@ -63,6 +63,24 @@ class TestRun < ActiveRecord::Base
|
|
63
63
|
AND test_runs.completed_at=most_recent_test_runs.completed_at
|
64
64
|
SQL
|
65
65
|
end
|
66
|
+
|
67
|
+
def rebuild_tests!(options={})
|
68
|
+
test_runs = where("tests is not null")
|
69
|
+
.where("id NOT IN (SELECT DISTINCT test_run_id FROM test_results)")
|
70
|
+
if options[:progress]
|
71
|
+
require "progressbar"
|
72
|
+
pbar = ProgressBar.new("test runs", test_runs.count)
|
73
|
+
end
|
74
|
+
test_runs.find_each do |test_run|
|
75
|
+
if test_run.read_attribute(:tests).nil?
|
76
|
+
test_run.update_column :tests, nil
|
77
|
+
else
|
78
|
+
test_run.save_tests_and_results
|
79
|
+
end
|
80
|
+
pbar.inc if options[:progress]
|
81
|
+
end
|
82
|
+
pbar.finish if options[:progress]
|
83
|
+
end
|
66
84
|
end
|
67
85
|
|
68
86
|
|
@@ -181,6 +199,10 @@ class TestRun < ActiveRecord::Base
|
|
181
199
|
completed_at.present?
|
182
200
|
end
|
183
201
|
|
202
|
+
def pending?
|
203
|
+
!completed?
|
204
|
+
end
|
205
|
+
|
184
206
|
def has_results?
|
185
207
|
result.present? and !aborted?
|
186
208
|
end
|
@@ -223,8 +245,9 @@ class TestRun < ActiveRecord::Base
|
|
223
245
|
def tests
|
224
246
|
@tests ||= test_results.includes(:error).joins(:test).select("test_results.*", "tests.suite", "tests.name").map do |test_result|
|
225
247
|
message, backtrace = test_result.error.output.split("\n\n") if test_result.error
|
226
|
-
{
|
227
|
-
|
248
|
+
{ test_id: test_result.test_id,
|
249
|
+
suite: test_result[:suite],
|
250
|
+
name: test_result[:name].to_s.gsub(/^(test :|: )/, ""),
|
228
251
|
status: test_result.status,
|
229
252
|
duration: test_result.duration,
|
230
253
|
error_message: message,
|
@@ -0,0 +1,100 @@
|
|
1
|
+
<div class="navbar navbar-fixed-top navbar-inverse">
|
2
|
+
<div class="navbar-inner">
|
3
|
+
<div class="container-fluid">
|
4
|
+
<%= link_to Houston.config.title, main_app.root_url, class: "brand" %>
|
5
|
+
|
6
|
+
<ul class="nav pull-right nav-inline">
|
7
|
+
<% if current_user -%>
|
8
|
+
<li class="current-user dropdown">
|
9
|
+
<a href="#" class="dropdown-toggle" data-toggle="dropdown"><%= avatar_for(current_user, size: 30) %> <b class="caret"></b></a>
|
10
|
+
<ul class="dropdown-menu">
|
11
|
+
<li><%= link_to "Settings", main_app.edit_user_path(current_user) %></li>
|
12
|
+
<li><%= link_to "Sign out", main_app.destroy_user_session_path %></li>
|
13
|
+
</ul>
|
14
|
+
</li>
|
15
|
+
<% else -%>
|
16
|
+
<li><%= link_to "Sign in", main_app.new_user_session_path %></li>
|
17
|
+
<% end -%>
|
18
|
+
</ul>
|
19
|
+
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<div id="slideout_menu" class="slideout-menu">
|
25
|
+
<div class="general-navbar">
|
26
|
+
<ul class="nav">
|
27
|
+
<% Houston.config.navigation.each do |navigation| %><%= render_navigation navigation %><% end %>
|
28
|
+
|
29
|
+
<li class="divider-horizontal"></li>
|
30
|
+
|
31
|
+
<% if can?(:read, Project) -%>
|
32
|
+
<%= render_nav_link "Projects", main_app.projects_path, icon: "fa-database" %>
|
33
|
+
<% end -%>
|
34
|
+
|
35
|
+
<% if can?(:read, User) -%>
|
36
|
+
<%= render_nav_link "Team", main_app.users_path, icon: "fa-user" %>
|
37
|
+
<% end -%>
|
38
|
+
|
39
|
+
<% if can?(:read, :job) -%>
|
40
|
+
<%= render_nav_link "Jobs", main_app.jobs_path, icon: "fa-user" %>
|
41
|
+
<% end -%>
|
42
|
+
|
43
|
+
</ul>
|
44
|
+
</div>
|
45
|
+
|
46
|
+
<% if current_project && current_project.persisted? %>
|
47
|
+
<div class="project-navbar <%= current_project.color %>">
|
48
|
+
<ul class="nav nav-inline">
|
49
|
+
<% if current_user -%>
|
50
|
+
<li class="dropdown current-project <%= current_project && current_project.color %>">
|
51
|
+
<a href="#" title="Feedback" class="dropdown-toggle" data-toggle="dropdown">
|
52
|
+
<%= current_project ? current_project.name : "Select Project" %>
|
53
|
+
</a>
|
54
|
+
<ul class="dropdown-menu">
|
55
|
+
<% followed_projects.each do |project| %>
|
56
|
+
<% if project == current_project %>
|
57
|
+
<li class="current">
|
58
|
+
<b class="bubble <%= project.color %>"></b> <%= project.name %></a>
|
59
|
+
</li>
|
60
|
+
<% else %>
|
61
|
+
<li>
|
62
|
+
<% path = if !current_feature
|
63
|
+
# we're not on a project page,
|
64
|
+
# just refresh the page and set the project
|
65
|
+
"?project=#{project.slug}"
|
66
|
+
elsif !project.features.include?(current_feature)
|
67
|
+
# we're using a feature that this project
|
68
|
+
# doesn't support. Navigate to the root URL
|
69
|
+
# and set the project
|
70
|
+
main_app.root_path(project: project.slug)
|
71
|
+
else
|
72
|
+
feature_path(project, current_feature)
|
73
|
+
end %>
|
74
|
+
<%= link_to path do %>
|
75
|
+
<b class="bubble <%= project.color %>"></b> <%= project.name %></a>
|
76
|
+
<% end %>
|
77
|
+
</li>
|
78
|
+
<% end %>
|
79
|
+
<% end %>
|
80
|
+
</ul>
|
81
|
+
</li>
|
82
|
+
<% end %>
|
83
|
+
</ul>
|
84
|
+
<% if current_project.features.any? %>
|
85
|
+
<ul class="nav">
|
86
|
+
<% current_project.features.each do |feature| %>
|
87
|
+
<%= render_nav_for_feature(feature) %>
|
88
|
+
<% end %>
|
89
|
+
</ul>
|
90
|
+
<% else %>
|
91
|
+
<div class="project-no-features">
|
92
|
+
No features are enabled for <%= current_project.name %>.
|
93
|
+
<% if can?(:update, current_project) %>
|
94
|
+
You can enable features in <%= link_to "Project Settings", main_app.edit_project_path(current_project) %>.
|
95
|
+
<% end %>
|
96
|
+
</div>
|
97
|
+
<% end %>
|
98
|
+
</div>
|
99
|
+
<% end %>
|
100
|
+
</div>
|