initial commit

Signed-off-by: jolheiser <john.olheiser@gmail.com>
main
jolheiser 2024-01-15 16:26:51 -06:00
parent 4731c0bd35
commit 868ca2125a
Signed by: jolheiser
GPG Key ID: B853ADA5DA7BBF7A
43 changed files with 2997 additions and 135 deletions

2
.gitignore vendored
View File

@ -1 +1,3 @@
/ugit*
.ssh/
.ugit/

View File

@ -0,0 +1,5 @@
[[language]]
name = "templ"
language-id = "html"
language-servers = ["templ", "vscode-html-language-server", "tailwindcss-ls"]

32
README.md 100644
View File

@ -0,0 +1,32 @@
# ugit
<img style="width: 50px;" alt="ugit logo" src="/ugit/tree/main/assets/ugit.svg?raw&pretty"/>
Minimal git server
ugit allows cloning via HTTPS/SSH, but can only be pushed to via SSH.
There are no plans to directly support issues or PR workflows, although webhooks are planned and auxillary software may be created to facilitate these things.
For now, if you wish to collaborate, please send me patches at [john+ugit@jolheiser.com](mailto:john+ugit@jolheiser.com).
Currently all HTML is allowed in markdown, ugit is intended to be run by/for a trusted user.
## Getting your public SSH keys from another forge
Using GitHub as an example (although Gitea/GitLab should have the same URL scheme)
Ba/sh
```sh
curl https://github.com/<username>.keys > path/to/authorized_keys
```
Nushell
```sh
http get https://github.com/<username>.keys | save --force path/to/authorized_keys
```
## License
[MIT](LICENSE)
Lots of inspiration and some starting code used from [wish](https://github.com/charmbracelet/wish) [(MIT)](https://github.com/charmbracelet/wish/blob/3e6f92a166118390484ce4a0904114b375b9e485/LICENSE) and [legit](https://github.com/icyphox/legit) [(MIT)](https://github.com/icyphox/legit/blob/bdfc973207a67a3b217c130520d53373d088763c/license).

19
assets/assets.go 100644
View File

@ -0,0 +1,19 @@
package assets
import "embed"
var (
//go:embed *.svg
Icons embed.FS
LinkIcon = must("link.svg")
EmailIcon = must("email.svg")
LogoIcon = must("ugit.svg")
)
func must(path string) []byte {
content, err := Icons.ReadFile(path)
if err != nil {
panic(err)
}
return content
}

1
assets/email.svg 100644
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><title>email icon</title><path d="M3 8L8.44992 11.6333C9.73295 12.4886 10.3745 12.9163 11.0678 13.0825C11.6806 13.2293 12.3194 13.2293 12.9322 13.0825C13.6255 12.9163 14.2671 12.4886 15.5501 11.6333L21 8M6.2 19H17.8C18.9201 19 19.4802 19 19.908 18.782C20.2843 18.5903 20.5903 18.2843 20.782 17.908C21 17.4802 21 16.9201 21 15.8V8.2C21 7.0799 21 6.51984 20.782 6.09202C20.5903 5.71569 20.2843 5.40973 19.908 5.21799C19.4802 5 18.9201 5 17.8 5H6.2C5.0799 5 4.51984 5 4.09202 5.21799C3.71569 5.40973 3.40973 5.71569 3.21799 6.09202C3 6.51984 3 7.07989 3 8.2V15.8C3 16.9201 3 17.4802 3.21799 17.908C3.40973 18.2843 3.71569 18.5903 4.09202 18.782C4.51984 19 5.07989 19 6.2 19Z" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>

After

Width:  |  Height:  |  Size: 861 B

1
assets/link.svg 100644
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><title>link icon</title><path d="M9.16488 17.6505C8.92513 17.8743 8.73958 18.0241 8.54996 18.1336C7.62175 18.6695 6.47816 18.6695 5.54996 18.1336C5.20791 17.9361 4.87912 17.6073 4.22153 16.9498C3.56394 16.2922 3.23514 15.9634 3.03767 15.6213C2.50177 14.6931 2.50177 13.5495 3.03767 12.6213C3.23514 12.2793 3.56394 11.9505 4.22153 11.2929L7.04996 8.46448C7.70755 7.80689 8.03634 7.47809 8.37838 7.28062C9.30659 6.74472 10.4502 6.74472 11.3784 7.28061C11.7204 7.47809 12.0492 7.80689 12.7068 8.46448C13.3644 9.12207 13.6932 9.45086 13.8907 9.7929C14.4266 10.7211 14.4266 11.8647 13.8907 12.7929C13.7812 12.9825 13.6314 13.1681 13.4075 13.4078M10.5919 10.5922C10.368 10.8319 10.2182 11.0175 10.1087 11.2071C9.57284 12.1353 9.57284 13.2789 10.1087 14.2071C10.3062 14.5492 10.635 14.878 11.2926 15.5355C11.9502 16.1931 12.279 16.5219 12.621 16.7194C13.5492 17.2553 14.6928 17.2553 15.621 16.7194C15.9631 16.5219 16.2919 16.1931 16.9495 15.5355L19.7779 12.7071C20.4355 12.0495 20.7643 11.7207 20.9617 11.3787C21.4976 10.4505 21.4976 9.30689 20.9617 8.37869C20.7643 8.03665 20.4355 7.70785 19.7779 7.05026C19.1203 6.39267 18.7915 6.06388 18.4495 5.8664C17.5212 5.3305 16.3777 5.3305 15.4495 5.8664C15.2598 5.97588 15.0743 6.12571 14.8345 6.34955" stroke-width="2" stroke-linecap="round"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

10
assets/ugit.svg 100644
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 24 24" stroke="#de4c36" fill="#de4c36" stroke-width="1" xmlns="http://www.w3.org/2000/svg">
<title>ugit icon</title>
<rect fill="none" x="1" y="1" rx="1" ry="1" width="22" height="22"></rect>
<ellipse cx="6" cy="6" rx="2" ry="2"></ellipse>
<ellipse cx="18" cy="6" rx="2" ry="2"></ellipse>
<ellipse cx="18" cy="18" rx="2" ry="2"></ellipse>
<line stroke-width="1.5" x1="18" y1="5" x2="18" y2="18"></line>
<path stroke-width="1.5" d="M6 6 q 0 12 12 12" fill="none" />
</svg>

After

Width:  |  Height:  |  Size: 543 B

View File

@ -2,19 +2,97 @@ package main
import (
"flag"
"fmt"
"strings"
"github.com/peterbourgon/ff/v3"
"github.com/peterbourgon/ff/v3/ffyaml"
)
type args struct {
db string
type cliArgs struct {
Debug bool
RepoDir string
SSH sshArgs
HTTP httpArgs
Meta metaArgs
Profile profileArgs
}
func parseArgs(args []string) (a args, e error) {
type sshArgs struct {
AuthorizedKeys string
CloneURL string
Port int
HostKey string
}
type httpArgs struct {
CloneURL string
Port int
}
type metaArgs struct {
Title string
Description string
}
type profileArgs struct {
Username string
Email string
Links []profileLink
}
type profileLink struct {
Name string
URL string
}
func parseArgs(args []string) (c cliArgs, e error) {
fs := flag.NewFlagSet("ugitd", flag.ContinueOnError)
fs.String("config", "ugit.yaml", "Path to config file")
return a, ff.Parse(fs, args,
c = cliArgs{
RepoDir: ".ugit",
SSH: sshArgs{
AuthorizedKeys: ".ssh/authorized_keys",
CloneURL: "ssh://localhost:8448",
Port: 8448,
HostKey: ".ssh/ugit_ed25519",
},
HTTP: httpArgs{
CloneURL: "http://localhost:8449",
Port: 8449,
},
Meta: metaArgs{
Title: "ugit",
Description: "Minimal git server",
},
}
fs.BoolVar(&c.Debug, "debug", c.Debug, "Debug logging")
fs.StringVar(&c.RepoDir, "repo-dir", c.RepoDir, "Path to directory containing repositories")
fs.StringVar(&c.SSH.AuthorizedKeys, "ssh.authorized-keys", c.SSH.AuthorizedKeys, "Path to authorized_keys")
fs.StringVar(&c.SSH.CloneURL, "ssh.clone-url", c.SSH.CloneURL, "SSH clone URL base")
fs.IntVar(&c.SSH.Port, "ssh.port", c.SSH.Port, "SSH port")
fs.StringVar(&c.SSH.HostKey, "ssh.host-key", c.SSH.HostKey, "SSH host key (created if it doesn't exist)")
fs.StringVar(&c.HTTP.CloneURL, "http.clone-url", c.HTTP.CloneURL, "HTTP clone URL base")
fs.IntVar(&c.HTTP.Port, "http.port", c.HTTP.Port, "HTTP port")
fs.StringVar(&c.Meta.Title, "meta.title", c.Meta.Title, "App title")
fs.StringVar(&c.Meta.Description, "meta.description", c.Meta.Description, "App description")
fs.StringVar(&c.Profile.Username, "profile.username", c.Profile.Username, "Username for index page")
fs.StringVar(&c.Profile.Email, "profile.email", c.Profile.Email, "Email for index page")
fs.Func("profile.links", "Link(s) for index page", func(s string) error {
parts := strings.SplitN(s, ",", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid profile link %q", s)
}
c.Profile.Links = append(c.Profile.Links, profileLink{
Name: parts[0],
URL: parts[1],
})
return nil
})
return c, ff.Parse(fs, args,
ff.WithEnvVarPrefix("UGIT"),
ff.WithConfigFileFlag("config"),
ff.WithAllowMissingConfigFile(true),

View File

@ -1,14 +1,85 @@
package main
import (
"errors"
"flag"
"fmt"
"os"
"os/signal"
"go.jolheiser.com/ugit/internal/http"
"go.jolheiser.com/ugit/internal/ssh"
"github.com/charmbracelet/log"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-git/go-git/v5/utils/trace"
)
func main() {
args, err := parseArgs(os.Args[1:])
if err != nil {
if errors.Is(err, flag.ErrHelp) {
return
}
panic(err)
}
fmt.Println(args)
if args.Debug {
trace.SetTarget(trace.Packet)
log.SetLevel(log.DebugLevel)
} else {
middleware.DefaultLogger = http.NoopLogger
ssh.DefaultLogger = ssh.NoopLogger
}
if err := os.MkdirAll(args.RepoDir, os.ModePerm); err != nil {
panic(err)
}
sshSettings := ssh.Settings{
AuthorizedKeys: args.SSH.AuthorizedKeys,
CloneURL: args.SSH.CloneURL,
Port: args.SSH.Port,
HostKey: args.SSH.HostKey,
RepoDir: args.RepoDir,
}
sshSrv, err := ssh.New(sshSettings)
if err != nil {
panic(err)
}
go func() {
fmt.Printf("SSH listening on ssh://localhost:%d\n", sshSettings.Port)
if err := sshSrv.ListenAndServe(); err != nil {
panic(err)
}
}()
httpSettings := http.Settings{
Title: args.Meta.Title,
Description: args.Meta.Description,
CloneURL: args.HTTP.CloneURL,
Port: args.HTTP.Port,
RepoDir: args.RepoDir,
Profile: http.Profile{
Username: args.Profile.Username,
Email: args.Profile.Email,
},
}
for _, link := range args.Profile.Links {
httpSettings.Profile.Links = append(httpSettings.Profile.Links, http.Link{
Name: link.Name,
URL: link.URL,
})
}
httpSrv := http.New(httpSettings)
go func() {
fmt.Printf("HTTP listening on http://localhost:%d\n", httpSettings.Port)
if err := httpSrv.ListenAndServe(); err != nil {
panic(err)
}
}()
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Kill, os.Interrupt)
<-ch
}

View File

@ -1,78 +1,26 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"id": "flake-utils",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1701020769,
"narHash": "sha256-4YzCo7xMzkG/t/VlTHqOg9hvXCvqdWYDX/jpF0h+Wr8=",
"lastModified": 1704161960,
"narHash": "sha256-QGua89Pmq+FBAro8NriTuoO/wNaUtugt29/qqA8zeeM=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b608fc233c0592210250974d1bb3c11dfaf95e58",
"rev": "63143ac2c9186be6d9da6035fa22620018c85932",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nur": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1700580516,
"narHash": "sha256-h72i6afGKreU+DjpZ6+qersarYYp4YjX+DBQ+MQkOG4=",
"ref": "refs/heads/main",
"rev": "a68a81cbc743e84aaee331ae7e58699398dd732d",
"revCount": 167,
"type": "git",
"url": "https://git.jojodev.com/jolheiser/nur"
},
"original": {
"type": "git",
"url": "https://git.jojodev.com/jolheiser/nur"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"nur": "nur",
"tailwind-ctp": "tailwind-ctp"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
"tailwind-ctp": "tailwind-ctp",
"tailwind-ctp-lsp": "tailwind-ctp-lsp"
}
},
"tailwind-ctp": {
@ -94,6 +42,26 @@
"type": "git",
"url": "https://git.jojodev.com/jolheiser/tailwind-ctp"
}
},
"tailwind-ctp-lsp": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1699401590,
"narHash": "sha256-nx8ExuBRUux9eXSUgkWp1LJMvA3dmA76+2xggZjHTU0=",
"ref": "refs/heads/master",
"rev": "b321333ad08bf21db242f246b10ad4a50b8fc8a0",
"revCount": 848,
"type": "git",
"url": "https://git.jojodev.com/jolheiser/tailwind-ctp-intellisense"
},
"original": {
"type": "git",
"url": "https://git.jojodev.com/jolheiser/tailwind-ctp-intellisense"
}
}
},
"root": "root",

