Skip to content
This repository was archived by the owner on Sep 11, 2020. It is now read-only.

Panic when calling Patch.String #523

Closed
emersion opened this issue Jul 30, 2017 · 6 comments
Closed

Panic when calling Patch.String #523

emersion opened this issue Jul 30, 2017 · 6 comments

Comments

@emersion
Copy link

When calling Patch.String on commit emersion/matcha@cb9c85f, this happens:

runtime error: slice bounds out of range
panic(0x980180, 0xce8540)
	/usr/lib/go/src/runtime/panic.go:489 +0x2cf
gopkg.in/src-d/go-git.v4/plumbing/format/diff.(*hunksGenerator).processEqualsLines(0xc4201178b8, 0xc42015e2c0, 0x1, 0x2, 0x11)
	/home/simon/go/src/gopkg.in/src-d/go-git.v4/plumbing/format/diff/unified_encoder.go:268 +0x355
gopkg.in/src-d/go-git.v4/plumbing/format/diff.(*hunksGenerator).Generate(0xc4201178b8, 0xc420199000, 0x12, 0x20)
	/home/simon/go/src/gopkg.in/src-d/go-git.v4/plumbing/format/diff/unified_encoder.go:183 +0x455
gopkg.in/src-d/go-git.v4/plumbing/format/diff.(*UnifiedEncoder).encodeFilePatch(0xc420193080, 0xc42018f0e0, 0x2, 0x2, 0x1, 0xc420193080)
	/home/simon/go/src/gopkg.in/src-d/go-git.v4/plumbing/format/diff/unified_encoder.go:88 +0x1a8
gopkg.in/src-d/go-git.v4/plumbing/format/diff.(*UnifiedEncoder).Encode(0xc420193080, 0xcb7600, 0xc4201a2ed0, 0x9c9ba0, 0x7fcc01)
	/home/simon/go/src/gopkg.in/src-d/go-git.v4/plumbing/format/diff/unified_encoder.go:71 +0x97
gopkg.in/src-d/go-git.v4/plumbing/object.(*Patch).Encode(0xc4201a2ed0, 0xcb30c0, 0xc42019dd50, 0x0, 0xc420117a28)
	/home/simon/go/src/gopkg.in/src-d/go-git.v4/plumbing/object/patch.go:105 +0x81
gopkg.in/src-d/go-git.v4/plumbing/object.(*Patch).String(0xc4201a2ed0, 0xc4201c60e0, 0xc4201a2ed0)
	/home/simon/go/src/gopkg.in/src-d/go-git.v4/plumbing/object/patch.go:110 +0x7f
github.com/emersion/matcha.(*server).commit(0xc42018eb20, 0xcc2a80, 0xc4201ac150, 0xc4201aa20c, 0x28, 0xa, 0x0)
	/home/simon/go/src/github.com/emersion/matcha/matcha.go:476 +0x279
github.com/emersion/matcha.New.func8(0xcc2a80, 0xc4201ac150, 0xc420036b68, 0x476fb2)
	/home/simon/go/src/github.com/emersion/matcha/matcha.go:526 +0x8c
github.com/labstack/echo.(*Echo).add.func1(0xcc2a80, 0xc4201ac150, 0xcf7d20, 0x28)
	/home/simon/go/src/github.com/labstack/echo/echo.go:467 +0x90
github.com/labstack/echo/middleware.LoggerWithConfig.func2.1(0xcc2a80, 0xc4201ac150, 0x0, 0x0)
	/home/simon/go/src/github.com/labstack/echo/middleware/logger.go:111 +0x12b
github.com/labstack/echo.(*Echo).ServeHTTP.func1(0xcc2a80, 0xc4201ac150, 0xc4200f4bb8, 0xa12520)
	/home/simon/go/src/github.com/labstack/echo/echo.go:558 +0x10e
github.com/labstack/echo.(*Echo).ServeHTTP(0xc4200f4b60, 0xcba500, 0xc4201c6000, 0xc42006c100)
	/home/simon/go/src/github.com/labstack/echo/echo.go:567 +0x24c
net/http.serverHandler.ServeHTTP(0xc420019810, 0xcba500, 0xc4201c6000, 0xc42006c100)
	/usr/lib/go/src/net/http/server.go:2568 +0x92
net/http.(*conn).serve(0xc4201b2000, 0xcbadc0, 0xc4201aa0c0)
	/usr/lib/go/src/net/http/server.go:1825 +0x612
created by net/http.(*Server).Serve
	/usr/lib/go/src/net/http/server.go:2668 +0x2ce
@ajnavarro
Copy link
Contributor

ajnavarro commented Aug 2, 2017

Hi,

Using go-git master, I'm not able to reproduce this panic.

Here is the code:

package main

import (
	"fmt"

	"gopkg.in/src-d/go-git.v4"
	. "gopkg.in/src-d/go-git.v4/_examples"
	"gopkg.in/src-d/go-git.v4/plumbing"
)

