Skip to content

Commit 075a288

Browse files
committed
feat: improve hook execution with real-time output and better error handling
1 parent e196274 commit 075a288

File tree

8 files changed

+148
-117
lines changed

8 files changed

+148
-117
lines changed

CHANGELOG.md

Lines changed: 4 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,8 @@
11
# Changelog
22

3-
All notable changes to Git Workers will be documented in this file.
3+
All notable changes to this project will be documented in the [GitHub Releases](https://github.com/wasabeef/yank-for-claude.nvim/releases) page.
44

5-
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6-
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5+
## Release History
76

8-
For detailed release notes and binary downloads, see [GitHub Releases](https://github.com/wasabeef/git-workers/releases).
9-
10-
## [Unreleased]
11-
12-
### Changed
13-
14-
- **BREAKING**: Removed command-line argument options (--list, --create, etc.) in favor of interactive menu-only interface
15-
- Simplified main.rs to focus solely on interactive menu operations
16-
- Improved worktree rename functionality with `git worktree repair` integration
17-
- Enhanced configuration lookup strategy:
18-
- Now checks current directory first (useful for bare repo worktrees)
19-
- Then checks parent directory's main/master worktree
20-
- Finally falls back to repository root
21-
- Improved path handling for worktree creation:
22-
- Paths are now canonicalized to eliminate "../" in display
23-
- "In subdirectory" option now correctly creates worktrees in subdirectories
24-
25-
### Added
26-
27-
- Edit hooks menu option (`λ`) for managing lifecycle hooks through the interface
28-
- Comprehensive Rustdoc documentation for all modules and functions
29-
- Current directory configuration lookup priority for .git-workers.toml
30-
- Parent directory configuration lookup for .git-workers.toml
31-
- Better error handling with mutex poison recovery in tests
32-
- Branch deletion functionality in batch delete operations
33-
- Orphaned branch detection when deleting worktrees
34-
- Repository URL validation in configuration files
35-
- New test files for batch delete and edit hooks functionality
36-
37-
### Fixed
38-
39-
- All clippy warnings resolved:
40-
- manual_div_ceil replaced with div_ceil() method
41-
- manual_unwrap_or patterns simplified
42-
- needless_borrows in format! macros removed
43-
- useless_vec replaced with arrays
44-
- manual_flatten replaced with .flatten() method
45-
- Test failures related to parent directory configuration search
46-
- ESC cancellation pattern tests updated for new code style
47-
- Worktree rename test expectations aligned with Git limitations
48-
- "In subdirectory" option now correctly creates worktrees in worktrees/ folder
49-
- Path display now shows clean canonical paths without "../"
50-
- Batch delete now properly deletes orphaned branches
51-
- Edit hooks no longer incorrectly identifies regular repos as bare
52-
53-
### Documentation
54-
55-
- Updated README.md with current features and usage:
56-
- Added configuration file lookup priority documentation
57-
- Updated worktree pattern examples
58-
- Added custom path creation examples
59-
- Added repository URL configuration example
60-
- Clarified batch delete branch deletion functionality
61-
- Enhanced CLAUDE.md with architectural details and development commands
62-
- Added detailed inline documentation for all public APIs
63-
- Updated all Rustdoc comments to reflect recent changes
64-
65-
## [0.1.0] - 2024-12-17
66-
67-
### Added
68-
69-
- Initial release of Git Workers
70-
- Interactive menu-driven interface for Git worktree management
71-
- List worktrees with detailed status information (branch, changes, ahead/behind)
72-
- Fuzzy search through worktrees with real-time filtering
73-
- Create new worktrees from branches or HEAD
74-
- Delete single or multiple worktrees with safety checks
75-
- Switch worktrees with automatic directory change via shell integration
76-
- Rename worktrees and optionally their branches
77-
- Cleanup old worktrees by age
78-
- Hook system for lifecycle events (post-create, pre-remove, post-switch)
79-
- Shell integration for Bash and Zsh
80-
- Configuration file support (.git-workers.toml)
81-
- Template variable support in hooks ({{worktree_name}}, {{worktree_path}})
82-
- Worktree pattern detection for organized directory structure
83-
- ESC key support for cancelling operations
84-
- Colored terminal output with theme support
85-
- Progress indicators for long operations
86-
- Homebrew installation support
7+
For detailed release notes, features, and bug fixes, please visit:
8+
https://github.com/wasabeef/git-workers/releases

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ cargo check --all-features
5454
cargo doc --no-deps --open
5555
```
5656

57-
5857
### Installation
5958

6059
```bash
@@ -71,10 +70,12 @@ source /path/to/git-workers/shell/gw.sh
7170
## Recent Changes
7271

7372
### Branch Option Simplification
73+
7474
- Reduced from 3 options to 2: "Create from current HEAD" and "Select branch (smart mode)"
7575
- Smart mode automatically handles branch conflicts and offers appropriate actions
7676

7777
### Key Methods Added/Modified
78+
7879
- **`get_branch_worktree_map()`**: Maps branch names to worktree names, including main worktree detection
7980
- **`list_all_branches()`**: Returns both local and remote branches (remote without "origin/" prefix)
8081
- **`create_worktree_with_new_branch()`**: Creates worktree with new branch from base branch (supports git-flow style workflows)

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "git-workers"
3-
version = "0.2.0"
3+
version = "0.2.1"
44
edition = "2021"
55
authors = ["Daichi Furiya"]
66
description = "Interactive Git worktree manager with shell integration"

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "git-workers",
3-
"version": "0.1.0",
3+
"version": "0.2.1",
44
"description": "Interactive Git worktree manager",
55
"repository": {
66
"type": "git",

src/config.rs

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ impl Config {
109109
/// }
110110
/// }
111111
/// ```
112+
#[allow(dead_code)]
112113
pub fn load() -> Result<Self> {
113114
if let Ok(repo) = git2::Repository::discover(".") {
114115
// Try to find .git-workers.toml in the default branch (main or master)
@@ -121,14 +122,105 @@ impl Config {
121122
Ok(Config::default())
122123
}
123124

125+
/// Loads configuration from a specific path context
126+
///
127+
/// This method loads configuration using the specified directory as the base
128+
/// path for the configuration lookup strategy. This is useful for hook execution
129+
/// where the configuration should be loaded relative to the worktree path
130+
/// rather than the current working directory.
131+
///
132+
/// # Process
133+
///
134+
/// 1. Discovers Git repository from the target path
135+
/// 2. Applies the configuration lookup strategy using the target path as context
136+
/// 3. Does not modify the current working directory (thread-safe)
137+
///
138+
/// # Arguments
139+
///
140+
/// * `path` - The directory path to use as context for configuration loading
141+
///
142+
/// # Returns
143+
///
144+
/// A `Result` containing the loaded configuration or a default configuration
145+
/// if no config file is found.
146+
///
147+
/// # Example
148+
///
149+
/// ```no_run
150+
/// use git_workers::config::Config;
151+
/// use std::path::Path;
152+
///
153+
/// let worktree_path = Path::new("/path/to/worktree");
154+
/// let config = Config::load_from_path(worktree_path)
155+
/// .expect("Failed to load config from worktree path");
156+
/// ```
157+
pub fn load_from_path(path: &std::path::Path) -> Result<Self> {
158+
if let Ok(repo) = git2::Repository::discover(path) {
159+
if let Some(config) = Self::load_from_path_context(path, &repo)? {
160+
return Ok(config);
161+
}
162+
}
163+
164+
// Return default config if no repo found
165+
Ok(Config::default())
166+
}
167+
168+
/// Loads configuration from a specific path context
169+
///
170+
/// This is the thread-safe implementation that loads configuration without
171+
/// changing the current working directory.
172+
///
173+
/// # Arguments
174+
///
175+
/// * `base_path` - The directory path to use as context
176+
/// * `repo` - The Git repository reference
177+
///
178+
/// # Returns
179+
///
180+
/// * `Ok(Some(config))` - Configuration was found and loaded
181+
/// * `Ok(None)` - No configuration file exists
182+
/// * `Err(...)` - An error occurred while loading
183+
fn load_from_path_context(
184+
base_path: &std::path::Path,
185+
repo: &git2::Repository,
186+
) -> Result<Option<Self>> {
187+
// Check parent directories first for main/master config (highest priority)
188+
if let Some(parent) = base_path.parent() {
189+
let main_path = parent.join("main").join(".git-workers.toml");
190+
let master_path = parent.join("master").join(".git-workers.toml");
191+
192+
if main_path.exists() {
193+
return Self::load_from_file(&main_path, repo);
194+
} else if master_path.exists() {
195+
return Self::load_from_file(&master_path, repo);
196+
}
197+
}
198+
199+
// Then check the base path directory
200+
let current_config = base_path.join(".git-workers.toml");
201+
if current_config.exists() {
202+
return Self::load_from_file(&current_config, repo);
203+
}
204+
205+
// If not found, check the repository working directory
206+
if let Some(workdir) = repo.workdir() {
207+
let config_path = workdir.join(".git-workers.toml");
208+
if config_path.exists() {
209+
return Self::load_from_file(&config_path, repo);
210+
}
211+
}
212+
213+
Ok(None)
214+
}
215+
124216
/// Loads configuration from the default branch (main or master)
125217
///
126218
/// This method implements the configuration lookup strategy:
127219
///
128-
/// 1. **Current directory**: First checks the current directory for `.git-workers.toml`
220+
/// 1. **Parent main/master**: First looks for config in the main/master worktree
221+
/// in the parent directory (for worktree structures)
222+
/// 2. **Current directory**: Then checks the current directory for `.git-workers.toml`
129223
/// (useful for bare repository worktrees)
130-
/// 2. **Parent main/master**: If in a worktree structure, looks for config in the
131-
/// main/master worktree in the parent directory
132224
/// 3. **Repository root**: Falls back to checking the current repository's
133225
/// working directory
134226
///
@@ -144,15 +236,12 @@ impl Config {
144236
/// * `Ok(Some(config))` - Configuration was found and loaded
145237
/// * `Ok(None)` - No configuration file exists
146238
/// * `Err(...)` - An error occurred while loading
239+
#[allow(dead_code)]
147240
fn load_from_default_branch(repo: &git2::Repository) -> Result<Option<Self>> {
148241
// First, check current directory (useful for bare repo worktrees)
149242
if let Ok(cwd) = std::env::current_dir() {
150-
let current_config = cwd.join(".git-workers.toml");
151-
if current_config.exists() {
152-
return Self::load_from_file(&current_config, repo);
153-
}
154-
155243
// Check if we're in a worktree structure like /path/to/repo/branch/worktree-name
244+
// Check parent directories first for main/master config
156245
if let Some(parent) = cwd.parent() {
157246
// Look for main or master directories in the parent
158247
let main_path = parent.join("main").join(".git-workers.toml");
@@ -164,6 +253,12 @@ impl Config {
164253
return Self::load_from_file(&master_path, repo);
165254
}
166255
}
256+
257+
// Then check current directory
258+
let current_config = cwd.join(".git-workers.toml");
259+
if current_config.exists() {
260+
return Self::load_from_file(&current_config, repo);
261+
}
167262
}
168263

169264
// If not found in parent, check the current repository

src/constants.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ pub const MSG_SWITCH_FILE_WARNING: &str = "Warning: Failed to write switch file:
1313
// UI Formatting
1414
pub const SEPARATOR_WIDTH: usize = 40;
1515
pub const HEADER_SEPARATOR_WIDTH: usize = 50;
16-
pub const OUTPUT_TRUNCATE_LIMIT: usize = 1000;
17-
pub const LARGE_OUTPUT_LIMIT: usize = 10000;
1816

1917
// Default Values
2018
pub const DEFAULT_BRANCH_UNKNOWN: &str = "unknown";

src/hooks.rs

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ use std::path::PathBuf;
3333
use std::process::Command;
3434

3535
use crate::config::Config;
36-
use crate::constants::{LARGE_OUTPUT_LIMIT, OUTPUT_TRUNCATE_LIMIT};
3736

3837
/// Context information passed to hook commands
3938
///
@@ -108,13 +107,22 @@ pub struct HookContext {
108107
/// execute_hooks("post-create", &context).ok();
109108
/// ```
110109
///
110+
/// # Configuration Loading
111+
///
112+
/// Configuration is loaded from the worktree's context rather than the
113+
/// current working directory. This ensures hooks use the correct configuration
114+
/// even when executed from a different directory.
115+
///
111116
/// # Error Handling
112117
///
113118
/// Hook failures are logged to stderr but do not stop execution of
114119
/// subsequent hooks or the main operation. This ensures that a failing
115120
/// hook doesn't prevent worktree operations from completing.
121+
///
122+
/// Command execution errors (spawn failures) are also handled gracefully,
123+
/// allowing other hooks to continue even if one command fails to start.
116124
pub fn execute_hooks(hook_type: &str, context: &HookContext) -> Result<()> {
117-
let config = Config::load()?;
125+
let config = Config::load_from_path(&context.worktree_path)?;
118126

119127
if let Some(commands) = config.hooks.get(hook_type) {
120128
println!("Running {} hooks...", hook_type);
@@ -132,28 +140,35 @@ pub fn execute_hooks(hook_type: &str, context: &HookContext) -> Result<()> {
132140

133141
// Execute the command in a shell for maximum compatibility
134142
// This allows complex commands with pipes, redirects, etc.
135-
let output = Command::new("sh")
143+
// Use spawn() and wait() to allow real-time output streaming
144+
match Command::new("sh")
136145
.arg("-c")
137146
.arg(&expanded_cmd)
138147
.current_dir(&context.worktree_path)
139-
.output()?;
140-
141-
if !output.status.success() {
142-
// Log hook failures but don't stop execution
143-
// This prevents a misconfigured hook from breaking worktree operations
144-
let stderr = String::from_utf8_lossy(&output.stderr);
145-
// Limit error output to prevent buffer overflow
146-
let error_msg = if stderr.len() > OUTPUT_TRUNCATE_LIMIT {
147-
format!("{}... (truncated)", &stderr[..OUTPUT_TRUNCATE_LIMIT])
148-
} else {
149-
stderr.to_string()
150-
};
151-
eprintln!("Hook command failed: {}", error_msg);
152-
}
153-
154-
// Also limit stdout output if it's too large
155-
if output.stdout.len() > LARGE_OUTPUT_LIMIT {
156-
println!(" (output truncated, {} bytes)", output.stdout.len());
148+
.stdout(std::process::Stdio::inherit())
149+
.stderr(std::process::Stdio::inherit())
150+
.spawn()
151+
{
152+
Ok(mut child) => {
153+
match child.wait() {
154+
Ok(status) => {
155+
if !status.success() {
156+
// Log hook failures but don't stop execution
157+
// This prevents a misconfigured hook from breaking worktree operations
158+
eprintln!(
159+
"Hook command failed with exit code: {:?}",
160+
status.code()
161+
);
162+
}
163+
}
164+
Err(e) => {
165+
eprintln!("Failed to wait for hook command: {}", e);
166+
}
167+
}
168+
}
169+
Err(e) => {
170+
eprintln!("Failed to execute hook command: {}", e);
171+
}
157172
}
158173
}
159174
}

0 commit comments

Comments
 (0)