Skip to content

Commit

Permalink
refactor: give conflict resolver full control about the file contents
Browse files Browse the repository at this point in the history
  • Loading branch information
majori committed Dec 20, 2023
1 parent b9822bf commit 3b5d8a3
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 27 deletions.
27 changes: 16 additions & 11 deletions internal/cli/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,18 +216,27 @@ func runUpgrade(cmd *cobra.Command, opts upgradeOptions) error {
continue
}

file := newSauce.Files[path]

// Check if the file has been modified manually
if prevFile, exists := oldSauce.Files[path]; exists && prevFile.HasBeenModified() {
if opts.NoInput {
return recipeutil.NewNoInputError(varsWithoutValues)
}

// The file contents has been modified
if !overrideNoticed {
cmd.Println("\nSome of the files has been manually modified. Do you want to override the following files:")
overrideNoticed = true
}

override, err := conflict.Solve(cmd.InOrStdin(), cmd.OutOrStdout(), path)
conflictResult, err := conflict.Solve(
cmd.InOrStdin(),
cmd.OutOrStdout(),
path,
oldSauce.Files[path].Content,
newSauce.Files[path].Content,
)

if err != nil {
if errors.Is(err, uiutil.ErrUserAborted) {
cmd.Println("User aborted")
Expand All @@ -237,17 +246,13 @@ func runUpgrade(cmd *cobra.Command, opts upgradeOptions) error {
return fmt.Errorf("error when prompting for question: %w", err)
}

if !override {
// User decided not to override the file with manual changes, remove from
// list of changes to write
cmd.Printf("%s: keep\n", path)
continue
}
// NOTE: We need to save the checksum of the original file from the new sauce
// so we would detect again if the file has been modified manually
// when upgrading again
file.Content = conflictResult
}

// Add new file or replace existing one
output[path] = newSauce.Files[path]
cmd.Printf("%s: replace\n", path)
output[path] = file
}

newSauce.Files = output
Expand Down
50 changes: 35 additions & 15 deletions pkg/ui/conflict/conflict.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,41 @@ import (
)

type Model struct {
Value bool
answer bool
filePath string
fileA []byte
fileB []byte
err error
submitted bool
}

var _ tea.Model = Model{}

func Solve(in io.Reader, out io.Writer, filePath string) (bool, error) {
func Solve(in io.Reader, out io.Writer, filePath string, fileA, fileB []byte) ([]byte, error) {
lipgloss.SetHasDarkBackground(termenv.HasDarkBackground())

p := tea.NewProgram(NewModel(filePath), tea.WithInput(in), tea.WithOutput(out))
p := tea.NewProgram(NewModel(filePath, fileA, fileB), tea.WithInput(in), tea.WithOutput(out))
if m, err := p.Run(); err != nil {
return false, err
return []byte{}, err
} else {
m, ok := m.(Model)
if !ok {
return false, errors.New("internal error: unexpected model type")
return []byte{}, errors.New("internal error: unexpected model type")
}

if m.err != nil {
return false, m.err
return []byte{}, m.err
}

return m.Value, nil
return m.Result(), nil
}
}

func NewModel(filePath string) Model {
func NewModel(filePath string, fileA, fileB []byte) Model {
return Model{
filePath: filePath,
fileA: fileA,
fileB: fileB,
}
}

Expand All @@ -62,33 +66,49 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.submitted = true
return m, tea.Quit
case tea.KeyRight:
m.Value = true
m.answer = true
case tea.KeyLeft:
m.Value = false
m.answer = false
case tea.KeyRunes:
switch string(msg.Runes) {
case "y", "Y":
m.Value = true
m.answer = true
case "n", "N":
m.Value = false
m.answer = false
}
}
}
return m, nil
}

// TODO: Make merge conflict solving more advanced instead of just file override confirmation
func (m Model) View() string {
var s strings.Builder
if m.submitted || m.err != nil {
return ""
s.WriteString(fmt.Sprintf("%s: ", m.filePath))
if m.answer {
s.WriteString("override")
} else {
s.WriteString("keep")
}

return s.String()
}

var s strings.Builder
s.WriteString(fmt.Sprintf("Override file '%s':\n", m.filePath))
if m.Value {
if m.answer {
s.WriteString(fmt.Sprintf("> No/%s", lipgloss.NewStyle().Bold(true).Render("Yes")))
} else {
s.WriteString(fmt.Sprintf("> %s/Yes", lipgloss.NewStyle().Bold(true).Render("No")))
}

return s.String()
}

func (m Model) Result() []byte {
if m.answer {
return m.fileB
} else {
return m.fileA
}
}
91 changes: 91 additions & 0 deletions pkg/ui/conflict/conflict_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package conflict_test

import (
"bytes"
"testing"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/x/exp/teatest"
"github.com/futurice/jalapeno/pkg/ui/conflict"
)

func TestSolveFileConflict(t *testing.T) {
testCases := []struct {
name string
filePath string
fileA []byte
fileB []byte
input string
expected []byte
}{
{
name: "no_answer",
filePath: "README.md",
fileA: []byte("foo"),
fileB: []byte("bar"),
input: "n\n",
expected: []byte("foo"),
},
{
name: "yes_answer",
filePath: "README.md",
fileA: []byte("foo"),
fileB: []byte("bar"),
input: "y\n",
expected: []byte("bar"),
},
}

for _, tc := range testCases {
t.Run(tc.name, func(tt *testing.T) {
tm := teatest.NewTestModel(
tt,
conflict.NewModel(tc.filePath, tc.fileA, tc.fileB),
teatest.WithInitialTermSize(300, 100),
)

for _, r := range tc.input {
tm.Send(RuneToKey(r))
}

m := tm.FinalModel(tt, teatest.WithFinalTimeout(time.Second)).(conflict.Model)

// Assert that the result is correct
result := m.Result()
if !bytes.Equal(result, tc.expected) {
t.Errorf("Unexpected result. Got %v, expected %v", result, tc.expected)
}
})
}
}

func RuneToKey(r rune) tea.KeyMsg {
switch r {
case '\n':
return tea.KeyMsg{
Type: tea.KeyEnter,
}
case '↑':
return tea.KeyMsg{
Type: tea.KeyUp,
}
case '↓':
return tea.KeyMsg{
Type: tea.KeyDown,
}
case '←':
return tea.KeyMsg{
Type: tea.KeyLeft,
}
case '→':
return tea.KeyMsg{
Type: tea.KeyRight,
}
default:
return tea.KeyMsg{
Type: tea.KeyRunes,
Runes: []rune{r},
}
}
}
1 change: 0 additions & 1 deletion pkg/ui/survey/survey_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ func TestPromptUserForValues(t *testing.T) {
}

m := tm.FinalModel(tt, teatest.WithFinalTimeout(time.Second)).(survey.SurveyModel)
m.Values()

// Assert that the result is correct
result := m.Values()
Expand Down

0 comments on commit 3b5d8a3

Please sign in to comment.