AuraRepo > Tree [f8c6941]

/git.go/

..
View Raw
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")
}