151
flake.nix
View File

@ -1,36 +1,163 @@
{
description = "Minimal git server";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs";
nur = {
url = "git+https://git.jojodev.com/jolheiser/nur";
inputs.nixpkgs.follows = "nixpkgs";
};
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
tailwind-ctp = {
url = "git+https://git.jojodev.com/jolheiser/tailwind-ctp";
inputs.nixpkgs.follows = "nixpkgs";
};
tailwind-ctp-lsp = {
url = "git+https://git.jojodev.com/jolheiser/tailwind-ctp-intellisense";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = {
self,
flake-utils,
nixpkgs,
nur,
tailwind-ctp,
tailwind-ctp-lsp,
} @ inputs: let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
nur = inputs.nur.packages.${system};
tailwind-ctp = inputs.tailwind-ctp.packages.${system}.default;
tailwind-ctp-lsp = inputs.tailwind-ctp-lsp.packages.${system}.default;
ugit = pkgs.buildGoModule rec {
pname = "ugitd";
version = "0.0.1";
src = pkgs.nix-gitignore.gitignoreSource [] (builtins.path {
name = pname;
path = ./.;
});
subPackages = ["cmd/ugitd"];
vendorHash = "sha256-E4cwC6c0d+HvHldqGYiWdPEdS2fch6imvAXzxb2MMdY=";
meta = with pkgs.lib; {
description = "Minimal git server";
homepage = "https://git.jolheiser.com/ugit";
maintainers = with maintainers; [jolheiser];
mainProgram = "ugitd";
};
};
in {
packages.${system}.default = ugit;
devShells.${system}.default = pkgs.mkShell {
nativeBuildInputs = with pkgs; [
# go
# gopls
nur.templ
go
gopls
templ
tailwind-ctp
sqlc
tailwind-ctp-lsp
vscode-langservers-extracted
];
};
nixosModules.default = {
pkgs,
lib,
config,
...
}: let
cfg = config.services.ugit;
configFile = pkgs.writeText "ugit.yaml" cfg.configFile;
authorizedKeysFile = pkgs.writeText "ugit_keys" (builtins.concatStringsSep "\n" cfg.authorizedKeys);
in {
options = with lib; {
services.ugit = {
enable = mkEnableOption "Enable ugit";
package = mkOption {
type = types.package;
description = "ugit package to use";
default = ugit;
};
repoDir = mkOption {
type = types.str;
description = "where ugit stores repositories";
default = "/var/lib/ugit/repos";
};
authorizedKeys = mkOption {
type = types.listOf types.str;
description = "list of keys to use for authorized_keys";
default = [];
};
authorizedKeysFile = mkOption {
type = types.str;
description = "path to authorized_keys file ugit uses for auth";
default = "/var/lib/ugit/authorized_keys";
};
hostKeyFile = mkOption {
type = types.str;
description = "path to host key file (will be created if it doesn't exist)";
default = "/var/lib/ugit/ugit_ed25519";
};
configFile = mkOption {
type = types.str;
default = "";
description = "config.yaml contents";
};
user = mkOption {
type = types.str;
default = "ugit";
description = "User account under which ugit runs";
};
group = mkOption {
type = types.str;
default = "ugit";
description = "Group account under which ugit runs";
};
debug = mkOption {
type = types.bool;
default = false;
};
openFirewall = mkOption {
type = types.bool;
default = false;
};
};
};
config = lib.mkIf cfg.enable {
users.users."${cfg.user}" = {
home = "/var/lib/ugit";
createHome = true;
group = "${cfg.group}";
isSystemUser = true;
isNormalUser = false;
description = "user for ugit service";
};
users.groups."${cfg.group}" = {};
networking.firewall = lib.mkIf cfg.openFirewall {
allowedTCPPorts = [8448 8449];
};
systemd.services.ugit = {
enable = true;
script = let
authorizedKeysPath =
if (builtins.length cfg.authorizedKeys) > 0
then authorizedKeysFile
else cfg.authorizedKeysFile;
args = ["--config=${configFile}" "--repo-dir=${cfg.repoDir}" "--ssh.authorized-keys=${authorizedKeysPath}" "--ssh.host-key=${cfg.hostKeyFile}"] ++ lib.optionals cfg.debug ["--debug"];
in "${cfg.package}/bin/ugitd ${builtins.concatStringsSep " " args}";
wantedBy = ["multi-user.target"];
after = ["network-online.target"];
serviceConfig = {
User = cfg.user;
Group = cfg.group;
Restart = "always";
RestartSec = "15";
WorkingDirectory = "/var/lib/ugit";
};
};
};
};
};
}

40
go.mod
View File

@ -3,49 +3,53 @@ module go.jolheiser.com/ugit
go 1.20
require (
github.com/a-h/templ v0.2.513
github.com/alecthomas/chroma/v2 v2.12.0
github.com/charmbracelet/log v0.2.5
github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103
github.com/charmbracelet/wish v1.2.0
github.com/dustin/go-humanize v1.0.1
github.com/go-chi/chi/v5 v5.0.11
github.com/go-git/go-billy/v5 v5.5.0
github.com/go-git/go-git/v5 v5.11.0
github.com/peterbourgon/ff/v3 v3.4.0
github.com/yuin/goldmark v1.6.0
github.com/yuin/goldmark-emoji v1.0.2
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
)
require (
dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
github.com/acomagu/bufpipe v1.0.4 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/keygen v0.5.0 // indirect
github.com/charmbracelet/lipgloss v0.9.1 // indirect
github.com/charmbracelet/log v0.2.5 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/cloudflare/circl v1.3.6 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/go-git/go-git/v5 v5.10.0 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/skeema/knownhosts v1.2.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/skeema/knownhosts v1.2.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/tools v0.16.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/peterbourgon/ff/v3 v3.4.0
golang.org/x/mod v0.12.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/tools v0.13.0 // indirect
)

81
go.sum
View File

@ -3,10 +3,16 @@ dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ=
github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4=
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE=
github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
github.com/a-h/templ v0.2.513 h1:ZmwGAOx4NYllnHy+FTpusc4+c5msoMpPIYX0Oy3dNqw=
github.com/a-h/templ v0.2.513/go.mod h1:9gZxTLtRzM3gQxO8jr09Na0v8/jfliS97S9W5SScanM=
github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink=
github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs=
github.com/alecthomas/chroma/v2 v2.12.0 h1:Wh8qLEgMMsN7mgyG8/qIpegky2Hvzr4By6gEF7cmWgw=
github.com/alecthomas/chroma/v2 v2.12.0/go.mod h1:4TQu7gdfuPjSh76j78ietmqh9LiurGF0EpseFXdKMBw=
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.2.0 h1:HAzS41CIzNW5syS8Mf9UwXhNH1J9aix/BvDRf1Ml2Yk=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
@ -23,29 +29,39 @@ github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103 h1:wpHMERIN0pQZE
github.com/charmbracelet/ssh v0.0.0-20221117183211-483d43d97103/go.mod h1:0Vm2/8yBljiLDnGJHU8ehswfawrEybGk33j5ssqKQVM=
github.com/charmbracelet/wish v1.2.0 h1:h5Wj9pr97IQz/l4gM5Xep2lXcY/YM+6O2RC2o3x0JIQ=
github.com/charmbracelet/wish v1.2.0/go.mod h1:JX3fC+178xadJYAhPu6qWtVDpJTwpnFvpdjz9RKJlUE=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg=
github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/go-chi/chi/v5 v5.0.11 h1:BnpYbFZ3T3S1WMpD79r7R5ThWX40TaFB7L31Y8xqSwA=
github.com/go-chi/chi/v5 v5.0.11/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU=
github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git/v5 v5.10.0 h1:F0x3xXrAWmhwtzoCokU4IMPcBdncG+HAAqi9FcOOjbQ=
github.com/go-git/go-git/v5 v5.10.0/go.mod h1:1FOZ/pQnqw24ghP2n7cunVl0ON55BsjPYvhWHvZGhoo=
github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4=
github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY=
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
@ -57,10 +73,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
@ -81,30 +95,39 @@ github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM=
github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo=
github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ=
github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.3.7/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-emoji v1.0.2 h1:c/RgTShNgHTtc6xdz2KKI74jJr6rWi7FPgnP9GAsO5s=
github.com/yuin/goldmark-emoji v1.0.2/go.mod h1:RhP/RWpexdp+KHs7ghKnifRoIs/Bq4nDS7tRbCkOwKY=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -112,12 +135,12 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -132,15 +155,15 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@ -148,13 +171,13 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -162,8 +185,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

139
internal/git/git.go 100644
View File

@ -0,0 +1,139 @@
package git
import (
"errors"
"io/fs"
"os"
"path/filepath"
"sort"
"github.com/dustin/go-humanize"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
)
// EnsureRepo ensures that the repo exists in the given directory
func EnsureRepo(dir string, repo string) error {
exists, err := PathExists(dir)
if err != nil {
return err
}
if !exists {
err = os.MkdirAll(dir, os.ModeDir|os.FileMode(0o700))
if err != nil {
return err
}
}
rp := filepath.Join(dir, repo)
exists, err = PathExists(rp)
if err != nil {
return err
}
if !exists {
_, err := git.PlainInit(rp, true)
if err != nil {
return err
}
}
return nil
}
// PathExists checks if a path exists and returns true if it does
func PathExists(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if errors.Is(err, fs.ErrNotExist) {
return false, nil
}
return true, err
}
func (r Repo) Tree(ref string) (*object.Tree, error) {
g, err := r.Git()
if err != nil {
return nil, err
}
hash, err := g.ResolveRevision(plumbing.Revision(ref))
if err != nil {
return nil, err
}
c, err := g.CommitObject(*hash)
if err != nil {
return nil, err
}
return c.Tree()
}
type FileInfo struct {
Path string
IsDir bool
Mode string
Size string
}
func (f FileInfo) Name() string {
return filepath.Base(f.Path)
}
func (r Repo) Dir(ref, path string) ([]FileInfo, error) {
t, err := r.Tree(ref)
if err != nil {
return nil, err
}
if path != "" {
t, err = t.Tree(path)
if err != nil {
return nil, err
}
}
fis := make([]FileInfo, 0)
for _, entry := range t.Entries {
fm, err := entry.Mode.ToOSFileMode()
if err != nil {
return nil, err
}
size, err := t.Size(entry.Name)
if err != nil {
return nil, err
}
fis = append(fis, FileInfo{
Path: filepath.Join(path, entry.Name),
IsDir: fm.IsDir(),
Mode: fm.String(),
Size: humanize.Bytes(uint64(size)),
})
}
sort.Slice(fis, func(i, j int) bool {
fi1 := fis[i]
fi2 := fis[j]
return (fi1.IsDir && !fi2.IsDir) || fi1.Name() < fi2.Name()
})
return fis, nil
}
func (r Repo) FileContent(ref, file string) (string, error) {
t, err := r.Tree(ref)
if err != nil {
return "", err
}
f, err := t.File(file)
if err != nil {
return "", err
}
content, err := f.Contents()
if err != nil {
return "", err
}
return content, nil
}

View File

@ -0,0 +1,62 @@
package git
import (
"encoding/json"
"errors"
"io/fs"
"os"
"path/filepath"
)
type RepoMeta struct {
Description string `json:"description"`
Private bool `json:"private"`
}
func (m *RepoMeta) Update(meta RepoMeta) error {
data, err := json.Marshal(meta)
if err != nil {
return err
}
return json.Unmarshal(data, m)
}
func (r Repo) metaPath() string {
return filepath.Join(r.path, "ugit.json")
}
func (r Repo) SaveMeta() error {
// Compatibility with gitweb, because why not
// Ignoring the error because it's not technically detrimental to ugit
desc, err := os.Create(filepath.Join(r.path, "description"))
if err == nil {
defer desc.Close()
desc.WriteString(r.Meta.Description)
}
fi, err := os.Create(r.metaPath())
if err != nil {
return err
}
defer fi.Close()
return json.NewEncoder(fi).Encode(r.Meta)
}
func ensureJSONFile(path string) error {
_, err := os.Stat(path)
if err == nil {
return nil
}
if !errors.Is(err, fs.ErrNotExist) {
return err
}
fi, err := os.Create(path)
if err != nil {
return err
}
defer fi.Close()
if _, err := fi.WriteString(`{"private":true}`); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,223 @@
package git
import (
"bufio"
"context"
"fmt"
"io"
"strconv"
"strings"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/format/pktline"
"github.com/go-git/go-git/v5/plumbing/protocol/packp"
"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
"github.com/go-git/go-git/v5/plumbing/serverinfo"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/server"
"github.com/go-git/go-git/v5/storage/filesystem"
"github.com/go-git/go-git/v5/utils/ioutil"
)
type ReadWriteContexter interface {
io.ReadWriteCloser
Context() context.Context
}
type Protocol struct {
endpoint *transport.Endpoint
server transport.Transport
}
func NewProtocol(repoPath string) (Protocol, error) {
endpoint, err := transport.NewEndpoint("/")
if err != nil {
return Protocol{}, err
}
fs := osfs.New(repoPath)
loader := server.NewFilesystemLoader(fs)
gitServer := server.NewServer(loader)
return Protocol{
endpoint: endpoint,
server: gitServer,
}, nil
}
func (p Protocol) HTTPInfoRefs(rwc ReadWriteContexter) error {
session, err := p.server.NewUploadPackSession(p.endpoint, nil)
if err != nil {
return err
}
defer ioutil.CheckClose(rwc, &err)
return p.infoRefs(rwc, session, "# service=git-upload-pack")
}
func (p Protocol) infoRefs(rwc ReadWriteContexter, session transport.UploadPackSession, prefix string) error {
ar, err := session.AdvertisedReferencesContext(rwc.Context())
if err != nil {
return err
}
if prefix != "" {
ar.Prefix = [][]byte{
[]byte(prefix),
pktline.Flush,
}
}
if err := ar.Encode(rwc); err != nil {
return err
}
return nil
}
func (p Protocol) HTTPUploadPack(rwc ReadWriteContexter) error {
return p.uploadPack(rwc, false)
}
func (p Protocol) SSHUploadPack(rwc ReadWriteContexter) error {
return p.uploadPack(rwc, true)
}
func (p Protocol) uploadPack(rwc ReadWriteContexter, ssh bool) error {
session, err := p.server.NewUploadPackSession(p.endpoint, nil)
if err != nil {
return err
}
defer ioutil.CheckClose(rwc, &err)
if ssh {
if err := p.infoRefs(rwc, session, ""); err != nil {
return err
}
}
req := packp.NewUploadPackRequest()
if err := req.Decode(rwc); err != nil {
return err
}
var resp *packp.UploadPackResponse
resp, err = session.UploadPack(rwc.Context(), req)
if err != nil {
return err
}
if err := resp.Encode(rwc); err != nil {
return fmt.Errorf("could not encode upload pack: %w", err)
}
return nil
}
func (p Protocol) SSHReceivePack(rwc ReadWriteContexter, repo *Repo) error {
buf := bufio.NewReader(rwc)
session, err := p.server.NewReceivePackSession(p.endpoint, nil)
if err != nil {
return err
}
ar, err := session.AdvertisedReferencesContext(rwc.Context())
if err != nil {
return fmt.Errorf("internal error in advertised references: %w", err)
}
_ = ar.Capabilities.Set(capability.PushOptions)
_ = ar.Capabilities.Set("no-thin")
if err := ar.Encode(rwc); err != nil {
return fmt.Errorf("error in advertised references encoding: %w", err)
}
req := packp.NewReferenceUpdateRequest()
_ = req.Capabilities.Set(capability.ReportStatus)
if err := req.Decode(buf); err != nil {
// FIXME this is a hack, but go-git doesn't accept a 0000 if there are no refs to update
if !strings.EqualFold(err.Error(), "capabilities delimiter not found") {
return fmt.Errorf("error decoding: %w", err)
}
}
// FIXME also a hack, if the next bytes are PACK then we have a packfile, otherwise assume it's push options
peek, err := buf.Peek(4)
if err != nil {
return err
}
if string(peek) != "PACK" {
s := pktline.NewScanner(buf)
for s.Scan() {
val := string(s.Bytes())
if val == "" {
break
}
if s.Err() != nil {
return s.Err()
}
parts := strings.SplitN(val, "=", 2)
req.Options = append(req.Options, &packp.Option{
Key: parts[0],
Value: parts[1],
})
}
}
if err := handlePushOptions(repo, req.Options); err != nil {
return fmt.Errorf("could not handle push options: %w", err)
}
// FIXME if there are only delete commands, there is no packfile and ReceivePack will block forever
noPack := true
for _, c := range req.Commands {
if c.Action() != packp.Delete {
noPack = false
break
}
}
if noPack {
req.Packfile = nil
}
rs, err := session.ReceivePack(rwc.Context(), req)
if err != nil {
return fmt.Errorf("error in receive pack: %w", err)
}
if err := rs.Encode(rwc); err != nil {
return fmt.Errorf("could not encode receive pack: %w", err)
}
return nil
}
func handlePushOptions(repo *Repo, opts []*packp.Option) error {
var changed bool
for _, opt := range opts {
switch strings.ToLower(opt.Key) {
case "desc", "description":
changed = repo.Meta.Description != opt.Value
repo.Meta.Description = opt.Value
case "private":
private, err := strconv.ParseBool(opt.Value)
if err != nil {
continue
}
changed = repo.Meta.Private != private
repo.Meta.Private = private
}
}
if changed {
return repo.SaveMeta()
}
return nil
}
func UpdateServerInfo(repo string) error {
r, err := git.PlainOpen(repo)
if err != nil {
return err
}
fs := r.Storer.(*filesystem.Storage).Filesystem()
return serverinfo.UpdateServerInfo(r.Storer, fs)
}

View File

@ -0,0 +1,103 @@
package git
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
)
type Repo struct {
path string
Meta RepoMeta
}
func (r Repo) Name() string {
return strings.TrimSuffix(filepath.Base(r.path), ".git")
}
func NewRepo(dir, name string) (*Repo, error) {
if !strings.HasSuffix(name, ".git") {
name += ".git"
}
r := &Repo{
path: filepath.Join(dir, name),
}
_, err := os.Stat(r.path)
if err != nil {
return nil, err
}
if err := ensureJSONFile(r.metaPath()); err != nil {
return nil, err
}
fi, err := os.Open(r.metaPath())
if err != nil {
return nil, err
}
defer fi.Close()
if err := json.NewDecoder(fi).Decode(&r.Meta); err != nil {
return nil, err
}
return r, nil
}
// DefaultBranch returns the branch referenced by HEAD, setting it if needed
func (r Repo) DefaultBranch() (string, error) {
repo, err := r.Git()
if err != nil {
return "", err
}
ref, err := repo.Head()
if err != nil {
if !errors.Is(err, plumbing.ErrReferenceNotFound) {
return "", err
}
brs, err := repo.Branches()
if err != nil {
return "", err
}
defer brs.Close()
fb, err := brs.Next()
if err != nil {
return "", err
}
// Rename the default branch to the first branch available
ref = fb
sym := plumbing.NewSymbolicReference(plumbing.HEAD, fb.Name())
if err := repo.Storer.SetReference(sym); err != nil {
return "", err
}
}
return strings.TrimPrefix(ref.Name().String(), "refs/heads/"), nil
}
// Git allows access to the git repository
func (r Repo) Git() (*git.Repository, error) {
return git.PlainOpen(r.path)
}
// LastCommit returns the last commit of the repo
func (r Repo) LastCommit() (*object.Commit, error) {
repo, err := r.Git()
if err != nil {
return nil, err
}
head, err := repo.Head()
if err != nil {
return nil, err
}
return repo.CommitObject(head.Hash())
}

View File

@ -0,0 +1,24 @@
package html
type BaseContext struct {
Title string
Description string
}
templ base(bc BaseContext) {
<!DOCTYPE html>
<html>
<head>
<title>{ bc.Title }</title>
<link rel="icon" href="/_/favicon.svg"/>
<link rel="stylesheet" href="/_/tailwind.css"/>
<meta property="og:title" content={ bc.Title }/>
<meta property="og:description" content={ bc.Description }/>
</head>
<body class="latte dark:mocha bg-base/50 dark:bg-base/95 max-w-7xl mx-auto my-10">
<h2 class="text-text text-xl mb-3"><a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href="/">Home</a></h2>
{ children... }
</body>
</html>
}

View File

@ -0,0 +1,86 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.501
package html
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
type BaseContext struct {
Title string
Description string
}
func base(bc BaseContext) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<!doctype html><html><head><title>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 string
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(bc.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `base.templ`, Line: 11, Col: 20}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</title><link rel=\"icon\" href=\"/_/favicon.svg\"><link rel=\"stylesheet\" href=\"/_/tailwind.css\"><meta property=\"og:title\" content=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(bc.Title))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"><meta property=\"og:description\" content=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(bc.Description))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\"></head><body class=\"latte dark:mocha bg-base/50 dark:bg-base/95 max-w-7xl mx-auto my-10\"><h2 class=\"text-text text-xl mb-3\"><a class=\"underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"/\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var3 := `Home`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></h2>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,42 @@
package html
import (
"io"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
)
var (
Formatter = html.New(
html.WithLineNumbers(true),
html.WithLinkableLineNumbers(true, "L"),
html.WithClasses(true),
html.LineNumbersInTable(true),
)
Code = code{}
)
type code struct{}
func (c code) Convert(source []byte, fileName string, writer io.Writer) error {
lexer := lexers.Match(fileName)
if lexer == nil {
lexer = lexers.Fallback
}
lexer = chroma.Coalesce(lexer)
style := styles.Get("catppuccin-mocha")
if style == nil {
style = styles.Fallback
}
iter, err := lexer.Tokenise(nil, string(source))
if err != nil {
return err
}
return Formatter.Format(writer, style, iter)
}

View File

@ -0,0 +1,44 @@
.markdown * {
all: revert;
color: rgb(var(--ctp-text));
}
.markdown a {
color: rgb(var(--ctp-blue));
text-decoration-line: underline;
text-decoration-style: dashed;
}
.markdown a:hover {
text-decoration-style: solid;
}
.chroma {
font-size: small;
}
.chroma * {
background-color: rgb(var(--ctp-base)) !important;
}
.chroma table {
border-spacing: 5px 0 !important;
}
.chroma .lnt {
color: rgb(var(--ctp-subtext1)) !important;
}
.chroma .lnt:target,
.chroma .lnt:focus {
color: rgb(var(--ctp-subtext0)) !important;
}
.chroma .line {
white-space: break-spaces;
}
.chroma .line.active,
.chroma .line.active * {
background: rgb(var(--ctp-surface0)) !important;
}

View File

@ -0,0 +1,96 @@
//go:build generate
package main
import (
"bytes"
_ "embed"
"fmt"
"go/format"
"os"
"os/exec"
"go.jolheiser.com/ugit/internal/html"
"github.com/alecthomas/chroma/v2/styles"
)
var (
tailwindCSS = `
@tailwind base;
@tailwind components;
@tailwind utilities;
`
//go:embed generate.css
otherCSS string
)
//go:generate templ generate
//go:generate go run generate.go
func main() {
if err := tailwind(); err != nil {
panic(err)
}
}
func tailwind() error {
fmt.Println("generating tailwind...")
tmp, err := os.CreateTemp(os.TempDir(), "ugit-tailwind*")
if err != nil {
return err
}
defer os.Remove(tmp.Name())
if _, err := tmp.WriteString(tailwindCSS + otherCSS); err != nil {
return err
}
fmt.Println("generating chroma styles...")
latte := styles.Get("catppuccin-latte")
if err := html.Formatter.WriteCSS(tmp, latte); err != nil {
return err
}
tmp.WriteString("@media (prefers-color-scheme: dark) {")
mocha := styles.Get("catppuccin-mocha")
if err := html.Formatter.WriteCSS(tmp, mocha); err != nil {
return err
}
tmp.WriteString("}")
fmt.Println("finished generating chroma styles")
tmp.Close()
styles, err := os.Create("tailwind.go")
if err != nil {
return err
}
defer styles.Close()
var buf bytes.Buffer
cmd := exec.Command("tailwind-ctp", "-i", tmp.Name(), "--minify")
cmd.Stdout = &buf
if err := cmd.Run(); err != nil {
return err
}
code := fmt.Sprintf(`// Code generated by generate.go - DO NOT EDIT.
package html
import "net/http"
func TailwindHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
w.Write([]byte(%q))
}`, buf.String())
formatted, err := format.Source([]byte(code))
if err != nil {
return err
}
styles.Write(formatted)
fmt.Println("finished generating tailwind")
return nil
}

View File

@ -0,0 +1,72 @@
package html
import "go.jolheiser.com/ugit/internal/git"
import "github.com/dustin/go-humanize"
import "go.jolheiser.com/ugit/assets"
type IndexContext struct {
BaseContext
Profile IndexProfile
CloneURL string
Repos []*git.Repo
}
type IndexProfile struct {
Username string
Email string
Links []IndexLink
}
type IndexLink struct {
Name string
URL string
}
func lastCommit(repo *git.Repo, human bool) string {
c, err := repo.LastCommit()
if err != nil {
return ""
}
if human {
return humanize.Time(c.Author.When)
}
return c.Author.When.Format("01/02/2006 03:04:05 PM")
}
templ Index(ic IndexContext) {
@base(ic.BaseContext) {
<header>
<h1 class="text-text text-xl font-bold">{ ic.Title }</h1>
<h2 class="text-subtext1 text-lg">{ ic.Description }</h2>
</header>
<main class="mt-5">
<div class="grid grid-cols-1 sm:grid-cols-8">
if ic.Profile.Username != "" {
<div class="text-mauve">{ `@` + ic.Profile.Username }</div>
}
if ic.Profile.Email != "" {
<div class="text-mauve col-span-2">
<div class="w-5 h-5 stroke-mauve inline-block mr-1 align-middle">@templ.Raw(string(assets.EmailIcon))</div>
<a class="underline decoration-mauve/50 decoration-dashed hover:decoration-solid" href="mailto:john.olheiser@gmail.com">john.olheiser@gmail.com</a>
</div>
}
</div>
<div class="grid grid-cols-1 sm:grid-cols-8">
for _, link := range ic.Profile.Links {
<div class="text-mauve">
<div class="w-5 h-5 stroke-mauve inline-block mr-1 align-middle">@templ.Raw(string(assets.LinkIcon))</div>
<a class="underline decoration-mauve/50 decoration-dashed hover:decoration-solid" rel="me" href={ templ.SafeURL(link.URL) }>{ link.Name }</a>
</div>
}
</div>
<div class="grid grid-cols-8 gap-1 mt-5">
for _, repo := range ic.Repos {
<div class="col-span-1 text-blue dark:text-lavender"><a class="underline decoration-blue/50 dark:decoration-lavender/50 decoration-dashed hover:decoration-solid" href={ templ.URL("/" + repo.Name()) }>{ repo.Name() }</a></div>
<div class="col-span-5 text-subtext0">{ repo.Meta.Description }</div>
<div class="col-span-2 text-text/80" title={ lastCommit(repo, false) }>{ lastCommit(repo, true) }</div>
}
</div>
</main>
}
}

View File

@ -0,0 +1,261 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.501
package html
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "go.jolheiser.com/ugit/internal/git"
import "github.com/dustin/go-humanize"
import "go.jolheiser.com/ugit/assets"
type IndexContext struct {
BaseContext
Profile IndexProfile
CloneURL string
Repos []*git.Repo
}
type IndexProfile struct {
Username string
Email string
Links []IndexLink
}
type IndexLink struct {
Name string
URL string
}
func lastCommit(repo *git.Repo, human bool) string {
c, err := repo.LastCommit()
if err != nil {
return ""
}
if human {
return humanize.Time(c.Author.When)
}
return c.Author.When.Format("01/02/2006 03:04:05 PM")
}
func Index(ic IndexContext) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<header><h1 class=\"text-text text-xl font-bold\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(ic.Title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 38, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h1><h2 class=\"text-subtext1 text-lg\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(ic.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 39, Col: 53}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</h2></header><main class=\"mt-5\"><div class=\"grid grid-cols-1 sm:grid-cols-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if ic.Profile.Username != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"text-mauve\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(`@` + ic.Profile.Username)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 44, Col: 56}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
if ic.Profile.Email != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"text-mauve col-span-2\"><div class=\"w-5 h-5 stroke-mauve inline-block mr-1 align-middle\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(string(assets.EmailIcon)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><a class=\"underline decoration-mauve/50 decoration-dashed hover:decoration-solid\" href=\"mailto:john.olheiser@gmail.com\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var6 := `john.olheiser@gmail.com`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"grid grid-cols-1 sm:grid-cols-8\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, link := range ic.Profile.Links {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"text-mauve\"><div class=\"w-5 h-5 stroke-mauve inline-block mr-1 align-middle\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(string(assets.LinkIcon)).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><a class=\"underline decoration-mauve/50 decoration-dashed hover:decoration-solid\" rel=\"me\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 templ.SafeURL = templ.SafeURL(link.URL)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var7)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 string
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 57, Col: 141}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"grid grid-cols-8 gap-1 mt-5\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
for _, repo := range ic.Repos {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-span-1 text-blue dark:text-lavender\"><a class=\"underline decoration-blue/50 dark:decoration-lavender/50 decoration-dashed hover:decoration-solid\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 templ.SafeURL = templ.URL("/" + repo.Name())
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var9)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(repo.Name())
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 63, Col: 218}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></div><div class=\"col-span-5 text-subtext0\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var11 string
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(repo.Meta.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 64, Col: 66}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"col-span-2 text-text/80\" title=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(lastCommit(repo, false)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var12 string
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(lastCommit(repo, true))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 65, Col: 100}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div></main>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base(ic.BaseContext).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,66 @@
package html
import (
"bytes"
"path/filepath"
"go.jolheiser.com/ugit/internal/git"
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
"github.com/yuin/goldmark"
emoji "github.com/yuin/goldmark-emoji"
highlighting "github.com/yuin/goldmark-highlighting/v2"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
goldmarkhtml "github.com/yuin/goldmark/renderer/html"
)
var Markdown = goldmark.New(
goldmark.WithRendererOptions(
goldmarkhtml.WithUnsafe(),
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithExtensions(
extension.GFM,
emoji.Emoji,
highlighting.NewHighlighting(
highlighting.WithStyle("catppuccin-mocha"),
highlighting.WithFormatOptions(
chromahtml.WithClasses(true),
chromahtml.WithLineNumbers(true),
chromahtml.WithLinkableLineNumbers(true, "md-"),
chromahtml.LineNumbersInTable(true),
),
),
),
)
func Readme(repo *git.Repo, ref, path string) (string, error) {
var readme string
var err error
for _, md := range []string{"README.md", "readme.md"} {
readme, err = repo.FileContent(ref, filepath.Join(path, md))
if err == nil {
break
}
}
if readme != "" {
var buf bytes.Buffer
if err := Markdown.Convert([]byte(readme), &buf); err != nil {
return "", err
}
return buf.String(), nil
}
for _, md := range []string{"README.txt", "README", "readme.txt", "readme"} {
readme, err = repo.FileContent(ref, filepath.Join(path, md))
if err == nil {
return readme, nil
}
}
return "", nil
}

View File

@ -0,0 +1,10 @@
package html
type ReadmeComponentContext struct {
Markdown string
}
templ readmeComponent(rcc ReadmeComponentContext) {
<div class="bg-base/50 px-5 rounded markdown">@templ.Raw(rcc.Markdown)</div>
}

View File

@ -0,0 +1,47 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.501
package html
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
type ReadmeComponentContext struct {
Markdown string
}
func readmeComponent(rcc ReadmeComponentContext) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"bg-base/50 px-5 rounded markdown\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(rcc.Markdown).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,23 @@
package html
import "fmt"
type RepoHeaderComponentContext struct {
Name string
Ref string
Description string
}
templ repoHeaderComponent(rhcc RepoHeaderComponentContext) {
if rhcc.Name != "" {
<div class="mb-1">
<a class="text-text text-lg underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL("/" + rhcc.Name) }>{ rhcc.Name }</a>
if rhcc.Ref != "" {
{ " " }
<a class="text-text/70 text-sm underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/", rhcc.Name, rhcc.Ref)) }>{ "@" + rhcc.Ref }</a>
}
</div>
}
<div class="text-text/80 mb-1">{ rhcc.Description }</div>
}

