dug 0.2.1 → 1.0.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 +4 -4
- data/.travis.yml +3 -0
- data/CHANGELOG.md +9 -0
- data/Gemfile +0 -5
- data/README.md +56 -63
- data/bin/console +2 -9
- data/dug.gemspec +7 -1
- data/lib/dug.rb +6 -0
- data/lib/dug/configurator.rb +51 -69
- data/lib/dug/gmail_servicer.rb +19 -6
- data/lib/dug/logger.rb +6 -1
- data/lib/dug/message_processor.rb +70 -0
- data/lib/dug/notification_decorator.rb +24 -4
- data/lib/dug/runner.rb +13 -32
- data/lib/dug/validations.rb +38 -0
- data/lib/dug/version.rb +1 -1
- metadata +92 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 165051f76455d5a9922ab89df1c866abe8f655aa
|
|
4
|
+
data.tar.gz: 4549c773dfb062ca0bbf472d0b096fe897ad4b28
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4d9357bda24f78bbe23a47fcfe74dca8c59604eea493e98a0b7493d9d3495f4e816b9e4909ea05bb1b605b9e16eec0ca5bdd7b28243d9aead462e6fe466f695a
|
|
7
|
+
data.tar.gz: 830abd72bb43db5ddbd6e5c45a6439522c2941e92ef6c386761463bdf869bc445220e672c25acc47ec926f8c34d13acff07bbfff4ea1c141e0b08f66f0b8ebb3
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Dug
|
|
2
2
|
[](https://badge.fury.io/rb/dug)
|
|
3
3
|
[](https://travis-ci.org/chrisarcand/dug)
|
|
4
|
-
[](https://codeclimate.com/github/chrisarcand/dug)
|
|
5
4
|
[](https://codeclimate.com/github/chrisarcand/dug/coverage)
|
|
5
|
+
[](https://codeclimate.com/github/chrisarcand/dug)
|
|
6
|
+
[](http://chrisarcand.mit-license.org)
|
|
6
7
|
|
|
7
|
-
Created
|
|
8
|
+
Created out of frustration. _"[D]amn yo[u], [G]mail!"_
|
|
8
9
|
|
|
9
10
|
**Dug is a simple, configurable gem to organize your GitHub notification emails
|
|
10
11
|
in ways Gmail can't and in an easier-to-maintain way than large, slow Google
|
|
@@ -13,16 +14,12 @@ people usually do with Gmail filters (label by organization name, repository
|
|
|
13
14
|
name) as well as parse GitHub's custom X headers to label your messages with
|
|
14
15
|
things like "Mentioned by name", "Assigned to me", "Commented", etc.
|
|
15
16
|
|
|
16
|
-

|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
runner class in any way you see fit (within a web app hook, a Rake task, a script,
|
|
23
|
-
whatever).
|
|
19
|
+
You can read more about the reasoning behind dug and why it's superior compared to other labeling methods in [my post
|
|
20
|
+
introducing this project.](http://chrisarcand.com/introducing-dug/)
|
|
24
21
|
|
|
25
|
-
|
|
22
|
+
## Quick Installation
|
|
26
23
|
|
|
27
24
|
1. **Install Dug**
|
|
28
25
|
|
|
@@ -35,45 +32,67 @@ Basic installation steps:
|
|
|
35
32
|
```
|
|
36
33
|
---
|
|
37
34
|
organizations:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
rails: Rails
|
|
36
|
+
rspec: RSpec
|
|
37
|
+
ManageIQ: ManageIQ
|
|
38
|
+
|
|
39
|
+
repositories:
|
|
40
|
+
rspec-expectations: RSpec/rspec-expectations
|
|
41
|
+
dug: dug
|
|
42
|
+
dotfiles:
|
|
43
|
+
- remote: chrisarcand
|
|
44
|
+
label: My dotfiles
|
|
45
|
+
- remote: juliancheal
|
|
46
|
+
label: Julian's dotfiles
|
|
45
47
|
|
|
46
48
|
reasons:
|
|
47
|
-
author:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
label: Assigned to me
|
|
49
|
+
author: Participating
|
|
50
|
+
comment: Participating
|
|
51
|
+
mention: Mentioned
|
|
52
|
+
team_mention: Team mention
|
|
53
|
+
assign: Assigned to me
|
|
54
|
+
|
|
55
|
+
states:
|
|
56
|
+
merged: Merged
|
|
57
|
+
closed: Closed
|
|
57
58
|
```
|
|
58
59
|
|
|
59
60
|
The above rule file will:
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
|
|
62
|
+
**For organizations...**
|
|
63
|
+
|
|
64
|
+
* Label notifications from the organization `rails` with the label `rails`, `rspec` with `RSpec`, `ManageIQ` with `ManageIQ`.
|
|
65
|
+
|
|
66
|
+
**For repositories...**
|
|
67
|
+
|
|
68
|
+
* Label notifications from the repository `rspec-expectations` with the label `RSpec/rspec-expectations`, `dug` with `dug`.
|
|
69
|
+
* Label notifications from chrisarcand's `dotfiles` repository with the label `My dotfiles`, juliancheal's with `Julian's dotfiles`.
|
|
70
|
+
|
|
71
|
+
**For the reason you're being notified...**
|
|
72
|
+
|
|
62
73
|
* Label notifications with `Participating` if I am the author of the Issue/PR or if I commented on it.
|
|
63
74
|
* Label notifications with `Mentioned by name` if I'm directly mentioned in it.
|
|
64
75
|
* Label notifications with `Team mention` if a team I am a part of is mentioned in it.
|
|
65
76
|
* Label notifications with `Assigned to me` if the Issue/PR is assigned to me.
|
|
66
77
|
|
|
78
|
+
**For state changes to the Issue/PR...**
|
|
79
|
+
|
|
80
|
+
* Label notifications that signal the issue as closed with `Closed`. This label will be automatically removed if the issue is reopened!
|
|
81
|
+
* Label notifications that signal the issue as merged with `Merged`
|
|
82
|
+
|
|
67
83
|
3. **Configure Gmail.** In Gmail...
|
|
68
84
|
|
|
69
85
|
* Create the label "GitHub" and then "Unprocessed" nested underneath it (will show up as "GitHub/Unprocessed").
|
|
70
86
|
* Create all of the labels in the preceding step if you don't have them already.
|
|
71
|
-
* Set up the following filters
|
|
87
|
+
* Set up the following filters. The 'GitHub/Unprocessed' label is the only required part, but I recommend you
|
|
88
|
+
skip your inbox or you will be pinged with GitHub notifications _a lot_.
|
|
72
89
|
```
|
|
73
90
|
Matches: from:(notifications@github.com)
|
|
74
|
-
Do this: Apply label "GitHub/Unprocessed"
|
|
91
|
+
Do this: Apply label "GitHub/Unprocessed", Skip Inbox
|
|
75
92
|
|
|
76
|
-
#
|
|
93
|
+
# Optionally, you may want to *remove the Skip Inbox from the previous filter* and
|
|
94
|
+
# add this one. This means messages directed at you go to your Inbox (setting
|
|
95
|
+
# off a notification) and the rest going straight to your archive (no notification).
|
|
77
96
|
Matches: from:(notifications@github.com) -{cc: youremail@example.non}
|
|
78
97
|
Do this: Skip Inbox
|
|
79
98
|
```
|
|
@@ -98,14 +117,16 @@ Basic installation steps:
|
|
|
98
117
|
Dug::Runner.run
|
|
99
118
|
```
|
|
100
119
|
|
|
101
|
-
6. **Run the script** and watch your notifications get organized! The first
|
|
102
|
-
|
|
120
|
+
6. **Run the script** and watch your notifications get organized! The first
|
|
121
|
+
time you run this you will be given a link to visit in your browser to sign
|
|
122
|
+
in to Gmail verify via a one time token. Also note each call to `#run`
|
|
123
|
+
processes 100 unprocessed notifications at a time.
|
|
103
124
|
|
|
104
125
|
```
|
|
105
126
|
$ ruby script.rb
|
|
106
127
|
```
|
|
107
128
|
|
|
108
|
-
7. Set a cron and forget about it
|
|
129
|
+
7. Set a cron and forget about it. I do, on a private VPS, with 60 second polling. Or deploy Dug in a web application. Or even
|
|
109
130
|
just write a loop in your script `loop do; Dug::Runner.run; sleep 60; end`. How you run it is completely up to you,
|
|
110
131
|
and really doesn't matter.
|
|
111
132
|
|
|
@@ -159,40 +180,12 @@ Dug.configure do |config|
|
|
|
159
180
|
end
|
|
160
181
|
```
|
|
161
182
|
|
|
162
|
-
### More configuration options
|
|
163
|
-
|
|
164
|
-
You can use public methods in Dug's configuration class to programmatically
|
|
165
|
-
configure Dug without a Rules YAML file. As a documenting example, here is the
|
|
166
|
-
same scenario in the YAML file used previously in this README but within a config
|
|
167
|
-
block:
|
|
168
|
-
|
|
169
|
-
```ruby
|
|
170
|
-
Dug.configure do |config|
|
|
171
|
-
config.set_organization_rule('rails')
|
|
172
|
-
config.set_organization_rule('rspec', label: 'RSpec')
|
|
173
|
-
config.set_organization_rule('ManageIQ')
|
|
174
|
-
|
|
175
|
-
config.set_repository_rule('rspec-expectations', label: 'RSpec/rspec-expectations')
|
|
176
|
-
|
|
177
|
-
config.set_reason_rule('author', label: 'Participating')
|
|
178
|
-
config.set_reason_rule('comment', label: 'Participating')
|
|
179
|
-
config.set_reason_rule('mention', label: 'Mention by name')
|
|
180
|
-
config.set_reason_rule('team_mention', label: 'Team mention')
|
|
181
|
-
end
|
|
182
|
-
```
|
|
183
|
-
|
|
184
183
|
## Development
|
|
185
184
|
|
|
186
185
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
|
187
186
|
`rake test` to run the tests. You can also run `bin/console` for an interactive
|
|
188
187
|
prompt that will allow you to experiment.
|
|
189
188
|
|
|
190
|
-
## TODO
|
|
191
|
-
* Add option to parse message body for labelling
|
|
192
|
-
* Add more built-in reasons such as "Closed" statuses
|
|
193
|
-
* The Google Auth Library has an API for Redis that could be supported easily.
|
|
194
|
-
* Moar tests, plz
|
|
195
|
-
|
|
196
189
|
## Contributing
|
|
197
190
|
|
|
198
191
|
Bug reports and pull requests are welcome on GitHub at https://github.com/chrisarcand/dug.
|
data/bin/console
CHANGED
|
@@ -3,12 +3,5 @@
|
|
|
3
3
|
require "bundler/setup"
|
|
4
4
|
require "dug"
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
|
10
|
-
# require "pry"
|
|
11
|
-
# Pry.start
|
|
12
|
-
|
|
13
|
-
require "irb"
|
|
14
|
-
IRB.start
|
|
6
|
+
require "pry"
|
|
7
|
+
Pry.start
|
data/dug.gemspec
CHANGED
|
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
spec.email = ["chris@chrisarcand.com"]
|
|
11
11
|
|
|
12
12
|
spec.summary = %q{[D]amn yo[u], [G]mail. A gem to organize your GitHub notification emails in ways Gmail filters can't.}
|
|
13
|
-
spec.description = %q{
|
|
13
|
+
spec.description = %q{A simple, configurable gem to organize your GitHub notification emails in ways Gmail can't and in an easier-to-maintain way than large, slow Google Apps Scripts.}
|
|
14
14
|
spec.homepage = "https://github.com/chrisarcand/dug"
|
|
15
15
|
spec.license = "MIT"
|
|
16
16
|
|
|
@@ -20,8 +20,14 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
spec.require_paths = ["lib"]
|
|
21
21
|
|
|
22
22
|
spec.add_dependency "google-api-client", "~> 0.9"
|
|
23
|
+
spec.add_dependency "activesupport", ">= 2.2.1"
|
|
23
24
|
|
|
24
25
|
spec.add_development_dependency "bundler", "~> 1.11"
|
|
25
26
|
spec.add_development_dependency "rake", "~> 10.0"
|
|
27
|
+
spec.add_development_dependency "pry", "~> 0"
|
|
26
28
|
spec.add_development_dependency "minitest", "~> 5.0"
|
|
29
|
+
spec.add_development_dependency "mocha", "~> 1.0"
|
|
30
|
+
spec.add_development_dependency "minitest-reporters", "~> 1.0"
|
|
31
|
+
spec.add_development_dependency "codeclimate-test-reporter", "~> 0"
|
|
32
|
+
spec.add_development_dependency "simplecov", "~> 0"
|
|
27
33
|
end
|
data/lib/dug.rb
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
require "dug/version"
|
|
2
|
+
require "dug/validations"
|
|
2
3
|
require "dug/configurator"
|
|
3
4
|
require "dug/gmail_servicer"
|
|
4
5
|
require "dug/logger"
|
|
6
|
+
require "dug/message_processor"
|
|
5
7
|
require "dug/notification_decorator"
|
|
6
8
|
require "dug/runner"
|
|
7
9
|
|
|
8
10
|
module Dug
|
|
11
|
+
LABEL_RULE_TYPES = %w(organization repository reason state)
|
|
12
|
+
GITHUB_REASONS = %w(author comment mention team_mention state_change assign manual subscribed)
|
|
13
|
+
ISSUE_STATES = %(merged closed reopened)
|
|
14
|
+
|
|
9
15
|
def self.authorize!
|
|
10
16
|
Dug::GmailServicer.new.authorize!
|
|
11
17
|
end
|
data/lib/dug/configurator.rb
CHANGED
|
@@ -1,49 +1,20 @@
|
|
|
1
1
|
require 'yaml'
|
|
2
|
+
require 'active_support/inflector'
|
|
2
3
|
|
|
3
4
|
module Dug
|
|
4
5
|
class Configurator
|
|
5
|
-
|
|
6
|
-
GITHUB_REASONS = %w(author comment mention team_mention state_change assign manual subscribed)
|
|
6
|
+
include Dug::Validations
|
|
7
7
|
|
|
8
8
|
attr_accessor :client_id
|
|
9
9
|
attr_accessor :client_secret
|
|
10
10
|
attr_accessor :token_store
|
|
11
11
|
attr_accessor :application_credentials_file
|
|
12
|
+
attr_accessor :unprocessed_label_name
|
|
12
13
|
|
|
13
14
|
def initialize
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def set_organization_rule(name, label: nil)
|
|
18
|
-
organizations[name] ||= { "repositories" => {} }
|
|
19
|
-
organizations[name]["label"] = label || name
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def set_repository_rule(name, organization:, label: nil)
|
|
23
|
-
organizations[organization] ||= { "repositories" => {} }
|
|
24
|
-
organizations[organization]["repositories"][name] ||= {}
|
|
25
|
-
organizations[organization]["repositories"][name]["label"] = label || name
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def set_reason_rule(name, label: nil)
|
|
29
|
-
validate_reason(name)
|
|
30
|
-
reasons[name] ||= {}
|
|
31
|
-
reasons[name]["label"] = label || name
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def label_for(type, name, organization: nil)
|
|
35
|
-
validate_label_type(type)
|
|
36
|
-
case type
|
|
37
|
-
when :organization
|
|
38
|
-
organizations.fetch(name, {})["label"]
|
|
39
|
-
when :repository
|
|
40
|
-
raise ArgumentError, "Repository label rules require an organization to be specified" unless organization
|
|
41
|
-
organizations.fetch(organization, {})
|
|
42
|
-
.fetch("repositories", {})
|
|
43
|
-
.fetch(name, {})["label"]
|
|
44
|
-
when :reason
|
|
45
|
-
validate_reason(name)
|
|
46
|
-
reasons.fetch(name, {})["label"]
|
|
15
|
+
@label_rules = {}
|
|
16
|
+
LABEL_RULE_TYPES.each do |type|
|
|
17
|
+
label_rules[type.pluralize] = {}
|
|
47
18
|
end
|
|
48
19
|
end
|
|
49
20
|
|
|
@@ -63,6 +34,10 @@ module Dug
|
|
|
63
34
|
ENV['TOKEN_STORE_PATH'] || @token_store || File.join(Dir.home, ".dug", "authorization.yaml")
|
|
64
35
|
end
|
|
65
36
|
|
|
37
|
+
def unprocessed_label_name
|
|
38
|
+
@unprocessed_label_name || "GitHub/Unprocessed"
|
|
39
|
+
end
|
|
40
|
+
|
|
66
41
|
def rule_file
|
|
67
42
|
@rule_file
|
|
68
43
|
end
|
|
@@ -73,53 +48,60 @@ module Dug
|
|
|
73
48
|
@rule_file
|
|
74
49
|
end
|
|
75
50
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
51
|
+
def label_for(type, name, opts={})
|
|
52
|
+
type = type.to_s
|
|
53
|
+
run_validations(type, name)
|
|
79
54
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if repos = org["repositories"]
|
|
90
|
-
repos.each do |repo|
|
|
91
|
-
set_repository_rule(repo["name"], organization: org["name"], label: repo["label"])
|
|
92
|
-
end
|
|
55
|
+
rule = label_rules[type.pluralize][name]
|
|
56
|
+
case rule
|
|
57
|
+
when String, nil
|
|
58
|
+
rule
|
|
59
|
+
when Array
|
|
60
|
+
if type == 'repository'
|
|
61
|
+
raise ArgumentError, "Multiple remotes possible and no remote specified" unless opts.keys.include?(:remote)
|
|
62
|
+
rule = rule.detect do |r|
|
|
63
|
+
r['remote'] == opts[:remote]
|
|
93
64
|
end
|
|
65
|
+
rule['label'] if rule
|
|
94
66
|
end
|
|
95
67
|
end
|
|
96
|
-
|
|
97
|
-
file["reasons"].keys.each do |reason|
|
|
98
|
-
validate_reason(reason)
|
|
99
|
-
set_reason_rule(reason, label: file["reasons"][reason]["label"])
|
|
100
|
-
end
|
|
101
68
|
end
|
|
102
69
|
|
|
103
|
-
def
|
|
104
|
-
|
|
105
|
-
|
|
70
|
+
def _clear!
|
|
71
|
+
%w(
|
|
72
|
+
application_credentials_file
|
|
73
|
+
client_id
|
|
74
|
+
client_secret
|
|
75
|
+
token_store
|
|
76
|
+
unprocessed_label_name
|
|
77
|
+
rule_file
|
|
78
|
+
).each do |var|
|
|
79
|
+
instance_variable_set("@#{var}", nil)
|
|
106
80
|
end
|
|
81
|
+
|
|
82
|
+
initialize
|
|
107
83
|
end
|
|
108
84
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
attr_accessor :label_rules
|
|
88
|
+
|
|
89
|
+
def load_rules
|
|
90
|
+
# TODO should validate incoming YAML
|
|
91
|
+
loaded_rules = YAML.load_file(rule_file)
|
|
92
|
+
|
|
93
|
+
LABEL_RULE_TYPES.each do |type|
|
|
94
|
+
type = type.pluralize
|
|
95
|
+
@label_rules[type] = loaded_rules[type] if loaded_rules[type]
|
|
112
96
|
end
|
|
113
|
-
end
|
|
114
97
|
|
|
115
|
-
|
|
116
|
-
label_rules["organizations"]
|
|
98
|
+
@label_rules
|
|
117
99
|
end
|
|
118
100
|
|
|
119
|
-
def
|
|
120
|
-
|
|
101
|
+
def run_validations(type, name)
|
|
102
|
+
validate_rule_type!(type)
|
|
103
|
+
validate_reason!(name) if type == 'reason'
|
|
104
|
+
validate_state!(name) if type == 'state'
|
|
121
105
|
end
|
|
122
106
|
end
|
|
123
|
-
|
|
124
|
-
ConfigurationError = Class.new(StandardError)
|
|
125
107
|
end
|
data/lib/dug/gmail_servicer.rb
CHANGED
|
@@ -5,6 +5,7 @@ require 'fileutils'
|
|
|
5
5
|
require 'forwardable'
|
|
6
6
|
|
|
7
7
|
module Dug
|
|
8
|
+
# @private
|
|
8
9
|
class GmailServicer
|
|
9
10
|
extend Forwardable
|
|
10
11
|
|
|
@@ -14,7 +15,8 @@ module Dug
|
|
|
14
15
|
|
|
15
16
|
def_delegators :@gmail, :get_user_message,
|
|
16
17
|
:list_user_messages,
|
|
17
|
-
:modify_message
|
|
18
|
+
:modify_message,
|
|
19
|
+
:modify_thread
|
|
18
20
|
|
|
19
21
|
def initialize
|
|
20
22
|
@gmail = Google::Apis::GmailV1::GmailService.new
|
|
@@ -29,13 +31,13 @@ module Dug
|
|
|
29
31
|
end
|
|
30
32
|
|
|
31
33
|
def add_labels_by_name(*args)
|
|
32
|
-
|
|
34
|
+
modification_request(*args) do |request, ids|
|
|
33
35
|
request.add_label_ids = ids
|
|
34
36
|
end
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
def remove_labels_by_name(*args)
|
|
38
|
-
|
|
40
|
+
modification_request(*args) do |request, ids|
|
|
39
41
|
request.remove_label_ids = ids
|
|
40
42
|
end
|
|
41
43
|
end
|
|
@@ -73,7 +75,7 @@ module Dug
|
|
|
73
75
|
|
|
74
76
|
private
|
|
75
77
|
|
|
76
|
-
def
|
|
78
|
+
def modification_request(message, label_names, entire_thread: false)
|
|
77
79
|
ids = []
|
|
78
80
|
label_names.each do |name|
|
|
79
81
|
unless labels[name] && id = labels[name].id
|
|
@@ -81,9 +83,20 @@ module Dug
|
|
|
81
83
|
end
|
|
82
84
|
ids << id
|
|
83
85
|
end
|
|
84
|
-
request =
|
|
86
|
+
request =
|
|
87
|
+
if entire_thread
|
|
88
|
+
Google::Apis::GmailV1::ModifyThreadRequest.new
|
|
89
|
+
else
|
|
90
|
+
Google::Apis::GmailV1::ModifyMessageRequest.new
|
|
91
|
+
end
|
|
92
|
+
|
|
85
93
|
yield request, ids
|
|
86
|
-
|
|
94
|
+
|
|
95
|
+
if entire_thread
|
|
96
|
+
modify_thread('me', message.thread_id, request)
|
|
97
|
+
else
|
|
98
|
+
modify_message('me', message.id, request)
|
|
99
|
+
end
|
|
87
100
|
end
|
|
88
101
|
end
|
|
89
102
|
|
data/lib/dug/logger.rb
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
module Dug
|
|
2
|
+
# @private
|
|
2
3
|
module Logger
|
|
4
|
+
# TODO This obviously needs a lot of love
|
|
5
|
+
|
|
3
6
|
def log(message, level: :info)
|
|
4
|
-
|
|
7
|
+
unless $testing
|
|
8
|
+
puts "[#{level.to_s.upcase}] #{Time.now} - #{message}"
|
|
9
|
+
end
|
|
5
10
|
end
|
|
6
11
|
end
|
|
7
12
|
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
module Dug
|
|
2
|
+
# @private
|
|
3
|
+
class MessageProcessor
|
|
4
|
+
include Dug::Logger
|
|
5
|
+
|
|
6
|
+
def initialize(message_id, servicer)
|
|
7
|
+
@servicer = servicer
|
|
8
|
+
@message = NotificationDecorator.new(servicer.get_user_message('me', message_id))
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def execute
|
|
12
|
+
%w(organization repository reason).each do |type|
|
|
13
|
+
if message_data = message.public_send(type)
|
|
14
|
+
opts = type == 'repository' ? { remote: message.public_send(:organization) } : {}
|
|
15
|
+
label = Dug.configuration.label_for(type, message_data, opts)
|
|
16
|
+
labels_to_add << label if label
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
%w(merged closed).each do |state|
|
|
21
|
+
if message.public_send("indicates_#{state}?")
|
|
22
|
+
label = Dug.configuration.label_for(:state, state)
|
|
23
|
+
labels_to_add << label if label
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
if message.indicates_reopened?
|
|
28
|
+
label = Dug.configuration.label_for(:state, 'closed')
|
|
29
|
+
reopened_label = Dug.configuration.label_for(:state, 'reopened')
|
|
30
|
+
labels_to_remove << label if label
|
|
31
|
+
labels_to_add << reopened_label if reopened_label
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
info = "Processing message:"
|
|
35
|
+
info << "\n ID: #{message.id}"
|
|
36
|
+
%w(Date From Subject).each do |header|
|
|
37
|
+
info << "\n #{header}: #{message.headers[header]}"
|
|
38
|
+
end
|
|
39
|
+
info << "\n * Applying labels: #{labels_to_add.join(' | ')} *"
|
|
40
|
+
info << "\n * Removing labels: #{labels_to_remove.join(' | ')} *"
|
|
41
|
+
log(info)
|
|
42
|
+
|
|
43
|
+
servicer.add_labels_by_name(message, labels_to_add)
|
|
44
|
+
servicer.remove_labels_by_name(message,
|
|
45
|
+
labels_to_remove,
|
|
46
|
+
entire_thread: modify_entire_thread?)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
attr_reader :message
|
|
52
|
+
attr_reader :servicer
|
|
53
|
+
|
|
54
|
+
def labels_to_add
|
|
55
|
+
@labels_to_add ||= ["GitHub"]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def labels_to_remove
|
|
59
|
+
@labels_to_remove ||= [Dug.configuration.unprocessed_label_name]
|
|
60
|
+
if @labels_to_remove.size > 1
|
|
61
|
+
@modify_entire_thread = true
|
|
62
|
+
end
|
|
63
|
+
@labels_to_remove
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def modify_entire_thread?
|
|
67
|
+
!!@modify_entire_thread
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -7,11 +7,11 @@ module Dug
|
|
|
7
7
|
end
|
|
8
8
|
|
|
9
9
|
def organization
|
|
10
|
-
list_match
|
|
10
|
+
list_match(1)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def repository
|
|
14
|
-
list_match
|
|
14
|
+
list_match(2)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def reason
|
|
@@ -24,10 +24,30 @@ module Dug
|
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
def indicates_merged?
|
|
28
|
+
!!(plaintext_body =~ /^Merged #(?:\d+)\./)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def indicates_closed?
|
|
32
|
+
# Note: Purposely more lax than Merged
|
|
33
|
+
# Issues can be closed via PR/commit ie "Closed #123 via #456."
|
|
34
|
+
!!(plaintext_body =~ /^Closed #(?:\d+)/)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def indicates_reopened?
|
|
38
|
+
!!(plaintext_body =~ /^Reopened #(?:\d+)\./)
|
|
39
|
+
end
|
|
40
|
+
|
|
27
41
|
private
|
|
28
42
|
|
|
29
|
-
def list_match
|
|
30
|
-
headers["List-ID"].match(/^([\w\-_]+)\/([\w\-_]+)/)
|
|
43
|
+
def list_match(index)
|
|
44
|
+
headers["List-ID"] && headers["List-ID"].match(/^([\w\-_]+)\/([\w\-_]+)/)[index]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def plaintext_body
|
|
48
|
+
payload.parts.detect { |part|
|
|
49
|
+
part.mime_type == 'text/plain'
|
|
50
|
+
}.body.data
|
|
31
51
|
end
|
|
32
52
|
end
|
|
33
53
|
end
|
data/lib/dug/runner.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Dug
|
|
|
17
17
|
if unprocessed_notifications?
|
|
18
18
|
log("Processing #{unprocessed_notifications.size} GitHub notifications...")
|
|
19
19
|
unprocessed_notifications.each do |message|
|
|
20
|
-
|
|
20
|
+
Dug::MessageProcessor.new(message.id, servicer).execute
|
|
21
21
|
end
|
|
22
22
|
log("Finished processing #{unprocessed_notifications.size} GitHub notifications.")
|
|
23
23
|
else
|
|
@@ -27,42 +27,23 @@ module Dug
|
|
|
27
27
|
|
|
28
28
|
private
|
|
29
29
|
|
|
30
|
-
def process_message(id)
|
|
31
|
-
message = NotificationDecorator.new(servicer.get_user_message('me', id))
|
|
32
|
-
|
|
33
|
-
labels_to_add = ["GitHub"]
|
|
34
|
-
labels_to_remove = ["GitHub/Unprocessed"]
|
|
35
|
-
if message.reason
|
|
36
|
-
labels_to_add << Dug.configuration.label_for(:reason, message.reason)
|
|
37
|
-
end
|
|
38
|
-
labels_to_add << Dug.configuration.label_for(:organization, message.organization)
|
|
39
|
-
labels_to_add << Dug.configuration.label_for(:repository,
|
|
40
|
-
message.repository,
|
|
41
|
-
organization: message.organization)
|
|
42
|
-
labels_to_add.flatten! and labels_to_remove.flatten!
|
|
43
|
-
labels_to_add.compact! and labels_to_remove.compact!
|
|
44
|
-
|
|
45
|
-
info = "Processing message:"
|
|
46
|
-
info << "\n ID: #{message.id}"
|
|
47
|
-
%w(Date From Subject).each do |header|
|
|
48
|
-
info << "\n #{header}: #{message.headers[header]}"
|
|
49
|
-
end
|
|
50
|
-
info << "\n * Applying labels: #{labels_to_add.join(' | ')} *"
|
|
51
|
-
log(info)
|
|
52
|
-
|
|
53
|
-
servicer.add_labels_by_name(message, labels_to_add)
|
|
54
|
-
servicer.remove_labels_by_name(message, labels_to_remove)
|
|
55
|
-
end
|
|
56
|
-
|
|
57
30
|
def unprocessed_notifications(use_cache: true)
|
|
58
31
|
unless use_cache
|
|
59
32
|
log("Requesting latest emails from Gmail...")
|
|
60
33
|
@unprocessed_notifications = nil
|
|
61
34
|
end
|
|
62
|
-
unprocessed_label = servicer.labels(use_cache: use_cache)[
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
35
|
+
unprocessed_label = servicer.labels(use_cache: use_cache)[Dug.configuration.unprocessed_label_name]
|
|
36
|
+
|
|
37
|
+
# The reverse! is required because we want to process messages in order
|
|
38
|
+
# and Google doesn't allow you to sort by anything because labels. Order
|
|
39
|
+
# is required here to account for state changes like reopened issues
|
|
40
|
+
@unprocessed_notifications ||=
|
|
41
|
+
begin
|
|
42
|
+
messages = servicer
|
|
43
|
+
.list_user_messages('me', label_ids: [unprocessed_label.id])
|
|
44
|
+
.messages
|
|
45
|
+
messages.reverse! if messages
|
|
46
|
+
end
|
|
66
47
|
end
|
|
67
48
|
|
|
68
49
|
def unprocessed_notifications?
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module Dug
|
|
2
|
+
module Validations
|
|
3
|
+
def valid_rule_type?(type)
|
|
4
|
+
LABEL_RULE_TYPES.include?(type.to_s)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def validate_rule_type!(type)
|
|
8
|
+
unless valid_rule_type?(type)
|
|
9
|
+
raise InvalidRuleType, "'#{type}' is not a valid label rule type. Valid types: #{LABEL_RULE_TYPES}"
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def valid_reason?(reason)
|
|
14
|
+
GITHUB_REASONS.include?(reason.to_s)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def validate_reason!(reason)
|
|
18
|
+
unless valid_reason?(reason)
|
|
19
|
+
raise InvalidGitHubReason, "'#{reason}' is not a valid GitHub notification reason. Valid reasons include: #{GITHUB_REASONS.map { |x| "'#{x}'" }.join(', ')}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def valid_state?(state)
|
|
24
|
+
ISSUE_STATES.include?(state.to_s)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate_state!(state)
|
|
28
|
+
unless valid_state?(state)
|
|
29
|
+
raise InvalidIssueState, "'#{state}' is not a valid issue state. Valid reasons include: #{ISSUE_STATES.map { |x| "'#{x}'" }.join(', ')}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
InvalidRuleType = Class.new(StandardError)
|
|
35
|
+
InvalidGitHubReason = Class.new(StandardError)
|
|
36
|
+
InvalidIssueState = Class.new(StandardError)
|
|
37
|
+
end
|
|
38
|
+
|
data/lib/dug/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dug
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Arcand
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2016-02-
|
|
11
|
+
date: 2016-02-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: google-api-client
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0.9'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activesupport
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 2.2.1
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 2.2.1
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: bundler
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,6 +66,20 @@ dependencies:
|
|
|
52
66
|
- - "~>"
|
|
53
67
|
- !ruby/object:Gem::Version
|
|
54
68
|
version: '10.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: pry
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
55
83
|
- !ruby/object:Gem::Dependency
|
|
56
84
|
name: minitest
|
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,9 +94,65 @@ dependencies:
|
|
|
66
94
|
- - "~>"
|
|
67
95
|
- !ruby/object:Gem::Version
|
|
68
96
|
version: '5.0'
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: mocha
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '1.0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '1.0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: minitest-reporters
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '1.0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '1.0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: codeclimate-test-reporter
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - "~>"
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - "~>"
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: simplecov
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - "~>"
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: '0'
|
|
146
|
+
type: :development
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - "~>"
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0'
|
|
153
|
+
description: A simple, configurable gem to organize your GitHub notification emails
|
|
154
|
+
in ways Gmail can't and in an easier-to-maintain way than large, slow Google Apps
|
|
155
|
+
Scripts.
|
|
72
156
|
email:
|
|
73
157
|
- chris@chrisarcand.com
|
|
74
158
|
executables: []
|
|
@@ -77,6 +161,7 @@ extra_rdoc_files: []
|
|
|
77
161
|
files:
|
|
78
162
|
- ".gitignore"
|
|
79
163
|
- ".travis.yml"
|
|
164
|
+
- CHANGELOG.md
|
|
80
165
|
- Gemfile
|
|
81
166
|
- LICENSE.txt
|
|
82
167
|
- README.md
|
|
@@ -88,8 +173,10 @@ files:
|
|
|
88
173
|
- lib/dug/configurator.rb
|
|
89
174
|
- lib/dug/gmail_servicer.rb
|
|
90
175
|
- lib/dug/logger.rb
|
|
176
|
+
- lib/dug/message_processor.rb
|
|
91
177
|
- lib/dug/notification_decorator.rb
|
|
92
178
|
- lib/dug/runner.rb
|
|
179
|
+
- lib/dug/validations.rb
|
|
93
180
|
- lib/dug/version.rb
|
|
94
181
|
homepage: https://github.com/chrisarcand/dug
|
|
95
182
|
licenses:
|