diff --git a/pkg/ext/mime.go b/pkg/ext/mime.go
index 6da66e3db03709439e1a8731116081dd5fe738bb..c42d4de563775a21af2511f6b2dc18562f62626a 100644
--- a/pkg/ext/mime.go
+++ b/pkg/ext/mime.go
@@ -5,7 +5,8 @@
type ContentType = string
const (
- TextHTML ContentType = "text/html"
+ TextHTML ContentType = "text/html"
+ ApplicationGZip ContentType = "application/gzip"
)
func Html(next func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) {
@@ -14,9 +15,17 @@ next(w, r)
}
}
+func SetFileName(w http.ResponseWriter, name string) {
+ h := "inline; filename=\"" + name + "\""
+ w.Header().Add("Content-Disposition", h)
+}
+
func SetHTML(w http.ResponseWriter) {
SetMIME(w, TextHTML)
+}
+func SetGZip(w http.ResponseWriter) {
+ SetMIME(w, ApplicationGZip)
}
func SetMIME(w http.ResponseWriter, mime ContentType) {
diff --git a/pkg/git/git.go b/pkg/git/git.go
index b725cd875a2615e043e4b15cad5b6bfc98860a85..591fafb069b141c9e7089f6b6982349b396feef4 100644
--- a/pkg/git/git.go
+++ b/pkg/git/git.go
@@ -1,9 +1,13 @@
package git
import (
+ "archive/tar"
"errors"
"fmt"
"io"
+ "io/fs"
+ "path"
+ "time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
@@ -25,6 +29,13 @@
ref plumbing.Hash
// this is setRef when ref is setRef
setRef bool
+ }
+ infoWrapper struct {
+ name string
+ size int64
+ mode fs.FileMode
+ modTime time.Time
+ isDir bool
}
)
@@ -213,3 +224,110 @@ } else {
return "Binary file", nil
}
}
+
+func (g *GitRepository) WriteTar(w io.Writer, prefix string) error {
+ tw := tar.NewWriter(w)
+ defer tw.Close()
+
+ tree, err := g.Tree("")
+ if err != nil {
+ return err
+ }
+
+ walker := object.NewTreeWalker(tree, true, nil)
+ defer walker.Close()
+
+ name, entry, err := walker.Next()
+ for ; err == nil; name, entry, err = walker.Next() {
+ info, err := newInfoWrapper(name, prefix, &entry, tree)
+ if err != nil {
+ return err
+ }
+
+ header, err := tar.FileInfoHeader(info, "")
+ if err != nil {
+ return err
+ }
+
+ err = tw.WriteHeader(header)
+ if err != nil {
+ return err
+ }
+
+ if !info.IsDir() {
+ c, err := g.FileContent(name)
+ if err != nil {
+ return err
+ }
+
+ _, err = tw.Write([]byte(c))
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+func newInfoWrapper(
+ filename string,
+ prefix string,
+ entry *object.TreeEntry,
+ tree *object.Tree,
+) (*infoWrapper, error) {
+ var (
+ size int64
+ mode fs.FileMode
+ isDir bool
+ )
+
+ if entry.Mode.IsFile() {
+ file, err := tree.TreeEntryFile(entry)
+ if err != nil {
+ return nil, err
+ }
+ mode = fs.FileMode(file.Mode)
+
+ size, err = tree.Size(filename)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ isDir = true
+ mode = fs.ModeDir | fs.ModePerm
+ }
+
+ fullname := path.Join(prefix, filename)
+ return &infoWrapper{
+ name: fullname,
+ size: size,
+ mode: mode,
+ modTime: time.Unix(0, 0),
+ isDir: isDir,
+ }, nil
+}
+
+func (i *infoWrapper) Name() string {
+ return i.name
+}
+
+func (i *infoWrapper) Size() int64 {
+ return i.size
+}
+
+func (i *infoWrapper) Mode() fs.FileMode {
+ return i.mode
+}
+
+func (i *infoWrapper) ModTime() time.Time {
+ return i.modTime
+}
+
+func (i *infoWrapper) IsDir() bool {
+ return i.isDir
+}
+
+func (i *infoWrapper) Sys() any {
+ return nil
+}
diff --git a/pkg/handler/git/handler.go b/pkg/handler/git/handler.go
index 25505ba4c8ebfc520f69f9e21b87eef41752cd66..aed991747bb2e3d5054d2a0921321de6388f48d9 100644
--- a/pkg/handler/git/handler.go
+++ b/pkg/handler/git/handler.go
@@ -2,10 +2,13 @@ package git
import (
"bytes"
+ "fmt"
"io"
+ "log/slog"
"net/http"
"os"
"path/filepath"
+ "strings"
"git.gabrielgio.me/cerrado/pkg/ext"
"git.gabrielgio.me/cerrado/pkg/service"
@@ -36,6 +39,7 @@ GetFileContent(name, ref, path string) (string, error)
GetAbout(name string) (string, error)
ListTags(name string) ([]*plumbing.Reference, error)
ListBranches(name string) ([]*plumbing.Reference, error)
+ WriteTarGZip(w io.Writer, name, ref, filename string) error
}
configurationRepository interface {
@@ -81,6 +85,38 @@ Respositories: repos,
About: bs,
}
templates.WritePageTemplate(w, gitList)
+ return nil
+}
+
+func (g *GitHandler) Archive(w http.ResponseWriter, r *http.Request) error {
+ ext.SetGZip(w)
+ name := r.PathValue("name")
+ refs := r.PathValue("refs")
+ ref := strings.TrimSuffix(refs, ".tar.gz")
+
+ // TODO: remove it once we can support more than gzip
+ if !strings.HasSuffix(refs, ".tar.gz") {
+ ext.NotFound(w)
+ return nil
+ }
+
+ filenameWithExt := fmt.Sprintf("%s-%s.tar.gz", name, ref)
+ ext.SetFileName(w, filenameWithExt)
+ filename := fmt.Sprintf("%s-%s", name, ref)
+
+ // writing to a buffer so we can run all the process before writing error
+ var buf bytes.Buffer
+ err := g.gitService.WriteTarGZip(&buf, name, ref, filename)
+ if err != nil {
+ return err
+ }
+
+ // since that has write to w it cannot return a error.
+ _, err = io.Copy(w, &buf)
+ if err != nil {
+ slog.Error("Error copying buffer", "error", err)
+ }
+
return nil
}
diff --git a/pkg/handler/router.go b/pkg/handler/router.go
index c8f89840effc9263911f7afc73b02722436b36f9..2293ab662f2e92a23bc3d3c953413ad00489cd58 100644
--- a/pkg/handler/router.go
+++ b/pkg/handler/router.go
@@ -41,6 +41,7 @@ mux.HandleFunc("/{name}/refs/{$}", gitHandler.Refs)
mux.HandleFunc("/{name}/tree/{ref}/{rest...}", gitHandler.Tree)
mux.HandleFunc("/{name}/blob/{ref}/{rest...}", gitHandler.Blob)
mux.HandleFunc("/{name}/log/{ref}/", gitHandler.Log)
+ mux.HandleFunc("/{name}/archive/{refs...}", gitHandler.Archive)
mux.HandleFunc("/config", configHandler)
mux.HandleFunc("/about", aboutHandler.About)
mux.HandleFunc("/", gitHandler.List)
diff --git a/pkg/service/git.go b/pkg/service/git.go
index 6bb6e9ebb7739ae025c124766ac402d19c429aad..cbee90a375721e07a0efea8b2653aa9a711d87ed 100644
--- a/pkg/service/git.go
+++ b/pkg/service/git.go
@@ -1,7 +1,9 @@
package service
import (
+ "compress/gzip"
"errors"
+ "io"
"log/slog"
"git.gabrielgio.me/cerrado/pkg/config"
@@ -90,6 +92,29 @@ if err != nil {
return nil, err
}
return repo.Commits(count)
+}
+
+func (g *GitService) WriteTarGZip(w io.Writer, name, ref string, filename string) error {
+ r := g.configRepo.GetByName(name)
+ if r == nil {
+ return RepositoryNotFoundErr
+ }
+
+ repo, err := git.OpenRepository(r.Path)
+ if err != nil {
+ return err
+ }
+
+ err = repo.SetRef(ref)
+ if err != nil {
+ return err
+ }
+
+ gw := gzip.NewWriter(w)
+ defer gw.Close()
+
+ return repo.WriteTar(gw, filename)
+
}
func (g *GitService) GetTree(name, ref, path string) (*object.Tree, error) {
diff --git a/templates/gititemrefs.qtpl b/templates/gititemrefs.qtpl
index ff1561b7626970689ef318510c4610dbf20d5623..624408297c9680822ebf314fd7fb955e823ab606 100644
--- a/templates/gititemrefs.qtpl
+++ b/templates/gititemrefs.qtpl
@@ -21,6 +21,7 @@ {%s t.Name().Short() %}
</div>
<div class="col-8">
<div class="float-end">
+ <a href="/{%s name %}/archive/{%s t.Name().Short() %}.tar.gz">tar.gz</a>
<a href="/{%s name %}/tree/{%s t.Name().Short() %}/">tree</a>
<a href="/{%s name %}/log/{%s t.Name().Short() %}/">log</a>
</div>
@@ -41,6 +42,7 @@ {%s b.Name().Short() %}
</div>
<div class="col-8">
<div class="float-end">
+ <a href="/{%s name %}/archive/{%s b.Name().Short() %}.tar.gz">tar.gz</a>
<a href="/{%s name %}/tree/{%s b.Name().Short() %}/">tree</a>
<a href="/{%s name %}/log/{%s b.Name().Short() %}/">log</a>
</div>
diff --git a/templates/gititemrefs.qtpl.go b/templates/gititemrefs.qtpl.go
index b00736ecb57ca6d0fca34676cb9e455591c92d7a..da9bfe757f4d6bc91fb3d53c65dc2cd0faa36074 100644
--- a/templates/gititemrefs.qtpl.go
+++ b/templates/gititemrefs.qtpl.go
@@ -90,113 +90,131 @@ <a href="/`)
//line gititemrefs.qtpl:24
qw422016.E().S(name)
//line gititemrefs.qtpl:24
- qw422016.N().S(`/tree/`)
+ qw422016.N().S(`/archive/`)
//line gititemrefs.qtpl:24
qw422016.E().S(t.Name().Short())
//line gititemrefs.qtpl:24
- qw422016.N().S(`/">tree</a>
+ qw422016.N().S(`.tar.gz">tar.gz</a>
<a href="/`)
//line gititemrefs.qtpl:25
qw422016.E().S(name)
//line gititemrefs.qtpl:25
- qw422016.N().S(`/log/`)
+ qw422016.N().S(`/tree/`)
//line gititemrefs.qtpl:25
qw422016.E().S(t.Name().Short())
//line gititemrefs.qtpl:25
+ qw422016.N().S(`/">tree</a>
+ <a href="/`)
+//line gititemrefs.qtpl:26
+ qw422016.E().S(name)
+//line gititemrefs.qtpl:26
+ qw422016.N().S(`/log/`)
+//line gititemrefs.qtpl:26
+ qw422016.E().S(t.Name().Short())
+//line gititemrefs.qtpl:26
qw422016.N().S(`/">log</a>
</div>
</div>
</div>
`)
-//line gititemrefs.qtpl:29
+//line gititemrefs.qtpl:30
}
-//line gititemrefs.qtpl:29
+//line gititemrefs.qtpl:30
qw422016.N().S(`
</div>
`)
-//line gititemrefs.qtpl:31
+//line gititemrefs.qtpl:32
} else {
-//line gititemrefs.qtpl:31
+//line gititemrefs.qtpl:32
qw422016.N().S(`
<p> No tags </p>
`)
-//line gititemrefs.qtpl:33
+//line gititemrefs.qtpl:34
}
-//line gititemrefs.qtpl:33
+//line gititemrefs.qtpl:34
qw422016.N().S(`
</div>
<div class="col-md-4">
<div class="event-list">
`)
-//line gititemrefs.qtpl:37
+//line gititemrefs.qtpl:38
for _, b := range g.Branches {
-//line gititemrefs.qtpl:37
+//line gititemrefs.qtpl:38
qw422016.N().S(`
<div class="row event">
<div class="col-4">
`)
-//line gititemrefs.qtpl:40
+//line gititemrefs.qtpl:41
qw422016.E().S(b.Name().Short())
-//line gititemrefs.qtpl:40
+//line gititemrefs.qtpl:41
qw422016.N().S(`
</div>
<div class="col-8">
<div class="float-end">
<a href="/`)
-//line gititemrefs.qtpl:44
+//line gititemrefs.qtpl:45
qw422016.E().S(name)
-//line gititemrefs.qtpl:44
+//line gititemrefs.qtpl:45
+ qw422016.N().S(`/archive/`)
+//line gititemrefs.qtpl:45
+ qw422016.E().S(b.Name().Short())
+//line gititemrefs.qtpl:45
+ qw422016.N().S(`.tar.gz">tar.gz</a>
+ <a href="/`)
+//line gititemrefs.qtpl:46
+ qw422016.E().S(name)
+//line gititemrefs.qtpl:46
qw422016.N().S(`/tree/`)
-//line gititemrefs.qtpl:44
+//line gititemrefs.qtpl:46
qw422016.E().S(b.Name().Short())
-//line gititemrefs.qtpl:44
+//line gititemrefs.qtpl:46
qw422016.N().S(`/">tree</a>
<a href="/`)
-//line gititemrefs.qtpl:45
+//line gititemrefs.qtpl:47
qw422016.E().S(name)
-//line gititemrefs.qtpl:45
+//line gititemrefs.qtpl:47
qw422016.N().S(`/log/`)
-//line gititemrefs.qtpl:45
+//line gititemrefs.qtpl:47
qw422016.E().S(b.Name().Short())
-//line gititemrefs.qtpl:45
+//line gititemrefs.qtpl:47
qw422016.N().S(`/">log</a>
</div>
</div>
</div>
`)
-//line gititemrefs.qtpl:49
+//line gititemrefs.qtpl:51
}
-//line gititemrefs.qtpl:49
+//line gititemrefs.qtpl:51
qw422016.N().S(`
</div>
</div>
</div>
`)
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
}
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
func (g *GitItemRefsPage) WriteGitContent(qq422016 qtio422016.Writer, name, ref string) {
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
qw422016 := qt422016.AcquireWriter(qq422016)
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
g.StreamGitContent(qw422016, name, ref)
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
qt422016.ReleaseWriter(qw422016)
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
}
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
func (g *GitItemRefsPage) GitContent(name, ref string) string {
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
qb422016 := qt422016.AcquireByteBuffer()
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
g.WriteGitContent(qb422016, name, ref)
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
qs422016 := string(qb422016.B)
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
qt422016.ReleaseByteBuffer(qb422016)
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
return qs422016
-//line gititemrefs.qtpl:53
+//line gititemrefs.qtpl:55
}
diff --git a/templates/gititemsummary.qtpl b/templates/gititemsummary.qtpl
index e3d3a0828874c51b78a70cc172d34a7fada73a1e..06a785ad74913ce89927a32daff49b18529e9159 100644
--- a/templates/gititemsummary.qtpl
+++ b/templates/gititemsummary.qtpl
@@ -23,6 +23,7 @@ {%s t.Name().Short() %}
</div>
<div class="col-8">
<div class="float-end">
+ <a href="/{%s name %}/archive/{%s t.Name().Short() %}.tar.gz">tar.gz</a>
<a href="/{%s name %}/tree/{%s t.Name().Short() %}/">tree</a>
<a href="/{%s name %}/log/{%s t.Name().Short() %}/">log</a>
</div>
diff --git a/templates/gititemsummary.qtpl.go b/templates/gititemsummary.qtpl.go
index ad025f7e8a8343850615d1c5e27c1f2cbf89d68c..4e48efd0da70a6301114cfd65ef9929f96804953 100644
--- a/templates/gititemsummary.qtpl.go
+++ b/templates/gititemsummary.qtpl.go
@@ -94,83 +94,92 @@ <a href="/`)
//line gititemsummary.qtpl:26
qw422016.E().S(name)
//line gititemsummary.qtpl:26
- qw422016.N().S(`/tree/`)
+ qw422016.N().S(`/archive/`)
//line gititemsummary.qtpl:26
qw422016.E().S(t.Name().Short())
//line gititemsummary.qtpl:26
- qw422016.N().S(`/">tree</a>
+ qw422016.N().S(`.tar.gz">tar.gz</a>
<a href="/`)
//line gititemsummary.qtpl:27
qw422016.E().S(name)
//line gititemsummary.qtpl:27
- qw422016.N().S(`/log/`)
+ qw422016.N().S(`/tree/`)
//line gititemsummary.qtpl:27
qw422016.E().S(t.Name().Short())
//line gititemsummary.qtpl:27
+ qw422016.N().S(`/">tree</a>
+ <a href="/`)
+//line gititemsummary.qtpl:28
+ qw422016.E().S(name)
+//line gititemsummary.qtpl:28
+ qw422016.N().S(`/log/`)
+//line gititemsummary.qtpl:28
+ qw422016.E().S(t.Name().Short())
+//line gititemsummary.qtpl:28
qw422016.N().S(`/">log</a>
</div>
</div>
</div>
`)
-//line gititemsummary.qtpl:31
+//line gititemsummary.qtpl:32
}
-//line gititemsummary.qtpl:31
+//line gititemsummary.qtpl:32
qw422016.N().S(`
</div>
`)
-//line gititemsummary.qtpl:33
+//line gititemsummary.qtpl:34
} else {
-//line gititemsummary.qtpl:33
+//line gititemsummary.qtpl:34
qw422016.N().S(`
<p> No tags </p>
`)
-//line gititemsummary.qtpl:35
+//line gititemsummary.qtpl:36
}
-//line gititemsummary.qtpl:35
+//line gititemsummary.qtpl:36
qw422016.N().S(`
</div>
<div class="col-md-4">
<div class="event-list">
`)
-//line gititemsummary.qtpl:39
+//line gititemsummary.qtpl:40
for _, b := range g.Branches {
-//line gititemsummary.qtpl:39
+//line gititemsummary.qtpl:40
qw422016.N().S(`
<div class="row event">
<div class="col-4">
`)
-//line gititemsummary.qtpl:42
+//line gititemsummary.qtpl:43
qw422016.E().S(b.Name().Short())
-//line gititemsummary.qtpl:42
+//line gititemsummary.qtpl:43
qw422016.N().S(`
</div>
<div class="col-8">
<div class="float-end">
<a href="/`)
-//line gititemsummary.qtpl:46
+//line gititemsummary.qtpl:47
qw422016.E().S(name)
-//line gititemsummary.qtpl:46
+//line gititemsummary.qtpl:47
qw422016.N().S(`/tree/`)
-//line gititemsummary.qtpl:46
+//line gititemsummary.qtpl:47
qw422016.E().S(b.Name().Short())
-//line gititemsummary.qtpl:46
+//line gititemsummary.qtpl:47
qw422016.N().S(`/">tree</a>
<a href="/`)
-//line gititemsummary.qtpl:47
+//line gititemsummary.qtpl:48
qw422016.E().S(name)
-//line gititemsummary.qtpl:47
+//line gititemsummary.qtpl:48
qw422016.N().S(`/log/`)
-//line gititemsummary.qtpl:47
+//line gititemsummary.qtpl:48
qw422016.E().S(b.Name().Short())
-//line gititemsummary.qtpl:47
+//line gititemsummary.qtpl:48
qw422016.N().S(`/">log</a>
</div>
</div>
</div>
`)
-//line gititemsummary.qtpl:51
+//line gititemsummary.qtpl:52
}
-//line gititemsummary.qtpl:51
+//line gititemsummary.qtpl:52
qw422016.N().S(`
</div>
</div>
@@ -178,70 +187,70 @@ </div>
<div class="row">
<div class="event-list">
`)
-//line gititemsummary.qtpl:57
+//line gititemsummary.qtpl:58
for _, c := range g.Commits {
-//line gititemsummary.qtpl:57
+//line gititemsummary.qtpl:58
qw422016.N().S(`
<div class="row event">
<div class="col-xxl-2">
`)
-//line gititemsummary.qtpl:60
+//line gititemsummary.qtpl:61
qw422016.E().S(TimeFormat(c.Committer.When))
-//line gititemsummary.qtpl:60
+//line gititemsummary.qtpl:61
qw422016.N().S(`
</div>
<div class="col-xxl-7 code-view">
<pre>`)
-//line gititemsummary.qtpl:63
+//line gititemsummary.qtpl:64
qw422016.E().S(c.Message)
-//line gititemsummary.qtpl:63
+//line gititemsummary.qtpl:64
qw422016.N().S(`</pre>
</div>
<div class="col-xxl-3">
<small>`)
-//line gititemsummary.qtpl:66
+//line gititemsummary.qtpl:67
qw422016.E().S(c.Committer.Name)
-//line gititemsummary.qtpl:66
+//line gititemsummary.qtpl:67
qw422016.N().S(` <`)
-//line gititemsummary.qtpl:66
+//line gititemsummary.qtpl:67
qw422016.E().S(c.Committer.Email)
-//line gititemsummary.qtpl:66
+//line gititemsummary.qtpl:67
qw422016.N().S(`></small>
</div>
</div>
`)
-//line gititemsummary.qtpl:69
+//line gititemsummary.qtpl:70
}
-//line gititemsummary.qtpl:69
+//line gititemsummary.qtpl:70
qw422016.N().S(`
</div>
</div>
`)
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
}
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
func (g *GitItemSummaryPage) WriteGitContent(qq422016 qtio422016.Writer, name, ref string) {
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
qw422016 := qt422016.AcquireWriter(qq422016)
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
g.StreamGitContent(qw422016, name, ref)
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
qt422016.ReleaseWriter(qw422016)
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
}
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
func (g *GitItemSummaryPage) GitContent(name, ref string) string {
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
qb422016 := qt422016.AcquireByteBuffer()
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
g.WriteGitContent(qb422016, name, ref)
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
qs422016 := string(qb422016.B)
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
qt422016.ReleaseByteBuffer(qb422016)
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
return qs422016
-//line gititemsummary.qtpl:72
+//line gititemsummary.qtpl:73
}