leg 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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