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.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +28 -0
- data/LICENSE +19 -0
- data/README.md +147 -0
- data/Rakefile +21 -0
- data/assets/defaultConf +21 -0
- data/assets/watson.svg +25 -0
- data/bin/watson +18 -0
- data/lib/watson.rb +69 -0
- data/lib/watson/bitbucket.rb +373 -0
- data/lib/watson/command.rb +470 -0
- data/lib/watson/config.rb +493 -0
- data/lib/watson/fs.rb +69 -0
- data/lib/watson/github.rb +369 -0
- data/lib/watson/parser.rb +405 -0
- data/lib/watson/printer.rb +293 -0
- data/lib/watson/remote.rb +120 -0
- data/lib/watson/version.rb +3 -0
- data/spec/config_spec.rb +119 -0
- data/spec/fs_spec.rb +56 -0
- data/spec/helper_spec.rb +32 -0
- data/spec/parser_spec.rb +128 -0
- data/watson.gemspec +33 -0
- metadata +116 -0
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
|