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,320 @@
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
+
7
+ class Repobrowse::GitCommitHTML < Repobrowse::HTML
8
+
9
+ # rugged doesn't seem to have a way to show diffstats, decorations
10
+ # or combined diffs (--cc/--combined) for merges, so use git-show here
11
+ CMT_CMD = %w(show -z --numstat -p --encoding=UTF-8 --pretty=format:%p%n%d%x00)
12
+
13
+ def commit_header(env, repo, cmt)
14
+ msg = Rugged.prettify_message(cmt.message)
15
+ subject, body = msg.split(/\r?\n\r?\n/, 2)
16
+ subject.strip!
17
+ ht(subject)
18
+ start(subject, repo)
19
+ parents = cmt.parents
20
+ oid = @commit = cmt.oid
21
+ @buf << ' commit ' + oid
22
+ @mhelp = nil
23
+ case parents.size
24
+ when 0
25
+ when 1
26
+ @buf << %Q[ (<a\nhref="#{oid}.patch">patch</a>)]
27
+ pfx = ' parent'
28
+ else
29
+ @mhelp = "\n This is a merge, showing combined diff:\n\n"
30
+ pfx = ' parents'
31
+ end
32
+
33
+ @buf << "\n "
34
+ @buf << %Q(tree <a\nrel=nofollow\nhref="src/#{oid}">#{cmt.tree_oid}</a>\n)
35
+ pad = 0
36
+ idents = [ ' author', 'committer' ].map do |field|
37
+ x = cmt.__send__(field.strip)
38
+ name_email = ht(+"#{x[:name]} <#{x[:email]}>")
39
+ len = name_email.size
40
+ pad = len if len > pad
41
+ [ field, name_email, x[:time].strftime('%Y-%m-%d %k:%M:%S %z') ]
42
+ end
43
+ idents.each do |field, name_email, time|
44
+ @buf << "#{field} #{[name_email].pack("A#{pad}")}\t#{time}\n"
45
+ end
46
+ @repo = repo
47
+ cmd = CMT_CMD.dup
48
+ cmd << @commit
49
+ cmd << '--'
50
+ @rd = repo.driver.popen(cmd, encoding: Encoding::UTF_8)
51
+ abbr = @rd.gets(chomp: true).split(' ')
52
+ parents.each_with_index do |pt, i|
53
+ title = Rugged.prettify_message(pt.summary)
54
+ title.strip!
55
+ @buf << %Q(#{pfx} <a id=P#{i}\nhref="#{pt.oid}">#{abbr[i]}</a> #{ht(title)}\n)
56
+ pfx = ' '
57
+ end
58
+ refnames = @rd.gets("\0", chomp: true)
59
+ @buf << "\n<b>#{subject}</b>#{ht(refnames)}\n\n"
60
+ @buf << ht(body) if body
61
+ @buf << "<a\nid=D>---</a>\n"
62
+ @anchors = {}
63
+ @parents = parents
64
+ @nchg = @nadd = @ndel = 0
65
+ @state = :stat_begin
66
+ end
67
+
68
+ # do not break anchor links if the combined diff doesn't show changes:
69
+ def show_unchanged
70
+ unchanged = @anchors.keys.sort!
71
+ unchanged[0] or return ''
72
+ buf = +<<EOS
73
+
74
+ There are uninteresting changes from this merge.
75
+ See the <a\nhref="#P0">parents</a>, or view final state(s) below:
76
+
77
+ EOS
78
+
79
+ unchanged.each do |anchor|
80
+ fn = @repo.driver.git_unquote(@anchors[anchor])
81
+ href = ha(+"src/#@commit:#{fn}")
82
+ buf << "\t<a\nrel=nofollow\nid=#{
83
+ anchor}
84
+ }\nhref=#{href}>#{ht(+fn)}</a>\n"
85
+ end
86
+ buf
87
+ end
88
+
89
+ def diffstat_rename_line(from, to)
90
+ anchor = -to_anchor(to)
91
+ @anchors[anchor] = to
92
+ from_parts = from.split('/')
93
+ to_parts = to.split('/')
94
+ base = []
95
+ while to_parts[0] && to_parts[0] == from_parts[0]
96
+ base << to_parts.shift
97
+ from_parts.shift
98
+ end
99
+ from = from_parts.join('/')
100
+ to = to_parts.join('/')
101
+ to = %Q(<a\nhref="##{anchor}">#{ht(to)}</a>)
102
+ if base[0]
103
+ base = ht(base.join('/'))
104
+ "#{base}/{#{from} =&gt; #{to}}"
105
+ else
106
+ "#{from} =&gt; #{to}"
107
+ end
108
+ end
109
+
110
+ def diffstat_line(line)
111
+ line =~ /\A(\S+)\t+(\S+)\t+(.*)/ or die("bad stat line: #{line.inspect}")
112
+ add = -$1
113
+ del = -$2
114
+ fn = -$3
115
+ if fn != '' # normal modification
116
+ anchor = -to_anchor(fn)
117
+ @anchors[anchor] = -fn
118
+ line = %Q(<a\nhref="##{anchor}">#{ht(fn.dup)}</a>)
119
+ else # rename
120
+ from = @rd.gets("\0", chomp: true) or die('EOF rename (from)')
121
+ to = @rd.gets("\0", chomp: true) or die('EOF rename (to)')
122
+ line = diffstat_rename_line(from, to);
123
+ end
124
+
125
+ # text changes show numerically, Binary does not
126
+ if add =~ /\A\d+\z/ && del =~ /\A\d+\z/
127
+ @nadd += add.to_i
128
+ @ndel += del.to_i
129
+ add = "+#{add}"
130
+ del = "-#{del}"
131
+ else # just in case...
132
+ ht(add)
133
+ ht(del)
134
+ end
135
+ @nchg += 1
136
+ " #{sprintf('% 6s/%-6s', del, add)}\t#{line}\n"
137
+ end
138
+
139
+ def diffstat_end
140
+ ret = +"\n #@nchg "
141
+ ret << (@nchg == 1 ? 'file changed, ' : 'files changed, ')
142
+ ret << @nadd.to_s
143
+ ret << (@nadd == 1 ? ' insertion(+), ' : ' insertions(+), ')
144
+ ret << @ndel.to_s
145
+ ret << (@ndel == 1 ? " deletion(-)\n\n" : " deletions(-)\n\n")
146
+ end
147
+
148
+ def die(msg)
149
+ raise RuntimeError, "#{msg} (#@commit)", []
150
+ end
151
+
152
+ # diff --git a/foo.c b/bar.c
153
+ def git_diff_ab_hdr(fa, fb)
154
+ html_a = ht(fa.dup)
155
+ html_b = ht(fb.dup)
156
+ fa = @repo.driver.git_unquote(fa)
157
+ fb = @repo.driver.git_unquote(fb)
158
+ fa.sub!(%r{\Aa/}, '')
159
+ fb.sub!(%r{\Ab/}, '')
160
+ anchor = -to_anchor(fb)
161
+ @anchors.delete(anchor)
162
+ @fa = fa
163
+ @fb = fb
164
+ # not wasting bandwidth on links here
165
+ # links in hunk headers are far more useful with line offsets
166
+ %Q(<a\nid="#{anchor}">diff</a> --git #{html_a} #{html_b}\n)
167
+ end
168
+
169
+ # diff (--cc|--combined)
170
+ def git_diff_cc_hdr(combined, path)
171
+ html_path = ht(path.dup)
172
+ path = @repo.driver.git_unquote(path)
173
+ anchor = to_anchor(path)
174
+ @anchors.delete(anchor)
175
+ @path_cc = path
176
+ %Q(<a\nid="#{anchor}">diff</a> --#{combined} #{html_path}\n)
177
+ end
178
+
179
+ # index abcdef89..01234567
180
+ def git_diff_ab_index(da, db, tail)
181
+ # not wasting bandwidth on links here, yet
182
+ # links in hunk headers are far more useful with line offsets
183
+ "index #{da}..#{db}#{ht(tail)}\n"
184
+ end
185
+
186
+ def git_diff_src_link(ref, file, lineno, text)
187
+ fragment = lineno ? "#n#{lineno}" : ''
188
+ href = ha(+"src/#{ref}:#{file}#{fragment}")
189
+ %Q(<a\nrel=nofollow\nhref=#{href}>#{text}</a>)
190
+ end
191
+
192
+ # @@ -1,2 +3,4 @@ (regular diff)
193
+ def git_diff_ab_hunk(ca, cb, ctx)
194
+ na = ca.match(/\A-(\d+)/)[1]
195
+ nb = cb.match(/\A\+(\d+)/)[1]
196
+
197
+ # we add "rel=nofollow" here to reduce load on search engines, here
198
+ rv = +'@@ '
199
+ rv << (na == '0' ? ca : git_diff_src_link(@parents[0], @fa, na, ca))
200
+ rv << ' '
201
+ rv << (nb == '0' ? cb : git_diff_src_link(@commit, @fb, nb, cb))
202
+ rv << " @@#{ht(ctx)}\n"
203
+ end
204
+
205
+ # index abcdef09,01234567..76543210
206
+ def git_diff_cc_index(before, last, tail)
207
+ ht(tail)
208
+ @parent_objs_cc = before.split(',')
209
+
210
+ # not wasting bandwidth on links here, yet
211
+ # links in hunk headers are far more useful with line offsets
212
+ "index #{before}..#{last}#{tail}\n"
213
+ end
214
+
215
+ # @@@ -1,2 -3,4 +5,6 @@@ (combined diff)
216
+ def git_diff_cc_hunk(at, offs, ctx)
217
+ offs = offs.split(' ')
218
+ last = offs.pop
219
+ rv = at.dup
220
+
221
+ offs.each_with_index do |off, i|
222
+ parent = @parents[i]
223
+ blob = @parent_objs_cc[i]
224
+ lineno = off.match(/\A-(\d+)/)[1]
225
+
226
+ if lineno == '0' # new file (does this happen with --cc?)
227
+ rv << " #{off}"
228
+ else
229
+ href = ha(+"src/#{parent}?id=#{blob}#n#{lineno}")
230
+ rv << %Q( <a\nhref=#{href}>#{off}</a>)
231
+ end
232
+ end
233
+
234
+ lineno = last.match(/\A\+(\d+)/)[1]
235
+ rv << ' '
236
+ if lineno == '0' # deleted file (does this happen with --cc?)
237
+ rv << last
238
+ else
239
+ rv << git_diff_src_link(@commit, @path_cc, lineno, last)
240
+ end
241
+ rv << " #{at}#{ht(ctx)}\n"
242
+ end
243
+
244
+ def diff_line(line)
245
+ # dfa and dfb class names match public-inbox search term prefix
246
+ case line
247
+ when /\A\+/
248
+ %Q{<span\nclass="dfa">#{ht(line.chomp!)}</span>\n}
249
+ when /\A\-/
250
+ %Q{<span\nclass="dfb">#{ht(line.chomp!)}</span>\n}
251
+ when %r{\Adiff --git ("?a/.+) ("?b/.+)\n\z} # regular
252
+ git_diff_ab_hdr($1, $2)
253
+ when /\Adiff --(cc|combined) (.+)\n\z/ # merge
254
+ git_diff_cc_hdr($1, $2)
255
+ when /\Aindex ([a-f0-9]+)\.\.([a-f0-9]+)(.*)\n\z/ # regular
256
+ git_diff_ab_index($1, $2, $3)
257
+ when /\A@@ ([\d,\+\-]+) ([\d,\+\-]+) @@(.*)\n\z/ # regular
258
+ git_diff_ab_hunk($1, $2, $3)
259
+ when /\Aindex ([a-f0-9]+,[^\.]+)\.\.([a-f0-9]+)(.*)\n\z/ # --cc
260
+ git_diff_cc_index($1, $2, $3)
261
+ when /\A(@@@+) (\S+.*\S+) @@@+(.*)\n\z/ # --cc
262
+ git_diff_cc_hunk($1, $2, $3)
263
+ when nil
264
+ diff_done
265
+ else
266
+ ht(line)
267
+ end
268
+ end
269
+
270
+ def diff_done
271
+ buf, @mhelp = @mhelp, nil
272
+ @state = :done
273
+ "#{buf}#{show_unchanged}</pre></body></html>"
274
+ end
275
+
276
+ # called by the Rack server
277
+ def each
278
+ buf = @buf
279
+ @buf = nil
280
+ yield buf
281
+ buf.clear
282
+ while buf = each_i
283
+ yield buf
284
+ buf.clear unless buf.frozen?
285
+ end
286
+ end
287
+
288
+ def each_i
289
+ case @state
290
+ when :stat_begin
291
+ # merges start with an extra '\0' before the diffstat
292
+ # non-merge commits start with an extra '\n', instead
293
+ sep = @mhelp ? "\0" : "\n"
294
+ @rd.gets(sep) == sep or die('diffstat line not empty')
295
+ @state = :stat
296
+ when :stat
297
+ case line = @rd.gets("\0", chomp: true)
298
+ when nil
299
+ if @mhelp
300
+ @mhelp = "\n This is a merge, and the combined diff is empty.\n"
301
+ return diff_done
302
+ end
303
+ die('premature EOF')
304
+ when ''
305
+ @state = :diff
306
+ return diffstat_end
307
+ else
308
+ return diffstat_line(line)
309
+ end
310
+ when :diff
311
+ return diff_line(@rd.gets)
312
+ when :done
313
+ return
314
+ end while true
315
+ end
316
+
317
+ def close
318
+ @rd = @rd&.close
319
+ end
320
+ end
@@ -0,0 +1,21 @@
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
+ module Repobrowse::GitDisambiguate
6
+ def git_disambiguate(r, repo, pfx, ref, path)
7
+ ambiguous = false
8
+ ambiguous = true if ref.squeeze!('/')
9
+ ambiguous = true if ref.chomp!('/')
10
+ if path
11
+ ambiguous = true if path.chomp!('/')
12
+ ambiguous = true if path.squeeze!('/')
13
+ end
14
+ if ambiguous
15
+ loc = r.base_url
16
+ loc << "#{r.script_name}/#{repo.name}/#{pfx}/#{ref}"
17
+ loc << ":#{path}" if path
18
+ r.redirect(loc) # throws
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,109 @@
1
+ # Copyright (C) 2017-2018 all contributors <repobrowse-public@80x24.org>
2
+ # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
3
+ # frozen_string_literal: true
4
+
5
+ require 'time'
6
+
7
+ # provides smart HTTP cloning for git repos
8
+ module Repobrowse::GitHTTPBackend
9
+
10
+ # used to provide streaming Rack response body
11
+ class EachWrap
12
+ def initialize(io)
13
+ @io = io
14
+ end
15
+
16
+ def each
17
+ begin
18
+ buf = @io.readpartial(16384, buf)
19
+ yield buf
20
+ rescue EOFError
21
+ return
22
+ ensure
23
+ buf&.clear
24
+ end while true
25
+ end
26
+
27
+ def close
28
+ @io.close
29
+ end
30
+ end
31
+
32
+ def input_to_file(env)
33
+ tmp = Tempfile.new('git-http-backend-in')
34
+ tmp.unlink
35
+ tmp.sync = true
36
+ IO.copy_stream(env['rack.input'], tmp.to_io)
37
+ tmp.rewind
38
+ tmp
39
+ end
40
+
41
+ def run_http_backend(r, repo, path)
42
+ penv = {
43
+ 'GIT_HTTP_EXPORT_ALL' => '1',
44
+ 'PATH_TRANSLATED' => -"#{repo.path}/#{path}",
45
+ }
46
+ env = r.env
47
+ # GIT_COMMITTER_NAME, GIT_COMMITTER_EMAIL
48
+ # may be set in the server-process and are passed as-is
49
+ %w(QUERY_STRING REMOTE_USER REMOTE_ADDR HTTP_CONTENT_ENCODING
50
+ CONTENT_TYPE SERVER_PROTOCOL REQUEST_METHOD).each do |k|
51
+ v = env[k] and penv[k] = v
52
+ end
53
+ IO.popen(penv, %W(git http-backend), in: input_to_file(env))
54
+ rescue => e
55
+ r_err(r, "E: #{e.message} (#{e.class}) on #{repo.path}")
56
+ end
57
+
58
+ # returns undef if 403 so it falls back to dumb HTTP
59
+ def serve_smart(r, repo, path)
60
+ rd = run_http_backend(r, repo, path)
61
+ code = 200
62
+ h = {}
63
+
64
+ # parse CGI response headers
65
+ case rd.gets
66
+ when %r{\AStatus:\s+(\d+)}i
67
+ code = $1.to_i
68
+ when %r{\A([^:]+):\s*(.*)\r\n\z}
69
+ h[-$1] = -$2
70
+ when "\r\n"
71
+ break # headers done onto the body
72
+ when nil
73
+ rd.close
74
+ r_err(r, "unexpected EOF on git http-backend in #{repo.path}")
75
+ end while true
76
+
77
+ r.halt [ code, h, EachWrap.new(rd) ]
78
+ end
79
+
80
+ def git_http_backend_routes(r, repo)
81
+ r.is(path = 'git-upload-pack') { r.post { serve_smart(r, repo, path) } }
82
+ r.is(path = 'info/refs') {
83
+ # QUERY_STRING has exactly one parameter
84
+ # See https://80x24.org/git/src/v2.16.1:Documentation/technical/http-protocol.txt
85
+ if /\Aservice=git-\w+-pack\z/ =~ r.env['QUERY_STRING']
86
+ r.get { serve_smart(r, repo, path) }
87
+ else
88
+ static(r, -"#{repo.path}/#{path}", 'text/plain', nil)
89
+ end
90
+ }
91
+ %w(HEAD cloneurl description
92
+ objects/info/http-alternates
93
+ objects/info/alternates
94
+ objects/info/packs).each { |txt|
95
+ r.is(txt) { static(r, -"#{repo.path}/#{txt}", 'text/plain', nil) }
96
+ }
97
+ r.is('objects', :git_x2, :git_x38) { |x2, x38|
98
+ static(r, -"#{repo.path}/objects/#{x2}/#{x38}",
99
+ 'application/x-git-loose-object')
100
+ }
101
+ [ :git_pack, 'application/x-git-packed-objects',
102
+ :git_pack_idx, 'application/x-git-packed-objects-toc'
103
+ ].each_slice(2) { |sym, type|
104
+ r.is('objects/pack', :sym) { |o|
105
+ static(r, -"#{repo.path}/objects/pack/#{o}", type)
106
+ }
107
+ }
108
+ end
109
+ end