issue-beaver 0.1.0

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