houston-core 0.5.6 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +94 -69
  3. data/app/adapters/houston/adapters/deployment/engineyard.rb +4 -3
  4. data/app/adapters/houston/adapters/version_control/git_adapter.rb +36 -42
  5. data/app/adapters/houston/adapters/version_control/git_adapter/github_repo.rb +2 -2
  6. data/app/adapters/houston/adapters/version_control/git_adapter/remote_repo.rb +23 -11
  7. data/app/adapters/houston/adapters/version_control/git_adapter/repo.rb +18 -8
  8. data/app/adapters/houston/adapters/version_control/null_repo.rb +8 -0
  9. data/app/assets/javascripts/core/handlebars_helpers.coffee +3 -3
  10. data/app/assets/stylesheets/application/github_repos.scss +77 -0
  11. data/app/assets/stylesheets/application/navigation.scss +2 -0
  12. data/app/assets/stylesheets/application/pull_requests.scss +44 -58
  13. data/app/assets/stylesheets/core/avatars.scss +5 -0
  14. data/app/assets/stylesheets/core/colors.scss.erb +10 -7
  15. data/app/concerns/commit_synchronizer.rb +3 -0
  16. data/app/controllers/github/pulls_controller.rb +12 -0
  17. data/app/controllers/project_tests_controller.rb +3 -3
  18. data/app/controllers/projects_controller.rb +45 -1
  19. data/app/controllers/releases_controller.rb +42 -26
  20. data/app/helpers/application_helper.rb +8 -0
  21. data/app/helpers/avatar_helper.rb +2 -2
  22. data/app/helpers/commit_helper.rb +2 -2
  23. data/app/helpers/oembed_helper.rb +8 -0
  24. data/app/helpers/project_helper.rb +4 -5
  25. data/app/helpers/release_helper.rb +11 -0
  26. data/app/helpers/timeline_helper.rb +1 -1
  27. data/app/helpers/url_helper.rb +0 -18
  28. data/app/interactors/cache_key_dependencies.rb +28 -0
  29. data/app/jobs/sync_all_tickets_job.rb +1 -0
  30. data/app/mailers/view_mailer.rb +0 -1
  31. data/app/models/commit.rb +1 -1
  32. data/app/models/github/pull_request.rb +82 -26
  33. data/app/models/github/pull_request_event.rb +2 -2
  34. data/app/models/milestone.rb +1 -0
  35. data/app/models/project.rb +14 -0
  36. data/app/models/project_dependencies.rb +5 -3
  37. data/app/models/task.rb +1 -1
  38. data/app/models/user.rb +41 -0
  39. data/app/views/commits/show.html.erb +9 -1
  40. data/app/views/github/pulls/index.html.erb +102 -0
  41. data/app/views/project_notification/new_release.html.erb +6 -0
  42. data/app/views/project_tickets/index.xls.erb +0 -7
  43. data/app/views/projects/_form.html.erb +29 -17
  44. data/app/views/projects/index.html.erb +3 -3
  45. data/app/views/projects/new_from_github.html.erb +67 -0
  46. data/app/views/releases/_commits.html.erb +1 -1
  47. data/app/views/releases/show.html.erb +9 -0
  48. data/app/views/users/_form.html.erb +35 -19
  49. data/config/application.rb +12 -0
  50. data/config/initializers/mime_types.rb +1 -0
  51. data/config/routes.rb +17 -3
  52. data/db/migrate/20151201042126_require_projects_to_have_name_and_slug.rb +6 -0
  53. data/db/migrate/20151202005557_add_head_sha_to_projects.rb +24 -0
  54. data/db/migrate/20151202011812_require_projects_to_have_color.rb +13 -0
  55. data/db/migrate/20151205204922_require_project_slugs_to_be_unique.rb +5 -0
  56. data/db/migrate/20151205214647_add_avatar_url_to_pull_requests.rb +5 -0
  57. data/db/migrate/20151209004458_add_json_labels_to_pull_requests.rb +5 -0
  58. data/db/migrate/20151209030113_add_timestamps_to_pull_requests.rb +6 -0
  59. data/db/structure.sql +31 -5
  60. data/houston.gemspec +7 -7
  61. data/lib/configuration.rb +3 -2
  62. data/lib/houston/version.rb +1 -1
  63. data/lib/rack/oembed.rb +23 -0
  64. data/templates/new-instance/config/jobs/cache_key_dependencies.rb +3 -0
  65. data/templates/new-instance/config/triggers/tests/slack_when_analyzed.rb +1 -4
  66. data/templates/new-instance/config/triggers/tests/slack_when_completed.rb +1 -1
  67. data/templates/new-instance/lib/slack_helpers.rb +1 -1
  68. data/test/integration/ticket_tasks_api_test.rb +1 -1
  69. data/test/unit/adapters/git_adapter_test.rb +29 -8
  70. data/test/unit/adapters/version_control_adapters_api_test.rb +2 -0
  71. data/test/unit/controllers/hooks_controller_test.rb +4 -4
  72. data/test/unit/models/commit_test.rb +2 -2
  73. data/test/unit/models/project_test.rb +2 -2
  74. data/test/unit/models/pull_request_test.rb +9 -4
  75. data/test/unit/models/task_test.rb +1 -1
  76. data/test/unit/models/ticket_test.rb +1 -1
  77. metadata +31 -16
