gtx


Branch: develop

Author
Spike Lindsey <spike01@gmail.com>
Date
Jan. 30 '23 17:23:55
Commit
418f38df5f9017558291e6b6bdbe41effe3e68b8
Parent
62b8370849a9f6b86c3514dde5370fbf7c31bf9c
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
+}