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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9aafb19505a577e0a92f20d17a00c462967940d5
4
- data.tar.gz: aff86cd29722fee471ad48976485d68d67a616cd
3
+ metadata.gz: 165051f76455d5a9922ab89df1c866abe8f655aa
4
+ data.tar.gz: 4549c773dfb062ca0bbf472d0b096fe897ad4b28
5
5
  SHA512:
6
- metadata.gz: d9ed3803b8e8ab13f029b66a81ca19e0010b630d5f96d9462be464cc2e32fefebafd6741512a1866d5ea6f791511ac2ba2163662c5490fdbe881ded47f72f467
7
- data.tar.gz: 889aa7a2ae8ee3020188897be7dd79196770a41cfb1a5c4d5e691315e50fb435f4a33bcb6ebc4b03c282002121863024901f9f879c75758e294b341dc627372f
6
+ metadata.gz: 4d9357bda24f78bbe23a47fcfe74dca8c59604eea493e98a0b7493d9d3495f4e816b9e4909ea05bb1b605b9e16eec0ca5bdd7b28243d9aead462e6fe466f695a
7
+ data.tar.gz: 830abd72bb43db5ddbd6e5c45a6439522c2941e92ef6c386761463bdf869bc445220e672c25acc47ec926f8c34d13acff07bbfff4ea1c141e0b08f66f0b8ebb3
@@ -1,4 +1,7 @@
1
1
  language: ruby
2
+ branches:
3
+ only:
4
+ - master
2
5
  rvm:
3
6
  - 2.1
4
7
  - 2.2
@@ -0,0 +1,9 @@
1
+ ### Unreleased
2
+ [Full Changelog](http://github.com/chrisarcand/dug/compare/v1.0.0...master)
3
+
4
+ Nothing yet!
5
+
6
+ ### 1.0.0 / 2016-02-11
7
+ [Full Changelog](http://github.com/chrisarcand/dug/compare/65eec3580879c3d4dc27bb6d6ed7716f965ec6ff...v1.0.0)
8
+
9
+ 1.0!
data/Gemfile CHANGED
@@ -1,8 +1,3 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in dug.gemspec
4
3
  gemspec
5
-
6
- group :test do
7
- gem "codeclimate-test-reporter", require: nil
8
- end
data/README.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # Dug
2
2
  [![Gem Version](https://badge.fury.io/rb/dug.svg)](https://badge.fury.io/rb/dug)
3
3
  [![Build Status](https://travis-ci.org/chrisarcand/dug.svg?branch=master)](https://travis-ci.org/chrisarcand/dug)
4
- [![Code Climate](https://codeclimate.com/github/chrisarcand/dug/badges/gpa.svg)](https://codeclimate.com/github/chrisarcand/dug)
5
4
  [![Test Coverage](https://codeclimate.com/github/chrisarcand/dug/badges/coverage.svg)](https://codeclimate.com/github/chrisarcand/dug/coverage)
5
+ [![Code Climate](https://codeclimate.com/github/chrisarcand/dug/badges/gpa.svg)](https://codeclimate.com/github/chrisarcand/dug)
6
+ [![License](http://img.shields.io/:license-mit-blue.svg?style=flat)](http://chrisarcand.mit-license.org)
6
7
 
7
- Created [out of frustration.](http://chrisarcand.com/introducing-dug/) _"[D]amn yo[u], [G]mail!"_
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
- ![](http://screenshots.chrisarcand.com/permxqkc5.jpg)
17
-
18
- ## Quick Installation
17
+ ![](http://screenshots.chrisarcand.com/permd0u3k.jpg)
19
18
 
20
- Dug is meant to be stupid simple. It's practically a gemified script with a
21
- configurable API. As such, you can programmatically configure and execute Dug's
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
- Basic installation steps:
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
- - rails
39
- - name: rspec
40
- label: RSpec
41
- repositories:
42
- - name: rspec-expectations
43
- label: RSpec/rspec-expectations
44
- - ManageIQ
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
- label: Participating
49
- comment:
50
- label: Participating
51
- mention:
52
- label: Mentioned by name
53
- team_mention:
54
- label: Team mention
55
- assign:
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
- * Label all notifications from the organization `rails` with the label `rails`, `rspec` with `RSpec`, `ManageIQ` with `ManageIQ`.
61
- * Label all notifications from the repository `rspec-expectations` with the label `RSpec/rspec-expectations`
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
- # This filter is optional but highly recommended.
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 time your run this you will be given a link to
102
- visit in your browser to sign in to Gmail verify via a one time token.
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 (this is what I do, 60 second polling). Or deploy Dug in a web application. Or even
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.
@@ -3,12 +3,5 @@
3
3
  require "bundler/setup"
4
4
  require "dug"
5
5
 
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
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
@@ -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{[D]amn yo[u], [G]mail. A gemified script to organize your GitHub notification emails using a simple configuration file in ways Gmail filters can't, such as X-GitHub-Reason headers.}
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
@@ -1,49 +1,20 @@
1
1
  require 'yaml'
2
+ require 'active_support/inflector'
2
3
 
3
4
  module Dug
4
5
  class Configurator
5
- LABEL_RULE_TYPES = %i(organization repository reason)
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
- self.label_rules = { "organizations" => {}, "reasons" => {} }
15
- end
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
- private
77
-
78
- attr_accessor :label_rules
51
+ def label_for(type, name, opts={})
52
+ type = type.to_s
53
+ run_validations(type, name)
79
54
 
80
- def load_rules
81
- file = YAML.load_file(rule_file)
82
-
83
- file["organizations"].each do |org|
84
- case org
85
- when String
86
- set_organization_rule(org)
87
- when Hash
88
- set_organization_rule(org["name"], label: org["label"])
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 validate_label_type(type)
104
- unless LABEL_RULE_TYPES.include?(type)
105
- raise ConfigurationError, "'#{type}' is not a valid label rule type. Valid types: #{}"
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
- def validate_reason(reason)
110
- unless GITHUB_REASONS.include?(reason)
111
- raise ConfigurationError, "'#{reason}' is not a valid GitHub notification reason. Valid reasons include: #{GITHUB_REASONS.map { |x| "'#{x}'" }.join(', ')}"
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
- def organizations
116
- label_rules["organizations"]
98
+ @label_rules
117
99
  end
118
100
 
119
- def reasons
120
- label_rules["reasons"]
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
@@ -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
- modify_message_request(*args) do |request, ids|
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
- modify_message_request(*args) do |request, ids|
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 modify_message_request(message, label_names)
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 = Google::Apis::GmailV1::ModifyMessageRequest.new
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
- modify_message('me', message.id, request)
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
 
@@ -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
- puts "[#{level.to_s.upcase}] #{Time.now} - #{message}"
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[1]
10
+ list_match(1)
11
11
  end
12
12
 
13
13
  def repository
14
- list_match[2]
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
@@ -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
- process_message(message.id)
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)["GitHub/Unprocessed"]
63
- @unprocessed_notifications ||= servicer
64
- .list_user_messages('me', label_ids: [unprocessed_label.id])
65
- .messages
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
+
@@ -1,3 +1,3 @@
1
1
  module Dug
2
- VERSION = "0.2.1"
2
+ VERSION = "1.0.0"
3
3
  end
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.2.1
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-02 00:00:00.000000000 Z
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
- description: "[D]amn yo[u], [G]mail. A gemified script to organize your GitHub notification
70
- emails using a simple configuration file in ways Gmail filters can't, such as X-GitHub-Reason
71
- headers."
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: