gtx


Branch: develop

Author
Sotiri Bakagiannis <thewhodidthis@users.noreply.github.com>
Date
Jan. 31 '23 12:40:24
Commit
8450e8b75c2353ca82b160b807544c3ecd9f37be
Parent
8eacca65707d833480645e00147993321c75e02e
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
+}