project

package
v1.11.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 18, 2026 License: MIT Imports: 15 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL