diff --git a/Cargo.lock b/Cargo.lock index 5697687a02..6b585a3c80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1028,6 +1028,18 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea3c6ecd8059b57859df5c69830340ed3c41d30e3da0c1cbed90a96ac853041b" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "windows-sys 0.59.0", +] + [[package]] name = "console-api" version = "0.8.1" @@ -1687,6 +1699,12 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -2662,6 +2680,7 @@ dependencies = [ "gitbutler-user", "gix", "infer", + "insta", "itertools 0.14.0", "resolve-path", "serde", @@ -2884,6 +2903,7 @@ dependencies = [ "parking_lot", "serde_json", "tempfile", + "termtree 0.5.1", "uuid", ] @@ -2961,8 +2981,8 @@ dependencies = [ [[package]] name = "gix" -version = "0.68.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.69.1" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "gix-actor 0.33.1", "gix-attributes 0.23.1", @@ -2970,13 +2990,13 @@ dependencies = [ "gix-commitgraph 0.25.1", "gix-config", "gix-credentials", - "gix-date 0.9.2", + "gix-date 0.9.3", "gix-diff", "gix-dir", "gix-discover 0.37.0", "gix-features 0.39.1", "gix-filter", - "gix-fs 0.12.0", + "gix-fs 0.12.1", "gix-glob 0.17.1", "gix-hash 0.15.1", "gix-hashtable 0.6.0", @@ -2985,26 +3005,27 @@ dependencies = [ "gix-lock 15.0.1", "gix-merge", "gix-negotiate", - "gix-object 0.46.0", + "gix-object 0.46.1", "gix-odb", "gix-pack", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-pathspec", "gix-prompt", "gix-protocol", - "gix-ref 0.49.0", + "gix-ref 0.49.1", "gix-refspec", "gix-revision", "gix-revwalk 0.17.0", - "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-shallow", "gix-status", "gix-submodule", "gix-tempfile 15.0.0", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-transport", - "gix-traverse 0.43.0", + "gix-traverse 0.43.1", "gix-url", - "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-validate 0.9.2", "gix-worktree 0.38.0", "gix-worktree-state", @@ -3030,11 +3051,11 @@ dependencies = [ [[package]] name = "gix-actor" version = "0.33.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", - "gix-date 0.9.2", - "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-date 0.9.3", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "itoa 1.0.11", "thiserror 2.0.9", "winnow 0.6.20", @@ -3060,13 +3081,13 @@ dependencies = [ [[package]] name = "gix-attributes" version = "0.23.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-glob 0.17.1", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "kstring", "smallvec", "thiserror 2.0.9", @@ -3085,7 +3106,7 @@ dependencies = [ [[package]] name = "gix-bitmap" version = "0.2.13" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "thiserror 2.0.9", ] @@ -3102,19 +3123,19 @@ dependencies = [ [[package]] name = "gix-chunk" version = "0.4.10" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "thiserror 2.0.9", ] [[package]] name = "gix-command" -version = "0.3.11" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.4.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "shell-words", ] @@ -3135,10 +3156,10 @@ dependencies = [ [[package]] name = "gix-commitgraph" version = "0.25.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", - "gix-chunk 0.4.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-chunk 0.4.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-features 0.39.1", "gix-hash 0.15.1", "memmap2", @@ -3148,15 +3169,15 @@ dependencies = [ [[package]] name = "gix-config" version = "0.42.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-config-value", "gix-features 0.39.1", "gix-glob 0.17.1", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-ref 0.49.0", - "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-ref 0.49.1", + "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "memchr", "once_cell", "smallvec", @@ -3168,27 +3189,27 @@ dependencies = [ [[package]] name = "gix-config-value" version = "0.14.10" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bitflags 2.6.0", "bstr", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "libc", "thiserror 2.0.9", ] [[package]] name = "gix-credentials" -version = "0.25.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.26.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-command", "gix-config-value", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-prompt", - "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-url", "thiserror 2.0.9", ] @@ -3207,8 +3228,8 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.9.2" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.9.3" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "itoa 1.0.11", @@ -3218,19 +3239,22 @@ dependencies = [ [[package]] name = "gix-diff" -version = "0.48.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.49.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", + "gix-attributes 0.23.1", "gix-command", "gix-filter", - "gix-fs 0.12.0", + "gix-fs 0.12.1", "gix-hash 0.15.1", - "gix-object 0.46.0", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-index 0.37.0", + "gix-object 0.46.1", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-pathspec", "gix-tempfile 15.0.0", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-traverse 0.43.0", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-traverse 0.43.1", "gix-worktree 0.38.0", "imara-diff", "thiserror 2.0.9", @@ -3238,19 +3262,19 @@ dependencies = [ [[package]] name = "gix-dir" -version = "0.10.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.11.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-discover 0.37.0", - "gix-fs 0.12.0", + "gix-fs 0.12.1", "gix-ignore 0.12.1", "gix-index 0.37.0", - "gix-object 0.46.0", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-object 0.46.1", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-pathspec", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-worktree 0.38.0", "thiserror 2.0.9", ] @@ -3274,15 +3298,15 @@ dependencies = [ [[package]] name = "gix-discover" version = "0.37.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "dunce", - "gix-fs 0.12.0", + "gix-fs 0.12.1", "gix-hash 0.15.1", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-ref 0.49.0", - "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-ref 0.49.1", + "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "thiserror 2.0.9", ] @@ -3304,15 +3328,15 @@ dependencies = [ [[package]] name = "gix-features" version = "0.39.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bytes", "crc32fast", "crossbeam-channel", "flate2", "gix-hash 0.15.1", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "libc", "once_cell", "parking_lot", @@ -3325,20 +3349,20 @@ dependencies = [ [[package]] name = "gix-filter" -version = "0.15.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.16.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "encoding_rs", "gix-attributes 0.23.1", "gix-command", "gix-hash 0.15.1", - "gix-object 0.46.0", + "gix-object 0.46.1", "gix-packetline-blocking", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "smallvec", "thiserror 2.0.9", ] @@ -3356,12 +3380,12 @@ dependencies = [ [[package]] name = "gix-fs" -version = "0.12.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.12.1" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "fastrand", "gix-features 0.39.1", - "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", ] [[package]] @@ -3379,12 +3403,12 @@ dependencies = [ [[package]] name = "gix-glob" version = "0.17.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bitflags 2.6.0", "bstr", "gix-features 0.39.1", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", ] [[package]] @@ -3400,7 +3424,7 @@ dependencies = [ [[package]] name = "gix-hash" version = "0.15.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "faster-hex", "thiserror 2.0.9", @@ -3420,7 +3444,7 @@ dependencies = [ [[package]] name = "gix-hashtable" version = "0.6.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "gix-hash 0.15.1", "hashbrown 0.14.5", @@ -3443,12 +3467,12 @@ dependencies = [ [[package]] name = "gix-ignore" version = "0.12.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-glob 0.17.1", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "unicode-bom", ] @@ -3483,20 +3507,20 @@ dependencies = [ [[package]] name = "gix-index" version = "0.37.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bitflags 2.6.0", "bstr", "filetime", "fnv", - "gix-bitmap 0.2.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-bitmap 0.2.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-features 0.39.1", - "gix-fs 0.12.0", + "gix-fs 0.12.1", "gix-hash 0.15.1", "gix-lock 15.0.1", - "gix-object 0.46.0", - "gix-traverse 0.43.0", - "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-object 0.46.1", + "gix-traverse 0.43.1", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-validate 0.9.2", "hashbrown 0.14.5", "itoa 1.0.11", @@ -3521,32 +3545,32 @@ dependencies = [ [[package]] name = "gix-lock" version = "15.0.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "gix-tempfile 15.0.0", - "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "thiserror 2.0.9", ] [[package]] name = "gix-merge" -version = "0.1.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.2.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-command", "gix-diff", "gix-filter", - "gix-fs 0.12.0", + "gix-fs 0.12.1", "gix-hash 0.15.1", "gix-index 0.37.0", - "gix-object 0.46.0", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-object 0.46.1", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-revision", "gix-revwalk 0.17.0", "gix-tempfile 15.0.0", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-worktree 0.38.0", "imara-diff", "thiserror 2.0.9", @@ -3555,13 +3579,13 @@ dependencies = [ [[package]] name = "gix-negotiate" version = "0.17.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bitflags 2.6.0", "gix-commitgraph 0.25.1", - "gix-date 0.9.2", + "gix-date 0.9.3", "gix-hash 0.15.1", - "gix-object 0.46.0", + "gix-object 0.46.1", "gix-revwalk 0.17.0", "smallvec", "thiserror 2.0.9", @@ -3588,17 +3612,17 @@ dependencies = [ [[package]] name = "gix-object" -version = "0.46.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.46.1" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-actor 0.33.1", - "gix-date 0.9.2", + "gix-date 0.9.3", "gix-features 0.39.1", "gix-hash 0.15.1", "gix-hashtable 0.6.0", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-validate 0.9.2", "itoa 1.0.11", "smallvec", @@ -3608,19 +3632,19 @@ dependencies = [ [[package]] name = "gix-odb" -version = "0.65.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.66.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "arc-swap", - "gix-date 0.9.2", + "gix-date 0.9.3", "gix-features 0.39.1", - "gix-fs 0.12.0", + "gix-fs 0.12.1", "gix-hash 0.15.1", "gix-hashtable 0.6.0", - "gix-object 0.46.0", + "gix-object 0.46.1", "gix-pack", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "parking_lot", "tempfile", "thiserror 2.0.9", @@ -3628,16 +3652,16 @@ dependencies = [ [[package]] name = "gix-pack" -version = "0.55.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.56.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "clru", - "gix-chunk 0.4.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-chunk 0.4.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-features 0.39.1", "gix-hash 0.15.1", "gix-hashtable 0.6.0", - "gix-object 0.46.0", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-object 0.46.1", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-tempfile 15.0.0", "memmap2", "parking_lot", @@ -3648,23 +3672,23 @@ dependencies = [ [[package]] name = "gix-packetline" -version = "0.18.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.18.2" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "faster-hex", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "thiserror 2.0.9", ] [[package]] name = "gix-packetline-blocking" version = "0.18.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "faster-hex", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "thiserror 2.0.9", ] @@ -3684,10 +3708,10 @@ dependencies = [ [[package]] name = "gix-path" version = "0.10.13" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "home", "once_cell", "thiserror 2.0.9", @@ -3696,21 +3720,21 @@ dependencies = [ [[package]] name = "gix-pathspec" version = "0.8.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bitflags 2.6.0", "bstr", "gix-attributes 0.23.1", "gix-config-value", "gix-glob 0.17.1", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "thiserror 2.0.9", ] [[package]] name = "gix-prompt" -version = "0.8.9" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.9.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "gix-command", "gix-config-value", @@ -3721,16 +3745,24 @@ dependencies = [ [[package]] name = "gix-protocol" -version = "0.46.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.47.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-credentials", - "gix-date 0.9.2", + "gix-date 0.9.3", "gix-features 0.39.1", "gix-hash 0.15.1", + "gix-lock 15.0.1", + "gix-negotiate", + "gix-object 0.46.1", + "gix-ref 0.49.1", + "gix-refspec", + "gix-revwalk 0.17.0", + "gix-shallow", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-transport", - "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "maybe-async", "thiserror 2.0.9", "winnow 0.6.20", @@ -3750,10 +3782,10 @@ dependencies = [ [[package]] name = "gix-quote" version = "0.4.14" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", - "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "thiserror 2.0.9", ] @@ -3781,18 +3813,18 @@ dependencies = [ [[package]] name = "gix-ref" -version = "0.49.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.49.1" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "gix-actor 0.33.1", "gix-features 0.39.1", - "gix-fs 0.12.0", + "gix-fs 0.12.1", "gix-hash 0.15.1", "gix-lock 15.0.1", - "gix-object 0.46.0", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-object 0.46.1", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-tempfile 15.0.0", - "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-utils 0.1.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-validate 0.9.2", "memmap2", "thiserror 2.0.9", @@ -3802,7 +3834,7 @@ dependencies = [ [[package]] name = "gix-refspec" version = "0.27.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-hash 0.15.1", @@ -3814,18 +3846,18 @@ dependencies = [ [[package]] name = "gix-revision" -version = "0.31.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.31.1" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bitflags 2.6.0", "bstr", "gix-commitgraph 0.25.1", - "gix-date 0.9.2", + "gix-date 0.9.3", "gix-hash 0.15.1", "gix-hashtable 0.6.0", - "gix-object 0.46.0", + "gix-object 0.46.1", "gix-revwalk 0.17.0", - "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-trace 0.1.11 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "thiserror 2.0.9", ] @@ -3847,13 +3879,13 @@ dependencies = [ [[package]] name = "gix-revwalk" version = "0.17.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "gix-commitgraph 0.25.1", - "gix-date 0.9.2", + "gix-date 0.9.3", "gix-hash 0.15.1", "gix-hashtable 0.6.0", - "gix-object 0.46.0", + "gix-object 0.46.1", "smallvec", "thiserror 2.0.9", ] @@ -3873,18 +3905,29 @@ dependencies = [ [[package]] name = "gix-sec" version = "0.10.10" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bitflags 2.6.0", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "libc", "windows-sys 0.52.0", ] +[[package]] +name = "gix-shallow" +version = "0.1.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" +dependencies = [ + "bstr", + "gix-hash 0.15.1", + "gix-lock 15.0.1", + "thiserror 2.0.9", +] + [[package]] name = "gix-status" -version = "0.15.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.16.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "filetime", @@ -3892,11 +3935,11 @@ dependencies = [ "gix-dir", "gix-features 0.39.1", "gix-filter", - "gix-fs 0.12.0", + "gix-fs 0.12.1", "gix-hash 0.15.1", "gix-index 0.37.0", - "gix-object 0.46.0", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-object 0.46.1", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-pathspec", "gix-worktree 0.38.0", "portable-atomic", @@ -3906,11 +3949,11 @@ dependencies = [ [[package]] name = "gix-submodule" version = "0.16.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-config", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-pathspec", "gix-refspec", "gix-url", @@ -3935,10 +3978,10 @@ dependencies = [ [[package]] name = "gix-tempfile" version = "15.0.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "dashmap", - "gix-fs 0.12.0", + "gix-fs 0.12.1", "libc", "once_cell", "parking_lot", @@ -3980,15 +4023,15 @@ checksum = "04bdde120c29f1fc23a24d3e115aeeea3d60d8e65bab92cc5f9d90d9302eb952" [[package]] name = "gix-trace" version = "0.1.11" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "tracing-core", ] [[package]] name = "gix-transport" -version = "0.43.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.44.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "base64 0.22.1", "bstr", @@ -3997,8 +4040,8 @@ dependencies = [ "gix-credentials", "gix-features 0.39.1", "gix-packetline", - "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", - "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-quote 0.4.14 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", + "gix-sec 0.10.10 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-url", "thiserror 2.0.9", ] @@ -4022,15 +4065,15 @@ dependencies = [ [[package]] name = "gix-traverse" -version = "0.43.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.43.1" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bitflags 2.6.0", "gix-commitgraph 0.25.1", - "gix-date 0.9.2", + "gix-date 0.9.3", "gix-hash 0.15.1", "gix-hashtable 0.6.0", - "gix-object 0.46.0", + "gix-object 0.46.1", "gix-revwalk 0.17.0", "smallvec", "thiserror 2.0.9", @@ -4038,12 +4081,12 @@ dependencies = [ [[package]] name = "gix-url" -version = "0.28.1" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.28.2" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-features 0.39.1", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "percent-encoding", "thiserror 2.0.9", "url", @@ -4062,7 +4105,7 @@ dependencies = [ [[package]] name = "gix-utils" version = "0.1.13" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "fastrand", @@ -4082,7 +4125,7 @@ dependencies = [ [[package]] name = "gix-validate" version = "0.9.2" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "thiserror 2.0.9", @@ -4110,35 +4153,35 @@ dependencies = [ [[package]] name = "gix-worktree" version = "0.38.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-attributes 0.23.1", "gix-features 0.39.1", - "gix-fs 0.12.0", + "gix-fs 0.12.1", "gix-glob 0.17.1", "gix-hash 0.15.1", "gix-ignore 0.12.1", "gix-index 0.37.0", - "gix-object 0.46.0", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-object 0.46.1", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-validate 0.9.2", ] [[package]] name = "gix-worktree-state" -version = "0.15.0" -source = "git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a#faa0cdeb35a8135ff9513a1c9884126f6b080f4a" +version = "0.16.0" +source = "git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562#af704f57bb9480c47cdd393465264d586f1d4562" dependencies = [ "bstr", "gix-features 0.39.1", "gix-filter", - "gix-fs 0.12.0", + "gix-fs 0.12.1", "gix-glob 0.17.1", "gix-hash 0.15.1", "gix-index 0.37.0", - "gix-object 0.46.0", - "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=faa0cdeb35a8135ff9513a1c9884126f6b080f4a)", + "gix-object 0.46.1", + "gix-path 0.10.13 (git+https://github.com/GitoxideLabs/gitoxide?rev=af704f57bb9480c47cdd393465264d586f1d4562)", "gix-worktree 0.38.0", "io-close", "thiserror 2.0.9", @@ -4806,6 +4849,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "insta" +version = "1.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9ffc4d4892617c50a928c52b2961cb5174b6fc6ebf252b2fac9d21955c48b8" +dependencies = [ + "console", + "lazy_static", + "linked-hash-map", + "similar", +] + [[package]] name = "instant" version = "0.1.13" @@ -5158,7 +5213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -5208,6 +5263,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-keyutils" version = "0.2.4" @@ -6405,7 +6466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" dependencies = [ "predicates-core", - "termtree", + "termtree 0.4.1", ] [[package]] @@ -7561,6 +7622,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "similar" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" + [[package]] name = "siphasher" version = "0.3.11" @@ -8381,6 +8448,12 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thin-slice" version = "0.1.1" @@ -9372,7 +9445,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 46dbabb408..b6bcc9c099 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ resolver = "2" [workspace.dependencies] bstr = "1.11.1" # Add the `tracing` or `tracing-detail` features to see more of gitoxide in the logs. Useful to see which programs it invokes. -gix = { git = "https://github.com/GitoxideLabs/gitoxide", rev = "faa0cdeb35a8135ff9513a1c9884126f6b080f4a", default-features = false, features = [] } +gix = { git = "https://github.com/GitoxideLabs/gitoxide", rev = "af704f57bb9480c47cdd393465264d586f1d4562", default-features = false, features = [] } git2 = { version = "0.20.0", features = [ "vendored-openssl", "vendored-libgit2", diff --git a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs index cfa1cc8c7a..fd61d4d68c 100644 --- a/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs +++ b/crates/gitbutler-branch-actions/src/branch_manager/branch_creation.rs @@ -13,6 +13,7 @@ use gitbutler_error::error::Marker; use gitbutler_oplog::SnapshotExt; use gitbutler_oxidize::GixRepositoryExt; use gitbutler_project::access::WorktreeWritePermission; +use gitbutler_project::AUTO_TRACK_LIMIT_BYTES; use gitbutler_reference::{Refname, RemoteRefname}; use gitbutler_repo::logging::{LogUntil, RepositoryExt as _}; use gitbutler_repo::{ @@ -305,7 +306,7 @@ impl BranchManager<'_> { // We don't support having two branches applied that conflict with each other { - let uncommited_changes_tree_id = repo.create_wd_tree()?.id(); + let uncommited_changes_tree_id = repo.create_wd_tree(AUTO_TRACK_LIMIT_BYTES)?.id(); let gix_repo = self.ctx.gix_repository_for_merging_non_persisting()?; let merges_cleanly = gix_repo .merges_cleanly_compat( diff --git a/crates/gitbutler-branch-actions/src/virtual.rs b/crates/gitbutler-branch-actions/src/virtual.rs index 62b3762c2f..5f1503642f 100644 --- a/crates/gitbutler-branch-actions/src/virtual.rs +++ b/crates/gitbutler-branch-actions/src/virtual.rs @@ -26,6 +26,7 @@ use gitbutler_oxidize::{ git2_signature_to_gix_signature, git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt, }; use gitbutler_project::access::WorktreeWritePermission; +use gitbutler_project::AUTO_TRACK_LIMIT_BYTES; use gitbutler_reference::{normalize_branch_name, Refname, RemoteRefname}; use gitbutler_repo::{ logging::{LogUntil, RepositoryExt as _}, @@ -1089,7 +1090,7 @@ pub fn is_remote_branch_mergeable( let base_tree = find_base_tree(ctx.repo(), &branch_commit, &target_commit)?; - let wd_tree = ctx.repo().create_wd_tree()?; + let wd_tree = ctx.repo().create_wd_tree(AUTO_TRACK_LIMIT_BYTES)?; let branch_tree = branch_commit.tree().context("failed to find branch tree")?; let gix_repo_in_memory = ctx.gix_repository_for_merging()?.with_object_memory(); diff --git a/crates/gitbutler-edit-mode/src/lib.rs b/crates/gitbutler-edit-mode/src/lib.rs index 72eee0caaa..c69937b4b9 100644 --- a/crates/gitbutler-edit-mode/src/lib.rs +++ b/crates/gitbutler-edit-mode/src/lib.rs @@ -234,7 +234,9 @@ pub(crate) fn save_and_return_to_workspace( let parents = commit.parents().collect::>(); // Recommit commit - let tree = repository.create_wd_tree()?; + // While we perform hard resets we should pick up everything to avoid loosing worktree state. + let pick_up_untracked_files_of_any_size = 0; + let tree = repository.create_wd_tree(pick_up_untracked_files_of_any_size)?; let (_, committer) = repository.signatures()?; let commit_headers = commit diff --git a/crates/gitbutler-oplog/src/oplog.rs b/crates/gitbutler-oplog/src/oplog.rs index c908979e11..d44cd75e76 100644 --- a/crates/gitbutler-oplog/src/oplog.rs +++ b/crates/gitbutler-oplog/src/oplog.rs @@ -20,7 +20,7 @@ use gitbutler_oxidize::{ }; use gitbutler_project::{ access::{WorktreeReadPermission, WorktreeWritePermission}, - Project, + Project, AUTO_TRACK_LIMIT_BYTES, }; use gitbutler_repo::RepositoryExt; use gitbutler_repo::SignaturePurpose; @@ -30,8 +30,6 @@ use gix::object::tree::diff::Change; use gix::prelude::ObjectIdExt; use tracing::instrument; -const SNAPSHOT_FILE_LIMIT_BYTES: u64 = 32 * 1024 * 1024; - /// The Oplog allows for crating snapshots of the current state of the project as well as restoring to a previous snapshot. /// Snapshots include the state of the working directory as well as all additional GitButler state (e.g. virtual branches, conflict state). /// The data is stored as git trees in the following shape: @@ -312,7 +310,7 @@ impl OplogExt for Project { let old_wd_tree_id = tree_from_applied_vbranches(&gix_repo, commit.parent(0)?.id())?; let old_wd_tree = repo.find_tree(old_wd_tree_id)?; - repo.ignore_large_files_in_diffs(SNAPSHOT_FILE_LIMIT_BYTES)?; + repo.ignore_large_files_in_diffs(AUTO_TRACK_LIMIT_BYTES)?; let mut diff_opts = git2::DiffOptions::new(); diff_opts @@ -602,7 +600,7 @@ fn restore_snapshot( let workdir_tree_id = tree_from_applied_vbranches(&gix_repo, snapshot_commit_id)?; let workdir_tree = repo.find_tree(workdir_tree_id)?; - repo.ignore_large_files_in_diffs(SNAPSHOT_FILE_LIMIT_BYTES)?; + repo.ignore_large_files_in_diffs(AUTO_TRACK_LIMIT_BYTES)?; // Define the checkout builder let mut checkout_builder = git2::build::CheckoutBuilder::new(); @@ -739,7 +737,7 @@ fn lines_since_snapshot(project: &Project, repo: &git2::Repository) -> Result Result; fn checkout_tree_builder<'a>(&'a self, tree: &'a git2::Tree<'a>) -> CheckoutTreeBuidler<'a>; fn maybe_find_branch_by_refname(&self, name: &Refname) -> Result>; - /// Based on the index, add all data similar to `git add .` and create a tree from it, which is returned. - fn create_wd_tree(&self) -> Result; + /// Add all untracked and modified files in the worktree to + /// the object database, and create a tree from it. + /// + /// Use `untracked_limit_in_bytes` to control the maximum file size for untracked files + /// before we stop tracking them automatically. Set it to 0 to disable the limit. + /// + /// It should also be noted that this will fail if run on an empty branch + /// or if the HEAD branch has no commits. + fn create_wd_tree(&self, untracked_limit_in_bytes: u64) -> Result; /// Returns the `gitbutler/workspace` branch if the head currently points to it, or fail otherwise. /// Use it before any modification to the repository, or extra defensively each time the @@ -106,16 +113,15 @@ impl RepositoryExt for git2::Repository { Ok(branch) } - /// Note that this will add all untracked and modified files in the worktree to - /// the object database, and create a tree from it. - /// - /// Note that right now, it doesn't skip big files. - /// - /// It should also be noted that this will fail if run on an empty branch - /// or if the HEAD branch has no commits - #[instrument(level = tracing::Level::DEBUG, skip(self), err(Debug))] - fn create_wd_tree(&self) -> Result { - let gix_repo = gix::open_opts( + #[instrument(level = tracing::Level::DEBUG, skip(self, untracked_limit_in_bytes), err(Debug))] + fn create_wd_tree(&self, untracked_limit_in_bytes: u64) -> Result { + use bstr::ByteSlice; + use gix::dir::walk::EmissionMode; + use gix::status; + use gix::status::plumbing::index_as_worktree::{Change, EntryStatus}; + use gix::status::tree_index::TrackRenames; + + let repo = gix::open_opts( self.path(), gix::open::Options::default().permissions(gix::open::Permissions { config: gix::open::permissions::Config { @@ -126,86 +132,196 @@ impl RepositoryExt for git2::Repository { ..Default::default() }), )?; - let (mut pipeline, index) = gix_repo.filter_pipeline(None)?; - let mut tree_update_builder = git2::build::TreeUpdateBuilder::new(); - - let worktree_path = self.workdir().context("Could not find worktree path")?; - - let statuses = self.statuses(Some( - StatusOptions::new() - .renames_from_rewrites(false) - .renames_head_to_index(false) - .renames_index_to_workdir(false) - .include_untracked(true) - .recurse_untracked_dirs(true), - ))?; - - // Truth table for upsert/remove: - // | HEAD Tree -> Index | Index -> Worktree | Action | - // | add | delete | no-action | - // | modify | delete | remove | - // | | delete | remove | - // | delete | | remove | - // | delete | add | upsert | - // | add | | upsert | - // | | add | upsert | - // | add | modify | upsert | - // | modify | modify | upsert | - - let mut buf = Vec::with_capacity(1024); - for status_entry in &statuses { - let status = status_entry.status(); - let path = status_entry.path().context("Failed to get path")?; - let path = Path::new(path); - - if status.is_index_new() && status.is_wt_deleted() { - // This is a no-op - } else if (status.is_index_deleted() && !status.is_wt_new()) || status.is_wt_deleted() { - tree_update_builder.remove(path); + let (mut pipeline, index) = repo.filter_pipeline(None)?; + let workdir = repo.work_dir().context("Need non-bare repository")?; + let mut added_worktree_file = |rela_path: &BStr, + head_tree_editor: &mut gix::object::tree::Editor<'_>| + -> anyhow::Result { + let rela_path_as_path = gix::path::from_bstr(rela_path); + let path = workdir.join(&rela_path_as_path); + let Ok(md) = std::fs::symlink_metadata(&path) else { + return Ok(false); + }; + if untracked_limit_in_bytes != 0 && md.len() > untracked_limit_in_bytes { + return Ok(false); + } + let (id, kind) = if md.is_symlink() { + let target = std::fs::read_link(&path).with_context(|| { + format!( + "Failed to read link at '{}' for adding to the object database", + path.display() + ) + })?; + let id = repo.write_blob(gix::path::into_bstr(target).as_bytes())?; + (id, gix::object::tree::EntryKind::Link) + } else if md.is_file() { + let file = std::fs::File::open(&path).with_context(|| { + format!( + "Could not open file at '{}' for adding it to the object database", + path.display() + ) + })?; + let file_for_git = + pipeline.convert_to_git(file, rela_path_as_path.as_ref(), &index)?; + let id = match file_for_git { + ToGitOutcome::Unchanged(mut file) => repo.write_blob_stream(&mut file)?, + ToGitOutcome::Buffer(buf) => repo.write_blob(buf)?, + ToGitOutcome::Process(mut read) => repo.write_blob_stream(&mut read)?, + }; + + let kind = if gix::fs::is_executable(&md) { + gix::object::tree::EntryKind::BlobExecutable + } else { + gix::object::tree::EntryKind::Blob + }; + (id, kind) } else { - let file_path = worktree_path.join(path).to_owned(); - - if file_path.is_symlink() { - let resolved_path = file_path.read_link()?; - let path_str = resolved_path - .to_str() - .context("Failed to convert path to str")?; - - let blob = self.blob(path_str.as_bytes())?; - tree_update_builder.upsert(path, blob, git2::FileMode::Link); - } else if let io::Result::Ok(file) = std::fs::File::open(&file_path) { - // We might have an entry for a file that does not exist on disk, - // like in the case of a file conflict. - let file_for_git = pipeline.convert_to_git(file, path, &index)?; - let data = match file_for_git { - ToGitOutcome::Unchanged(mut file) => { - buf.clear(); - std::io::copy(&mut file, &mut buf)?; - &buf - } - ToGitOutcome::Buffer(buf) => buf, - ToGitOutcome::Process(mut read) => { - buf.clear(); - std::io::copy(&mut read, &mut buf)?; - &buf - } - }; - let blob_id = self.blob(data)?; - - let file_type = if is_executable(&file_path.metadata()?) { - git2::FileMode::BlobExecutable - } else { - git2::FileMode::Blob - }; - - tree_update_builder.upsert(path, blob_id, file_type); + // This is probably a type-change to something we can't track. Instead of keeping + // what's in `HEAD^{tree}` we remove the entry. + head_tree_editor.remove(rela_path)?; + return Ok(true); + }; + + head_tree_editor.upsert(rela_path, kind, id)?; + Ok(true) + }; + let mut head_tree_editor = repo.edit_tree(repo.head_tree_id()?)?; + let status_changes = repo + .status(gix::progress::Discard)? + .tree_index_track_renames(TrackRenames::Disabled) + .index_worktree_rewrites(None) + .index_worktree_submodules(gix::status::Submodule::Given { + ignore: gix::submodule::config::Ignore::Dirty, + check_dirty: true, + }) + .index_worktree_options_mut(|opts| { + if let Some(opts) = opts.dirwalk_options.as_mut() { + opts.set_emit_ignored(None) + .set_emit_pruned(false) + .set_emit_tracked(false) + .set_emit_untracked(EmissionMode::Matching) + .set_emit_collapsed(None); + } + }) + .into_iter(None)?; + + let mut worktreepaths_changed = HashSet::new(); + // We have to apply untracked items last, but don't have ordering here so impose it ourselves. + let mut untracked_items = Vec::new(); + for change in status_changes { + let change = change?; + match change { + status::Item::TreeIndex(gix::diff::index::Change::Deletion { + location, .. + }) => { + // These changes play second fiddle - they are overwritten by worktree-changes, + // or we assure we don't overwrite, as we may arrive out of order. + if !worktreepaths_changed.contains(location.as_bstr()) { + head_tree_editor.remove(location.as_ref())?; + } + } + status::Item::TreeIndex( + gix::diff::index::Change::Addition { + location, + entry_mode, + id, + .. + } + | gix::diff::index::Change::Modification { + location, + entry_mode, + id, + .. + }, + ) => { + if let Some(entry_mode) = entry_mode + .to_tree_entry_mode() + // These changes play second fiddle - they are overwritten by worktree-changes, + // or we assure we don't overwrite, as we may arrive out of order. + .filter(|_| !worktreepaths_changed.contains(location.as_bstr())) + { + head_tree_editor.upsert( + location.as_ref(), + entry_mode.kind(), + id.as_ref(), + )?; + } + } + status::Item::IndexWorktree(index_worktree::Item::Modification { + rela_path, + status: EntryStatus::Change(Change::Removed), + .. + }) => { + head_tree_editor.remove(rela_path.as_bstr())?; + worktreepaths_changed.insert(rela_path); } + // modified or untracked files are unconditionally added as blob. + // Note that this implementation will re-read the whole blob even on type-change + status::Item::IndexWorktree(index_worktree::Item::Modification { + rela_path, + status: + EntryStatus::Change(Change::Type | Change::Modification { .. }) + | EntryStatus::IntentToAdd, + .. + }) => { + if added_worktree_file(rela_path.as_ref(), &mut head_tree_editor)? { + worktreepaths_changed.insert(rela_path); + } + } + status::Item::IndexWorktree(index_worktree::Item::DirectoryContents { + entry: + gix::dir::Entry { + rela_path, + status: gix::dir::entry::Status::Untracked, + .. + }, + .. + }) => { + untracked_items.push(rela_path); + } + status::Item::IndexWorktree(index_worktree::Item::Modification { + rela_path, + status: EntryStatus::Change(Change::SubmoduleModification(change)), + .. + }) => { + if let Some(possibly_changed_head_commit) = change.checked_out_head_id { + head_tree_editor.upsert( + rela_path.as_bstr(), + gix::object::tree::EntryKind::Commit, + possibly_changed_head_commit, + )?; + worktreepaths_changed.insert(rela_path); + } + } + status::Item::IndexWorktree(index_worktree::Item::Rewrite { .. }) + | status::Item::TreeIndex(gix::diff::index::Change::Rewrite { .. }) => { + unreachable!("disabled") + } + status::Item::IndexWorktree( + index_worktree::Item::Modification { + status: EntryStatus::Conflict(_) | EntryStatus::NeedsUpdate(_), + .. + } + | index_worktree::Item::DirectoryContents { + entry: + gix::dir::Entry { + status: + gix::dir::entry::Status::Tracked + | gix::dir::entry::Status::Pruned + | gix::dir::entry::Status::Ignored(_), + .. + }, + .. + }, + ) => {} } } - let head_tree = self.head()?.peel_to_tree()?; - let tree_oid = tree_update_builder.create_updated(self, &head_tree)?; + for rela_path in untracked_items { + added_worktree_file(rela_path.as_ref(), &mut head_tree_editor)?; + } + let tree_oid = gix_to_git2_oid(head_tree_editor.write()?); Ok(self.find_tree(tree_oid)?) } diff --git a/crates/gitbutler-repo/tests/create_wd_tree.rs b/crates/gitbutler-repo/tests/create_wd_tree.rs index 02bffdef4c..e477f76238 100644 --- a/crates/gitbutler-repo/tests/create_wd_tree.rs +++ b/crates/gitbutler-repo/tests/create_wd_tree.rs @@ -4,9 +4,11 @@ use std::{ }; use gitbutler_repo::RepositoryExt as _; -use gitbutler_testsupport::testing_repository::{ - assert_tree_matches, assert_tree_matches_with_mode, EntryAttribute, TestingRepository, -}; +use gitbutler_testsupport::gix_testtools::scripted_fixture_read_only; +use gitbutler_testsupport::testing_repository::TestingRepository; +use gitbutler_testsupport::visualize_git2_tree; + +const MAX_SIZE: u64 = 20; /// These tests exercise the truth table that we use to update the HEAD /// tree to match the worktree. @@ -25,384 +27,612 @@ use gitbutler_testsupport::testing_repository::{ #[cfg(test)] mod head_upsert_truthtable { use super::*; + use gitbutler_testsupport::visualize_git2_tree; // | add | delete | no-action | #[test] - fn index_new_worktree_delete() { - let test_repository = TestingRepository::open_with_initial_commit(&[]); + fn index_new_worktree_delete() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[]); - std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content1").unwrap(); + std::fs::write(test.tempdir.path().join("file1.txt"), "content1")?; - let mut index = test_repository.repository.index().unwrap(); - index.add_path(Path::new("file1.txt")).unwrap(); - index.write().unwrap(); + let mut index = test.repository.index()?; + index.add_path(Path::new("file1.txt"))?; + index.write()?; - std::fs::remove_file(test_repository.tempdir.path().join("file1.txt")).unwrap(); + std::fs::remove_file(test.tempdir.path().join("file1.txt"))?; - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; assert_eq!(tree.len(), 0, "Tree should end up empty"); + Ok(()) } // | modify | delete | remove | #[test] - fn index_modify_worktree_delete() { - let test_repository = - TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); + fn index_modify_worktree_delete() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); - std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2").unwrap(); + std::fs::write(test.tempdir.path().join("file1.txt"), "content2")?; - let mut index = test_repository.repository.index().unwrap(); - index.add_path(Path::new("file1.txt")).unwrap(); - index.write().unwrap(); + let mut index = test.repository.index()?; + index.add_path(Path::new("file1.txt"))?; + index.write()?; - std::fs::remove_file(test_repository.tempdir.path().join("file1.txt")).unwrap(); + std::fs::remove_file(test.tempdir.path().join("file1.txt"))?; - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; assert_eq!(tree.len(), 0, "Tree should end up empty"); + Ok(()) } // | | delete | remove | #[test] - fn worktree_delete() { - let test_repository = - TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); + fn worktree_delete() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); - std::fs::remove_file(test_repository.tempdir.path().join("file1.txt")).unwrap(); + std::fs::remove_file(test.tempdir.path().join("file1.txt"))?; - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; assert_eq!(tree.len(), 0, "Tree should end up empty"); + Ok(()) } // | delete | | remove | #[test] - fn index_delete() { - let test_repository = - TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); - - let mut index = test_repository.repository.index().unwrap(); - index.remove_all(["*"], None).unwrap(); - index.write().unwrap(); - - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); - - // We should ignore whatever happens to the index - assert_tree_matches( - &test_repository.repository, - &tree, - &[("file1.txt", b"content1")], - ); + fn index_delete() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); + + let mut index = test.repository.index()?; + index.remove_all(["*"], None)?; + index.write()?; + + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + + // We should ignore whatever happens to the index - the current worktree state matters. + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + 7cd1c45 + └── file1.txt:100644:dd954e7 "content1" + "#); + Ok(()) } // | delete | add | upsert | #[test] - fn index_delete_worktree_add() { - let test_repository = - TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); + fn index_delete_worktree_add() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); - let mut index = test_repository.repository.index().unwrap(); - index.remove_all(["*"], None).unwrap(); - index.write().unwrap(); + let mut index = test.repository.index()?; + index.remove_all(["*"], None)?; + index.write()?; - std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2").unwrap(); + std::fs::write(test.tempdir.path().join("file1.txt"), "content2")?; - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; // Tree should match whatever is written on disk - assert_tree_matches( - &test_repository.repository, - &tree, - &[("file1.txt", b"content2")], - ); + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + f87e9ef + └── file1.txt:100644:db00fd6 "content2" + "#); + Ok(()) } // | add | | upsert | #[test] - fn index_add() { - let test_repository = TestingRepository::open_with_initial_commit(&[]); + fn index_add() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[]); - std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2").unwrap(); + std::fs::write(test.tempdir.path().join("file1.txt"), "content2")?; - let mut index = test_repository.repository.index().unwrap(); - index.add_path(Path::new("file1.txt")).unwrap(); - index.write().unwrap(); + let mut index = test.repository.index()?; + index.add_path(Path::new("file1.txt"))?; + index.write()?; - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; - // Tree should match whatever is written on disk - assert_tree_matches( - &test_repository.repository, - &tree, - &[("file1.txt", b"content2")], - ); + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + f87e9ef + └── file1.txt:100644:db00fd6 "content2" + "#); + Ok(()) } // | | add | upsert | #[test] - fn worktree_add() { - let test_repository = TestingRepository::open_with_initial_commit(&[]); + fn worktree_add() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[]); - std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2").unwrap(); + std::fs::write(test.tempdir.path().join("file1.txt"), "content2")?; - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; - // Tree should match whatever is written on disk - assert_tree_matches( - &test_repository.repository, - &tree, - &[("file1.txt", b"content2")], - ); + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + f87e9ef + └── file1.txt:100644:db00fd6 "content2" + "#); + Ok(()) } // | add | modify | upsert | #[test] - fn index_add_worktree_modify() { - let test_repository = TestingRepository::open_with_initial_commit(&[]); + fn index_add_worktree_modify() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[]); - std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content1").unwrap(); + std::fs::write(test.tempdir.path().join("file1.txt"), "content1")?; - let mut index = test_repository.repository.index().unwrap(); - index.add_path(Path::new("file1.txt")).unwrap(); - index.write().unwrap(); + let mut index = test.repository.index()?; + index.add_path(Path::new("file1.txt"))?; + index.write()?; - std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2").unwrap(); + std::fs::write(test.tempdir.path().join("file1.txt"), "content2")?; - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; - // Tree should match whatever is written on disk - assert_tree_matches( - &test_repository.repository, - &tree, - &[("file1.txt", b"content2")], - ); + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + f87e9ef + └── file1.txt:100644:db00fd6 "content2" + "#); + Ok(()) } // | modify | modify | upsert | #[test] - fn index_modify_worktree_modify() { - let test_repository = - TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); + fn index_modify_worktree_modify_racy_git() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); - std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content2").unwrap(); + let file_path = test.tempdir.path().join("file1.txt"); + std::fs::write(&file_path, "content2")?; - let mut index = test_repository.repository.index().unwrap(); - index.add_path(Path::new("file1.txt")).unwrap(); - index.write().unwrap(); + let mut index = test.repository.index()?; + index.add_path(Path::new("file1.txt"))?; + index.write()?; - std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content3").unwrap(); + // This change is made within the same second, so if racy-git isn't handled correctly, + // this change won't be seen. + std::fs::write(file_path, "content3")?; - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; - // Tree should match whatever is written on disk - assert_tree_matches( - &test_repository.repository, - &tree, - &[("file1.txt", b"content3")], - ); + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + d377861 + └── file1.txt:100644:a2b3229 "content3" + "#); + Ok(()) } -} -#[test] -fn lists_uncommited_changes() { - let test_repository = TestingRepository::open_with_initial_commit(&[]); + // | modify | | upsert | + #[test] + fn index_modify() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); - std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content1").unwrap(); - std::fs::write(test_repository.tempdir.path().join("file2.txt"), "content2").unwrap(); + let file_path = test.tempdir.path().join("file1.txt"); + std::fs::write(&file_path, "content2")?; - let tree = test_repository.repository.create_wd_tree().unwrap(); + let mut index = test.repository.index()?; + index.add_path(Path::new("file1.txt"))?; + index.write()?; - assert_tree_matches( - &test_repository.repository, - &tree, - &[("file1.txt", b"content1"), ("file2.txt", b"content2")], - ); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + f87e9ef + └── file1.txt:100644:db00fd6 "content2" + "#); + Ok(()) + } } #[test] -fn does_not_include_staged_but_deleted_files() { - let test_repository = TestingRepository::open_with_initial_commit(&[]); +fn lists_uncommited_changes() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[]); - std::fs::write(test_repository.tempdir.path().join("file1.txt"), "content1").unwrap(); - std::fs::write(test_repository.tempdir.path().join("file2.txt"), "content2").unwrap(); + std::fs::write(test.tempdir.path().join("file1.txt"), "content1")?; + std::fs::write(test.tempdir.path().join("file2.txt"), "content2")?; - std::fs::write(test_repository.tempdir.path().join("file3.txt"), "content2").unwrap(); - let mut index: git2::Index = test_repository.repository.index().unwrap(); - index.add_path(Path::new("file3.txt")).unwrap(); - index.write().unwrap(); - std::fs::remove_file(test_repository.tempdir.path().join("file3.txt")).unwrap(); + let tree = test.repository.create_wd_tree(MAX_SIZE)?; - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + 1ae8c21 + ├── file1.txt:100644:dd954e7 "content1" + └── file2.txt:100644:db00fd6 "content2" + "#); + Ok(()) +} - assert_tree_matches( - &test_repository.repository, - &tree, - &[("file1.txt", b"content1"), ("file2.txt", b"content2")], - ); - assert!(tree.get_name("file3.txt").is_none()); +#[test] +fn does_not_include_staged_but_deleted_files() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[]); + + std::fs::write(test.tempdir.path().join("file1.txt"), "content1")?; + std::fs::write(test.tempdir.path().join("file2.txt"), "content2")?; + + std::fs::write(test.tempdir.path().join("file3.txt"), "content2")?; + let mut index: git2::Index = test.repository.index()?; + index.add_path(Path::new("file3.txt"))?; + index.write()?; + std::fs::remove_file(test.tempdir.path().join("file3.txt"))?; + + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + 1ae8c21 + ├── file1.txt:100644:dd954e7 "content1" + └── file2.txt:100644:db00fd6 "content2" + "#); + Ok(()) } #[test] -fn should_be_empty_after_checking_out_empty_tree() { - let test_repository = TestingRepository::open_with_initial_commit(&[ +fn should_be_empty_after_checking_out_empty_tree() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[ ("file1.txt", "content1"), ("file2.txt", "content2"), ]); // Checkout an empty tree { - let tree_oid = test_repository - .repository - .treebuilder(None) - .unwrap() - .write() - .unwrap(); - let tree = test_repository.repository.find_tree(tree_oid).unwrap(); - test_repository - .repository + let tree_oid = test.repository.treebuilder(None)?.write()?; + let tree = test.repository.find_tree(tree_oid)?; + test.repository .checkout_tree_builder(&tree) .force() .remove_untracked() - .checkout() - .unwrap(); + .checkout()?; } - assert!(!test_repository.tempdir.path().join("file1.txt").exists()); - assert!(!test_repository.tempdir.path().join("file2.txt").exists()); + assert!(!test.tempdir.path().join("file1.txt").exists()); + assert!(!test.tempdir.path().join("file2.txt").exists()); - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; - // Fails because `create_wd_tree` uses the head commit as the base, - // and then performs modifications to the tree + // `create_wd_tree` uses the head commit as the base, and then performs + // modifications to the tree to match the working tree. assert_eq!(tree.len(), 0); + Ok(()) } #[test] -fn should_track_deleted_files() { - let test_repository = TestingRepository::open_with_initial_commit(&[ +fn should_track_deleted_files() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[ ("file1.txt", "content1"), ("file2.txt", "content2"), ]); // Make sure the index is empty, perhaps the user did this action - let mut index: git2::Index = test_repository.repository.index().unwrap(); - index.remove_all(["*"], None).unwrap(); - index.write().unwrap(); + let mut index: git2::Index = test.repository.index()?; + index.remove_all(["*"], None)?; + index.write()?; - std::fs::remove_file(test_repository.tempdir.path().join("file1.txt")).unwrap(); + std::fs::remove_file(test.tempdir.path().join("file1.txt"))?; - assert!(!test_repository.tempdir.path().join("file1.txt").exists()); - assert!(test_repository.tempdir.path().join("file2.txt").exists()); + assert!(!test.tempdir.path().join("file1.txt").exists()); + assert!(test.tempdir.path().join("file2.txt").exists()); - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; - // Fails because `create_wd_tree` uses the head commit as the base, - // and then performs modifications to the tree - assert!(tree.get_name("file1.txt").is_none()); - assert!(tree.get_name("file2.txt").is_some()); + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + 295a2e4 + └── file2.txt:100644:db00fd6 "content2" + "#); + Ok(()) } #[test] -fn should_not_change_index() { - let test_repository = TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); +fn should_not_change_index() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[("file1.txt", "content1")]); - let mut index = test_repository.repository.index().unwrap(); - index.remove_all(["*"], None).unwrap(); - index.write().unwrap(); + let mut index = test.repository.index()?; + index.remove_all(["*"], None)?; + index.write()?; - let index_tree = index.write_tree().unwrap(); - let index_tree = test_repository.repository.find_tree(index_tree).unwrap(); + let index_tree = index.write_tree()?; + let index_tree = test.repository.find_tree(index_tree)?; assert_eq!(index_tree.len(), 0); - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; - let mut index = test_repository.repository.index().unwrap(); - let index_tree = index.write_tree().unwrap(); - let index_tree = test_repository.repository.find_tree(index_tree).unwrap(); + let mut index = test.repository.index()?; + let index_tree = index.write_tree()?; + let index_tree = test.repository.find_tree(index_tree)?; assert_eq!(index_tree.len(), 0); // Tree should match whatever is written on disk - assert_tree_matches( - &test_repository.repository, - &tree, - &[("file1.txt", b"content1")], - ); + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + 7cd1c45 + └── file1.txt:100644:dd954e7 "content1" + "#); + Ok(()) } #[test] -fn tree_behavior() { - let test_repository = TestingRepository::open_with_initial_commit(&[ +fn tree_behavior() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[ ("dir1/file1.txt", "content1"), ("dir2/file2.txt", "content2"), ]); // Update a file in a directory - std::fs::write( - test_repository.tempdir.path().join("dir1/file1.txt"), - "new1", - ) - .unwrap(); + std::fs::write(test.tempdir.path().join("dir1/file1.txt"), "new1")?; // Make a new directory and file - std::fs::create_dir(test_repository.tempdir.path().join("dir3")).unwrap(); - std::fs::write( - test_repository.tempdir.path().join("dir3/file1.txt"), - "new2", - ) - .unwrap(); - - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); - - assert_tree_matches( - &test_repository.repository, - &tree, - &[ - ("dir1/file1.txt", b"new1"), - ("dir2/file2.txt", b"content2"), - ("dir3/file1.txt", b"new2"), - ], - ); + std::fs::create_dir(test.tempdir.path().join("dir3"))?; + std::fs::write(test.tempdir.path().join("dir3/file1.txt"), "new2")?; + + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + c8aa4f7 + ├── dir1:dce0d03 + │ └── file1.txt:100644:e4a8953 "new1" + ├── dir2:295a2e4 + │ └── file2.txt:100644:db00fd6 "content2" + └── dir3:92e07f7 + └── file1.txt:100644:1fda1b4 "new2" + "#); + Ok(()) } -#[cfg(unix)] #[test] -fn executable_blobs() { +#[cfg(unix)] +fn executable_blobs() -> anyhow::Result<()> { use std::{io::Write, os::unix::fs::PermissionsExt as _}; - let test_repository = TestingRepository::open_with_initial_commit(&[]); + let test = TestingRepository::open_with_initial_commit(&[]); - let mut file = File::create(test_repository.tempdir.path().join("file1.txt")).unwrap(); - file.set_permissions(Permissions::from_mode(0o755)).unwrap(); - file.write_all(b"content1").unwrap(); + let mut file = File::create(test.tempdir.path().join("file1.txt"))?; + file.set_permissions(Permissions::from_mode(0o755))?; + file.write_all(b"content1")?; - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; - assert_tree_matches_with_mode( - &test_repository.repository, - tree.id(), - &[( - "file1.txt", - b"content1", - &[EntryAttribute::Blob, EntryAttribute::Executable], - )], - ); + // The executable bit is also present in the tree. + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + 4cb9de9 + └── file1.txt:100755:dd954e7 "content1" + "#); + Ok(()) } +#[test] #[cfg(unix)] +fn links() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[("target", "helloworld")]); + + std::os::unix::fs::symlink("target", test.tempdir.path().join("link1.txt"))?; + + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + + // Links are also present in the tree. + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + 0aefe10 + ├── link1.txt:120000:1de5659 "target" + └── target:100644:620ffd0 "helloworld" + "#); + Ok(()) +} + #[test] -fn links() { - let test_repository = TestingRepository::open_with_initial_commit(&[("target", "helloworld")]); +fn tracked_file_becomes_directory_in_worktree() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[( + "soon-directory", + "this tracked file becomes a directory", + )]); + let worktree_path = test.tempdir.path().join("soon-directory"); + std::fs::remove_file(&worktree_path)?; + std::fs::create_dir(&worktree_path)?; + std::fs::write(worktree_path.join("file"), "content in directory")?; + + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + 8b80519 + └── soon-directory:df6d699 + └── file:100644:dadf628 "content in directory" + "#); + Ok(()) +} - std::os::unix::fs::symlink("target", test_repository.tempdir.path().join("link1.txt")).unwrap(); +#[test] +fn tracked_directory_becomes_file_in_worktree() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[( + "soon-file/content", + "this tracked is removed and the parent dir becomes a file", + )]); + let worktree_path = test.tempdir.path().join("soon-file"); + std::fs::remove_dir_all(&worktree_path)?; + std::fs::write(worktree_path, "content")?; + + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + 637be29 + └── soon-file:100644:6b584e8 "content" + "#); + Ok(()) +} - let tree: git2::Tree = test_repository.repository.create_wd_tree().unwrap(); +#[test] +#[cfg(unix)] +fn non_files_are_ignored() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[]); + + let fifo_path = test.tempdir.path().join("fifo"); + assert!(std::process::Command::new("mkfifo") + .arg(&fifo_path) + .status()? + .success()); + + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + assert_eq!( + tree.len(), + 0, + "It completely ignores non-files, it doesn't see them, just like Git" + ); + Ok(()) +} - assert_tree_matches_with_mode( - &test_repository.repository, - tree.id(), - &[ - ("link1.txt", b"target", &[EntryAttribute::Link]), - ("target", b"helloworld", &[EntryAttribute::Blob]), - ], +#[test] +#[cfg(unix)] +fn tracked_file_swapped_with_non_file() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[("soon-fifo", "actual content")]); + + let fifo_path = test.tempdir.path().join("soon-fifo"); + std::fs::remove_file(&fifo_path)?; + assert!(std::process::Command::new("mkfifo") + .arg(&fifo_path) + .status()? + .success()); + + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + assert_eq!( + tree.len(), + 0, + "It completely ignores non-files, it doesn't see them, just like Git, even when previously tracked" ); + Ok(()) +} + +#[test] +fn ignored_files() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[ + ("tracked", "content"), + (".gitignore", "*.ignored"), + ]); + + let ignored_path = test.tempdir.path().join("I-am.ignored"); + std::fs::write(&ignored_path, "")?; + + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + // ignored files aren't picked up. + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + 38b94c0 + ├── .gitignore:100644:669be81 "*.ignored" + └── tracked:100644:6b584e8 "content" + "#); + Ok(()) +} + +#[test] +fn can_autotrack_empty_files() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[("soon-empty", "content")]); + + let ignored_path = test.tempdir.path().join("soon-empty"); + std::fs::write(&ignored_path, "")?; + + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + // ignored files aren't picked up. + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + 4fe2781 + └── soon-empty:100644:e69de29 "" + "#); + Ok(()) +} + +#[test] +fn intent_to_add_is_picked_up_just_like_untracked() -> anyhow::Result<()> { + let repo = repo("intent-to-add")?; + + let tree: git2::Tree = repo.create_wd_tree(MAX_SIZE)?; + // We pick up what's in the worktree, independently of the intent-to-add flag. + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &repo), @r#" + d6a22f9 + └── to-be-added:100644:6b584e8 "content" + "#); + Ok(()) +} + +#[test] +fn submodule_in_index_is_picked_up() -> anyhow::Result<()> { + let repo = repo("with-submodule-in-index")?; + + let tree: git2::Tree = repo.create_wd_tree(MAX_SIZE)?; + // Everything that is not contending with the worktree that is already in the index + // is picked up, even if it involves submodules. + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &repo), @r#" + de956ee + ├── .gitmodules:100644:db28142 "[submodule \"sm\"]\n\tpath = sm\n\turl = ../module\n" + └── sm:160000:2e70126 + "#); + Ok(()) +} + +#[test] +fn submodule_change() -> anyhow::Result<()> { + let repo = repo("with-submodule-new-commit")?; + + let tree: git2::Tree = repo.create_wd_tree(MAX_SIZE)?; + + // Changes to submodule heads are also picked up. + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &repo), @r#" + 8b0adff + ├── .gitmodules:100644:db28142 "[submodule \"sm\"]\n\tpath = sm\n\turl = ../module\n" + └── sm:160000:e8a2d3a + "#); + Ok(()) +} + +#[test] +fn big_files_check_is_disabled_with_zero() -> anyhow::Result<()> { + let test = TestingRepository::open_with_initial_commit(&[]); + + std::fs::write(test.tempdir.path().join("empty"), "")?; + std::fs::write(test.tempdir.path().join("with-content"), "content")?; + + let tree: git2::Tree = test.repository.create_wd_tree(0)?; + + // Everything goes with 0 + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + f6e159b + ├── empty:100644:e69de29 "" + └── with-content:100644:6b584e8 "content" + "#); + Ok(()) +} + +#[test] +fn big_files_are_ignored_based_on_threshold_in_working_tree() -> anyhow::Result<()> { + let test = + TestingRepository::open_with_initial_commit(&[("soon-too-big", "still small enough")]); + + let big_file_path = test.tempdir.path().join("soon-too-big"); + std::fs::write(&big_file_path, "a massive file above the threshold")?; + + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + + // It does not pickup the big worktree change. + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + 26ea3c5 + └── soon-too-big:100644:7d72316 "still small enough" + "#); + Ok(()) +} + +#[test] +fn big_files_are_fine_when_in_the_index() -> anyhow::Result<()> { + let test = + TestingRepository::open_with_initial_commit(&[("soon-too-big", "still small enough")]); + + std::fs::write( + test.tempdir.path().join("soon-too-big"), + "a massive file above the threshold", + )?; + let mut index = test.repository.index()?; + index.add_path("soon-too-big".as_ref())?; + index.write()?; + + let tree: git2::Tree = test.repository.create_wd_tree(MAX_SIZE)?; + + // It keeps files that were already added. + insta::assert_snapshot!(visualize_git2_tree(tree.id(), &test.repository), @r#" + bbd82c6 + └── soon-too-big:100644:1799e5a "a massive file above the threshold" + "#); + Ok(()) +} + +fn repo(name: &str) -> anyhow::Result { + let worktree_dir = scripted_fixture_read_only("make_create_wd_tree_repos.sh") + .map_err(anyhow::Error::from_boxed)? + .join(name); + Ok(git2::Repository::open(worktree_dir)?) } diff --git a/crates/gitbutler-repo/tests/fixtures/make_create_wd_tree_repos.sh b/crates/gitbutler-repo/tests/fixtures/make_create_wd_tree_repos.sh new file mode 100644 index 0000000000..bc79de2a10 --- /dev/null +++ b/crates/gitbutler-repo/tests/fixtures/make_create_wd_tree_repos.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -eu -o pipefail + +git init intent-to-add +(cd intent-to-add + git commit -m "init" --allow-empty + echo -n content >to-be-added + git add --intent-to-add to-be-added +) + +git init module; +(cd module + mkdir dir + touch a dir/b + git add . && git commit -m "init" +) + +git init with-submodule-in-index +(cd with-submodule-in-index + git commit -m "init" --allow-empty + git submodule add ../module sm +) + +git init with-submodule-new-commit +(cd with-submodule-new-commit + git commit -m "init" --allow-empty + git submodule add ../module sm + git commit -m "add submodule" + (cd sm + echo -n change >>a && git commit -am "change file in submodule" + ) +) diff --git a/crates/gitbutler-testsupport/Cargo.toml b/crates/gitbutler-testsupport/Cargo.toml index 2c13835510..735a0e6488 100644 --- a/crates/gitbutler-testsupport/Cargo.toml +++ b/crates/gitbutler-testsupport/Cargo.toml @@ -31,4 +31,5 @@ gitbutler-url.workspace = true gitbutler-stack.workspace = true gitbutler-oxidize.workspace = true gitbutler-commit.workspace = true +termtree = "0.5.1" uuid.workspace = true diff --git a/crates/gitbutler-testsupport/src/lib.rs b/crates/gitbutler-testsupport/src/lib.rs index 6435561356..6a6ae70c31 100644 --- a/crates/gitbutler-testsupport/src/lib.rs +++ b/crates/gitbutler-testsupport/src/lib.rs @@ -1,6 +1,7 @@ #![forbid(rust_2018_idioms)] pub const VAR_NO_CLEANUP: &str = "GITBUTLER_TESTS_NO_CLEANUP"; +use gix::bstr::BStr; /// Direct access to lower-level utilities for cases where this is enough. /// /// Prefer to use [`read_only`] and [`writable`] otherwise. @@ -101,6 +102,63 @@ pub mod writable { Ok((project, root)) } } +/// Display a Git tree in the style of the `tree` CLI program, but include blob contents and usful Git metadata. +pub fn visualize_gix_tree(tree_id: gix::Id<'_>) -> termtree::Tree { + fn visualize_tree( + id: gix::Id<'_>, + name_and_mode: Option<(&BStr, gix::object::tree::EntryMode)>, + ) -> anyhow::Result> { + fn short_id(id: &gix::hash::oid) -> String { + id.to_string()[..7].to_string() + } + let repo = id.repo; + let entry_name = + |id: &gix::hash::oid, name: Option<(&BStr, gix::object::tree::EntryMode)>| -> String { + match name { + None => short_id(id), + Some((name, mode)) => { + format!( + "{name}:{mode}{} {}", + short_id(id), + match repo.find_blob(id) { + Ok(blob) => format!("{:?}", blob.data.as_bstr()), + Err(_) => "".into(), + }, + mode = if mode.is_tree() { + "".into() + } else { + format!("{:o}:", mode.0) + } + ) + } + } + }; + + let mut tree = termtree::Tree::new(entry_name(&id, name_and_mode)); + for entry in repo.find_tree(id)?.iter() { + let entry = entry?; + if entry.mode().is_tree() { + tree.push(visualize_tree( + entry.id(), + Some((entry.filename(), entry.mode())), + )?); + } else { + tree.push(entry_name( + entry.oid(), + Some((entry.filename(), entry.mode())), + )); + } + } + Ok(tree) + } + visualize_tree(tree_id, None).unwrap() +} + +/// Visualize a git2 tree, otherwise just like [`visualize_gix_tree()`]. +pub fn visualize_git2_tree(tree_id: git2::Oid, repo: &git2::Repository) -> termtree::Tree { + let repo = gix::open_opts(repo.path(), gix::open::Options::isolated()).unwrap(); + visualize_gix_tree(git2_to_gix_object_id(tree_id).attach(&repo)) +} pub mod read_only { use crate::DRIVER; @@ -153,8 +211,12 @@ pub mod read_only { } } +use gitbutler_oxidize::git2_to_gix_object_id; +use gix::bstr::ByteSlice; +use gix::prelude::ObjectIdExt; use once_cell::sync::Lazy; use std::path::{Path, PathBuf}; + pub(crate) static DRIVER: Lazy = Lazy::new(|| { let mut cargo = std::process::Command::new(env!("CARGO")); let res = cargo diff --git a/crates/gitbutler-testsupport/src/testing_repository.rs b/crates/gitbutler-testsupport/src/testing_repository.rs index 7c962ccc2d..86f8c9b040 100644 --- a/crates/gitbutler-testsupport/src/testing_repository.rs +++ b/crates/gitbutler-testsupport/src/testing_repository.rs @@ -145,6 +145,7 @@ impl TestingRepository { index .add_all(["*"], git2::IndexAddOption::DEFAULT, None) .unwrap(); + index.write().unwrap(); let signature = git2::Signature::now("Caleb", "caleb@gitbutler.com").unwrap(); let commit_headers = diff --git a/crates/gitbutler-workspace/src/branch_trees.rs b/crates/gitbutler-workspace/src/branch_trees.rs index aabe46b850..64f8756287 100644 --- a/crates/gitbutler-workspace/src/branch_trees.rs +++ b/crates/gitbutler-workspace/src/branch_trees.rs @@ -4,6 +4,7 @@ use gitbutler_command_context::CommandContext; use gitbutler_commit::commit_ext::CommitExt as _; use gitbutler_oxidize::{git2_to_gix_object_id, gix_to_git2_oid, GixRepositoryExt}; use gitbutler_project::access::WorktreeWritePermission; +use gitbutler_project::AUTO_TRACK_LIMIT_BYTES; use gitbutler_repo::rebase::cherry_rebase_group; use gitbutler_repo::RepositoryExt as _; use gitbutler_stack::{Stack, VirtualBranchesHandle}; @@ -23,7 +24,7 @@ pub fn checkout_branch_trees<'a>( if stacks.is_empty() { // If there are no applied branches, then return the current uncommtied state - return repository.create_wd_tree(); + return repository.create_wd_tree(AUTO_TRACK_LIMIT_BYTES); }; if stacks.len() == 1 { diff --git a/crates/gitbutler-workspace/tests/mod.rs b/crates/gitbutler-workspace/tests/mod.rs index ddc69695ec..d421f91da1 100644 --- a/crates/gitbutler-workspace/tests/mod.rs +++ b/crates/gitbutler-workspace/tests/mod.rs @@ -6,6 +6,7 @@ mod checkout_branch_trees { use gitbutler_branch::BranchCreateRequest; use gitbutler_branch_actions as branch_actions; use gitbutler_command_context::CommandContext; + use gitbutler_project::AUTO_TRACK_LIMIT_BYTES; use gitbutler_repo::RepositoryExt as _; use gitbutler_testsupport::{paths, testing_repository::assert_tree_matches, TestProject}; use gitbutler_workspace::checkout_branch_trees; @@ -40,7 +41,10 @@ mod checkout_branch_trees { branch_actions::create_commit(&project, branch_2, "commit two", None, false).unwrap(); - let tree = test_project.local_repository.create_wd_tree().unwrap(); + let tree = test_project + .local_repository + .create_wd_tree(AUTO_TRACK_LIMIT_BYTES) + .unwrap(); // Assert original state assert_tree_matches( @@ -70,7 +74,10 @@ mod checkout_branch_trees { // Assert tree is indeed empty { - let tree: git2::Tree = test_project.local_repository.create_wd_tree().unwrap(); + let tree: git2::Tree = test_project + .local_repository + .create_wd_tree(AUTO_TRACK_LIMIT_BYTES) + .unwrap(); // Tree should be empty assert_eq!( @@ -85,7 +92,10 @@ mod checkout_branch_trees { checkout_branch_trees(&ctx, guard.write_permission()).unwrap(); - let tree = test_project.local_repository.create_wd_tree().unwrap(); + let tree = test_project + .local_repository + .create_wd_tree(AUTO_TRACK_LIMIT_BYTES) + .unwrap(); // Should be back to original state assert_tree_matches(