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,105 @@
1
+ require "pathname"
2
+
3
+ require_relative "./base"
4
+ require_relative "../repository/inspector"
5
+
6
+ module Command
7
+ class Rm < Base
8
+
9
+ BOTH_CHANGED = "staged content different from both the file and the HEAD"
10
+ INDEX_CHANGED = "changes staged in the index"
11
+ WORKSPACE_CHANGED = "local modifications"
12
+
13
+ def define_options
14
+ @parser.on("--cached") { @options[:cached] = true }
15
+ @parser.on("-f", "--force") { @options[:force] = true }
16
+ @parser.on("-r") { @options[:recursive] = true }
17
+ end
18
+
19
+ def run
20
+ repo.index.load_for_update
21
+
22
+ @head_oid = repo.refs.read_head
23
+ @inspector = Repository::Inspector.new(repo)
24
+ @uncommitted = []
25
+ @unstaged = []
26
+ @both_changed = []
27
+
28
+ @args = @args.flat_map { |path| expand_path(path) }
29
+ .map { |path| Pathname.new(path) }
30
+
31
+ @args.each { |path| plan_removal(path) }
32
+ exit_on_errors
33
+
34
+ @args.each { |path| remove_file(path) }
35
+ repo.index.write_updates
36
+
37
+ exit 0
38
+
39
+ rescue => error
40
+ repo.index.release_lock
41
+ @stderr.puts "fatal: #{ error.message }"
42
+ exit 128
43
+ end
44
+
45
+ private
46
+
47
+ def expand_path(path)
48
+ if repo.index.tracked_directory?(path)
49
+ return repo.index.child_paths(path) if @options[:recursive]
50
+ raise "not removing '#{ path }' recursively without -r"
51
+ end
52
+
53
+ return [path] if repo.index.tracked_file?(path)
54
+ raise "pathspec '#{ path }' did not match any files"
55
+ end
56
+
57
+ def plan_removal(path)
58
+ return if @options[:force]
59
+
60
+ stat = repo.workspace.stat_file(path)
61
+ raise "jit rm: '#{ path }': Operation not permitted" if stat&.directory?
62
+
63
+ item = repo.database.load_tree_entry(@head_oid, path)
64
+ entry = repo.index.entry_for_path(path)
65
+
66
+ staged_change = @inspector.compare_tree_to_index(item, entry)
67
+ unstaged_change = @inspector.compare_index_to_workspace(entry, stat) if stat
68
+
69
+ if staged_change and unstaged_change
70
+ @both_changed.push(path)
71
+ elsif staged_change
72
+ @uncommitted.push(path) unless @options[:cached]
73
+ elsif unstaged_change
74
+ @unstaged.push(path) unless @options[:cached]
75
+ end
76
+ end
77
+
78
+ def remove_file(path)
79
+ repo.index.remove(path)
80
+ repo.workspace.remove(path) unless @options[:cached]
81
+ puts "rm '#{ path }'"
82
+ end
83
+
84
+ def exit_on_errors
85
+ return if [@both_changed, @uncommitted, @unstaged].all?(&:empty?)
86
+
87
+ print_errors(@both_changed, BOTH_CHANGED)
88
+ print_errors(@uncommitted, INDEX_CHANGED)
89
+ print_errors(@unstaged, WORKSPACE_CHANGED)
90
+
91
+ repo.index.release_lock
92
+ exit 1
93
+ end
94
+
95
+ def print_errors(paths, message)
96
+ return if paths.empty?
97
+
98
+ files_have = (paths.size == 1) ? "file has" : "files have"
99
+
100
+ @stderr.puts "error: the following #{ files_have } #{ message }:"
101
+ paths.each { |path| @stderr.puts " #{ path }" }
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "../../merge/common_ancestors"
2
+
3
+ module Command
4
+ module FastForward
5
+
6
+ def fast_forward_error(old_oid, new_oid)
7
+ return nil unless old_oid and new_oid
8
+ return "fetch first" unless repo.database.has?(old_oid)
9
+ return "non-fast-forward" unless fast_forward?(old_oid, new_oid)
10
+ end
11
+
12
+ def fast_forward?(old_oid, new_oid)
13
+ common = ::Merge::CommonAncestors.new(repo.database, old_oid, [new_oid])
14
+ common.find
15
+ common.marked?(old_oid, :parent2)
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,116 @@
1
+ require "pathname"
2
+ require_relative "../../diff"
3
+
4
+ module Command
5
+ module PrintDiff
6
+
7
+ DIFF_FORMATS = {
8
+ :context => :normal,
9
+ :meta => :bold,
10
+ :frag => :cyan,
11
+ :old => :red,
12
+ :new => :green
13
+ }
14
+
15
+ NULL_OID = "0" * 40
16
+ NULL_PATH = "/dev/null"
17
+
18
+ Target = Struct.new(:path, :oid, :mode, :data) do
19
+ def diff_path
20
+ mode ? path : NULL_PATH
21
+ end
22
+ end
23
+
24
+ def define_print_diff_options
25
+ @parser.on("-p", "-u", "--patch") { @options[:patch] = true }
26
+ @parser.on("-s", "--no-patch") { @options[:patch] = false }
27
+ end
28
+
29
+ private
30
+
31
+ def diff_fmt(name, text)
32
+ key = ["color", "diff", name]
33
+ style = repo.config.get(key)&.split(/ +/) || DIFF_FORMATS.fetch(name)
34
+
35
+ fmt(style, text)
36
+ end
37
+
38
+ def header(string)
39
+ puts diff_fmt(:meta, string)
40
+ end
41
+
42
+ def short(oid)
43
+ repo.database.short_oid(oid)
44
+ end
45
+
46
+ def print_diff(a, b)
47
+ return if a.oid == b.oid and a.mode == b.mode
48
+
49
+ a.path = Pathname.new("a").join(a.path)
50
+ b.path = Pathname.new("b").join(b.path)
51
+
52
+ header("diff --git #{ a.path } #{ b.path }")
53
+ print_diff_mode(a, b)
54
+ print_diff_content(a, b)
55
+ end
56
+
57
+ def print_diff_mode(a, b)
58
+ if a.mode == nil
59
+ header("new file mode #{ b.mode }")
60
+ elsif b.mode == nil
61
+ header("deleted file mode #{ a.mode }")
62
+ elsif a.mode != b.mode
63
+ header("old mode #{ a.mode }")
64
+ header("new mode #{ b.mode }")
65
+ end
66
+ end
67
+
68
+ def print_diff_content(a, b)
69
+ return if a.oid == b.oid
70
+
71
+ oid_range = "index #{ short a.oid }..#{ short b.oid }"
72
+ oid_range.concat(" #{ a.mode }") if a.mode == b.mode
73
+
74
+ header(oid_range)
75
+ header("--- #{ a.diff_path }")
76
+ header("+++ #{ b.diff_path }")
77
+
78
+ hunks = ::Diff.diff_hunks(a.data, b.data)
79
+ hunks.each { |hunk| print_diff_hunk(hunk) }
80
+ end
81
+
82
+ def print_combined_diff(as, b)
83
+ header("diff --cc #{ b.path }")
84
+
85
+ a_oids = as.map { |a| short a.oid }
86
+ oid_range = "index #{ a_oids.join(",") }..#{ short b.oid }"
87
+ header(oid_range)
88
+
89
+ unless as.all? { |a| a.mode == b.mode }
90
+ header("mode #{ as.map(&:mode).join(",") }..#{ b.mode }")
91
+ end
92
+
93
+ header("--- a/#{ b.diff_path }")
94
+ header("+++ b/#{ b.diff_path }")
95
+
96
+ hunks = ::Diff.combined_hunks(as.map(&:data), b.data)
97
+ hunks.each { |hunk| print_diff_hunk(hunk) }
98
+ end
99
+
100
+ def print_diff_hunk(hunk)
101
+ puts diff_fmt(:frag, hunk.header)
102
+ hunk.edits.each { |edit| print_diff_edit(edit) }
103
+ end
104
+
105
+ def print_diff_edit(edit)
106
+ text = edit.to_s.rstrip
107
+
108
+ case edit.type
109
+ when :eql then puts diff_fmt(:context, text)
110
+ when :ins then puts diff_fmt(:new, text)
111
+ when :del then puts diff_fmt(:old, text)
112
+ end
113
+ end
114
+
115
+ end
116
+ end
@@ -0,0 +1,37 @@
1
+ require_relative "../../pack"
2
+ require_relative "../../progress"
3
+
4
+ module Command
5
+ module ReceiveObjects
6
+
7
+ UNPACK_LIMIT = 100
8
+
9
+ def recv_packed_objects(unpack_limit = nil, prefix = "")
10
+ stream = Pack::Stream.new(@conn.input, prefix)
11
+ reader = Pack::Reader.new(stream)
12
+ progress = Progress.new(@stderr) unless @conn.input == STDIN
13
+
14
+ reader.read_header
15
+
16
+ factory = select_processor_class(reader, unpack_limit)
17
+ processor = factory.new(repo.database, reader, stream, progress)
18
+
19
+ processor.process_pack
20
+ end
21
+
22
+ def select_processor_class(reader, unpack_limit)
23
+ unpack_limit ||= transfer_unpack_limit
24
+
25
+ if unpack_limit and reader.count > unpack_limit
26
+ Pack::Indexer
27
+ else
28
+ Pack::Unpacker
29
+ end
30
+ end
31
+
32
+ def transfer_unpack_limit
33
+ repo.config.get(["transfer", "unpackLimit"]) || UNPACK_LIMIT
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,44 @@
1
+ require_relative "../../repository"
2
+ require_relative "../../remotes/protocol"
3
+
4
+ module Command
5
+ module RemoteAgent
6
+
7
+ ZERO_OID = "0" * 40
8
+
9
+ def accept_client(name, capabilities = [])
10
+ @conn = Remotes::Protocol.new(name, @stdin, @stdout, capabilities)
11
+ end
12
+
13
+ def repo
14
+ @repo ||= Repository.new(detect_git_dir)
15
+ end
16
+
17
+ def detect_git_dir
18
+ pathname = expanded_pathname(@args[0])
19
+ dirs = pathname.ascend.flat_map { |dir| [dir, dir.join(".git")] }
20
+ dirs.find { |dir| git_repository?(dir) }
21
+ end
22
+
23
+ def git_repository?(dirname)
24
+ File.file?(dirname.join("HEAD")) and
25
+ File.directory?(dirname.join("objects")) and
26
+ File.directory?(dirname.join("refs"))
27
+ end
28
+
29
+ def send_references
30
+ refs = repo.refs.list_all_refs
31
+ sent = false
32
+
33
+ refs.sort_by(&:path).each do |symref|
34
+ next unless oid = symref.read_oid
35
+ @conn.send_packet("#{ oid.downcase } #{ symref.path }")
36
+ sent = true
37
+ end
38
+
39
+ @conn.send_packet("#{ ZERO_OID } capabilities^{}") unless sent
40
+ @conn.send_packet(nil)
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,82 @@
1
+ require "open3"
2
+ require "shellwords"
3
+ require "uri"
4
+
5
+ require_relative "../../remotes/protocol"
6
+
7
+ module Command
8
+ module RemoteClient
9
+
10
+ REF_LINE = /^([0-9a-f]+) (.*)$/
11
+ ZERO_OID = "0" * 40
12
+
13
+ def start_agent(name, program, url, capabilities = [])
14
+ argv = build_agent_command(program, url)
15
+ input, output, _ = Open3.popen2(Shellwords.shelljoin(argv))
16
+ @conn = Remotes::Protocol.new(name, output, input, capabilities)
17
+ end
18
+
19
+ def build_agent_command(program, url)
20
+ uri = URI.parse(url)
21
+ argv = Shellwords.shellsplit(program) + [uri.path]
22
+
23
+ case uri.scheme
24
+ when "file" then argv
25
+ when "ssh" then ssh_command(uri, argv)
26
+ end
27
+ end
28
+
29
+ def ssh_command(uri, argv)
30
+ ssh = ["ssh", uri.host]
31
+ ssh += ["-p", uri.port.to_s] if uri.port
32
+ ssh += ["-l", uri.user] if uri.user
33
+
34
+ ssh + [Shellwords.shelljoin(argv)]
35
+ end
36
+
37
+ def recv_references
38
+ @remote_refs = {}
39
+
40
+ @conn.recv_until(nil) do |line|
41
+ oid, ref = REF_LINE.match(line).captures
42
+ @remote_refs[ref] = oid.downcase unless oid == ZERO_OID
43
+ end
44
+ end
45
+
46
+ def report_ref_update(ref_names, error, old_oid = nil, new_oid = nil, is_ff = false)
47
+ return show_ref_update("!", "[rejected]", ref_names, error) if error
48
+ return if old_oid == new_oid
49
+
50
+ if old_oid == nil
51
+ show_ref_update("*", "[new branch]", ref_names)
52
+ elsif new_oid == nil
53
+ show_ref_update("-", "[deleted]", ref_names)
54
+ else
55
+ report_range_update(ref_names, old_oid, new_oid, is_ff)
56
+ end
57
+ end
58
+
59
+ def report_range_update(ref_names, old_oid, new_oid, is_ff)
60
+ old_oid = repo.database.short_oid(old_oid)
61
+ new_oid = repo.database.short_oid(new_oid)
62
+
63
+ if is_ff
64
+ revisions = "#{ old_oid }..#{ new_oid }"
65
+ show_ref_update(" ", revisions, ref_names)
66
+ else
67
+ revisions = "#{ old_oid }...#{ new_oid }"
68
+ show_ref_update("+", revisions, ref_names, "forced update")
69
+ end
70
+ end
71
+
72
+ def show_ref_update(flag, summary, ref_names, reason = nil)
73
+ names = ref_names.compact.map { |name| repo.refs.short_name(name) }
74
+
75
+ message = " #{ flag } #{ summary } #{ names.join(" -> ") }"
76
+ message.concat(" (#{ reason })") if reason
77
+
78
+ @stderr.puts message
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,24 @@
1
+ require_relative "../../pack"
2
+ require_relative "../../progress"
3
+ require_relative "../../rev_list"
4
+
5
+ module Command
6
+ module SendObjects
7
+
8
+ def send_packed_objects(revs)
9
+ rev_opts = { :objects => true, :missing => true }
10
+ rev_list = ::RevList.new(repo, revs, rev_opts)
11
+
12
+ pack_compression = repo.config.get(["pack", "compression"]) ||
13
+ repo.config.get(["core", "compression"])
14
+
15
+ writer = Pack::Writer.new(@conn.output, repo.database,
16
+ :compression => pack_compression,
17
+ :allow_ofs => @conn.capable?("ofs-delta"),
18
+ :progress => Progress.new(@stderr))
19
+
20
+ writer.write_objects(rev_list)
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,146 @@
1
+ require_relative "../../merge/resolve"
2
+ require_relative "../../repository/sequencer"
3
+
4
+ module Command
5
+ module Sequencing
6
+
7
+ CONFLICT_NOTES = <<~MSG
8
+ after resolving the conflicts, mark the corrected paths
9
+ with 'jit add <paths>' or 'jit rm <paths>'
10
+ and commit the result with 'jit commit'
11
+ MSG
12
+
13
+ def define_options
14
+ @options[:mode] = :run
15
+
16
+ @parser.on("--continue") { @options[:mode] = :continue }
17
+ @parser.on("--abort") { @options[:mode] = :abort }
18
+ @parser.on("--quit" ) { @options[:mode] = :quit }
19
+
20
+ @parser.on "-m <parent>", "--mainline=<parent>", Integer do |parent|
21
+ @options[:mainline] = parent
22
+ end
23
+ end
24
+
25
+ def run
26
+ case @options[:mode]
27
+ when :continue then handle_continue
28
+ when :abort then handle_abort
29
+ when :quit then handle_quit
30
+ end
31
+
32
+ sequencer.start(@options)
33
+ store_commit_sequence
34
+ resume_sequencer
35
+ end
36
+
37
+ private
38
+
39
+ def sequencer
40
+ @sequencer ||= Repository::Sequencer.new(repo)
41
+ end
42
+
43
+ def resolve_merge(inputs)
44
+ repo.index.load_for_update
45
+ ::Merge::Resolve.new(repo, inputs).execute
46
+ repo.index.write_updates
47
+ end
48
+
49
+ def fail_on_conflict(inputs, message)
50
+ sequencer.dump
51
+ pending_commit.start(inputs.right_oid, merge_type)
52
+
53
+ edit_file(pending_commit.message_path) do |editor|
54
+ editor.puts(message)
55
+ editor.puts("")
56
+ editor.note("Conflicts:")
57
+ repo.index.conflict_paths.each { |name| editor.note("\t#{ name }") }
58
+ editor.close
59
+ end
60
+
61
+ @stderr.puts "error: could not apply #{ inputs.right_name }"
62
+ CONFLICT_NOTES.each_line { |line| @stderr.puts "hint: #{ line }" }
63
+ exit 1
64
+ end
65
+
66
+ def finish_commit(commit)
67
+ repo.database.store(commit)
68
+ repo.refs.update_head(commit.oid)
69
+ print_commit(commit)
70
+ end
71
+
72
+ def handle_continue
73
+ repo.index.load
74
+
75
+ case pending_commit.merge_type
76
+ when :cherry_pick then write_cherry_pick_commit
77
+ when :revert then write_revert_commit
78
+ end
79
+
80
+ sequencer.load
81
+ sequencer.drop_command
82
+ resume_sequencer
83
+
84
+ rescue Repository::PendingCommit::Error => error
85
+ @stderr.puts "fatal: #{ error.message }"
86
+ exit 128
87
+ end
88
+
89
+ def resume_sequencer
90
+ loop do
91
+ action, commit = sequencer.next_command
92
+ break unless commit
93
+
94
+ case action
95
+ when :pick then pick(commit)
96
+ when :revert then revert(commit)
97
+ end
98
+ sequencer.drop_command
99
+ end
100
+
101
+ sequencer.quit
102
+ exit 0
103
+ end
104
+
105
+ def select_parent(commit)
106
+ mainline = sequencer.get_option("mainline")
107
+
108
+ if commit.merge?
109
+ return commit.parents[mainline - 1] if mainline
110
+
111
+ @stderr.puts <<~ERROR
112
+ error: commit #{ commit.oid } is a merge but no -m option was given
113
+ ERROR
114
+ exit 1
115
+ else
116
+ return commit.parent unless mainline
117
+
118
+ @stderr.puts <<~ERROR
119
+ error: mainline was specified but commit #{ commit.oid } is not a merge
120
+ ERROR
121
+ exit 1
122
+ end
123
+ end
124
+
125
+ def handle_abort
126
+ pending_commit.clear(merge_type) if pending_commit.in_progress?
127
+ repo.index.load_for_update
128
+
129
+ begin
130
+ sequencer.abort
131
+ rescue => error
132
+ @stderr.puts "warning: #{ error.message }"
133
+ end
134
+
135
+ repo.index.write_updates
136
+ exit 0
137
+ end
138
+
139
+ def handle_quit
140
+ pending_commit.clear(merge_type) if pending_commit.in_progress?
141
+ sequencer.quit
142
+ exit 0
143
+ end
144
+
145
+ end
146
+ end