289 lines
7.5 KiB
Go
289 lines
7.5 KiB
Go
package cmd
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"plandex-cli/api"
|
|
"plandex-cli/auth"
|
|
"plandex-cli/lib"
|
|
"plandex-cli/term"
|
|
"plandex-cli/ui"
|
|
|
|
"github.com/eiannone/keyboard"
|
|
"github.com/fatih/color"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var showDiffUi bool = true
|
|
var diffUiSideBySide = true
|
|
var diffUiLineByLine bool
|
|
var diffGit bool
|
|
|
|
var fromTellMenu bool
|
|
|
|
var diffsCmd = &cobra.Command{
|
|
Use: "diff",
|
|
Aliases: []string{"diffs"},
|
|
Short: "Review pending changes",
|
|
Run: diffs,
|
|
}
|
|
|
|
func init() {
|
|
RootCmd.AddCommand(diffsCmd)
|
|
|
|
diffsCmd.Flags().BoolVarP(&plainTextOutput, "plain", "p", false, "Output diffs in plain text with no ANSI codes")
|
|
diffsCmd.Flags().BoolVar(&showDiffUi, "ui", false, "Show diffs in a browser UI")
|
|
diffsCmd.Flags().BoolVar(&diffGit, "git", true, "Show diffs in git diff format")
|
|
diffsCmd.Flags().BoolVarP(&diffUiSideBySide, "side", "s", true, "Show diffs UI in side-by-side view")
|
|
diffsCmd.Flags().BoolVarP(&diffUiLineByLine, "line", "l", false, "Show diffs UI in line-by-line view")
|
|
|
|
diffsCmd.Flags().BoolVar(&fromTellMenu, "from-tell-menu", false, "Show diffs from the tell menu")
|
|
diffsCmd.Flags().MarkHidden("from-tell-menu")
|
|
|
|
}
|
|
|
|
func diffs(cmd *cobra.Command, args []string) {
|
|
auth.MustResolveAuthWithOrg()
|
|
lib.MaybeResolveProject()
|
|
|
|
if lib.CurrentPlanId == "" {
|
|
term.OutputNoCurrentPlanErrorAndExit()
|
|
}
|
|
|
|
term.StartSpinner("")
|
|
|
|
if showDiffUi {
|
|
diffGit = false
|
|
} else if diffGit || plainTextOutput {
|
|
showDiffUi = false
|
|
} else {
|
|
diffGit = true
|
|
}
|
|
|
|
diffs, err := api.Client.GetPlanDiffs(lib.CurrentPlanId, lib.CurrentBranch, plainTextOutput || showDiffUi)
|
|
term.StopSpinner()
|
|
if err != nil {
|
|
term.OutputErrorAndExit("Error getting plan diffs: %v", err)
|
|
return
|
|
}
|
|
|
|
if len(diffs) == 0 {
|
|
fmt.Println("🤷♂️ No pending changes")
|
|
return
|
|
}
|
|
|
|
if showDiffUi {
|
|
getNewListener := func() net.Listener {
|
|
outputFormat := "line-by-line"
|
|
if diffUiSideBySide {
|
|
outputFormat = "side-by-side"
|
|
} else if diffUiLineByLine {
|
|
outputFormat = "line-by-line"
|
|
}
|
|
|
|
// Properly escape the diff content for JavaScript
|
|
diffJSON, err := json.Marshal(diffs)
|
|
if err != nil {
|
|
term.OutputErrorAndExit("Error encoding diff content: %v", err)
|
|
}
|
|
|
|
// Create template data
|
|
data := struct {
|
|
DiffContent template.JS
|
|
OutputFormat string
|
|
}{
|
|
DiffContent: template.JS(diffJSON),
|
|
OutputFormat: outputFormat,
|
|
}
|
|
|
|
// Parse and execute the template
|
|
tmpl, err := template.New("diff").Parse(htmlTemplate)
|
|
if err != nil {
|
|
term.OutputErrorAndExit("Error parsing template: %v", err)
|
|
}
|
|
|
|
// Use :0 to let the OS pick an available port
|
|
listener, err := net.Listen("tcp", ":0")
|
|
if err != nil {
|
|
term.OutputErrorAndExit("Error starting server: %v", err)
|
|
}
|
|
|
|
// Get the actual port chosen
|
|
port := listener.Addr().(*net.TCPAddr).Port
|
|
|
|
// Start web server
|
|
go func() {
|
|
http.HandleFunc("/"+outputFormat, func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
err := tmpl.Execute(w, data)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
}
|
|
})
|
|
http.Serve(listener, nil)
|
|
}()
|
|
|
|
ui.OpenURL("Showing "+outputFormat+" diffs in your default browser...", fmt.Sprintf("http://localhost:%d/%s", port, outputFormat))
|
|
|
|
fmt.Println()
|
|
|
|
return listener
|
|
}
|
|
|
|
listener := getNewListener()
|
|
defer listener.Close()
|
|
|
|
var relaunch bool
|
|
|
|
for {
|
|
if relaunch {
|
|
listener.Close()
|
|
listener = getNewListener()
|
|
defer listener.Close()
|
|
relaunch = false
|
|
}
|
|
|
|
if diffUiLineByLine {
|
|
fmt.Printf("%s for side-by-side view\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(s)"))
|
|
} else {
|
|
fmt.Printf("%s for line-by-line view\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(l)"))
|
|
}
|
|
|
|
fmt.Printf("%s for git diff format\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(g)"))
|
|
// fmt.Printf("%s to quit\n", color.New(color.Bold, term.ColorHiGreen).Sprintf("(q)"))
|
|
|
|
s := "to exit menu/continue"
|
|
if fromTellMenu {
|
|
s = "to go back"
|
|
}
|
|
|
|
fmt.Printf("%s %s %s %s %s",
|
|
color.New(term.ColorHiMagenta, color.Bold).Sprint("Press a hotkey,"),
|
|
color.New(color.FgHiWhite, color.Bold).Sprintf("↓"),
|
|
color.New(term.ColorHiMagenta, color.Bold).Sprintf("to select, or"),
|
|
color.New(color.FgHiWhite, color.Bold).Sprintf("enter"),
|
|
color.New(term.ColorHiMagenta, color.Bold).Sprintf("%s>", s),
|
|
)
|
|
|
|
char, key, err := term.GetUserKeyInput()
|
|
if err != nil {
|
|
term.OutputErrorAndExit("Error getting key: %v", err)
|
|
return
|
|
}
|
|
fmt.Println()
|
|
|
|
if key == keyboard.KeyArrowDown {
|
|
options := []string{}
|
|
if diffUiLineByLine {
|
|
options = append(options, "side-by-side")
|
|
} else {
|
|
options = append(options, "line-by-line")
|
|
}
|
|
|
|
options = append(options, "git diff")
|
|
options = append(options, "exit menu")
|
|
|
|
selected, err := term.SelectFromList(
|
|
"Select an action",
|
|
options,
|
|
)
|
|
if err != nil {
|
|
term.OutputErrorAndExit("Error selecting action: %v", err)
|
|
return
|
|
}
|
|
|
|
if selected == "side-by-side" {
|
|
diffUiSideBySide = true
|
|
diffUiLineByLine = false
|
|
relaunch = true
|
|
} else if selected == "line-by-line" {
|
|
diffUiSideBySide = false
|
|
diffUiLineByLine = true
|
|
relaunch = true
|
|
} else if selected == "git diff" {
|
|
showGitDiff()
|
|
} else if selected == "exit menu" {
|
|
fmt.Println()
|
|
break
|
|
}
|
|
} else if string(char) == "g" {
|
|
showGitDiff()
|
|
} else if string(char) == "s" {
|
|
diffUiSideBySide = true
|
|
diffUiLineByLine = false
|
|
relaunch = true
|
|
} else if string(char) == "l" {
|
|
diffUiSideBySide = false
|
|
diffUiLineByLine = true
|
|
relaunch = true
|
|
} else if key == 13 || key == 10 || string(char) == "q" { // Check raw key codes for Enter/Return
|
|
if term.IsRepl {
|
|
fmt.Println()
|
|
break
|
|
} else {
|
|
fmt.Println()
|
|
break
|
|
}
|
|
} else if string(char) == "\x03" { // Ctrl+C
|
|
os.Exit(0)
|
|
} else {
|
|
fmt.Println()
|
|
term.OutputSimpleError("Invalid hotkey")
|
|
fmt.Println()
|
|
}
|
|
}
|
|
} else {
|
|
if plainTextOutput {
|
|
fmt.Println(diffs)
|
|
} else {
|
|
term.PageOutput(diffs)
|
|
}
|
|
fmt.Println()
|
|
}
|
|
}
|
|
|
|
func showGitDiff() {
|
|
_, err := lib.ExecPlandexCommandWithParams([]string{"diff", "--git"}, lib.ExecPlandexCommandParams{
|
|
DisableSuggestions: true,
|
|
})
|
|
if err != nil {
|
|
term.OutputErrorAndExit("Error showing git diff: %v", err)
|
|
}
|
|
}
|
|
|
|
var htmlTemplate = `<!doctype html>
|
|
<html lang="en-us">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<!-- Make sure to load the highlight.js CSS file before the Diff2Html CSS file -->
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/vs.min.css" />
|
|
<link
|
|
rel="stylesheet"
|
|
type="text/css"
|
|
href="https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css"
|
|
/>
|
|
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js"></script>
|
|
</head>
|
|
<script>
|
|
// Parse the JSON-encoded diff content
|
|
const diffString = {{.DiffContent}};
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
var targetElement = document.getElementById('myDiffElement');
|
|
var configuration = {
|
|
outputFormat: '{{.OutputFormat}}'
|
|
};
|
|
var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);
|
|
diff2htmlUi.draw();
|
|
diff2htmlUi.highlightCode();
|
|
});
|
|
</script>
|
|
<body>
|
|
<div id="myDiffElement"></div>
|
|
</body>
|
|
</html>`
|