View File

@ -0,0 +1,68 @@
package html
type RepoFileContext struct {
BaseContext
RepoHeaderComponentContext
Code string
Path string
}
templ RepoFile(rfc RepoFileContext) {
@base(rfc.BaseContext) {
@repoHeaderComponent(rfc.RepoHeaderComponentContext)
<div class="mt-2 text-text"><a class="text-text underline decoration-text/50 decoration-dashed hover:decoration-solid" href="?raw">Raw</a><span>{ " - " }{ rfc.Path }</span>@templ.Raw(rfc.Code)</div>
}
<script>
const lineRe = /#L(\d+)(?:-L(\d+))?/g
const $lineLines = document.querySelectorAll(".chroma .lntable .lnt");
const $codeLines = document.querySelectorAll(".chroma .lntable .line");
let start = 0;
let end = 0;
const results = [...location.hash.matchAll(lineRe)];
if (0 in results) {
start = results[0][1] !== undefined ? parseInt(results[0][1]) : 0;
end = results[0][2] !== undefined ? parseInt(results[0][2]) : 0;
}
if (start != 0) {
deactivateLines();
activateLines(start, end);
}
for (let line of $lineLines) {
line.addEventListener("click", (event) => {
event.preventDefault();
deactivateLines();
const n = parseInt(line.id.substring(1));
let anchor = "";
if (event.shiftKey) {
end = n;
anchor = `#L${start}-L${end}`;
} else {
start = n;
end = 0;
anchor = `#L${start}`;
}
history.pushState(null, null, anchor);
activateLines(start, end);
});
}
function activateLines(start, end) {
if (end < start) end = start;
for (let idx = start - 1; idx < end; idx++) {
$codeLines[idx].classList.add("active");
}
}
function deactivateLines() {
for (let code of $codeLines) {
code.classList.remove("active");
}
}
</script>
}

