mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 15:46:22 +00:00
180 lines
4.4 KiB
Go
180 lines
4.4 KiB
Go
package git
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"os/exec"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"bdev/internal/patch"
|
|
)
|
|
|
|
// DiffNameStatus returns file paths mapped to their operation.
|
|
// Diffs BASE against the working tree (not HEAD) so uncommitted patch
|
|
// applications are visible.
|
|
func DiffNameStatus(dir, base string) (map[string]patch.FileOp, error) {
|
|
out, err := Run(dir, "diff", "--name-status", "-M", base)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("diff --name-status %s: %w", base, err)
|
|
}
|
|
|
|
result := make(map[string]patch.FileOp)
|
|
for _, line := range strings.Split(out, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
fields := strings.Fields(line)
|
|
if len(fields) < 2 {
|
|
continue
|
|
}
|
|
|
|
status := fields[0]
|
|
path := fields[len(fields)-1]
|
|
|
|
switch {
|
|
case status == "M":
|
|
result[path] = patch.OpModified
|
|
case status == "A":
|
|
result[path] = patch.OpAdded
|
|
case status == "D":
|
|
result[path] = patch.OpDeleted
|
|
case strings.HasPrefix(status, "R"):
|
|
result[path] = patch.OpRenamed
|
|
default:
|
|
result[path] = patch.OpModified
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// DiffFull returns the complete unified diff of BASE vs working tree.
|
|
func DiffFull(dir, base string) ([]byte, error) {
|
|
out, err := RunBytes(dir, "diff", "-M", "--full-index", base)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("diff %s: %w", base, err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// DiffFile returns the working-tree diff for a single file.
|
|
func DiffFile(dir, base, file string) ([]byte, error) {
|
|
out, err := RunBytes(dir, "diff", "-M", "--full-index", base, "--", file)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("diff %s -- %s: %w", base, file, err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// DiffFiles returns working-tree diffs for multiple files in one call.
|
|
func DiffFiles(dir, base string, files []string) ([]byte, error) {
|
|
args := []string{"diff", "-M", "--full-index", base, "--"}
|
|
args = append(args, files...)
|
|
out, err := RunBytes(dir, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("diff %s -- [%d files]: %w", base, len(files), err)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// DiffNoIndexFile builds a patch for an untracked file as if it were added from /dev/null.
|
|
// git diff --no-index exits with status 1 when files differ; treat that as success.
|
|
func DiffNoIndexFile(dir, file string) ([]byte, error) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
|
|
defer cancel()
|
|
|
|
cmd := exec.CommandContext(ctx, "git", "diff", "--no-index", "--full-index", "/dev/null", file)
|
|
cmd.Dir = dir
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
|
|
err := cmd.Run()
|
|
if err == nil {
|
|
return stdout.Bytes(), nil
|
|
}
|
|
|
|
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
|
|
return stdout.Bytes(), nil
|
|
}
|
|
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
return nil, fmt.Errorf("diff --no-index %s: timed out", file)
|
|
}
|
|
return nil, fmt.Errorf("diff --no-index %s: %w\n%s", file, err, stderr.String())
|
|
}
|
|
|
|
// UntrackedFiles returns all untracked files (exclude-standard) in the repo.
|
|
func UntrackedFiles(dir string) ([]string, error) {
|
|
out, err := Run(dir, "ls-files", "--others", "--exclude-standard")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ls-files --others: %w", err)
|
|
}
|
|
if strings.TrimSpace(out) == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
var files []string
|
|
for _, line := range strings.Split(out, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line != "" {
|
|
files = append(files, line)
|
|
}
|
|
}
|
|
sort.Strings(files)
|
|
return files, nil
|
|
}
|
|
|
|
// DiffChangedPathsBetween returns changed paths between two revisions.
|
|
// It includes old and new paths for rename/copy records.
|
|
func DiffChangedPathsBetween(dir, fromRev, toRev string, pathspec ...string) ([]string, error) {
|
|
args := []string{
|
|
"diff",
|
|
"--name-status",
|
|
"--find-renames",
|
|
fmt.Sprintf("%s..%s", fromRev, toRev),
|
|
}
|
|
if len(pathspec) > 0 {
|
|
args = append(args, "--")
|
|
args = append(args, pathspec...)
|
|
}
|
|
|
|
out, err := Run(dir, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("diff --name-status %s..%s: %w", fromRev, toRev, err)
|
|
}
|
|
|
|
seen := make(map[string]bool)
|
|
for _, line := range strings.Split(out, "\n") {
|
|
line = strings.TrimSpace(line)
|
|
if line == "" {
|
|
continue
|
|
}
|
|
|
|
fields := strings.Split(line, "\t")
|
|
if len(fields) < 2 {
|
|
continue
|
|
}
|
|
|
|
for _, p := range fields[1:] {
|
|
p = strings.TrimSpace(p)
|
|
if p != "" {
|
|
seen[p] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
paths := make([]string, 0, len(seen))
|
|
for p := range seen {
|
|
paths = append(paths, p)
|
|
}
|
|
sort.Strings(paths)
|
|
return paths, nil
|
|
}
|