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