View File

@ -0,0 +1,164 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.501
package html
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
type RepoFileContext struct {
BaseContext
RepoHeaderComponentContext
Code string
Path string
}
func RepoFile(rfc RepoFileContext) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
templ_7745c5c3_Err = repoHeaderComponent(rfc.RepoHeaderComponentContext).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <div class=\"mt-2 text-text\"><a class=\"text-text underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"?raw\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var3 := `Raw`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a><span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(" - ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_file.templ`, Line: 12, Col: 153}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 string
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(rfc.Path)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_file.templ`, Line: 12, Col: 165}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</span>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templ.Raw(rfc.Code).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base(rfc.BaseContext).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var6 := `
const lineRe = /#L(\d+)(?:-L(\d+))?/g
const $lineLines = document.querySelectorAll(".chroma .lntable .lnt");
const $codeLines = document.querySelectorAll(".chroma .lntable .line");
let start = 0;
let end = 0;
const results = [...location.hash.matchAll(lineRe)];
if (0 in results) {
start = results[0][1] !== undefined ? parseInt(results[0][1]) : 0;
end = results[0][2] !== undefined ? parseInt(results[0][2]) : 0;
}
if (start != 0) {
deactivateLines();
activateLines(start, end);
}
for (let line of $lineLines) {
line.addEventListener("click", (event) => {
event.preventDefault();
deactivateLines();
const n = parseInt(line.id.substring(1));
let anchor = "";
if (event.shiftKey) {
end = n;
anchor = ` + "`" + `#L${start}-L${end}` + "`" + `;
} else {
start = n;
end = 0;
anchor = ` + "`" + `#L${start}` + "`" + `;
}
history.pushState(null, null, anchor);
activateLines(start, end);
});
}
function activateLines(start, end) {
if (end < start) end = start;
for (let idx = start - 1; idx < end; idx++) {
$codeLines[idx].classList.add("active");
}
}
function deactivateLines() {
for (let code of $codeLines) {
code.classList.remove("active");
}
}
`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,125 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.501
package html
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import "fmt"
type RepoHeaderComponentContext struct {
Name string
Ref string
Description string
}
func repoHeaderComponent(rhcc RepoHeaderComponentContext) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
if rhcc.Name != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"mb-1\"><a class=\"text-text text-lg underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var2 templ.SafeURL = templ.SafeURL("/" + rhcc.Name)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var2)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var3 string
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(rhcc.Name)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo.templ`, Line: 13, Col: 153}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a> ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if rhcc.Ref != "" {
var templ_7745c5c3_Var4 string
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(" ")
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo.templ`, Line: 15, Col: 9}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" <a class=\"text-text/70 text-sm underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var5 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/tree/%s/", rhcc.Name, rhcc.Ref))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var5)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs("@" + rhcc.Ref)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo.templ`, Line: 16, Col: 195}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"text-text/80 mb-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(rhcc.Description)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo.templ`, Line: 20, Col: 50}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,51 @@
package html
import (
"fmt"
"go.jolheiser.com/ugit/internal/git"
)
type RepoTreeContext struct {
BaseContext
RepoHeaderComponentContext
RepoTreeComponentContext
ReadmeComponentContext
Description string
}
templ RepoTree(rtc RepoTreeContext) {
@base(rtc.BaseContext) {
@repoHeaderComponent(rtc.RepoHeaderComponentContext)
@repoTreeComponent(rtc.RepoTreeComponentContext)
@readmeComponent(rtc.ReadmeComponentContext)
}
}
type RepoTreeComponentContext struct {
Repo string
Ref string
Tree []git.FileInfo
Back string
}
func slashDir(name string, isDir bool) string {
if isDir {
return name + "/"
}
return name
}
templ repoTreeComponent(rtcc RepoTreeComponentContext) {
<div class="grid grid-cols-8 text-text py-5 rounded px-5 bg-base/50">
if rtcc.Back != "" {
<div class="col-span-2"></div>
<div class="col-span-6"><a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rtcc.Repo, rtcc.Ref, rtcc.Back)) }>..</a></div>
}
for _, fi := range rtcc.Tree {
<div class="col-span-1">{ fi.Mode }</div>
<div class="col-span-1">{ fi.Size }</div>
<div class="col-span-6"><a class="underline decoration-text/50 decoration-dashed hover:decoration-solid" href={ templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rtcc.Repo, rtcc.Ref, fi.Path)) }>{ slashDir(fi.Name(), fi.IsDir) }</a></div>
}
</div>
}