func main() {
	url := "https://github.com/emersion/matcha.git"
	directory := "./test"

	r, err := git.PlainClone(directory, false, &git.CloneOptions{
		URL:               url,
		RecurseSubmodules: git.DefaultSubmoduleRecursionDepth,
	})

	CheckIfError(err)

	// ... retrieving the branch being pointed by HEAD
	ref, err := r.Head()
	CheckIfError(err)

	// ... retrieving the commit object
	commit, err := r.CommitObject(plumbing.NewHash("cb9c85f3b53438246a18499bcdf32ce4f6d7b468"))
	CheckIfError(err)

	head, err := r.CommitObject(ref.Hash())
	CheckIfError(err)

	p, err := commit.Patch(head)
	CheckIfError(err)
	fmt.Println(p.String())
}
And here is the result: (Click to expand)
/usr/local/go/bin/go run /home/antonio/work/src/gopkg.in/src-d/go-git.v4/_examples/clone/main.go
diff --git a/README.md b/README.md
index b0d4bdd35403f65b2188d622a43e13ac044654fc..30edbcbf337b6cbde8be6b12882eaca54f72033b 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +-1,16 @@
 # matcha
-A read-only web interface for Git repositories
+
+A simple read-only web interface for Git repositories.
+
+## Usage
+
+```sh
+go get -u github.com/emersion/matcha/cmd/matcha
+cd $GOPATH/src/github.com/emersion/matcha
+(cd public && npm install)
+matcha
+```
+
+## License
+
+MIT
diff --git a/matcha.go b/matcha.go
index 31428fff1e9c865590dc40d1eb6bdc14c8357d92..e496976ea042c275d000e59d854c21c87113a72b 100644
--- a/matcha.go
+++ b/matcha.go
@@ -2,111 +2,501 @@ package matcha
 
 import (
 	"html/template"
-	"io"
 	"net/http"
+	"path"
 	"path/filepath"
+	"sort"
+	"strings"
 
 	"github.com/labstack/echo"
+	"github.com/russross/blackfriday"
 	"gopkg.in/src-d/go-git.v4"
+	"gopkg.in/src-d/go-git.v4/plumbing"
+	"gopkg.in/src-d/go-git.v4/plumbing/filemode"
 	"gopkg.in/src-d/go-git.v4/plumbing/object"
+	"gopkg.in/src-d/go-git.v4/plumbing/storer"
 )
 
