ofsht 0.6.1

Git worktree management tool
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
# Contributing to ofsht

Thank you for your interest in contributing to ofsht! This document provides guidelines and information for contributors.

## Table of Contents

- [Development Setup]#development-setup
- [Development Workflow]#development-workflow
- [Code Architecture]#code-architecture
- [Testing]#testing
- [Coding Guidelines]#coding-guidelines
- [Documentation Style]#documentation-style
- [Submitting Changes]#submitting-changes

## Development Setup

### Prerequisites

- [Rust]https://www.rust-lang.org/tools/install 1.70+ (2021 edition)
- [mise]https://mise.jdx.dev/ - Development tool version manager (includes just and vhs)
- Git 2.5+ (for worktree support)

#### Installing mise

**macOS/Linux (Homebrew)**:
```bash
brew install mise
```

**Linux/macOS (Script)**:
```bash
curl https://mise.run | sh
```

**Other platforms**: See [mise installation guide](https://mise.jdx.dev/getting-started.html)

> [!TIP]
> **Dogfooding**: ofsht itself is also installable via mise: `mise use -g ubi:wadackel/ofsht`
>
> Note: `mise install` (without arguments) only installs development tools from `mise.toml` (just, vhs). Installing ofsht via mise is optional and requires the explicit `mise use -g` command above.

#### Installing Development Tools

After installing mise:
```bash
mise install  # Installs just and vhs as specified in mise.toml
```

This will install:
- `just` 1.43.1 - Command runner for development tasks
- `vhs` (latest) - Terminal demo video generator (optional, for documentation work)

### Building from Source

```bash
# Clone the repository
git clone https://github.com/wadackel/ofsht.git
cd ofsht

# Install development tools
mise install

# Build the project
cargo build

# Build with optimizations
cargo build --release

# Run all quality checks
just check
```

### Testing the CLI During Development

```bash
# Run the CLI in development mode
cargo run -- add feature-branch
cargo run -- ls
cargo run -- cd feature-branch
cargo run -- rm .
cargo run -- rm /path/to/worktree
cargo run -- rm feature-a feature-b  # Remove multiple worktrees
cargo run -- rm feature-a . feature-b  # Remove multiple including current

# Use release binary
./target/release/ofsht add feature-branch
```

See [TEST.md](./TEST.md) for comprehensive manual testing procedures.

## Development Workflow

This project follows **t_wada's philosophy**:
- **TDD**: Write tests first, then minimal implementation
- **Small steps**: Incremental changes with test verification
- **YAGNI**: Only implement what's needed now
- **Quality built-in**: All checks must pass before committing (`just check`)

### Available Just Recipes

**Development** (fast feedback):
- `just build` - Build the project
- `just test` - Run tests (simple)
- `just fmt` - Check code formatting (simple)
- `just clippy` - Run clippy linter (simple)

**CI-Equivalent** (strict):
- `just check` - Run all checks (fmt-ci + clippy-ci + test-ci)
- `just fmt-ci` - Check formatting (all packages)
- `just clippy-ci` - Run clippy (all targets, locked dependencies)
- `just test-ci` - Run tests (all targets, no-fail-fast)

**Utilities**:
- `just install` - Install ofsht to cargo bin directory
- `just clean` - Clean build artifacts
- `just help` (or `just --list`) - Show all available recipes

### Running Tests

```bash
# Run all tests
cargo test

# Run all tests (CI-equivalent)
just test-ci

# Run specific test module
cargo test config::tests
cargo test integrations::zoxide::tests
```

See [TEST_COVERAGE.md](./TEST_COVERAGE.md) for comprehensive test coverage tracking and metrics.

### Code Quality Checks

```bash
# Check formatting
just fmt-ci

# Run clippy with strict warnings
just clippy-ci

# Run all checks (required before committing)
just check
```

## Code Architecture

### Refactoring Benefits (v0.2.0)

The codebase underwent comprehensive refactoring in [PR #43](https://github.com/wadackel/ofsht/pull/43) to improve:
- **Maintainability**: `main.rs` reduced from ~1800 to ~160 lines (90% reduction)
- **Testability**: 230 tests total (177 bin + 53 integration) with clear module boundaries
- **Code Organization**: Commands, domain logic, and integrations properly separated into focused modules
- **Error Handling**: All `unwrap()` calls replaced with explicit error handling using `let-else` and pattern matching

### Module Structure

The codebase follows a modular design with clear separation of concerns:

```
src/
├── main.rs              # CLI entry point and command routing
├── cli.rs               # CLI argument definitions (clap)
├── color.rs             # Color output utilities
├── hooks.rs             # Hook execution engine (run/copy/link)
├── service.rs           # Service layer orchestrating git, hooks, and integrations
├── commands/
│   ├── common.rs        # Shared utilities for command handlers
│   ├── open.rs          # Open all worktrees in tmux
│   └── sync.rs          # Sync hooks to existing worktrees
├── config/
│   ├── mod.rs           # Config module root (re-exports)
│   ├── schema.rs        # Type definitions and templates
│   └── loader.rs        # Configuration loading logic
├── domain/
│   └── worktree.rs      # Domain entities and parsers
└── integrations/
    ├── mod.rs           # Integrations module root
    ├── fzf.rs           # fzf integration for interactive selection
    ├── git.rs           # Git client abstraction
    ├── tmux.rs          # tmux integration for window/pane creation
    ├── zoxide.rs        # zoxide integration with graceful degradation
    └── gh/              # GitHub CLI integration
        ├── mod.rs
        ├── client.rs
        └── input.rs
```

### Key Design Patterns

**Two-tier Configuration System** (`config/loader.rs`)
- Local config: `.ofsht.toml` in **main repository root** (highest priority)
  - **Always loaded from main repository root**, not from individual worktrees
  - Use `Config::load_from_repo_root(&repo_root)` for git operations
  - Ensures consistent behavior across all worktrees
  - **Integration settings are NOT read from local config** - they are always sourced from global config
- Global config: Respects `XDG_CONFIG_HOME` on all platforms
  - Uses `$XDG_CONFIG_HOME/ofsht/config.toml` if set (must be absolute path)
  - Fallbacks to `~/.config/ofsht/config.toml` otherwise
  - **Integration settings (`zoxide`, `fzf`, `tmux`, `gh`) are ONLY available in global config**
- Default config: Hard-coded defaults if no files exist
- The `Config::load_impl()` function handles this cascading logic

**Hook Execution Flow** (`hooks.rs`)
1. Commands run first (`actions.run`)
2. Files copied second (`actions.copy`)
3. Symlinks created last (`actions.link`)
- All hooks execute in the worktree directory context
- Source files are resolved from the original repository
- Missing source files emit warnings but don't fail execution
- **Output Streams**: Hook command stdout/stderr are redirected to stderr to prevent polluting the stdout stream

**Graceful Degradation for zoxide** (`integrations/zoxide.rs`)
- Checks `zoxide --version` to detect availability
- If zoxide is not installed, silently skips registration
- If installed but `enabled = false` in config, skips registration
- Only fails if zoxide is installed, enabled, and returns an error

**Interactive Selection with fzf** (`integrations/fzf.rs`)
- When `ofsht cd` or `ofsht rm` is run without arguments, fzf launches for interactive selection
- Graceful degradation: returns error if fzf is disabled or not installed
- Uses `FzfPicker` trait for testability (`MockFzfPicker` in tests)
- `build_worktree_items()` parses `git worktree list --porcelain` output
- `RealFzfPicker::pick()` handles multi-select mode (for `rm`) and single-select (for `cd`)
- Exit codes 130/1 (Esc/no selection) are treated as Ok(vec![]) rather than errors
- Preview window shows `git log --oneline -n 10` for each worktree

**Path Template Expansion** (`commands/add.rs`, `commands/create.rs`)
- Template variables: `{repo}` and `{branch}`
- Supports both absolute paths (`/tmp/worktrees/...`) and relative paths (`../{repo}-worktrees/...`)
- Repository name extracted from main repository root directory name
- Relative paths are resolved from main repository root (not current directory)
- Default template: `../{repo}-worktrees/{branch}`
- Uses `git rev-parse --git-common-dir` to find main repository (requires Git ≥2.5, 2015)

### Command Implementation

All commands in `src/commands/` modules follow a consistent pattern:
1. Get main repository root via `get_main_repo_root()` (from `commands/common.rs`)
2. Load configuration via `Config::load_from_repo_root()`
3. Execute git command via `Command::new("git")`
4. Check exit status and handle stderr
5. Execute side effects (hooks, zoxide, tmux)

**Command Modules** (in `src/commands/`):
- **add.rs** (`cmd_new`): GitHub integration (PR/issue detection) → worktree creation → hooks → zoxide → optional tmux window/pane
- **cd.rs** (`cmd_goto`): Parse worktree list → find by branch name → optional fzf selection → print path for shell integration
- **common.rs**: Shared utilities (`get_main_repo_root`, `parse_all_worktrees`, `resolve_worktree_target`, etc.)
- **completion.rs** (`cmd_completion`): Print shell-specific completion setup instructions
- **create.rs** (`cmd_create`): Simple worktree creation without GitHub/tmux (deprecated command)
- **init.rs** (`cmd_init`): Generate global/local config templates
- **list.rs** (`cmd_list`): Format and display worktree list (interactive vs pipe mode)
- **open.rs** (`cmd_open`): Open all worktrees in tmux windows or panes, skipping the current worktree
- **rm.rs** (`cmd_rm_many`): Multi-target removal with fzf support → duplicate detection → current worktree last
- **shell_init.rs** (`cmd_shell_init`): Generate shell wrapper functions for cd/add/rm integration
- **sync.rs** (`cmd_sync`): Re-apply `hooks.create` (run/copy/link) to all existing non-main worktrees with `--run`/`--copy`/`--link` flag filtering

**Key Shared Functions** (in `commands/common.rs`):
- `get_main_repo_root()`: Find main repo using `git rev-parse --git-common-dir`
- `resolve_worktree_target()`: Resolve branch name, path, or `.` to canonical worktree path
- `parse_all_worktrees()`: Parse `git worktree list --porcelain` output
- `find_worktree_by_branch()`: Locate worktree path by branch name

### Configuration Schema

```toml
[worktree]
dir = "../{repo}-worktrees/{branch}"

[hooks.create]
run = ["command1", "command2"]  # Shell commands
copy = ["file1", "file2"]       # Files to copy
link = { "src" = "dest" }       # Symlinks (src in repo -> dest in worktree)

[hooks.delete]
run = ["command1"]
# Note: copy and link actions are not supported in delete hooks

[integration.zoxide]
enabled = true  # Default: true (only available in global config)

[integration.fzf]
enabled = true  # Default: true (only available in global config)
options = ["--height=50%", "--border"]  # Additional fzf command-line options

[integration.tmux]
behavior = "auto"   # "auto" (flag-based, default), "always", "never"
create = "window"   # "window" (default), "pane"
open = "window"     # Default mode for `ofsht open`: "window" or "pane"

[integration.gh]
enabled = true  # Default: true (only available in global config)
```

## Testing

### Testing Strategy

- Each module has a `#[cfg(test)]` section with unit tests
- Test-first development: write test, see it fail, implement, see it pass
- Mock external dependencies (e.g., `MockZoxideClient` in `zoxide.rs`)
- Integration tests verify actual git and zoxide commands work
- Shell completion tests (`tests/completions.rs`): smoke tests that verify generation succeeds and basic commands are present

### Manual Testing

See [TEST.md](./TEST.md) for comprehensive manual testing procedures covering:
- Worktree creation and navigation
- Hook execution
- zoxide integration
- fzf interactive selection
- Shell completion
- Configuration loading
- Error handling

### Demo Videos with VHS

Demo videos are generated using VHS from `.tape` files in `docs/assets/vhs/`.

**Generate a specific demo**:
```bash
just demo quick-start
```

**Generate all demos**:
```bash
just demo-all
```

**Validate tape files** (dry-run, no GIF generation):
```bash
just demo-verify
```

> [!NOTE]
> When contributing documentation changes, run `just demo-verify` to ensure tape files remain valid. Generated GIFs are committed to the repository for use in README.md.

## Coding Guidelines

### Clippy Configuration

Strict linting is enabled in `Cargo.toml`:
```toml
[lints.clippy]
all = { level = "warn", priority = -1 }
pedantic = { level = "warn", priority = -1 }
nursery = { level = "warn", priority = -1 }
```

Common allowed lints in code:
- `#[allow(clippy::literal_string_with_formatting_args)]` for path templates
- `#[allow(clippy::missing_const_for_fn)]` is fixed by making functions `const`

### Error Handling

Uses `anyhow::Result` throughout with `.context()` for error context:
```rust
.with_context(|| format!("Failed to read config file: {}", path.display()))?
```

### Git Integration

All git operations use `std::process::Command` with:
- `.output()` to capture stdout/stderr
- Status code checking
- UTF8 conversion with `String::from_utf8_lossy`

### Platform-Specific Code

Symlink creation differs between Unix and Windows (`hooks.rs:92-115`):
- Unix: `std::os::unix::fs::symlink`
- Windows: `symlink_dir` vs `symlink_file` based on source type

## Documentation Style

When contributing to documentation (README.md, CONTRIBUTING.md, TEST.md), follow these GitHub Alert patterns:

### GitHub Alerts Usage

Use GitHub-style alerts to highlight important information. Choose the appropriate alert type based on the content:

#### Alert Types and When to Use

**`> [!NOTE]`** - Factual clarifications or reminders (no action required)
- Capabilities, supported platforms, background context
- Reassuring users about edge cases
- Example: "ofsht can remove worktrees even if directories were manually deleted"

**`> [!TIP]`** - Productivity hints and optional shortcuts
- Alternate commands or workflows
- Recommended sequences that improve user experience
- Graceful degradation explanations
- Example: "Use `create` instead of `add` when you don't want auto-navigation"

**`> [!IMPORTANT]`** - Prerequisites and silent failure conditions
- Hard requirements before running a command
- Configuration constraints that silently disable features
- Settings that must be in specific files
- Example: "Integration settings are ONLY read from global config"

**`> [!WARNING]`** - Actions that destroy state (normal but destructive)
- Commands that overwrite files or delete data
- Permanent changes that can't be undone
- Example: "`ofsht init --force` will overwrite existing config files"

**`> [!CAUTION]`** - Environment-sensitive requirements
- Commands that require specific runtime environments
- Authentication or session prerequisites
- Platform-specific limitations
- Example: "tmux integration requires running inside an active tmux session"

### Alert Best Practices

- **Be concise**: Keep alerts to 2-3 sentences maximum
- **Be specific**: Mention exact commands, files, or error messages when relevant
- **Avoid alert proliferation**: Only use alerts for truly important callouts
- **Consistent formatting**: Use the same visual language throughout all docs
- **Test clarity**: If you can't decide between NOTE and TIP, it might not need an alert

### Examples from the Codebase

Good alert usage in README.md:
```markdown
> [!TIP]
> The `add` command automatically navigates to the new worktree when shell integration is enabled. Use `create` instead if you want to stay in the current directory.

> [!IMPORTANT]
> Integration settings (`[integration.zoxide]`, `[integration.fzf]`, etc.) are ONLY read from global config. Placing them in `.ofsht.toml` will silently have no effect.

> [!CAUTION]
> tmux integration requires running `ofsht add` inside an active tmux session. Using `--tmux` outside of tmux will fail with an error.
```

## Submitting Changes

### Pull Request Process

1. Fork the repository and create a feature branch
2. Make your changes following the TDD workflow
3. Ensure all tests pass: `just check`
4. Write clear, descriptive commit messages
5. Push your branch and create a pull request
6. Address any review feedback

### Commit Message Guidelines

This project follows the [Conventional Commits](https://www.conventionalcommits.org/) specification. This enables automatic changelog generation and semantic versioning for releases.

#### Format

```
<type>[optional scope]: <description>

[optional body]

[optional footer(s)]
```

#### Commit Types

| Type | Description | Changelog Section | Version Bump |
|------|-------------|-------------------|--------------|
| `feat` | New feature | Added | Minor (0.x.0) |
| `fix` | Bug fix | Fixed | Patch (0.0.x) |
| `perf` | Performance improvement | Performance | Patch |
| `refactor` | Code refactoring | Changed | Patch |
| `docs` | Documentation changes | Documentation | Patch |
| `test` | Test additions/changes | Testing | Patch |
| `build` | Build system changes | Build | Patch |
| `deps` | Dependency updates | Dependencies | Patch |
| `ci` | CI/CD changes | CI/CD | Patch |
| `chore` | Other maintenance | Miscellaneous | Patch |
| `style` | Code style changes | Styling | Patch |

#### Breaking Changes

Breaking changes trigger a major version bump (x.0.0 in 1.x, or 0.x.0 in 0.x):

```bash
# Method 1: Use BREAKING CHANGE in footer
git commit -m "feat: redesign config file format

BREAKING CHANGE: Config file format changed from JSON to TOML"

# Method 2: Add ! after type
git commit -m "feat!: redesign config file format"
```

#### Examples

```bash
# Feature addition (bumps minor version)
git commit -m "feat: add fzf integration for interactive worktree selection"

# Bug fix (bumps patch version)
git commit -m "fix: resolve terminal hang by properly handling stdio pipes"

# Performance improvement
git commit -m "perf: optimize worktree path resolution"

# Documentation update
git commit -m "docs: standardize all content to English"

# Dependency update
git commit -m "deps: update clap to 4.5.0"

# Breaking change
git commit -m "feat!: change default worktree directory template"
```

#### Optional Scope

Add scope to provide additional context:

```bash
git commit -m "feat(tmux): add pane creation support"
git commit -m "fix(config): resolve XDG_CONFIG_HOME path correctly"
git commit -m "docs(hooks): document run/copy/link actions"
```

> [!NOTE]
> Conventional Commits format is **required** for proper changelog generation. All pull requests must follow this convention.

## Release Process

### Automated Release Flow

ofsht uses an automated release process powered by [release-plz](https://release-plz.dev/):

1. **Development**: Make changes following conventional commits
2. **Release PR**: release-plz automatically creates/updates a PR with:
   - Version bump based on commit types
   - Updated CHANGELOG.md
   - Updated Cargo.toml version
3. **Merge**: Merge the release PR to trigger the release
4. **Automated Publishing**:
   - GitHub release is created
   - Binaries are built for all platforms (macOS arm64/x86_64, Linux x86_64/musl)
   - Binaries are attached to the GitHub release
   - Package is published to crates.io
   - **Homebrew formula is automatically updated via PR**

### Homebrew Distribution

The Homebrew tap (`wadackel/homebrew-tap`) is automatically updated on each release:

1. **Trigger**: The `update-homebrew-tap` job runs after binaries are uploaded
2. **Workflow**: Triggers `bump-formula.yaml` in the tap repository
3. **Formula Update**: Downloads release binaries, calculates SHA256, updates formula
4. **Pull Request**: Creates PR in tap repository with updated formula
5. **Review & Merge**: Manually review and merge the PR to publish the update

**Tap Repository**: https://github.com/wadackel/homebrew-tap

**Troubleshooting Homebrew Updates**:

- If the tap update fails, check the [tap repository actions]https://github.com/wadackel/homebrew-tap/actions
- The main release will still succeed even if tap update fails
- Tap updates can be triggered manually via the tap repository's Actions tab
- Required secrets/variables:
  - `OFSHT_APP_ID` (variable): GitHub App ID for tap repository access
  - `OFSHT_APP_PRIVATE_KEY` (secret): GitHub App private key
  - GitHub App must be installed on both `ofsht` and `homebrew-tap` repositories
  - GitHub App permissions required: actions (read/write), contents (read)

### mise ubi Distribution

ofsht binaries are automatically compatible with mise's [ubi backend](https://mise.jdx.dev/dev-tools/backends/ubi.html):

```bash
mise use -g ubi:wadackel/ofsht
```

**How it works**:
1. mise's ubi backend reads GitHub releases directly from this repository
2. Automatically detects the user's platform (OS + CPU architecture)
3. Downloads and extracts the appropriate binary from release assets
4. No maintainer action or registry registration required

**Requirements** (already met):
- ✅ Asset naming follows platform conventions: `ofsht-${target}.tar.gz`
- ✅ Archive contains single executable at root level
- ✅ Supported platforms: Linux (x86_64 gnu/musl), macOS (x86_64/aarch64)

**Important**: Asset naming is critical for mise compatibility. The release workflow (`.github/workflows/release.yaml:181-185`) generates archives in the format `ofsht-{target}.tar.gz` containing a single `ofsht` binary. Changing this format will break mise installations.

**Registry decision**: We intentionally do NOT register ofsht in the [mise registry](https://mise.jdx.dev/registry.html). The ubi backend works perfectly without registration, and users can install via `ubi:wadackel/ofsht` directly. This avoids the maintenance burden of keeping a registry entry updated.

### Manual Release

If you need to trigger a release manually:

1. Ensure all changes are committed and pushed
2. Use the GitHub Actions UI to run the release workflow
3. Or create a release tag manually: `git tag v0.x.y && git push origin v0.x.y`

> [!WARNING]
> Manual releases bypass the conventional commit validation and may result in incorrect version bumps or changelogs.

### Code Review

- All submissions require review before merging
- Address review comments promptly
- Be open to feedback and suggestions
- Ensure CI checks pass before requesting review

## Questions or Issues?

- Open an issue for bugs or feature requests
- Start a discussion for questions or design proposals
- Check existing issues before creating new ones

Thank you for contributing to ofsht!