leg 0.0.1 → 0.0.2

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.
Files changed (78) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +10 -0
  3. data/CODE_OF_CONDUCT.md +74 -0
  4. data/Gemfile +6 -0
  5. data/Gemfile.lock +34 -0
  6. data/LICENSE +21 -0
  7. data/README.md +59 -0
  8. data/Rakefile +11 -0
  9. data/TUTORIAL.md +243 -0
  10. data/bin/console +9 -0
  11. data/bin/setup +6 -0
  12. data/exe/leg +6 -0
  13. data/leg.gemspec +31 -0
  14. data/lib/leg.rb +27 -0
  15. data/lib/leg/cli.rb +42 -0
  16. data/lib/leg/commands.rb +19 -0
  17. data/lib/leg/commands/amend.rb +49 -0
  18. data/lib/leg/commands/base_command.rb +99 -0
  19. data/lib/leg/commands/build.rb +126 -0
  20. data/lib/leg/commands/commit.rb +48 -0
  21. data/lib/leg/commands/diff.rb +29 -0
  22. data/lib/leg/commands/help.rb +43 -0
  23. data/lib/leg/commands/init.rb +46 -0
  24. data/lib/leg/commands/reset.rb +26 -0
  25. data/lib/leg/commands/resolve.rb +31 -0
  26. data/lib/leg/commands/save.rb +31 -0
  27. data/lib/leg/commands/status.rb +54 -0
  28. data/lib/leg/commands/step.rb +31 -0
  29. data/lib/leg/config.rb +42 -0
  30. data/lib/leg/default_templates.rb +340 -0
  31. data/lib/leg/diff.rb +151 -0
  32. data/lib/leg/diff_transformers.rb +11 -0
  33. data/lib/leg/diff_transformers/base_transformer.rb +13 -0
  34. data/lib/leg/diff_transformers/fold_sections.rb +89 -0
  35. data/lib/leg/diff_transformers/omit_adjacent_removals.rb +38 -0
  36. data/lib/leg/diff_transformers/syntax_highlight.rb +32 -0
  37. data/lib/leg/diff_transformers/trim_blank_lines.rb +25 -0
  38. data/lib/leg/line.rb +83 -0
  39. data/lib/leg/markdown.rb +20 -0
  40. data/lib/leg/page.rb +27 -0
  41. data/lib/leg/representations.rb +9 -0
  42. data/lib/leg/representations/base_representation.rb +42 -0
  43. data/lib/leg/representations/git.rb +388 -0
  44. data/lib/leg/representations/litdiff.rb +85 -0
  45. data/lib/leg/step.rb +16 -0
  46. data/lib/leg/template.rb +95 -0
  47. data/lib/leg/tutorial.rb +49 -0
  48. data/lib/leg/version.rb +3 -0
  49. metadata +112 -38
  50. data/bin/leg +0 -9
  51. data/lib/snaptoken.rb +0 -24
  52. data/lib/snaptoken/cli.rb +0 -61
  53. data/lib/snaptoken/commands.rb +0 -13
  54. data/lib/snaptoken/commands/amend.rb +0 -27
  55. data/lib/snaptoken/commands/base_command.rb +0 -92
  56. data/lib/snaptoken/commands/build.rb +0 -107
  57. data/lib/snaptoken/commands/commit.rb +0 -27
  58. data/lib/snaptoken/commands/help.rb +0 -38
  59. data/lib/snaptoken/commands/resolve.rb +0 -27
  60. data/lib/snaptoken/commands/status.rb +0 -21
  61. data/lib/snaptoken/commands/step.rb +0 -35
  62. data/lib/snaptoken/default_templates.rb +0 -287
  63. data/lib/snaptoken/diff.rb +0 -180
  64. data/lib/snaptoken/diff_line.rb +0 -54
  65. data/lib/snaptoken/diff_transformers.rb +0 -9
  66. data/lib/snaptoken/diff_transformers/base_transformer.rb +0 -9
  67. data/lib/snaptoken/diff_transformers/fold_sections.rb +0 -85
  68. data/lib/snaptoken/diff_transformers/omit_adjacent_removals.rb +0 -28
  69. data/lib/snaptoken/diff_transformers/trim_blank_lines.rb +0 -21
  70. data/lib/snaptoken/markdown.rb +0 -18
  71. data/lib/snaptoken/page.rb +0 -64
  72. data/lib/snaptoken/representations.rb +0 -8
  73. data/lib/snaptoken/representations/base_representation.rb +0 -38
  74. data/lib/snaptoken/representations/git.rb +0 -262
  75. data/lib/snaptoken/representations/litdiff.rb +0 -81
  76. data/lib/snaptoken/step.rb +0 -27
  77. data/lib/snaptoken/template.rb +0 -53
  78. data/lib/snaptoken/tutorial.rb +0 -64
