Goit

Simple and lightweight Git web server
git clone http://git.omkov.net/Goit
Log | Tree | Refs | README | Download

Goit/src/repo/blame.go (179 lines, 4.4 KiB) -rw-r--r-- blame download

0123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
// Copyright (C) 2023, Jakob Wakeling
// All rights reserved.

package repo

import (
	"errors"
	"fmt"
	"html/template"
	"net/http"
	"path"
	"strings"

	"github.com/Jamozed/Goit/src/goit"
	"github.com/Jamozed/Goit/src/util"
	"github.com/dustin/go-humanize"
	"github.com/go-chi/chi/v5"
	"github.com/go-git/go-git/v5"
	"github.com/go-git/go-git/v5/plumbing"
	"github.com/go-git/go-git/v5/plumbing/object"
)

func HandleBlame(w http.ResponseWriter, r *http.Request) {
	auth, user, err := goit.Auth(w, r, true)
	if err != nil {
		util.PrintFuncError(err)
		goit.HttpError(w, http.StatusInternalServerError)
		return
	}

	tpath := chi.URLParam(r, "*")

	repo, err := goit.GetRepoByName(chi.URLParam(r, "repo"))
	if err != nil {
		goit.HttpError(w, http.StatusInternalServerError)
		return
	} else if repo == nil || !goit.IsVisible(repo, auth, user) {
		goit.HttpError(w, http.StatusNotFound)
		return
	}

	type Bline struct {
		Hash, ShortHash, Author, Date, Line string
		LineHTML                            template.HTML
	}

	data := struct {
		HeaderFields
		Title, Path, LineC, Size, Mode string
		Blines                         []Bline
		PathHTML                       template.HTML
	}{
		Title:        repo.Name + " - " + tpath,
		HeaderFields: GetHeaderFields(auth, user, repo, r.Host),
	}

	gr, err := git.PlainOpen(goit.RepoPath(repo.Name, true))
	if err != nil {
		util.PrintFuncError(err)
		goit.HttpError(w, http.StatusInternalServerError)
		return
	}

	ref, err := gr.Head()
	if errors.Is(err, plumbing.ErrReferenceNotFound) {
		goit.HttpError(w, http.StatusNotFound)
		return
	} else if err != nil {
		util.PrintFuncError(err)
		goit.HttpError(w, http.StatusInternalServerError)
		return
	}

	if readme, _ := findPattern(gr, ref, readmePattern); readme != "" {
		data.Readme = path.Join("/", repo.Name, "file", readme)
	}
	if licence, _ := findPattern(gr, ref, licencePattern); licence != "" {
		data.Licence = path.Join("/", repo.Name, "file", licence)
	}

	commit, err := gr.CommitObject(ref.Hash())
	if err != nil {
		util.PrintFuncError(err)
		goit.HttpError(w, http.StatusInternalServerError)
		return
	}

	file, err := commit.File(tpath)
	if errors.Is(err, object.ErrFileNotFound) {
		goit.HttpError(w, http.StatusNotFound)
		return
	} else if err != nil {
		util.PrintFuncError(err)
		goit.HttpError(w, http.StatusInternalServerError)
		return
	}

	data.Mode = util.ModeString(uint32(file.Mode))
	data.Path = file.Name
	data.Size = humanize.IBytes(uint64(file.Size))

	parts := strings.Split(file.Name, "/")
	htmlPath := "<b style=\"padding-left: 0.4rem;\"><a href=\"/" + repo.Name + "/tree\">" + repo.Name + "</a></b>/"
	dirPath := ""

	for i := 0; i < len(parts)-1; i += 1 {
		dirPath = path.Join(dirPath, parts[i])
		htmlPath += "<a href=\"/" + repo.Name + "/tree/" + dirPath + "\">" + parts[i] + "</a>/"
	}
	htmlPath += parts[len(parts)-1]

	data.PathHTML = template.HTML(htmlPath)

	ftype, err := goit.GetFileType(file)
	if err != nil {
		util.PrintFuncError(err)
		goit.HttpError(w, http.StatusInternalServerError)
		return
	}

	/* Only populate blines for text files */
	if strings.HasPrefix(ftype, "text") {
		rc, err := file.Blob.Reader()
		if err != nil {
			util.PrintFuncError(err)
			goit.HttpError(w, http.StatusInternalServerError)
			return
		}
		defer rc.Close()

		buf := make([]byte, min(file.Size, (10*1024*1024)))
		if _, err := rc.Read(buf); err != nil {
			util.PrintFuncError(err)
			goit.HttpError(w, http.StatusInternalServerError)
			return
		}

		body, _, err := Highlight(file.Name, string(buf), true)
		if err != nil {
			util.PrintFuncError(err)
			goit.HttpError(w, http.StatusInternalServerError)
			return
		}

		blame, err := git.Blame(commit, tpath)
		if err != nil {
			util.PrintFuncError(err)
			goit.HttpError(w, http.StatusInternalServerError)
			return
		}

		htmlLines := strings.Split(body, "\n")

		for i, bline := range blame.Lines {
			data.Blines = append(data.Blines, Bline{
				Hash:      bline.Hash.String(),
				ShortHash: bline.Hash.String()[:7],
				Author:    bline.AuthorName,
				Date:      bline.Date.Format("2006-01-02 15:04:05"),
				Line:      bline.Text,
			})

			if i < len(htmlLines) {
				htmlLines[i] = strings.TrimPrefix(htmlLines[i], "</span></span>")
				htmlLines[i] += "</span></span>"
				data.Blines[i].LineHTML = template.HTML(htmlLines[i])
			}
		}

		data.Blines = append(data.Blines, Bline{})
	}

	data.LineC = fmt.Sprint(len(data.Blines), " lines")

	if err := goit.Tmpl.ExecuteTemplate(w, "repo/blame", data); err != nil {
		util.PrintFuncError(err)
	}
}