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