From 54781e2ad9a88101d0cc6fb8f36220ea6767e7c2 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 12 Aug 2026 10:40:12 +0700 Subject: [PATCH] add ref viewer --- Cargo.lock | 343 ++++++++-------- crates/assets/assets/icons/git-branch.svg | 1 + crates/assets/assets/icons/tag.svg | 1 + crates/assets/src/lib.rs | 4 + crates/signed_git/src/lib.rs | 234 ++++++++++- crates/workspace/src/views/repo_detail/mod.rs | 379 +++++++++++++++++- 6 files changed, 751 insertions(+), 211 deletions(-) create mode 100644 crates/assets/assets/icons/git-branch.svg create mode 100644 crates/assets/assets/icons/tag.svg diff --git a/Cargo.lock b/Cargo.lock index ca5895e..1e79761 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -480,9 +480,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", @@ -591,7 +591,7 @@ dependencies = [ "num-traits", "pastey", "rayon", - "thiserror 2.0.19", + "thiserror 2.0.20", "v_frame", "y4m", ] @@ -890,9 +890,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", "regex-automata", @@ -1244,7 +1244,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "gpui_util", "indexmap", @@ -1595,9 +1595,9 @@ dependencies = [ [[package]] name = "ctor" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d83cb7e7a873830708d6b02a78cd36a592c6fa14bf267b68725103b85c0d77f" +checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d" dependencies = [ "link-section", "linktime-proc-macro", @@ -1652,7 +1652,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -1687,7 +1687,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "proc-macro2", "quote", @@ -2293,9 +2293,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -2308,9 +2308,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -2331,15 +2331,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -2348,9 +2348,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-lite" @@ -2367,32 +2367,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -2545,7 +2545,7 @@ dependencies = [ "gix-zlib", "nonempty", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2572,7 +2572,7 @@ dependencies = [ "gix-quote", "gix-trace", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", "unicode-bom", ] @@ -2636,7 +2636,7 @@ dependencies = [ "gix-sec", "gix-utils", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", "unicode-bom", ] @@ -2650,7 +2650,7 @@ dependencies = [ "bstr", "gix-path", "libc", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2669,7 +2669,7 @@ dependencies = [ "gix-sec", "gix-trace", "gix-url", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2693,7 +2693,7 @@ dependencies = [ "bstr", "gix-hash", "gix-object", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2708,7 +2708,7 @@ dependencies = [ "gix-path", "gix-ref", "gix-sec", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2757,7 +2757,7 @@ dependencies = [ "gix-trace", "gix-utils", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2770,7 +2770,7 @@ dependencies = [ "gix-features", "gix-path", "gix-utils", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2794,7 +2794,7 @@ dependencies = [ "faster-hex", "gix-features", "sha1-checked", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2846,7 +2846,7 @@ dependencies = [ "memmap2", "rustix 1.1.4", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2857,7 +2857,7 @@ checksum = "d4c69157820343bf1c6e4b88b9808e920900de02e18aaf5862b30ada43814848" dependencies = [ "gix-tempfile", "gix-utils", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2890,7 +2890,7 @@ dependencies = [ "gix-validate", "itoa", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2912,7 +2912,7 @@ dependencies = [ "memmap2", "parking_lot", "tempfile", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2934,7 +2934,7 @@ dependencies = [ "memmap2", "parking_lot", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2946,7 +2946,7 @@ dependencies = [ "bstr", "faster-hex", "gix-trace", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2958,7 +2958,7 @@ dependencies = [ "bstr", "gix-trace", "gix-validate", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2973,7 +2973,7 @@ dependencies = [ "gix-config-value", "gix-glob", "gix-path", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -2986,7 +2986,7 @@ dependencies = [ "gix-config-value", "parking_lot", "rustix 1.1.4", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3012,7 +3012,7 @@ dependencies = [ "gix-transport", "gix-utils", "nonempty", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3043,7 +3043,7 @@ dependencies = [ "gix-utils", "gix-validate", "memmap2", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3059,7 +3059,7 @@ dependencies = [ "gix-revision", "gix-validate", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3094,7 +3094,7 @@ dependencies = [ "gix-hashtable", "gix-object", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3119,7 +3119,7 @@ dependencies = [ "gix-hash", "gix-lock", "nonempty", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3134,7 +3134,7 @@ dependencies = [ "gix-pathspec", "gix-refspec", "gix-url", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3173,7 +3173,7 @@ dependencies = [ "gix-url", "parking_lot", "reqwest", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3190,7 +3190,7 @@ dependencies = [ "gix-object", "gix-revwalk", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3203,7 +3203,7 @@ dependencies = [ "gix-path", "gix-utils", "percent-encoding", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3260,7 +3260,7 @@ dependencies = [ "gix-path", "gix-worktree", "io-close", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -3287,7 +3287,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e8813f5579b3075ff9c90f7c59cd2b62b4ebb639361f0911648b22d7446cc7c" dependencies = [ - "thiserror 2.0.19", + "thiserror 2.0.20", "zlib-rs", ] @@ -3375,7 +3375,7 @@ dependencies = [ "hashbrown 0.16.1", "log", "presser", - "thiserror 2.0.19", + "thiserror 2.0.20", "windows 0.62.2", ] @@ -3402,7 +3402,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "accesskit", "anyhow", @@ -3468,7 +3468,7 @@ dependencies = [ "strum", "sum_tree", "taffy", - "thiserror 2.0.19", + "thiserror 2.0.20", "tracing", "ttf-parser", "url", @@ -3486,7 +3486,7 @@ dependencies = [ [[package]] name = "gpui-component" version = "0.5.2" -source = "git+https://github.com/longbridge/gpui-component#f96f1576ee704b48476f6782962690e0feb7985b" +source = "git+https://github.com/longbridge/gpui-component#6e3f241136ed9a60d0f38d53ac3bdf285a3a13f7" dependencies = [ "aho-corasick", "anyhow", @@ -3569,7 +3569,7 @@ dependencies = [ [[package]] name = "gpui-component-assets" version = "0.5.1" -source = "git+https://github.com/longbridge/gpui-component#f96f1576ee704b48476f6782962690e0feb7985b" +source = "git+https://github.com/longbridge/gpui-component#6e3f241136ed9a60d0f38d53ac3bdf285a3a13f7" dependencies = [ "anyhow", "gpui", @@ -3583,7 +3583,7 @@ dependencies = [ [[package]] name = "gpui-component-macros" version = "0.5.1" -source = "git+https://github.com/longbridge/gpui-component#f96f1576ee704b48476f6782962690e0feb7985b" +source = "git+https://github.com/longbridge/gpui-component#6e3f241136ed9a60d0f38d53ac3bdf285a3a13f7" dependencies = [ "proc-macro2", "quote", @@ -3593,7 +3593,7 @@ dependencies = [ [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "accesskit", "accesskit_unix", @@ -3645,7 +3645,7 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "accesskit", "accesskit_macos", @@ -3694,7 +3694,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3705,7 +3705,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "console_error_panic_hook", "gpui", @@ -3718,7 +3718,7 @@ dependencies = [ [[package]] name = "gpui_shared_string" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "schemars", "serde", @@ -3728,7 +3728,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "anyhow", "log", @@ -3738,7 +3738,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "anyhow", "console_error_panic_hook", @@ -3761,7 +3761,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "anyhow", "bytemuck", @@ -3791,7 +3791,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "accesskit", "accesskit_windows", @@ -4110,7 +4110,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "anyhow", "async-compression", @@ -4130,7 +4130,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "rustls", "rustls-platform-verifier 0.5.3", @@ -4649,7 +4649,7 @@ dependencies = [ "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror 2.0.19", + "thiserror 2.0.20", "walkdir", "windows-link 0.2.1", ] @@ -4707,9 +4707,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", @@ -4858,15 +4858,15 @@ checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" [[package]] name = "link-section" -version = "0.19.2" +version = "0.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee1a0d6e252afe82e7bc2db42fba60e02ddf3b1accaf8cb21d96e34ba61f3d4" +checksum = "39c29a617ce3df32c08497bdc1ab6e2376e0b17948ac166a2fbe5977c5954cd9" [[package]] name = "linktime-proc-macro" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348d0075b1fc163b26d72a7f75fc5141daf2fd1bdf128d873cbaf6785d495bdf" +checksum = "7e57c38c1e860fd37c604281cdfb1dd2216977fd76a50f85ba2f388ef3219616" [[package]] name = "linux-raw-sys" @@ -5105,7 +5105,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "anyhow", "bindgen", @@ -5232,7 +5232,7 @@ dependencies = [ "once_cell", "rustc-hash 1.1.0", "spirv", - "thiserror 2.0.19", + "thiserror 2.0.20", "unicode-ident", ] @@ -5566,9 +5566,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -6153,7 +6153,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "collections", "serde", @@ -6378,9 +6378,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" @@ -6463,28 +6463,6 @@ dependencies = [ "toml_edit 0.25.13+spec-1.1.0", ] -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "proc-macro2" version = "1.0.107" @@ -6575,15 +6553,6 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" -[[package]] -name = "quick-xml" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" -dependencies = [ - "memchr", -] - [[package]] name = "quick-xml" version = "0.41.0" @@ -6607,7 +6576,7 @@ dependencies = [ "rustc-hash 2.1.3", "rustls", "socket2", - "thiserror 2.0.19", + "thiserror 2.0.20", "tokio", "tracing", "web-time", @@ -6630,7 +6599,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.19", + "thiserror 2.0.20", "tinyvec", "tracing", "web-time", @@ -6798,7 +6767,7 @@ dependencies = [ "rand 0.9.5", "rand_chacha 0.9.0", "simd_helpers", - "thiserror 2.0.19", + "thiserror 2.0.20", "v_frame", "wasm-bindgen", ] @@ -6910,7 +6879,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.17", "libredox", - "thiserror 2.0.19", + "thiserror 2.0.20", ] [[package]] @@ -6936,7 +6905,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "derive_refineable", ] @@ -7019,7 +6988,7 @@ dependencies = [ [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "anyhow", "bytes", @@ -7349,9 +7318,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" dependencies = [ "aws-lc-rs", "ring", @@ -7420,7 +7389,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "async-task", "backtrace", @@ -8042,9 +8011,9 @@ dependencies = [ [[package]] name = "stacksafe" -version = "0.1.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d9c1172965d317e87ddb6d364a040d958b40a1db82b6ef97da26253a8b3d090" +checksum = "95f9c34983ac74195c710c473db6fdf1085f64a47dbaa0090d1bea03be70da66" dependencies = [ "stacker", "stacksafe-macro", @@ -8052,13 +8021,13 @@ dependencies = [ [[package]] name = "stacksafe-macro" -version = "0.1.4" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172175341049678163e979d9107ca3508046d4d2a7c6682bee46ac541b17db69" +checksum = "6feeae42a2d6b0dcb8aeb2f08d9e48cdac600239cf8a20fc59f9e252e86bdfe1" dependencies = [ - "proc-macro-error2", + "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", ] [[package]] @@ -8143,7 +8112,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "heapless 0.9.3", "log", @@ -8393,7 +8362,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade" dependencies = [ - "thiserror 2.0.19", + "thiserror 2.0.20", "windows 0.61.3", "windows-version", ] @@ -8405,7 +8374,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix 1.1.4", "windows-sys 0.61.2", @@ -8442,11 +8411,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ - "thiserror-impl 2.0.19", + "thiserror-impl 2.0.20", ] [[package]] @@ -8462,9 +8431,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", @@ -9268,7 +9237,7 @@ dependencies = [ "rustls", "rustls-pki-types", "sha1", - "thiserror 2.0.19", + "thiserror 2.0.20", "utf-8", ] @@ -9492,7 +9461,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "perf", "quote", @@ -9640,9 +9609,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -9653,9 +9622,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ "js-sys", "wasm-bindgen", @@ -9663,9 +9632,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -9673,9 +9642,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -9686,9 +9655,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] @@ -9799,7 +9768,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" dependencies = [ "proc-macro2", - "quick-xml 0.41.0", + "quick-xml", "quote", ] @@ -9817,9 +9786,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -9947,7 +9916,7 @@ dependencies = [ "raw-window-handle", "rustc-hash 1.1.0", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", "wgpu-core-deps-apple", "wgpu-core-deps-emscripten", "wgpu-core-deps-wasm", @@ -10036,7 +10005,7 @@ dependencies = [ "raw-window-metal", "renderdoc-sys", "smallvec", - "thiserror 2.0.19", + "thiserror 2.0.20", "wasm-bindgen", "wayland-sys", "web-sys", @@ -10167,7 +10136,7 @@ checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" dependencies = [ "parking_lot", "rayon", - "thiserror 2.0.19", + "thiserror 2.0.20", "windows 0.61.3", "windows-future 0.2.1", ] @@ -10822,13 +10791,13 @@ checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" [[package]] name = "xcb" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4c580d8205abb0a5cf4eb7e927bd664e425b6c3263f9c5310583da96970cf6" +checksum = "a6c2ad15e0e922856ee89afe862b8992334bbe7953adad56cd1199358cb30566" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.13.1", "libc", - "quick-xml 0.30.0", + "quick-xml", "x11", ] @@ -10943,9 +10912,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.18.0" +version = "5.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a" +checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907" dependencies = [ "async-broadcast", "async-executor", @@ -11002,14 +10971,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.18.0" +version = "5.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" +checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", "zbus_names", "zvariant", "zvariant_utils", @@ -11038,6 +11007,15 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zcheapstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473" +dependencies = [ + "serde", +] + [[package]] name = "zed-font-kit" version = "0.14.1-zed" @@ -11266,7 +11244,7 @@ checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "anyhow", "chrono", @@ -11283,7 +11261,7 @@ checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" dependencies = [ "tracing", "tracing-subscriber", @@ -11294,7 +11272,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#38ca9106c5306ef93e52c35643df015a27f15b72" +source = "git+https://github.com/zed-industries/zed#6bd93fc3195242834f4999f3b3daab294df6b253" [[package]] name = "zune-core" @@ -11337,41 +11315,42 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.13.1" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" +checksum = "b5e28c25bd8bb8da5a1f3e7065d0c156b9ee9a7973adf78b0e35eaefdf3b1b5c" dependencies = [ "endi", "enumflags2", "serde", "serde_bytes", "winnow 1.0.4", + "zcheapstr", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.13.1" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" +checksum = "d496a145685283b67e232bd9e47377f6b60ad9d51e3601b23867f77c42477f96" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.3", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.5.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +checksum = "629d80ece222cad20fe0e8741be493c4ab166acf3b85341bdc2cdbcfd8f3c2d6" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.119", + "syn 3.0.3", "winnow 1.0.4", ] diff --git a/crates/assets/assets/icons/git-branch.svg b/crates/assets/assets/icons/git-branch.svg new file mode 100644 index 0000000..d021e7f --- /dev/null +++ b/crates/assets/assets/icons/git-branch.svg @@ -0,0 +1 @@ + diff --git a/crates/assets/assets/icons/tag.svg b/crates/assets/assets/icons/tag.svg new file mode 100644 index 0000000..429e28f --- /dev/null +++ b/crates/assets/assets/icons/tag.svg @@ -0,0 +1 @@ + diff --git a/crates/assets/src/lib.rs b/crates/assets/src/lib.rs index b0d1fb8..75fa882 100644 --- a/crates/assets/src/lib.rs +++ b/crates/assets/src/lib.rs @@ -53,6 +53,8 @@ pub enum CustomIconName { GlobalOn, GlobalOff, GitClone, + GitBranch, + Tag, } impl IconNamed for CustomIconName { @@ -63,6 +65,8 @@ impl IconNamed for CustomIconName { CustomIconName::GlobalOn => "icons/global-on.svg", CustomIconName::GlobalOff => "icons/global-off.svg", CustomIconName::GitClone => "icons/git-clone.svg", + CustomIconName::GitBranch => "icons/git-branch.svg", + CustomIconName::Tag => "icons/tag.svg", } .into() } diff --git a/crates/signed_git/src/lib.rs b/crates/signed_git/src/lib.rs index d9e0435..454a09c 100644 --- a/crates/signed_git/src/lib.rs +++ b/crates/signed_git/src/lib.rs @@ -309,6 +309,104 @@ pub fn worktree_all_commits(workdir: &Path) -> Result> { all_commits(&gix::open(workdir)?) } +/// Short names of local branches (`refs/heads/*`), sorted alphabetically. +pub fn worktree_branches(workdir: &Path) -> Result> { + let repo = gix::open(workdir)?; + let mut names = Vec::new(); + for reference in repo.references()?.local_branches()? { + let reference = reference.map_err(|error| anyhow::anyhow!("{error}"))?; + names.push(String::from_utf8_lossy(reference.name().shorten()).into_owned()); + } + names.sort(); + Ok(names) +} + +/// Short names of tags (`refs/tags/*`), sorted alphabetically. +pub fn worktree_tags(workdir: &Path) -> Result> { + let repo = gix::open(workdir)?; + let mut names = Vec::new(); + for reference in repo.references()?.tags()? { + let reference = reference.map_err(|error| anyhow::anyhow!("{error}"))?; + names.push(String::from_utf8_lossy(reference.name().shorten()).into_owned()); + } + names.sort(); + Ok(names) +} + +/// Short name of the branch HEAD points to, or `None` when detached (e.g. +/// after checking out a tag or a commit directly). +pub fn current_branch(repo: &gix::Repository) -> Result> { + let head = repo.head()?; + let Some(name) = head.referent_name() else { + return Ok(None); + }; + Ok(Some(String::from_utf8_lossy(name.shorten()).into_owned())) +} + +/// Everything the browser needs to refresh after a branch or tag switch. +pub struct WorktreeSnapshot { + /// Relative paths of all worktree entries, directories first. + pub entries: Vec, + /// README path relative to the worktree, if any. + pub readme_path: Option, + /// Contents of the README, if any. + pub readme: Option>, + /// Branch HEAD points to (`None` when detached, e.g. on a tag). + pub current_branch: Option, +} + +/// Snapshot the worktree after a branch/tag switch: entries, README and the +/// branch HEAD points to, opening the repository once. +pub fn worktree_snapshot(workdir: &Path) -> Result { + let repo = gix::open(workdir)?; + let readme_path = find_readme(&repo)?; + let readme = match &readme_path { + Some(path) => worktree_read(&repo, path)?, + None => None, + }; + Ok(WorktreeSnapshot { + entries: worktree_entries(&repo)?, + readme_path, + readme, + current_branch: current_branch(&repo)?, + }) +} + +/// Switch the checked-out ref and update the worktree to match, like +/// `git checkout --force`. Local modifications are discarded since these +/// clones are read-only browser copies. +fn checkout(workdir: &Path, args: &[&str]) -> Result<()> { + let output = Command::new("git") + .arg("checkout") + .arg("--force") + .args(args) + .current_dir(workdir) + .output() + .context("failed to spawn `git checkout`")?; + if !output.status.success() { + bail!( + "git checkout {} failed: {}", + args.join(" "), + String::from_utf8_lossy(&output.stderr) + ); + } + Ok(()) +} + +/// Check out the local branch `name`; HEAD stays attached to it. +pub fn worktree_checkout_branch(workdir: &Path, name: &str) -> Result<()> { + // The short name (not `refs/heads/`) keeps HEAD attached; the + // full ref name would be treated as a commit-ish and detach it. + checkout(workdir, &[name]) +} + +/// Check out the tag `name`; HEAD becomes detached at the tagged commit, +/// which [`current_branch`] reports as `None`. +pub fn worktree_checkout_tag(workdir: &Path, name: &str) -> Result<()> { + // `--detach` pins the full tag ref so HEAD always ends up detached. + checkout(workdir, &["--detach", &format!("refs/tags/{name}")]) +} + fn collect_entries(root: &Path, dir: &Path, out: &mut Vec<(PathBuf, bool)>) -> Result<()> { for entry in std::fs::read_dir(dir)? { let entry = entry?; @@ -433,22 +531,23 @@ mod tests { /// Stage everything and create a commit with the git CLI (like /// [`apply_patch`], the crate already shells out to the CLI). fn commit_all(repo: &gix::Repository, message: &str) { - let dir = repo.workdir().expect("workdir"); - let run = |args: &[&str]| { - let status = Command::new("git") - .current_dir(dir) - .env("GIT_AUTHOR_NAME", "Test Author") - .env("GIT_AUTHOR_EMAIL", "test@example.com") - .env("GIT_COMMITTER_NAME", "Test Author") - .env("GIT_COMMITTER_EMAIL", "test@example.com") - .env("GIT_EDITOR", "true") - .args(args) - .status() - .expect("spawn git"); - assert!(status.success(), "git {args:?} failed"); - }; - run(&["add", "-A"]); - run(&["commit", "-m", message]); + git_run(repo.workdir().expect("workdir"), &["add", "-A"]); + git_run(repo.workdir().expect("workdir"), &["commit", "-m", message]); + } + + /// Run a git command in `dir`, asserting success. + fn git_run(dir: &Path, args: &[&str]) { + let status = Command::new("git") + .current_dir(dir) + .env("GIT_AUTHOR_NAME", "Test Author") + .env("GIT_AUTHOR_EMAIL", "test@example.com") + .env("GIT_COMMITTER_NAME", "Test Author") + .env("GIT_COMMITTER_EMAIL", "test@example.com") + .env("GIT_EDITOR", "true") + .args(args) + .status() + .expect("spawn git"); + assert!(status.success(), "git {args:?} failed"); } #[test] @@ -572,4 +671,107 @@ mod tests { let (_dir, repo) = fixture(&[("main.rs", b"")]); assert!(find_readme(&repo).expect("find").is_none()); } + + #[test] + fn worktree_branches_and_tags_list_short_names() { + let (dir, repo) = fixture(&[("a.txt", b"one")]); + commit_all(&repo, "initial"); + let dir = dir.path(); + + git_run(dir, &["checkout", "-b", "feature"]); + git_run(dir, &["tag", "v0.9"]); + git_run(dir, &["tag", "v1.0"]); + + // The initial branch name depends on git configuration; only the + // branch we created is fixed. + let branches = worktree_branches(dir).expect("branches"); + assert_eq!(branches.len(), 2); + assert!(branches.contains(&"feature".to_string())); + assert!(branches.windows(2).all(|pair| pair[0] <= pair[1]), "sorted"); + + assert_eq!( + worktree_tags(dir).expect("tags"), + vec!["v0.9".to_string(), "v1.0".to_string()] + ); + } + + #[test] + fn current_branch_tracks_checkout() { + let (dir, repo) = fixture(&[("a.txt", b"one")]); + commit_all(&repo, "initial"); + let dir = dir.path(); + + let default = worktree_branches(dir) + .expect("branches") + .into_iter() + .next() + .expect("default branch"); + assert_eq!( + current_branch(&repo).expect("branch").as_deref(), + Some(default.as_str()) + ); + + git_run(dir, &["checkout", "-b", "feature"]); + assert_eq!( + current_branch(&repo).expect("branch").as_deref(), + Some("feature") + ); + + // Tags detach HEAD. + git_run(dir, &["tag", "v1.0"]); + worktree_checkout_tag(dir, "v1.0").expect("checkout tag"); + assert_eq!(current_branch(&repo).expect("branch"), None); + + // Branches re-attach HEAD. + worktree_checkout_branch(dir, &default).expect("checkout branch"); + assert_eq!( + current_branch(&repo).expect("branch").as_deref(), + Some(default.as_str()) + ); + } + + #[test] + fn worktree_snapshot_reflects_checked_out_ref() { + let (dir, repo) = fixture(&[("README.md", b"# main"), ("a.txt", b"one")]); + commit_all(&repo, "initial"); + let dir = dir.path(); + + git_run(dir, &["checkout", "-b", "feature"]); + std::fs::write(dir.join("README.md"), b"# feature").expect("write"); + std::fs::write(dir.join("b.txt"), b"b").expect("write"); + commit_all(&repo, "feature work"); + + let snapshot = worktree_snapshot(dir).expect("snapshot"); + assert_eq!(snapshot.current_branch.as_deref(), Some("feature")); + assert_eq!( + String::from_utf8(snapshot.readme.expect("readme")).expect("utf8"), + "# feature" + ); + let entries: Vec = snapshot + .entries + .iter() + .map(|p| p.to_string_lossy().into_owned()) + .collect(); + assert!(entries.contains(&"b.txt".to_string())); + + let default = worktree_branches(dir) + .expect("branches") + .into_iter() + .find(|name| name != "feature") + .expect("default branch"); + worktree_checkout_branch(dir, &default).expect("checkout"); + + let snapshot = worktree_snapshot(dir).expect("snapshot"); + assert_eq!(snapshot.current_branch.as_deref(), Some(default.as_str())); + assert_eq!( + String::from_utf8(snapshot.readme.expect("readme")).expect("utf8"), + "# main" + ); + assert!( + !snapshot + .entries + .iter() + .any(|p| p.to_string_lossy() == "b.txt") + ); + } } diff --git a/crates/workspace/src/views/repo_detail/mod.rs b/crates/workspace/src/views/repo_detail/mod.rs index 59a6e61..3e5d130 100644 --- a/crates/workspace/src/views/repo_detail/mod.rs +++ b/crates/workspace/src/views/repo_detail/mod.rs @@ -6,17 +6,19 @@ use anyhow::Error; use assets::CustomIconName; use gpui::prelude::*; use gpui::{ - App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render, - SharedString, Size, Task, Window, div, px, size, + AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, + Render, SharedString, Size, Subscription, Task, Window, div, px, size, }; use gpui_component::button::{Button, ButtonVariants, DropdownButton}; +use gpui_component::combobox::{Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerCtx}; use gpui_component::dock::{Panel, PanelEvent}; use gpui_component::menu::PopupMenuItem; +use gpui_component::searchable_list::SearchableVec; use gpui_component::tab::{Tab, TabBar}; use gpui_component::tag::Tag; use gpui_component::tree::TreeState; use gpui_component::{ - ActiveTheme, IconName, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex, + ActiveTheme, Icon, IconName, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex, }; use signed_core::Announcement; use signed_git::FileCommit; @@ -30,6 +32,15 @@ use browser::{CodeView, FileContent, MAX_PREVIEW_BYTES, MarkdownView}; use commits::COMMIT_ROW_HEIGHT; use helpers::{build_tree_items, is_markdown_path}; +/// What kind of ref the header selectors switch to. +#[derive(Clone, Copy, PartialEq, Eq)] +enum RefKind { + /// A local branch (`refs/heads/*`); HEAD stays attached. + Branch, + /// A tag (`refs/tags/*`); HEAD becomes detached. + Tag, +} + /// Detail view of a repository: header, stats, a file explorer with README /// preview (cloned from the announcement's `clone` URLs), and metadata. pub struct RepoDetailView { @@ -68,6 +79,17 @@ pub struct RepoDetailView { /// A clone/fetch is in flight. loading: bool, error: Option, + /// Branch selector (header): local branches, searchable. + branch_select: Entity>>, + /// Tag selector (header): tags, searchable. + tag_select: Entity>>, + /// A branch/tag switch is in flight (checkout plus explorer reload). + switching_ref: bool, + /// Bumped on every branch/tag switch; in-flight loads tagged with an + /// older generation are discarded when they complete. + ref_generation: u64, + /// Subscriptions keeping the selectors' confirm events alive. + _subscriptions: Vec, focus_handle: FocusHandle, tasks: Vec>>, } @@ -77,9 +99,49 @@ impl RepoDetailView { let store = cx.new(|cx| RepoStore::new(initial.addr(), cx)); let tree_state = cx.new(|cx| TreeState::new(cx)); + // Empty until the clone completes; populated with the local refs. + let branch_select: Entity>> = cx.new(|cx| { + ComboboxState::new( + SearchableVec::new(Vec::::new()), + Vec::new(), + window, + cx, + ) + .searchable(true) + }); + let tag_select: Entity>> = cx.new(|cx| { + ComboboxState::new( + SearchableVec::new(Vec::::new()), + Vec::new(), + window, + cx, + ) + .searchable(true) + }); + + let subscriptions = vec![ + cx.subscribe_in(&branch_select, window, |this, _state, event, window, cx| { + // `Change` fires only when the selection actually changed + // (picking the already-selected branch emits nothing), so a + // confirmed value always means a switch. + if let ComboboxEvent::Change(values) = event + && let Some(name) = values.first() + { + this.switch_ref(RefKind::Branch, name.clone(), window, cx); + } + }), + cx.subscribe_in(&tag_select, window, |this, _state, event, window, cx| { + if let ComboboxEvent::Change(values) = event + && let Some(name) = values.first() + { + this.switch_ref(RefKind::Tag, name.clone(), window, cx); + } + }), + ]; + // Defer loading the repository until the window is ready. - cx.defer_in(window, |this, _window, cx| { - this.load_repo(cx); + cx.defer_in(window, |this, window, cx| { + this.load_repo(window, cx); }); Self { @@ -102,13 +164,18 @@ impl RepoDetailView { item_sizes: Rc::new(Vec::new()), loading: true, error: None, + branch_select, + tag_select, + switching_ref: false, + ref_generation: 0, + _subscriptions: subscriptions, focus_handle: cx.focus_handle(), tasks: Vec::new(), } } /// Clone (or fetch) the repository and populate the file explorer. - fn load_repo(&mut self, cx: &mut Context) { + fn load_repo(&mut self, window: &mut Window, cx: &mut Context) { self.loading = true; self.error = None; cx.notify(); @@ -126,20 +193,63 @@ impl RepoDetailView { None => None, }; let worktree = repo.workdir().map(Path::to_path_buf); + // Ref listing is auxiliary UI: a broken ref must not prevent the + // explorer from loading, so failures degrade to empty selectors. + let (branches, tags, current_branch) = match &worktree { + Some(worktree) => ( + signed_git::worktree_branches(worktree).unwrap_or_default(), + signed_git::worktree_tags(worktree).unwrap_or_default(), + signed_git::current_branch(&repo).unwrap_or(None), + ), + None => (Vec::new(), Vec::new(), None), + }; - Ok::<_, Error>((entries, readme_path, readme, worktree)) + Ok::<_, Error>(( + entries, + readme_path, + readme, + worktree, + branches, + tags, + current_branch, + )) }); - let task = cx.spawn(async move |this, cx| { + let task = cx.spawn_in(window, async move |this, cx| { let result = load.await; - this.update(cx, |this, cx| { + this.update_in(cx, |this, window, cx| { match result { - Ok((entries, readme_path, readme, Some(worktree))) => { + Ok(( + entries, + readme_path, + readme, + Some(worktree), + branches, + tags, + current_branch, + )) => { this.worktree = Some(worktree); this.tree_state.update(cx, |state, cx| { state.set_items(build_tree_items(&entries), cx); }); + + // Populate the branch/tag selectors with the local + // refs, selecting the branch HEAD points to. + let branches: Vec = + branches.into_iter().map(Into::into).collect(); + let tags: Vec = tags.into_iter().map(Into::into).collect(); + this.branch_select.update(cx, |state, cx| { + state.set_items(SearchableVec::from(branches), window, cx); + if let Some(branch) = current_branch { + let branch: SharedString = branch.into(); + state.set_selected_values(&[branch], window, cx); + } + }); + this.tag_select.update(cx, |state, cx| { + state.set_items(SearchableVec::from(tags), window, cx); + }); + this.load_all_commits(cx); if let Some((path, bytes)) = readme_path.zip(readme) { this.readme_name = Some(path.to_string_lossy().into()); @@ -149,7 +259,7 @@ impl RepoDetailView { } } } - Ok((_, _, _, None)) => { + Ok((_, _, _, None, _, _, _)) => { this.error = Some("Repository has no worktree".into()); } Err(error) => { @@ -197,6 +307,7 @@ impl RepoDetailView { self.loading_files.insert(path.to_string()); let path = path.to_string(); self.load_commit(&path, cx); + let generation = self.ref_generation; let task = cx.spawn(async move |this, cx| { let path_for_read = path.clone(); @@ -221,6 +332,11 @@ impl RepoDetailView { .await; this.update(cx, |this, cx| { + // The worktree was switched while this file was reading; + // the result belongs to the previous branch. + if generation != this.ref_generation { + return; + } this.loading_files.remove(&path); match content { Ok(kind) => { @@ -267,6 +383,7 @@ impl RepoDetailView { self.loading_commits.insert(path.to_string()); let path = path.to_string(); + let generation = self.ref_generation; let task = cx.spawn(async move |this, cx| { let path_for_query = path.clone(); @@ -277,6 +394,9 @@ impl RepoDetailView { .await; this.update(cx, |this, cx| { + if generation != this.ref_generation { + return; + } this.loading_commits.remove(&path); if let Ok(Some(commit)) = result { this.commits.insert(path, commit); @@ -302,6 +422,7 @@ impl RepoDetailView { }; self.loading_all_commits = true; + let generation = self.ref_generation; let task = cx.spawn(async move |this, cx| { let result = cx @@ -309,6 +430,9 @@ impl RepoDetailView { .await; this.update(cx, |this, cx| { + if generation != this.ref_generation { + return; + } if let Ok(commits) = result { let count = commits.len(); this.item_sizes = Rc::new(vec![size(px(0.), px(COMMIT_ROW_HEIGHT)); count]); @@ -323,6 +447,192 @@ impl RepoDetailView { self.tasks.push(task); } + + /// Check out `name` (a branch or tag picked in the header) and refresh + /// the explorer once the switch completes. + fn switch_ref( + &mut self, + kind: RefKind, + name: SharedString, + window: &mut Window, + cx: &mut Context, + ) { + if self.switching_ref { + return; + } + let Some(worktree) = self.worktree.clone() else { + return; + }; + + // Branches and tags are mutually exclusive states of HEAD: selecting + // one clears the other selector. Remember the previous selections so + // they can be restored if the checkout fails. + let previous_branch = self.branch_select.read(cx).selected_value(); + let previous_tag = self.tag_select.read(cx).selected_value(); + + match kind { + RefKind::Branch => { + self.tag_select + .update(cx, |state, cx| state.clear_selection(cx)); + } + RefKind::Tag => { + self.branch_select + .update(cx, |state, cx| state.clear_selection(cx)); + } + } + self.switching_ref = true; + // In-flight loads of the previous branch are discarded when they + // complete. + self.ref_generation += 1; + cx.notify(); + + let checkout_name = name.clone(); + let task = cx.spawn_in(window, async move |this, cx| { + let result = cx + .background_spawn(async move { + match kind { + RefKind::Branch => { + signed_git::worktree_checkout_branch(&worktree, &checkout_name) + } + RefKind::Tag => { + signed_git::worktree_checkout_tag(&worktree, &checkout_name) + } + } + }) + .await; + + this.update_in(cx, |this, window, cx| { + match result { + Ok(()) => this.reload_worktree(cx), + Err(error) => { + this.error = Some(format!("Failed to check out {name}: {error}").into()); + this.switching_ref = false; + this.restore_selection(&this.branch_select, &previous_branch, window, cx); + this.restore_selection(&this.tag_select, &previous_tag, window, cx); + } + } + cx.notify(); + })?; + + Ok(()) + }); + + self.tasks.push(task); + } + + /// Restore a selector to `previous`, or clear it (after a failed switch). + fn restore_selection( + &self, + select: &Entity>>, + previous: &Option, + window: &mut Window, + cx: &mut Context, + ) { + select.update(cx, |state, cx| match previous { + Some(value) => state.set_selected_values(std::slice::from_ref(value), window, cx), + None => state.clear_selection(cx), + }); + } + + /// Trigger body for the branch/tag selectors: the kind icon, the + /// selection (or placeholder) and the caret. `Combobox` replaces its + /// default trigger entirely, which is the only way to show an icon + /// inside the trigger label. + fn render_ref_trigger( + ctx: &ComboboxTriggerCtx>, + icon: CustomIconName, + cx: &App, + ) -> AnyElement { + let muted = cx.theme().muted_foreground; + + h_flex() + .w_full() + .min_w_0() + .gap_1() + .items_center() + .child(Icon::new(icon).small().flex_shrink_0()) + .child( + div() + .flex_1() + .min_w_0() + .overflow_hidden() + .text_ellipsis() + .whitespace_nowrap() + .when(ctx.selection.is_empty(), |this| this.text_color(muted)) + .child( + ctx.selection + .first() + .map(|(_, item)| item.clone()) + .or_else(|| ctx.placeholder.cloned()) + .unwrap_or_default(), + ), + ) + .child(Caret::new(ctx.size).text_color(muted)) + .into_any_element() + } + + /// Refresh the file explorer, preview pane and commit list after a + /// successful branch or tag switch. The selectors were already updated + /// by [`Self::switch_ref`]; [`Self::switching_ref`] stays set until this + /// reload finishes, so a second switch cannot interleave. + fn reload_worktree(&mut self, cx: &mut Context) { + let Some(worktree) = self.worktree.clone() else { + return; + }; + + let task = cx.spawn(async move |this, cx| { + let result = cx + .background_spawn(async move { signed_git::worktree_snapshot(&worktree) }) + .await; + + this.update(cx, |this, cx| { + this.switching_ref = false; + match result { + Ok(snapshot) => { + // Rebuild the tree from scratch: entries of the + // previous branch are gone, and with them the + // expansion state. + this.tree_state.update(cx, |state, cx| { + state.set_items(build_tree_items(&snapshot.entries), cx); + }); + + // Drop cached previews and commits of the old branch. + this.selected_file = None; + this.files.clear(); + this.loading_files.clear(); + this.commits.clear(); + this.loading_commits.clear(); + this.md = None; + this.code = None; + this.readme_name = None; + this.all_commits = None; + this.loading_all_commits = false; + + if let Some((path, bytes)) = snapshot.readme_path.zip(snapshot.readme) { + this.readme_name = Some(path.to_string_lossy().into()); + this.load_commit(&path.to_string_lossy(), cx); + if let Ok(text) = String::from_utf8(bytes) { + this.set_markdown(None, &text, cx); + } + } + this.load_all_commits(cx); + } + Err(error) => { + this.error = Some(error.to_string().into()); + // The tree may show files that no longer exist. + this.tree_state.update(cx, |state, cx| { + state.set_items(Vec::new(), cx); + }); + } + } + cx.notify(); + })?; + + Ok(()) + }); + + self.tasks.push(task); + } } impl Panel for RepoDetailView { @@ -384,6 +694,7 @@ impl Render for RepoDetailView { let relays = announcement.relays.clone(); let web = announcement.web.clone(); let commits_count = self.all_commits.as_ref().map(Vec::len); + let worktree_empty = self.switching_ref || self.worktree.is_none(); v_flex() .id("repo") @@ -394,7 +705,7 @@ impl Render for RepoDetailView { .pt_2() .pb_2() .w_full() - .gap_4() + .gap_8() .border_b_1() .border_color(cx.theme().border) .child( @@ -497,6 +808,7 @@ impl Render for RepoDetailView { ) .child( h_flex() + .items_center() .child( TabBar::new("repo-tabs") .segmented() @@ -518,7 +830,48 @@ impl Render for RepoDetailView { cx.notify(); })), ) - .child(div().flex_1()), + .child( + h_flex() + .flex_1() + .gap_2() + .justify_end() + .child( + div().w(px(120.)).child( + Combobox::new(&self.branch_select) + .placeholder("Branch") + .appearance(false) + .menu_width(px(200.)) + .disabled(worktree_empty) + .bg(cx.theme().muted) + .rounded(cx.theme().radius) + .render_trigger(|ctx, _window, cx| { + Self::render_ref_trigger( + ctx, + CustomIconName::GitBranch, + cx, + ) + }), + ), + ) + .child( + div().w(px(120.)).child( + Combobox::new(&self.tag_select) + .placeholder("Tag") + .appearance(false) + .menu_width(px(200.)) + .disabled(worktree_empty) + .bg(cx.theme().muted) + .rounded(cx.theme().radius) + .render_trigger(|ctx, _window, cx| { + Self::render_ref_trigger( + ctx, + CustomIconName::Tag, + cx, + ) + }), + ), + ), + ), ), ) .child(match self.active_tab {