this repo has no description

legit: init

+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
··· 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
··· 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
··· 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
··· 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
··· 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=
+2
legit/.gitignore
··· 1 + legit 2 + result
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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

This is a binary file and will not be displayed.

+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
··· 1 + {{ define "404" }} 2 + <html> 3 + <title>404</title> 4 + {{ template "head" . }} 5 + <body> 6 + {{ template "nav" . }} 7 + <main> 8 + <h3>404 &mdash; nothing like that here.</h3> 9 + </main> 10 + </body> 11 + 12 + </html> 13 + {{ end }}
+13
legit/templates/500.html
··· 1 + {{ define "500" }} 2 + <html> 3 + <title>500</title> 4 + {{ template "head" . }} 5 + <body> 6 + {{ template "nav" . }} 7 + <main> 8 + <h3>500 &mdash; something broke!</h3> 9 + </main> 10 + </body> 11 + 12 + </html> 13 + {{ end }}
+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 + &#8594; 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
··· 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
··· 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 }} &mdash; {{ .name }} ({{ .ref }}): {{ .parent }}/</title> 9 + 10 + {{ else if .path }} 11 + <title>{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }}): {{ .path }}</title> 12 + {{ else if .files }} 13 + <title>{{ .meta.Title }} &mdash; {{ .name }} ({{ .ref }})</title> 14 + {{ else if .commit }} 15 + <title>{{ .meta.Title }} &mdash; {{ .name }}: {{ .commit.This }}</title> 16 + {{ else if .branches }} 17 + <title>{{ .meta.Title }} &mdash; {{ .name }}: refs</title> 18 + {{ else if .commits }} 19 + {{ if .log }} 20 + <title>{{ .meta.Title }} &mdash; {{ .name }}: log</title> 21 + {{ else }} 22 + <title>{{ .meta.Title }} &mdash; {{ .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
··· 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
··· 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 }}
+14
legit/templates/nav.html
··· 1 + {{ define "nav" }} 2 + <nav> 3 + <ul> 4 + {{ if .name }} 5 + <li><a href="/{{ .name }}">summary</a> 6 + <li><a href="/{{ .name }}/refs">refs</a> 7 + {{ if .ref }} 8 + <li><a href="/{{ .name }}/tree/{{ .ref }}/">tree</a> 9 + <li><a href="/{{ .name }}/log/{{ .ref }}">log</a> 10 + {{ end }} 11 + {{ end }} 12 + </ul> 13 + </nav> 14 + {{ end }}
+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
··· 1 + {{ define "repoheader" }} 2 + <header> 3 + <h2> 4 + <a href="/">all repos</a> 5 + &mdash; {{ .displayname }} 6 + {{ if .ref }} 7 + <span class="ref">@ {{ .ref }}</span> 8 + {{ end }} 9 + </h2> 10 + <h3 class="desc">{{ .desc }}</h3> 11 + </header> 12 + {{ end }}
+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
··· 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 }}