-
Notifications
You must be signed in to change notification settings - Fork 0
feat(dotfiles): enhance pull command with detailed per-app file status tracking #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
926b4a3
3a35d1d
e3f9ecf
5f69531
c696d5f
63ebf68
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,19 +5,129 @@ import ( | |
| "os" | ||
|
|
||
| "github.com/malamtime/cli/model" | ||
| "github.com/pterm/pterm" | ||
| "github.com/sirupsen/logrus" | ||
| "github.com/urfave/cli/v2" | ||
| "go.opentelemetry.io/otel/attribute" | ||
| "go.opentelemetry.io/otel/trace" | ||
| ) | ||
|
|
||
| type dotfilePullFileResult struct { | ||
| path string | ||
| isSuccess bool | ||
| isSkipped bool | ||
| isFailed bool | ||
| } | ||
|
|
||
| // printPullResults prints the pull operation results in a formatted way | ||
| func printPullResults(result map[model.DotfileAppName][]dotfilePullFileResult, dryRun bool) { | ||
| // Calculate totals from result map | ||
| var totalProcessed, totalFailed, totalSkipped int | ||
| for _, fileResults := range result { | ||
| for _, fileResult := range fileResults { | ||
| if fileResult.isSuccess { | ||
| totalProcessed++ | ||
| } else if fileResult.isFailed { | ||
| totalFailed++ | ||
| } else if fileResult.isSkipped { | ||
| totalSkipped++ | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // No files to process | ||
| if totalProcessed == 0 && totalFailed == 0 && totalSkipped == 0 { | ||
| logrus.Infoln("No dotfiles found to process") | ||
| pterm.Info.Println("No dotfiles to process") | ||
| return | ||
| } | ||
|
|
||
| // Print header | ||
| fmt.Println() | ||
| if dryRun { | ||
| pterm.DefaultHeader.WithBackgroundStyle(pterm.NewStyle(pterm.BgBlue)).Println("DRY RUN - Dotfiles Pull Summary") | ||
| } else { | ||
| pterm.DefaultHeader.WithBackgroundStyle(pterm.NewStyle(pterm.BgGreen)).Println("Dotfiles Pull Summary") | ||
| } | ||
|
|
||
| // Print summary statistics | ||
| summaryData := pterm.TableData{ | ||
| {"Status", "Count"}, | ||
| } | ||
|
|
||
| if totalProcessed > 0 { | ||
| if dryRun { | ||
| summaryData = append(summaryData, []string{pterm.FgYellow.Sprint("Would Update"), fmt.Sprintf("%d", totalProcessed)}) | ||
| } else { | ||
| summaryData = append(summaryData, []string{pterm.FgGreen.Sprint("Updated"), fmt.Sprintf("%d", totalProcessed)}) | ||
| } | ||
| } | ||
|
|
||
| if totalFailed > 0 { | ||
| summaryData = append(summaryData, []string{pterm.FgRed.Sprint("Failed"), fmt.Sprintf("%d", totalFailed)}) | ||
| } | ||
|
|
||
| if totalSkipped > 0 { | ||
| summaryData = append(summaryData, []string{pterm.FgGray.Sprint("Skipped"), fmt.Sprintf("%d", totalSkipped)}) | ||
| } | ||
|
|
||
| pterm.DefaultTable.WithHasHeader().WithData(summaryData).Render() | ||
|
|
||
| // If there are no updates or failures, just show the summary | ||
| if totalProcessed == 0 && totalFailed == 0 { | ||
| pterm.Success.Println("All dotfiles are up to date") | ||
| return | ||
| } | ||
|
|
||
| // Build detailed table for updated and failed files | ||
| var detailsData pterm.TableData | ||
| detailsData = append(detailsData, []string{"App", "File", "Status"}) | ||
|
|
||
| // Collect all non-skipped files | ||
| for appName, fileResults := range result { | ||
| for _, fileResult := range fileResults { | ||
| if fileResult.isSkipped { | ||
| continue // Skip files that are already identical | ||
| } | ||
|
|
||
| var status string | ||
| if fileResult.isSuccess { | ||
| if dryRun { | ||
| status = pterm.FgYellow.Sprint("Would Update") | ||
| } else { | ||
| status = pterm.FgGreen.Sprint("Updated") | ||
| } | ||
| } else if fileResult.isFailed { | ||
| status = pterm.FgRed.Sprint("Failed") | ||
| } | ||
|
|
||
| detailsData = append(detailsData, []string{ | ||
| string(appName), | ||
| fileResult.path, | ||
| status, | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // Only show details table if there are updated or failed files | ||
| if len(detailsData) > 1 { | ||
| fmt.Println() // Add spacing | ||
| pterm.DefaultSection.Println("File Details") | ||
| pterm.DefaultTable.WithHasHeader().WithData(detailsData).Render() | ||
| } | ||
|
|
||
| // Log for debugging | ||
| logrus.Infof("Pull complete - Processed: %d, Skipped: %d, Failed: %d", totalProcessed, totalSkipped, totalFailed) | ||
| } | ||
|
|
||
| func pullDotfiles(c *cli.Context) error { | ||
| ctx, span := commandTracer.Start(c.Context, "dotfiles-pull", trace.WithSpanKind(trace.SpanKindClient)) | ||
| defer span.End() | ||
| SetupLogger(os.ExpandEnv("$HOME/" + model.COMMAND_BASE_STORAGE_FOLDER)) | ||
|
|
||
| apps := c.StringSlice("apps") | ||
| span.SetAttributes(attribute.StringSlice("apps", apps)) | ||
| dryRun := c.Bool("dry-run") | ||
| span.SetAttributes(attribute.StringSlice("apps", apps), attribute.Bool("dry-run", dryRun)) | ||
|
|
||
| config, err := configService.ReadConfigFile(ctx) | ||
| if err != nil { | ||
|
|
@@ -34,12 +144,28 @@ func pullDotfiles(c *cli.Context) error { | |
| Token: config.Token, | ||
| } | ||
|
|
||
| // Initialize app handlers based on apps parameter | ||
| var appHandlers map[model.DotfileAppName]model.DotfileApp | ||
|
|
||
| // Prepare filter if apps are specified | ||
| var filter *model.DotfileFilter | ||
| if len(apps) > 0 { | ||
| filter = &model.DotfileFilter{ | ||
| Apps: apps, | ||
| } | ||
| // Only include specified apps | ||
| allAppsMap := model.GetAllAppsMap() | ||
| appHandlers = make(map[model.DotfileAppName]model.DotfileApp) | ||
| for _, appNameStr := range apps { | ||
| appName := model.DotfileAppName(appNameStr) | ||
| if appHandler, exists := allAppsMap[appName]; exists { | ||
| appHandlers[appName] = appHandler | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if len(appHandlers) == 0 { | ||
| appHandlers = model.GetAllAppsMap() | ||
| } | ||
|
|
||
| // Fetch dotfiles from server | ||
|
|
@@ -56,31 +182,11 @@ func pullDotfiles(c *cli.Context) error { | |
| return nil | ||
| } | ||
|
|
||
| // Initialize app handlers based on apps parameter | ||
| var allApps map[model.DotfileAppName]model.DotfileApp | ||
| if len(apps) == 0 { | ||
| // If no specific apps specified, use all available apps | ||
| allApps = model.GetAllAppsMap() | ||
| } else { | ||
| // Only include specified apps | ||
| allAppsMap := model.GetAllAppsMap() | ||
| allApps = make(map[model.DotfileAppName]model.DotfileApp) | ||
| for _, appNameStr := range apps { | ||
| appName := model.DotfileAppName(appNameStr) | ||
| if appHandler, exists := allAppsMap[appName]; exists { | ||
| allApps[appName] = appHandler | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Process fetched dotfiles | ||
| totalProcessed := 0 | ||
| totalSkipped := 0 | ||
| totalFailed := 0 | ||
| result := map[model.DotfileAppName][]dotfilePullFileResult{} | ||
|
|
||
| for _, appData := range resp.Data.FetchUser.Dotfiles.Apps { | ||
| appName := model.DotfileAppName(appData.App) | ||
| app, exists := allApps[appName] | ||
| app, exists := appHandlers[appName] | ||
| if !exists { | ||
| logrus.Warnf("Unknown app type: %s", appData.App) | ||
| continue | ||
|
|
@@ -90,7 +196,6 @@ func pullDotfiles(c *cli.Context) error { | |
|
|
||
| // Collect files to process for this app | ||
| filesToProcess := make(map[string]string) | ||
| var pathsToBackup []string | ||
|
|
||
| for _, file := range appData.Files { | ||
| if len(file.Records) == 0 { | ||
|
|
@@ -102,6 +207,8 @@ func pullDotfiles(c *cli.Context) error { | |
| var selectedRecord *model.DotfileRecord | ||
| var latestRecord *model.DotfileRecord | ||
|
|
||
| result[appName] = make([]dotfilePullFileResult, 0) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This initialization is inside the loop over files ( if result[appName] == nil {
result[appName] = make([]dotfilePullFileResult, 0)
} |
||
|
|
||
| for i := range file.Records { | ||
| record := &file.Records[i] | ||
|
|
||
|
|
@@ -130,7 +237,6 @@ func pullDotfiles(c *cli.Context) error { | |
| // Adjust path for current user | ||
| adjustedPath := AdjustPathForCurrentUser(file.Path) | ||
| filesToProcess[adjustedPath] = selectedRecord.Content | ||
| pathsToBackup = append(pathsToBackup, adjustedPath) | ||
| } | ||
|
|
||
| if len(filesToProcess) == 0 { | ||
|
|
@@ -150,7 +256,10 @@ func pullDotfiles(c *cli.Context) error { | |
| for path, content := range filesToProcess { | ||
| if isEqual, exists := equalityMap[path]; exists && isEqual { | ||
| logrus.Debugf("Skipping %s - content is identical", path) | ||
| totalSkipped++ | ||
| result[appName] = append(result[appName], dotfilePullFileResult{ | ||
| path: path, | ||
| isSkipped: true, | ||
| }) | ||
| } else { | ||
| filesToUpdate[path] = content | ||
| pathsToActuallyBackup = append(pathsToActuallyBackup, path) | ||
|
|
@@ -162,38 +271,35 @@ func pullDotfiles(c *cli.Context) error { | |
| continue | ||
| } | ||
|
|
||
| // Backup files that will be modified | ||
| if err := app.Backup(ctx, pathsToActuallyBackup); err != nil { | ||
| results := make([]dotfilePullFileResult, 0) | ||
|
|
||
| // Backup files that will be modified (handles dry-run internally) | ||
| if err := app.Backup(ctx, pathsToActuallyBackup, dryRun); err != nil { | ||
| logrus.Warnf("Failed to backup files for %s: %v", appData.App, err) | ||
| } | ||
|
|
||
| // Save the updated files | ||
| if err := app.Save(ctx, filesToUpdate); err != nil { | ||
| // Save the updated files (handles dry-run internally) | ||
| if err := app.Save(ctx, filesToUpdate, dryRun); err != nil { | ||
| logrus.Errorf("Failed to save files for %s: %v", appData.App, err) | ||
| totalFailed += len(filesToUpdate) | ||
| for f := range filesToUpdate { | ||
| results = append(results, dotfilePullFileResult{ | ||
| path: f, | ||
| isFailed: true, | ||
| }) | ||
| } | ||
| } else { | ||
| totalProcessed += len(filesToUpdate) | ||
| for f := range filesToUpdate { | ||
| results = append(results, dotfilePullFileResult{ | ||
| path: f, | ||
| isSuccess: true, | ||
| }) | ||
| } | ||
| } | ||
| result[appName] = append(result[appName], results...) | ||
| } | ||
|
|
||
| if totalProcessed == 0 && totalFailed == 0 && totalSkipped == 0 { | ||
| logrus.Infoln("No dotfiles found to process") | ||
| fmt.Println("\n📭 No dotfiles to process") | ||
| } else if totalProcessed == 0 && totalFailed == 0 { | ||
| logrus.Infof("All dotfiles are up to date - Skipped: %d", totalSkipped) | ||
| fmt.Println("\n✅ All dotfiles are up to date") | ||
| fmt.Printf("🔄 Skipped: %d files (already identical)\n", totalSkipped) | ||
| } else { | ||
| logrus.Infof("Pull complete - Processed: %d, Skipped: %d, Failed: %d", totalProcessed, totalSkipped, totalFailed) | ||
| fmt.Printf("\n✅ Pull complete\n") | ||
| fmt.Printf("📥 Updated: %d files\n", totalProcessed) | ||
| if totalSkipped > 0 { | ||
| fmt.Printf("🔄 Skipped: %d files (already identical)\n", totalSkipped) | ||
| } | ||
| if totalFailed > 0 { | ||
| fmt.Printf("⚠️ Failed: %d files\n", totalFailed) | ||
| } | ||
| } | ||
| // Print the results | ||
| printPullResults(result, dryRun) | ||
|
|
||
| return nil | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current logic for handling the
--appsflag can lead to unexpected behavior. If a user provides only invalid app names, the command falls back to pulling dotfiles for all applications instead of none. This is not intuitive. The logic should be refactored to handle specified apps, warn about invalid ones, and only use all apps when the flag is not provided at all. This would be more consistent with thepushcommand's behavior.