code_review_notifier 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Brewfile +2 -0
- data/Gemfile +6 -0
- data/bin/code_review_notifier +5 -0
- data/lib/api.rb +7 -0
- data/lib/base_api.rb +62 -0
- data/lib/cipher.rb +31 -0
- data/lib/code_review_notifier.rb +62 -0
- data/lib/database.rb +48 -0
- data/lib/gerrit_api.rb +45 -0
- data/lib/models/code_change.rb +21 -0
- data/lib/models/code_change_activity.rb +47 -0
- data/lib/notifier.rb +14 -0
- metadata +57 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: edabbf2ed573f0042d93ae9559b8345bfa2b4ad697db78b43a1d6e736d06355e
|
4
|
+
data.tar.gz: 5b9be1ccc0d16556a7045e4b639ae809c9afea160461581b9bf6dd95cb137bae
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 567ac263894da8edae616278d2d558b9efce9f29987f24feb5497447b4d49f8328aa35beefff1c51c244ea1cc01b33c5445c441a485f50d056ed2b12c82e3926
|
7
|
+
data.tar.gz: 9f89909ef2d81828fb9d06b4e0d9a5bcb418b45c3814a584bd0845cafba0f4fc26d4c6a064d6deb7eab8e96aa99e21086a465f9892b90d10f4013e1a6d231606
|
data/Brewfile
ADDED
data/Gemfile
ADDED
data/lib/api.rb
ADDED
data/lib/base_api.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require "httparty"
|
2
|
+
require_relative "./gerrit_api.rb"
|
3
|
+
|
4
|
+
class HTTParty::Parser
|
5
|
+
def json
|
6
|
+
JSON.parse(body.gsub(")]}'", ""))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class BaseApi
|
11
|
+
include HTTParty
|
12
|
+
|
13
|
+
def self.authenticate
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.all_code_changes
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.favicon
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.code_change_url(code_change)
|
26
|
+
raise NotImplementedError
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.wrap_with_authentication(&block)
|
30
|
+
res = block.call
|
31
|
+
if res.code == 401 || res.code == 403
|
32
|
+
self.authenticate
|
33
|
+
block.call
|
34
|
+
else
|
35
|
+
res
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.is_setup?
|
40
|
+
base_api_url && username && password && account_id
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.base_api_url
|
44
|
+
@base_api_url ||= begin
|
45
|
+
url = DB.get_setting("base_api_url")
|
46
|
+
base_uri(url)
|
47
|
+
url
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.username
|
52
|
+
@username ||= DB.get_setting("username")
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.password
|
56
|
+
@password ||= DB.get_setting("password")
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.account_id
|
60
|
+
@account_id ||= DB.get_setting("account_id")
|
61
|
+
end
|
62
|
+
end
|
data/lib/cipher.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "openssl"
|
3
|
+
require "digest/sha1"
|
4
|
+
|
5
|
+
KEY = ""
|
6
|
+
|
7
|
+
class Cipher
|
8
|
+
def self.encrypt(text)
|
9
|
+
cipher = OpenSSL::Cipher.new("aes-256-cbc")
|
10
|
+
cipher.encrypt
|
11
|
+
key = Digest::SHA1.hexdigest("yourpass")[0..31]
|
12
|
+
iv = cipher.random_iv
|
13
|
+
cipher.key = key
|
14
|
+
cipher.iv = iv
|
15
|
+
encrypted = cipher.update(text)
|
16
|
+
encrypted << cipher.final
|
17
|
+
["#{key}:#{Base64.encode64(iv).encode('utf-8')}", Base64.encode64(encrypted).encode('utf-8')]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.decrypt(encrypted, salt)
|
21
|
+
key, iv = salt.split(":")
|
22
|
+
iv = Base64.decode64(iv.encode('ascii-8bit'))
|
23
|
+
cipher = OpenSSL::Cipher.new("aes-256-cbc")
|
24
|
+
cipher.decrypt
|
25
|
+
cipher.key = key
|
26
|
+
cipher.iv = iv
|
27
|
+
decrypted = cipher.update(Base64.decode64(encrypted.encode('ascii-8bit')))
|
28
|
+
decrypted << cipher.final
|
29
|
+
decrypted
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "io/console"
|
2
|
+
require_relative "./api.rb"
|
3
|
+
require_relative "./notifier.rb"
|
4
|
+
|
5
|
+
SECONDS_BETWEEN_RUNS = 120
|
6
|
+
SECONDS_BETWEEN_NOTIFICATIONS = 5
|
7
|
+
|
8
|
+
class CodeReviewNotifier
|
9
|
+
def self.setup(args)
|
10
|
+
system("mkdir -p ~/.code_review_notifier")
|
11
|
+
system("brew bundle --file #{File.expand_path(File.dirname(__FILE__) + "/..")}/Brewfile")
|
12
|
+
|
13
|
+
if args[0] == "--setup" || !Api.current_api.is_setup?
|
14
|
+
print("What's the base URL? (i.e. https://gerrit.google.com) ")
|
15
|
+
DB.save_setting("base_api_url", STDIN.gets.chomp, is_secret: false)
|
16
|
+
|
17
|
+
print("What's the account username? ")
|
18
|
+
DB.save_setting("username", STDIN.gets.chomp, is_secret: false)
|
19
|
+
|
20
|
+
print("What's the account password? (hiding input) ")
|
21
|
+
DB.save_setting("password", STDIN.noecho(&:gets).chomp, is_secret: true)
|
22
|
+
puts
|
23
|
+
|
24
|
+
print("What's the account id? (check #{Api.current_api.base_api_url}/settings/) ")
|
25
|
+
DB.save_setting("account_id", STDIN.gets.chomp, is_secret: false)
|
26
|
+
|
27
|
+
puts("All setup!")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.call(args)
|
32
|
+
setup(args)
|
33
|
+
|
34
|
+
while true
|
35
|
+
is_first_run = is_first_run?
|
36
|
+
puts
|
37
|
+
puts("Querying API...")
|
38
|
+
all_code_changes = Api.current_api.all_code_changes
|
39
|
+
puts("Checking for notifications to display...")
|
40
|
+
all_activity = []
|
41
|
+
all_code_changes.each do |cc|
|
42
|
+
activity_for_code_change = cc.code_change_activity
|
43
|
+
activity_for_code_change.sort! { |a, b| a.created_at <=> b.created_at }
|
44
|
+
cc.activity_from_self_at = activity_for_code_change.find { |a| a.is_self }&.created_at
|
45
|
+
all_activity.concat(activity_for_code_change)
|
46
|
+
end
|
47
|
+
all_activity.select(&:should_notify?).each do |code_change_activity|
|
48
|
+
code_change_activity.notified
|
49
|
+
unless is_first_run
|
50
|
+
puts("Notifying of change!")
|
51
|
+
Notifier.notify(code_change_activity)
|
52
|
+
sleep SECONDS_BETWEEN_NOTIFICATIONS
|
53
|
+
end
|
54
|
+
end
|
55
|
+
sleep SECONDS_BETWEEN_RUNS
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.is_first_run?
|
60
|
+
DB.query_single_row("SELECT id FROM code_change_activity_notified;").nil?
|
61
|
+
end
|
62
|
+
end
|
data/lib/database.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "sqlite3"
|
2
|
+
require_relative "./cipher.rb"
|
3
|
+
|
4
|
+
class DB
|
5
|
+
def self.db
|
6
|
+
@db ||= begin
|
7
|
+
db = SQLite3::Database.new(File.expand_path("~/.code_review_notifier/data.db"))
|
8
|
+
migrate_if_needed(db)
|
9
|
+
db
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.migrate_if_needed(db)
|
14
|
+
db.execute("CREATE TABLE IF NOT EXISTS code_change_activity_notified (id VARCHAR(50) PRIMARY KEY, notified_at TIMESTAMP);")
|
15
|
+
db.execute("CREATE TABLE IF NOT EXISTS settings (key VARCHAR(50) PRIMARY KEY, value TEXT, salt TEXT);")
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.execute(sql)
|
19
|
+
db.execute(sql)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.query_single_row(sql)
|
23
|
+
db.execute(sql) do |row|
|
24
|
+
return row
|
25
|
+
end
|
26
|
+
return nil
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.save_setting(key, value, is_secret:)
|
30
|
+
salt = "NULL"
|
31
|
+
if is_secret
|
32
|
+
salt, encrypted = Cipher.encrypt(value)
|
33
|
+
salt = "'#{salt}'"
|
34
|
+
value = encrypted
|
35
|
+
end
|
36
|
+
db.execute("DELETE FROM settings WHERE key = '#{key}';")
|
37
|
+
db.execute("INSERT INTO settings (key, value, salt) VALUES('#{key}', '#{value}', #{salt});")
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.get_setting(key)
|
41
|
+
row = query_single_row("SELECT value, salt FROM settings WHERE key = '#{key}'")
|
42
|
+
if row && row[1]
|
43
|
+
Cipher.decrypt(row[0], row[1])
|
44
|
+
elsif row
|
45
|
+
row[0]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/gerrit_api.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require_relative "./base_api.rb"
|
2
|
+
require_relative "./models/code_change.rb"
|
3
|
+
require_relative "./models/code_change_activity.rb"
|
4
|
+
|
5
|
+
class GerritApi < BaseApi
|
6
|
+
def self.authenticate
|
7
|
+
res = post("/login/", {
|
8
|
+
body: "username=#{username}&password=#{URI.escape(password)}&rememberme=1",
|
9
|
+
headers: {
|
10
|
+
"Content-Type" => "application/x-www-form-urlencoded",
|
11
|
+
},
|
12
|
+
follow_redirects: false
|
13
|
+
})
|
14
|
+
@token = res.headers["set-cookie"].match("GerritAccount=(.*?);")[1]
|
15
|
+
raise "Your username or password is incorrect. Trying running `code_review_notifier --setup` again." if @token.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.all_code_changes
|
19
|
+
wrap_with_authentication do
|
20
|
+
get("/changes/?S=0&q=is%3Aopen%20owner%3Aself%20-is%3Awip%20-is%3Aignored%20limit%3A25&q=is%3Aopen%20owner%3Aself%20is%3Awip%20limit%3A25&q=is%3Aopen%20-owner%3Aself%20-is%3Awip%20-is%3Aignored%20reviewer%3Aself%20limit%3A25&q=is%3Aclosed%20-is%3Aignored%20%28-is%3Awip%20OR%20owner%3Aself%29%20%28owner%3Aself%20OR%20reviewer%3Aself%29%20-age%3A4w%20limit%3A10&o=DETAILED_ACCOUNTS&o=MESSAGES", {
|
21
|
+
headers: {
|
22
|
+
"Cookie" => "GerritAccount=#{@token};"
|
23
|
+
}
|
24
|
+
})
|
25
|
+
end.parsed_response.flat_map { |js| js.map { |j| code_change_from_json(j) } }
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.code_change_from_json(json)
|
29
|
+
code_change = CodeChange.new(json["_number"].to_s, json["owner"]["name"], json["project"], json["subject"], Time.parse(json["updated"]).to_i)
|
30
|
+
code_change.code_change_activity = json["messages"].map { |m| code_change_activity_from_json(code_change, m) }
|
31
|
+
code_change
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.code_change_activity_from_json(code_change, json)
|
35
|
+
CodeChangeActivity.new(json["id"].to_s, json["author"]["name"], json["author"]["_account_id"].to_s == account_id, json["message"], Time.parse(json["date"]), code_change)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.favicon
|
39
|
+
"#{base_api_url}/favicon.ico"
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.code_change_url(code_change)
|
43
|
+
"#{base_api_url}/c/#{code_change.id}"
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require "date"
|
2
|
+
require_relative "../database.rb"
|
3
|
+
|
4
|
+
class CodeChange
|
5
|
+
attr_accessor :code_change_activity
|
6
|
+
attr_reader :id, :owner, :project, :subject, :updated_at
|
7
|
+
attr_writer :activity_from_self_at
|
8
|
+
|
9
|
+
def initialize(id, owner, project, subject, updated_at)
|
10
|
+
@id = id
|
11
|
+
@owner = owner
|
12
|
+
@project = project
|
13
|
+
@subject = subject.gsub("'", "")
|
14
|
+
@updated_at = updated_at
|
15
|
+
end
|
16
|
+
|
17
|
+
def activity_from_self_at
|
18
|
+
raise "@activity_from_self_at is not set" unless instance_variable_defined?("@activity_from_self_at")
|
19
|
+
@activity_from_self_at
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
MESSAGES_TO_IGNORE = [/Uploaded patch set 1/, /Build Started/, /owns \d+% of/]
|
2
|
+
AUTHOR_TRANSLATIONS = {
|
3
|
+
"Service Cloud Jenkins" => "Jenkins",
|
4
|
+
/\w+ Gergich \(Bot\)/ => "Gergich"
|
5
|
+
}
|
6
|
+
|
7
|
+
class CodeChangeActivity
|
8
|
+
attr_reader :id, :author, :is_self, :message, :created_at, :code_change
|
9
|
+
|
10
|
+
def initialize(id, author, is_self, message, created_at, code_change)
|
11
|
+
@id = id
|
12
|
+
@author = CodeChangeActivity.translate_author(author)
|
13
|
+
@is_self = is_self
|
14
|
+
@message = CodeChangeActivity.translate_message(message)
|
15
|
+
@created_at = created_at
|
16
|
+
@code_change = code_change
|
17
|
+
end
|
18
|
+
|
19
|
+
def notified
|
20
|
+
DB.execute("INSERT INTO code_change_activity_notified (id, notified_at) VALUES('#{id}', CURRENT_TIMESTAMP);")
|
21
|
+
end
|
22
|
+
|
23
|
+
def should_notify?
|
24
|
+
!is_self &&
|
25
|
+
code_change.activity_from_self_at && created_at > code_change.activity_from_self_at &&
|
26
|
+
MESSAGES_TO_IGNORE.none? { |m| message.match(m) } &&
|
27
|
+
!DB.query_single_row("SELECT id FROM code_change_activity_notified WHERE id = '#{id}'")
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.translate_author(author)
|
31
|
+
AUTHOR_TRANSLATIONS.keys.each do |pattern|
|
32
|
+
author.sub!(pattern, AUTHOR_TRANSLATIONS[pattern])
|
33
|
+
end
|
34
|
+
author
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.translate_message(message)
|
38
|
+
message.sub(/^Patch Set \d+:\s+/, "")
|
39
|
+
.gsub("'", %q(\\\\\\\\'))
|
40
|
+
.gsub("\n", " ")
|
41
|
+
.gsub(" ", " ")
|
42
|
+
.gsub(">", "")
|
43
|
+
.sub(/^\(/, "\\(")
|
44
|
+
.sub(/^\[/, "\\[")
|
45
|
+
.sub(/^-/, "\\-")
|
46
|
+
end
|
47
|
+
end
|
data/lib/notifier.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative './api.rb'
|
2
|
+
|
3
|
+
class Notifier
|
4
|
+
def self.notify(code_change_activity)
|
5
|
+
code_change = code_change_activity.code_change
|
6
|
+
id = code_change.id
|
7
|
+
owner = code_change.owner
|
8
|
+
subject = code_change.subject
|
9
|
+
|
10
|
+
message = code_change_activity.message
|
11
|
+
author = code_change_activity.author
|
12
|
+
system("terminal-notifier -title '#{author}' -subtitle '#{owner}: #{subject}' -message '#{message}' -appIcon #{Api.current_api.favicon} -open '#{Api.current_api.code_change_url(code_change)}'")
|
13
|
+
end
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: code_review_notifier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kyle Grinstead
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-07-07 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description:
|
14
|
+
email: kyleag@hey.com
|
15
|
+
executables:
|
16
|
+
- code_review_notifier
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- Brewfile
|
21
|
+
- Gemfile
|
22
|
+
- bin/code_review_notifier
|
23
|
+
- lib/api.rb
|
24
|
+
- lib/base_api.rb
|
25
|
+
- lib/cipher.rb
|
26
|
+
- lib/code_review_notifier.rb
|
27
|
+
- lib/database.rb
|
28
|
+
- lib/gerrit_api.rb
|
29
|
+
- lib/models/code_change.rb
|
30
|
+
- lib/models/code_change_activity.rb
|
31
|
+
- lib/notifier.rb
|
32
|
+
homepage: https://rubygems.org/gems/code_review_notifier
|
33
|
+
licenses:
|
34
|
+
- MIT
|
35
|
+
metadata:
|
36
|
+
source_code_uri: https://github.com/MrGrinst/code_review_notifier
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options: []
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
requirements: []
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 2.7.6.2
|
54
|
+
signing_key:
|
55
|
+
specification_version: 4
|
56
|
+
summary: Get notifications when updates happen to patch sets/pull requests!
|
57
|
+
test_files: []
|