@@ -14,6 +14,8 @@ module CommitSynchronizer
14
14
 
15
15
  unreachable_commits = project.commits.unreachable.pluck(:sha)
16
16
  flag_reachable_commits! unreachable_commits & expected_commits
17
+
18
+ project.update_column :head_sha, project.repo.branch("master")
17
19
  end
18
20
  end
19
21
 
@@ -82,6 +84,7 @@ private
82
84
  commit = find_by_sha(native_commit.sha)
83
85
  return commit if commit
84
86
 
87
+ $!.additional_information["project"] = project.slug
85
88
  $!.additional_information["native_commit.sha"] = native_commit.sha
86
89
  raise
87
90
  end
@@ -0,0 +1,12 @@
1
+ class Github::PullsController < ApplicationController
2
+
3
+ def index
4
+ @pulls = Github::PullRequest.order(created_at: :desc).preload(:project, :user)
5
+ @labels = @pulls.flat_map(&:labels).uniq { |label| label["name"] }.sort_by { |label| label["name"] }
6
+ @selected_labels = params.fetch(:only, "").split(/,\s*/)
7
+ @selected_labels = @labels.map { |label| label["name"] } if @selected_labels.none?
8
+ @selected_labels -= params.fetch(:except, "").split(/,\s*/)
9
+ @title = "Pull Requests (#{@pulls.count})"
10
+ end
11
+
12
+ end
@@ -3,7 +3,7 @@ class ProjectTestsController < ApplicationController
3
3
  def index
4
4
  @project = Project.find_by_slug! params[:slug]
5
5
 
6
- head = params.fetch :at, @project.repo.branch("master")
6
+ head = params.fetch :at, @project.head_sha
7
7
  commits = params.fetch(:limit, 500).to_i
8
8
 
