reapack-index 1.0beta2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,437 @@
1
+ class ReaPack::Index::CLI
2
+ CONFIG_SEARCH = [
3
+ '~',
4
+ '.',
5
+ ].freeze
6
+
7
+ PROGRAM_NAME = 'reapack-index'.freeze
8
+
9
+ DEFAULTS = {
10
+ verbose: false,
11
+ warnings: true,
12
+ progress: true,
13
+ quiet: false,
14
+ commit: nil,
15
+ output: './index.xml',
16
+ }.freeze
17
+
18
+ def initialize(argv = [])
19
+ @opts = parse_options(argv)
20
+ path = argv.last || Dir.pwd
21
+
22
+ return unless @exit.nil?
23
+
24
+ @git = Rugged::Repository.discover path
25
+ @opts = parse_options(read_config).merge @opts unless @opts[:noconfig]
26
+
27
+ @opts = DEFAULTS.merge @opts
28
+
29
+ log Hash[@opts.sort].inspect if @exit.nil?
30
+ rescue Rugged::OSError, Rugged::RepositoryError => e
31
+ $stderr.puts e.message
32
+ @exit = false
33
+ end
34
+
35
+ def run
36
+ return @exit unless @exit.nil?
37
+
38
+ @db = ReaPack::Index.new File.expand_path(@opts[:output], @git.workdir)
39
+ @db.amend = @opts[:amend]
40
+
41
+ if @opts[:check]
42
+ return check
43
+ end
44
+
45
+ if @opts[:lslinks]
46
+ print_links
47
+ return true
48
+ end
49
+
50
+ if @opts[:dump_about]
51
+ print @db.description
52
+ return true
53
+ end
54
+
55
+ if remote = @git.remotes['origin']
56
+ @db.source_pattern = ReaPack::Index.source_for remote.url
57
+ end
58
+
59
+ set_about
60
+ eval_links
61
+ scan_commits
62
+
63
+ unless @db.modified?
64
+ $stderr.puts 'Nothing to do!' unless @opts[:quiet]
65
+ return true
66
+ end
67
+
68
+ # changelog will be cleared by Index#write!
69
+ changelog = @db.changelog
70
+ puts changelog unless @opts[:quiet]
71
+
72
+ @db.write!
73
+ commit changelog
74
+
75
+ true
76
+ end
77
+
78
+ private
79
+ def prompt(question, &block)
80
+ $stderr.print "#{question} [y/N] "
81
+ answer = $stdin.getch
82
+ $stderr.puts answer
83
+
84
+ yes = answer.downcase == 'y'
85
+ block[] if block_given? && yes
86
+
87
+ yes
88
+ end
89
+
90
+ def scan_commits
91
+ if @git.empty?
92
+ warn 'The current branch does not contains any commit.'
93
+ return
94
+ end
95
+
96
+ walker = Rugged::Walker.new @git
97
+ walker.sorting Rugged::SORT_TOPO | Rugged::SORT_REVERSE
98
+ walker.push @git.head.target_id
99
+
100
+ last_commit = @db.commit.to_s
101
+ if Rugged.valid_full_oid?(last_commit) && last_commit.size <= 40
102
+ walker.hide last_commit if @git.include? last_commit
103
+ end
104
+
105
+ commits = walker.each.to_a
106
+
107
+ @done, @total = 0, commits.size
108
+
109
+ unless commits.empty?
110
+ print_progress
111
+ commits.each {|commit| process commit }
112
+ $stderr.print "\n" if @add_nl
113
+ end
114
+ end
115
+
116
+ def process(commit)
117
+ if @opts[:verbose]
118
+ sha = commit.oid[0..6]
119
+ message = commit.message.lines.first.chomp
120
+ log "processing %s: %s" % [sha, message]
121
+ end
122
+
123
+ @db.commit = commit.oid
124
+ @db.time = commit.time
125
+ @db.files = lsfiles commit.tree
126
+
127
+ parent = commit.parents.first
128
+
129
+ if parent
130
+ diff = parent.diff commit.oid
131
+ else
132
+ diff = commit.diff
133
+ end
134
+
135
+ diff.each_delta {|delta| index delta, parent.nil? }
136
+ ensure
137
+ @done += 1
138
+ print_progress
139
+ end
140
+
141
+ def index(delta, is_initial)
142
+ if is_initial
143
+ status = 'new'
144
+ file = delta.old_file
145
+ else
146
+ status = delta.status
147
+ file = delta.new_file
148
+ end
149
+
150
+ return unless ReaPack::Index.type_of file[:path]
151
+
152
+ log "-> indexing #{status} file #{file[:path]}"
153
+
154
+ if status == :deleted
155
+ @db.remove file[:path]
156
+ else
157
+ blob = @git.lookup file[:oid]
158
+
159
+ begin
160
+ @db.scan file[:path], blob.content.force_encoding("UTF-8")
161
+ rescue ReaPack::Index::Error => e
162
+ warn "#{file[:path]}: #{e.message}"
163
+ end
164
+ end
165
+ end
166
+
167
+ def lsfiles(tree, base = String.new)
168
+ files = []
169
+
170
+ tree.each {|obj|
171
+ fullname = base.empty? ? obj[:name] : File.join(base, obj[:name])
172
+ case obj[:type]
173
+ when :blob
174
+ files << fullname
175
+ when :tree
176
+ files.concat lsfiles(@git.lookup(obj[:oid]), fullname)
177
+ end
178
+ }
179
+
180
+ files
181
+ end
182
+
183
+ def eval_links
184
+ Array(@opts[:links]).each {|link|
185
+ begin
186
+ @db.eval_link *link
187
+ rescue ReaPack::Index::Error => e
188
+ warn e.message
189
+ end
190
+ }
191
+ end
192
+
193
+ def print_links
194
+ ReaPack::Index::Link::VALID_TYPES.each {|type|
195
+ prefix = "[#{type}]".bold.light_black
196
+ @db.links(type).each {|link|
197
+ display = link.name == link.url ? link.url : '%s (%s)' % [link.name, link.url]
198
+ puts '%s %s' % [prefix, display]
199
+ }
200
+ }
201
+ end
202
+
203
+ def set_about
204
+ path = @opts[:about]
205
+
206
+ unless path
207
+ @db.description = String.new if @opts[:rmabout]
208
+ return
209
+ end
210
+
211
+ log "converting #{path} into RTF..."
212
+
213
+ # look for the file in the working directory, not on the repository root
214
+ @db.description = File.read(path)
215
+ rescue Errno::ENOENT => e
216
+ warn '--about: ' + e.message.sub(' @ rb_sysopen', '')
217
+ rescue ReaPack::Index::Error => e
218
+ warn e.message
219
+ end
220
+
221
+ def check
222
+ failures = []
223
+ root = @git.workdir
224
+
225
+ types = ReaPack::Index::FILE_TYPES.keys
226
+ files = Dir.glob "#{Regexp.quote(root)}**/*.{#{types.join ','}}"
227
+
228
+ files.sort.each {|file|
229
+ errors = ReaPack::Index.validate_file file
230
+
231
+ if errors
232
+ $stderr.print 'F' unless @opts[:quiet]
233
+ prefix = "\n - "
234
+ file[0..root.size-1] = ''
235
+
236
+ failures << "%s contains invalid metadata:#{prefix}%s" %
237
+ [file, errors.join(prefix)]
238
+ else
239
+ $stderr.print '.' unless @opts[:quiet]
240
+ end
241
+ }
242
+
243
+ $stderr.puts "\n" unless @opts[:quiet]
244
+
245
+ failures.each_with_index {|msg, index|
246
+ $stderr.puts unless @opts[:quiet] && index == 0
247
+ $stderr.puts msg.yellow
248
+ }
249
+
250
+ unless @opts[:quiet]
251
+ $stderr.puts "\n"
252
+
253
+ $stderr.puts "Finished checks for %d package%s with %d failure%s" % [
254
+ files.size, files.size == 1 ? '' : 's',
255
+ failures.size, failures.size == 1 ? '' : 's'
256
+ ]
257
+ end
258
+
259
+ failures.empty?
260
+ end
261
+
262
+ def commit(changelog)
263
+ return unless case @opts[:commit]
264
+ when false, true
265
+ @opts[:commit]
266
+ else
267
+ prompt 'Commit the new index?'
268
+ end
269
+
270
+ target = @git.head.target
271
+ root = Pathname.new @git.workdir
272
+ file = Pathname.new @db.path
273
+
274
+ old_index = @git.index
275
+ old_index.read_tree target.tree
276
+
277
+ index = @git.index
278
+ index.add file.relative_path_from(root).to_s
279
+
280
+ Rugged::Commit.create @git, \
281
+ tree: index.write_tree(@git),
282
+ message: "index: #{changelog}",
283
+ parents: [target],
284
+ update_ref: 'HEAD'
285
+
286
+ old_index.write
287
+
288
+ $stderr.puts 'commit created'
289
+ end
290
+
291
+ def log(line)
292
+ $stderr.puts line if @opts[:verbose]
293
+ end
294
+
295
+ def warn(line)
296
+ return unless @opts[:warnings]
297
+
298
+ if @add_nl
299
+ $stderr.puts
300
+ @add_nl = false
301
+ end
302
+
303
+ $stderr.puts "Warning: #{line}".yellow
304
+ end
305
+
306
+ def print_progress
307
+ return if @opts[:verbose] || !@opts[:progress]
308
+
309
+ percent = (@done.to_f / @total) * 100
310
+ $stderr.print "\rIndexing commit %d of %d (%d%%)..." %
311
+ [[@done + 1, @total].min, @total, percent]
312
+
313
+ @add_nl = true
314
+ end
315
+
316
+ def parse_options(args)
317
+ opts = Hash.new
318
+
319
+ OptionParser.new do |op|
320
+ op.program_name = PROGRAM_NAME
321
+ op.version = ReaPack::Index::VERSION
322
+ op.banner = "Package indexer for ReaPack-based repositories\n" +
323
+ "Usage: #{PROGRAM_NAME} [options] [directory]"
324
+
325
+ op.separator 'Options:'
326
+
327
+ op.on '-a', '--[no-]amend', 'Reindex existing versions' do |bool|
328
+ opts[:amend] = bool
329
+ end
330
+
331
+ op.on '-c', '--check', 'Test every package including uncommited changes and exit' do
332
+ opts[:check] = true
333
+ end
334
+
335
+ op.on '-o', "--output FILE=#{DEFAULTS[:output]}",
336
+ 'Set the output filename and path for the index' do |file|
337
+ opts[:output] = file.strip
338
+ end
339
+
340
+ op.on '-l', '--link LINK', 'Add or remove a website link' do |link|
341
+ opts[:links] ||= Array.new
342
+ opts[:links] << [:website, link.strip]
343
+ end
344
+
345
+ op.on '--donation-link LINK', 'Add or remove a donation link' do |link|
346
+ opts[:links] ||= Array.new
347
+ opts[:links] << [:donation, link]
348
+ end
349
+
350
+ op.on '--ls-links', 'Display the link list then exit' do |link|
351
+ opts[:lslinks] = true
352
+ end
353
+
354
+ op.on '-A', '--about=FILE', 'Set the about content from a file' do |file|
355
+ opts[:about] = file.strip
356
+ end
357
+
358
+ op.on '--remove-about', 'Remove the about content from the index' do
359
+ opts[:rmabout] = true
360
+ end
361
+
362
+ op.on '--dump-about', 'Dump the raw about content in RTF and exit' do
363
+ opts[:dump_about] = true
364
+ end
365
+
366
+ op.on '--[no-]progress', 'Enable or disable progress information' do |bool|
367
+ opts[:progress] = bool
368
+ end
369
+
370
+ op.on '-V', '--[no-]verbose', 'Activate diagnosis messages' do |bool|
371
+ opts[:verbose] = bool
372
+ end
373
+
374
+ op.on '-C', '--[no-]commit', 'Select whether to commit the modified index' do |bool|
375
+ opts[:commit] = bool
376
+ end
377
+
378
+ op.on '--prompt-commit', 'Ask at runtime whether to commit the index' do
379
+ opts[:commit] = nil
380
+ end
381
+
382
+ op.on '-W', '--warnings', 'Enable warnings' do
383
+ opts[:warnings] = true
384
+ end
385
+
386
+ op.on '-w', '--no-warnings', 'Turn off warnings' do
387
+ opts[:warnings] = false
388
+ end
389
+
390
+ op.on '-q', '--[no-]quiet', 'Disable almost all output' do
391
+ opts[:warnings] = false
392
+ opts[:progress] = false
393
+ opts[:verbose] = false
394
+ opts[:quiet] = true
395
+ end
396
+
397
+ op.on '--no-config', 'Bypass the configuration files' do
398
+ opts[:noconfig] = true
399
+ end
400
+
401
+ op.on_tail '-v', '--version', 'Display version information' do
402
+ puts op.ver
403
+ @exit = true
404
+ end
405
+
406
+ op.on_tail '-h', '--help', 'Prints this help' do
407
+ puts op
408
+ @exit = true
409
+ end
410
+ end.parse! args
411
+
412
+ opts
413
+ rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
414
+ $stderr.puts "#{PROGRAM_NAME}: #{e.message}"
415
+ $stderr.puts "Try '#{PROGRAM_NAME} --help' for more information."
416
+ @exit = false
417
+ opts
418
+ end
419
+
420
+ def read_config
421
+ CONFIG_SEARCH.map {|dir|
422
+ dir = File.expand_path dir, @git.workdir
423
+ path = File.expand_path '.reapack-index.conf', dir
424
+
425
+ log 'reading configuration from %s' % path
426
+
427
+ unless File.readable? path
428
+ log 'configuration file is unreadable, skipping'
429
+ next
430
+ end
431
+
432
+ opts = Array.new
433
+ File.foreach(path) {|line| opts << Shellwords.split(line) }
434
+ opts
435
+ }.flatten.compact
436
+ end
437
+ end
@@ -0,0 +1,5 @@
1
+ module ReaPack
2
+ class Index
3
+ VERSION = '1.0beta2'
4
+ end
5
+ end
@@ -0,0 +1,185 @@
1
+ class ReaPack::Index
2
+ class Metadata
3
+ TAG = 'metadata'.freeze
4
+ DESC = 'description'.freeze
5
+
6
+ def initialize(parent)
7
+ @parent = parent
8
+
9
+ @root = parent.element_children.find {|node| node.name == TAG }
10
+ end
11
+
12
+ def links(type)
13
+ Link.find_all(type, @root).map {|node| Link.from_node node }
14
+ .select {|link| link.url.index('http') == 0 }
15
+ end
16
+
17
+ def push_link(type, name = nil, url)
18
+ Link.check_type type
19
+
20
+ unless url =~ /\A#{URI::regexp(['http', 'https'])}\z/
21
+ raise Error, "invalid URL: #{url}"
22
+ end
23
+
24
+ make_root
25
+
26
+ link = Link.new name || url, url
27
+ node = Link.find type, link.name, @root
28
+ node ||= Link.find type, link.url, @root
29
+
30
+ if node
31
+ link.instance_variable_set :@is_new, false
32
+ link.instance_variable_set :@modified, link != Link.from_node(node)
33
+
34
+ node.remove_attribute Link::URL
35
+ else
36
+ link.instance_variable_set :@is_new, true
37
+ link.instance_variable_set :@modified, true
38
+
39
+ node = Nokogiri::XML::Node.new Link::TAG, @root.document
40
+ node.parent = @root
41
+ node[Link::REL] = type
42
+ end
43
+
44
+ if name
45
+ node[Link::URL] = url
46
+ node.content = name
47
+ else
48
+ node.content = url
49
+ end
50
+
51
+ link
52
+ end
53
+
54
+ def remove_link(type, search)
55
+ node = Link.find type, search, @root
56
+
57
+ raise Error, "no such #{type} link: #{search}" unless node
58
+
59
+ node.remove
60
+ auto_remove
61
+ end
62
+
63
+ def description
64
+ cdata = nil
65
+
66
+ if @root
67
+ desc = @root.element_children.find {|node| node.name == DESC }
68
+ cdata = desc.children.first if desc
69
+ end
70
+
71
+ cdata ? cdata.content : String.new
72
+ end
73
+
74
+ def description=(content)
75
+ return if content == description
76
+
77
+ make_root
78
+ desc = @root.element_children.find {|node| node.name == DESC }
79
+
80
+ if content.empty?
81
+ desc.remove
82
+ auto_remove
83
+ return
84
+ elsif content.index("{\\rtf") != 0
85
+ content = make_rtf content
86
+ end
87
+
88
+ if desc
89
+ desc.children.each {|n| n.remove }
90
+ else
91
+ desc = Nokogiri::XML::Node.new DESC, @root.document
92
+ desc.parent = @root
93
+ end
94
+
95
+ cdata = Nokogiri::XML::CDATA.new desc, content
96
+ cdata.parent = desc
97
+ end
98
+
99
+ private
100
+ def make_root
101
+ unless @root
102
+ @root = Nokogiri::XML::Node.new TAG, @parent.document
103
+ @root.parent = @parent
104
+ end
105
+
106
+ @root
107
+ end
108
+
109
+ def auto_remove
110
+ @root.remove if @root.children.empty?
111
+ end
112
+
113
+ def make_rtf(content)
114
+ PandocRuby.new(content).to_rtf :standalone
115
+ rescue Errno::ENOENT
116
+ raise Error, [
117
+ "RTF conversion failed because the pandoc executable " \
118
+ "cannot be found in your PATH.",
119
+ "Try again after installing pandoc from <http://pandoc.org/>."
120
+ ].join("\n")
121
+ end
122
+ end
123
+
124
+ class Link
125
+ TAG = 'link'.freeze
126
+ REL = 'rel'.freeze
127
+ URL = 'href'.freeze
128
+
129
+ # the first type will be the default one
130
+ VALID_TYPES = [:website, :donation].freeze
131
+
132
+ def self.from_node(node)
133
+ name, url = node.text.to_s, node[URL].to_s
134
+ url, name = name, url if url.empty?
135
+ name = url if name.empty?
136
+
137
+ self.new name, url
138
+ end
139
+
140
+ def self.check_type(type)
141
+ raise ArgumentError unless VALID_TYPES.include? type
142
+ end
143
+
144
+ def self.same_type?(type, user)
145
+ # match invalid types by the first value of VALID_TYPES
146
+ # while the other values require an exact match
147
+ user == type || (type == VALID_TYPES[0] && VALID_TYPES.index(user).to_i < 1)
148
+ end
149
+
150
+ def self.find_all(type, parent)
151
+ Link.check_type type
152
+
153
+ return [] unless parent
154
+
155
+ parent.element_children.select {|node|
156
+ node.name == TAG && Link.same_type?(type, node[REL].to_s.to_sym)
157
+ }
158
+ end
159
+
160
+ def self.find(type, search, parent)
161
+ Link.find_all(type, parent).find {|node|
162
+ node.text == search || node[URL] == search
163
+ }
164
+ end
165
+
166
+ def initialize(name, url)
167
+ @name, @url = name, url
168
+ @is_new = @modified = false
169
+ end
170
+
171
+ attr_accessor :name, :url
172
+
173
+ def ==(other)
174
+ name == other.name && url == other.url
175
+ end
176
+
177
+ def is_new?
178
+ @is_new
179
+ end
180
+
181
+ def modified?
182
+ @modified
183
+ end
184
+ end
185
+ end