puppet-courseware-manager 0.5.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/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
|