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/repo.git or https://github.com/org/repo.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 := utils.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 } successCount := 0 failedCount := 0 skippedCount := 0 for _, project := range projects { log.Info("Fetching project: %s", project.Name) projectPath := filepath.Join(devPath, project.Name) for _, repo := range project.Repos { repoPath, err := repo.GetEffectivePath() if err != nil { log.Error(" Failed to determine path for %s: %s", repo.URL, err) failedCount++ continue } fullRepoPath := filepath.Join(projectPath, repoPath) if !isRepoCloned(fullRepoPath) { log.Verbose(isVerbose, " Skipping %s (not cloned)", repoPath) skippedCount++ continue } if dryRun { log.Info(" [DRY RUN] Would fetch: %s", repoPath) successCount++ continue } log.Info(" Fetching %s...", repoPath) if err := fetchRepo(fullRepoPath); err != nil { log.Error(" Failed to fetch %s: %s", repoPath, err) failedCount++ continue } log.Success(" Fetched %s", repoPath) successCount++ } } log.Info("") log.Info("Fetch complete: %d successful, %d skipped, %d failed", successCount, skippedCount, failedCount) }, }
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 := utils.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 := utils.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 := utils.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 } successCount := 0 failedCount := 0 skippedCount := 0 dirtyCount := 0 for _, project := range projects { log.Info("Pulling project: %s", project.Name) projectPath := filepath.Join(devPath, project.Name) for _, r := range project.Repos { repoPath, err := r.GetEffectivePath() if err != nil { log.Error(" Failed to determine path for %s: %s", r.URL, err) failedCount++ continue } fullRepoPath := filepath.Join(projectPath, repoPath) if !isRepoCloned(fullRepoPath) { log.Verbose(isVerbose, " Skipping %s (not cloned)", repoPath) skippedCount++ continue } if dryRun { log.Info(" [DRY RUN] Would pull: %s", repoPath) successCount++ continue } isDirty, err := repo.IsDirty(fullRepoPath) if err != nil { log.Error(" Failed to check status of %s: %s", repoPath, err) failedCount++ continue } if isDirty { log.Warn(" Skipping %s (has uncommitted changes)", repoPath) dirtyCount++ continue } log.Info(" Pulling %s...", repoPath) if err := repo.PullLatestCode(fullRepoPath); err != nil { if errors.Is(err, git.NoErrAlreadyUpToDate) { log.Info(" %s is already up to date", repoPath) successCount++ continue } log.Error(" Failed to pull %s: %s", repoPath, err) failedCount++ continue } log.Success(" Pulled %s", repoPath) successCount++ } } log.Info("") log.Info( "Pull complete: %d successful, %d skipped, %d dirty, %d failed", successCount, skippedCount, dirtyCount, failedCount, ) if dirtyCount > 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) { log.Start("Setting up project repositories") isVerbose := utils.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) log.Verbose(isVerbose, "Development path: %s", devPath) if dryRun { log.Info("Dry run mode - no actual changes will be made") } 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 } totalRepos := 0 clonedCount := 0 skippedCount := 0 failedCount := 0 for _, project := range projects { log.Info("Processing project: %s", project.Name) projectPath := filepath.Join(devPath, project.Name) if dryRun { log.Info(" [DRY RUN] Would ensure directory exists: %s", projectPath) } else { if err := os.MkdirAll(projectPath, 0o755); err != nil { log.Error(" Failed to create project directory: %s", err) continue } log.Verbose(isVerbose, " Project directory ready: %s", projectPath) } for _, repo := range project.Repos { totalRepos++ repoPath, err := repo.GetEffectivePath() if err != nil { log.Error(" Failed to determine path for %s: %s", repo.URL, err) failedCount++ continue } fullRepoPath := filepath.Join(projectPath, repoPath) if _, err := os.Stat(filepath.Join(fullRepoPath, ".git")); err == nil { log.Verbose(isVerbose, " Repository already exists: %s", repoPath) skippedCount++ continue } if dryRun { log.Info(" [DRY RUN] Would clone %s to %s", repo.URL, fullRepoPath) clonedCount++ continue } log.Info(" Cloning %s...", repoPath) if err := cloneRepository(repo.URL, fullRepoPath); err != nil { log.Error(" Failed to clone %s: %s", repo.URL, err) failedCount++ continue } log.Success(" Cloned %s", repoPath) clonedCount++ } } log.Info("") log.Info("Setup complete:") log.Info(" Total repositories: %d", totalRepos) log.Info(" Cloned: %d", clonedCount) log.Info(" Already present: %d", skippedCount) if failedCount > 0 { log.Warn(" Failed: %d", failedCount) } if failedCount > 0 { log.Warn("Some repositories failed to clone. Check the output above for details.") log.Info("Common issues:") log.Info(" - SSH key not configured for the repository host") log.Info(" - Repository URL is incorrect") log.Info(" - Network connectivity issues") } else if !dryRun && clonedCount > 0 { log.Success("All project repositories set up successfully!") } }, }
SetupCmd defines the cobra command for setting up project repositories. It ensures project directories exist and clones any missing 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 := utils.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 } fetchSuccess := 0 fetchFailed := 0 pullSuccess := 0 pullFailed := 0 skippedCount := 0 dirtyCount := 0 for _, project := range projects { log.Info("Syncing project: %s", project.Name) projectPath := filepath.Join(devPath, project.Name) for _, r := range project.Repos { repoPath, err := r.GetEffectivePath() if err != nil { log.Error(" Failed to determine path for %s: %s", r.URL, err) fetchFailed++ pullFailed++ continue } fullRepoPath := filepath.Join(projectPath, repoPath) if !isRepoCloned(fullRepoPath) { log.Verbose(isVerbose, " Skipping %s (not cloned)", repoPath) skippedCount++ continue } if dryRun { log.Info(" [DRY RUN] Would sync: %s", repoPath) fetchSuccess++ pullSuccess++ continue } log.Info(" Syncing %s...", repoPath) if err := fetchRepo(fullRepoPath); err != nil { log.Error(" Fetch failed: %s", err) fetchFailed++ } else { log.Verbose(isVerbose, " Fetched successfully") fetchSuccess++ } isDirty, err := repo.IsDirty(fullRepoPath) if err != nil { log.Error(" Failed to check status: %s", err) pullFailed++ continue } if isDirty { log.Warn(" Skipping pull (has uncommitted changes)") dirtyCount++ continue } if err := repo.PullLatestCode(fullRepoPath); err != nil { if errors.Is(err, git.NoErrAlreadyUpToDate) { log.Verbose(isVerbose, " Already up to date") pullSuccess++ continue } log.Error(" Pull failed: %s", err) pullFailed++ continue } log.Success(" Synced %s", repoPath) pullSuccess++ } } log.Info("") log.Info("Sync complete:") log.Info(" Fetch: %d successful, %d failed", fetchSuccess, fetchFailed) log.Info( " Pull: %d successful, %d failed, %d dirty, %d skipped", pullSuccess, pullFailed, dirtyCount, skippedCount, ) if dirtyCount > 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 ¶
This section is empty.