code_review_notifier 0.1.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.
- 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: []
|