Lindenii Project Forge
Commit info | |
---|---|
ID | 64f79d9f0f6b7a0cfdad797203d71ad055837210 |
Author | Runxi Yu<me@runxiyu.org> |
Author date | Wed, 05 Mar 2025 09:57:55 +0800 |
Committer | Runxi Yu<me@runxiyu.org> |
Committer date | Wed, 05 Mar 2025 09:58:06 +0800 |
Actions | Get patch |
*: Replacing more := with var
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "bytes" "html" "html/template" "strings" "github.com/go-git/go-git/v5/plumbing/object" "github.com/microcosm-cc/bluemonday" "github.com/niklasfasching/go-org/org" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" ) var markdown_converter = goldmark.New(goldmark.WithExtensions(extension.GFM)) func render_readme_at_tree(tree *object.Tree) (readme_filename string, readme_content template.HTML) { var readme_rendered_unsafe bytes.Buffer
var readme_file *object.File var readme_file_contents string var err error
readme_file, err := tree.File("README") if err == nil { readme_file_contents, err := readme_file.Contents() if err != nil {
if readme_file, err = tree.File("README"); err != nil { if readme_file_contents, err = readme_file.Contents(); err != nil {
return "Error fetching README", string_escape_html("Unable to fetch contents of README: " + err.Error()) }
return "README", template.HTML("<pre>" + html.EscapeString(readme_file_contents) + "</pre>") }
readme_file, err = tree.File("README.md") if err == nil { readme_file_contents, err := readme_file.Contents() if err != nil {
if readme_file, err = tree.File("README.md"); err != nil { if readme_file_contents, err = readme_file.Contents(); err != nil {
return "Error fetching README", string_escape_html("Unable to fetch contents of README: " + err.Error()) }
err = markdown_converter.Convert([]byte(readme_file_contents), &readme_rendered_unsafe) if err != nil {
if err = markdown_converter.Convert([]byte(readme_file_contents), &readme_rendered_unsafe); err != nil {
return "Error fetching README", string_escape_html("Unable to render README: " + err.Error()) }
return "README.md", template.HTML(bluemonday.UGCPolicy().SanitizeBytes(readme_rendered_unsafe.Bytes())) }
readme_file, err = tree.File("README.org") if err == nil { readme_file_contents, err := readme_file.Contents() if err != nil {
if readme_file, err = tree.File("README.org"); err != nil { if readme_file_contents, err = readme_file.Contents(); err != nil {
return "Error fetching README", string_escape_html("Unable to fetch contents of README: " + err.Error()) }
org_html, err := org.New().Parse(strings.NewReader(readme_file_contents), readme_filename).Write(org.NewHTMLWriter()) if err != nil { return "Error fetching README", string_escape_html("Unable to render README: " + err.Error()) }
return "README.org", template.HTML(bluemonday.UGCPolicy().Sanitize(org_html)) } return "", "" } func string_escape_html(s string) template.HTML { return template.HTML(html.EscapeString(s)) }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "errors" "fmt" "net/http" "strconv" "strings" "github.com/jackc/pgx/v5" "go.lindenii.runxiyu.org/lindenii-common/clog" ) type http_router_t struct{} func (router *http_router_t) ServeHTTP(w http.ResponseWriter, r *http.Request) { clog.Info("Incoming HTTP: " + r.RemoteAddr + " " + r.Method + " " + r.RequestURI)
segments, _, err := parse_request_uri(r.RequestURI) if err != nil {
var segments []string var err error var non_empty_last_segments_len int var params map[string]any var separator_index int if segments, _, err = parse_request_uri(r.RequestURI); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) return }
non_empty_last_segments_len := len(segments)
non_empty_last_segments_len = len(segments)
if segments[len(segments)-1] == "" { non_empty_last_segments_len-- } if segments[0] == ":" { if len(segments) < 2 { http.Error(w, "Blank system endpoint", http.StatusNotFound) return } else if len(segments) == 2 && redirect_with_slash(w, r) { return } switch segments[1] { case "static": static_handler.ServeHTTP(w, r) return case "source": source_handler.ServeHTTP(w, r) return } }
params := make(map[string]any)
params["url_segments"] = segments params["global"] = global_data var _user_id int // 0 for none _user_id, params["username"], err = get_user_info_from_request(r) if errors.Is(err, http.ErrNoCookie) { } else if errors.Is(err, pgx.ErrNoRows) { } else if err != nil { http.Error(w, "Error getting user info from request: "+err.Error(), http.StatusInternalServerError) return } if _user_id == 0 { params["user_id"] = "" } else { params["user_id"] = strconv.Itoa(_user_id) } if segments[0] == ":" { switch segments[1] { case "login": handle_login(w, r, params) return case "users": handle_users(w, r, params) return default: http.Error(w, fmt.Sprintf("Unknown system module type: %s", segments[1]), http.StatusNotFound) return } }
separator_index := -1
separator_index = -1
for i, part := range segments { if part == ":" { separator_index = i break } } params["separator_index"] = separator_index // TODO if separator_index > 1 { http.Error(w, "Subgroups haven't been implemented yet", http.StatusNotImplemented) return }
var module_type string var module_name string var group_name string
switch { case non_empty_last_segments_len == 0: handle_index(w, r, params) case separator_index == -1: http.Error(w, "Group indexing hasn't been implemented yet", http.StatusNotImplemented) case non_empty_last_segments_len == separator_index+1: http.Error(w, "Group root hasn't been implemented yet", http.StatusNotImplemented) case non_empty_last_segments_len == separator_index+2: if redirect_with_slash(w, r) { return }
module_type := segments[separator_index+1]
module_type = segments[separator_index+1]
params["group_name"] = segments[0] switch module_type { case "repos": handle_group_repos(w, r, params) default: http.Error(w, fmt.Sprintf("Unknown module type: %s", module_type), http.StatusNotFound) } default:
module_type := segments[separator_index+1] module_name := segments[separator_index+2] group_name := segments[0]
module_type = segments[separator_index+1] module_name = segments[separator_index+2] group_name = segments[0]
params["group_name"] = group_name switch module_type { case "repos": params["repo_name"] = module_name if non_empty_last_segments_len > separator_index+3 { switch segments[separator_index+3] { case "info":
err = handle_repo_info(w, r, params) if err != nil {
if err = handle_repo_info(w, r, params); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) } return case "git-upload-pack":
err = handle_upload_pack(w, r, params) if err != nil {
if err = handle_upload_pack(w, r, params); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) } return } }
params["ref_type"], params["ref_name"], err = get_param_ref_and_type(r) if err != nil {
if params["ref_type"], params["ref_name"], err = get_param_ref_and_type(r); err != nil {
if errors.Is(err, err_no_ref_spec) { params["ref_type"] = "" } else { http.Error(w, "Error querying ref type: "+err.Error(), http.StatusInternalServerError) return } } // TODO: subgroups
params["repo"], params["repo_description"], params["repo_id"], err = open_git_repo(r.Context(), group_name, module_name) if err != nil {
if params["repo"], params["repo_description"], params["repo_id"], err = open_git_repo(r.Context(), group_name, module_name); err != nil {
http.Error(w, "Error opening repo: "+err.Error(), http.StatusInternalServerError) return } if non_empty_last_segments_len == separator_index+3 { if redirect_with_slash(w, r) { return } handle_repo_index(w, r, params) return } repo_feature := segments[separator_index+3] switch repo_feature { case "tree": params["rest"] = strings.Join(segments[separator_index+4:], "/") if len(segments) < separator_index+5 && redirect_with_slash(w, r) { return } handle_repo_tree(w, r, params) case "raw": params["rest"] = strings.Join(segments[separator_index+4:], "/") if len(segments) < separator_index+5 && redirect_with_slash(w, r) { return } handle_repo_raw(w, r, params) case "log": if non_empty_last_segments_len > separator_index+4 { http.Error(w, "Too many parameters", http.StatusBadRequest) return } if redirect_with_slash(w, r) { return } handle_repo_log(w, r, params) case "commit": if redirect_without_slash(w, r) { return } params["commit_id"] = segments[separator_index+4] handle_repo_commit(w, r, params) case "contrib": if redirect_with_slash(w, r) { return } switch non_empty_last_segments_len { case separator_index + 4: handle_repo_contrib_index(w, r, params) case separator_index + 5: params["mr_id"] = segments[separator_index+4] handle_repo_contrib_one(w, r, params) default: http.Error(w, "Too many parameters", http.StatusBadRequest) } default: http.Error(w, fmt.Sprintf("Unknown repo feature: %s", repo_feature), http.StatusNotFound) } default: http.Error(w, fmt.Sprintf("Unknown module type: %s", module_type), http.StatusNotFound) } } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import "net/http" // render_template abstracts out the annoyances of reporting template rendering // errors. func render_template(w http.ResponseWriter, template_name string, params map[string]any) {
err := templates.ExecuteTemplate(w, template_name, params) if err != nil {
if err := templates.ExecuteTemplate(w, template_name, params); err != nil {
http.Error(w, "Error rendering template: "+err.Error(), http.StatusInternalServerError) } }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "errors" "flag" "net" "net/http" "syscall" "go.lindenii.runxiyu.org/lindenii-common/clog" ) func main() { config_path := flag.String( "config", "/etc/lindenii/forge.scfg", "path to configuration file", ) flag.Parse() if err := load_config(*config_path); err != nil { clog.Fatal(1, "Loading configuration: "+err.Error()) } if err := deploy_hooks_to_filesystem(); err != nil { clog.Fatal(1, "Deploying hooks to filesystem: "+err.Error()) } if err := load_templates(); err != nil { clog.Fatal(1, "Loading templates: "+err.Error()) } // UNIX socket listener for hooks var hooks_listener net.Listener var err error hooks_listener, err = net.Listen("unix", config.Hooks.Socket) if errors.Is(err, syscall.EADDRINUSE) { clog.Warn("Removing stale socket " + config.Hooks.Socket)
if err := syscall.Unlink(config.Hooks.Socket); err != nil {
if err = syscall.Unlink(config.Hooks.Socket); err != nil {
clog.Fatal(1, "Removing stale socket: "+err.Error()) }
hooks_listener, err = net.Listen("unix", config.Hooks.Socket) if err != nil {
if hooks_listener, err = net.Listen("unix", config.Hooks.Socket); err != nil {
clog.Fatal(1, "Listening hooks: "+err.Error()) } } else if err != nil { clog.Fatal(1, "Listening hooks: "+err.Error()) } clog.Info("Listening hooks on unix " + config.Hooks.Socket) go func() {
if err := serve_git_hooks(hooks_listener); err != nil {
if err = serve_git_hooks(hooks_listener); err != nil {
clog.Fatal(1, "Serving hooks: "+err.Error()) } }() // SSH listener ssh_listener, err := net.Listen(config.SSH.Net, config.SSH.Addr) if errors.Is(err, syscall.EADDRINUSE) && config.SSH.Net == "unix" { clog.Warn("Removing stale socket " + config.SSH.Addr)
if err := syscall.Unlink(config.SSH.Addr); err != nil {
if err = syscall.Unlink(config.SSH.Addr); err != nil {
clog.Fatal(1, "Removing stale socket: "+err.Error()) }
ssh_listener, err = net.Listen(config.SSH.Net, config.SSH.Addr) if err != nil {
if ssh_listener, err = net.Listen(config.SSH.Net, config.SSH.Addr); err != nil {
clog.Fatal(1, "Listening SSH: "+err.Error()) } } else if err != nil { clog.Fatal(1, "Listening SSH: "+err.Error()) } clog.Info("Listening SSH on " + config.SSH.Net + " " + config.SSH.Addr) go func() {
if err := serve_ssh(ssh_listener); err != nil {
if err = serve_ssh(ssh_listener); err != nil {
clog.Fatal(1, "Serving SSH: "+err.Error()) } }() // HTTP listener http_listener, err := net.Listen(config.HTTP.Net, config.HTTP.Addr) if errors.Is(err, syscall.EADDRINUSE) && config.HTTP.Net == "unix" { clog.Warn("Removing stale socket " + config.HTTP.Addr)
if err := syscall.Unlink(config.HTTP.Addr); err != nil {
if err = syscall.Unlink(config.HTTP.Addr); err != nil {
clog.Fatal(1, "Removing stale socket: "+err.Error()) }
http_listener, err = net.Listen(config.HTTP.Net, config.HTTP.Addr) if err != nil {
if http_listener, err = net.Listen(config.HTTP.Net, config.HTTP.Addr); err != nil {
clog.Fatal(1, "Listening HTTP: "+err.Error()) } } else if err != nil { clog.Fatal(1, "Listening HTTP: "+err.Error()) } clog.Info("Listening HTTP on " + config.HTTP.Net + " " + config.HTTP.Addr) go func() {
if err := http.Serve(http_listener, &http_router_t{}); err != nil {
if err = http.Serve(http_listener, &http_router_t{}); err != nil {
clog.Fatal(1, "Serving HTTP: "+err.Error()) } }() select {} }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "embed" "html/template" "io/fs" "net/http" ) // We embed all source for easy AGPL compliance. // //go:embed .gitignore .gitattributes //go:embed LICENSE README.md //go:embed *.go go.mod go.sum //go:embed *.scfg //go:embed Makefile //go:embed schema.sql //go:embed static/* templates/* //go:embed git_hooks_client/*.c //go:embed vendor/* var source_fs embed.FS var source_handler = http.StripPrefix( "/:/source/", http.FileServer(http.FS(source_fs)), ) //go:embed templates/* static/* git_hooks_client/git_hooks_client var resources_fs embed.FS var templates *template.Template func load_templates() (err error) { templates, err = template.New("templates").Funcs(template.FuncMap{ "first_line": first_line, "base_name": base_name, }).ParseFS(resources_fs, "templates/*") return err } var static_handler http.Handler func init() {
static_fs, err := fs.Sub(resources_fs, "static") if err != nil {
if static_fs, err := fs.Sub(resources_fs, "static"); err != nil {
panic(err) } static_handler = http.StripPrefix("/:/static/", http.FileServer(http.FS(static_fs))) }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "errors" "fmt" "os" "os/exec" glider_ssh "github.com/gliderlabs/ssh" "github.com/go-git/go-git/v5" "go.lindenii.runxiyu.org/lindenii-common/cmap" ) type pack_to_hook_t struct { session glider_ssh.Session repo *git.Repository pubkey string direct_access bool repo_path string user_id int repo_id int group_name string repo_name string } var pack_to_hook_by_cookie = cmap.Map[string, pack_to_hook_t]{} // ssh_handle_receive_pack handles attempts to push to repos. func ssh_handle_receive_pack(session glider_ssh.Session, pubkey string, repo_identifier string) (err error) { group_name, repo_name, repo_id, repo_path, direct_access, contrib_requirements, user_type, user_id, err := get_repo_path_perms_from_ssh_path_pubkey(session.Context(), repo_identifier, pubkey) if err != nil { return err } repo, err := git.PlainOpen(repo_path) if err != nil { return err } repo_config, err := repo.Config() if err != nil { return err } repo_config_core := repo_config.Raw.Section("core") if repo_config_core == nil { return errors.New("Repository has no core section in config") } hooksPath := repo_config_core.OptionAll("hooksPath") if len(hooksPath) != 1 || hooksPath[0] != config.Hooks.Execs { return errors.New("Repository has hooksPath set to an unexpected value") } if !direct_access { switch contrib_requirements { case "closed": if !direct_access { return errors.New("You need direct access to push to this repo.") } case "registered_user": if user_type != "registered" { return errors.New("You need to be a registered user to push to this repo.") } case "ssh_pubkey": if pubkey == "" { return errors.New("You need to have an SSH public key to push to this repo.") } if user_type == "" { user_id, err = add_user_ssh(session.Context(), pubkey) if err != nil { return err } fmt.Fprintln(session.Stderr(), "You are now registered as user ID", user_id) } case "public": default: panic("unknown contrib_requirements value " + contrib_requirements) } } cookie, err := random_urlsafe_string(16) if err != nil { fmt.Fprintln(session.Stderr(), "Error while generating cookie:", err) } fmt.Println(group_name, repo_name) pack_to_hook_by_cookie.Store(cookie, pack_to_hook_t{ session: session, pubkey: pubkey, direct_access: direct_access, repo_path: repo_path, user_id: user_id, repo_id: repo_id, group_name: group_name, repo_name: repo_name, repo: repo, }) defer pack_to_hook_by_cookie.Delete(cookie) // The Delete won't execute until proc.Wait returns unless something // horribly wrong such as a panic occurs. proc := exec.CommandContext(session.Context(), "git-receive-pack", repo_path) proc.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+config.Hooks.Socket, "LINDENII_FORGE_HOOKS_COOKIE="+cookie, ) proc.Stdin = session proc.Stdout = session proc.Stderr = session.Stderr()
err = proc.Start() if err != nil {
if err = proc.Start(); err != nil {
fmt.Fprintln(session.Stderr(), "Error while starting process:", err) return err } err = proc.Wait() if exitError, ok := err.(*exec.ExitError); ok { fmt.Fprintln(session.Stderr(), "Process exited with error", exitError.ExitCode()) } else if err != nil { fmt.Fprintln(session.Stderr(), "Error while waiting for process:", err) } return err }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "fmt" "os" "os/exec" glider_ssh "github.com/gliderlabs/ssh" ) // ssh_handle_upload_pack handles clones/fetches. It just uses git-upload-pack // and has no ACL checks. func ssh_handle_upload_pack(session glider_ssh.Session, pubkey string, repo_identifier string) (err error) {
_, _, _, repo_path, _, _, _, _, err := get_repo_path_perms_from_ssh_path_pubkey(session.Context(), repo_identifier, pubkey) if err != nil {
var repo_path string if _, _, _, repo_path, _, _, _, _, err = get_repo_path_perms_from_ssh_path_pubkey(session.Context(), repo_identifier, pubkey); err != nil {
return err } proc := exec.CommandContext(session.Context(), "git-upload-pack", repo_path) proc.Env = append(os.Environ(), "LINDENII_FORGE_HOOKS_SOCKET_PATH="+config.Hooks.Socket) proc.Stdin = session proc.Stdout = session proc.Stderr = session.Stderr()
err = proc.Start() if err != nil {
if err = proc.Start(); err != nil {
fmt.Fprintln(session.Stderr(), "Error while starting process:", err) return err } err = proc.Wait() if exitError, ok := err.(*exec.ExitError); ok { fmt.Fprintln(session.Stderr(), "Process exited with error", exitError.ExitCode()) } else if err != nil { fmt.Fprintln(session.Stderr(), "Error while waiting for process:", err) } return err }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "fmt" "net" "os" "strings" glider_ssh "github.com/gliderlabs/ssh" "go.lindenii.runxiyu.org/lindenii-common/ansiec" "go.lindenii.runxiyu.org/lindenii-common/clog" go_ssh "golang.org/x/crypto/ssh" ) var ( server_public_key_string string server_public_key_fingerprint string server_public_key go_ssh.PublicKey ) func serve_ssh(listener net.Listener) error {
host_key_bytes, err := os.ReadFile(config.SSH.Key) if err != nil {
var host_key_bytes []byte var host_key go_ssh.Signer var err error var server *glider_ssh.Server if host_key_bytes, err = os.ReadFile(config.SSH.Key); err != nil {
return err }
host_key, err := go_ssh.ParsePrivateKey(host_key_bytes) if err != nil {
if host_key, err = go_ssh.ParsePrivateKey(host_key_bytes); err != nil {
return err } server_public_key = host_key.PublicKey() server_public_key_string = string(go_ssh.MarshalAuthorizedKey(server_public_key)) server_public_key_fingerprint = string(go_ssh.FingerprintSHA256(server_public_key))
server := &glider_ssh.Server{
server = &glider_ssh.Server{
Handler: func(session glider_ssh.Session) { client_public_key := session.PublicKey() var client_public_key_string string if client_public_key != nil { client_public_key_string = strings.TrimSuffix(string(go_ssh.MarshalAuthorizedKey(client_public_key)), "\n") } clog.Info("Incoming SSH: " + session.RemoteAddr().String() + " " + client_public_key_string + " " + session.RawCommand()) fmt.Fprintln(session.Stderr(), ansiec.Blue+"Lindenii Forge "+VERSION+", source at "+strings.TrimSuffix(config.HTTP.Root, "/")+"/:/source/"+ansiec.Reset+"\r") cmd := session.Command() if len(cmd) < 2 { fmt.Fprintln(session.Stderr(), "Insufficient arguments\r") return } switch cmd[0] { case "git-upload-pack": if len(cmd) > 2 { fmt.Fprintln(session.Stderr(), "Too many arguments\r") return } err = ssh_handle_upload_pack(session, client_public_key_string, cmd[1]) case "git-receive-pack": if len(cmd) > 2 { fmt.Fprintln(session.Stderr(), "Too many arguments\r") return } err = ssh_handle_receive_pack(session, client_public_key_string, cmd[1]) default: fmt.Fprintln(session.Stderr(), "Unsupported command: "+cmd[0]+"\r") return } if err != nil { fmt.Fprintln(session.Stderr(), err.Error()) return } }, PublicKeyHandler: func(ctx glider_ssh.Context, key glider_ssh.PublicKey) bool { return true }, KeyboardInteractiveHandler: func(ctx glider_ssh.Context, challenge go_ssh.KeyboardInteractiveChallenge) bool { return true }, // It is intentional that we do not check any credentials and accept all connections. // This allows all users to connect and clone repositories. However, the public key // is passed to handlers, so e.g. the push handler could check the key and reject the // push if it needs to. } server.AddHostKey(host_key)
err = server.Serve(listener) if err != nil {
if err = server.Serve(listener); err != nil {
clog.Fatal(1, "Serving SSH: "+err.Error()) } return nil }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "context" "errors" "fmt" "io" "net/url" "strings" "go.lindenii.runxiyu.org/lindenii-common/ansiec" ) var err_ssh_illegal_endpoint = errors.New("illegal endpoint during SSH access") func get_repo_path_perms_from_ssh_path_pubkey(ctx context.Context, ssh_path string, ssh_pubkey string) (group_name string, repo_name string, repo_id int, repo_path string, direct_access bool, contrib_requirements string, user_type string, user_id int, err error) {
segments := strings.Split(strings.TrimPrefix(ssh_path, "/"), "/")
var segments []string var separator_index int var module_type, module_name string segments = strings.Split(strings.TrimPrefix(ssh_path, "/"), "/")
for i, segment := range segments { var err error segments[i], err = url.PathUnescape(segment) if err != nil { return "", "", 0, "", false, "", "", 0, err } } if segments[0] == ":" { return "", "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint }
separator_index := -1
separator_index = -1
for i, part := range segments { if part == ":" { separator_index = i break } } if segments[len(segments)-1] == "" { segments = segments[:len(segments)-1] } switch { case separator_index == -1: return "", "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint case len(segments) <= separator_index+2: return "", "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint } group_name = segments[0]
module_type := segments[separator_index+1] module_name := segments[separator_index+2]
module_type = segments[separator_index+1] module_name = segments[separator_index+2]
repo_name = module_name switch module_type { case "repos": _1, _2, _3, _4, _5, _6, _7 := get_path_perm_by_group_repo_key(ctx, group_name, module_name, ssh_pubkey) return group_name, repo_name, _1, _2, _3, _4, _5, _6, _7 default: return "", "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint } } func wf_error(w io.Writer, format string, args ...any) { fmt.Fprintln(w, ansiec.Red+fmt.Sprintf(format, args...)+ansiec.Reset) }
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileContributor: Runxi Yu <https://runxiyu.org> package main import ( "context"
"github.com/jackc/pgx/v5"
) func add_user_ssh(ctx context.Context, pubkey string) (user_id int, err error) {
tx, err := database.Begin(ctx) if err != nil {
var tx pgx.Tx if tx, err = database.Begin(ctx); err != nil {
return } defer tx.Rollback(ctx)
err = tx.QueryRow(ctx, `INSERT INTO users (type) VALUES ('pubkey_only') RETURNING id`).Scan(&user_id) if err != nil {
if err = tx.QueryRow(ctx, `INSERT INTO users (type) VALUES ('pubkey_only') RETURNING id`).Scan(&user_id); err != nil {
return }
_, err = tx.Exec(ctx, `INSERT INTO ssh_public_keys (key_string, user_id) VALUES ($1, $2)`, pubkey, user_id) if err != nil {
if _, err = tx.Exec(ctx, `INSERT INTO ssh_public_keys (key_string, user_id) VALUES ($1, $2)`, pubkey, user_id); err != nil {
return } err = tx.Commit(ctx) return }