package cmd import ( "fmt" "os" "path/filepath" "time" "bdev/internal/config" "bdev/internal/engine" "bdev/internal/git" "bdev/internal/log" "bdev/internal/ui" "github.com/spf13/cobra" ) var cloneCmd = &cobra.Command{ Use: "clone", Short: "Fresh-apply all patches (for CI/new checkouts)", Long: `Apply all patches from the patches repository onto the current Chromium checkout. Used for CI builds and new checkout setup. Unlike pull, clone does not compare existing state — it applies everything.`, RunE: runClone, } var ( clonePatchesRepo string cloneVerifyBase bool cloneClean bool cloneDryRun bool cloneName string ) func init() { cloneCmd.Flags().StringVar(&clonePatchesRepo, "patches-repo", "", "path to BrowserOS packages directory") cloneCmd.Flags().BoolVar(&cloneVerifyBase, "verify-base", false, "fail if HEAD != BASE_COMMIT") cloneCmd.Flags().BoolVar(&cloneClean, "clean", false, "reset all modified files to BASE before applying") cloneCmd.Flags().BoolVar(&cloneDryRun, "dry-run", false, "show what would be applied") cloneCmd.Flags().StringVar(&cloneName, "name", "", "checkout name (default: directory name)") rootCmd.AddCommand(cloneCmd) } func runClone(cmd *cobra.Command, args []string) error { cwd, err := os.Getwd() if err != nil { return fmt.Errorf("getting cwd: %w", err) } // Try loading existing context, or create one from flags ctx, err := config.LoadContext() if err != nil { // No existing .bros/ — need --patches-repo if clonePatchesRepo == "" { return fmt.Errorf("no .bros/ found and --patches-repo not specified") } patchesRepo, err := filepath.Abs(clonePatchesRepo) if err != nil { return fmt.Errorf("resolving patches repo: %w", err) } baseCommit, err := config.ReadBaseCommit(patchesRepo) if err != nil { return err } name := cloneName if name == "" { name = filepath.Base(cwd) } brosDir := filepath.Join(cwd, config.BrosDirName) cfg := &config.Config{ Name: name, PatchesRepo: patchesRepo, } if !cloneDryRun { if err := config.WriteConfig(brosDir, cfg); err != nil { return err } _ = os.MkdirAll(filepath.Join(brosDir, "logs"), 0o755) } chromiumVersion, _ := config.ReadChromiumVersion(patchesRepo) ctx = &config.Context{ Config: cfg, State: &config.State{}, ChromiumDir: cwd, BrosDir: brosDir, PatchesRepo: patchesRepo, PatchesDir: filepath.Join(patchesRepo, "chromium_patches"), BaseCommit: baseCommit, ChromiumVersion: chromiumVersion, } } if cloneDryRun { fmt.Println(ui.MutedStyle.Render("dry run — no files will be modified")) fmt.Println() } opts := engine.CloneOpts{ VerifyBase: cloneVerifyBase, Clean: cloneClean, DryRun: cloneDryRun, } result, err := engine.Clone(ctx, opts) if err != nil { return err } // Reuse pull rendering fmt.Println(ui.TitleStyle.Render("bdev clone")) fmt.Println() fmt.Printf(" %s %d patches applied\n", ui.SuccessStyle.Render("+"), len(result.Applied)) if len(result.Conflicts) > 0 { fmt.Printf(" %s %d conflicts\n", ui.ErrorStyle.Render("x"), len(result.Conflicts)) } if len(result.Deleted) > 0 { fmt.Printf(" %s %d files deleted\n", ui.DeletedPrefix, len(result.Deleted)) } if len(result.Conflicts) > 0 { fmt.Print(ui.RenderConflictReport(result.Conflicts)) } if !cloneDryRun { repoRev, _ := git.HeadRev(ctx.PatchesRepo) ctx.State.LastPull = &config.SyncEvent{ PatchesRepoRev: repoRev, BaseCommit: ctx.BaseCommit, Timestamp: time.Now(), FileCount: len(result.Applied) + len(result.Deleted), } _ = config.WriteState(ctx.BrosDir, ctx.State) logger := log.New(ctx.BrosDir) _ = logger.LogClone(ctx.BaseCommit, result) } if len(result.Conflicts) > 0 { return fmt.Errorf("%d conflicts — see above for details", len(result.Conflicts)) } return nil }