diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..42b9b8b --- /dev/null +++ b/flags.go @@ -0,0 +1,19 @@ +package main + +import "strings" + +// https://stackoverflow.com/questions/28322997/how-to-get-a-list-of-values-into-a-flag-in-golang/ +type manyflag []string + +func (f *manyflag) Set(value string) error { + // Make sure there are no duplicates. + if !contains(*f, value) { + *f = append(*f, value) + } + + return nil +} + +func (f *manyflag) String() string { + return strings.Join(*f, ", ") +} diff --git a/main.go b/main.go index ff52b22..b831dab 100644 --- a/main.go +++ b/main.go @@ -102,48 +102,6 @@ type author struct { Name string } -// https://stackoverflow.com/questions/28322997/how-to-get-a-list-of-values-into-a-flag-in-golang/ -type manyflag []string - -func (f *manyflag) Set(value string) error { - // Make sure there are no duplicates. - if !contains(*f, value) { - *f = append(*f, value) - } - - return nil -} - -func (f *manyflag) String() string { - return strings.Join(*f, ", ") -} - -type options struct { - Branches manyflag `json:"branches"` - config string - Force bool `json:"force"` - Name string `json:"name"` - Quiet bool `json:"quiet"` - Source string `json:"source"` - Template string `json:"template"` - URL string `json:"url"` -} - -// Helps store options into a JSON config file. -func (o *options) save(p string) error { - bs, err := json.MarshalIndent(o, "", " ") - - if err != nil { - return fmt.Errorf("unable to encode config file: %v", err) - } - - if err := os.WriteFile(filepath.Join(p, o.config), bs, 0644); err != nil { - return fmt.Errorf("unable to save config file: %v", err) - } - - return nil -} - func init() { // Override default usage output. flag.Usage = func() { diff --git a/options.go b/options.go new file mode 100644 index 0000000..dc74081 --- /dev/null +++ b/options.go @@ -0,0 +1,34 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" +) + +type options struct { + Branches manyflag `json:"branches"` + config string + Force bool `json:"force"` + Name string `json:"name"` + Quiet bool `json:"quiet"` + Source string `json:"source"` + Template string `json:"template"` + URL string `json:"url"` +} + +// Helps store options into a JSON config file. +func (o *options) save(p string) error { + bs, err := json.MarshalIndent(o, "", " ") + + if err != nil { + return fmt.Errorf("unable to encode config file: %v", err) + } + + if err := os.WriteFile(filepath.Join(p, o.config), bs, 0644); err != nil { + return fmt.Errorf("unable to save config file: %v", err) + } + + return nil +} diff --git a/parsers.go b/parsers.go new file mode 100644 index 0000000..eac48ab --- /dev/null +++ b/parsers.go @@ -0,0 +1,87 @@ +package main + +import ( + "fmt" + "html/template" + "regexp" + "strings" +) + +// Match diff body @@ del, ins line numbers. +var aline = regexp.MustCompile(`\-(.*?),`) +var bline = regexp.MustCompile(`\+(.*?),`) + +// Match diff body keywords. +var xline = regexp.MustCompile(`^(deleted|index|new|rename|similarity)`) + +// Helps target file specific diff blocks. +var diffanchor = regexp.MustCompile(`b\/(.*?)$`) + +func diffbodyparser(d diff) template.HTML { + var results []string + feed := strings.Split(strings.TrimSuffix(template.HTMLEscapeString(d.Body), "\n"), "\n") + + var a, b string + + for _, line := range feed { + if strings.HasPrefix(line, "diff") { + line = diffanchor.ReplaceAllString(line, `b/<a id="$1">$1</a>`) + line = fmt.Sprintf("<strong>%s</strong>", line) + } + + line = xline.ReplaceAllString(line, "<em>$1</em>") + + if strings.HasPrefix(line, "@@") { + if a != "" && !strings.HasPrefix(a, "---") { + repl := fmt.Sprintf(`<a href="commit/%s/%s.html#L$1">-$1</a>,`, d.Parent, a) + line = aline.ReplaceAllString(line, repl) + } + + if b != "" && !strings.HasPrefix(b, "+++") { + repl := fmt.Sprintf(`<a href="commit/%s/%s.html#L$1">+$1</a>,`, d.Commit.Hash, b) + line = bline.ReplaceAllString(line, repl) + } + } + + if strings.HasPrefix(line, "---") { + a = strings.TrimPrefix(line, "--- a/") + line = fmt.Sprintf("<mark>%s</mark>", line) + } else if strings.HasPrefix(line, "-") { + line = fmt.Sprintf("<del>%s</del>", line) + } + + if strings.HasPrefix(line, "+++") { + b = strings.TrimPrefix(line, "+++ b/") + line = fmt.Sprintf("<mark>%s</mark>", line) + } else if strings.HasPrefix(line, "+") { + line = fmt.Sprintf("<ins>%s</ins>", line) + } + + results = append(results, line) + } + + return template.HTML(strings.Join(results, "\n")) +} + +func diffstatbodyparser(o overview) template.HTML { + var results []string + feed := strings.Split(strings.TrimSuffix(o.Body, "\n"), "\n") + + for i, line := range feed { + if i < len(feed)-1 { + // Link files to corresponding diff. + columns := strings.Split(line, "|") + files := strings.Split(columns[0], "=>") + + a := strings.TrimSpace(files[len(files)-1]) + b := fmt.Sprintf(`<a href="commit/%s/diff-%s.html#%s">%s</a>`, o.Hash, o.Parent, a, a) + l := strings.LastIndex(line, a) + + line = line[:l] + strings.Replace(line[l:], a, b, 1) + } + + results = append(results, line) + } + + return template.HTML(strings.Join(results, "\n")) +} diff --git a/project.go b/project.go new file mode 100644 index 0000000..9bad5f0 --- /dev/null +++ b/project.go @@ -0,0 +1,277 @@ +package main + +import ( + "bufio" + "bytes" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" +) + +// SEP is a browser generated UUID v4 used to separate out commit line items. +const SEP = "6f6c1745-e902-474a-9e99-08d0084fb011" + +// Helps keep track of file extensions git thinks of as binary. +var types = make(map[string]bool) + +type project struct { + base string + Branches []branch + Name string + repo string +} + +// Creates base directories for holding objects, branches, and commits. +func (pro *project) init(f bool) error { + dirs := []string{"branch", "commit", "object"} + + for _, dir := range dirs { + d := filepath.Join(pro.base, dir) + + // Clear existing dirs when -f true. + if f && dir != "branch" { + if err := os.RemoveAll(d); err != nil { + return fmt.Errorf("unable to remove directory: %v", err) + } + } + + if err := os.MkdirAll(d, 0755); err != nil { + return fmt.Errorf("unable to create directory: %v", err) + } + } + + return nil +} + +// Saves a local clone of `target` repo. +func (pro *project) save(target string) error { + if _, err := os.Stat(pro.repo); err != nil { + return err + } + + return exec.Command("git", "clone", target, pro.repo).Run() +} + +// Goes through list of branches and returns those that match whitelist. +func (pro *project) branchfilter(whitelist manyflag) ([]branch, error) { + cmd := exec.Command("git", "branch", "-a") + cmd.Dir = pro.repo + + out, err := cmd.Output() + + if err != nil { + return nil, err + } + + var b = make(map[string]branch) + var m = make(map[string]bool) + + scanner := bufio.NewScanner(bytes.NewReader(out)) + + for scanner.Scan() { + t := strings.TrimSpace(strings.TrimPrefix(scanner.Text(), "*")) + _, f := filepath.Split(t) + + m[f] = true + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + // Filter to match options, but return all if no branch flags given. + if len(whitelist) > 0 { + for k := range m { + m[k] = contains(whitelist, k) + } + } else { + // In git given order at this point. + for k := range m { + whitelist = append(whitelist, k) + } + } + + for k, v := range m { + if v { + // TODO: Try a goroutine? + commits, err := pro.commitparser(k) + + if err != nil { + continue + } + + b[k] = branch{commits, k, pro.Name} + } + } + + // Fill in resulting slice with desired branches in order. + var results []branch + + for _, v := range whitelist { + results = append(results, b[v]) + } + + return results, nil +} + +func (pro *project) commitparser(b string) ([]commit, error) { + fst := strings.Join([]string{"%H", "%P", "%s", "%aN", "%aE", "%aD", "%h"}, SEP) + ref := fmt.Sprintf("origin/%s", b) + + cmd := exec.Command("git", "log", fmt.Sprintf("--format=%s", fst), ref) + cmd.Dir = pro.repo + + out, err := cmd.Output() + + if err != nil { + return nil, err + } + + results := []commit{} + scanner := bufio.NewScanner(bytes.NewReader(out)) + + for scanner.Scan() { + text := strings.TrimSpace(scanner.Text()) + data := strings.Split(text, SEP) + + h := data[0] + + var history []overview + var parents []string + + if data[1] != "" { + parents = strings.Split(data[1], " ") + } + + for _, p := range parents { + diffstat, err := pro.diffstatparser(h, p) + + if err != nil { + log.Printf("unable to diffstat against parent: %s", err) + + continue + } + + history = append(history, overview{diffstat, h, p}) + } + + a := author{data[4], data[3]} + + date, err := time.Parse("Mon, 2 Jan 2006 15:04:05 -0700", data[5]) + + if err != nil { + log.Printf("unable to parse commit date: %s", err) + + continue + } + + body, err := pro.bodyparser(h) + + if err != nil { + log.Printf("unable to parse commit body: %s", err) + + continue + } + + tree, err := pro.treeparser(h) + + if err != nil { + log.Printf("unable to parse commit tree: %s", err) + + continue + } + + c := commit{ + Abbr: data[6], + Author: a, + Body: body, + Branch: b, + Date: date, + Hash: h, + History: history, + Parents: parents, + Project: pro.Name, + Subject: data[2], + Tree: tree, + } + + results = append(results, c) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return results, nil +} + +func (pro *project) treeparser(h string) ([]object, error) { + cmd := exec.Command("git", "ls-tree", "-r", "--format=%(objectname) %(path)", h) + cmd.Dir = pro.repo + + out, err := cmd.Output() + + if err != nil { + return nil, err + } + + var results []object + feed := strings.Split(strings.TrimSuffix(fmt.Sprintf("%s", out), "\n"), "\n") + + for _, line := range feed { + w := strings.Split(line, " ") + + results = append(results, object{ + Hash: w[0], + Path: w[1], + }) + } + + return results, nil +} + +func (pro *project) diffstatparser(h, p string) (string, error) { + cmd := exec.Command("git", "diff", "--stat", fmt.Sprintf("%s..%s", p, h)) + cmd.Dir = pro.repo + + out, err := cmd.Output() + + if err != nil { + return "", err + } + + var results []string + feed := strings.Split(strings.TrimSuffix(fmt.Sprintf("%s", out), "\n"), "\n") + + for _, line := range feed { + // NOTE: This is hackish I know, attach to project? + i := strings.Index(line, "|") + + if i != -1 { + ext := filepath.Ext(strings.TrimSpace(line[:i])) + types[ext] = strings.Contains(line, "Bin") + } + + results = append(results, strings.TrimSpace(line)) + } + + return strings.Join(results, "\n"), nil +} + +func (pro *project) bodyparser(h string) (string, error) { + // Because the commit message body is multiline and is tripping the scanner. + cmd := exec.Command("git", "show", "--no-patch", "--format=%B", h) + cmd.Dir = pro.repo + + out, err := cmd.Output() + + if err != nil { + return "", err + } + + return strings.TrimSuffix(fmt.Sprintf("%s", out), "\n"), nil +}
home › develop › 62b8370 › 418f38d