From 8ed0dbe4201a58b00d6f3743178f4cbe5328e2b0 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Thu, 06 Mar 2025 15:17:57 +0800 Subject: [PATCH] *: Support subgroups via SQL recursion --- acl.go | 69 +++++++++++++++++++++++++++++++++-------------------- git_hooks_handle.go | 2 +- git_misc.go | 43 ++++++++++++++++++++++++++++++++++++++----- http_handle_group_index.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++--- http_handle_index.go | 2 +- http_handle_repo_index.go | 9 +++++---- http_handle_repo_info.go | 42 +++++++++++++++++++++++++++++++++++++----- http_handle_repo_upload_pack.go | 44 +++++++++++++++++++++++++++++++++++++++----- http_server.go | 42 +++++++++++++++++++----------------------- remote_url.go | 8 ++++---- ssh_handle_receive_pack.go | 8 +++----- ssh_utils.go | 18 +++++++++--------- static/style.css | 3 +++ templates/_group_path.tmpl | 8 ++++++++ templates/_group_view.tmpl | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ templates/group.tmpl | 24 ++++++++++++++++++++++++ templates/group_repos.tmpl | 40 ---------------------------------------- templates/index.tmpl | 37 +++++++++++++++++-------------------- templates/repo_commit.tmpl | 2 +- templates/repo_contrib_index.tmpl | 2 +- templates/repo_contrib_one.tmpl | 2 +- templates/repo_index.tmpl | 2 +- templates/repo_log.tmpl | 2 +- templates/repo_raw_dir.tmpl | 2 +- templates/repo_tree_dir.tmpl | 2 +- templates/repo_tree_file.tmpl | 4 ++-- url.go | 7 +++++++ diff --git a/acl.go b/acl.go index a629da68099e8d0a3ffbfc72f3bb9498498097f8..597760a69c3335d01fb26368d38f637ce4bbefba 100644 --- a/acl.go +++ b/acl.go @@ -5,36 +5,53 @@ package main import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) // get_path_perm_by_group_repo_key returns the filesystem path and direct // access permission for a given repo and a provided ssh public key. -func get_path_perm_by_group_repo_key(ctx context.Context, group_name, repo_name, ssh_pubkey string) (repo_id int, filesystem_path string, access bool, contrib_requirements string, user_type string, user_id int, err error) { - err = database.QueryRow(ctx, - `SELECT - r.id, - r.filesystem_path, - CASE - WHEN ugr.user_id IS NOT NULL THEN TRUE - ELSE FALSE - END AS has_role_in_group, - r.contrib_requirements, - COALESCE(u.type, ''), - COALESCE(u.id, 0) - FROM - groups g - JOIN - repos r ON r.group_id = g.id - LEFT JOIN - ssh_public_keys s ON s.key_string = $3 - LEFT JOIN - users u ON u.id = s.user_id - LEFT JOIN - user_group_roles ugr ON ugr.group_id = g.id AND ugr.user_id = u.id - WHERE - g.name = $1 - AND r.name = $2;`, - group_name, repo_name, ssh_pubkey, +func get_path_perm_by_group_repo_key(ctx context.Context, group_path []string, repo_name, ssh_pubkey string) (repo_id int, filesystem_path string, access bool, contrib_requirements string, user_type string, user_id int, err error) { + err = database.QueryRow(ctx, ` +WITH RECURSIVE group_path_cte AS ( + -- Start: match the first name in the path where parent_group IS NULL + SELECT + id, + parent_group, + name, + 1 AS depth + FROM groups + WHERE name = ($1::text[])[1] + AND parent_group IS NULL + + UNION ALL + + -- Recurse: join next segment of the path + SELECT + g.id, + g.parent_group, + g.name, + group_path_cte.depth + 1 + FROM groups g + JOIN group_path_cte ON g.parent_group = group_path_cte.id + WHERE g.name = ($1::text[])[group_path_cte.depth + 1] + AND group_path_cte.depth + 1 <= cardinality($1::text[]) +) +SELECT + r.id, + r.filesystem_path, + CASE WHEN ugr.user_id IS NOT NULL THEN TRUE ELSE FALSE END AS has_role_in_group, + r.contrib_requirements, + COALESCE(u.type, ''), + COALESCE(u.id, 0) +FROM group_path_cte g +JOIN repos r ON r.group_id = g.id +LEFT JOIN ssh_public_keys s ON s.key_string = $3 +LEFT JOIN users u ON u.id = s.user_id +LEFT JOIN user_group_roles ugr ON ugr.group_id = g.id AND ugr.user_id = u.id +WHERE g.depth = cardinality($1::text[]) + AND r.name = $2 +`, pgtype.FlatArray[string](group_path), repo_name, ssh_pubkey, ).Scan(&repo_id, &filesystem_path, &access, &contrib_requirements, &user_type, &user_id) return } diff --git a/git_hooks_handle.go b/git_hooks_handle.go index f2359a2300f107ab27e002fbc7e2f3216d0fc547..063eb50900fca7b0db2913350b363322af9833fa 100644 --- a/git_hooks_handle.go +++ b/git_hooks_handle.go @@ -159,7 +159,7 @@ if err != nil { wf_error(ssh_stderr, "Error creating merge request: %v", err) return 1 } - fmt.Fprintln(ssh_stderr, ansiec.Blue+"Created merge request at", generate_http_remote_url(pack_to_hook.group_name, pack_to_hook.repo_name)+"/contrib/"+strconv.FormatUint(uint64(new_mr_id), 10)+"/"+ansiec.Reset) + fmt.Fprintln(ssh_stderr, ansiec.Blue+"Created merge request at", generate_http_remote_url(pack_to_hook.group_path, pack_to_hook.repo_name)+"/contrib/"+strconv.FormatUint(uint64(new_mr_id), 10)+"/"+ansiec.Reset) } else { // Existing contrib branch var existing_merge_request_user_id int var is_ancestor bool diff --git a/git_misc.go b/git_misc.go index 484500914eac7b93ced7fbfd1e89f56c9ba4781f..e80c737b2f86f38adf3169aa281f70d2a90b62e1 100644 --- a/git_misc.go +++ b/git_misc.go @@ -10,21 +10,54 @@ "io" "os" "strings" + "github.com/jackc/pgx/v5/pgtype" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" ) // open_git_repo opens a git repository by group and repo name. -func open_git_repo(ctx context.Context, group_name, repo_name string) (repo *git.Repository, description string, repo_id int, err error) { +func open_git_repo(ctx context.Context, group_path []string, repo_name string) (repo *git.Repository, description string, repo_id int, err error) { var fs_path string - err = database.QueryRow(ctx, - "SELECT r.filesystem_path, COALESCE(r.description, ''), r.id FROM repos r JOIN groups g ON r.group_id = g.id WHERE g.name = $1 AND r.name = $2;", - group_name, repo_name, - ).Scan(&fs_path, &description, &repo_id) + + err = database.QueryRow(ctx, ` +WITH RECURSIVE group_path_cte AS ( + -- Start: match the first name in the path where parent_group IS NULL + SELECT + id, + parent_group, + name, + 1 AS depth + FROM groups + WHERE name = ($1::text[])[1] + AND parent_group IS NULL + + UNION ALL + + -- Recurse: join next segment of the path + SELECT + g.id, + g.parent_group, + g.name, + group_path_cte.depth + 1 + FROM groups g + JOIN group_path_cte ON g.parent_group = group_path_cte.id + WHERE g.name = ($1::text[])[group_path_cte.depth + 1] + AND group_path_cte.depth + 1 <= cardinality($1::text[]) +) +SELECT + r.filesystem_path, + COALESCE(r.description, ''), + r.id +FROM group_path_cte g +JOIN repos r ON r.group_id = g.id +WHERE g.depth = cardinality($1::text[]) + AND r.name = $2 + `, pgtype.FlatArray[string](group_path), repo_name).Scan(&fs_path, &description, &repo_id) if err != nil { return } + repo, err = git.PlainOpen(fs_path) return } diff --git a/http_handle_group_index.go b/http_handle_group_index.go index 09a85f8f8d3279ea8e19a534af489b7a1541b3d8..bbdff469dc52150729527dcfec9f772aaf6e7b3d 100644 --- a/http_handle_group_index.go +++ b/http_handle_group_index.go @@ -4,21 +4,127 @@ package main import ( + "fmt" "net/http" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgtype" ) -func handle_group_repos(w http.ResponseWriter, r *http.Request, params map[string]any) { - var group_name string +func handle_group_index(w http.ResponseWriter, r *http.Request, params map[string]any) { + var group_path []string var repos []name_desc_t + var subgroups []name_desc_t var err error - group_name = params["group_name"].(string) - repos, err = query_name_desc_list(r.Context(), "SELECT r.name, COALESCE(r.description, '') FROM repos r JOIN groups g ON r.group_id = g.id WHERE g.name = $1;", group_name) + group_path = params["group_path"].([]string) + + // Repos + var rows pgx.Rows + rows, err = database.Query(r.Context(), ` + WITH RECURSIVE group_path_cte AS ( + SELECT + id, + parent_group, + name, + 1 AS depth + FROM groups + WHERE name = ($1::text[])[1] + AND parent_group IS NULL + + UNION ALL + + SELECT + g.id, + g.parent_group, + g.name, + group_path_cte.depth + 1 + FROM groups g + JOIN group_path_cte ON g.parent_group = group_path_cte.id + WHERE g.name = ($1::text[])[group_path_cte.depth + 1] + AND group_path_cte.depth + 1 <= cardinality($1::text[]) + ) + SELECT r.name, COALESCE(r.description, '') + FROM group_path_cte c + JOIN repos r ON r.group_id = c.id + WHERE c.depth = cardinality($1::text[]) + `, + pgtype.FlatArray[string](group_path), + ) if err != nil { - http.Error(w, "Error getting groups: "+err.Error(), http.StatusInternalServerError) + http.Error(w, "Error getting repos: "+err.Error(), http.StatusInternalServerError) return } + defer rows.Close() + + for rows.Next() { + var name, description string + if err = rows.Scan(&name, &description); err != nil { + http.Error(w, "Error getting repos: "+err.Error(), http.StatusInternalServerError) + return + } + repos = append(repos, name_desc_t{name, description}) + } + if err = rows.Err(); err != nil { + http.Error(w, "Error getting repos: "+err.Error(), http.StatusInternalServerError) + return + } + + // Subgroups + rows, err = database.Query(r.Context(), ` + WITH RECURSIVE group_path_cte AS ( + SELECT + id, + parent_group, + name, + 1 AS depth + FROM groups + WHERE name = ($1::text[])[1] + AND parent_group IS NULL + + UNION ALL + + SELECT + g.id, + g.parent_group, + g.name, + group_path_cte.depth + 1 + FROM groups g + JOIN group_path_cte ON g.parent_group = group_path_cte.id + WHERE g.name = ($1::text[])[group_path_cte.depth + 1] + AND group_path_cte.depth + 1 <= cardinality($1::text[]) + ) + SELECT g.name, COALESCE(g.description, '') + FROM group_path_cte c + JOIN groups g ON g.parent_group = c.id + WHERE c.depth = cardinality($1::text[]) + `, + pgtype.FlatArray[string](group_path), + ) + if err != nil { + http.Error(w, "Error getting subgroups: "+err.Error(), http.StatusInternalServerError) + return + } + defer rows.Close() + + for rows.Next() { + var name, description string + if err = rows.Scan(&name, &description); err != nil { + http.Error(w, "Error getting subgroups: "+err.Error(), http.StatusInternalServerError) + return + } + subgroups = append(subgroups, name_desc_t{name, description}) + } + if err = rows.Err(); err != nil { + http.Error(w, "Error getting subgroups: "+err.Error(), http.StatusInternalServerError) + return + } + params["repos"] = repos + params["subgroups"] = subgroups + + fmt.Println(group_path) - render_template(w, "group_repos", params) + render_template(w, "group", params) } + diff --git a/http_handle_index.go b/http_handle_index.go index f710315bc0a84dae6817ffc8f8bc9393a8edbffc..4632526ed5fcd102e51d8cbb5a7252fa4cef1959 100644 --- a/http_handle_index.go +++ b/http_handle_index.go @@ -11,7 +11,7 @@ func handle_index(w http.ResponseWriter, r *http.Request, params map[string]any) { var err error var groups []name_desc_t - groups, err = query_name_desc_list(r.Context(), "SELECT name, COALESCE(description, '') FROM groups") + groups, err = query_name_desc_list(r.Context(), "SELECT name, COALESCE(description, '') FROM groups WHERE parent_group IS NULL") if err != nil { http.Error(w, "Error querying groups: "+err.Error(), http.StatusInternalServerError) return diff --git a/http_handle_repo_index.go b/http_handle_repo_index.go index 73f10072f00d6b298e338429ff621084e7dc5a1d..66116c7f145779c16d7cad9c606c06ef1d95bcbd 100644 --- a/http_handle_repo_index.go +++ b/http_handle_repo_index.go @@ -13,14 +13,15 @@ ) func handle_repo_index(w http.ResponseWriter, r *http.Request, params map[string]any) { var repo *git.Repository - var repo_name, group_name string + var repo_name string + var group_path []string var ref_hash plumbing.Hash var err error var recent_commits []*object.Commit var commit_object *object.Commit var tree *object.Tree - repo, repo_name, group_name = params["repo"].(*git.Repository), params["repo_name"].(string), params["group_name"].(string) + repo, repo_name, group_path = params["repo"].(*git.Repository), params["repo_name"].(string), params["group_path"].([]string) if ref_hash, err = get_ref_hash_from_type_and_name(repo, params["ref_type"].(string), params["ref_name"].(string)); err != nil { http.Error(w, "Error getting ref hash: "+err.Error(), http.StatusInternalServerError) @@ -46,8 +47,8 @@ params["readme_filename"], params["readme"] = render_readme_at_tree(tree) params["files"] = build_display_git_tree(tree) - params["http_clone_url"] = generate_http_remote_url(group_name, repo_name) - params["ssh_clone_url"] = generate_ssh_remote_url(group_name, repo_name) + params["http_clone_url"] = generate_http_remote_url(group_path, repo_name) + params["ssh_clone_url"] = generate_ssh_remote_url(group_path, repo_name) render_template(w, "repo_index", params) } diff --git a/http_handle_repo_info.go b/http_handle_repo_info.go index d8227c9f2ab7f37c4580bcf062f67d3a7916af57..466e0bba71b7f87a2e53ab094279cfb3d8adb486 100644 --- a/http_handle_repo_info.go +++ b/http_handle_repo_info.go @@ -8,15 +8,47 @@ "fmt" "io" "net/http" "os/exec" + + "github.com/jackc/pgx/v5/pgtype" ) func handle_repo_info(w http.ResponseWriter, r *http.Request, params map[string]any) (err error) { - var group_name, repo_name, repo_path string + var group_path []string + var repo_name, repo_path string - group_name, repo_name = params["group_name"].(string), params["repo_name"].(string) - if err = database.QueryRow(r.Context(), - "SELECT r.filesystem_path FROM repos r JOIN groups g ON r.group_id = g.id WHERE g.name = $1 AND r.name = $2;", - group_name, repo_name, + if err := database.QueryRow(r.Context(), ` + WITH RECURSIVE group_path_cte AS ( + -- Start: match the first name in the path where parent_group IS NULL + SELECT + id, + parent_group, + name, + 1 AS depth + FROM groups + WHERE name = ($1::text[])[1] + AND parent_group IS NULL + + UNION ALL + + -- Recurse: jion next segment of the path + SELECT + g.id, + g.parent_group, + g.name, + group_path_cte.depth + 1 + FROM groups g + JOIN group_path_cte ON g.parent_group = group_path_cte.id + WHERE g.name = ($1::text[])[group_path_cte.depth + 1] + AND group_path_cte.depth + 1 <= cardinality($1::text[]) + ) + SELECT r.filesystem_path + FROM group_path_cte c + JOIN repos r ON r.group_id = c.id + WHERE c.depth = cardinality($1::text[]) + AND r.name = $2 + `, + pgtype.FlatArray[string](group_path), + repo_name, ).Scan(&repo_path); err != nil { return err } diff --git a/http_handle_repo_upload_pack.go b/http_handle_repo_upload_pack.go index b273da92b81105f9a1c5fb9bc4a4e64f6b736238..4d3ec0987f89a9153422b4ad5db44bd70b2ca1e0 100644 --- a/http_handle_repo_upload_pack.go +++ b/http_handle_repo_upload_pack.go @@ -8,23 +8,57 @@ "io" "net/http" "os" "os/exec" + + "github.com/jackc/pgx/v5/pgtype" ) func handle_upload_pack(w http.ResponseWriter, r *http.Request, params map[string]any) (err error) { - var group_name, repo_name string + var group_path []string + var repo_name string var repo_path string var stdout io.ReadCloser var stdin io.WriteCloser var cmd *exec.Cmd - group_name, repo_name = params["group_name"].(string), params["repo_name"].(string) + group_path, repo_name = params["group_path"].([]string), params["repo_name"].(string) - if err = database.QueryRow(r.Context(), - "SELECT r.filesystem_path FROM repos r JOIN groups g ON r.group_id = g.id WHERE g.name = $1 AND r.name = $2;", - group_name, repo_name, + if err := database.QueryRow(r.Context(), ` + WITH RECURSIVE group_path_cte AS ( + -- Start: match the first name in the path where parent_group IS NULL + SELECT + id, + parent_group, + name, + 1 AS depth + FROM groups + WHERE name = ($1::text[])[1] + AND parent_group IS NULL + + UNION ALL + + -- Recurse: jion next segment of the path + SELECT + g.id, + g.parent_group, + g.name, + group_path_cte.depth + 1 + FROM groups g + JOIN group_path_cte ON g.parent_group = group_path_cte.id + WHERE g.name = ($1::text[])[group_path_cte.depth + 1] + AND group_path_cte.depth + 1 <= cardinality($1::text[]) + ) + SELECT r.filesystem_path + FROM group_path_cte c + JOIN repos r ON r.group_id = c.id + WHERE c.depth = cardinality($1::text[]) + AND r.name = $2 + `, + pgtype.FlatArray[string](group_path), + repo_name, ).Scan(&repo_path); err != nil { return err } + w.Header().Set("Content-Type", "application/x-git-upload-pack-result") w.Header().Set("Connection", "Keep-Alive") diff --git a/http_server.go b/http_server.go index 0acb9a773a4fa6395573d162d38db83a56326634..7790d1f88889ffd16d2c50b933c8737408e6ab51 100644 --- a/http_server.go +++ b/http_server.go @@ -93,40 +93,34 @@ } params["separator_index"] = separator_index - // TODO - if separator_index > 1 { - http.Error(w, "Subgroups haven't been implemented yet", http.StatusNotImplemented) - return - } - + var group_path []string var module_type string var module_name string - var group_name string + + if separator_index > 0 { + group_path = segments[:separator_index] + } else { + group_path = segments[:len(segments) - 1] + } + params["group_path"] = group_path 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] - 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) - } + handle_group_index(w, r, params) + case non_empty_last_segments_len == separator_index+1: + http.Error(w, "Illegal path 1", http.StatusNotImplemented) + return + case non_empty_last_segments_len == separator_index+2: + http.Error(w, "Illegal path 2", http.StatusNotImplemented) + return default: 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 @@ -157,14 +151,16 @@ } // TODO: subgroups - if params["repo"], params["repo_description"], params["repo_id"], err = open_git_repo(r.Context(), group_name, module_name); err != nil { + if params["repo"], params["repo_description"], params["repo_id"], err = open_git_repo(r.Context(), group_path, module_name); err != nil { http.Error(w, "Error opening repo: "+err.Error(), http.StatusInternalServerError) return } + fmt.Println(non_empty_last_segments_len, separator_index, segments) + if non_empty_last_segments_len == separator_index+3 { if redirect_with_slash(w, r) { - return + return } handle_repo_index(w, r, params) return diff --git a/remote_url.go b/remote_url.go index e478324bca87520ee231e27206fe45d090e84a77..506e35cefc1ebb9e130f9e1b8db6567ea33826e1 100644 --- a/remote_url.go +++ b/remote_url.go @@ -10,10 +10,10 @@ ) // We don't use path.Join because it collapses multiple slashes into one. -func generate_ssh_remote_url(group_name, repo_name string) string { - return strings.TrimSuffix(config.SSH.Root, "/") + "/" + url.PathEscape(group_name) + "/:/repos/" + url.PathEscape(repo_name) +func generate_ssh_remote_url(group_path []string, repo_name string) string { + return strings.TrimSuffix(config.SSH.Root, "/") + "/" + path_escape_cat_segments(group_path) + "/:/repos/" + url.PathEscape(repo_name) } -func generate_http_remote_url(group_name, repo_name string) string { - return strings.TrimSuffix(config.HTTP.Root, "/") + "/" + url.PathEscape(group_name) + "/:/repos/" + url.PathEscape(repo_name) +func generate_http_remote_url(group_path []string, repo_name string) string { + return strings.TrimSuffix(config.HTTP.Root, "/") + "/" + path_escape_cat_segments(group_path) + "/:/repos/" + url.PathEscape(repo_name) } diff --git a/ssh_handle_receive_pack.go b/ssh_handle_receive_pack.go index 4816c61f69eeaaddbe4e3be498ef3c84457c8641..45610bb5c3378e22000c10d76e2a3293540b5134 100644 --- a/ssh_handle_receive_pack.go +++ b/ssh_handle_receive_pack.go @@ -22,7 +22,7 @@ direct_access bool repo_path string user_id int repo_id int - group_name string + group_path []string repo_name string } @@ -30,7 +30,7 @@ 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) + group_path, 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 } @@ -86,8 +86,6 @@ 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, @@ -95,7 +93,7 @@ direct_access: direct_access, repo_path: repo_path, user_id: user_id, repo_id: repo_id, - group_name: group_name, + group_path: group_path, repo_name: repo_name, repo: repo, }) diff --git a/ssh_utils.go b/ssh_utils.go index 0ec20659a0d618e87fe662cc3451b5c85f76aa45..d40facf1ebb73982c51498c10fff14918fcd836d 100644 --- a/ssh_utils.go +++ b/ssh_utils.go @@ -16,7 +16,7 @@ ) 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) { +func get_repo_path_perms_from_ssh_path_pubkey(ctx context.Context, ssh_path string, ssh_pubkey string) (group_path []string, repo_name string, repo_id int, repo_path string, direct_access bool, contrib_requirements string, user_type string, user_id int, err error) { var segments []string var separator_index int var module_type, module_name string @@ -27,12 +27,12 @@ for i, segment := range segments { var err error segments[i], err = url.PathUnescape(segment) if err != nil { - return "", "", 0, "", false, "", "", 0, err + return []string{}, "", 0, "", false, "", "", 0, err } } if segments[0] == ":" { - return "", "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint + return []string{}, "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint } separator_index = -1 @@ -48,21 +48,21 @@ } switch { case separator_index == -1: - return "", "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint + return []string{}, "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint case len(segments) <= separator_index+2: - return "", "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint + return []string{}, "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint } - group_name = segments[0] + group_path = segments[:separator_index] 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 + _1, _2, _3, _4, _5, _6, _7 := get_path_perm_by_group_repo_key(ctx, group_path, module_name, ssh_pubkey) + return group_path, repo_name, _1, _2, _3, _4, _5, _6, _7 default: - return "", "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint + return []string{}, "", 0, "", false, "", "", 0, err_ssh_illegal_endpoint } } diff --git a/static/style.css b/static/style.css index 278ea2249fe335a9f0a8825a6b71d234f2a4051b..5e35cd1df13bd3f320d84ba8431c084b909fa281 100644 --- a/static/style.css +++ b/static/style.css @@ -336,3 +336,6 @@ header#main-header > div#main-header-user { display: flex; align-items: center; } +table + table { + margin-top: 1rem; +} diff --git a/templates/_group_path.tmpl b/templates/_group_path.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..f8b5f47dfc7febe49134b95d9c8c003d7fafe42e --- /dev/null +++ b/templates/_group_path.tmpl @@ -0,0 +1,8 @@ +{{/* + SPDX-License-Identifier: AGPL-3.0-only + SPDX-FileContributor: Runxi Yu +*/}} +{{- define "group_path_plain" -}} +{{ $p := . }} +{{ range $i, $s := . }}{{ $s }}{{ if ne $i (len $p) }}/{{ end }}{{ end }} +{{ end }} diff --git a/templates/_group_view.tmpl b/templates/_group_view.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..4783162f09e2893a47a163a1d366014cd444ba3a --- /dev/null +++ b/templates/_group_view.tmpl @@ -0,0 +1,48 @@ +{{/* + SPDX-License-Identifier: AGPL-3.0-only + SPDX-FileContributor: Runxi Yu +*/}} +{{- define "group_view" -}} +{{ if .subgroups }} + + + + + + + + {{- range .subgroups }} + + + + + {{- end }} + +
Subgroups
+ {{ .Name }} + + {{ .Description }} +
+{{ end }} +{{ if .repos }} + + + + + + + + {{- range .repos }} + + + + + {{- end }} + +
Repos
+ {{ .Name }} + + {{ .Description }} +
+{{ end }} +{{- end -}} diff --git a/templates/group.tmpl b/templates/group.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..f88145805d4c1317878a5ba70bec12440dd3ca96 --- /dev/null +++ b/templates/group.tmpl @@ -0,0 +1,24 @@ +{{/* + SPDX-License-Identifier: AGPL-3.0-only + SPDX-FileContributor: Runxi Yu +*/}} +{{- define "group" -}} +{{ $group_path := .group_path }} + + + + {{ template "head_common" . }} + {{ range $i, $s := .group_path }}{{ $s }}{{ if ne $i (len $group_path) }} / {{ end }}{{ end }} – {{ .global.forge_title }} + + + {{ template "header" . }} +
+

