repobrowse 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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