Author | Jakob Wakeling <[email protected]> |
Date | 2024-07-06 11:32:05 |
Commit | a0ac27c1150c392bcc36911016fd12b2ddac0d1f |
Parent | 2954edd7dff67c4d435e513612e741d9483bba35 |
Add Git blame page for files
Diffstat
M | README.md | | | 2 | +- |
M | go.mod | | | 16 | ++++++++-------- |
M | go.sum | | | 40 | ++++++++++++++++++++-------------------- |
A | res/repo/blame.html | | | 31 | +++++++++++++++++++++++++++++++ |
M | res/repo/file.html | | | 4 | +++- |
M | res/repo/tree.html | | | 4 | +++- |
M | res/res.go | | | 3 | +++ |
M | src/goit/git.go | | | 15 | +++++++++++++++ |
M | src/goit/http.go | | | 1 | + |
M | src/main.go | | | 4 | ++++ |
A | src/repo/blame.go | | | 178 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
M | src/repo/file.go | | | 65 | ++++++++++++++++++++++++++++++++--------------------------------- |
M | src/repo/highlight.go | | | 4 | ++-- |
M | src/repo/tree.go | | | 16 | +++++++++++++--- |
M | src/util/log.go | | | 16 | +++++++++++++++- |
15 files changed, 329 insertions, 70 deletions
diff --git a/README.md b/README.md index 0510319..64d9832 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Note that at present, compatibility between updates is not guaranteed. - Git SSH protocol (planned) - Repository log, tree, refs, and commit viewers - File viewer with syntax highlighting -- File log, blame (planned), and raw views +- File log, blame, and raw views - Public and private repositories - Read and write permissions for non owners (planned) - Repository importing and mirroring diff --git a/go.mod b/go.mod index 80ac4e3..cef3f9c 100644 --- a/go.mod +++ b/go.mod @@ -4,22 +4,22 @@ go 1.22 require ( github.com/alecthomas/chroma v0.10.0 - github.com/buildkite/terminal-to-html/v3 v3.11.0 + github.com/buildkite/terminal-to-html/v3 v3.13.0 github.com/dustin/go-humanize v1.0.1 - github.com/go-chi/chi/v5 v5.0.12 + github.com/go-chi/chi/v5 v5.1.0 github.com/go-git/go-git/v5 v5.12.0 github.com/gorilla/csrf v1.7.2 github.com/mattn/go-sqlite3 v1.14.22 - golang.org/x/crypto v0.22.0 + golang.org/x/crypto v0.25.0 ) require ( dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.0.0 // indirect - github.com/cloudflare/circl v1.3.7 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect - github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/cloudflare/circl v1.3.9 // indirect + github.com/cyphar/filepath-securejoin v0.2.5 // indirect + github.com/dlclark/regexp2 v1.11.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect @@ -31,7 +31,7 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/skeema/knownhosts v1.2.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 0dc51e1..1af3e22 100644 --- a/go.sum +++ b/go.sum @@ -11,20 +11,20 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/buildkite/terminal-to-html/v3 v3.11.0 h1:wMTpKgR61lqmxMz1FKjCaW5mq6DqeEgFZdJ+SU4hP30= -github.com/buildkite/terminal-to-html/v3 v3.11.0/go.mod h1:8JACDet3vmvWLsL4IBobweQYtf19W5J+EKM3LEE1c+4= +github.com/buildkite/terminal-to-html/v3 v3.13.0 h1:TBRfvqZWoIpxxiiM9rdIn2bbI7pwxBjlQf8/cbLJnss= +github.com/buildkite/terminal-to-html/v3 v3.13.0/go.mod h1:33sojHDaRBSMwwkKXPEkb5Uc7LKF79rWGurL3ei/GX0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= -github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= +github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/cyphar/filepath-securejoin v0.2.5 h1:6iR5tXJ/e6tJZzzdMc1km3Sa7RRIVBKAK32O2s7AYfo= +github.com/cyphar/filepath-securejoin v0.2.5/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= -github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dlclark/regexp2 v1.11.1 h1:CJs78ewKXO9PuNf6Xwlw6eibMadBkXTRpOeUdv+IcWM= +github.com/dlclark/regexp2 v1.11.1/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= @@ -33,8 +33,8 @@ github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= -github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= -github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= @@ -95,8 +95,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -106,8 +106,8 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -124,15 +124,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -140,8 +140,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/res/repo/blame.html b/res/repo/blame.html new file mode 100644 index 0000000..c98a1f7 --- /dev/null +++ b/res/repo/blame.html @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html lang="en"> + <head>{{template "base/head" .}}</head> + <body> + <header> + {{template "repo/header" .}}<hr> + {{.PathHTML}} ({{.LineC}}, {{.Size}}) {{.Mode}} + <a href="/{{.Name}}/file/{{.Path}}">file</a> + <a href="/{{.Name}}/download/{{.Path}}">download</a> + </header><hr> + <main> + <table class="highlight-row"> + {{if .Blines}} + {{range $i, $l := .Blines}} + <tr> + <td><a href="/{{$.Name}}/commit/{{.Hash}}">{{.ShortHash}}</a></td> + <td>{{.Author}}</td> + <td>{{.Date}}</td> + <td class="lnum" style="text-align: right;"> + <pre><a id="{{$i}}" href="#{{$i}}">{{$i}}</a></pre> + </td> + <td class="line"><pre>{{.LineHTML}}</pre></td> + </tr> + {{end}} + {{else}} + <tr><td>Binary file</td></tr> + {{end}} + </table> + </main> + </body> +</html> diff --git a/res/repo/file.html b/res/repo/file.html index 5009c56..7754abb 100644 --- a/res/repo/file.html +++ b/res/repo/file.html @@ -4,7 +4,9 @@ <body> <header> {{template "repo/header" .}}<hr> - {{.HtmlPath}} ({{.LineC}}, {{.Size}}) {{.Mode}} <a href="/{{.Name}}/download/{{.Path}}">download</a> + {{.HtmlPath}} ({{.LineC}}, {{.Size}}) {{.Mode}} + {{if .IsText}}<a href="/{{.Name}}/blame/{{.Path}}">blame</a>{{end}} + <a href="/{{.Name}}/download/{{.Path}}">download</a> </header><hr> <main> <table> diff --git a/res/repo/tree.html b/res/repo/tree.html index 809fc4a..023833c 100644 --- a/res/repo/tree.html +++ b/res/repo/tree.html @@ -27,7 +27,9 @@ {{if .RawPath}} <a href="/{{$.Name}}/log/{{.RawPath}}">log</a> {{if .IsFile}} - blame + {{if .IsText}} + <a href="/{{$.Name}}/blame/{{.RawPath}}">blame</a> + {{end}} <a href="/{{$.Name}}/raw/{{.RawPath}}">raw</a> {{end}} <a href="/{{$.Name}}/download/{{.RawPath}}">download</a> diff --git a/res/res.go b/res/res.go index af125bd..772f4e6 100644 --- a/res/res.go +++ b/res/res.go @@ -70,6 +70,9 @@ var RepoTree string //go:embed repo/file.html var RepoFile string +//go:embed repo/blame.html +var RepoBlame string + //go:embed repo/refs.html var RepoRefs string diff --git a/src/goit/git.go b/src/goit/git.go index 5464296..101eb8a 100644 --- a/src/goit/git.go +++ b/src/goit/git.go @@ -359,3 +359,18 @@ func CommitCount(repo, branch string, hash plumbing.Hash) (uint64, error) { return count, nil } + +func GetFileType(file *object.File) (string, error) { + rc, err := file.Blob.Reader() + if err != nil { + return "", err + } + defer rc.Close() + + buf := make([]byte, min(file.Size, 512)) + if _, err := rc.Read(buf); err != nil { + return "", err + } + + return http.DetectContentType(buf), nil +} diff --git a/src/goit/http.go b/src/goit/http.go index 1fea509..62f81ed 100644 --- a/src/goit/http.go +++ b/src/goit/http.go @@ -40,6 +40,7 @@ func init() { template.Must(Tmpl.New("repo/commit").Parse(res.RepoCommit)) template.Must(Tmpl.New("repo/tree").Parse(res.RepoTree)) template.Must(Tmpl.New("repo/file").Parse(res.RepoFile)) + template.Must(Tmpl.New("repo/blame").Parse(res.RepoBlame)) template.Must(Tmpl.New("repo/refs").Parse(res.RepoRefs)) } diff --git a/src/main.go b/src/main.go index 590a187..879c6e5 100644 --- a/src/main.go +++ b/src/main.go @@ -313,6 +313,10 @@ found: rctx.URLParams.Add("*", strings.TrimPrefix(spath, "/raw/")) protect(http.HandlerFunc(repo.HandleRaw)).ServeHTTP(w, r) + case strings.HasPrefix(spath, "/blame/"): + rctx.URLParams.Add("*", strings.TrimPrefix(spath, "/blame/")) + protect(http.HandlerFunc(repo.HandleBlame)).ServeHTTP(w, r) + case strings.HasPrefix(spath, "/download"): rctx.URLParams.Add("*", strings.TrimLeft(strings.TrimPrefix(spath, "/download"), "/")) protect(http.HandlerFunc(repo.HandleDownload)).ServeHTTP(w, r) diff --git a/src/repo/blame.go b/src/repo/blame.go new file mode 100644 index 0000000..c42d3bb --- /dev/null +++ b/src/repo/blame.go @@ -0,0 +1,178 @@ +// Copyright (C) 2023, Jakob Wakeling +// All rights reserved. + +package repo + +import ( + "errors" + "fmt" + "html/template" + "net/http" + "path" + "strings" + + "github.com/Jamozed/Goit/src/goit" + "github.com/Jamozed/Goit/src/util" + "github.com/dustin/go-humanize" + "github.com/go-chi/chi/v5" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" +) + +func HandleBlame(w http.ResponseWriter, r *http.Request) { + auth, user, err := goit.Auth(w, r, true) + if err != nil { + util.PrintFuncError(err) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + tpath := chi.URLParam(r, "*") + + repo, err := goit.GetRepoByName(chi.URLParam(r, "repo")) + if err != nil { + goit.HttpError(w, http.StatusInternalServerError) + return + } else if repo == nil || !goit.IsVisible(repo, auth, user) { + goit.HttpError(w, http.StatusNotFound) + return + } + + type Bline struct { + Hash, ShortHash, Author, Date, Line string + LineHTML template.HTML + } + + data := struct { + HeaderFields + Title, Path, LineC, Size, Mode string + Blines []Bline + PathHTML template.HTML + }{ + Title: repo.Name + " - " + tpath, + HeaderFields: GetHeaderFields(auth, user, repo, r.Host), + } + + gr, err := git.PlainOpen(goit.RepoPath(repo.Name, true)) + if err != nil { + util.PrintFuncError(err) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + ref, err := gr.Head() + if errors.Is(err, plumbing.ErrReferenceNotFound) { + goit.HttpError(w, http.StatusNotFound) + return + } else if err != nil { + util.PrintFuncError(err) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + if readme, _ := findPattern(gr, ref, readmePattern); readme != "" { + data.Readme = path.Join("/", repo.Name, "file", readme) + } + if licence, _ := findPattern(gr, ref, licencePattern); licence != "" { + data.Licence = path.Join("/", repo.Name, "file", licence) + } + + commit, err := gr.CommitObject(ref.Hash()) + if err != nil { + util.PrintFuncError(err) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + file, err := commit.File(tpath) + if errors.Is(err, object.ErrFileNotFound) { + goit.HttpError(w, http.StatusNotFound) + return + } else if err != nil { + util.PrintFuncError(err) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + data.Mode = util.ModeString(uint32(file.Mode)) + data.Path = file.Name + data.Size = humanize.IBytes(uint64(file.Size)) + + parts := strings.Split(file.Name, "/") + htmlPath := "<b style=\"padding-left: 0.4rem;\"><a href=\"/" + repo.Name + "/tree\">" + repo.Name + "</a></b>/" + dirPath := "" + + for i := 0; i < len(parts)-1; i += 1 { + dirPath = path.Join(dirPath, parts[i]) + htmlPath += "<a href=\"/" + repo.Name + "/tree/" + dirPath + "\">" + parts[i] + "</a>/" + } + htmlPath += parts[len(parts)-1] + + data.PathHTML = template.HTML(htmlPath) + + ftype, err := goit.GetFileType(file) + if err != nil { + util.PrintFuncError(err) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + /* Only populate blines for text files */ + if strings.HasPrefix(ftype, "text") { + rc, err := file.Blob.Reader() + if err != nil { + util.PrintFuncError(err) + goit.HttpError(w, http.StatusInternalServerError) + return + } + defer rc.Close() + + buf := make([]byte, min(file.Size, (10*1024*1024))) + if _, err := rc.Read(buf); err != nil { + util.PrintFuncError(err) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + body, _, err := Highlight(file.Name, string(buf), true) + if err != nil { + util.PrintFuncError(err) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + blame, err := git.Blame(commit, tpath) + if err != nil { + util.PrintFuncError(err) + goit.HttpError(w, http.StatusInternalServerError) + return + } + + htmlLines := strings.Split(body, "\n") + + for i, bline := range blame.Lines { + data.Blines = append(data.Blines, Bline{ + Hash: bline.Hash.String(), + ShortHash: bline.Hash.String()[:7], + Author: bline.AuthorName, + Date: bline.Date.Format("2006-01-02 15:04:05"), + Line: bline.Text, + }) + + if i < len(htmlLines) { + htmlLines[i] = strings.TrimPrefix(htmlLines[i], "</span></span>") + htmlLines[i] += "</span></span>" + data.Blines[i].LineHTML = template.HTML(htmlLines[i]) + } + } + + data.Blines = append(data.Blines, Bline{}) + } + + data.LineC = fmt.Sprint(len(data.Blines), " lines") + + if err := goit.Tmpl.ExecuteTemplate(w, "repo/blame", data); err != nil { + util.PrintFuncError(err) + } +} diff --git a/src/repo/file.go b/src/repo/file.go index 01badea..161573e 100644 --- a/src/repo/file.go +++ b/src/repo/file.go @@ -7,8 +7,6 @@ import ( "errors" "fmt" "html/template" - "io" - "log" "net/http" "path" "strings" @@ -25,7 +23,7 @@ import ( func HandleFile(w http.ResponseWriter, r *http.Request) { auth, user, err := goit.Auth(w, r, true) if err != nil { - log.Println("[admin]", err.Error()) + util.PrintFuncError(err) goit.HttpError(w, http.StatusInternalServerError) } @@ -45,6 +43,7 @@ func HandleFile(w http.ResponseWriter, r *http.Request) { Title, Path, LineC, Size, Mode string Lines []string HtmlBody, HtmlPath, BodyCss template.HTML + IsText bool }{ Title: repo.Name + " - " + tpath, HeaderFields: GetHeaderFields(auth, user, repo, r.Host), @@ -52,7 +51,7 @@ func HandleFile(w http.ResponseWriter, r *http.Request) { gr, err := git.PlainOpen(goit.RepoPath(repo.Name, true)) if err != nil { - log.Println("[/repo/file]", err.Error()) + util.PrintFuncError(err) goit.HttpError(w, http.StatusInternalServerError) return } @@ -62,7 +61,7 @@ func HandleFile(w http.ResponseWriter, r *http.Request) { goit.HttpError(w, http.StatusNotFound) return } else if err != nil { - log.Println("[/repo/file]", err.Error()) + util.PrintFuncError(err) goit.HttpError(w, http.StatusInternalServerError) return } @@ -76,7 +75,7 @@ func HandleFile(w http.ResponseWriter, r *http.Request) { commit, err := gr.CommitObject(ref.Hash()) if err != nil { - log.Println("[/repo/file]", err.Error()) + util.PrintFuncError(err) goit.HttpError(w, http.StatusInternalServerError) return } @@ -86,7 +85,7 @@ func HandleFile(w http.ResponseWriter, r *http.Request) { goit.HttpError(w, http.StatusNotFound) return } else if err != nil { - log.Println("[/repo/file]", err.Error()) + util.PrintFuncError(err) goit.HttpError(w, http.StatusInternalServerError) return } @@ -107,46 +106,46 @@ func HandleFile(w http.ResponseWriter, r *http.Request) { data.HtmlPath = template.HTML(htmlPath) - if rc, err := file.Blob.Reader(); err != nil { - log.Println("[/repo/file]", err.Error()) + ftype, err := goit.GetFileType(file) + if err != nil { + util.PrintFuncError(err) goit.HttpError(w, http.StatusInternalServerError) return - } else { - buf := make([]byte, min(file.Size, 512)) + } + + /* Only populate lines for text files */ + if strings.HasPrefix(ftype, "text") { + rc, err := file.Blob.Reader() + if err != nil { + util.PrintFuncError(err) + goit.HttpError(w, http.StatusInternalServerError) + return + } + defer rc.Close() + buf := make([]byte, min(file.Size, (10*1024*1024))) if _, err := rc.Read(buf); err != nil { - log.Println("[/repo/file]", err.Error()) + util.PrintFuncError(err) goit.HttpError(w, http.StatusInternalServerError) return } - if strings.HasPrefix(http.DetectContentType(buf), "text") { - buf2 := make([]byte, min(file.Size-int64(len(buf)), (10*1024*1024)-int64(len(buf)))) - if _, err := rc.Read(buf2); err != nil && !errors.Is(err, io.EOF) { - log.Println("[/repo/file]", err.Error()) - goit.HttpError(w, http.StatusInternalServerError) - return - } - - body := string(append(buf, buf2...)) - buf, css, err := Highlight(file.Name, body) - if err != nil { - log.Println("[/repo/file]", err.Error()) - goit.HttpError(w, http.StatusInternalServerError) - return - } - - data.HtmlBody = template.HTML(buf) - data.BodyCss = template.HTML("<style>" + css + "</style>") - data.Lines = strings.Split(body, "\n") + body, css, err := Highlight(file.Name, string(buf), false) + if err != nil { + util.PrintFuncError(err) + goit.HttpError(w, http.StatusInternalServerError) + return } - rc.Close() + data.HtmlBody = template.HTML(body) + data.BodyCss = template.HTML("<style>" + css + "</style>") + data.Lines = strings.Split(string(buf), "\n") + data.IsText = true } data.LineC = fmt.Sprint(len(data.Lines), " lines") if err := goit.Tmpl.ExecuteTemplate(w, "repo/file", data); err != nil { - log.Println("[/repo/file]", err.Error()) + util.PrintFuncError(err) } } diff --git a/src/repo/highlight.go b/src/repo/highlight.go index 16e75ca..2be1ef0 100644 --- a/src/repo/highlight.go +++ b/src/repo/highlight.go @@ -12,7 +12,7 @@ import ( "github.com/alecthomas/chroma/styles" ) -func Highlight(name, input string) (string, string, error) { +func Highlight(name, input string, splittable bool) (string, string, error) { var buf, css bytes.Buffer lexer := lexers.Match(name) @@ -20,7 +20,7 @@ func Highlight(name, input string) (string, string, error) { lexer = lexers.Fallback } - formatter := html.New(html.WithClasses(true)) + formatter := html.New(html.WithClasses(!splittable), html.PreventSurroundingPre(splittable)) iter, err := lexer.Tokenise(nil, input) if err != nil { diff --git a/src/repo/tree.go b/src/repo/tree.go index 22e307f..3a14e26 100644 --- a/src/repo/tree.go +++ b/src/repo/tree.go @@ -41,7 +41,7 @@ func HandleTree(w http.ResponseWriter, r *http.Request) { type row struct { Mode, Name, Path, RawPath, Size string - IsFile, B bool + IsFile, IsText, B bool } data := struct { HeaderFields @@ -129,7 +129,7 @@ func HandleTree(w http.ResponseWriter, r *http.Request) { for _, v := range tree.Entries { var fpath, rpath, size string - var isFile bool + var isFile, isText bool if v.Mode&0o40000 == 0 { fpath = path.Join("file", tpath, v.Name) @@ -157,6 +157,16 @@ func HandleTree(w http.ResponseWriter, r *http.Request) { size = humanize.IBytes(sz) totalSize += sz + + file, err := tree.File(v.Name) + if err == nil { + ftype, err := goit.GetFileType(file) + if err == nil { + if strings.HasPrefix(ftype, "text") { + isText = true + } + } + } } else { fpath = path.Join("tree", tpath, v.Name) rpath = path.Join(tpath, v.Name) @@ -196,7 +206,7 @@ func HandleTree(w http.ResponseWriter, r *http.Request) { data.Files = append(data.Files, row{ Mode: util.ModeString(uint32(v.Mode)), Name: v.Name, Path: fpath, RawPath: rpath, Size: size, - IsFile: isFile, B: util.If(strings.HasSuffix(size, " B"), true, false), + IsFile: isFile, IsText: isText, B: util.If(strings.HasSuffix(size, " B"), true, false), }) } diff --git a/src/util/log.go b/src/util/log.go index d22fcb6..fa149fe 100644 --- a/src/util/log.go +++ b/src/util/log.go @@ -3,10 +3,24 @@ package util -import "log" +import ( + "log" + "runtime" +) var Debug = false +func PrintFuncError(err error) { + pc, _, _, ok := runtime.Caller(1) + if !ok { + log.Println(err) + return + } + + fn := runtime.FuncForPC(pc) + log.Printf("[%s] %s\n", fn.Name(), err.Error()) +} + func Debugln(v ...any) { if Debug { var a = []any{"\033[34m[DEBUG]\033[0m"}