issuesrc 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- OTk4MDIyOTg3MjA5YWNmY2IwNTBmNzcyMTNlODAxMTlmN2YyNTZiOA==
4
+ YmZiMTYzMmRlMWVkZDQxZGU2NjcxYTBhY2JhM2ZhZTRiMDk2ZTkwNw==
5
5
  data.tar.gz: !binary |-
6
- NjdkYzAwNDE4NDNjMGIzMWQ5ZmQ2ZjViZTE1MGQ0MzYwZmU4ZjEyYw==
6
+ ZTQzZGVhY2Q2ZDhiMjdkNmQ2ZmJkMjM0MjI0MTQyMWE2ZmUyMmE4ZA==
7
7
  SHA512:
8
8
  metadata.gz: !binary |-
9
- MTAyNWM0NjM1ZGRiNjY5NmE4OWUwOGFhMmMzMjYwNDUzNThlMjI5NGFiZjk1
10
- YTgxODhhMDFmOGU1OGYwNDgxY2VkOWQ3ZWEyOWM2M2UyOGU4YzA2Y2QzNDM5
11
- YWZiYjAyZWZhZTkwMWRkMDQwODE0NzdlMmRiNDI5NDU4YzRmYTg=
9
+ YmM1YzFmZDRiYmQ1M2ZjN2VhM2E2ZGM0NDQ0NjIwYTQ1NDY2YzM4ZWY3MDNi
10
+ NjM0OGI4MzAyMTc2MmQ2ZTg0ZTZjMjE3MWJiZGYzNzNiOTlmZWExYjM1M2I2
11
+ MzJhN2Q1NTIwZTJhYWMzNjk1NzVjNGZmNmUwMGE2ZDgxMWUxMmE=
12
12
  data.tar.gz: !binary |-
13
- ZjhlMzlkYTcxZDg2YTAzZTNkZTNkZGMxZjM5OTQ0MjhkYjc2ODdjYTM3Zjhi
14
- NjQyNTY0NzYzNmYxOGE3OGI2OTUyOWE3Nzk3Yzc2N2Q1OGQ3ZDRiMmRiMDRj
15
- MDkzODFlMWY5ZWVmNzUxMTQzNTA3Yzk5YTg4OWFmNGYxOWQwZTk=
13
+ MmM3OTEzODY0ZDZhMTkyMzgwYTNlYmU5ODUwZjZkZDFhMGUzMjNkYjBmNDZh
14
+ N2QzODUyNjdmNjUwMDBkNzgxZDNkNGJhZWZkNmUwNDY2MGQ2NGI3NDEyZjc4
15
+ MmYwZWFkMTA4ZjYyZWFmNzczZjBiZTdjOGQ1ODlmOTljYTE2YTQ=
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+
data/Gemfile CHANGED
@@ -7,3 +7,6 @@ gem 'toml', '~> 0.1.2'
7
7
  gem 'plist', '~> 3.1.0'
8
8
  gem 'em-http-request', '~> 1.1.2'
9
9
  gem 'ptools', '~> 1.2.6'
10
+ group :coverage do
11
+ gem 'simplecov', :require => false
12
+ end
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # issuesrc
1
+ # issuesrc [![Build Status](https://secure.travis-ci.org/tcard/issuesrc.svg?branch=master)](http://travis-ci.org/tcard/issuesrc) [![Gem Version](https://badge.fury.io/rb/issuesrc.svg)](http://badge.fury.io/rb/issuesrc) [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/tcard/issuesrc/master)
2
2
 
3
3
  **WARNING: very early stage of development. Test at your own risk!**
4
4
 
@@ -6,10 +6,10 @@ Synchronize in-source commented tasks with your issue tracker.
6
6
 
7
7
  issuesrc scans your files looking for comments tagged with labels such as TODO, BUG, FIXME, etc., and adds them your issue tracker.
8
8
 
9
- * Newly found tags **will be opened as issues**. Each ID will be added in the source code, for keeping them in sync.
10
- * From the source code you can change the label or the description of the issue.
9
+ * Newly found tags **will be opened as issues**. Each tag will be edited in the source code to add its issue number next to it.
10
+ * From the source code you can change the label or the description of the issue. Running the command again will synchronize changes in the repo.
11
11
  * You can also **appoint an assignee** by putting her username alongside the tag (eg. `TODO(tcard)`; `TODO(tcard#12345)`).
12
- * Synchronization is one-way; changes that you do in the issue tracker will be lost when you run the program again.
12
+ * Synchronization is one-way; changes that you do to a issuesrc issue from the issue tracker will be lost when you run the program again. You should add any further information as comments.
13
13
  * When a tag is removed from the code, it is **closed in the issue tracker**.
14
14
 
15
15
  ## Installation
@@ -20,10 +20,16 @@ issuesrc scans your files looking for comments tagged with labels such as TODO,
20
20
 
21
21
  issuesrc connects comments found in source code with an issue tracker. It needs to be configured to talk to both.
22
22
 
