pdd 0.20.6 → 0.21.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 +4 -4
- data/.github/workflows/codecov.yml +20 -0
- data/.github/workflows/rake.yml +24 -0
- data/.gitignore +5 -5
- data/.overcommit.yml +96 -0
- data/.pdd +3 -0
- data/.rubocop.yml +3 -4
- data/.rultor.yml +8 -15
- data/.simplecov +6 -6
- data/Gemfile +1 -1
- data/LICENSE.txt +1 -1
- data/README.md +67 -37
- data/Rakefile +7 -1
- data/assets/puzzles.xsd +1 -1
- data/assets/puzzles.xsl +1 -1
- data/bin/pdd +31 -18
- data/features/catches_broken_puzzles.feature +3 -36
- data/features/cli.feature +1 -1
- data/features/html_output.feature +1 -1
- data/features/parsing.feature +31 -1
- data/features/rake.feature +14 -12
- data/features/step_definitions/steps.rb +7 -10
- data/features/support/env.rb +1 -1
- data/features/uses_config.feature +1 -1
- data/lib/pdd/puzzle.rb +1 -1
- data/lib/pdd/rake_task.rb +11 -12
- 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 +86 -75
- data/lib/pdd/sources.rb +42 -21
- data/lib/pdd/version.rb +3 -3
- data/lib/pdd.rb +20 -13
- data/pdd.gemspec +8 -5
- 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 +7 -4
- data/test/test_roles.rb +2 -2
- data/test/test_source.rb +168 -51
- data/test/test_source_todo.rb +38 -29
- data/test/test_sources.rb +18 -3
- data/test/test_text.rb +2 -2
- data/utils/glob.rb +65 -0
- metadata +37 -7
- data/.travis.yml +0 -13
- data/appveyor.yml +0 -21
@@ -11,45 +11,12 @@ Feature: Catches Broken Puzzles
|
|
11
11
|
* Some other documentation
|
12
12
|
* text that is not relevant to
|
13
13
|
* the puzzle below.
|
14
|
-
* @todo
|
15
|
-
* because it doesn't
|
16
|
-
* the second and the third lines
|
14
|
+
* @todo This puzzle has an incorrect format
|
15
|
+
* because it doesn't have a ticket number
|
17
16
|
*/
|
18
17
|
public void main(String[] args) {
|
19
18
|
// later
|
20
19
|
}
|
21
20
|
}
|
22
21
|
"""
|
23
|
-
When I run pdd it fails with "
|
24
|
-
When I run pdd it fails with "puzzle at line #6"
|
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"
|
40
|
-
|
41
|
-
Scenario: Throwing exception on yet another broken puzzle
|
42
|
-
Given I have a "Sample.java" file with content:
|
43
|
-
"""
|
44
|
-
public class Main {
|
45
|
-
//
|
46
|
-
// @todo #13 This puzzle has an incorrect format
|
47
|
-
// because there is no space character in the
|
48
|
-
// second and third lines
|
49
|
-
//
|
50
|
-
public void main(String[] args) {
|
51
|
-
// later
|
52
|
-
}
|
53
|
-
}
|
54
|
-
"""
|
55
|
-
When I run pdd it fails with "Space expected"
|
22
|
+
When I run pdd it fails with "Sample.java:6"
|
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
|
|
data/features/parsing.feature
CHANGED
@@ -25,6 +25,32 @@ Feature: Parsing
|
|
25
25
|
And XML matches "//puzzle[role='DEV']"
|
26
26
|
And XML matches "//puzzle[estimate='0']"
|
27
27
|
|
28
|
+
Scenario: Simple puzzle within comment block
|
29
|
+
Given I have a "test/a/b/Sample.java" file with content:
|
30
|
+
"""
|
31
|
+
public class Main {
|
32
|
+
/**
|
33
|
+
* Some other documentation
|
34
|
+
* text that is not relevant to
|
35
|
+
* the puzzle below.
|
36
|
+
* @todo #13 This puzzle has a correct format
|
37
|
+
* It doesn't start with a space on
|
38
|
+
* the second and the third lines
|
39
|
+
*/
|
40
|
+
public void main(String[] args) {
|
41
|
+
// later
|
42
|
+
}
|
43
|
+
}
|
44
|
+
"""
|
45
|
+
When I run pdd
|
46
|
+
Then XML matches "/puzzles[count(puzzle)=1]"
|
47
|
+
And XML matches "//puzzle[file='test/a/b/Sample.java']"
|
48
|
+
And XML matches "//puzzle[ticket='13']"
|
49
|
+
And XML matches "//puzzle[lines='6-8']"
|
50
|
+
And XML matches "//puzzle[starts-with(body,'This')]"
|
51
|
+
And XML matches "//puzzle[role='DEV']"
|
52
|
+
And XML matches "//puzzle[estimate='0']"
|
53
|
+
|
28
54
|
Scenario: Multiple puzzles in one file
|
29
55
|
Given I have a "test/a/b/c/Sample.java" file with content:
|
30
56
|
"""
|
@@ -34,6 +60,9 @@ Feature: Parsing
|
|
34
60
|
* @todo #ABC-67:15min And this one ever later
|
35
61
|
* @todo #F-78-3:2h/DEV This is for a developer
|
36
62
|
* who will join us later
|
63
|
+
* @todo #44 This puzzle has a correct format
|
64
|
+
* even though it doesn't start with a space on
|
65
|
+
* the second and the third lines
|
37
66
|
*/
|
38
67
|
public void main(String[] args) {
|
39
68
|
// later
|
@@ -41,10 +70,11 @@ Feature: Parsing
|
|
41
70
|
}
|
42
71
|
"""
|
43
72
|
When I run pdd
|
44
|
-
Then XML matches "/puzzles[count(puzzle)=
|
73
|
+
Then XML matches "/puzzles[count(puzzle)=4]"
|
45
74
|
And XML matches "//puzzle[ticket='13' and lines='3-3']"
|
46
75
|
And XML matches "//puzzle[ticket='13' and body='This one later']"
|
47
76
|
And XML matches "//puzzle[ticket='ABC-67' and lines='4-4']"
|
48
77
|
And XML matches "//puzzle[ticket='F-78-3' and lines='5-6']"
|
49
78
|
And XML matches "//puzzle[ticket='ABC-67' and estimate='15']"
|
50
79
|
And XML matches "//puzzle[ticket='F-78-3' and estimate='120']"
|
80
|
+
And XML matches "//puzzle[ticket='44' and lines='7-9']"
|
data/features/rake.feature
CHANGED
@@ -1,19 +1,21 @@
|
|
1
1
|
Feature: Rake Task
|
2
|
-
|
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
|
3
5
|
Given It is Unix
|
4
|
-
And
|
6
|
+
And I have a "Rakefile" file with content:
|
5
7
|
"""
|
6
8
|
require 'pdd/rake_task'
|
7
|
-
PDD::RakeTask.new
|
9
|
+
PDD::RakeTask.new(:pdd) do |task|
|
10
|
+
task.includes = ['a.txt']
|
11
|
+
end
|
8
12
|
"""
|
9
|
-
|
10
|
-
"""bash
|
11
|
-
rake pdd
|
13
|
+
And I have a "a.txt" file with content:
|
12
14
|
"""
|
13
|
-
|
14
|
-
And the stderr should contain:
|
15
|
+
\x40todo #55 hello!
|
15
16
|
"""
|
16
|
-
|
17
|
-
""
|
18
|
-
|
19
|
-
|
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-2022 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
|
@@ -79,9 +79,7 @@ When(%r{^I run bin/pdd with "([^"]*)"$}) do |arg|
|
|
79
79
|
end
|
80
80
|
|
81
81
|
Then(/^Stdout contains "([^"]*)"$/) do |txt|
|
82
|
-
unless @stdout.include?(txt)
|
83
|
-
raise "STDOUT doesn't contain '#{txt}':\n#{@stdout}"
|
84
|
-
end
|
82
|
+
raise "STDOUT doesn't contain '#{txt}':\n#{@stdout}" unless @stdout.include?(txt)
|
85
83
|
end
|
86
84
|
|
87
85
|
Then(/^Stdout is empty$/) do
|
@@ -90,11 +88,10 @@ end
|
|
90
88
|
|
91
89
|
Then(/^XML file "([^"]+)" matches "([^"]+)"$/) do |file, xpath|
|
92
90
|
raise "File #{file} doesn't exit" unless File.exist?(file)
|
91
|
+
|
93
92
|
xml = Nokogiri::XML.parse(File.read(file))
|
94
93
|
xml.remove_namespaces!
|
95
|
-
if xml.xpath(xpath).empty?
|
96
|
-
raise "XML file #{file} doesn't match \"#{xpath}\":\n#{xml}"
|
97
|
-
end
|
94
|
+
raise "XML file #{file} doesn't match \"#{xpath}\":\n#{xml}" if xml.xpath(xpath).empty?
|
98
95
|
end
|
99
96
|
|
100
97
|
Then(/^Exit code is zero$/) do
|
data/features/support/env.rb
CHANGED
data/lib/pdd/puzzle.rb
CHANGED
data/lib/pdd/rake_task.rb
CHANGED
@@ -1,30 +1,29 @@
|
|
1
1
|
require 'rake'
|
2
2
|
require 'rake/tasklib'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'slop'
|
7
|
+
require 'pdd'
|
3
8
|
|
4
9
|
# PDD Rake task
|
5
10
|
module PDD
|
6
11
|
# Rake task
|
7
12
|
class RakeTask < Rake::TaskLib
|
8
|
-
attr_accessor :name
|
13
|
+
attr_accessor :name, :fail_on_error, :includes, :license, :quiet
|
14
|
+
|
9
15
|
def initialize(*args, &task_block)
|
10
|
-
# @todo #125:30m Needs to have more parameters to run this task.
|
11
|
-
# For now, we just have a single parameter - the name.
|
12
|
-
# Needs more parameters like the xcop rake task has.
|
13
16
|
@name = args.shift || :pdd
|
17
|
+
@includes = []
|
18
|
+
@excludes = []
|
19
|
+
@license = nil
|
20
|
+
@quiet = false
|
14
21
|
desc 'Run PDD' unless ::Rake.application.last_description
|
15
22
|
task(name, *args) do |_, task_args|
|
16
23
|
RakeFileUtils.send(:verbose, true) do
|
17
24
|
yield(*[self, task_args].slice(0, task_block.arity)) if block_given?
|
18
|
-
run
|
19
25
|
end
|
20
26
|
end
|
21
27
|
end
|
22
|
-
|
23
|
-
def run
|
24
|
-
# @todo #125:30m need to implement this method.
|
25
|
-
# For now, it's just a task,
|
26
|
-
# that accepts one parameter and throws a system error.
|
27
|
-
abort('NOT IMPLEMENTED')
|
28
|
-
end
|
29
28
|
end
|
30
29
|
end
|
data/lib/pdd/rule/duplicates.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright (c) 2014-
|
1
|
+
# Copyright (c) 2014-2022 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-2022 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-2022 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-2022 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,21 +37,47 @@ module PDD
|
|
36
37
|
@path = path
|
37
38
|
end
|
38
39
|
|
40
|
+
def match_markers(l)
|
41
|
+
if l.downcase.include? 'todo'
|
42
|
+
/[^\s]\x40todo/.match(l) do |_|
|
43
|
+
raise Error, get_no_leading_space_error("\x40todo")
|
44
|
+
end
|
45
|
+
/\x40todo(?!\s+#)/.match(l) do |_|
|
46
|
+
raise Error, get_no_puzzle_marker_error("\x40todo")
|
47
|
+
end
|
48
|
+
/\x40todo\s+#\s/.match(l) do |_|
|
49
|
+
raise Error, get_space_after_hash_error("\x40todo")
|
50
|
+
end
|
51
|
+
/[^\s]TODO:?/.match(l) do |_|
|
52
|
+
raise Error, get_no_leading_space_error('TODO')
|
53
|
+
end
|
54
|
+
/TODO(?!:?\s+#)/.match(l) do |_|
|
55
|
+
raise Error, get_no_puzzle_marker_error('TODO')
|
56
|
+
end
|
57
|
+
/TODO:?\s+#\s/.match(l) do |_|
|
58
|
+
raise Error, get_space_after_hash_error('TODO')
|
59
|
+
end
|
60
|
+
a = [%r{(.*(?:^|\s))(?:\x40todo|TODO:|TODO)\s+#([\w\-.:/]+)\s+(.+)}.match(l)]
|
61
|
+
a.compact
|
62
|
+
else
|
63
|
+
[]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
39
67
|
# Fetch all puzzles.
|
40
68
|
def puzzles
|
41
|
-
PDD.log.info "Reading #{@path}..."
|
69
|
+
PDD.log.info "Reading #{@path} ..."
|
42
70
|
puzzles = []
|
43
71
|
lines = File.readlines(@file, encoding: 'UTF-8')
|
44
72
|
lines.each_with_index do |line, idx|
|
45
73
|
begin
|
46
|
-
|
47
|
-
|
48
|
-
%r{(.*(?:^|\s))#{pfx}\s+#([\w\-\.:/]+)\s+(.+)}.match(line) do |m|
|
49
|
-
puzzles << puzzle(lines.drop(idx + 1), m, idx)
|
50
|
-
end
|
74
|
+
match_markers(line).each do |m|
|
75
|
+
puzzles << puzzle(lines.drop(idx + 1), m, idx)
|
51
76
|
end
|
52
77
|
rescue Error, ArgumentError => ex
|
53
|
-
|
78
|
+
message = "#{@path}:#{idx + 1} #{ex.message}"
|
79
|
+
raise Error, message unless PDD.opts && PDD.opts['skip-errors']
|
80
|
+
PDD.log.warn message
|
54
81
|
end
|
55
82
|
end
|
56
83
|
puzzles
|
@@ -60,46 +87,25 @@ module PDD
|
|
60
87
|
|
61
88
|
def get_no_leading_space_error(todo)
|
62
89
|
"#{todo} must have a leading space to become \
|
63
|
-
a puzzle, as this page explains: https://github.com/
|
90
|
+
a puzzle, as this page explains: https://github.com/cqfn/pdd#how-to-format"
|
64
91
|
end
|
65
92
|
|
66
93
|
def get_no_puzzle_marker_error(todo)
|
67
94
|
"#{todo} found, but puzzle can't be parsed, \
|
68
95
|
most probably because #{todo} is not followed by a puzzle marker, \
|
69
|
-
as this page explains: https://github.com/
|
96
|
+
as this page explains: https://github.com/cqfn/pdd#how-to-format"
|
70
97
|
end
|
71
98
|
|
72
99
|
def get_space_after_hash_error(todo)
|
73
100
|
"#{todo} found, but there is an unexpected space \
|
74
101
|
after the hash sign, it should not be there, \
|
75
|
-
see https://github.com/
|
76
|
-
end
|
77
|
-
|
78
|
-
def check_rules(line)
|
79
|
-
/[^\s]\x40todo/.match(line) do |_|
|
80
|
-
raise Error, get_no_leading_space_error("\x40todo")
|
81
|
-
end
|
82
|
-
/\x40todo(?!\s+#)/.match(line) do |_|
|
83
|
-
raise Error, get_no_puzzle_marker_error("\x40todo")
|
84
|
-
end
|
85
|
-
/\x40todo\s+#\s/.match(line) do |_|
|
86
|
-
raise Error, get_space_after_hash_error("\x40todo")
|
87
|
-
end
|
88
|
-
/[^\s]TODO:?/.match(line) do |_|
|
89
|
-
raise Error, get_no_leading_space_error('TODO')
|
90
|
-
end
|
91
|
-
/TODO(?!:?\s+#)/.match(line) do |_|
|
92
|
-
raise Error, get_no_puzzle_marker_error('TODO')
|
93
|
-
end
|
94
|
-
/TODO:?\s+#\s/.match(line) do |_|
|
95
|
-
raise Error, get_space_after_hash_error('TODO')
|
96
|
-
end
|
102
|
+
see https://github.com/cqfn/pdd#how-to-format"
|
97
103
|
end
|
98
104
|
|
99
105
|
# Fetch puzzle
|
100
106
|
def puzzle(lines, match, idx)
|
101
|
-
tail = tail(lines, match[1]
|
102
|
-
body =
|
107
|
+
tail = tail(lines, match[1])
|
108
|
+
body = "#{match[3]} #{tail.join(' ')}".gsub(/\s+/, ' ').strip
|
103
109
|
body = body.chomp('*/-->').strip
|
104
110
|
marker = marker(match[2])
|
105
111
|
Puzzle.new(
|
@@ -114,11 +120,11 @@ see https://github.com/yegor256/pdd#how-to-format"
|
|
114
120
|
|
115
121
|
# Parse a marker.
|
116
122
|
def marker(text)
|
117
|
-
re = %r{([\w
|
123
|
+
re = %r{([\w\-.]+)(?::(\d+)(?:(m|h)[a-z]*)?)?(?:/([A-Z]+))?}
|
118
124
|
match = re.match(text)
|
119
125
|
if match.nil?
|
120
126
|
raise "Invalid puzzle marker \"#{text}\", most probably formatted \
|
121
|
-
against the rules explained here: https://github.com/
|
127
|
+
against the rules explained here: https://github.com/cqfn/pdd#how-to-format"
|
122
128
|
end
|
123
129
|
{
|
124
130
|
ticket: match[1],
|
@@ -135,24 +141,17 @@ against the rules explained here: https://github.com/yegor256/pdd#how-to-format"
|
|
135
141
|
end
|
136
142
|
|
137
143
|
# Fetch puzzle tail (all lines after the first one)
|
138
|
-
def tail(lines, prefix
|
144
|
+
def tail(lines, prefix)
|
145
|
+
prefix = prefix.rstrip
|
139
146
|
lines
|
140
|
-
.take_while { |t| t.start_with?(prefix) }
|
141
|
-
.
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
raise Error, "Space expected at #{start + i + 2}:#{prefix.length}; \
|
146
|
-
make sure all lines in the puzzle body have a single leading space."
|
147
|
-
end
|
148
|
-
.each_with_index do |t, i|
|
149
|
-
next if t !~ /^\s{2,}/
|
150
|
-
raise Error, "Too many leading spaces \
|
151
|
-
at #{start + i + 2}:#{prefix.length}; \
|
152
|
-
make sure all lines that include the puzzle body start \
|
153
|
-
at position ##{prefix.length + 1}."
|
147
|
+
.take_while { |t| match_markers(t).none? && t.start_with?(prefix) }
|
148
|
+
.take_while do |t|
|
149
|
+
# account for carriage return in line endings
|
150
|
+
t_len = t.length - 1
|
151
|
+
t_len <= prefix.length || t_len > prefix.length + 2
|
154
152
|
end
|
155
|
-
.map { |t| t[
|
153
|
+
.map { |t| t[prefix.length, t.length] }
|
154
|
+
.map { |t| t.start_with?(' ') ? t[1, t.length] : t }
|
156
155
|
end
|
157
156
|
|
158
157
|
# @todo #75:30min Let's make it possible to fetch Subversion data
|
@@ -167,28 +166,29 @@ at position ##{prefix.length + 1}."
|
|
167
166
|
if `#{git} rev-parse --is-inside-work-tree 2>/dev/null`.strip == 'true'
|
168
167
|
cmd = "#{git} blame -L #{pos},#{pos} --porcelain #{name}"
|
169
168
|
add_github_login(Hash[
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
169
|
+
`#{cmd}`.split("\n").map do |line|
|
170
|
+
case line
|
171
|
+
when /^author /
|
172
|
+
[:author, line.sub(/^author /, '')]
|
173
|
+
when /^author-mail [^@]+@[^.]+\..+/
|
174
|
+
[:email, line.sub(/^author-mail <(.+)>$/, '\1')]
|
175
|
+
when /^author-time /
|
176
|
+
[
|
177
|
+
:time,
|
178
|
+
Time.at(
|
179
|
+
line.sub(/^author-time ([0-9]+)$/, '\1').to_i
|
180
|
+
).utc.iso8601
|
181
|
+
]
|
182
|
+
end
|
183
|
+
end.compact
|
184
|
+
])
|
185
185
|
else
|
186
186
|
{}
|
187
187
|
end
|
188
188
|
end
|
189
189
|
|
190
190
|
def add_github_login(info)
|
191
|
-
login = find_github_login(info
|
191
|
+
login = find_github_login(info)
|
192
192
|
info[:author] = "@#{login}" unless login.empty?
|
193
193
|
info
|
194
194
|
end
|
@@ -203,15 +203,26 @@ at position ##{prefix.length + 1}."
|
|
203
203
|
JSON.parse res.body
|
204
204
|
end
|
205
205
|
|
206
|
-
def find_github_user(
|
207
|
-
|
208
|
-
|
206
|
+
def find_github_user(info)
|
207
|
+
email, author = info.values_at(:email, :author)
|
208
|
+
# if email is not defined, changes have not been committed
|
209
|
+
return if email.nil?
|
210
|
+
|
211
|
+
base_uri = 'https://api.github.com/search/users?per_page=1'
|
212
|
+
query = base_uri + "&q=#{email}+in:email"
|
209
213
|
json = get_json query
|
214
|
+
# find user by name instead since users can make github email private
|
215
|
+
unless json['total_count'].positive?
|
216
|
+
return if author.nil?
|
217
|
+
|
218
|
+
query = base_uri + "&q=#{author}+in:fullname"
|
219
|
+
json = get_json query
|
220
|
+
end
|
210
221
|
json['items'].first
|
211
222
|
end
|
212
223
|
|
213
|
-
def find_github_login(
|
214
|
-
user = find_github_user
|
224
|
+
def find_github_login(info)
|
225
|
+
user = find_github_user info
|
215
226
|
user['login']
|
216
227
|
rescue StandardError
|
217
228
|
''
|
@@ -231,8 +242,8 @@ at position ##{prefix.length + 1}."
|
|
231
242
|
# Fetch all puzzles.
|
232
243
|
def puzzles
|
233
244
|
@source.puzzles
|
234
|
-
rescue Error =>
|
235
|
-
raise Error, "#{@file}; #{
|
245
|
+
rescue Error => e
|
246
|
+
raise Error, "#{@file}; #{e.message}"
|
236
247
|
end
|
237
248
|
end
|
238
249
|
end
|