Skip to content
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ semantic versioning once releases begin.
in [#13](https://github.com/cosentinode/bitbygit/issues/13).
- Added strict typed TOML configuration loading with complete safe-default
fallback and redacted path-aware diagnostics.
- Applied configured pull request base defaults after explicit prompt targets
and before provider defaults, with provider validation and visible safe config
diagnostics in the TUI.
21 changes: 21 additions & 0 deletions crates/bitbygit-core/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub struct EffectivePolicy {
additional_protected_branches: Vec<String>,
confirmation: ConfirmationConfig,
disabled_operations: Vec<OperationFamily>,
default_pull_request_base: Option<String>,
prompt_enabled: bool,
}

Expand All @@ -25,6 +26,7 @@ impl EffectivePolicy {
additional_protected_branches: config.policy.additional_protected_branches.clone(),
confirmation: config.policy.confirmation.clone(),
disabled_operations: config.policy.disabled_operations.clone(),
default_pull_request_base: config.pull_requests.default_base_branch.clone(),
prompt_enabled: config.prompt.enabled,
}
}
Expand Down Expand Up @@ -111,6 +113,10 @@ impl EffectivePolicy {
pub const fn prompt_enabled(&self) -> bool {
self.prompt_enabled
}

pub fn default_pull_request_base(&self) -> Option<&str> {
self.default_pull_request_base.as_deref()
}
}

impl Default for EffectivePolicy {
Expand Down Expand Up @@ -390,6 +396,21 @@ mod tests {
assert!(!EffectivePolicy::new(&config).prompt_enabled());
}

#[test]
fn pull_request_default_is_available_to_runtime_planning() {
let mut config = AppConfig::default();
assert_eq!(
EffectivePolicy::new(&config).default_pull_request_base(),
None
);

config.pull_requests.default_base_branch = Some("develop".to_owned());
assert_eq!(
EffectivePolicy::new(&config).default_pull_request_base(),
Some("develop")
);
}

#[test]
fn safe_fallback_blocks_every_operation_and_prompt() {
let policy = EffectivePolicy::safe_fallback();
Expand Down
88 changes: 88 additions & 0 deletions crates/bitbygit-git/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,45 @@ impl Git {
Ok(Some((remote.to_owned(), push_branch.to_owned())))
}

pub fn typed_push_target(
&self,
branch: &str,
upstream: Option<(&str, &str)>,
default_remote: Option<&str>,
) -> Result<Option<(String, String)>, GitError> {
let remote = self
.config_value(["config", "--get", &format!("branch.{branch}.pushRemote")])?
.or(self.config_value(["config", "--get", "remote.pushDefault"])?)
.or_else(|| upstream.map(|(remote, _)| remote.to_owned()))
.or_else(|| default_remote.map(ToOwned::to_owned));
let Some(remote) = remote else {
return Ok(None);
};
if remote.is_empty() {
return Ok(None);
}
let Some((upstream_remote, upstream_branch)) = upstream else {
return Ok(Some((remote, branch.to_owned())));
};
let push_default = self
.config_value(["config", "--get", "push.default"])?
.unwrap_or_else(|| "simple".to_owned());
let target = match push_default.as_str() {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve support for push.default=tracking. tracking is a valid deprecated Git synonym for upstream, but it falls into the unsupported-value arm here. Because this resolver now backs ordinary push planning as well as PR planning/revalidation, repositories using that valid setting can no longer push or open a PR; on develop, push planning ignored this setting and PR planning used push_target, which resolves it successfully. Normalize tracking to upstream and include it in the resolver parity test.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c92e89d. typed_push_target now handles push.default=tracking in the same match arm as upstream. The existing resolver parity test now includes tracking and compares the typed result directly with Git-derived push_target; the targeted Git test passes, as do format, clippy, and full workspace tests.

"simple" if remote != upstream_remote => (remote, branch.to_owned()),
"simple" | "upstream" | "tracking" => {
(upstream_remote.to_owned(), upstream_branch.to_owned())
}
"current" | "matching" => (remote, branch.to_owned()),
"nothing" => return Ok(None),
_ => {
return Err(GitError::Blocked {
message: format!("push.default has unsupported value {push_default}"),
});
}
};
Ok(Some(target))
}

pub fn remote_tracking_oid(
&self,
remote: &str,
Expand Down Expand Up @@ -3219,6 +3258,55 @@ mod tests {
Ok(())
}

#[test]
fn typed_push_target_resolves_tracked_and_new_branches() -> Result<(), Box<dyn Error>> {
let repo = TempRepo::new()?;
repo.run(["init", "-b", "topic"])?;
repo.run(["config", "user.email", "bitbygit@example.invalid"])?;
repo.run(["config", "user.name", "bitbygit test"])?;
repo.run(["commit", "--allow-empty", "-m", "initial"])?;
repo.run(["remote", "add", "fork", "https://github.com/octo/repo.git"])?;
repo.run([
"remote",
"add",
"origin",
"https://github.com/upstream/repo.git",
])?;
repo.run(["config", "branch.topic.remote", "fork"])?;
repo.run(["config", "branch.topic.merge", "refs/heads/topic"])?;
let git = Git::new(repo.path());

assert_eq!(
git.typed_push_target("topic", Some(("fork", "topic")), Some("origin"))?,
git.push_target("topic")?
);

repo.run(["config", "remote.pushDefault", "origin"])?;
for push_default in [
"simple", "current", "upstream", "tracking", "matching", "nothing",
] {
repo.run(["config", "push.default", push_default])?;
assert_eq!(
git.typed_push_target("topic", Some(("fork", "topic")), Some("fork"))?,
git.push_target("topic")?,
"push.default={push_default}"
);
}

repo.run(["config", "branch.topic.pushRemote", "fork"])?;
repo.run(["config", "push.default", "current"])?;
assert_eq!(
git.typed_push_target("topic", Some(("fork", "topic")), Some("origin"))?,
git.push_target("topic")?
);
repo.run(["config", "branch.new.pushRemote", "fork"])?;
assert_eq!(
git.typed_push_target("new", None, Some("origin"))?,
Some(("fork".to_owned(), "new".to_owned()))
);
Ok(())
}

#[cfg(unix)]
#[test]
fn remote_url_head_oid_ignores_configured_ssh_command() -> Result<(), Box<dyn Error>> {
Expand Down
Loading
Loading