View File

@ -0,0 +1,199 @@
// Code generated by templ - DO NOT EDIT.
// templ: version: v0.2.501
package html
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import "context"
import "io"
import "bytes"
import (
"fmt"
"go.jolheiser.com/ugit/internal/git"
)
type RepoTreeContext struct {
BaseContext
RepoHeaderComponentContext
RepoTreeComponentContext
ReadmeComponentContext
Description string
}
func RepoTree(rtc RepoTreeContext) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var1 := templ.GetChildren(ctx)
if templ_7745c5c3_Var1 == nil {
templ_7745c5c3_Var1 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
templ_7745c5c3_Var2 := templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
templ_7745c5c3_Err = repoHeaderComponent(rtc.RepoHeaderComponentContext).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = repoTreeComponent(rtc.RepoTreeComponentContext).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = readmeComponent(rtc.ReadmeComponentContext).Render(ctx, templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = io.Copy(templ_7745c5c3_W, templ_7745c5c3_Buffer)
}
return templ_7745c5c3_Err
})
templ_7745c5c3_Err = base(rtc.BaseContext).Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}
type RepoTreeComponentContext struct {
Repo string
Ref string
Tree []git.FileInfo
Back string
}
func slashDir(name string, isDir bool) string {
if isDir {
return name + "/"
}
return name
}
func repoTreeComponent(rtcc RepoTreeComponentContext) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, templ_7745c5c3_W io.Writer) (templ_7745c5c3_Err error) {
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templ_7745c5c3_W.(*bytes.Buffer)
if !templ_7745c5c3_IsBuffer {
templ_7745c5c3_Buffer = templ.GetBuffer()
defer templ.ReleaseBuffer(templ_7745c5c3_Buffer)
}
ctx = templ.InitializeContext(ctx)
templ_7745c5c3_Var3 := templ.GetChildren(ctx)
if templ_7745c5c3_Var3 == nil {
templ_7745c5c3_Var3 = templ.NopComponent
}
ctx = templ.ClearChildren(ctx)
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"grid grid-cols-8 text-text py-5 rounded px-5 bg-base/50\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if rtcc.Back != "" {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-span-2\"></div><div class=\"col-span-6\"><a class=\"underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var4 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rtcc.Repo, rtcc.Ref, rtcc.Back))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var4)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Var5 := `..`
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
for _, fi := range rtcc.Tree {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("<div class=\"col-span-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var6 string
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fi.Mode)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_tree.templ`, Line: 44, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"col-span-1\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var7 string
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fi.Size)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_tree.templ`, Line: 45, Col: 36}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div><div class=\"col-span-6\"><a class=\"underline decoration-text/50 decoration-dashed hover:decoration-solid\" href=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var8 templ.SafeURL = templ.SafeURL(fmt.Sprintf("/%s/tree/%s/%s", rtcc.Repo, rtcc.Ref, fi.Path))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(string(templ_7745c5c3_Var8)))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(slashDir(fi.Name(), fi.IsDir))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `repo_tree.templ`, Line: 46, Col: 223}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</a></div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("</div>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if !templ_7745c5c3_IsBuffer {
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteTo(templ_7745c5c3_W)
}
return templ_7745c5c3_Err
})
}

