From 9dd4809cdb18199dbbafedc974f115832cb66252 Mon Sep 17 00:00:00 2001 From: "Mike JS. Choi" <mkchoi212@icloud.com> Date: Fri, 18 May 2018 09:26:28 -0500 Subject: [PATCH] Refactor: Remove all global state via File structs --- conflict/command.go | 6 -- conflict/conflict.go | 114 +++++++++++++++++------- conflict/diff.go | 7 -- conflict/file.go | 91 +++++++++++++++++++ conflict/parse.go | 202 +++++++++++-------------------------------- layout.go | 26 +++--- main.go | 48 +++++----- summary.go | 52 ++--------- 8 files changed, 267 insertions(+), 279 deletions(-) delete mode 100644 conflict/diff.go create mode 100644 conflict/file.go diff --git a/conflict/command.go b/conflict/command.go index 34d1cec..bd273c6 100644 --- a/conflict/command.go +++ b/conflict/command.go @@ -65,9 +65,3 @@ func TopLevelPath(cwd string) (string, error) { return string(strings.Split(stdout, "\n")[0]), nil } - -// DiffLines is incomplete (TODO) -func DiffLines(cwd string) ([]string, error) { - stdout, _, _ := run("git", cwd, "--no-pager", "diff", "--color") - return []string{stdout}, nil -} diff --git a/conflict/conflict.go b/conflict/conflict.go index 962778e..c287bf7 100644 --- a/conflict/conflict.go +++ b/conflict/conflict.go @@ -1,21 +1,26 @@ package conflict import ( + "bytes" "sort" "strings" + "github.com/alecthomas/chroma" + "github.com/alecthomas/chroma/formatters" + "github.com/alecthomas/chroma/lexers" + "github.com/alecthomas/chroma/styles" "github.com/mkchoi212/fac/color" ) // Conflict represents a single conflict that may have occurred type Conflict struct { - Choice int - FileName string - AbsolutePath string - Start int - Middle int - End int - Diff3 []int + File *File + + Choice int + Start int + Middle int + End int + Diff3 []int LocalLines []string LocalPureLines []string @@ -26,9 +31,8 @@ type Conflict struct { CurrentName string ForeignName string - TopPeek int - BottomPeek int - DisplayDiff bool + TopPeek int + BottomPeek int } // Supported git conflict styles @@ -40,6 +44,11 @@ const ( end ) +const ( + Local = 1 + Incoming = 2 +) + // IdentifyStyle identifies the conflict marker style of provided text func IdentifyStyle(line string) (style int) { line = strings.TrimSpace(line) @@ -61,37 +70,36 @@ func IdentifyStyle(line string) (style int) { // Valid checks if the parsed conflict has corresponding begin, separator, // and middle line numbers func (c *Conflict) Valid() bool { - return c.Middle != 0 && c.End != 0 -} - -func (c *Conflict) Equal(c2 *Conflict) bool { - return c.AbsolutePath == c2.AbsolutePath && c.Start == c2.Start + return c.File != nil && c.Middle != 0 && c.End != 0 } -func (c *Conflict) ToggleDiff() { - c.DisplayDiff = !(c.DisplayDiff) +// Equal checks if two `Conflict`s are equal +func (c Conflict) Equal(c2 *Conflict) bool { + return c.File.AbsolutePath == c2.File.AbsolutePath && c.Start == c2.Start } // Extract extracts lines where conflicts exist // and corresponding branch names func (c *Conflict) Extract(lines []string) error { - c.LocalLines = lines[c.Start : c.Middle-1] + c.LocalLines = lines[c.Start+1 : c.Middle] if len(c.Diff3) != 0 { sort.Ints(c.Diff3) diff3Barrier := c.Diff3[0] - c.LocalPureLines = lines[c.Start : diff3Barrier-1] + c.LocalPureLines = lines[c.Start+1 : diff3Barrier] } else { c.LocalPureLines = c.LocalLines } - c.IncomingLines = lines[c.Middle : c.End-1] - c.CurrentName = strings.Split(lines[c.Start-1], " ")[1] - c.ForeignName = strings.Split(lines[c.End-1], " ")[1] + c.IncomingLines = lines[c.Middle+1 : c.End] + c.CurrentName = strings.Split(lines[c.Start], " ")[1] + c.ForeignName = strings.Split(lines[c.End], " ")[1] return nil } +// PaddingLines returns top and bottom padding lines based on +// `TopPeek` and `BottomPeek` values func (c *Conflict) PaddingLines() (topPadding, bottomPadding []string) { - lines := FileLines[c.AbsolutePath] - start, end := c.Start-1, c.End + lines := c.File.Lines + start, end := c.Start, c.End if c.TopPeek >= start { c.TopPeek = start @@ -115,12 +123,58 @@ func (c *Conflict) PaddingLines() (topPadding, bottomPadding []string) { return } -// In finds `Conflict`s that are from the provided file name -func In(fname string, conflicts []Conflict) (res []Conflict) { - for _, c := range conflicts { - if c.AbsolutePath == fname && c.Choice != 0 { - res = append(res, c) +// HighlightSyntax highlights the stored file lines; both local and incoming lines +// The highlighted versions of the lines are stored in Conflict.Colored____Lines +// If the file extension is not supported, no highlights are applied +func (c *Conflict) HighlightSyntax() error { + var lexer chroma.Lexer + + if lexer = lexers.Match(c.File.Name); lexer == nil { + for _, block := range [][]string{c.LocalLines, c.IncomingLines} { + if trial := lexers.Analyse(strings.Join(block, "")); trial != nil { + lexer = trial + break + } } } - return + + if lexer == nil { + lexer = lexers.Fallback + c.ColoredLocalLines = c.LocalLines + c.ColoredIncomingLines = c.IncomingLines + return nil + } + + style := styles.Get("emacs") + formatter := formatters.Get("terminal") + + var it chroma.Iterator + var err error + buf := new(bytes.Buffer) + var colorLine string + +tokenizer: + for i, block := range [][]string{c.LocalLines, c.IncomingLines} { + for _, line := range block { + if IdentifyStyle(line) == diff3 { + colorLine = color.Red(color.Regular, line) + } else { + if it, err = lexer.Tokenise(nil, line); err != nil { + break tokenizer + } + if err = formatter.Format(buf, style, it); err != nil { + break tokenizer + } + colorLine = buf.String() + } + + if i == 0 { + c.ColoredLocalLines = append(c.ColoredLocalLines, colorLine) + } else { + c.ColoredIncomingLines = append(c.ColoredIncomingLines, colorLine) + } + buf.Reset() + } + } + return err } diff --git a/conflict/diff.go b/conflict/diff.go deleted file mode 100644 index 9fef039..0000000 --- a/conflict/diff.go +++ /dev/null @@ -1,7 +0,0 @@ -package conflict - -// Diff is incomplete (TODO) -func (c *Conflict) Diff() []string { - lines, _ := DiffLines("") - return lines -} diff --git a/conflict/file.go b/conflict/file.go new file mode 100644 index 0000000..e6de101 --- /dev/null +++ b/conflict/file.go @@ -0,0 +1,91 @@ +package conflict + +import ( + "bufio" + "io" + "os" + "strings" +) + +// File represents a single file that contains git merge conflicts +type File struct { + AbsolutePath string + Name string + Lines []string + Conflicts []Conflict +} + +// readFile reads all lines of a given file +func (f *File) Read() (err error) { + input, err := os.Open(f.AbsolutePath) + if err != nil { + return + } + defer input.Close() + + r := bufio.NewReader(input) + + for { + data, err := r.ReadBytes('\n') + if err == nil || err == io.EOF { + // gocui currently doesn't support printing \r + line := strings.Replace(string(data), "\r", "", -1) + f.Lines = append(f.Lines, line) + } + + if err != nil { + if err != io.EOF { + return err + } + break + } + } + + return +} + +// WriteChanges writes all the resolved conflicts in a given file +// to the file system. +func (f File) WriteChanges() (err error) { + var replacementLines []string + + for _, c := range f.Conflicts { + if c.Choice == Local { + replacementLines = append([]string{}, c.LocalPureLines...) + } else if c.Choice == Incoming { + replacementLines = append([]string{}, c.IncomingLines...) + } else { + continue + } + + i := 0 + for ; i < len(replacementLines); i++ { + f.Lines[c.Start+i] = replacementLines[i] + } + for ; i <= c.End-c.Start; i++ { + f.Lines[c.Start+i] = "" + } + } + + if err = write(f.AbsolutePath, f.Lines); err != nil { + return + } + return +} + +func write(absPath string, lines []string) (err error) { + f, err := os.Create(absPath) + if err != nil { + return + } + defer f.Close() + + w := bufio.NewWriter(f) + for _, line := range lines { + if _, err = w.WriteString(line); err != nil { + return + } + } + err = w.Flush() + return +} diff --git a/conflict/parse.go b/conflict/parse.go index 9d4a399..288866f 100644 --- a/conflict/parse.go +++ b/conflict/parse.go @@ -1,214 +1,112 @@ package conflict import ( - "bufio" - "bytes" "errors" - "fmt" - "io" - "os" "path" "strconv" "strings" - - "github.com/alecthomas/chroma" - "github.com/alecthomas/chroma/formatters" - "github.com/alecthomas/chroma/lexers" - "github.com/alecthomas/chroma/styles" - "github.com/mkchoi212/fac/color" ) -var FileLines map[string][]string - -func (c *Conflict) HighlightSyntax() error { - var lexer chroma.Lexer - - if lexer = lexers.Match(c.FileName); lexer == nil { - for _, block := range [][]string{c.LocalLines, c.IncomingLines} { - fmt.Print(strings.Join(block, "\n")) - if trial := lexers.Analyse(strings.Join(block, "")); trial != nil { - lexer = trial - break - } - } - } - - if lexer == nil { - lexer = lexers.Fallback - c.ColoredLocalLines = c.LocalLines - c.ColoredIncomingLines = c.IncomingLines - return nil - } - - style := styles.Get("emacs") - formatter := formatters.Get("terminal") - - var it chroma.Iterator - var err error - buf := new(bytes.Buffer) - var colorLine string - -tokenizer: - for i, block := range [][]string{c.LocalLines, c.IncomingLines} { - for _, line := range block { - if IdentifyStyle(line) == diff3 { - colorLine = color.Red(color.Regular, line) - } else { - if it, err = lexer.Tokenise(nil, line); err != nil { - break tokenizer - } - if err = formatter.Format(buf, style, it); err != nil { - break tokenizer - } - colorLine = buf.String() - } - - if i == 0 { - c.ColoredLocalLines = append(c.ColoredLocalLines, colorLine) - } else { - c.ColoredIncomingLines = append(c.ColoredIncomingLines, colorLine) - } - buf.Reset() - } - } - return err -} - -// ReadFile reads all lines of a given file -func ReadFile(absPath string) ([]string, error) { - input, err := os.Open(absPath) - if err != nil { - return nil, err - } - defer input.Close() - - r := bufio.NewReader(input) - lines := []string{} - - for { - data, err := r.ReadBytes('\n') - if err == nil || err == io.EOF { - // gocui currently doesn't support printing \r - line := strings.Replace(string(data), "\r", "", -1) - lines = append(lines, line) - } - - if err != nil { - if err != io.EOF { - return nil, err - } - break - } - } - return lines, nil -} - -func parseGitMarkerInfo(diff string, dict map[string][]int) error { +func parseGitMarkerInfo(diff string) (fname string, lineNum int, err error) { parts := strings.Split(diff, ":") - if len(parts) < 3 || !strings.Contains(diff, "marker") { - return nil + if len(parts) < 3 || !strings.Contains(diff, "leftover conflict marker") { + err = errors.New("Line does not contain marker location info") + return } fname, lineData := string(parts[0]), parts[1] - - if lineNum, err := strconv.Atoi(string(lineData)); err == nil { - lines := append(dict[fname], lineNum) - dict[fname] = lines + if lineNum, err = strconv.Atoi(string(lineData)); err != nil { + return } - return nil + return } -func newConflicts(absPath string, fname string, markerLocations []int) ([]Conflict, error) { - parsedConflicts := []Conflict{} - lines := FileLines[absPath] - +func parseConflictsIn(f File, markerLocations []int) (conflicts []Conflict, err error) { var conf Conflict for _, lineNum := range markerLocations { - line := lines[lineNum-1] + line := f.Lines[lineNum-1] + index := lineNum - 1 switch IdentifyStyle(line) { case start: conf = Conflict{} - conf.AbsolutePath = absPath - conf.FileName = fname - conf.Start = lineNum + conf.Start = index case separator: - conf.Middle = lineNum + conf.Middle = index case diff3: - conf.Diff3 = append(conf.Diff3, lineNum) + conf.Diff3 = append(conf.Diff3, index) case end: - conf.End = lineNum - parsedConflicts = append(parsedConflicts, conf) + conf.End = index + conflicts = append(conflicts, conf) default: continue } } - // Verify all markers are properly parsed/paired - for _, c := range parsedConflicts { + for i := range conflicts { + c := &conflicts[i] + c.File = &f + if !(c.Valid()) { return nil, errors.New("Invalid number of remaining conflict markers") } + + if err = c.Extract(f.Lines); err != nil { + return + } + + if err = c.HighlightSyntax(); err != nil { + return + } } - return parsedConflicts, nil + + return } // Find runs `git --no-pager diff --check` in order to detect git conflicts -// If there are no conflicts, it returns a `ErrNoConflict` -// If there are conflicts, it parses the corresponding files -func Find(cwd string) ([]Conflict, error) { - allConflicts := []Conflict{} - - topPath, ok := TopLevelPath(cwd) - if ok != nil { - return nil, ok +// It returns an array of `File`s where each `File` contains conflicts within itself +// If the parsing fails, it returns an error +func Find(cwd string) (files []File, err error) { + topPath, err := TopLevelPath(cwd) + if err != nil { + return } - markerLocations, ok := MarkerLocations(topPath) - if ok != nil { - return nil, ok + markerLocations, err := MarkerLocations(topPath) + if err != nil { + return } markerLocMap := make(map[string][]int) - FileLines = make(map[string][]string) - for _, line := range markerLocations { if len(line) == 0 { continue } - if err := parseGitMarkerInfo(line, markerLocMap); err != nil { - return nil, err + fname, line, ok := parseGitMarkerInfo(line) + if ok != nil { + continue } + markerLocMap[fname] = append(markerLocMap[fname], line) } for fname := range markerLocMap { absPath := path.Join(topPath, fname) + file := File{Name: fname, AbsolutePath: absPath} - lines, err := ReadFile(absPath) - if err != nil { - return nil, err + if err = file.Read(); err != nil { + return } - FileLines[absPath] = append(FileLines[absPath], lines...) - if conflicts, err := newConflicts(absPath, fname, markerLocMap[fname]); err == nil { - allConflicts = append(allConflicts, conflicts...) - } else { + conflicts, err := parseConflictsIn(file, markerLocMap[fname]) + if err != nil { return nil, err } - } - for i := range allConflicts { - fileLines := FileLines[allConflicts[i].AbsolutePath] - if err := allConflicts[i].Extract(fileLines); err != nil { - return nil, err - } - if err := allConflicts[i].HighlightSyntax(); err != nil { - return nil, err - } + file.Conflicts = conflicts + files = append(files, file) } - - return allConflicts, nil + return } diff --git a/layout.go b/layout.go index f01e6d0..a1f41fc 100644 --- a/layout.go +++ b/layout.go @@ -15,10 +15,8 @@ const ( Prompt = "prompt" Input = "input prompt" - Local = 1 - Incoming = 2 - Up = 3 - Down = 4 + Up = 3 + Down = 4 Horizontal = 5 Vertical = -6 @@ -143,12 +141,12 @@ func Select(c *conflict.Conflict, g *gocui.Gui, showHelp bool) error { } v.Clear() - for idx, conflict := range all { + for idx, conflict := range conflicts { var out string if conflict.Choice != 0 { - out = color.Green(color.Regular, "✔ %s:%d", conflict.FileName, conflict.Start) + out = color.Green(color.Regular, "✔ %s:%d", conflict.File.Name, conflict.Start) } else { - out = color.Red(color.Regular, "%d. %s:%d", idx+1, conflict.FileName, conflict.Start) + out = color.Red(color.Regular, "%d. %s:%d", idx+1, conflict.File.Name, conflict.Start) } if conflict.Equal(c) { @@ -177,7 +175,7 @@ func Select(c *conflict.Conflict, g *gocui.Gui, showHelp bool) error { printLines(v, top) printLines(v, c.ColoredLocalLines) printLines(v, bottom) - if c.Choice == Local { + if c.Choice == conflict.Local { v.FgColor = gocui.ColorGreen } @@ -192,7 +190,7 @@ func Select(c *conflict.Conflict, g *gocui.Gui, showHelp bool) error { printLines(v, top) printLines(v, c.ColoredIncomingLines) printLines(v, bottom) - if c.Choice == Incoming { + if c.Choice == conflict.Incoming { v.FgColor = gocui.ColorGreen } @@ -220,22 +218,22 @@ func MoveToItem(dir int, g *gocui.Gui, v *gocui.View) error { cur++ } - if cur >= numConflicts { + if cur >= len(conflicts) { cur = 0 } else if cur < 0 { - cur = numConflicts - 1 + cur = len(conflicts) - 1 } - if all[cur].Choice == 0 || originalCur == cur { + if conflicts[cur].Choice == 0 || originalCur == cur { break } } - if originalCur == cur && all[cur].Choice != 0 { + if originalCur == cur && conflicts[cur].Choice != 0 { globalQuit(g) } - Select(&all[cur], g, false) + Select(conflicts[cur], g, false) return nil } diff --git a/main.go b/main.go index 0b4028a..bf7c01b 100644 --- a/main.go +++ b/main.go @@ -12,9 +12,8 @@ import ( ) var ( + conflicts = []*conflict.Conflict{} cur = 0 - all = []conflict.Conflict{} - numConflicts = 0 consecutiveError = 0 ) @@ -32,19 +31,19 @@ func parseInput(g *gocui.Gui, v *gocui.View) error { evalCmd := func(in rune, g *gocui.Gui) { switch in { case 'j': - Scroll(g, &all[cur], Up) + Scroll(g, conflicts[cur], Up) case 'k': - Scroll(g, &all[cur], Down) + Scroll(g, conflicts[cur], Down) case 'w': - all[cur].TopPeek++ - Select(&all[cur], g, false) + conflicts[cur].TopPeek++ + Select(conflicts[cur], g, false) case 's': - all[cur].BottomPeek++ - Select(&all[cur], g, false) + conflicts[cur].BottomPeek++ + Select(conflicts[cur], g, false) case 'a': - Resolve(&all[cur], g, v, Local) + Resolve(conflicts[cur], g, v, conflict.Local) case 'd': - Resolve(&all[cur], g, v, Incoming) + Resolve(conflicts[cur], g, v, conflict.Incoming) case 'n': MoveToItem(Down, g, v) case 'p': @@ -53,7 +52,7 @@ func parseInput(g *gocui.Gui, v *gocui.View) error { ViewOrientation = ^ViewOrientation layout(g) case 'h', '?': - Select(&all[cur], g, true) + Select(conflicts[cur], g, true) case 'q': globalQuit(g) default: @@ -61,7 +60,7 @@ func parseInput(g *gocui.Gui, v *gocui.View) error { consecutiveError++ if consecutiveError == 2 { consecutiveError = 0 - Select(&all[cur], g, true) + Select(conflicts[cur], g, true) } return } @@ -89,20 +88,26 @@ func parseInput(g *gocui.Gui, v *gocui.View) error { } func main() { + // Find and parse conflicts cwd, _ := os.Getwd() - conflicts, err := conflict.Find(cwd) + files, err := conflict.Find(cwd) if err != nil { fmt.Println(color.Red(color.Regular, err.Error())) return } - if len(conflicts) == 0 { + if len(files) == 0 { fmt.Println(color.Green(color.Regular, "No conflicts detected 🎉")) return } - all = conflicts - numConflicts = len(conflicts) + for i := range files { + file := &files[i] + for j := range file.Conflicts { + conflicts = append(conflicts, &file.Conflicts[j]) + } + } + // Setup CUI Environment g, err := gocui.NewGui(gocui.OutputNormal) if err != nil { log.Panicln(err) @@ -117,18 +122,15 @@ func main() { log.Panicln(err) } - Select(&all[0], g, false) - + // Main UI loop + Select(conflicts[0], g, false) if err := g.MainLoop(); err != nil && err != gocui.ErrQuit { log.Panicln(err) } - g.Close() - for absPath := range conflict.FileLines { - targetConflicts := conflict.In(absPath, all) - finalLines := FinalizeChanges(targetConflicts, conflict.FileLines[absPath]) - if err = writeChanges(absPath, finalLines); err != nil { + for _, file := range files { + if err = file.WriteChanges(); err != nil { fmt.Println(color.Red(color.Underline, "%s\n", err)) } } diff --git a/summary.go b/summary.go index 93e49c8..7fd33d8 100644 --- a/summary.go +++ b/summary.go @@ -1,14 +1,11 @@ package main import ( - "bufio" "bytes" "fmt" - "os" "github.com/jroimartin/gocui" "github.com/mkchoi212/fac/color" - "github.com/mkchoi212/fac/conflict" ) func printHelp(v *gocui.View) { @@ -35,63 +32,24 @@ func printSummary() { resolvedCnt := 0 var line string - for _, c := range all { + for _, c := range conflicts { if c.Choice != 0 { - line = color.Green(color.Regular, "✔ %s: %d", c.FileName, c.Start) + line = color.Green(color.Regular, "✔ %s: %d", c.File.Name, c.Start) resolvedCnt++ } else { - line = color.Red(color.Regular, "✘ %s: %d", c.FileName, c.Start) + line = color.Red(color.Regular, "✘ %s: %d", c.File.Name, c.Start) } fmt.Println(line) } var buf bytes.Buffer - if resolvedCnt != numConflicts { + if resolvedCnt != len(conflicts) { buf.WriteString("\nResolved ") buf.WriteString(color.Red(color.Light, "%d ", resolvedCnt)) buf.WriteString("conflict(s) out of ") - buf.WriteString(color.Red(color.Light, "%d", numConflicts)) + buf.WriteString(color.Red(color.Light, "%d", len(conflicts))) } else { buf.WriteString(color.Green(color.Regular, "\nFixed All Conflicts 🎉")) } fmt.Println(buf.String()) } - -func writeChanges(absPath string, lines []string) (err error) { - f, err := os.Create(absPath) - if err != nil { - return - } - defer f.Close() - - w := bufio.NewWriter(f) - for _, line := range lines { - if _, err = w.WriteString(line); err != nil { - return - } - } - err = w.Flush() - return -} - -// FinalizeChanges constructs final lines of the file with conflicts removed -func FinalizeChanges(conflicts []conflict.Conflict, fileLines []string) []string { - var replacementLines []string - - for _, c := range conflicts { - if c.Choice == Local { - replacementLines = append([]string{}, c.LocalPureLines...) - } else { - replacementLines = append([]string{}, c.IncomingLines...) - } - i := 0 - for ; i < len(replacementLines); i++ { - fileLines[c.Start+i-1] = replacementLines[i] - } - for ; c.End-c.Start >= i; i++ { - fileLines[c.Start+i-1] = "" - } - } - - return fileLines -}