Files
BrowserOS/packages/browseros/tools/bdev/internal/engine/pull.go
2026-03-05 10:06:41 -08:00

346 lines
8.4 KiB
Go

package engine
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"bdev/internal/config"
"bdev/internal/git"
"bdev/internal/patch"
)
type PullOpts struct {
DryRun bool
Files []string
KeepLocalOnly bool
}
func Pull(ctx *config.Context, opts PullOpts) (*patch.PullResult, error) {
repoPatchSet, err := patch.ReadPatchSet(ctx.PatchesDir)
if err != nil {
return nil, fmt.Errorf("pull: reading repo patches: %w", err)
}
repoHead, err := git.HeadRev(ctx.PatchesRepo)
if err != nil {
return nil, fmt.Errorf("pull: reading patches repo HEAD: %w", err)
}
incrementalPaths, shouldUseIncremental, err := resolveIncrementalPaths(ctx, repoHead, opts.Files)
if err != nil {
return nil, fmt.Errorf("pull: resolving incremental scope: %w", err)
}
if shouldUseIncremental {
result, err := incrementalPull(ctx, repoPatchSet, incrementalPaths, opts.DryRun)
if err != nil {
return nil, err
}
sortPullResult(result)
return result, nil
}
result, err := fullPull(ctx, repoPatchSet, opts)
if err != nil {
return nil, err
}
sortPullResult(result)
return result, nil
}
func resolveIncrementalPaths(ctx *config.Context, repoHead string, filesFilter []string) ([]string, bool, error) {
if len(filesFilter) > 0 {
return nil, false, nil
}
if ctx.State == nil || ctx.State.LastPull == nil {
return nil, false, nil
}
lastPull := ctx.State.LastPull
if strings.TrimSpace(lastPull.PatchesRepoRev) == "" {
return nil, false, nil
}
if lastPull.BaseCommit != ctx.BaseCommit {
return nil, false, nil
}
if !git.CommitExists(ctx.PatchesRepo, lastPull.PatchesRepoRev) {
return nil, false, nil
}
if lastPull.PatchesRepoRev == repoHead {
return []string{}, true, nil
}
repoPaths, err := git.DiffChangedPathsBetween(
ctx.PatchesRepo,
lastPull.PatchesRepoRev,
repoHead,
"chromium_patches",
)
if err != nil {
return nil, false, err
}
seen := make(map[string]bool)
for _, repoPath := range repoPaths {
chromiumPath, ok := normalizeRepoPatchPath(repoPath)
if !ok {
continue
}
seen[chromiumPath] = true
}
paths := make([]string, 0, len(seen))
for p := range seen {
paths = append(paths, p)
}
sort.Strings(paths)
return paths, true, nil
}
func normalizeRepoPatchPath(repoPath string) (string, bool) {
p := filepath.ToSlash(strings.TrimSpace(repoPath))
if !strings.HasPrefix(p, "chromium_patches/") {
return "", false
}
chromiumPath := strings.TrimPrefix(p, "chromium_patches/")
chromiumPath = strings.TrimSuffix(chromiumPath, ".deleted")
chromiumPath = strings.TrimSuffix(chromiumPath, ".binary")
chromiumPath = strings.TrimSuffix(chromiumPath, ".rename")
if chromiumPath == "" {
return "", false
}
return chromiumPath, true
}
func incrementalPull(
ctx *config.Context,
repoPatchSet *patch.PatchSet,
paths []string,
dryRun bool,
) (*patch.PullResult, error) {
result := &patch.PullResult{}
for _, path := range paths {
repoPatch, exists := repoPatchSet.Patches[path]
if !exists {
if !dryRun {
if err := resetPathToBase(ctx, path); err != nil {
return nil, fmt.Errorf("pull: reverting removed patch %s: %w", path, err)
}
}
result.Reverted = append(result.Reverted, path)
continue
}
switch repoPatch.Op {
case patch.OpDeleted:
if !dryRun {
if err := deletePath(ctx, path); err != nil {
return nil, err
}
}
result.Deleted = append(result.Deleted, path)
case patch.OpBinary:
result.Skipped = append(result.Skipped, path)
default:
if !dryRun {
if err := resetPathToBase(ctx, path); err != nil {
return nil, fmt.Errorf("pull: resetting %s to base: %w", path, err)
}
if err := applyRepoPatch(ctx, repoPatch, path, result); err != nil {
return nil, err
}
} else {
result.Applied = append(result.Applied, path)
}
}
}
return result, nil
}
func fullPull(ctx *config.Context, repoPatchSet *patch.PatchSet, opts PullOpts) (*patch.PullResult, error) {
result := &patch.PullResult{}
diffOutput, err := git.DiffFull(ctx.ChromiumDir, ctx.BaseCommit)
if err != nil {
return nil, fmt.Errorf("pull: reading local diffs: %w", err)
}
localPatchSet, err := patch.ParseUnifiedDiff(diffOutput)
if err != nil {
return nil, fmt.Errorf("pull: parsing local diffs: %w", err)
}
delta := patch.Compare(localPatchSet, repoPatchSet)
if len(opts.Files) > 0 {
delta = filterDelta(delta, opts.Files)
}
if opts.DryRun {
result.Applied = append(delta.NeedsUpdate, delta.NeedsApply...)
result.Skipped = delta.UpToDate
result.Deleted = delta.Deleted
if opts.KeepLocalOnly {
result.LocalOnly = delta.Orphaned
} else {
result.Reverted = delta.Orphaned
}
return result, nil
}
filesToReset := make([]string, 0, len(delta.NeedsUpdate)+len(delta.Orphaned))
filesToReset = append(filesToReset, delta.NeedsUpdate...)
if !opts.KeepLocalOnly {
filesToReset = append(filesToReset, delta.Orphaned...)
}
for _, path := range filesToReset {
if err := resetPathToBase(ctx, path); err != nil {
return nil, fmt.Errorf("pull: resetting %s to base: %w", path, err)
}
}
if opts.KeepLocalOnly {
result.LocalOnly = append(result.LocalOnly, delta.Orphaned...)
} else {
result.Reverted = append(result.Reverted, delta.Orphaned...)
}
filesToApply := make([]string, 0, len(delta.NeedsUpdate)+len(delta.NeedsApply))
filesToApply = append(filesToApply, delta.NeedsUpdate...)
filesToApply = append(filesToApply, delta.NeedsApply...)
for _, path := range filesToApply {
repoPatch, ok := repoPatchSet.Patches[path]
if !ok || repoPatch.Op == patch.OpDeleted || repoPatch.Op == patch.OpBinary {
continue
}
if err := applyRepoPatch(ctx, repoPatch, path, result); err != nil {
return nil, err
}
}
for _, path := range delta.Deleted {
if err := deletePath(ctx, path); err != nil {
return nil, err
}
result.Deleted = append(result.Deleted, path)
}
result.Skipped = delta.UpToDate
return result, nil
}
func applyRepoPatch(
ctx *config.Context,
repoPatch *patch.FilePatch,
path string,
result *patch.PullResult,
) error {
patchContent := repoPatch.Content
patchFile := filepath.Join(ctx.PatchesDir, path)
if len(patchContent) == 0 {
onDiskContent, err := os.ReadFile(patchFile)
if err == nil {
patchContent = onDiskContent
}
}
if len(patchContent) == 0 {
result.Skipped = append(result.Skipped, path)
return nil
}
if !git.FileExistsInCommit(ctx.ChromiumDir, ctx.BaseCommit, path) {
_ = os.Remove(filepath.Join(ctx.ChromiumDir, path))
}
conflict, err := git.Apply(ctx.ChromiumDir, patchContent, patchFile)
if err != nil {
return fmt.Errorf("pull: applying %s: %w", path, err)
}
if conflict != nil {
conflict.File = path
conflict.RejectFile = path + ".rej"
result.Conflicts = append(result.Conflicts, *conflict)
} else {
result.Applied = append(result.Applied, path)
}
return nil
}
func resetPathToBase(ctx *config.Context, chromiumPath string) error {
if git.FileExistsInCommit(ctx.ChromiumDir, ctx.BaseCommit, chromiumPath) {
return git.CheckoutFiles(ctx.ChromiumDir, ctx.BaseCommit, []string{chromiumPath})
}
target := filepath.Join(ctx.ChromiumDir, chromiumPath)
if err := os.Remove(target); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
func deletePath(ctx *config.Context, chromiumPath string) error {
target := filepath.Join(ctx.ChromiumDir, chromiumPath)
if err := os.Remove(target); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("pull: deleting %s: %w", chromiumPath, err)
}
return nil
}
func filterDelta(d *patch.Delta, files []string) *patch.Delta {
fileSet := make(map[string]bool)
for _, f := range files {
fileSet[f] = true
}
filtered := &patch.Delta{}
for _, f := range d.NeedsUpdate {
if fileSet[f] {
filtered.NeedsUpdate = append(filtered.NeedsUpdate, f)
}
}
for _, f := range d.NeedsApply {
if fileSet[f] {
filtered.NeedsApply = append(filtered.NeedsApply, f)
}
}
for _, f := range d.UpToDate {
if fileSet[f] {
filtered.UpToDate = append(filtered.UpToDate, f)
}
}
for _, f := range d.Orphaned {
if fileSet[f] {
filtered.Orphaned = append(filtered.Orphaned, f)
}
}
for _, f := range d.Deleted {
if fileSet[f] {
filtered.Deleted = append(filtered.Deleted, f)
}
}
return filtered
}
func sortPullResult(result *patch.PullResult) {
sort.Strings(result.Applied)
sort.Strings(result.Skipped)
sort.Strings(result.Reverted)
sort.Strings(result.LocalOnly)
sort.Strings(result.Deleted)
sort.Slice(result.Conflicts, func(i, j int) bool {
return result.Conflicts[i].File < result.Conflicts[j].File
})
}