123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119 | package sandbox
import (
"fmt"
"path/filepath"
"strconv"
"strings"
"unicode/utf8"
"blinker/internal/diff"
)
// maxShown is the most of a file shown as text, before or after.
const maxShown = 1 << 20
// Describe shows a preview's changes, for the user to decide on: text
// files as a unified diff, with paths relative to project where they are
// in it, and the rest, with modes, in words. Names are quoted where they
// hold what could pass for something else, but the text is the command's:
// the caller strips terminal control sequences from it.
func (p *Preview) Describe(project string) (diffs, about string, err error) {
var d, notes []string
note := func(format string, args ...any) { notes = append(notes, fmt.Sprintf(format, args...)) }
for _, c := range p.Changes {
name := c.Path
if rel, err := filepath.Rel(project, c.Path); err == nil && filepath.IsLocal(rel) {
name = rel
}
name = quoted(name)
was, wasMode := c.Before.Type, c.Before.Mode
switch c.Kind {
case Write:
if c.Size > maxShown || c.Before.Size > maxShown {
note("%s: a file too large to show here, now %d bytes", name, c.Size)
} else {
old, err := c.Old()
if err != nil {
return "", "", err
}
data, err := c.Contents()
if err != nil {
return "", "", err
}
switch {
case !isText(string(old)) || !isText(string(data)):
note("%s: a binary file, now %d bytes", name, len(data))
case was == "" && len(data) == 0:
note("%s: a new empty file", name)
default:
d = append(d, diff.Unified(name, string(old), string(data), 3))
}
}
switch {
case was == "file" && wasMode != c.Mode:
note("%s: mode %o → %o", name, wasMode, c.Mode)
case was == "" && c.Mode&0o111 != 0:
note("%s: a new executable file (mode %o)", name, c.Mode)
case was != "file" && was != "":
note("%s: a file (mode %o), in place of %s", name, c.Mode, replaced(was))
}
case Mkdir:
switch {
case was == "":
note("%s/: a new directory (mode %o)", name, c.Mode)
case c.Opaque && was == "dir":
note("%s/: emptied, and holding only what is shown here", name)
case c.Opaque:
note("%s/: a directory, in place of %s", name, replaced(was))
}
if was == "dir" && wasMode != c.Mode {
note("%s/: mode %o → %o", name, wasMode, c.Mode)
}
case Symlink:
if was == "" || was == "symlink" {
note("%s: a symlink to %s", name, quoted(c.Target))
} else {
note("%s: a symlink to %s, in place of %s", name, quoted(c.Target), replaced(was))
}
case Remove:
if was == "dir" {
note("%s/: removed, with everything in it", name)
continue
}
note("%s: removed", name)
if was != "file" || c.Before.Size > maxShown {
continue
}
if old, err := c.Old(); err != nil {
return "", "", err
} else if isText(string(old)) {
d = append(d, diff.Unified(name, string(old), "", 3))
}
}
}
return strings.Join(d, ""), strings.Join(notes, "\n"), nil
}
// replaced names what a change replaced.
func replaced(was string) string {
switch was {
case "dir":
return "the directory there, and everything in it"
case "other":
return "the special file there"
}
return "the " + was + " there"
}
// quoted gives a name as it is, or quoted if it holds what could pass for
// part of the prompt: a newline starting a diff header, say.
func quoted(name string) string {
if q := strconv.Quote(name); q[1:len(q)-1] != name {
return q
}
return name
}
// isText reports whether data can be shown as text.
func isText(s string) bool { return utf8.ValidString(s) && !strings.ContainsRune(s, 0) }
|