repobrowse 0.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 (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +5 -0
  3. data/.gitignore +3 -0
  4. data/COPYING +661 -0
  5. data/GNUmakefile +38 -0
  6. data/MANIFEST +42 -0
  7. data/README +35 -0
  8. data/lib/repobrowse.rb +7 -0
  9. data/lib/repobrowse/app.rb +60 -0
  10. data/lib/repobrowse/config.rb +66 -0
  11. data/lib/repobrowse/error.rb +21 -0
  12. data/lib/repobrowse/escape.rb +28 -0
  13. data/lib/repobrowse/git.rb +71 -0
  14. data/lib/repobrowse/git_atom.rb +109 -0
  15. data/lib/repobrowse/git_commit_html.rb +320 -0
  16. data/lib/repobrowse/git_disambiguate.rb +21 -0
  17. data/lib/repobrowse/git_http_backend.rb +109 -0
  18. data/lib/repobrowse/git_log.rb +4 -0
  19. data/lib/repobrowse/git_patch.rb +55 -0
  20. data/lib/repobrowse/git_raw.rb +50 -0
  21. data/lib/repobrowse/git_raw_tree_html.rb +50 -0
  22. data/lib/repobrowse/git_show.rb +32 -0
  23. data/lib/repobrowse/git_src.rb +37 -0
  24. data/lib/repobrowse/git_src_blob_html.rb +89 -0
  25. data/lib/repobrowse/git_src_tree_html.rb +118 -0
  26. data/lib/repobrowse/html.rb +66 -0
  27. data/lib/repobrowse/limit_rd.rb +86 -0
  28. data/lib/repobrowse/pipe_body.rb +25 -0
  29. data/lib/repobrowse/repo.rb +21 -0
  30. data/lib/repobrowse/static.rb +96 -0
  31. data/repobrowse.gemspec +29 -0
  32. data/test/covshow.rb +30 -0
  33. data/test/git.fast-import-data +101 -0
  34. data/test/helper.rb +182 -0
  35. data/test/test_config.rb +29 -0
  36. data/test/test_git.rb +15 -0
  37. data/test/test_git_atom.rb +27 -0
  38. data/test/test_git_clone.rb +73 -0
  39. data/test/test_git_patch.rb +44 -0
  40. data/test/test_git_raw_tree_html.rb +34 -0
  41. data/test/test_git_show.rb +17 -0
  42. data/test/test_html.rb +23 -0
  43. metadata +139 -0
@@ -0,0 +1,4 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2017-2018 all contributors <repobrowse-public@80x24.org>
3
+ # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4
+ # frozen_string_literal: true
@@ -0,0 +1,55 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2017-2018 all contributors <repobrowse-public@80x24.org>
3
+ # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4
+ # frozen_string_literal: true
5
+ require 'time'
6
+ require_relative 'pipe_body'
7
+
8
+ # included by Repobrowse::Git
9
+ module Repobrowse::GitPatch
10
+
11
+ # mboxrd support requires git 2.10+ (2016/09/02)
12
+ @@mboxrd = '--pretty=mboxrd'
13
+
14
+ # /$REPO_NAME/$HEX.patch
15
+ def patch(r, repo, objid)
16
+ rgd = repo.driver.rugged
17
+ cmt = rgd.rev_parse("#{objid}^{commit}") rescue nil
18
+ return r404(r) unless Rugged::Commit === cmt
19
+ time = cmt.author[:time]
20
+ cmt = cmt.oid
21
+
22
+ # disambiguate, expand abbreviated commit IDs to be cache-friendly
23
+ if objid != cmt
24
+ loc = r.base_url
25
+ loc << "#{r.script_name}/#{repo.name}/#{cmt}.patch"
26
+ return r.redirect(loc)
27
+ end
28
+
29
+ # command should match signature, so not using rugged, here;
30
+ # and mboxrd support is fairly recent in git.git. rugged/libgit2
31
+ # developers also do not use an email+patch-driven workflow, so I
32
+ # don't expect it to support patch formatting as well as git.git does
33
+ begin
34
+ cmd = %W(format-patch -M --stdout --encoding=utf8 -1 #{cmt})
35
+ mboxrd = @@mboxrd and cmd << mboxrd
36
+ cmd << "--signature=git #{cmd.join(' ')}"
37
+ io = repo.driver.popen(cmd)
38
+ if buf = io.read(0x2000)
39
+ h = {
40
+ 'Content-Type' => 'text/plain; charset=UTF-8',
41
+ 'Last-Modified' => (time || Time.now).httpdate,
42
+ 'Expires' => (Time.now + 604800).httpdate,
43
+ 'Content-Disposition' => %Q{inline; filename="#{cmt}.patch"},
44
+ }
45
+ return r.halt [ 200, h, Repobrowse::PipeBody.new(io, buf) ]
46
+ end
47
+ io.close
48
+ if mboxrd
49
+ @@mboxrd = nil
50
+ else
51
+ return r_err(r, 'Internal Server Error')
52
+ end
53
+ end while true
54
+ end
55
+ end
@@ -0,0 +1,50 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2017-2018 all contributors <repobrowse-public@80x24.org>
3
+ # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'escape'
7
+ require_relative 'git_raw_tree_html'
8
+ require_relative 'git_disambiguate'
9
+ require_relative 'pipe_body'
10
+
11
+ module Repobrowse::GitRaw
12
+ include Repobrowse::GitDisambiguate
13
+
14
+ # /$REPO_NAME/raw/$REF:$PATH
15
+ def raw(r, repo, ref, path)
16
+ git_disambiguate(r, repo, 'raw', ref, path)
17
+ h = { 'Content-Type' => 'text/plain; charset=UTF-8' }
18
+ spec = "#{ref}:#{path}"
19
+ rgd = rugged
20
+ begin
21
+ oid = rgd.rev_parse_oid(spec)
22
+ hdr = rgd.read_header(oid)
23
+ rescue
24
+ return r404(r)
25
+ end
26
+ type = hdr[:type]
27
+ if type == :tree
28
+ body = Repobrowse::GitRawTreeHTML.new(rgd.lookup(oid), ref, path)
29
+ h['Content-Type'] = 'text/html; charset=UTF-8'
30
+ else
31
+ fn = path.split('/')[-1]
32
+ h['ETag'] = %Q{"#{oid}"}
33
+ len = hdr[:len]
34
+ h['Content-Length'] = len.to_s
35
+ h['Content-Disposition'] = %Q{inline; filename="#{fn}"}
36
+ if len < 512 * 1024
37
+ body = rgd.lookup(oid).content
38
+ peek = body[0, 8000]
39
+ body = [ body ]
40
+ else # large objects
41
+ io = repo.driver.popen(%Q(cat-file -t #{type} #{oid}))
42
+ peek = io.read(8000) or return r404(r)
43
+ body = Repobrowse::PipeBody.new(io, peek)
44
+ end
45
+ peek.include?("\0") and
46
+ h['Content-Type'] = 'application/octet-stream'
47
+ end
48
+ r.halt [ 200, h, body ]
49
+ end
50
+ end
@@ -0,0 +1,50 @@
1
+ # -*- encoding: binary -*-
2
+ # Copyright (C) 2017-2018 all contributors <repobrowse-public@80x24.org>
3
+ # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4
+ # frozen_string_literal: true
5
+ require_relative 'escape'
6
+
7
+ # ugly "raw" tree view, "git_src_tree_html" has the prettier one
8
+ class Repobrowse::GitRawTreeHTML
9
+ include Repobrowse::Escape
10
+
11
+ def initialize(tree, ref, path)
12
+ @tree = tree
13
+ @ref = ref
14
+ @path = path # nil ok
15
+ end
16
+
17
+ def each
18
+ if @path
19
+ t = ht(+"/#@path/")
20
+ path = @path.split('/')
21
+ case path.size
22
+ when 1
23
+ ref = CGI.escape(@ref.split('/')[-1])
24
+ pfx = "./#{ref}:#{CGI.escape(path[0])}/"
25
+ up = ha('./' + ref)
26
+ else
27
+ pfx = "#{CGI.escape(path[-1])}/"
28
+ up = "#@ref:#@path".split('/')[-2]
29
+ up = up.split(':').map! { |u| CGI.escape(u) }.join(':')
30
+ up = ha('../' + up)
31
+ end
32
+ else
33
+ t = '/'
34
+ pfx = "./#{CGI.escape(@ref.split('/')[-1])}:"
35
+ end
36
+ yield "<html><head><title>#{t}</title></head><body><h2>#{t}</h2><ul>"
37
+ yield %Q(<li><a\nhref=#{up}>../</a></li>) if up
38
+ @tree.each do |ent|
39
+ name = ent[:name]
40
+ href = ha(pfx + CGI.escape(name))
41
+ name = ht(name)
42
+ yield %Q(<li><a\nhref=#{href}>#{name}</a></li>)
43
+ end
44
+ yield '</ul></body></html>'
45
+ self
46
+ end
47
+
48
+ def close
49
+ end
50
+ end
@@ -0,0 +1,32 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # Copyright (C) 2017-2018 all contributors <repobrowse-public@80x24.org>
3
+ # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'git_commit_html'
7
+
8
+ module Repobrowse::GitShow
9
+
10
+ def show(r, repo, objid)
11
+ rgd = repo.driver.rugged
12
+ begin
13
+ oid = rgd.rev_parse_oid(objid)
14
+ rescue
15
+ return r404(r)
16
+ end
17
+ # disambiguate, expand abbreviated commit IDs to be cache-friendly
18
+ if oid != objid
19
+ return r.redirect("#{r.base_url}#{r.script_name}/#{repo.name}/#{oid}")
20
+ end
21
+ hdr = rgd.read_header(oid)
22
+ case hdr[:type]
23
+ when :commit
24
+ html = Repobrowse::GitCommitHTML.new
25
+ html.commit_header(r.env, repo, rgd.lookup(oid))
26
+ r.halt html.response(200)
27
+ # TODO: other types
28
+ else
29
+ r404(r)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # Copyright (C) 2017-2018 all contributors <repobrowse-public@80x24.org>
3
+ # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'git_src_blob_html'
7
+ require_relative 'git_src_tree_html'
8
+ require_relative 'git_disambiguate'
9
+
10
+ module Repobrowse::GitSrc
11
+ include Repobrowse::GitDisambiguate
12
+
13
+ MAX = 1024 * 1024
14
+
15
+ # /$REPO_NAME/src/$REF:$PATH
16
+ def src(r, repo, ref, path)
17
+ git_disambiguate(r, repo, 'src', ref, path)
18
+ spec = "#{ref}:#{path}"
19
+ rgd = rugged
20
+ begin
21
+ oid = rgd.rev_parse_oid(spec)
22
+ hdr = rgd.read_header(oid)
23
+ rescue => e
24
+ warn e.message
25
+ return r404(r)
26
+ end
27
+ h = { 'ETag' => %Q{"#{oid}"} }
28
+ case hdr[:type]
29
+ when :tree
30
+ tree = rgd.lookup(oid)
31
+ html = Repobrowse::GitSrcTreeHTML.new(tree, ref, path, repo)
32
+ when :blob
33
+ html = Repobrowse::GitSrcBlobHTML.new(hdr, oid, ref, path, repo)
34
+ end
35
+ r.halt(html.response(200, h))
36
+ end
37
+ end
@@ -0,0 +1,89 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # Copyright (C) 2017-2018 all contributors <repobrowse-public@80x24.org>
3
+ # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4
+ # frozen_string_literal: true
5
+ require_relative 'html'
6
+ require 'uri'
7
+
8
+ class Repobrowse::GitSrcBlobHTML < Repobrowse::HTML
9
+ def initialize(hdr, oid, ref, path, repo)
10
+ super()
11
+ @repo = repo
12
+ ref_parts = ref.split('/')
13
+ ref_uri = ref_parts.map { |s| CGI.escape(s) }
14
+ ref_html = ht(ref.dup)
15
+ path_parts = path.split('/')
16
+ path_uri = path_parts.map { |s| CGI.escape(s) }
17
+ path_html = ht(path.dup)
18
+ relup = (2..(ref_parts.size + path_parts.size)).map { '../' }.join
19
+ raw = +"#{relup}/raw/#{ref_uri.join('/')}:#{path_uri.join('/')}"
20
+ path_uri[0] = "#{ref_uri[-1]}:#{path_uri[0]}" if path_uri[0]
21
+ relup = (2..path_parts.size).map { '../' }.join
22
+ base = path_parts.pop
23
+ _base_uri = path_uri.pop
24
+ tmp = []
25
+ path_parts.map! do |x|
26
+ tmp << path_uri.shift
27
+ href = +"#{relup}#{tmp.join('/')}"
28
+ %Q(<a\nhref=#{ha(href)}>#{ht(x)}</a>)
29
+ end
30
+ path_parts << "<b>#{ht(base)}</b>"
31
+ @buf = start("#{ref_html}:#{path_html}", repo)
32
+ len = hdr[:len]
33
+ @buf << <<EOS
34
+ <a
35
+ href=#{ha(relup + ref_uri[-1])}>#{ref_html}</a> : #{path_parts.join(' / ')}
36
+
37
+ blob #{oid}\t#{len} bytes (<a
38
+ href=#{ha(raw)}>raw</a>)</pre><hr/>
39
+ EOS
40
+ @buf.chomp!
41
+ if len < (512 * 1024)
42
+ @oid = oid
43
+ end
44
+ end
45
+
46
+ # Called by the rack server
47
+ def each
48
+ if @buf
49
+ yield @buf
50
+ @buf.clear
51
+ @buf = nil
52
+ end
53
+
54
+ # TODO: optional syntax highlighting
55
+ # highlight(1) supports streaming; but needs be spawned
56
+ # highlight should also be familiar to gitweb and cgit users
57
+ # rouge allows streaming output, but not input :<
58
+ # coderay doesn't seem to support streaming at all
59
+ if @oid
60
+ body = @repo.driver.rugged.lookup(@oid).content
61
+ if body[0, 8000].include?("\0")
62
+ yield "<pre>Binary file, save using the 'raw' link above</pre>"
63
+ else
64
+ yield '<table><tr><td><pre>'
65
+ n = body.count("\n")
66
+ yield ht(body)
67
+ unless body.end_with?("\n")
68
+ n += 1
69
+ yield "\n\"
70
+ end
71
+ yield '</pre></td><td><pre>'
72
+ pfx = @repo.lineno_prefix
73
+ (1..n).each do |i|
74
+ anchor = "#{pfx}#{i}"
75
+ yield %Q(<a\nid=#{anchor}\nhref="##{anchor}">#{i}</a>\n)
76
+ end
77
+ yield '</pre></td></tr></table>'
78
+ end
79
+ else
80
+ yield "<pre>File is too big to display, save using the 'raw' link</pre>"
81
+ end
82
+ yield '</body></html>'
83
+ self
84
+ end
85
+
86
+ # called by the Rack server
87
+ def close
88
+ end
89
+ end
@@ -0,0 +1,118 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # Copyright (C) 2017-2018 all contributors <repobrowse-public@80x24.org>
3
+ # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4
+ # frozen_string_literal: true
5
+ require_relative 'html'
6
+ require 'uri'
7
+
8
+ class Repobrowse::GitSrcTreeHTML < Repobrowse::HTML
9
+ def initialize(tree, ref, path, repo)
10
+ super()
11
+ @tree = tree
12
+ @repo = repo
13
+ ref_parts = ref.split('/')
14
+ ref_uri = ref_parts.map { |s| CGI.escape(s) }
15
+ ref_html = ht(ref.dup)
16
+ if path
17
+ path_parts = path.split('/')
18
+ path_uri = path_parts.map { |s| CGI.escape(s) }
19
+ path_html = ht(path.dup)
20
+ path_uri[0] = "#{ref_uri[-1]}:#{path_uri[0]}" if path_uri[0]
21
+ relup = (2..path_parts.size).map { '../' }.join
22
+ base = path_parts.pop
23
+ base_uri = path_uri.pop
24
+ tmp = []
25
+ path_parts.map! do |x|
26
+ tmp << path_uri.shift
27
+ href = ha(+"#{relup}#{tmp.join('/')}")
28
+ %Q(<a\nhref=#{href}>#{ht(x)}</a>)
29
+ end
30
+ path_parts << "<b>#{ht(base)}</b>"
31
+ @rel_prefix = "./#{base_uri}/"
32
+ else
33
+ path_parts = [ '[root]' ]
34
+ relup = ''
35
+ @rel_prefix = "./#{ref_uri[-1]}:"
36
+ end
37
+ @buf = start("#{ref_html}:#{path_html}", repo)
38
+ @buf << <<EOS
39
+ <a
40
+ href=#{ha(relup + ref_uri[-1])}>#{ref_html}</a> : #{path_parts.join(' / ')}
41
+
42
+ tree #{@tree.oid}</pre><hr/><pre>
43
+ EOS
44
+ @buf.chomp!
45
+ end
46
+
47
+ def fmt_ent(ent, rgd, parts)
48
+ name = ent[:name]
49
+ name_text = ht(name.dup)
50
+ case ent[:filemode]
51
+ when 0100644 # plain blob, nothing special
52
+ md = ' '
53
+ when 0040000
54
+ return do_descend(ent, rgd) unless parts
55
+ md = 'd'
56
+ len = '-'
57
+ when 0100755
58
+ md = 'x'
59
+ name_text = "<b>#{name_text}</b>"
60
+ when 0120000
61
+ md = 'l'
62
+ name_text = "<i>#{name_text}</i>"
63
+ when 0160000
64
+ md = 'g'
65
+ name_text = "#{name_text} <u>@ #{ent[:oid]}</u>"
66
+ len = '-'
67
+ else
68
+ warn "unknown mode: #{'%06o' % ent[:filemode]}"
69
+ end
70
+ len ||= rgd.read_header(ent[:oid])[:len].to_s
71
+ if parts && dir = parts.shift
72
+ href = @rel_prefix + URI.escape(dir)
73
+ dir = "<a\nhref=#{ha(href.dup)}>#{ht(dir)}</a>"
74
+ parts.map! do |part|
75
+ href << "/#{URI.escape(part)}"
76
+ "<a\nhref=#{ha(href.dup)}>#{ht(part)}</a>"
77
+ end
78
+ parts.unshift(dir)
79
+ href << "/#{URI.escape(name)}"
80
+ parts << "<a\nhref=#{ha(href)}>#{name_text}</a>"
81
+ rest = parts.join(' / ')
82
+ else
83
+ rest = "<a\nhref=#{ha(@rel_prefix + URI.escape(name))}>#{name_text}</a>"
84
+ end
85
+ "#{md} #{sprintf('% 8s', len)}\t#{rest}\n"
86
+ end
87
+
88
+ def do_descend(ent, rgd)
89
+ parts = []
90
+ begin
91
+ tree = rgd.lookup(ent[:oid])
92
+ if tree.count != 1
93
+ break
94
+ else
95
+ parts << ent[:name]
96
+ ent = tree.get_entry(0)
97
+ ent[:filemode] == 0040000 or break
98
+ end
99
+ end while true
100
+ fmt_ent(ent, rgd, parts)
101
+ end
102
+
103
+ # called by the Rack server
104
+ def each
105
+ yield @buf
106
+ @buf.clear
107
+ rgd = @repo.driver.rugged
108
+ @tree.each do |ent|
109
+ yield fmt_ent(ent, rgd, nil)
110
+ end
111
+ yield '</pre></body></html>'
112
+ self
113
+ end
114
+
115
+ # called by the Rack server
116
+ def close
117
+ end
118
+ end
@@ -0,0 +1,66 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # Copyright (C) 2017-2018 all contributors <repobrowse-public@80x24.org>
3
+ # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
4
+ # frozen_string_literal: true
5
+
6
+ require_relative 'escape'
7
+
8
+ # used to give each HTML page a consistent look, this is a Rack response body
9
+ class Repobrowse::HTML
10
+ attr_reader :buf # meant for appending to via String#<<
11
+
12
+ include Repobrowse::Escape
13
+
14
+ def initialize
15
+ @buf = nil
16
+ end
17
+
18
+ def start(title_html, desc, robots: nil)
19
+ meta = %Q(<meta\nname=robots\ncontent="#{robots}" />) if robots
20
+ @buf = +<<~EOF
21
+ <!DOCTYPE html>
22
+ <html><head><title>#{
23
+ title_html
24
+ }</title><style>pre{white-space:pre-wrap}</style>#{
25
+ meta
26
+ }</head><body><pre><b>#{
27
+ case desc
28
+ when String, nil
29
+ desc
30
+ else
31
+ desc.driver.description.encode(xml: :text)
32
+ end
33
+ }</b>
34
+ EOF
35
+ end
36
+
37
+ ESCAPES = { '/' => ':' }
38
+ (0..255).each { |x|
39
+ key = -x.chr
40
+ next if key =~ %r{\A[\w\.\-/]}n
41
+ ESCAPES[key] = -sprintf('::%02x', x)
42
+ }
43
+
44
+ def to_anchor(str)
45
+ str = str.dup
46
+ first = ''
47
+ # must start with alphanum
48
+ str.sub!(/\A([^A-Ya-z])/n, '') and first = sprintf('Z%02x', $1.ord)
49
+ str.gsub!(/([^\w\.\-])/n, ESCAPES)
50
+ "#{first}#{str}"
51
+ end
52
+
53
+ def from_anchor(str)
54
+ str = str.dup
55
+ first = ''
56
+ str.sub!(/\AZ([a-f0-9]{2})/n, '') and first = -$1.hex.chr
57
+ str.gsub!(/::([a-f0-9]{2})/n) { $1.hex.chr }
58
+ str.tr!(':', '/')
59
+ "#{first}#{str}"
60
+ end
61
+
62
+ def response(code, headers = {})
63
+ headers['Content-Type'] ||= 'text/html; charset=UTF-8'
64
+ [ 200, headers, self ]
65
+ end
66
+ end