-type templateRenderer struct {
-	templates *template.Template
+type server struct {
+	dir string
+	r *git.Repository
+}
+
+type headerData struct {
+	RepoName string
 }
 
-func (r *templateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
-	return r.templates.ExecuteTemplate(w, name, data)
+func (s *server) headerData() *headerData {
+	return &headerData{
+		RepoName: filepath.Base(s.dir),
+	}
 }
 
-func New(e *echo.Echo, dir string) error {
-	dir, err := filepath.Abs(dir)
+func (s *server) commitFromRev(revName string) (*object.Commit, error) {
+	// First try to resolve a hash
+	commit, err := s.r.CommitObject(plumbing.NewHash(revName))
+	if err != plumbing.ErrObjectNotFound {
+		return commit, err
+	}
+
+	// Then a branch
+	ref, err := s.r.Reference(plumbing.ReferenceName("refs/heads/"+revName), true)
+	if err == nil {
+		return s.r.CommitObject(ref.Hash())
+	} else if err != plumbing.ErrReferenceNotFound {
+		return nil, err
+	}
+
+	// Finally a tag
+	ref, err = s.r.Reference(plumbing.ReferenceName("refs/tags/"+revName), true)
+	if err != nil {
+		return nil, err
+	}
+
+	tag, err := s.r.TagObject(ref.Hash())
 	if err != nil {
-		return err
+		return nil, err
 	}
 
-	t := template.Must(template.ParseGlob("public/views/*.html"))
-	e.Renderer = &templateRenderer{t}
+	return tag.Commit()
+}
 
-	r, err := git.PlainOpen(dir)
-	if err == git.ErrRepositoryNotExists {
-		return err //return c.String(http.StatusNotFound, "No such repository")
-	} else if err != nil {
-		return err
+func (s *server) lastCommits(from *object.Commit, patterns []string) ([]*object.Commit, error) {
+	last := make([]*object.Commit, len(patterns))
+	/*for i := range last {
+		last[i] = from
 	}
+	return last, nil*/
 
-	e.GET("/", func(c echo.Context) error {
-		ref, err := r.Head()
+	remaining := len(patterns)
+
+	commits, err := s.r.Log(&git.LogOptions{From: from.Hash})
+	if err != nil {
+		return nil, err
+	}
+
+	err = commits.ForEach(func(c *object.Commit) error {
+		ctree, err := c.Tree()
 		if err != nil {
 			return err
 		}
 
-		commit, err := r.CommitObject(ref.Hash())
+		parents := 0
+		err = c.Parents().ForEach(func(p *object.Commit) error {
+			parents++
+
+			ptree, err := p.Tree()
+			if err != nil {
+				return err
+			}
+
+			changes, err := ptree.Diff(ctree)
+			if err != nil {
+				return err
+			}
+
+			for _, change := range changes {
+				for i, pattern := range patterns {
+					if last[i] == nil && strings.HasPrefix(change.To.Name, pattern) {
+						last[i] = c
+						remaining--
+						if remaining == 0 {
+							return storer.ErrStop
+						}
+					}
+				}
+			}
+			return nil
+		})
 		if err != nil {
 			return err
 		}
 
-		tree, err := commit.Tree()
-		if err != nil {
-			return err
+		if parents == 0 {
+			for i, l := range last {
+				if l == nil {
+					last[i] = c
+					remaining--
+				}
+			}
 		}
 
-		var data struct{
-			RepoName string
-			Files []string
+		if remaining == 0 {
+			return storer.ErrStop
 		}
+		return nil
+	})
 
-		data.RepoName = filepath.Base(dir)
+	return last, err
+}
 
-		err = tree.Files().ForEach(func(f *object.File) error {
-			data.Files = append(data.Files, f.Name)
-			return nil
-		})
-		if err != nil {
+type treeEntry struct {
+	*object.TreeEntry
+	LastCommit *object.Commit
+}
+
+func (s *server) tree(c echo.Context, revName, p string) error {
+	commit, err := s.commitFromRev(revName)
+	if err == plumbing.ErrReferenceNotFound {
+		return c.String(http.StatusNotFound, "No such revision")
+	} else if err != nil {
+		return err
+	}
+
+	tree, err := commit.Tree()
+	if err != nil {
+		return err
+	}
+
+	if p == "" {
+		p = "/"
+	}
+	if p != "/" {
+		tree, err = tree.Tree(p)
+		if err == object.ErrDirectoryNotFound {
+			return c.String(http.StatusNotFound, "No such directory")
+		} else if err != nil {
 			return err
 		}
+	}
 
-		return c.Render(http.StatusOK, "tree-dir.html", data)
+	var data struct{
+		*headerData
+		Revision string
+		DirName, DirSep string
+		Parents []breadcumbItem
+		Entries []treeEntry
+		LastCommit *object.Commit
+		ReadMe template.HTML
+	}
+
+	data.headerData = s.headerData()
+	data.Revision = revName
+
+	sort.Slice(tree.Entries, func(i, j int) bool {
+		a, b := &tree.Entries[i], &tree.Entries[j]
+		if a.Mode & filemode.Dir != 0 {
+			return true
+		}
+		if b.Mode & filemode.Dir != 0 {
+			return false
+		}
+		return a.Name < b.Name
 	})
 
-	e.GET("/blob/:branch/*", func(c echo.Context) error {
-		if branch := c.Param("branch"); branch != "master" {
-			// TODO
-			return c.String(http.StatusNotFound, "No such branch")
+	patterns := make([]string, 0, len(tree.Entries) + 1)
+	pathPattern := p + "/"
+	if p == "/" {
+		pathPattern = ""
+	}
+	patterns = append(patterns, pathPattern)
+	for _, e := range tree.Entries {
+		pattern := e.Name
+		if p != "/" {
+			pattern = path.Join(p, pattern)
 		}
-		path := c.Param("*")
+		if e.Mode & filemode.Dir != 0 {
+			pattern += "/"
+		}
+		patterns = append(patterns, pattern)
+	}
+
+	lastCommits, err := s.lastCommits(commit, patterns)
+	if err != nil {
+		return err
+	}
+	for _, c := range lastCommits {
+		c.Message = cleanupCommitMessage(c.Message)
+	}
+
+	data.Entries = make([]treeEntry, len(tree.Entries))
+	data.LastCommit = lastCommits[0]
+	for i := range tree.Entries {
+		data.Entries[i] = treeEntry{&tree.Entries[i], lastCommits[i+1]}
+	}
 
-		ref, err := r.Head()
-		if err != nil {
-			return err
+	for _, e := range tree.Entries {
+		name := strings.TrimSuffix(e.Name, path.Ext(e.Name))
+		if strings.EqualFold(name, "README") && e.Mode & filemode.Regular != 0 {
+			f, err := tree.TreeEntryFile(&e)
+			if err != nil {
+				return err
+			}
+
+			raw, err := f.Contents()
+			if err != nil {
+				return err
+			}
+
+			rendered := blackfriday.MarkdownCommon([]byte(raw))
+			data.ReadMe = template.HTML(string(rendered))
+			break
 		}
+	}
 
-		commit, err := r.CommitObject(ref.Hash())
+	dirpath, filepath := path.Split(p)
+	data.DirName = filepath
+	data.Parents = pathBreadcumb(dirpath)
+
+	data.DirSep = "/"+p+"/"
+	if p == "/" {
+		data.DirSep = "/"
+	}
+
+	return c.Render(http.StatusOK, "tree.html", data)
+}
+
+func (s *server) blob(c echo.Context, revName, p string) error {
+	commit, err := s.commitFromRev(revName)
+	if err == plumbing.ErrReferenceNotFound {
+		return c.String(http.StatusNotFound, "No such revision")
+	} else if err != nil {
+		return err
+	}
+
+	f, err := commit.File(p)
+	if err == object.ErrFileNotFound {
+		return c.String(http.StatusNotFound, "No such file")
+	} else if err != nil {
+		return err
+	}
+
+	var data struct{
+		*headerData
+		Revision string
+		Filepath, Filename, Extension string
+		Parents []breadcumbItem
+		IsBinary bool
+		Rendered template.HTML
+		Contents string
+	}
+
+	data.headerData = s.headerData()
+	data.Revision = revName
+
+	dirpath, filename := path.Split(p)
+	data.Filepath = p
+	data.Filename = filename
+	data.Extension = strings.TrimLeft(path.Ext(p), ".")
+	data.Parents = pathBreadcumb(dirpath)
+
+	if f.Size > 1024*1024 {
+		data.IsBinary = true
+	} else if binary, err := f.IsBinary(); err != nil || binary {
+		data.IsBinary = true
+	}
+
+	if !data.IsBinary {
+		contents, err := f.Contents()
 		if err != nil {
 			return err
 		}
+		data.Contents = contents
 
-		f, err := commit.File(path)
-		if err == object.ErrFileNotFound {
-			return c.String(http.StatusNotFound, "No such file")
-		} else if err != nil {
-			return err
+		switch data.Extension {
+		case "md", "markdown":
+			rendered := blackfriday.MarkdownCommon([]byte(contents))
+			data.Rendered = template.HTML(string(rendered))
 		}
+	}
 
-		r, err := f.Reader()
+	return c.Render(http.StatusOK, "blob.html", data)
+}
+
+func (s *server) raw(c echo.Context, revName, p string) error {
+	commit, err := s.commitFromRev(revName)
+	if err == plumbing.ErrReferenceNotFound {
+		return c.String(http.StatusNotFound, "No such revision")
+	} else if err != nil {
+		return err
+	}
+
+	f, err := commit.File(p)
+	if err == object.ErrFileNotFound {
+		return c.String(http.StatusNotFound, "No such file")
+	} else if err != nil {
+		return err
+	}
+
+	r, err := f.Reader()
+	if err != nil {
+		return err
+	}
+	defer r.Close()
+
+	// TODO: autodetect file type
+	mediaType := "application/octet-stream"
+	if binary, err := f.IsBinary(); err == nil && !binary {
+		mediaType = "text/plain"
+	}
+
+	// TODO: set filename
+	return c.Stream(http.StatusOK, mediaType, r)
+}
+
+func (s *server) branches(c echo.Context) error {
+	branches, err := s.r.Branches()
+	if err != nil {
+		return err
+	}
+	defer branches.Close()
+
+	var data struct{
+		*headerData
+		Branches []string
+	}
+
+	data.headerData = s.headerData()
+
+	err = branches.ForEach(func(ref *plumbing.Reference) error {
+		data.Branches = append(data.Branches, ref.Name().Short())
+		return nil
+	})
+	if err != nil {
+		return err
+	}
+
+	return c.Render(http.StatusOK, "branches.html", data)
+}
+
+func (s *server) tags(c echo.Context) error {
+	tags, err := s.r.TagObjects()
+	if err != nil {
+		return err
+	}
+	defer tags.Close()
+
+	var data struct{
+		*headerData
+		Tags []*object.Tag
+	}
+
+	data.headerData = s.headerData()
+
+	err = tags.ForEach(func(t *object.Tag) error {
+		data.Tags = append(data.Tags, t)
+		return nil
+	})
+	if err != nil {
+		return err
+	}
+
+	return c.Render(http.StatusOK, "tags.html", data)
+}
+
+func (s *server) commits(c echo.Context, revName string) error {
+	commit, err := s.commitFromRev(revName)
+	if err == plumbing.ErrReferenceNotFound {
+		return c.String(http.StatusNotFound, "No such revision")
+	} else if err != nil {
+		return err
+	}
+
+	commits, err := s.r.Log(&git.LogOptions{From: commit.Hash})
+	if err != nil {
+		return err
+	}
+	defer commits.Close()
+
+	var data struct{
+		*headerData
+		Commits []*object.Commit
+	}
+
+	data.headerData = s.headerData()
+
+	err = commits.ForEach(func(c *object.Commit) error {
+		c.Message = cleanupCommitMessage(c.Message)
+
+		data.Commits = append(data.Commits, c)
+		return nil
+	})
+	if err != nil {
+		return err
+	}
+
+	return c.Render(http.StatusOK, "commits.html", data)
+}
+
+func (s *server) commit(c echo.Context, hash string) error {
+	commit, err := s.r.CommitObject(plumbing.NewHash(hash))
+	if err == plumbing.ErrObjectNotFound {
+		return c.String(http.StatusNotFound, "No such commit")
+	} else if err != nil {
+		return err
+	}
+
+	var data struct{
+		*headerData
+		Commit *object.Commit
+		Diff string
+	}
+
+	data.headerData = s.headerData()
+
+	commit.Message = cleanupCommitMessage(commit.Message)
+	data.Commit = commit
+
+	if len(commit.ParentHashes) > 0 {
+		// TODO
+		parent, err := s.r.CommitObject(commit.ParentHashes[0])
 		if err != nil {
 			return err
 		}
-		defer r.Close()
 
-		// TODO: autodetect file type
-		mediaType := "application/octet-stream"
-		if binary, err := f.IsBinary(); err == nil && !binary {
-			mediaType = "text/plain"
+		patch, err := parent.Patch(commit)
+		if err != nil {
+			return err
 		}
 
-		// TODO: set filename
-		return c.Stream(http.StatusOK, mediaType, r)
+		data.Diff = patch.String()
+	}
+
+	return c.Render(http.StatusOK, "commit.html", data)
+}
+
+func New(e *echo.Echo, dir string) error {
+	dir, err := filepath.Abs(dir)
+	if err != nil {
+		return err
+	}
+
+	e.Renderer, err = loadTemplateRenderer()
+	if err != nil {
+		return err
+	}
+
+	r, err := git.PlainOpen(dir)
+	if err == git.ErrRepositoryNotExists {
+		return err //return c.String(http.StatusNotFound, "No such repository")
+	} else if err != nil {
+		return err
+	}
+
+	s := &server{dir, r}
+
+	e.GET("/", func(c echo.Context) error {
+		return s.tree(c, "master", "/")
+	})
+	e.GET("/tree/:ref", func(c echo.Context) error {
+		return s.tree(c, c.Param("ref"), "")
+	})
+	e.GET("/tree/:ref/*", func(c echo.Context) error {
+		return s.tree(c, c.Param("ref"), c.Param("*"))
+	})
+	e.GET("/blob/:ref/*", func(c echo.Context) error {
+		return s.blob(c, c.Param("ref"), c.Param("*"))
+	})
+	e.GET("/raw/:ref/*", func(c echo.Context) error {
+		return s.raw(c, c.Param("ref"), c.Param("*"))
+	})
+	e.GET("/branches", s.branches)
+	e.GET("/tags", s.tags)
+	e.GET("/commits/:ref", func(c echo.Context) error {
+		return s.commits(c, c.Param("ref"))
+	})
+	e.GET("/commit/:hash", func(c echo.Context) error {
+		return s.commit(c, c.Param("hash"))
 	})
 
 	e.Static("/static", "public/node_modules")
diff --git a/public/package-lock.json b/public/package-lock.json
index 410c8347f8fda6486b6c9697bfd34890c2b3b682..f4d7eaafbd5bef0030ac1b026bae9ce25f34f051 100644
--- a/public/package-lock.json
+++ b/public/package-lock.json
@@ -4,6 +4,11 @@   "version": "0.0.0",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
+    "octicons": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/octicons/-/octicons-5.0.1.tgz",
+      "integrity": "sha1-D6DE/TaA0lEmbwc3NlOTF8uEbk8="
+    },
     "primer-alerts": {
       "version": "1.1.8",
       "resolved": "https://registry.npmjs.org/primer-alerts/-/primer-alerts-1.1.8.tgz",
diff --git a/public/package.json b/public/package.json
index 24908180a4a53fbd033b44be82efe87d2988b0c9..13de70375891d684eb77369857340b8e33af573c 100644
--- a/public/package.json
+++ b/public/package.json
@@ -3,6 +3,7 @@   "name": "matcha",
   "version": "0.0.0",
   "license": "MIT",
   "dependencies": {
+    "octicons": "^5.0.1",
     "primer-css": "^9.0.0"
   }
 }
diff --git a/public/views/blob.html b/public/views/blob.html
new file mode 100644
index 0000000000000000000000000000000000000000..48d80021baf860b41bd2eef9186a33de1fefd2f6
--- /dev/null
+++ b/public/views/blob.html
@@ -0,0 +1,34 @@
+{{template "header.html" .}}
+
+<nav>
+	<p>
+		<a href="/tree/{{$.Revision}}"><strong>{{.RepoName}}</strong></a>
+		{{range .Parents}} / <a href="/tree/{{$.Revision}}/{{.Path}}">{{.Name}}</a>{{end}}
+		/ <strong>{{.Filename}}</strong>
+	</p>
+</nav>
+
+<p>
+	<a href="/raw/{{$.Revision}}/{{.Filepath}}" class="btn btn-sm">Raw</a>
+</p>
+
+{{if .IsBinary}}
+	<div class="blankslate">
+		<h3>Cannot preview file</h3>
+		<p>You can download it by clicking on "Raw".</p>
+	</div>
+{{else}}
+	{{if .Rendered}}
+		<div class="Box Box--condensed">
+			<div class="Box-body markdown-body">
+				{{.Rendered}}
+			</div>
+		</div>
+	{{else}}
+		<div class="markdown-body">
+			<pre><code class="language-{{.Extension}}">{{.Contents}}</code></pre>
+		</div>
+	{{end}}
+{{end}}
+
+{{template "footer.html"}}
diff --git a/public/views/branches.html b/public/views/branches.html
new file mode 100644
index 0000000000000000000000000000000000000000..42b8453a7447c95c03c2b64d4a7138b21ab09fe1
--- /dev/null
+++ b/public/views/branches.html
@@ -0,0 +1,9 @@
+{{template "header.html" .}}
+
+<nav class="menu">
+	{{range .Branches}}
+		<a class="menu-item" href="/tree/{{.}}">{{.}}</a>
+	{{end}}
+</nav>
+
+{{template "footer.html"}}
diff --git a/public/views/commit.html b/public/views/commit.html
new file mode 100644
index 0000000000000000000000000000000000000000..3d2331be92b313b95bb69a5a00d9bb16115c5c6e
--- /dev/null
+++ b/public/views/commit.html
@@ -0,0 +1,22 @@
+{{template "header.html" .}}
+
+{{with .Commit}}
+	<div class="Box Box--blue">
+		<div class="Box-header">
+			<a class="btn btn-outline float-right" type="button" href="/tree/{{.Hash}}">Browse files</a>
+			<h3>{{.Message}}</h3>
+		</div>
+		<div class="Box-body">
+			<code class="float-right">{{.Hash}}</code>
+			<strong>{{.Author.Name}}</strong> commited {{date .Author.When}}
+		</div>
+	</div>
+{{end}}
+
+<p></p>
+
+<div class="markdown-body">
+	<pre><code class="language-diff">{{.Diff}}</code></pre>
+</div>
+
+{{template "footer.html"}}
diff --git a/public/views/commits.html b/public/views/commits.html
new file mode 100644
index 0000000000000000000000000000000000000000..52d914837854e31257f2e60d805406cb6bb1fe9d
--- /dev/null
+++ b/public/views/commits.html
@@ -0,0 +1,16 @@
+{{template "header.html" .}}
+
+<nav class="menu">
+	{{range .Commits}}
+		<div class="menu-item">
+			<span class="float-right">
+				<a href="/commit/{{.Hash}}" class="btn btn-outline">{{.Hash}}</a>
+				<a href="/tree/{{.Hash}}" class="btn btn-outline">{{icon "code"}}</a>
+			</span>
+			<strong>{{.Message}}</strong><br>
+			<small><strong>{{.Author.Name}}</strong> committed {{date .Author.When}}</small>
+		</div>
+	{{end}}
+</nav>
+
+{{template "footer.html"}}
diff --git a/public/views/footer.html b/public/views/footer.html
new file mode 100644
index 0000000000000000000000000000000000000000..6a44d9af2836f1681f9ae2809a094dc3142be1ff
--- /dev/null
+++ b/public/views/footer.html
@@ -0,0 +1,3 @@
+<p></p>
+
+{{template "foot.html"}}
diff --git a/public/views/head.html b/public/views/head.html
index abb7461ebce0c321d4768189e0416717323800a8..8c2dcfa421119cbc2b5f415b70c8b58e2860cb55 100644
--- a/public/views/head.html
+++ b/public/views/head.html
@@ -5,6 +5,13 @@ 		<meta charset="utf-8">
 		<title>Matcha</title>
 
 		<link rel="stylesheet" href="/static/primer-css/build/build.css">
+		<link rel="stylesheet" href="/static/primer-utilities/build/build.css">
+		<link rel="stylesheet" href="/static/octicons/build/octicons.min.css">
+
+		<!-- TODO: use local version of highlight.js, or switch to server-side rendering -->
+		<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/styles/default.min.css">
+		<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.12.0/highlight.min.js"></script>
+		<script>hljs.initHighlightingOnLoad();</script>
 	</head>
 	<body>
 		<div class="container">
diff --git a/public/views/header.html b/public/views/header.html
new file mode 100644
index 0000000000000000000000000000000000000000..4d8414de4d59f8e03fb2ce079f4c08308a3accc5
--- /dev/null
+++ b/public/views/header.html
@@ -0,0 +1,3 @@
+{{template "head.html"}}
+
+<h1><a href="/">{{.RepoName}}</a></h1>
diff --git a/public/views/tags.html b/public/views/tags.html
new file mode 100644
index 0000000000000000000000000000000000000000..f21bdd381fbabfd451bc4a52756e4c1e7e673739
--- /dev/null
+++ b/public/views/tags.html
@@ -0,0 +1,19 @@
+{{template "header.html" .}}
+
+<nav>
+	{{range .Tags}}
+		<div>
+			<span class="float-right">
+				<a href="/commit/{{.Target}}" class="btn btn-outline">{{.Target}}</a>
+				<a href="/tree/{{.Name}}" class="btn btn-outline">{{icon "code"}}</a>
+			</span>
+
+			<h3>{{.Name}}</h3>
+			<p><strong>{{.Tagger.Name}}</strong> tagged this {{date .Tagger.When}}</p>
+			<p>{{.Message}}</p>
+		</div>
+		<hr>
+	{{end}}
+</nav>
+
+{{template "footer.html"}}
diff --git a/public/views/tree-dir.html b/public/views/tree-dir.html
deleted file mode 100644
index 7b787783aad7425cb76b3e34ec77c75f4095af63..0000000000000000000000000000000000000000
--- a/public/views/tree-dir.html
+++ /dev/null
@@ -1,11 +0,0 @@
-{{template "head.html"}}
-
-<h1>{{.RepoName}}</h1>
-
-<nav class="menu">
-	{{range .Files}}
-	<a class="menu-item" href="/blob/master/{{.}}">{{.}}</a>
-	{{end}}
-</nav>
-
-{{template "foot.html"}}
diff --git a/public/views/tree.html b/public/views/tree.html
new file mode 100644
index 0000000000000000000000000000000000000000..36c522f7b34dd040fa0fd0f6adc556cd0afd8d61
--- /dev/null
+++ b/public/views/tree.html
@@ -0,0 +1,57 @@
+{{template "header.html" .}}
+
+{{if eq .DirName ""}}
+	<nav class="subnav" aria-label="Repository">
+		<a href="/commits/{{$.Revision}}" class="tabnav-tab">{{icon "git-commit"}} Commits</a>
+		<a href="/branches" class="tabnav-tab">{{icon "git-branch"}} Branches</a>
+		<a href="/tags" class="tabnav-tab">{{icon "tag"}} Tags</a>
+	</nav>
+{{else}}
+	<nav>
+		<p>
+			<a href="/tree/{{$.Revision}}"><strong>{{.RepoName}}</strong></a>
+			{{range .Parents}} / <a href="/tree/{{$.Revision}}/{{.Path}}">{{.Name}}</a>{{end}}
+			/ <strong>{{.DirName}}</strong>
+		</p>
+	</nav>
+{{end}}
+
+<div class="Box Box--condensed Box--blue">
+	{{with .LastCommit}}
+		<div class="Box-header">
+			<span class="text-gray float-right">{{date .Author.When}}</span>
+			<strong>{{.Author.Name}}</strong> <a href="/commit/{{.Hash}}" class="muted-link">{{.Message}}</a>
+		</div>
+	{{end}}
+	<ul>
+		{{range .Entries}}
+			<li class="Box-row">
+				{{with .LastCommit}}
+					<span class="text-gray float-right">{{date .Author.When}}</span>
+				{{end}}
+				{{if .Mode.ToOSFileMode.IsDir}}
+					<span class="text-blue">{{icon "file-directory"}}</span>&nbsp;<a href="/tree/{{$.Revision}}{{$.DirSep}}{{.Name}}">{{.Name}}</a>
+				{{else}}
+					<span class="text-blue">{{icon "file"}}</span>&nbsp;<a href="/blob/{{$.Revision}}{{$.DirSep}}{{.Name}}">{{.Name}}</a>
+				{{end}}
+				{{with .LastCommit}}
+					<a href="/commit/{{.Hash}}" class="muted-link">{{.Message}}</a>
+				{{end}}
+			</li>
+		{{end}}
+	</ul>
+</div>
+
+{{if .ReadMe}}
+	<p></p>
+	<div class="Box Box--condensed">
+		<div class="Box-header">
+			{{icon "book"}} README
+		</div>
+		<div class="Box-body markdown-body">
+			{{.ReadMe}}
+		</div>
+	</div>
+{{end}}
+
+{{template "footer.html"}}
diff --git a/template.go b/template.go
new file mode 100644
index 0000000000000000000000000000000000000000..6f497c943a9de0dcf5ba96277f24a2ad3aa6579e
--- /dev/null
+++ b/template.go
@@ -0,0 +1,103 @@
+package matcha
+
+import (
+	"bytes"
+	"fmt"
+	"html/template"
+	"io"
+	"path"
+	"strings"
+	"time"
+
+	"github.com/labstack/echo"
+	"github.com/shurcooL/octiconssvg"
+	nethtml "golang.org/x/net/html"
+)
+
+const pgpSigEndTag = "-----END PGP SIGNATURE-----"
+
+func cleanupCommitMessage(msg string) string {
+	if i := strings.Index(msg, pgpSigEndTag); i >= 0 {
+		msg = msg[i+len(pgpSigEndTag):]
+	}
+	return msg
+}
+
+type breadcumbItem struct {
+	Name string
+	Path string
+}
+
+func pathBreadcumb(p string) []breadcumbItem {
+	var breadcumb []breadcumbItem
+	if p := strings.Trim(p, "/"); p != "" {
+		names := strings.Split(p, "/")
+		breadcumb = make([]breadcumbItem, len(names))
+		for i, name := range names {
+			breadcumb[i] = breadcumbItem{
+				Name: name,
+				Path: path.Join(names[:i+1]...),
+			}
+		}
+	}
+	return breadcumb
+}
+
+func formatDuration(d time.Duration) string {
+	if d < time.Minute {
+		return fmt.Sprintf("%d seconds", d / time.Second)
+	}
+	if d < time.Hour {
+		return fmt.Sprintf("%d minutes", d / time.Minute)
+	}
+	if d < 24 * time.Hour {
+		return fmt.Sprintf("%d hours", d / time.Hour)
+	}
+	return fmt.Sprintf("%d days", d / (24 * time.Hour))
+}
+
+func formatDate(t time.Time, d time.Duration) string {
+	if d < 365 * 24 * time.Hour { // 1 year
+		return t.Format("Jan 2")
+	}
+	return t.Format("Jan 2, 2006")
+}
+
+type templateRenderer struct {
+	templates *template.Template
+}
+
+func (r *templateRenderer) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
+	return r.templates.ExecuteTemplate(w, name, data)
+}
+
+func loadTemplateRenderer() (echo.Renderer, error) {
+	funcs := template.FuncMap{
+		"icon": func(name string) template.HTML {
+			var b bytes.Buffer
+			nethtml.Render(&b, octiconssvg.Icon(name))
+			return template.HTML(b.String())
+		},
+		"date": func(t time.Time) template.HTML {
+			d := time.Since(t)
+
+			var s string
+			if d >= 0 && d < 30 * 24 * time.Hour { // 1 month
+				s = formatDuration(d) + " ago"
+			} else {
+				s = "on " + formatDate(t, d)
+			}
+
+			full := t.Format("Jan 02, 2006, 15:04 -0700")
+			s = `<relative-time datetime="`+t.Format(time.RFC3339)+`" title="`+full+`">`+s+`</relative-time>`
+			return template.HTML(s)
+		},
+	}
+
+	t, err := template.New("").Funcs(funcs).ParseGlob("public/views/*.html")
+	if err != nil {
+		return nil, err
+	}
+
+	return &templateRenderer{t}, nil
+}


Process finished with exit code 0

Can you check again using go-git master? thanks!

@mcuadros
Copy link
Contributor

Please reopen if you can replicate with master

@pnegahdar
Copy link

pnegahdar commented Feb 12, 2018

running into this issue as well, was wondering if there was a solution. Go-git(e9247/4.1)

@mcuadros
Copy link
Contributor

Can you provide a repository and code?

@jimdoescode
Copy link

jimdoescode commented Aug 4, 2018

@mcuadros Here's a repo (https://github.com/jimdoescode/git-snitch/) where this fails consistently for me. I'm using go-git at commit c17627c which was the HEAD commit of the v4 branch I got off gopkg.

panic: runtime error: slice bounds out of range

goroutine 1 [running]:
gopkg.in/src-d/go-git.v4/plumbing/format/diff.(*hunksGenerator).processEqualsLines(0xc4201a3c28, 0xc42021cb20, 0x1, 0x2, 0x17)
	/Users/jim/Programs/go/src/gopkg.in/src-d/go-git.v4/plumbing/format/diff/unified_encoder.go:268 +0x501
gopkg.in/src-d/go-git.v4/plumbing/format/diff.(*hunksGenerator).Generate(0xc4201a3c28, 0xc42020e200, 0x18, 0x20)
	/Users/jim/Programs/go/src/gopkg.in/src-d/go-git.v4/plumbing/format/diff/unified_encoder.go:183 +0x3a2
gopkg.in/src-d/go-git.v4/plumbing/format/diff.(*UnifiedEncoder).encodeFilePatch(0xc420102c80, 0xc420176510, 0x1, 0x1, 0xc4200c6501, 0xc420102c80)
	/Users/jim/Programs/go/src/gopkg.in/src-d/go-git.v4/plumbing/format/diff/unified_encoder.go:88 +0x1fd
gopkg.in/src-d/go-git.v4/plumbing/format/diff.(*UnifiedEncoder).Encode(0xc420102c80, 0x4617640, 0xc4201b8630, 0x4142701, 0xc4200f7b20)
	/Users/jim/Programs/go/src/gopkg.in/src-d/go-git.v4/plumbing/format/diff/unified_encoder.go:71 +0x65
gopkg.in/src-d/go-git.v4/plumbing/object.(*Patch).Encode(0xc4201b8630, 0x46150a0, 0xc4200f7b20, 0x0, 0xc4201a3d90)
	/Users/jim/Programs/go/src/gopkg.in/src-d/go-git.v4/plumbing/object/patch.go:105 +0x7c
gopkg.in/src-d/go-git.v4/plumbing/object.(*Patch).String(0xc4201b8630, 0xc42010c2a0, 0xc4201b8630)
	/Users/jim/Programs/go/src/gopkg.in/src-d/go-git.v4/plumbing/object/patch.go:110 +0x5a
main.NewDiff(0x45d5db2, 0x28, 0x45d5c9a, 0x28, 0x45c4cb6, 0x6, 0xc420071f78, 0x4006e7c, 0xc4200a4058)
	/Users/jim/Programs/go/src/github.com/jimdoescode/git-snitch/diff.go:46 +0x4df
main.main()
	/Users/jim/Programs/go/src/github.com/jimdoescode/git-snitch/main.go:10 +0x72

This script just diffs its own commits and spits out only the lines that changed. Oddly enough if you change the "from" hash passed to NewDiff to the initial commit (68fa16a021ff0956b6fe0d13f338116996cb0283) of the repo it doesn't panic. Using the hash of any subsequent commits as the "from" hash results in a panic.

@jimdoescode
Copy link

I can confirm that switching to the master branch does seem to fix the issue.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants