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 }) }