watson-ruby 1.0.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/lib/watson/fs.rb ADDED
@@ -0,0 +1,69 @@
1
+ module Watson
2
+ # File system utility function class
3
+ # Contains all methods for file access in watson
4
+ class FS
5
+
6
+ # Debug printing for this class
7
+ DEBUG = false
8
+
9
+ class << self
10
+ # [todo] - Not sure whether to make check* methods return FP
11
+ # Makes it nice to get it returned and use it but
12
+ # not sure how to deal with closing the FP after
13
+ # Currently just close inside
14
+
15
+ # Include for debug_print
16
+ include Watson
17
+
18
+ ###########################################################
19
+ # Check if file exists and can be opened
20
+ def check_file(file)
21
+
22
+ # Identify method entry
23
+ debug_print "#{ self } : #{ __method__ }\n"
24
+
25
+ # Error check for input
26
+ if file.length == 0
27
+ debug_print "No file specified\n"
28
+ return false
29
+ end
30
+
31
+ # Check if file can be opened
32
+ if File.readable?(file)
33
+ debug_print "#{ file } exists and opened successfully\n"
34
+ return true
35
+ else
36
+ debug_print "Could not open #{ file }, skipping\n"
37
+ return false
38
+ end
39
+ end
40
+
41
+
42
+ ###########################################################
43
+ # Check if directory exists and can be opened
44
+ def check_dir(dir)
45
+
46
+ # Identify method entry
47
+ debug_print "#{ self } : #{ __method__ }\n"
48
+
49
+ # Error check for input
50
+ if dir.length == 0
51
+ debug_print "No directory specified\n"
52
+ return false
53
+ end
54
+
55
+ # Check if directory exists
56
+ if Dir.exists?(dir)
57
+ debug_print "#{ dir } exists and opened succesfully\n"
58
+ return true
59
+ else
60
+ debug_print "Could not open #{ dir }, skipping\n"
61
+ return false
62
+ end
63
+ end
64
+
65
+ end
66
+ end
67
+ end
68
+
69
+
@@ -0,0 +1,369 @@
1
+ module Watson
2
+ class Remote
3
+ # GitHub remote access class
4
+ # Contains all necessary methods to obtain access to, get issue list,
5
+ # and post issues to GitHub
6
+ class GitHub
7
+
8
+ # Debug printing for this class
9
+ DEBUG = false
10
+
11
+ class << self
12
+
13
+ # [todo] - Allow closing of issues from watson? Don't like that idea but maybe
14
+ # [review] - Properly scope Printer class so we dont need the Printer. for
15
+ # method calls?
16
+ # [todo] - Keep asking for user data until valid instead of leaving app
17
+
18
+
19
+ # Include for debug_print
20
+ include Watson
21
+
22
+ #############################################################################
23
+ # Setup remote access to GitHub
24
+ # Get Username, Repo, and PW and perform necessary HTTP calls to check validity
25
+ def setup(config)
26
+
27
+ # Identify method entry
28
+ debug_print "#{ self.class } : #{ __method__ }\n"
29
+
30
+ Printer.print_status "+", GREEN
31
+ print BOLD + "Obtaining OAuth Token for GitHub...\n" + RESET
32
+
33
+ # Check config to make sure no previous API exists
34
+ unless config.github_api.empty? && config.github_repo.empty?
35
+ Printer.print_status "!", RED
36
+ print BOLD + "Previous GitHub API + Repo is in RC, are you sure you want to overwrite?\n" + RESET
37
+ print " (Y)es/(N)o: "
38
+
39
+ # Get user input
40
+ _overwrite = $stdin.gets.chomp
41
+ if ["no", "n"].include?(_overwrite.downcase)
42
+ print "\n"
43
+ Printer.print_status "x", RED
44
+ print BOLD + "Not overwriting current GitHub API + repo info\n" + RESET
45
+ return false
46
+ end
47
+ end
48
+
49
+
50
+ Printer.print_status "!", YELLOW
51
+ print BOLD + "Access to your GitHub account required to make/update issues\n" + RESET
52
+ print " See help or README for more details on GitHub/Bitbucket access\n\n"
53
+
54
+
55
+ # [todo] - Don't just check for blank password but invalid as well
56
+ # Poor mans username/password grabbing
57
+ print BOLD + "Username: " + RESET
58
+ _username = $stdin.gets.chomp
59
+ if _username.empty?
60
+ Printer.print_status "x", RED
61
+ print BOLD + "Input blank. Please enter your username!\n\n" + RESET
62
+ return false
63
+ end
64
+
65
+ # [fix] - Crossplatform password block needed, not sure if current method is safe either
66
+ # Block output to tty to prevent PW showing, Linux/Unix only :(
67
+ print BOLD + "Password: " + RESET
68
+ system "stty -echo"
69
+ _password = $stdin.gets.chomp
70
+ system "stty echo"
71
+ print "\n\n"
72
+ if _password.empty?
73
+ Printer.print_status "x", RED
74
+ print BOLD + "Input is blank. Please enter your password!\n\n" + RESET
75
+ return false
76
+ end
77
+
78
+ # HTTP Request to get OAuth Token
79
+ # GitHub API v3 - http://developer.github.com/v3/
80
+
81
+ # Create options hash to pass to Remote::http_call
82
+ # Auth URL for GitHub + SSL
83
+ # Repo scope + notes for watson
84
+ # Basic auth with user input
85
+ opts = {:url => "https://api.github.com/authorizations",
86
+ :ssl => true,
87
+ :method => "POST",
88
+ :basic_auth => [_username, _password],
89
+ :data => {"scopes" => ["repo"],
90
+ "note" => "watson",
91
+ "note_url" => "http://watson.goosecode.com/" },
92
+ :verbose => false
93
+ }
94
+
95
+ _json, _resp = Watson::Remote.http_call(opts)
96
+
97
+ # Check response to validate authorization
98
+ if _resp.code == "201"
99
+ Printer.print_status "o", GREEN
100
+ print BOLD + "Obtained OAuth Token\n\n" + RESET
101
+ else
102
+ Printer.print_status "x", RED
103
+ print BOLD + "Unable to obtain OAuth Token\n" + RESET
104
+ print " Status: #{ _resp.code } - #{ _resp.message }\n\n"
105
+ return false
106
+ end
107
+
108
+ # Store API key obtained from POST to @config.github_api
109
+ config.github_api = _json["token"]
110
+ debug_print "Config GitHub API Key updated to: #{ config.github_api }\n"
111
+
112
+
113
+ # Get repo information, if blank give error
114
+ Printer.print_status "!", YELLOW
115
+ print BOLD + "Repo information required\n" + RESET
116
+ print " Please provide owner that repo is under followed by repo name\n"
117
+ print " e.g. owner: nhmood, repo: watson (case sensitive)\n"
118
+ print " See help or README for more details on GitHub access\n\n"
119
+
120
+ print BOLD + "Owner: " + RESET
121
+ _owner = $stdin.gets.chomp
122
+ if _owner.empty?
123
+ print "\n"
124
+ Printer.print_status "x", RED
125
+ print BOLD + "Input blank. Please enter the owner the repo is under!\n\n" + RESET
126
+ return false
127
+ end
128
+
129
+ print BOLD + "Repo: " + RESET
130
+ _repo = $stdin.gets.chomp
131
+ if _repo.empty?
132
+ print "\n"
133
+ Printer.print_status "x", RED
134
+ print BOLD + "Input blank. Please enter the repo name!\n\n" + RESET
135
+ return false
136
+ end
137
+
138
+
139
+ # Make call to GitHub API to create new label for Issues
140
+ # If status returns not 404, then we have access to repo (+ it exists)
141
+ # If 422, then (most likely) the label already exists
142
+
143
+ # Create options hash to pass to Remote::http_call
144
+ # Label URL for GitHub + SSL
145
+ #
146
+ # Auth token
147
+ opts = {:url => "https://api.github.com/repos/#{ _owner }/#{ _repo }/labels",
148
+ :ssl => true,
149
+ :method => "POST",
150
+ :auth => config.github_api,
151
+ :data => {"name" => "watson",
152
+ "color" => "00AEEF" },
153
+ :verbose => false
154
+ }
155
+
156
+ _json, _resp = Watson::Remote.http_call(opts)
157
+
158
+ # [review] - This is pretty messy, maybe clean it up later
159
+ # Check response to validate repo access
160
+ if _resp.code == "404"
161
+ print "\n"
162
+ Printer.print_status "x", RED
163
+ print BOLD + "Unable to access /#{ _owner }/#{ _repo } with given credentials\n" + RESET
164
+ print " Check that credentials are correct and repository exists under user\n"
165
+ print " Status: #{ _resp.code } - #{ _resp.message }\n\n"
166
+ return false
167
+
168
+ else
169
+ # If it is anything but a 404, I THINK it means we have access...
170
+ # Will assume that until proven otherwise
171
+ print "\n"
172
+ Printer.print_status "o", GREEN
173
+ print BOLD + "Repo successfully accessed\n\n" + RESET
174
+ end
175
+
176
+ # Store owner/repo obtained from POST to @config.github_repo
177
+ config.github_repo = "#{ _owner }/#{ _repo }"
178
+ debug_print "Config GitHub API Key updated to: #{ config.github_repo }\n"
179
+
180
+ # Inform user of label creation status (created above)
181
+ Printer.print_status "+", GREEN
182
+ print BOLD + "Creating label for watson on GitHub...\n" + RESET
183
+ if _resp.code == "201"
184
+ Printer.print_status "+", GREEN
185
+ print BOLD + "Label successfully created\n" + RESET
186
+ elsif _resp.code == "422" && _json["code"] == "already_exists"
187
+ Printer.print_status "!", YELLOW
188
+ print BOLD + "Label already exists\n" + RESET
189
+ else
190
+ Printer.print_status "x", RED
191
+ print BOLD + "Unable to create label for /#{ _owner }/#{ _repo }\n" + RESET
192
+ print " Status: #{ _resp.code } - #{ _resp.message }\n"
193
+ end
194
+
195
+ # All setup has been completed, need to update RC
196
+ # Call config updater/writer from @config to write config
197
+ debug_print "Updating config with new GitHub info\n"
198
+ config.update_conf("github_api", "github_repo")
199
+
200
+ # Give user some info
201
+ print "\n"
202
+ Printer.print_status "o", GREEN
203
+ print BOLD + "GitHub successfully setup\n" + RESET
204
+ print " Issues will now automatically be retrieved from GitHub by default\n"
205
+ print " Use -p, --push to post issues to GitHub\n"
206
+ print " See help or README for more details on GitHub/Bitbucket access\n\n"
207
+
208
+ return true
209
+
210
+ end
211
+
212
+
213
+ ###########################################################
214
+ # Get all remote GitHub issues and store into Config container class
215
+
216
+ def get_issues(config)
217
+
218
+ # Identify method entry
219
+ debug_print "#{ self.class } : #{ __method__ }\n"
220
+
221
+ # Only attempt to get issues if API is specified
222
+ if config.github_api.empty?
223
+ debug_print "No API found, this shouldn't be called...\n"
224
+ return false
225
+ end
226
+
227
+
228
+ # Get all open tickets
229
+ # Create options hash to pass to Remote::http_call
230
+ # Issues URL for GitHub + SSL
231
+ opts = {:url => "https://api.github.com/repos/#{ config.github_repo }/issues?labels=watson&state=open",
232
+ :ssl => true,
233
+ :method => "GET",
234
+ :auth => config.github_api,
235
+ :verbose => false
236
+ }
237
+
238
+ _json, _resp = Watson::Remote.http_call(opts)
239
+
240
+
241
+ # Check response to validate repo access
242
+ if _resp.code != "200"
243
+ Printer.print_status "x", RED
244
+ print BOLD + "Unable to access remote #{ config.github_repo }, GitHub API may be invalid\n" + RESET
245
+ print " Consider running --remote (-r) option to regenerate key\n\n"
246
+ print " Status: #{ _resp.code } - #{ _resp.message }\n"
247
+
248
+ debug_print "GitHub invalid, setting config var\n"
249
+ config.github_valid = false
250
+ return false
251
+ end
252
+
253
+ config.github_issues[:open] = _json.empty? ? Hash.new : _json
254
+ config.github_valid = true
255
+
256
+ # Get all closed tickets
257
+ # Create option hash to pass to Remote::http_call
258
+ # Issues URL for GitHub + SSL
259
+ opts = {:url => "https://api.github.com/repos/#{ config.github_repo }/issues?labels=watson&state=closed",
260
+ :ssl => true,
261
+ :method => "GET",
262
+ :auth => config.github_api,
263
+ :verbose => false
264
+ }
265
+
266
+ _json, _resp = Watson::Remote.http_call(opts)
267
+
268
+ # Check response to validate repo access
269
+ # Shouldn't be necessary if we passed the last check but just to be safe
270
+ if _resp.code != "200"
271
+ Printer.print_status "x", RED
272
+ print BOLD + "Unable to get closed issues.\n" + RESET
273
+ print " Since the open issues were obtained, something is probably wrong and you should file a bug report or something...\n"
274
+ print " Status: #{ _resp.code } - #{ _resp.message }\n"
275
+
276
+ debug_print "GitHub invalid, setting config var\n"
277
+ config.github_valid = false
278
+ return false
279
+ end
280
+
281
+ config.github_issues[:closed] = _json.empty? ? Hash.new : _json
282
+ config.github_valid = true
283
+ return true
284
+ end
285
+
286
+
287
+ ###########################################################
288
+ # Post given issue to remote GitHub repo
289
+ def post_issue(issue, config)
290
+ # [todo] - Better way to identify/compare remote->local issues than md5
291
+ # Current md5 based on some things that easily can change, need better ident
292
+
293
+ # Identify method entry
294
+ debug_print "#{ self.class } : #{ __method__ }\n"
295
+
296
+
297
+ # Only attempt to get issues if API is specified
298
+ if config.github_api.empty?
299
+ debug_print "No API found, this shouldn't be called...\n"
300
+ return false
301
+ end
302
+
303
+ # Check that issue hasn't been posted already by comparing md5s
304
+ # Go through all open issues, if there is a match in md5, return out of method
305
+ # [todo] - Play with idea of making body of GitHub issue hash format to be exec'd
306
+ # Store pieces in text as :md5 => "whatever" so when we get issues we can
307
+ # call exec and turn it into a real hash for parsing in watson
308
+ # Makes watson code cleaner but not as readable comment on GitHub...?
309
+ debug_print "Checking open issues to see if already posted\n"
310
+ config.github_issues[:open].each do | _open |
311
+ if _open["body"].include?(issue[:md5])
312
+ debug_print "Found in #{ _open["title"] }, not posting\n"
313
+ return false
314
+ end
315
+ debug_print "Did not find in #{ _open["title"] }\n"
316
+ end
317
+
318
+
319
+ debug_print "Checking closed issues to see if already posted\n"
320
+ config.github_issues[:closed].each do | _closed |
321
+ if _closed["body"].include?(issue[:md5])
322
+ debug_print "Found in #{ _closed["title"] }, not posting\n"
323
+ return false
324
+ end
325
+ debug_print "Did not find in #{ _closed["title"] }\n"
326
+ end
327
+
328
+ # We didn't find the md5 for this issue in the open or closed issues, so safe to post
329
+
330
+ # Create the body text for the issue here, too long to fit nicely into opts hash
331
+ # [review] - Only give relative path for privacy when posted
332
+ _body = "__filename__ : #{ issue[:path] }\n" +
333
+ "__line #__ : #{ issue[:line_number] }\n" +
334
+ "__tag__ : #{ issue[:tag] }\n" +
335
+ "__md5__ : #{ issue[:md5] }\n\n" +
336
+ "#{ issue[:context].join }\n"
337
+
338
+ # Create option hash to pass to Remote::http_call
339
+ # Issues URL for GitHub + SSL
340
+ opts = {:url => "https://api.github.com/repos/#{ config.github_repo }/issues",
341
+ :ssl => true,
342
+ :method => "POST",
343
+ :auth => config.github_api,
344
+ :data => { "title" => issue[:title] + " [#{ issue[:path] }]",
345
+ "labels" => [issue[:tag], "watson"],
346
+ "body" => _body },
347
+ :verbose => false
348
+ }
349
+
350
+ _json, _resp = Watson::Remote.http_call(opts)
351
+
352
+
353
+ # Check response to validate repo access
354
+ # Shouldn't be necessary if we passed the last check but just to be safe
355
+ if _resp.code != "201"
356
+ Printer.print_status "x", RED
357
+ print BOLD + "Post unsuccessful. \n" + RESET
358
+ print " Since the open issues were obtained earlier, something is probably wrong and you should let someone know...\n"
359
+ print " Status: #{ _resp.code } - #{ _resp.message }\n"
360
+ return false
361
+ end
362
+
363
+ return true
364
+ end
365
+
366
+ end
367
+ end
368
+ end
369
+ end
@@ -0,0 +1,405 @@
1
+ module Watson
2
+ # Dir/File parser class
3
+ # Contains all necessary methods to parse through files and directories
4
+ # for specified tags and generate data structure containing found issues
5
+ class Parser
6
+
7
+ # Include for debug_print
8
+ include Watson
9
+
10
+ # [review] - Should this require be required higher up or fine here
11
+ # Include for Digest::MD5.hexdigest used in issue creating
12
+ require 'digest'
13
+ require 'pp'
14
+
15
+ # Debug printing for this class
16
+ DEBUG = false
17
+
18
+ ###########################################################
19
+ # Initialize the parser with the current watson config
20
+ def initialize(config)
21
+ # [review] - Not sure if passing config here is best way to access it
22
+
23
+ # Identify method entry
24
+ debug_print "#{ self } : #{ __method__ }\n"
25
+
26
+ @config = config
27
+ end
28
+
29
+
30
+ ###########################################################
31
+ # Begins parsing of files / dirs specified in the initial dir/file lists
32
+ def run
33
+
34
+ # Identify method entry
35
+ debug_print "#{ self } : #{ __method__ }\n"
36
+
37
+ # Go through all files added from CL (sort them first)
38
+ # If empty, sort and each will do nothing, no errors
39
+ _completed_dirs = Array.new()
40
+ _completed_files = Array.new()
41
+ if @config.cl_entry_set
42
+ @config.file_list.sort.each do | _file |
43
+ _completed_files.push(parse_file(_file))
44
+ end
45
+ end
46
+
47
+ # Then go through all the specified directories
48
+ # Initial parse depth to parse_dir is 0 (unlimited)
49
+ @config.dir_list.sort.each do | _dir |
50
+ _completed_dirs.push(parse_dir(_dir, 0))
51
+ end
52
+
53
+ # Create overall hash for parsed files
54
+ _structure = Hash.new()
55
+ _structure[:files] = _completed_files
56
+ _structure[:subdirs] = _completed_dirs
57
+
58
+ debug_print "_structure dump\n\n"
59
+ debug_print PP.pp(_structure, "")
60
+ debug_print "\n\n"
61
+
62
+ return _structure
63
+ end
64
+
65
+
66
+ ###########################################################
67
+ # Parse through specified directory and find all subdirs and files
68
+ def parse_dir(dir, depth)
69
+
70
+ # Identify method entry
71
+ debug_print "#{ self } : #{ __method__ }\n"
72
+
73
+ # Error check on input
74
+ if !Watson::FS.check_dir(dir)
75
+ print "Unable to open #{ dir }, exiting\n"
76
+ return false
77
+ else
78
+ debug_print "Opened #{ dir } for parsing\n"
79
+ end
80
+
81
+ debug_print "Parsing through all files/directories in #{ dir }\n"
82
+
83
+ # [review] - Shifted away from single Dir.glob loop to separate for dir/file
84
+ # This duplicates code but is much better for readability
85
+ # Not sure which is preferred?
86
+
87
+
88
+ # Remove leading . or ./
89
+ _glob_dir = dir.gsub(/^\.(\/?)/, '')
90
+ debug_print "_glob_dir: #{_glob_dir}\n"
91
+
92
+
93
+ # Go through directory to find all files
94
+ # Create new array to hold all parsed files
95
+ _completed_files = Array.new()
96
+ Dir.glob("#{ _glob_dir }{*,.*}").select { | _fn | File.file?(_fn) }.sort.each do | _entry |
97
+ debug_print "Entry: #{_entry} is a file\n"
98
+
99
+
100
+ # [review] - Warning to user when file is ignored? (outside of debug_print)
101
+ # Check against ignore list, if match, set to "" which will be ignored
102
+ @config.ignore_list.each do | _ignore |
103
+ # [review] - Better "Ruby" way to check for "*"?
104
+ # [review] - Probably cleaner way to perform multiple checks below
105
+ # Look for *.type on list, regex to match entry
106
+ if _ignore[0] == "*"
107
+ _cut = _ignore[1..-1]
108
+ if _entry.match(/#{ _cut }/)
109
+ debug_print "#{ _entry } is on the ignore list, setting to \"\"\n"
110
+ _entry = ""
111
+ break
112
+ end
113
+ # Else check for verbose ignore match
114
+ else
115
+ if _entry == _ignore || File.absolute_path(_entry) == _ignore
116
+ debug_print "#{ _entry } is on the ignore list, setting to \"\"\n"
117
+ _entry = ""
118
+ break
119
+ end
120
+ end
121
+ end
122
+
123
+ # If the resulting entry (after filtering) isn't empty, parse it and push into file array
124
+ if !_entry.empty?
125
+ debug_print "Parsing #{ _entry }\n"
126
+ _completed_files.push(parse_file(_entry))
127
+ end
128
+
129
+ end
130
+
131
+
132
+ # Go through directory to find all subdirs
133
+ # Create new array to hold all parsed subdirs
134
+ _completed_dirs = Array.new()
135
+ Dir.glob("#{ _glob_dir }{*, .*}").select { | _fn | File.directory?(_fn) }.sort.each do | _entry |
136
+ debug_print "Entry: #{ _entry } is a dir\n"
137
+
138
+
139
+
140
+ ## Depth limit logic
141
+ # Current depth is depth of previous parse_dir (passed in as second param) + 1
142
+ _cur_depth = depth + 1
143
+ debug_print "Current Folder depth: #{ _cur_depth }\n"
144
+
145
+ # If Config.parse_depth is 0, no limit on subdir parsing
146
+ if @config.parse_depth == 0
147
+ debug_print "No max depth, parsing directory\n"
148
+ _completed_dirs.push(parse_dir("#{ _entry }/", _cur_depth))
149
+ # If current depth is less than limit (set in config), parse directory and pass depth
150
+ elsif _cur_depth < @config.parse_depth.to_i + 1
151
+ debug_print "Depth less than max dept (from config), parsing directory\n"
152
+ _completed_dirs.push(parse_dir("#{ _entry }/", _cur_depth))
153
+ # Else, depth is greater than limit, ignore the directory
154
+ else
155
+ debug_print "Depth greater than max depth, ignoring\n"
156
+ end
157
+
158
+ # Add directory to ignore list so it isn't repeated again accidentally
159
+ @config.ignore_list.push(_entry)
160
+ end
161
+
162
+
163
+ # [review] - Not sure if Dir.glob requires a explicit directory/file close?
164
+
165
+ # Create hash to hold all parsed files and directories
166
+ _structure = Hash.new()
167
+ _structure[:curdir] = dir
168
+ _structure[:files] = _completed_files
169
+ _structure[:subdirs] = _completed_dirs
170
+ return _structure
171
+ end
172
+
173
+
174
+ ###########################################################
175
+ # Parse through individual files looking for issue tags, then generate formatted issue hash
176
+ def parse_file(filename)
177
+ # [review] - Rename method input param to filename (more verbose?)
178
+
179
+ # Identify method entry
180
+ debug_print "#{ self } : #{ __method__ }\n"
181
+
182
+ _relative_path = filename
183
+ _absolute_path = File.absolute_path(filename)
184
+
185
+ # Error check on input, use input filename to make sure relative path is correct
186
+ if !Watson::FS.check_file(_relative_path)
187
+ print "Unable to open #{ _relative_path }, exiting\n"
188
+ return false
189
+ else
190
+ debug_print "Opened #{ _relative_path } for parsing\n"
191
+ debug_print "Short path: #{ _relative_path }\n"
192
+ end
193
+
194
+
195
+ # Get filetype and set corresponding comment type
196
+ _comment_type = get_comment_type(_relative_path)
197
+ if !_comment_type
198
+ debug_print "Using default (#) comment type\n"
199
+ _comment_type = "#"
200
+ end
201
+
202
+
203
+ # Open file and read in entire thing into an array
204
+ # Use an array so we can look ahead when creating issues later
205
+ # [review] - Not sure if explicit file close is required here
206
+ # [review] - Better var name than data for read in file?
207
+ _data = Array.new()
208
+ File.open(_absolute_path, 'r').read.each_line do | _line |
209
+ _data.push(_line)
210
+ end
211
+
212
+
213
+ # Initialize issue list hash
214
+ _issue_list = Hash.new()
215
+ _issue_list[:relative_path] = _relative_path
216
+ _issue_list[:absolute_path] = _absolute_path
217
+ _issue_list[:has_issues] = false
218
+ @config.tag_list.each do | _tag |
219
+ debug_print "Creating array named #{ _tag }\n"
220
+ # [review] - Use to_sym to make tag into symbol instead of string?
221
+ _issue_list[_tag] = Array.new
222
+ end
223
+
224
+ # Loop through all array elements (lines in file) and look for issues
225
+ _data.each_with_index do | _line, _i |
226
+
227
+ # Find any comment line with [tag] - text (any comb of space and # acceptable)
228
+ # Using if match to stay consistent (with config.rb) see there for
229
+ # explanation of why I do this (not a good good one persay...)
230
+ _mtch = _line.encode('UTF-8', :invalid => :replace).match(/^[#{ _comment_type }+?\s+?]+\[(\w+)\]\s+-\s+(.+)/)
231
+ if !_mtch
232
+ debug_print "No valid tag found in line, skipping\n"
233
+ next
234
+ end
235
+
236
+ # Set tag
237
+ _tag = _mtch[1]
238
+
239
+ # Make sure that the tag that was found is something we accept
240
+ # If not, skip it but tell user about an unrecognized tag
241
+ if !@config.tag_list.include?(_tag)
242
+ Printer.print_status "!", RED
243
+ print "Unknown tag [#{ _tag }] found, ignoring\n"
244
+ print " You might want to include it in your RC or with the -t/--tags flag\n"
245
+ next
246
+ end
247
+
248
+ # Found a valid match (with recognized tag)
249
+ # Set flag for this issue_list (for file) to indicate that
250
+ _issue_list[:has_issues] = true
251
+
252
+ _title = _mtch[2]
253
+ debug_print "Issue found\n"
254
+ debug_print "Tag: #{ _tag }\n"
255
+ debug_print "Issue: #{ _title }\n"
256
+
257
+ # Create hash for each issue found
258
+ _issue = Hash.new
259
+ _issue[:line_number] = _i + 1
260
+ _issue[:title] = _title
261
+
262
+ # Grab context of issue specified by Config param (+1 to include issue itself)
263
+ _context = _data[_i..(_i + @config.context_depth + 1)]
264
+
265
+ # [review] - There has got to be a better way to do this...
266
+ # Go through each line of context and determine indentation
267
+ # Used to preserve indentation in post
268
+ _cut = Array.new
269
+ _context.each do | _line |
270
+ _max = 0
271
+ # Until we reach a non indent OR the line is empty, keep slicin'
272
+ until !_line.match(/^( |\t|\n)/) || _line.empty?
273
+ # [fix] - Replace with inplace slice!
274
+ _line = _line.slice(1..-1)
275
+ _max = _max + 1
276
+
277
+ debug_print "New line: #{ _line }\n"
278
+ debug_print "Max indent: #{ _max }\n"
279
+ end
280
+
281
+ # Push max indent for current line to the _cut array
282
+ _cut.push(_max)
283
+ end
284
+
285
+ # Print old _context
286
+ debug_print "\n\n Old Context \n"
287
+ debug_print PP.pp(_context, "")
288
+ debug_print "\n\n"
289
+
290
+ # Trim the context lines to be left aligned but maintain indentation
291
+ # Then add a single \t to the beginning so the Markdown is pretty on GitHub/Bitbucket
292
+ _context.map! { | _line | "\t#{ _line.slice(_cut.min .. -1) }" }
293
+
294
+ # Print new _context
295
+ debug_print("\n\n New Context \n")
296
+ debug_print PP.pp(_context, "")
297
+ debug_print("\n\n")
298
+
299
+ _issue[:context] = _context
300
+
301
+ # These are accessible from _issue_list, but we pass individual issues
302
+ # to the remote poster, so we need this here to reference them for GitHub/Bitbucket
303
+ _issue[:tag] = _tag
304
+ _issue[:path] = _relative_path
305
+
306
+ # Generate md5 hash for each specific issue (for bookkeeping)
307
+ _issue[:md5] = ::Digest::MD5.hexdigest("#{ _tag }, #{ _relative_path }, #{ _title }")
308
+ debug_print "#{ _issue }\n"
309
+
310
+
311
+ # [todo] - Figure out a way to queue up posts so user has a progress bar?
312
+ # That way user can tell that wait is because of http calls not app
313
+
314
+ # If GitHub is valid, pass _issue to GitHub poster function
315
+ # [review] - Keep Remote as a static method and pass config every time?
316
+ # Or convert to a regular class and make an instance with @config
317
+
318
+ if @config.remote_valid
319
+ if @config.github_valid
320
+ debug_print "GitHub is valid, posting issue\n"
321
+ Remote::GitHub.post_issue(_issue, @config)
322
+ else
323
+ debug_print "GitHub invalid, not posting issue\n"
324
+ end
325
+
326
+
327
+ if @config.bitbucket_valid
328
+ debug_print "Bitbucket is valid, posting issue\n"
329
+ Remote::Bitbucket.post_issue(_issue, @config)
330
+ else
331
+ debug_print "Bitbucket invalid, not posting issue\n"
332
+ end
333
+ end
334
+
335
+ # [review] - Use _tag string as symbol reference in hash or keep as string?
336
+ # Look into to_sym to keep format of all _issue params the same
337
+ _issue_list[_tag].push( _issue )
338
+
339
+ end
340
+
341
+ # [review] - Return of parse_file is different than watson-perl
342
+ # Not sure which makes more sense, ruby version seems simpler
343
+ # perl version might have to stay since hash scoping is weird in perl
344
+ debug_print "\nIssue list: #{ _issue_list }\n"
345
+
346
+ return _issue_list
347
+ end
348
+
349
+
350
+ ###########################################################
351
+ # Get comment syntax for given file
352
+ def get_comment_type(filename)
353
+
354
+ # Identify method entry
355
+ debug_print "#{ self } : #{ __method__ }\n"
356
+
357
+ # Grab the file extension (.something)
358
+ # Check to see whether it is recognized and set comment type
359
+ # If unrecognized, try to grab the next .something extension
360
+ # This is to account for file.cpp.1 or file.cpp.bak, ect
361
+
362
+ # [review] - Matz style while loop a la http://stackoverflow.com/a/10713963/1604424
363
+ # Create _mtch var so we can access it outside of the do loop
364
+
365
+ _mtch = String.new()
366
+ loop do
367
+ _mtch = filename.match(/(\.(\w+))$/)
368
+ debug_print "Extension: #{ _mtch }\n"
369
+
370
+ # Break if we don't find a match
371
+ break if _mtch.nil?
372
+
373
+ # Determine file type
374
+ case _mtch[0]
375
+ # C / C++, Java, C#
376
+ # [todo] - Add /* style comment
377
+ when ".cpp", ".cc", ".c", ".hpp", ".h",
378
+ ".java", ".class", ".cs"
379
+ debug_print "Comment type is: //\n"
380
+ return "//"
381
+
382
+ # Bash, Ruby, Perl, Python
383
+ when ".sh", ".rb", ".pl", ".py"
384
+ debug_print "Comment type is: #\n"
385
+ return "#"
386
+
387
+ # Can't recognize extension, keep looping in case of .bk, .#, ect
388
+ else
389
+ filename = filename.gsub(/(\.(\w+))$/, "")
390
+ debug_print "Didn't recognize, searching #{ filename }\n"
391
+
392
+ end
393
+ end
394
+
395
+ # We didn't find any matches from the filename, return error (0)
396
+ # Deal with what default to use in calling method
397
+ # [review] - Is Ruby convention to return 1 or 0 (or -1) on failure/error?
398
+ debug_print "Couldn't find any recognized extension type\n"
399
+ return false
400
+
401
+ end
402
+
403
+
404
+ end
405
+ end