From d15089b985122d0841afdd1379791fa9deefa374 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sat, 05 Apr 2025 12:34:14 +0800 Subject: [PATCH] HTTP: Make the tree and raw endpoints use git2d --- chroma.go | 35 +++++++++++++++++++++++++++++++++++ http_handle_repo_raw.go | 159 ++++++++++++++++++++++++++++++++--------------------- http_handle_repo_tree.go | 216 +++++++++++++++++++++++------------------------------ diff --git a/chroma.go b/chroma.go new file mode 100644 index 0000000000000000000000000000000000000000..5a9f99f5a8fcfbc1f86962785d8e8e05e2ff5106 --- /dev/null +++ b/chroma.go @@ -0,0 +1,35 @@ +package main + +import ( + "bytes" + "html/template" + + chromaHTML "github.com/alecthomas/chroma/v2/formatters/html" + chromaLexers "github.com/alecthomas/chroma/v2/lexers" + chromaStyles "github.com/alecthomas/chroma/v2/styles" +) + +func renderHighlightedFile(filename, content string) template.HTML { + lexer := chromaLexers.Match(filename) + if lexer == nil { + lexer = chromaLexers.Fallback + } + + iterator, err := lexer.Tokenise(nil, content) + if err != nil { + return template.HTML("
Error tokenizing file: " + err.Error() + "
") + } + + var buf bytes.Buffer + style := chromaStyles.Get("autumn") + formatter := chromaHTML.New( + chromaHTML.WithClasses(true), + chromaHTML.TabWidth(8), + ) + + if err := formatter.Format(&buf, style, iterator); err != nil { + return template.HTML("
Error formatting file: " + err.Error() + "
") + } + + return template.HTML(buf.Bytes()) //#nosec G203 +} diff --git a/http_handle_repo_raw.go b/http_handle_repo_raw.go index 75296d6929edb793aa359e07b303c2b506ebad0e..5928f8c284d3df29fbfc32c10151c7bbc518d932 100644 --- a/http_handle_repo_raw.go +++ b/http_handle_repo_raw.go @@ -4,102 +4,135 @@ package main import ( + "errors" "fmt" + "html/template" + "io" + "net" "net/http" "strings" - "time" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" + "git.sr.ht/~sircmpwn/go-bare" ) // httpHandleRepoRaw serves raw files, or directory listings that point to raw // files. func httpHandleRepoRaw(writer http.ResponseWriter, request *http.Request, params map[string]any) { - var rawPathSpec, pathSpec string - var repo *git.Repository - var refHash plumbing.Hash - var refHashSlice []byte - var commitObj *object.Commit - var tree *object.Tree - var err error - - rawPathSpec = params["rest"].(string) - repo, pathSpec = params["repo"].(*git.Repository), strings.TrimSuffix(rawPathSpec, "/") + repoName := params["repo_name"].(string) + groupPath := params["group_path"].([]string) + rawPathSpec := params["rest"].(string) + pathSpec := strings.TrimSuffix(rawPathSpec, "/") params["path_spec"] = pathSpec - if refHash, err = getRefHash(repo, params["ref_type"].(string), params["ref_name"].(string)); err != nil { - errorPage500(writer, params, "Error getting ref hash: "+err.Error()) + _, repoPath, _, _, _, _, _ := getRepoInfo(request.Context(), groupPath, repoName, "") + + conn, err := net.Dial("unix", config.Git.Socket) + if err != nil { + errorPage500(writer, params, "git2d connection failed: "+err.Error()) return } - refHashSlice = refHash[:] + defer conn.Close() - cacheHandle := append(refHashSlice, stringToBytes(pathSpec)...) //nolint:gocritic + brWriter := bare.NewWriter(conn) + brReader := bare.NewReader(conn) - if value, found := treeReadmeCache.Get(cacheHandle); found { - params["files"] = value.DisplayTree - renderTemplate(writer, "repo_raw_dir", params) + if err := brWriter.WriteData([]byte(repoPath)); err != nil { + errorPage500(writer, params, "sending repo path failed: "+err.Error()) return } - if value, found := commitPathFileRawCache.Get(cacheHandle); found { - fmt.Fprint(writer, value) + if err := brWriter.WriteUint(2); err != nil { + errorPage500(writer, params, "sending command failed: "+err.Error()) return } - - if commitObj, err = repo.CommitObject(refHash); err != nil { - errorPage500(writer, params, "Error getting commit object: "+err.Error()) + if err := brWriter.WriteData([]byte(pathSpec)); err != nil { + errorPage500(writer, params, "sending path failed: "+err.Error()) return } - if tree, err = commitObj.Tree(); err != nil { - errorPage500(writer, params, "Error getting file tree: "+err.Error()) + + status, err := brReader.ReadUint() + if err != nil { + errorPage500(writer, params, "reading status failed: "+err.Error()) return } - start := time.Now() - var target *object.Tree - if pathSpec == "" { - target = tree - } else { - if target, err = tree.Tree(pathSpec); err != nil { - var file *object.File - var fileContent string - if file, err = tree.File(pathSpec); err != nil { - errorPage500(writer, params, "Error retrieving path: "+err.Error()) + switch status { + case 0: + kind, err := brReader.ReadUint() + if err != nil { + errorPage500(writer, params, "reading object kind failed: "+err.Error()) + return + } + + switch kind { + case 1: + // Tree + if redirectDir(writer, request) { + return + } + count, err := brReader.ReadUint() + if err != nil { + errorPage500(writer, params, "reading entry count failed: "+err.Error()) return } + + files := make([]displayTreeEntry, 0, count) + for i := uint64(0); i < count; i++ { + typeCode, err := brReader.ReadUint() + if err != nil { + errorPage500(writer, params, "error reading entry type: "+err.Error()) + return + } + mode, err := brReader.ReadUint() + if err != nil { + errorPage500(writer, params, "error reading entry mode: "+err.Error()) + return + } + size, err := brReader.ReadUint() + if err != nil { + errorPage500(writer, params, "error reading entry size: "+err.Error()) + return + } + name, err := brReader.ReadData() + if err != nil { + errorPage500(writer, params, "error reading entry name: "+err.Error()) + return + } + files = append(files, displayTreeEntry{ + Name: string(name), + Mode: fmt.Sprintf("%06o", mode), + Size: int64(size), + IsFile: typeCode == 2, + IsSubtree: typeCode == 1, + }) + } + + params["files"] = files + params["readme_filename"] = "README.md" + params["readme"] = template.HTML("

README rendering here is WIP again

") // TODO + + renderTemplate(writer, "repo_raw_dir", params) + + case 2: + // Blob if redirectNoDir(writer, request) { return } - if fileContent, err = file.Contents(); err != nil { - errorPage500(writer, params, "Error reading file: "+err.Error()) + content, err := brReader.ReadData() + if err != nil && !errors.Is(err, io.EOF) { + errorPage500(writer, params, "error reading blob content: "+err.Error()) return } - cost := time.Since(start).Nanoseconds() - commitPathFileRawCache.Set(cacheHandle, fileContent, cost) writer.Header().Set("Content-Type", "application/octet-stream") - fmt.Fprint(writer, fileContent) - return + fmt.Fprint(writer, string(content)) + + default: + errorPage500(writer, params, fmt.Sprintf("unknown object kind: %d", kind)) } - } + + case 3: + errorPage500(writer, params, fmt.Sprintf("path not found: %s", pathSpec)) - if redirectDir(writer, request) { - return + default: + errorPage500(writer, params, fmt.Sprintf("unknown status code: %d", status)) } - - displayTree := makeDisplayTree(target) - readmeFilename, readmeRendered := renderReadmeAtTree(target) - cost := time.Since(start).Nanoseconds() - - params["files"] = displayTree - params["readme_filename"] = readmeFilename - params["readme"] = readmeRendered - - treeReadmeCache.Set(cacheHandle, treeReadmeCacheEntry{ - DisplayTree: displayTree, - ReadmeFilename: readmeFilename, - ReadmeRendered: readmeRendered, - }, cost) - - renderTemplate(writer, "repo_raw_dir", params) } diff --git a/http_handle_repo_tree.go b/http_handle_repo_tree.go index 3e59dab68548a51f5ffbd9dd7534d96a946d4708..24595809cd9c07a05678f85a80babf397a393df0 100644 --- a/http_handle_repo_tree.go +++ b/http_handle_repo_tree.go @@ -4,20 +4,15 @@ package main import ( - "bytes" + "errors" + "fmt" "html/template" + "io" + "net" "net/http" - "path" "strings" - "time" - "github.com/alecthomas/chroma/v2" - chromaHTML "github.com/alecthomas/chroma/v2/formatters/html" - chromaLexers "github.com/alecthomas/chroma/v2/lexers" - chromaStyles "github.com/alecthomas/chroma/v2/styles" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" + "git.sr.ht/~sircmpwn/go-bare" ) // httpHandleRepoTree provides a friendly, syntax-highlighted view of @@ -25,143 +20,116 @@ // individual files, and provides directory views that link to these files. // // TODO: Do not highlight files that are too large. func httpHandleRepoTree(writer http.ResponseWriter, request *http.Request, params map[string]any) { - var rawPathSpec, pathSpec string - var repo *git.Repository - var refHash plumbing.Hash - var refHashSlice []byte - var commitObject *object.Commit - var tree *object.Tree - var err error - - rawPathSpec = params["rest"].(string) - repo, pathSpec = params["repo"].(*git.Repository), strings.TrimSuffix(rawPathSpec, "/") + repoName := params["repo_name"].(string) + groupPath := params["group_path"].([]string) + rawPathSpec := params["rest"].(string) + pathSpec := strings.TrimSuffix(rawPathSpec, "/") params["path_spec"] = pathSpec - if refHash, err = getRefHash(repo, params["ref_type"].(string), params["ref_name"].(string)); err != nil { - errorPage500(writer, params, "Error getting ref hash: "+err.Error()) + _, repoPath, _, _, _, _, _ := getRepoInfo(request.Context(), groupPath, repoName, "") + + conn, err := net.Dial("unix", config.Git.Socket) + if err != nil { + errorPage500(writer, params, "git2d connection failed: "+err.Error()) return } - refHashSlice = refHash[:] + defer conn.Close() - cacheHandle := append(refHashSlice, stringToBytes(pathSpec)...) //nolint:gocritic + brWriter := bare.NewWriter(conn) + brReader := bare.NewReader(conn) - if value, found := treeReadmeCache.Get(cacheHandle); found { - params["files"] = value.DisplayTree - params["readme_filename"] = value.ReadmeFilename - params["readme"] = value.ReadmeRendered - renderTemplate(writer, "repo_tree_dir", params) + if err := brWriter.WriteData([]byte(repoPath)); err != nil { + errorPage500(writer, params, "sending repo path failed: "+err.Error()) return } - - if value, found := commitPathFileHTMLCache.Get(cacheHandle); found { - params["file_contents"] = value - renderTemplate(writer, "repo_tree_file", params) + if err := brWriter.WriteUint(2); err != nil { + errorPage500(writer, params, "sending command failed: "+err.Error()) return } - start := time.Now() - - var target *object.Tree - if pathSpec == "" { - if commitObject, err = repo.CommitObject(refHash); err != nil { - errorPage500(writer, params, "Error getting commit object: "+err.Error()) - return - } - if tree, err = commitObject.Tree(); err != nil { - errorPage500(writer, params, "Error getting file tree: "+err.Error()) - return - } - - displayTree := makeDisplayTree(tree) - readmeFilename, readmeRendered := renderReadmeAtTree(tree) - cost := time.Since(start).Nanoseconds() - - params["files"] = displayTree - params["readme_filename"] = readmeFilename - params["readme"] = readmeRendered - - entry := treeReadmeCacheEntry{ - DisplayTree: displayTree, - ReadmeFilename: readmeFilename, - ReadmeRendered: readmeRendered, - } - treeReadmeCache.Set(cacheHandle, entry, cost) - - renderTemplate(writer, "repo_tree_dir", params) + if err := brWriter.WriteData([]byte(pathSpec)); err != nil { + errorPage500(writer, params, "sending path failed: "+err.Error()) return } - if commitObject, err = repo.CommitObject(refHash); err != nil { - errorPage500(writer, params, "Error getting commit object: "+err.Error()) - return - } - if tree, err = commitObject.Tree(); err != nil { - errorPage500(writer, params, "Error getting file tree: "+err.Error()) + status, err := brReader.ReadUint() + if err != nil { + errorPage500(writer, params, "reading status failed: "+err.Error()) return } - if target, err = tree.Tree(pathSpec); err != nil { - var file *object.File - var fileContent string - var lexer chroma.Lexer - var iterator chroma.Iterator - var style *chroma.Style - var formatter *chromaHTML.Formatter - var formattedHTML template.HTML - if file, err = tree.File(pathSpec); err != nil { - errorPage500(writer, params, "Error retrieving path: "+err.Error()) - return - } - if redirectNoDir(writer, request) { - return - } - if fileContent, err = file.Contents(); err != nil { - errorPage500(writer, params, "Error reading file: "+err.Error()) + switch status { + case 0: + kind, err := brReader.ReadUint() + if err != nil { + errorPage500(writer, params, "reading object kind failed: "+err.Error()) return } - lexer = chromaLexers.Match(pathSpec) - if lexer == nil { - lexer = chromaLexers.Fallback - } - if iterator, err = lexer.Tokenise(nil, fileContent); err != nil { - errorPage500(writer, params, "Error tokenizing code: "+err.Error()) - return - } - var formattedHTMLStr bytes.Buffer - style = chromaStyles.Get("autumn") - formatter = chromaHTML.New(chromaHTML.WithClasses(true), chromaHTML.TabWidth(8)) - if err = formatter.Format(&formattedHTMLStr, style, iterator); err != nil { - errorPage500(writer, params, "Error formatting code: "+err.Error()) - return - } - formattedHTML = template.HTML(formattedHTMLStr.Bytes()) //#nosec G203 - cost := time.Since(start).Nanoseconds() - commitPathFileHTMLCache.Set(cacheHandle, formattedHTML, cost) + switch kind { + case 1: + // Tree + count, err := brReader.ReadUint() + if err != nil { + errorPage500(writer, params, "reading entry count failed: "+err.Error()) + return + } + files := make([]displayTreeEntry, 0, count) + for i := uint64(0); i < count; i++ { + typeCode, err := brReader.ReadUint() + if err != nil { + errorPage500(writer, params, "error reading entry type: "+err.Error()) + return + } + mode, err := brReader.ReadUint() + if err != nil { + errorPage500(writer, params, "error reading entry mode: "+err.Error()) + return + } + size, err := brReader.ReadUint() + if err != nil { + errorPage500(writer, params, "error reading entry size: "+err.Error()) + return + } + name, err := brReader.ReadData() + if err != nil { + errorPage500(writer, params, "error reading entry name: "+err.Error()) + return + } - params["file_contents"] = formattedHTML + files = append(files, displayTreeEntry{ + Name: string(name), + Mode: fmt.Sprintf("%06o", mode), + Size: int64(size), + IsFile: typeCode == 2, + IsSubtree: typeCode == 1, + }) + } + params["files"] = files + params["readme_filename"] = "README.md" + params["readme"] = template.HTML("

README rendering here is WIP again

") // TODO + renderTemplate(writer, "repo_tree_dir", params) - renderTemplate(writer, "repo_tree_file", params) - return - } + case 2: + // Blob + content, err := brReader.ReadData() + if err != nil && !errors.Is(err, io.EOF) { + errorPage500(writer, params, "error reading file content: "+err.Error()) + return + } + rendered := renderHighlightedFile(pathSpec, string(content)) + params["file_contents"] = rendered + renderTemplate(writer, "repo_tree_file", params) - if len(rawPathSpec) != 0 && rawPathSpec[len(rawPathSpec)-1] != '/' { - http.Redirect(writer, request, path.Base(pathSpec)+"/", http.StatusSeeOther) + default: + errorPage500(writer, params, fmt.Sprintf("unknown kind: %d", kind)) + return + } + + case 3: + errorPage500(writer, params, fmt.Sprintf("path not found: %s", pathSpec)) return - } - displayTree := makeDisplayTree(target) - readmeFilename, readmeRendered := renderReadmeAtTree(target) - cost := time.Since(start).Nanoseconds() - - entry := treeReadmeCacheEntry{ - DisplayTree: displayTree, - ReadmeFilename: readmeFilename, - ReadmeRendered: readmeRendered, + default: + errorPage500(writer, params, fmt.Sprintf("unknown status code: %d", status)) } - treeReadmeCache.Set(cacheHandle, entry, cost) - - params["readme_filename"], params["readme"] = readmeFilename, readmeRendered - params["files"] = displayTree - - renderTemplate(writer, "repo_tree_dir", params) } -- 2.48.1