diff --git a/main.go b/main.go index 720f8c8..b9da049 100644 --- a/main.go +++ b/main.go @@ -15,18 +15,29 @@ import ( "os/exec" "path/filepath" "reflect" + "regexp" "strings" "sync" "text/tabwriter" "time" ) -// SEP is a UUID v4 used to separate out commit line items. +// 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\/(.*?)$`) + type project struct { base string Branches []branch @@ -34,10 +45,13 @@ type project struct { repo string } +// Data is the generic content map passed on to the page template. +type Data map[string]interface{} type page struct { Breadcrumbs []string - Data map[string]interface{} - Title string + Data + Stylesheet string + Title string } type branch struct { @@ -50,6 +64,18 @@ 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 @@ -68,7 +94,7 @@ type commit struct { Branch string Body string Abbr string - History []string + History []overview Parents []string Graph string Hash string @@ -208,8 +234,6 @@ func main() { flagset[f.Name] = true }) - log.Print(flagset) - ref := reflect.ValueOf(store) tab := tabwriter.NewWriter(log.Writer(), 0, 0, 0, '.', 0) @@ -275,29 +299,33 @@ func main() { log.Printf("user cache set: %s", tmp) - prj := &project{ + pro := &project{ base: dir, Name: opt.Name, repo: tmp, } // Create base directories. - if err := prj.init(opt.Force); err != nil { + if err := pro.init(opt.Force); err != nil { log.Fatalf("unable to initialize output directory: %v", err) } // Clone target repo. - if err := prj.save(opt.Source); err != nil { + if err := pro.save(opt.Source); err != nil { log.Fatalf("unable to set up repo: %v", err) } - branches, err := prj.branchfilter(opt.Branches) + branches, err := pro.branchfilter(opt.Branches) 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 { @@ -305,21 +333,25 @@ func main() { ref := fmt.Sprintf("refs/heads/%s:refs/origin/%s", b, b) cmd := exec.Command("git", "fetch", "--force", "origin", ref) - cmd.Dir = prj.repo + 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) wg.Add(1) go func() { defer wg.Done() - dst := filepath.Join(prj.base, "branch", b.Name, "index.html") + dst := filepath.Join(pro.base, "branch", b.Name, "index.html") if err := os.MkdirAll(filepath.Dir(dst), 0750); err != nil { if err != nil { @@ -340,13 +372,12 @@ func main() { return } - t := template.Must(template.New("branch").Parse(tpl)) p := page{ - Data: map[string]interface{}{ + Data: Data{ "Commits": b.Commits, - "Project": prj.Name, + "Project": pro.Name, }, - Title: strings.Join([]string{prj.Name, b.Name}, ": "), + Title: strings.Join([]string{pro.Name, b.Name}, ": "), } if err := t.Execute(f, p); err != nil { @@ -355,13 +386,11 @@ func main() { return } }() - } - for _, b := range branches { for i, c := range b.Commits { log.Printf("processing commit: %s: %d/%d", c.Abbr, i+1, len(b.Commits)) - base := filepath.Join(prj.base, "commit", c.Hash) + base := filepath.Join(pro.base, "commit", c.Hash) if err := os.MkdirAll(base, 0750); err != nil { if err != nil { @@ -371,15 +400,14 @@ func main() { continue } - for _, psh := range c.Parents { + for _, par := range c.Parents { wg.Add(1) go func() { defer wg.Done() - // NOTE: Use <em>, <ins>, and <del> instead of blue, green, red <font> elements - cmd := exec.Command("git", "diff", "-p", fmt.Sprintf("%s..%s", psh, c.Hash)) - cmd.Dir = prj.repo + cmd := exec.Command("git", "diff", "-p", fmt.Sprintf("%s..%s", par, c.Hash)) + cmd.Dir = pro.repo out, err := cmd.Output() @@ -389,32 +417,27 @@ func main() { return } - dst := filepath.Join(base, fmt.Sprintf("diff-to-%s.html", psh)) + 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 page: %v", err) + log.Printf("unable to create commit diff to parent: %v", err) return } - t := template.Must(template.New("diff").Parse(tpl)) p := page{ - Data: map[string]interface{}{ - "Diff": struct { - Body string - Commit commit - Parent string - }{ + Data: Data{ + "Diff": diff{ Body: fmt.Sprintf("%s", out), Commit: c, - Parent: psh, + Parent: par, }, - "Project": prj.Name, + "Project": pro.Name, }, - Title: strings.Join([]string{prj.Name, b.Name, c.Abbr}, ": "), + Title: strings.Join([]string{pro.Name, b.Name, c.Abbr}, ": "), } if err := t.Execute(f, p); err != nil { @@ -426,7 +449,7 @@ func main() { } for _, obj := range c.Tree { - dst := filepath.Join(prj.base, "object", obj.Hash[0:2], obj.Hash) + dst := filepath.Join(pro.base, "object", obj.Hash[0:2], obj.Hash) if err := os.MkdirAll(filepath.Dir(dst), 0750); err != nil { if err != nil { @@ -438,7 +461,7 @@ func main() { func(name string) { cmd := exec.Command("git", "show", "--no-notes", obj.Hash) - cmd.Dir = prj.repo + cmd.Dir = pro.repo out, err := cmd.Output() @@ -464,9 +487,8 @@ func main() { lines[i] = i + 1 } - t := template.Must(template.New("object").Parse(tpl)) p := page{ - Data: map[string]interface{}{ + Data: Data{ "Object": struct { Body string Hash string @@ -476,9 +498,9 @@ func main() { Hash: obj.Hash, Lines: lines, }, - "Project": prj.Name, + "Project": pro.Name, }, - Title: strings.Join([]string{prj.Name, b.Name, c.Abbr, obj.Path}, ": "), + Title: strings.Join([]string{pro.Name, b.Name, c.Abbr, obj.Path}, ": "), } if err := t.Execute(f, p); err != nil { @@ -508,7 +530,7 @@ func main() { func(name string) { cmd := exec.Command("git", "cat-file", "blob", obj.Hash) - cmd.Dir = prj.repo + cmd.Dir = pro.repo out, err := cmd.Output() @@ -536,11 +558,7 @@ func main() { }(dst) } - wg.Add(1) - - go func() { - defer wg.Done() - + func() { dst := filepath.Join(base, "index.html") f, err := os.Create(dst) @@ -552,13 +570,12 @@ func main() { return } - t := template.Must(template.New("commit").Parse(tpl)) p := page{ - Data: map[string]interface{}{ + Data: Data{ "Commit": c, - "Project": prj.Name, + "Project": pro.Name, }, - Title: strings.Join([]string{prj.Name, b.Name, c.Abbr}, ": "), + Title: strings.Join([]string{pro.Name, b.Name, c.Abbr}, ": "), } if err := t.Execute(f, p); err != nil { @@ -576,7 +593,7 @@ func main() { defer wg.Done() // This is the main index or project home. - f, err := os.Create(filepath.Join(prj.base, "index.html")) + f, err := os.Create(filepath.Join(pro.base, "index.html")) defer f.Close() @@ -584,14 +601,13 @@ func main() { log.Fatalf("unable to create home page: %v", err) } - t := template.Must(template.New("home").Parse(tpl)) p := page{ - Data: map[string]interface{}{ + Data: Data{ "Branches": branches, "Link": opt.URL, - "Project": prj.Name, + "Project": pro.Name, }, - Title: prj.Name, + Title: pro.Name, } if err := t.Execute(f, p); err != nil { @@ -603,11 +619,11 @@ func main() { } // Creates base directories for holding objects, branches, and commits. -func (prj *project) init(f bool) error { +func (pro *project) init(f bool) error { dirs := []string{"branch", "commit", "object"} for _, dir := range dirs { - d := filepath.Join(prj.base, dir) + d := filepath.Join(pro.base, dir) // Clear existing dirs when -f true. if f && dir != "branch" { @@ -625,18 +641,18 @@ func (prj *project) init(f bool) error { } // Saves a local clone of `target` repo. -func (prj *project) save(target string) error { - if _, err := os.Stat(prj.repo); err != nil { +func (pro *project) save(target string) error { + if _, err := os.Stat(pro.repo); err != nil { return err } - return exec.Command("git", "clone", target, prj.repo).Run() + return exec.Command("git", "clone", target, pro.repo).Run() } // Goes through list of branches and returns those that match whitelist. -func (prj *project) branchfilter(whitelist manyflag) ([]branch, error) { +func (pro *project) branchfilter(whitelist manyflag) ([]branch, error) { cmd := exec.Command("git", "branch", "-a") - cmd.Dir = prj.repo + cmd.Dir = pro.repo out, err := cmd.Output() @@ -644,15 +660,16 @@ func (prj *project) branchfilter(whitelist manyflag) ([]branch, error) { 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(scanner.Text()) + t := strings.TrimSpace(strings.TrimPrefix(scanner.Text(), "*")) _, f := filepath.Split(t) - m[f] = !strings.Contains(f, "HEAD") + m[f] = true } if err := scanner.Err(); err != nil { @@ -664,32 +681,42 @@ func (prj *project) branchfilter(whitelist manyflag) ([]branch, error) { 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) + } } - // Fill in resulting slice with desired branches. - var results []branch - for k, v := range m { if v { - commits, err := prj.commitparser(k) + // TODO: Try a goroutine? + commits, err := pro.commitparser(k) if err != nil { continue } - results = append(results, branch{commits, k, prj.Name}) + 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 (prj *project) commitparser(b string) ([]commit, error) { +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", "--graph", fmt.Sprintf("--format=%s", fst), ref) - cmd.Dir = prj.repo + cmd.Dir = pro.repo out, err := cmd.Output() @@ -715,7 +742,7 @@ func (prj *project) commitparser(b string) ([]commit, error) { continue } - body, err := prj.bodyparser(h) + body, err := pro.bodyparser(h) if err != nil { log.Printf("unable to parse commit body: %s", err) @@ -723,7 +750,7 @@ func (prj *project) commitparser(b string) ([]commit, error) { continue } - tree, err := prj.treeparser(h) + tree, err := pro.treeparser(h) if err != nil { log.Printf("unable to parse commit tree: %s", err) @@ -731,7 +758,7 @@ func (prj *project) commitparser(b string) ([]commit, error) { continue } - var history []string + var history []overview var parents []string if data[1] != "" { @@ -739,7 +766,7 @@ func (prj *project) commitparser(b string) ([]commit, error) { } for _, p := range parents { - diffstat, err := prj.diffparser(h, p) + diffstat, err := pro.diffstatparser(h, p) if err != nil { log.Printf("unable to diff stat against parent: %s", err) @@ -747,22 +774,22 @@ func (prj *project) commitparser(b string) ([]commit, error) { continue } - history = append(history, diffstat) + history = append(history, overview{diffstat, h, p}) } c := commit{ Abbr: data[6], Author: a, - Branch: b, Body: body, + Branch: b, Date: date, Hash: h, History: history, - Tree: tree, Graph: g, Parents: parents, - Project: prj.Name, + Project: pro.Name, Subject: data[2], + Tree: tree, } results = append(results, c) @@ -775,10 +802,10 @@ func (prj *project) commitparser(b string) ([]commit, error) { return results, nil } -func (prj *project) treeparser(h string) ([]object, error) { +func (pro *project) treeparser(h string) ([]object, error) { // git ls-tree --format='%(objectname) %(path)' <tree-ish> cmd := exec.Command("git", "ls-tree", "-r", "--format=%(objectname) %(path)", h) - cmd.Dir = prj.repo + cmd.Dir = pro.repo out, err := cmd.Output() @@ -801,10 +828,9 @@ func (prj *project) treeparser(h string) ([]object, error) { return results, nil } -func (prj *project) diffparser(h, p string) (string, error) { - // histo, file, changes, sum +func (pro *project) diffstatparser(h, p string) (string, error) { cmd := exec.Command("git", "diff", "--stat", fmt.Sprintf("%s..%s", p, h)) - cmd.Dir = prj.repo + cmd.Dir = pro.repo out, err := cmd.Output() @@ -815,23 +841,17 @@ func (prj *project) diffparser(h, p string) (string, error) { var results []string feed := strings.Split(strings.TrimSuffix(fmt.Sprintf("%s", out), "\n"), "\n") - for i, line := range feed { - if i < len(feed) { - // TODO: Parse filenames and stats. - } else { - // Last line needs no parsing. - } - + for _, line := range feed { results = append(results, strings.TrimSpace(line)) } return strings.Join(results, "\n"), nil } -func (prj *project) bodyparser(h string) (string, error) { +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 = prj.repo + cmd.Dir = pro.repo out, err := cmd.Output() @@ -841,3 +861,72 @@ func (prj *project) bodyparser(h string) (string, error) { 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) + } + + return template.HTML(strings.Join(results, "\n")) +}
home › develop › f874ede › 9c7a467