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 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
+ ![a beaver](http://kidsfront.com/coloring-pages/sm_color/beaver.jpg)
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,5 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
3
+
4
+ require 'issue_beaver'
5
+ IssueBeaver::Runner.run(*ARGV)
@@ -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,9 @@
1
+ module IssueBeaver
2
+ module Grammars
3
+ module RubyComments
4
+ class Mention < Treetop::Runtime::SyntaxNode
5
+
6
+ end
7
+ end
8
+ end
9
+ 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,4 @@
1
+ require 'treetop'
2
+
3
+ require 'issue_beaver/grammars/ruby_comments'
4
+ Treetop.load File.expand_path('../grammars/ruby_comments.treetop', __FILE__)
@@ -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