diff --git a/main.go b/main.go index 35bb7f9..2da686d 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,7 @@ import ( "path/filepath" "reflect" "strings" + "sync" "text/tabwriter" "time" ) @@ -35,27 +36,52 @@ var cTmpl string //go:embed diff.html.tmpl var dTmpl string +//go:embed object.html.tmpl +var oTmpl string + type repository struct { - base string - name string - path string + base string + Branches []branch + Name string + temp string } type branch struct { - Commits []*commit + Commits []commit Name string + Project string } func (b branch) String() string { return b.Name } +type hash struct { + Hash string + Short string +} + +func (h hash) String() string { + return h.Hash +} + +type object struct { + Hash string + Path string +} + type commit struct { + Branch string + Body string + Abbr string + History []string + Parents []string Graph string Hash string Author author Date time.Time - Parent string + Project string + Tree []object Subject string } @@ -91,14 +117,14 @@ type options struct { } // Helps store options into a JSON config file. -func (o *options) save(out string) error { +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(out, o.config), bs, 0644); err != nil { + if err := os.WriteFile(filepath.Join(p, o.config), bs, 0644); err != nil { return fmt.Errorf("unable to save config file: %v", err) } @@ -142,11 +168,11 @@ func main() { } // Defaults to the current working directory if no argument present. - out := flag.Arg(0) + outpath := flag.Arg(0) - // Make sure `out` is an absolute path. - if ok := filepath.IsAbs(out); !ok { - out = filepath.Join(cwd, out) + // Make sure `outpath` is an absolute path. + if ok := filepath.IsAbs(outpath); !ok { + outpath = filepath.Join(cwd, outpath) } // Create a separate options instance for reading config file values into. @@ -156,7 +182,7 @@ func main() { store.Branches = append(store.Branches, opt.Branches...) // Attempt to read saved settings. - bs, err := os.ReadFile(filepath.Join(out, opt.config)) + bs, err := os.ReadFile(filepath.Join(outpath, opt.config)) if err != nil { log.Printf("unable to read config file: %v", err) @@ -216,13 +242,13 @@ func main() { } } - // Make sure `out` exists. - if err := os.MkdirAll(out, 0750); err != nil { + // Make sure `outpath` exists. + if err := os.MkdirAll(outpath, 0750); err != nil { log.Fatalf("unable to create output directory: %v", err) } // Save current settings for future use. - if err := opt.save(out); err != nil { + if err := opt.save(outpath); err != nil { log.Fatalf("unable to save options: %v", err) } @@ -232,116 +258,327 @@ func main() { log.Fatalf("unable to locate user cache folder: %s", err) } - p, err := os.MkdirTemp(ucd, "gtx") + p, err := os.MkdirTemp(ucd, "gtx-*") if err != nil { log.Fatalf("unable to locate temporary host dir: %s", err) } + log.Printf("user cache set: %s", p) + repo := &repository{ - base: out, - name: opt.Project, - path: p, + base: outpath, + Name: opt.Project, + temp: p, } + // Create base directories. if err := repo.init(opt.Force); err != nil { log.Fatalf("unable to initialize output directory: %v", err) } - // Get an up to date copy. + // Clone target repo. if err := repo.save(opt.Repo); err != nil { log.Fatalf("unable to set up repo: %v", err) } - branches, err := repo.branchfinder(opt.Branches) + branches, err := repo.branchfilter(opt.Branches) if err != nil { log.Fatalf("unable to filter branches: %v", err) } + var wg sync.WaitGroup + // 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 = repo.path + cmd.Dir = repo.temp if _, err := cmd.Output(); err != nil { log.Printf("unable to fetch branch: %v", err) continue } - } - for _, b := range branches { - c, err := repo.commitparser(b.Name) + log.Printf("processing branch: %s", b) + wg.Add(1) - if err != nil { - log.Printf("unable to parse %s commit objects: %v", b, err) + go func() { + defer wg.Done() - continue - } + dst := filepath.Join(outpath, "branch", b.Name, "index.html") + + if err := os.MkdirAll(filepath.Dir(dst), 0750); 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 + } - b.Commits = c + t := template.Must(template.New("branch").Parse(bTmpl)) + + if err := t.Execute(f, b); err != nil { + log.Printf("unable to apply template: %v", err) + + return + } + }() } for _, b := range branches { - f, err := os.Create(filepath.Join(out, fmt.Sprintf("%s.html", b))) + for i, c := range b.Commits { + log.Printf("processing commit: %s: %d/%d", c.Abbr, i+1, len(b.Commits)) - defer f.Close() + base := filepath.Join(outpath, "commit", c.Hash) - if err != nil { - log.Fatalf("unable to create branch index: %v", err) - } + if err := os.MkdirAll(base, 0750); err != nil { + if err != nil { + log.Printf("unable to create commit directory: %v", err) + } + + continue + } + + for _, parent := range c.Parents { + wg.Add(1) + + go func() { + defer wg.Done() + + dst := filepath.Join(base, fmt.Sprintf("diff-to-%s.html", parent)) + f, err := os.Create(dst) - t := template.Must(template.New("branch").Parse(bTmpl)) + defer f.Close() + + if err != nil { + log.Printf("unable to create commit diff to parent page: %v", err) + + return + } + + t := template.Must(template.New("diff").Parse(dTmpl)) + + // NOTE: Use <em>, <ins>, and <del> instead of blue, green, red <font> elements + cmd := exec.Command("git", "diff", "-p", fmt.Sprintf("%s..%s", parent, c.Hash)) + cmd.Dir = repo.temp + + out, err := cmd.Output() + + if err != nil { + log.Printf("unable to diff against parent: %v", err) + + return + } + + body := fmt.Sprintf("%s", out) + data := struct { + Body string + Commit commit + // TODO: Make this a hash type? + Parent string + }{ + Body: body, + Commit: c, + Parent: parent, + } + + if err := t.Execute(f, data); err != nil { + log.Printf("unable to apply template: %v", err) + + return + } + }() + } - if err := t.Execute(f, b); err != nil { - log.Fatalf("unable to apply branch template: %v", err) + for _, object := range c.Tree { + dst := filepath.Join(outpath, "object", object.Hash[0:2], object.Hash) + + if err := os.MkdirAll(filepath.Dir(dst), 0750); err != nil { + if err != nil { + log.Printf("unable to create object directory: %v", err) + } + + continue + } + + wg.Add(1) + + go func(name string) { + defer wg.Done() + + cmd := exec.Command("git", "show", "--no-notes", object.Hash) + cmd.Dir = repo.temp + + out, err := cmd.Output() + + if err != nil { + log.Printf("unable to show object: %v", err) + + return + } + + f, err := os.Create(name) + + defer f.Close() + + if err != nil { + log.Printf("unable to create object: %v", err) + + return + } + + body := fmt.Sprintf("%s", out) + data := struct { + Body string + Hash string + Project string + }{ + Body: body, + Hash: object.Hash, + Project: opt.Project, + } + + t := template.Must(template.New("object").Parse(oTmpl)) + + if err := t.Execute(f, data); err != nil { + log.Printf("unable to apply template: %v", err) + + return + } + + link := filepath.Join(base, fmt.Sprintf("%s.html", object.Path)) + + if err := os.MkdirAll(filepath.Dir(link), 0750); err != nil { + if err != nil { + log.Printf("unable to create hard link path: %v", err) + } + + return + } + + if err := os.Link(name, link); 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(name string) { + cmd := exec.Command("git", "cat-file", "blob", object.Hash) + cmd.Dir = repo.temp + + 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 + } + }(dst) + } + + wg.Add(1) + + go func() { + defer wg.Done() + + 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 + } + + t := template.Must(template.New("commit").Parse(cTmpl)) + + if err := t.Execute(f, c); err != nil { + log.Printf("unable to apply template: %v", err) + + return + } + }() } } - // NOTE: Why is this even necessary? - top := branches[0] - cmd := exec.Command("git", "checkout", filepath.Join("origin", top.Name)) - cmd.Dir = repo.path + wg.Add(1) - if err := cmd.Run(); err != nil { - log.Printf("unable to checkout default branch: %v", err) - } + go func() { + defer wg.Done() - // This is the main index or repo home. - ri, err := os.Create(filepath.Join(out, "index.html")) + // This is the main index or project home. + f, err := os.Create(filepath.Join(outpath, "index.html")) - defer ri.Close() + defer f.Close() - if err != nil { - log.Fatalf("unable to create home: %v", err) - } + if err != nil { + log.Fatalf("unable to create home page: %v", err) + } - rt := template.Must(template.New("home").Parse(rTmpl)) - rd := struct { - Branches []*branch - Link string - Project string - }{ - Branches: branches, - Link: opt.URL, - Project: opt.Project, - } + t := template.Must(template.New("home").Parse(rTmpl)) + data := struct { + Branches []branch + Link string + Project string + }{ + Branches: branches, + Link: opt.URL, + Project: opt.Project, + } - if err := rt.Execute(ri, rd); err != nil { - log.Fatalf("unable to apply home template: %v", err) - } + if err := t.Execute(f, data); err != nil { + log.Fatalf("unable to apply template: %v", err) + } + }() + + wg.Wait() } +// Creates base directories for holding objects, branches, and commits. func (r *repository) init(f bool) error { dirs := []string{"branch", "commit", "object"} for _, dir := range dirs { d := filepath.Join(r.base, dir) - // Clear existing dirs when -force true. + // 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) @@ -356,55 +593,19 @@ func (r *repository) init(f bool) error { return nil } +// Saves a local clone of `target` repo. func (r *repository) save(target string) error { - _, err := os.Stat(r.path) - - if err := exec.Command("git", "clone", target, r.path).Run(); err != nil { - return err - } - - // NOTE: Should this be in a separate method? - cmd := exec.Command("git", "branch", "-l") - cmd.Dir = r.path - out, err := cmd.Output() - - if err != nil { - return err - } - - all := fmt.Sprintf("%s", out) - - // NOTE: Requires go1.18. - _, star, found := strings.Cut(all, "*") - - if !found { - return fmt.Errorf("unable to locate the default branch") - } - - star = strings.TrimSpace(star) - star = strings.TrimRight(star, "\n") - - // NOTE: Not sure why this is added in the original. - // star = filepath.Join("origin", star) - - cmd = exec.Command("git", "checkout", "--detach", star) - cmd.Dir = r.path - err = cmd.Run() - - if err != nil { + if _, err := os.Stat(r.temp); err != nil { return err } - cmd = exec.Command("git", "branch", "-D", star) - cmd.Dir = r.path - err = cmd.Run() - - return err + return exec.Command("git", "clone", target, r.temp).Run() } -func (r *repository) branchfinder(bf manyflag) ([]*branch, error) { +// Goes through list of branches and returns those that match whitelist. +func (r *repository) branchfilter(whitelist manyflag) ([]branch, error) { cmd := exec.Command("git", "branch", "-a") - cmd.Dir = r.path + cmd.Dir = r.temp out, err := cmd.Output() @@ -412,7 +613,6 @@ func (r *repository) branchfinder(bf manyflag) ([]*branch, error) { return nil, err } - var results []*branch var m = make(map[string]bool) scanner := bufio.NewScanner(bytes.NewReader(out)) @@ -429,28 +629,36 @@ func (r *repository) branchfinder(bf manyflag) ([]*branch, error) { } // Filter to match options, but return all if no branch flags given. - if len(bf) > 0 { + if len(whitelist) > 0 { for k := range m { - m[k] = contains(bf, k) + m[k] = contains(whitelist, k) } } - // Transfer desired branch names to resulting slice. + // Fill in resulting slice with desired branches. + var results []branch + for k, v := range m { if v { - results = append(results, &branch{Name: k}) + commits, err := r.commitparser(k) + + if err != nil { + continue + } + + results = append(results, branch{commits, k, r.Name}) } } return results, nil } -func (r *repository) commitparser(b string) ([]*commit, error) { - fst := strings.Join([]string{"%H", "%P", "%s", "%aN", "%aE", "%aD"}, SEP) +func (r *repository) 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 = r.path + cmd := exec.Command("git", "log", "--graph", fmt.Sprintf("--format=%s", fst), ref) + cmd.Dir = r.temp out, err := cmd.Output() @@ -458,24 +666,71 @@ func (r *repository) commitparser(b string) ([]*commit, error) { return nil, err } - results := []*commit{} + results := []commit{} scanner := bufio.NewScanner(bytes.NewReader(out)) for scanner.Scan() { data := strings.Split(scanner.Text(), SEP) + k := strings.Split(data[0], " ") + g, h := k[0], k[1] a := author{data[4], data[3]} - d, err := time.Parse(time.RFC1123Z, data[5]) + + 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 } - c := &commit{ + body, err := r.bodyparser(h) + + if err != nil { + log.Printf("unable to parse commit body: %s", err) + + continue + } + + tree, err := r.treeparser(h) + + if err != nil { + log.Printf("unable to parse commit tree: %s", err) + + continue + } + + var history []string + var parents []string + + if data[1] != "" { + parents = strings.Split(data[1], " ") + } + + for _, p := range parents { + diffstat, err := r.diffparser(h, p) + + if err != nil { + log.Printf("unable to diff stat against parent: %s", err) + + continue + } + + history = append(history, diffstat) + } + + c := commit{ + Abbr: data[6], Author: a, - Date: d, - Hash: data[0], - Parent: data[1], + Branch: b, + Body: body, + Date: date, + Hash: h, + History: history, + Tree: tree, + Graph: g, + Parents: parents, + Project: r.Name, Subject: data[2], } @@ -488,3 +743,70 @@ func (r *repository) commitparser(b string) ([]*commit, error) { return results, nil } + +func (r *repository) 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 = r.temp + + 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 (r *repository) diffparser(h, p string) (string, error) { + // histo, file, changes, sum + cmd := exec.Command("git", "diff", "--stat", fmt.Sprintf("%s..%s", p, h)) + cmd.Dir = r.temp + + out, err := cmd.Output() + + if err != nil { + return "", err + } + + 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. + } + + results = append(results, strings.TrimSpace(line)) + } + + return strings.Join(results, "\n"), nil +} + +func (r *repository) 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 = r.temp + + out, err := cmd.Output() + + if err != nil { + return "", err + } + + return strings.TrimSuffix(fmt.Sprintf("%s", out), "\n"), nil +}
home › develop › 4bfb0b0 › 0e489b8