Changes
diff --git a/branch_filter.go b/branch_filter.go
new file mode 100644
index 0000000..19f6127
--- /dev/null
+++ b/branch_filter.go
@@ -0,0 +1,234 @@
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "log"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+// Goes through list of branches and returns those that match whitelist.
+func branchFilter(repo string, options *options) ([]branch, error) {
+ cmd := exec.Command("git", "branch", "-a")
+ cmd.Dir = repo
+
+ whitelist := options.Branches
+
+ out, err := cmd.Output()
+
+ if err != nil {
+ return nil, err
+ }
+
+ var b = make(map[string]branch)
+ var m = make(map[string]bool)
+
+ scanner := bufio.NewScanner(bytes.NewReader(out))
+
+ for scanner.Scan() {
+ t := strings.TrimSpace(strings.TrimPrefix(scanner.Text(), "*"))
+ _, f := filepath.Split(t)
+
+ m[f] = true
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+
+ // Filter to match options, but return all if no branch flags given.
+ if len(whitelist) > 0 {
+ for k := range m {
+ m[k] = contains(whitelist, k)
+ }
+ } else {
+ // In git given order at this point.
+ for k := range m {
+ whitelist = append(whitelist, k)
+ }
+ }
+
+ for k, v := range m {
+ if v {
+ // TODO: Try a goroutine?
+ commits, err := commitParser(k, repo, options.Name)
+
+ if err != nil {
+ continue
+ }
+
+ b[k] = branch{commits, k, options.Name}
+ }
+ }
+
+ // Fill in resulting slice with desired branches in order.
+ var results []branch
+
+ for _, v := range whitelist {
+ results = append(results, b[v])
+ }
+
+ return results, nil
+}
+
+func commitParser(b string, repo string, name string) ([]commit, error) {
+ fst := strings.Join([]string{"%H", "%P", "%s", "%aN", "%aE", "%aD", "%h"}, SEP)
+ ref := fmt.Sprintf("origin/%s", b)
+
+ cmd := exec.Command("git", "log", fmt.Sprintf("--format=%s", fst), ref)
+ cmd.Dir = repo
+
+ out, err := cmd.Output()
+
+ if err != nil {
+ return nil, err
+ }
+
+ results := []commit{}
+ scanner := bufio.NewScanner(bytes.NewReader(out))
+
+ for scanner.Scan() {
+ text := strings.TrimSpace(scanner.Text())
+ data := strings.Split(text, SEP)
+
+ h := data[0]
+
+ var history []overview
+ var parents []string
+
+ if data[1] != "" {
+ parents = strings.Split(data[1], " ")
+ }
+
+ for _, parent := range parents {
+ diffstat, err := diffStatParser(h, parent, repo)
+
+ if err != nil {
+ log.Printf("unable to diffstat against parent: %s", err)
+
+ continue
+ }
+
+ history = append(history, overview{diffstat, h, parent})
+ }
+
+ a := author{data[4], data[3]}
+
+ date, err := time.Parse("Mon, 2 Jan 2006 15:04:05 -0700", data[5])
+
+ if err != nil {
+ log.Printf("unable to parse commit date: %s", err)
+
+ continue
+ }
+
+ body, err := bodyParser(h, repo)
+
+ if err != nil {
+ log.Printf("unable to parse commit body: %s", err)
+
+ continue
+ }
+
+ tree, err := treeParser(h, repo)
+
+ if err != nil {
+ log.Printf("unable to parse commit tree: %s", err)
+
+ continue
+ }
+
+ c := commit{
+ Abbr: data[6],
+ Author: a,
+ Body: body,
+ Branch: b,
+ Date: date,
+ Hash: h,
+ History: history,
+ Parents: parents,
+ Project: name,
+ Subject: data[2],
+ Tree: tree,
+ }
+
+ results = append(results, c)
+ }
+
+ if err := scanner.Err(); err != nil {
+ return nil, err
+ }
+
+ return results, nil
+}
+
+func treeParser(h string, repo string) ([]object, error) {
+ cmd := exec.Command("git", "ls-tree", "-r", "--format=%(objectname) %(path)", h)
+ cmd.Dir = repo
+
+ out, err := cmd.Output()
+
+ if err != nil {
+ return nil, err
+ }
+
+ var results []object
+ feed := strings.Split(strings.TrimSuffix(fmt.Sprintf("%s", out), "\n"), "\n")
+
+ for _, line := range feed {
+ w := strings.Split(line, " ")
+
+ results = append(results, object{
+ Hash: w[0],
+ Path: w[1],
+ })
+ }
+
+ return results, nil
+}
+
+func diffStatParser(h, parent string, repo string) (string, error) {
+ cmd := exec.Command("git", "diff", "--stat", fmt.Sprintf("%s..%s", parent, h))
+ cmd.Dir = repo
+
+ out, err := cmd.Output()
+
+ if err != nil {
+ return "", err
+ }
+
+ var results []string
+ feed := strings.Split(strings.TrimSuffix(fmt.Sprintf("%s", out), "\n"), "\n")
+
+ for _, line := range feed {
+ // NOTE: This is hackish I know, attach to project?
+ i := strings.Index(line, "|")
+
+ if i != -1 {
+ ext := filepath.Ext(strings.TrimSpace(line[:i]))
+ types[ext] = strings.Contains(line, "Bin")
+ }
+
+ results = append(results, strings.TrimSpace(line))
+ }
+
+ return strings.Join(results, "\n"), nil
+}
+
+func bodyParser(h string, repo string) (string, error) {
+ // Because the commit message body is multiline and is tripping the scanner.
+ cmd := exec.Command("git", "show", "--no-patch", "--format=%B", h)
+ cmd.Dir = repo
+
+ out, err := cmd.Output()
+
+ if err != nil {
+ return "", err
+ }
+
+ return strings.TrimSuffix(fmt.Sprintf("%s", out), "\n"), nil
+}
diff --git a/diff_parsers.go b/diff_parsers.go
new file mode 100644
index 0000000..eac48ab
--- /dev/null
+++ b/diff_parsers.go
@@ -0,0 +1,87 @@
+package main
+
+import (
+ "fmt"
+ "html/template"
+ "regexp"
+ "strings"
+)
+
+// Match diff body @@ del, ins line numbers.
+var aline = regexp.MustCompile(`\-(.*?),`)
+var bline = regexp.MustCompile(`\+(.*?),`)
+
+// Match diff body keywords.
+var xline = regexp.MustCompile(`^(deleted|index|new|rename|similarity)`)
+
+// Helps target file specific diff blocks.
+var diffanchor = regexp.MustCompile(`b\/(.*?)$`)
+
+func diffbodyparser(d diff) template.HTML {
+ var results []string
+ feed := strings.Split(strings.TrimSuffix(template.HTMLEscapeString(d.Body), "\n"), "\n")
+
+ var a, b string
+
+ for _, line := range feed {
+ if strings.HasPrefix(line, "diff") {
+ line = diffanchor.ReplaceAllString(line, `b/<a id="$1">$1</a>`)
+ line = fmt.Sprintf("<strong>%s</strong>", line)
+ }
+
+ line = xline.ReplaceAllString(line, "<em>$1</em>")
+
+ if strings.HasPrefix(line, "@@") {
+ if a != "" && !strings.HasPrefix(a, "---") {
+ repl := fmt.Sprintf(`<a href="commit/%s/%s.html#L$1">-$1</a>,`, d.Parent, a)
+ line = aline.ReplaceAllString(line, repl)
+ }
+
+ if b != "" && !strings.HasPrefix(b, "+++") {
+ repl := fmt.Sprintf(`<a href="commit/%s/%s.html#L$1">+$1</a>,`, d.Commit.Hash, b)
+ line = bline.ReplaceAllString(line, repl)
+ }
+ }
+
+ if strings.HasPrefix(line, "---") {
+ a = strings.TrimPrefix(line, "--- a/")
+ line = fmt.Sprintf("<mark>%s</mark>", line)
+ } else if strings.HasPrefix(line, "-") {
+ line = fmt.Sprintf("<del>%s</del>", line)
+ }
+
+ if strings.HasPrefix(line, "+++") {
+ b = strings.TrimPrefix(line, "+++ b/")
+ line = fmt.Sprintf("<mark>%s</mark>", line)
+ } else if strings.HasPrefix(line, "+") {
+ line = fmt.Sprintf("<ins>%s</ins>", line)
+ }
+
+ results = append(results, line)
+ }
+
+ return template.HTML(strings.Join(results, "\n"))
+}
+
+func diffstatbodyparser(o overview) template.HTML {
+ var results []string
+ feed := strings.Split(strings.TrimSuffix(o.Body, "\n"), "\n")
+
+ for i, line := range feed {
+ if i < len(feed)-1 {
+ // Link files to corresponding diff.
+ columns := strings.Split(line, "|")
+ files := strings.Split(columns[0], "=>")
+
+ a := strings.TrimSpace(files[len(files)-1])
+ b := fmt.Sprintf(`<a href="commit/%s/diff-%s.html#%s">%s</a>`, o.Hash, o.Parent, a, a)
+ l := strings.LastIndex(line, a)
+
+ line = line[:l] + strings.Replace(line[l:], a, b, 1)
+ }
+
+ results = append(results, line)
+ }
+
+ return template.HTML(strings.Join(results, "\n"))
+}
diff --git a/flags.go b/flags.go
new file mode 100644
index 0000000..42b9b8b
--- /dev/null
+++ b/flags.go
@@ -0,0 +1,19 @@
+package main
+
+import "strings"
+
+// https://stackoverflow.com/questions/28322997/how-to-get-a-list-of-values-into-a-flag-in-golang/
+type manyflag []string
+
+func (f *manyflag) Set(value string) error {
+ // Make sure there are no duplicates.
+ if !contains(*f, value) {
+ *f = append(*f, value)
+ }
+
+ return nil
+}
+
+func (f *manyflag) String() string {
+ return strings.Join(*f, ", ")
+}
diff --git a/helper.go b/helper.go
index d0a380e..67954e7 100644
--- a/helper.go
+++ b/helper.go
@@ -1,9 +1,5 @@
package main
-import (
- "reflect"
-)
-
type void struct{}
// Helps decide if value contained in slice.
@@ -20,12 +16,16 @@ func contains(s []string, n string) bool {
// Helps clear duplicates in slice.
// https://stackoverflow.com/questions/66643946/how-to-remove-duplicates-strings-or-int-from-slice-in-go
-func dedupe(input []string) []reflect.Value {
+func dedupe(input []string) []string {
set := make(map[string]void)
+ list := []string{}
for _, v := range input {
- set[v] = void{}
+ if _, ok := set[v]; !ok {
+ set[v] = void{}
+ list = append(list, v)
+ }
}
- return reflect.ValueOf(set).MapKeys()
+ return list
}
diff --git a/helper_test.go b/helper_test.go
index f62d053..0b551b0 100644
--- a/helper_test.go
+++ b/helper_test.go
@@ -17,3 +17,11 @@ func TestContains(t *testing.T) {
}
}
}
+
+func TestDedupe(t *testing.T) {
+ dupes := []string{"one", "one", "two", "three", "three", "three"}
+
+ if len(dedupe(dupes)) != 3 {
+ t.Fail()
+ }
+}
diff --git a/main.go b/main.go
index 9211304..116162e 100644
--- a/main.go
+++ b/main.go
@@ -1,178 +1,30 @@
package main
import (
- "bufio"
- "bytes"
_ "embed"
"encoding/json"
"flag"
"fmt"
- "html/template"
"io"
"log"
"net/url"
"os"
- "os/exec"
"path/filepath"
"reflect"
- "regexp"
"strings"
- "sync"
"text/tabwriter"
- "time"
)
// EMPTY is git's magic empty tree hash.
const EMPTY = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
-// SEP is a browser generated UUID v4 used to separate out commit line items.
-const SEP = "6f6c1745-e902-474a-9e99-08d0084fb011"
-
//go:embed page.html.tmpl
var tpl string
-// Match diff body keywords.
-var xline = regexp.MustCompile(`^(deleted|index|new|rename|similarity)`)
-
-// Match diff body @@ del, ins line numbers.
-var aline = regexp.MustCompile(`\-(.*?),`)
-var bline = regexp.MustCompile(`\+(.*?),`)
-
-// Helps target file specific diff blocks.
-var diffanchor = regexp.MustCompile(`b\/(.*?)$`)
-
-// Helps keep track of file extensions git thinks of as binary.
-var types = make(map[string]bool)
-
-type project struct {
- base string
- Branches []branch
- Name string
- repo string
-}
-
-// Data is the generic content map passed on to the page template.
-type Data map[string]interface{}
-type page struct {
- Data
- Base string
- Stylesheet string
- Title string
-}
-
-type branch struct {
- Commits []commit
- Name string
- Project string
-}
-
-func (b branch) String() string {
- return b.Name
-}
-
-type diff struct {
- Body string
- Commit commit
- Parent string
-}
-
-type overview struct {
- Body string
- Hash string
- Parent string
-}
-
-type hash struct {
- Hash string
- Short string
-}
-
-func (h hash) String() string {
- return h.Hash
-}
-
-type object struct {
- Hash string
- Path string
-}
-
-func (o object) Dir() string {
- return filepath.Join(o.Hash[0:2], o.Hash[2:])
-}
-
-type show struct {
- Body string
- Bin bool
- Lines []int
- object
-}
-
-type commit struct {
- Branch string
- Body string
- Abbr string
- History []overview
- Parents []string
- Hash string
- Author author
- Date time.Time
- Project string
- Tree []object
- Types map[string]bool
- Subject string
-}
-
-type author struct {
- Email string
- Name string
-}
-
-// https://stackoverflow.com/questions/28322997/how-to-get-a-list-of-values-into-a-flag-in-golang/
-type manyflag []string
-
-func (f *manyflag) Set(value string) error {
- // Make sure there are no duplicates.
- if !contains(*f, value) {
- *f = append(*f, value)
- }
-
- return nil
-}
-
-func (f *manyflag) String() string {
- return strings.Join(*f, ", ")
-}
-
-type options struct {
- Branches manyflag `json:"branches"`
- config string
- Force bool `json:"force"`
- Name string `json:"name"`
- Quiet bool `json:"quiet"`
- Source string `json:"source"`
- Template string `json:"template"`
- URL string `json:"url"`
-}
-
-// Helps store options into a JSON config file.
-func (o *options) save(p string) error {
- bs, err := json.MarshalIndent(o, "", " ")
-
- if err != nil {
- return fmt.Errorf("unable to encode config file: %v", err)
- }
-
- if err := os.WriteFile(filepath.Join(p, o.config), bs, 0644); err != nil {
- return fmt.Errorf("unable to save config file: %v", err)
- }
-
- return nil
-}
-
func init() {
// Override default usage output.
flag.Usage = func() {
- // Print usage example ahead of lisiting default options.
+ // Print usage example ahead of listing default options.
fmt.Fprintln(flag.CommandLine.Output(), "usage:", os.Args[0], "[<options>] <path>")
flag.PrintDefaults()
}
@@ -187,7 +39,7 @@ func main() {
config: ".jimmy.json",
}
- // NOTE: Flags need match each option key's first letter.
+ // NOTE: Flags need to match each option key's first letter.
flag.StringVar(&opt.Name, "n", "Jimbo", "Project title")
flag.StringVar(&opt.Source, "s", "", "Source repository")
flag.Var(&opt.Branches, "b", "Target branches")
@@ -316,652 +168,25 @@ func main() {
log.Printf("user cache set: %s", tmp)
- pro := &project{
- base: dir,
- Name: opt.Name,
- repo: tmp,
- }
+ pro := NewProject(dir, tmp, opt)
// Create base directories.
- if err := pro.init(opt.Force); err != nil {
+ if err := pro.init(); err != nil {
log.Fatalf("unable to initialize output directory: %v", err)
}
// Clone target repo.
- if err := pro.save(opt.Source); err != nil {
+ if err := pro.save(); err != nil {
log.Fatalf("unable to set up repo: %v", err)
}
- branches, err := pro.branchfilter(opt.Branches)
-
+ branches, err := branchFilter(tmp, opt)
if err != nil {
log.Fatalf("unable to filter branches: %v", err)
}
- var wg sync.WaitGroup
- t := template.Must(template.New("page").Funcs(template.FuncMap{
- "diffstatbodyparser": diffstatbodyparser,
- "diffbodyparser": diffbodyparser,
- }).Parse(tpl))
-
- // Update each branch.
- for _, b := range branches {
- // NOTE: Is this needed still if the repo is downloaded each time the script is run?
- ref := fmt.Sprintf("refs/heads/%s:refs/origin/%s", b, b)
-
- cmd := exec.Command("git", "fetch", "--force", "origin", ref)
- cmd.Dir = pro.repo
-
- log.Printf("updating branch: %s", b)
-
- if _, err := cmd.Output(); err != nil {
- log.Printf("unable to fetch branch: %v", err)
-
- continue
- }
- }
-
- for _, b := range branches {
- log.Printf("processing branch: %s", b)
-
- go func() {
- dst := filepath.Join(pro.base, "branch", b.Name, "index.html")
-
- if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
- if err != nil {
- log.Fatalf("unable to create branch directory: %v", err)
- }
-
- return
- }
-
- f, err := os.Create(dst)
-
- defer f.Close()
-
- if err != nil {
- // TODO: Remove from branches slice?
- log.Printf("unable to create branch page: %v", err)
-
- return
- }
-
- p := page{
- Data: Data{
- "Commits": b.Commits,
- "Branch": b,
- "Project": pro.Name,
- },
- Base: "../../",
- Title: strings.Join([]string{pro.Name, b.Name}, ": "),
- }
-
- if err := t.Execute(f, p); err != nil {
- log.Printf("unable to apply template: %v", err)
-
- return
- }
- }()
-
- for i, c := range b.Commits {
- log.Printf("processing commit: %s: %d/%d", c.Abbr, i+1, len(b.Commits))
-
- base := filepath.Join(pro.base, "commit", c.Hash)
-
- if err := os.MkdirAll(base, 0755); err != nil {
- if err != nil {
- log.Printf("unable to create commit directory: %v", err)
- }
-
- continue
- }
-
- for _, par := range c.Parents {
- func() {
- cmd := exec.Command("git", "diff", "-p", fmt.Sprintf("%s..%s", par, c.Hash))
- cmd.Dir = pro.repo
-
- out, err := cmd.Output()
-
- if err != nil {
- log.Printf("unable to diff against parent: %v", err)
-
- return
- }
-
- dst := filepath.Join(base, fmt.Sprintf("diff-%s.html", par))
- f, err := os.Create(dst)
-
- defer f.Close()
-
- if err != nil {
- log.Printf("unable to create commit diff to parent: %v", err)
-
- return
- }
-
- p := page{
- Data: Data{
- "Diff": diff{
- Body: fmt.Sprintf("%s", out),
- Commit: c,
- Parent: par,
- },
- "Project": pro.Name,
- },
- Base: "../../",
- Title: strings.Join([]string{pro.Name, b.Name, c.Abbr}, ": "),
- }
-
- if err := t.Execute(f, p); err != nil {
- log.Printf("unable to apply template: %v", err)
-
- return
- }
- }()
- }
-
- for _, obj := range c.Tree {
- dst := filepath.Join(pro.base, "object", obj.Dir())
-
- if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
- if err != nil {
- log.Printf("unable to create object directory: %v", err)
- }
-
- continue
- }
-
- func() {
- cmd := exec.Command("git", "cat-file", "blob", obj.Hash)
- cmd.Dir = pro.repo
-
- out, err := cmd.Output()
-
- if err != nil {
- log.Printf("unable to save object: %v", err)
-
- return
- }
-
- f, err := os.Create(dst)
-
- defer f.Close()
-
- if err != nil {
- log.Printf("unable to create object: %v", err)
-
- return
- }
-
- if _, err := f.Write(out); err != nil {
- log.Printf("unable to write object blob: %v", err)
-
- return
- }
- }()
-
- func(nom string) {
- f, err := os.Create(nom)
-
- defer f.Close()
-
- if err != nil {
- log.Printf("unable to create object: %v", err)
-
- return
- }
-
- o := &show{
- object: object{
- Hash: obj.Hash,
- Path: obj.Path,
- },
- Bin: types[filepath.Ext(obj.Path)],
- }
-
- if o.Bin {
- // TODO.
- } else {
- cmd := exec.Command("git", "show", "--no-notes", obj.Hash)
- cmd.Dir = pro.repo
-
- out, err := cmd.Output()
-
- if err != nil {
- log.Printf("unable to show object: %v", err)
-
- return
- }
-
- sep := []byte("\n")
- var lines = make([]int, bytes.Count(out, sep))
-
- for i := range lines {
- lines[i] = i + 1
- }
-
- if bytes.LastIndex(out, sep) != len(out)-1 {
- lines = append(lines, len(lines))
- }
-
- o.Lines = lines
- o.Body = fmt.Sprintf("%s", out)
- }
-
- p := page{
- Data: Data{
- "Object": *o,
- "Project": pro.Name,
- },
- Base: "../../",
- Title: strings.Join([]string{pro.Name, b.Name, c.Abbr, obj.Path}, ": "),
- }
-
- if err := t.Execute(f, p); err != nil {
- log.Printf("unable to apply template: %v", err)
-
- return
- }
-
- lnk := filepath.Join(base, fmt.Sprintf("%s.html", obj.Path))
-
- if err := os.MkdirAll(filepath.Dir(lnk), 0755); err != nil {
- if err != nil {
- log.Printf("unable to create hard link path: %v", err)
- }
-
- return
- }
-
- if err := os.Link(nom, lnk); err != nil {
- if os.IsExist(err) {
- return
- }
-
- log.Printf("unable to hard link object into commit folder: %v", err)
- }
- }(fmt.Sprintf("%s.html", dst))
- }
-
- func() {
- dst := filepath.Join(base, "index.html")
- f, err := os.Create(dst)
-
- defer f.Close()
-
- if err != nil {
- log.Printf("unable to create commit page: %v", err)
-
- return
- }
-
- p := page{
- Data: Data{
- "Commit": c,
- "Project": pro.Name,
- },
- Base: "../../",
- Title: strings.Join([]string{pro.Name, b.Name, c.Abbr}, ": "),
- }
-
- if err := t.Execute(f, p); err != nil {
- log.Printf("unable to apply template: %v", err)
-
- return
- }
- }()
- }
- }
-
- wg.Add(1)
-
- go func() {
- defer wg.Done()
-
- // This is the main index or project home.
- f, err := os.Create(filepath.Join(pro.base, "index.html"))
-
- defer f.Close()
-
- if err != nil {
- log.Fatalf("unable to create home page: %v", err)
- }
-
- p := page{
- Data: Data{
- "Branches": branches,
- "Link": opt.URL,
- "Project": pro.Name,
- },
- Base: "./",
- Title: pro.Name,
- }
-
- if err := t.Execute(f, p); err != nil {
- log.Fatalf("unable to apply template: %v", err)
- }
- }()
-
- wg.Wait()
-}
-
-// Creates base directories for holding objects, branches, and commits.
-func (pro *project) init(f bool) error {
- dirs := []string{"branch", "commit", "object"}
-
- for _, dir := range dirs {
- d := filepath.Join(pro.base, dir)
-
- // Clear existing dirs when -f true.
- if f && dir != "branch" {
- if err := os.RemoveAll(d); err != nil {
- return fmt.Errorf("unable to remove directory: %v", err)
- }
- }
-
- if err := os.MkdirAll(d, 0755); err != nil {
- return fmt.Errorf("unable to create directory: %v", err)
- }
- }
-
- return nil
-}
-
-// Saves a local clone of `target` repo.
-func (pro *project) save(target string) error {
- if _, err := os.Stat(pro.repo); err != nil {
- return err
- }
-
- return exec.Command("git", "clone", target, pro.repo).Run()
-}
-
-// Goes through list of branches and returns those that match whitelist.
-func (pro *project) branchfilter(whitelist manyflag) ([]branch, error) {
- cmd := exec.Command("git", "branch", "-a")
- cmd.Dir = pro.repo
-
- out, err := cmd.Output()
-
- if err != nil {
- return nil, err
- }
-
- var b = make(map[string]branch)
- var m = make(map[string]bool)
-
- scanner := bufio.NewScanner(bytes.NewReader(out))
-
- for scanner.Scan() {
- t := strings.TrimSpace(strings.TrimPrefix(scanner.Text(), "*"))
- _, f := filepath.Split(t)
-
- m[f] = true
- }
-
- if err := scanner.Err(); err != nil {
- return nil, err
- }
-
- // Filter to match options, but return all if no branch flags given.
- if len(whitelist) > 0 {
- for k := range m {
- m[k] = contains(whitelist, k)
- }
- } else {
- // In git given order at this point.
- for k := range m {
- whitelist = append(whitelist, k)
- }
- }
-
- for k, v := range m {
- if v {
- // TODO: Try a goroutine?
- commits, err := pro.commitparser(k)
-
- if err != nil {
- continue
- }
-
- b[k] = branch{commits, k, pro.Name}
- }
- }
-
- // Fill in resulting slice with desired branches in order.
- var results []branch
-
- for _, v := range whitelist {
- results = append(results, b[v])
- }
-
- return results, nil
-}
-
-func (pro *project) commitparser(b string) ([]commit, error) {
- fst := strings.Join([]string{"%H", "%P", "%s", "%aN", "%aE", "%aD", "%h"}, SEP)
- ref := fmt.Sprintf("origin/%s", b)
-
- cmd := exec.Command("git", "log", fmt.Sprintf("--format=%s", fst), ref)
- cmd.Dir = pro.repo
-
- out, err := cmd.Output()
-
- if err != nil {
- return nil, err
- }
-
- results := []commit{}
- scanner := bufio.NewScanner(bytes.NewReader(out))
-
- for scanner.Scan() {
- text := strings.TrimSpace(scanner.Text())
- data := strings.Split(text, SEP)
-
- h := data[0]
-
- var history []overview
- var parents []string
-
- if data[1] != "" {
- parents = strings.Split(data[1], " ")
- }
-
- for _, p := range parents {
- diffstat, err := pro.diffstatparser(h, p)
-
- if err != nil {
- log.Printf("unable to diffstat against parent: %s", err)
-
- continue
- }
-
- history = append(history, overview{diffstat, h, p})
- }
-
- a := author{data[4], data[3]}
-
- date, err := time.Parse("Mon, 2 Jan 2006 15:04:05 -0700", data[5])
-
- if err != nil {
- log.Printf("unable to parse commit date: %s", err)
-
- continue
- }
-
- body, err := pro.bodyparser(h)
-
- if err != nil {
- log.Printf("unable to parse commit body: %s", err)
-
- continue
- }
-
- tree, err := pro.treeparser(h)
-
- if err != nil {
- log.Printf("unable to parse commit tree: %s", err)
-
- continue
- }
-
- c := commit{
- Abbr: data[6],
- Author: a,
- Body: body,
- Branch: b,
- Date: date,
- Hash: h,
- History: history,
- Parents: parents,
- Project: pro.Name,
- Subject: data[2],
- Tree: tree,
- }
-
- results = append(results, c)
- }
-
- if err := scanner.Err(); err != nil {
- return nil, err
- }
-
- return results, nil
-}
-
-func (pro *project) treeparser(h string) ([]object, error) {
- cmd := exec.Command("git", "ls-tree", "-r", "--format=%(objectname) %(path)", h)
- cmd.Dir = pro.repo
-
- out, err := cmd.Output()
-
- if err != nil {
- return nil, err
- }
-
- var results []object
- feed := strings.Split(strings.TrimSuffix(fmt.Sprintf("%s", out), "\n"), "\n")
-
- for _, line := range feed {
- w := strings.Split(line, " ")
-
- results = append(results, object{
- Hash: w[0],
- Path: w[1],
- })
- }
-
- return results, nil
-}
-
-func (pro *project) diffstatparser(h, p string) (string, error) {
- cmd := exec.Command("git", "diff", "--stat", fmt.Sprintf("%s..%s", p, h))
- cmd.Dir = pro.repo
-
- out, err := cmd.Output()
-
- if err != nil {
- return "", err
- }
-
- var results []string
- feed := strings.Split(strings.TrimSuffix(fmt.Sprintf("%s", out), "\n"), "\n")
-
- for _, line := range feed {
- // NOTE: This is hackish I know, attach to project?
- i := strings.Index(line, "|")
-
- if i != -1 {
- ext := filepath.Ext(strings.TrimSpace(line[:i]))
- types[ext] = strings.Contains(line, "Bin")
- }
-
- results = append(results, strings.TrimSpace(line))
- }
-
- return strings.Join(results, "\n"), nil
-}
-
-func (pro *project) bodyparser(h string) (string, error) {
- // Because the commit message body is multiline and is tripping the scanner.
- cmd := exec.Command("git", "show", "--no-patch", "--format=%B", h)
- cmd.Dir = pro.repo
-
- out, err := cmd.Output()
-
- if err != nil {
- return "", err
- }
-
- return strings.TrimSuffix(fmt.Sprintf("%s", out), "\n"), nil
-}
-
-func diffbodyparser(d diff) template.HTML {
- var results []string
- feed := strings.Split(strings.TrimSuffix(template.HTMLEscapeString(d.Body), "\n"), "\n")
-
- var a, b string
-
- for _, line := range feed {
- if strings.HasPrefix(line, "diff") {
- line = diffanchor.ReplaceAllString(line, `b/<a id="$1">$1</a>`)
- line = fmt.Sprintf("<strong>%s</strong>", line)
- }
-
- line = xline.ReplaceAllString(line, "<em>$1</em>")
-
- if strings.HasPrefix(line, "@@") {
- if a != "" && !strings.HasPrefix(a, "---") {
- repl := fmt.Sprintf(`<a href="commit/%s/%s.html#L$1">-$1</a>,`, d.Parent, a)
- line = aline.ReplaceAllString(line, repl)
- }
-
- if b != "" && !strings.HasPrefix(b, "+++") {
- repl := fmt.Sprintf(`<a href="commit/%s/%s.html#L$1">+$1</a>,`, d.Commit.Hash, b)
- line = bline.ReplaceAllString(line, repl)
- }
- }
-
- if strings.HasPrefix(line, "---") {
- a = strings.TrimPrefix(line, "--- a/")
- line = fmt.Sprintf("<mark>%s</mark>", line)
- } else if strings.HasPrefix(line, "-") {
- line = fmt.Sprintf("<del>%s</del>", line)
- }
-
- if strings.HasPrefix(line, "+++") {
- b = strings.TrimPrefix(line, "+++ b/")
- line = fmt.Sprintf("<mark>%s</mark>", line)
- } else if strings.HasPrefix(line, "+") {
- line = fmt.Sprintf("<ins>%s</ins>", line)
- }
-
- results = append(results, line)
- }
-
- return template.HTML(strings.Join(results, "\n"))
-}
-
-func diffstatbodyparser(o overview) template.HTML {
- var results []string
- feed := strings.Split(strings.TrimSuffix(o.Body, "\n"), "\n")
-
- for i, line := range feed {
- if i < len(feed)-1 {
- // Link files to corresponding diff.
- columns := strings.Split(line, "|")
- files := strings.Split(columns[0], "=>")
-
- a := strings.TrimSpace(files[len(files)-1])
- b := fmt.Sprintf(`<a href="commit/%s/diff-%s.html#%s">%s</a>`, o.Hash, o.Parent, a, a)
- l := strings.LastIndex(line, a)
-
- line = line[:l] + strings.Replace(line[l:], a, b, 1)
- }
-
- results = append(results, line)
- }
+ pro.updateBranches(branches)
- return template.HTML(strings.Join(results, "\n"))
+ pro.writePages(branches)
+ pro.writeMainIndex(branches)
}
diff --git a/options.go b/options.go
new file mode 100644
index 0000000..dc74081
--- /dev/null
+++ b/options.go
@@ -0,0 +1,34 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "path/filepath"
+)
+
+type options struct {
+ Branches manyflag `json:"branches"`
+ config string
+ Force bool `json:"force"`
+ Name string `json:"name"`
+ Quiet bool `json:"quiet"`
+ Source string `json:"source"`
+ Template string `json:"template"`
+ URL string `json:"url"`
+}
+
+// Helps store options into a JSON config file.
+func (o *options) save(p string) error {
+ bs, err := json.MarshalIndent(o, "", " ")
+
+ if err != nil {
+ return fmt.Errorf("unable to encode config file: %v", err)
+ }
+
+ if err := os.WriteFile(filepath.Join(p, o.config), bs, 0644); err != nil {
+ return fmt.Errorf("unable to save config file: %v", err)
+ }
+
+ return nil
+}
diff --git a/project.go b/project.go
new file mode 100644
index 0000000..dbedb25
--- /dev/null
+++ b/project.go
@@ -0,0 +1,368 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+)
+
+// SEP is a browser generated UUID v4 used to separate out commit line items.
+const SEP = "6f6c1745-e902-474a-9e99-08d0084fb011"
+
+// Helps keep track of file extensions git thinks of as binary.
+var types = make(map[string]bool)
+
+type project struct {
+ base string
+ Name string
+ repo string
+ options *options
+ template *template.Template
+}
+
+func NewProject(base string, repo string, options *options) *project {
+ funcMap := template.FuncMap{
+ "diffstatbodyparser": diffstatbodyparser,
+ "diffbodyparser": diffbodyparser,
+ }
+ template := template.Must(template.New("page").Funcs(funcMap).Parse(tpl))
+
+ return &project{
+ base: base,
+ Name: options.Name,
+ repo: repo,
+ options: options,
+ template: template,
+ }
+}
+
+// Creates base directories for holding objects, branches, and commits.
+func (p *project) init() error {
+ dirs := []string{"branch", "commit", "object"}
+
+ for _, dir := range dirs {
+ d := filepath.Join(p.base, dir)
+
+ // Clear existing dirs when -f true.
+ if p.options.Force && dir != "branch" {
+ if err := os.RemoveAll(d); err != nil {
+ return fmt.Errorf("unable to remove directory: %v", err)
+ }
+ }
+
+ if err := os.MkdirAll(d, 0755); err != nil {
+ return fmt.Errorf("unable to create directory: %v", err)
+ }
+ }
+
+ return nil
+}
+
+// Saves a local clone of `target` repo.
+func (p *project) save() error {
+ if _, err := os.Stat(p.repo); err != nil {
+ return err
+ }
+
+ return exec.Command("git", "clone", p.options.Source, p.repo).Run()
+}
+
+func (p *project) updateBranches(branches []branch) {
+ for _, b := range branches {
+ // NOTE: Is this needed still if the repo is downloaded each time the script is run?
+ ref := fmt.Sprintf("refs/heads/%s:refs/origin/%s", b, b)
+
+ cmd := exec.Command("git", "fetch", "--force", "origin", ref)
+ cmd.Dir = p.repo
+
+ log.Printf("updating branch: %s", b)
+
+ if _, err := cmd.Output(); err != nil {
+ log.Printf("unable to fetch branch: %v", err)
+ continue
+ }
+ }
+}
+
+func (p *project) writePages(branches []branch) {
+ for _, b := range branches {
+ log.Printf("processing branch: %s", b)
+
+ go p.writeBranchPage(b)
+
+ for i, c := range b.Commits {
+ log.Printf("processing commit: %s: %d/%d", c.Abbr, i+1, len(b.Commits))
+
+ base := filepath.Join(p.base, "commit", c.Hash)
+
+ if err := os.MkdirAll(base, 0755); err != nil {
+ if err != nil {
+ log.Printf("unable to create commit directory: %v", err)
+ }
+
+ continue
+ }
+
+ for _, par := range c.Parents {
+ p.writeCommitDiff(par, c, base, b)
+ }
+
+ for _, obj := range c.Tree {
+ dst := filepath.Join(p.base, "object", obj.Dir())
+
+ if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
+ if err != nil {
+ log.Printf("unable to create object directory: %v", err)
+ }
+ continue
+ }
+
+ p.writeObjectBlob(obj, dst)
+ p.writeNom(fmt.Sprintf("%s.html", dst), obj, b, c, base)
+ }
+
+ p.writeCommitPage(base, c, b)
+ }
+ }
+}
+
+func (p *project) writeMainIndex(branches []branch) {
+ // This is the main index or project home.
+ f, err := os.Create(filepath.Join(p.base, "index.html"))
+
+ defer f.Close()
+
+ if err != nil {
+ log.Fatalf("unable to create home page: %v", err)
+ }
+
+ page := page{
+ Data: Data{
+ "Branches": branches,
+ "Link": p.options.URL,
+ "Project": p.Name,
+ },
+ Base: "./",
+ Title: p.Name,
+ }
+
+ if err := p.template.Execute(f, page); err != nil {
+ log.Fatalf("unable to apply template: %v", err)
+ }
+}
+
+func (p *project) writeCommitDiff(par string, c commit, base string, b branch) {
+ cmd := exec.Command("git", "diff", "-p", fmt.Sprintf("%s..%s", par, c.Hash))
+ cmd.Dir = p.repo
+
+ out, err := cmd.Output()
+
+ if err != nil {
+ log.Printf("unable to diff against parent: %v", err)
+
+ return
+ }
+
+ dst := filepath.Join(base, fmt.Sprintf("diff-%s.html", par))
+ f, err := os.Create(dst)
+
+ defer f.Close()
+
+ if err != nil {
+ log.Printf("unable to create commit diff to parent: %v", err)
+
+ return
+ }
+
+ page := page{
+ Data: Data{
+ "Diff": diff{
+ Body: fmt.Sprintf("%s", out),
+ Commit: c,
+ Parent: par,
+ },
+ "Project": p.Name,
+ },
+ Base: "../../",
+ Title: strings.Join([]string{p.Name, b.Name, c.Abbr}, ": "),
+ }
+
+ if err := p.template.Execute(f, page); err != nil {
+ log.Printf("unable to apply template: %v", err)
+
+ return
+ }
+}
+
+func (p *project) writeBranchPage(b branch) {
+ dst := filepath.Join(p.base, "branch", b.Name, "index.html")
+
+ if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
+ if err != nil {
+ log.Fatalf("unable to create branch directory: %v", err)
+ }
+ return
+ }
+
+ f, err := os.Create(dst)
+
+ defer f.Close()
+
+ if err != nil {
+ // TODO: Remove from branches slice?
+ log.Printf("unable to create branch page: %v", err)
+
+ return
+ }
+
+ page := page{
+ Data: Data{
+ "Commits": b.Commits,
+ "Branch": b,
+ "Project": p.Name,
+ },
+ Base: "../../",
+ Title: strings.Join([]string{p.Name, b.Name}, ": "),
+ }
+
+ if err := p.template.Execute(f, page); err != nil {
+ log.Printf("unable to apply template: %v", err)
+ return
+ }
+}
+
+func (p *project) writeObjectBlob(obj object, dst string) {
+ cmd := exec.Command("git", "cat-file", "blob", obj.Hash)
+ cmd.Dir = p.repo
+
+ out, err := cmd.Output()
+
+ if err != nil {
+ log.Printf("unable to save object: %v", err)
+ return
+ }
+
+ f, err := os.Create(dst)
+
+ defer f.Close()
+
+ if err != nil {
+ log.Printf("unable to create object: %v", err)
+ return
+ }
+
+ if _, err := f.Write(out); err != nil {
+ log.Printf("unable to write object blob: %v", err)
+ return
+ }
+}
+
+func (p *project) writeNom(nom string, obj object, b branch, c commit, base string) {
+ f, err := os.Create(nom)
+ defer f.Close()
+
+ if err != nil {
+ log.Printf("unable to create object: %v", err)
+ return
+ }
+
+ o := &show{
+ object: object{
+ Hash: obj.Hash,
+ Path: obj.Path,
+ },
+ Bin: types[filepath.Ext(obj.Path)],
+ }
+
+ if o.Bin {
+ // TODO.
+ } else {
+ cmd := exec.Command("git", "show", "--no-notes", obj.Hash)
+ cmd.Dir = p.repo
+
+ out, err := cmd.Output()
+
+ if err != nil {
+ log.Printf("unable to show object: %v", err)
+
+ return
+ }
+
+ sep := []byte("\n")
+ var lines = make([]int, bytes.Count(out, sep))
+
+ for i := range lines {
+ lines[i] = i + 1
+ }
+
+ if bytes.LastIndex(out, sep) != len(out)-1 {
+ lines = append(lines, len(lines))
+ }
+
+ o.Lines = lines
+ o.Body = fmt.Sprintf("%s", out)
+ }
+
+ page := page{
+ Data: Data{
+ "Object": *o,
+ "Project": p.Name,
+ },
+ Base: "../../",
+ Title: strings.Join([]string{p.Name, b.Name, c.Abbr, obj.Path}, ": "),
+ }
+
+ if err := p.template.Execute(f, page); err != nil {
+ log.Printf("unable to apply template: %v", err)
+ return
+ }
+
+ lnk := filepath.Join(base, fmt.Sprintf("%s.html", obj.Path))
+
+ if err := os.MkdirAll(filepath.Dir(lnk), 0755); err != nil {
+ if err != nil {
+ log.Printf("unable to create hard link path: %v", err)
+ }
+ return
+ }
+
+ if err := os.Link(nom, lnk); err != nil {
+ if os.IsExist(err) {
+ return
+ }
+
+ log.Printf("unable to hard link object into commit folder: %v", err)
+ }
+}
+
+func (p *project) writeCommitPage(base string, c commit, b branch) {
+ dst := filepath.Join(base, "index.html")
+ f, err := os.Create(dst)
+
+ defer f.Close()
+
+ if err != nil {
+ log.Printf("unable to create commit page: %v", err)
+ // TODO(spike): handle error?
+ return
+ }
+
+ page := page{
+ Data: Data{
+ "Commit": c,
+ "Project": p.Name,
+ },
+ Base: "../../",
+ Title: strings.Join([]string{p.Name, b.Name, c.Abbr}, ": "),
+ }
+
+ if err := p.template.Execute(f, page); err != nil {
+ log.Printf("unable to apply template: %v", err)
+ return
+ }
+}
diff --git a/types.go b/types.go
new file mode 100644
index 0000000..7966a44
--- /dev/null
+++ b/types.go
@@ -0,0 +1,82 @@
+package main
+
+import (
+ "path/filepath"
+ "time"
+)
+
+// Data is the generic content map passed on to the page template.
+type Data map[string]interface{}
+type page struct {
+ Data
+ Base string
+ Stylesheet string
+ Title string
+}
+
+type branch struct {
+ Commits []commit
+ Name string
+ Project string
+}
+
+func (b branch) String() string {
+ return b.Name
+}
+
+type diff struct {
+ Body string
+ Commit commit
+ Parent string
+}
+
+type overview struct {
+ Body string
+ Hash string
+ Parent string
+}
+
+type hash struct {
+ Hash string
+ Short string
+}
+
+func (h hash) String() string {
+ return h.Hash
+}
+
+type object struct {
+ Hash string
+ Path string
+}
+
+func (o object) Dir() string {
+ return filepath.Join(o.Hash[0:2], o.Hash[2:])
+}
+
+type show struct {
+ Body string
+ Bin bool
+ Lines []int
+ object
+}
+
+type commit struct {
+ Branch string
+ Body string
+ Abbr string
+ History []overview
+ Parents []string
+ Hash string
+ Author author
+ Date time.Time
+ Project string
+ Tree []object
+ Types map[string]bool
+ Subject string
+}
+
+type author struct {
+ Email string
+ Name string
+}