Personal Git forge.

Code git clone ssh://git.poy.ooo:6969/~poyo/farga ZIP
app/repo-view.go / 8KiB #1db9b4db
Raw
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,	})}