pdd 0.20.4 → 0.20.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +5 -4
- data/.overcommit.yml +96 -0
- data/.pdd +15 -1
- data/.rubocop.yml +3 -2
- data/.rultor.yml +14 -15
- data/.simplecov +6 -6
- data/.travis.yml +2 -2
- data/Gemfile +1 -1
- data/LICENSE.txt +1 -1
- data/README.md +72 -67
- data/Rakefile +7 -1
- data/assets/puzzles.xsd +1 -1
- data/assets/puzzles.xsl +1 -1
- data/bin/pdd +30 -19
- data/features/catches_broken_puzzles.feature +1 -16
- data/features/cli.feature +1 -1
- data/features/html_output.feature +1 -1
- data/features/rake.feature +21 -0
- data/features/step_definitions/steps.rb +11 -4
- data/features/support/env.rb +2 -1
- data/features/uses_config.feature +1 -1
- data/lib/pdd/puzzle.rb +1 -1
- data/lib/pdd/rake_task.rb +39 -0
- data/lib/pdd/rule/duplicates.rb +2 -1
- data/lib/pdd/rule/estimates.rb +1 -1
- data/lib/pdd/rule/roles.rb +2 -1
- data/lib/pdd/rule/text.rb +2 -1
- data/lib/pdd/source.rb +59 -47
- data/lib/pdd/sources.rb +34 -13
- data/lib/pdd/version.rb +3 -3
- data/lib/pdd.rb +20 -15
- data/pdd.gemspec +13 -10
- data/test/test__helper.rb +15 -2
- data/test/test_duplicates.rb +2 -2
- data/test/test_estimates.rb +2 -2
- data/test/test_pdd.rb +4 -2
- data/test/test_rake_task.rb +18 -0
- data/test/test_roles.rb +2 -2
- data/test/test_source.rb +154 -47
- data/test/test_source_todo.rb +36 -9
- data/test/test_sources.rb +18 -2
- data/test/test_text.rb +2 -2
- data/utils/glob.rb +67 -0
- metadata +51 -17
data/bin/pdd
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
# Copyright (c) 2014-
|
2
|
+
# Copyright (c) 2014-2021 Yegor Bugayenko
|
3
3
|
#
|
4
4
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
5
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -19,8 +19,11 @@
|
|
19
19
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
20
20
|
# SOFTWARE.
|
21
21
|
|
22
|
-
|
22
|
+
$stdout.sync = true
|
23
23
|
|
24
|
+
require 'shellwords'
|
25
|
+
require 'English'
|
26
|
+
require 'find'
|
24
27
|
require 'slop'
|
25
28
|
require 'nokogiri'
|
26
29
|
require 'rainbow'
|
@@ -46,6 +49,8 @@ begin
|
|
46
49
|
o.bool '-v', '--verbose', 'Enable verbose mode (a lot of logging)'
|
47
50
|
o.bool '-q', '--quiet', 'Enable quiet mode (almost no logging)'
|
48
51
|
o.bool '--skip-gitignore', 'Don\'t look into .gitignore for excludes'
|
52
|
+
o.bool '--skip-errors', 'Suppress error as warning and skip badly
|
53
|
+
formatted puzzles'
|
49
54
|
o.bool '-i', '--version', 'Show current version' do
|
50
55
|
puts PDD::VERSION
|
51
56
|
exit
|
@@ -53,21 +58,23 @@ begin
|
|
53
58
|
o.string '-s', '--source', 'Source directory to parse ("." by default)'
|
54
59
|
o.string '-f', '--file', 'File to save XML into'
|
55
60
|
o.array '-e', '--exclude', 'Glob pattern to exclude, e.g. "**/*.jpg"',
|
56
|
-
|
61
|
+
default: []
|
62
|
+
o.array '-n', '--include', 'Glob pattern to include, e.g. "**/*.jpg"',
|
63
|
+
default: []
|
57
64
|
o.string '-t', '--format', 'Format of the report (xml|html)'
|
58
65
|
o.array(
|
59
66
|
'-r', '--rule', 'Rule to apply (can be used many times)',
|
60
67
|
delimiter: ';'
|
61
68
|
)
|
62
69
|
end
|
63
|
-
rescue Slop::Error =>
|
64
|
-
raise StandardError, "#{
|
70
|
+
rescue Slop::Error => e
|
71
|
+
raise StandardError, "#{e.message}, try --help"
|
65
72
|
end
|
66
73
|
|
67
74
|
if opts.help?
|
68
75
|
puts opts
|
69
76
|
puts "This is our README to learn more: \
|
70
|
-
https://github.com/
|
77
|
+
https://github.com/cqfn/pdd/blob/master/README.md"
|
71
78
|
exit
|
72
79
|
end
|
73
80
|
|
@@ -75,13 +82,17 @@ https://github.com/yegor256/pdd/blob/master/README.md"
|
|
75
82
|
raise '-f is mandatory when using -v, try --help for more information'
|
76
83
|
end
|
77
84
|
|
78
|
-
if opts['skip-gitignore']
|
79
|
-
|
85
|
+
if opts['skip-gitignore'] && File.exist?('.gitignore')
|
86
|
+
cfg = File.new('.gitignore')
|
87
|
+
body = File.read(cfg)
|
88
|
+
extra = body.split(/\s+/).map(&:strip)
|
89
|
+
opts['skip-gitignore'] = extra
|
90
|
+
puts "Found #{body.split(/\n/).length} lines in #{File.absolute_path(cfg)}"
|
80
91
|
end
|
81
92
|
|
82
93
|
Encoding.default_external = Encoding::UTF_8
|
83
94
|
Encoding.default_internal = Encoding::UTF_8
|
84
|
-
file = opts.file? ? File.new(opts[:file], 'w') :
|
95
|
+
file = opts.file? ? File.new(opts[:file], 'w') : $stdout
|
85
96
|
output = PDD::Base.new(opts).xml
|
86
97
|
if opts[:format]
|
87
98
|
if opts[:format] == 'html'
|
@@ -95,21 +106,21 @@ https://github.com/yegor256/pdd/blob/master/README.md"
|
|
95
106
|
end
|
96
107
|
end
|
97
108
|
file << output
|
98
|
-
rescue SystemExit =>
|
99
|
-
puts
|
100
|
-
PDD.log.info "Exit code is #{
|
101
|
-
exit(
|
102
|
-
rescue PDD::Error =>
|
103
|
-
puts "#{Rainbow('ERROR').red}: #{
|
109
|
+
rescue SystemExit => e
|
110
|
+
puts e.message unless e.success?
|
111
|
+
PDD.log.info "Exit code is #{e.status}"
|
112
|
+
exit(e.status)
|
113
|
+
rescue PDD::Error => e
|
114
|
+
puts "#{Rainbow('ERROR').red}: #{e.message}
|
104
115
|
If you can't understand the cause of this issue or you don't know \
|
105
116
|
how to fix it, please submit a GitHub issue, we will try to help you: \
|
106
|
-
https://github.com/
|
117
|
+
https://github.com/cqfn/pdd/issues. This tool is still in its beta \
|
107
118
|
version and we will appreciate your feedback. Here is where you can find \
|
108
|
-
more documentation: https://github.com/
|
119
|
+
more documentation: https://github.com/cqfn/pdd/blob/master/README.md."
|
109
120
|
PDD.log.info 'Exit code is 1'
|
110
121
|
exit(1)
|
111
|
-
rescue StandardError =>
|
112
|
-
puts "#{Rainbow('ERROR').red} (#{
|
122
|
+
rescue StandardError => e
|
123
|
+
puts "#{Rainbow('ERROR').red} (#{e.class.name}): #{e.message}"
|
113
124
|
PDD.log.info 'Exit code is 255'
|
114
125
|
exit(255)
|
115
126
|
end
|
@@ -21,22 +21,7 @@ Feature: Catches Broken Puzzles
|
|
21
21
|
}
|
22
22
|
"""
|
23
23
|
When I run pdd it fails with "Space expected"
|
24
|
-
When I run pdd it fails with "
|
25
|
-
|
26
|
-
Scenario: Throwing exception on another broken puzzle
|
27
|
-
Given I have a "Sample.java" file with content:
|
28
|
-
"""
|
29
|
-
public class Main {
|
30
|
-
/**
|
31
|
-
* @todo #13 This puzzle has an incorrect format
|
32
|
-
* because its second line starts with too many spaces
|
33
|
-
*/
|
34
|
-
public void main(String[] args) {
|
35
|
-
// later
|
36
|
-
}
|
37
|
-
}
|
38
|
-
"""
|
39
|
-
When I run pdd it fails with "Too many leading spaces"
|
24
|
+
When I run pdd it fails with "Sample.java:6"
|
40
25
|
|
41
26
|
Scenario: Throwing exception on yet another broken puzzle
|
42
27
|
Given I have a "Sample.java" file with content:
|
data/features/cli.feature
CHANGED
@@ -26,7 +26,7 @@ Feature: Command Line Processing
|
|
26
26
|
"""
|
27
27
|
When I run bin/pdd with "-v -s . -f out.xml"
|
28
28
|
Then Exit code is zero
|
29
|
-
And Stdout contains "Reading ."
|
29
|
+
And Stdout contains "Reading from root dir ."
|
30
30
|
And XML file "out.xml" matches "/puzzles[count(puzzle)=1]"
|
31
31
|
And XML file "out.xml" matches "//puzzle[starts-with(body,'Привет, Let')]"
|
32
32
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
Feature: Rake Task
|
2
|
+
As a source code writer I want to be able to
|
3
|
+
run PDD from Rakefile
|
4
|
+
Scenario: PDD can be used in Rakefile
|
5
|
+
Given It is Unix
|
6
|
+
And I have a "Rakefile" file with content:
|
7
|
+
"""
|
8
|
+
require 'pdd/rake_task'
|
9
|
+
PDD::RakeTask.new(:pdd) do |task|
|
10
|
+
task.includes = ['a.txt']
|
11
|
+
end
|
12
|
+
"""
|
13
|
+
And I have a "a.txt" file with content:
|
14
|
+
"""
|
15
|
+
\x40todo #55 hello!
|
16
|
+
"""
|
17
|
+
|
18
|
+
When I run bash with "rake pdd"
|
19
|
+
Then Exit code is zero
|
20
|
+
|
21
|
+
|
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2021 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -64,9 +64,9 @@ When(/^I run pdd it fails with "([^"]*)"$/) do |txt|
|
|
64
64
|
begin
|
65
65
|
PDD::Base.new(@opts).xml
|
66
66
|
passed = true
|
67
|
-
rescue PDD::Error =>
|
68
|
-
unless
|
69
|
-
raise "PDD failed but exception doesn't contain \"#{txt}\": #{
|
67
|
+
rescue PDD::Error => e
|
68
|
+
unless e.message.include?(txt)
|
69
|
+
raise "PDD failed but exception doesn't contain \"#{txt}\": #{e.message}"
|
70
70
|
end
|
71
71
|
end
|
72
72
|
raise "PDD didn't fail" if passed
|
@@ -90,6 +90,7 @@ end
|
|
90
90
|
|
91
91
|
Then(/^XML file "([^"]+)" matches "([^"]+)"$/) do |file, xpath|
|
92
92
|
raise "File #{file} doesn't exit" unless File.exist?(file)
|
93
|
+
|
93
94
|
xml = Nokogiri::XML.parse(File.read(file))
|
94
95
|
xml.remove_namespaces!
|
95
96
|
if xml.xpath(xpath).empty?
|
@@ -111,6 +112,12 @@ When(/^I run bash with$/) do |text|
|
|
111
112
|
@exitstatus = $CHILD_STATUS.exitstatus
|
112
113
|
end
|
113
114
|
|
115
|
+
When(/^I run bash with "([^"]*)"$/) do |text|
|
116
|
+
FileUtils.copy_entry(@cwd, File.join(@dir, 'pdd'))
|
117
|
+
@stdout = `#{text}`
|
118
|
+
@exitstatus = $CHILD_STATUS.exitstatus
|
119
|
+
end
|
120
|
+
|
114
121
|
Given(/^It is Unix$/) do
|
115
122
|
pending if Gem.win_platform?
|
116
123
|
end
|
data/features/support/env.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2021 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -20,3 +20,4 @@
|
|
20
20
|
|
21
21
|
require 'simplecov'
|
22
22
|
require_relative '../../lib/pdd'
|
23
|
+
require 'aruba/cucumber'
|
data/lib/pdd/puzzle.rb
CHANGED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/tasklib'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'slop'
|
7
|
+
require 'pdd'
|
8
|
+
|
9
|
+
# PDD Rake task
|
10
|
+
module PDD
|
11
|
+
# Rake task
|
12
|
+
class RakeTask < Rake::TaskLib
|
13
|
+
attr_accessor :name, :fail_on_error, :includes, :license, :quiet
|
14
|
+
|
15
|
+
def initialize(*args, &task_block)
|
16
|
+
@name = args.shift || :pdd
|
17
|
+
@includes = []
|
18
|
+
@excludes = []
|
19
|
+
@license = nil
|
20
|
+
@quiet = false
|
21
|
+
desc 'Run PDD' unless ::Rake.application.last_description
|
22
|
+
task(name, *args) do |_, task_args|
|
23
|
+
RakeFileUtils.send(:verbose, true) do
|
24
|
+
yield(*[self, task_args].slice(0, task_block.arity)) if block_given?
|
25
|
+
run
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def run
|
33
|
+
# @todo #125:30m need to implement this method.
|
34
|
+
# For now, it's just a task,
|
35
|
+
# that prints a simple Running pdd... message to user
|
36
|
+
puts 'Running pdd...' unless @quiet
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/lib/pdd/rule/duplicates.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2021 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -35,6 +35,7 @@ module PDD
|
|
35
35
|
.group_by { |p| p.xpath('body/text()').to_s }
|
36
36
|
.map do |_, puzzles|
|
37
37
|
next nil if puzzles.count <= @max
|
38
|
+
|
38
39
|
"there are #{puzzles.count} duplicate(s) of the same puzzle: " +
|
39
40
|
puzzles.map do |p|
|
40
41
|
"#{p.xpath('file/text()')}:#{p.xpath('lines/text()')}"
|
data/lib/pdd/rule/estimates.rb
CHANGED
data/lib/pdd/rule/roles.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2021 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -34,6 +34,7 @@ module PDD
|
|
34
34
|
@xml.xpath('//puzzle').map do |p|
|
35
35
|
role = p.xpath('role/text()').to_s
|
36
36
|
next nil if @roles.include?(role)
|
37
|
+
|
37
38
|
"puzzle #{p.xpath('file/text()')}:#{p.xpath('lines/text()')}" +
|
38
39
|
if role.empty?
|
39
40
|
" doesn't define any role"\
|
data/lib/pdd/rule/text.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2021 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -34,6 +34,7 @@ module PDD
|
|
34
34
|
@xml.xpath('//puzzle').map do |p|
|
35
35
|
words = p.xpath('body/text()').to_s.split.size
|
36
36
|
next nil if words >= @min
|
37
|
+
|
37
38
|
"Puzzle #{p.xpath('file/text()')}:#{p.xpath('lines/text()')}"\
|
38
39
|
" has a very short description of just #{words} words while"\
|
39
40
|
" a minimum of #{@min} is required"
|
data/lib/pdd/source.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2021 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -26,6 +26,7 @@ require_relative '../pdd'
|
|
26
26
|
require_relative '../pdd/puzzle'
|
27
27
|
|
28
28
|
module PDD
|
29
|
+
MARKERS = ["\x40todo", 'TODO:?'].freeze
|
29
30
|
# Source.
|
30
31
|
class Source
|
31
32
|
# Ctor.
|
@@ -36,22 +37,27 @@ module PDD
|
|
36
37
|
@path = path
|
37
38
|
end
|
38
39
|
|
40
|
+
def match_markers(line)
|
41
|
+
MARKERS.map do |mkr|
|
42
|
+
%r{(.*(?:^|\s))#{mkr}\s+#([\w\-.:/]+)\s+(.+)}.match(line)
|
43
|
+
end.compact
|
44
|
+
end
|
45
|
+
|
39
46
|
# Fetch all puzzles.
|
40
47
|
def puzzles
|
41
|
-
PDD.log.info "Reading #{@path}..."
|
48
|
+
PDD.log.info "Reading #{@path} ..."
|
42
49
|
puzzles = []
|
43
50
|
lines = File.readlines(@file, encoding: 'UTF-8')
|
44
51
|
lines.each_with_index do |line, idx|
|
45
52
|
begin
|
46
53
|
check_rules(line)
|
47
|
-
|
48
|
-
puzzles << puzzle(lines.drop(idx + 1), m, idx)
|
49
|
-
end
|
50
|
-
%r{(.*(?:^|\s))TODO:?\s+#([\w\-\.:/]+)\s+(.+)}.match(line) do |m|
|
54
|
+
match_markers(line).each do |m|
|
51
55
|
puzzles << puzzle(lines.drop(idx + 1), m, idx)
|
52
56
|
end
|
53
57
|
rescue Error, ArgumentError => ex
|
54
|
-
|
58
|
+
message = "#{@path}:#{idx + 1} #{ex.message}"
|
59
|
+
raise Error, message unless PDD.opts && PDD.opts['skip-errors']
|
60
|
+
PDD.log.warn message
|
55
61
|
end
|
56
62
|
end
|
57
63
|
puzzles
|
@@ -61,19 +67,19 @@ module PDD
|
|
61
67
|
|
62
68
|
def get_no_leading_space_error(todo)
|
63
69
|
"#{todo} must have a leading space to become \
|
64
|
-
a puzzle, as this page explains: https://github.com/
|
70
|
+
a puzzle, as this page explains: https://github.com/cqfn/pdd#how-to-format"
|
65
71
|
end
|
66
72
|
|
67
73
|
def get_no_puzzle_marker_error(todo)
|
68
74
|
"#{todo} found, but puzzle can't be parsed, \
|
69
75
|
most probably because #{todo} is not followed by a puzzle marker, \
|
70
|
-
as this page explains: https://github.com/
|
76
|
+
as this page explains: https://github.com/cqfn/pdd#how-to-format"
|
71
77
|
end
|
72
78
|
|
73
79
|
def get_space_after_hash_error(todo)
|
74
80
|
"#{todo} found, but there is an unexpected space \
|
75
81
|
after the hash sign, it should not be there, \
|
76
|
-
see https://github.com/
|
82
|
+
see https://github.com/cqfn/pdd#how-to-format"
|
77
83
|
end
|
78
84
|
|
79
85
|
def check_rules(line)
|
@@ -100,7 +106,7 @@ see https://github.com/yegor256/pdd#how-to-format"
|
|
100
106
|
# Fetch puzzle
|
101
107
|
def puzzle(lines, match, idx)
|
102
108
|
tail = tail(lines, match[1], idx)
|
103
|
-
body =
|
109
|
+
body = "#{match[3]} #{tail.join(' ')}".gsub(/\s+/, ' ').strip
|
104
110
|
body = body.chomp('*/-->').strip
|
105
111
|
marker = marker(match[2])
|
106
112
|
Puzzle.new(
|
@@ -115,11 +121,11 @@ see https://github.com/yegor256/pdd#how-to-format"
|
|
115
121
|
|
116
122
|
# Parse a marker.
|
117
123
|
def marker(text)
|
118
|
-
re = %r{([\w
|
124
|
+
re = %r{([\w\-.]+)(?::(\d+)(?:(m|h)[a-z]*)?)?(?:/([A-Z]+))?}
|
119
125
|
match = re.match(text)
|
120
126
|
if match.nil?
|
121
127
|
raise "Invalid puzzle marker \"#{text}\", most probably formatted \
|
122
|
-
against the rules explained here: https://github.com/
|
128
|
+
against the rules explained here: https://github.com/cqfn/pdd#how-to-format"
|
123
129
|
end
|
124
130
|
{
|
125
131
|
ticket: match[1],
|
@@ -138,21 +144,15 @@ against the rules explained here: https://github.com/yegor256/pdd#how-to-format"
|
|
138
144
|
# Fetch puzzle tail (all lines after the first one)
|
139
145
|
def tail(lines, prefix, start)
|
140
146
|
lines
|
141
|
-
.take_while { |t| t.start_with?(prefix) }
|
147
|
+
.take_while { |t| match_markers(t).none? && t.start_with?(prefix) }
|
142
148
|
.map { |t| t[prefix.length, t.length] }
|
143
149
|
.take_while { |t| t =~ /^[ a-zA-Z0-9]/ }
|
144
150
|
.each_with_index do |t, i|
|
145
|
-
|
146
|
-
|
151
|
+
next if t.start_with?(' ')
|
152
|
+
|
153
|
+
raise Error, "Space expected at #{start + i + 2}:#{prefix.length}; \
|
147
154
|
make sure all lines in the puzzle body have a single leading space."
|
148
|
-
|
149
|
-
.each_with_index do |t, i|
|
150
|
-
next if t !~ /^\s{2,}/
|
151
|
-
raise Error, "Too many leading spaces \
|
152
|
-
at #{start + i + 2}:#{prefix.length}; \
|
153
|
-
make sure all lines that include the puzzle body start \
|
154
|
-
at position ##{prefix.length + 1}."
|
155
|
-
end
|
155
|
+
end
|
156
156
|
.map { |t| t[1, t.length] }
|
157
157
|
end
|
158
158
|
|
@@ -168,28 +168,29 @@ at position ##{prefix.length + 1}."
|
|
168
168
|
if `#{git} rev-parse --is-inside-work-tree 2>/dev/null`.strip == 'true'
|
169
169
|
cmd = "#{git} blame -L #{pos},#{pos} --porcelain #{name}"
|
170
170
|
add_github_login(Hash[
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
171
|
+
`#{cmd}`.split("\n").map do |line|
|
172
|
+
case line
|
173
|
+
when /^author /
|
174
|
+
[:author, line.sub(/^author /, '')]
|
175
|
+
when /^author-mail [^@]+@[^.]+\..+/
|
176
|
+
[:email, line.sub(/^author-mail <(.+)>$/, '\1')]
|
177
|
+
when /^author-time /
|
178
|
+
[
|
179
|
+
:time,
|
180
|
+
Time.at(
|
181
|
+
line.sub(/^author-time ([0-9]+)$/, '\1').to_i
|
182
|
+
).utc.iso8601
|
183
|
+
]
|
184
|
+
end
|
185
|
+
end.compact
|
186
|
+
])
|
186
187
|
else
|
187
188
|
{}
|
188
189
|
end
|
189
190
|
end
|
190
191
|
|
191
192
|
def add_github_login(info)
|
192
|
-
login = find_github_login(info
|
193
|
+
login = find_github_login(info)
|
193
194
|
info[:author] = "@#{login}" unless login.empty?
|
194
195
|
info
|
195
196
|
end
|
@@ -204,15 +205,26 @@ at position ##{prefix.length + 1}."
|
|
204
205
|
JSON.parse res.body
|
205
206
|
end
|
206
207
|
|
207
|
-
def find_github_user(
|
208
|
-
|
209
|
-
|
208
|
+
def find_github_user(info)
|
209
|
+
email, author = info.values_at(:email, :author)
|
210
|
+
# if email is not defined, changes have not been committed
|
211
|
+
return if email.nil?
|
212
|
+
|
213
|
+
base_uri = 'https://api.github.com/search/users?per_page=1'
|
214
|
+
query = base_uri + "&q=#{email}+in:email"
|
210
215
|
json = get_json query
|
216
|
+
# find user by name instead since users can make github email private
|
217
|
+
unless json['total_count'].positive?
|
218
|
+
return if author.nil?
|
219
|
+
|
220
|
+
query = base_uri + "&q=#{author}+in:fullname"
|
221
|
+
json = get_json query
|
222
|
+
end
|
211
223
|
json['items'].first
|
212
224
|
end
|
213
225
|
|
214
|
-
def find_github_login(
|
215
|
-
user = find_github_user
|
226
|
+
def find_github_login(info)
|
227
|
+
user = find_github_user info
|
216
228
|
user['login']
|
217
229
|
rescue StandardError
|
218
230
|
''
|
@@ -232,8 +244,8 @@ at position ##{prefix.length + 1}."
|
|
232
244
|
# Fetch all puzzles.
|
233
245
|
def puzzles
|
234
246
|
@source.puzzles
|
235
|
-
rescue Error =>
|
236
|
-
raise Error, "#{@file}; #{
|
247
|
+
rescue Error => e
|
248
|
+
raise Error, "#{@file}; #{e.message}"
|
237
249
|
end
|
238
250
|
end
|
239
251
|
end
|
data/lib/pdd/sources.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2021 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -18,41 +18,61 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
19
|
# SOFTWARE.
|
20
20
|
|
21
|
+
require 'rainbow'
|
21
22
|
require 'shellwords'
|
22
23
|
require 'English'
|
23
24
|
require_relative 'source'
|
25
|
+
require_relative '../../utils/glob'
|
24
26
|
|
25
27
|
module PDD
|
26
28
|
# Code base abstraction
|
27
29
|
class Sources
|
28
30
|
# Ctor.
|
29
31
|
# +dir+:: Directory with source code files
|
30
|
-
def initialize(dir
|
32
|
+
def initialize(dir)
|
31
33
|
@dir = File.absolute_path(dir)
|
32
|
-
@exclude =
|
34
|
+
@exclude = ['.git/**/*']
|
35
|
+
@include = []
|
33
36
|
end
|
34
37
|
|
35
38
|
# Fetch all sources.
|
36
39
|
def fetch
|
40
|
+
exclude_paths = @exclude.map do |ptn|
|
41
|
+
Glob.new(File.join(@dir, ptn)).to_regexp
|
42
|
+
end
|
37
43
|
files = Dir.glob(
|
38
44
|
File.join(@dir, '**/*'), File::FNM_DOTMATCH
|
39
|
-
).reject
|
40
|
-
|
41
|
-
@exclude.each do |ptn|
|
42
|
-
Dir.glob(File.join(@dir, ptn), File::FNM_DOTMATCH) do |f|
|
43
|
-
files.delete_if { |i| i == f }
|
44
|
-
excluded += 1
|
45
|
-
end
|
45
|
+
).reject do |f|
|
46
|
+
File.directory?(f) || exclude_paths.any? { |ptn| f.match(ptn) }
|
46
47
|
end
|
47
|
-
|
48
|
+
files += Dir.glob(
|
49
|
+
@include.map { |ptn| File.join(@dir, ptn) }
|
50
|
+
).reject { |f| File.directory?(f) }
|
51
|
+
files = files.uniq # remove duplicates
|
48
52
|
files.reject { |f| binary?(f) }.map do |file|
|
49
53
|
path = file[@dir.length + 1, file.length]
|
50
54
|
VerboseSource.new(path, Source.new(file, path))
|
51
55
|
end
|
52
56
|
end
|
53
57
|
|
54
|
-
def exclude(
|
55
|
-
|
58
|
+
def exclude(paths)
|
59
|
+
paths = paths.nil? ? [] : paths
|
60
|
+
paths = paths.is_a?(Array) ? paths : [paths]
|
61
|
+
@exclude.push(*paths)
|
62
|
+
paths&.each do |path|
|
63
|
+
PDD.log.info "#{Rainbow('Excluding').orange} #{path}"
|
64
|
+
end
|
65
|
+
self
|
66
|
+
end
|
67
|
+
|
68
|
+
def include(paths)
|
69
|
+
paths = paths.nil? ? [] : paths
|
70
|
+
paths = paths.is_a?(Array) ? paths : [paths]
|
71
|
+
@include.push(*paths)
|
72
|
+
paths&.each do |path|
|
73
|
+
PDD.log.info "#{Rainbow('Including').blue} #{path}"
|
74
|
+
end
|
75
|
+
self
|
56
76
|
end
|
57
77
|
|
58
78
|
private
|
@@ -63,6 +83,7 @@ module PDD
|
|
63
83
|
# `test_ignores_binary_files` in `test_sources.rb`.
|
64
84
|
def binary?(file)
|
65
85
|
return false if Gem.win_platform?
|
86
|
+
|
66
87
|
`grep -qI '.' #{Shellwords.escape(file)}`
|
67
88
|
if $CHILD_STATUS.success?
|
68
89
|
false
|
data/lib/pdd/version.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2021 Yegor Bugayenko
|
2
2
|
#
|
3
3
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
4
|
# of this software and associated documentation files (the 'Software'), to deal
|
@@ -20,8 +20,8 @@
|
|
20
20
|
|
21
21
|
# PDD main module.
|
22
22
|
# Author:: Yegor Bugayenko (yegor256@gmail.com)
|
23
|
-
# Copyright:: Copyright (c) 2014-
|
23
|
+
# Copyright:: Copyright (c) 2014-2021 Yegor Bugayenko
|
24
24
|
# License:: MIT
|
25
25
|
module PDD
|
26
|
-
VERSION = '0.20.
|
26
|
+
VERSION = '0.20.8'.freeze
|
27
27
|
end
|