AuraRepo > Tree [f8c6941]
/git.go/
package aurarepo
import (
"bytes"
"errors"
"fmt"
"io"
"net/url"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gabriel-vasile/mimetype"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/format/diff"
"github.com/go-git/go-git/v5/plumbing/object"
sis "gitlab.com/sis-suite/smallnetinformationservices"
)
// Gets commit from the following references: branch name, commit hash, tag name, annotated tag hash
func (repo *Repo) getCommitFromReference(name string) (*object.Commit, string, error) {
// Test if branch first
ref, _ := repo.Reference(plumbing.NewBranchReferenceName(name), true)
if ref != nil {
commit, _ := repo.CommitObject(ref.Hash())
if commit != nil {
return commit, name, nil
}
}
// Then test if commit hash
commit, _ := repo.CommitObject(plumbing.NewHash(name))
if commit != nil {
return commit, commit.Hash.String()[:7], nil
}
// Then test if tag name (which references a commit)
tref, _ := repo.Reference(plumbing.NewTagReferenceName(name), true)
if tref != nil {
commit, _ := repo.CommitObject(tref.Hash())
if commit != nil {
return commit, name, nil
}
}
// Lastly, test if hash to annotated tag object
atag, _ := repo.TagObject(plumbing.NewHash(name))
if atag != nil {
commit, _ := repo.CommitObject(atag.Target)
if commit != nil {
return commit, atag.Name, nil
}
}
// Error out if none of the above worked.
return nil, "", errors.New("Invalid reference.")
}
func GetLatestTreeOfNotespace(repo *git.Repository, notespaceRef *plumbing.Reference) (*object.Tree, error) {
// Get last commit of notespace reference/"branch"
commit, err := repo.CommitObject(notespaceRef.Hash())
if commit == nil {
return nil, err
}
// Get tree of notespace to get all notes. Blob contents are the note contents. Filenames are the
// hashes of the commit the note was attached to.
return commit.Tree()
}
// Returns a map connecting the notespace to the note blob for the given commit.
func GetNotesOfCommit(repo *git.Repository, commit *object.Commit) (map[string]*object.Blob, error) {
var result = make(map[string]*object.Blob)
notespaces, _ := repo.Notes()
err := notespaces.ForEach(func(ref *plumbing.Reference) error {
tree, _ := GetLatestTreeOfNotespace(repo, ref)
if tree == nil {
return nil
}
// Go through each entry of tree, searching for the a filename that matches the commit hash we are looking for.
obj, _ := tree.FindEntry(commit.Hash.String())
if obj == nil {
return nil
}
blob, _ := repo.BlobObject(obj.Hash)
if blob == nil {
return nil
}
result[strings.TrimPrefix(ref.Name().Short(), "notes/")] = blob
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
// Returns default and docs branch refs
func (repo *Repo) GetImportantRefs() (def *plumbing.Reference, docs *plumbing.Reference, todo *plumbing.Reference, wiki *plumbing.Reference) {
potentialDefaultBranchNames := []string{"master", "main", "trunk"}
for _, def_name := range potentialDefaultBranchNames {
def, _ = repo.Reference(plumbing.NewBranchReferenceName(def_name), true)
if def != nil {
break
}
}
// Get commit of default branch
def_commit, _ := repo.CommitObject(def.Hash())
def_tree, _ := def_commit.Tree()
// Search for docs branch
var docs_tree *object.Tree
potentialDocNames := []string{"docs", "Docs", "documentation", "Documentation", "user-guide", "UserGuide", "User-Guide", "User-guide"}
for _, doc_name := range potentialDocNames {
docs, _ = repo.Reference(plumbing.NewBranchReferenceName(doc_name), true)
if docs != nil {
break
}
docs_tree, _ = def_tree.Tree(doc_name)
}
// If no docs branch, search for a subdirectory of the latest commit of the default branch.
// If found, create a new HashReference for it.
if docs == nil && docs_tree != nil {
docs = plumbing.NewHashReference(plumbing.ReferenceName(docs_tree.Hash.String()), docs_tree.Hash)
}
// Search for a todo branch
// NOTE: An example of a project that uses this is the git project itself!
var todo_tree *object.Tree
potentialTodoNames := []string{"todo"}
for _, todo_name := range potentialTodoNames {
todo, _ = repo.Reference(plumbing.NewBranchReferenceName(todo_name), true)
if todo != nil {
break
}
todo_tree, _ = def_tree.Tree(todo_name)
}
if todo == nil && todo_tree != nil {
todo = plumbing.NewHashReference(plumbing.ReferenceName(todo_tree.Hash.String()), todo_tree.Hash)
}
wiki, _ = repo.Reference(plumbing.NewBranchReferenceName("ar-wiki"), true)
return def, docs, todo, wiki
}
// ---------------------------------------------------------------
// NOTE: These functions assume they are used in routes attached to a RouteGroup
func (c *AuraRepoContext) GitRepoHomepage(request *sis.Request, repo *Repo) {
bref, docsref, todoref, wikiref := repo.GetImportantRefs()
if bref == nil {
request.TemporaryFailure("Couldn't find default branch.")
return
}
bcommit, _ := repo.CommitObject(bref.Hash())
if bcommit == nil {
request.TemporaryFailure("Couldn't find commit of default branch.")
return
}
tree, _ := bcommit.Tree()
if tree == nil {
request.TemporaryFailure("Couldn't find tree of default branch.")
return
}
request.Gemini(fmt.Sprintf("# %s\n\n", repo.Title))
request.Gemini(fmt.Sprintf("%s\n", repo.Description))
request.Link("/tags/", "🔖 Tags")
request.Link(fmt.Sprintf("/tree/%s", bref.Name().Short()), "🗎 File Tree")
request.Link("/branches/", "⌥ Branches")
request.Link("/notes/", "📝 Notespaces")
// If there is a "docs" or "documentation" branch (or directory), link to it.
if docsref != nil {
request.Link(fmt.Sprintf("/tree/%s", docsref.Name().Short()), "Documentation")
}
// If there is a "todo" branch, link to it.
if todoref != nil {
request.Link(fmt.Sprintf("/tree/%s", todoref.Name().Short()), "Todo")
}
// If there is an AuraRepo Wiki ("ar-wiki") branch, link to it.
if wikiref != nil {
request.Link("/wiki/", "Project Wiki")
}
request.Gemini("\n")
if c.http_clone_prefix != "" {
request.Link(fmt.Sprintf("%s/%s", c.http_clone_prefix, repo.Path), "Clone URL")
request.Gemini("\n")
}
// Show the five Latest Commits
request.Gemini("\n## Latest Commits\n\n")
showNum := 5
iterator, _ := repo.Log(&git.LogOptions{From: bcommit.Hash, Order: git.LogOrderCommitterTime})
i := 0
commit, _ := iterator.Next()
for commit != nil {
truncatedMessage, _, _ := strings.Cut(commit.Message, "\n")
if len(truncatedMessage) > 65 {
truncatedMessage = truncatedMessage[:65] + "..."
}
request.Link(fmt.Sprintf("/commit/%s", commit.Hash.String()), fmt.Sprintf("%s %s", commit.Committer.When.Format("2006-01-02"), truncatedMessage))
i += 1
if i >= showNum {
break
}
commit, _ = iterator.Next()
}
request.Link(fmt.Sprintf("/commits/%s", bref.Name().Short()), "More...")
request.Gemini("\n")
readmeNames := []string{"readme.gmi", "README.gmi", "readme.gemini", "README.gemini", "readme.txt", "README.txt", "readme", "README", "readme.md", "README.md"}
var readme *object.TreeEntry
for _, name := range readmeNames {
readme, _ = tree.FindEntry(name)
if readme != nil {
break
}
}
// TODO: Convert relative links in gmi/markdown documents to work with tree pathing
if readme != nil {
blob, _ := repo.BlobObject(readme.Hash)
if blob != nil {
reader, _ := blob.Reader()
contents, _ := io.ReadAll(reader)
ext := path.Ext(readme.Name)
if ext == "md" {
request.Markdown(string(contents))
} else if ext == "gmi" || ext == "gemini" {
request.Gemini(string(contents))
} else {
request.PlainText("%s", contents)
}
request.Gemini("\n")
}
}
// TODO: Always show the last commit details of the tree/directory
// Show README content of repo at root, or of directory
// Links to directories add to the filepath, links to files go to /:reponame/blob/:ref/* route
// At root of tree, show links to CONTRIBUTING, LICENSE, Makefile, COMPATIBLITY, EXTENDING, SECURITY, and CODE_OF_CONDUCT
}
// TODO: Order based on default, active, and stale
func (c *AuraRepoContext) GitRepoBranches(request *sis.Request, repo *Repo) {
request.Gemini(fmt.Sprintf("# %s > Branches\n\n", repo.Title))
request.Link("/", "Repo Home")
request.Gemini("\n")
branches, _ := repo.Branches()
branches.ForEach(func(branch *plumbing.Reference) error {
return request.Link((fmt.Sprintf("/tree/%s", branch.Name().Short())), branch.Name().Short())
})
}
// TODO: Paginate
func (c *AuraRepoContext) GitRepoTags(request *sis.Request, repo *Repo) {
request.Gemini(fmt.Sprintf("# %s > Tags\n\n", repo.Title))
request.Link("/", "Repo Home")
request.Gemini("\n")
tags, _ := repo.Tags()
tags.ForEach(func(tag *plumbing.Reference) error {
obj, err := repo.TagObject(tag.Hash())
if err != nil && !errors.Is(err, plumbing.ErrObjectNotFound) {
// Some other error
return err
} else if errors.Is(err, plumbing.ErrObjectNotFound) {
// Lightweight Tag
request.Link(fmt.Sprintf("/tree/%s", tag.Name().Short()), fmt.Sprintf("%s", tag.Name().Short())) // NOTE: Goes to tree of commit referenced by tag
return request.Gemini("\n")
} else {
// Annotated Tag
request.Link(fmt.Sprintf("/tree/%s", obj.Name), fmt.Sprintf("%s %s", obj.Tagger.When.Format("2006-01-02"), obj.Name)) // NOTE: Goes to tree of commit referenced by annotated tag
request.Link(fmt.Sprintf("mailto:%s", obj.Tagger.Email), fmt.Sprintf("Tagged by %s", obj.Tagger.Name))
return request.Gemini(fmt.Sprintf("```\n%s\n```\n", obj.Message))
}
})
tags.Close()
}
// Provides details of specific commit given a ref
func (c *AuraRepoContext) GitRepoCommitDetails(request *sis.Request, repo *Repo) {
reference := request.GetParam("ref")
if !plumbing.IsHash(reference) {
request.TemporaryFailure("Given reference not a hash.")
return
}
commit, err := repo.CommitObject(plumbing.NewHash(reference))
if errors.Is(err, plumbing.ErrObjectNotFound) {
request.TemporaryFailure("Commit hash object not found.")
return
}
request.Gemini(fmt.Sprintf("# %s > Commit [%s]\n\n", repo.Title, commit.Hash.String()[:7]))
// Show Author and Committer links
request.Link(fmt.Sprintf("mailto:%s", commit.Author.Email), commit.Author.Name)
if commit.Committer.Name != commit.Author.Name || commit.Committer.Email != commit.Author.Email {
request.Link(fmt.Sprintf("mailto:%s", commit.Committer.Email), fmt.Sprintf("Committer: %s", commit.Committer.Name))
}
// Show Commit Date
request.Gemini(commit.Committer.When.Format("Mon January 2, 2006 3:04 PM -0700") + "\n")
// Full Description
request.Gemini("```\n")
msg_str := strings.ReplaceAll(string(commit.Message), "\n```", "\n\\```")
request.Gemini(fmt.Sprintf("%s\n", msg_str))
// Get all notes for commit
notes, _ := GetNotesOfCommit(repo.Repository, commit)
// Show notes/commits note first, then show other notes
if note, exists := notes["commits"]; exists {
reader, _ := note.Reader()
content, err := io.ReadAll(reader)
if err == nil {
content_str := strings.ReplaceAll(string(content), "\n```", "\n\\```")
request.Gemini(fmt.Sprintf("Notes:\n %s\n", content_str))
}
}
// Show other notes
for notespace, note := range notes {
if notespace == "commits" {
continue
}
reader, _ := note.Reader()
content, err := io.ReadAll(reader)
if err != nil {
continue
}
content_str := strings.ReplaceAll(string(content), "\n```", "\n\\```")
request.Gemini(fmt.Sprintf("Notes (%s):\n %s\n", notespace, content_str))
}
// Commit Stats
request.Gemini("\n")
stats, _ := commit.Stats()
request.Gemini(fmt.Sprintf("%s\n", stats.String()))
request.Gemini("```\n")
// Extra commit details
request.Gemini(fmt.Sprintf("Commit Hash: %s\n", commit.Hash.String()))
request.Gemini(fmt.Sprintf("Tree Hash: %s\n", commit.TreeHash.String()))
request.Gemini(fmt.Sprintf("Date: %s\n", commit.Committer.When.Format(time.RFC3339))) // TODO: Author Date could be different from Comitter Date
if commit.PGPSignature != "" {
// TODO: PGP Verification
request.Gemini(fmt.Sprintf("PGP Signature: %s\n", commit.PGPSignature))
}
// TODO: Branches containing commit
// TODO: Show any tags that link to this commit
request.Gemini("\n")
// Link to file tree at commit
request.Link(fmt.Sprintf("/tree/%s", commit.Hash), "Browse Tree")
// Parent hash(es) that link to their commit details
// TODO: For merge commits, the first parent is always the branch merged on, while the second is the branch that was merged.
commit.Parents().ForEach(func(parent *object.Commit) error {
request.Link(fmt.Sprintf("/commit/%s", parent.Hash.String()), fmt.Sprintf("Parent %s", parent.Hash.String()[:7]))
return nil
})
request.Link("/commits/", "Commits") // TODO: This goes to default branch, but we should track what branch someone is viewing via query strings so that this will go to the right thing.
request.Link("/", "Repo Home")
// Other Links: Plain Diff Download, Patches Download, Add Tag on Commit, Cherry-pick, Revert Commit
// No Parents, or more than one parent, return
if commit.NumParents() == 0 || commit.NumParents() > 1 {
return
}
// Show patch details
// TODO: handle commits that have multiple parents
// TODO: For merge commits, the first parent is always the branch merged on, while the second is the branch that was merged.
request.Gemini("## Changes\n")
firstParent, _ := commit.Parent(0)
patch, _ := firstParent.Patch(commit)
for _, fp := range patch.FilePatches() {
from, to := fp.Files()
if from == nil {
// File created
request.Gemini(fmt.Sprintf("### %s (created)\n", to.Path()))
} else if to == nil {
// File deleted
request.Gemini(fmt.Sprintf("### %s (deleted)\n", from.Path()))
} else {
// File changed
request.Gemini(fmt.Sprintf("### %s\n", to.Path()))
}
if fp.IsBinary() {
request.Gemini("Binary content not displayed.\n")
continue
}
PrintFilePatchChunks(request, fp)
}
}
func (c *AuraRepoContext) GitRepoCommitsRedirect(request *sis.Request, repo *Repo) {
defaultBranch := "main"
bref, _ := repo.Reference(plumbing.NewBranchReferenceName("main"), true)
if bref == nil {
bref, _ = repo.Reference(plumbing.NewBranchReferenceName("master"), true)
defaultBranch = "master"
if bref == nil {
request.TemporaryFailure("Couldn't find default branch.")
return
}
}
request.Redirect("/commits/%s/%s", defaultBranch, request.GlobString)
}
// Goes to commits of branch name, commit ref, or tag name or annotated tag's hash
func (c *AuraRepoContext) GitRepoCommitsList(request *sis.Request, repo *Repo) {
reference := request.GetParam("ref") // TODO: If there's no reference, then default to default branch
query, _ := request.Query()
startInt, _ := strconv.Atoi(query)
// TODO: Use GlobString to specify a folder/file to get commits for. Use LogOptions.PathFilter field for this.
from_commit, refName, _ := repo.getCommitFromReference(reference)
if from_commit == nil {
request.NotFound("Invalid reference/hash.") // This shouldn't ever happen.
return
}
maxPerPage := 30
request.Gemini(fmt.Sprintf("# %s > Commits [%s]\n\n", repo.Title, refName))
request.Link(fmt.Sprintf("/"), "Repo Home")
if startInt > 0 {
i := max(startInt-maxPerPage, 0)
request.Link(fmt.Sprintf("/commits/%s?%d", reference, i), fmt.Sprintf("Previous %d commits", maxPerPage))
}
request.Gemini("\n")
iterator, _ := repo.Log(&git.LogOptions{From: from_commit.Hash, Order: git.LogOrderCommitterTime})
i := 0
commit, _ := iterator.Next()
for commit != nil {
if i < startInt {
i += 1
commit, _ = iterator.Next()
continue
}
truncatedMessage, _, _ := strings.Cut(commit.Message, "\n")
if len(truncatedMessage) > 65 {
truncatedMessage = truncatedMessage[:65] + "..."
}
request.Link(fmt.Sprintf("/commit/%s", commit.Hash.String()), fmt.Sprintf("%s %s", commit.Committer.When.Format("2006-01-02"), truncatedMessage))
request.Link(fmt.Sprintf("mailto:%s", commit.Author.Email), commit.Author.Name)
request.Gemini("\n")
i += 1
if i >= startInt+maxPerPage {
break
}
commit, _ = iterator.Next()
}
if commit != nil && commit.NumParents() > 0 {
request.Link(fmt.Sprintf("/commits/%s?%d", reference, i), fmt.Sprintf("Next %d commits", maxPerPage))
}
// Author(s), date of authorship
// PGP Verification
// Main link is to a specific commit's details
// Show the SHA of the commit
// Link to tree at commit
// Show commit description (truncated?)
// Organize by day
}
func (c *AuraRepoContext) GitRepoNotespaces(request *sis.Request, repo *Repo) {
request.Gemini(fmt.Sprintf("# %s > Notespaces\n\n", repo.Title))
request.Link("/", "Repo Home")
request.Gemini("\n")
notes, _ := repo.Notes() // Gets all references to notespaces, which point to the last commit of that notespace.
notes.ForEach(func(notespace *plumbing.Reference) error {
shorterName := strings.TrimPrefix(notespace.Name().Short(), "notes/")
request.Link(fmt.Sprintf("/notes/%s", shorterName), shorterName)
return nil
})
}
func (c *AuraRepoContext) GitRepoNotespaceNotes(request *sis.Request, repo *Repo) {
name := request.GetParam("name")
// Get notespace reference
ref, _ := repo.Reference(plumbing.NewNoteReferenceName(name), false)
if ref == nil {
request.TemporaryFailure("Cannot find notespace.")
return
}
tree, _ := GetLatestTreeOfNotespace(repo.Repository, ref)
if tree == nil {
request.TemporaryFailure("Cannot get tree of commit of notespace reference.")
return
}
request.Gemini(fmt.Sprintf("# %s > Notespace: %s\n\n", repo.Title, name))
request.Link("/notes/", "Notespaces")
// TODO: I could one day add a link to show all of the commits of the notespace, and show the notes from each of these commit trees.
request.Gemini("\n")
for _, entry := range tree.Entries {
// Get the commit that the note entry refers to (via filename)
commit, _ := repo.CommitObject(plumbing.NewHash(entry.Name))
if commit == nil {
continue
}
// Get the note's blob object and contents
blob, _ := repo.BlobObject(entry.Hash)
if blob == nil {
continue
}
truncatedMessage, _, _ := strings.Cut(commit.Message, "\n")
if len(truncatedMessage) > 65 {
truncatedMessage = truncatedMessage[:65] + "..."
}
reader, _ := blob.Reader()
contents, _ := io.ReadAll(reader)
request.Link(fmt.Sprintf("/commit/%s", commit.Hash), fmt.Sprintf("Commit: %s", truncatedMessage))
request.Gemini("```\n")
request.PlainText("%s", string(contents))
request.Gemini("```\n\n")
}
}
// Goes to file tree of branch name, commit ref, or tag name (or annotated tag's hash?)
func (c *AuraRepoContext) GitRepoTree(request *sis.Request, repo *Repo) {
reference := request.GetParam("ref")
repo.handleTreeFromReference(request, reference)
}
// Goes to file in a file tree of branch name, commit ref, or tag name (or annotated tag's hash?)
func (c *AuraRepoContext) GitRepoBlob(request *sis.Request, repo *Repo) {
reference := request.GetParam("ref")
repo.handleBlobFromTreeReference(request, reference, false)
// TODO: Always show the last commit details of the file
// Line of code count, file size
}
// Goes to blame of each line of a file in a file tree of branch name, commit ref, or tag name (or annotated tag's hash?)
func (c *AuraRepoContext) GitRepoBlame(request *sis.Request, repo *Repo) {
// reference := request.GetParam("ref")
// TODO: Always show the last commit details of the file
// Line of code count, file size
// Link to go back to containing directory
// Number of contributors listed at top
// Show line numbers, with truncated commit description and date of the line, its author, and the line's contents
// - If multiple lines use same commit, group them together so only one commit is shown
}
// Downloads raw file in a file tree of branch name, commit ref, or tag name (or annotated tag's hash?)
func (c *AuraRepoContext) GitRepoRaw(request *sis.Request, repo *Repo) {
reference := request.GetParam("ref")
repo.handleBlobFromTreeReference(request, reference, true)
}
func (c *AuraRepoContext) GitRepoArchiveHead(request *sis.Request, repo *Repo) {
}
func (c *AuraRepoContext) GitRepoArchiveTag(request *sis.Request, repo *Repo) {
}
func (c *AuraRepoContext) GitRepoWiki(request *sis.Request, repo *Repo) {
_, _, _, wiki := repo.GetImportantRefs()
if wiki == nil {
request.TemporaryFailure("Project does not have ar-wiki branch.")
return
}
wiki_latest, _ := repo.CommitObject(wiki.Hash())
tree, _ := wiki_latest.Tree()
if tree == nil {
request.TemporaryFailure("Cannot get tree of ar-wiki branch.")
return
}
requested_entry, _ := tree.FindEntry(request.GlobString)
if requested_entry == nil && request.GlobString != "" {
request.NotFound("Path not in wiki.")
return
} else if request.GlobString != "" && requested_entry.Mode.IsFile() {
// Get the file
file, _ := tree.TreeEntryFile(requested_entry)
if file == nil {
request.NotFound("File not in wiki.")
return
}
// Send over the file
reader, _ := file.Reader()
contentBytes, _ := io.ReadAll(reader)
newreader := bytes.NewReader(contentBytes)
request.DataStream(request.GlobString, newreader)
} else {
var directory *object.Tree
if request.GlobString == "" {
directory = tree
} else {
// Get the directory
directory, _ := tree.Tree(request.GlobString)
if directory == nil {
request.NotFound("Directory not in wiki.")
return
}
}
// Find index.gmi page
index_entry, _ := directory.FindEntry("index.gmi")
if index_entry != nil {
// Display the index file
index_file, _ := directory.TreeEntryFile(index_entry)
contents, _ := index_file.Contents()
request.Gemini(contents)
} else {
// Otherwise, show a directory listing page
request.Gemini(fmt.Sprintf("# /wiki/%s\n\n", request.GlobString))
for _, entry := range directory.Entries {
if !entry.Mode.IsFile() {
request.Link(path.Join("/wiki/", request.GlobString, url.PathEscape(entry.Name))+"/", entry.Name+"/")
} else {
request.Link(path.Join("/wiki/", request.GlobString, url.PathEscape(entry.Name)), entry.Name)
}
}
request.Gemini("\n")
}
}
}
/*
func (c *AuraRepoContext) RepoWikiUpload(request *sis.Request) {
repo := c.repos[request.GetParam("reponame")]
_, _, _, wiki := repo.GetImportantRefs()
if wiki == nil {
request.TemporaryFailure("Project does not have ar-wiki branch.")
return
}
wiki_latest, _ := repo.CommitObject(wiki.Hash())
latest_tree, _ := wiki_latest.Tree()
if latest_tree == nil {
request.TemporaryFailure("Cannot get tree of ar-wiki branch.")
return
}
uploadEntry := request.GlobString
uploadData, _ := request.GetUploadData()
modificationTime := time.Now().UTC()
// Build the file
obj := repo.Storer.NewEncodedObject()
obj.SetType(plumbing.BlobObject)
obj.SetSize(request.DataSize) // TODO
writer, err := obj.Writer()
io.Copy(writer, bytes.NewReader(uploadData))
ioutil.CheckClose(writer, &err)
fileHash, _ := repo.Storer.SetEncodedObject(obj)
// Build the tree by getting entries from latest_tree and replacing the hash of the file we are uploading
idx := &index.Index{}
for _, e := range latest_tree.Entries {
if e.Name != uploadEntry {
addedEntry := idx.Add(e.Name)
addedEntry.Hash = e.Hash
addedEntry.Mode = e.Mode
if e.Mode.IsFile() {
blob, _ := repo.BlobObject(e.Hash)
addedEntry.Size = uint32(blob.Size) // TODO: Why are these different types?
} else {
t, _ := repo.TreeObject(e.Hash)
}
} else {
addedEntry := idx.Add(uploadEntry)
addedEntry.Hash = fileHash
//addedEntry.Mode =
addedEntry.ModifiedAt = modificationTime
addedEntry.Size = uint32(obj.Size())
}
}
previousTree := wiki_latest.TreeHash
opts := &git.CommitOptions{}
opts.Parents = []plumbing.Hash{wiki_latest.Hash}
}
*/
// ------------------------------------------------------------
func (repo *Repo) handleTreeFromReference(request *sis.Request, name string) {
// Test if branch first
ref, _ := repo.Reference(plumbing.NewBranchReferenceName(name), true)
if ref != nil {
commit, _ := repo.CommitObject(ref.Hash())
if commit != nil {
tree, _ := commit.Tree()
if tree != nil {
repo.handleBranchTreeHeader(request, ref.Name().Short())
repo.handleTreeListing(request, tree)
return
} else {
request.TemporaryFailure("Cannot get tree of commit.")
return
}
} else {
request.TemporaryFailure("Cannot get commit of branch.")
}
}
// Then test if commit hash
commit, _ := repo.CommitObject(plumbing.NewHash(name))
if commit != nil {
tree, _ := commit.Tree()
if tree != nil {
repo.handleCommitTreeHeader(request, commit)
repo.handleTreeListing(request, tree)
return
} else {
request.TemporaryFailure("Cannot get tree of commit.")
return
}
}
// Then test if tag name (which references a commit)
tref, _ := repo.Reference(plumbing.NewTagReferenceName(name), true)
if tref != nil {
commit, _ := repo.CommitObject(tref.Hash())
if commit != nil {
tree, _ := commit.Tree()
if tree != nil {
repo.handleTagNameTreeHeader(request, name)
repo.handleTreeListing(request, tree)
return
} else {
request.TemporaryFailure("Cannot get tree of tag name.")
return
}
} else {
request.TemporaryFailure("Cannot get commit of tag name.")
return
}
}
// Lastly, test if hash to annotated tag object
atag, _ := repo.TagObject(plumbing.NewHash(name))
if atag != nil {
tree, _ := atag.Tree()
if tree != nil {
repo.handleTagObjectTreeHeader(request, atag)
repo.handleTreeListing(request, tree)
return
} else {
request.TemporaryFailure("Cannot get tree of annotated tag hash.")
return
}
}
// TODO: Add support for a regular tree object hash.
tree, _ := repo.TreeObject(plumbing.NewHash(name))
if tree != nil {
//tree.Tree(path string)
repo.handleTreeObjectTreeHeader(request, tree)
repo.handleTreeListing(request, tree)
return
}
// Error out if none of the above worked.
request.TemporaryFailure("Could not parse name/hash.")
}
func (repo *Repo) handleBlobFromTreeReference(request *sis.Request, name string, raw bool) {
// Test if branch first
ref, _ := repo.Reference(plumbing.NewBranchReferenceName(name), true)
if ref != nil {
commit, _ := repo.CommitObject(ref.Hash())
if commit != nil {
tree, _ := commit.Tree()
if tree != nil {
if !raw {
repo.handleBranchTreeHeader(request, ref.Name().Short())
}
repo.handleTreeBlob(request, tree, raw)
return
} else {
request.TemporaryFailure("Cannot get tree of commit.")
return
}
} else {
request.TemporaryFailure("Cannot get commit of branch.")
}
}
// Then test if commit hash
commit, _ := repo.CommitObject(plumbing.NewHash(name))
if commit != nil {
tree, _ := commit.Tree()
if tree != nil {
if !raw {
repo.handleCommitTreeHeader(request, commit)
}
repo.handleTreeBlob(request, tree, raw)
return
} else {
request.TemporaryFailure("Cannot get tree of commit.")
return
}
}
// Then test if tag name (which references a commit)
tref, _ := repo.Reference(plumbing.NewTagReferenceName(name), true)
if tref != nil {
commit, _ := repo.CommitObject(tref.Hash())
if commit != nil {
tree, _ := commit.Tree()
if tree != nil {
if !raw {
repo.handleTagNameTreeHeader(request, name)
}
repo.handleTreeBlob(request, tree, raw)
return
} else {
request.TemporaryFailure("Cannot get tree of tag name.")
return
}
} else {
request.TemporaryFailure("Cannot get commit of tag name.")
return
}
}
// Lastly, test if hash to annotated tag object
atag, _ := repo.TagObject(plumbing.NewHash(name))
if atag != nil {
tree, _ := atag.Tree()
if tree != nil {
if !raw {
repo.handleTagObjectTreeHeader(request, atag)
}
repo.handleTreeBlob(request, tree, raw)
return
} else {
request.TemporaryFailure("Cannot get tree of annotated tag hash.")
return
}
}
// TODO: Add support for a regular tree object hash.
// Error out if none of the above worked.
request.TemporaryFailure("Could not parse name/hash.")
}
func (repo *Repo) handleBranchTreeHeader(request *sis.Request, branchName string) {
//reference, err := context.repo.Reference(plumbing.NewBranchReferenceName(branch.Name), true)
request.Gemini(fmt.Sprintf("# %s > Tree [%s]\n", repo.Title, branchName))
}
func (repo *Repo) handleCommitTreeHeader(request *sis.Request, commit *object.Commit) {
request.Gemini(fmt.Sprintf("# %s > Tree [%s]\n", repo.Title, commit.Hash.String()[:7]))
}
func (repo *Repo) handleTagNameTreeHeader(request *sis.Request, tagName string) {
request.Gemini(fmt.Sprintf("# %s > Tree [%s]\n", repo.Title, tagName))
}
// Annotated Tags
func (repo *Repo) handleTagObjectTreeHeader(request *sis.Request, atag *object.Tag) {
request.Gemini(fmt.Sprintf("# %s > Tree [%s]\n", repo.Title, atag.Name))
}
func (repo *Repo) handleTreeObjectTreeHeader(request *sis.Request, tree *object.Tree) {
request.Gemini(fmt.Sprintf("# %s > Subtree [%s]\n", repo.Title, tree.Hash.String()[:7]))
}
// The actual file list
func (repo *Repo) handleTreeListing(request *sis.Request, root_tree *object.Tree) {
request.Gemini(fmt.Sprintf("/%s\n\n", request.GlobString)) // Current Path
if strings.HasSuffix(request.GlobString, "/") {
request.GlobString = strings.TrimSuffix(request.GlobString, "/")
}
var tree *object.Tree
if request.GlobString == "" {
tree = root_tree
// Add Repo Home link
request.Link("/", "..")
} else {
entry, _ := root_tree.FindEntry(request.GlobString)
if !entry.Mode.IsFile() {
subtree, _ := repo.TreeObject(entry.Hash)
if subtree != nil {
tree = subtree
} else {
// Error
panic(fmt.Sprintf("Tree object not found: %s", entry.Hash))
}
} else {
// Error
panic("Entry is a file.")
}
// Add ".." link
up_path := path.Clean(path.Join("/tree/", request.GetParam("ref"), request.GlobString, ".."))
request.Link(up_path, "..")
}
// TODO: Hack fix until I can fix SIS's GlobString escaping stuff
request.GlobString = strings.ReplaceAll(request.GlobString, " ", "%20")
for _, entry := range tree.Entries {
if !entry.Mode.IsFile() {
request.Link(path.Join("/tree/", request.GetParam("ref"), request.GlobString, url.PathEscape(entry.Name))+"/", entry.Name+"/")
} else {
request.Link(path.Join("/blob/", request.GetParam("ref"), request.GlobString, url.PathEscape(entry.Name))+"/", entry.Name)
}
}
// TODO: Always show the last commit details of the tree/directory
// Link to commits of ref
// Show README content of repo at root, or of directory
// Links to directories add to the filepath, links to files go to /:reponame/blob/:ref/* route
// At root of tree, show links to CONTRIBUTING, LICENSE, Makefile, COMPATIBLITY, EXTENDING, SECURITY, and CODE_OF_CONDUCT
}
func (repo *Repo) handleTreeBlob(request *sis.Request, root_tree *object.Tree, raw bool) {
if !raw {
request.Gemini(fmt.Sprintf("/%s\n\n", request.GlobString)) // Current Path
}
if strings.HasSuffix(request.GlobString, "/") {
request.GlobString = strings.TrimSuffix(request.GlobString, "/")
}
directory := path.Dir(request.GlobString)
var tree *object.Tree
if directory == "." || directory == "" || directory == "/" {
tree = root_tree
} else {
entry, _ := root_tree.FindEntry(directory)
if !entry.Mode.IsFile() {
subtree, _ := repo.TreeObject(entry.Hash)
if subtree != nil {
tree = subtree
} else {
// Error
panic(fmt.Sprintf("Tree object not found: %s", entry.Hash))
}
} else {
// Error
panic("Entry is a file.")
}
}
filename, _ := url.PathUnescape(path.Base(request.GlobString))
entry, _ := tree.FindEntry(filename)
if entry == nil {
request.Gemini("Blob not found.")
return
}
blob, _ := repo.BlobObject(entry.Hash)
if blob == nil {
request.Gemini("Blob not found.")
return
}
reader, _ := blob.Reader()
content, _ := io.ReadAll(reader)
mt := ""
extension := filepath.Ext(entry.Name)
mt = "text/plain"
if extension == ".gmi" || extension == ".gemini" {
mt = "text/gemini"
} else if extension == ".scroll" {
mt = "text/scroll"
} else if entry.Name == "index" || extension == ".nex" {
mt = "text/nex" // TODO: Assume nex for this for now, but come up with a better way later.
} else if strings.HasSuffix(entry.Name, "gophermap") {
mt = "application/gopher-menu"
} else if extension == ".pem" {
mt = "application/x-pem-file"
} else if extension == ".md" {
mt = "text/markdown"
} else {
mime := mimetype.Detect(content)
mt = mime.String()
}
// TODO: Hack fix until I can fix SIS's GlobString escaping stuff
// glob := strings.ReplaceAll(request.GlobString, " ", "%20")
dirPath, _ := url.JoinPath("/tree", request.GetParam("ref"), directory)
rawPath, _ := url.JoinPath("/raw", request.GetParam("ref"), request.GlobString)
if raw {
// Get Mimetype stuff
request.Bytes(mt, content)
} else if strings.HasPrefix(mt, "text/") {
request.Link(dirPath, "..")
request.Link(rawPath, "View Raw")
if mt == "text/gemini" {
request.Gemini(string(content))
} else if mt == "text/scroll" {
request.Scroll(string(content))
} else if mt == "text/markdown" {
request.Markdown(string(content))
} else if mt == "text/nex" {
request.NexListing(string(content))
} else if mt == "application/gopher-menu" {
request.Gophermap(string(content))
} else {
request.Gemini("```\n")
request.TextWithMimetype(mt, string(content))
request.Gemini("```\n")
}
} else {
request.Link(dirPath, "..")
request.Link(rawPath, "View Raw")
//request.Link(rawPath, "View Raw")
request.Gemini("Binary file not shown.")
}
}
func PrintFilePatchChunks(request *sis.Request, fp diff.FilePatch) {
request.Gemini("```\n")
fromLine := 1
toLine := 1
previousChunkLastFiveLines := ""
printNextFiveLinesOfEqualChunk := false
for _, chunk := range fp.Chunks() {
if chunk.Type() == diff.Equal {
if printNextFiveLinesOfEqualChunk {
PrintIndentedLinesFromNumber(request, GetFirstFiveLinesOfString(chunk.Content()), fromLine, diff.Equal)
printNextFiveLinesOfEqualChunk = false
}
fromLine += strings.Count(chunk.Content(), "\n")
toLine += strings.Count(chunk.Content(), "\n")
previousChunkLastFiveLines = GetLastFiveLinesOfString(chunk.Content())
continue
}
printNextFiveLinesOfEqualChunk = false
if chunk.Type() == diff.Add {
if previousChunkLastFiveLines != "" {
request.PlainText(" %5s | ...\n", "...")
PrintIndentedLinesFromNumber(request, previousChunkLastFiveLines, fromLine-5, diff.Equal)
}
PrintIndentedLinesFromNumber(request, chunk.Content(), toLine, diff.Add)
toLine += strings.Count(chunk.Content(), "\n")
printNextFiveLinesOfEqualChunk = true
} else if chunk.Type() == diff.Delete {
if previousChunkLastFiveLines != "" {
request.PlainText(" %5s | ...\n", "...")
PrintIndentedLinesFromNumber(request, previousChunkLastFiveLines, fromLine-5, diff.Equal)
}
PrintIndentedLinesFromNumber(request, chunk.Content(), fromLine, diff.Delete)
fromLine += strings.Count(chunk.Content(), "\n")
printNextFiveLinesOfEqualChunk = true
}
previousChunkLastFiveLines = ""
}
request.Gemini("```\n")
}