jit 0.0.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +674 -0
  3. data/bin/jit +21 -0
  4. data/lib/color.rb +32 -0
  5. data/lib/command.rb +62 -0
  6. data/lib/command/add.rb +65 -0
  7. data/lib/command/base.rb +92 -0
  8. data/lib/command/branch.rb +199 -0
  9. data/lib/command/checkout.rb +104 -0
  10. data/lib/command/cherry_pick.rb +51 -0
  11. data/lib/command/commit.rb +86 -0
  12. data/lib/command/config.rb +126 -0
  13. data/lib/command/diff.rb +114 -0
  14. data/lib/command/fetch.rb +116 -0
  15. data/lib/command/init.rb +41 -0
  16. data/lib/command/log.rb +188 -0
  17. data/lib/command/merge.rb +148 -0
  18. data/lib/command/push.rb +172 -0
  19. data/lib/command/receive_pack.rb +92 -0
  20. data/lib/command/remote.rb +55 -0
  21. data/lib/command/reset.rb +64 -0
  22. data/lib/command/rev_list.rb +33 -0
  23. data/lib/command/revert.rb +69 -0
  24. data/lib/command/rm.rb +105 -0
  25. data/lib/command/shared/fast_forward.rb +19 -0
  26. data/lib/command/shared/print_diff.rb +116 -0
  27. data/lib/command/shared/receive_objects.rb +37 -0
  28. data/lib/command/shared/remote_agent.rb +44 -0
  29. data/lib/command/shared/remote_client.rb +82 -0
  30. data/lib/command/shared/send_objects.rb +24 -0
  31. data/lib/command/shared/sequencing.rb +146 -0
  32. data/lib/command/shared/write_commit.rb +167 -0
  33. data/lib/command/status.rb +210 -0
  34. data/lib/command/upload_pack.rb +54 -0
  35. data/lib/config.rb +240 -0
  36. data/lib/config/stack.rb +42 -0
  37. data/lib/database.rb +112 -0
  38. data/lib/database/author.rb +27 -0
  39. data/lib/database/backends.rb +57 -0
  40. data/lib/database/blob.rb +24 -0
  41. data/lib/database/commit.rb +70 -0
  42. data/lib/database/entry.rb +7 -0
  43. data/lib/database/loose.rb +70 -0
  44. data/lib/database/packed.rb +75 -0
  45. data/lib/database/tree.rb +77 -0
  46. data/lib/database/tree_diff.rb +88 -0
  47. data/lib/diff.rb +46 -0
  48. data/lib/diff/combined.rb +72 -0
  49. data/lib/diff/hunk.rb +64 -0
  50. data/lib/diff/myers.rb +90 -0
  51. data/lib/editor.rb +59 -0
  52. data/lib/index.rb +212 -0
  53. data/lib/index/checksum.rb +44 -0
  54. data/lib/index/entry.rb +91 -0
  55. data/lib/lockfile.rb +55 -0
  56. data/lib/merge/bases.rb +38 -0
  57. data/lib/merge/common_ancestors.rb +77 -0
  58. data/lib/merge/diff3.rb +156 -0
  59. data/lib/merge/inputs.rb +42 -0
  60. data/lib/merge/resolve.rb +178 -0
  61. data/lib/pack.rb +45 -0
  62. data/lib/pack/compressor.rb +83 -0
  63. data/lib/pack/delta.rb +58 -0
  64. data/lib/pack/entry.rb +54 -0
  65. data/lib/pack/expander.rb +54 -0
  66. data/lib/pack/index.rb +100 -0
  67. data/lib/pack/indexer.rb +200 -0
  68. data/lib/pack/numbers.rb +79 -0
  69. data/lib/pack/reader.rb +98 -0
  70. data/lib/pack/stream.rb +80 -0
  71. data/lib/pack/unpacker.rb +62 -0
  72. data/lib/pack/window.rb +47 -0
  73. data/lib/pack/writer.rb +92 -0
  74. data/lib/pack/xdelta.rb +118 -0
  75. data/lib/pager.rb +24 -0
  76. data/lib/progress.rb +78 -0
  77. data/lib/refs.rb +260 -0
  78. data/lib/remotes.rb +82 -0
  79. data/lib/remotes/protocol.rb +82 -0
  80. data/lib/remotes/refspec.rb +70 -0
  81. data/lib/remotes/remote.rb +57 -0
  82. data/lib/repository.rb +64 -0
  83. data/lib/repository/divergence.rb +21 -0
  84. data/lib/repository/hard_reset.rb +35 -0
  85. data/lib/repository/inspector.rb +49 -0
  86. data/lib/repository/migration.rb +168 -0
  87. data/lib/repository/pending_commit.rb +60 -0
  88. data/lib/repository/sequencer.rb +118 -0
  89. data/lib/repository/status.rb +98 -0
  90. data/lib/rev_list.rb +244 -0
  91. data/lib/revision.rb +155 -0
  92. data/lib/sorted_hash.rb +17 -0
  93. data/lib/temp_file.rb +34 -0
  94. data/lib/workspace.rb +107 -0
  95. metadata +103 -9
