fix(pr): strip terminal control sequences from pr diff on a terminal

pr diff copied the server's patch bytes straight to stdout, so a pull
request could change the terminal title, clear the screen or move the
cursor of whoever viewed its diff. Add terminal.SafeWriter, which
filters streamed server text like terminal.SafeText when stdout is a
terminal, and use it for pr diff. Piped output stays byte-exact, so
`tea pr diff N | git apply` keeps working.

Signed-off-by: bircni <bircni@icloud.com>
This commit is contained in:
bircni committed 2026-10-11 21:49:14 +02:00
1 parent 299428bf10
commit b86fcb2ff5
4 files changed
+158 -1

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package terminal
import (
"io"
"unicode"
"unicode/utf8"
)
// maxSGRLength bounds how many bytes of a possible SGR sequence are held back between writes.
const maxSGRLength = 32
// SafeWriter returns w unchanged when tty is false, so piped output stays byte-exact.
// On a terminal it filters streamed server text like SafeText, and with allowSGR it keeps
// SGR color sequences (ESC [ ... m) while dropping every other escape sequence's ESC.
func SafeWriter(w io.Writer, tty, allowSGR bool) io.Writer {
if !tty {
return w
}
return &safeWriter{w: w, allowSGR: allowSGR}
}
type safeWriter struct {
w io.Writer
allowSGR bool
// pending holds a rune or SGR sequence split across writes.
pending []byte
}
func (s *safeWriter) Write(p []byte) (int, error) {
data := append(s.pending, p...)
out := make([]byte, 0, len(data))
i := 0
for i < len(data) {
if s.allowSGR && data[i] == '\x1b' {
n, complete := sgrLength(data[i:])
if !complete {
break
}
if n == 0 {
i++
continue
}
out = append(out, data[i:i+n]...)
i += n
continue
}
if !utf8.FullRune(data[i:]) {
break
}
r, size := utf8.DecodeRune(data[i:])
if r == '\t' || r == '\n' || !(unicode.IsControl(r) || unicode.Is(unicode.Cf, r)) {
out = utf8.AppendRune(out, r)
}
i += size
}
s.pending = append(s.pending[:0], data[i:]...)
if _, err := s.w.Write(out); err != nil {
return 0, err
}
return len(p), nil
}
// sgrLength returns the length of the SGR sequence at the start of b, which begins with ESC,
// or 0 when b starts another escape sequence. complete is false while b may be a truncated SGR sequence.
func sgrLength(b []byte) (n int, complete bool) {
if len(b) < 2 {
return 0, false
}
if b[1] != '[' {
return 0, true
}
for j := 2; j < len(b); j++ {
c := b[j]
if c == 'm' {
return j + 1, true
}
if j >= maxSGRLength || ((c < '0' || c > '9') && c != ';' && c != ':') {
return 0, true
}
}
return 0, false
}
+45
View File
@@ -0,0 +1,45 @@
// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package terminal
import (
"bytes"
"testing"
"github.com/stretchr/testify/require"
)
func TestSafeWriter(t *testing.T) {
const hostile = "+hello \x1b]0;PWNED\x07 \x1b[1;31mred\x1b[0m\x1b[2J\r\n\u009b2J\u202eend\t\xff\x1b[1111111111111111111111111111111111m\n"
var piped bytes.Buffer
_, err := SafeWriter(&piped, false, false).Write([]byte(hostile))
require.NoError(t, err)
require.Equal(t, hostile, piped.String())
for _, tc := range []struct {
name string
allowSGR bool
want string
}{
{"drops every escape", false, "+hello ]0;PWNED [1;31mred[0m[2J\n2Jend\t\uFFFD[1111111111111111111111111111111111m\n"},
{"keeps colors", true, "+hello ]0;PWNED \x1b[1;31mred\x1b[0m[2J\n2Jend\t\uFFFD[1111111111111111111111111111111111m\n"},
} {
t.Run(tc.name, func(t *testing.T) {
if !tc.allowSGR {
require.Equal(t, SafeText(hostile), tc.want)
}
for _, chunk := range []int{len(hostile), 1, 3} {
var out bytes.Buffer
w := SafeWriter(&out, true, tc.allowSGR)
for data := []byte(hostile); len(data) > 0; {
n := min(chunk, len(data))
written, err := w.Write(data[:n])
require.NoError(t, err)
require.Equal(t, n, written)
data = data[n:]
}
require.Equal(t, tc.want, out.String(), "chunk size %d", chunk)
}
})
}
}