issue-beaver 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.rspec +1 -0
- data/Gemfile +19 -0
- data/Gemfile.lock +72 -0
- data/README.md +38 -0
- data/Rakefile +16 -0
- data/bin/issuebeaver +5 -0
- data/issue-beaver.gemspec +18 -0
- data/lib/enumerable/merge.rb +24 -0
- data/lib/issue_beaver/grammars/ruby_comments.rb +9 -0
- data/lib/issue_beaver/grammars/ruby_comments.treetop +110 -0
- data/lib/issue_beaver/grammars.rb +4 -0
- data/lib/issue_beaver/models/git.rb +63 -0
- data/lib/issue_beaver/models/github_issue.rb +106 -0
- data/lib/issue_beaver/models/github_issue_repository.rb +96 -0
- data/lib/issue_beaver/models/merger.rb +146 -0
- data/lib/issue_beaver/models/todo_comments.rb +57 -0
- data/lib/issue_beaver/models.rb +5 -0
- data/lib/issue_beaver/runner.rb +176 -0
- data/lib/issue_beaver/shared/attributes_model.rb +51 -0
- data/lib/issue_beaver/shared/hash.rb +16 -0
- data/lib/issue_beaver/shared/model_collection.rb +27 -0
- data/lib/issue_beaver/shared.rb +3 -0
- data/lib/issue_beaver.rb +7 -0
- data/lib/password/password.rb +132 -0
- data/spec/fixtures/ruby.rb +6 -0
- data/spec/fixtures/ruby2.rb +16 -0
- data/spec/grammars/ruby_comments_spec.rb +60 -0
- data/spec/spec_helper.rb +2 -0
- metadata +78 -0
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
source 'http://rubygems.org'
|
2
|
+
|
3
|
+
gem 'treetop'
|
4
|
+
gem 'octokit'
|
5
|
+
gem 'activemodel'
|
6
|
+
gem 'levenshtein'
|
7
|
+
gem 'hashie'
|
8
|
+
gem 'time-lord'
|
9
|
+
gem 'enumerable-lazy', '~> 0.0.1'
|
10
|
+
gem 'enumerator-memoizing'
|
11
|
+
gem 'grit'
|
12
|
+
|
13
|
+
group :test do
|
14
|
+
gem 'rspec'
|
15
|
+
end
|
16
|
+
|
17
|
+
group :development do
|
18
|
+
gem 'debugger'
|
19
|
+
end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (3.2.8)
|
5
|
+
activesupport (= 3.2.8)
|
6
|
+
builder (~> 3.0.0)
|
7
|
+
activesupport (3.2.8)
|
8
|
+
i18n (~> 0.6)
|
9
|
+
multi_json (~> 1.0)
|
10
|
+
addressable (2.3.2)
|
11
|
+
builder (3.0.3)
|
12
|
+
columnize (0.3.6)
|
13
|
+
debugger (1.2.0)
|
14
|
+
columnize (>= 0.3.1)
|
15
|
+
debugger-linecache (~> 1.1.1)
|
16
|
+
debugger-ruby_core_source (~> 1.1.3)
|
17
|
+
debugger-linecache (1.1.2)
|
18
|
+
debugger-ruby_core_source (>= 1.1.1)
|
19
|
+
debugger-ruby_core_source (1.1.3)
|
20
|
+
diff-lcs (1.1.3)
|
21
|
+
enumerable-lazy (0.0.1)
|
22
|
+
enumerator-memoizing (0.0.1)
|
23
|
+
faraday (0.8.4)
|
24
|
+
multipart-post (~> 1.1)
|
25
|
+
faraday_middleware (0.8.8)
|
26
|
+
faraday (>= 0.7.4, < 0.9)
|
27
|
+
grit (2.5.0)
|
28
|
+
diff-lcs (~> 1.1)
|
29
|
+
mime-types (~> 1.15)
|
30
|
+
posix-spawn (~> 0.3.6)
|
31
|
+
hashie (1.2.0)
|
32
|
+
i18n (0.6.1)
|
33
|
+
levenshtein (0.2.2)
|
34
|
+
mime-types (1.19)
|
35
|
+
multi_json (1.3.6)
|
36
|
+
multipart-post (1.1.5)
|
37
|
+
octokit (1.13.0)
|
38
|
+
addressable (~> 2.2)
|
39
|
+
faraday (~> 0.8)
|
40
|
+
faraday_middleware (~> 0.8)
|
41
|
+
hashie (~> 1.2)
|
42
|
+
multi_json (~> 1.3)
|
43
|
+
polyglot (0.3.3)
|
44
|
+
posix-spawn (0.3.6)
|
45
|
+
rspec (2.11.0)
|
46
|
+
rspec-core (~> 2.11.0)
|
47
|
+
rspec-expectations (~> 2.11.0)
|
48
|
+
rspec-mocks (~> 2.11.0)
|
49
|
+
rspec-core (2.11.1)
|
50
|
+
rspec-expectations (2.11.3)
|
51
|
+
diff-lcs (~> 1.1.3)
|
52
|
+
rspec-mocks (2.11.2)
|
53
|
+
time-lord (0.2.5)
|
54
|
+
treetop (1.4.10)
|
55
|
+
polyglot
|
56
|
+
polyglot (>= 0.3.1)
|
57
|
+
|
58
|
+
PLATFORMS
|
59
|
+
ruby
|
60
|
+
|
61
|
+
DEPENDENCIES
|
62
|
+
activemodel
|
63
|
+
debugger
|
64
|
+
enumerable-lazy (~> 0.0.1)
|
65
|
+
enumerator-memoizing
|
66
|
+
grit
|
67
|
+
hashie
|
68
|
+
levenshtein
|
69
|
+
octokit
|
70
|
+
rspec
|
71
|
+
time-lord
|
72
|
+
treetop
|
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
Issue Beaver
|
2
|
+
============
|
3
|
+
|
4
|
+
**This is work in progress**
|
5
|
+
|
6
|
+
* **Issue Beaver** scans your project's source code for comments containing *TODO*.
|
7
|
+
|
8
|
+
* **Issue Beaver** automatically creates new issues on Github for each TODO comment it finds.
|
9
|
+
|
10
|
+
* **Issue Beaver** automatically closes issues on Github when you remove a TODO comment.
|
11
|
+
|
12
|
+
The goal is to provide simple and lightweight tracking of low-level technical issues (TODOs) and make the project's progress more transparent for people who don't want to read the source code.
|
13
|
+
|
14
|
+

