Personal Git forge.
package appimport ( "context" "errors" md "farga/app/middleware" "farga/db/types" "farga/lib" "farga/lib/git" hk "farga/lib/hackatime" "farga/lib/mime" "farga/templating" "fmt" "io" "net/http" "path" "path/filepath" "slices" "strconv" "strings" "github.com/go-git/go-git/v6/plumbing" "github.com/go-git/go-git/v6/plumbing/object" "github.com/gorilla/mux")func extractHandle(r *http.Request) string { return strings.TrimPrefix(mux.Vars(r)["user"], "~")}const modeKey = "mode"func withMode(mode string, h http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), modeKey, mode) h(w, r.WithContext(ctx)) }}var availableTypes = []string{"tree", "blob", "raw", "log"}type RepoViewOptions struct { View string Git *git.GitRepo DB *types.Repo HkTime *hk.ProjectItem Path string Blob *git.File Log *git.Log Issues []types.Issue Issue types.Issue IssueComments []types.IssueComment Projects []hk.ProjectItem}// little wrapper because I was scared of missing somethingfunc (a *App) renderRepo(w http.ResponseWriter, r *http.Request, template string, o RepoViewOptions) { user, ok := r.Context().Value(lib.UserKey).(*types.User) isOwner := false if ok && o.DB != nil { isOwner = (*o.DB.UserID == user.ID) } var options = map[string]any{ "Meta": o.DB, "Blob": o.Blob, "Log": o.Log, "Git": o.Git, "Owner": o.DB.User, "Path": o.Path, "Parent": filepath.Dir(strings.TrimSuffix(o.Path, "/")), "View": o.View, "Issues": o.Issues, "Issue": o.Issue, "IssueComments": o.IssueComments, "IsOwner": isOwner, "Projects": o.Projects, "Hackatime": o.HkTime, } if o.Git != nil { if o.Git.Head != nil { options["Branch"] = o.Git.Head.Name().Short() var branches []string gitBranches, _ := o.Git.Git.Branches() gitBranches.ForEach(func(r *plumbing.Reference) error { branches = append(branches, r.Name().Short()) return nil }) options["Branches"] = branches } options["Commit"] = o.Git.LatestCommit options["Readme"] = o.Git.Readme options["Files"] = o.Git.Files var formattedReadme, err = lib.FormatMarkdown([]byte(o.Git.Readme.Content)) if err == nil { options["ReadmeHTML"] = formattedReadme } } templating.Render(w, r, "repo", "repo/"+template, options)}type redirectToRepoIndexOptions struct { Handle string Name string // Should NOT INCLUDE THE "?" , although I will remove it anyways UrlParams string}func (a *App) redirectToRepoIndex(w http.ResponseWriter, r *http.Request, o redirectToRepoIndexOptions) { urlParams := strings.TrimPrefix(o.UrlParams, "?") http.Redirect(w, r, fmt.Sprintf("/~%s/%s?%s", o.Handle, o.Name, urlParams), http.StatusTemporaryRedirect)}func (a *App) RepoHandler(w http.ResponseWriter, r *http.Request) { reqHandle := extractHandle(r) vars := mux.Vars(r) reqName := vars["repo"] reqBranch := vars["branch"] reqPath := vars["path"] reqMode := r.Context().Value(modeKey).(string) if reqMode != "" && !slices.Contains(availableTypes, reqMode) { http.NotFound(w, r) return } cleanPath := path.Clean(reqPath) // empty if cleanPath == "." { cleanPath = "" } // strip slash prefix if before, ok := strings.CutSuffix(cleanPath, "/"); ok { cleanPath = before } // don't waste resources if the branch is invalid. if reqBranch != "" { if !lib.GitBranchRegex.MatchString(reqBranch) { http.Error(w, "Invalid branch name.", 400) return } } dbRepo, ok := r.Context().Value(md.RepoKey).(types.Repo) if !ok { http.Error(w, "Internal Server Error: Middleware did not provide repo data.", http.StatusInternalServerError) return } // self explanatory but I'll comment on it anyways. // opens a git repo. // mind-blowing, ain't it? gitRepo, err := git.Repo(*dbRepo.UserID, dbRepo.ID, reqBranch) if err != nil { switch err { case git.ErrInvalidBranch: a.redirectToRepoIndex(w, r, redirectToRepoIndexOptions{ Handle: reqHandle, Name: reqName, UrlParams: "err=GitBranchNotFound", }) default: io.WriteString(w, err.Error()) } return } defer gitRepo.Git.Close() repoPath := git.RepoPath(*dbRepo.UserID, dbRepo.ID) var baseTree *object.Tree if gitRepo.Head != nil { baseTree, err = git.BaseTree(gitRepo.Git, gitRepo.Head) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } var blob *git.File var log git.Log switch reqMode { case "blob": if baseTree != nil { entry, err := git.FindFileEntry(baseTree, cleanPath) if err != nil { http.NotFound(w, r) return } shortHash, err := git.ShortenHash(entry.Hash.String()) if err != nil { http.Error(w, "Invalid Git hash.", http.StatusInternalServerError) return } size, err := git.BlobSize(repoPath, entry.Hash) if err != nil { fmt.Println(err) http.Error(w, err.Error(), 500) return } blob = &git.File{ Name: path.Base(entry.Name), Path: cleanPath, Hash: entry.Hash, ShortHash: shortHash, Size: uint(size), TooLarge: size > lib.MaxBlobPreviewBytes, } if !blob.TooLarge { obj, err := gitRepo.Git.Storer.EncodedObject(plumbing.BlobObject, entry.Hash) if err != nil { fmt.Println(err) http.Error(w, err.Error(), 500) return } reader, err := obj.Reader() if err != nil { fmt.Println(err) http.Error(w, err.Error(), 500) return } defer reader.Close() content, err := io.ReadAll(reader) if err != nil { fmt.Println(err) http.Error(w, err.Error(), 500) return } blob.Content = content blob.Mime = mime.Type(content) } else { // it's too big, but if it's a video we can maybe use raw. head, err := git.ReadBlobPrefix(repoPath, entry.Hash, 512) if err != nil { fmt.Println(err) http.Error(w, err.Error(), 500) return } blob.Mime = mime.Type(head) } } case "raw": if baseTree != nil { entry, err := git.FindFileEntry(baseTree, cleanPath) if err != nil { http.NotFound(w, r) return } size, err := git.BlobSize(repoPath, entry.Hash) if err != nil { fmt.Println("Repo raw handling size error: ", err) http.Error(w, err.Error(), 500) return } if size > lib.MaxFileRawBytes { http.Error(w, "File too large.", http.StatusRequestEntityTooLarge) return } head, err := git.ReadBlobPrefix(repoPath, entry.Hash, 512) if err != nil { fmt.Println(err) http.Error(w, err.Error(), 500) return } if mime.IsText(mime.Type(head)) { w.Header().Set("Content-Type", "text/plain") // prevents stored-XSS } w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) if err := git.StreamBlob(repoPath, entry.Hash, w); err != nil { fmt.Println("Repo raw handling stream error: ", err) } } else { http.Error(w, "No such file exists.", 404) } return case "log": log, err = git.GetLog(git.LogOptions{ // todo Repo: gitRepo.Git, Branch: reqBranch, FileName: cleanPath, }) if err != nil { if errors.Is(err, git.ErrInvalidBranch) { a.redirectToRepoIndex(w, r, redirectToRepoIndexOptions{ Handle: reqHandle, Name: reqName, UrlParams: "?err=GitBranchNotFound", }) return } else if !errors.Is(err, git.ErrRepoHasNoHead) { // maybe it's an empty repo http.NotFound(w, r) return } } default: // tree if baseTree != nil { gitRepo.Files, err = git.DirFiles(baseTree, cleanPath) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } } var template = "index" if reqMode != "" && !(reqMode == "tree" && cleanPath == "") { template = reqMode } if cleanPath != "" && !strings.HasPrefix(cleanPath, "/") { cleanPath = "/" + cleanPath } var hkTime *hk.ProjectItem // if index or root. // TODO: caching. if (template == "index" || template == "tree") && cleanPath == "" && dbRepo.HackatimeName != "" { project, err := hk.Project(dbRepo.User.HackatimeToken, dbRepo.HackatimeName) if err == nil && project.TotalSeconds != 0 { // TODO: handle invalid hackatime repo hkTime = &project } } a.renderRepo(w, r, template, RepoViewOptions{ DB: &dbRepo, Git: gitRepo, Path: cleanPath, Blob: blob, Log: &log, View: reqMode, HkTime: hkTime, })}