9
9
  @commits = Houston.benchmark("[project_tests#index] fetch commits") {
@@ -35,7 +35,7 @@ class ProjectTestsController < ApplicationController
35
35
  @totals = Hash[@test.test_results.group(:status).pluck(:status, "COUNT(*)")]
36
36
 
37
37
  begin
38
- head = params.fetch :at, @project.repo.branch("master")
38
+ head = params.fetch :at, @project.head_sha
39
39
  stop_shas = @test.introduced_in_shas
40
40
  @commits = Houston.benchmark("[project_tests#show] fetch commits") {
41
41
  @project.repo.ancestors(head, including_self: true, limit: 100, hide: stop_shas) }
@@ -54,7 +54,7 @@ class ProjectTestsController < ApplicationController
54
54
 
55
55
  @results = @test.test_results.where(test_run_id: @runs.map(&:id))
56
56
  .joins(:test_run)
57
- .select("test_runs.sha", :*)
57
+ .select("test_runs.sha", "test_results.*")
58
58
  .index_by { |result| result[:sha] }
59
59
  @runs = @runs.index_by(&:sha)
60
60
  end
@@ -9,8 +9,8 @@ class ProjectsController < ApplicationController
9
9
  @projects = Project \
10
10
  .includes(:owners)
11
11
  .includes(:maintainers)
12
+ .includes(:head)
12
13
  .unretired
13
- .map { |project| ProjectDependencies.new(project) }
14
14
  @test_runs = TestRun.most_recent.index_by(&:project_id)
15
15
  @releases = Release.where(environment_name: "production").most_recent.index_by(&:project_id)
16
16
  end
@@ -28,6 +28,50 @@ class ProjectsController < ApplicationController
28
28
  @project.roles.build(user: current_user) if @project.roles.none?
29
29
  end
30
30
 
31
+ def new_from_github
32
+ authorize! :create, Project
33
+
34
+ existing_projects = Project.unscoped.where("extended_attributes->'git_location' LIKE '%github.com%'")
35
+ github_repos = Houston.benchmark "Fetching repos" do
36
+ Houston.github.repos
37
+ end
38
+ @repos = github_repos.map do |repo|
39
+ project = existing_projects.detect { |project|
40
+ [repo.git_url, repo.ssh_url, repo.clone_url].member?(project.extended_attributes["git_location"]) }
41
+ { name: repo.name,
42
+ owner: repo.owner.login,
43
+ full_name: repo.full_name,
44
+ private: repo[:private],
45
+ git_location: repo.ssh_url,
46
+ project: project }
47
+ end
48
+ end
49
+
50
+
51
+ def create_from_github
52
+ authorize! :create, Project
53
+
54
+ repos = params.fetch(:repos, [])
55
+ projects = Project.transaction do
56
+ repos.map do |repo|
57
+ owner, name = repo.split("/")
58
+ title = name.humanize.gsub(/\b(?<!['’.`])[a-z]/) { $&.capitalize }.gsub("-", "::")
59
+ Project.create!(
60
+ name: title,
61
+ slug: name,
62
+ version_control_name: "Git",
63
+ extended_attributes: {"git_location" => "git@github.com:#{repo}.git"})
64
+ end
65
+ end
66
+
67
+ flash[:notice] = "#{projects.count} projects added"
68
+ redirect_to projects_path
69
+
70
+ rescue ActiveRecord::RecordInvalid
71
+ flash[:error] = $!.message
72
+ redirect_to :back
73
+ end
74
+
31
75
 
32
76
  def edit
33
77
  @project = Project.find_by_slug!(params[:id])
@@ -1,20 +1,17 @@
1
1
  class ReleasesController < ApplicationController
2
2
  include UrlHelper
3
- before_filter :get_project_and_environment
3
+ include ReleaseHelper
4
+ before_filter :get_release_and_project, only: [:show, :edit, :update, :destroy]
5
+ before_filter :get_project_and_environment, only: [:index, :new, :create]
4
6
  before_filter :load_tickets, only: [:new, :edit, :create, :update]
5
7
 
8
+
9
+
6
10
  def index
7
11
  @title = "Releases • #{@project.name}"
8
12
  @title << " (#{@environment})" if @environment
9
13
  end
10
14
 
11
- def show
12
- @release = @releases.find(params[:id])
13
- authorize! :show, @release
14
-
15
- @title = "Release #{@release.release_date.strftime("%b %-d")} • #{@project.name}"
16
- end
17
-
18
15
  def new
19
16
  @title = "New Release (#{@environment}) • #{@project.name}"
20
17
 
@@ -42,22 +39,6 @@ class ReleasesController < ApplicationController
42
39
  end
43
40
  end
44
41
 
45
- def edit
46
- @release = @releases.find(params[:id])
47
- authorize! :update, @release
48
-
49
- if params[:recreate]
50
- if @release.can_read_commits?
51
- @release.load_commits!
52
- @release.load_tickets!
53
- @release.build_changes_from_commits
54
- end
55
- end
56
-
57
- @release.release_changes = [ReleaseChange.new(@release, "", "")] if @release.release_changes.none?
58
- @release.valid?
59
- end
60
-
61
42
  def create
62
43
  @release = @releases.new(params[:release])
63
44
  @release.user = current_user
@@ -81,8 +62,39 @@ class ReleasesController < ApplicationController
81
62
  end
82
63
  end
83
64
 
65
+
66
+
67
+ def show
68
+ authorize! :show, @release
69
+
70
+ @title = "Release #{@release.release_date.strftime("%b %-d")} • #{@project.name}"
71
+
72
+ if request.format.oembed?
73
+ render json: {
74
+ version: "1.0",
75
+ type: "link",
76
+ author_name: "#{@project.slug} / #{@release.environment_name}",
77
+ title: format_release_subject(@release),
78
+ html: format_release_description(@release) }
79
+ end
80
+ end
81
+
82
+ def edit
83
+ authorize! :update, @release
84
+
85
+ if params[:recreate]
86
+ if @release.can_read_commits?
87
+ @release.load_commits!
88
+ @release.load_tickets!
89
+ @release.build_changes_from_commits
90
+ end
91
+ end
92
+
93
+ @release.release_changes = [ReleaseChange.new(@release, "", "")] if @release.release_changes.none?
94
+ @release.valid?
95
+ end
96
+
84
97
  def update
85
- @release = @releases.find(params[:id])
86
98
  authorize! :update, @release
87
99
 
88
100
  if @release.update_attributes(params[:release])
@@ -93,7 +105,6 @@ class ReleasesController < ApplicationController
93
105
  end
94
106
 
95
107
  def destroy
96
- @release = @releases.find(params[:id])
97
108
  authorize! :destroy, @release
98
109
 
99
110
  @release.destroy
@@ -103,6 +114,11 @@ class ReleasesController < ApplicationController
103
114
 
104
115
  private
105
116
 
117
+ def get_release_and_project
118
+ @release = Release.find(params[:id])
119
+ @project = @release.project
120
+ end
121
+
106
122
  def get_project_and_environment
107
123
  @project = Project.find_by_slug!(params[:project_id])
108
124
  @environment = params[:environment] || @project.environments_with_release_notes.first
@@ -75,6 +75,14 @@ module ApplicationHelper
75
75
 
76
76
 
77
77
 
78
+ def pull_request_label(label)
79
+ background = "##{label["color"]}"
80
+ foreground = "#fff"
81
+ foreground = "#333" if %w{#f7c6c7 #d4c5f9 #fbca04 #fad8c7 #bfe5bf}.member? background
82
+ "<span class=\"label\" style=\"background: #{background}; color: #{foreground};\">#{label["name"]}</span>".html_safe
83
+ end
84
+
85
+
78
86
  end
79
87
 
80
88
 
@@ -22,11 +22,11 @@ module AvatarHelper
22
22
  # http://en.gravatar.com/site/implement/ruby
23
23
  # http://en.gravatar.com/site/implement/url
24
24
  def gravatar_url(email, options={})
25
- url = "//www.gravatar.com/avatar/#{Digest::MD5::hexdigest(email)}?r=g&d=retro"
25
+ url = "https://www.gravatar.com/avatar/#{Digest::MD5::hexdigest(email)}?r=g&d=retro"
26
26
  url << "&s=#{options[:size]}" if options.key?(:size)
27
27
  url
28
28
  end
29
29
 
30
30
 
31
31
 
32
- end
32
+ end
@@ -13,8 +13,8 @@ module CommitHelper
13
13
  project = commit.project
14
14
  content = block_given? ? yield : "<span class=\"commit-sha\">#{commit.sha[0...7]}</span>".html_safe
15
15
 
16
- return content unless github_url?(project)
17
- link_to content, github_commit_url(project, commit.sha), options.reverse_merge(target: "_blank")
16
+ return content unless url = github_commit_url(project, commit.sha)
17
+ link_to content, url, options.reverse_merge(target: "_blank")
18
18
  end
19
19
 
20
20
  def link_to_release_commit_range(release)
@@ -0,0 +1,8 @@
1
+ module OembedHelper
2
+
3
+ def link_to_oembed(url)
4
+ url = "#{main_app.root_url}oembed/1.0?url=#{CGI.escape(url)}"
5
+ tag "link", rel: "alternate", type: "application/json+oembed", href: url
6
+ end
7
+
8
+ end
@@ -1,10 +1,9 @@
1
1
  module ProjectHelper
2
2
 
3
3
  def with_most_recent_commit(project)
4
- return if project.repo.nil?
5
- commit = project.find_commit_by_sha project.repo.branch("master")
4
+ commit = project.head
6
5
  if commit
7
- commit.project = project.model # so that _Commit_ doesn't load project again
6
+ commit.project = project # so that _Commit_ doesn't load project again
8
7
  yield commit
9
8
  end
10
9
  end
@@ -12,7 +11,7 @@ module ProjectHelper
12
11
  def with_most_recent_release(project)
13
12
  release = @releases[project.id]
14
13
  if release
15
- release.project = project.model # so that _Release_ doesn't load project again
14
+ release.project = project # so that _Release_ doesn't load project again
16
15
  yield release
17
16
  end
18
17
  end
@@ -20,7 +19,7 @@ module ProjectHelper
20
19
  def with_most_recent_test_run(project)
21
20
  test_run = @test_runs[project.id]
22
21
  if test_run
23
- test_run.project = project.model # so that _TestRun_ doesn't load project again
22
+ test_run.project = project # so that _TestRun_ doesn't load project again
24
23
  yield test_run
25
24
  end
26
25
  end
@@ -4,6 +4,17 @@ module ReleaseHelper
4
4
  "<span class=\"weekday\">#{date.strftime("%A")}</span> #{date.strftime("%b %e, %Y")}".html_safe
5
5
  end
6
6
 
7
+ def format_release_subject(release)
8
+ release.date.strftime("%b %e, %Y • ") + release.released_at.strftime("%-I:%M%p").downcase
9
+ end
10
+
11
+ def format_release_description(release)
12
+ ordered_by_tag(release.release_changes)
13
+ .map { |change| "#{change.tag.name.upcase}&nbsp;&nbsp;&nbsp;#{change.description}" }
14
+ .join("\r\n")
15
+ .html_safe
16
+ end
17
+
7
18
  def ordered_by_tag(changes)
8
19
  changes.sort_by { |change| change.tag ? change.tag.position : 99 }
9
20
  end
@@ -3,7 +3,7 @@ module TimelineHelper
3
3
  def render_timeline_gap_for(date_range)
4
4
  days = date_range.end - date_range.begin
5
5
  if days < 3
6
- date_range.inject("") { |html, date| html << render_timeline_date(date) }.html_safe
6
+ date_range.to_a.reverse.inject("") { |html, date| html << render_timeline_date(date) }.html_safe
7
7
  else
8
8
  <<-HTML.html_safe
9
9
  <div class="timeline-date-gap"></div>
@@ -48,24 +48,6 @@ module UrlHelper
48
48
  end
49
49
  end
50
50
 
51
- def release_path(release, options={})
52
- super(release.project.to_param, release.environment_name, release, options)
53
- end
54
-
55
- def edit_release_path(release, options={})
56
- super(release.project.to_param, release.environment_name, release, options)
57
- end
58
-
59
-
60
-
61
- def release_url(release, options={})
62
- super(release.project.to_param, release.environment_name, release, options)
63
- end
64
-
65
- def edit_release_url(release, options={})
66
- super(release.project.to_param, release.environment_name, release, options)
67
- end
68
-
69
51
  def new_release_url(release, options={})
70
52
  super(release.project.to_param, release.environment_name, options.merge(deploy_id: release.deploy_id))
71
53
  end
@@ -0,0 +1,28 @@
1
+ class CacheKeyDependencies
2
+ attr_reader :project
3
+
4
+ def self.for(*projects)
5
+ projects = projects[0] if projects.length == 1 && projects[0].respond_to?(:each)
6
+ projects.each do |project|
7
+ begin
8
+ self.new(project).perform!
9
+ rescue StandardError => e
10
+ Houston.report_exception(e)
11
+ end
12
+ end
13
+ end
14
+
15
+ def initialize(project)
16
+ @project = ProjectDependencies.new(project)
17
+ end
18
+
19
+ def perform!
20
+ KeyDependency.all.each do |dependency|
21
+ version = ProjectDependency.new(project, dependency).version
22
+ project.extended_attributes = project.extended_attributes.merge(
23
+ "key_dependency.#{dependency.slug}" => version)
24
+ end
25
+ project.update_column :extended_attributes, project.extended_attributes
26
+ end
27
+
28
+ end
@@ -16,6 +16,7 @@ class SyncAllTicketsJob
16
16
  end
17
17
 
18
18
  def update_tickets_for_project!(project)
19
+ connection_retry_count ||= 0
19
20
  SyncProjectTicketsJob.new(project).run!
20
21
 
21
22
  rescue Houston::Adapters::TicketTracker::ConnectionError
@@ -23,7 +23,6 @@ class ViewMailer < ActionMailer::Base
23
23
  application/markdown.scss
24
24
  application/test_run.scss
25
25
  application/releases.scss
26
- application/pull_requests.scss
27
26
  application/follow_up.scss
28
27
  }
29
28
 
@@ -206,7 +206,7 @@ class Commit < ActiveRecord::Base
206
206
  self.tasks = identify_tasks
207
207
 
208
208
  tasks.each do |task|
209
- task.committed!(self)
209
+ task.mark_committed!(self)
210
210
  end
211
211
  end
212
212
 
@@ -21,21 +21,63 @@ module Github
21
21
  validates :number, uniqueness: { scope: :project_id }
22
22
 
23
23
  class << self
24
- def fetch!
24
+ # Makes X + Y requests to GitHub
25
+ # where X is the number of projects in Houston on GitHub
26
+ # and Y is the number of pull requests for those projects
27
+ #
28
+ # We _could_ group repos by their owner and fetch `org_issues`
29
+ # but that will only work for organizations, not personal
30
+ # accounts.
31
+ #
32
+ # This method can chomp through your rate limit rather quickly.
33
+ # Also, on my computer it took 19 seconds to fetch 39 pull
34
+ # requests from 52 repos.
35
+ def fetch!(projects = Project.unretired)
36
+ repos = projects
37
+ .where("extended_attributes->'git_location' LIKE '%github.com%'")
38
+ .pluck("extended_attributes->'git_location'")
39
+ .map { |url| _repo_name_from_url(url) }
40
+ .compact
41
+
25
42
  Houston.benchmark "Fetching pull requests" do
26
- Houston.github.org_issues(Houston.config.github[:organization], filter: "all", state: "open")
43
+ requests = 0
44
+ issues = repos.flat_map do |repo|
45
+ _fetch_issues_for!(repo).tap do |results|
46
+ requests += 1 + (results.length / 30)
47
+ end
48
+ end
49
+
50
+ pulls = issues
27
51
  .select { |issue| !issue.pull_request.nil? }
28
- .map { |issue| Houston.github.pull_request(issue.repository.full_name, issue.number)
29
- .to_h
30
- .merge(labels: issue.labels.map(&:name))
31
- .with_indifferent_access }
52
+ .map { |issue|
53
+ requests += 1
54
+ repo = issue.pull_request.url[/https:\/\/api.github.com\/repos\/(.*)\/pulls\/\d+/, 1]
55
+ Houston.github.pull_request(repo, issue.number)
56
+ .to_h
57
+ .merge(labels: issue.labels)
58
+ .with_indifferent_access }
59
+
60
+ Rails.logger.info "[pulls] #{requests} requests; #{Houston.github.last_response.headers["x-ratelimit-remaining"]} remaining"
61
+ pulls
62
+ end
63
+ end
64
+
65
+ def _repo_name_from_url(url)
66
+ url[/\Agit@github\.com:(.*)\.git\Z/, 1] || url[/\Agit:\/\/github.com\/(.*)\.git\Z/, 1]
67
+ end
68
+
69
+ def _fetch_issues_for!(repo)
70
+ if repo.end_with? "/*"
71
+ Houston.github.org_issues(repo[0...-2], filter: "all", state: "open")
72
+ else
73
+ Houston.github.issues(repo, filter: "all", state: "open")
32
74
  end
75
+ rescue Octokit::NotFound
76
+ []
33
77
  end
34
78
 
35
- def sync!
36
- expected_pulls = fetch!
37
- expected_pulls.select! { |pr| pr["base"]["repo"]["name"] == pr["head"]["repo"]["name"] }
38
- # select only ones where head and base are the same repo
79
+ def sync!(projects = Project.unretired)
80
+ expected_pulls = fetch!(projects)
39
81
  Houston.benchmark "Syncing pull requests" do
40
82
  existing_pulls = all.to_a
41
83
 
@@ -56,7 +98,9 @@ module Github
56
98
 
57
99
  existing_pr ||= Github::PullRequest.new
58
100
  existing_pr.merge_attributes(expected_pr)
59
- existing_pr.save if existing_pr.valid?
101
+ unless existing_pr.save
102
+ Rails.logger.warn "\e[31m[pulls] Invalid PR: #{existing_pr.errors.full_messages.join("; ")}\e[0m"
103
+ end
60
104
  existing_pr
61
105
  end
62
106
  end
@@ -93,42 +137,52 @@ module Github
93
137
  end
94
138
 
95
139
  def labeled(*labels)
96
- where(["labels && ARRAY[?]", labels])
140
+ where(["exists (select 1 from jsonb_array_elements(pull_requests.json_labels) as \"label\" where \"label\"->>'name' IN (?))", labels])
97
141
  end
98
142
  end
99
143
 
100
144
 
101
145
 
102
146
  def labels=(value)
103
- super Array(value).uniq
147
+ self.json_labels = value.map { |label| label.to_h.stringify_keys.pick("name", "color") }
148
+ end
149
+
150
+ def labels
151
+ json_labels
104
152
  end
105
153
 
106
154
  def add_label!(label, options={})
155
+ label = label.to_h.stringify_keys.pick("name", "color")
156
+
107
157
  transaction do
108
158
  pr = self.class.lock.find id
109
- pr.update_attributes! labels: pr.labels + [label], actor: options[:as]
159
+ new_labels = pr.json_labels.reject { |l| l["name"] == label["name"] } + [label]
160
+ pr.update_attributes! json_labels: new_labels, actor: options[:as]
110
161
  end
111
162
  end
112
163
 
113
164
  def remove_label!(label, options={})
165
+ label = label.to_h.stringify_keys.pick("name", "color")
166
+
114
167
  transaction do
115
168
  pr = self.class.lock.find id
116
- pr.update_attributes! labels: pr.labels - [label], actor: options[:as]
169
+ new_labels = pr.json_labels.reject { |l| l["name"] == label["name"] }
170
+ pr.update_attributes! json_labels: new_labels, actor: options[:as]
117
171
  end
118
172
  end
119
173
 
120
174
 
121
175
 
122
176
  def merge_attributes(pr)
123
- if new_record?
124
- self.repo = pr["base"]["repo"]["name"]
125
- self.number = pr["number"]
126
- self.username = pr["user"]["login"]
127
- self.url = pr["html_url"]
128
- self.base_sha = pr["base"]["sha"]
129
- self.base_ref = pr["base"]["ref"]
130
- end
131
-
177
+ self.repo = pr["base"]["repo"]["name"] unless repo
178
+ self.number = pr["number"] unless number
179
+ self.username = pr["user"]["login"] unless username
180
+ self.avatar_url = pr["user"]["avatar_url"] unless avatar_url
181
+ self.url = pr["html_url"] unless url
182
+ self.base_sha = pr["base"]["sha"] unless base_sha
183
+ self.base_ref = pr["base"]["ref"] unless base_ref
184
+
185
+ self.created_at = pr["created_at"]
132
186
  self.title = pr["title"]
133
187
  self.body = pr["body"]
134
188
  self.head_sha = pr["head"]["sha"]
@@ -145,11 +199,13 @@ module Github
145
199
  end
146
200
 
147
201
  def associate_user_with_self
148
- self.user = User.find_by_nickname(username)
202
+ self.user = User.find_by_github_username(username)
149
203
  end
150
204
 
151
205
  def associate_commits_with_self
152
- self.commits = project.commits.between(base_sha, head_sha)
206
+ Houston.try({max_tries: 2, base: 0}, ActiveRecord::RecordNotUnique) do
207
+ self.commits = project.commits.between(base_sha, head_sha)
208
+ end
153
209
  end
154
210
 
155
211
  end