View File

@ -0,0 +1,7 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./**/*.templ"],
plugins: [require("@catppuccin/tailwindcss")],
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,50 @@
package http
import (
"errors"
"net/http"
"path/filepath"
"go.jolheiser.com/ugit/internal/git"
"go.jolheiser.com/ugit/internal/http/httperr"
"github.com/go-chi/chi/v5"
)
func (rh repoHandler) infoRefs(w http.ResponseWriter, r *http.Request) error {
if r.URL.Query().Get("service") != "git-upload-pack" {
return httperr.Status(errors.New("pushing isn't supported via HTTP(S), use SSH"), http.StatusBadRequest)
}
w.Header().Set("Content-Type", "application/x-git-upload-pack-advertisement")
rp := filepath.Join(rh.s.RepoDir, chi.URLParam(r, "repo")+".git")
repo, err := git.NewProtocol(rp)
if err != nil {
return httperr.Error(err)
}
if err := repo.HTTPInfoRefs(Session{
w: w,
r: r,
}); err != nil {
return httperr.Error(err)
}
return nil
}
func (rh repoHandler) uploadPack(w http.ResponseWriter, r *http.Request) error {
w.Header().Set("content-type", "application/x-git-upload-pack-result")
rp := filepath.Join(rh.s.RepoDir, chi.URLParam(r, "repo")+".git")
repo, err := git.NewProtocol(rp)
if err != nil {
return httperr.Error(err)
}
if err := repo.HTTPUploadPack(Session{
w: w,
r: r,
}); err != nil {
return httperr.Error(err)
}
return nil
}

