upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorm0wer <m0wer@sgn.space>2026-03-29 16:45:52 +0200
committerDanConwayDev <DanConwayDev@protonmail.com>2026-03-30 16:29:23 +0000
commit820fd706a24be7a58554a27e411e120cfa28d9a6 (patch)
tree2b95e4f835122dabfcd1f72f637a12368a4c18f9
parente3276e74bc45cb4fb8f158b8249bee3d12a0805f (diff)
feat: git worktree support
Git worktrees don't have a .git directory with a parent, so we need to look for the git dir via git2's Repository::discover() and then look for the cache database there. This allows the client to work correctly when run from a worktree, and also allows the cache database to be shared between the main repo and its worktrees (since they share the same git dir and thus the same cache path).
-rw-r--r--src/lib/client.rs14
-rw-r--r--src/lib/git/mod.rs133
-rw-r--r--test_utils/src/git.rs43
-rw-r--r--test_utils/src/lib.rs8
4 files changed, 189 insertions, 9 deletions
diff --git a/src/lib/client.rs b/src/lib/client.rs
index 01662cd..86e4097 100644
--- a/src/lib/client.rs
+++ b/src/lib/client.rs
@@ -1325,14 +1325,22 @@ fn pb_after_style(succeed: bool) -> indicatif::ProgressStyle {
1325} 1325}
1326 1326
1327async fn get_local_cache_database(git_repo_path: &Path) -> Result<NostrLMDB> { 1327async fn get_local_cache_database(git_repo_path: &Path) -> Result<NostrLMDB> {
1328 NostrLMDB::open(git_repo_path.join(".git/nostr-cache.lmdb")) 1328 let git_dir = git2::Repository::discover(git_repo_path)
1329 .context("failed to open or create nostr cache database at .git/nostr-cache.lmdb") 1329 .context("failed to discover git repository")?
1330 .commondir()
1331 .to_path_buf();
1332 NostrLMDB::open(git_dir.join("nostr-cache.lmdb"))
1333 .context("failed to open or create nostr cache database at <git-dir>/nostr-cache.lmdb")
1330} 1334}
1331 1335
1332async fn get_global_cache_database(git_repo_path: Option<&Path>) -> Result<NostrLMDB> { 1336async fn get_global_cache_database(git_repo_path: Option<&Path>) -> Result<NostrLMDB> {
1333 let path = if std::env::var("NGITTEST").is_ok() { 1337 let path = if std::env::var("NGITTEST").is_ok() {
1334 if let Some(git_repo_path) = git_repo_path { 1338 if let Some(git_repo_path) = git_repo_path {
1335 git_repo_path.join(".git/test-global-cache.lmdb") 1339 let git_dir = git2::Repository::discover(git_repo_path)
1340 .context("failed to discover git repository")?
1341 .commondir()
1342 .to_path_buf();
1343 git_dir.join("test-global-cache.lmdb")
1336 } else { 1344 } else {
1337 bail!("git_repo must be supplied to get_global_cache_database during integration tests") 1345 bail!("git_repo must be supplied to get_global_cache_database during integration tests")
1338 } 1346 }
diff --git a/src/lib/git/mod.rs b/src/lib/git/mod.rs
index 641349c..0001ca1 100644
--- a/src/lib/git/mod.rs
+++ b/src/lib/git/mod.rs
@@ -105,10 +105,9 @@ pub trait RepoActions {
105 105
106impl RepoActions for Repo { 106impl RepoActions for Repo {
107 fn get_path(&self) -> Result<&Path> { 107 fn get_path(&self) -> Result<&Path> {
108 self.git_repo 108 self.git_repo.workdir().context(
109 .path() 109 "failed to find repository working directory (bare repositories are not supported)",
110 .parent() 110 )
111 .context("failed to find repositiory path as .git has no parent")
112 } 111 }
113 112
114 fn get_origin_url(&self) -> Result<String> { 113 fn get_origin_url(&self) -> Result<String> {
@@ -2848,4 +2847,130 @@ index ce01362..a21e91c 100644\n\
2848 } 2847 }
2849 } 2848 }
2850 2849
2850 mod worktree {
2851 use super::*;
2852
2853 #[test]
2854 fn get_path_returns_worktree_working_dir() -> Result<()> {
2855 let test_repo = GitTestRepo::default();
2856 test_repo.populate()?;
2857
2858 let worktree_repo = test_repo.create_worktree("wt-branch")?;
2859 let git_repo = Repo::from_path(&worktree_repo.dir)?;
2860
2861 let path = git_repo.get_path()?;
2862 // get_path() should return the worktree's working directory, not
2863 // somewhere inside the main repo's .git/worktrees/
2864 assert_eq!(path.canonicalize()?, worktree_repo.dir.canonicalize()?,);
2865 Ok(())
2866 }
2867
2868 #[test]
2869 fn get_path_returns_normal_repo_working_dir() -> Result<()> {
2870 let test_repo = GitTestRepo::default();
2871 test_repo.populate()?;
2872
2873 let git_repo = Repo::from_path(&test_repo.dir)?;
2874 let path = git_repo.get_path()?;
2875 assert_eq!(path.canonicalize()?, test_repo.dir.canonicalize()?,);
2876 Ok(())
2877 }
2878
2879 #[test]
2880 fn from_path_works_with_worktree_dir() -> Result<()> {
2881 let test_repo = GitTestRepo::default();
2882 test_repo.populate()?;
2883
2884 let worktree_repo = test_repo.create_worktree("wt-open")?;
2885 // Opening from the worktree's working directory should succeed
2886 let git_repo = Repo::from_path(&worktree_repo.dir)?;
2887 // And get_path() should return the worktree dir
2888 assert_eq!(
2889 git_repo.get_path()?.canonicalize()?,
2890 worktree_repo.dir.canonicalize()?,
2891 );
2892 Ok(())
2893 }
2894
2895 #[test]
2896 fn worktree_can_read_branches() -> Result<()> {
2897 let test_repo = GitTestRepo::default();
2898 test_repo.populate()?;
2899
2900 let worktree_repo = test_repo.create_worktree("wt-branches")?;
2901 let git_repo = Repo::from_path(&worktree_repo.dir)?;
2902
2903 let branches = git_repo.get_local_branch_names()?;
2904 assert!(branches.contains(&"main".to_string()));
2905 assert!(branches.contains(&"wt-branches".to_string()));
2906 Ok(())
2907 }
2908
2909 #[test]
2910 fn worktree_can_read_git_config() -> Result<()> {
2911 let test_repo = GitTestRepo::default();
2912 test_repo.populate()?;
2913
2914 let worktree_repo = test_repo.create_worktree("wt-config")?;
2915 let git_repo = Repo::from_path(&worktree_repo.dir)?;
2916
2917 // nostr.repo is set by GitTestRepo::default() on the main repo
2918 // and should be readable from the worktree since config is shared
2919 let nostr_repo = git_repo.get_git_config_item("nostr.repo", None)?;
2920 assert!(nostr_repo.is_some());
2921 Ok(())
2922 }
2923
2924 #[test]
2925 fn worktree_get_head_commit_works() -> Result<()> {
2926 let test_repo = GitTestRepo::default();
2927 test_repo.populate()?;
2928
2929 let worktree_repo = test_repo.create_worktree("wt-head")?;
2930 let git_repo = Repo::from_path(&worktree_repo.dir)?;
2931
2932 // Should not error - worktree has its own HEAD
2933 let _head = git_repo.get_head_commit()?;
2934 Ok(())
2935 }
2936
2937 #[test]
2938 fn worktree_files_accessible_from_get_path() -> Result<()> {
2939 let test_repo = GitTestRepo::default();
2940 test_repo.populate()?;
2941
2942 let worktree_repo = test_repo.create_worktree("wt-files")?;
2943 let git_repo = Repo::from_path(&worktree_repo.dir)?;
2944
2945 // Create a file in the worktree
2946 let test_file = worktree_repo.dir.join("worktree-test.txt");
2947 fs::write(&test_file, "hello from worktree")?;
2948
2949 // get_path() should point to the worktree dir where the file lives
2950 let path = git_repo.get_path()?;
2951 assert!(path.join("worktree-test.txt").exists());
2952 Ok(())
2953 }
2954
2955 #[test]
2956 fn worktree_opened_via_git_dir_env_works() -> Result<()> {
2957 let test_repo = GitTestRepo::default();
2958 test_repo.populate()?;
2959
2960 let worktree_repo = test_repo.create_worktree("wt-gitdir")?;
2961
2962 // In a worktree, GIT_DIR points to the worktree-specific git dir
2963 // (e.g., .git/worktrees/<name>). Simulate what git does when
2964 // calling a remote helper.
2965 let git_dir = worktree_repo.git_repo.path().to_path_buf();
2966 let git_repo = Repo::from_path(&git_dir)?;
2967
2968 // get_path() should still return the worktree working directory
2969 assert_eq!(
2970 git_repo.get_path()?.canonicalize()?,
2971 worktree_repo.dir.canonicalize()?,
2972 );
2973 Ok(())
2974 }
2975 }
2851} 2976}
diff --git a/test_utils/src/git.rs b/test_utils/src/git.rs
index a18f81c..2668b3f 100644
--- a/test_utils/src/git.rs
+++ b/test_utils/src/git.rs
@@ -276,6 +276,49 @@ impl GitTestRepo {
276 Ok(()) 276 Ok(())
277 } 277 }
278 278
279 /// Creates a git worktree linked to this repository.
280 /// Returns a `GitTestRepo` whose `dir` points to the worktree working
281 /// directory.
282 pub fn create_worktree(&self, branch_name: &str) -> Result<GitTestRepo> {
283 let worktree_path = self
284 .dir
285 .parent()
286 .unwrap()
287 .join(format!("tmpgit-worktree-{}", rand::random::<u64>()));
288
289 // Create the branch at the current HEAD
290 let head_commit = self.git_repo.head()?.peel_to_commit()?;
291 self.git_repo
292 .branch(branch_name, &head_commit, false)
293 .context("failed to create branch for worktree")?;
294
295 // Add worktree via git2
296 let worktree = self
297 .git_repo
298 .worktree(
299 branch_name,
300 &worktree_path,
301 Some(
302 git2::WorktreeAddOptions::new().reference(Some(
303 &self
304 .git_repo
305 .find_branch(branch_name, git2::BranchType::Local)?
306 .into_reference(),
307 )),
308 ),
309 )
310 .context("failed to create worktree")?;
311
312 let worktree_repo = git2::Repository::open_from_worktree(&worktree)
313 .context("failed to open repo from worktree")?;
314
315 Ok(GitTestRepo {
316 dir: worktree_path,
317 git_repo: worktree_repo,
318 delete_dir_on_drop: true,
319 })
320 }
321
279 pub fn checkout_remote_branch(&self, branch_name: &str) -> Result<Oid> { 322 pub fn checkout_remote_branch(&self, branch_name: &str) -> Result<Oid> {
280 self.checkout(&format!("remotes/origin/{branch_name}"))?; 323 self.checkout(&format!("remotes/origin/{branch_name}"))?;
281 let mut branch = self.create_branch(branch_name)?; 324 let mut branch = self.create_branch(branch_name)?;
diff --git a/test_utils/src/lib.rs b/test_utils/src/lib.rs
index 48273e8..89cbaa9 100644
--- a/test_utils/src/lib.rs
+++ b/test_utils/src/lib.rs
@@ -1208,8 +1208,12 @@ where
1208 1208
1209/** copied from client.rs */ 1209/** copied from client.rs */
1210async fn get_local_cache_database(git_repo_path: &Path) -> Result<NostrLMDB> { 1210async fn get_local_cache_database(git_repo_path: &Path) -> Result<NostrLMDB> {
1211 NostrLMDB::open(git_repo_path.join(".git/nostr-cache.lmdb")) 1211 let git_dir = git2::Repository::discover(git_repo_path)
1212 .context("failed to open or create nostr cache database at .git/nostr-cache.lmdb") 1212 .context("failed to discover git repository")?
1213 .commondir()
1214 .to_path_buf();
1215 NostrLMDB::open(git_dir.join("nostr-cache.lmdb"))
1216 .context("failed to open or create nostr cache database at <git-dir>/nostr-cache.lmdb")
1213} 1217}
1214 1218
1215/** copied from client.rs */ 1219/** copied from client.rs */