reapack-index 1.0beta2

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