View File

@ -0,0 +1,121 @@
package http
import (
"fmt"
"net/http"
"net/url"
"go.jolheiser.com/ugit/assets"
"go.jolheiser.com/ugit/internal/git"
"go.jolheiser.com/ugit/internal/html"
"go.jolheiser.com/ugit/internal/http/httperr"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
// Server is the container struct for the HTTP server
type Server struct {
port int
mux *chi.Mux
}
// ListenAndServe simply wraps http.ListenAndServe to contain the functionality here
func (s Server) ListenAndServe() error {
return http.ListenAndServe(fmt.Sprintf("localhost:%d", s.port), s.mux)
}
// Settings is the configuration for the HTTP server
type Settings struct {
Title string
Description string
CloneURL string
Port int
RepoDir string
Profile Profile
}
// Profile is the index profile
type Profile struct {
Username string
Email string
Links []Link
}
// Link is a profile link
type Link struct {
Name string
URL string
}
func (s Settings) goGet(repo string) string {
u, _ := url.Parse(s.CloneURL)
return fmt.Sprintf(`<!DOCTYPE html><title>%[1]s</title><meta name="go-import" content="%[2]s/%[1]s git %[3]s/%[1]s.git"><meta name="go-source" content="%[2]s/%[1]s _ %[3]s/%[1]s/tree/main{/dir}/{file}#L{line}">`, repo, u.Hostname(), s.CloneURL)
}
// New returns a new HTTP server
func New(settings Settings) Server {
mux := chi.NewMux()
mux.Use(middleware.Logger)
mux.Use(middleware.Recoverer)
rh := repoHandler{s: settings}
mux.Route("/{repo}.git", func(r chi.Router) {
r.Get("/info/refs", httperr.Handler(rh.infoRefs))
r.Post("/git-upload-pack", httperr.Handler(rh.uploadPack))
})
mux.Route("/", func(r chi.Router) {
r.Get("/", httperr.Handler(rh.index))
r.Route("/{repo}", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Query().Has("go-get") {
repo := chi.URLParam(r, "repo")
w.Write([]byte(settings.goGet(repo)))
return
}
rh.repoTree("", "").ServeHTTP(w, r)
})
r.Get("/tree/{ref}/*", func(w http.ResponseWriter, r *http.Request) {
rh.repoTree(chi.URLParam(r, "ref"), chi.URLParam(r, "*")).ServeHTTP(w, r)
})
})
})
mux.Route("/_", func(r chi.Router) {
r.Get("/favicon.svg", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "image/svg+xml")
w.Write(assets.LogoIcon)
})
r.Get("/tailwind.css", html.TailwindHandler)
})
return Server{mux: mux, port: settings.Port}
}
type repoHandler struct {
s Settings
}
func (rh repoHandler) baseContext() html.BaseContext {
return html.BaseContext{
Title: rh.s.Title,
Description: rh.s.Description,
}
}
func (rh repoHandler) repoHeaderContext(repo *git.Repo, r *http.Request) html.RepoHeaderComponentContext {
return html.RepoHeaderComponentContext{
Description: repo.Meta.Description,
Name: chi.URLParam(r, "repo"),
Ref: chi.URLParam(r, "ref"),
}
}
// NoopLogger is a no-op logging middleware
func NoopLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
})
}

View File

@ -0,0 +1,45 @@
package httperr
import (
"errors"
"net/http"
"github.com/charmbracelet/log"
)
type httpError struct {
err error
status int
}
func (h httpError) Error() string {
return h.err.Error()
}
func (h httpError) Unwrap() error {
return h.err
}
func Error(err error) httpError {
return Status(err, http.StatusInternalServerError)
}
func Status(err error, status int) httpError {
return httpError{err: err, status: status}
}
func Handler(fn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := fn(w, r); err != nil {
status := http.StatusInternalServerError
var httpErr httpError
if errors.As(err, &httpErr) {
status = httpErr.status
}
log.Error(err)
http.Error(w, http.StatusText(status), status)
}
}
}

View File

@ -0,0 +1,63 @@
package http
import (
"net/http"
"os"
"sort"
"time"
"go.jolheiser.com/ugit/internal/git"
"go.jolheiser.com/ugit/internal/html"
"go.jolheiser.com/ugit/internal/http/httperr"
)
func (rh repoHandler) index(w http.ResponseWriter, r *http.Request) error {
repoPaths, err := os.ReadDir(rh.s.RepoDir)
if err != nil {
return httperr.Error(err)
}
repos := make([]*git.Repo, 0, len(repoPaths))
for _, repoName := range repoPaths {
repo, err := git.NewRepo(rh.s.RepoDir, repoName.Name())
if err != nil {
return httperr.Error(err)
}
if !repo.Meta.Private {
repos = append(repos, repo)
}
}
sort.Slice(repos, func(i, j int) bool {
var when1, when2 time.Time
if c, err := repos[i].LastCommit(); err == nil {
when1 = c.Author.When
}
if c, err := repos[j].LastCommit(); err == nil {
when2 = c.Author.When
}
return when1.After(when2)
})
links := make([]html.IndexLink, 0, len(rh.s.Profile.Links))
for _, link := range rh.s.Profile.Links {
links = append(links, html.IndexLink{
Name: link.Name,
URL: link.URL,
})
}
if err := html.Index(html.IndexContext{
BaseContext: rh.baseContext(),
Profile: html.IndexProfile{
Username: rh.s.Profile.Username,
Email: rh.s.Profile.Email,
Links: links,
},
CloneURL: rh.s.CloneURL,
Repos: repos,
}).Render(r.Context(), w); err != nil {
return httperr.Error(err)
}
return nil
}

View File

