Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
All notable changes to Braid are recorded here. The project follows Semantic
Versioning once release artifacts are published.

## [0.3.1] - unreleased

### Fixed

- PR worktree provisioning fetched through libgit2, which ignores the
operator's credential helpers and proxy configuration and failed on real
networks ("no TLS stream available"). The fetch now uses the configured
system `git` executable; libgit2 remains for local reference/worktree
operations.

## [0.3.0] - 2026-08-31

### Added
Expand Down
1 change: 1 addition & 0 deletions src/group/pr_agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,7 @@ pub(crate) fn provision_pr_agent_worktree(
target: &target,
repository: &config.github.repository,
remote: "origin",
git: &config.tools.git,
head_ref: &prepared.head_ref,
local_branch: &local_branch,
})?;
Expand Down
70 changes: 44 additions & 26 deletions src/worktree.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::path::{Path, PathBuf};

use git2::{ErrorClass, ErrorCode, Repository, WorktreeAddOptions};
use git2::{ErrorClass, ErrorCode, Repository};
use thiserror::Error;

#[derive(Debug, Error)]
Expand All @@ -27,6 +27,10 @@ pub struct WorktreeRequest<'a> {
pub target: &'a Path,
pub repository: &'a str,
pub remote: &'a str,
/// System `git` executable used for the network fetch: it honors the
/// operator's credential helpers and proxy configuration, which libgit2
/// does not.
pub git: &'a Path,
pub head_ref: &'a str,
pub local_branch: &'a str,
}
Expand Down Expand Up @@ -62,31 +66,45 @@ pub fn provision(request: &WorktreeRequest<'_>) -> Result<ProvisionedWorktree, W
.map_err(|source| WorktreeError::Io { path: parent.to_path_buf(), source })?;
}
tokio::task::block_in_place(|| {
let repo = Repository::open(&source)?;
let mut remote = repo.find_remote(request.remote)?;
remote.fetch(
&[&format!("refs/heads/{0}:refs/remotes/{1}/{0}", request.head_ref, request.remote)],
None,
None,
)?;
let reference = repo
.find_reference(&format!("refs/remotes/{}/{}", request.remote, request.head_ref))
.map_err(|error| {
WorktreeError::Git(format!(
"fetched ref refs/remotes/{}/{} not found: {error}",
request.remote, request.head_ref
))
})?;
let mut options = WorktreeAddOptions::new();
options.reference(Some(&reference));
let _worktree = repo
.worktree(request.local_branch, request.target, Some(&options))
.map_err(|error| {
WorktreeError::Git(format!(
"cannot add worktree at {}: {error}",
request.target.display()
))
})?;
let git = |args: &[&str]| -> Result<(), WorktreeError> {
let output = std::process::Command::new(request.git)
.arg("-C")
.arg(&source)
.args(args)
.output()
.map_err(|source_err| WorktreeError::Io {
path: source.clone(),
source: source_err,
})?;
if !output.status.success() {
return Err(WorktreeError::Git(format!(
"git {} failed: {}",
args.first().unwrap_or(&""),
String::from_utf8_lossy(&output.stderr).trim()
)));
}
Ok(())
};
let remote_ref = format!("refs/remotes/{}/{}", request.remote, request.head_ref);
git(&[
"fetch",
request.remote,
&format!("+refs/heads/{0}:{1}", request.head_ref, remote_ref),
])?;
// libgit2's worktree add rejects remote-tracking references
// ("reference is not a branch"); the system git creates the
// generation-scoped local branch and the worktree in one step.
git(&[
"worktree",
"add",
request
.target
.to_str()
.ok_or_else(|| WorktreeError::Git("worktree target path is not UTF-8".into()))?,
"-B",
request.local_branch,
&remote_ref,
])?;
Ok::<(), WorktreeError>(())
})?;
verify_existing(request, &source)
Expand Down
Loading