document_generator 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +24 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +7 -0
- data/Gemfile.lock +83 -0
- data/LICENSE.txt +22 -0
- data/README.md +46 -0
- data/Rakefile +6 -0
- data/_layouts/default.html +100 -0
- data/assets/_layouts/default.html +100 -0
- data/bin/doc_generate +5 -0
- data/coderay.css +129 -0
- data/document_generator.gemspec +31 -0
- data/index.md +7 -0
- data/lib/document_generator.rb +17 -0
- data/lib/document_generator/cli.rb +27 -0
- data/lib/document_generator/commit.rb +85 -0
- data/lib/document_generator/diff_file.rb +157 -0
- data/lib/document_generator/output.rb +27 -0
- data/lib/document_generator/repository.rb +89 -0
- data/lib/document_generator/version.rb +3 -0
- data/spec/lib/document_generator/cli_spec.rb +27 -0
- data/spec/lib/document_generator/commit_spec.rb +243 -0
- data/spec/lib/document_generator/diff_file_spec.rb +438 -0
- data/spec/lib/document_generator/repository_spec.rb +75 -0
- data/spec/spec_helper.rb +21 -0
- metadata +179 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'document_generator/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'document_generator'
|
8
|
+
spec.version = DocumentGenerator::VERSION
|
9
|
+
spec.authors = ['wiscoDude', 'm5rk', 'stevenhallen']
|
10
|
+
spec.email = ['philip@stevenhallen.com', 'mark@stevenhallen.com']
|
11
|
+
spec.description = <<-DOCUMENT_GENERATOR
|
12
|
+
Generate documentation from a git repository.
|
13
|
+
DOCUMENT_GENERATOR
|
14
|
+
spec.summary = 'Generate documentation from a git repository.'
|
15
|
+
spec.homepage = 'http://github.com/stevenhallen/document_generator'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
spec.files = `git ls-files`.split($/)
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
|
+
spec.require_paths = ['lib']
|
22
|
+
|
23
|
+
spec.add_runtime_dependency 'addressable', '~> 2.3'
|
24
|
+
spec.add_runtime_dependency 'git', '~> 1.2'
|
25
|
+
|
26
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
27
|
+
spec.add_development_dependency 'rake'
|
28
|
+
spec.add_development_dependency 'rspec', '~> 2.14'
|
29
|
+
spec.add_development_dependency 'rspec-fire', '~> 1.3'
|
30
|
+
spec.add_development_dependency 'simplecov', '~> 0.7'
|
31
|
+
end
|
data/index.md
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'addressable/uri'
|
2
|
+
require 'git'
|
3
|
+
require 'optparse'
|
4
|
+
require 'ostruct'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
require 'document_generator/version'
|
9
|
+
|
10
|
+
require 'document_generator/cli'
|
11
|
+
require 'document_generator/commit'
|
12
|
+
require 'document_generator/diff_file'
|
13
|
+
require 'document_generator/output'
|
14
|
+
require 'document_generator/repository'
|
15
|
+
|
16
|
+
module DocumentGenerator
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module DocumentGenerator
|
2
|
+
class CLI
|
3
|
+
def self.start(args)
|
4
|
+
options = parse(args)
|
5
|
+
|
6
|
+
Repository.new(options.url).generate
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.parse(args)
|
10
|
+
options = OpenStruct.new
|
11
|
+
|
12
|
+
parser = OptionParser.new do |opts|
|
13
|
+
opts.on('-u', '--url URL',
|
14
|
+
'URL for the repository') do |url|
|
15
|
+
options.url = url
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
parser.parse!(args)
|
20
|
+
|
21
|
+
# TODO: Do something better than this.
|
22
|
+
raise OptionParser::MissingArgument unless options.url
|
23
|
+
|
24
|
+
options
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module DocumentGenerator
|
2
|
+
class Commit
|
3
|
+
attr_accessor :base_url, :git_commit
|
4
|
+
|
5
|
+
def initialize(base_url, git_commit)
|
6
|
+
@base_url = base_url
|
7
|
+
@git_commit = git_commit
|
8
|
+
end
|
9
|
+
|
10
|
+
def diff_files
|
11
|
+
return [] unless git_commit.parent
|
12
|
+
|
13
|
+
git_commit.parent.diff(git_commit).map do |git_diff_file|
|
14
|
+
DiffFile.new(git_diff_file)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def relative_filename
|
19
|
+
filename
|
20
|
+
end
|
21
|
+
|
22
|
+
def create
|
23
|
+
File.open(relative_filename, 'w') do |writer|
|
24
|
+
writer.write(header)
|
25
|
+
writer.write(details_of_commit_message) if details_of_commit_message
|
26
|
+
|
27
|
+
diff_files.each do |diff_file|
|
28
|
+
writer.write(diff_file.content)
|
29
|
+
end
|
30
|
+
|
31
|
+
writer.write(additional)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def header
|
36
|
+
<<-HEADER
|
37
|
+
---
|
38
|
+
layout: default
|
39
|
+
title: #{first_line_of_commit_message}
|
40
|
+
---
|
41
|
+
|
42
|
+
<h1 id="main">#{first_line_of_commit_message}</h1>
|
43
|
+
HEADER
|
44
|
+
end
|
45
|
+
|
46
|
+
def additional
|
47
|
+
<<-ADDITIONAL
|
48
|
+
|
49
|
+
### Additional Resources
|
50
|
+
|
51
|
+
* [Changes in this step in `diff` format](#{URI.join(base_url, 'commit/', git_commit.sha)})
|
52
|
+
|
53
|
+
ADDITIONAL
|
54
|
+
end
|
55
|
+
|
56
|
+
def commit_message_lines
|
57
|
+
git_commit.message.split("\n")
|
58
|
+
end
|
59
|
+
|
60
|
+
def first_line_of_commit_message
|
61
|
+
commit_message_lines.first
|
62
|
+
end
|
63
|
+
|
64
|
+
def details_of_commit_message
|
65
|
+
commit_message_lines[1..-1].join("\n") if commit_message_lines.length > 1
|
66
|
+
end
|
67
|
+
|
68
|
+
def basename_prefix
|
69
|
+
message = first_line_of_commit_message
|
70
|
+
message = message.split.join('-')
|
71
|
+
message.gsub!(%r{[^\w-]}, '')
|
72
|
+
message.downcase!
|
73
|
+
message.tr!('_', '-')
|
74
|
+
message
|
75
|
+
end
|
76
|
+
|
77
|
+
def filename
|
78
|
+
"#{basename_prefix}.md"
|
79
|
+
end
|
80
|
+
|
81
|
+
def link
|
82
|
+
"<li><a href='#{basename_prefix}.html'>#{first_line_of_commit_message}</a></li>"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module DocumentGenerator
|
4
|
+
class DiffFile
|
5
|
+
attr_accessor :git_diff_file
|
6
|
+
|
7
|
+
def type
|
8
|
+
git_diff_file.type
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(git_diff_file)
|
12
|
+
@git_diff_file = git_diff_file
|
13
|
+
end
|
14
|
+
|
15
|
+
def git_diff_file_lines
|
16
|
+
git_diff_file.patch.split("\n")
|
17
|
+
end
|
18
|
+
|
19
|
+
def patch_heading
|
20
|
+
"#{action_type} `#{git_diff_file.path}`"
|
21
|
+
end
|
22
|
+
|
23
|
+
def content
|
24
|
+
if type == 'deleted'
|
25
|
+
return patch_heading + "\n\n"
|
26
|
+
end
|
27
|
+
|
28
|
+
temp = []
|
29
|
+
temp << patch_heading
|
30
|
+
|
31
|
+
if markdown_outputs.any?
|
32
|
+
markdown_outputs.each do |output|
|
33
|
+
temp << "\n\n"
|
34
|
+
temp << output.description
|
35
|
+
temp << "\n<pre><code>"
|
36
|
+
temp << output.escaped_content
|
37
|
+
temp << "</code></pre>\n"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
if git_diff_file.type == "modified"
|
42
|
+
temp << "\n\n"
|
43
|
+
temp << "Becomes"
|
44
|
+
temp << "\n<pre><code>"
|
45
|
+
temp << ending_code
|
46
|
+
temp << "\n</code></pre>\n"
|
47
|
+
end
|
48
|
+
|
49
|
+
temp << "\n\n"
|
50
|
+
|
51
|
+
temp.join
|
52
|
+
end
|
53
|
+
|
54
|
+
def ending_code
|
55
|
+
clean_lines = []
|
56
|
+
git_diff_file_lines[code_line_start..-1].each_with_index do |line, index|
|
57
|
+
|
58
|
+
if (line[0]) == "-" || ignore_line?(line)
|
59
|
+
next
|
60
|
+
end
|
61
|
+
|
62
|
+
if (line[0]) == "+"
|
63
|
+
line = remove_first_character(line)
|
64
|
+
end
|
65
|
+
clean_lines << line
|
66
|
+
end
|
67
|
+
Output.no_really_escape(CGI.escapeHTML(clean_lines.join("\n")))
|
68
|
+
end
|
69
|
+
|
70
|
+
def action_type
|
71
|
+
{ new: 'Create file',
|
72
|
+
modified: 'Update file',
|
73
|
+
deleted: 'Remove file' }.fetch(type.to_sym, type)
|
74
|
+
end
|
75
|
+
|
76
|
+
def markdown_outputs # returns an array of outputs
|
77
|
+
outputs = []
|
78
|
+
last_line = 0
|
79
|
+
git_diff_file_lines.each_with_index do |line, index|
|
80
|
+
next if index < code_line_start
|
81
|
+
next if index <= last_line
|
82
|
+
case line.strip[0]
|
83
|
+
|
84
|
+
when "+"
|
85
|
+
last_line = last_same_line(index)
|
86
|
+
output = Output.new
|
87
|
+
output.description = "Add"
|
88
|
+
output.content = line_block(index, last_line)
|
89
|
+
outputs << output
|
90
|
+
when "-"
|
91
|
+
if line_sign(index + 1) == "+"
|
92
|
+
output = Output.new
|
93
|
+
output.description = "Change"
|
94
|
+
output.content = line_block(index, last_same_line(index))
|
95
|
+
outputs << output
|
96
|
+
last_line = last_same_line(last_same_line(index) + 1)
|
97
|
+
output = Output.new
|
98
|
+
output.description = "To"
|
99
|
+
output.content = line_block(last_same_line(index) + 1, last_line)
|
100
|
+
outputs << output
|
101
|
+
else
|
102
|
+
output = Output.new
|
103
|
+
output.description = "Remove"
|
104
|
+
last_line = last_same_line(index)
|
105
|
+
output.content = line_block(index, last_line)
|
106
|
+
outputs << output
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
outputs
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
def ignore_line?(line)
|
117
|
+
line.strip == 'No newline at end of file'
|
118
|
+
end
|
119
|
+
|
120
|
+
def last_same_line(line_index)
|
121
|
+
starting_sign = line_sign(line_index)
|
122
|
+
|
123
|
+
git_diff_file_lines[line_index..-1].each_with_index do |line, index|
|
124
|
+
if line_sign(index + 1 + line_index) != starting_sign
|
125
|
+
return (index + line_index)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def line_block(beginning, ending)
|
131
|
+
lines = []
|
132
|
+
git_diff_file_lines[beginning..ending].each do |line|
|
133
|
+
if ["+", "-"].include?(line[0..0])
|
134
|
+
line = remove_first_character(line)
|
135
|
+
end
|
136
|
+
if !ignore_line?(line)
|
137
|
+
lines << line
|
138
|
+
end
|
139
|
+
end
|
140
|
+
lines
|
141
|
+
end
|
142
|
+
|
143
|
+
def line_sign(line_number)
|
144
|
+
(git_diff_file_lines[line_number] || '').strip[0]
|
145
|
+
end
|
146
|
+
|
147
|
+
def remove_first_character(line)
|
148
|
+
" " + line[1..-1]
|
149
|
+
end
|
150
|
+
|
151
|
+
def code_line_start
|
152
|
+
git_diff_file_lines.each_with_index do |line, index|
|
153
|
+
return (index + 1) if line[0..1] == "@@"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module DocumentGenerator
|
2
|
+
class Output
|
3
|
+
attr_accessor :description, :content
|
4
|
+
|
5
|
+
# TODO: This is due to a bug in maruku. We should create
|
6
|
+
# an issue there--and possibly a PR to fix?
|
7
|
+
def self.no_really_escape(value)
|
8
|
+
value.split("\n").map do |line|
|
9
|
+
if line.strip.size.zero?
|
10
|
+
" "
|
11
|
+
else
|
12
|
+
line
|
13
|
+
end
|
14
|
+
end.join("\n")
|
15
|
+
end
|
16
|
+
|
17
|
+
def escaped_content
|
18
|
+
temp = []
|
19
|
+
content.each do |line|
|
20
|
+
temp << line
|
21
|
+
temp << "\n"
|
22
|
+
end
|
23
|
+
|
24
|
+
Output.no_really_escape(CGI.escapeHTML(temp.join.rstrip))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module DocumentGenerator
|
2
|
+
class Repository
|
3
|
+
attr_accessor :url
|
4
|
+
|
5
|
+
def self.menu_dirname
|
6
|
+
'_includes'
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.menu_relative_filename
|
10
|
+
File.join(menu_dirname, 'menu.md')
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.default_dirname
|
14
|
+
'_layouts'
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.default_relative_filename
|
18
|
+
File.join(default_dirname, 'default.html')
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(url)
|
22
|
+
@url = url
|
23
|
+
end
|
24
|
+
|
25
|
+
def base_url
|
26
|
+
"https://#{uri.host}#{uri.path}/"
|
27
|
+
end
|
28
|
+
|
29
|
+
def name
|
30
|
+
uri.path.split('/')[-1]
|
31
|
+
end
|
32
|
+
|
33
|
+
def generate
|
34
|
+
prepare
|
35
|
+
|
36
|
+
File.open(Repository.menu_relative_filename, 'w') do |menu_writer|
|
37
|
+
commits do |commit|
|
38
|
+
menu_writer.write(commit.link)
|
39
|
+
commit.create
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def commits
|
45
|
+
Dir.mktmpdir do |path|
|
46
|
+
repo = Git.clone(url, name, path: path)
|
47
|
+
|
48
|
+
# TODO: Allow options to influence branch, number of commits, etc.
|
49
|
+
repo.log(nil).reverse_each.map do |git_commit|
|
50
|
+
yield Commit.new(base_url, git_commit)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
def prepare
|
57
|
+
Dir.mkdir(Repository.menu_dirname) unless Dir.exists?(Repository.menu_dirname)
|
58
|
+
copy_layout
|
59
|
+
end
|
60
|
+
|
61
|
+
def copy_layout
|
62
|
+
return if File.exists?(Repository.default_relative_filename)
|
63
|
+
|
64
|
+
Dir.mkdir(Repository.default_dirname) unless Dir.exists?(Repository.default_dirname)
|
65
|
+
|
66
|
+
src = File.expand_path('../../../assets/_layouts/default.html', __FILE__)
|
67
|
+
dest = Repository.default_relative_filename
|
68
|
+
|
69
|
+
FileUtils.copy_file(src, dest)
|
70
|
+
end
|
71
|
+
|
72
|
+
def normalized_url
|
73
|
+
replacements = [
|
74
|
+
[%r(\Agit@github\.com:), 'git://github.com/'],
|
75
|
+
[%r(\.git\Z), '']
|
76
|
+
]
|
77
|
+
|
78
|
+
replacements.each do |pattern, replacement|
|
79
|
+
url.gsub!(pattern, replacement)
|
80
|
+
end
|
81
|
+
|
82
|
+
url
|
83
|
+
end
|
84
|
+
|
85
|
+
def uri
|
86
|
+
Addressable::URI.parse(normalized_url)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|