Changes
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/main.go b/main.go
index ff52b22..b831dab 100644
--- a/main.go
+++ b/main.go
@@ -102,48 +102,6 @@ type author struct {
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() {
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/parsers.go b/parsers.go
new file mode 100644
index 0000000..eac48ab
--- /dev/null
+++ b/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/project.go b/project.go
new file mode 100644
index 0000000..9bad5f0
--- /dev/null
+++ b/project.go
@@ -0,0 +1,277 @@
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+// 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
+ Branches []branch
+ Name string
+ repo string
+}
+
+// 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
+}