@ -0,0 +1,109 @@
package http
import (
"bytes"
"errors"
"io/fs"
"mime"
"net/http"
"path/filepath"
"go.jolheiser.com/ugit/internal/git"
"go.jolheiser.com/ugit/internal/html"
"go.jolheiser.com/ugit/internal/http/httperr"
"github.com/go-chi/chi/v5"
"github.com/go-git/go-git/v5/plumbing/object"
)
func (rh repoHandler) repoTree(ref, path string) http.HandlerFunc {
return httperr.Handler(func(w http.ResponseWriter, r *http.Request) error {
repoName := chi.URLParam(r, "repo")
repo, err := git.NewRepo(rh.s.RepoDir, repoName)
if err != nil {
httpErr := http.StatusInternalServerError
if errors.Is(err, fs.ErrNotExist) {
httpErr = http.StatusNotFound
}
return httperr.Status(err, httpErr)
}
if repo.Meta.Private {
return httperr.Status(errors.New("could not get git repo"), http.StatusNotFound)
}
if ref == "" {
ref, err = repo.DefaultBranch()
if err != nil {
return httperr.Error(err)
}
}
tree, err := repo.Dir(ref, path)
if err != nil {
if errors.Is(err, object.ErrDirectoryNotFound) {
return rh.repoFile(w, r, repo, ref, path)
}
return httperr.Error(err)
}
readmeContent, err := html.Readme(repo, ref, path)
if err != nil {
return httperr.Error(err)
}
var back string
if path != "" {
back = filepath.Dir(path)
}
if err := html.RepoTree(html.RepoTreeContext{
Description: repo.Meta.Description,
BaseContext: rh.baseContext(),
RepoHeaderComponentContext: rh.repoHeaderContext(repo, r),
RepoTreeComponentContext: html.RepoTreeComponentContext{
Repo: repoName,
Ref: ref,
Tree: tree,
Back: back,
},
ReadmeComponentContext: html.ReadmeComponentContext{
Markdown: readmeContent,
},
}).Render(r.Context(), w); err != nil {
return httperr.Error(err)
}
return nil
})
}
func (rh repoHandler) repoFile(w http.ResponseWriter, r *http.Request, repo *git.Repo, ref, path string) error {
content, err := repo.FileContent(ref, path)
if err != nil {
return httperr.Error(err)
}
if r.URL.Query().Has("raw") {
if r.URL.Query().Has("pretty") {
ext := filepath.Ext(path)
w.Header().Set("Content-Type", mime.TypeByExtension(ext))
}
w.Write([]byte(content))
return nil
}
var buf bytes.Buffer
if err := html.Code.Convert([]byte(content), filepath.Base(path), &buf); err != nil {
return httperr.Error(err)
}
if err := html.RepoFile(html.RepoFileContext{
BaseContext: rh.baseContext(),
RepoHeaderComponentContext: rh.repoHeaderContext(repo, r),
Code: buf.String(),
Path: path,
}).Render(r.Context(), w); err != nil {
return httperr.Error(err)
}
return nil
}

View File

@ -0,0 +1,32 @@
package http
import (
"context"
"net/http"
)
// Session fulfills git.ReadWriteContexter for an HTTP request
type Session struct {
w http.ResponseWriter
r *http.Request
}
// Read implements io.Reader
func (s Session) Read(p []byte) (n int, err error) {
return s.r.Body.Read(p)
}
// Write implements io.Writer
func (s Session) Write(p []byte) (n int, err error) {
return s.w.Write(p)
}
// Close implements io.Closer
func (s Session) Close() error {
return s.r.Body.Close()
}
// Context implements git.ReadWriteContexter
func (s Session) Context() context.Context {
return s.r.Context()
}

View File

@ -3,20 +3,30 @@ package ssh
import (
"fmt"
"github.com/charmbracelet/log"
"github.com/charmbracelet/ssh"
"github.com/charmbracelet/wish"
"github.com/charmbracelet/wish/git"
"github.com/charmbracelet/wish/logging"
)
func New() (*ssh.Server, error) {
// Settings holds the configuration for the SSH server
type Settings struct {
AuthorizedKeys string
CloneURL string
Port int
HostKey string
RepoDir string
}
// New creates a new SSH server.
func New(settings Settings) (*ssh.Server, error) {
s, err := wish.NewServer(
wish.WithAuthorizedKeys(".ssh/authorized_keys"),
wish.WithAddress("localhost:8448"),
wish.WithHostKeyPath(".ssh/ugit_ed25519"),
wish.WithAuthorizedKeys(settings.AuthorizedKeys),
wish.WithAddress(fmt.Sprintf(":%d", settings.Port)),
wish.WithHostKeyPath(settings.HostKey),
wish.WithMiddleware(
git.Middleware(".ugit", app{}),
logging.Middleware(),
Middleware(settings.RepoDir, settings.CloneURL, settings.Port, hooks{}),
logging.MiddlewareWithLogger(DefaultLogger),
),
)
if err != nil {
@ -26,10 +36,16 @@ func New() (*ssh.Server, error) {
return s, nil
}
type app struct{}
type hooks struct{}
func (a app) AuthRepo(repo string, pk ssh.PublicKey) git.AccessLevel {
return git.ReadWriteAccess
}
func (a app) Push(_ string, _ ssh.PublicKey) {}
func (a app) Fetch(_ string, _ ssh.PublicKey) {}
func (a hooks) Push(_ string, _ ssh.PublicKey) {}
func (a hooks) Fetch(_ string, _ ssh.PublicKey) {}
var (
DefaultLogger logging.Logger = log.StandardLog()
NoopLogger logging.Logger = noopLogger{}
)
type noopLogger struct{}
func (n noopLogger) Printf(format string, v ...interface{}) {}

View File

@ -0,0 +1,164 @@
package ssh
import (
"context"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
"go.jolheiser.com/ugit/internal/git"
"github.com/charmbracelet/log"
"github.com/charmbracelet/ssh"
"github.com/charmbracelet/wish"
)
// ErrSystemMalfunction represents a general system error returned to clients.
var ErrSystemMalfunction = errors.New("something went wrong")
// ErrInvalidRepo represents an attempt to access a non-existent repo.
var ErrInvalidRepo = errors.New("invalid repo")
// Hooks is an interface that allows for custom authorization
// implementations and post push/fetch notifications. Prior to git access,
// AuthRepo will be called with the ssh.Session public key and the repo name.
// Implementers return the appropriate AccessLevel.
type Hooks interface {
Push(string, ssh.PublicKey)
Fetch(string, ssh.PublicKey)
}
// Session wraps sn ssh.Session to implement git.ReadWriteContexter
type Session struct {
s ssh.Session
}
// Read implements io.Reader
func (s Session) Read(p []byte) (n int, err error) {
return s.s.Read(p)
}
// Write implements io.Writer
func (s Session) Write(p []byte) (n int, err error) {
return s.s.Write(p)
}
// Close implements io.Closer
func (s Session) Close() error {
return nil
}
// Context returns an interface context.Context
func (s Session) Context() context.Context {
return s.s.Context()
}
// Middleware adds Git server functionality to the ssh.Server. Repos are stored
// in the specified repo directory. The provided Hooks implementation will be
// checked for access on a per repo basis for a ssh.Session public key.
// Hooks.Push and Hooks.Fetch will be called on successful completion of
// their commands.
func Middleware(repoDir string, cloneURL string, port int, gh Hooks) wish.Middleware {
return func(sh ssh.Handler) ssh.Handler {
return func(s ssh.Session) {
sess := Session{s: s}
cmd := s.Command()
// Git operations
if len(cmd) == 2 {
gc := cmd[0]
// repo should be in the form of "repo.git" or "user/repo.git"
repo := strings.TrimSuffix(strings.TrimPrefix(cmd[1], "/"), "/")
repo = filepath.Clean(repo)
if n := strings.Count(repo, "/"); n > 1 {
Fatal(s, ErrInvalidRepo)
return
}
pk := s.PublicKey()
switch gc {
case "git-receive-pack":
if err := gitPack(sess, gc, repoDir, repo); err != nil {
Fatal(s, ErrSystemMalfunction)
}
gh.Push(repo, pk)
return
case "git-upload-archive", "git-upload-pack":
if err := gitPack(sess, gc, repoDir, repo); err != nil {
if errors.Is(err, ErrInvalidRepo) {
Fatal(s, ErrInvalidRepo)
}
log.Error("unknown git error", "error", err)
Fatal(s, ErrSystemMalfunction)
}
gh.Fetch(repo, pk)
return
}
}
// Repo list
if len(cmd) == 0 {
des, err := os.ReadDir(repoDir)
if err != nil && err != fs.ErrNotExist {
log.Error("invalid repository", "error", err)
}
for _, de := range des {
fmt.Fprintln(s, de.Name())
fmt.Fprintf(s, "\tgit clone %s/%s\n", cloneURL, de.Name())
}
}
sh(s)
}
}
}
func gitPack(s Session, gitCmd string, repoDir string, repoName string) error {
rp := filepath.Join(repoDir, repoName)
protocol, err := git.NewProtocol(rp)
if err != nil {
return err
}
switch gitCmd {
case "git-upload-pack":
exists, err := git.PathExists(rp)
if !exists {
return ErrInvalidRepo
}
if err != nil {
return err
}
return protocol.SSHUploadPack(s)
case "git-receive-pack":
err := git.EnsureRepo(repoDir, repoName)
if err != nil {
return err
}
repo, err := git.NewRepo(repoDir, repoName)
if err != nil {
return err
}
err = protocol.SSHReceivePack(s, repo)
if err != nil {
return err
}
_, err = repo.DefaultBranch()
if err != nil {
return err
}
// Needed for git dumb http server
return git.UpdateServerInfo(rp)
default:
return fmt.Errorf("unknown git command: %s", gitCmd)
}
}
// Fatal prints to the session's STDOUT as a git response and exit 1.
func Fatal(s ssh.Session, v ...interface{}) {
msg := fmt.Sprint(v...)
// hex length includes 4 byte length prefix and ending newline
pktLine := fmt.Sprintf("%04x%s\n", len(msg)+5, msg)
_, _ = wish.WriteString(s, pktLine)
s.Exit(1) // nolint: errcheck
}