Developer Onboarding

Complete guide for new developers joining the govman project.

Welcome! šŸŽ‰

Thank you for your interest in contributing to govman! This guide will help you get started quickly and effectively.

What is govman?

govman is a Go Version Manager that allows developers to:

  • Install and manage multiple Go versions
  • Quickly switch between Go versions
  • Automatically switch versions based on project requirements
  • Work across Linux, macOS, and Windows

Prerequisites

Before you begin, ensure you have:

Required

  • VS Code with Go extension
  • delve debugger: go install github.com/go-delve/delve/cmd/dlv@latest
  • Terminal with shell support (Bash, Zsh, Fish, or PowerShell)

Check Your Setup

# Verify Go
go version  # Should be 1.20 or higher

# Verify Git
git --version

# Verify Make
make --version

Quick Start (5 Minutes)

1. Clone the Repository

# Clone
git clone https://github.com/justjundana/govman.git
cd govman

# Fork first if you plan to contribute
# Then clone your fork:
# git clone https://github.com/YOUR_USERNAME/govman.git

2. Build the Project

# Download dependencies
go mod download

# Build binary
make build

# Verify build
./build/govman --version

3. Run Tests

# Run all tests
make test

# Run with coverage
make test-coverage

# Run specific package tests
go test ./internal/manager

4. Try It Out

# Install locally (optional)
make install

# Or run directly
./build/govman list

# Try installing a Go version
./build/govman install 1.21.5

Project Structure

govman/
ā”œā”€ā”€ cmd/
│   └── govman/          # Main application entry point
│       └── main.go      # CLI initialization
│
ā”œā”€ā”€ internal/            # Private application code
│   ā”œā”€ā”€ cli/             # CLI commands (10 files)
│   ā”œā”€ā”€ config/          # Configuration management
│   ā”œā”€ā”€ downloader/      # Download & extraction
│   ā”œā”€ā”€ golang/          # Go releases API
│   ā”œā”€ā”€ logger/          # Logging system
│   ā”œā”€ā”€ manager/         # Core business logic
│   ā”œā”€ā”€ progress/        # Progress bars
│   ā”œā”€ā”€ shell/           # Shell integration
│   ā”œā”€ā”€ symlink/         # Symlink management
│   ā”œā”€ā”€ util/            # Utilities
│   └── version/         # Version information
│
ā”œā”€ā”€ docs/                # Documentation (you're here!)
ā”œā”€ā”€ scripts/             # Installation scripts
ā”œā”€ā”€ build/               # Build output (ignored by git)
ā”œā”€ā”€ Makefile             # Build automation
ā”œā”€ā”€ go.mod               # Go module definition
└── go.sum               # Dependency checksums

Key Files to Know

FilePurpose
cmd/govman/main.goApplication entry point
internal/cli/cli.goRoot command & CLI setup
internal/manager/manager.goCore business logic
internal/config/config.goConfiguration management
MakefileBuild commands

Development Workflow

1. Create a Branch

# Update main
git checkout main
git pull origin main

# Create feature branch
git checkout -b feature/my-new-feature

# Or for bug fixes
git checkout -b fix/issue-123

2. Make Changes

# Edit files
vim internal/cli/install.go

# Build frequently
make build

# Test your changes
go test ./internal/cli

# Run the binary
./build/govman install 1.21.5

3. Write Tests

