jit 0.0.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|