mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 15:46:22 +00:00
346 lines
8.4 KiB
Go
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
|
|
})
|
|
}
|