// internal/cli/install_test.go
func TestInstall(t *testing.T) {
    tests := []struct {
        name    string
        version string
        wantErr bool
    }{
        {"valid version", "1.21.5", false},
        {"invalid version", "invalid", true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := runInstall(tt.version)
            if (err != nil) != tt.wantErr {
                t.Errorf("runInstall() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

4. Format and Lint

# Format code
go fmt ./...

# Run linter (install first: go install golang.org/x/lint/golint@latest)
golint ./...

# Run vet
go vet ./...

5. Commit Your Changes

# Stage changes
git add .

# Commit with descriptive message
git commit -m "feat: add support for arm64 architecture"

# Or for bug fixes
git commit -m "fix: resolve symlink issue on Windows"

Commit Message Format:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation changes
  • test: Test additions/changes
  • refactor: Code refactoring
  • chore: Maintenance tasks

6. Push and Create PR

# Push to your fork
git push origin feature/my-new-feature

# Create Pull Request on GitHub
# Go to https://github.com/justjundana/govman/pulls
# Click "New Pull Request"

Common Development Tasks

Adding a New CLI Command

  1. Create command file:
touch internal/cli/mycommand.go
  1. Implement command:
// internal/cli/mycommand.go
package cli

import (
    "github.com/spf13/cobra"
)

var myCmd = &cobra.Command{
    Use:   "mycommand <arg>",
    Short: "Description of my command",
    Long:  "Longer description...",
    Args:  cobra.ExactArgs(1),
    Run:   runMyCommand,
}

func runMyCommand(cmd *cobra.Command, args []string) {
    arg := args[0]
    
    // Use manager
    mgr := getManager()
    if err := mgr.MyOperation(arg); err != nil {
        _logger.Error("Operation failed: %v", err)
        os.Exit(1)
    }
    
    _logger.Success("Operation completed!")
}

func init() {
    // Add flags if needed
    myCmd.Flags().StringP("option", "o", "", "option description")
}
  1. Register command:
// internal/cli/command.go
func init() {
    rootCmd.AddCommand(
        // ... existing commands
        myCmd,  // Add your command
    )
}
  1. Add tests:
// internal/cli/mycommand_test.go
func TestMyCommand(t *testing.T) {
    // Test implementation
}

Adding a Configuration Option

  1. Update Config struct:
// internal/config/config.go
type Config struct {
    // Existing fields...
    MyNewOption string `mapstructure:"my_new_option"`
}
  1. Set default value:
func (c *Config) setDefaults() {
    // Existing defaults...
    viper.SetDefault("my_new_option", "default_value")
}
  1. Update example config:
# config.yaml.example
my_new_option: "default_value"  # Description
  1. Document in docs:
<!-- docs/configuration.md -->
### my_new_option
Description of the option.

Adding Shell Support

  1. Implement Shell interface:
// internal/shell/shell.go
type MyShell struct{}

func (s *MyShell) Name() string {
    return "myshell"
}

func (s *MyShell) ConfigFile() string {
    return filepath.Join(os.Getenv("HOME"), ".myshellrc")
}

func (s *MyShell) PathCommand(binPath string) string {
    return fmt.Sprintf(`export PATH="%s:$PATH"`, binPath)
}

func (s *MyShell) SetupCommands(binPath string) []string {
    return []string{
        "# govman initialization",
        fmt.Sprintf(`export PATH="%s:$PATH"`, binPath),
        // Auto-switch hook
        `govman_auto_switch() {`,
        `    if [[ -f .govman-version ]]; then`,
        `        local required_version=$(cat .govman-version 2>/dev/null)`,
        `        govman use "$required_version" >/dev/null 2>&1`,
        `    fi`,
        `}`,
        `PROMPT_COMMAND="govman_auto_switch; $PROMPT_COMMAND"`,
    }
}
  1. Register shell:
// internal/shell/shell.go
func Detect() Shell {
    shell := os.Getenv("SHELL")
    switch {
    // Existing cases...
    case strings.Contains(shell, "myshell"):
        return &MyShell{}
    }
}

Testing Guidelines

Unit Tests

Test individual functions:

func TestNormalizeVersion(t *testing.T) {
    tests := []struct {
        input string
        want  string
    }{
        {"1.21.5", "go1.21.5"},
        {"go1.21.5", "go1.21.5"},
        {"1.21", "go1.21.0"},
    }
    
    for _, tt := range tests {
        t.Run(tt.input, func(t *testing.T) {
            got := normalizeVersion(tt.input)
            if got != tt.want {
                t.Errorf("normalizeVersion(%q) = %q, want %q", tt.input, got, tt.want)
            }
        })
    }
}

Integration Tests

Test component interactions:

//go:build integration

func TestRealInstall(t *testing.T) {
    if testing.Short() {
        t.Skip("Skipping integration test")
    }
    
    // Real test with actual downloads
    mgr := setupTestManager(t)
    err := mgr.Install("1.21.5")
    if err != nil {
        t.Fatalf("Install failed: %v", err)
    }
}

Run:

# Unit tests only
go test -short ./...

# Integration tests
go test -tags=integration ./...

Debugging

VS Code Debug Configuration

// .vscode/launch.json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug govman",
            "type": "go",
            "request": "launch",
            "mode": "debug",
            "program": "${workspaceFolder}/cmd/govman",
            "args": ["install", "1.21.5"],
            "env": {
                "GOVMAN_CONFIG": "${workspaceFolder}/test-config.yaml"
            }
        }
    ]
}

Command Line Debugging

# Using delve
dlv debug ./cmd/govman -- install 1.21.5

# Set breakpoint
(dlv) break internal/manager/manager.go:45
(dlv) continue

# Inspect
(dlv) print version
(dlv) locals
func (m *Manager) Install(version string) error {
    fmt.Printf("DEBUG: Install called with version=%q\n", version)
    fmt.Printf("DEBUG: config=%+v\n", m.config)
    
    // ... rest of function
}

Code Style

Follow Effective Go

Key Conventions

  1. Naming:
// Good
func GetVersion() string
var maxRetries int

// Bad
func get_version() string
var MAX_RETRIES int
  1. Error handling:
// Good
if err != nil {
    return fmt.Errorf("operation failed: %w", err)
}

// Bad
if err != nil {
    panic(err)
}
  1. Comments:
// Good: Comment explains why
// Use larger buffer for better performance with large files
const bufferSize = 64 * 1024

// Bad: Comment states the obvious
// Set buffer size
const bufferSize = 64 * 1024

Getting Help

Documentation

Community

Before Asking

  1. Check existing documentation
  2. Search closed issues
  3. Try debugging yourself
  4. Prepare a minimal reproduction

When Asking

Include:

  1. What you're trying to do
  2. What you've tried
  3. Relevant code snippets
  4. Error messages
  5. Environment details (OS, Go version)

Contribution Checklist

Before submitting a PR:

  • Code builds successfully: make build
  • Tests pass: make test
  • Code is formatted: go fmt ./...
  • Code is vetted: go vet ./...
  • New features have tests
  • Documentation is updated
  • Commit messages are clear
  • Branch is up to date with main

Learning Path

Week 1: Familiarization

  • Clone and build project
  • Run all tests
  • Read architecture documentation
  • Try using govman yourself
  • Explore codebase structure

Week 2: Small Changes

  • Fix a typo in documentation
  • Add a test case
  • Improve an error message
  • Submit your first PR

Week 3: Feature Development

  • Pick a "good first issue"
  • Implement the feature
  • Write comprehensive tests
  • Update documentation

Week 4+: Core Contributor

  • Review others' PRs
  • Help with issues
  • Propose new features
  • Improve architecture

Best Practices

1. Write Tests First

// Write the test
func TestNewFeature(t *testing.T) {
    got := newFeature()
    want := "expected"
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

// Then implement
func newFeature() string {
    return "expected"
}

2. Keep Changes Small

  • One PR per feature/fix
  • Easy to review
  • Faster to merge

3. Document As You Go

  • Update docs with code
  • Add code comments
  • Write clear commit messages

4. Communicate Early

  • Open issue before big changes
  • Ask questions in discussions
  • Request feedback early

Next Steps

Now that you're set up:

  1. Explore: Browse the codebase, read the architecture docs
  2. Build: Make a small change and see it work
  3. Contribute: Pick an issue and submit a PR
  4. Learn: Review others' code, ask questions

Welcome to the team! šŸš€

See Also


Happy coding! We're excited to have you here! šŸ’»āœØ