leg 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/leg +9 -0
- data/lib/snaptoken.rb +24 -0
- data/lib/snaptoken/cli.rb +61 -0
- data/lib/snaptoken/commands.rb +13 -0
- data/lib/snaptoken/commands/amend.rb +27 -0
- data/lib/snaptoken/commands/base_command.rb +92 -0
- data/lib/snaptoken/commands/build.rb +107 -0
- data/lib/snaptoken/commands/commit.rb +27 -0
- data/lib/snaptoken/commands/help.rb +38 -0
- data/lib/snaptoken/commands/resolve.rb +27 -0
- data/lib/snaptoken/commands/status.rb +21 -0
- data/lib/snaptoken/commands/step.rb +35 -0
- data/lib/snaptoken/default_templates.rb +287 -0
- data/lib/snaptoken/diff.rb +180 -0
- data/lib/snaptoken/diff_line.rb +54 -0
- data/lib/snaptoken/diff_transformers.rb +9 -0
- data/lib/snaptoken/diff_transformers/base_transformer.rb +9 -0
- data/lib/snaptoken/diff_transformers/fold_sections.rb +85 -0
- data/lib/snaptoken/diff_transformers/omit_adjacent_removals.rb +28 -0
- data/lib/snaptoken/diff_transformers/trim_blank_lines.rb +21 -0
- data/lib/snaptoken/markdown.rb +18 -0
- data/lib/snaptoken/page.rb +64 -0
- data/lib/snaptoken/representations.rb +8 -0
- data/lib/snaptoken/representations/base_representation.rb +38 -0
- data/lib/snaptoken/representations/git.rb +262 -0
- data/lib/snaptoken/representations/litdiff.rb +81 -0
- data/lib/snaptoken/step.rb +27 -0
- data/lib/snaptoken/template.rb +53 -0
- data/lib/snaptoken/tutorial.rb +64 -0
- metadata +115 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
class Snaptoken::DiffLine
|
2
|
+
TYPES = [:added, :removed, :unchanged, :folded]
|
3
|
+
|
4
|
+
attr_reader :type, :source, :line_numbers
|
5
|
+
attr_writer :source, :line_numbers
|
6
|
+
|
7
|
+
def initialize(type, source, line_numbers)
|
8
|
+
unless TYPES.include? type
|
9
|
+
raise ArgumentError, "type must be one of: #{TYPES.inspect}"
|
10
|
+
end
|
11
|
+
@type = type
|
12
|
+
@source = source.chomp
|
13
|
+
@line_numbers = line_numbers
|
14
|
+
end
|
15
|
+
|
16
|
+
def clone
|
17
|
+
Snaptoken::DiffLine.new(@type, @source.dup, @line_numbers.dup)
|
18
|
+
end
|
19
|
+
|
20
|
+
def type=(type)
|
21
|
+
unless TYPES.include? type
|
22
|
+
raise ArgumentError, "type must be one of: #{TYPES.inspect}"
|
23
|
+
end
|
24
|
+
@type = type
|
25
|
+
end
|
26
|
+
|
27
|
+
def blank?
|
28
|
+
@source.strip.empty?
|
29
|
+
end
|
30
|
+
|
31
|
+
def line_number
|
32
|
+
case @type
|
33
|
+
when :removed, :folded
|
34
|
+
@line_numbers[0]
|
35
|
+
when :added, :unchanged
|
36
|
+
@line_numbers[1]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_patch(options = {})
|
41
|
+
options[:unchanged_char] ||= " "
|
42
|
+
|
43
|
+
case @type
|
44
|
+
when :added
|
45
|
+
"+#{@source}\n"
|
46
|
+
when :removed
|
47
|
+
"-#{@source}\n"
|
48
|
+
when :unchanged
|
49
|
+
"#{options[:unchanged_char]}#{@source}\n"
|
50
|
+
when :folded
|
51
|
+
raise "can't convert folded line to patch"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Snaptoken::DiffTransformers
|
2
|
+
end
|
3
|
+
|
4
|
+
require 'snaptoken/diff_transformers/base_transformer'
|
5
|
+
|
6
|
+
require 'snaptoken/diff_transformers/fold_sections'
|
7
|
+
require 'snaptoken/diff_transformers/omit_adjacent_removals'
|
8
|
+
require 'snaptoken/diff_transformers/trim_blank_lines'
|
9
|
+
|
@@ -0,0 +1,85 @@
|
|
1
|
+
class Snaptoken::DiffTransformers::FoldSections < Snaptoken::DiffTransformers::BaseTransformer
|
2
|
+
def transform(diff)
|
3
|
+
sections = @options[:section_types].map { [] }
|
4
|
+
|
5
|
+
cur_sections = @options[:section_types].map { nil }
|
6
|
+
diff.lines.each.with_index do |line, idx|
|
7
|
+
@options[:section_types].each.with_index do |section_type, level|
|
8
|
+
if line.source =~ Regexp.new(section_type[:start])
|
9
|
+
if !section_type[:end] && cur_sections[level]
|
10
|
+
cur_sections[level].end_line = idx - 1
|
11
|
+
if @options[:unfold_before_new_section]
|
12
|
+
cur_sections[level].dirty! if [:added, :removed].include? line.type
|
13
|
+
end
|
14
|
+
sections[level] << cur_sections[level]
|
15
|
+
end
|
16
|
+
|
17
|
+
cur_sections[level] = Section.new(level, idx)
|
18
|
+
|
19
|
+
if [:added, :removed].include? line.type
|
20
|
+
cur_sections[level].dirty!
|
21
|
+
end
|
22
|
+
elsif section_type[:end] && line.source =~ Regexp.new(section_type[:end])
|
23
|
+
if [:added, :removed].include? line.type
|
24
|
+
cur_sections[level].dirty!
|
25
|
+
end
|
26
|
+
|
27
|
+
cur_sections[level].end_line = idx
|
28
|
+
sections[level] << cur_sections[level]
|
29
|
+
cur_sections[level] = nil
|
30
|
+
elsif cur_sections[level]
|
31
|
+
if [:added, :removed].include? line.type
|
32
|
+
cur_sections[level].dirty!
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
cur_sections.each.with_index do |section, level|
|
38
|
+
unless section.nil?
|
39
|
+
section.end_line = diff.lines.length - 1
|
40
|
+
sections[level] << section
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
new_diff = diff.clone
|
45
|
+
sections.each.with_index do |level_sections, level|
|
46
|
+
level_sections.each do |section|
|
47
|
+
if !section.dirty? && !new_diff.lines[section.to_range].any?(&:nil?)
|
48
|
+
start_line = new_diff.lines[section.start_line]
|
49
|
+
end_line = new_diff.lines[section.end_line]
|
50
|
+
|
51
|
+
summary_lines = [start_line]
|
52
|
+
summary_lines << end_line if @options[:section_types][level][:end]
|
53
|
+
summary = summary_lines.map(&:source).join(" … ")
|
54
|
+
|
55
|
+
line_numbers = [start_line.line_number, end_line.line_number]
|
56
|
+
|
57
|
+
folded_line = Snaptoken::DiffLine.new(:folded, summary, line_numbers)
|
58
|
+
|
59
|
+
section.to_range.each do |idx|
|
60
|
+
new_diff.lines[idx] = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
new_diff.lines[section.start_line] = folded_line
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
new_diff.lines.compact!
|
68
|
+
new_diff
|
69
|
+
end
|
70
|
+
|
71
|
+
class Section
|
72
|
+
attr_accessor :level, :start_line, :end_line, :dirty
|
73
|
+
|
74
|
+
def initialize(level, start_line, end_line = nil, dirty = false)
|
75
|
+
@level, @start_line, @end_line, @dirty = level, start_line, end_line, dirty
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_range
|
79
|
+
start_line..end_line
|
80
|
+
end
|
81
|
+
|
82
|
+
def dirty?; @dirty; end
|
83
|
+
def dirty!; @dirty = true; end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
class Snaptoken::DiffTransformers::OmitAdjacentRemovals < Snaptoken::DiffTransformers::BaseTransformer
|
2
|
+
def transform(diff)
|
3
|
+
new_diff = diff.clone
|
4
|
+
|
5
|
+
removed_lines = []
|
6
|
+
saw_added_line = false
|
7
|
+
new_diff.lines.each.with_index do |line, idx|
|
8
|
+
case line.type
|
9
|
+
when :unchanged, :folded
|
10
|
+
if saw_added_line
|
11
|
+
removed_lines.each do |removed_idx|
|
12
|
+
new_diff.lines[removed_idx] = nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
removed_lines = []
|
17
|
+
saw_added_line = false
|
18
|
+
when :added
|
19
|
+
saw_added_line = true
|
20
|
+
when :removed
|
21
|
+
removed_lines << idx
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
new_diff.lines.compact!
|
26
|
+
new_diff
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Snaptoken::DiffTransformers::TrimBlankLines < Snaptoken::DiffTransformers::BaseTransformer
|
2
|
+
def transform(diff)
|
3
|
+
new_diff = diff.clone_empty
|
4
|
+
diff.lines.each.with_index do |line, idx|
|
5
|
+
line = line.clone
|
6
|
+
if line.blank? && [:added, :removed].include?(line.type)
|
7
|
+
prev_line = idx > 0 ? diff.lines[idx - 1] : nil
|
8
|
+
next_line = idx < diff.lines.length - 1 ? diff.lines[idx + 1] : nil
|
9
|
+
|
10
|
+
prev_changed = prev_line && [:added, :removed].include?(prev_line.type)
|
11
|
+
next_changed = next_line && [:added, :removed].include?(next_line.type)
|
12
|
+
|
13
|
+
if !prev_changed || !next_changed
|
14
|
+
line.type = :unchanged
|
15
|
+
end
|
16
|
+
end
|
17
|
+
new_diff.lines << line
|
18
|
+
end
|
19
|
+
new_diff
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Snaptoken::Markdown
|
2
|
+
class HTMLRouge < Redcarpet::Render::HTML
|
3
|
+
include Rouge::Plugins::Redcarpet
|
4
|
+
end
|
5
|
+
|
6
|
+
HTML_RENDERER = HTMLRouge.new(with_toc_data: true)
|
7
|
+
MARKDOWN_RENDERER = Redcarpet::Markdown.new(HTML_RENDERER, fenced_code_blocks: true)
|
8
|
+
|
9
|
+
def self.render(source)
|
10
|
+
html = MARKDOWN_RENDERER.render(source)
|
11
|
+
html = Redcarpet::Render::SmartyPants.render(html)
|
12
|
+
html.gsub!(/<\/code>‘/) { "</code>’" }
|
13
|
+
html.gsub!(/^\s*<h([23456]) id="([^"]+)">(.+)<\/h\d>$/) {
|
14
|
+
"<h#{$1} id=\"#{$2}\"><a href=\"##{$2}\">#{$3}</a></h#{$1}>"
|
15
|
+
}
|
16
|
+
html
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class Snaptoken::Page
|
2
|
+
attr_accessor :filename, :steps, :footer_text
|
3
|
+
|
4
|
+
def initialize(filename = "tutorial")
|
5
|
+
@filename = filename
|
6
|
+
@steps = []
|
7
|
+
@footer_text = nil
|
8
|
+
end
|
9
|
+
|
10
|
+
def <<(step)
|
11
|
+
@steps << step
|
12
|
+
self
|
13
|
+
end
|
14
|
+
|
15
|
+
def empty?
|
16
|
+
@steps.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
def title
|
20
|
+
first_line = @steps.first ? @steps.first.text.lines.first : (@footer_text ? @footer_text.lines.first : nil)
|
21
|
+
if first_line && first_line.start_with?("# ")
|
22
|
+
first_line[2..-1].strip
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_html(tutorial, offline)
|
27
|
+
content = ""
|
28
|
+
@steps.each do |step|
|
29
|
+
if !step.text.strip.empty?
|
30
|
+
html = Snaptoken::Markdown.render(step.text)
|
31
|
+
html.gsub!(/<p>{{step (\d+)}}<\/p>/) do
|
32
|
+
step = tutorial.step($1.to_i)
|
33
|
+
step.syntax_highlight!
|
34
|
+
step.to_html(tutorial, offline)
|
35
|
+
end
|
36
|
+
content << html
|
37
|
+
end
|
38
|
+
|
39
|
+
step.syntax_highlight!
|
40
|
+
content << step.to_html(tutorial, offline)
|
41
|
+
end
|
42
|
+
if @footer_text
|
43
|
+
# TODO: DRY this up. Please.
|
44
|
+
html = Snaptoken::Markdown.render(@footer_text)
|
45
|
+
html.gsub!(/<p>{{step (\d+)}}<\/p>/) do
|
46
|
+
step = tutorial.step($1.to_i)
|
47
|
+
step.syntax_highlight!
|
48
|
+
step.to_html(tutorial, offline)
|
49
|
+
end
|
50
|
+
content << html
|
51
|
+
end
|
52
|
+
|
53
|
+
page_number = tutorial.pages.index(self) + 1
|
54
|
+
|
55
|
+
Snaptoken::Template.new(tutorial.page_template, tutorial,
|
56
|
+
offline: offline,
|
57
|
+
page_title: title,
|
58
|
+
content: content,
|
59
|
+
page_number: page_number,
|
60
|
+
prev_page: page_number > 1 ? tutorial.pages[page_number - 2] : nil,
|
61
|
+
next_page: page_number < tutorial.pages.length ? tutorial.pages[page_number] : nil
|
62
|
+
).render_template
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Snaptoken::Representations::BaseRepresentation
|
2
|
+
def initialize(tutorial)
|
3
|
+
@tutorial = tutorial
|
4
|
+
end
|
5
|
+
|
6
|
+
# Should save @tutorial to disk.
|
7
|
+
def save!(options = {})
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
|
11
|
+
# Should load @tutorial (in place) from disk, and return it.
|
12
|
+
def load!(options = {})
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns true if this representation has been modified by the user since the
|
17
|
+
# last sync.
|
18
|
+
def modified?
|
19
|
+
synced_at = @tutorial.last_synced_at
|
20
|
+
repr_modified_at = modified_at
|
21
|
+
return false if synced_at.nil? or repr_modified_at.nil?
|
22
|
+
|
23
|
+
repr_modified_at > synced_at
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns true if this representation currently exists on disk.
|
27
|
+
def exists?
|
28
|
+
!modified_at.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# Should return the Time the representation on disk was last modified, or nil
|
34
|
+
# if the representation doesn't exist.
|
35
|
+
def modified_at
|
36
|
+
raise NotImplementedError
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,262 @@
|
|
1
|
+
class Snaptoken::Representations::Git < Snaptoken::Representations::BaseRepresentation
|
2
|
+
def save!(options = {})
|
3
|
+
FileUtils.rm_rf(repo_path)
|
4
|
+
FileUtils.mkdir_p(repo_path)
|
5
|
+
|
6
|
+
FileUtils.cd(repo_path) do
|
7
|
+
repo = Rugged::Repository.init_at(".")
|
8
|
+
|
9
|
+
step_num = 1
|
10
|
+
@tutorial.pages.each do |page|
|
11
|
+
message = "~~~ #{page.filename}"
|
12
|
+
message << "\n\n#{page.footer_text}" if page.footer_text
|
13
|
+
add_commit(repo, nil, message, step_num)
|
14
|
+
page.steps.each do |step|
|
15
|
+
message = "#{step.summary}\n\n#{step.text}".strip
|
16
|
+
add_commit(repo, step.to_patch, message, step_num)
|
17
|
+
|
18
|
+
yield step_num if block_given?
|
19
|
+
step_num += 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
#if options[:extra_path]
|
24
|
+
# FileUtils.cp_r(File.join(options[:extra_path], "."), ".")
|
25
|
+
# add_commit(repo, nil, "-", step_num, counter)
|
26
|
+
#end
|
27
|
+
|
28
|
+
repo.checkout_head(strategy: :force)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Options:
|
33
|
+
# full_diffs: If true, diffs contain the entire file in one hunk instead of
|
34
|
+
# multiple contextual hunks.
|
35
|
+
# diffs_ignore_whitespace: If true, diffs don't show changes to lines when
|
36
|
+
# only the amount of whitespace is changed.
|
37
|
+
def load!(options = {})
|
38
|
+
git_diff_options = {}
|
39
|
+
git_diff_options[:context_lines] = 100_000 if options[:full_diffs]
|
40
|
+
git_diff_options[:ignore_whitespace_change] = true if options[:diffs_ignore_whitespace]
|
41
|
+
|
42
|
+
page = nil
|
43
|
+
@tutorial.clear
|
44
|
+
each_step(git_diff_options) do |step_num, commit, summary, text, patches|
|
45
|
+
if patches.empty?
|
46
|
+
if summary =~ /^~~~ (.+)$/
|
47
|
+
@tutorial << page unless page.nil?
|
48
|
+
|
49
|
+
page = Snaptoken::Page.new($1)
|
50
|
+
page.footer_text = text unless text.empty?
|
51
|
+
else
|
52
|
+
puts "Warning: ignoring empty commit."
|
53
|
+
end
|
54
|
+
else
|
55
|
+
patch = patches.map(&:to_s).join("\n")
|
56
|
+
step_diffs = Snaptoken::Diff.parse(patch)
|
57
|
+
|
58
|
+
page ||= Snaptoken::Page.new
|
59
|
+
page << Snaptoken::Step.new(step_num, summary, text, step_diffs)
|
60
|
+
|
61
|
+
yield step_num if block_given?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
@tutorial << page unless page.nil?
|
65
|
+
@tutorial
|
66
|
+
end
|
67
|
+
|
68
|
+
def copy_repo_to_step!
|
69
|
+
FileUtils.mkdir_p(step_path)
|
70
|
+
FileUtils.rm_rf(File.join(step_path, "."), secure: true)
|
71
|
+
FileUtils.cd(repo_path) do
|
72
|
+
files = Dir.glob("*", File::FNM_DOTMATCH) - [".", "..", ".git"]
|
73
|
+
files.each do |f|
|
74
|
+
FileUtils.cp_r(f, File.join(step_path, f))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def copy_step_to_repo!
|
80
|
+
FileUtils.mv(
|
81
|
+
File.join(repo_path, ".git"),
|
82
|
+
File.join(repo_path, "../.gittemp")
|
83
|
+
)
|
84
|
+
FileUtils.rm_rf(File.join(repo_path, "."), secure: true)
|
85
|
+
FileUtils.mv(
|
86
|
+
File.join(repo_path, "../.gittemp"),
|
87
|
+
File.join(repo_path, ".git")
|
88
|
+
)
|
89
|
+
FileUtils.cd(step_path) do
|
90
|
+
files = Dir.glob("*", File::FNM_DOTMATCH) - [".", ".."]
|
91
|
+
files.each do |f|
|
92
|
+
FileUtils.cp_r(f, File.join(repo_path, f))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def repo_path
|
98
|
+
File.join(@tutorial.config[:path], ".leg/repo")
|
99
|
+
end
|
100
|
+
|
101
|
+
def repo
|
102
|
+
@repo ||= Rugged::Repository.new(repo_path)
|
103
|
+
end
|
104
|
+
|
105
|
+
def each_commit(options = {})
|
106
|
+
walker = Rugged::Walker.new(repo)
|
107
|
+
walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE)
|
108
|
+
|
109
|
+
master_commit = repo.branches["master"].target
|
110
|
+
walker.push(master_commit)
|
111
|
+
|
112
|
+
return [] if master_commit.oid == options[:after]
|
113
|
+
walker.hide(options[:after]) if options[:after]
|
114
|
+
|
115
|
+
return walker.to_a if not block_given?
|
116
|
+
|
117
|
+
walker.each do |commit|
|
118
|
+
yield commit
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
alias_method :commits, :each_commit
|
123
|
+
|
124
|
+
def each_step(git_diff_options = {})
|
125
|
+
empty_tree = Rugged::Tree.empty(repo)
|
126
|
+
step_num = 1
|
127
|
+
each_commit do |commit|
|
128
|
+
commit_message = commit.message.strip
|
129
|
+
summary = commit_message.lines.first.strip
|
130
|
+
text = (commit_message.lines[2..-1] || []).join.strip
|
131
|
+
next if commit_message == "-"
|
132
|
+
commit_message = "" if commit_message == "~"
|
133
|
+
last_commit = commit.parents.first
|
134
|
+
diff = (last_commit || empty_tree).diff(commit, git_diff_options)
|
135
|
+
patches = diff.each_patch.to_a
|
136
|
+
|
137
|
+
if patches.empty?
|
138
|
+
yield nil, commit, summary, text, patches
|
139
|
+
else
|
140
|
+
yield step_num, commit, summary, text, patches
|
141
|
+
step_num += 1
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def commit!(options = {})
|
147
|
+
copy_step_to_repo!
|
148
|
+
remaining_commits = commits(after: repo.head.target).map(&:oid)
|
149
|
+
FileUtils.cd(repo_path) do
|
150
|
+
`git add -A`
|
151
|
+
`git commit #{'--amend' if options[:amend]} -m"TODO: let user specify commit message"`
|
152
|
+
end
|
153
|
+
rebase!(remaining_commits)
|
154
|
+
end
|
155
|
+
|
156
|
+
def resolve!
|
157
|
+
copy_step_to_repo!
|
158
|
+
FileUtils.cd(repo_path) do
|
159
|
+
`git add -A`
|
160
|
+
`git -c core.editor=true cherry-pick --allow-empty --allow-empty-message --keep-redundant-commits --continue`
|
161
|
+
end
|
162
|
+
rebase!(load_remaining_commits)
|
163
|
+
end
|
164
|
+
|
165
|
+
def rebase!(remaining_commits)
|
166
|
+
FileUtils.cd(repo_path) do
|
167
|
+
remaining_commits.each.with_index do |commit, commit_idx|
|
168
|
+
`git cherry-pick --allow-empty --allow-empty-message --keep-redundant-commits #{commit}`
|
169
|
+
|
170
|
+
if not $?.success?
|
171
|
+
copy_repo_to_step!
|
172
|
+
save_remaining_commits(remaining_commits[(commit_idx+1)..-1])
|
173
|
+
return false
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
save_remaining_commits(nil)
|
179
|
+
|
180
|
+
repo.references.update(repo.branches["master"], repo.head.target_id)
|
181
|
+
repo.head = "refs/heads/master"
|
182
|
+
|
183
|
+
true
|
184
|
+
end
|
185
|
+
|
186
|
+
private
|
187
|
+
|
188
|
+
def step_path
|
189
|
+
File.join(@tutorial.config[:path], "step")
|
190
|
+
end
|
191
|
+
|
192
|
+
def remaining_commits_path
|
193
|
+
File.join(@tutorial.config[:path], ".leg/remaining_commits")
|
194
|
+
end
|
195
|
+
|
196
|
+
def modified_at
|
197
|
+
if File.exist? repo_path
|
198
|
+
repo = Rugged::Repository.new(repo_path)
|
199
|
+
if master = repo.branches.find { |b| b.name == "master" }
|
200
|
+
master.target.time
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def load_remaining_commits
|
206
|
+
if File.exist?(remaining_commits_path)
|
207
|
+
File.readlines(remaining_commits_path).map(&:strip).reject(&:empty?)
|
208
|
+
else
|
209
|
+
[]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def save_remaining_commits(remaining_commits)
|
214
|
+
if remaining_commits && !remaining_commits.empty?
|
215
|
+
File.write(remaining_commits_path, remaining_commits.join("\n"))
|
216
|
+
else
|
217
|
+
FileUtils.rm_f(remaining_commits_path)
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def add_commit(repo, diff, message, step_num)
|
222
|
+
message ||= "~"
|
223
|
+
message.strip!
|
224
|
+
message = "~" if message.empty?
|
225
|
+
|
226
|
+
if diff
|
227
|
+
stdin = IO.popen("git apply -", "w")
|
228
|
+
stdin.write diff
|
229
|
+
stdin.close
|
230
|
+
end
|
231
|
+
|
232
|
+
index = repo.index
|
233
|
+
index.read_tree(repo.head.target.tree) unless repo.empty?
|
234
|
+
|
235
|
+
Dir["**/*"].each do |path|
|
236
|
+
unless File.directory?(path)
|
237
|
+
oid = repo.write(File.read(path), :blob)
|
238
|
+
index.add(path: path, oid: oid, mode: 0100644)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
options = {}
|
243
|
+
options[:tree] = index.write_tree(repo)
|
244
|
+
if @tutorial.config[:repo_author_name]
|
245
|
+
options[:author] = {
|
246
|
+
name: @tutorial.config[:repo_author_name],
|
247
|
+
email: @tutorial.config[:repo_author_email],
|
248
|
+
time: Time.now
|
249
|
+
}
|
250
|
+
options[:committer] = options[:author]
|
251
|
+
end
|
252
|
+
options[:message] = message
|
253
|
+
options[:parents] = repo.empty? ? [] : [repo.head.target]
|
254
|
+
options[:update_ref] = "HEAD"
|
255
|
+
|
256
|
+
commit_oid = Rugged::Commit.create(repo, options)
|
257
|
+
|
258
|
+
if diff
|
259
|
+
repo.references.create("refs/tags/step-#{step_num}", commit_oid)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|