jit 0.0.0 → 1.0.0

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 (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