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.
- checksums.yaml +5 -5
- data/LICENSE.txt +674 -0
- data/bin/jit +21 -0
- data/lib/color.rb +32 -0
- data/lib/command.rb +62 -0
- data/lib/command/add.rb +65 -0
- data/lib/command/base.rb +92 -0
- data/lib/command/branch.rb +199 -0
- data/lib/command/checkout.rb +104 -0
- data/lib/command/cherry_pick.rb +51 -0
- data/lib/command/commit.rb +86 -0
- data/lib/command/config.rb +126 -0
- data/lib/command/diff.rb +114 -0
- data/lib/command/fetch.rb +116 -0
- data/lib/command/init.rb +41 -0
- data/lib/command/log.rb +188 -0
- data/lib/command/merge.rb +148 -0
- data/lib/command/push.rb +172 -0
- data/lib/command/receive_pack.rb +92 -0
- data/lib/command/remote.rb +55 -0
- data/lib/command/reset.rb +64 -0
- data/lib/command/rev_list.rb +33 -0
- data/lib/command/revert.rb +69 -0
- data/lib/command/rm.rb +105 -0
- data/lib/command/shared/fast_forward.rb +19 -0
- data/lib/command/shared/print_diff.rb +116 -0
- data/lib/command/shared/receive_objects.rb +37 -0
- data/lib/command/shared/remote_agent.rb +44 -0
- data/lib/command/shared/remote_client.rb +82 -0
- data/lib/command/shared/send_objects.rb +24 -0
- data/lib/command/shared/sequencing.rb +146 -0
- data/lib/command/shared/write_commit.rb +167 -0
- data/lib/command/status.rb +210 -0
- data/lib/command/upload_pack.rb +54 -0
- data/lib/config.rb +240 -0
- data/lib/config/stack.rb +42 -0
- data/lib/database.rb +112 -0
- data/lib/database/author.rb +27 -0
- data/lib/database/backends.rb +57 -0
- data/lib/database/blob.rb +24 -0
- data/lib/database/commit.rb +70 -0
- data/lib/database/entry.rb +7 -0
- data/lib/database/loose.rb +70 -0
- data/lib/database/packed.rb +75 -0
- data/lib/database/tree.rb +77 -0
- data/lib/database/tree_diff.rb +88 -0
- data/lib/diff.rb +46 -0
- data/lib/diff/combined.rb +72 -0
- data/lib/diff/hunk.rb +64 -0
- data/lib/diff/myers.rb +90 -0
- data/lib/editor.rb +59 -0
- data/lib/index.rb +212 -0
- data/lib/index/checksum.rb +44 -0
- data/lib/index/entry.rb +91 -0
- data/lib/lockfile.rb +55 -0
- data/lib/merge/bases.rb +38 -0
- data/lib/merge/common_ancestors.rb +77 -0
- data/lib/merge/diff3.rb +156 -0
- data/lib/merge/inputs.rb +42 -0
- data/lib/merge/resolve.rb +178 -0
- data/lib/pack.rb +45 -0
- data/lib/pack/compressor.rb +83 -0
- data/lib/pack/delta.rb +58 -0
- data/lib/pack/entry.rb +54 -0
- data/lib/pack/expander.rb +54 -0
- data/lib/pack/index.rb +100 -0
- data/lib/pack/indexer.rb +200 -0
- data/lib/pack/numbers.rb +79 -0
- data/lib/pack/reader.rb +98 -0
- data/lib/pack/stream.rb +80 -0
- data/lib/pack/unpacker.rb +62 -0
- data/lib/pack/window.rb +47 -0
- data/lib/pack/writer.rb +92 -0
- data/lib/pack/xdelta.rb +118 -0
- data/lib/pager.rb +24 -0
- data/lib/progress.rb +78 -0
- data/lib/refs.rb +260 -0
- data/lib/remotes.rb +82 -0
- data/lib/remotes/protocol.rb +82 -0
- data/lib/remotes/refspec.rb +70 -0
- data/lib/remotes/remote.rb +57 -0
- data/lib/repository.rb +64 -0
- data/lib/repository/divergence.rb +21 -0
- data/lib/repository/hard_reset.rb +35 -0
- data/lib/repository/inspector.rb +49 -0
- data/lib/repository/migration.rb +168 -0
- data/lib/repository/pending_commit.rb +60 -0
- data/lib/repository/sequencer.rb +118 -0
- data/lib/repository/status.rb +98 -0
- data/lib/rev_list.rb +244 -0
- data/lib/revision.rb +155 -0
- data/lib/sorted_hash.rb +17 -0
- data/lib/temp_file.rb +34 -0
- data/lib/workspace.rb +107 -0
- metadata +103 -9
@@ -0,0 +1,167 @@
|
|
1
|
+
module Command
|
2
|
+
module WriteCommit
|
3
|
+
|
4
|
+
CONFLICT_MESSAGE = <<~MSG
|
5
|
+
hint: Fix them up in the work tree, and then use 'jit add/rm <file>'
|
6
|
+
hint: as appropriate to mark resolution and make a commit.
|
7
|
+
fatal: Exiting because of an unresolved conflict.
|
8
|
+
MSG
|
9
|
+
|
10
|
+
MERGE_NOTES = <<~MSG
|
11
|
+
|
12
|
+
It looks like you may be committing a merge.
|
13
|
+
If this is not correct, please remove the file
|
14
|
+
\t.git/MERGE_HEAD
|
15
|
+
and try again.
|
16
|
+
MSG
|
17
|
+
|
18
|
+
CHERRY_PICK_NOTES = <<~MSG
|
19
|
+
|
20
|
+
It looks like you may be committing a cherry-pick.
|
21
|
+
If this is not correct, please remove the file
|
22
|
+
\t.git/CHERRY_PICK_HEAD
|
23
|
+
and try again.
|
24
|
+
MSG
|
25
|
+
|
26
|
+
def define_write_commit_options
|
27
|
+
@options[:edit] = :auto
|
28
|
+
@parser.on("-e", "--[no-]edit") { |value| @options[:edit] = value }
|
29
|
+
|
30
|
+
@parser.on "-m <message>", "--message=<message>" do |message|
|
31
|
+
@options[:message] = message
|
32
|
+
@options[:edit] = false if @options[:edit] == :auto
|
33
|
+
end
|
34
|
+
|
35
|
+
@parser.on "-F <file>", "--file=<file>" do |file|
|
36
|
+
@options[:file] = expanded_pathname(file)
|
37
|
+
@options[:edit] = false if @options[:edit] == :auto
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def read_message
|
42
|
+
if @options.has_key?(:message)
|
43
|
+
"#{ @options[:message] }\n"
|
44
|
+
elsif @options.has_key?(:file)
|
45
|
+
File.read(@options[:file])
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def write_commit(parents, message)
|
50
|
+
unless message
|
51
|
+
@stderr.puts "Aborting commit due to empty commit message."
|
52
|
+
exit 1
|
53
|
+
end
|
54
|
+
|
55
|
+
tree = write_tree
|
56
|
+
author = current_author
|
57
|
+
commit = Database::Commit.new(parents, tree.oid, author, author, message)
|
58
|
+
|
59
|
+
repo.database.store(commit)
|
60
|
+
repo.refs.update_head(commit.oid)
|
61
|
+
|
62
|
+
commit
|
63
|
+
end
|
64
|
+
|
65
|
+
def write_tree
|
66
|
+
root = Database::Tree.build(repo.index.each_entry)
|
67
|
+
root.traverse { |tree| repo.database.store(tree) }
|
68
|
+
root
|
69
|
+
end
|
70
|
+
|
71
|
+
def current_author
|
72
|
+
config_name = repo.config.get(["user", "name"])
|
73
|
+
config_email = repo.config.get(["user", "email"])
|
74
|
+
|
75
|
+
name = @env.fetch("GIT_AUTHOR_NAME", config_name)
|
76
|
+
email = @env.fetch("GIT_AUTHOR_EMAIL", config_email)
|
77
|
+
|
78
|
+
Database::Author.new(name, email, Time.now)
|
79
|
+
end
|
80
|
+
|
81
|
+
def print_commit(commit)
|
82
|
+
ref = repo.refs.current_ref
|
83
|
+
info = ref.head? ? "detached HEAD" : ref.short_name
|
84
|
+
oid = repo.database.short_oid(commit.oid)
|
85
|
+
|
86
|
+
info.concat(" (root-commit)") unless commit.parent
|
87
|
+
info.concat(" #{ oid }")
|
88
|
+
|
89
|
+
puts "[#{ info }] #{ commit.title_line }"
|
90
|
+
end
|
91
|
+
|
92
|
+
def pending_commit
|
93
|
+
@pending_commit ||= repo.pending_commit
|
94
|
+
end
|
95
|
+
|
96
|
+
def resume_merge(type)
|
97
|
+
case type
|
98
|
+
when :merge then write_merge_commit
|
99
|
+
when :cherry_pick then write_cherry_pick_commit
|
100
|
+
when :revert then write_revert_commit
|
101
|
+
end
|
102
|
+
|
103
|
+
exit 0
|
104
|
+
end
|
105
|
+
|
106
|
+
def write_merge_commit
|
107
|
+
handle_conflicted_index
|
108
|
+
|
109
|
+
parents = [repo.refs.read_head, pending_commit.merge_oid]
|
110
|
+
message = compose_merge_message(MERGE_NOTES)
|
111
|
+
write_commit(parents, message)
|
112
|
+
|
113
|
+
pending_commit.clear(:merge)
|
114
|
+
end
|
115
|
+
|
116
|
+
def write_cherry_pick_commit
|
117
|
+
handle_conflicted_index
|
118
|
+
|
119
|
+
parents = [repo.refs.read_head]
|
120
|
+
message = compose_merge_message(CHERRY_PICK_NOTES)
|
121
|
+
|
122
|
+
pick_oid = pending_commit.merge_oid(:cherry_pick)
|
123
|
+
commit = repo.database.load(pick_oid)
|
124
|
+
|
125
|
+
picked = Database::Commit.new(parents, write_tree.oid,
|
126
|
+
commit.author, current_author,
|
127
|
+
message)
|
128
|
+
|
129
|
+
repo.database.store(picked)
|
130
|
+
repo.refs.update_head(picked.oid)
|
131
|
+
pending_commit.clear(:cherry_pick)
|
132
|
+
end
|
133
|
+
|
134
|
+
def write_revert_commit
|
135
|
+
handle_conflicted_index
|
136
|
+
|
137
|
+
parents = [repo.refs.read_head]
|
138
|
+
message = compose_merge_message
|
139
|
+
write_commit(parents, message)
|
140
|
+
|
141
|
+
pending_commit.clear(:revert)
|
142
|
+
end
|
143
|
+
|
144
|
+
def compose_merge_message(notes = nil)
|
145
|
+
edit_file(commit_message_path) do |editor|
|
146
|
+
editor.puts(pending_commit.merge_message)
|
147
|
+
editor.note(notes) if notes
|
148
|
+
editor.puts("")
|
149
|
+
editor.note(Commit::COMMIT_NOTES)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def commit_message_path
|
154
|
+
repo.git_path.join("COMMIT_EDITMSG")
|
155
|
+
end
|
156
|
+
|
157
|
+
def handle_conflicted_index
|
158
|
+
return unless repo.index.conflict?
|
159
|
+
|
160
|
+
message = "Committing is not possible because you have unmerged files"
|
161
|
+
@stderr.puts "error: #{ message }."
|
162
|
+
@stderr.puts CONFLICT_MESSAGE
|
163
|
+
exit 128
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require_relative "./base"
|
2
|
+
|
3
|
+
module Command
|
4
|
+
class Status < Base
|
5
|
+
|
6
|
+
LABEL_WIDTH = 12
|
7
|
+
|
8
|
+
LONG_STATUS = {
|
9
|
+
:added => "new file:",
|
10
|
+
:deleted => "deleted:",
|
11
|
+
:modified => "modified:"
|
12
|
+
}
|
13
|
+
|
14
|
+
SHORT_STATUS = {
|
15
|
+
:added => "A",
|
16
|
+
:deleted => "D",
|
17
|
+
:modified => "M"
|
18
|
+
}
|
19
|
+
|
20
|
+
CONFLICT_LABEL_WIDTH = 17
|
21
|
+
|
22
|
+
CONFLICT_LONG_STATUS = {
|
23
|
+
[1, 2, 3] => "both modified:",
|
24
|
+
[1, 2] => "deleted by them:",
|
25
|
+
[1, 3] => "deleted by us:",
|
26
|
+
[2, 3] => "both added:",
|
27
|
+
[2] => "added by us:",
|
28
|
+
[3] => "added by them:"
|
29
|
+
}
|
30
|
+
|
31
|
+
CONFLICT_SHORT_STATUS = {
|
32
|
+
[1, 2, 3] => "UU",
|
33
|
+
[1, 2] => "UD",
|
34
|
+
[1, 3] => "DU",
|
35
|
+
[2, 3] => "AA",
|
36
|
+
[2] => "AU",
|
37
|
+
[3] => "UA"
|
38
|
+
}
|
39
|
+
|
40
|
+
UI_LABELS = { :normal => LONG_STATUS, :conflict => CONFLICT_LONG_STATUS }
|
41
|
+
UI_WIDTHS = { :normal => LABEL_WIDTH, :conflict => CONFLICT_LABEL_WIDTH }
|
42
|
+
|
43
|
+
def define_options
|
44
|
+
@options[:format] = "long"
|
45
|
+
@parser.on("--porcelain") { @options[:format] = "porcelain" }
|
46
|
+
end
|
47
|
+
|
48
|
+
def run
|
49
|
+
repo.index.load_for_update
|
50
|
+
@status = repo.status
|
51
|
+
repo.index.write_updates
|
52
|
+
|
53
|
+
print_results
|
54
|
+
exit 0
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def print_results
|
60
|
+
case @options[:format]
|
61
|
+
when "long" then print_long_format
|
62
|
+
when "porcelain" then print_porcelain_format
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def print_long_format
|
67
|
+
print_branch_status
|
68
|
+
print_upstream_status
|
69
|
+
print_pending_commit_status
|
70
|
+
|
71
|
+
print_changes("Changes to be committed",
|
72
|
+
@status.index_changes, :green)
|
73
|
+
|
74
|
+
print_changes("Unmerged paths",
|
75
|
+
@status.conflicts, :red, :conflict)
|
76
|
+
|
77
|
+
print_changes("Changes not staged for commit",
|
78
|
+
@status.workspace_changes, :red)
|
79
|
+
|
80
|
+
print_changes("Untracked files",
|
81
|
+
@status.untracked_files, :red)
|
82
|
+
|
83
|
+
print_commit_status
|
84
|
+
end
|
85
|
+
|
86
|
+
def print_branch_status
|
87
|
+
current = repo.refs.current_ref
|
88
|
+
|
89
|
+
if current.head?
|
90
|
+
puts fmt(:red, "Not currently on any branch.")
|
91
|
+
else
|
92
|
+
puts "On branch #{ current.short_name }"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def print_upstream_status
|
97
|
+
divergence = repo.divergence(repo.refs.current_ref)
|
98
|
+
return unless divergence.upstream
|
99
|
+
|
100
|
+
base = repo.refs.short_name(divergence.upstream)
|
101
|
+
ahead = divergence.ahead
|
102
|
+
behind = divergence.behind
|
103
|
+
|
104
|
+
if ahead == 0 and behind == 0
|
105
|
+
puts "Your branch is up to date with '#{ base }'."
|
106
|
+
elsif behind == 0
|
107
|
+
puts "Your branch is ahead of '#{ base }' by #{ commits ahead }."
|
108
|
+
elsif ahead == 0
|
109
|
+
puts "Your branch is behind '#{ base }' by #{ commits behind }, " +
|
110
|
+
"and can be fast-forwarded."
|
111
|
+
else
|
112
|
+
puts <<~MSG
|
113
|
+
Your branch and '#{ base }' have diverged,
|
114
|
+
and have #{ ahead } and #{ behind } different commits each, respectively.
|
115
|
+
MSG
|
116
|
+
end
|
117
|
+
|
118
|
+
puts ""
|
119
|
+
end
|
120
|
+
|
121
|
+
def commits(n)
|
122
|
+
n == 1 ? "1 commit" : "#{ n } commits"
|
123
|
+
end
|
124
|
+
|
125
|
+
def print_pending_commit_status
|
126
|
+
case repo.pending_commit.merge_type
|
127
|
+
when :merge
|
128
|
+
if @status.conflicts.empty?
|
129
|
+
puts "All conflicts fixed but you are still merging."
|
130
|
+
hint "use 'jit commit' to conclude merge"
|
131
|
+
else
|
132
|
+
puts "You have unmerged paths."
|
133
|
+
hint "fix conflicts and run 'jit commit'"
|
134
|
+
hint "use 'jit merge --abort' to abort the merge"
|
135
|
+
end
|
136
|
+
puts ""
|
137
|
+
when :cherry_pick
|
138
|
+
print_pending_type("cherry-pick")
|
139
|
+
when :revert
|
140
|
+
print_pending_type("revert")
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def print_pending_type(op)
|
145
|
+
oid = repo.database.short_oid(repo.pending_commit.merge_oid)
|
146
|
+
puts "You are currently #{ op }ing commit #{ oid }."
|
147
|
+
|
148
|
+
if @status.conflicts.empty?
|
149
|
+
hint "all conflicts fixed: run 'jit #{ op } --continue'"
|
150
|
+
else
|
151
|
+
hint "fix conflicts and run 'jit #{ op } --continue'"
|
152
|
+
end
|
153
|
+
hint "use 'jit #{ op } --abort' to cancel the #{ op } operation"
|
154
|
+
puts ""
|
155
|
+
end
|
156
|
+
|
157
|
+
def hint(message)
|
158
|
+
puts " (#{ message })"
|
159
|
+
end
|
160
|
+
|
161
|
+
def print_changes(message, changeset, style, label_set = :normal)
|
162
|
+
return if changeset.empty?
|
163
|
+
|
164
|
+
labels = UI_LABELS[label_set]
|
165
|
+
width = UI_WIDTHS[label_set]
|
166
|
+
|
167
|
+
puts "#{ message }:"
|
168
|
+
puts ""
|
169
|
+
changeset.each do |path, type|
|
170
|
+
status = type ? labels[type].ljust(width, " ") : ""
|
171
|
+
puts "\t" + fmt(style, status + path)
|
172
|
+
end
|
173
|
+
puts ""
|
174
|
+
end
|
175
|
+
|
176
|
+
def print_commit_status
|
177
|
+
return if @status.index_changes.any?
|
178
|
+
|
179
|
+
if @status.workspace_changes.any?
|
180
|
+
puts "no changes added to commit"
|
181
|
+
elsif @status.untracked_files.any?
|
182
|
+
puts "nothing added to commit but untracked files present"
|
183
|
+
else
|
184
|
+
puts "nothing to commit, working tree clean"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def print_porcelain_format
|
189
|
+
@status.changed.each do |path|
|
190
|
+
status = status_for(path)
|
191
|
+
puts "#{ status } #{ path }"
|
192
|
+
end
|
193
|
+
|
194
|
+
@status.untracked_files.each do |path|
|
195
|
+
puts "?? #{ path }"
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def status_for(path)
|
200
|
+
if @status.conflicts.has_key?(path)
|
201
|
+
CONFLICT_SHORT_STATUS[@status.conflicts[path]]
|
202
|
+
else
|
203
|
+
left = SHORT_STATUS.fetch(@status.index_changes[path], " ")
|
204
|
+
right = SHORT_STATUS.fetch(@status.workspace_changes[path], " ")
|
205
|
+
left + right
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
require_relative "./base"
|
4
|
+
require_relative "./shared/remote_agent"
|
5
|
+
require_relative "./shared/send_objects"
|
6
|
+
|
7
|
+
module Command
|
8
|
+
class UploadPack < Base
|
9
|
+
|
10
|
+
include RemoteAgent
|
11
|
+
include SendObjects
|
12
|
+
|
13
|
+
CAPABILITIES = ["ofs-delta"]
|
14
|
+
|
15
|
+
def run
|
16
|
+
accept_client("upload-pack", CAPABILITIES)
|
17
|
+
|
18
|
+
send_references
|
19
|
+
recv_want_list
|
20
|
+
recv_have_list
|
21
|
+
send_objects
|
22
|
+
|
23
|
+
exit 0
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def recv_want_list
|
29
|
+
@wanted = recv_oids("want", nil)
|
30
|
+
exit 0 if @wanted.empty?
|
31
|
+
end
|
32
|
+
|
33
|
+
def recv_have_list
|
34
|
+
@remote_has = recv_oids("have", "done")
|
35
|
+
@conn.send_packet("NAK")
|
36
|
+
end
|
37
|
+
|
38
|
+
def recv_oids(prefix, terminator)
|
39
|
+
pattern = /^#{ prefix } ([0-9a-f]+)$/
|
40
|
+
result = Set.new
|
41
|
+
|
42
|
+
@conn.recv_until(terminator) do |line|
|
43
|
+
result.add(pattern.match(line)[1])
|
44
|
+
end
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
def send_objects
|
49
|
+
revs = @wanted + @remote_has.map { |oid| "^#{ oid }" }
|
50
|
+
send_packed_objects(revs)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
data/lib/config.rb
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
require_relative "./lockfile"
|
2
|
+
|
3
|
+
class Config
|
4
|
+
SECTION_LINE = /^\s*\[([a-z0-9-]+)( "(.+)")?\]\s*($|#|;)/i
|
5
|
+
VARIABLE_LINE = /^\s*([a-z][a-z0-9-]*)\s*=\s*(.*?)\s*($|#|;)/i
|
6
|
+
BLANK_LINE = /^\s*($|#|;)/
|
7
|
+
INTEGER = /^-?[1-9][0-9]*$/
|
8
|
+
|
9
|
+
VALID_SECTION = /^[a-z0-9-]+$/i
|
10
|
+
VALID_VARIABLE = /^[a-z][a-z0-9-]*$/i
|
11
|
+
|
12
|
+
Conflict = Class.new(StandardError)
|
13
|
+
ParseError = Class.new(StandardError)
|
14
|
+
|
15
|
+
Line = Struct.new(:text, :section, :variable) do
|
16
|
+
def normal_variable
|
17
|
+
Variable.normalize(variable&.name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
Section = Struct.new(:name) do
|
22
|
+
def self.normalize(name)
|
23
|
+
return [] if name.empty?
|
24
|
+
[name.first.downcase, name.drop(1).join(".")]
|
25
|
+
end
|
26
|
+
|
27
|
+
def headling_line
|
28
|
+
line = "[#{ name.first }"
|
29
|
+
line.concat(%' "#{ name.drop(1).join(".") }"') if name.size > 1
|
30
|
+
line.concat("]\n")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Variable = Struct.new(:name, :value) do
|
35
|
+
def self.normalize(name)
|
36
|
+
name&.downcase
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.serialize(name, value)
|
40
|
+
"\t#{ name } = #{ value }\n"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.valid_key?(key)
|
45
|
+
VALID_SECTION =~ key.first and VALID_VARIABLE =~ key.last
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize(path)
|
49
|
+
@path = path
|
50
|
+
@lockfile = Lockfile.new(path)
|
51
|
+
@lines = nil
|
52
|
+
end
|
53
|
+
|
54
|
+
def open
|
55
|
+
read_config_file unless @lines
|
56
|
+
end
|
57
|
+
|
58
|
+
def open_for_update
|
59
|
+
@lockfile.hold_for_update
|
60
|
+
read_config_file
|
61
|
+
end
|
62
|
+
|
63
|
+
def save
|
64
|
+
@lines.each do |section, lines|
|
65
|
+
lines.each { |line| @lockfile.write(line.text) }
|
66
|
+
end
|
67
|
+
@lockfile.commit
|
68
|
+
end
|
69
|
+
|
70
|
+
def get(key)
|
71
|
+
get_all(key).last
|
72
|
+
end
|
73
|
+
|
74
|
+
def get_all(key)
|
75
|
+
key, var = split_key(key)
|
76
|
+
_, lines = find_lines(key, var)
|
77
|
+
|
78
|
+
lines.map { |line| line.variable.value }
|
79
|
+
end
|
80
|
+
|
81
|
+
def add(key, value)
|
82
|
+
key, var = split_key(key)
|
83
|
+
section, _ = find_lines(key, var)
|
84
|
+
|
85
|
+
add_variable(section, key, var, value)
|
86
|
+
end
|
87
|
+
|
88
|
+
def set(key, value)
|
89
|
+
key, var = split_key(key)
|
90
|
+
section, lines = find_lines(key, var)
|
91
|
+
|
92
|
+
case lines.size
|
93
|
+
when 0 then add_variable(section, key, var, value)
|
94
|
+
when 1 then update_variable(lines.first, var, value)
|
95
|
+
else
|
96
|
+
message = "cannot overwrite multiple values with a single value"
|
97
|
+
raise Conflict, message
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def replace_all(key, value)
|
102
|
+
key, var = split_key(key)
|
103
|
+
section, lines = find_lines(key, var)
|
104
|
+
|
105
|
+
remove_all(section, lines)
|
106
|
+
add_variable(section, key, var, value)
|
107
|
+
end
|
108
|
+
|
109
|
+
def unset(key)
|
110
|
+
unset_all(key) do |lines|
|
111
|
+
raise Conflict, "#{ key } has multiple values" if lines.size > 1
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def unset_all(key)
|
116
|
+
key, var = split_key(key)
|
117
|
+
section, lines = find_lines(key, var)
|
118
|
+
|
119
|
+
return unless section
|
120
|
+
yield lines if block_given?
|
121
|
+
|
122
|
+
remove_all(section, lines)
|
123
|
+
lines = lines_for(section)
|
124
|
+
remove_section(key) if lines.size == 1
|
125
|
+
end
|
126
|
+
|
127
|
+
def remove_section(key)
|
128
|
+
key = Section.normalize(key)
|
129
|
+
@lines.delete(key) ? true : false
|
130
|
+
end
|
131
|
+
|
132
|
+
def subsections(name)
|
133
|
+
name, _ = Section.normalize([name])
|
134
|
+
sections = @lines.keys
|
135
|
+
|
136
|
+
sections.select { |main, _| main == name }
|
137
|
+
.reject { |_, sub| sub == nil }
|
138
|
+
.map(&:last)
|
139
|
+
end
|
140
|
+
|
141
|
+
def section?(key)
|
142
|
+
key = Section.normalize(key)
|
143
|
+
@lines.has_key?(key)
|
144
|
+
end
|
145
|
+
|
146
|
+
private
|
147
|
+
|
148
|
+
def line_count
|
149
|
+
@lines.each_value.reduce(0) { |n, lines| n + lines.size }
|
150
|
+
end
|
151
|
+
|
152
|
+
def lines_for(section)
|
153
|
+
@lines[Section.normalize(section.name)]
|
154
|
+
end
|
155
|
+
|
156
|
+
def split_key(key)
|
157
|
+
key = key.map(&:to_s)
|
158
|
+
var = key.pop
|
159
|
+
|
160
|
+
[key, var]
|
161
|
+
end
|
162
|
+
|
163
|
+
def find_lines(key, var)
|
164
|
+
name = Section.normalize(key)
|
165
|
+
return [nil, []] unless @lines.has_key?(name)
|
166
|
+
|
167
|
+
lines = @lines[name]
|
168
|
+
section = lines.first.section
|
169
|
+
normal = Variable.normalize(var)
|
170
|
+
|
171
|
+
lines = lines.select { |l| normal == l.normal_variable }
|
172
|
+
[section, lines]
|
173
|
+
end
|
174
|
+
|
175
|
+
def add_section(key)
|
176
|
+
section = Section.new(key)
|
177
|
+
line = Line.new(section.headling_line, section)
|
178
|
+
|
179
|
+
lines_for(section).push(line)
|
180
|
+
section
|
181
|
+
end
|
182
|
+
|
183
|
+
def add_variable(section, key, var, value)
|
184
|
+
section ||= add_section(key)
|
185
|
+
|
186
|
+
text = Variable.serialize(var, value)
|
187
|
+
var = Variable.new(var, value)
|
188
|
+
line = Line.new(text, section, var)
|
189
|
+
|
190
|
+
lines_for(section).push(line)
|
191
|
+
end
|
192
|
+
|
193
|
+
def update_variable(line, var, value)
|
194
|
+
line.variable.value = value
|
195
|
+
line.text = Variable.serialize(var, value)
|
196
|
+
end
|
197
|
+
|
198
|
+
def remove_all(section, lines)
|
199
|
+
lines.each { |line| lines_for(section).delete(line) }
|
200
|
+
end
|
201
|
+
|
202
|
+
def read_config_file
|
203
|
+
@lines = Hash.new { |hash, key| hash[key] = [] }
|
204
|
+
section = Section.new([])
|
205
|
+
|
206
|
+
File.open(@path, File::RDONLY) do |file|
|
207
|
+
until file.eof?
|
208
|
+
line = parse_line(section, file.readline)
|
209
|
+
section = line.section
|
210
|
+
|
211
|
+
lines_for(section).push(line)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
rescue Errno::ENOENT
|
215
|
+
end
|
216
|
+
|
217
|
+
def parse_line(section, line)
|
218
|
+
if match = SECTION_LINE.match(line)
|
219
|
+
section = Section.new([match[1], match[3]].compact)
|
220
|
+
Line.new(line, section)
|
221
|
+
elsif match = VARIABLE_LINE.match(line)
|
222
|
+
variable = Variable.new(match[1], parse_value(match[2]))
|
223
|
+
Line.new(line, section, variable)
|
224
|
+
elsif match = BLANK_LINE.match(line)
|
225
|
+
Line.new(line, section, nil)
|
226
|
+
else
|
227
|
+
message = "bad config line #{ line_count + 1 } in file #{ @path }"
|
228
|
+
raise ParseError, message
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def parse_value(value)
|
233
|
+
case value
|
234
|
+
when "yes", "on", "true" then true
|
235
|
+
when "no", "off", "false" then false
|
236
|
+
when INTEGER then value.to_i
|
237
|
+
else value
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|