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
@@ -1,54 +0,0 @@
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
@@ -1,9 +0,0 @@
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
-
@@ -1,9 +0,0 @@
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
@@ -1,85 +0,0 @@
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
@@ -1,28 +0,0 @@
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
@@ -1,21 +0,0 @@
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
@@ -1,18 +0,0 @@
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
@@ -1,64 +0,0 @@
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
@@ -1,8 +0,0 @@
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
-
@@ -1,38 +0,0 @@
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
@@ -1,262 +0,0 @@
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