Documentation
¶
Overview ¶
Package project provides cobra commands for managing project-based repository collections. A project is a logical grouping of related repositories (e.g., a product with multiple microservices).
Index ¶
Constants ¶
This section is empty.
Variables ¶
var AddCmd = &cobra.Command{ Use: "add", Short: "Add a new project or repository to configuration", Long: `This command interactively adds a new project or adds a repository to an existing project. You will be prompted for: - Project name (new or existing) - Repository URL (SSH or HTTPS) - Optional custom directory name Example: eng project add # Interactive add eng project add -p MyProject # Add a repo to the specified project`, Run: func(cmd *cobra.Command, args []string) { log.Start("Adding project configuration") projectFilter, _ := cmd.Flags().GetString("project") existingNames := config.GetProjectNames() var projectName string if projectFilter != "" { projectName = projectFilter found := false for _, name := range existingNames { if name == projectFilter { found = true break } } if !found { // Project doesn't exist, confirm creation var confirmCreate bool prompt := &survey.Confirm{ Message: "Project '" + projectFilter + "' doesn't exist. Create it?", Default: true, } if err := survey.AskOne(prompt, &confirmCreate); err != nil { log.Error("Prompt failed: %s", err) return } if !confirmCreate { log.Info("Canceled.") return } } } else { options := append([]string{"[Create new project]"}, existingNames...) var selection string prompt := &survey.Select{ Message: "Select a project or create a new one:", Options: options, } if err := survey.AskOne(prompt, &selection); err != nil { log.Error("Prompt failed: %s", err) return } if selection == "[Create new project]" { namePrompt := &survey.Input{ Message: "Enter new project name:", } if err := survey.AskOne(namePrompt, &projectName, survey.WithValidator(survey.Required)); err != nil { log.Error("Prompt failed: %s", err) return } } else { projectName = selection } } // Get repository URL var repoURL string urlPrompt := &survey.Input{ Message: "Enter repository URL (SSH or HTTPS):", Help: "Examples: git@github.com:org/git.git or https://github.com/org/git.git", } if err := survey.AskOne(urlPrompt, &repoURL, survey.WithValidator(survey.Required)); err != nil { log.Error("Prompt failed: %s", err) return } defaultPath, err := config.RepoNameFromURL(repoURL) if err != nil { log.Error("Could not parse repository URL: %s", err) return } // Ask for optional custom path var customPath string pathPrompt := &survey.Input{ Message: "Custom directory name (leave empty for default):", Default: "", Help: "Default: " + defaultPath, } if err := survey.AskOne(pathPrompt, &customPath); err != nil { log.Error("Prompt failed: %s", err) return } repo := config.ProjectRepo{ URL: repoURL, Path: customPath, } existingProject := config.GetProjectByName(projectName) if existingProject == nil { newProject := config.Project{ Name: projectName, Repos: []config.ProjectRepo{repo}, } if err := config.AddProject(newProject); err != nil { log.Error("Failed to add project: %s", err) return } log.Success("Created new project '%s' with repository", projectName) } else { if err := config.AddRepoToProject(projectName, repo); err != nil { log.Error("Failed to add repository: %s", err) return } log.Success("Added repository to project '%s'", projectName) } // Ask if they want to add more var addMore bool morePrompt := &survey.Confirm{ Message: "Add another repository?", Default: false, } if err := survey.AskOne(morePrompt, &addMore); err != nil { log.Error("Prompt failed: %s", err) return } if addMore { _ = cmd.Flags().Set("project", projectName) cmd.Run(cmd, args) return } log.Info("") log.Info("Run 'eng project setup' to clone the new repositories.") updatedProjects := config.GetProjects() for _, p := range updatedProjects { if p.Name == projectName { log.Info("") log.Info("Project '%s' now has %d repository(ies):", p.Name, len(p.Repos)) for _, r := range p.Repos { path, _ := r.GetEffectivePath() log.Info(" - %s", path) } break } } }, }
AddCmd defines the cobra command for adding projects or repositories. It provides interactive prompts for configuration.
var FetchCmd = &cobra.Command{ Use: "fetch", Short: "Fetch updates for all project repositories", Long: `This command fetches updates from remote for all repositories in configured projects. Example: eng project fetch # Fetch all projects eng project fetch -p MyProject # Fetch only the specified project eng project fetch --dry-run # Preview what would be fetched`, Run: func(cmd *cobra.Command, args []string) { log.Start("Fetching project repositories") isVerbose := cmdutil.IsVerbose(cmd) dryRun, _ := cmd.Parent().PersistentFlags().GetBool("dry-run") projectFilter, _ := cmd.Parent().PersistentFlags().GetString("project") devPath := viper.GetString("git.dev_path") if devPath == "" { log.Error("Development folder path is not set. Use 'eng config git-dev-path' to set it.") return } devPath = os.ExpandEnv(devPath) if dryRun { log.Info("Dry run mode - no actual git operations will be performed") } projects := config.GetProjects() if len(projects) == 0 { log.Warn("No projects configured. Use 'eng project add' to add a project.") return } if projectFilter != "" { filtered := make([]config.Project, 0) for _, p := range projects { if p.Name == projectFilter { filtered = append(filtered, p) break } } if len(filtered) == 0 { log.Error("Project '%s' not found in configuration", projectFilter) return } projects = filtered } var successCount atomic.Int32 var failedCount atomic.Int32 var skippedCount atomic.Int32 multi, err := ui.NewMultiSpinner() if err != nil { log.Error("Failed to initialize UI: %s", err) return } defer multi.Stop() var eg errgroup.Group eg.SetLimit(10) for _, project := range projects { projectPath := filepath.Join(devPath, project.Name) for _, repo := range project.Repos { r := repo eg.Go(func() error { repoPath, err := r.GetEffectivePath() if err != nil { failedCount.Add(1) return nil } fullRepoPath := filepath.Join(projectPath, repoPath) if !isRepoCloned(fullRepoPath) { if isVerbose { spinner := multi.AddSpinner(fmt.Sprintf("Skipping %s (not cloned)", repoPath)) spinner.Warning() } skippedCount.Add(1) return nil } if dryRun { spinner := multi.AddSpinner(fmt.Sprintf("[DRY RUN] Would fetch: %s", repoPath)) spinner.Success() successCount.Add(1) return nil } spinner := multi.AddSpinner(fmt.Sprintf("Fetching %s...", repoPath)) if err := fetchRepo(fullRepoPath); err != nil { spinner.Fail(fmt.Sprintf("Failed to fetch %s: %s", repoPath, err)) failedCount.Add(1) return nil } spinner.Success(fmt.Sprintf("Fetched %s", repoPath)) successCount.Add(1) return nil }) } } _ = eg.Wait() multi.Stop() log.Info("") log.Info( "Fetch complete: %d successful, %d skipped, %d failed", successCount.Load(), skippedCount.Load(), failedCount.Load(), ) }, }
FetchCmd defines the cobra command for fetching all project repositories. It runs git fetch on all repositories in configured projects.
var ListCmd = &cobra.Command{ Use: "list", Short: "List configured projects and their repositories", Long: `This command displays all configured projects and their repositories. Use the --verbose flag to see detailed information including: - Repository URLs - Clone status (✓ cloned / ✗ missing) - Local paths Example: eng project list # Show projects summary eng project list -v # Show detailed repository information eng project list -p MyProject # Show only the specified project`, Run: func(cmd *cobra.Command, args []string) { isVerbose := cmdutil.IsVerbose(cmd) projectFilter, _ := cmd.Parent().PersistentFlags().GetString("project") devPath := viper.GetString("git.dev_path") if devPath == "" { log.Warn("Development folder path is not set. Use 'eng config git-dev-path' to set it.") devPath = "(not configured)" } else { devPath = os.ExpandEnv(devPath) } projects := config.GetProjects() if len(projects) == 0 { log.Info("No projects configured.") log.Info("Use 'eng project add' to add a project.") return } if projectFilter != "" { filtered := make([]config.Project, 0) for _, p := range projects { if p.Name == projectFilter { filtered = append(filtered, p) break } } if len(filtered) == 0 { log.Error("Project '%s' not found in configuration", projectFilter) return } projects = filtered } log.Info("Development path: %s", devPath) log.Info("") for _, project := range projects { projectPath := filepath.Join(devPath, project.Name) if isVerbose { log.Info("Project: %s", project.Name) log.Info(" Path: %s", projectPath) log.Info(" Repositories (%d):", len(project.Repos)) for _, repo := range project.Repos { repoPath, err := repo.GetEffectivePath() if err != nil { log.Error(" ✗ %s (invalid path)", repo.URL) continue } fullRepoPath := filepath.Join(projectPath, repoPath) cloned := isRepoCloned(fullRepoPath) if cloned { log.Success(" ✓ %s", repoPath) } else { log.Warn(" ✗ %s (not cloned)", repoPath) } log.Info(" URL: %s", repo.URL) if repo.Path != "" { log.Info(" Custom path: %s", repo.Path) } } } else { clonedCount := 0 for _, repo := range project.Repos { repoPath, err := repo.GetEffectivePath() if err != nil { continue } fullRepoPath := filepath.Join(projectPath, repoPath) if isRepoCloned(fullRepoPath) { clonedCount++ } } statusIcon := "✓" if clonedCount < len(project.Repos) { statusIcon = "○" } log.Info("%s %s (%d/%d repos cloned)", statusIcon, project.Name, clonedCount, len(project.Repos)) } log.Info("") } if !isVerbose { log.Info("Use -v for detailed repository information") } }, }
ListCmd defines the cobra command for listing configured projects. It displays project names, repository counts, and clone status.
var ProjectCmd = &cobra.Command{ Use: "project", Short: "Manage project-based repository collections", Long: `This command facilitates the management of project-based repository collections. A project is a logical grouping of related repositories. For example, you might have a project containing multiple microservices, or a shared infrastructure project. Projects are stored in your development folder (configured via 'eng config git-dev-path'), with each project having its own subdirectory containing all related repositories. Example structure: ~/Development/ MyProject/ api/ web/ shared/ Infrastructure/ core/ auth/`, Run: func(cmd *cobra.Command, args []string) { showInfo, _ := cmd.Flags().GetBool("info") isVerbose := cmdutil.IsVerbose(cmd) if showInfo { log.Info("Current project management configuration:") devPath := viper.GetString("git.dev_path") if devPath == "" { log.Warn(" Development Path: Not Set") log.Info(" Use 'eng config git-dev-path' to set your development folder path") } else { log.Info(" Development Path: %s", devPath) } // Show configured projects var projects []map[string]interface{} if err := viper.UnmarshalKey("projects", &projects); err == nil && len(projects) > 0 { log.Info(" Configured Projects: %d", len(projects)) } else { log.Info(" Configured Projects: 0") log.Info(" Use 'eng project add' to configure a project") } return } if len(args) == 0 { log.Verbose(isVerbose, "No subcommand provided, showing help.") err := cmd.Help() cobra.CheckErr(err) } else { log.Verbose(isVerbose, "Subcommand '%s' provided.", args[0]) } }, }
ProjectCmd serves as the base command for all project management operations. It groups subcommands like setup, list, add, remove, fetch, pull, and sync.
var PullCmd = &cobra.Command{ Use: "pull", Short: "Pull updates for all project repositories", Long: `This command pulls the latest changes from remote for all repositories in configured projects. Note: Repositories with uncommitted changes will be skipped. Example: eng project pull # Pull all projects eng project pull -p MyProject # Pull only the specified project eng project pull --dry-run # Preview what would be pulled`, Run: func(cmd *cobra.Command, args []string) { log.Start("Pulling project repositories") isVerbose := cmdutil.IsVerbose(cmd) dryRun, _ := cmd.Parent().PersistentFlags().GetBool("dry-run") projectFilter, _ := cmd.Parent().PersistentFlags().GetString("project") devPath := viper.GetString("git.dev_path") if devPath == "" { log.Error("Development folder path is not set. Use 'eng config git-dev-path' to set it.") return } devPath = os.ExpandEnv(devPath) if dryRun { log.Info("Dry run mode - no actual git operations will be performed") } projects := config.GetProjects() if len(projects) == 0 { log.Warn("No projects configured. Use 'eng project add' to add a project.") return } if projectFilter != "" { filtered := make([]config.Project, 0) for _, p := range projects { if p.Name == projectFilter { filtered = append(filtered, p) break } } if len(filtered) == 0 { log.Error("Project '%s' not found in configuration", projectFilter) return } projects = filtered } var successCount atomic.Int32 var failedCount atomic.Int32 var skippedCount atomic.Int32 var dirtyCount atomic.Int32 multi, err := ui.NewMultiSpinner() if err != nil { log.Error("Failed to initialize UI: %s", err) return } defer multi.Stop() cmdCtx := cmd.Context() if cmdCtx == nil { cmdCtx = context.Background() } eg, ctx := errgroup.WithContext(cmdCtx) eg.SetLimit(10) for _, project := range projects { projectPath := filepath.Join(devPath, project.Name) for _, r := range project.Repos { repoObj := r eg.Go(func() error { repoPath, err := repoObj.GetEffectivePath() if err != nil { failedCount.Add(1) return nil } fullRepoPath := filepath.Join(projectPath, repoPath) if !isRepoCloned(fullRepoPath) { if isVerbose { spinner := multi.AddSpinner(fmt.Sprintf("Skipping %s (not cloned)", repoPath)) spinner.Warning() } skippedCount.Add(1) return nil } if dryRun { spinner := multi.AddSpinner(fmt.Sprintf("[DRY RUN] Would pull: %s", repoPath)) spinner.Success() successCount.Add(1) return nil } isDirty, err := repo.IsDirty(ctx, fullRepoPath) if err != nil { spinner := multi.AddSpinner(fmt.Sprintf("Checking %s...", repoPath)) spinner.Fail(fmt.Sprintf("Failed to check status of %s: %s", repoPath, err)) failedCount.Add(1) return nil } if isDirty { spinner := multi.AddSpinner(fmt.Sprintf("Skipping %s (has uncommitted changes)", repoPath)) spinner.Warning() dirtyCount.Add(1) return nil } spinner := multi.AddSpinner(fmt.Sprintf("Pulling %s...", repoPath)) if err := repo.PullLatestCode(ctx, fullRepoPath); err != nil { if errors.Is(err, git.NoErrAlreadyUpToDate) { spinner.Info(fmt.Sprintf("%s is already up to date", repoPath)) successCount.Add(1) return nil } spinner.Fail(fmt.Sprintf("Failed to pull %s: %s", repoPath, err)) failedCount.Add(1) return nil } spinner.Success(fmt.Sprintf("Pulled %s", repoPath)) successCount.Add(1) return nil }) } } _ = eg.Wait() multi.Stop() log.Info("") log.Info( "Pull complete: %d successful, %d skipped, %d dirty, %d failed", successCount.Load(), skippedCount.Load(), dirtyCount.Load(), failedCount.Load(), ) if dirtyCount.Load() > 0 { log.Warn("Some repositories were skipped due to uncommitted changes.") log.Info("Commit or stash your changes, then run again.") } }, }
PullCmd defines the cobra command for pulling all project repositories. It runs git pull on all repositories in configured projects.
var RemoveCmd = &cobra.Command{ Use: "remove", Short: "Remove a project or repository from configuration", Long: `This command removes a project or a repository from a project's configuration. Note: This only removes the entry from your configuration. It does NOT delete any files from disk. Example: eng project remove # Interactive removal eng project remove -p MyProject # Remove from the specified project`, Run: func(cmd *cobra.Command, args []string) { log.Start("Removing project configuration") projectFilter, _ := cmd.Flags().GetString("project") projects := config.GetProjects() if len(projects) == 0 { log.Info("No projects configured.") return } existingNames := config.GetProjectNames() var projectName string if projectFilter != "" { projectName = projectFilter found := false for _, name := range existingNames { if name == projectFilter { found = true break } } if !found { log.Error("Project '%s' not found", projectFilter) return } } else { prompt := &survey.Select{ Message: "Select a project:", Options: existingNames, } if err := survey.AskOne(prompt, &projectName); err != nil { log.Error("Prompt failed: %s", err) return } } project := config.GetProjectByName(projectName) if project == nil { log.Error("Project '%s' not found", projectName) return } options := []string{"[Remove entire project]"} for _, repo := range project.Repos { path, err := repo.GetEffectivePath() if err != nil { path = repo.URL } options = append(options, path) } var selection string selectPrompt := &survey.Select{ Message: "What would you like to remove?", Options: options, } if err := survey.AskOne(selectPrompt, &selection); err != nil { log.Error("Prompt failed: %s", err) return } if selection == "[Remove entire project]" { // Confirm project removal var confirm bool confirmPrompt := &survey.Confirm{ Message: "Remove project '" + projectName + "' with " + strconv.Itoa( len(project.Repos), ) + " repositories?", Default: false, } if err := survey.AskOne(confirmPrompt, &confirm); err != nil { log.Error("Prompt failed: %s", err) return } if !confirm { log.Info("Canceled.") return } if err := config.RemoveProject(projectName); err != nil { log.Error("Failed to remove project: %s", err) return } log.Success("Removed project '%s' from configuration", projectName) log.Info("Note: Files on disk were not deleted.") } else { // Find the repo URL for the selected path var repoURL string for _, repo := range project.Repos { path, _ := repo.GetEffectivePath() if path == selection { repoURL = repo.URL break } } if repoURL == "" { log.Error("Repository not found") return } // Confirm repo removal var confirm bool confirmPrompt := &survey.Confirm{ Message: "Remove repository '" + selection + "' from project '" + projectName + "'?", Default: false, } if err := survey.AskOne(confirmPrompt, &confirm); err != nil { log.Error("Prompt failed: %s", err) return } if !confirm { log.Info("Canceled.") return } if err := config.RemoveRepoFromProject(projectName, repoURL); err != nil { log.Error("Failed to remove repository: %s", err) return } log.Success("Removed repository '%s' from project '%s'", selection, projectName) log.Info("Note: Files on disk were not deleted.") } }, }
RemoveCmd defines the cobra command for removing projects or repositories. It provides interactive prompts for safe removal.
var SetupCmd = &cobra.Command{ Use: "setup", Short: "Setup project directories and clone missing repositories", Long: `This command ensures all configured projects have their directory structure set up and all repositories are cloned. It is safe to run multiple times - existing repositories will be skipped. Use this command when: - Setting up a new development machine - A new repository has been added to a project's configuration - You want to verify all project repos are present Example: eng project setup # Setup all projects eng project setup -p MyProject # Setup only the specified project eng project setup --dry-run # Preview what would be done`, Run: func(cmd *cobra.Command, args []string) { dryRun, _ := cmd.Parent().PersistentFlags().GetBool("dry-run") projectFilter, _ := cmd.Parent().PersistentFlags().GetString("project") opts := project.SetupOptions{ DryRun: dryRun, IsVerbose: cmdutil.IsVerbose(cmd), ProjectFilter: projectFilter, } project.Setup(cmd.Context(), opts) }, }
SetupCmd defines the cobra command for setting up project repositories.
var SyncCmd = &cobra.Command{ Use: "sync", Short: "Sync all project repositories (fetch + pull)", Long: `This command synchronizes all repositories in configured projects by: 1. Fetching updates from remote (git fetch --all --prune) 2. Pulling changes for the current branch (git pull) Repositories with uncommitted changes will have fetch performed but pull will be skipped. Example: eng project sync # Sync all projects eng project sync -p MyProject # Sync only the specified project eng project sync --dry-run # Preview what would be synced`, Run: func(cmd *cobra.Command, args []string) { log.Start("Syncing project repositories") isVerbose := cmdutil.IsVerbose(cmd) dryRun, _ := cmd.Parent().PersistentFlags().GetBool("dry-run") projectFilter, _ := cmd.Parent().PersistentFlags().GetString("project") devPath := viper.GetString("git.dev_path") if devPath == "" { log.Error("Development folder path is not set. Use 'eng config git-dev-path' to set it.") return } devPath = os.ExpandEnv(devPath) if dryRun { log.Info("Dry run mode - no actual git operations will be performed") } projects := config.GetProjects() if len(projects) == 0 { log.Warn("No projects configured. Use 'eng project add' to add a project.") return } if projectFilter != "" { filtered := make([]config.Project, 0) for _, p := range projects { if p.Name == projectFilter { filtered = append(filtered, p) break } } if len(filtered) == 0 { log.Error("Project '%s' not found in configuration", projectFilter) return } projects = filtered } var fetchSuccess atomic.Int32 var fetchFailed atomic.Int32 var pullSuccess atomic.Int32 var pullFailed atomic.Int32 var skippedCount atomic.Int32 var dirtyCount atomic.Int32 multi, err := ui.NewMultiSpinner() if err != nil { log.Error("Failed to initialize UI: %s", err) return } defer multi.Stop() cmdCtx := cmd.Context() if cmdCtx == nil { cmdCtx = context.Background() } eg, ctx := errgroup.WithContext(cmdCtx) eg.SetLimit(10) for _, project := range projects { projectPath := filepath.Join(devPath, project.Name) for _, r := range project.Repos { repoObj := r eg.Go(func() error { repoPath, err := repoObj.GetEffectivePath() if err != nil { fetchFailed.Add(1) pullFailed.Add(1) return nil } fullRepoPath := filepath.Join(projectPath, repoPath) if !isRepoCloned(fullRepoPath) { if isVerbose { spinner := multi.AddSpinner(fmt.Sprintf("Skipping %s (not cloned)", repoPath)) spinner.Warning() } skippedCount.Add(1) return nil } if dryRun { spinner := multi.AddSpinner(fmt.Sprintf("[DRY RUN] Would sync: %s", repoPath)) spinner.Success() fetchSuccess.Add(1) pullSuccess.Add(1) return nil } spinner := multi.AddSpinner(fmt.Sprintf("Syncing %s...", repoPath)) spinner.UpdateText(fmt.Sprintf("Fetching %s...", repoPath)) if err := fetchRepo(fullRepoPath); err != nil { spinner.Fail(fmt.Sprintf("Fetch failed for %s: %s", repoPath, err)) fetchFailed.Add(1) pullFailed.Add(1) return nil } fetchSuccess.Add(1) spinner.UpdateText(fmt.Sprintf("Checking %s...", repoPath)) isDirty, err := repo.IsDirty(ctx, fullRepoPath) if err != nil { spinner.Fail(fmt.Sprintf("Failed to check status for %s: %s", repoPath, err)) pullFailed.Add(1) return nil } if isDirty { spinner.Warning(fmt.Sprintf("Skipped pull for %s (has uncommitted changes)", repoPath)) dirtyCount.Add(1) return nil } spinner.UpdateText(fmt.Sprintf("Pulling %s...", repoPath)) if err := repo.PullLatestCode(ctx, fullRepoPath); err != nil { if errors.Is(err, git.NoErrAlreadyUpToDate) { spinner.Info(fmt.Sprintf("Synced %s (already up to date)", repoPath)) pullSuccess.Add(1) return nil } spinner.Fail(fmt.Sprintf("Pull failed for %s: %s", repoPath, err)) pullFailed.Add(1) return nil } spinner.Success(fmt.Sprintf("Synced %s", repoPath)) pullSuccess.Add(1) return nil }) } } _ = eg.Wait() multi.Stop() log.Info("") log.Info("Sync complete:") log.Info(" Fetch: %d successful, %d failed", fetchSuccess.Load(), fetchFailed.Load()) log.Info( " Pull: %d successful, %d failed, %d dirty, %d skipped", pullSuccess.Load(), pullFailed.Load(), dirtyCount.Load(), skippedCount.Load(), ) if dirtyCount.Load() > 0 { log.Warn("Some repositories were not pulled due to uncommitted changes.") } }, }
SyncCmd defines the cobra command for syncing all project repositories. It fetches and pulls all repositories in configured projects.
Functions ¶
This section is empty.
Types ¶
type ThreadSafeBuffer ¶ added in v1.13.0
type ThreadSafeBuffer struct {
// contains filtered or unexported fields
}
ThreadSafeBuffer is a goroutine-safe bytes.Buffer.
func (*ThreadSafeBuffer) String ¶ added in v1.13.0
func (b *ThreadSafeBuffer) String() string