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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +20 -22
  3. data/README.md +1 -1
  4. data/app/adapters/houston/adapters/version_control/git_adapter/repo.rb +6 -3
  5. data/app/assets/javascripts/app/boot.coffee +9 -0
  6. data/app/assets/javascripts/app/infinite_scroll.coffee +6 -3
  7. data/app/assets/javascripts/app/models/ticket.coffee +1 -1
  8. data/app/assets/javascripts/core/app.coffee +4 -1
  9. data/app/assets/javascripts/core/core_ext/array.coffee +11 -0
  10. data/app/assets/javascripts/core/core_ext/date.coffee +8 -0
  11. data/app/assets/javascripts/core/handlebars_helpers.coffee +12 -8
  12. data/app/assets/javascripts/vendor.js +2 -2
  13. data/app/assets/stylesheets/application/mobile.scss +96 -0
  14. data/app/assets/stylesheets/application/test.scss +58 -0
  15. data/app/assets/stylesheets/application/test_run.scss +14 -5
  16. data/app/assets/stylesheets/application/timeline.scss +2 -4
  17. data/app/concerns/commit_synchronizer.rb +38 -2
  18. data/app/controllers/application_controller.rb +3 -0
  19. data/app/controllers/hooks_controller.rb +18 -0
  20. data/app/controllers/project_tests_controller.rb +46 -0
  21. data/app/helpers/commit_helper.rb +7 -0
  22. data/app/helpers/test_run_helper.rb +16 -0
  23. data/app/models/commit.rb +4 -0
  24. data/app/models/github/pull_request.rb +7 -7
  25. data/app/models/milestone.rb +1 -1
  26. data/app/models/run_tests_on_post_receive.rb +2 -0
  27. data/app/models/test.rb +4 -0
  28. data/app/models/test_result.rb +1 -1
  29. data/app/models/test_run.rb +25 -2
  30. data/app/views/layouts/_mobile_navigation.html.erb +100 -0
  31. data/app/views/layouts/application.html.erb +20 -10
  32. data/app/views/layouts/dashboard.html.erb +1 -1
  33. data/app/views/layouts/minimal.html.erb +1 -1
  34. data/app/views/layouts/naked_dashboard.html.erb +1 -1
  35. data/app/views/project_notification/test_run.html.erb +97 -120
  36. data/app/views/project_tests/_commits.html.erb +14 -0
  37. data/app/views/project_tests/index.html.erb +39 -0
  38. data/app/views/projects/_form.html.erb +6 -2
  39. data/config/application.rb +1 -2
  40. data/config/routes.rb +2 -0
  41. data/db/migrate/20151108221505_convert_pull_request_labels_to_array.rb +22 -0
  42. data/db/migrate/20151108223154_sync_body_also_for_pull_requests.rb +5 -0
  43. data/db/migrate/20151108233510_add_props_to_pull_requests.rb +5 -0
  44. data/db/structure.sql +10 -1
  45. data/houston.gemspec +4 -5
  46. data/lib/houston/version.rb +1 -1
  47. data/test/integration/web_hook_test.rb +7 -1
  48. data/test/unit/concerns/commit_synchronizer_test.rb +13 -0
  49. data/test/unit/models/pull_request_test.rb +17 -0
  50. data/vendor/assets/javascripts/showdown.js +2489 -0
  51. data/vendor/assets/javascripts/slideout.js +493 -0
  52. metadata +25 -29
  53. data/lib/tasks/config.rake +0 -255
  54. 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
- &.fail { background-color: rgba(226, 78, 50, 0.15); }
82
- &.error { background-color: rgb(252, 247, 207); }
83
- &.skip { }
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 16px;
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: -21px;
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
- flag_unreachable_commits! existing_commits - expected_commits
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
@@ -9,6 +9,9 @@ class ApplicationController < ActionController::Base
9
9
  after_filter :save_current_project
10
10
 
11
11
 
12
+ delegate :mobile?, to: :browser
13
+ helper_method :mobile?
14
+
12
15
 
13
16
  rescue_from CanCan::AccessDenied do |exception|
14
17
  if current_user
@@ -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
@@ -43,6 +43,10 @@ class Commit < ActiveRecord::Base
43
43
  where(unreachable: false)
44
44
  end
45
45
 
46
+ def unreachable
47
+ where(unreachable: true)
48
+ end
49
+
46
50
  def latest
47
51
  last
48
52
  end
@@ -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.join("\n")
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")
@@ -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
@@ -5,4 +5,8 @@ class Test < ActiveRecord::Base
5
5
 
6
6
  validates :project_id, :suite, :name, presence: true
7
7
 
8
+ def introduced_in_shas
9
+ test_results.where(new_test: true).joins(:test_run).pluck("test_runs.sha")
10
+ end
11
+
8
12
  end
@@ -11,7 +11,7 @@ class TestResult < ActiveRecord::Base
11
11
  return if attributes.none?
12
12
  columns = attributes.first.keys
13
13
  values = attributes.map(&:values)
14
- import columns, values
14
+ import columns, values, validate: false
15
15
  end
16
16
 
17
17
  end
@@ -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
- { suite: test_result[:suite],
227
- name: test_result[:name],
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>