{{ range $i, $s := .group_path }}{{ $s }}{{ if ne $i (len $group_path) }} / {{ end }}{{ end }} + {{ template "group_view" . }} +

+
+ {{ template "footer" . }} +
+ + +{{- end -}} diff --git a/templates/group_repos.tmpl b/templates/group_repos.tmpl deleted file mode 100644 index 3eae4f4573db3a6ed93b85a41021309e5cdaa6a3..0000000000000000000000000000000000000000 --- a/templates/group_repos.tmpl +++ /dev/null @@ -1,40 +0,0 @@ -{{/* - SPDX-License-Identifier: AGPL-3.0-only - SPDX-FileContributor: Runxi Yu -*/}} -{{- define "group_repos" -}} - - - - {{ template "head_common" . }} - Repos – {{ .group_name }} – {{ .global.forge_title }} - - - {{ template "header" . }} -
- - - - - - - - {{- range .repos }} - - - - - {{- end }} - -
Repos in {{ .group_name }}
- {{ .Name }} - - {{ .Description }} -
-
-
- {{ template "footer" . }} -
- - -{{- end -}} diff --git a/templates/index.tmpl b/templates/index.tmpl index c3b8bef67e3fae2a1b0153c75e2ee0346ac2afe1..2e039a9162e4a694daf6db7b58c32c9f0998d541 100644 --- a/templates/index.tmpl +++ b/templates/index.tmpl @@ -12,28 +12,25 @@ {{ template "header" . }}
- - +
+ + + + + + + {{- range .groups }} - + + - - - {{- range .groups }} - - - - - {{- end }} - -
Groups
- Groups - + {{ .Name }} + + {{ .Description }} +
- {{ .Name }} - - {{ .Description }} -
-
+ {{- end }} + +
diff --git a/templates/repo_commit.tmpl b/templates/repo_commit.tmpl index 1e346044808a6f939c37f40b6a29f4f1032e116c..bdb8c24f51376a20a215909df08a6425d0c65d96 100644 --- a/templates/repo_commit.tmpl +++ b/templates/repo_commit.tmpl @@ -7,7 +7,7 @@ {{ template "head_common" . }} - Commit {{ .commit_id }} – {{ .repo_name }} – {{ .group_name }} – {{ .global.forge_title }} + Commit {{ .commit_id }} – {{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .global.forge_title }} {{ template "header" . }} diff --git a/templates/repo_contrib_index.tmpl b/templates/repo_contrib_index.tmpl index da779f84623a3c7dfee7751de4029cc61ed7115b..512e352318889573fa8fc20f4e31590bb8685b8a 100644 --- a/templates/repo_contrib_index.tmpl +++ b/templates/repo_contrib_index.tmpl @@ -7,7 +7,7 @@ {{ template "head_common" . }} - Merge requests – {{ .repo_name }} – {{ .group_name }} – {{ .global.forge_title }} + Merge requests – {{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .global.forge_title }} {{ template "header" . }} diff --git a/templates/repo_contrib_one.tmpl b/templates/repo_contrib_one.tmpl index 1083e8c0a7c7fbb430754fdeb0d25b8d6800352d..88bc2fc50e2b728b63fa44bbd75979206d2f5d9f 100644 --- a/templates/repo_contrib_one.tmpl +++ b/templates/repo_contrib_one.tmpl @@ -7,7 +7,7 @@ {{ template "head_common" . }} - Merge requests – {{ .repo_name }} – {{ .group_name }} – {{ .global.forge_title }} + Merge requests – {{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .global.forge_title }} {{ template "header" . }} diff --git a/templates/repo_index.tmpl b/templates/repo_index.tmpl index 9dec74535e3421c11237335d388d28efa79b683e..3ce6d330414075fabf449badc51b18ca674c8a83 100644 --- a/templates/repo_index.tmpl +++ b/templates/repo_index.tmpl @@ -7,7 +7,7 @@ {{ template "head_common" . }} - {{ .repo_name }} – {{ .group_name }} – {{ .global.forge_title }} + {{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .global.forge_title }} {{ template "header" . }} diff --git a/templates/repo_log.tmpl b/templates/repo_log.tmpl index 8e33beac9f77dc651838ef79029ce93b3457ccb7..dbe764e75d79f4fad4031935c553fa4b9d8a6578 100644 --- a/templates/repo_log.tmpl +++ b/templates/repo_log.tmpl @@ -7,7 +7,7 @@ {{ template "head_common" . }} - Log – {{ .repo_name }} – {{ .group_name }} – {{ .global.forge_title }} + Log – {{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .global.forge_title }} {{ template "header" . }} diff --git a/templates/repo_raw_dir.tmpl b/templates/repo_raw_dir.tmpl index 47ef69fa35e116b0fff220f0df1e5e4a528672ec..07f577a45ea6fa1c7336c20ec5d825ff741a0471 100644 --- a/templates/repo_raw_dir.tmpl +++ b/templates/repo_raw_dir.tmpl @@ -7,7 +7,7 @@ {{ template "head_common" . }} - /{{ .path_spec }}{{ if ne .path_spec "" }}/{{ end }} – {{ .repo_name }} – {{ .group_name }} – {{ .global.forge_title }} + /{{ .path_spec }}{{ if ne .path_spec "" }}/{{ end }} – {{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .global.forge_title }} {{ template "header" . }} diff --git a/templates/repo_tree_dir.tmpl b/templates/repo_tree_dir.tmpl index e520225886987d0f866b28d88a02e2bda5be776e..ab9788b867c7da1df510d2b93cb574e3c160b181 100644 --- a/templates/repo_tree_dir.tmpl +++ b/templates/repo_tree_dir.tmpl @@ -7,7 +7,7 @@ {{ template "head_common" . }} - /{{ .path_spec }}{{ if ne .path_spec "" }}/{{ end }} – {{ .repo_name }} – {{ .group_name }} – {{ .global.forge_title }} + /{{ .path_spec }}{{ if ne .path_spec "" }}/{{ end }} – {{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .global.forge_title }} {{ template "header" . }} diff --git a/templates/repo_tree_file.tmpl b/templates/repo_tree_file.tmpl index d6890b8e7c33bf69dd644ca18404ae26f26d00a4..333e1dc11a4d010694d1b6db8c9f5b51ee9dd4aa 100644 --- a/templates/repo_tree_file.tmpl +++ b/templates/repo_tree_file.tmpl @@ -8,13 +8,13 @@ {{ template "head_common" . }} - /{{ .path_spec }} – {{ .repo_name }} – {{ .group_name }} – {{ .global.forge_title }} + /{{ .path_spec }} – {{ .repo_name }} – {{ template "group_path_plain" .group_path }} – {{ .global.forge_title }} {{ template "header" . }}

- /{{ .path_spec }} (raw) + /{{ .path_spec }} (raw)

{{ .file_contents }}
diff --git a/url.go b/url.go index d391de2a1a528a0cfd89c52799512faaa6447ce1..8be7047a44b76bee433c22cd495fb1830d71ddd2 100644 --- a/url.go +++ b/url.go @@ -98,3 +98,10 @@ return true } return false } + +func path_escape_cat_segments(segments []string) string { + for i, segment := range segments { + segments[i] = url.PathEscape(segment) + } + return strings.Join(segments, "/") +} -- 2.48.1