|
15
|
+
|
16
|
+
Configuration
|
17
|
+
-------------
|
18
|
+
|
19
|
+
### Repository
|
20
|
+
Issue beaver tries to use the Github repository specified in **remote.origin** of your local git repository for storing the issues. If you want to use a different repository (e.g. that of your own fork) you can set the **issuebeaver.repository** config variable:
|
21
|
+
|
22
|
+
```
|
23
|
+
git config issuebeaver.repository eckardt/issue-beaver
|
24
|
+
```
|
25
|
+
|
26
|
+
### Github login
|
27
|
+
If you don't want to be asked for your Github login you can set the **github.user** config variable. Your Github password won't be stored.
|
28
|
+
|
29
|
+
```
|
30
|
+
git config github.user eckardt
|
31
|
+
```
|
32
|
+
|
33
|
+
### Issue labels
|
34
|
+
You can specify a list of labels that should be used for issues created by Issue Beaver. Make sure to create the labels for your repository using Github Issues' *Manage Labels* feature, otherwise Issue Beaver will fail.
|
35
|
+
|
36
|
+
```
|
37
|
+
git config issuebeaver.labels todo,@high
|
38
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
|
3
|
+
class GemHelperWithForce < Bundler::GemHelper
|
4
|
+
def build_gem
|
5
|
+
file_name = nil
|
6
|
+
sh("gem build -f -V '#{spec_path}'") { |out, code|
|
7
|
+
file_name = File.basename(built_gem_path)
|
8
|
+
FileUtils.mkdir_p(File.join(base, 'pkg'))
|
9
|
+
FileUtils.mv(built_gem_path, 'pkg')
|
10
|
+
Bundler.ui.confirm "#{name} #{version} built to pkg/#{file_name}"
|
11
|
+
}
|
12
|
+
File.join(base, 'pkg', file_name)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
GemHelperWithForce.install_tasks
|
data/bin/issuebeaver
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = "issue-beaver"
|
6
|
+
s.version = "0.1.0"
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.authors = ["Stephan Eckardt"]
|
9
|
+
s.email = ["mail@stephaneckardt.com"]
|
10
|
+
s.homepage = "https://github.com/eckardt/issue-beaver"
|
11
|
+
s.summary = %q{Issue Beaver creates Github Issues for TODO comments in your source code}
|
12
|
+
s.description = s.summary
|
13
|
+
|
14
|
+
s.files = `git ls-files`.split("\n")
|
15
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Enumerable
|
2
|
+
# The right enumerator is accessed first
|
3
|
+
def merge_right(other_enum, id_method = nil)
|
4
|
+
Enumerator.new do |yielder|
|
5
|
+
yielded_ids = []
|
6
|
+
|
7
|
+
other_enum.each do |x|
|
8
|
+
id = id_method ? x.send(id_method) : x
|
9
|
+
yielded_ids << id
|
10
|
+
yielder << x
|
11
|
+
end
|
12
|
+
|
13
|
+
self.each do |x|
|
14
|
+
id = id_method ? x.send(id_method) : x
|
15
|
+
yielder << x unless yielded_ids.include? id
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# The left enumerator is accessed first
|
21
|
+
def merge_left(other_enum, id_method = nil)
|
22
|
+
other_enum.merge_right(self, id_method)
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module IssueBeaver
|
2
|
+
module Grammars
|
3
|
+
grammar RubyComments
|
4
|
+
rule comments
|
5
|
+
(comment / non_comment)* {
|
6
|
+
def comments
|
7
|
+
elements.map{|element|
|
8
|
+
next unless element.respond_to?(:title)
|
9
|
+
|
10
|
+
{
|
11
|
+
'begin_line' => element.begin_line,
|
12
|
+
'title' => element.title,
|
13
|
+
'body' => element.body,
|
14
|
+
'assignee' => element.assignee
|
15
|
+
}
|
16
|
+
}.compact
|
17
|
+
end
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
rule non_comment
|
22
|
+
.
|
23
|
+
end
|
24
|
+
|
25
|
+
rule comment
|
26
|
+
todo_comment / normal_comment
|
27
|
+
end
|
28
|
+
|
29
|
+
rule todo_comment
|
30
|
+
todo_comment_title todo_comment_body {
|
31
|
+
def label() todo_comment_title.label end
|
32
|
+
def title() todo_comment_title.text end
|
33
|
+
def assignee() todo_comment_title.assignee end
|
34
|
+
|
35
|
+
def begin_line
|
36
|
+
parent.text_value.line_of interval.begin
|
37
|
+
end
|
38
|
+
|
39
|
+
def body
|
40
|
+
todo_comment_body.text
|
41
|
+
end
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
rule todo_comment_title
|
46
|
+
'#' white? 'TODO' ':'? white? comment_line white? eoc {
|
47
|
+
def label() 'todo' end
|
48
|
+
|
49
|
+
def assignee() comment_line.mentions.first end
|
50
|
+
|
51
|
+
def text
|
52
|
+
comment_line.text
|
53
|
+
end
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
rule todo_comment_body
|
58
|
+
todo_comment_body_line* {
|
59
|
+
def text
|
60
|
+
elements.map(&:text).join("\n") if elements.any?
|
61
|
+
end
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
rule todo_comment_body_line
|
66
|
+
white? '#' white? comment_line eoc {
|
67
|
+
def text
|
68
|
+
comment_line.text_value
|
69
|
+
end
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
rule normal_comment
|
74
|
+
'#' white? comment_line eoc
|
75
|
+
end
|
76
|
+
|
77
|
+
# TODO: Big refactor to make this fast
|
78
|
+
rule comment_line
|
79
|
+
(mention / (!eoc .))* {
|
80
|
+
def mentions
|
81
|
+
elements.select {|element|
|
82
|
+
element.kind_of? Mention
|
83
|
+
}.map(&:text_value)
|
84
|
+
end
|
85
|
+
def text
|
86
|
+
elements.reject {|element|
|
87
|
+
element.kind_of? Mention
|
88
|
+
}.map(&:text_value).join("")
|
89
|
+
end
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
rule mention
|
94
|
+
white '@' [a-zA-Z]+ white? <Mention> {
|
95
|
+
def text_value
|
96
|
+
elements[2].text_value
|
97
|
+
end
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
rule white
|
102
|
+
[ \t]+
|
103
|
+
end
|
104
|
+
|
105
|
+
rule eoc
|
106
|
+
"\n" / !.
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'grit'
|
2
|
+
|
3
|
+
module IssueBeaver
|
4
|
+
module Models
|
5
|
+
class Git
|
6
|
+
|
7
|
+
def initialize(dir, repo_name)
|
8
|
+
@root_dir = discover_root_dir(dir)
|
9
|
+
@git = Grit::Repo.new(@root_dir)
|
10
|
+
@repo_name = repo_name || @git.config['issuebeaver.repository'] || 'remote.origin'
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :root_dir
|
14
|
+
|
15
|
+
|
16
|
+
def slug
|
17
|
+
@slug ||= discover_github_repo(@repo_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
def github_user
|
22
|
+
@github_user ||= @git.config['github.user']
|
23
|
+
end
|
24
|
+
|
25
|
+
def labels
|
26
|
+
@labels ||= (@git.config['issuebeaver.labels'] || "").split(',')
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def discover_github_repo(repo_name)
|
31
|
+
github_repo = nil
|
32
|
+
if repo_name.match(/[^\.]+\/[^\.]+/)
|
33
|
+
github_repo = repo_name
|
34
|
+
else
|
35
|
+
url = @git.config["#{repo_name}.url"]
|
36
|
+
github_repo = url.match(/git@github\.com:([^\.]+)\.git/)[1] if url
|
37
|
+
end
|
38
|
+
github_repo
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
def files(dir)
|
43
|
+
IO.popen(%Q{cd "#{@root_dir}" && git ls-files "#{dir}"}).lazy.memoizing.
|
44
|
+
map(&:chomp).map{|file| File.absolute_path(file, @root_dir) }.lazy
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def discover_root_dir(dir)
|
51
|
+
cd = dir
|
52
|
+
loop do
|
53
|
+
return nil if !Dir.exists?(cd)
|
54
|
+
return nil if File.absolute_path(cd) == '/'
|
55
|
+
return cd if Dir.exists?(File.join(cd, '.git'))
|
56
|
+
cd = File.join(cd, '..')
|
57
|
+
end
|
58
|
+
return nil
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'active_support/core_ext' # Needed for delegate
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module IssueBeaver
|
5
|
+
module Models
|
6
|
+
class GithubIssue
|
7
|
+
|
8
|
+
def self.use_repository(repository)
|
9
|
+
@repository = repository
|
10
|
+
|
11
|
+
class << self
|
12
|
+
delegate :update, :create, :default_attributes, :first, to: :@repository
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
def self.repo_name() @repository.repo if @repository end
|
18
|
+
|
19
|
+
|
20
|
+
def self.all
|
21
|
+
Shared::ModelCollection.new(self, @repository.all.map{|attrs| new_from_github(attrs.dup)})
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
include Shared::AttributesModel
|
26
|
+
ATTRIBUTES = [:number, :state, :title, :body, :file, :begin_line, :created_at, :updated_at, :labels, :assignee]
|
27
|
+
|
28
|
+
|
29
|
+
def closed?() state == "closed" end
|
30
|
+
|
31
|
+
|
32
|
+
def open?() state == "open" end
|
33
|
+
|
34
|
+
|
35
|
+
def new?() !number end
|
36
|
+
|
37
|
+
|
38
|
+
def persisted?() !new? && !changed? end
|
39
|
+
|
40
|
+
|
41
|
+
def must_update?() changed_attributes_for_update.any? end
|
42
|
+
|
43
|
+
|
44
|
+
def changed_attributes_for_update
|
45
|
+
Hashie::Mash.new(changed_attributes).only(:title, :body, :assignee)
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
def self.new_from_github(attrs)
|
50
|
+
new(attrs).tap do |obj|
|
51
|
+
obj.clean_attributes_from_github
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
def self.new_from_todo(attrs)
|
57
|
+
new(attrs).tap do |obj|
|
58
|
+
obj.clean_attributes_from_todo
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def initialize(attrs = {})
|
64
|
+
@attributes = Hashie::Mash.new(attrs)
|
65
|
+
ATTRIBUTES.each do |attr| @attributes[attr] ||= nil end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
def clean_attributes_from_github
|
70
|
+
self.attributes.merge! self.class.default_attributes
|
71
|
+
self.created_at = Time.parse(created_at) if created_at.kind_of?(String)
|
72
|
+
self.updated_at = Time.parse(updated_at) if updated_at.kind_of?(String)
|
73
|
+
self.assignee = assignee.login if assignee.respond_to?(:login)
|
74
|
+
@changed_attributes = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
def clean_attributes_from_todo
|
79
|
+
basename = File.basename(self.file)
|
80
|
+
self.title ||= ""
|
81
|
+
self.title = "#{self.title.capitalize} (#{basename}:#{self.begin_line})"
|
82
|
+
github_line_url = "https://github.com/#{self.class.repo_name}/blob/master/#{self.file}\#L#{self.begin_line}"
|
83
|
+
self.body = %Q{#{self.body}\n\n#{github_line_url}}
|
84
|
+
@changed_attributes = nil
|
85
|
+
end
|
86
|
+
|
87
|
+
|
88
|
+
def update_attributes_with_limit(attrs)
|
89
|
+
update_attributes_without_limit(attrs.only(:title, :body, :updated_at, :assignee))
|
90
|
+
end
|
91
|
+
alias_method_chain :update_attributes, :limit
|
92
|
+
|
93
|
+
|
94
|
+
def save
|
95
|
+
if new?
|
96
|
+
@attributes = self.class.create(attributes)
|
97
|
+
elsif changed?
|
98
|
+
@attributes = self.class.update(number, attributes)
|
99
|
+
end
|
100
|
+
@changed_attributes.clear
|
101
|
+
true
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|