Source: stgit Version: 2.6.1-1.1 Severity: important Tags: patch User: [email protected] Usertags: gix-0.87 X-Debbugs-Cc: [email protected]
Dear maintainer, I am preparing a rust-gix transition to 0.87, which will likely be uploaded soon. stgit currently depends on gix 0.86. My patch was rejected upstream in favor of a 0.87 port, that needs to be cherry-picked with a small test fix. I have attached a debdiff fixing this. Best regards, Simon Quigley [email protected]
diff -Nru stgit-2.6.1/debian/changelog stgit-2.6.1/debian/changelog --- stgit-2.6.1/debian/changelog 2026-08-24 09:43:17.000000000 -0500 +++ stgit-2.6.1/debian/changelog 2026-09-18 10:19:50.000000000 -0500 @@ -1,3 +1,12 @@ +stgit (2.6.1-1.2) unstable; urgency=medium + + * Non-maintainer upload. + * Drop gix-0.86.patch in favor of a direct upstream cherry-pick in + gix-0.87.patch. + * Cherry-pick upstream fix to reject a lone "@" as a partial ref name. + + -- Simon Quigley <[email protected]> Fri, 18 Sep 2026 10:19:50 -0500 + stgit (2.6.1-1.1) unstable; urgency=medium * Non-maintainer upload (Closes: #1144970). diff -Nru stgit-2.6.1/debian/control stgit-2.6.1/debian/control --- stgit-2.6.1/debian/control 2026-08-24 09:43:17.000000000 -0500 +++ stgit-2.6.1/debian/control 2026-09-18 10:01:34.000000000 -0500 @@ -24,7 +24,7 @@ librust-curl-dev, librust-encoding-rs-dev, librust-flate2-dev, - librust-gix-dev (>= 0.86), + librust-gix-dev (>= 0.87), librust-indexmap-dev, librust-is-terminal-dev, librust-jiff-dev (>= 0.2.1), diff -Nru stgit-2.6.1/debian/patches/gix-0.86.patch stgit-2.6.1/debian/patches/gix-0.86.patch --- stgit-2.6.1/debian/patches/gix-0.86.patch 2026-08-24 09:43:17.000000000 -0500 +++ stgit-2.6.1/debian/patches/gix-0.86.patch 1969-12-31 18:00:00.000000000 -0600 @@ -1,430 +0,0 @@ -Description: Port to gix 0.86 - Bump the gix crate from 0.84 to 0.86 and update call sites for the - gix-config 0.59 API that gix 0.86 re-exports. A version-only bump does - not compile. - . - gix::config::File no longer takes a lifetime, so File<'static> and - CommitOptions<'_> lose that parameter. Snapshot::string() now returns - Option<BString> rather than Option<Cow<BStr>>, so CommitOptions stores - BString and for_label() takes s.as_ref(). - . - Snapshot::string_by and boolean_by are gone. Look up dotted keys with - string() / boolean() / try_boolean(), which stgit already uses for - keys such as branch.{name}.remote. try_boolean() is Result<Option<bool>> - rather than Option<Result<bool>>, so drop the extra transpose() on the - autostash lookups. - . - SectionRef::num_values() moved to BodyRef (section.body().num_values()). - value_names() yields String rather than BStr, so skip to_str() and pass - the name through to value(). File::rename_section's new subsection is - IntoBStringOpt (Option<BString>), not Option<Cow<BStr>>. - . - Snapshot::trusted_path returns Result<Option<PathBuf>, _> instead of an - Option that needs transpose(). Hook path construction can use PathBuf - directly; editor lookup maps PathBuf with into_os_string(). -Author: Simon Quigley <[email protected]> -Origin: vendor -Bug-Debian: https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1144970 -Forwarded: https://github.com/stacked-git/stgit/pull/648 -Last-Update: 2026-08-20 ---- -This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ ---- a/Cargo.toml -+++ b/Cargo.toml -@@ -43,7 +43,7 @@ clap = { version = "~4.6", default-featu - ctrlc = "3.4" - encoding_rs = "0.8" - flate2 = "1" --gix = { version = "0.84", default-features = false, features = [ -+gix = { version = "0.86", default-features = false, features = [ - "command", - "revision", - "sha1", ---- a/src/alias.rs -+++ b/src/alias.rs -@@ -100,15 +100,9 @@ where - .filter(|section| section.header().subsection_name() == Some("alias".into())) - { - for value_name in section.value_names() { -- let name = value_name.to_str().map_err(|_| { -- anyhow!( -- "alias name `{}` in {} is not valid UTF-8", -- value_name.to_str_lossy(), -- config_source_str(section.meta().source), -- ) -- })?; -+ let name = value_name.as_str(); - if let Some(value) = section -- .value(value_name) -+ .value(name) - .and_then(|v| (!v.is_empty()).then_some(v)) - { - if !exclude(name) { ---- a/src/cmd/branch/list.rs -+++ b/src/cmd/branch/list.rs -@@ -101,7 +101,7 @@ pub(super) fn dispatch(repo: &gix::Repos - stdout.set_color(&color_spec)?; - - let description = config -- .string_by("branch", Some(branchname.into()), "description") -+ .string(format!("branch.{branchname}.description").as_str()) - .unwrap_or_default(); - if description.is_empty() { - writeln!(stdout)?; ---- a/src/cmd/branch/mod.rs -+++ b/src/cmd/branch/mod.rs -@@ -151,7 +151,7 @@ fn set_description( - value.delete(); - } - if let Ok(section) = local_config_file.section("branch", Some(branchname.into())) { -- if section.num_values() == 0 { -+ if section.body().num_values() == 0 { - local_config_file.remove_section_by_id(section.id()); - } - } -@@ -171,11 +171,7 @@ fn set_description( - - fn get_stgit_parent(config: &gix::config::Snapshot, branchname: &PartialRefName) -> Option<String> { - config -- .string_by( -- "branch", -- Some(format!("{branchname}.stgit").as_str().into()), -- "parentbranch", -- ) -+ .string(format!("branch.{branchname}.stgit.parentbranch").as_str()) - .and_then(|bs| bs.to_str().ok().map(str::to_string)) - } - -@@ -202,7 +198,7 @@ fn set_stgit_parent( - value.delete(); - } - if let Ok(section) = local_config_file.section("branch", Some(subsection.as_str().into())) { -- if section.num_values() == 0 { -+ if section.body().num_values() == 0 { - local_config_file.remove_section_by_id(section.id()); - } - } ---- a/src/cmd/branch/rename.rs -+++ b/src/cmd/branch/rename.rs -@@ -84,8 +84,7 @@ pub(super) fn dispatch(repo: &gix::Repos - .section("branch", Some(old_section_name)) - .is_ok() - { -- let new_section_name = -- std::borrow::Cow::Owned(BString::from(format!("{new_branchname}.stgit"))); -+ let new_section_name = BString::from(format!("{new_branchname}.stgit")); - local_config_file - .rename_section( - "branch", ---- a/src/cmd/pick.rs -+++ b/src/cmd/pick.rs -@@ -297,7 +297,7 @@ fn pick_picks( - {body}" - ) - } else if matches.get_flag("expose") { -- let expose_format = config.string_by("stgit", Some("pick".into()), "expose-format"); -+ let expose_format = config.string("stgit.pick.expose-format"); - let expose_format = expose_format - .as_ref() - .map(|bs| bs.to_str().ok()) ---- a/src/cmd/pull.rs -+++ b/src/cmd/pull.rs -@@ -101,11 +101,7 @@ fn run(matches: &ArgMatches) -> Result<( - let config = repo.config_snapshot(); - let policy = PullPolicy::from_str( - &config -- .string_by( -- "branch", -- Some(format!("{branch_name}.stgit").as_str().into()), -- "pull-policy", -- ) -+ .string(format!("branch.{branch_name}.stgit.pull-policy").as_str()) - .or_else(|| config.string("stgit.pull-policy")) - .map(|bs| bs.to_str_lossy().to_string()) - .unwrap_or_else(|| "pull".to_string()), -@@ -125,7 +121,7 @@ fn run(matches: &ArgMatches) -> Result<( - } - PullPolicy::Pull | PullPolicy::FetchRebase => { - parent_remote = config -- .string_by("branch", Some(branch_name.as_str().into()), "remote") -+ .string(format!("branch.{branch_name}.remote").as_str()) - .and_then(|bs| bs.to_str().map(str::to_string).ok()); - let remote_name = matches - .get_one::<String>("repository") -@@ -163,11 +159,7 @@ fn run(matches: &ArgMatches) -> Result<( - let rebase_target = match policy { - PullPolicy::Pull => { - let pull_cmd = config -- .string_by( -- "branch", -- Some(format!("{branch_name}.stgit").as_str().into()), -- "pullcmd", -- ) -+ .string(format!("branch.{branch_name}.stgit.pullcmd").as_str()) - .or_else(|| config.string("stgit.pullcmd")) - .and_then(|bs| bs.to_str().map(str::to_string).ok()) - .unwrap_or_else(|| "git pull".to_string()); -@@ -183,11 +175,7 @@ fn run(matches: &ArgMatches) -> Result<( - } - PullPolicy::FetchRebase => { - let fetch_cmd = config -- .string_by( -- "branch", -- Some(format!("{branch_name}.stgit").as_str().into()), -- "fetchcmd", -- ) -+ .string(format!("branch.{branch_name}.stgit.fetchcmd").as_str()) - .or_else(|| config.string("stgit.fetchcmd")) - .and_then(|bs| bs.to_str().map(str::to_string).ok()) - .unwrap_or_else(|| "git fetch".to_string()); -@@ -203,11 +191,8 @@ fn run(matches: &ArgMatches) -> Result<( - Some(target_id) - } - PullPolicy::Rebase => { -- let parent_branch_name = config.string_by( -- "branch", -- Some(format!("{branch_name}.stgit").as_str().into()), -- "parentbranch", -- ); -+ let parent_branch_name = -+ config.string(format!("branch.{branch_name}.stgit.parentbranch").as_str()); - let parent_branch_name = parent_branch_name.as_ref().and_then(|bs| bs.to_str().ok()); - - let parent_object = if let Some(name) = parent_branch_name { -@@ -227,11 +212,7 @@ fn run(matches: &ArgMatches) -> Result<( - - if let Some(rebase_target) = rebase_target { - let rebase_cmd = config -- .string_by( -- "branch", -- Some(format!("{branch_name}.stgit").as_str().into()), -- "rebasecmd", -- ) -+ .string(format!("branch.{branch_name}.stgit.rebasecmd").as_str()) - .or_else(|| config.string("stgit.rebasecmd")) - .and_then(|bs| bs.to_str().map(str::to_string).ok()) - .unwrap_or_else(|| "git reset --hard".to_string()); ---- a/src/cmd/rebase.rs -+++ b/src/cmd/rebase.rs -@@ -175,12 +175,7 @@ fn run(matches: &ArgMatches) -> Result<( - true - } else { - config -- .boolean_by( -- "branch", -- Some(format!("{branch_name}.stgit").as_str().into()), -- "autostash", -- ) -- .transpose() -+ .try_boolean(format!("branch.{branch_name}.stgit.autostash").as_str()) - .unwrap_or_else(|e| { - crate::print_warning_message( - matches, -@@ -189,16 +184,13 @@ fn run(matches: &ArgMatches) -> Result<( - Some(false) - }) - .or_else(|| { -- config -- .try_boolean("stgit.autostash") -- .transpose() -- .unwrap_or_else(|e| { -- crate::print_warning_message( -- matches, -- &format!("Invalid config value `stgit.autostash`: {e}"), -- ); -- Some(false) -- }) -+ config.try_boolean("stgit.autostash").unwrap_or_else(|e| { -+ crate::print_warning_message( -+ matches, -+ &format!("Invalid config value `stgit.autostash`: {e}"), -+ ); -+ Some(false) -+ }) - }) - .unwrap_or(false) - }; -@@ -225,11 +217,7 @@ fn run(matches: &ArgMatches) -> Result<( - .execute("rebase (pop)")?; - - let rebase_cmd = config -- .string_by( -- "branch", -- Some(format!("{branch_name}.stgit").as_str().into()), -- "rebasecmd", -- ) -+ .string(format!("branch.{branch_name}.stgit.rebasecmd").as_str()) - .or_else(|| config.string("stgit.rebasecmd")) - .and_then(|bs| bs.to_str().map(str::to_string).ok()) - .unwrap_or_else(|| "git reset --hard".to_string()); ---- a/src/ext/repository.rs -+++ b/src/ext/repository.rs -@@ -1,9 +1,7 @@ - // SPDX-License-Identifier: GPL-2.0-only - --use std::borrow::Cow; -- - use anyhow::{anyhow, Result}; --use bstr::{BStr, ByteSlice}; -+use bstr::{BString, ByteSlice}; - - use crate::{ - stupid::Stupid, -@@ -42,7 +40,7 @@ pub(crate) trait RepositoryExtended { - - /// Get repository-local config file which can be used to change local - /// configuration. -- fn local_config_file(&self) -> Result<gix::config::File<'static>>; -+ fn local_config_file(&self) -> Result<gix::config::File>; - - /// Write repository-local config file. - fn write_local_config(&self, file: gix::config::File) -> Result<()>; -@@ -73,7 +71,7 @@ pub(crate) trait RepositoryExtended { - message: &Message, - tree_id: gix::ObjectId, - parent_ids: impl IntoIterator<Item = gix::ObjectId>, -- options: &CommitOptions<'_>, -+ options: &CommitOptions, - ) -> Result<gix::ObjectId>; - - /// [`gix::Repository::rev_parse_single()`] with StGit-specific error mapping. -@@ -88,9 +86,9 @@ pub(crate) trait RepositoryExtended { - } - - /// Options for creating a git commit object. --pub(crate) struct CommitOptions<'a> { -+pub(crate) struct CommitOptions { - /// The target encoding for the commit message. -- pub(crate) commit_encoding: Option<Cow<'a, BStr>>, -+ pub(crate) commit_encoding: Option<BString>, - - /// Determine whether the commit object should be signed with GPG. - pub(crate) gpgsign: bool, -@@ -171,7 +169,7 @@ impl RepositoryExtended for gix::Reposit - } - } - -- fn local_config_file(&self) -> Result<gix::config::File<'static>> { -+ fn local_config_file(&self) -> Result<gix::config::File> { - let source = gix::config::Source::Local; - - let local_config_path = self.common_dir().join( -@@ -233,11 +231,11 @@ impl RepositoryExtended for gix::Reposit - message: &Message, - tree_id: gix::ObjectId, - parent_ids: impl IntoIterator<Item = gix::ObjectId>, -- options: &CommitOptions<'_>, -+ options: &CommitOptions, - ) -> Result<gix::ObjectId> { - let commit_encoding = match &options.commit_encoding { - Some(s) => { -- let encoding = encoding_rs::Encoding::for_label(s) -+ let encoding = encoding_rs::Encoding::for_label(s.as_ref()) - .ok_or_else(|| anyhow!("unhandled i18n.commitEncoding `{s}`"))?; - Some(encoding) - } ---- a/src/hook.rs -+++ b/src/hook.rs -@@ -3,7 +3,6 @@ - //! Support for using git repository hooks. - - use std::{ -- borrow::Cow, - io::Write, - path::{Path, PathBuf}, - }; -@@ -18,22 +17,21 @@ use crate::wrap::Message; - /// Returns None if the hook script is not found or is not executable. - fn get_hook_path(repo: &gix::Repository, hook_name: &str) -> Result<Option<PathBuf>> { - let config = repo.config_snapshot(); -- let hooks_path = -- if let Some(core_hooks_path) = config.trusted_path("core.hookspath").transpose()? { -- if core_hooks_path.is_absolute() { -- core_hooks_path -- } else if repo.is_bare() { -- // The hooks path is relative to GIT_DIR in the case of a bare repo -- Cow::Owned(repo.common_dir().join(core_hooks_path)) -- } else { -- // The hooks path is relative to the root of the working tree otherwise -- let work_dir = repo.workdir().expect("non-bare repo must have work dir"); -- Cow::Owned(work_dir.join(core_hooks_path)) -- } -+ let hooks_path = if let Some(core_hooks_path) = config.trusted_path("core.hookspath")? { -+ if core_hooks_path.is_absolute() { -+ core_hooks_path -+ } else if repo.is_bare() { -+ // The hooks path is relative to GIT_DIR in the case of a bare repo -+ repo.common_dir().join(core_hooks_path) - } else { -- // No core.hookspath, use default .git/hooks location -- Cow::Owned(repo.common_dir().join("hooks")) -- }; -+ // The hooks path is relative to the root of the working tree otherwise -+ let work_dir = repo.workdir().expect("non-bare repo must have work dir"); -+ work_dir.join(core_hooks_path) -+ } -+ } else { -+ // No core.hookspath, use default .git/hooks location -+ repo.common_dir().join("hooks") -+ }; - let hook_path = hooks_path.join(hook_name); - - let hook_meta = match std::fs::metadata(&hook_path) { ---- a/src/patch/edit/interactive.rs -+++ b/src/patch/edit/interactive.rs -@@ -135,15 +135,13 @@ fn get_editor(config: &gix::config::Snap - let editor = if let Some(editor) = std::env::var_os("GIT_EDITOR") { - editor - } else if let Some(editor) = config -- .trusted_path("stgit.editor") -- .transpose()? -- .map(|p| p.as_os_str().to_os_string()) -+ .trusted_path("stgit.editor")? -+ .map(|p| p.into_os_string()) - { - editor - } else if let Some(editor) = config -- .trusted_path("core.editor") -- .transpose()? -- .map(|p| p.as_os_str().to_os_string()) -+ .trusted_path("core.editor")? -+ .map(|p| p.into_os_string()) - { - editor - } else if let Some(editor) = std::env::var_os("VISUAL") { ---- a/src/stack/stack.rs -+++ b/src/stack/stack.rs -@@ -238,12 +238,7 @@ impl<'repo> Stack<'repo> { - /// Check whether the stack is marked as protected in the config. - pub(crate) fn is_protected(&self, config: &gix::config::Snapshot) -> bool { - config -- .boolean_by( -- "branch", -- Some(format!("{}.stgit", self.branch_name).as_str().into()), -- "protect", -- ) -- .unwrap_or(Ok(false)) -+ .boolean(format!("branch.{}.stgit.protect", self.branch_name).as_str()) - .unwrap_or(false) - } - -@@ -271,7 +266,7 @@ impl<'repo> Stack<'repo> { - if let Ok(section) = local_config_file - .section_by_key(format!("{section}.{subsection}").as_bytes().as_bstr()) - { -- if section.num_values() == 0 { -+ if section.body().num_values() == 0 { - local_config_file.remove_section_by_id(section.id()); - } - } ---- a/src/stack/upgrade.rs -+++ b/src/stack/upgrade.rs -@@ -325,7 +325,7 @@ fn rm_stackformatversion(repo: &gix::Rep - if let Ok(section) = - local_config_file.section_by_key(format!("{section}.{subsection}").as_bytes().as_bstr()) - { -- if section.num_values() == 0 { -+ if section.body().num_values() == 0 { - local_config_file.remove_section_by_id(section.id()); - } - } diff -Nru stgit-2.6.1/debian/patches/gix-0.87.patch stgit-2.6.1/debian/patches/gix-0.87.patch --- stgit-2.6.1/debian/patches/gix-0.87.patch 1969-12-31 18:00:00.000000000 -0600 +++ stgit-2.6.1/debian/patches/gix-0.87.patch 2026-09-18 10:01:21.000000000 -0500 @@ -0,0 +1,474 @@ +Description: feat: update gix to 0.87.1 introducing git approxidates + Skip gix 0.86.0 and 0.87.0 because both use a version of gix-protocol + that depends on bisync, and every published bisync release has been + yanked. So neither gix 0.86.0 or 0.87.0 resolve at all. gix 0.87.1 drops + bisync. + . + gix-date 0.16 reads relative dates the way git's approxidate does, so + StGit now accepts spellings it used to reject: "2 days 3 hours ago", + "one week ago", "last week", "3.days.ago", and a "@" before an epoch. + . + Relative dates are also anchored in the local zone rather than UTC. That + same leniency made the offset-appending retry in parse_time unsound: "a + long time ago" carries no offset, so the retry turned it into "a long + time ago-04:00", which parses as four seconds ago. Git accepts that + mangled string too, so the fault is appending an offset blind; only + inputs that look like a time of day are retried now. + . + The remaining changes are mechanical responses to gix interface changes. +Author: Peter Grayson <[email protected]> +Origin: upstream +Bug: https://github.com/stacked-git/stgit/pull/648 +Applied-Upstream: 68ad69139ad69cb90bd1ea2f5c379dc342fa57a0 +Reviewed-by: Simon Quigley <[email protected]> +Last-Update: 2026-09-18 +--- +This patch header follows DEP-3: http://dep.debian.net/deps/dep3/ +--- a/Cargo.toml ++++ b/Cargo.toml +@@ -43,7 +43,7 @@ clap = { version = "~4.6", default-featu + ctrlc = "3.4" + encoding_rs = "0.8" + flate2 = "1" +-gix = { version = "0.84", default-features = false, features = [ ++gix = { version = "0.87", default-features = false, features = [ + "command", + "revision", + "sha1", +--- a/src/alias.rs ++++ b/src/alias.rs +@@ -100,15 +100,9 @@ where + .filter(|section| section.header().subsection_name() == Some("alias".into())) + { + for value_name in section.value_names() { +- let name = value_name.to_str().map_err(|_| { +- anyhow!( +- "alias name `{}` in {} is not valid UTF-8", +- value_name.to_str_lossy(), +- config_source_str(section.meta().source), +- ) +- })?; ++ let name = value_name.as_str(); + if let Some(value) = section +- .value(value_name) ++ .value(name) + .and_then(|v| (!v.is_empty()).then_some(v)) + { + if !exclude(name) { +--- a/src/cmd/branch/mod.rs ++++ b/src/cmd/branch/mod.rs +@@ -151,7 +151,7 @@ fn set_description( + value.delete(); + } + if let Ok(section) = local_config_file.section("branch", Some(branchname.into())) { +- if section.num_values() == 0 { ++ if section.body().num_values() == 0 { + local_config_file.remove_section_by_id(section.id()); + } + } +@@ -202,7 +202,7 @@ fn set_stgit_parent( + value.delete(); + } + if let Ok(section) = local_config_file.section("branch", Some(subsection.as_str().into())) { +- if section.num_values() == 0 { ++ if section.body().num_values() == 0 { + local_config_file.remove_section_by_id(section.id()); + } + } +--- a/src/cmd/branch/rename.rs ++++ b/src/cmd/branch/rename.rs +@@ -84,8 +84,7 @@ pub(super) fn dispatch(repo: &gix::Repos + .section("branch", Some(old_section_name)) + .is_ok() + { +- let new_section_name = +- std::borrow::Cow::Owned(BString::from(format!("{new_branchname}.stgit"))); ++ let new_section_name = BString::from(format!("{new_branchname}.stgit")); + local_config_file + .rename_section( + "branch", +--- a/src/cmd/rebase.rs ++++ b/src/cmd/rebase.rs +@@ -180,7 +180,6 @@ fn run(matches: &ArgMatches) -> Result<( + Some(format!("{branch_name}.stgit").as_str().into()), + "autostash", + ) +- .transpose() + .unwrap_or_else(|e| { + crate::print_warning_message( + matches, +@@ -189,16 +188,13 @@ fn run(matches: &ArgMatches) -> Result<( + Some(false) + }) + .or_else(|| { +- config +- .try_boolean("stgit.autostash") +- .transpose() +- .unwrap_or_else(|e| { +- crate::print_warning_message( +- matches, +- &format!("Invalid config value `stgit.autostash`: {e}"), +- ); +- Some(false) +- }) ++ config.try_boolean("stgit.autostash").unwrap_or_else(|e| { ++ crate::print_warning_message( ++ matches, ++ &format!("Invalid config value `stgit.autostash`: {e}"), ++ ); ++ Some(false) ++ }) + }) + .unwrap_or(false) + }; +--- a/src/ext/repository.rs ++++ b/src/ext/repository.rs +@@ -1,9 +1,7 @@ + // SPDX-License-Identifier: GPL-2.0-only + +-use std::borrow::Cow; +- + use anyhow::{anyhow, Result}; +-use bstr::{BStr, ByteSlice}; ++use bstr::{BString, ByteSlice}; + + use crate::{ + stupid::Stupid, +@@ -42,7 +40,7 @@ pub(crate) trait RepositoryExtended { + + /// Get repository-local config file which can be used to change local + /// configuration. +- fn local_config_file(&self) -> Result<gix::config::File<'static>>; ++ fn local_config_file(&self) -> Result<gix::config::File>; + + /// Write repository-local config file. + fn write_local_config(&self, file: gix::config::File) -> Result<()>; +@@ -73,7 +71,7 @@ pub(crate) trait RepositoryExtended { + message: &Message, + tree_id: gix::ObjectId, + parent_ids: impl IntoIterator<Item = gix::ObjectId>, +- options: &CommitOptions<'_>, ++ options: &CommitOptions, + ) -> Result<gix::ObjectId>; + + /// [`gix::Repository::rev_parse_single()`] with StGit-specific error mapping. +@@ -88,9 +86,9 @@ pub(crate) trait RepositoryExtended { + } + + /// Options for creating a git commit object. +-pub(crate) struct CommitOptions<'a> { ++pub(crate) struct CommitOptions { + /// The target encoding for the commit message. +- pub(crate) commit_encoding: Option<Cow<'a, BStr>>, ++ pub(crate) commit_encoding: Option<BString>, + + /// Determine whether the commit object should be signed with GPG. + pub(crate) gpgsign: bool, +@@ -171,7 +169,7 @@ impl RepositoryExtended for gix::Reposit + } + } + +- fn local_config_file(&self) -> Result<gix::config::File<'static>> { ++ fn local_config_file(&self) -> Result<gix::config::File> { + let source = gix::config::Source::Local; + + let local_config_path = self.common_dir().join( +@@ -233,11 +231,11 @@ impl RepositoryExtended for gix::Reposit + message: &Message, + tree_id: gix::ObjectId, + parent_ids: impl IntoIterator<Item = gix::ObjectId>, +- options: &CommitOptions<'_>, ++ options: &CommitOptions, + ) -> Result<gix::ObjectId> { + let commit_encoding = match &options.commit_encoding { + Some(s) => { +- let encoding = encoding_rs::Encoding::for_label(s) ++ let encoding = encoding_rs::Encoding::for_label(s.as_ref()) + .ok_or_else(|| anyhow!("unhandled i18n.commitEncoding `{s}`"))?; + Some(encoding) + } +@@ -296,7 +294,7 @@ impl RepositoryExtended for gix::Reposit + // is malformed" by looking for a typed NotFound frame anywhere in the + // error tree underlying SingleError::Parse. + if let SingleError::Parse(ref gix_err) = single_err { +- let not_found = gix_err.sources().any(|src| { ++ let not_found = gix_err.iter_errors().any(|src| { + matches!( + src.downcast_ref::<RefFindError>(), + Some(RefFindError::NotFound { .. }) +--- a/src/ext/time.rs ++++ b/src/ext/time.rs +@@ -19,29 +19,37 @@ pub(crate) trait TimeExtended { + /// | `gitoxide default`| `Thu Jan 6 2022 09:32:07 -0500` | + fn parse_time(time_str: &str) -> Result<gix::date::Time> { + let time_str = time_str.trim(); +- let now = std::time::SystemTime::now(); +- let zoned_now = jiff::Zoned::try_from(now).unwrap_or_else(|_| jiff::Zoned::now()); ++ let zoned_now = jiff::Zoned::try_from(std::time::SystemTime::now()) ++ .unwrap_or_else(|_| jiff::Zoned::now()); + + if time_str == "now" { +- Ok(gix::date::Time::new( ++ return Ok(gix::date::Time::new( + zoned_now.timestamp().as_second(), + zoned_now.offset().seconds(), +- )) +- } else if let Ok(time) = gix::date::parse(time_str, Some(now)) { +- Ok(time) +- } else if let Ok(time) = gix::date::parse( +- &format!("{time_str} {}", zoned_now.strftime("%z")), +- Some(now), +- ) { +- Ok(time) +- } else if let Ok(time) = gix::date::parse( +- &format!("{time_str}{}", zoned_now.strftime("%:z")), +- Some(now), +- ) { +- Ok(time) +- } else { +- Err(anyhow!("invalid date `{time_str}`")) ++ )); + } ++ ++ if let Ok(time) = gix::date::parse(time_str, Some(zoned_now.clone())) { ++ return Ok(time); ++ } ++ ++ // A date-time that is only missing its UTC offset is retried with the local ++ // offset appended. Only do that for something that looks like a time of day: ++ // git's approxidate reads the appended offset as part of a relative date, so ++ // "a long time ago" would otherwise become "a long time ago-04:00", which ++ // parses as four seconds ago. ++ if time_str.contains(':') && time_str.ends_with(|c: char| c.is_ascii_digit()) { ++ for with_offset in [ ++ format!("{time_str} {}", zoned_now.strftime("%z")), ++ format!("{time_str}{}", zoned_now.strftime("%:z")), ++ ] { ++ if let Ok(time) = gix::date::parse(&with_offset, Some(zoned_now.clone())) { ++ return Ok(time); ++ } ++ } ++ } ++ ++ Err(anyhow!("invalid date `{time_str}`")) + } + } + +@@ -95,6 +103,69 @@ mod tests { + Time::parse_time("now").unwrap(); + } + ++ /// Formats that carry no UTC offset are retried with the local offset appended. ++ /// Comparing wall clock time keeps this independent of the test machine's zone. ++ #[test] ++ fn parse_time_without_offset() { ++ // 2022-01-06T09:32:07 read as if it were UTC. ++ let wall_clock = 1641461527; ++ for s in [ ++ "Thu Jan 6 09:32:07 2022", ++ "Thu, 6 Jan 2022 09:32:07", ++ "2022-01-06 09:32:07", ++ "2022-01-06T09:32:07", ++ ] { ++ let time = Time::parse_time(s).unwrap(); ++ assert_eq!( ++ time.seconds + i64::from(time.offset), ++ wall_clock, ++ "`{s}` did not parse to the expected wall clock time" ++ ); ++ } ++ } ++ ++ /// Git allows a `@` before a commit-header date, with or without an offset. ++ #[test] ++ fn parse_time_at_prefixed_epoch() { ++ let time = Time::parse_time("@1641479527").unwrap(); ++ assert_eq!(time.seconds, 1641479527); ++ ++ let time = Time::parse_time("@1641479527 -0500").unwrap(); ++ assert_eq!(time.seconds, 1641479527); ++ assert_eq!(time.offset, -5 * 60 * 60); ++ } ++ ++ /// Relative dates are resolved against the current time, so equivalent spellings ++ /// are compared against each other rather than against a fixed value. ++ #[test] ++ fn parse_time_relative_spellings() { ++ fn assert_same_moment(a: &str, b: &str) { ++ let (ta, tb) = (Time::parse_time(a).unwrap(), Time::parse_time(b).unwrap()); ++ let delta = (ta.seconds - tb.seconds).abs(); ++ assert!(delta <= 2, "`{a}` and `{b}` differ by {delta} seconds"); ++ } ++ ++ assert_same_moment("one week ago", "7 days ago"); ++ assert_same_moment("last week", "7 days ago"); ++ assert_same_moment("ten days ago", "10 days ago"); ++ assert_same_moment("3.days.ago", "3 days ago"); ++ assert_same_moment("2 days 3 hours ago", "51 hours ago"); ++ assert_same_moment("last month", "1 month ago"); ++ } ++ ++ /// Relative dates are anchored in the local zone, not UTC. ++ #[test] ++ fn parse_time_relative_uses_local_offset() { ++ let now = Time::parse_time("now").unwrap(); ++ for s in ["3 days ago", "yesterday", "1 month ago"] { ++ assert_eq!( ++ Time::parse_time(s).unwrap().offset, ++ now.offset, ++ "`{s}` did not use the same UTC offset as `now`" ++ ); ++ } ++ } ++ + #[test] + fn test_parse_time_negative_offset() { + let time = Time::parse_time("123456 -0230").unwrap(); +@@ -110,6 +181,8 @@ mod tests { + "123456 -060", + "123456 +06000", + "123456 06000", ++ "a long time ago", ++ "bogus nonsense", + ] { + assert!(Time::parse_time(bad_str).is_err()); + } +--- a/src/hook.rs ++++ b/src/hook.rs +@@ -3,7 +3,6 @@ + //! Support for using git repository hooks. + + use std::{ +- borrow::Cow, + io::Write, + path::{Path, PathBuf}, + }; +@@ -18,22 +17,21 @@ use crate::wrap::Message; + /// Returns None if the hook script is not found or is not executable. + fn get_hook_path(repo: &gix::Repository, hook_name: &str) -> Result<Option<PathBuf>> { + let config = repo.config_snapshot(); +- let hooks_path = +- if let Some(core_hooks_path) = config.trusted_path("core.hookspath").transpose()? { +- if core_hooks_path.is_absolute() { +- core_hooks_path +- } else if repo.is_bare() { +- // The hooks path is relative to GIT_DIR in the case of a bare repo +- Cow::Owned(repo.common_dir().join(core_hooks_path)) +- } else { +- // The hooks path is relative to the root of the working tree otherwise +- let work_dir = repo.workdir().expect("non-bare repo must have work dir"); +- Cow::Owned(work_dir.join(core_hooks_path)) +- } ++ let hooks_path = if let Some(core_hooks_path) = config.trusted_path("core.hookspath")? { ++ if core_hooks_path.is_absolute() { ++ core_hooks_path ++ } else if repo.is_bare() { ++ // The hooks path is relative to GIT_DIR in the case of a bare repo ++ repo.common_dir().join(core_hooks_path) + } else { +- // No core.hookspath, use default .git/hooks location +- Cow::Owned(repo.common_dir().join("hooks")) +- }; ++ // The hooks path is relative to the root of the working tree otherwise ++ let work_dir = repo.workdir().expect("non-bare repo must have work dir"); ++ work_dir.join(core_hooks_path) ++ } ++ } else { ++ // No core.hookspath, use default .git/hooks location ++ repo.common_dir().join("hooks") ++ }; + let hook_path = hooks_path.join(hook_name); + + let hook_meta = match std::fs::metadata(&hook_path) { +--- a/src/patch/edit/interactive.rs ++++ b/src/patch/edit/interactive.rs +@@ -135,15 +135,13 @@ fn get_editor(config: &gix::config::Snap + let editor = if let Some(editor) = std::env::var_os("GIT_EDITOR") { + editor + } else if let Some(editor) = config +- .trusted_path("stgit.editor") +- .transpose()? +- .map(|p| p.as_os_str().to_os_string()) ++ .trusted_path("stgit.editor")? ++ .map(|p| p.into_os_string()) + { + editor + } else if let Some(editor) = config +- .trusted_path("core.editor") +- .transpose()? +- .map(|p| p.as_os_str().to_os_string()) ++ .trusted_path("core.editor")? ++ .map(|p| p.into_os_string()) + { + editor + } else if let Some(editor) = std::env::var_os("VISUAL") { +--- a/src/stack/stack.rs ++++ b/src/stack/stack.rs +@@ -243,7 +243,7 @@ impl<'repo> Stack<'repo> { + Some(format!("{}.stgit", self.branch_name).as_str().into()), + "protect", + ) +- .unwrap_or(Ok(false)) ++ .unwrap_or(Some(false)) + .unwrap_or(false) + } + +@@ -271,7 +271,7 @@ impl<'repo> Stack<'repo> { + if let Ok(section) = local_config_file + .section_by_key(format!("{section}.{subsection}").as_bytes().as_bstr()) + { +- if section.num_values() == 0 { ++ if section.body().num_values() == 0 { + local_config_file.remove_section_by_id(section.id()); + } + } +--- a/src/stack/upgrade.rs ++++ b/src/stack/upgrade.rs +@@ -325,7 +325,7 @@ fn rm_stackformatversion(repo: &gix::Rep + if let Ok(section) = + local_config_file.section_by_key(format!("{section}.{subsection}").as_bytes().as_bstr()) + { +- if section.num_values() == 0 { ++ if section.body().num_values() == 0 { + local_config_file.remove_section_by_id(section.id()); + } + } +--- a/t/t3300-edit.sh ++++ b/t/t3300-edit.sh +@@ -345,6 +345,36 @@ test_expect_success 'Set author date to + printf "$before\n$(adate HEAD)\n$after\n" | sort -c - + ' + ++test_expect_success 'Set author date to a relative date' ' ++ stg edit p2 --authdate "one week ago" && ++ week=$(adate HEAD) && ++ stg edit p2 --authdate "3 days ago" && ++ days=$(adate HEAD) && ++ stg edit p2 --authdate "3.days.ago" && ++ dotted=$(adate HEAD) && ++ stg edit p2 --authdate "2 days 3 hours ago" && ++ hours=$(adate HEAD) && ++ test "$week" \< "$days" && ++ test "$week" \< "$dotted" && ++ test "$days" \< "$hours" && ++ test "$dotted" \< "$hours" ++' ++ ++test_expect_success 'Relative author date uses the local time zone' ' ++ TZ=Asia/Kolkata stg edit p2 --authdate "1 day ago" && ++ case "$(adate HEAD)" in ++ *" +0530") : ;; ++ *) echo "expected +0530, got $(adate HEAD)" && false ;; ++ esac ++' ++ ++test_expect_success 'Set author date from epoch with @ prefix' ' ++ stg edit p2 --authdate "@1641479527 -0500" && ++ test "$(adate HEAD)" = "2022-01-06 09:32:07 -0500" && ++ stg edit p2 --authdate "@1641479527" && ++ test "$(adate HEAD)" = "2022-01-06 14:32:07 +0000" ++' ++ + test_expect_success 'Set patch tree' ' + p2tree=$(git log -1 --pretty=format:%T $(stg id p2)) && + p4commit=$(stg id p4) && diff -Nru stgit-2.6.1/debian/patches/reject-lone-at-partial-ref.patch stgit-2.6.1/debian/patches/reject-lone-at-partial-ref.patch --- stgit-2.6.1/debian/patches/reject-lone-at-partial-ref.patch 1969-12-31 18:00:00.000000000 -0600 +++ stgit-2.6.1/debian/patches/reject-lone-at-partial-ref.patch 2026-09-18 10:19:36.000000000 -0500 @@ -0,0 +1,53 @@ +Description: Reject a lone "@" as a partial ref name +Author: Peter Grayson <[email protected]> +Origin: upstream +Applied-Upstream: 10d8dec999f783459ad45671539d37752fad1406 +Last-Update: 2026-09-18 +--- +--- a/src/wrap/partialrefname.rs ++++ b/src/wrap/partialrefname.rs +@@ -101,7 +101,9 @@ pub(crate) fn partial_ref_name(input: &m + + let name = input.next_slice(split_offset); + +- if name.is_empty() || name == "-" { ++ if name.is_empty() || name == "-" || name == "@" { ++ // A lone "@" is shorthand for "HEAD" in revspecs, so git does not allow it as a ++ // reference name. It remains valid as a component, e.g. "refs/heads/@". + Err(ErrMode::Backtrack(ContextError::from_input(input))) + } else if name.ends_with(".lock") { + // Names ending with ".lock" are invalid and there is no recovery. +@@ -141,5 +143,15 @@ mod tests { + partial_ref_name.parse_peek("abc.def/"), + Ok(("/", PartialRefName(String::from("abc.def")))) + ); ++ assert!(partial_ref_name.parse_peek("@").is_err()); ++ assert!(partial_ref_name.parse_peek("@~").is_err()); ++ assert_eq!( ++ partial_ref_name.parse_peek("@abc"), ++ Ok(("", PartialRefName(String::from("@abc")))) ++ ); ++ assert_eq!( ++ partial_ref_name.parse_peek("abc/@"), ++ Ok(("", PartialRefName(String::from("abc/@")))) ++ ); + } + } +--- a/t/t1000-branch-create.sh ++++ b/t/t1000-branch-create.sh +@@ -168,4 +168,15 @@ test_expect_success 'Reuse name of parti + stg branch --create new + ' + ++test_expect_success 'Create branch with reserved name "@"' ' ++ general_error stg branch --create @ && ++ test_must_fail git rev-parse --verify refs/heads/@ ++' ++ ++test_expect_success 'Create branch with "@" as a name component' ' ++ stg branch --create @-suffix && ++ stg branch --create with/@ && ++ stg branch master ++' ++ + test_done diff -Nru stgit-2.6.1/debian/patches/series stgit-2.6.1/debian/patches/series --- stgit-2.6.1/debian/patches/series 2026-08-24 09:43:17.000000000 -0500 +++ stgit-2.6.1/debian/patches/series 2026-09-18 10:10:37.000000000 -0500 @@ -2,4 +2,5 @@ disable_interactive_test use_bzip2_crate use_winnow_0_7 -gix-0.86.patch +gix-0.87.patch +reject-lone-at-partial-ref.patch
OpenPGP_signature.asc
Description: OpenPGP digital signature

