watson-ruby 1.0.0

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