23
- Configuration is done both via a .toml config file and via command line arguments. See `example.toml` and run `issuesrc -h` for details.
23
+ Configuration is done both via a .toml config file and via command line arguments. See [`example.toml`](https://github.com/tcard/issuesrc/blob/master/example.toml) and run `issuesrc -h` for details.
24
24
 
25
25
  Currently, issuesrc only supports Git for retrieving source code, and GitHub as issue tracker.
26
26
 
27
+ The easiest way to get started would be something like this:
28
+
29
+ $ issuesrc --repo youruser/yourrepo --github-token xxxxxxxxxxx
30
+
31
+ That will extract tasks from the comments at github.com/youruser/yourrepo, open issues for them, add each issue's number next to its comment, and commit and push the changes. (You will thus need to have push access from the environment you run this command in.)
32
+
27
33
  ## Contributing
28
34
 
29
35
  1. Fork it ( https://github.com/tcard/issuesrc/fork )
@@ -1,4 +1,5 @@
1
1
  require 'issuesrc/config'
2
+ require 'issuers/issuers'
2
3
  require 'em-http-request'
3
4
  require 'json'
4
5
  require 'set'
@@ -7,35 +8,6 @@ module Issuesrc
7
8
  module Issuers
8
9
  DEFAULT_LABEL = 'issuesrc'
9
10
 
10
- class Issues
11
- def initialize(queue)
12
- @queue = queue
13
- @queue_done = false
14
- @cache = []
15
- end
16
-
17
- def each
18
- i = 0
19
- while i < @cache.length
20
- yield @cache[i]
21
- i += 1
22
- end
23
-
24
- while !@queue_done
25
- @queue.pop do |issue_page|
26
- if issue_page == :end
27
- @queue_done = true
28
- next
29
- end
30
- issue_page.each do |issue|
31
- yield issue unless issue.include? 'pull_request'
32
- end
33
- end
34
- end
35
- end
36
-
37
- end
38
-
39
11
  class GithubIssuer
40
12
  def initialize(args, config, event_loop)
41
13
  @user, @repo = find_repo(args, config)
@@ -109,25 +81,25 @@ module Issuesrc
109
81
  end
110
82
  end
111
83
 
84
+ private
112
85
  def make_sure_issue_exists_and_then
113
86
  # TODO(#21)
114
87
  yield true
115
88
  end
116
89
 
117
- private
118
90
  def find_repo(args, config)
119
- repo_arg = Issuesrc::Config::option_from_both(
91
+ repo_arg = Issuesrc::Config.option_from_both(
120
92
  :repo, ['github', 'repo'], args, config, :require => true)
121
93
  repo_arg.split('/')
122
94
  end
123
95
 
124
96
  def try_find_token(args, config)
125
- Issuesrc::Config::option_from_both(
97
+ Issuesrc::Config.option_from_both(
126
98
  :github_token, ['github', 'auth_token'], args, config)
127
99
  end
128
100
 
129
101
  def try_find_issuesrc_label(args, config)
130
- label = Issuesrc::Config::option_from_both(
102
+ label = Issuesrc::Config.option_from_both(
131
103
  :issuesrc_label, ['issuer', 'issuesrc_label'], args, config)
132
104
  if label.nil?
133
105
  label = DEFAULT_LABEL
@@ -0,0 +1,102 @@
1
+ module Issuesrc
2
+
3
+ # This module holds the different classes that can be used as issuers.
4
+ #
5
+ # An issuer handles an external issue tracker. It retrieves, creates,
6
+ # updates and deletes issues in an external service.
7
+ #
8
+ # Every issuer must implement the interface defined in the
9
+ # {Issuers::IssuerInterface} class.
10
+ module Issuers
11
+
12
+ # This class is here for documentation only. All classes in the Issuers
13
+ # module that want to be considered issuers need to implement this
14
+ # interface.
15
+ class IssuerInterface
16
+ # @param args Command line arguments, as key => value.
17
+ # @param config Arguments from the configuration file, as key => value.
18
+ # @param [Issuesrc::EventLoop] event_loop An event loop that can be used
19
+ # to make asynchronous I/O.
20
+ def initialize(args, config, event_loop); end
21
+
22
+ # Loads all the open issues marked with
23
+ # {Issuesrc::Issuers::DEFAULT_LABEL} (or the label chose in the config)
24
+ # from the issue tracker.
25
+ #
26
+ # @return [Issuesrc::Issuers::Issues]
27
+ def async_load_issues(); end
28
+
29
+ # Opens a new issue with the information hold in +tag+. Sets the just
30
+ # created issue ID as +tag+'s +issue_id+ attribute.
31
+ #
32
+ # @param [Issuesrc::Tag] tag
33
+ # @yieldparam [Issuesrc::Tag] The passed tag, with the issue ID updated.
34
+ def async_create_issue(tag, &block); end
35
+
36
+ # Updates an existing issue with the information hold in +tag+.
37
+ #
38
+ # @param issue_id The ID of the issue that should be updated.
39
+ # @param [Issuesrc::Tag] tag
40
+ # @yieldparam [Issuesrc::Tag] The passed tag.
41
+ def async_update_issue(issue_id, tag, &block); end
42
+
43
+ # Closes an issue.
44
+ #
45
+ # @param issue_id The ID of the issue that should be closed.
46
+ # @yieldparam [Issuesrc::Tag] The passed tag.
47
+ def async_close_issue(issue_id, &block); end
48
+
49
+ # Updates and closes a bunch of issues.
50
+ #
51
+ # It matches the issues that are currently open in the issue tracker with
52
+ # the tags found in the source code. Those that are only in the issue
53
+ # tracker are closed. Those that are in both are updated with the
54
+ # information from the source code.
55
+ #
56
+ # Reports what is being done to the passed block.
57
+ #
58
+ # @param prev_issues An array of issues, as they are returned from
59
+ # {Issuesrc::Issuers::IssuerInterface.async_load_issues}.
60
+ # @param tags_by_issue_id
61
+ # @yieldparam issue_id The ID of an issue.
62
+ # @yieldparam {Issuesrc::Tag} tag The tag associated with the issue.
63
+ # @yieldparam action Either +:updated+ or +:closed+.
64
+ def async_update_or_close_issues(prev_issues, tags_by_issue_id, &block)
65
+ end
66
+ end
67
+
68
+ # A generator of issues.
69
+ #
70
+ # Reads issues from the queue passed to the constructor and yields them.
71
+ #
72
+ # The format of each issue is specific to a particular issuer.
73
+ class Issues
74
+ def initialize(queue)
75
+ @queue = queue
76
+ @queue_done = false
77
+ @cache = []
78
+ end
79
+
80
+ def each
81
+ i = 0
82
+ while i < @cache.length
83
+ yield @cache[i]
84
+ i += 1
85
+ end
86
+
87
+ while !@queue_done
88
+ @queue.pop do |issue_page|
89
+ if issue_page == :end
90
+ @queue_done = true
91
+ next
92
+ end
93
+ issue_page.each do |issue|
94
+ yield issue unless issue.include? 'pull_request'
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ end
101
+ end
102
+ end
data/lib/issuesrc.rb CHANGED
@@ -40,9 +40,10 @@ module Issuesrc
40
40
  issues = issuer.async_load_issues()
41
41
 
42
42
  created_tags, updated_tags, closed_issues = [], [], []
43
+ tags_by_issue_id = {}
43
44
 
44
45
  sourcer.retrieve_files().each do |file|
45
- if Issuesrc::Config::option_from_args(:verbose, args)
46
+ if Issuesrc::Config.option_from_args(:verbose, args)
46
47
  puts file.path
47
48
  end
48
49
 
@@ -54,7 +55,8 @@ module Issuesrc
54
55
  tags = []
55
56
  tag_finder.find_tags(file) { |tag| tags << tag }
56
57
 
57
- tags_by_issue_id, new_tags = program.classify_tags(tags, file)
58
+ tags_in_file, new_tags = program.classify_tags(tags)
59
+ tags_by_issue_id.update(tags_in_file)
58
60
 
59
61
  new_tags.each do |tag|
60
62
  created_tags << tag
@@ -62,16 +64,16 @@ module Issuesrc
62
64
  program.save_tag_in_file(tag)
63
65
  end
64
66
  end
67
+ end
65
68
 
66
- issuer.async_update_or_close_issues(issues, tags_by_issue_id) do
67
- |issue_id, tag, action|
68
- case action
69
- when :updated
70
- program.save_tag_in_file(tag)
71
- updated_tags << tag
72
- when :closed
73
- closed_issues << issue_id
74
- end
69
+ issuer.async_update_or_close_issues(issues, tags_by_issue_id) do
70
+ |issue_id, tag, action|
71
+ case action
72
+ when :updated
73
+ program.save_tag_in_file(tag)
74
+ updated_tags << tag
75
+ when :closed
76
+ closed_issues << issue_id
75
77
  end
76
78
  end
77
79
 
@@ -87,6 +89,8 @@ module Issuesrc
87
89
  @config = config
88
90
  end
89
91
 
92
+ attr_accessor :files_offsets
93
+
90
94
  def init_files_offsets
91
95
  @files_offsets = {}
92
96
  end
@@ -128,7 +132,7 @@ module Issuesrc
128
132
  end
129
133
 
130
134
  def load_component(config_key, arg_key, default, options)
131
- type = Config::option_from_both(arg_key, config_key, @args, @config)
135
+ type = Config.option_from_both(arg_key, config_key, @args, @config)
132
136
  if type.nil?
133
137
  type = default
134
138
  end
@@ -137,7 +141,7 @@ module Issuesrc
137
141
 
138
142
  def load_component_by_type(type, options)
139
143
  if !options.include?(type)
140
- exec_fail 'Unrecognized sourcer type: #{type}'
144
+ Issuesrc::exec_fail 'Unrecognized sourcer type: #{type}'
141
145
  end
142
146
 
143
147
  options[type]
@@ -146,7 +150,7 @@ module Issuesrc
146
150
  # Like `load_sourcer` but for the tag finders. It only looks at
147
151
  # `[tag_finders] tag_finders = [...]` from the config file.
148
152
  def load_tag_finders
149
- tag_finders = Config::option_from_config(
153
+ tag_finders = Config.option_from_config(
150
154
  ['tag_finders', 'tag_finders'], @config)
151
155
  if tag_finders.nil?
152
156
  tag_finders = DEFAULT_TAG_FINDERS
@@ -155,7 +159,7 @@ module Issuesrc
155
159
  end
156
160
 
157
161
  def load_tag_finders_by_types(types)
158
- tag_extractor = Issuesrc::TagExtractor.new(@args, @config)
162
+ tag_extractor = load_tag_extractor()
159
163
  tag_finders = []
160
164
  types.each do |type|
161
165
  path, cls = load_component_by_type(type, TAG_FINDERS)
@@ -165,6 +169,10 @@ module Issuesrc
165
169
  tag_finders
166
170
  end
167
171
 
172
+ def load_tag_extractor
173
+ Issuesrc::TagExtractor.new(@args, @config)
174
+ end
175
+
168
176
  def make_tag_finder(cls, tag_extractor)
169
177
  Issuesrc::TagFinders.const_get(cls).new(tag_extractor, @args, @config)
170
178
  end
@@ -180,7 +188,7 @@ module Issuesrc
180
188
  ret
181
189
  end
182
190
 
183
- def classify_tags(tags, file)
191
+ def classify_tags(tags)
184
192
  tags_by_issue = {}
185
193
  new_tags = []
186
194
  tags.each do |tag|
data/lib/issuesrc/file.rb CHANGED
@@ -1,4 +1,25 @@
1
1
  module Issuesrc
2
+ # This class is here for documentation only. All classes in the Sourcers
3
+ # module that want to be considered issuers need to implement this
4
+ # interface.
5
+ class FileInterface
6
+ # @return The type of the file as a file extension.
7
+ def type; end
8
+
9
+ # @return [IO] The body of the file.
10
+ def body; end
11
+
12
+ # Replaces part of the body of the file, and saves it.
13
+ #
14
+ # @param pos Position from the beginning of the body in which the new
15
+ # content starts.
16
+ # @param old_content_length Length of previous content that should be
17
+ # replaced.
18
+ # @param new_content A string that will be written in +pos+ at the file.
19
+ def replace_at(pos, old_content_length, new_content); end
20
+ end
21
+
22
+ # A file from the filesystem.
2
23
  class FSFile
3
24
  attr_reader :type
4
25
  attr_reader :path
@@ -15,7 +36,7 @@ module Issuesrc
15
36
  def replace_at(pos, old_content_length, new_content)
16
37
  fbody = body.read
17
38
  fbody = replace_in_string(fbody, pos, old_content_length, new_content)
18
- f = File.open(@path, 'wb')
39
+ f = body_for_writing()
19
40
  f.write(fbody)
20
41
  f.close()
21
42
  end
@@ -24,8 +45,13 @@ module Issuesrc
24
45
  def replace_in_string(s, pos, deleted_length, new_content)
25
46
  (s[0...pos] || '') + new_content + (s[pos + deleted_length..-1] || '')
26
47
  end
48
+
49
+ def body_for_writing
50
+ File.open(@path, 'wb')
51
+ end
27
52
  end
28
53
 
54
+ # A file from the filesystem that is indexed in a Git repository.
29
55
  class GitFile < FSFile
30
56
  attr_reader :repo
31
57
  attr_reader :path_in_repo
data/lib/issuesrc/tag.rb CHANGED
@@ -1,13 +1,17 @@
1
1
  module Issuesrc
2
+
3
+ # A tag is an annotation found in the source code of a file that holds
4
+ # information about the issue it corresponds to, the author or assignee, a
5
+ # label, a title for the isssue, and its position in the file.
2
6
  class Tag
3
7
  attr_reader :label
4
8
  attr_accessor :issue_id
5
9
  attr_reader :author
6
10
  attr_reader :title
7
- attr_reader :file
8
- attr_reader :line
9
- attr_reader :begin_pos
10
- attr_reader :end_pos
11
+ attr_accessor :file
12
+ attr_accessor :line
13
+ attr_accessor :begin_pos
14
+ attr_accessor :end_pos
11
15
 
12
16
  def initialize(label, issue_id, author, title, file, line,
13
17
  begin_pos, end_pos)
@@ -21,6 +25,7 @@ module Issuesrc
21
25
  @end_pos = end_pos
22
26
  end
23
27
 
28
+ # The string representation of the tag, to be included in the source file.
24
29
  def to_s
25
30
  ret = ""
26
31
  ret << @label
@@ -40,6 +45,17 @@ module Issuesrc
40
45
  ret
41
46
  end
42
47
 
48
+ # Writes the tag in its file, using its string representation.
49
+ #
50
+ # Also updates the tag position information depending on +offset+
51
+ #
52
+ # @param offsets As the tag's position information might have been outdated
53
+ # by other tags having been written to the file, this function needs
54
+ # to know how much does it need to correct its position. +offsets+
55
+ # is a list of pairs +(position, offset)+ that tells that at
56
+ # a given position a given offset has been added or substracted.
57
+ # @return +offsets+, updated with the new offset resulting from editing the
58
+ # file.
43
59
  def write_in_file(offsets)
44
60
  total_offset = 0
45
61
  offsets.each do |pos, offset|
@@ -1,4 +1,5 @@
1
1
  require 'issuesrc/config'
2
+ require 'issuesrc/tag'
2
3
 
3
4
  module Issuesrc
4
5
  TAG_EXTRACTORS = [
@@ -22,11 +23,26 @@ module Issuesrc
22
23
  end
23
24
  end
24
25
 
26
+ # Extracts a tag from a line of source code, if there is one.
27
+ #
28
+ # It passes the line through one or more extractor functions. The first
29
+ # that returns non-nil will be returned.
30
+ #
31
+ # @param source A string.
32
+ # @return [Issuesrc::Tag] Or +nil+ if no tag is found.
25
33
  def extract(source)
26
34
  @extractors.each do |extr|
27
- tag = try_extractor(extr, source)
28
- if !tag.nil?
29
- return tag
35
+ tag_data = try_extractor(extr, source)
36
+ if !tag_data.nil?
37
+ return Issuesrc::Tag.new(
38
+ tag_data['type'],
39
+ tag_data['issue_id'],
40
+ tag_data['author'],
41
+ tag_data['title'],
42
+ nil, nil,
43
+ tag_data['begin_pos'],
44
+ tag_data['end_pos']
45
+ )
30
46
  end
31
47
  end
32
48
  nil
@@ -1,3 +1,3 @@
1
1
  module Issuesrc
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.5"
3
3
  end
@@ -77,7 +77,7 @@ module Issuesrc
77
77
  end
78
78
 
79
79
  def init_exclude(config)
80
- @exclude = Issuesrc::Config::option_from_config(
80
+ @exclude = Issuesrc::Config.option_from_config(
81
81
  ['sourcer', 'exclude_files'], config)
82
82
  if @exclude.nil?
83
83
  @exclude = []
@@ -86,12 +86,12 @@ module Issuesrc
86
86
 
87
87
 
88
88
  def try_find_repo_url(args, config)
89
- Issuesrc::Config::option_from_both(:repo_url, ['git', 'repo'],
89
+ Issuesrc::Config.option_from_both(:repo_url, ['git', 'repo'],
90
90
  args, config)
91
91
  end
92
92
 
93
93
  def try_find_repo_path(args, config)
94
- Issuesrc::Config::option_from_both(:repo_path, ['git', 'repo_path'],
94
+ Issuesrc::Config.option_from_both(:repo_path, ['git', 'repo_path'],
95
95
  args, config)
96
96
  end
97
97
 
@@ -104,7 +104,7 @@ module Issuesrc
104
104
  end
105
105
 
106
106
  def decide_when_done(args_key, config_key, args, config)
107
- opt = Issuesrc::Config::option_from_both(
107
+ opt = Issuesrc::Config.option_from_both(
108
108
  args_key, ['git', config_key], args, config)
109
109
  if opt.nil?
110
110
  opt = is_downloaded?
@@ -17,7 +17,7 @@ module Issuesrc
17
17
 
18
18
  private
19
19
  def try_find_repo(args, config)
20
- repo_arg = Issuesrc::Config::option_from_both(
20
+ repo_arg = Issuesrc::Config.option_from_both(
21
21
  :repo, ['github', 'repo'], args, config)
22
22
  repo_arg.split('/')
23
23
  end
@@ -0,0 +1,33 @@
1
+ module Issuesrc
2
+
3
+ # This module holds the different classes that can be used as sourcers.
4
+ #
5
+ # A sourcer handles source code. It retrieves, reads from and edits the files
6
+ # in which tags can be found.
7
+ #
8
+ # Every sourcer must implement the interface defined in the
9
+ # {Sourcers::SourcerInterface} class.
10
+ module Sourcers
11
+
12
+ # This class is here for documentation only. All classes in the Sourcers
13
+ # module that want to be considered issuers need to implement this
14
+ # interface.
15
+ class SourcerInterface
16
+ # @param args Command line arguments, as key => value.
17
+ # @param config Arguments from the configuration file, as key => value.
18
+ def initialize(args, config); end
19
+
20
+ # Retrieves all the files in which there may be tags to find.
21
+ #
22
+ # @return [Enumerator] Enumerator of {Issuesrc::FileInterface}.
23
+ def retrieve_files; end
24
+
25
+ # Optional. Called when the execution of the program finishes.
26
+ #
27
+ # @param created_tags Array of {Issuesrc::Tag}.
28
+ # @param updated_tags Array of {Issuesrc::Tag}.
29
+ # @param closed_issue_ids Array of IDs, which are Strings.
30
+ def finish(created_tags, updated_tags, closed_issue_ids); end
31
+ end
32
+ end
33
+ end
@@ -2,6 +2,11 @@ require 'issuesrc/tag'
2
2
 
3
3
  module Issuesrc
4
4
  module TagFinders
5
+
6
+ # A tag finder that doesn't do any parsing; it just bluntly traverses the
7
+ # source code looking for things that look like comments.
8
+ #
9
+ # It tries to skip comments inside strings.
5
10
  class BluntTagFinder
6
11
  DEFAULT_COMMENT_MARKERS = [['//', "\n"], ['/*', '*/']]
7
12
  DEFAULT_STRING_MARKERS = ['"', "'"]
@@ -38,18 +43,13 @@ module Issuesrc
38
43
  # span several lines.
39
44
  nline_offset = 0
40
45
  comment.split("\n").each do |line|
41
- tag_data = @tag_extractor.extract(line)
42
- if !tag_data.nil?
43
- yield Issuesrc::Tag.new(
44
- tag_data['type'],
45
- tag_data['issue_id'],
46
- tag_data['author'],
47
- tag_data['title'],
48
- file,
49
- nline + nline_offset,
50
- pos + tag_data['begin_pos'],
51
- pos + tag_data['end_pos']
52
- )
46
+ tag = @tag_extractor.extract(line)
47
+ if !tag.nil?
48
+ tag.file = file
49
+ tag.line = nline + nline_offset
50
+ tag.begin_pos += pos
51
+ tag.end_pos += pos
52
+ yield tag
53
53
  end
54
54
  pos += line.length + 1
55
55
  nline_offset += 1
@@ -60,7 +60,7 @@ module Issuesrc
60
60
  private
61
61
  def find_comments(file)
62
62
  comment_markers, string_markers = decide_markers(file)
63
- body = file.body.read.force_encoding('BINARY') # TODO(#26): Use less memory here.
63
+ body = get_file_body(file)
64
64
  comment_finder = CommentFinder.new(body, comment_markers, string_markers)
65
65
  pos = 0
66
66
 
@@ -76,6 +76,10 @@ module Issuesrc
76
76
  ]
77
77
  end
78
78
 
79
+ def get_file_body(file)
80
+ file.body.read.force_encoding('BINARY') # TODO(#26): Use less memory here.
81
+ end
82
+
79
83
  class CommentFinder
80
84
  def initialize(body, comment_markers, string_markers)
81
85
  @body = body
@@ -129,15 +133,15 @@ module Issuesrc
129
133
  end
130
134
 
131
135
  def read_delimited(markers)
132
- consumed = state_boundary(markers, :begin)
136
+ marker_i, consumed = state_boundary(markers, :begin)
133
137
  lex = @body[0...consumed]
134
138
  consume_body(consumed)
135
139
 
136
- consumed_end = state_boundary(markers, :end)
140
+ _, consumed_end = state_boundary(markers, :end, marker_i)
137
141
  while !@body.empty? && consumed_end.nil?
138
142
  lex << @body[0]
139
143
  consumed += consume_body(1)
140
- consumed_end = state_boundary(markers, :end)
144
+ _, consumed_end = state_boundary(markers, :end, marker_i)
141
145
  end
142
146
 
143
147
  if !consumed_end.nil?
@@ -158,18 +162,24 @@ module Issuesrc
158
162
  end
159
163
  end
160
164
 
161
- def state_boundary(markers, begin_or_end)
165
+ def state_boundary(markers, begin_or_end, marker_i=nil)
162
166
  if @body.nil?
163
167
  nil
164
168
  end
165
169
 
170
+ i = 0
166
171
  markers.each do |marker|
172
+ if !marker_i.nil? && i != marker_i
173
+ i += 1
174
+ next
175
+ end
167
176
  if marker.instance_of? Array
168
177
  marker = marker[begin_or_end == :begin ? 0 : 1]
169
178
  end
170
179
  if @body.start_with? marker
171
- return marker.length
180
+ return [i, marker.length]
172
181
  end
182
+ i += 1
173
183
  end
174
184
  nil
175
185
  end
@@ -0,0 +1,37 @@
1
+ module Issuesrc
2
+
3
+ # This module holds the different classes that can be used as tag finders.
4
+ #
5
+ # An issuer handles an external issue tracker. It retrieves, creates,
6
+ # updates and deletes issues in an external service.
7
+ #
8
+ # Every tag finder must implement the interface defined in the
9
+ # {TagFinders::TagFinderInterface} class.
10
+ module TagFinders
11
+
12
+ # This class is here for documentation only. All classes in the TagFinders
13
+ # module that want to be considered tag finders need to implement this
14
+ # interface.
15
+ class TagFinderInterface
16
+ # @param tag_extractor [Issuesrc::TagExtractor]
17
+ # @param args Command line arguments, as key => value.
18
+ # @param config Arguments from the configuration file, as key => value.
19
+ def initialize(tag_extractor, args, config); end
20
+
21
+ # Tells if the tag finder can process the given file or not.
22
+ #
23
+ # @param [Issuesrc::FileInterface] file
24
+ # @return [Bool]
25
+ def accepts?(file); end
26
+
27
+ # Finds all the tags in a file.
28
+ #
29
+ # Reads in the file's body looking for tags, using the instance's
30
+ # +tag_extractor+.
31
+ #
32
+ # @param [Issuesrc::FileInterface] file
33
+ # @yieldparam tag [Issuesrc::Tag]
34
+ def find_tags(file); end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,4 @@
1
+ require_relative '../spec_helper'
2
+ require_relative '../../lib/issuers/github_issuer'
3
+
4
+ # TODO(#29): Test GithubIssuer.
@@ -0,0 +1,4 @@
1
+ require_relative '../spec_helper'
2
+ require_relative '../../lib/issuesrc/config'
3
+
4
+ # TODO(#30): Test Config.
@@ -0,0 +1,4 @@
1
+ require_relative '../spec_helper'
2
+ require_relative '../../lib/issuesrc/event_loop'
3
+
4
+ # TODO(#31): Test EventLoop.
@@ -0,0 +1,47 @@
1
+ require_relative '../spec_helper'
2
+ require_relative '../../lib/issuesrc/file'
3
+
4
+ describe Issuesrc::FSFile do
5
+ before :all do
6
+ @obj = Issuesrc::FSFile.new('/made/up/path.txt')
7
+ end
8
+
9
+ it 'extracts the type from the extension' do
10
+ expect(@obj.type).to be == 'txt'
11
+ end
12
+
13
+ describe '#replace_at' do
14
+ it 'replaces in the middle of the body' do
15
+ body = double()
16
+ allow(body).to receive(:write).with('Lorem ipsum REPLACED sit amet')
17
+ allow(@obj).to receive(:body).and_return(
18
+ double(:read => 'Lorem ipsum dolor sit amet'))
19
+ allow(@obj).to receive(:body_for_writing).and_return(body)
20
+ expect(body).to receive(:close)
21
+
22
+ @obj.replace_at(12, 5, 'REPLACED')
23
+ end
24
+
25
+ it 'replaces at the beginning of the body' do
26
+ body = double()
27
+ allow(body).to receive(:write).with('REPLACED ipsum dolor sit amet')
28
+ allow(@obj).to receive(:body).and_return(
29
+ double(:read => 'Lorem ipsum dolor sit amet'))
30
+ allow(@obj).to receive(:body_for_writing).and_return(body)
31
+ expect(body).to receive(:close)
32
+
33
+ @obj.replace_at(0, 5, 'REPLACED')
34
+ end
35
+
36
+ it 'replaces at the end of the body' do
37
+ body = double()
38
+ allow(body).to receive(:write).with('Lorem ipsum dolor sit amREPLACED')
39
+ allow(@obj).to receive(:body).and_return(
40
+ double(:read => 'Lorem ipsum dolor sit amet'))
41
+ allow(@obj).to receive(:body_for_writing).and_return(body)
42
+ expect(body).to receive(:close)
43
+
44
+ @obj.replace_at(24, 2, 'REPLACED')
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,31 @@
1
+ require_relative '../spec_helper'
2
+ require_relative '../../lib/issuesrc/tag_extractor'
3
+
4
+ describe Issuesrc::TagExtractor do
5
+ before :each do
6
+ @tag_extractor = Issuesrc::TagExtractor.new({}, {})
7
+ end
8
+
9
+ it 'parses several representations OK' do
10
+ cases = {
11
+ 'TODO() Test ' => 'TODO: Test',
12
+ 'FIXME:Test' => 'FIXME: Test',
13
+ 'FIXME' => 'FIXME',
14
+ 'TODO ( # 1234 )Test' => 'TODO(#1234): Test',
15
+ 'BUG ( tcard)' => 'BUG(tcard)',
16
+ 'BUG ( tcard # 1234 ):Test ' => 'BUG(tcard#1234): Test',
17
+ }
18
+
19
+ cases.each do |input, expected|
20
+ got = @tag_extractor.extract(input)
21
+ expect(got.to_s).to be == expected
22
+ idempotent = @tag_extractor.extract(expected)
23
+ expect(idempotent.to_s).to be == expected
24
+ end
25
+ end
26
+
27
+ it 'returns nil when no tag is found' do
28
+ got = @tag_extractor.extract('no tag here, sorry')
29
+ expect(got).to be == nil
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ require_relative '../spec_helper'
2
+ require_relative '../../lib/issuesrc/tag'
3
+
4
+ describe Issuesrc::Tag do
5
+ before :all do
6
+ @file = instance_double('Issuesrc::FSFile')
7
+ @obj = Issuesrc::Tag.new(
8
+ 'LABEL',
9
+ 'abc123',
10
+ 'theauthor',
11
+ 'The title',
12
+ @file,
13
+ 111,
14
+ 222,
15
+ 300)
16
+ end
17
+
18
+ describe '#write_in_file' do
19
+ it 'writes itself in its file with offsets' do
20
+ offset = [[50, 10], [60, 15], [999, 999]]
21
+
22
+ allow(@file).to receive(:replace_at)
23
+ .with(222 + 10 + 15, 300 - 222, @obj.to_s)
24
+ old_length = 300 - 222
25
+ new_length = @obj.to_s.length
26
+ expect(@obj.write_in_file(offset.clone))
27
+ .to be == (offset + [[222, new_length - old_length]])
28
+ end
29
+ end
30
+ end
@@ -1,15 +1,18 @@
1
+ require_relative 'spec_helper'
1
2
  require_relative '../lib/issuesrc'
2
3
 
3
4
  describe Issuesrc do
4
5
  before :each do
6
+ @args = {
7
+ :arg => 'fake arg'
8
+ }
9
+ @config = {
10
+ 'config' => ['fake', 'config']
11
+ }
5
12
  @obj = Class.new do
6
13
  include Issuesrc
7
14
  end.new
8
- @obj.send(:set_config, {
9
- :arg => 'fake arg'
10
- }, {
11
- 'config' => ['fake', 'config']
12
- })
15
+ @obj.send(:set_config, @args, @config)
13
16
  end
14
17
 
15
18
  describe '#load_sourcer' do
@@ -70,12 +73,129 @@ describe Issuesrc do
70
73
  end
71
74
 
72
75
  it 'fails when loading an unknown component' do
73
- allow(@obj).to receive(:exec_fail).and_raise('failed')
74
-
75
76
  expect { @obj.send(:load_component, ['fake'], :fake, 'def', {}) }
76
- .to raise_error('failed')
77
+ .to raise_error(Issuesrc::IssuesrcError)
78
+ end
79
+ end
80
+
81
+ describe '#load_tag_finders' do
82
+ def test_load_tag_finders(from_config, mock_load_comp=true)
83
+ allow(Issuesrc::Config).to receive(:option_from_config)
84
+ .with(['tag_finders', 'tag_finders'], @config)
85
+ .and_return(from_config)
86
+ allow(@obj).to receive(:load_tag_extractor)
87
+ .and_return('fake tag extractor')
88
+
89
+ types = from_config ? from_config : Issuesrc::DEFAULT_TAG_FINDERS
90
+ if types
91
+ i = 1
92
+ types.each do |v|
93
+ if mock_load_comp
94
+ path, cls = ["fake path #{i}", "fake cls #{i}"]
95
+ allow(@obj).to receive(:load_component_by_type)
96
+ .with(v, Issuesrc::TAG_FINDERS)
97
+ .and_return([path, cls])
98
+ else
99
+ path, cls = @obj.send(
100
+ :load_component_by_type, v, Issuesrc::TAG_FINDERS)
101
+ end
102
+ allow(@obj).to receive(:do_require)
103
+ .with(path)
104
+ allow(@obj).to receive(:make_tag_finder)
105
+ .with(cls, 'fake tag extractor')
106
+ .and_return("finder #{v}")
107
+ i += 1
108
+ end
109
+ end
110
+
111
+ @obj.send(:load_tag_finders)
112
+ end
113
+
114
+ it 'loads the required tag finders' do
115
+ got = test_load_tag_finders(['fake', 'types'])
116
+ expect(got).to be == ['finder fake', 'finder types']
117
+ end
118
+
119
+ it 'loads the default tag finders when no option is given' do
120
+ got = test_load_tag_finders(nil)
121
+ expect(got).to be == (Issuesrc::DEFAULT_TAG_FINDERS.collect do |v|
122
+ "finder #{v}"
123
+ end)
124
+ end
125
+
126
+ it 'fails when loading an unknown component' do
127
+ expect { test_load_tag_finders(['madeup'], false) }
128
+ .to raise_error(Issuesrc::IssuesrcError)
129
+ end
130
+ end
131
+
132
+ describe '#select_tag_finder_for' do
133
+ before :each do
134
+ @file = instance_double('Issuesrc::FSFile', :type => 'ty')
135
+ @tag_finders = [
136
+ instance_double('Issuesrc::TagFinder'),
137
+ instance_double('Issuesrc::TagFinder')
138
+ ]
139
+ end
140
+
141
+ it 'returns the finder which accepts this file type' do
142
+ allow(@tag_finders[0]).to receive(:accepts?)
143
+ .with(@file).and_return(false)
144
+ allow(@tag_finders[1]).to receive(:accepts?)
145
+ .with(@file).and_return(true)
146
+
147
+ got = @obj.select_tag_finder_for(@file, @tag_finders)
148
+
149
+ expect(got).to be == @tag_finders[1]
150
+ end
151
+
152
+ it 'returns nil when no finder is found' do
153
+ allow(@tag_finders[0]).to receive(:accepts?)
154
+ .with(@file).and_return(false)
155
+ allow(@tag_finders[1]).to receive(:accepts?)
156
+ .with(@file).and_return(false)
157
+
158
+ got = @obj.select_tag_finder_for(@file, @tag_finders)
159
+
160
+ expect(got).to be == nil
77
161
  end
78
162
  end
79
163
 
80
- # TODO(#27): Complete testing.
164
+ describe '#classify_tags' do
165
+ before :each do
166
+ @tags = [
167
+ instance_double('Issuesrc::Tag', :issue_id => '123'),
168
+ instance_double('Issuesrc::Tag', :issue_id => nil),
169
+ instance_double('Issuesrc::Tag', :issue_id => '456'),
170
+ instance_double('Issuesrc::Tag', :issue_id => nil),
171
+ ]
172
+ end
173
+
174
+ it 'classifies tags between those with IDs and new ones' do
175
+ tags_by_issue, new_tags = @obj.send(:classify_tags, @tags)
176
+ expect(tags_by_issue).to be == {
177
+ '123' => @tags[0],
178
+ '456' => @tags[2],
179
+ }
180
+ expect(new_tags).to be == [@tags[1], @tags[3]]
181
+ end
182
+ end
183
+
184
+ describe '#save_tag_in_file' do
185
+ before :each do
186
+ @obj.init_files_offsets()
187
+ @tag = instance_double('Issuesrc::Tag',
188
+ :issue_id => '123',
189
+ :file => instance_double('Issuesrc::FSFile', :path => 'fake path'))
190
+ end
191
+
192
+ it 'saves a tag in its file and saves the given offset' do
193
+ allow(@tag).to receive(:write_in_file).with([]).and_return('offsets')
194
+ @obj.send(:save_tag_in_file, @tag)
195
+
196
+ expect(@obj.files_offsets).to be == {
197
+ 'fake path' => 'offsets'
198
+ }
199
+ end
200
+ end
81
201
  end
@@ -0,0 +1,4 @@
1
+ require_relative '../spec_helper'
2
+ require_relative '../../lib/sourcers/git_sourcer'
3
+
4
+ # TODO(#34): Test GitSourcer.
@@ -0,0 +1,4 @@
1
+ require_relative '../spec_helper'
2
+ require_relative '../../lib/sourcers/github_sourcer'
3
+
4
+ # TODO(#35): Test GithubSourcer.
@@ -0,0 +1,5 @@
1
+ if ENV['COVERAGE'] == 'true'
2
+ require 'simplecov'
3
+
4
+ SimpleCov.start
5
+ end
@@ -0,0 +1,71 @@
1
+ require_relative '../spec_helper'
2
+ require_relative '../../lib/tag_finders/blunt_tag_finder'
3
+
4
+ describe Issuesrc::TagFinders::BluntTagFinder do
5
+ before :all do
6
+ tag_extractor = Issuesrc::TagExtractor.new({}, {})
7
+ @obj = Issuesrc::TagFinders::BluntTagFinder.new(tag_extractor, {}, {})
8
+
9
+ # BUG: Comments in @base_file are being parsed! We need a way of overriding this.
10
+ @base_file = <<EOD
11
+ This is a file with several kind of comments supposed to be caught.
12
+
13
+ /*This is a normal block of BUG code*/
14
+
15
+ <!--
16
+ TODO: Multiline should work too.
17
+ abc BUG: Another one.
18
+ -->
19
+
20
+ Haskell has {- TODO comments like-} this.
21
+
22
+
23
+ // This should match FIXME(tcard): until the end of the line.
24
+
25
+ Also # TODO(#38): like this.
26
+ "Don't" be -- crazy TODO and # TODO(#39): mix them!
27
+
28
+ EOD
29
+
30
+ @cases = {
31
+ 'madeup' => [
32
+ # BUG(#40): Shouldn't match end of block comments.
33
+ [97, 107, 'BUG: code*/'],
34
+ [240, 280, 'FIXME(tcard): until the end of the line.'],
35
+ ],
36
+ 'html' => [
37
+ [114, 146, 'TODO: Multiline should work too.'],
38
+ [152, 169, 'BUG: Another one.'],
39
+ ],
40
+ 'sql' => [
41
+ [326, 351, 'TODO: and # TODO mix them!'],
42
+ ],
43
+ 'sh' => [
44
+ [289, 305, 'TODO: like this.'],
45
+ [337, 351, 'TODO: mix them!'],
46
+ ],
47
+ 'hs' => [
48
+ [190, 210, 'TODO: comments like-}'],
49
+ [326, 351, 'TODO: and # TODO mix them!'],
50
+ ],
51
+ }
52
+ end
53
+
54
+ it 'finds all the expected tags for each language' do
55
+ @cases.each do |type, expected|
56
+ file = instance_double('Issuesrc::FSFile', :type => type)
57
+ expect(@obj.accepts?(file)).to be == true
58
+
59
+ allow(@obj).to receive(:get_file_body).with(file)
60
+ .and_return(@base_file)
61
+
62
+ tags = []
63
+ @obj.find_tags(file) do |tag|
64
+ expect(tag.file).to be == file
65
+ tags << [tag.begin_pos, tag.end_pos, tag.to_s]
66
+ end
67
+
68
+ expect(tags).to be == expected
69
+ end
70
+ end
71
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: issuesrc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Toni Cárdenas
@@ -89,6 +89,7 @@ extensions: []
89
89
  extra_rdoc_files: []
90
90
  files:
91
91
  - .gitignore
92
+ - .travis.yml
92
93
  - Gemfile
93
94
  - LICENSE.txt
94
95
  - README.md
@@ -97,6 +98,7 @@ files:
97
98
  - example.toml
98
99
  - issuesrc.gemspec
99
100
  - lib/issuers/github_issuer.rb
101
+ - lib/issuers/issuers.rb
100
102
  - lib/issuesrc.rb
101
103
  - lib/issuesrc/config.rb
102
104
  - lib/issuesrc/event_loop.rb
@@ -106,7 +108,9 @@ files:
106
108
  - lib/issuesrc/version.rb
107
109
  - lib/sourcers/git_sourcer.rb
108
110
  - lib/sourcers/github_sourcer.rb
111
+ - lib/sourcers/sourcers.rb
109
112
  - lib/tag_finders/blunt_tag_finder.rb
113
+ - lib/tag_finders/tag_finders.rb
110
114
  - spec/issuers/github_issuer_spec.rb
111
115
  - spec/issuesrc/config_spec.rb
112
116
  - spec/issuesrc/event_loop_spec.rb
@@ -116,6 +120,7 @@ files:
116
120
  - spec/issuesrc_spec.rb
117
121
  - spec/sourcers/git_sourcer_spec.rb
118
122
  - spec/sourcers/github_sourcer_spec.rb
123
+ - spec/spec_helper.rb
119
124
  - spec/tag_finders/blunt_tag_finder_spec.rb
120
125
  homepage: https://github.com/tcard/issuesrc
121
126
  licenses:
@@ -151,5 +156,6 @@ test_files:
151
156
  - spec/issuesrc_spec.rb
152
157
  - spec/sourcers/git_sourcer_spec.rb
153
158
  - spec/sourcers/github_sourcer_spec.rb
159
+ - spec/spec_helper.rb
154
160
  - spec/tag_finders/blunt_tag_finder_spec.rb
155
161
  has_rdoc: