puppet-courseware-manager 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.txt +60 -0
- data/LICENSE +202 -0
- data/README.txt +100 -0
- data/bin/courseware +146 -0
- data/lib/courseware.rb +165 -0
- data/lib/courseware/cache.rb +35 -0
- data/lib/courseware/composer.rb +135 -0
- data/lib/courseware/dummy.rb +5 -0
- data/lib/courseware/generator.rb +91 -0
- data/lib/courseware/help.rb +108 -0
- data/lib/courseware/manager.rb +129 -0
- data/lib/courseware/manager/validators.rb +72 -0
- data/lib/courseware/printer.rb +224 -0
- data/lib/courseware/repository.rb +189 -0
- data/lib/courseware/utils.rb +118 -0
- data/lib/courseware/version.rb +4 -0
- metadata +107 -0
@@ -0,0 +1,129 @@
|
|
1
|
+
require 'courseware/printer'
|
2
|
+
require 'courseware/repository'
|
3
|
+
require 'io/console'
|
4
|
+
require 'nokogiri'
|
5
|
+
|
6
|
+
class Courseware::Manager
|
7
|
+
require 'courseware/manager/validators'
|
8
|
+
|
9
|
+
attr_reader :coursename, :prefix, :warnings, :errors
|
10
|
+
|
11
|
+
def initialize(config, repository=nil, generator=nil, printer=nil)
|
12
|
+
@config = config
|
13
|
+
@repository = repository || Courseware::Repository.new(config)
|
14
|
+
@generator = generator || Courseware::Generator.new(config)
|
15
|
+
@warnings = 0
|
16
|
+
@errors = 0
|
17
|
+
|
18
|
+
if File.exists?(@config[:presfile])
|
19
|
+
showoff = JSON.parse(File.read(@config[:presfile]))
|
20
|
+
@coursename = showoff['name']
|
21
|
+
@prefix = showoff['name'].gsub(' ', '_')
|
22
|
+
@sections = showoff['sections']
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def releasenotes
|
27
|
+
courselevel!
|
28
|
+
master!
|
29
|
+
clean!
|
30
|
+
|
31
|
+
@repository.update
|
32
|
+
current = @repository.current(@coursename)
|
33
|
+
version = Courseware.increment(current)
|
34
|
+
tag = "#{@coursename}-#{current}"
|
35
|
+
|
36
|
+
notes = @repository.releasenotes(tag, version)
|
37
|
+
|
38
|
+
# print to screen
|
39
|
+
puts notes
|
40
|
+
|
41
|
+
# and copy if on OS X
|
42
|
+
begin
|
43
|
+
IO.popen('pbcopy', 'w') { |f| f.puts notes }
|
44
|
+
puts
|
45
|
+
puts "{{ Copied to clipboard }}"
|
46
|
+
rescue
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def release(type)
|
51
|
+
courselevel!
|
52
|
+
master!
|
53
|
+
clean!
|
54
|
+
|
55
|
+
@repository.update
|
56
|
+
version = Courseware.increment(@repository.current(@coursename), type)
|
57
|
+
Courseware.bailout?("Building a release for #{@coursename} version #{version}.")
|
58
|
+
|
59
|
+
raise "Release notes not updated for #{version}" unless Courseware.grep(version, 'Release-Notes.md')
|
60
|
+
|
61
|
+
Courseware.dialog('Last Repository Commit', @repository.last_commit)
|
62
|
+
Courseware.bailout?('Abort now if the commit message displayed is not what you expected.')
|
63
|
+
build_pdfs(version)
|
64
|
+
point_of_no_return
|
65
|
+
Courseware.bailout?('Please inspect the generated PDF files and abort if corrections must be made.') do
|
66
|
+
@repository.discard(@config[:stylesheet])
|
67
|
+
end
|
68
|
+
|
69
|
+
@repository.commit(@config[:stylesheet], "Updating for #{@coursename} release #{version}")
|
70
|
+
@repository.tag("#{@prefix}-#{version}", "Releasing #{@coursename} version #{version}")
|
71
|
+
puts "Release shipped. Please upload PDF files to printer and break out the bubbly."
|
72
|
+
end
|
73
|
+
|
74
|
+
def wordcount(subject)
|
75
|
+
$logger.debug "Counting words for #{subject}"
|
76
|
+
opts = print_opts(@repository.current(prefix))
|
77
|
+
puts "Words longer than a single character:"
|
78
|
+
Courseware::Printer.new(@config, opts) do |printer|
|
79
|
+
subject.each do |item|
|
80
|
+
printer.generate_html(item)
|
81
|
+
doc = Nokogiri::HTML(File.read('static/index.html'))
|
82
|
+
count = doc.css('body').text.split.select {|w| w.size > 1 }.count
|
83
|
+
|
84
|
+
puts " * #{item}: #{count}"
|
85
|
+
|
86
|
+
FileUtils.rm_rf('static')
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
def toplevel!
|
93
|
+
raise 'This task must be run from the repository root.' unless @repository.toplevel?
|
94
|
+
end
|
95
|
+
|
96
|
+
def courselevel!
|
97
|
+
raise 'This task must be run from within a course directory' unless @repository.courselevel?
|
98
|
+
end
|
99
|
+
|
100
|
+
def master!
|
101
|
+
raise 'You should release from the master branch' unless @repository.on_branch? 'master'
|
102
|
+
end
|
103
|
+
|
104
|
+
def clean!
|
105
|
+
raise 'Your working directory has local modifications' unless @repository.clean?
|
106
|
+
end
|
107
|
+
|
108
|
+
def point_of_no_return
|
109
|
+
Courseware.dialog('Point of No Return', 'Proceeding past this point will result in permanent repository changes.')
|
110
|
+
end
|
111
|
+
|
112
|
+
def build_pdfs(version)
|
113
|
+
@generator.styles(@coursename, version)
|
114
|
+
Courseware::Printer.new(@config, print_opts(version)) do |printer|
|
115
|
+
printer.print
|
116
|
+
end
|
117
|
+
system("open #{@config[:output]} >/dev/null 2>&1")
|
118
|
+
end
|
119
|
+
|
120
|
+
def print_opts(version)
|
121
|
+
{
|
122
|
+
:course => @coursename,
|
123
|
+
:prefix => @prefix,
|
124
|
+
:version => version,
|
125
|
+
:variant => Courseware.choose_variant,
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
class Courseware::Manager
|
2
|
+
|
3
|
+
def obsolete
|
4
|
+
puts "Obsolete images:"
|
5
|
+
Dir.glob('**/_images/*') do |file|
|
6
|
+
next if File.symlink? file
|
7
|
+
next if File.directory? file
|
8
|
+
next if system("grep #{file} *.css */*.md >/dev/null 2>&1")
|
9
|
+
|
10
|
+
puts " * #{file}"
|
11
|
+
@warnings += 1
|
12
|
+
end
|
13
|
+
|
14
|
+
puts "Obsolete slides:"
|
15
|
+
Dir.glob('**/*.md') do |file|
|
16
|
+
next if File.symlink? file
|
17
|
+
next if File.directory? file
|
18
|
+
next if file =~ /^_.*$|^[^\/]*$/
|
19
|
+
next if @sections.include? file
|
20
|
+
|
21
|
+
puts " * #{file}"
|
22
|
+
@warnings += 1
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def missing
|
27
|
+
sections = @sections.dup
|
28
|
+
|
29
|
+
# This seems backwards, but we do it this way to get a case sensitive match
|
30
|
+
Dir.glob('**/*.md') do |file|
|
31
|
+
sections.delete(file)
|
32
|
+
end
|
33
|
+
return if sections.empty?
|
34
|
+
|
35
|
+
puts "Missing slides:"
|
36
|
+
sections.each do |slide|
|
37
|
+
puts " * #{slide}"
|
38
|
+
@errors += 1
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def lint
|
43
|
+
puts "Checking Markdown style:"
|
44
|
+
style = File.join(@config[:cachedir], 'templates', 'markdown_style.rb')
|
45
|
+
style = File.exists?(style) ? style : 'all'
|
46
|
+
issues = 0
|
47
|
+
|
48
|
+
unless system('mdl', '--version')
|
49
|
+
puts ' * Markdown linter not found: gem install mdl'
|
50
|
+
puts
|
51
|
+
@warnings += 1
|
52
|
+
return
|
53
|
+
end
|
54
|
+
|
55
|
+
Dir.glob('**/*.md') do |file|
|
56
|
+
next if File.symlink? file
|
57
|
+
next if File.directory? file
|
58
|
+
next if file =~ /^_.*$|^[^\/]*$/
|
59
|
+
|
60
|
+
issues += 1 unless system('mdl', '-s', style, file)
|
61
|
+
end
|
62
|
+
|
63
|
+
if issues > 0
|
64
|
+
puts
|
65
|
+
puts 'Rule explanations can be found at:'
|
66
|
+
puts ' * https://github.com/mivok/markdownlint/blob/master/docs/RULES.md'
|
67
|
+
puts
|
68
|
+
@warnings += issues
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
@@ -0,0 +1,224 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
class Courseware::Printer
|
3
|
+
|
4
|
+
def initialize(config, opts)
|
5
|
+
@config = config
|
6
|
+
@course = opts[:course] or raise 'Course is a required option'
|
7
|
+
@prefix = opts[:prefix] or raise 'Prefix is a required option'
|
8
|
+
@version = opts[:version] or raise 'Version is a required option'
|
9
|
+
@varfile = opts[:variant] or raise 'Variant is a required option'
|
10
|
+
|
11
|
+
@variant = File.basename(@varfile, '.json') unless @varfile == 'showoff.json'
|
12
|
+
|
13
|
+
raise unless can_print?
|
14
|
+
|
15
|
+
@pdfopts = "--pdf-title '#{@course}' --pdf-author '#{@config[:pdf][:author]}' --pdf-subject '#{@config[:pdf][:subject]}'"
|
16
|
+
@pdfopts << " --disallow-modify" if @config[:pdf][:protected]
|
17
|
+
|
18
|
+
if @config[:pdf][:watermark]
|
19
|
+
@event_id = Courseware.question('Enter the Event ID:')
|
20
|
+
@password = Courseware.question('Enter desired password:', (@event_id[/-?(\w*)$/, 1] rescue nil))
|
21
|
+
@watermark_style = File.join(@config[:cachedir], 'templates', 'watermark.css')
|
22
|
+
@watermark_pdf = File.join(@config[:cachedir], 'templates', 'watermark.pdf')
|
23
|
+
end
|
24
|
+
|
25
|
+
FileUtils.mkdir(config[:output]) unless File.directory?(config[:output])
|
26
|
+
|
27
|
+
if block_given?
|
28
|
+
yield self
|
29
|
+
FileUtils.rm_rf('static')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def print
|
34
|
+
handouts
|
35
|
+
exercises
|
36
|
+
solutions
|
37
|
+
end
|
38
|
+
|
39
|
+
def handouts
|
40
|
+
$logger.info "Generating handouts pdf for #{@course} #{@version}..."
|
41
|
+
|
42
|
+
generate_pdf(:print)
|
43
|
+
end
|
44
|
+
|
45
|
+
def exercises
|
46
|
+
$logger.info "Generating exercise guide pdf for #{@course} #{@version}..."
|
47
|
+
|
48
|
+
generate_pdf(:exercises)
|
49
|
+
end
|
50
|
+
|
51
|
+
def solutions
|
52
|
+
$logger.info "Generating solutions guide pdf for #{@course} #{@version}..."
|
53
|
+
|
54
|
+
generate_pdf(:solutions)
|
55
|
+
end
|
56
|
+
|
57
|
+
def guide
|
58
|
+
$logger.info "Generating instructor guide pdf for #{@course} #{@version}..."
|
59
|
+
|
60
|
+
generate_pdf(:guide)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Ensure that the printing toolchain is in place.
|
64
|
+
def can_print?
|
65
|
+
case @config[:renderer]
|
66
|
+
when :wkhtmltopdf
|
67
|
+
# Fonts must be installed locally for wkhtmltopdf to find them
|
68
|
+
if RUBY_PLATFORM =~ /darwin/
|
69
|
+
fontpath = File.expand_path('~/Library/Fonts')
|
70
|
+
Dir.glob('_fonts/*').each do |font|
|
71
|
+
destination = "#{fontpath}/#{File.basename(font)}"
|
72
|
+
FileUtils.cp(font, destination) unless File.exists?(destination)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
unless system 'wkhtmltopdf --version >/dev/null 2>&1' and system 'pdftk --version >/dev/null 2>&1'
|
77
|
+
msg = "We use wkhtmltopdf and pdftk to generate courseware PDF files.\n" +
|
78
|
+
"\n" +
|
79
|
+
"You should install them using Homebrew or directly from:\n" +
|
80
|
+
" * http://wkhtmltopdf.org/downloads.htm\n" +
|
81
|
+
" * https://www.pdflabs.com/tools/pdftk-server/#download"
|
82
|
+
|
83
|
+
unless RUBY_PLATFORM =~ /darwin/
|
84
|
+
msg << "\n\n"
|
85
|
+
msg << 'Please install all the fonts in the `_fonts` directory to your system.'
|
86
|
+
end
|
87
|
+
|
88
|
+
Courseware.dialog('Printing toolchain unavailable.', msg)
|
89
|
+
return false
|
90
|
+
end
|
91
|
+
|
92
|
+
when :prince
|
93
|
+
unless system 'prince --version >/dev/null 2>&1'
|
94
|
+
msg = "This course is configured to use PrinceXMLto generate PDF files.\n" +
|
95
|
+
"\n" +
|
96
|
+
"You should install version 9 from:\n" +
|
97
|
+
" * http://www.princexml.com/download/\n" +
|
98
|
+
"\n" +
|
99
|
+
"And the license from:\n" +
|
100
|
+
" * https://confluence.puppet.com/display/EDU/Licenses"
|
101
|
+
|
102
|
+
Courseware.dialog('Printing toolchain unavailable.', msg)
|
103
|
+
return false
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
return true
|
108
|
+
end
|
109
|
+
|
110
|
+
# clean out the static dir and build the source html
|
111
|
+
def generate_html(subject)
|
112
|
+
case subject
|
113
|
+
when :handouts, :print
|
114
|
+
subject = 'print'
|
115
|
+
when :exercises, :solutions
|
116
|
+
subject = "supplemental #{subject}"
|
117
|
+
when :guide
|
118
|
+
subject = 'print guide'
|
119
|
+
else
|
120
|
+
raise "I don't know how to generate HTML of #{subject}."
|
121
|
+
end
|
122
|
+
|
123
|
+
begin
|
124
|
+
# Until showoff static knows about -f, we have to schlup around files
|
125
|
+
if @variant
|
126
|
+
FileUtils.mv 'showoff.json', 'showoff.json.tmp'
|
127
|
+
FileUtils.cp @varfile, 'showoff.json'
|
128
|
+
end
|
129
|
+
|
130
|
+
FileUtils.rm_rf('static')
|
131
|
+
system("showoff static #{subject}")
|
132
|
+
if File.exists? 'cobrand.png'
|
133
|
+
FileUtils.mkdir(File.join('static', 'image'))
|
134
|
+
FileUtils.cp('cobrand.png', File.join('static', 'image', 'cobrand.png'))
|
135
|
+
end
|
136
|
+
ensure
|
137
|
+
FileUtils.mv('showoff.json.tmp', 'showoff.json') if File.exist? 'showoff.json.tmp'
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def generate_pdf(subject)
|
142
|
+
# TODO screen printing
|
143
|
+
# @pdfopts << " #{SCREENHACK}"
|
144
|
+
# @suffix = "-screen"
|
145
|
+
|
146
|
+
# Build the filename for the output PDF. This is ugly.
|
147
|
+
output = File.join(@config[:output], "#{@prefix}-")
|
148
|
+
output << "#{@variant}-" if @variant
|
149
|
+
output << 'w-' if @config[:pdf][:watermark]
|
150
|
+
case subject
|
151
|
+
when :print
|
152
|
+
output << "#{@version}.pdf"
|
153
|
+
when :exercises, :solutions, :guide
|
154
|
+
output << "#{@version}-#{subject}.pdf"
|
155
|
+
else
|
156
|
+
raise "I don't know how to generate a PDF of #{subject}."
|
157
|
+
end
|
158
|
+
|
159
|
+
generate_html(subject)
|
160
|
+
FileUtils.mkdir(@config[:output]) unless File.directory?(@config[:output])
|
161
|
+
|
162
|
+
case @config[:renderer]
|
163
|
+
when :wkhtmltopdf
|
164
|
+
infofile = File.join(@config[:output], 'info.txt')
|
165
|
+
scratchfile = File.join(@config[:output], 'scratch.pdf')
|
166
|
+
|
167
|
+
command = ['wkhtmltopdf', '-s', 'Letter', '--print-media-type', '--quiet']
|
168
|
+
command << ['--footer-left', "#{@course} #{@version}", '--footer-center', '[page]']
|
169
|
+
command << ['--footer-right', "©#{Time.now.year} Puppet", '--header-center', '[section]']
|
170
|
+
command << ['--title', @course, File.join('static', 'index.html'), output]
|
171
|
+
system(*command.flatten)
|
172
|
+
raise 'Error generating PDF files' unless $?.success?
|
173
|
+
|
174
|
+
if `pdftk #{output} dump_data | grep NumberOfPages`.chomp == 'NumberOfPages: 1'
|
175
|
+
puts "#{output} is empty; aborting and cleaning up."
|
176
|
+
FileUtils.rm(output)
|
177
|
+
return
|
178
|
+
end
|
179
|
+
|
180
|
+
# We can't add metadata in the same run. It requires dumping, modifying, and updating
|
181
|
+
if @event_id
|
182
|
+
command = ['pdftk', output, 'dump_data', 'output', infofile]
|
183
|
+
system(*command.flatten)
|
184
|
+
raise 'Error retrieving PDF info' unless $?.success?
|
185
|
+
info = File.read(infofile)
|
186
|
+
File.open(infofile, 'w+') do |file|
|
187
|
+
file.write("InfoBegin\n")
|
188
|
+
file.write("InfoKey: Subject\n")
|
189
|
+
file.write("InfoValue: #{@event_id}\n")
|
190
|
+
file.write(info)
|
191
|
+
end
|
192
|
+
command = ['pdftk', output, 'update_info', infofile, 'output', scratchfile]
|
193
|
+
system(*command.flatten)
|
194
|
+
raise 'Error updating PDF info' unless $?.success?
|
195
|
+
else
|
196
|
+
FileUtils.mv(output, scratchfile)
|
197
|
+
end
|
198
|
+
|
199
|
+
command = ['pdftk', scratchfile, 'output', output]
|
200
|
+
command << ['owner_pw', @config[:pdf][:password], 'allow', 'printing', 'CopyContents']
|
201
|
+
command << ['background', @watermark_pdf] if @config[:pdf][:watermark]
|
202
|
+
command << ['user_pw', @password] if @password
|
203
|
+
system(*command.flatten)
|
204
|
+
raise 'Error watermarking PDF files' unless $?.success?
|
205
|
+
|
206
|
+
FileUtils.rm(infofile) if File.exists? infofile
|
207
|
+
FileUtils.rm(scratchfile) if File.exists? scratchfile
|
208
|
+
|
209
|
+
when :prince
|
210
|
+
command = ['prince', File.join('static', 'index.html')]
|
211
|
+
command << ['--pdf-title', @course, '--pdf-author', @config[:pdf][:author], '--disallow-modify']
|
212
|
+
command << ['--pdf-subject', @event_id] if @event_id
|
213
|
+
command << ['--style', @watermark_style] if @config[:pdf][:watermark]
|
214
|
+
command << ['--encrypt', '--user-password', @password] if @password
|
215
|
+
command << ['--license-file', @config[:pdf][:license]] if (@config[:pdf][:license] and File.exists? @config[:pdf][:license])
|
216
|
+
command << ['-o', output]
|
217
|
+
system(*command.flatten)
|
218
|
+
raise 'Error generating PDF files' unless $?.success?
|
219
|
+
end
|
220
|
+
|
221
|
+
FileUtils.rm_rf('static')
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'rubygems'
|
3
|
+
class Courseware::Repository
|
4
|
+
|
5
|
+
def initialize(config)
|
6
|
+
raise 'This is not a courseware repository' unless Courseware::Repository.repository?
|
7
|
+
@config = config
|
8
|
+
|
9
|
+
configure_courseware
|
10
|
+
end
|
11
|
+
|
12
|
+
def tag(tag, message=nil)
|
13
|
+
if tag
|
14
|
+
system("git tag -a #{tag} -m '#{message}'")
|
15
|
+
else
|
16
|
+
system("git tag #{tag}")
|
17
|
+
end
|
18
|
+
|
19
|
+
system("git push upstream master")
|
20
|
+
system("git push upstream #{tag}")
|
21
|
+
system("git push courseware master")
|
22
|
+
system("git push courseware #{tag}")
|
23
|
+
end
|
24
|
+
|
25
|
+
def update
|
26
|
+
system('git fetch upstream')
|
27
|
+
system('git fetch upstream --tags')
|
28
|
+
end
|
29
|
+
|
30
|
+
def create(branch)
|
31
|
+
system("git checkout -b #{branch}")
|
32
|
+
system("git push upstream #{branch}")
|
33
|
+
end
|
34
|
+
|
35
|
+
def checkout(branch, pull=false)
|
36
|
+
system("git checkout #{branch}")
|
37
|
+
pull(branch) if pull
|
38
|
+
end
|
39
|
+
|
40
|
+
def pull(branch)
|
41
|
+
system('git', 'pull', 'upstream', branch)
|
42
|
+
end
|
43
|
+
|
44
|
+
def merge(branch)
|
45
|
+
system('git checkout master')
|
46
|
+
system("git merge #{branch}")
|
47
|
+
system('git push upstream master')
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete(branch)
|
51
|
+
system("git branch -d #{branch}")
|
52
|
+
system("git push upstream --delete #{branch}")
|
53
|
+
end
|
54
|
+
|
55
|
+
def commit(*args)
|
56
|
+
message = args.pop
|
57
|
+
args.each do |file|
|
58
|
+
system('git', 'add', file)
|
59
|
+
end
|
60
|
+
system('git', 'commit', '-m', message)
|
61
|
+
end
|
62
|
+
|
63
|
+
def discard(*args)
|
64
|
+
args.each do |file|
|
65
|
+
$logger.warn "Discarding changes to #{file}"
|
66
|
+
system('git', 'checkout', '--', file)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def last_commit
|
71
|
+
`git show --name-status --no-color`.chomp.gsub("\t", ' ')
|
72
|
+
end
|
73
|
+
|
74
|
+
def on_branch?(branch='master')
|
75
|
+
`git symbolic-ref -q --short HEAD`.chomp == branch
|
76
|
+
end
|
77
|
+
|
78
|
+
def clean?
|
79
|
+
system('git diff-index --quiet HEAD')
|
80
|
+
end
|
81
|
+
|
82
|
+
# clean working tree and no untracked files
|
83
|
+
def pristine?
|
84
|
+
clean? and `git ls-files --other --directory --exclude-standard`.empty?
|
85
|
+
end
|
86
|
+
|
87
|
+
def branch_exists?(branch)
|
88
|
+
`git branch --list '#{branch}'` != ''
|
89
|
+
end
|
90
|
+
|
91
|
+
def toplevel?
|
92
|
+
Dir.pwd == `git rev-parse --show-toplevel`.chomp
|
93
|
+
end
|
94
|
+
|
95
|
+
def courselevel?
|
96
|
+
File.expand_path("#{Dir.pwd}/..") == `git rev-parse --show-toplevel`.chomp
|
97
|
+
end
|
98
|
+
|
99
|
+
def outstanding_commits(prefix, verbose=false)
|
100
|
+
last = current(prefix)
|
101
|
+
commits = `git log --no-merges --oneline #{prefix}-#{last}..HEAD -- .`.each_line.map {|line| line.chomp }
|
102
|
+
|
103
|
+
verbose ? commits : commits.count
|
104
|
+
end
|
105
|
+
|
106
|
+
def releasenotes(last, version)
|
107
|
+
str = "### #{version}\n"
|
108
|
+
str << "{{{Please summarize the release here}}}\n"
|
109
|
+
str << "\n"
|
110
|
+
str << `git log --no-merges --pretty="format:* (%h) %s [%aN]" #{last}..HEAD -- .`
|
111
|
+
str
|
112
|
+
end
|
113
|
+
|
114
|
+
# This gets a list of all tags matching a prefix.
|
115
|
+
def tags(prefix, count=1)
|
116
|
+
prefix ||= 'v' # even if we pass in nil, we want to default to this
|
117
|
+
tags = `git tag -l '#{prefix}*'`.split("\n").sort_by { |tag| version(tag) }.last(count)
|
118
|
+
tags.empty? ? ['v0.0.0'] : tags
|
119
|
+
end
|
120
|
+
|
121
|
+
def current(prefix)
|
122
|
+
tags(prefix).first.gsub(/^#{prefix}-/, '')
|
123
|
+
end
|
124
|
+
|
125
|
+
private
|
126
|
+
|
127
|
+
def configure_courseware
|
128
|
+
courseware = "#{@config[:github][:public]}/#{@config[:github][:repository]}"
|
129
|
+
upstream = "#{@config[:github][:development]}/#{@config[:github][:repository]}"
|
130
|
+
|
131
|
+
# Check the origin to see which scheme we should use
|
132
|
+
origin = `git config --get remote.origin.url`.chomp
|
133
|
+
if origin =~ /^(git@|https:\/\/)github.com[:\/].*\/#{@config[:github][:repository]}(?:-.*)?(?:.git)?$/
|
134
|
+
case $1
|
135
|
+
when 'git@'
|
136
|
+
ensure_remote('courseware', "git@github.com:#{courseware}.git")
|
137
|
+
ensure_remote('upstream', "git@github.com:#{upstream}.git")
|
138
|
+
when 'https://'
|
139
|
+
ensure_remote('courseware', "https://github.com/#{courseware}.git")
|
140
|
+
ensure_remote('upstream', "https://github.com/#{upstream}.git")
|
141
|
+
end
|
142
|
+
elsif origin.empty?
|
143
|
+
$logger.warn 'Your origin remote is not set properly.'
|
144
|
+
$logger.warn 'Generating PDF files and other local operations will work properly, but many repository actions will fail.'
|
145
|
+
else
|
146
|
+
raise "Your origin (#{origin}) does not appear to be configured correctly."
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def ensure_remote(remote, url)
|
151
|
+
# If we *have* the remote, but it's not correct, then let's repair it.
|
152
|
+
if `git config --get remote.#{remote}.url`.chomp != url and $?.success?
|
153
|
+
if Courseware.confirm("Your '#{remote}' remote should be #{url}. May I correct this?")
|
154
|
+
raise "Error correcting remote." unless system("git remote remove #{remote}")
|
155
|
+
else
|
156
|
+
raise "Please configure your '#{remote}' remote before proceeding."
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# add the remote, either for the first time or because we removed it
|
161
|
+
unless system("git config --get remote.#{remote}.url > /dev/null")
|
162
|
+
# Add the remote if it doesn't already exist
|
163
|
+
unless system("git remote add #{remote} #{url}")
|
164
|
+
raise "Could not add the '#{remote}' remote."
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# for pedantry, validate the refspec too
|
169
|
+
unless `git config --get remote.#{remote}.fetch`.chomp == "+refs/heads/*:refs/remotes/#{remote}/*"
|
170
|
+
if Courseware.confirm("Your '#{remote}' remote has an invalid refspec. May I correct this?")
|
171
|
+
unless system("git config remote.#{remote}.fetch '+refs/heads/*:refs/remotes/#{remote}/*'")
|
172
|
+
raise "Could not repair the '#{remote}' refspec."
|
173
|
+
end
|
174
|
+
else
|
175
|
+
raise "Please configure your '#{remote}' remote before proceeding."
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
# Gem::Version is used simply for semantic version comparisons.
|
182
|
+
def version(tag)
|
183
|
+
Gem::Version.new(tag.gsub(/^.*-?v/, '')) rescue Gem::Version.new(0)
|
184
|
+
end
|
185
|
+
|
186
|
+
def self.repository?
|
187
|
+
system('git status >/dev/null 2>&1')
|
188
|
+
end
|
189
|
+
end
|