@@ -0,0 +1,20 @@
1
+ module Leg
2
+ module Markdown
3
+ class HTMLRouge < Redcarpet::Render::HTML
4
+ include Rouge::Plugins::Redcarpet
5
+ end
6
+
7
+ HTML_RENDERER = HTMLRouge.new(with_toc_data: true)
8
+ MARKDOWN_RENDERER = Redcarpet::Markdown.new(HTML_RENDERER, fenced_code_blocks: true)
9
+
10
+ def self.render(source)
11
+ html = MARKDOWN_RENDERER.render(source)
12
+ html = Redcarpet::Render::SmartyPants.render(html)
13
+ html.gsub!(/<\/code>&lsquo;/) { "</code>&rsquo;" }
14
+ html.gsub!(/^\s*<h([23456]) id="([^"]+)">(.+)<\/h\d>$/) {
15
+ "<h#{$1} id=\"#{$2}\"><a href=\"##{$2}\">#{$3}</a></h#{$1}>"
16
+ }
17
+ html
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ module Leg
2
+ class Page
3
+ attr_accessor :filename, :steps, :footer_text
4
+
5
+ def initialize(filename = "tutorial")
6
+ @filename = filename
7
+ @steps = []
8
+ @footer_text = nil
9
+ end
10
+
11
+ def <<(step)
12
+ @steps << step
13
+ self
14
+ end
15
+
16
+ def empty?
17
+ @steps.empty?
18
+ end
19
+
20
+ def title
21
+ first_line = @steps.first ? @steps.first.text.lines.first : (@footer_text ? @footer_text.lines.first : nil)
22
+ if first_line && first_line.start_with?("# ")
23
+ first_line[2..-1].strip
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ require 'leg/representations/base_representation'
2
+
3
+ require 'leg/representations/git'
4
+ require 'leg/representations/litdiff'
5
+
6
+ module Leg
7
+ module Representations
8
+ end
9
+ end
@@ -0,0 +1,42 @@
1
+ module Leg
2
+ module Representations
3
+ class BaseRepresentation
4
+ def initialize(config)
5
+ @config = config
6
+ end
7
+
8
+ # Should save tutorial to disk.
9
+ def save!(tutorial, options = {})
10
+ raise NotImplementedError
11
+ end
12
+
13
+ # Should load tutorial from disk, and return it.
14
+ def load!(options = {})
15
+ raise NotImplementedError
16
+ end
17
+
18
+ # Returns true if this representation has been modified by the user since the
19
+ # last sync.
20
+ def modified?
21
+ synced_at = @config.last_synced_at
22
+ repr_modified_at = modified_at
23
+ return false if synced_at.nil? or repr_modified_at.nil?
24
+
25
+ repr_modified_at > synced_at
26
+ end
27
+
28
+ # Returns true if this representation currently exists on disk.
29
+ def exists?
30
+ !modified_at.nil?
31
+ end
32
+
33
+ private
34
+
35
+ # Should return the Time the representation on disk was last modified, or nil
36
+ # if the representation doesn't exist.
37
+ def modified_at
38
+ raise NotImplementedError
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,388 @@
1
+ module Leg
2
+ module Representations
3
+ class Git < BaseRepresentation
4
+ def save!(tutorial, options = {})
5
+ FileUtils.rm_rf(repo_path)
6
+ FileUtils.mkdir_p(repo_path)
7
+
8
+ FileUtils.cd(repo_path) do
9
+ repo = Rugged::Repository.init_at(".")
10
+
11
+ step_num = 1
12
+ tutorial.pages.each do |page|
13
+ message = "~~~ #{page.filename}"
14
+ message << "\n\n#{page.footer_text}" if page.footer_text
15
+ add_commit(repo, nil, message, step_num)
16
+ page.steps.each do |step|
17
+ message = "#{step.summary}\n\n#{step.text}".strip
18
+ add_commit(repo, step.to_patch, message, step_num)
19
+
20
+ yield step_num if block_given?
21
+ step_num += 1
22
+ end
23
+ end
24
+
25
+ #if options[:extra_path]
26
+ # FileUtils.cp_r(File.join(options[:extra_path], "."), ".")
27
+ # add_commit(repo, nil, "-", step_num, counter)
28
+ #end
29
+
30
+ repo.checkout_head(strategy: :force) if repo.branches["master"]
31
+ end
32
+ end
33
+
34
+ # Options:
35
+ # full_diffs: If true, diffs contain the entire file in one hunk instead of
36
+ # multiple contextual hunks.
37
+ # diffs_ignore_whitespace: If true, diffs don't show changes to lines when
38
+ # only the amount of whitespace is changed.
39
+ def load!(options = {})
40
+ git_diff_options = {}
41
+ git_diff_options[:context_lines] = 100_000 if options[:full_diffs]
42
+ git_diff_options[:ignore_whitespace_change] = true if options[:diffs_ignore_whitespace]
43
+
44
+ page = nil
45
+ tutorial = Leg::Tutorial.new(@config)
46
+ each_step(git_diff_options) do |step_num, commit, summary, text, patches|
47
+ if patches.empty?
48
+ if summary =~ /^~~~ (.+)$/
49
+ tutorial << page unless page.nil?
50
+
51
+ page = Leg::Page.new($1)
52
+ page.footer_text = text unless text.empty?
53
+ else
54
+ puts "Warning: ignoring empty commit."
55
+ end
56
+ else
57
+ patch = patches.map(&:to_s).join("\n")
58
+ step_diffs = Leg::Diff.parse(patch)
59
+
60
+ page ||= Leg::Page.new
61
+ page << Leg::Step.new(step_num, summary, text, step_diffs)
62
+
63
+ yield step_num if block_given?
64
+ end
65
+ end
66
+ tutorial << page unless page.nil?
67
+ tutorial
68
+ end
69
+
70
+ def copy_repo_to_step!
71
+ FileUtils.mkdir_p(step_path)
72
+ FileUtils.rm_rf(File.join(step_path, "."), secure: true)
73
+ FileUtils.cd(repo_path) do
74
+ files = Dir.glob("*", File::FNM_DOTMATCH) - [".", "..", ".git"]
75
+ files.each do |f|
76
+ FileUtils.cp_r(f, File.join(step_path, f))
77
+ end
78
+ end
79
+ end
80
+
81
+ def copy_step_to_repo!
82
+ FileUtils.mv(
83
+ File.join(repo_path, ".git"),
84
+ File.join(repo_path, "../.gittemp")
85
+ )
86
+ FileUtils.rm_rf(File.join(repo_path, "."), secure: true)
87
+ FileUtils.mv(
88
+ File.join(repo_path, "../.gittemp"),
89
+ File.join(repo_path, ".git")
90
+ )
91
+ FileUtils.cd(step_path) do
92
+ files = Dir.glob("*", File::FNM_DOTMATCH) - [".", ".."]
93
+ files.each do |f|
94
+ FileUtils.cp_r(f, File.join(repo_path, f))
95
+ end
96
+ end
97
+ end
98
+
99
+ def repo_path
100
+ File.join(@config.path, ".leg/repo")
101
+ end
102
+
103
+ def repo
104
+ @repo ||= Rugged::Repository.new(repo_path)
105
+ end
106
+
107
+ def each_commit(options = {})
108
+ walker = Rugged::Walker.new(repo)
109
+ walker.sorting(Rugged::SORT_TOPO | Rugged::SORT_REVERSE)
110
+
111
+ master_commit = repo.branches["master"].target
112
+ walker.push(master_commit)
113
+
114
+ return [] if master_commit.oid == options[:after]
115
+ walker.hide(options[:after]) if options[:after]
116
+
117
+ return walker.to_a if not block_given?
118
+
119
+ walker.each do |commit|
120
+ yield commit
121
+ end
122
+ end
123
+
124
+ alias_method :commits, :each_commit
125
+
126
+ def each_step(git_diff_options = {})
127
+ empty_tree = Rugged::Tree.empty(repo)
128
+ step_num = 1
129
+ each_commit do |commit|
130
+ commit_message = commit.message.strip
131
+ summary = commit_message.lines.first.strip
132
+ text = (commit_message.lines[2..-1] || []).join.strip
133
+ next if commit_message == "-"
134
+ commit_message = "" if commit_message == "~"
135
+ last_commit = commit.parents.first
136
+ diff = (last_commit || empty_tree).diff(commit, git_diff_options)
137
+ patches = diff.each_patch.to_a
138
+
139
+ if patches.empty?
140
+ yield nil, commit, summary, text, patches
141
+ else
142
+ yield step_num, commit, summary, text, patches
143
+ step_num += 1
144
+ end
145
+ end
146
+ end
147
+
148
+ def init!
149
+ FileUtils.mkdir_p(repo_path)
150
+ FileUtils.cd(repo_path) { `git init` }
151
+ end
152
+
153
+ def checkout!(step_number)
154
+ each_step do |cur_step, commit|
155
+ if cur_step == step_number
156
+ FileUtils.cd(repo_path) { `git checkout #{commit.oid} 2>/dev/null` }
157
+ save_state(load_state.step!(step_number))
158
+ copy_repo_to_step!
159
+ return true
160
+ end
161
+ end
162
+ end
163
+
164
+ def commit!(options = {})
165
+ copy_step_to_repo!
166
+ remaining_commits = repo.branches["master"] ? commits(after: repo.head.target.oid).map(&:oid) : []
167
+ FileUtils.cd(repo_path) do
168
+ `git add -A`
169
+
170
+ cmd = ["git", "commit", "-q"]
171
+ cmd << "--amend" if options[:amend]
172
+ cmd << "-m" << options[:message] if options[:message]
173
+ cmd << "--no-edit" if options[:use_default_message] && options[:amend]
174
+ cmd << "-m" << "Untitled step" if options[:use_default_message] && !options[:amend]
175
+ system(*cmd)
176
+ end
177
+ if options[:amend]
178
+ save_state(load_state.amend!)
179
+ else
180
+ save_state(load_state.add_commit!)
181
+ end
182
+ if options[:no_rebase]
183
+ save_remaining_commits(remaining_commits)
184
+ true
185
+ else
186
+ rebase!(remaining_commits)
187
+ end
188
+ end
189
+
190
+ def resolve!
191
+ copy_step_to_repo!
192
+ FileUtils.cd(repo_path) do
193
+ `git add -A`
194
+ `git -c core.editor=true cherry-pick --allow-empty --allow-empty-message --keep-redundant-commits --continue 2>/dev/null`
195
+ end
196
+ rebase!(load_remaining_commits)
197
+ end
198
+
199
+ def rebase_remaining!
200
+ rebase!(load_remaining_commits)
201
+ end
202
+
203
+ def rebase!(remaining_commits)
204
+ FileUtils.cd(repo_path) do
205
+ remaining_commits.each.with_index do |commit, commit_idx|
206
+ `git cherry-pick --allow-empty --allow-empty-message --keep-redundant-commits #{commit} 2>/dev/null`
207
+
208
+ if not $?.success?
209
+ copy_repo_to_step!
210
+ save_remaining_commits(remaining_commits[(commit_idx+1)..-1])
211
+ save_state(load_state.conflict!)
212
+ return false
213
+ end
214
+ end
215
+ end
216
+
217
+ save_remaining_commits(nil)
218
+ save_state(nil)
219
+
220
+ repo.references.update(repo.branches["master"], repo.head.target_id)
221
+ repo.head = "refs/heads/master"
222
+
223
+ copy_repo_to_step!
224
+
225
+ true
226
+ end
227
+
228
+ def reset!
229
+ save_state(nil)
230
+ save_remaining_commits(nil)
231
+ FileUtils.cd(repo_path) do
232
+ `git cherry-pick --abort 2>/dev/null`
233
+ end
234
+ repo.head = "refs/heads/master"
235
+ repo.checkout_head(strategy: :force)
236
+ copy_repo_to_step!
237
+ end
238
+
239
+ def state
240
+ load_state
241
+ end
242
+
243
+ private
244
+
245
+ def step_path
246
+ File.join(@config.path, "step")
247
+ end
248
+
249
+ def remaining_commits_path
250
+ File.join(@config.path, ".leg/remaining_commits")
251
+ end
252
+
253
+ def modified_at
254
+ if File.exist? repo_path
255
+ repo = Rugged::Repository.new(repo_path)
256
+ if master = repo.branches["master"]
257
+ master.target.time
258
+ end
259
+ end
260
+ end
261
+
262
+ def state_path
263
+ File.join(@config.path, ".leg/state.yml")
264
+ end
265
+
266
+ def load_state
267
+ @state ||=
268
+ if File.exist?(state_path)
269
+ YAML.load_file(state_path)
270
+ else
271
+ State.new
272
+ end
273
+ end
274
+
275
+ def save_state(state)
276
+ @state = state
277
+ if state.nil?
278
+ FileUtils.rm_f(state_path)
279
+ else
280
+ File.write(state_path, state.to_yaml)
281
+ end
282
+ end
283
+
284
+ def load_remaining_commits
285
+ if File.exist?(remaining_commits_path)
286
+ File.readlines(remaining_commits_path).map(&:strip).reject(&:empty?)
287
+ else
288
+ []
289
+ end
290
+ end
291
+
292
+ def save_remaining_commits(remaining_commits)
293
+ if remaining_commits && !remaining_commits.empty?
294
+ File.write(remaining_commits_path, remaining_commits.join("\n"))
295
+ else
296
+ FileUtils.rm_f(remaining_commits_path)
297
+ end
298
+ end
299
+
300
+ def add_commit(repo, diff, message, step_num)
301
+ message ||= "~"
302
+ message.strip!
303
+ message = "~" if message.empty?
304
+
305
+ if diff
306
+ stdin = IO.popen("git apply -", "w")
307
+ stdin.write diff
308
+ stdin.close
309
+ end
310
+
311
+ index = repo.index
312
+ index.read_tree(repo.head.target.tree) unless repo.empty?
313
+
314
+ Dir["**/*"].each do |path|
315
+ unless File.directory?(path)
316
+ oid = repo.write(File.read(path), :blob)
317
+ index.add(path: path, oid: oid, mode: 0100644)
318
+ end
319
+ end
320
+
321
+ options = {}
322
+ options[:tree] = index.write_tree(repo)
323
+ if @config.options[:repo_author_name]
324
+ options[:author] = {
325
+ name: @config.options[:repo_author_name],
326
+ email: @config.options[:repo_author_email],
327
+ time: Time.now
328
+ }
329
+ options[:committer] = options[:author]
330
+ end
331
+ options[:message] = message
332
+ options[:parents] = repo.empty? ? [] : [repo.head.target]
333
+ options[:update_ref] = "HEAD"
334
+
335
+ commit_oid = Rugged::Commit.create(repo, options)
336
+
337
+ if diff
338
+ repo.references.create("refs/tags/step-#{step_num}", commit_oid)
339
+ end
340
+ end
341
+
342
+ class State
343
+ attr_accessor :step_number, :operation, :args, :conflict
344
+
345
+ def initialize
346
+ @step_number = nil
347
+ @operation = nil
348
+ @args = []
349
+ @conflict = false
350
+ end
351
+
352
+ def step!(step_number)
353
+ @step_number = step_number
354
+ self
355
+ end
356
+
357
+ def add_commit!
358
+ if @operation.nil?
359
+ @operation = :commit
360
+ @args = [1, false]
361
+ elsif @operation == :commit
362
+ @args[0] += 1
363
+ else
364
+ raise "@operation must be :commit or nil"
365
+ end
366
+ self
367
+ end
368
+
369
+ def amend!
370
+ if @operation.nil?
371
+ @operation = :commit
372
+ @args = [0, true]
373
+ elsif @operation == :commit
374
+ @args[1] = true
375
+ else
376
+ raise "@operation must be :commit or nil"
377
+ end
378
+ self
379
+ end
380
+
381
+ def conflict!
382
+ @conflict = true
383
+ self
384
+ end
385
+ end
386
+ end
387
+ end
388
+ end