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