+27
cmd/legit/main.go
+27
cmd/legit/main.go
···
1
+
package main
2
+
3
+
import (
4
+
"flag"
5
+
"fmt"
6
+
"log"
7
+
"net/http"
8
+
9
+
"github.com/icyphox/bild/legit/config"
10
+
"github.com/icyphox/bild/legit/routes"
11
+
)
12
+
13
+
func main() {
14
+
var cfg string
15
+
flag.StringVar(&cfg, "config", "./config.yaml", "path to config file")
16
+
flag.Parse()
17
+
18
+
c, err := config.Read(cfg)
19
+
if err != nil {
20
+
log.Fatal(err)
21
+
}
22
+
23
+
mux := routes.Handlers(c)
24
+
addr := fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
25
+
log.Println("starting server on", addr)
26
+
log.Fatal(http.ListenAndServe(addr, mux))
27
+
}
+20
config.yaml
+20
config.yaml
···
1
+
repo:
2
+
scanPath: /home/icy/code/tmp/testrepos
3
+
readme:
4
+
- readme
5
+
- README
6
+
- readme.md
7
+
- README.md
8
+
mainBranch:
9
+
- master
10
+
- main
11
+
dirs:
12
+
templates: ./templates
13
+
static: ./static
14
+
meta:
15
+
title: icy does git
16
+
description: come get your free software
17
+
server:
18
+
name: github.com/icyphox/bild
19
+
host: 0.0.0.0
20
+
port: 5555
+26
flake.lock
+26
flake.lock
···
1
+
{
2
+
"nodes": {
3
+
"nixpkgs": {
4
+
"locked": {
5
+
"lastModified": 1718558927,
6
+
"narHash": "sha256-PRqvkPqX5luuZ0WcUbz2zATGp4IzybDU0K33MxO9Sd0=",
7
+
"owner": "nixos",
8
+
"repo": "nixpkgs",
9
+
"rev": "f82fe275d98c521c051af4892cd8b3406cee67a3",
10
+
"type": "github"
11
+
},
12
+
"original": {
13
+
"owner": "nixos",
14
+
"repo": "nixpkgs",
15
+
"type": "github"
16
+
}
17
+
},
18
+
"root": {
19
+
"inputs": {
20
+
"nixpkgs": "nixpkgs"
21
+
}
22
+
}
23
+
},
24
+
"root": "root",
25
+
"version": 7
26
+
}
+62
flake.nix
+62
flake.nix
···
1
+
{
2
+
description = "web frontend for git";
3
+
4
+
inputs.nixpkgs.url = "github:nixos/nixpkgs";
5
+
6
+
outputs =
7
+
{ self
8
+
, nixpkgs
9
+
,
10
+
}:
11
+
let
12
+
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
13
+
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
14
+
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
15
+
in
16
+
{
17
+
packages = forAllSystems (system:
18
+
let
19
+
pkgs = nixpkgsFor.${system};
20
+
legit = self.packages.${system}.legit;
21
+
files = pkgs.lib.fileset.toSource {
22
+
root = ./.;
23
+
fileset = pkgs.lib.fileset.unions [
24
+
./config.yaml
25
+
./static
26
+
./templates
27
+
];
28
+
};
29
+
in
30
+
{
31
+
legit = pkgs.buildGoModule {
32
+
name = "legit";
33
+
rev = "master";
34
+
src = ./.;
35
+
36
+
vendorHash = "sha256-ynv0pBdVPIhTz7RvCwVWr0vUWwfw+PEjFXs9PdQMqm8=";
37
+
};
38
+
docker = pkgs.dockerTools.buildLayeredImage {
39
+
name = "sini:5000/legit";
40
+
tag = "latest";
41
+
contents = [ files legit pkgs.git ];
42
+
config = {
43
+
Entrypoint = [ "${legit}/bin/legit" ];
44
+
ExposedPorts = { "5555/tcp" = { }; };
45
+
};
46
+
};
47
+
});
48
+
49
+
defaultPackage = forAllSystems (system: self.packages.${system}.legit);
50
+
devShells = forAllSystems (system:
51
+
let
52
+
pkgs = nixpkgsFor.${system};
53
+
in
54
+
{
55
+
default = pkgs.mkShell {
56
+
nativeBuildInputs = with pkgs; [
57
+
go
58
+
];
59
+
};
60
+
});
61
+
};
62
+
}
+44
go.mod
+44
go.mod
···
1
+
module github.com/icyphox/bild
2
+
3
+
go 1.22.0
4
+
5
+
require (
6
+
github.com/alecthomas/chroma/v2 v2.14.0
7
+
github.com/bluekeyes/go-gitdiff v0.8.0
8
+
github.com/dustin/go-humanize v1.0.1
9
+
github.com/go-chi/chi/v5 v5.2.0
10
+
github.com/go-git/go-git/v5 v5.12.0
11
+
github.com/microcosm-cc/bluemonday v1.0.27
12
+
github.com/russross/blackfriday/v2 v2.1.0
13
+
golang.org/x/sys v0.26.0
14
+
gopkg.in/yaml.v3 v3.0.1
15
+
)
16
+
17
+
require (
18
+
github.com/Microsoft/go-winio v0.6.2 // indirect
19
+
github.com/ProtonMail/go-crypto v1.0.0 // indirect
20
+
github.com/acomagu/bufpipe v1.0.4 // indirect
21
+
github.com/aymerick/douceur v0.2.0 // indirect
22
+
github.com/cloudflare/circl v1.4.0 // indirect
23
+
github.com/cyphar/filepath-securejoin v0.3.3 // indirect
24
+
github.com/dlclark/regexp2 v1.11.4 // indirect
25
+
github.com/emirpasic/gods v1.18.1 // indirect
26
+
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
27
+
github.com/go-git/go-billy/v5 v5.5.0 // indirect
28
+
github.com/google/go-cmp v0.6.0 // indirect
29
+
github.com/gorilla/css v1.0.1 // indirect
30
+
github.com/imdario/mergo v0.3.16 // indirect
31
+
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
32
+
github.com/kevinburke/ssh_config v1.2.0 // indirect
33
+
github.com/pjbgf/sha1cd v0.3.0 // indirect
34
+
github.com/sergi/go-diff v1.3.1 // indirect
35
+
github.com/skeema/knownhosts v1.3.0 // indirect
36
+
github.com/xanzy/ssh-agent v0.3.3 // indirect
37
+
golang.org/x/crypto v0.28.0 // indirect
38
+
golang.org/x/net v0.30.0 // indirect
39
+
gopkg.in/warnings.v0 v0.1.2 // indirect
40
+
)
41
+
42
+
replace github.com/sergi/go-diff => github.com/sergi/go-diff v1.1.0
43
+
44
+
replace github.com/go-git/go-git/v5 => github.com/go-git/go-git/v5 v5.6.1
+200
go.sum
+200
go.sum
···
1
+
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
2
+
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
3
+
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
4
+
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
5
+
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
6
+
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
7
+
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
8
+
github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
9
+
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
10
+
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
11
+
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
12
+
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
13
+
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
14
+
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
15
+
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
16
+
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
17
+
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
18
+
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
19
+
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
20
+
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
21
+
github.com/bluekeyes/go-gitdiff v0.8.0 h1:Nn1wfw3/XeKoc3lWk+2bEXGUHIx36kj80FM1gVcBk+o=
22
+
github.com/bluekeyes/go-gitdiff v0.8.0/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE=
23
+
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
24
+
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
25
+
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
26
+
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
27
+
github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY=
28
+
github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
29
+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
30
+
github.com/cyphar/filepath-securejoin v0.3.3 h1:lofZkCEVFIBe0KcdQOzFs8Soy9oaHOWl4gGtPI+gCFc=
31
+
github.com/cyphar/filepath-securejoin v0.3.3/go.mod h1:8s/MCNJREmFK0H02MF6Ihv1nakJe4L/w3WZLHNkvlYM=
32
+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
33
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
34
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
35
+
github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
36
+
github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
37
+
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
38
+
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
39
+
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
40
+
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
41
+
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
42
+
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
43
+
github.com/go-chi/chi/v5 v5.2.0 h1:Aj1EtB0qR2Rdo2dG4O94RIU35w2lvQSj6BRA4+qwFL0=
44
+
github.com/go-chi/chi/v5 v5.2.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
45
+
github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E=
46
+
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
47
+
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
48
+
github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0=
49
+
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
50
+
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
51
+
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
52
+
github.com/go-git/go-git-fixtures/v4 v4.3.1 h1:y5z6dd3qi8Hl+stezc8p3JxDkoTRqMAlKnXHuzrfjTQ=
53
+
github.com/go-git/go-git-fixtures/v4 v4.3.1/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
54
+
github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk=
55
+
github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8=
56
+
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
57
+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
58
+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
59
+
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
60
+
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
61
+
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
62
+
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
63
+
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
64
+
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
65
+
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
66
+
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
67
+
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
68
+
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
69
+
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
70
+
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
71
+
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
72
+
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
73
+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
74
+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
75
+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
76
+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
77
+
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
78
+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
79
+
github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
80
+
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
81
+
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
82
+
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
83
+
github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM=
84
+
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
85
+
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
86
+
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
87
+
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
88
+
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
89
+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
90
+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
91
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
92
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
93
+
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
94
+
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
95
+
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
96
+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
97
+
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
98
+
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
99
+
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
100
+
github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag=
101
+
github.com/skeema/knownhosts v1.3.0 h1:AM+y0rI04VksttfwjkSTNQorvGqmwATnvnAHpSgc0LY=
102
+
github.com/skeema/knownhosts v1.3.0/go.mod h1:sPINvnADmT/qYH1kfv+ePMmOBTH6Tbl7b5LvTDjFK7M=
103
+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
104
+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
105
+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
106
+
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
107
+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
108
+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
109
+
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
110
+
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
111
+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
112
+
golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
113
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
114
+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
115
+
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
116
+
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
117
+
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
118
+
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
119
+
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
120
+
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
121
+
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
122
+
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
123
+
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
124
+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
125
+
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
126
+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
127
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
128
+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
129
+
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
130
+
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
131
+
golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
132
+
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
133
+
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
134
+
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
135
+
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
136
+
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
137
+
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
138
+
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
139
+
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
140
+
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
141
+
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
142
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
143
+
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
144
+
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
145
+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
146
+
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
147
+
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
148
+
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
149
+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
150
+
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
151
+
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
152
+
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
153
+
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
154
+
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
155
+
golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
156
+
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
157
+
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
158
+
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
159
+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
160
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
161
+
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
162
+
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
163
+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
164
+
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
165
+
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
166
+
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
167
+
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
168
+
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
169
+
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
170
+
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
171
+
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
172
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
173
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
174
+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
175
+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
176
+
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
177
+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
178
+
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
179
+
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
180
+
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
181
+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
182
+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
183
+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
184
+
golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA=
185
+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
186
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
187
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
188
+
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
189
+
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
190
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
191
+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
192
+
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
193
+
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
194
+
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
195
+
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
196
+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
197
+
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
198
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
199
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
200
+
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+57
legit/config/config.go
+57
legit/config/config.go
···
1
+
package config
2
+
3
+
import (
4
+
"fmt"
5
+
"os"
6
+
"path/filepath"
7
+
8
+
"gopkg.in/yaml.v3"
9
+
)
10
+
11
+
type Config struct {
12
+
Repo struct {
13
+
ScanPath string `yaml:"scanPath"`
14
+
Readme []string `yaml:"readme"`
15
+
MainBranch []string `yaml:"mainBranch"`
16
+
Ignore []string `yaml:"ignore,omitempty"`
17
+
Unlisted []string `yaml:"unlisted,omitempty"`
18
+
} `yaml:"repo"`
19
+
Dirs struct {
20
+
Templates string `yaml:"templates"`
21
+
Static string `yaml:"static"`
22
+
} `yaml:"dirs"`
23
+
Meta struct {
24
+
Title string `yaml:"title"`
25
+
Description string `yaml:"description"`
26
+
SyntaxHighlight string `yaml:"syntaxHighlight"`
27
+
} `yaml:"meta"`
28
+
Server struct {
29
+
Name string `yaml:"name,omitempty"`
30
+
Host string `yaml:"host"`
31
+
Port int `yaml:"port"`
32
+
} `yaml:"server"`
33
+
}
34
+
35
+
func Read(f string) (*Config, error) {
36
+
b, err := os.ReadFile(f)
37
+
if err != nil {
38
+
return nil, fmt.Errorf("reading config: %w", err)
39
+
}
40
+
41
+
c := Config{}
42
+
if err := yaml.Unmarshal(b, &c); err != nil {
43
+
return nil, fmt.Errorf("parsing config: %w", err)
44
+
}
45
+
46
+
if c.Repo.ScanPath, err = filepath.Abs(c.Repo.ScanPath); err != nil {
47
+
return nil, err
48
+
}
49
+
if c.Dirs.Templates, err = filepath.Abs(c.Dirs.Templates); err != nil {
50
+
return nil, err
51
+
}
52
+
if c.Dirs.Static, err = filepath.Abs(c.Dirs.Static); err != nil {
53
+
return nil, err
54
+
}
55
+
56
+
return &c, nil
57
+
}
+22
legit/contrib/Dockerfile
+22
legit/contrib/Dockerfile
···
1
+
FROM golang:1.22-alpine AS builder
2
+
3
+
WORKDIR /app
4
+
5
+
COPY . .
6
+
RUN go mod download
7
+
RUN go mod verify
8
+
9
+
RUN go build -o legit
10
+
11
+
FROM scratch AS build-release-stage
12
+
13
+
WORKDIR /app
14
+
15
+
COPY static ./static
16
+
COPY templates ./templates
17
+
COPY config.yaml ./
18
+
COPY --from=builder /app/legit ./
19
+
20
+
EXPOSE 5555
21
+
22
+
CMD ["./legit"]
+14
legit/contrib/docker-compose.yml
+14
legit/contrib/docker-compose.yml
···
1
+
services:
2
+
legit:
3
+
container_name: legit
4
+
build:
5
+
context: ../
6
+
dockerfile: contrib/Dockerfile
7
+
restart: unless-stopped
8
+
ports:
9
+
- "5555:5555"
10
+
volumes:
11
+
- /var/www/git:/var/www/git
12
+
- ../config.yaml:/app/config.yaml
13
+
- ../static:/app/static
14
+
- ../templates:/app/templates
+17
legit/contrib/legit.service
+17
legit/contrib/legit.service
···
1
+
[Unit]
2
+
Description=legit Server
3
+
After=network-online.target
4
+
Requires=network-online.target
5
+
6
+
[Service]
7
+
User=git
8
+
Group=git
9
+
ExecStart=/usr/bin/legit -config /etc/legit/config.yaml
10
+
ProtectSystem=strict
11
+
ProtectHome=strict
12
+
NoNewPrivileges=true
13
+
PrivateTmp=true
14
+
PrivateDevices=true
15
+
16
+
[Install]
17
+
WantedBy=multi-user.target
+119
legit/git/diff.go
+119
legit/git/diff.go
···
1
+
package git
2
+
3
+
import (
4
+
"fmt"
5
+
"log"
6
+
"strings"
7
+
8
+
"github.com/bluekeyes/go-gitdiff/gitdiff"
9
+
"github.com/go-git/go-git/v5/plumbing/object"
10
+
)
11
+
12
+
type TextFragment struct {
13
+
Header string
14
+
Lines []gitdiff.Line
15
+
}
16
+
17
+
type Diff struct {
18
+
Name struct {
19
+
Old string
20
+
New string
21
+
}
22
+
TextFragments []TextFragment
23
+
IsBinary bool
24
+
IsNew bool
25
+
IsDelete bool
26
+
}
27
+
28
+
// A nicer git diff representation.
29
+
type NiceDiff struct {
30
+
Commit struct {
31
+
Message string
32
+
Author object.Signature
33
+
This string
34
+
Parent string
35
+
}
36
+
Stat struct {
37
+
FilesChanged int
38
+
Insertions int
39
+
Deletions int
40
+
}
41
+
Diff []Diff
42
+
}
43
+
44
+
func (g *GitRepo) Diff() (*NiceDiff, error) {
45
+
c, err := g.r.CommitObject(g.h)
46
+
if err != nil {
47
+
return nil, fmt.Errorf("commit object: %w", err)
48
+
}
49
+
50
+
patch := &object.Patch{}
51
+
commitTree, err := c.Tree()
52
+
parent := &object.Commit{}
53
+
if err == nil {
54
+
parentTree := &object.Tree{}
55
+
if c.NumParents() != 0 {
56
+
parent, err = c.Parents().Next()
57
+
if err == nil {
58
+
parentTree, err = parent.Tree()
59
+
if err == nil {
60
+
patch, err = parentTree.Patch(commitTree)
61
+
if err != nil {
62
+
return nil, fmt.Errorf("patch: %w", err)
63
+
}
64
+
}
65
+
}
66
+
} else {
67
+
patch, err = parentTree.Patch(commitTree)
68
+
if err != nil {
69
+
return nil, fmt.Errorf("patch: %w", err)
70
+
}
71
+
}
72
+
}
73
+
74
+
diffs, _, err := gitdiff.Parse(strings.NewReader(patch.String()))
75
+
if err != nil {
76
+
log.Println(err)
77
+
}
78
+
79
+
nd := NiceDiff{}
80
+
nd.Commit.This = c.Hash.String()
81
+
82
+
if parent.Hash.IsZero() {
83
+
nd.Commit.Parent = ""
84
+
} else {
85
+
nd.Commit.Parent = parent.Hash.String()
86
+
}
87
+
nd.Commit.Author = c.Author
88
+
nd.Commit.Message = c.Message
89
+
90
+
for _, d := range diffs {
91
+
ndiff := Diff{}
92
+
ndiff.Name.New = d.NewName
93
+
ndiff.Name.Old = d.OldName
94
+
ndiff.IsBinary = d.IsBinary
95
+
ndiff.IsNew = d.IsNew
96
+
ndiff.IsDelete = d.IsDelete
97
+
98
+
for _, tf := range d.TextFragments {
99
+
ndiff.TextFragments = append(ndiff.TextFragments, TextFragment{
100
+
Header: tf.Header(),
101
+
Lines: tf.Lines,
102
+
})
103
+
for _, l := range tf.Lines {
104
+
switch l.Op {
105
+
case gitdiff.OpAdd:
106
+
nd.Stat.Insertions += 1
107
+
case gitdiff.OpDelete:
108
+
nd.Stat.Deletions += 1
109
+
}
110
+
}
111
+
}
112
+
113
+
nd.Diff = append(nd.Diff, ndiff)
114
+
}
115
+
116
+
nd.Stat.FilesChanged = len(diffs)
117
+
118
+
return &nd, nil
119
+
}
+344
legit/git/git.go
+344
legit/git/git.go
···
1
+
package git
2
+
3
+
import (
4
+
"archive/tar"
5
+
"fmt"
6
+
"io"
7
+
"io/fs"
8
+
"path"
9
+
"sort"
10
+
"time"
11
+
12
+
"github.com/go-git/go-git/v5"
13
+
"github.com/go-git/go-git/v5/plumbing"
14
+
"github.com/go-git/go-git/v5/plumbing/object"
15
+
)
16
+
17
+
type GitRepo struct {
18
+
r *git.Repository
19
+
h plumbing.Hash
20
+
}
21
+
22
+
type TagList struct {
23
+
refs []*TagReference
24
+
r *git.Repository
25
+
}
26
+
27
+
// TagReference is used to list both tag and non-annotated tags.
28
+
// Non-annotated tags should only contains a reference.
29
+
// Annotated tags should contain its reference and its tag information.
30
+
type TagReference struct {
31
+
ref *plumbing.Reference
32
+
tag *object.Tag
33
+
}
34
+
35
+
// infoWrapper wraps the property of a TreeEntry so it can export fs.FileInfo
36
+
// to tar WriteHeader
37
+
type infoWrapper struct {
38
+
name string
39
+
size int64
40
+
mode fs.FileMode
41
+
modTime time.Time
42
+
isDir bool
43
+
}
44
+
45
+
func (self *TagList) Len() int {
46
+
return len(self.refs)
47
+
}
48
+
49
+
func (self *TagList) Swap(i, j int) {
50
+
self.refs[i], self.refs[j] = self.refs[j], self.refs[i]
51
+
}
52
+
53
+
// sorting tags in reverse chronological order
54
+
func (self *TagList) Less(i, j int) bool {
55
+
var dateI time.Time
56
+
var dateJ time.Time
57
+
58
+
if self.refs[i].tag != nil {
59
+
dateI = self.refs[i].tag.Tagger.When
60
+
} else {
61
+
c, err := self.r.CommitObject(self.refs[i].ref.Hash())
62
+
if err != nil {
63
+
dateI = time.Now()
64
+
} else {
65
+
dateI = c.Committer.When
66
+
}
67
+
}
68
+
69
+
if self.refs[j].tag != nil {
70
+
dateJ = self.refs[j].tag.Tagger.When
71
+
} else {
72
+
c, err := self.r.CommitObject(self.refs[j].ref.Hash())
73
+
if err != nil {
74
+
dateJ = time.Now()
75
+
} else {
76
+
dateJ = c.Committer.When
77
+
}
78
+
}
79
+
80
+
return dateI.After(dateJ)
81
+
}
82
+
83
+
func Open(path string, ref string) (*GitRepo, error) {
84
+
var err error
85
+
g := GitRepo{}
86
+
g.r, err = git.PlainOpen(path)
87
+
if err != nil {
88
+
return nil, fmt.Errorf("opening %s: %w", path, err)
89
+
}
90
+
91
+
if ref == "" {
92
+
head, err := g.r.Head()
93
+
if err != nil {
94
+
return nil, fmt.Errorf("getting head of %s: %w", path, err)
95
+
}
96
+
g.h = head.Hash()
97
+
} else {
98
+
hash, err := g.r.ResolveRevision(plumbing.Revision(ref))
99
+
if err != nil {
100
+
return nil, fmt.Errorf("resolving rev %s for %s: %w", ref, path, err)
101
+
}
102
+
g.h = *hash
103
+
}
104
+
return &g, nil
105
+
}
106
+
107
+
func (g *GitRepo) Commits() ([]*object.Commit, error) {
108
+
ci, err := g.r.Log(&git.LogOptions{From: g.h})
109
+
if err != nil {
110
+
return nil, fmt.Errorf("commits from ref: %w", err)
111
+
}
112
+
113
+
commits := []*object.Commit{}
114
+
ci.ForEach(func(c *object.Commit) error {
115
+
commits = append(commits, c)
116
+
return nil
117
+
})
118
+
119
+
return commits, nil
120
+
}
121
+
122
+
func (g *GitRepo) LastCommit() (*object.Commit, error) {
123
+
c, err := g.r.CommitObject(g.h)
124
+
if err != nil {
125
+
return nil, fmt.Errorf("last commit: %w", err)
126
+
}
127
+
return c, nil
128
+
}
129
+
130
+
func (g *GitRepo) FileContent(path string) (string, error) {
131
+
c, err := g.r.CommitObject(g.h)
132
+
if err != nil {
133
+
return "", fmt.Errorf("commit object: %w", err)
134
+
}
135
+
136
+
tree, err := c.Tree()
137
+
if err != nil {
138
+
return "", fmt.Errorf("file tree: %w", err)
139
+
}
140
+
141
+
file, err := tree.File(path)
142
+
if err != nil {
143
+
return "", err
144
+
}
145
+
146
+
isbin, _ := file.IsBinary()
147
+
148
+
if !isbin {
149
+
return file.Contents()
150
+
} else {
151
+
return "Not displaying binary file", nil
152
+
}
153
+
}
154
+
155
+
func (g *GitRepo) Tags() ([]*TagReference, error) {
156
+
iter, err := g.r.Tags()
157
+
if err != nil {
158
+
return nil, fmt.Errorf("tag objects: %w", err)
159
+
}
160
+
161
+
tags := make([]*TagReference, 0)
162
+
163
+
if err := iter.ForEach(func(ref *plumbing.Reference) error {
164
+
obj, err := g.r.TagObject(ref.Hash())
165
+
switch err {
166
+
case nil:
167
+
tags = append(tags, &TagReference{
168
+
ref: ref,
169
+
tag: obj,
170
+
})
171
+
case plumbing.ErrObjectNotFound:
172
+
tags = append(tags, &TagReference{
173
+
ref: ref,
174
+
})
175
+
default:
176
+
return err
177
+
}
178
+
return nil
179
+
}); err != nil {
180
+
return nil, err
181
+
}
182
+
183
+
tagList := &TagList{r: g.r, refs: tags}
184
+
sort.Sort(tagList)
185
+
return tags, nil
186
+
}
187
+
188
+
func (g *GitRepo) Branches() ([]*plumbing.Reference, error) {
189
+
bi, err := g.r.Branches()
190
+
if err != nil {
191
+
return nil, fmt.Errorf("branchs: %w", err)
192
+
}
193
+
194
+
branches := []*plumbing.Reference{}
195
+
196
+
_ = bi.ForEach(func(ref *plumbing.Reference) error {
197
+
branches = append(branches, ref)
198
+
return nil
199
+
})
200
+
201
+
return branches, nil
202
+
}
203
+
204
+
func (g *GitRepo) FindMainBranch(branches []string) (string, error) {
205
+
for _, b := range branches {
206
+
_, err := g.r.ResolveRevision(plumbing.Revision(b))
207
+
if err == nil {
208
+
return b, nil
209
+
}
210
+
}
211
+
return "", fmt.Errorf("unable to find main branch")
212
+
}
213
+
214
+
// WriteTar writes itself from a tree into a binary tar file format.
215
+
// prefix is root folder to be appended.
216
+
func (g *GitRepo) WriteTar(w io.Writer, prefix string) error {
217
+
tw := tar.NewWriter(w)
218
+
defer tw.Close()
219
+
220
+
c, err := g.r.CommitObject(g.h)
221
+
if err != nil {
222
+
return fmt.Errorf("commit object: %w", err)
223
+
}
224
+
225
+
tree, err := c.Tree()
226
+
if err != nil {
227
+
return err
228
+
}
229
+
230
+
walker := object.NewTreeWalker(tree, true, nil)
231
+
defer walker.Close()
232
+
233
+
name, entry, err := walker.Next()
234
+
for ; err == nil; name, entry, err = walker.Next() {
235
+
info, err := newInfoWrapper(name, prefix, &entry, tree)
236
+
if err != nil {
237
+
return err
238
+
}
239
+
240
+
header, err := tar.FileInfoHeader(info, "")
241
+
if err != nil {
242
+
return err
243
+
}
244
+
245
+
err = tw.WriteHeader(header)
246
+
if err != nil {
247
+
return err
248
+
}
249
+
250
+
if !info.IsDir() {
251
+
file, err := tree.File(name)
252
+
if err != nil {
253
+
return err
254
+
}
255
+
256
+
reader, err := file.Blob.Reader()
257
+
if err != nil {
258
+
return err
259
+
}
260
+
261
+
_, err = io.Copy(tw, reader)
262
+
if err != nil {
263
+
reader.Close()
264
+
return err
265
+
}
266
+
reader.Close()
267
+
}
268
+
}
269
+
270
+
return nil
271
+
}
272
+
273
+
func newInfoWrapper(
274
+
name string,
275
+
prefix string,
276
+
entry *object.TreeEntry,
277
+
tree *object.Tree,
278
+
) (*infoWrapper, error) {
279
+
var (
280
+
size int64
281
+
mode fs.FileMode
282
+
isDir bool
283
+
)
284
+
285
+
if entry.Mode.IsFile() {
286
+
file, err := tree.TreeEntryFile(entry)
287
+
if err != nil {
288
+
return nil, err
289
+
}
290
+
mode = fs.FileMode(file.Mode)
291
+
292
+
size, err = tree.Size(name)
293
+
if err != nil {
294
+
return nil, err
295
+
}
296
+
} else {
297
+
isDir = true
298
+
mode = fs.ModeDir | fs.ModePerm
299
+
}
300
+
301
+
fullname := path.Join(prefix, name)
302
+
return &infoWrapper{
303
+
name: fullname,
304
+
size: size,
305
+
mode: mode,
306
+
modTime: time.Unix(0, 0),
307
+
isDir: isDir,
308
+
}, nil
309
+
}
310
+
311
+
func (i *infoWrapper) Name() string {
312
+
return i.name
313
+
}
314
+
315
+
func (i *infoWrapper) Size() int64 {
316
+
return i.size
317
+
}
318
+
319
+
func (i *infoWrapper) Mode() fs.FileMode {
320
+
return i.mode
321
+
}
322
+
323
+
func (i *infoWrapper) ModTime() time.Time {
324
+
return i.modTime
325
+
}
326
+
327
+
func (i *infoWrapper) IsDir() bool {
328
+
return i.isDir
329
+
}
330
+
331
+
func (i *infoWrapper) Sys() any {
332
+
return nil
333
+
}
334
+
335
+
func (t *TagReference) Name() string {
336
+
return t.ref.Name().Short()
337
+
}
338
+
339
+
func (t *TagReference) Message() string {
340
+
if t.tag != nil {
341
+
return t.tag.Message
342
+
}
343
+
return ""
344
+
}
+121
legit/git/service/service.go
+121
legit/git/service/service.go
···
1
+
package service
2
+
3
+
import (
4
+
"bytes"
5
+
"fmt"
6
+
"io"
7
+
"log"
8
+
"net/http"
9
+
"os/exec"
10
+
"strings"
11
+
"syscall"
12
+
)
13
+
14
+
// Mostly from charmbracelet/soft-serve and sosedoff/gitkit.
15
+
16
+
type ServiceCommand struct {
17
+
Dir string
18
+
Stdin io.Reader
19
+
Stdout http.ResponseWriter
20
+
}
21
+
22
+
func (c *ServiceCommand) InfoRefs() error {
23
+
cmd := exec.Command("git", []string{
24
+
"upload-pack",
25
+
"--stateless-rpc",
26
+
"--advertise-refs",
27
+
".",
28
+
}...)
29
+
30
+
cmd.Dir = c.Dir
31
+
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
32
+
stdoutPipe, _ := cmd.StdoutPipe()
33
+
cmd.Stderr = cmd.Stdout
34
+
35
+
if err := cmd.Start(); err != nil {
36
+
log.Printf("git: failed to start git-upload-pack (info/refs): %s", err)
37
+
return err
38
+
}
39
+
40
+
if err := packLine(c.Stdout, "# service=git-upload-pack\n"); err != nil {
41
+
log.Printf("git: failed to write pack line: %s", err)
42
+
return err
43
+
}
44
+
45
+
if err := packFlush(c.Stdout); err != nil {
46
+
log.Printf("git: failed to flush pack: %s", err)
47
+
return err
48
+
}
49
+
50
+
buf := bytes.Buffer{}
51
+
if _, err := io.Copy(&buf, stdoutPipe); err != nil {
52
+
log.Printf("git: failed to copy stdout to tmp buffer: %s", err)
53
+
return err
54
+
}
55
+
56
+
if err := cmd.Wait(); err != nil {
57
+
out := strings.Builder{}
58
+
_, _ = io.Copy(&out, &buf)
59
+
log.Printf("git: failed to run git-upload-pack; err: %s; output: %s", err, out.String())
60
+
return err
61
+
}
62
+
63
+
if _, err := io.Copy(c.Stdout, &buf); err != nil {
64
+
log.Printf("git: failed to copy stdout: %s", err)
65
+
}
66
+
67
+
return nil
68
+
}
69
+
70
+
func (c *ServiceCommand) UploadPack() error {
71
+
cmd := exec.Command("git", []string{
72
+
"-c", "uploadpack.allowFilter=true",
73
+
"upload-pack",
74
+
"--stateless-rpc",
75
+
".",
76
+
}...)
77
+
cmd.Dir = c.Dir
78
+
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
79
+
80
+
stdoutPipe, _ := cmd.StdoutPipe()
81
+
cmd.Stderr = cmd.Stdout
82
+
defer stdoutPipe.Close()
83
+
84
+
stdinPipe, err := cmd.StdinPipe()
85
+
if err != nil {
86
+
return err
87
+
}
88
+
defer stdinPipe.Close()
89
+
90
+
if err := cmd.Start(); err != nil {
91
+
log.Printf("git: failed to start git-upload-pack: %s", err)
92
+
return err
93
+
}
94
+
95
+
if _, err := io.Copy(stdinPipe, c.Stdin); err != nil {
96
+
log.Printf("git: failed to copy stdin: %s", err)
97
+
return err
98
+
}
99
+
stdinPipe.Close()
100
+
101
+
if _, err := io.Copy(newWriteFlusher(c.Stdout), stdoutPipe); err != nil {
102
+
log.Printf("git: failed to copy stdout: %s", err)
103
+
return err
104
+
}
105
+
if err := cmd.Wait(); err != nil {
106
+
log.Printf("git: failed to wait for git-upload-pack: %s", err)
107
+
return err
108
+
}
109
+
110
+
return nil
111
+
}
112
+
113
+
func packLine(w io.Writer, s string) error {
114
+
_, err := fmt.Fprintf(w, "%04x%s", len(s)+4, s)
115
+
return err
116
+
}
117
+
118
+
func packFlush(w io.Writer) error {
119
+
_, err := fmt.Fprint(w, "0000")
120
+
return err
121
+
}
+25
legit/git/service/write_flusher.go
+25
legit/git/service/write_flusher.go
···
1
+
package service
2
+
3
+
import (
4
+
"io"
5
+
"net/http"
6
+
)
7
+
8
+
func newWriteFlusher(w http.ResponseWriter) io.Writer {
9
+
return writeFlusher{w.(interface {
10
+
io.Writer
11
+
http.Flusher
12
+
})}
13
+
}
14
+
15
+
type writeFlusher struct {
16
+
wf interface {
17
+
io.Writer
18
+
http.Flusher
19
+
}
20
+
}
21
+
22
+
func (w writeFlusher) Write(p []byte) (int, error) {
23
+
defer w.wf.Flush()
24
+
return w.wf.Write(p)
25
+
}
+66
legit/git/tree.go
+66
legit/git/tree.go
···
1
+
package git
2
+
3
+
import (
4
+
"fmt"
5
+
6
+
"github.com/go-git/go-git/v5/plumbing/object"
7
+
)
8
+
9
+
func (g *GitRepo) FileTree(path string) ([]NiceTree, error) {
10
+
c, err := g.r.CommitObject(g.h)
11
+
if err != nil {
12
+
return nil, fmt.Errorf("commit object: %w", err)
13
+
}
14
+
15
+
files := []NiceTree{}
16
+
tree, err := c.Tree()
17
+
if err != nil {
18
+
return nil, fmt.Errorf("file tree: %w", err)
19
+
}
20
+
21
+
if path == "" {
22
+
files = makeNiceTree(tree)
23
+
} else {
24
+
o, err := tree.FindEntry(path)
25
+
if err != nil {
26
+
return nil, err
27
+
}
28
+
29
+
if !o.Mode.IsFile() {
30
+
subtree, err := tree.Tree(path)
31
+
if err != nil {
32
+
return nil, err
33
+
}
34
+
35
+
files = makeNiceTree(subtree)
36
+
}
37
+
}
38
+
39
+
return files, nil
40
+
}
41
+
42
+
// A nicer git tree representation.
43
+
type NiceTree struct {
44
+
Name string
45
+
Mode string
46
+
Size int64
47
+
IsFile bool
48
+
IsSubtree bool
49
+
}
50
+
51
+
func makeNiceTree(t *object.Tree) []NiceTree {
52
+
nts := []NiceTree{}
53
+
54
+
for _, e := range t.Entries {
55
+
mode, _ := e.Mode.ToOSFileMode()
56
+
sz, _ := t.Size(e.Name)
57
+
nts = append(nts, NiceTree{
58
+
Name: e.Name,
59
+
Mode: mode.String(),
60
+
IsFile: e.Mode.IsFile(),
61
+
Size: sz,
62
+
})
63
+
}
64
+
65
+
return nts
66
+
}
+69
legit/routes/git.go
+69
legit/routes/git.go
···
1
+
package routes
2
+
3
+
import (
4
+
"compress/gzip"
5
+
"io"
6
+
"log"
7
+
"net/http"
8
+
"path/filepath"
9
+
10
+
"github.com/icyphox/bild/legit/git/service"
11
+
)
12
+
13
+
func (d *deps) InfoRefs(w http.ResponseWriter, r *http.Request) {
14
+
name := uniqueName(r)
15
+
name = filepath.Clean(name)
16
+
17
+
repo := filepath.Join(d.c.Repo.ScanPath, name)
18
+
19
+
w.Header().Set("content-type", "application/x-git-upload-pack-advertisement")
20
+
w.WriteHeader(http.StatusOK)
21
+
22
+
cmd := service.ServiceCommand{
23
+
Dir: repo,
24
+
Stdout: w,
25
+
}
26
+
27
+
if err := cmd.InfoRefs(); err != nil {
28
+
http.Error(w, err.Error(), 500)
29
+
log.Printf("git: failed to execute git-upload-pack (info/refs) %s", err)
30
+
return
31
+
}
32
+
}
33
+
34
+
func (d *deps) UploadPack(w http.ResponseWriter, r *http.Request) {
35
+
name := uniqueName(r)
36
+
name = filepath.Clean(name)
37
+
38
+
repo := filepath.Join(d.c.Repo.ScanPath, name)
39
+
40
+
w.Header().Set("content-type", "application/x-git-upload-pack-result")
41
+
w.Header().Set("Connection", "Keep-Alive")
42
+
w.Header().Set("Transfer-Encoding", "chunked")
43
+
w.WriteHeader(http.StatusOK)
44
+
45
+
cmd := service.ServiceCommand{
46
+
Dir: repo,
47
+
Stdout: w,
48
+
}
49
+
50
+
var reader io.ReadCloser
51
+
reader = r.Body
52
+
53
+
if r.Header.Get("Content-Encoding") == "gzip" {
54
+
reader, err := gzip.NewReader(r.Body)
55
+
if err != nil {
56
+
http.Error(w, err.Error(), 500)
57
+
log.Printf("git: failed to create gzip reader: %s", err)
58
+
return
59
+
}
60
+
defer reader.Close()
61
+
}
62
+
63
+
cmd.Stdin = reader
64
+
if err := cmd.UploadPack(); err != nil {
65
+
http.Error(w, err.Error(), 500)
66
+
log.Printf("git: failed to execute git-upload-pack %s", err)
67
+
return
68
+
}
69
+
}
+64
legit/routes/handler.go
+64
legit/routes/handler.go
···
1
+
package routes
2
+
3
+
import (
4
+
"net/http"
5
+
6
+
"github.com/go-chi/chi/v5"
7
+
"github.com/icyphox/bild/legit/config"
8
+
)
9
+
10
+
// Checks for gitprotocol-http(5) specific smells; if found, passes
11
+
// the request on to the git http service, else render the web frontend.
12
+
func (d *deps) Multiplex(w http.ResponseWriter, r *http.Request) {
13
+
path := chi.URLParam(r, "*")
14
+
15
+
if r.URL.RawQuery == "service=git-receive-pack" {
16
+
w.WriteHeader(http.StatusBadRequest)
17
+
w.Write([]byte("no pushing allowed!"))
18
+
return
19
+
}
20
+
21
+
if path == "info/refs" &&
22
+
r.URL.RawQuery == "service=git-upload-pack" &&
23
+
r.Method == "GET" {
24
+
d.InfoRefs(w, r)
25
+
} else if path == "git-upload-pack" && r.Method == "POST" {
26
+
d.UploadPack(w, r)
27
+
} else if r.Method == "GET" {
28
+
d.RepoIndex(w, r)
29
+
}
30
+
}
31
+
32
+
func Handlers(c *config.Config) http.Handler {
33
+
r := chi.NewRouter()
34
+
d := deps{c}
35
+
36
+
r.Get("/", d.Index)
37
+
r.Get("/static/{file}", d.ServeStatic)
38
+
39
+
r.Route("/@{user}", func(r chi.Router) {
40
+
r.Route("/{name}", func(r chi.Router) {
41
+
r.Get("/", d.Multiplex)
42
+
r.Post("/", d.Multiplex)
43
+
44
+
r.Route("/tree/{ref}", func(r chi.Router) {
45
+
r.Get("/*", d.RepoTree)
46
+
})
47
+
48
+
r.Route("/blob/{ref}", func(r chi.Router) {
49
+
r.Get("/*", d.FileContent)
50
+
})
51
+
52
+
r.Get("/log/{ref}", d.Log)
53
+
r.Get("/archive/{file}", d.Archive)
54
+
r.Get("/commit/{ref}", d.Diff)
55
+
r.Get("/refs/", d.Refs)
56
+
57
+
// Catch-all routes
58
+
r.Get("/*", d.Multiplex)
59
+
r.Post("/*", d.Multiplex)
60
+
})
61
+
})
62
+
63
+
return r
64
+
}
+439
legit/routes/routes.go
+439
legit/routes/routes.go
···
1
+
package routes
2
+
3
+
import (
4
+
"compress/gzip"
5
+
"fmt"
6
+
"html/template"
7
+
"log"
8
+
"net/http"
9
+
"os"
10
+
"path/filepath"
11
+
"sort"
12
+
"strconv"
13
+
"strings"
14
+
"time"
15
+
16
+
"github.com/dustin/go-humanize"
17
+
"github.com/go-chi/chi/v5"
18
+
"github.com/icyphox/bild/legit/config"
19
+
"github.com/icyphox/bild/legit/git"
20
+
"github.com/russross/blackfriday/v2"
21
+
)
22
+
23
+
type deps struct {
24
+
c *config.Config
25
+
}
26
+
27
+
func (d *deps) Index(w http.ResponseWriter, r *http.Request) {
28
+
dirs, err := os.ReadDir(d.c.Repo.ScanPath)
29
+
if err != nil {
30
+
d.Write500(w)
31
+
log.Printf("reading scan path: %s", err)
32
+
return
33
+
}
34
+
35
+
type info struct {
36
+
DisplayName, Name, Desc, Idle string
37
+
d time.Time
38
+
}
39
+
40
+
infos := []info{}
41
+
42
+
for _, dir := range dirs {
43
+
name := dir.Name()
44
+
if !dir.IsDir() || d.isIgnored(name) || d.isUnlisted(name) {
45
+
continue
46
+
}
47
+
48
+
path := filepath.Join(d.c.Repo.ScanPath, name)
49
+
gr, err := git.Open(path, "")
50
+
if err != nil {
51
+
log.Println(err)
52
+
continue
53
+
}
54
+
55
+
c, err := gr.LastCommit()
56
+
if err != nil {
57
+
d.Write500(w)
58
+
log.Println(err)
59
+
return
60
+
}
61
+
62
+
infos = append(infos, info{
63
+
DisplayName: getDisplayName(name),
64
+
Name: name,
65
+
Desc: getDescription(path),
66
+
Idle: humanize.Time(c.Author.When),
67
+
d: c.Author.When,
68
+
})
69
+
}
70
+
71
+
sort.Slice(infos, func(i, j int) bool {
72
+
return infos[j].d.Before(infos[i].d)
73
+
})
74
+
75
+
tpath := filepath.Join(d.c.Dirs.Templates, "*")
76
+
t := template.Must(template.ParseGlob(tpath))
77
+
78
+
data := make(map[string]interface{})
79
+
data["meta"] = d.c.Meta
80
+
data["info"] = infos
81
+
82
+
if err := t.ExecuteTemplate(w, "index", data); err != nil {
83
+
log.Println(err)
84
+
return
85
+
}
86
+
}
87
+
88
+
func (d *deps) RepoIndex(w http.ResponseWriter, r *http.Request) {
89
+
name := uniqueName(r)
90
+
if d.isIgnored(name) {
91
+
d.Write404(w)
92
+
return
93
+
}
94
+
95
+
name = filepath.Clean(name)
96
+
path := filepath.Join(d.c.Repo.ScanPath, name)
97
+
98
+
fmt.Println(path)
99
+
gr, err := git.Open(path, "")
100
+
if err != nil {
101
+
d.Write404(w)
102
+
return
103
+
}
104
+
commits, err := gr.Commits()
105
+
if err != nil {
106
+
d.Write500(w)
107
+
log.Println(err)
108
+
return
109
+
}
110
+
111
+
var readmeContent template.HTML
112
+
for _, readme := range d.c.Repo.Readme {
113
+
ext := filepath.Ext(readme)
114
+
content, _ := gr.FileContent(readme)
115
+
if len(content) > 0 {
116
+
switch ext {
117
+
case ".md", ".mkd", ".markdown":
118
+
unsafe := blackfriday.Run(
119
+
[]byte(content),
120
+
blackfriday.WithExtensions(blackfriday.CommonExtensions),
121
+
)
122
+
html := sanitize(unsafe)
123
+
readmeContent = template.HTML(html)
124
+
default:
125
+
safe := sanitize([]byte(content))
126
+
readmeContent = template.HTML(
127
+
fmt.Sprintf(`<pre>%s</pre>`, safe),
128
+
)
129
+
}
130
+
break
131
+
}
132
+
}
133
+
134
+
if readmeContent == "" {
135
+
log.Printf("no readme found for %s", name)
136
+
}
137
+
138
+
mainBranch, err := gr.FindMainBranch(d.c.Repo.MainBranch)
139
+
if err != nil {
140
+
d.Write500(w)
141
+
log.Println(err)
142
+
return
143
+
}
144
+
145
+
tpath := filepath.Join(d.c.Dirs.Templates, "*")
146
+
t := template.Must(template.ParseGlob(tpath))
147
+
148
+
if len(commits) >= 3 {
149
+
commits = commits[:3]
150
+
}
151
+
152
+
data := make(map[string]any)
153
+
data["name"] = name
154
+
data["displayname"] = getDisplayName(name)
155
+
data["ref"] = mainBranch
156
+
data["readme"] = readmeContent
157
+
data["commits"] = commits
158
+
data["desc"] = getDescription(path)
159
+
data["servername"] = d.c.Server.Name
160
+
data["meta"] = d.c.Meta
161
+
data["gomod"] = isGoModule(gr)
162
+
163
+
if err := t.ExecuteTemplate(w, "repo", data); err != nil {
164
+
log.Println(err)
165
+
return
166
+
}
167
+
168
+
return
169
+
}
170
+
171
+
func (d *deps) RepoTree(w http.ResponseWriter, r *http.Request) {
172
+
name := uniqueName(r)
173
+
if d.isIgnored(name) {
174
+
d.Write404(w)
175
+
return
176
+
}
177
+
treePath := chi.URLParam(r, "*")
178
+
ref := chi.URLParam(r, "ref")
179
+
180
+
name = filepath.Clean(name)
181
+
path := filepath.Join(d.c.Repo.ScanPath, name)
182
+
fmt.Println(path)
183
+
gr, err := git.Open(path, ref)
184
+
if err != nil {
185
+
d.Write404(w)
186
+
return
187
+
}
188
+
189
+
files, err := gr.FileTree(treePath)
190
+
if err != nil {
191
+
d.Write500(w)
192
+
log.Println(err)
193
+
return
194
+
}
195
+
196
+
data := make(map[string]any)
197
+
data["name"] = name
198
+
data["displayname"] = getDisplayName(name)
199
+
data["ref"] = ref
200
+
data["parent"] = treePath
201
+
data["desc"] = getDescription(path)
202
+
data["dotdot"] = filepath.Dir(treePath)
203
+
204
+
d.listFiles(files, data, w)
205
+
return
206
+
}
207
+
208
+
func (d *deps) FileContent(w http.ResponseWriter, r *http.Request) {
209
+
var raw bool
210
+
if rawParam, err := strconv.ParseBool(r.URL.Query().Get("raw")); err == nil {
211
+
raw = rawParam
212
+
}
213
+
214
+
name := uniqueName(r)
215
+
216
+
if d.isIgnored(name) {
217
+
d.Write404(w)
218
+
return
219
+
}
220
+
treePath := chi.URLParam(r, "*")
221
+
ref := chi.URLParam(r, "ref")
222
+
223
+
name = filepath.Clean(name)
224
+
path := filepath.Join(d.c.Repo.ScanPath, name)
225
+
gr, err := git.Open(path, ref)
226
+
if err != nil {
227
+
d.Write404(w)
228
+
return
229
+
}
230
+
231
+
contents, err := gr.FileContent(treePath)
232
+
if err != nil {
233
+
d.Write500(w)
234
+
return
235
+
}
236
+
data := make(map[string]any)
237
+
data["name"] = name
238
+
data["displayname"] = getDisplayName(name)
239
+
data["ref"] = ref
240
+
data["desc"] = getDescription(path)
241
+
data["path"] = treePath
242
+
243
+
safe := sanitize([]byte(contents))
244
+
245
+
if raw {
246
+
d.showRaw(string(safe), w)
247
+
} else {
248
+
if d.c.Meta.SyntaxHighlight == "" {
249
+
d.showFile(string(safe), data, w)
250
+
} else {
251
+
d.showFileWithHighlight(treePath, string(safe), data, w)
252
+
}
253
+
}
254
+
}
255
+
256
+
func (d *deps) Archive(w http.ResponseWriter, r *http.Request) {
257
+
name := uniqueName(r)
258
+
if d.isIgnored(name) {
259
+
d.Write404(w)
260
+
return
261
+
}
262
+
263
+
file := chi.URLParam(r, "file")
264
+
265
+
// TODO: extend this to add more files compression (e.g.: xz)
266
+
if !strings.HasSuffix(file, ".tar.gz") {
267
+
d.Write404(w)
268
+
return
269
+
}
270
+
271
+
ref := strings.TrimSuffix(file, ".tar.gz")
272
+
273
+
// This allows the browser to use a proper name for the file when
274
+
// downloading
275
+
filename := fmt.Sprintf("%s-%s.tar.gz", name, ref)
276
+
setContentDisposition(w, filename)
277
+
setGZipMIME(w)
278
+
279
+
path := filepath.Join(d.c.Repo.ScanPath, name)
280
+
gr, err := git.Open(path, ref)
281
+
if err != nil {
282
+
d.Write404(w)
283
+
return
284
+
}
285
+
286
+
gw := gzip.NewWriter(w)
287
+
defer gw.Close()
288
+
289
+
prefix := fmt.Sprintf("%s-%s", name, ref)
290
+
err = gr.WriteTar(gw, prefix)
291
+
if err != nil {
292
+
// once we start writing to the body we can't report error anymore
293
+
// so we are only left with printing the error.
294
+
log.Println(err)
295
+
return
296
+
}
297
+
298
+
err = gw.Flush()
299
+
if err != nil {
300
+
// once we start writing to the body we can't report error anymore
301
+
// so we are only left with printing the error.
302
+
log.Println(err)
303
+
return
304
+
}
305
+
}
306
+
307
+
func (d *deps) Log(w http.ResponseWriter, r *http.Request) {
308
+
name := uniqueName(r)
309
+
if d.isIgnored(name) {
310
+
d.Write404(w)
311
+
return
312
+
}
313
+
ref := chi.URLParam(r, "ref")
314
+
315
+
path := filepath.Join(d.c.Repo.ScanPath, name)
316
+
gr, err := git.Open(path, ref)
317
+
if err != nil {
318
+
d.Write404(w)
319
+
return
320
+
}
321
+
322
+
commits, err := gr.Commits()
323
+
if err != nil {
324
+
d.Write500(w)
325
+
log.Println(err)
326
+
return
327
+
}
328
+
329
+
tpath := filepath.Join(d.c.Dirs.Templates, "*")
330
+
t := template.Must(template.ParseGlob(tpath))
331
+
332
+
data := make(map[string]interface{})
333
+
data["commits"] = commits
334
+
data["meta"] = d.c.Meta
335
+
data["name"] = name
336
+
data["displayname"] = getDisplayName(name)
337
+
data["ref"] = ref
338
+
data["desc"] = getDescription(path)
339
+
data["log"] = true
340
+
341
+
if err := t.ExecuteTemplate(w, "log", data); err != nil {
342
+
log.Println(err)
343
+
return
344
+
}
345
+
}
346
+
347
+
func (d *deps) Diff(w http.ResponseWriter, r *http.Request) {
348
+
name := uniqueName(r)
349
+
if d.isIgnored(name) {
350
+
d.Write404(w)
351
+
return
352
+
}
353
+
ref := chi.URLParam(r, "ref")
354
+
355
+
path := filepath.Join(d.c.Repo.ScanPath, name)
356
+
gr, err := git.Open(path, ref)
357
+
if err != nil {
358
+
d.Write404(w)
359
+
return
360
+
}
361
+
362
+
diff, err := gr.Diff()
363
+
if err != nil {
364
+
d.Write500(w)
365
+
log.Println(err)
366
+
return
367
+
}
368
+
369
+
tpath := filepath.Join(d.c.Dirs.Templates, "*")
370
+
t := template.Must(template.ParseGlob(tpath))
371
+
372
+
data := make(map[string]interface{})
373
+
374
+
data["commit"] = diff.Commit
375
+
data["stat"] = diff.Stat
376
+
data["diff"] = diff.Diff
377
+
data["meta"] = d.c.Meta
378
+
data["name"] = name
379
+
data["displayname"] = getDisplayName(name)
380
+
data["ref"] = ref
381
+
data["desc"] = getDescription(path)
382
+
383
+
if err := t.ExecuteTemplate(w, "commit", data); err != nil {
384
+
log.Println(err)
385
+
return
386
+
}
387
+
}
388
+
389
+
func (d *deps) Refs(w http.ResponseWriter, r *http.Request) {
390
+
name := chi.URLParam(r, "name")
391
+
if d.isIgnored(name) {
392
+
d.Write404(w)
393
+
return
394
+
}
395
+
396
+
path := filepath.Join(d.c.Repo.ScanPath, name)
397
+
gr, err := git.Open(path, "")
398
+
if err != nil {
399
+
d.Write404(w)
400
+
return
401
+
}
402
+
403
+
tags, err := gr.Tags()
404
+
if err != nil {
405
+
// Non-fatal, we *should* have at least one branch to show.
406
+
log.Println(err)
407
+
}
408
+
409
+
branches, err := gr.Branches()
410
+
if err != nil {
411
+
log.Println(err)
412
+
d.Write500(w)
413
+
return
414
+
}
415
+
416
+
tpath := filepath.Join(d.c.Dirs.Templates, "*")
417
+
t := template.Must(template.ParseGlob(tpath))
418
+
419
+
data := make(map[string]interface{})
420
+
421
+
data["meta"] = d.c.Meta
422
+
data["name"] = name
423
+
data["displayname"] = getDisplayName(name)
424
+
data["branches"] = branches
425
+
data["tags"] = tags
426
+
data["desc"] = getDescription(path)
427
+
428
+
if err := t.ExecuteTemplate(w, "refs", data); err != nil {
429
+
log.Println(err)
430
+
return
431
+
}
432
+
}
433
+
434
+
func (d *deps) ServeStatic(w http.ResponseWriter, r *http.Request) {
435
+
f := chi.URLParam(r, "file")
436
+
f = filepath.Clean(filepath.Join(d.c.Dirs.Static, f))
437
+
438
+
http.ServeFile(w, r, f)
439
+
}
+150
legit/routes/template.go
+150
legit/routes/template.go
···
1
+
package routes
2
+
3
+
import (
4
+
"bytes"
5
+
"html/template"
6
+
"io"
7
+
"log"
8
+
"net/http"
9
+
"path/filepath"
10
+
"strings"
11
+
12
+
"github.com/alecthomas/chroma/v2/formatters/html"
13
+
"github.com/alecthomas/chroma/v2/lexers"
14
+
"github.com/alecthomas/chroma/v2/styles"
15
+
"github.com/icyphox/bild/legit/git"
16
+
)
17
+
18
+
func (d *deps) Write404(w http.ResponseWriter) {
19
+
tpath := filepath.Join(d.c.Dirs.Templates, "*")
20
+
t := template.Must(template.ParseGlob(tpath))
21
+
w.WriteHeader(404)
22
+
if err := t.ExecuteTemplate(w, "404", nil); err != nil {
23
+
log.Printf("404 template: %s", err)
24
+
}
25
+
}
26
+
27
+
func (d *deps) Write500(w http.ResponseWriter) {
28
+
tpath := filepath.Join(d.c.Dirs.Templates, "*")
29
+
t := template.Must(template.ParseGlob(tpath))
30
+
w.WriteHeader(500)
31
+
if err := t.ExecuteTemplate(w, "500", nil); err != nil {
32
+
log.Printf("500 template: %s", err)
33
+
}
34
+
}
35
+
36
+
func (d *deps) listFiles(files []git.NiceTree, data map[string]any, w http.ResponseWriter) {
37
+
tpath := filepath.Join(d.c.Dirs.Templates, "*")
38
+
t := template.Must(template.ParseGlob(tpath))
39
+
40
+
data["files"] = files
41
+
data["meta"] = d.c.Meta
42
+
43
+
if err := t.ExecuteTemplate(w, "tree", data); err != nil {
44
+
log.Println(err)
45
+
return
46
+
}
47
+
}
48
+
49
+
func countLines(r io.Reader) (int, error) {
50
+
buf := make([]byte, 32*1024)
51
+
bufLen := 0
52
+
count := 0
53
+
nl := []byte{'\n'}
54
+
55
+
for {
56
+
c, err := r.Read(buf)
57
+
if c > 0 {
58
+
bufLen += c
59
+
}
60
+
count += bytes.Count(buf[:c], nl)
61
+
62
+
switch {
63
+
case err == io.EOF:
64
+
/* handle last line not having a newline at the end */
65
+
if bufLen >= 1 && buf[(bufLen-1)%(32*1024)] != '\n' {
66
+
count++
67
+
}
68
+
return count, nil
69
+
case err != nil:
70
+
return 0, err
71
+
}
72
+
}
73
+
}
74
+
75
+
func (d *deps) showFileWithHighlight(name, content string, data map[string]any, w http.ResponseWriter) {
76
+
tpath := filepath.Join(d.c.Dirs.Templates, "*")
77
+
t := template.Must(template.ParseGlob(tpath))
78
+
79
+
lexer := lexers.Get(name)
80
+
if lexer == nil {
81
+
lexer = lexers.Get(".txt")
82
+
}
83
+
84
+
style := styles.Get(d.c.Meta.SyntaxHighlight)
85
+
if style == nil {
86
+
style = styles.Get("monokailight")
87
+
}
88
+
89
+
formatter := html.New(
90
+
html.WithLineNumbers(true),
91
+
html.WithLinkableLineNumbers(true, "L"),
92
+
)
93
+
94
+
iterator, err := lexer.Tokenise(nil, content)
95
+
if err != nil {
96
+
d.Write500(w)
97
+
return
98
+
}
99
+
100
+
var code bytes.Buffer
101
+
err = formatter.Format(&code, style, iterator)
102
+
if err != nil {
103
+
d.Write500(w)
104
+
return
105
+
}
106
+
107
+
data["content"] = template.HTML(code.String())
108
+
data["meta"] = d.c.Meta
109
+
data["chroma"] = true
110
+
111
+
if err := t.ExecuteTemplate(w, "file", data); err != nil {
112
+
log.Println(err)
113
+
return
114
+
}
115
+
}
116
+
117
+
func (d *deps) showFile(content string, data map[string]any, w http.ResponseWriter) {
118
+
tpath := filepath.Join(d.c.Dirs.Templates, "*")
119
+
t := template.Must(template.ParseGlob(tpath))
120
+
121
+
lc, err := countLines(strings.NewReader(content))
122
+
if err != nil {
123
+
// Non-fatal, we'll just skip showing line numbers in the template.
124
+
log.Printf("counting lines: %s", err)
125
+
}
126
+
127
+
lines := make([]int, lc)
128
+
if lc > 0 {
129
+
for i := range lines {
130
+
lines[i] = i + 1
131
+
}
132
+
}
133
+
134
+
data["linecount"] = lines
135
+
data["content"] = content
136
+
data["meta"] = d.c.Meta
137
+
data["chroma"] = false
138
+
139
+
if err := t.ExecuteTemplate(w, "file", data); err != nil {
140
+
log.Println(err)
141
+
return
142
+
}
143
+
}
144
+
145
+
func (d *deps) showRaw(content string, w http.ResponseWriter) {
146
+
w.WriteHeader(http.StatusOK)
147
+
w.Header().Set("Content-Type", "text/plain")
148
+
w.Write([]byte(content))
149
+
return
150
+
}
+131
legit/routes/util.go
+131
legit/routes/util.go
···
1
+
package routes
2
+
3
+
import (
4
+
"fmt"
5
+
"io/fs"
6
+
"log"
7
+
"net/http"
8
+
"os"
9
+
"path/filepath"
10
+
"strings"
11
+
12
+
"github.com/go-chi/chi/v5"
13
+
"github.com/icyphox/bild/legit/git"
14
+
"github.com/microcosm-cc/bluemonday"
15
+
)
16
+
17
+
func sanitize(content []byte) []byte {
18
+
return bluemonday.UGCPolicy().SanitizeBytes([]byte(content))
19
+
}
20
+
21
+
func isGoModule(gr *git.GitRepo) bool {
22
+
_, err := gr.FileContent("go.mod")
23
+
return err == nil
24
+
}
25
+
26
+
func uniqueName(r *http.Request) string {
27
+
user := chi.URLParam(r, "user")
28
+
name := chi.URLParam(r, "name")
29
+
return fmt.Sprintf("@%s/%s", user, name)
30
+
}
31
+
32
+
func getDisplayName(name string) string {
33
+
return strings.TrimSuffix(name, ".git")
34
+
}
35
+
36
+
func getDescription(path string) (desc string) {
37
+
db, err := os.ReadFile(filepath.Join(path, "description"))
38
+
if err == nil {
39
+
desc = string(db)
40
+
} else {
41
+
desc = ""
42
+
}
43
+
return
44
+
}
45
+
46
+
func (d *deps) isUnlisted(name string) bool {
47
+
for _, i := range d.c.Repo.Unlisted {
48
+
if name == i {
49
+
return true
50
+
}
51
+
}
52
+
53
+
return false
54
+
}
55
+
56
+
func (d *deps) isIgnored(name string) bool {
57
+
for _, i := range d.c.Repo.Ignore {
58
+
if name == i {
59
+
return true
60
+
}
61
+
}
62
+
63
+
return false
64
+
}
65
+
66
+
type repoInfo struct {
67
+
Git *git.GitRepo
68
+
Path string
69
+
Category string
70
+
}
71
+
72
+
func (d *deps) getAllRepos() ([]repoInfo, error) {
73
+
repos := []repoInfo{}
74
+
max := strings.Count(d.c.Repo.ScanPath, string(os.PathSeparator)) + 2
75
+
76
+
err := filepath.WalkDir(d.c.Repo.ScanPath, func(path string, de fs.DirEntry, err error) error {
77
+
if err != nil {
78
+
return err
79
+
}
80
+
81
+
if de.IsDir() {
82
+
// Check if we've exceeded our recursion depth
83
+
if strings.Count(path, string(os.PathSeparator)) > max {
84
+
return fs.SkipDir
85
+
}
86
+
87
+
if d.isIgnored(path) {
88
+
return fs.SkipDir
89
+
}
90
+
91
+
// A bare repo should always have at least a HEAD file, if it
92
+
// doesn't we can continue recursing
93
+
if _, err := os.Lstat(filepath.Join(path, "HEAD")); err == nil {
94
+
repo, err := git.Open(path, "")
95
+
if err != nil {
96
+
log.Println(err)
97
+
} else {
98
+
relpath, _ := filepath.Rel(d.c.Repo.ScanPath, path)
99
+
repos = append(repos, repoInfo{
100
+
Git: repo,
101
+
Path: relpath,
102
+
Category: d.category(path),
103
+
})
104
+
// Since we found a Git repo, we don't want to recurse
105
+
// further
106
+
return fs.SkipDir
107
+
}
108
+
}
109
+
}
110
+
return nil
111
+
})
112
+
113
+
return repos, err
114
+
}
115
+
116
+
func (d *deps) category(path string) string {
117
+
return strings.TrimPrefix(filepath.Dir(strings.TrimPrefix(path, d.c.Repo.ScanPath)), string(os.PathSeparator))
118
+
}
119
+
120
+
func setContentDisposition(w http.ResponseWriter, name string) {
121
+
h := "inline; filename=\"" + name + "\""
122
+
w.Header().Add("Content-Disposition", h)
123
+
}
124
+
125
+
func setGZipMIME(w http.ResponseWriter) {
126
+
setMIME(w, "application/gzip")
127
+
}
128
+
129
+
func setMIME(w http.ResponseWriter, mime string) {
130
+
w.Header().Add("Content-Type", mime)
131
+
}
legit/static/legit.png
legit/static/legit.png
This is a binary file and will not be displayed.
+331
legit/static/style.css
+331
legit/static/style.css
···
1
+
:root {
2
+
--white: #fff;
3
+
--light: #f4f4f4;
4
+
--cyan: #509c93;
5
+
--light-gray: #eee;
6
+
--medium-gray: #ddd;
7
+
--gray: #6a6a6a;
8
+
--dark: #444;
9
+
--darker: #222;
10
+
11
+
--sans-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Segoe UI", sans-serif;
12
+
--display-font: -apple-system, BlinkMacSystemFont, "Inter", "Roboto", "Segoe UI", sans-serif;
13
+
--mono-font: 'SF Mono', SFMono-Regular, ui-monospace, 'DejaVu Sans Mono', 'Roboto Mono', Menlo, Consolas, monospace;
14
+
}
15
+
16
+
@media (prefers-color-scheme: dark) {
17
+
:root {
18
+
color-scheme: dark light;
19
+
--light: #181818;
20
+
--cyan: #76c7c0;
21
+
--light-gray: #333;
22
+
--medium-gray: #444;
23
+
--gray: #aaa;
24
+
--dark: #ddd;
25
+
--darker: #f4f4f4;
26
+
}
27
+
}
28
+
29
+
html {
30
+
background: var(--white);
31
+
-webkit-text-size-adjust: none;
32
+
font-family: var(--sans-font);
33
+
font-weight: 380;
34
+
}
35
+
36
+
pre {
37
+
font-family: var(--mono-font);
38
+
}
39
+
40
+
::selection {
41
+
background: var(--medium-gray);
42
+
opacity: 0.3;
43
+
}
44
+
45
+
* {
46
+
box-sizing: border-box;
47
+
padding: 0;
48
+
margin: 0;
49
+
}
50
+
51
+
body {
52
+
max-width: 1000px;
53
+
padding: 0 13px;
54
+
margin: 40px auto;
55
+
}
56
+
57
+
main, footer {
58
+
font-size: 1rem;
59
+
padding: 0;
60
+
line-height: 160%;
61
+
}
62
+
63
+
header h1, h2, h3 {
64
+
font-family: var(--display-font);
65
+
}
66
+
67
+
h2 {
68
+
font-weight: 400;
69
+
}
70
+
71
+
strong {
72
+
font-weight: 500;
73
+
}
74
+
75
+
main h1 {
76
+
padding: 10px 0 10px 0;
77
+
}
78
+
79
+
main h2 {
80
+
font-size: 18px;
81
+
}
82
+
83
+
main h2, h3 {
84
+
padding: 20px 0 15px 0;
85
+
}
86
+
87
+
nav {
88
+
padding: 0.4rem 0 1.5rem 0;
89
+
}
90
+
91
+
nav ul {
92
+
padding: 0;
93
+
margin: 0;
94
+
list-style: none;
95
+
padding-bottom: 20px;
96
+
}
97
+
98
+
nav ul li {
99
+
padding-right: 10px;
100
+
display: inline-block;
101
+
}
102
+
103
+
a {
104
+
margin: 0;
105
+
padding: 0;
106
+
box-sizing: border-box;
107
+
text-decoration: none;
108
+
word-wrap: break-word;
109
+
}
110
+
111
+
a {
112
+
color: var(--darker);
113
+
border-bottom: 1.5px solid var(--medium-gray);
114
+
}
115
+
116
+
a:hover {
117
+
border-bottom: 1.5px solid var(--gray);
118
+
}
119
+
120
+
.index {
121
+
padding-top: 2em;
122
+
display: grid;
123
+
grid-template-columns: 6em 1fr minmax(0, 7em);
124
+
grid-row-gap: 0.5em;
125
+
min-width: 0;
126
+
}
127
+
128
+
.clone-url {
129
+
padding-top: 2rem;
130
+
}
131
+
132
+
.clone-url pre {
133
+
color: var(--dark);
134
+
white-space: pre-wrap;
135
+
}
136
+
137
+
.desc {
138
+
font-weight: normal;
139
+
color: var(--gray);
140
+
font-style: italic;
141
+
}
142
+
143
+
.tree {
144
+
display: grid;
145
+
grid-template-columns: 10ch auto 1fr;
146
+
grid-row-gap: 0.5em;
147
+
grid-column-gap: 1em;
148
+
min-width: 0;
149
+
}
150
+
151
+
.log {
152
+
display: grid;
153
+
grid-template-columns: 20rem minmax(0, 1fr);
154
+
grid-row-gap: 0.8em;
155
+
grid-column-gap: 8rem;
156
+
margin-bottom: 2em;
157
+
padding-bottom: 1em;
158
+
border-bottom: 1.5px solid var(--medium-gray);
159
+
}
160
+
161
+
.log pre {
162
+
white-space: pre-wrap;
163
+
}
164
+
165
+
.mode, .size {
166
+
font-family: var(--mono-font);
167
+
}
168
+
.size {
169
+
text-align: right;
170
+
}
171
+
172
+
.readme pre {
173
+
white-space: pre-wrap;
174
+
overflow-x: auto;
175
+
}
176
+
177
+
.readme {
178
+
background: var(--light-gray);
179
+
padding: 0.5rem;
180
+
}
181
+
182
+
.readme ul {
183
+
padding: revert;
184
+
}
185
+
186
+
.readme img {
187
+
max-width: 100%;
188
+
}
189
+
190
+
.diff {
191
+
margin: 1rem 0 1rem 0;
192
+
padding: 1rem 0 1rem 0;
193
+
border-bottom: 1.5px solid var(--medium-gray);
194
+
}
195
+
196
+
.diff pre {
197
+
overflow: scroll;
198
+
}
199
+
200
+
.diff-stat {
201
+
padding: 1rem 0 1rem 0;
202
+
}
203
+
204
+
.commit-hash, .commit-email {
205
+
font-family: var(--mono-font);
206
+
}
207
+
208
+
.commit-email:before {
209
+
content: '<';
210
+
}
211
+
212
+
.commit-email:after {
213
+
content: '>';
214
+
}
215
+
216
+
.commit {
217
+
margin-bottom: 1rem;
218
+
}
219
+
220
+
.commit pre {
221
+
padding-bottom: 1rem;
222
+
white-space: pre-wrap;
223
+
}
224
+
225
+
.diff-stat ul li {
226
+
list-style: none;
227
+
padding-left: 0.5em;
228
+
}
229
+
230
+
.diff-add {
231
+
color: green;
232
+
}
233
+
234
+
.diff-del {
235
+
color: red;
236
+
}
237
+
238
+
.diff-noop {
239
+
color: var(--gray);
240
+
}
241
+
242
+
.ref {
243
+
font-family: var(--sans-font);
244
+
font-size: 14px;
245
+
color: var(--gray);
246
+
display: inline-block;
247
+
padding-top: 0.7em;
248
+
}
249
+
250
+
.refs pre {
251
+
white-space: pre-wrap;
252
+
padding-bottom: 0.5rem;
253
+
}
254
+
255
+
.refs strong {
256
+
padding-right: 1em;
257
+
}
258
+
259
+
.line-numbers {
260
+
white-space: pre-line;
261
+
-moz-user-select: -moz-none;
262
+
-khtml-user-select: none;
263
+
-webkit-user-select: none;
264
+
-o-user-select: none;
265
+
user-select: none;
266
+
display: flex;
267
+
float: left;
268
+
flex-direction: column;
269
+
margin-right: 1ch;
270
+
}
271
+
272
+
.file-wrapper {
273
+
display: flex;
274
+
flex-direction: row;
275
+
grid-template-columns: 1rem minmax(0, 1fr);
276
+
gap: 1rem;
277
+
padding: 0.5rem;
278
+
background: var(--light-gray);
279
+
overflow-x: auto;
280
+
}
281
+
282
+
.chroma-file-wrapper {
283
+
display: flex;
284
+
flex-direction: row;
285
+
grid-template-columns: 1rem minmax(0, 1fr);
286
+
overflow-x: auto;
287
+
}
288
+
289
+
.file-content {
290
+
background: var(--light-gray);
291
+
overflow-y: hidden;
292
+
overflow-x: auto;
293
+
}
294
+
295
+
.diff-type {
296
+
color: var(--gray);
297
+
}
298
+
299
+
.commit-info {
300
+
color: var(--gray);
301
+
padding-bottom: 1.5rem;
302
+
font-size: 0.85rem;
303
+
}
304
+
305
+
@media (max-width: 600px) {
306
+
.index {
307
+
grid-row-gap: 0.8em;
308
+
}
309
+
310
+
.log {
311
+
grid-template-columns: 1fr;
312
+
grid-row-gap: 0em;
313
+
}
314
+
315
+
.index {
316
+
grid-template-columns: 1fr;
317
+
grid-row-gap: 0em;
318
+
}
319
+
320
+
.index-name:not(:first-child) {
321
+
padding-top: 1.5rem;
322
+
}
323
+
324
+
.commit-info:not(:last-child) {
325
+
padding-bottom: 1.5rem;
326
+
}
327
+
328
+
pre {
329
+
font-size: 0.8rem;
330
+
}
331
+
}
+13
legit/templates/404.html
+13
legit/templates/404.html
+13
legit/templates/500.html
+13
legit/templates/500.html
+104
legit/templates/commit.html
+104
legit/templates/commit.html
···
1
+
{{ define "commit" }}
2
+
<html>
3
+
{{ template "head" . }}
4
+
5
+
{{ template "repoheader" . }}
6
+
<body>
7
+
{{ template "nav" . }}
8
+
<main>
9
+
<section class="commit">
10
+
<pre>
11
+
{{- .commit.Message -}}
12
+
</pre>
13
+
<div class="commit-info">
14
+
{{ .commit.Author.Name }} <a href="mailto:{{ .commit.Author.Email }}" class="commit-email">{{ .commit.Author.Email}}</a>
15
+
<div>{{ .commit.Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</div>
16
+
</div>
17
+
18
+
<div>
19
+
<strong>commit</strong>
20
+
<p><a href="/{{ .name }}/commit/{{ .commit.This }}" class="commit-hash">
21
+
{{ .commit.This }}
22
+
</a>
23
+
</p>
24
+
</div>
25
+
26
+
{{ if .commit.Parent }}
27
+
<div>
28
+
<strong>parent</strong>
29
+
<p><a href="/{{ .name }}/commit/{{ .commit.Parent }}" class="commit-hash">
30
+
{{ .commit.Parent }}
31
+
</a></p>
32
+
</div>
33
+
34
+
{{ end }}
35
+
<div class="diff-stat">
36
+
<div>
37
+
{{ .stat.FilesChanged }} files changed,
38
+
{{ .stat.Insertions }} insertions(+),
39
+
{{ .stat.Deletions }} deletions(-)
40
+
</div>
41
+
<div>
42
+
<br>
43
+
<strong>jump to</strong>
44
+
{{ range .diff }}
45
+
<ul>
46
+
<li><a href="#{{ .Name.New }}">{{ .Name.New }}</a></li>
47
+
</ul>
48
+
{{ end }}
49
+
</div>
50
+
</div>
51
+
</section>
52
+
<section>
53
+
{{ $repo := .name }}
54
+
{{ $this := .commit.This }}
55
+
{{ $parent := .commit.Parent }}
56
+
{{ range .diff }}
57
+
<div id="{{ .Name.New }}">
58
+
<div class="diff">
59
+
{{ if .IsNew }}
60
+
<span class="diff-type">A</span>
61
+
{{ end }}
62
+
{{ if .IsDelete }}
63
+
<span class="diff-type">D</span>
64
+
{{ end }}
65
+
{{ if not (or .IsNew .IsDelete) }}
66
+
<span class="diff-type">M</span>
67
+
{{ end }}
68
+
{{ if .Name.Old }}
69
+
<a href="/{{ $repo }}/blob/{{ $parent }}/{{ .Name.Old }}">{{ .Name.Old }}</a>
70
+
{{ if .Name.New }}
71
+
→
72
+
<a href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}">{{ .Name.New }}</a>
73
+
{{ end }}
74
+
{{ else }}
75
+
<a href="/{{ $repo }}/blob/{{ $this }}/{{ .Name.New }}">{{ .Name.New }}</a>
76
+
{{- end -}}
77
+
{{ if .IsBinary }}
78
+
<p>Not showing binary file.</p>
79
+
{{ else }}
80
+
<pre>
81
+
{{- range .TextFragments -}}
82
+
<p>{{- .Header -}}</p>
83
+
{{- range .Lines -}}
84
+
{{- if eq .Op.String "+" -}}
85
+
<span class="diff-add">{{ .String }}</span>
86
+
{{- end -}}
87
+
{{- if eq .Op.String "-" -}}
88
+
<span class="diff-del">{{ .String }}</span>
89
+
{{- end -}}
90
+
{{- if eq .Op.String " " -}}
91
+
<span class="diff-noop">{{ .String }}</span>
92
+
{{- end -}}
93
+
{{- end -}}
94
+
{{- end -}}
95
+
{{- end -}}
96
+
</pre>
97
+
</div>
98
+
</div>
99
+
{{ end }}
100
+
</section>
101
+
</main>
102
+
</body>
103
+
</html>
104
+
{{ end }}
+36
legit/templates/file.html
+36
legit/templates/file.html
···
1
+
{{ define "file" }}
2
+
<html>
3
+
{{ template "head" . }}
4
+
{{ template "repoheader" . }}
5
+
<body>
6
+
{{ template "nav" . }}
7
+
<main>
8
+
<p>{{ .path }} (<a style="color: gray" href="?raw=true">view raw</a>)</p>
9
+
{{if .chroma }}
10
+
<div class="chroma-file-wrapper">
11
+
{{ .content }}
12
+
</div>
13
+
{{else}}
14
+
<div class="file-wrapper">
15
+
<table>
16
+
<tbody><tr>
17
+
<td class="line-numbers">
18
+
<pre>
19
+
{{- range .linecount }}
20
+
<a id="L{{ . }}" href="#L{{ . }}">{{ . }}</a>
21
+
{{- end -}}
22
+
</pre>
23
+
</td>
24
+
<td class="file-content">
25
+
<pre>
26
+
{{- .content -}}
27
+
</pre>
28
+
</td>
29
+
</tbody></tr>
30
+
</table>
31
+
</div>
32
+
{{end}}
33
+
</main>
34
+
</body>
35
+
</html>
36
+
{{ end }}
+32
legit/templates/head.html
+32
legit/templates/head.html
···
1
+
{{ define "head" }}
2
+
<head>
3
+
<meta charset="utf-8">
4
+
<meta name="viewport" content="width=device-width, initial-scale=1">
5
+
<link rel="stylesheet" href="/static/style.css" type="text/css">
6
+
<link rel="icon" type="image/png" size="32x32" href="/static/legit.png">
7
+
{{ if .parent }}
8
+
<title>{{ .meta.Title }} — {{ .name }} ({{ .ref }}): {{ .parent }}/</title>
9
+
10
+
{{ else if .path }}
11
+
<title>{{ .meta.Title }} — {{ .name }} ({{ .ref }}): {{ .path }}</title>
12
+
{{ else if .files }}
13
+
<title>{{ .meta.Title }} — {{ .name }} ({{ .ref }})</title>
14
+
{{ else if .commit }}
15
+
<title>{{ .meta.Title }} — {{ .name }}: {{ .commit.This }}</title>
16
+
{{ else if .branches }}
17
+
<title>{{ .meta.Title }} — {{ .name }}: refs</title>
18
+
{{ else if .commits }}
19
+
{{ if .log }}
20
+
<title>{{ .meta.Title }} — {{ .name }}: log</title>
21
+
{{ else }}
22
+
<title>{{ .meta.Title }} — {{ .name }}</title>
23
+
{{ end }}
24
+
{{ else }}
25
+
<title>{{ .meta.Title }}</title>
26
+
{{ end }}
27
+
{{ if and .servername .gomod }}
28
+
<meta name="go-import" content="{{ .servername}}/{{ .name }} git https://{{ .servername }}/{{ .name }}">
29
+
{{ end }}
30
+
<!-- other meta tags here -->
31
+
</head>
32
+
{{ end }}
+21
legit/templates/index.html
+21
legit/templates/index.html
···
1
+
{{ define "index" }}
2
+
<html>
3
+
{{ template "head" . }}
4
+
5
+
<header>
6
+
<h1>{{ .meta.Title }}</h1>
7
+
<h2>{{ .meta.Description }}</h2>
8
+
</header>
9
+
<body>
10
+
<main>
11
+
<div class="index">
12
+
{{ range .info }}
13
+
<div class="index-name"><a href="/{{ .Name }}">{{ .DisplayName }}</a></div>
14
+
<div class="desc">{{ .Desc }}</div>
15
+
<div>{{ .Idle }}</div>
16
+
{{ end }}
17
+
</div>
18
+
</main>
19
+
</body>
20
+
</html>
21
+
{{ end }}
+25
legit/templates/log.html
+25
legit/templates/log.html
···
1
+
{{ define "log" }}
2
+
<html>
3
+
{{ template "head" . }}
4
+
5
+
{{ template "repoheader" . }}
6
+
<body>
7
+
{{ template "nav" . }}
8
+
<main>
9
+
{{ $repo := .name }}
10
+
<div class="log">
11
+
{{ range .commits }}
12
+
<div>
13
+
<div><a href="/{{ $repo }}/commit/{{ .Hash.String }}" class="commit-hash">{{ slice .Hash.String 0 8 }}</a></div>
14
+
<pre>{{ .Message }}</pre>
15
+
</div>
16
+
<div class="commit-info">
17
+
{{ .Author.Name }} <a href="mailto:{{ .Author.Email }}" class="commit-email">{{ .Author.Email }}</a>
18
+
<div>{{ .Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</div>
19
+
</div>
20
+
{{ end }}
21
+
</div>
22
+
</main>
23
+
</body>
24
+
</html>
25
+
{{ end }}
+40
legit/templates/refs.html
+40
legit/templates/refs.html
···
1
+
{{ define "refs" }}
2
+
<html>
3
+
{{ template "head" . }}
4
+
5
+
{{ template "repoheader" . }}
6
+
<body>
7
+
{{ template "nav" . }}
8
+
<main>
9
+
{{ $name := .name }}
10
+
<h3>branches</h3>
11
+
<div class="refs">
12
+
{{ range .branches }}
13
+
<div>
14
+
<strong>{{ .Name.Short }}</strong>
15
+
<a href="/{{ $name }}/tree/{{ .Name.Short }}/">browse</a>
16
+
<a href="/{{ $name }}/log/{{ .Name.Short }}">log</a>
17
+
<a href="/{{ $name }}/archive/{{ .Name.Short }}.tar.gz">tar.gz</a>
18
+
</div>
19
+
{{ end }}
20
+
</div>
21
+
{{ if .tags }}
22
+
<h3>tags</h3>
23
+
<div class="refs">
24
+
{{ range .tags }}
25
+
<div>
26
+
<strong>{{ .Name }}</strong>
27
+
<a href="/{{ $name }}/tree/{{ .Name }}/">browse</a>
28
+
<a href="/{{ $name }}/log/{{ .Name }}">log</a>
29
+
<a href="/{{ $name }}/archive/{{ .Name }}.tar.gz">tar.gz</a>
30
+
{{ if .Message }}
31
+
<pre>{{ .Message }}</pre>
32
+
</div>
33
+
{{ end }}
34
+
{{ end }}
35
+
</div>
36
+
{{ end }}
37
+
</main>
38
+
</body>
39
+
</html>
40
+
{{ end }}
+12
legit/templates/repo-header.html
+12
legit/templates/repo-header.html
+38
legit/templates/repo.html
+38
legit/templates/repo.html
···
1
+
{{ define "repo" }}
2
+
<html>
3
+
{{ template "head" . }}
4
+
5
+
{{ template "repoheader" . }}
6
+
7
+
<body>
8
+
{{ template "nav" . }}
9
+
<main>
10
+
{{ $repo := .name }}
11
+
<div class="log">
12
+
{{ range .commits }}
13
+
<div>
14
+
<div><a href="/{{ $repo }}/commit/{{ .Hash.String }}" class="commit-hash">{{ slice .Hash.String 0 8 }}</a></div>
15
+
<pre>{{ .Message }}</pre>
16
+
</div>
17
+
<div class="commit-info">
18
+
{{ .Author.Name }} <a href="mailto:{{ .Author.Email }}" class="commit-email">{{ .Author.Email }}</a>
19
+
<div>{{ .Author.When.Format "Mon, 02 Jan 2006 15:04:05 -0700" }}</div>
20
+
</div>
21
+
{{ end }}
22
+
</div>
23
+
{{- if .readme }}
24
+
<article class="readme">
25
+
{{- .readme -}}
26
+
</article>
27
+
{{- end -}}
28
+
29
+
<div class="clone-url">
30
+
<strong>clone</strong>
31
+
<pre>
32
+
git clone https://{{ .servername }}/{{ .name }}
33
+
</pre>
34
+
</div>
35
+
</main>
36
+
</body>
37
+
</html>
38
+
{{ end }}
+55
legit/templates/tree.html
+55
legit/templates/tree.html
···
1
+
{{ define "tree" }}
2
+
<html>
3
+
4
+
{{ template "head" . }}
5
+
6
+
{{ template "repoheader" . }}
7
+
<body>
8
+
{{ template "nav" . }}
9
+
<main>
10
+
{{ $repo := .name }}
11
+
{{ $ref := .ref }}
12
+
{{ $parent := .parent }}
13
+
14
+
<div class="tree">
15
+
{{ if $parent }}
16
+
<div></div>
17
+
<div></div>
18
+
<div><a href="/{{ $repo }}/tree/{{ $ref }}/{{ .dotdot }}">..</a></div>
19
+
{{ end }}
20
+
{{ range .files }}
21
+
{{ if not .IsFile }}
22
+
<div class="mode">{{ .Mode }}</div>
23
+
<div class="size">{{ .Size }}</div>
24
+
<div>
25
+
{{ if $parent }}
26
+
<a href="/{{ $repo }}/tree/{{ $ref }}/{{ $parent }}/{{ .Name }}">{{ .Name }}/</a>
27
+
{{ else }}
28
+
<a href="/{{ $repo }}/tree/{{ $ref }}/{{ .Name }}">{{ .Name }}/</a>
29
+
{{ end }}
30
+
</div>
31
+
{{ end }}
32
+
{{ end }}
33
+
{{ range .files }}
34
+
{{ if .IsFile }}
35
+
<div class="mode">{{ .Mode }}</div>
36
+
<div class="size">{{ .Size }}</div>
37
+
<div>
38
+
{{ if $parent }}
39
+
<a href="/{{ $repo }}/blob/{{ $ref }}/{{ $parent }}/{{ .Name }}">{{ .Name }}</a>
40
+
{{ else }}
41
+
<a href="/{{ $repo }}/blob/{{ $ref }}/{{ .Name }}">{{ .Name }}</a>
42
+
{{ end }}
43
+
</div>
44
+
{{ end }}
45
+
{{ end }}
46
+
</div>
47
+
<article>
48
+
<pre>
49
+
{{- if .readme }}{{ .readme }}{{- end -}}
50
+
</pre>
51
+
</article>
52
+
</main>
53
+
</body>
54
+
</html>
55
+
{{ end }}