dug 0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: