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 +}
home › develop › 8eacca6 › 8450e8b