mirror of https://git.jolheiser.com/ugit.git
parent
4731c0bd35
commit
868ca2125a
|
@ -1 +1,3 @@
|
|||
/ugit*
|
||||
.ssh/
|
||||
.ugit/
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
[[language]]
|
||||
name = "templ"
|
||||
language-id = "html"
|
||||
language-servers = ["templ", "vscode-html-language-server", "tailwindcss-ls"]
|
||||
|
|
@ -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).
|
|
@ -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
|
||||
}
|
|
@ -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 |
|
@ -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 |
|
@ -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 |
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
84
flake.lock
84
flake.lock
|
@ -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
151
flake.nix
|
@ -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
40
go.mod
|
@ -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
81
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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{}) {}
|
||||
|
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue