From 93108b365b9363de89a3379e7ec7ab429c517a77 Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Sat, 19 Jul 2025 14:27:35 +0100 Subject: [PATCH] appview/pages: support split diffs for interdiff Change-Id: mumnxrlvslvpxmksvqkovrrrvnrtktmu Signed-off-by: oppiliappan --- appview/pages/pages.go | 1 + .../pages/templates/repo/fragments/diff.html | 123 ---------------- .../repo/fragments/diffStatPill.html | 13 ++ .../templates/repo/fragments/interdiff.html | 13 +- .../templates/repo/fragments/splitDiff.html | 61 ++++++++ .../templates/repo/fragments/unifiedDiff.html | 47 +++++++ appview/pulls/pulls.go | 11 +- patchutil/interdiff.go | 25 ++++ types/split.go | 131 ++++++++++++++++++ 9 files changed, 289 insertions(+), 136 deletions(-) create mode 100644 appview/pages/templates/repo/fragments/diffStatPill.html create mode 100644 appview/pages/templates/repo/fragments/splitDiff.html create mode 100644 appview/pages/templates/repo/fragments/unifiedDiff.html create mode 100644 types/split.go diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 51e339f..649d8d4 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -876,6 +876,7 @@ type RepoPullInterdiffParams struct { Round int Interdiff *patchutil.InterdiffResult OrderedReactionKinds []db.ReactionKind + DiffOpts types.DiffOpts } // this name is a mouthful diff --git a/appview/pages/templates/repo/fragments/diff.html b/appview/pages/templates/repo/fragments/diff.html index 2e79bb9..f3ebdf2 100644 --- a/appview/pages/templates/repo/fragments/diff.html +++ b/appview/pages/templates/repo/fragments/diff.html @@ -104,126 +104,3 @@ {{ end }} {{ end }} {{ end }} - -{{ define "unifiedDiffLines" }} -{{ $name := .Name.New }} -
{{- range .TextFragments -}}
···
- {{- $oldStart := .OldPosition -}} - {{- $newStart := .NewPosition -}} - {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 target:bg-yellow-200 target:dark:bg-yellow-600" -}} - {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} - {{- $lineNrSepStyle1 := "" -}} - {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} - {{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} - {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} - {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} - {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} - {{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} - {{- range .Lines -}} - {{- if eq .Op.String "+" -}} -
-
- -
{{ .Op.String }}
-
{{ .Line }}
-
- {{- $newStart = add64 $newStart 1 -}} - {{- end -}} - {{- if eq .Op.String "-" -}} -
- -
-
{{ .Op.String }}
-
{{ .Line }}
-
- {{- $oldStart = add64 $oldStart 1 -}} - {{- end -}} - {{- if eq .Op.String " " -}} -
- - -
{{ .Op.String }}
-
{{ .Line }}
-
- {{- $newStart = add64 $newStart 1 -}} - {{- $oldStart = add64 $oldStart 1 -}} - {{- end -}} - {{- end -}} - {{- end -}}
-{{ end }} - -{{ define "splitDiffLines" }} -{{ $name := .Name.New }} -{{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}} -{{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} -{{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} -{{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} -{{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} -{{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} -{{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} -{{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} -{{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} -
-
{{- range .TextFragments -}}
···
- {{- range .LeftLines -}} - {{- if .IsEmpty -}} -
-
-
- -
- {{- else if eq .Op.String "-" -}} -
- -
{{ .Op.String }}
-
{{ .Content }}
-
- {{- else if eq .Op.String " " -}} -
- -
{{ .Op.String }}
-
{{ .Content }}
-
- {{- end -}} - {{- end -}} - {{- end -}}
- -
{{- range .TextFragments -}}
···
- {{- range .RightLines -}} - {{- if .IsEmpty -}} -
-
-
- -
- {{- else if eq .Op.String "+" -}} -
- -
{{ .Op.String }}
-
{{ .Content }}
-
- {{- else if eq .Op.String " " -}} -
- -
{{ .Op.String }}
-
{{ .Content }}
-
- {{- end -}} - {{- end -}} - {{- end -}}
-
-{{ end }} - -{{ define "statPill" }} -
- {{ if and .Insertions .Deletions }} - +{{ .Insertions }} - -{{ .Deletions }} - {{ else if .Insertions }} - +{{ .Insertions }} - {{ else if .Deletions }} - -{{ .Deletions }} - {{ end }} -
-{{ end }} - diff --git a/appview/pages/templates/repo/fragments/diffStatPill.html b/appview/pages/templates/repo/fragments/diffStatPill.html new file mode 100644 index 0000000..4ebbf74 --- /dev/null +++ b/appview/pages/templates/repo/fragments/diffStatPill.html @@ -0,0 +1,13 @@ +{{ define "repo/fragments/diffStatPill" }} +
+ {{ if and .Insertions .Deletions }} + +{{ .Insertions }} + -{{ .Deletions }} + {{ else if .Insertions }} + +{{ .Insertions }} + {{ else if .Deletions }} + -{{ .Deletions }} + {{ end }} +
+{{ end }} + diff --git a/appview/pages/templates/repo/fragments/interdiff.html b/appview/pages/templates/repo/fragments/interdiff.html index 70c99fb..f3825c3 100644 --- a/appview/pages/templates/repo/fragments/interdiff.html +++ b/appview/pages/templates/repo/fragments/interdiff.html @@ -1,6 +1,7 @@ {{ define "repo/fragments/interdiff" }} {{ $repo := index . 0 }} {{ $x := index . 1 }} +{{ $opts := index . 2 }} {{ $fileTree := fileTree $x.AffectedFiles }} {{ $diff := $x.Files }} {{ $last := sub (len $diff) 1 }} @@ -87,15 +88,3 @@ {{ end }} {{ end }} -{{ define "statPill" }} -
- {{ if and .Insertions .Deletions }} - +{{ .Insertions }} - -{{ .Deletions }} - {{ else if .Insertions }} - +{{ .Insertions }} - {{ else if .Deletions }} - -{{ .Deletions }} - {{ end }} -
-{{ end }} diff --git a/appview/pages/templates/repo/fragments/splitDiff.html b/appview/pages/templates/repo/fragments/splitDiff.html new file mode 100644 index 0000000..1fe3c9a --- /dev/null +++ b/appview/pages/templates/repo/fragments/splitDiff.html @@ -0,0 +1,61 @@ +{{ define "repo/fragments/splitDiff" }} +{{ $name := .Id }} +{{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800" -}} +{{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} +{{- $lineNrSepStyle := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} +{{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} +{{- $emptyStyle := "bg-gray-200/30 dark:bg-gray-700/30" -}} +{{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400" -}} +{{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} +{{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} +{{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} +
+
{{- range .TextFragments -}}
···
+ {{- range .LeftLines -}} + {{- if .IsEmpty -}} +
+
+
+ +
+ {{- else if eq .Op.String "-" -}} +
+ +
{{ .Op.String }}
+
{{ .Content }}
+
+ {{- else if eq .Op.String " " -}} +
+ +
{{ .Op.String }}
+
{{ .Content }}
+
+ {{- end -}} + {{- end -}} + {{- end -}}
+ +
{{- range .TextFragments -}}
···
+ {{- range .RightLines -}} + {{- if .IsEmpty -}} +
+
+
+ +
+ {{- else if eq .Op.String "+" -}} +
+ +
{{ .Op.String }}
+
{{ .Content }}
+
+ {{- else if eq .Op.String " " -}} +
+ +
{{ .Op.String }}
+
{{ .Content }}
+
+ {{- end -}} + {{- end -}} + {{- end -}}
+
+{{ end }} diff --git a/appview/pages/templates/repo/fragments/unifiedDiff.html b/appview/pages/templates/repo/fragments/unifiedDiff.html new file mode 100644 index 0000000..e036410 --- /dev/null +++ b/appview/pages/templates/repo/fragments/unifiedDiff.html @@ -0,0 +1,47 @@ +{{ define "repo/fragments/unifiedDiff" }} +{{ $name := .Id }} +
{{- range .TextFragments -}}
···
+ {{- $oldStart := .OldPosition -}} + {{- $newStart := .NewPosition -}} + {{- $lineNrStyle := "min-w-[3.5rem] flex-shrink-0 select-none text-right bg-white dark:bg-gray-800 target:bg-yellow-200 target:dark:bg-yellow-600" -}} + {{- $linkStyle := "text-gray-400 dark:text-gray-500 hover:underline" -}} + {{- $lineNrSepStyle1 := "" -}} + {{- $lineNrSepStyle2 := "pr-2 border-r border-gray-200 dark:border-gray-700" -}} + {{- $containerStyle := "flex min-w-full items-center target:border target:rounded-sm target:border-yellow-200 target:dark:border-yellow-700 scroll-mt-20" -}} + {{- $addStyle := "bg-green-100 dark:bg-green-800/30 text-green-700 dark:text-green-400 " -}} + {{- $delStyle := "bg-red-100 dark:bg-red-800/30 text-red-700 dark:text-red-400 " -}} + {{- $ctxStyle := "bg-white dark:bg-gray-800 text-gray-500 dark:text-gray-400" -}} + {{- $opStyle := "w-5 flex-shrink-0 select-none text-center" -}} + {{- range .Lines -}} + {{- if eq .Op.String "+" -}} +
+
+ +
{{ .Op.String }}
+
{{ .Line }}
+
+ {{- $newStart = add64 $newStart 1 -}} + {{- end -}} + {{- if eq .Op.String "-" -}} +
+ +
+
{{ .Op.String }}
+
{{ .Line }}
+
+ {{- $oldStart = add64 $oldStart 1 -}} + {{- end -}} + {{- if eq .Op.String " " -}} +
+ + +
{{ .Op.String }}
+
{{ .Line }}
+
+ {{- $newStart = add64 $newStart 1 -}} + {{- $oldStart = add64 $oldStart 1 -}} + {{- end -}} + {{- end -}} + {{- end -}}
+{{ end }} + diff --git a/appview/pulls/pulls.go b/appview/pulls/pulls.go index 11f926d..907d2dc 100644 --- a/appview/pulls/pulls.go +++ b/appview/pulls/pulls.go @@ -414,6 +414,11 @@ func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { return } + var diffOpts types.DiffOpts + if d := r.URL.Query().Get("diff"); d == "split" { + diffOpts.Split = true + } + pull, ok := r.Context().Value("pull").(*db.Pull) if !ok { log.Println("failed to get pull") @@ -462,6 +467,10 @@ func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { interdiff := patchutil.Interdiff(previousPatch, currentPatch) + for _, f := range interdiff.Files { + log.Println("", "", f.Split()) + } + s.pages.RepoPullInterdiffPage(w, pages.RepoPullInterdiffParams{ LoggedInUser: s.oauth.GetUser(r), RepoInfo: f.RepoInfo(user), @@ -469,8 +478,8 @@ func (s *Pulls) RepoPullInterdiff(w http.ResponseWriter, r *http.Request) { Round: roundIdInt, DidHandleMap: didHandleMap, Interdiff: interdiff, + DiffOpts: diffOpts, }) - return } func (s *Pulls) RepoPullPatchRaw(w http.ResponseWriter, r *http.Request) { diff --git a/patchutil/interdiff.go b/patchutil/interdiff.go index 716a9a3..3a9b5e7 100644 --- a/patchutil/interdiff.go +++ b/patchutil/interdiff.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/bluekeyes/go-gitdiff/gitdiff" + "tangled.sh/tangled.sh/core/types" ) type InterdiffResult struct { @@ -35,6 +36,30 @@ type InterdiffFile struct { Status InterdiffFileStatus } +func (s *InterdiffFile) Split() *types.SplitDiff { + fragments := make([]types.SplitFragment, len(s.TextFragments)) + + for i, fragment := range s.TextFragments { + leftLines, rightLines := types.SeparateLines(fragment) + + fragments[i] = types.SplitFragment{ + Header: fragment.Header(), + LeftLines: leftLines, + RightLines: rightLines, + } + } + + return &types.SplitDiff{ + Name: s.Id(), + TextFragments: fragments, + } +} + +// used by html elements as a unique ID for hrefs +func (s *InterdiffFile) Id() string { + return s.Name +} + func (s *InterdiffFile) String() string { var b strings.Builder b.WriteString(s.Status.String()) diff --git a/types/split.go b/types/split.go new file mode 100644 index 0000000..d2ad0c6 --- /dev/null +++ b/types/split.go @@ -0,0 +1,131 @@ +package types + +import ( + "github.com/bluekeyes/go-gitdiff/gitdiff" +) + +type SplitLine struct { + LineNumber int `json:"line_number,omitempty"` + Content string `json:"content"` + Op gitdiff.LineOp `json:"op"` + IsEmpty bool `json:"is_empty"` +} + +type SplitFragment struct { + Header string `json:"header"` + LeftLines []SplitLine `json:"left_lines"` + RightLines []SplitLine `json:"right_lines"` +} + +type SplitDiff struct { + Name string `json:"name"` + TextFragments []SplitFragment `json:"fragments"` +} + +// used by html elements as a unique ID for hrefs +func (d *SplitDiff) Id() string { + return d.Name +} + +// separate lines into left and right, this includes additional logic to +// group consecutive runs of additions and deletions in order to align them +// properly in the final output +// +// TODO: move all diff stuff to a single package, we are spread across patchutil and types right now +func SeparateLines(fragment *gitdiff.TextFragment) ([]SplitLine, []SplitLine) { + lines := fragment.Lines + var leftLines, rightLines []SplitLine + oldLineNum := fragment.OldPosition + newLineNum := fragment.OldPosition + + // process deletions and additions in groups for better alignment + i := 0 + for i < len(lines) { + line := lines[i] + + switch line.Op { + case gitdiff.OpContext: + leftLines = append(leftLines, SplitLine{ + LineNumber: int(oldLineNum), + Content: line.Line, + Op: gitdiff.OpContext, + IsEmpty: false, + }) + rightLines = append(rightLines, SplitLine{ + LineNumber: int(newLineNum), + Content: line.Line, + Op: gitdiff.OpContext, + IsEmpty: false, + }) + oldLineNum++ + newLineNum++ + i++ + + case gitdiff.OpDelete: + deletionCount := 0 + for j := i; j < len(lines) && lines[j].Op == gitdiff.OpDelete; j++ { + leftLines = append(leftLines, SplitLine{ + LineNumber: int(oldLineNum), + Content: lines[j].Line, + Op: gitdiff.OpDelete, + IsEmpty: false, + }) + oldLineNum++ + deletionCount++ + } + i += deletionCount + + additionCount := 0 + for j := i; j < len(lines) && lines[j].Op == gitdiff.OpAdd; j++ { + rightLines = append(rightLines, SplitLine{ + LineNumber: int(newLineNum), + Content: lines[j].Line, + Op: gitdiff.OpAdd, + IsEmpty: false, + }) + newLineNum++ + additionCount++ + } + i += additionCount + + // add empty lines to balance the sides + if deletionCount > additionCount { + // more deletions than additions - pad right side + for k := 0; k < deletionCount-additionCount; k++ { + rightLines = append(rightLines, SplitLine{ + Content: "", + Op: gitdiff.OpContext, + IsEmpty: true, + }) + } + } else if additionCount > deletionCount { + // more additions than deletions - pad left side + for k := 0; k < additionCount-deletionCount; k++ { + leftLines = append(leftLines, SplitLine{ + Content: "", + Op: gitdiff.OpContext, + IsEmpty: true, + }) + } + } + + case gitdiff.OpAdd: + // standalone addition (not preceded by deletion) + leftLines = append(leftLines, SplitLine{ + Content: "", + Op: gitdiff.OpContext, + IsEmpty: true, + }) + rightLines = append(rightLines, SplitLine{ + LineNumber: int(newLineNum), + Content: line.Line, + Op: gitdiff.OpAdd, + IsEmpty: false, + }) + newLineNum++ + i++ + } + } + + return leftLines, rightLines +} -- 2.43.0