leg 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,9 @@
1
+ class Snaptoken::DiffTransformers::BaseTransformer
2
+ def initialize(options = {})
3
+ @options = options
4
+ end
5
+
6
+ def transform(diff)
7
+ raise NotImplementedError
8
+ end
9
+ end
@@ -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>&lsquo;/) { "</code>&rsquo;" }
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,8 @@
1
+ module Snaptoken::Representations
2
+ end
3
+
4
+ require 'snaptoken/representations/base_representation'
5
+
6
+ require 'snaptoken/representations/git'
7
+ require 'snaptoken/representations/litdiff'
8
+
@@ -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