@@ -0,0 +1,168 @@
1
+ require "set"
2
+ require_relative "./inspector"
3
+
4
+ class Repository
5
+ class Migration
6
+
7
+ Conflict = Class.new(StandardError)
8
+
9
+ MESSAGES = {
10
+ :stale_file => [
11
+ "Your local changes to the following files would be overwritten by checkout:",
12
+ "Please commit your changes or stash them before you switch branches."
13
+ ],
14
+ :stale_directory => [
15
+ "Updating the following directories would lose untracked files in them:",
16
+ "\n"
17
+ ],
18
+ :untracked_overwritten => [
19
+ "The following untracked working tree files would be overwritten by checkout:",
20
+ "Please move or remove them before you switch branches."
21
+ ],
22
+ :untracked_removed => [
23
+ "The following untracked working tree files would be removed by checkout:",
24
+ "Please move or remove them before you switch branches."
25
+ ]
26
+ }
27
+
28
+ attr_reader :changes, :mkdirs, :rmdirs, :errors
29
+
30
+ def initialize(repository, tree_diff)
31
+ @repo = repository
32
+ @diff = tree_diff
33
+
34
+ @inspector = Inspector.new(repository)
35
+
36
+ @changes = { :create => [], :update => [], :delete => [] }
37
+ @mkdirs = Set.new
38
+ @rmdirs = Set.new
39
+ @errors = []
40
+
41
+ @conflicts = {
42
+ :stale_file => SortedSet.new,
43
+ :stale_directory => SortedSet.new,
44
+ :untracked_overwritten => SortedSet.new,
45
+ :untracked_removed => SortedSet.new
46
+ }
47
+ end
48
+
49
+ def apply_changes
50
+ plan_changes
51
+ update_workspace
52
+ update_index
53
+ end
54
+
55
+ def blob_data(oid)
56
+ @repo.database.load(oid).data
57
+ end
58
+
59
+ private
60
+
61
+ def plan_changes
62
+ @diff.each do |path, (old_item, new_item)|
63
+ check_for_conflict(path, old_item, new_item)
64
+ record_change(path, old_item, new_item)
65
+ end
66
+
67
+ collect_errors
68
+ end
69
+
70
+ def update_workspace
71
+ @repo.workspace.apply_migration(self)
72
+ end
73
+
74
+ def update_index
75
+ @changes[:delete].each do |path, _|
76
+ @repo.index.remove(path)
77
+ end
78
+
79
+ [:create, :update].each do |action|
80
+ @changes[action].each do |path, entry|
81
+ stat = @repo.workspace.stat_file(path)
82
+ @repo.index.add(path, entry.oid, stat)
83
+ end
84
+ end
85
+ end
86
+
87
+ def record_change(path, old_item, new_item)
88
+ if old_item == nil
89
+ @mkdirs.merge(path.dirname.descend)
90
+ action = :create
91
+ elsif new_item == nil
92
+ @rmdirs.merge(path.dirname.descend)
93
+ action = :delete
94
+ else
95
+ @mkdirs.merge(path.dirname.descend)
96
+ action = :update
97
+ end
98
+ @changes[action].push([path, new_item])
99
+ end
100
+
101
+ def check_for_conflict(path, old_item, new_item)
102
+ entry = @repo.index.entry_for_path(path)
103
+
104
+ if index_differs_from_trees(entry, old_item, new_item)
105
+ @conflicts[:stale_file].add(path.to_s)
106
+ return
107
+ end
108
+
109
+ stat = @repo.workspace.stat_file(path)
110
+ type = get_error_type(stat, entry, new_item)
111
+
112
+ if stat == nil
113
+ parent = untracked_parent(path)
114
+ @conflicts[type].add(entry ? path.to_s : parent.to_s) if parent
115
+
116
+ elsif stat.file?
117
+ changed = @inspector.compare_index_to_workspace(entry, stat)
118
+ @conflicts[type].add(path.to_s) if changed
119
+
120
+ elsif stat.directory?
121
+ trackable = @inspector.trackable_file?(path, stat)
122
+ @conflicts[type].add(path.to_s) if trackable
123
+ end
124
+ end
125
+
126
+ def get_error_type(stat, entry, item)
127
+ if entry
128
+ :stale_file
129
+ elsif stat&.directory?
130
+ :stale_directory
131
+ elsif item
132
+ :untracked_overwritten
133
+ else
134
+ :untracked_removed
135
+ end
136
+ end
137
+
138
+ def index_differs_from_trees(entry, old_item, new_item)
139
+ @inspector.compare_tree_to_index(old_item, entry) and
140
+ @inspector.compare_tree_to_index(new_item, entry)
141
+ end
142
+
143
+ def untracked_parent(path)
144
+ path.dirname.ascend.find do |parent|
145
+ next if parent.to_s == "."
146
+
147
+ parent_stat = @repo.workspace.stat_file(parent)
148
+ next unless parent_stat&.file?
149
+
150
+ @inspector.trackable_file?(parent, parent_stat)
151
+ end
152
+ end
153
+
154
+ def collect_errors
155
+ @conflicts.each do |type, paths|
156
+ next if paths.empty?
157
+
158
+ lines = paths.map { |name| "\t#{ name }" }
159
+ header, footer = MESSAGES.fetch(type)
160
+
161
+ @errors.push([header, *lines, footer].join("\n"))
162
+ end
163
+
164
+ raise Conflict unless @errors.empty?
165
+ end
166
+
167
+ end
168
+ end
@@ -0,0 +1,60 @@
1
+ class Repository
2
+ class PendingCommit
3
+
4
+ Error = Class.new(StandardError)
5
+
6
+ HEAD_FILES = {
7
+ :merge => "MERGE_HEAD",
8
+ :cherry_pick => "CHERRY_PICK_HEAD",
9
+ :revert => "REVERT_HEAD"
10
+ }
11
+
12
+ attr_reader :message_path
13
+
14
+ def initialize(pathname)
15
+ @pathname = pathname
16
+ @message_path = pathname.join("MERGE_MSG")
17
+ end
18
+
19
+ def start(oid, type = :merge)
20
+ path = @pathname.join(HEAD_FILES.fetch(type))
21
+ flags = File::WRONLY | File::CREAT | File::EXCL
22
+ File.open(path, flags) { |f| f.puts(oid) }
23
+ end
24
+
25
+ def in_progress?
26
+ not merge_type.nil?
27
+ end
28
+
29
+ def merge_type
30
+ HEAD_FILES.each do |type, name|
31
+ path = @pathname.join(name)
32
+ return type if File.file?(path)
33
+ end
34
+
35
+ nil
36
+ end
37
+
38
+ def merge_oid(type = :merge)
39
+ head_path = @pathname.join(HEAD_FILES.fetch(type))
40
+ File.read(head_path).strip
41
+ rescue Errno::ENOENT
42
+ name = head_path.basename
43
+ raise Error, "There is no merge in progress (#{ name } missing)."
44
+ end
45
+
46
+ def merge_message
47
+ File.read(@message_path)
48
+ end
49
+
50
+ def clear(type = :merge)
51
+ head_path = @pathname.join(HEAD_FILES.fetch(type))
52
+ File.unlink(head_path)
53
+ File.unlink(@message_path)
54
+ rescue Errno::ENOENT
55
+ name = head_path.basename
56
+ raise Error, "There is no merge to abort (#{ name } missing)."
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,118 @@
1
+ require "fileutils"
2
+
3
+ require_relative "../config"
4
+ require_relative "../lockfile"
5
+
6
+ class Repository
7
+ class Sequencer
8
+
9
+ UNSAFE_MESSAGE = "You seem to have moved HEAD. Not rewinding, check your HEAD!"
10
+
11
+ def initialize(repository)
12
+ @repo = repository
13
+ @pathname = repository.git_path.join("sequencer")
14
+ @abort_path = @pathname.join("abort-safety")
15
+ @head_path = @pathname.join("head")
16
+ @todo_path = @pathname.join("todo")
17
+ @config = Config.new(@pathname.join("opts"))
18
+ @todo_file = nil
19
+ @commands = []
20
+ end
21
+
22
+ def start(options)
23
+ Dir.mkdir(@pathname)
24
+
25
+ head_oid = @repo.refs.read_head
26
+ write_file(@head_path, head_oid)
27
+ write_file(@abort_path, head_oid)
28
+
29
+ @config.open_for_update
30
+ options.each { |key, value| @config.set(["options", key], value) }
31
+ @config.save
32
+
33
+ open_todo_file
34
+ end
35
+
36
+ def get_option(name)
37
+ @config.open
38
+ @config.get(["options", name])
39
+ end
40
+
41
+ def pick(commit)
42
+ @commands.push([:pick, commit])
43
+ end
44
+
45
+ def revert(commit)
46
+ @commands.push([:revert, commit])
47
+ end
48
+
49
+ def next_command
50
+ @commands.first
51
+ end
52
+
53
+ def drop_command
54
+ @commands.shift
55
+ write_file(@abort_path, @repo.refs.read_head)
56
+ end
57
+
58
+ def load
59
+ open_todo_file
60
+ return unless File.file?(@todo_path)
61
+
62
+ @commands = File.read(@todo_path).lines.map do |line|
63
+ action, oid, _ = /^(\S+) (\S+) (.*)$/.match(line).captures
64
+
65
+ oids = @repo.database.prefix_match(oid)
66
+ commit = @repo.database.load(oids.first)
67
+ [action.to_sym, commit]
68
+ end
69
+ end
70
+
71
+ def dump
72
+ return unless @todo_file
73
+
74
+ @commands.each do |action, commit|
75
+ short = @repo.database.short_oid(commit.oid)
76
+ @todo_file.write("#{ action } #{ short } #{ commit.title_line }")
77
+ end
78
+
79
+ @todo_file.commit
80
+ end
81
+
82
+ def abort
83
+ head_oid = File.read(@head_path).strip
84
+ expected = File.read(@abort_path).strip
85
+ actual = @repo.refs.read_head
86
+
87
+ quit
88
+
89
+ raise UNSAFE_MESSAGE unless actual == expected
90
+
91
+ @repo.hard_reset(head_oid)
92
+ orig_head = @repo.refs.update_head(head_oid)
93
+ @repo.refs.update_ref(Refs::ORIG_HEAD, orig_head)
94
+ end
95
+
96
+ def quit
97
+ FileUtils.rm_rf(@pathname)
98
+ end
99
+
100
+ private
101
+
102
+ def write_file(path, content)
103
+ lockfile = Lockfile.new(path)
104
+ lockfile.hold_for_update
105
+ lockfile.write(content)
106
+ lockfile.write("\n")
107
+ lockfile.commit
108
+ end
109
+
110
+ def open_todo_file
111
+ return unless File.directory?(@pathname)
112
+
113
+ @todo_file = Lockfile.new(@todo_path)
114
+ @todo_file.hold_for_update
115
+ end
116
+
117
+ end
118
+ end
@@ -0,0 +1,98 @@
1
+ require "set"
2
+
3
+ require_relative "./inspector"
4
+ require_relative "../sorted_hash"
5
+
6
+ class Repository
7
+ class Status
8
+
9
+ attr_reader :changed,
10
+ :stats,
11
+ :head_tree,
12
+ :index_changes,
13
+ :conflicts,
14
+ :workspace_changes,
15
+ :untracked_files
16
+
17
+ def initialize(repository, commit_oid = nil)
18
+ @repo = repository
19
+ @stats = {}
20
+
21
+ @inspector = Inspector.new(@repo)
22
+
23
+ @changed = SortedSet.new
24
+ @index_changes = SortedHash.new
25
+ @conflicts = SortedHash.new
26
+ @workspace_changes = SortedHash.new
27
+ @untracked_files = SortedSet.new
28
+
29
+ commit_oid ||= @repo.refs.read_head
30
+ @head_tree = @repo.database.load_tree_list(commit_oid)
31
+
32
+ scan_workspace
33
+ check_index_entries
34
+ collect_deleted_head_files
35
+ end
36
+
37
+ private
38
+
39
+ def record_change(path, set, type)
40
+ @changed.add(path)
41
+ set[path] = type
42
+ end
43
+
44
+ def scan_workspace(prefix = nil)
45
+ @repo.workspace.list_dir(prefix).each do |path, stat|
46
+ if @repo.index.tracked?(path)
47
+ @stats[path] = stat if stat.file?
48
+ scan_workspace(path) if stat.directory?
49
+ elsif @inspector.trackable_file?(path, stat)
50
+ path += File::SEPARATOR if stat.directory?
51
+ @untracked_files.add(path)
52
+ end
53
+ end
54
+ end
55
+
56
+ def check_index_entries
57
+ @repo.index.each_entry do |entry|
58
+ if entry.stage == 0
59
+ check_index_against_workspace(entry)
60
+ check_index_against_head_tree(entry)
61
+ else
62
+ @changed.add(entry.path)
63
+ @conflicts[entry.path] ||= []
64
+ @conflicts[entry.path].push(entry.stage)
65
+ end
66
+ end
67
+ end
68
+
69
+ def check_index_against_workspace(entry)
70
+ stat = @stats[entry.path]
71
+ status = @inspector.compare_index_to_workspace(entry, stat)
72
+
73
+ if status
74
+ record_change(entry.path, @workspace_changes, status)
75
+ else
76
+ @repo.index.update_entry_stat(entry, stat)
77
+ end
78
+ end
79
+
80
+ def check_index_against_head_tree(entry)
81
+ item = @head_tree[entry.path]
82
+ status = @inspector.compare_tree_to_index(item, entry)
83
+
84
+ if status
85
+ record_change(entry.path, @index_changes, status)
86
+ end
87
+ end
88
+
89
+ def collect_deleted_head_files
90
+ @head_tree.each_key do |path|
91
+ unless @repo.index.tracked_file?(path)
92
+ record_change(path, @index_changes, :deleted)
93
+ end
94
+ end
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,244 @@
1
+ require "pathname"
2
+ require "set"
3
+
4
+ require_relative "./revision"
5
+
6
+ class RevList
7
+ include Enumerable
8
+
9
+ RANGE = /^(.*)\.\.(.*)$/
10
+ EXCLUDE = /^\^(.+)$/
11
+
12
+ def initialize(repo, revs, options = {})
13
+ @repo = repo
14
+ @commits = {}
15
+ @flags = Hash.new { |hash, oid| hash[oid] = Set.new }
16
+ @queue = []
17
+ @limited = false
18
+ @prune = []
19
+ @diffs = {}
20
+ @output = []
21
+ @pending = []
22
+ @paths = {}
23
+
24
+ @objects = options.fetch(:objects, false)
25
+ @missing = options.fetch(:missing, false)
26
+ @walk = options.fetch(:walk, true)
27
+
28
+ include_refs(repo.refs.list_all_refs) if options[:all]
29
+ include_refs(repo.refs.list_branches) if options[:branches]
30
+ include_refs(repo.refs.list_remotes) if options[:remotes]
31
+
32
+ revs.each { |rev| handle_revision(rev) }
33
+ handle_revision(Revision::HEAD) if @queue.empty?
34
+ end
35
+
36
+ def each
37
+ limit_list if @limited
38
+ mark_edges_uninteresting if @objects
39
+ traverse_commits { |commit| yield commit }
40
+ traverse_pending { |object| yield object, @paths[object.oid] }
41
+ end
42
+
43
+ def tree_diff(old_oid, new_oid)
44
+ key = [old_oid, new_oid]
45
+ @diffs[key] ||= @repo.database.tree_diff(old_oid, new_oid, @prune)
46
+ end
47
+
48
+ private
49
+
50
+ def include_refs(refs)
51
+ oids = refs.map(&:read_oid).compact
52
+ oids.each { |oid| handle_revision(oid) }
53
+ end
54
+
55
+ def handle_revision(rev)
56
+ if @repo.workspace.stat_file(rev)
57
+ @prune.push(Pathname.new(rev))
58
+ elsif match = RANGE.match(rev)
59
+ set_start_point(match[1], false)
60
+ set_start_point(match[2], true)
61
+ @walk = true
62
+ elsif match = EXCLUDE.match(rev)
63
+ set_start_point(match[1], false)
64
+ @walk = true
65
+ else
66
+ set_start_point(rev, true)
67
+ end
68
+ end
69
+
70
+ def set_start_point(rev, interesting)
71
+ rev = Revision::HEAD if rev == ""
72
+ oid = Revision.new(@repo, rev).resolve(Revision::COMMIT)
73
+
74
+ commit = load_commit(oid)
75
+ enqueue_commit(commit)
76
+
77
+ unless interesting
78
+ @limited = true
79
+ mark(oid, :uninteresting)
80
+ mark_parents_uninteresting(commit)
81
+ end
82
+
83
+ rescue Revision::InvalidObject => error
84
+ raise error unless @missing
85
+ end
86
+
87
+ def enqueue_commit(commit)
88
+ return unless mark(commit.oid, :seen)
89
+
90
+ if @walk
91
+ index = @queue.find_index { |c| c.date < commit.date }
92
+ @queue.insert(index || @queue.size, commit)
93
+ else
94
+ @queue.push(commit)
95
+ end
96
+ end
97
+
98
+ def limit_list
99
+ while still_interesting?
100
+ commit = @queue.shift
101
+ add_parents(commit)
102
+
103
+ unless marked?(commit.oid, :uninteresting)
104
+ @output.push(commit)
105
+ end
106
+ end
107
+
108
+ @queue = @output
109
+ end
110
+
111
+ def still_interesting?
112
+ return false if @queue.empty?
113
+
114
+ oldest_out = @output.last
115
+ newest_in = @queue.first
116
+
117
+ return true if oldest_out and oldest_out.date <= newest_in.date
118
+
119
+ if @queue.any? { |commit| not marked?(commit.oid, :uninteresting) }
120
+ return true
121
+ end
122
+
123
+ false
124
+ end
125
+
126
+ def add_parents(commit)
127
+ return unless @walk and mark(commit.oid, :added)
128
+
129
+ if marked?(commit.oid, :uninteresting)
130
+ parents = commit.parents.map { |oid| load_commit(oid) }
131
+ parents.each { |parent| mark_parents_uninteresting(parent) }
132
+ else
133
+ parents = simplify_commit(commit).map { |oid| load_commit(oid) }
134
+ end
135
+
136
+ parents.each { |parent| enqueue_commit(parent) }
137
+ end
138
+
139
+ def mark_parents_uninteresting(commit)
140
+ queue = commit.parents.clone
141
+
142
+ until queue.empty?
143
+ oid = queue.shift
144
+
145
+ while oid
146
+ break unless mark(oid, :uninteresting)
147
+
148
+ parent = @commits[oid]
149
+ break unless parent
150
+
151
+ oid = parent.parents.first
152
+ queue.concat(parent.parents.drop(1))
153
+ end
154
+ end
155
+ end
156
+
157
+ def mark_edges_uninteresting
158
+ @queue.each do |commit|
159
+ if marked?(commit.oid, :uninteresting)
160
+ mark_tree_uninteresting(commit.tree)
161
+ end
162
+
163
+ commit.parents.each do |oid|
164
+ next unless marked?(oid, :uninteresting)
165
+
166
+ parent = load_commit(oid)
167
+ mark_tree_uninteresting(parent.tree)
168
+ end
169
+ end
170
+ end
171
+
172
+ def mark_tree_uninteresting(tree_oid)
173
+ entry = @repo.database.tree_entry(tree_oid)
174
+ traverse_tree(entry) { |object| mark(object.oid, :uninteresting) }
175
+ end
176
+
177
+ def simplify_commit(commit)
178
+ return commit.parents if @prune.empty?
179
+
180
+ parents = commit.parents
181
+ parents = [nil] if parents.empty?
182
+
183
+ parents.each do |oid|
184
+ next unless tree_diff(oid, commit.oid).empty?
185
+ mark(commit.oid, :treesame)
186
+ return [*oid]
187
+ end
188
+
189
+ commit.parents
190
+ end
191
+
192
+ def traverse_commits
193
+ until @queue.empty?
194
+ commit = @queue.shift
195
+ add_parents(commit) unless @limited
196
+
197
+ next if marked?(commit.oid, :uninteresting)
198
+ next if marked?(commit.oid, :treesame)
199
+
200
+ @pending.push(@repo.database.tree_entry(commit.tree))
201
+ yield commit
202
+ end
203
+ end
204
+
205
+ def traverse_pending
206
+ return unless @objects
207
+
208
+ @pending.each do |entry|
209
+ traverse_tree(entry) do |object|
210
+ next if marked?(object.oid, :uninteresting)
211
+ next unless mark(object.oid, :seen)
212
+
213
+ yield object
214
+ true
215
+ end
216
+ end
217
+ end
218
+
219
+ def traverse_tree(entry, path = Pathname.new(""))
220
+ @paths[entry.oid] ||= path
221
+
222
+ return unless yield entry
223
+ return unless entry.tree?
224
+
225
+ tree = @repo.database.load(entry.oid)
226
+
227
+ tree.each_entry do |name, item|
228
+ traverse_tree(item, path.join(name)) { |object| yield object }
229
+ end
230
+ end
231
+
232
+ def load_commit(oid)
233
+ return nil unless oid
234
+ @commits[oid] ||= @repo.database.load(oid)
235
+ end
236
+
237
+ def mark(oid, flag)
238
+ @flags[oid].add?(flag)
239
+ end
240
+
241
+ def marked?(oid, flag)
242
+ @flags[oid].include?(flag)
243
+ end
244
+ end