History

Merge branch 'fetch-pack'

Sebastian Thiel committed 4 years ago

Commit 3c494002 parents a0dd96b ef9fa98103 files · +3156 −1174Browse files

Unified · Split

103 files
  1. Cargo.lock +41 −0
  2. Cargo.toml +4 −0
  3. DEVELOPMENT.md +15 −0
  4. README.md +3 −0
  5. etc/check-package-size.sh +1 −1
  6. git-config/tests/file/init/from_paths/includes/conditional/onbranch.rs +1 −0
  7. git-pack/src/bundle/write/error.rs +2 −0
  8. git-pack/src/bundle/write/mod.rs +2 −2
  9. git-pack/src/bundle/write/types.rs +2 −2
  10. git-pack/src/index/write/error.rs +1 −1
  11. git-pack/src/index/write/mod.rs +4 −9
  12. git-pack/tests/pack/bundle.rs +2 −2
  13. git-pack/tests/pack/data/output/count_and_entries.rs +15 −0
  14. git-pack/tests/pack/index.rs +1 −1
  15. git-protocol/src/fetch/arguments/blocking_io.rs +2 −1
  16. git-protocol/src/fetch/arguments/mod.rs +11 −1
  17. git-protocol/src/fetch/command.rs +14 −4
  18. git-protocol/src/fetch/handshake.rs +1 −1
  19. git-protocol/src/fetch/refs/function.rs +2 −2
  20. git-protocol/src/fetch/refs/mod.rs +1 −1
  21. git-protocol/src/fetch/response/mod.rs +2 −1
  22. git-protocol/src/fetch/tests/arguments.rs +14 −0
  23. git-protocol/src/remote_progress.rs +1 −1
  24. git-ref/src/fullname.rs +9 −0
  25. git-ref/src/store/file/transaction/prepare.rs +8 −4
  26. git-ref/src/target.rs +1 −1
  27. git-ref/src/transaction/mod.rs +9 −1
  28. git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs +18 −3
  29. git-ref/tests/file/transaction/prepare_and_commit/delete.rs +13 −0
  30. git-ref/tests/file/worktree.rs +11 −5
  31. git-refspec/src/spec.rs +19 −3
  32. git-refspec/tests/refspec.rs +1 −0
  33. git-refspec/tests/spec/mod.rs +41 −0
  34. git-repository/examples/init-repo-and-commit.rs +26 −39
  35. git-repository/src/config/cache/access.rs +103 −0
  36. git-repository/src/config/cache/init.rs +31 −53
  37. git-repository/src/config/cache/mod.rs +1 −22
  38. git-repository/src/config/cache/util.rs +22 −0
  39. git-repository/src/config/mod.rs +21 −7
  40. git-repository/src/config/snapshot/_impls.rs +62 −0
  41. git-repository/src/config/snapshot/access.rs +58 −0
  42. git-repository/src/config/snapshot/apply_cli_overrides.rs +5 −2
  43. git-repository/src/config/snapshot/mod.rs +1 −41
  44. git-repository/src/head/mod.rs +8 −5
  45. git-repository/src/open.rs +1 −1
  46. git-repository/src/reference/edits.rs +5 −20
  47. git-repository/src/reference/errors.rs +2 −0
  48. git-repository/src/reference/remote.rs +2 −0
  49. git-repository/src/remote/connect.rs +21 −9
  50. git-repository/src/remote/connection/access.rs +48 −0
  51. git-repository/src/remote/connection/fetch/config.rs +66 −0
  52. git-repository/src/remote/connection/fetch/mod.rs +263 −0
  53. git-repository/src/remote/connection/fetch/negotiate.rs +60 −0
  54. git-repository/src/remote/connection/fetch/update_refs/mod.rs +149 −0
  55. git-repository/src/remote/connection/fetch/update_refs/tests.rs +263 −0
  56. git-repository/src/remote/connection/fetch/update_refs/update.rs +92 −0
  57. git-repository/src/remote/connection/mod.rs +8 −48
  58. git-repository/src/remote/connection/ref_map.rs +68 −9
  59. git-repository/src/remote/fetch.rs +59 −0
  60. git-repository/src/remote/mod.rs +1 −38
  61. git-repository/src/remote/url/mod.rs +2 −2
  62. git-repository/src/repository/config.rs +9 −2
  63. git-repository/src/repository/identity.rs +9 −2
  64. git-repository/src/repository/object.rs +24 −30
  65. git-repository/src/repository/reference.rs +28 −45
  66. git-repository/src/repository/remote.rs +11 −0
  67. git-repository/src/repository/worktree.rs +1 −11
  68. git-repository/src/worktree/mod.rs +3 −1
  69. git-repository/tests/fixtures/generated-archives/.gitignore +1 −0
  70. git-repository/tests/fixtures/make_fetch_repos.sh +30 −0
  71. git-repository/tests/fixtures/make_remote_repos.sh +4 −0
  72. git-repository/tests/head/mod.rs +11 −1
  73. git-repository/tests/remote/fetch.rs +123 −0
  74. git-repository/tests/remote/mod.rs +2 −1
  75. git-repository/tests/remote/ref_map.rs +2 −3
  76. git-repository/tests/repository/config/config_snapshot/mod.rs +44 −0
  77. git-repository/tests/repository/mod.rs +1 −1
  78. git-repository/tests/repository/object.rs +14 −34
  79. git-repository/tests/repository/remote.rs +19 −0
  80. git-repository/tests/util/mod.rs +22 −2
  81. git-transport/src/client/blocking_io/connect.rs +4 −4
  82. git-transport/src/client/blocking_io/file.rs +6 −0
  83. git-transport/src/client/blocking_io/http/curl/mod.rs +5 −0
  84. git-transport/src/client/blocking_io/http/mod.rs +9 −5
  85. git-transport/src/client/blocking_io/http/traits.rs +8 −0
  86. git-transport/src/client/capabilities.rs +1 −1
  87. git-transport/src/client/git/async_io.rs +5 −0
  88. git-transport/src/client/git/blocking_io.rs +6 −0
  89. git-transport/src/client/traits.rs +14 −0
  90. git-transport/tests/client/blocking_io/http/mock.rs +1 −1
  91. gitoxide-core/src/pack/index.rs +1 −1
  92. gitoxide-core/src/pack/receive.rs +3 −3
  93. gitoxide-core/src/repository/remote.rs +13 −1
  94. src/plumbing/main.rs +4 −2
  95. src/plumbing/mod.rs +4 −0
  96. src/plumbing/options.rs +0 −677
  97. src/plumbing/options/free.rs +368 −0
  98. src/plumbing/options/mod.rs +310 −0
  99. src/plumbing/progress.rs +305 −0
  100. tests/snapshots/plumbing/no-repo/pack/index/create/no-output-dir-as-json-success +1 −1
  101. tests/snapshots/plumbing/no-repo/pack/index/create/output-dir-restore-as-json-success +1 −1
  102. tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output-json +1 −1
  103. tests/tools/src/lib.rs +8 −0
Cargo.lock+41−0
@@ -297,6 +297,12 @@ dependencies = [ · 296 unmodified lines
297297 "utf8-width",
298298 ]
299299
300+[[package]]
301+name = "bytecount"
302+version = "0.6.3"
303+source = "registry+https://github.com/rust-lang/crates.io-index"
304+checksum = "2c676a478f63e9fa2dd5368a42f28bba0d6c560b775f38583c8bbaa7fcd67c9c"
305+
300306 [[package]]
301307 name = "bytes"
302308 version = "1.2.1"
@@ -954,6 +960,12 @@ dependencies = [ · 651 unmodified lines
954960 "miniz_oxide",
955961 ]
956962
963+[[package]]
964+name = "fnv"
965+version = "1.0.7"
966+source = "registry+https://github.com/rust-lang/crates.io-index"
967+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
968+
957969 [[package]]
958970 name = "form_urlencoded"
959971 version = "1.1.0"
@@ -1835,7 +1847,9 @@ dependencies = [ · 875 unmodified lines
18351847 "git-repository",
18361848 "git-transport",
18371849 "gitoxide-core",
1850+ "owo-colors",
18381851 "prodash",
1852+ "tabled",
18391853 ]
18401854
18411855 [[package]]
@@ -2380,6 +2394,23 @@ dependencies = [ · 538 unmodified lines
23802394 "winapi",
23812395 ]
23822396
2397+[[package]]
2398+name = "owo-colors"
2399+version = "3.5.0"
2400+source = "registry+https://github.com/rust-lang/crates.io-index"
2401+checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
2402+
2403+[[package]]
2404+name = "papergrid"
2405+version = "0.5.1"
2406+source = "registry+https://github.com/rust-lang/crates.io-index"
2407+checksum = "453cf71f2a37af495a1a124bf30d4d7469cfbea58e9f2479be9d222396a518a2"
2408+dependencies = [
2409+ "bytecount",
2410+ "fnv",
2411+ "unicode-width",
2412+]
2413+
23832414 [[package]]
23842415 name = "parking"
23852416 version = "2.0.0"
@@ -2926,6 +2957,16 @@ dependencies = [ · 540 unmodified lines
29262957 "unicode-ident",
29272958 ]
29282959
2960+[[package]]
2961+name = "tabled"
2962+version = "0.8.0"
2963+source = "registry+https://github.com/rust-lang/crates.io-index"
2964+checksum = "e5b2f8c37d26d87d2252187b0a45ea3cbf42baca10377c7e7eaaa2800fa9bf97"
2965+dependencies = [
2966+ "papergrid",
2967+ "unicode-width",
2968+]
2969+
29292970 [[package]]
29302971 name = "tar"
29312972 version = "0.4.38"
Cargo.toml+4−0
@@ -92,6 +92,10 @@ env_logger = { version = "0.9.0", default-features = false } · 91 unmodified lines
9292 crosstermion = { version = "0.10.1", optional = true, default-features = false }
9393 futures-lite = { version = "1.12.0", optional = true, default-features = false, features = ["std"] }
9494
95+# for progress
96+owo-colors = "3.5.0"
97+tabled = { version = "0.8.0", default-features = false }
98+
9599 document-features = { version = "0.2.0", optional = true }
96100
97101 [profile.dev.package]
DEVELOPMENT.md+15−0
@@ -18,6 +18,21 @@ · 17 unmodified lines
1818 * We `thiserror` generally.
1919 * Adhere to the [stability guide](https://github.com/Byron/gitoxide/blob/main/STABILITY.md)
2020
21+## Configuration and overrides
22+
23+As a general rule, respect and implement all applicable [git-config](https://git-scm.com/docs/git-config) by default, but allow the
24+caller to set overrides. How overrides work depends on the goals of the particular API so it can be done on the main call path,
25+forcing a choice, or more typically, as a side-lane where overrides can be done on demand.
26+
27+Note that it should be possible to obtain the current configuration for modification by the user for selective overrides, either
28+by calling methods or by obtaining a data structure that can be set as a whole using a `get -> modify -> set` cycle.
29+
30+Note that without any of that, one should document that with `config_snapshot_mut()` any of the relevant configuration can be
31+changed in memory before invoking a method in order to affect it.
32+
33+Parameters which are not available in git or specific to `gitoxide` or the needs of the caller can be passed as parameters or via
34+`Options` or `Context` structures as needed.
35+
2136 ## General
2237
2338 * **async**
README.md+3−0
@@ -27,6 +27,9 @@ Please see _'Development Status'_ for a listing of all crates and their capabili · 26 unmodified lines
2727 * Based on the [git-hours] algorithm.
2828 * See the [discussion][git-hours-discussion] for some performance data.
2929 * **the `gix` program** _(plumbing)_ - lower level commands for use in automation
30+ * **progress** - provide an overview of what works and what doesn't from the perspective of the git configuration.
31+ This is likely to change a lot over time depending on actual needs, but maybe useful for you to see
32+ if particular git-configuration is picked up and where it deviates.
3033 * **config** - list the complete git configuration in human-readable form and optionally filter sections by name.
3134 * **exclude**
3235 * [x] **query** - check if path specs are excluded via gits exclusion rules like `.gitignore`.
etc/check-package-size.sh+1−1
@@ -57,6 +57,6 @@ echo "in root: gitoxide CLI" · 56 unmodified lines
5757 (enter git-odb && indent cargo diet -n --package-size-limit 120KB)
5858 (enter git-protocol && indent cargo diet -n --package-size-limit 50KB)
5959 (enter git-packetline && indent cargo diet -n --package-size-limit 35KB)
60−(enter git-repository && indent cargo diet -n --package-size-limit 175KB)
60+(enter git-repository && indent cargo diet -n --package-size-limit 185KB)
6161 (enter git-transport && indent cargo diet -n --package-size-limit 55KB)
6262 (enter gitoxide-core && indent cargo diet -n --package-size-limit 90KB)
git-config/tests/file/init/from_paths/includes/conditional/onbranch.rs+1−0
@@ -285,6 +285,7 @@ value = branch-override-by-include · 284 unmodified lines
285285 deref: false,
286286 }),
287287 git_repository::lock::acquire::Fail::Immediately,
288+ git_repository::lock::acquire::Fail::Immediately,
288289 )?
289290 .commit(repo.committer_or_default())?;
290291
git-pack/src/bundle/write/error.rs+2−0
@@ -2,7 +2,9 @@ use std::io; · 1 unmodified line
22
33 use git_tempfile::handle::Writable;
44
5+/// The error returned by [`Bundle::write_to_directory()`][crate::Bundle::write_to_directory()]
56 #[derive(thiserror::Error, Debug)]
7+#[allow(missing_docs)]
68 pub enum Error {
79 #[error("An IO error occurred when reading the pack or creating a temporary file")]
810 Io(#[from] io::Error),
git-pack/src/bundle/write/mod.rs+2−2
@@ -10,7 +10,7 @@ use git_tempfile::{handle::Writable, AutoRemove, ContainingDirectory}; · 9 unmodified lines
1010 use crate::data;
1111
1212 mod error;
13−use error::Error;
13+pub use error::Error;
1414
1515 mod types;
1616 use types::{LockWriter, PassThrough};
@@ -228,7 +228,7 @@ impl crate::Bundle { · 211 unmodified lines
228228 Options {
229229 thread_limit,
230230 iteration_mode: _,
231− index_kind,
231+ index_version: index_kind,
232232 object_hash,
233233 }: Options,
234234 data_file: Arc<parking_lot::Mutex<git_tempfile::Handle<Writable>>>,
git-pack/src/bundle/write/types.rs+2−2
@@ -11,7 +11,7 @@ pub struct Options { · 10 unmodified lines
1111 /// Determine how much processing to spend on protecting against corruption or recovering from errors.
1212 pub iteration_mode: crate::data::input::Mode,
1313 /// The version of pack index to write, should be [`crate::index::Version::default()`]
14− pub index_kind: crate::index::Version,
14+ pub index_version: crate::index::Version,
1515 /// The kind of hash to use when writing the bundle.
1616 pub object_hash: git_hash::Kind,
1717 }
@@ -22,7 +22,7 @@ impl Default for Options { · 4 unmodified lines
2222 Options {
2323 thread_limit: None,
2424 iteration_mode: crate::data::input::Mode::Verify,
25− index_kind: Default::default(),
25+ index_version: Default::default(),
2626 object_hash: Default::default(),
2727 }
2828 }
git-pack/src/index/write/error.rs+1−1
@@ -14,7 +14,7 @@ pub enum Error { · 13 unmodified lines
1414 IteratorInvariantNoRefDelta,
1515 #[error("The iterator failed to set a trailing hash over all prior pack entries in the last provided entry")]
1616 IteratorInvariantTrailer,
17− #[error("Did not encounter a single base")]
17+ #[error("Did not encounter a single base - refusing to write empty pack.")]
1818 IteratorInvariantBasesPresent,
1919 #[error("Only u32::MAX objects can be stored in a pack, found {0}")]
2020 IteratorInvariantTooManyObjects(usize),
git-pack/src/index/write/mod.rs+4−9
@@ -18,7 +18,7 @@ pub(crate) struct TreeEntry { · 17 unmodified lines
1818 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
1919 pub struct Outcome {
2020 /// The version of the verified index
21− pub index_kind: crate::index::Version,
21+ pub index_version: crate::index::Version,
2222 /// The verified checksum of the verified index
2323 pub index_hash: git_hash::ObjectId,
2424
@@ -171,7 +171,7 @@ impl crate::index::File { · 146 unmodified lines
171171 modify_base(data, entry, bytes, kind.hash());
172172 Ok::<_, Error>(())
173173 },
174− crate::cache::delta::traverse::Options {
174+ traverse::Options {
175175 object_progress: root_progress.add_child("Resolving"),
176176 size_progress: root_progress.add_child("Decoding"),
177177 thread_limit,
@@ -207,7 +207,7 @@ impl crate::index::File { · 29 unmodified lines
207207 progress::MessageLevel::Success,
208208 );
209209 Ok(Outcome {
210− index_kind: kind,
210+ index_version: kind,
211211 index_hash,
212212 data_hash: pack_hash,
213213 num_objects,
@@ -215,12 +215,7 @@ impl crate::index::File { · 1 unmodified line
215215 }
216216 }
217217
218−fn modify_base(
219− entry: &mut crate::index::write::TreeEntry,
220− pack_entry: &crate::data::Entry,
221− decompressed: &[u8],
222− hash: git_hash::Kind,
223−) {
218+fn modify_base(entry: &mut TreeEntry, pack_entry: &crate::data::Entry, decompressed: &[u8], hash: git_hash::Kind) {
224219 fn compute_hash(kind: git_object::Kind, bytes: &[u8], object_hash: git_hash::Kind) -> git_hash::ObjectId {
225220 let mut hasher = git_features::hash::hasher(object_hash);
226221 hasher.update(&git_object::encode::loose_header(kind, bytes.len()));
git-pack/tests/pack/bundle.rs+2−2
@@ -90,7 +90,7 @@ mod write_to_directory { · 89 unmodified lines
9090 fn expected_outcome() -> Result<pack::bundle::write::Outcome, Box<dyn std::error::Error>> {
9191 Ok(pack::bundle::write::Outcome {
9292 index: pack::index::write::Outcome {
93− index_kind: pack::index::Version::V2,
93+ index_version: pack::index::Version::V2,
9494 index_hash: git_hash::ObjectId::from_hex(b"544a7204a55f6e9cacccf8f6e191ea8f83575de3")?,
9595 data_hash: git_hash::ObjectId::from_hex(b"0f3ea84cd1bba10c2a03d736a460635082833e59")?,
9696 num_objects: 42,
@@ -156,7 +156,7 @@ mod write_to_directory { · 59 unmodified lines
156156 pack::bundle::write::Options {
157157 thread_limit: None,
158158 iteration_mode: pack::data::input::Mode::Verify,
159− index_kind: pack::index::Version::V2,
159+ index_version: pack::index::Version::V2,
160160 object_hash: git_hash::Kind::Sha1,
161161 },
162162 )
git-pack/tests/pack/data/output/count_and_entries.rs+15−0
@@ -321,6 +321,21 @@ fn traversals() -> crate::Result { · 320 unmodified lines
321321 Ok(())
322322 }
323323
324+#[test]
325+fn empty_pack_is_not_allowed() {
326+ assert_eq!(
327+ write_and_verify(
328+ db(DbKind::DeterministicGeneratedContent).unwrap(),
329+ vec![],
330+ hex_to_id("029d08823bd8a8eab510ad6ac75c823cfd3ed31e"),
331+ None,
332+ )
333+ .unwrap_err()
334+ .to_string(),
335+ "Did not encounter a single base - refusing to write empty pack."
336+ );
337+}
338+
324339 fn write_and_verify(
325340 db: git_odb::HandleArc,
326341 entries: Vec<output::Entry>,
git-pack/tests/pack/index.rs+1−1
@@ -205,7 +205,7 @@ mod version { · 204 unmodified lines
205205 outcome.num_objects, num_objects,
206206 "it wrote the entire iterator worth of entries"
207207 );
208− assert_eq!(outcome.index_kind, desired_kind);
208+ assert_eq!(outcome.index_version, desired_kind);
209209 assert_eq!(
210210 outcome.index_hash,
211211 git_hash::ObjectId::from(&expected[end_of_pack_hash..end_of_index_hash])
git-protocol/src/fetch/arguments/blocking_io.rs+2−1
@@ -5,7 +5,8 @@ use git_transport::{client, client::TransportV2Ext}; · 4 unmodified lines
55 use crate::fetch::{Arguments, Command};
66
77 impl Arguments {
8− pub(crate) fn send<'a, T: client::Transport + 'a>(
8+ /// Send fetch arguments to the server, and indicate this is the end of negotiations only if `add_done_argument` is present.
9+ pub fn send<'a, T: client::Transport + 'a>(
910 &mut self,
1011 transport: &'a mut T,
1112 add_done_argument: bool,
git-protocol/src/fetch/arguments/mod.rs+11−1
@@ -3,6 +3,7 @@ use std::fmt; · 2 unmodified lines
33 use bstr::{BStr, BString, ByteVec};
44
55 /// The arguments passed to a server command.
6+#[derive(Debug)]
67 pub struct Arguments {
78 /// The active features/capabilities of the fetch invocation
89 #[cfg(any(feature = "async-client", feature = "blocking-client"))]
@@ -24,6 +25,13 @@ pub struct Arguments { · 15 unmodified lines
2425 }
2526
2627 impl Arguments {
28+ /// Return true if there is no argument at all.
29+ ///
30+ /// This can happen if callers assure that they won't add 'wants' if their 'have' is the same, i.e. if the remote has nothing
31+ /// new for them.
32+ pub fn is_empty(&self) -> bool {
33+ self.args.is_empty()
34+ }
2735 /// Return true if ref filters is supported.
2836 pub fn can_use_filter(&self) -> bool {
2937 self.filter
@@ -125,8 +133,10 @@ impl Arguments { · 95 unmodified lines
125133 fn prefixed(&mut self, prefix: &str, value: impl fmt::Display) {
126134 self.args.push(format!("{}{}", prefix, value).into());
127135 }
136+ /// Create a new instance to help setting up arguments to send to the server as part of a `fetch` operation
137+ /// for which `features` are the available and configured features to use.
128138 #[cfg(any(feature = "async-client", feature = "blocking-client"))]
129− pub(crate) fn new(version: git_transport::Protocol, features: Vec<crate::fetch::command::Feature>) -> Self {
139+ pub fn new(version: git_transport::Protocol, features: Vec<crate::fetch::command::Feature>) -> Self {
130140 use crate::fetch::Command;
131141 let has = |name: &str| features.iter().any(|f| f.0 == name);
132142 let filter = has("filter");
git-protocol/src/fetch/command.rs+14−4
@@ -50,9 +50,12 @@ mod with_io { · 49 unmodified lines
5050 "filter ", // filter-spec
5151 // ref-in-want feature
5252 "want-ref ", // ref path
53+ // sideband-all feature
5354 "sideband-all",
5455 // packfile-uris feature
5556 "packfile-uris ", // protocols
57+ // wait-for-done feature
58+ "wait-for-done",
5659 ],
5760 }
5861 }
@@ -79,9 +82,14 @@ mod with_io { · 20 unmodified lines
7982 "no-done",
8083 "filter",
8184 ],
82− git_transport::Protocol::V2 => {
83− &["shallow", "filter", "ref-in-want", "sideband-all", "packfile-uris"]
84− }
85+ git_transport::Protocol::V2 => &[
86+ "shallow",
87+ "filter",
88+ "ref-in-want",
89+ "sideband-all",
90+ "packfile-uris",
91+ "wait-for-done",
92+ ],
8593 },
8694 }
8795 }
@@ -104,7 +112,9 @@ mod with_io { · 16 unmodified lines
104112 }
105113 }
106114
107− pub(crate) fn default_features(
115+ /// Turns on all modern features for V1 and all supported features for V2, returning them as a vector of features.
116+ /// Note that this is the basis for any fetch operation as these features fulfil basic requirements and reasonably up-to-date servers.
117+ pub fn default_features(
108118 &self,
109119 version: git_transport::Protocol,
110120 server_capabilities: &Capabilities,
git-protocol/src/fetch/handshake.rs+1−1
@@ -3,7 +3,7 @@ use git_transport::client::Capabilities; · 2 unmodified lines
33 use crate::fetch::Ref;
44
55 /// The result of the [`handshake()`][super::handshake()] function.
6−#[derive(Debug, Clone)]
6+#[derive(Default, Debug, Clone)]
77 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
88 pub struct Outcome {
99 /// The protocol version the server responded with. It might have downgraded the desired version.
git-protocol/src/fetch/refs/function.rs+2−2
@@ -10,13 +10,13 @@ use super::Error; · 9 unmodified lines
1010 use crate::fetch::{indicate_end_of_interaction, refs::from_v2_refs, Command, LsRefsAction, Ref};
1111
1212 /// Invoke an ls-refs command on `transport` (assuming `protocol_version` 2 or panic), which requires a prior handshake that yielded
13−/// server `capabilities`. `prepare_ls_refs(arguments, features)` can be used to alter the _ls-refs_. `progress` is used to provide feedback.
13+/// server `capabilities`. `prepare_ls_refs(capabilities, arguments, features)` can be used to alter the _ls-refs_. `progress` is used to provide feedback.
1414 #[maybe_async]
1515 pub async fn refs(
1616 mut transport: impl Transport,
1717 protocol_version: Protocol,
1818 capabilities: &Capabilities,
19− mut prepare_ls_refs: impl FnMut(
19+ prepare_ls_refs: impl FnOnce(
2020 &Capabilities,
2121 &mut Vec<BString>,
2222 &mut Vec<(&str, Option<&str>)>,
git-protocol/src/fetch/refs/mod.rs+1−1
@@ -50,7 +50,7 @@ pub mod parse { · 49 unmodified lines
5050 pub enum Ref {
5151 /// A ref pointing to a `tag` object, which in turns points to an `object`, usually a commit
5252 Peeled {
53− /// The name at which the ref is located, like `refs/heads/main`.
53+ /// The name at which the ref is located, like `refs/tags/1.0`.
5454 full_ref_name: BString,
5555 /// The hash of the tag the ref points to.
5656 tag: git_hash::ObjectId,
git-protocol/src/fetch/response/mod.rs+2−1
@@ -152,7 +152,8 @@ impl Response { · 151 unmodified lines
152152 self.has_pack
153153 }
154154
155− /// Return an error if the given `features` don't contain the required ones for the given `version` of the protocol.
155+ /// Return an error if the given `features` don't contain the required ones (the ones this implementation needs)
156+ /// for the given `version` of the protocol.
156157 ///
157158 /// Even though technically any set of features supported by the server could work, we only implement the ones that
158159 /// make it easy to maintain all versions with a single code base that aims to be and remain maintainable.
git-protocol/src/fetch/tests/arguments.rs+14−0
@@ -46,6 +46,13 @@ mod impls { · 45 unmodified lines
4646 fn connection_persists_across_multiple_requests(&self) -> bool {
4747 self.stateful
4848 }
49+
50+ fn configure(
51+ &mut self,
52+ config: &dyn std::any::Any,
53+ ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
54+ self.inner.configure(config)
55+ }
4956 }
5057
5158 impl<T: client::Transport> client::Transport for Transport<T> {
@@ -89,6 +96,13 @@ mod impls { · 37 unmodified lines
8996 fn connection_persists_across_multiple_requests(&self) -> bool {
9097 self.stateful
9198 }
99+
100+ fn configure(
101+ &mut self,
102+ config: &dyn std::any::Any,
103+ ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
104+ self.inner.configure(config)
105+ }
92106 }
93107
94108 #[async_trait(?Send)]
git-protocol/src/remote_progress.rs+1−1
@@ -54,7 +54,7 @@ impl<'a> RemoteProgress<'a> { · 53 unmodified lines
5454 progress.fail(progress_name(None, text));
5555 }
5656 } else {
57− match Self::from_bytes(text) {
57+ match RemoteProgress::from_bytes(text) {
5858 Some(RemoteProgress {
5959 action,
6060 percent: _,
git-ref/src/fullname.rs+9−0
@@ -38,6 +38,15 @@ impl TryFrom<BString> for FullName { · 37 unmodified lines
3838 }
3939 }
4040
41+impl TryFrom<&BString> for FullName {
42+ type Error = git_validate::refname::Error;
43+
44+ fn try_from(value: &BString) -> Result<Self, Self::Error> {
45+ git_validate::refname(value.as_ref())?;
46+ Ok(FullName(value.clone()))
47+ }
48+}
49+
4150 impl From<FullName> for BString {
4251 fn from(name: FullName) -> Self {
4352 name.0
git-ref/src/store/file/transaction/prepare.rs+8−4
@@ -176,7 +176,8 @@ impl<'s> Transaction<'s> { · 175 unmodified lines
176176 pub fn prepare(
177177 mut self,
178178 edits: impl IntoIterator<Item = RefEdit>,
179− lock_fail_mode: git_lock::acquire::Fail,
179+ ref_files_lock_fail_mode: git_lock::acquire::Fail,
180+ packed_refs_lock_fail_mode: git_lock::acquire::Fail,
180181 ) -> Result<Self, Error> {
181182 assert!(self.updates.is_none(), "BUG: Must not call prepare(…) multiple times");
182183 let store = self.store;
@@ -267,7 +268,7 @@ impl<'s> Transaction<'s> { · 84 unmodified lines
267268 let packed_transaction: Option<_> = if maybe_updates_for_packed_refs.unwrap_or(0) > 0 {
268269 // We have to create a packed-ref even if it doesn't exist
269270 self.store
270− .packed_transaction(lock_fail_mode)
271+ .packed_transaction(packed_refs_lock_fail_mode)
271272 .map_err(|err| match err {
272273 file::packed::transaction::Error::BufferOpen(err) => Error::from(err),
273274 file::packed::transaction::Error::TransactionLock(err) => {
@@ -280,7 +281,10 @@ impl<'s> Transaction<'s> { · 6 unmodified lines
280281 // no packed-ref file exists anyway
281282 self.store
282283 .assure_packed_refs_uptodate()?
283− .map(|p| buffer_into_transaction(p, lock_fail_mode).map_err(Error::PackedTransactionAcquire))
284+ .map(|p| {
285+ buffer_into_transaction(p, packed_refs_lock_fail_mode)
286+ .map_err(Error::PackedTransactionAcquire)
287+ })
284288 .transpose()?
285289 };
286290 if let Some(transaction) = packed_transaction {
@@ -302,7 +306,7 @@ impl<'s> Transaction<'s> { · 15 unmodified lines
302306 let change = &mut updates[cid];
303307 if let Err(err) = Self::lock_ref_and_apply_change(
304308 self.store,
305− lock_fail_mode,
309+ ref_files_lock_fail_mode,
306310 self.packed_transaction.as_ref().and_then(|t| t.buffer()),
307311 change,
308312 ) {
git-ref/src/target.rs+1−1
@@ -19,7 +19,7 @@ impl<'a> TargetRef<'a> { · 18 unmodified lines
1919 TargetRef::Peeled(oid) => Some(oid),
2020 }
2121 }
22− /// Interpret this target as object id or panic if it is symbolic.
22+ /// Interpret this target as object id or **panic** if it is symbolic.
2323 pub fn id(&self) -> &oid {
2424 match self {
2525 TargetRef::Symbolic(_) => panic!("BUG: tries to obtain object id from symbolic target"),
git-ref/src/transaction/mod.rs+9−1
@@ -83,7 +83,15 @@ pub enum Change { · 82 unmodified lines
8383 }
8484
8585 impl Change {
86− /// Return references to values that are in common between all variants.
86+ /// Return references to values that are the new value after the change is applied, if this is an update.
87+ pub fn new_value(&self) -> Option<crate::TargetRef<'_>> {
88+ match self {
89+ Change::Update { new, .. } => new.to_ref().into(),
90+ Change::Delete { .. } => None,
91+ }
92+ }
93+
94+ /// Return references to values that are in common between all variants and denote the previous observed value.
8795 pub fn previous_value(&self) -> Option<crate::TargetRef<'_>> {
8896 match self {
8997 // TODO: use or-patterns once MRV is larger than 1.52 (and this is supported)
git-ref/tests/file/transaction/prepare_and_commit/create_or_update.rs+18−3
@@ -44,6 +44,7 @@ fn reference_with_equally_named_empty_or_non_empty_directory_already_in_place_ca · 43 unmodified lines
4444 deref: false,
4545 }),
4646 Fail::Immediately,
47+ Fail::Immediately,
4748 )?
4849 .commit(committer().to_ref());
4950 if *is_empty {
@@ -83,6 +84,7 @@ fn reference_with_old_value_must_exist_when_creating_it() -> crate::Result { · 33 unmodified lines
8384 deref: false,
8485 }),
8586 Fail::Immediately,
87+ Fail::Immediately,
8688 );
8789
8890 match res {
@@ -114,6 +116,7 @@ fn reference_with_explicit_value_must_match_the_value_on_update() -> crate::Resu · 25 unmodified lines
114116 deref: false,
115117 }),
116118 Fail::Immediately,
119+ Fail::Immediately,
117120 );
118121 match res {
119122 Err(transaction::prepare::Error::ReferenceOutOfDate { full_name, actual, .. }) => {
@@ -142,6 +145,7 @@ fn the_existing_must_match_constraint_allow_non_existing_references_to_be_create · 22 unmodified lines
142145 deref: false,
143146 }),
144147 Fail::Immediately,
148+ Fail::Immediately,
145149 )?
146150 .commit(committer().to_ref())?;
147151
@@ -180,6 +184,7 @@ fn the_existing_must_match_constraint_requires_existing_references_to_have_the_g · 32 unmodified lines
180184 deref: false,
181185 }),
182186 Fail::Immediately,
187+ Fail::Immediately,
183188 );
184189 match res {
185190 Err(transaction::prepare::Error::ReferenceOutOfDate { full_name, actual, .. }) => {
@@ -208,6 +213,7 @@ fn reference_with_must_not_exist_constraint_cannot_be_created_if_it_exists_alrea · 22 unmodified lines
208213 deref: false,
209214 }),
210215 Fail::Immediately,
216+ Fail::Immediately,
211217 );
212218 match res {
213219 Err(transaction::prepare::Error::MustNotExist { full_name, actual, .. }) => {
@@ -246,6 +252,7 @@ fn namespaced_updates_or_deletions_are_transparent_and_not_observable() -> crate · 32 unmodified lines
246252 },
247253 ],
248254 Fail::Immediately,
255+ Fail::Immediately,
249256 )?
250257 .commit(committer().to_ref())?;
251258
@@ -295,6 +302,7 @@ fn reference_with_must_exist_constraint_must_exist_already_with_any_value() -> c · 43 unmodified lines
295302 deref: false,
296303 }),
297304 Fail::Immediately,
305+ Fail::Immediately,
298306 )?
299307 .commit(committer().to_ref())?;
300308
@@ -340,6 +348,7 @@ fn reference_with_must_not_exist_constraint_may_exist_already_if_the_new_value_m · 39 unmodified lines
340348 deref: false,
341349 }),
342350 Fail::Immediately,
351+ Fail::Immediately,
343352 )?
344353 .commit(committer().to_ref())?;
345354
@@ -387,6 +396,7 @@ fn cancellation_after_preparation_leaves_no_change() -> crate::Result { · 41 unmodified lines
387396 deref: false,
388397 }),
389398 Fail::Immediately,
399+ Fail::Immediately,
390400 )?;
391401 assert_eq!(std::fs::read_dir(dir.path())?.count(), 1, "the lock file was created");
392402
@@ -424,6 +434,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { · 31 unmodified lines
424434 deref: false,
425435 }),
426436 Fail::Immediately,
437+ Fail::Immediately,
427438 )?
428439 .commit(committer().to_ref())?;
429440 assert_eq!(
@@ -475,6 +486,7 @@ fn symbolic_head_missing_referent_then_update_referent() -> crate::Result { · 45 unmodified lines
475486 deref: true,
476487 }),
477488 Fail::Immediately,
489+ Fail::Immediately,
478490 )?
479491 .commit(committer().to_ref())?;
480492
@@ -568,6 +580,7 @@ fn write_reference_to_which_head_points_to_does_not_update_heads_reflog_even_tho · 87 unmodified lines
568580 deref: false,
569581 }),
570582 Fail::Immediately,
583+ Fail::Immediately,
571584 )?
572585 .commit(committer().to_ref())?;
573586
@@ -632,7 +645,8 @@ fn packed_refs_are_looked_up_when_checking_existing_values() -> crate::Result { · 58 unmodified lines
632645 name: "refs/heads/main".try_into()?,
633646 deref: false,
634647 }),
635− git_lock::acquire::Fail::Immediately,
648+ Fail::Immediately,
649+ Fail::Immediately,
636650 )?
637651 .commit(committer().to_ref())?;
638652
@@ -687,7 +701,8 @@ fn packed_refs_creation_with_packed_refs_mode_prune_removes_original_loose_refs( · 48 unmodified lines
687701 name: r.name,
688702 deref: false,
689703 }),
690− git_lock::acquire::Fail::Immediately,
704+ Fail::Immediately,
705+ Fail::Immediately,
691706 )?
692707 .commit(committer().to_ref())?;
693708
@@ -743,7 +758,7 @@ fn packed_refs_creation_with_packed_refs_mode_leave_keeps_original_loose_refs() · 49 unmodified lines
743758 .packed_refs(PackedRefs::DeletionsAndNonSymbolicUpdates(Box::new(|_, _| {
744759 Ok(Some(git_object::Kind::Commit))
745760 })))
746− .prepare(edits, git_lock::acquire::Fail::Immediately)?
761+ .prepare(edits, Fail::Immediately, Fail::Immediately)?
747762 .commit(committer().to_ref())?;
748763 assert_eq!(
749764 edits.len(),
git-ref/tests/file/transaction/prepare_and_commit/delete.rs+13−0
@@ -28,6 +28,7 @@ fn delete_a_ref_which_is_gone_succeeds() -> crate::Result { · 27 unmodified lines
2828 deref: false,
2929 }),
3030 Fail::Immediately,
31+ Fail::Immediately,
3132 )?
3233 .commit(committer().to_ref())?;
3334 assert_eq!(edits.len(), 1);
@@ -47,6 +48,7 @@ fn delete_a_ref_which_is_gone_but_must_exist_fails() -> crate::Result { · 13 unmodified lines
4748 deref: false,
4849 }),
4950 Fail::Immediately,
51+ Fail::Immediately,
5052 );
5153 match res {
5254 Ok(_) => unreachable!("must exist, but it doesn't actually exist"),
@@ -77,6 +79,7 @@ fn delete_ref_and_reflog_on_symbolic_no_deref() -> crate::Result { · 24 unmodified lines
7779 deref: false,
7880 }),
7981 Fail::Immediately,
82+ Fail::Immediately,
8083 )?
8184 .commit(committer().to_ref())?;
8285
@@ -117,6 +120,7 @@ fn delete_ref_with_incorrect_previous_value_fails() -> crate::Result { · 34 unmodified lines
117120 deref: true,
118121 }),
119122 Fail::Immediately,
123+ Fail::Immediately,
120124 );
121125
122126 match res {
@@ -151,6 +155,7 @@ fn delete_reflog_only_of_symbolic_no_deref() -> crate::Result { · 28 unmodified lines
151155 deref: false,
152156 }),
153157 Fail::Immediately,
158+ Fail::Immediately,
154159 )?
155160 .commit(committer().to_ref())?;
156161
@@ -185,6 +190,7 @@ fn delete_reflog_only_of_symbolic_with_deref() -> crate::Result { · 28 unmodified lines
185190 deref: true,
186191 }),
187192 Fail::Immediately,
193+ Fail::Immediately,
188194 )?
189195 .commit(committer().to_ref())?;
190196
@@ -218,6 +224,7 @@ fn delete_broken_ref_that_must_exist_fails_as_it_is_no_valid_ref() -> crate::Res · 27 unmodified lines
218224 deref: true,
219225 }),
220226 Fail::Immediately,
227+ Fail::Immediately,
221228 );
222229 match res {
223230 Err(err) => {
@@ -248,6 +255,7 @@ fn non_existing_can_be_deleted_with_the_may_exist_match_constraint() -> crate::R · 24 unmodified lines
248255 deref: true,
249256 }),
250257 Fail::Immediately,
258+ Fail::Immediately,
251259 )?
252260 .commit(committer().to_ref())?;
253261
@@ -284,6 +292,7 @@ fn delete_broken_ref_that_may_not_exist_works_even_in_deref_mode() -> crate::Res · 30 unmodified lines
284292 deref: true,
285293 }),
286294 Fail::Immediately,
295+ Fail::Immediately,
287296 )?
288297 .commit(committer().to_ref())?;
289298
@@ -325,6 +334,7 @@ fn store_write_mode_has_no_effect_and_reflogs_are_always_deleted() -> crate::Res · 35 unmodified lines
325334 deref: false,
326335 }),
327336 Fail::Immediately,
337+ Fail::Immediately,
328338 )?
329339 .commit(committer().to_ref())?;
330340 assert_eq!(edits.len(), 1);
@@ -360,6 +370,7 @@ fn packed_refs_are_consulted_when_determining_previous_value_of_ref_to_be_delete · 29 unmodified lines
360370 deref: false,
361371 }),
362372 Fail::Immediately,
373+ Fail::Immediately,
363374 )?
364375 .commit(committer().to_ref())?;
365376
@@ -393,6 +404,7 @@ fn a_loose_ref_with_old_value_check_and_outdated_packed_refs_value_deletes_both_ · 27 unmodified lines
393404 deref: false,
394405 }),
395406 Fail::Immediately,
407+ Fail::Immediately,
396408 )?
397409 .commit(committer().to_ref())?;
398410
@@ -432,6 +444,7 @@ fn all_contained_references_deletes_the_packed_ref_file_too() -> crate::Result { · 33 unmodified lines
432444 }
433445 }),
434446 Fail::Immediately,
447+ Fail::Immediately,
435448 )?
436449 .commit(committer().to_ref())?;
437450
git-ref/tests/file/worktree.rs+11−5
@@ -191,6 +191,7 @@ mod read_only { · 190 unmodified lines
191191 }
192192
193193 mod writable {
194+ use git_lock::acquire::Fail;
194195 use std::convert::TryInto;
195196
196197 use git_ref::{
@@ -258,7 +259,8 @@ mod writable { · 61 unmodified lines
258259 deref: false,
259260 },
260261 ],
261− git_lock::acquire::Fail::Immediately,
262+ Fail::Immediately,
263+ Fail::Immediately,
262264 )?
263265 .commit(committer().to_ref())
264266 .expect("successful commit as even similar resolved names live in different base locations");
@@ -405,7 +407,8 @@ mod writable { · 140 unmodified lines
405407 deref: false,
406408 },
407409 ],
408− git_lock::acquire::Fail::Immediately,
410+ Fail::Immediately,
411+ Fail::Immediately,
409412 ),
410413 Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. })
411414 ), "prefixed refs resolve to the same name and will fail to be locked (so we don't check for this when doing dupe checking)");
@@ -424,7 +427,8 @@ mod writable { · 12 unmodified lines
424427 deref: false,
425428 },
426429 ],
427− git_lock::acquire::Fail::Immediately,
430+ Fail::Immediately,
431+ Fail::Immediately,
428432 ),
429433 Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. })
430434 ));
@@ -467,7 +471,8 @@ mod writable { · 36 unmodified lines
467471 deref: false,
468472 },
469473 ],
470− git_lock::acquire::Fail::Immediately,
474+ Fail::Immediately,
475+ Fail::Immediately,
471476 ),
472477 Err(git_ref::file::transaction::prepare::Error::LockAcquire { .. })
473478 ), "prefixed refs resolve to the same name and will fail to be locked (so we don't check for this when doing dupe checking)");
@@ -509,7 +514,8 @@ mod writable { · 35 unmodified lines
509514 deref: false,
510515 },
511516 ],
512− git_lock::acquire::Fail::Immediately,
517+ Fail::Immediately,
518+ Fail::Immediately,
513519 )?
514520 .commit(committer().to_ref())
515521 .expect("successful commit as even similar resolved names live in different base locations");
git-refspec/src/spec.rs+19−3
@@ -1,4 +1,4 @@
1−use bstr::BStr;
1+use bstr::{BStr, ByteSlice};
22
33 use crate::{
44 instruction::{Fetch, Push},
@@ -18,6 +18,11 @@ impl RefSpec { · 13 unmodified lines
1818 dst: self.dst.as_ref().map(|b| b.as_ref()),
1919 }
2020 }
21+
22+ /// Return true if the spec stats with a `+` and thus forces setting the reference.
23+ pub fn allow_non_fast_forward(&self) -> bool {
24+ matches!(self.mode, Mode::Force)
25+ }
2126 }
2227
2328 mod impls {
@@ -84,7 +89,7 @@ mod impls { · 60 unmodified lines
8489 }
8590
8691 /// Access
87−impl RefSpecRef<'_> {
92+impl<'a> RefSpecRef<'a> {
8893 /// Return the left-hand side of the spec, typically the source.
8994 /// It takes many different forms so don't rely on this being a ref name.
9095 ///
@@ -117,8 +122,19 @@ impl RefSpecRef<'_> { · 26 unmodified lines
117122 }
118123 }
119124
125+ /// Derive the prefix from the `source` side of this spec, if possible.
126+ ///
127+ /// This means it starts with `refs/`. Note that it won't contain more than two components, like `refs/heads/`
128+ pub fn prefix(&self) -> Option<&BStr> {
129+ let source = self.source()?;
130+ let suffix = source.strip_prefix(b"refs/")?;
131+ let slash_pos = suffix.find_byte(b'/')?;
132+ let prefix = source[..="refs/".len() + slash_pos].as_bstr();
133+ (!prefix.contains(&b'*')).then(|| prefix)
134+ }
135+
120136 /// Transform the state of the refspec into an instruction making clear what to do with it.
121− pub fn instruction(&self) -> Instruction<'_> {
137+ pub fn instruction(&self) -> Instruction<'a> {
122138 match self.op {
123139 Operation::Fetch => match (self.mode, self.src, self.dst) {
124140 (Mode::Normal | Mode::Force, Some(src), None) => Instruction::Fetch(Fetch::Only { src }),
git-refspec/tests/refspec.rs+1−0
@@ -6,4 +6,5 @@ mod impls; · 5 unmodified lines
66 mod match_group;
77 mod matching;
88 mod parse;
9+mod spec;
910 mod write;
git-refspec/tests/spec/mod.rs+41−0
@@ -0,0 +1,41 @@
1+mod prefix {
2+ use git_refspec::parse::Operation;
3+ use git_refspec::RefSpec;
4+
5+ #[test]
6+ fn partial_refs_have_no_prefix() {
7+ assert_eq!(parse("main").to_ref().prefix(), None);
8+ }
9+
10+ #[test]
11+ fn short_absolute_refs_have_no_prefix() {
12+ assert_eq!(parse("refs/short").to_ref().prefix(), None);
13+ }
14+
15+ #[test]
16+ fn full_names_have_a_prefix() {
17+ assert_eq!(parse("refs/heads/main").to_ref().prefix().unwrap(), "refs/heads/");
18+ assert_eq!(parse("refs/foo/bar").to_ref().prefix().unwrap(), "refs/foo/");
19+ assert_eq!(
20+ parse("refs/heads/*:refs/remotes/origin/*").to_ref().prefix().unwrap(),
21+ "refs/heads/"
22+ );
23+ }
24+
25+ #[test]
26+ fn strange_glob_patterns_have_no_prefix() {
27+ assert_eq!(parse("refs/*/main:refs/*/main").to_ref().prefix(), None);
28+ }
29+
30+ #[test]
31+ fn object_names_have_no_prefix() {
32+ assert_eq!(
33+ parse("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").to_ref().prefix(),
34+ None
35+ );
36+ }
37+
38+ fn parse(spec: &str) -> RefSpec {
39+ git_refspec::parse(spec.into(), Operation::Fetch).unwrap().to_owned()
40+ }
41+}
git-repository/examples/init-repo-and-commit.rs+26−39
@@ -13,49 +13,36 @@ fn main() -> anyhow::Result<()> { · 12 unmodified lines
1313 let git_dir = std::env::args_os()
1414 .nth(1)
1515 .context("First argument needs to be the directory to initialize the repository in")?;
16− let repo = git::init_bare(git_dir)?;
16+ let mut repo = git::init_bare(git_dir)?;
1717
1818 println!("Repo (bare): {:?}", repo.git_dir());
1919
2020 let mut tree = git::objs::Tree::empty();
21− let empty_tree_id = repo.write_object(&tree)?;
22−
23− let author = git::actor::SignatureRef {
24− name: "Maria Sanchez".into(),
25− email: "maria@example.com".into(),
26− time: git_date::Time::now_local_or_utc(),
27− };
28− let initial_commit_id = repo.commit(
29− "HEAD",
30− author,
31− author,
32− "initial commit",
33− empty_tree_id,
34− git::commit::NO_PARENT_IDS,
35− )?;
36−
37− println!("initial commit id with empty tree: {:?}", initial_commit_id);
38−
39− let blob_id = repo.write_blob("hello world")?.into();
40− let entry = tree::Entry {
41− mode: tree::EntryMode::Blob,
42− oid: blob_id,
43− filename: "hello.txt".into(),
44− };
45−
46− tree.entries.push(entry);
47− let hello_tree_id = repo.write_object(&tree)?;
48−
49− let blob_commit_id = repo.commit(
50− "HEAD",
51− author,
52− author,
53− "hello commit",
54− hello_tree_id,
55− [initial_commit_id],
56− )?;
57−
58− println!("commit id for 'hello world' blob: {:?}", blob_commit_id);
21+ let empty_tree_id = repo.write_object(&tree)?.detach();
22+
23+ let mut config = repo.config_snapshot_mut();
24+ config.set_raw_value("author", None, "name", "Maria Sanchez")?;
25+ config.set_raw_value("author", None, "email", "maria@example.com")?;
26+ {
27+ let repo = config.commit_auto_rollback()?;
28+ let initial_commit_id = repo.commit("HEAD", "initial commit", empty_tree_id, git::commit::NO_PARENT_IDS)?;
29+
30+ println!("initial commit id with empty tree: {:?}", initial_commit_id);
31+
32+ let blob_id = repo.write_blob("hello world")?.into();
33+ let entry = tree::Entry {
34+ mode: tree::EntryMode::Blob,
35+ oid: blob_id,
36+ filename: "hello.txt".into(),
37+ };
38+
39+ tree.entries.push(entry);
40+ let hello_tree_id = repo.write_object(&tree)?;
41+
42+ let blob_commit_id = repo.commit("HEAD", "hello commit", hello_tree_id, [initial_commit_id])?;
43+
44+ println!("commit id for 'hello world' blob: {:?}", blob_commit_id);
45+ }
5946
6047 Ok(())
6148 }
git-repository/src/config/cache/access.rs+103−0
@@ -0,0 +1,103 @@
1+use crate::config::Cache;
2+use crate::{remote, repository::identity};
3+use git_lock::acquire::Fail;
4+use std::convert::TryInto;
5+use std::path::PathBuf;
6+use std::time::Duration;
7+
8+/// Access
9+impl Cache {
10+ pub(crate) fn personas(&self) -> &identity::Personas {
11+ self.personas
12+ .get_or_init(|| identity::Personas::from_config_and_env(&self.resolved, self.git_prefix))
13+ }
14+
15+ pub(crate) fn url_rewrite(&self) -> &remote::url::Rewrite {
16+ self.url_rewrite
17+ .get_or_init(|| remote::url::Rewrite::from_config(&self.resolved, self.filter_config_section))
18+ }
19+
20+ #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
21+ pub(crate) fn url_scheme(
22+ &self,
23+ ) -> Result<&remote::url::SchemePermission, remote::url::scheme_permission::init::Error> {
24+ self.url_scheme.get_or_try_init(|| {
25+ remote::url::SchemePermission::from_config(&self.resolved, self.git_prefix, self.filter_config_section)
26+ })
27+ }
28+
29+ /// Returns (file-timeout, pack-refs timeout)
30+ pub(crate) fn lock_timeout(
31+ &self,
32+ ) -> Result<(git_lock::acquire::Fail, git_lock::acquire::Fail), git_config::value::Error> {
33+ enum Kind {
34+ RefFiles,
35+ RefPackFile,
36+ }
37+ let mut out: [git_lock::acquire::Fail; 2] = Default::default();
38+
39+ for (idx, kind) in [Kind::RefFiles, Kind::RefPackFile].iter().enumerate() {
40+ let (key, default_ms) = match kind {
41+ Kind::RefFiles => ("filesRefLockTimeout", 100),
42+ Kind::RefPackFile => ("packedRefsTimeout", 1000),
43+ };
44+ let mk_default = || Fail::AfterDurationWithBackoff(Duration::from_millis(default_ms));
45+ let mut fnp = self.filter_config_section;
46+
47+ let lock_mode = match self.resolved.integer_filter("core", None, key, &mut fnp) {
48+ Some(Ok(val)) if val < 0 => Fail::AfterDurationWithBackoff(Duration::from_secs(u64::MAX)),
49+ Some(Ok(val)) if val == 0 => Fail::Immediately,
50+ Some(Ok(val)) => Fail::AfterDurationWithBackoff(Duration::from_millis(
51+ val.try_into().expect("i64 can be repsented by u64"),
52+ )),
53+ Some(Err(_)) if self.lenient_config => mk_default(),
54+ Some(Err(err)) => return Err(err),
55+ None => mk_default(),
56+ };
57+ out[idx] = lock_mode;
58+ }
59+ Ok((out[0], out[1]))
60+ }
61+
62+ /// The path to the user-level excludes file to ignore certain files in the worktree.
63+ pub(crate) fn excludes_file(&self) -> Result<Option<PathBuf>, git_config::path::interpolate::Error> {
64+ let home = self.home_dir();
65+ let install_dir = crate::path::install_dir().ok();
66+ let ctx = crate::config::cache::interpolate_context(install_dir.as_deref(), home.as_deref());
67+ match self
68+ .resolved
69+ .path_filter("core", None, "excludesFile", &mut self.filter_config_section.clone())
70+ .map(|p| p.interpolate(ctx).map(|p| p.into_owned()))
71+ .transpose()
72+ {
73+ Ok(f) => Ok(f),
74+ Err(_err) if self.lenient_config => Ok(None),
75+ Err(err) => Err(err),
76+ }
77+ }
78+
79+ /// Return a path by using the `$XDF_CONFIG_HOME` or `$HOME/.config/…` environment variables locations.
80+ pub fn xdg_config_path(
81+ &self,
82+ resource_file_name: &str,
83+ ) -> Result<Option<PathBuf>, git_sec::permission::Error<PathBuf>> {
84+ std::env::var_os("XDG_CONFIG_HOME")
85+ .map(|path| (path, &self.xdg_config_home_env))
86+ .or_else(|| std::env::var_os("HOME").map(|path| (path, &self.home_env)))
87+ .and_then(|(base, permission)| {
88+ let resource = std::path::PathBuf::from(base).join("git").join(resource_file_name);
89+ permission.check(resource).transpose()
90+ })
91+ .transpose()
92+ }
93+
94+ /// Return the home directory if we are allowed to read it and if it is set in the environment.
95+ ///
96+ /// We never fail for here even if the permission is set to deny as we `git-config` will fail later
97+ /// if it actually wants to use the home directory - we don't want to fail prematurely.
98+ pub fn home_dir(&self) -> Option<PathBuf> {
99+ std::env::var_os("HOME")
100+ .map(PathBuf::from)
101+ .and_then(|path| self.home_env.check_opt(path))
102+ }
103+}
git-repository/src/config/cache/init.rs+31−53
@@ -1,7 +1,5 @@
1−use std::path::PathBuf;
2−
31 use super::{interpolate_context, util, Error, StageOne};
4−use crate::{config::Cache, repository, revision::spec::parse::ObjectKindHint};
2+use crate::{config::Cache, repository};
53
64 /// Initialization
75 impl Cache {
@@ -17,7 +15,7 @@ impl Cache { · 9 unmodified lines
1715 }: StageOne,
1816 git_dir: &std::path::Path,
1917 branch_name: Option<&git_ref::FullNameRef>,
20− mut filter_config_section: fn(&git_config::file::Metadata) -> bool,
18+ filter_config_section: fn(&git_config::file::Metadata) -> bool,
2119 git_install_dir: Option<&std::path::Path>,
2220 home: Option<&std::path::Path>,
2321 repository::permissions::Environment {
@@ -113,36 +111,14 @@ impl Cache { · 89 unmodified lines
113111 globals
114112 };
115113
116− let excludes_file = match config
117− .path_filter("core", None, "excludesFile", &mut filter_config_section)
118− .map(|p| p.interpolate(options.includes.interpolate).map(|p| p.into_owned()))
119− .transpose()
120− {
121− Ok(f) => f,
122− Err(_err) if lenient_config => None,
123− Err(err) => return Err(err.into()),
124− };
125−
126− let hex_len = match util::parse_core_abbrev(&config, object_hash) {
127− Ok(v) => v,
128− Err(_err) if lenient_config => None,
129− Err(err) => return Err(err),
130− };
114+ let hex_len = util::check_lenient(util::parse_core_abbrev(&config, object_hash), lenient_config)?;
131115
132116 use util::config_bool;
133117 let reflog = util::query_refupdates(&config);
134118 let ignore_case = config_bool(&config, "core.ignoreCase", false, lenient_config)?;
135119 let use_multi_pack_index = config_bool(&config, "core.multiPackIndex", true, lenient_config)?;
136− let object_kind_hint = config.string("core", None, "disambiguate").and_then(|value| {
137− Some(match value.as_ref().as_ref() {
138− b"commit" => ObjectKindHint::Commit,
139− b"committish" => ObjectKindHint::Committish,
140− b"tree" => ObjectKindHint::Tree,
141− b"treeish" => ObjectKindHint::Treeish,
142− b"blob" => ObjectKindHint::Blob,
143− _ => return None,
144− })
145− });
120+ let object_kind_hint = util::disambiguate_hint(&config);
121+ // NOTE: When adding a new initial cache, consider adjusting `reread_values_and_clear_caches()` as well.
146122 Ok(Cache {
147123 resolved: config.into(),
148124 use_multi_pack_index,
@@ -153,39 +129,41 @@ impl Cache { · 4 unmodified lines
153129 ignore_case,
154130 hex_len,
155131 filter_config_section,
156− excludes_file,
157132 xdg_config_home_env,
158133 home_env,
134+ lenient_config,
159135 personas: Default::default(),
160136 url_rewrite: Default::default(),
161− #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
137+ #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
162138 url_scheme: Default::default(),
163139 git_prefix,
164140 })
165141 }
166142
167− /// Return a path by using the `$XDF_CONFIG_HOME` or `$HOME/.config/…` environment variables locations.
168− pub fn xdg_config_path(
169− &self,
170− resource_file_name: &str,
171− ) -> Result<Option<PathBuf>, git_sec::permission::Error<PathBuf>> {
172− std::env::var_os("XDG_CONFIG_HOME")
173− .map(|path| (path, &self.xdg_config_home_env))
174− .or_else(|| std::env::var_os("HOME").map(|path| (path, &self.home_env)))
175− .and_then(|(base, permission)| {
176− let resource = std::path::PathBuf::from(base).join("git").join(resource_file_name);
177− permission.check(resource).transpose()
178− })
179− .transpose()
180− }
181−
182− /// Return the home directory if we are allowed to read it and if it is set in the environment.
143+ /// Call this with new `config` to update values and clear caches. Note that none of the values will be applied if a single
144+ /// one is invalid.
145+ /// However, those that are lazily read won't be re-evaluated right away and might thus pass now but fail later.
183146 ///
184− /// We never fail for here even if the permission is set to deny as we `git-config` will fail later
185− /// if it actually wants to use the home directory - we don't want to fail prematurely.
186− pub fn home_dir(&self) -> Option<PathBuf> {
187− std::env::var_os("HOME")
188− .map(PathBuf::from)
189− .and_then(|path| self.home_env.check_opt(path))
147+ /// Note that we unconditionally re-read all values.
148+ pub fn reread_values_and_clear_caches(&mut self, config: crate::Config) -> Result<(), Error> {
149+ let hex_len = util::check_lenient(util::parse_core_abbrev(&config, self.object_hash), self.lenient_config)?;
150+
151+ use util::config_bool;
152+ let ignore_case = config_bool(&config, "core.ignoreCase", false, self.lenient_config)?;
153+ let object_kind_hint = util::disambiguate_hint(&config);
154+
155+ self.personas = Default::default();
156+ self.url_rewrite = Default::default();
157+ #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
158+ {
159+ self.url_scheme = Default::default();
160+ }
161+
162+ self.resolved = config;
163+ self.hex_len = hex_len;
164+ self.ignore_case = ignore_case;
165+ self.object_kind_hint = object_kind_hint;
166+
167+ Ok(())
190168 }
191169 }
git-repository/src/config/cache/mod.rs+1−22
@@ -1,5 +1,4 @@
11 use super::{Cache, Error};
2−use crate::{remote, repository::identity};
32
43 mod incubate;
54 pub(crate) use incubate::StageOne;
@@ -12,27 +11,7 @@ impl std::fmt::Debug for Cache { · 6 unmodified lines
1211 }
1312 }
1413
15−/// Access
16−impl Cache {
17− pub(crate) fn personas(&self) -> &identity::Personas {
18− self.personas
19− .get_or_init(|| identity::Personas::from_config_and_env(&self.resolved, self.git_prefix))
20− }
21−
22− pub(crate) fn url_rewrite(&self) -> &remote::url::Rewrite {
23− self.url_rewrite
24− .get_or_init(|| remote::url::Rewrite::from_config(&self.resolved, self.filter_config_section))
25− }
26−
27− #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
28− pub(crate) fn url_scheme(
29− &self,
30− ) -> Result<&remote::url::SchemePermission, remote::url::scheme_permission::init::Error> {
31− self.url_scheme.get_or_try_init(|| {
32− remote::url::SchemePermission::from_config(&self.resolved, self.git_prefix, self.filter_config_section)
33− })
34− }
35−}
14+mod access;
3615
3716 mod util;
3817 pub(crate) use util::interpolate_context;
git-repository/src/config/cache/util.rs+22−0
@@ -2,6 +2,7 @@ use std::convert::TryFrom; · 1 unmodified line
22
33 use super::Error;
44 use crate::bstr::ByteSlice;
5+use crate::revision::spec::parse::ObjectKindHint;
56
67 pub(crate) fn interpolate_context<'a>(
78 git_install_dir: Option<&'a std::path::Path>,
@@ -54,6 +55,14 @@ pub(crate) fn query_refupdates(config: &git_config::File<'static>) -> Option<git · 46 unmodified lines
5455 })
5556 }
5657
58+pub(crate) fn check_lenient<T, E>(v: Result<Option<T>, E>, lenient: bool) -> Result<Option<T>, E> {
59+ match v {
60+ Ok(v) => Ok(v),
61+ Err(_) if lenient => Ok(None),
62+ Err(err) => Err(err),
63+ }
64+}
65+
5766 pub(crate) fn parse_core_abbrev(
5867 config: &git_config::File<'static>,
5968 object_hash: git_hash::Kind,
@@ -93,3 +102,16 @@ pub(crate) fn parse_core_abbrev( · 33 unmodified lines
93102 None => Ok(None),
94103 }
95104 }
105+
106+pub(crate) fn disambiguate_hint(config: &git_config::File<'static>) -> Option<ObjectKindHint> {
107+ config.string("core", None, "disambiguate").and_then(|value| {
108+ Some(match value.as_ref().as_ref() {
109+ b"commit" => ObjectKindHint::Commit,
110+ b"committish" => ObjectKindHint::Committish,
111+ b"tree" => ObjectKindHint::Tree,
112+ b"treeish" => ObjectKindHint::Treeish,
113+ b"blob" => ObjectKindHint::Blob,
114+ _ => return None,
115+ })
116+ })
117+}
git-repository/src/config/mod.rs+21−7
@@ -14,16 +14,26 @@ pub struct Snapshot<'repo> { · 13 unmodified lines
1414 pub(crate) repo: &'repo Repository,
1515 }
1616
17−/// A platform to access configuration values and modify them in memory, while making them available when this platform is dropped.
17+/// A platform to access configuration values and modify them in memory, while making them available when this platform is dropped
18+/// as form of auto-commit.
1819 /// Note that the values will only affect this instance of the parent repository, and not other clones that may exist.
1920 ///
2021 /// Note that these values won't update even if the underlying file(s) change.
21−// TODO: make it possible to load snapshots with reloading via .config() and write mutated snapshots back to disk.
22+///
23+/// Use [`forget()`][Self::forget()] to not apply any of the changes.
24+// TODO: make it possible to load snapshots with reloading via .config() and write mutated snapshots back to disk which should be the way
25+// to affect all instances of a repo, probably via `config_mut()` and `config_mut_at()`.
2226 pub struct SnapshotMut<'repo> {
23− pub(crate) repo: &'repo mut Repository,
27+ pub(crate) repo: Option<&'repo mut Repository>,
2428 pub(crate) config: git_config::File<'static>,
2529 }
2630
31+/// A utility structure created by [`SnapshotMut::commit_auto_rollback()`] that restores the previous configuration on drop.
32+pub struct CommitAutoRollback<'repo> {
33+ pub(crate) repo: Option<&'repo mut Repository>,
34+ pub(crate) prev_config: crate::Config,
35+}
36+
2737 pub(crate) mod section {
2838 pub fn is_trusted(meta: &git_config::file::Metadata) -> bool {
2939 meta.trust == git_sec::Trust::Full || meta.source.kind() != git_config::source::Kind::Repository
@@ -54,7 +64,10 @@ pub enum Error { · 24 unmodified lines
5464 PathInterpolation(#[from] git_config::path::interpolate::Error),
5565 }
5666
57−/// Utility type to keep pre-obtained configuration values.
67+/// Utility type to keep pre-obtained configuration values, only for those required during initial setup
68+/// and other basic operations that are common enough to warrant a permanent cache.
69+///
70+/// All other values are obtained lazily using OnceCell.
5871 #[derive(Clone)]
5972 pub(crate) struct Cache {
6073 pub resolved: crate::Config,
@@ -73,7 +86,7 @@ pub(crate) struct Cache { · 12 unmodified lines
7386 /// A lazily loaded rewrite list for remote urls
7487 pub url_rewrite: OnceCell<remote::url::Rewrite>,
7588 /// A lazily loaded mapping to know which url schemes to allow
76− #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
89+ #[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
7790 pub url_scheme: OnceCell<remote::url::SchemePermission>,
7891 /// The config section filter from the options used to initialize this instance. Keep these in sync!
7992 filter_config_section: fn(&git_config::file::Metadata) -> bool,
@@ -81,8 +94,9 @@ pub(crate) struct Cache { · 1 unmodified line
8194 pub object_kind_hint: Option<spec::parse::ObjectKindHint>,
8295 /// If true, we are on a case-insensitive file system.
8396 pub ignore_case: bool,
84− /// The path to the user-level excludes file to ignore certain files in the worktree.
85− pub excludes_file: Option<std::path::PathBuf>,
97+ /// If true, we should default what's possible if something is misconfigured, on case by case basis, to be more resilient.
98+ /// Also available in options! Keep in sync!
99+ pub lenient_config: bool,
86100 /// Define how we can use values obtained with `xdg_config(…)` and its `XDG_CONFIG_HOME` variable.
87101 xdg_config_home_env: git_sec::Permission,
88102 /// Define how we can use values obtained with `xdg_config(…)`. and its `HOME` variable.
git-repository/src/config/snapshot/_impls.rs+62−0
@@ -0,0 +1,62 @@
1+use std::{
2+ fmt::{Debug, Formatter},
3+ ops::{Deref, DerefMut},
4+};
5+
6+use crate::config::{CommitAutoRollback, Snapshot, SnapshotMut};
7+
8+impl Debug for Snapshot<'_> {
9+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
10+ f.write_str(&self.repo.config.resolved.to_string())
11+ }
12+}
13+
14+impl Debug for CommitAutoRollback<'_> {
15+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
16+ f.write_str(&self.repo.as_ref().expect("still present").config.resolved.to_string())
17+ }
18+}
19+
20+impl Debug for SnapshotMut<'_> {
21+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
22+ f.write_str(&self.config.to_string())
23+ }
24+}
25+
26+impl Drop for SnapshotMut<'_> {
27+ fn drop(&mut self) {
28+ if let Some(repo) = self.repo.take() {
29+ self.commit_inner(repo).ok();
30+ };
31+ }
32+}
33+
34+impl Drop for CommitAutoRollback<'_> {
35+ fn drop(&mut self) {
36+ if let Some(repo) = self.repo.take() {
37+ self.rollback_inner(repo).ok();
38+ }
39+ }
40+}
41+
42+impl Deref for SnapshotMut<'_> {
43+ type Target = git_config::File<'static>;
44+
45+ fn deref(&self) -> &Self::Target {
46+ &self.config
47+ }
48+}
49+
50+impl Deref for CommitAutoRollback<'_> {
51+ type Target = crate::Repository;
52+
53+ fn deref(&self) -> &Self::Target {
54+ self.repo.as_ref().expect("always present")
55+ }
56+}
57+
58+impl DerefMut for SnapshotMut<'_> {
59+ fn deref_mut(&mut self) -> &mut Self::Target {
60+ &mut self.config
61+ }
62+}
git-repository/src/config/snapshot/access.rs+58−0
@@ -1,5 +1,7 @@
1+use git_features::threading::OwnShared;
12 use std::borrow::Cow;
23
4+use crate::config::{CommitAutoRollback, SnapshotMut};
35 use crate::{
46 bstr::BStr,
57 config::{cache::interpolate_context, Snapshot},
@@ -90,3 +92,59 @@ impl<'repo> Snapshot<'repo> { · 84 unmodified lines
9092 &self.repo.config.resolved
9193 }
9294 }
95+
96+/// Utilities
97+impl<'repo> SnapshotMut<'repo> {
98+ /// Apply all changes made to this instance.
99+ ///
100+ /// Note that this would also happen once this instance is dropped, but using this method may be more intuitive and won't squelch errors
101+ /// in case the new configuration is partially invalid.
102+ pub fn commit(mut self) -> Result<&'repo mut crate::Repository, crate::config::Error> {
103+ let repo = self.repo.take().expect("always present here");
104+ self.commit_inner(repo)
105+ }
106+
107+ pub(crate) fn commit_inner(
108+ &mut self,
109+ repo: &'repo mut crate::Repository,
110+ ) -> Result<&'repo mut crate::Repository, crate::config::Error> {
111+ repo.config
112+ .reread_values_and_clear_caches(std::mem::take(&mut self.config).into())?;
113+ Ok(repo)
114+ }
115+
116+ /// Create a structure the temporarily commits the changes, but rolls them back when dropped.
117+ pub fn commit_auto_rollback(mut self) -> Result<CommitAutoRollback<'repo>, crate::config::Error> {
118+ let repo = self.repo.take().expect("this only runs once on consumption");
119+ let prev_config = OwnShared::clone(&repo.config.resolved);
120+
121+ Ok(CommitAutoRollback {
122+ repo: self.commit_inner(repo)?.into(),
123+ prev_config,
124+ })
125+ }
126+
127+ /// Don't apply any of the changes after consuming this instance, effectively forgetting them, returning the changed configuration.
128+ pub fn forget(mut self) -> git_config::File<'static> {
129+ self.repo.take();
130+ std::mem::take(&mut self.config)
131+ }
132+}
133+
134+/// Utilities
135+impl<'repo> CommitAutoRollback<'repo> {
136+ /// Rollback the changes previously applied and all values before the change.
137+ pub fn rollback(mut self) -> Result<&'repo mut crate::Repository, crate::config::Error> {
138+ let repo = self.repo.take().expect("still present, consumed only once");
139+ self.rollback_inner(repo)
140+ }
141+
142+ pub(crate) fn rollback_inner(
143+ &mut self,
144+ repo: &'repo mut crate::Repository,
145+ ) -> Result<&'repo mut crate::Repository, crate::config::Error> {
146+ repo.config
147+ .reread_values_and_clear_caches(OwnShared::clone(&self.prev_config))?;
148+ Ok(repo)
149+ }
150+}
git-repository/src/config/snapshot/apply_cli_overrides.rs+5−2
@@ -23,7 +23,10 @@ pub enum Error { · 22 unmodified lines
2323 impl SnapshotMut<'_> {
2424 /// Apply configuration values of the form `core.abbrev=5` or `remote.origin.url = foo` or `core.bool-implicit-true`
2525 /// to the repository configuration, marked with [source CLI][git_config::Source::Cli].
26− pub fn apply_cli_overrides(&mut self, values: impl IntoIterator<Item = impl AsRef<BStr>>) -> Result<(), Error> {
26+ pub fn apply_cli_overrides(
27+ &mut self,
28+ values: impl IntoIterator<Item = impl AsRef<BStr>>,
29+ ) -> Result<&mut Self, Error> {
2730 let mut file = git_config::File::new(git_config::file::Metadata::from(git_config::Source::Cli));
2831 for key_value in values {
2932 let key_value = key_value.as_ref();
@@ -44,6 +47,6 @@ impl SnapshotMut<'_> { · 14 unmodified lines
4447 );
4548 }
4649 self.config.append(file);
47− Ok(())
50+ Ok(self)
4851 }
4952 }
git-repository/src/config/snapshot/mod.rs+1−41
@@ -1,3 +1,4 @@
1+mod _impls;
12 mod access;
23
34 ///
@@ -5,44 +6,3 @@ pub mod apply_cli_overrides; · 1 unmodified line
56
67 ///
78 pub mod credential_helpers;
8−
9−mod _impls {
10− use std::{
11− fmt::{Debug, Formatter},
12− ops::{Deref, DerefMut},
13− };
14−
15− use crate::config::{Snapshot, SnapshotMut};
16−
17− impl Debug for Snapshot<'_> {
18− fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
19− f.write_str(&self.repo.config.resolved.to_string())
20− }
21− }
22−
23− impl Debug for SnapshotMut<'_> {
24− fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
25− f.write_str(&self.config.to_string())
26− }
27− }
28−
29− impl Drop for SnapshotMut<'_> {
30− fn drop(&mut self) {
31− self.repo.config.resolved = std::mem::take(&mut self.config).into();
32− }
33− }
34−
35− impl Deref for SnapshotMut<'_> {
36− type Target = git_config::File<'static>;
37−
38− fn deref(&self) -> &Self::Target {
39− &self.config
40− }
41− }
42−
43− impl DerefMut for SnapshotMut<'_> {
44− fn deref_mut(&mut self) -> &mut Self::Target {
45− &mut self.config
46− }
47− }
48−}
git-repository/src/head/mod.rs+8−5
@@ -87,19 +87,22 @@ mod remote { · 86 unmodified lines
8787 /// Remote
8888 impl<'repo> Head<'repo> {
8989 /// Return the remote with which the currently checked our reference can be handled as configured by `branch.<name>.remote|pushRemote`
90− /// or fall back to the non-branch specific remote configuration. `None` is returned if the head is detached or unborn.
90+ /// or fall back to the non-branch specific remote configuration. `None` is returned if the head is detached or unborn, so there is
91+ /// no branch specific remote.
9192 ///
9293 /// This is equivalent to calling [`Reference::remote(…)`][crate::Reference::remote()] and
9394 /// [`Repository::remote_default_name()`][crate::Repository::remote_default_name()] in order.
95+ ///
96+ /// Combine it with [`find_default_remote()`][crate::Repository::find_default_remote()] as fallback to handle detached heads,
97+ /// i.e. obtain a remote even in case of detached heads.
9498 pub fn into_remote(
9599 self,
96100 direction: remote::Direction,
97101 ) -> Option<Result<Remote<'repo>, remote::find::existing::Error>> {
98102 let repo = self.repo;
99− self.try_into_referent()?.remote(direction).or_else(|| {
100− repo.remote_default_name(direction)
101− .map(|name| repo.find_remote(name.as_ref()))
102− })
103+ self.try_into_referent()?
104+ .remote(direction)
105+ .or_else(|| repo.find_default_remote(direction))
103106 }
104107 }
105108 }
git-repository/src/open.rs+1−1
@@ -201,7 +201,7 @@ impl Options { · 200 unmodified lines
201201 /// If set, default is false, invalid configuration values will cause an error even if these can safely be defaulted.
202202 ///
203203 /// This is recommended for all applications that prefer correctness over usability.
204− /// `git` itself by defaults to strict configuration mode to let you know if configuration is incorrect.
204+ /// `git` itself defaults to strict configuration mode, flagging incorrect configuration immediately.
205205 pub fn strict_config(mut self, toggle: bool) -> Self {
206206 self.lenient_config = !toggle;
207207 self
git-repository/src/reference/edits.rs+5−20
@@ -55,35 +55,20 @@ pub mod delete { · 54 unmodified lines
5555
5656 use crate::Reference;
5757
58− mod error {
59− /// The error returned by [`Reference::delete()`][super::Reference::delete()].
60− #[derive(Debug, thiserror::Error)]
61− #[allow(missing_docs)]
62− pub enum Error {
63− #[error(transparent)]
64− ReferenceEdit(#[from] crate::reference::edit::Error),
65− }
66− }
67− pub use error::Error;
68− use git_lock::acquire::Fail;
69−
7058 impl<'repo> Reference<'repo> {
7159 /// Delete this reference or fail if it was changed since last observed.
7260 /// Note that this instance remains available in memory but probably shouldn't be used anymore.
73− pub fn delete(&self) -> Result<(), Error> {
74− self.repo.edit_reference(
75− RefEdit {
61+ pub fn delete(&self) -> Result<(), crate::reference::edit::Error> {
62+ self.repo
63+ .edit_reference(RefEdit {
7664 change: Change::Delete {
7765 expected: PreviousValue::MustExistAndMatch(self.inner.target.clone()),
7866 log: RefLog::AndReference,
7967 },
8068 name: self.inner.name.clone(),
8169 deref: false,
82− },
83− Fail::Immediately,
84− self.repo.committer_or_default(),
85− )?;
86− Ok(())
70+ })
71+ .map(|_| ())
8772 }
8873 }
8974 }
git-repository/src/reference/errors.rs+2−0
@@ -11,6 +11,8 @@ pub mod edit { · 10 unmodified lines
1111 FileTransactionCommit(#[from] git_ref::file::transaction::commit::Error),
1212 #[error(transparent)]
1313 NameValidation(#[from] git_validate::reference::name::Error),
14+ #[error("Could not interpret core.filesRefLockTimeout or core.packedRefsTimeout, it must be the number in milliseconds to wait for locks or negative to wait forever")]
15+ LockTimeoutConfiguration(#[from] git_config::value::Error),
1416 }
1517 }
1618
git-repository/src/reference/remote.rs+2−0
@@ -72,6 +72,8 @@ impl<'repo> Reference<'repo> { · 71 unmodified lines
7272 /// Like [`remote_name(…)`][Self::remote_name()], but configures the returned `Remote` with additional information like
7373 ///
7474 /// - `branch.<name>.merge` to know which branch on the remote side corresponds to this one for merging when pulling.
75+ ///
76+ /// It also handles if the remote is a configured URL, which has no name.
7577 pub fn remote(
7678 &self,
7779 direction: remote::Direction,
git-repository/src/remote/connect.rs+21−9
@@ -1,8 +1,6 @@
1−use git_protocol::transport::client::Transport;
2−
31 use crate::{remote::Connection, Progress, Remote};
2+use git_protocol::transport::client::Transport;
43
5−#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
64 mod error {
75 use crate::{bstr::BString, remote};
86
@@ -24,10 +22,9 @@ mod error { · 15 unmodified lines
2422 FileUrl(#[from] git_discover::is_git::Error),
2523 }
2624 }
27−#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
2825 pub use error::Error;
2926
30−/// Establishing connections to remote hosts
27+/// Establishing connections to remote hosts (without performing a git-handshake).
3128 impl<'repo> Remote<'repo> {
3229 /// Create a new connection using `transport` to communicate, with `progress` to indicate changes.
3330 ///
@@ -40,7 +37,7 @@ impl<'repo> Remote<'repo> { · 6 unmodified lines
4037 {
4138 Connection {
4239 remote: self,
43− credentials: None,
40+ authenticate: None,
4441 transport,
4542 progress,
4643 }
@@ -50,6 +47,10 @@ impl<'repo> Remote<'repo> { · 3 unmodified lines
5047 ///
5148 /// Note that the `protocol.version` configuration key affects the transport protocol used to connect,
5249 /// with `2` being the default.
50+ ///
51+ /// The transport used for connection can be configured via `transport_mut().configure()` assuming the actually
52+ /// used transport is well known. If that's not the case, the transport can be created by hand and passed to
53+ /// [to_connection_with_transport()][Self::to_connection_with_transport()].
5354 #[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
5455 #[git_protocol::maybe_async::maybe_async]
5556 pub async fn connect<P>(
@@ -60,7 +61,18 @@ impl<'repo> Remote<'repo> { · 4 unmodified lines
6061 where
6162 P: Progress,
6263 {
63− use git_protocol::transport::Protocol;
64+ let (url, version) = self.sanitized_url_and_version(direction)?;
65+ let transport = git_protocol::transport::connect(url, version).await?;
66+ Ok(self.to_connection_with_transport(transport, progress))
67+ }
68+
69+ /// Produce the sanitized URL and protocol version to use as obtained by querying the repository configuration.
70+ ///
71+ /// This can be useful when using custom transports to allow additional configuration.
72+ pub fn sanitized_url_and_version(
73+ &self,
74+ direction: crate::remote::Direction,
75+ ) -> Result<(git_url::Url, git_protocol::transport::Protocol), Error> {
6476 fn sanitize(mut url: git_url::Url) -> Result<git_url::Url, Error> {
6577 if url.scheme == git_url::Scheme::File {
6678 let mut dir = git_path::from_bstr(url.path.as_ref());
@@ -75,6 +87,7 @@ impl<'repo> Remote<'repo> { · 8 unmodified lines
7587 Ok(url)
7688 }
7789
90+ use git_protocol::transport::Protocol;
7891 let version = self
7992 .repo
8093 .config
@@ -101,7 +114,6 @@ impl<'repo> Remote<'repo> { · 20 unmodified lines
101114 scheme: url.scheme,
102115 });
103116 }
104− let transport = git_protocol::transport::connect(sanitize(url)?, version).await?;
105− Ok(self.to_connection_with_transport(transport, progress))
117+ Ok((sanitize(url)?, version))
106118 }
107119 }
git-repository/src/remote/connection/access.rs+48−0
@@ -0,0 +1,48 @@
1+use crate::{
2+ remote::{connection::AuthenticateFn, Connection},
3+ Remote,
4+};
5+
6+/// Builder
7+impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> {
8+ /// Set a custom credentials callback to provide credentials if the remotes require authentication.
9+ ///
10+ /// Otherwise we will use the git configuration to perform the same task as the `git credential` helper program,
11+ /// which is calling other helper programs in succession while resorting to a prompt to obtain credentials from the
12+ /// user.
13+ ///
14+ /// A custom function may also be used to prevent accessing resources with authentication.
15+ pub fn with_credentials(
16+ mut self,
17+ helper: impl FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a,
18+ ) -> Self {
19+ self.authenticate = Some(Box::new(helper));
20+ self
21+ }
22+}
23+
24+/// Access
25+impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> {
26+ /// A utility to return a function that will use this repository's configuration to obtain credentials, similar to
27+ /// what `git credential` is doing.
28+ ///
29+ /// It's meant to be used by users of the [`with_credentials()`][Self::with_credentials()] builder to gain access to the
30+ /// default way of handling credentials, which they can call as fallback.
31+ pub fn configured_credentials(
32+ &self,
33+ url: git_url::Url,
34+ ) -> Result<AuthenticateFn<'a>, crate::config::credential_helpers::Error> {
35+ let (mut cascade, _action_with_normalized_url, prompt_opts) =
36+ self.remote.repo.config_snapshot().credential_helpers(url)?;
37+ Ok(Box::new(move |action| cascade.invoke(action, prompt_opts.clone())) as AuthenticateFn<'_>)
38+ }
39+ /// Return the underlying remote that instantiate this connection.
40+ pub fn remote(&self) -> &Remote<'repo> {
41+ self.remote
42+ }
43+
44+ /// Provide a mutable transport to allow configuring it with [`configure()`][git_protocol::transport::client::TransportWithoutIO::configure()]
45+ pub fn transport_mut(&mut self) -> &mut T {
46+ &mut self.transport
47+ }
48+}
git-repository/src/remote/connection/fetch/config.rs+66−0
@@ -0,0 +1,66 @@
1+use super::Error;
2+use crate::Repository;
3+use std::convert::TryInto;
4+
5+pub fn index_threads(repo: &Repository) -> Result<Option<usize>, Error> {
6+ let lenient_config = repo.options.lenient_config;
7+ let message = "The configured pack.threads is invalid. It must be 0 or greater, with 0 turning it off";
8+ Ok(
9+ match repo
10+ .config
11+ .resolved
12+ .integer_filter("pack", None, "threads", &mut repo.filter_config_section())
13+ .transpose()
14+ {
15+ Ok(Some(0)) => Some(1),
16+ Ok(Some(n)) => match n.try_into() {
17+ Ok(n) => Some(n),
18+ Err(_) if lenient_config => None,
19+ Err(_) => {
20+ return Err(Error::Configuration {
21+ message: "The value for pack.threads is out of range",
22+ desired: n.into(),
23+ source: None,
24+ })
25+ }
26+ },
27+ Ok(None) => None,
28+ Err(_) if lenient_config => None,
29+ Err(err) => {
30+ return Err(Error::Configuration {
31+ message,
32+ desired: None,
33+ source: err.into(),
34+ })
35+ }
36+ },
37+ )
38+}
39+
40+pub fn pack_index_version(repo: &Repository) -> Result<git_pack::index::Version, Error> {
41+ use git_pack::index::Version;
42+ let lenient_config = repo.options.lenient_config;
43+ let message = "The configured pack.indexVersion is invalid. It must be 1 or 2, with 2 being the default";
44+ Ok(
45+ match repo.config.resolved.integer("pack", None, "indexVersion").transpose() {
46+ Ok(Some(v)) if v == 1 => Version::V1,
47+ Ok(Some(v)) if v == 2 => Version::V2,
48+ Ok(None) => Version::V2,
49+ Ok(Some(_)) | Err(_) if lenient_config => Version::V2,
50+ Ok(Some(v)) => {
51+ return Err(Error::Configuration {
52+ message,
53+ desired: v.into(),
54+ source: None,
55+ })
56+ }
57+ Err(err) => {
58+ return Err(Error::Configuration {
59+ message,
60+ desired: None,
61+ source: err.into(),
62+ })
63+ }
64+ },
65+ )
66+}
git-repository/src/remote/connection/fetch/mod.rs+263−0
@@ -0,0 +1,263 @@
1+use crate::remote::fetch::{DryRun, RefMap};
2+use crate::remote::{fetch, ref_map, Connection};
3+use crate::Progress;
4+use git_odb::FindExt;
5+use git_protocol::transport::client::Transport;
6+use std::sync::atomic::AtomicBool;
7+
8+mod error {
9+ /// The error returned by [`receive()`](super::Prepare::receive()).
10+ #[derive(Debug, thiserror::Error)]
11+ #[allow(missing_docs)]
12+ pub enum Error {
13+ #[error("{message}{}", desired.map(|n| format!(" (got {})", n)).unwrap_or_default())]
14+ Configuration {
15+ message: &'static str,
16+ desired: Option<i64>,
17+ source: Option<git_config::value::Error>,
18+ },
19+ #[error(transparent)]
20+ FetchResponse(#[from] git_protocol::fetch::response::Error),
21+ #[error(transparent)]
22+ Negotiate(#[from] super::negotiate::Error),
23+ #[error(transparent)]
24+ Client(#[from] git_protocol::transport::client::Error),
25+ #[error(transparent)]
26+ WritePack(#[from] git_pack::bundle::write::Error),
27+ #[error(transparent)]
28+ UpdateRefs(#[from] super::refs::update::Error),
29+ }
30+}
31+pub use error::Error;
32+
33+/// The status of the repository after the fetch operation
34+#[derive(Debug, Clone)]
35+pub enum Status {
36+ /// Nothing changed as the remote didn't have anything new.
37+ NoChange,
38+ /// There was at least one tip with a new object which we received.
39+ Change {
40+ /// Information collected while writing the pack and its index.
41+ write_pack_bundle: git_pack::bundle::write::Outcome,
42+ /// Information collected while updating references.
43+ update_refs: refs::update::Outcome,
44+ },
45+ /// A dry run was performed which leaves the local repository without any change
46+ /// nor will a pack have been received.
47+ DryRun {
48+ /// Information about what updates to refs would have been done.
49+ update_refs: refs::update::Outcome,
50+ },
51+}
52+
53+/// The outcome of receiving a pack via [`Prepare::receive()`].
54+#[derive(Debug, Clone)]
55+pub struct Outcome<'spec> {
56+ /// The result of the initial mapping of references, the prerequisite for any fetch.
57+ pub ref_map: RefMap<'spec>,
58+ /// The status of the operation to indicate what happened.
59+ pub status: Status,
60+}
61+
62+///
63+pub mod negotiate;
64+
65+impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P>
66+where
67+ T: Transport,
68+ P: Progress,
69+{
70+ /// Perform a handshake with the remote and obtain a ref-map with `options`, and from there one
71+ /// Note that at this point, the `transport` should already be configured using the [`transport_mut()`][Self::transport_mut()]
72+ /// method, as it will be consumed here.
73+ ///
74+ /// From there additional properties of the fetch can be adjusted to override the defaults that are configured via git-config.
75+ ///
76+ /// # Blocking Only
77+ ///
78+ /// Note that this implementation is currently limited to blocking mode as it relies on Drop semantics to close the connection
79+ /// should the fetch not be performed. Furthermore, there the code doing the fetch is inherently blocking so there is no benefit.
80+ /// It's best to unblock it by placing it into its own thread or offload it should usage in an async context be required.
81+ pub fn prepare_fetch(mut self, options: ref_map::Options) -> Result<Prepare<'remote, 'repo, T, P>, ref_map::Error> {
82+ let ref_map = self.ref_map_inner(options)?;
83+ Ok(Prepare {
84+ con: Some(self),
85+ ref_map,
86+ dry_run: fetch::DryRun::No,
87+ })
88+ }
89+}
90+
91+impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P>
92+where
93+ T: Transport,
94+ P: Progress,
95+{
96+ /// Receive the pack and perform the operation as configured by git via `git-config` or overridden by various builder methods.
97+ /// Return `Ok(None)` if there was nothing to do because all remote refs are at the same state as they are locally, or `Ok(Some(outcome))`
98+ /// to inform about all the changes that were made.
99+ ///
100+ /// ### Negotiation
101+ ///
102+ /// "fetch.negotiationAlgorithm" describes algorithms `git` uses currently, with the default being `consecutive` and `skipping` being
103+ /// experimented with. We currently implement something we could call 'naive' which works for now.
104+ pub fn receive(mut self, should_interrupt: &AtomicBool) -> Result<Outcome<'remote>, Error> {
105+ let mut con = self.con.take().expect("receive() can only be called once");
106+
107+ let handshake = &self.ref_map.handshake;
108+ let protocol_version = handshake.server_protocol_version;
109+
110+ let fetch = git_protocol::fetch::Command::Fetch;
111+ let fetch_features = fetch.default_features(protocol_version, &handshake.capabilities);
112+
113+ git_protocol::fetch::Response::check_required_features(protocol_version, &fetch_features)?;
114+ let sideband_all = fetch_features.iter().any(|(n, _)| *n == "sideband-all");
115+ let mut arguments = git_protocol::fetch::Arguments::new(protocol_version, fetch_features);
116+ let mut previous_response = None::<git_protocol::fetch::Response>;
117+ let mut round = 1;
118+ let progress = &mut con.progress;
119+ let repo = con.remote.repo;
120+
121+ let reader = 'negotiation: loop {
122+ progress.step();
123+ progress.set_name(format!("negotiate (round {})", round));
124+ let is_done = match negotiate::one_round(
125+ negotiate::Algorithm::Naive,
126+ round,
127+ repo,
128+ &self.ref_map,
129+ &mut arguments,
130+ previous_response.as_ref(),
131+ ) {
132+ Ok(_) if arguments.is_empty() => {
133+ git_protocol::fetch::indicate_end_of_interaction(&mut con.transport).ok();
134+ return Ok(Outcome {
135+ ref_map: std::mem::take(&mut self.ref_map),
136+ status: Status::NoChange,
137+ });
138+ }
139+ Ok(is_done) => is_done,
140+ Err(err) => {
141+ git_protocol::fetch::indicate_end_of_interaction(&mut con.transport).ok();
142+ return Err(err.into());
143+ }
144+ };
145+ round += 1;
146+ let mut reader = arguments.send(&mut con.transport, is_done)?;
147+ if sideband_all {
148+ setup_remote_progress(progress, &mut reader);
149+ }
150+ let response = git_protocol::fetch::Response::from_line_reader(protocol_version, &mut reader)?;
151+ if response.has_pack() {
152+ progress.step();
153+ progress.set_name("receiving pack");
154+ if !sideband_all {
155+ setup_remote_progress(progress, &mut reader);
156+ }
157+ break 'negotiation reader;
158+ } else {
159+ previous_response = Some(response);
160+ }
161+ };
162+
163+ let options = git_pack::bundle::write::Options {
164+ thread_limit: config::index_threads(repo)?,
165+ index_version: config::pack_index_version(repo)?,
166+ iteration_mode: git_pack::data::input::Mode::Verify,
167+ object_hash: con.remote.repo.object_hash(),
168+ };
169+
170+ let write_pack_bundle = if matches!(self.dry_run, fetch::DryRun::No) {
171+ Some(git_pack::Bundle::write_to_directory(
172+ reader,
173+ Some(repo.objects.store_ref().path().join("pack")),
174+ con.progress,
175+ should_interrupt,
176+ Some(Box::new({
177+ let repo = repo.clone();
178+ move |oid, buf| repo.objects.find(oid, buf).ok()
179+ })),
180+ options,
181+ )?)
182+ } else {
183+ drop(reader);
184+ None
185+ };
186+
187+ if matches!(protocol_version, git_protocol::transport::Protocol::V2) {
188+ git_protocol::fetch::indicate_end_of_interaction(&mut con.transport).ok();
189+ }
190+
191+ let update_refs = refs::update(
192+ repo,
193+ "fetch",
194+ &self.ref_map.mappings,
195+ con.remote.refspecs(crate::remote::Direction::Fetch),
196+ self.dry_run,
197+ )?;
198+
199+ Ok(Outcome {
200+ ref_map: std::mem::take(&mut self.ref_map),
201+ status: match write_pack_bundle {
202+ Some(write_pack_bundle) => Status::Change {
203+ write_pack_bundle,
204+ update_refs,
205+ },
206+ None => Status::DryRun { update_refs },
207+ },
208+ })
209+ }
210+}
211+
212+fn setup_remote_progress(
213+ progress: &mut impl Progress,
214+ reader: &mut Box<dyn git_protocol::transport::client::ExtendedBufRead + Unpin + '_>,
215+) {
216+ use git_protocol::transport::client::ExtendedBufRead;
217+ reader.set_progress_handler(Some(Box::new({
218+ let mut remote_progress = progress.add_child("remote");
219+ move |is_err: bool, data: &[u8]| {
220+ git_protocol::RemoteProgress::translate_to_progress(is_err, data, &mut remote_progress)
221+ }
222+ }) as git_protocol::transport::client::HandleProgress));
223+}
224+
225+mod config;
226+///
227+#[path = "update_refs/mod.rs"]
228+pub mod refs;
229+
230+/// A structure to hold the result of the handshake with the remote and configure the upcoming fetch operation.
231+pub struct Prepare<'remote, 'repo, T, P>
232+where
233+ T: Transport,
234+{
235+ con: Option<Connection<'remote, 'repo, T, P>>,
236+ ref_map: RefMap<'remote>,
237+ dry_run: fetch::DryRun,
238+}
239+
240+/// Builder
241+impl<'remote, 'repo, T, P> Prepare<'remote, 'repo, T, P>
242+where
243+ T: Transport,
244+{
245+ /// If dry run is enabled, no change to the repository will be made.
246+ ///
247+ /// This works by not actually fetching the pack after negotiating it, nor will refs be updated.
248+ pub fn with_dry_run(mut self, enabled: bool) -> Self {
249+ self.dry_run = enabled.then(|| fetch::DryRun::Yes).unwrap_or(DryRun::No);
250+ self
251+ }
252+}
253+
254+impl<'remote, 'repo, T, P> Drop for Prepare<'remote, 'repo, T, P>
255+where
256+ T: Transport,
257+{
258+ fn drop(&mut self) {
259+ if let Some(mut con) = self.con.take() {
260+ git_protocol::fetch::indicate_end_of_interaction(&mut con.transport).ok();
261+ }
262+ }
263+}
git-repository/src/remote/connection/fetch/negotiate.rs+60−0
@@ -0,0 +1,60 @@
1+/// The way the negotiation is performed
2+#[derive(Copy, Clone)]
3+pub(crate) enum Algorithm {
4+ /// Our very own implementation that probably should be replaced by one of the known algorithms soon.
5+ Naive,
6+}
7+
8+/// The error returned during negotiation.
9+#[derive(Debug, thiserror::Error)]
10+#[allow(missing_docs)]
11+pub enum Error {
12+ #[error("We were unable to figure out what objects the server should send after {rounds} round(s)")]
13+ NegotiationFailed { rounds: usize },
14+}
15+
16+/// Negotiate one round with `algo` by looking at `ref_map` and adjust `arguments` to contain the haves and wants.
17+/// If this is not the first round, the `previous_response` is set with the last recorded server response.
18+/// Returns `true` if the negotiation is done from our side so the server won't keep asking.
19+pub(crate) fn one_round(
20+ algo: Algorithm,
21+ round: usize,
22+ repo: &crate::Repository,
23+ ref_map: &crate::remote::fetch::RefMap<'_>,
24+ arguments: &mut git_protocol::fetch::Arguments,
25+ _previous_response: Option<&git_protocol::fetch::Response>,
26+) -> Result<bool, Error> {
27+ match algo {
28+ Algorithm::Naive => {
29+ assert_eq!(round, 1, "Naive always finishes after the first round, and claims.");
30+ let mut has_missing_tracking_branch = false;
31+ for mapping in &ref_map.mappings {
32+ let have_id = mapping.local.as_ref().and_then(|name| {
33+ repo.find_reference(name)
34+ .ok()
35+ .and_then(|r| r.target().try_id().map(ToOwned::to_owned))
36+ });
37+ match have_id {
38+ Some(have_id) if mapping.remote.as_id() != have_id => {
39+ arguments.want(mapping.remote.as_id());
40+ arguments.have(have_id);
41+ }
42+ Some(_) => {}
43+ None => {
44+ arguments.want(mapping.remote.as_id());
45+ has_missing_tracking_branch = true;
46+ }
47+ }
48+ }
49+
50+ if has_missing_tracking_branch {
51+ if let Ok(Some(r)) = repo.head_ref() {
52+ if let Some(id) = r.target().try_id() {
53+ arguments.have(id);
54+ }
55+ }
56+ }
57+ Ok(true)
58+ }
59+ }
60+}
git-repository/src/remote/connection/fetch/update_refs/mod.rs+149−0
@@ -0,0 +1,149 @@
1+use crate::remote::fetch;
2+use crate::remote::fetch::refs::update::Mode;
3+use crate::Repository;
4+use git_pack::Find;
5+use git_ref::transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog};
6+use git_ref::{Target, TargetRef};
7+use std::collections::BTreeMap;
8+use std::convert::TryInto;
9+use std::path::PathBuf;
10+
11+///
12+pub mod update;
13+
14+/// Information about the update of a single reference, corresponding the respective entry in [`RefMap::mappings`][crate::remote::fetch::RefMap::mappings].
15+#[derive(Debug, Clone, PartialEq, Eq)]
16+pub struct Update {
17+ /// The way the update was performed.
18+ pub mode: update::Mode,
19+ /// The index to the edit that was created from the corresponding mapping, or `None` if there was no local ref.
20+ pub edit_index: Option<usize>,
21+}
22+
23+impl From<update::Mode> for Update {
24+ fn from(mode: Mode) -> Self {
25+ Update { mode, edit_index: None }
26+ }
27+}
28+
29+/// Update all refs as derived from `mappings` and produce an `Outcome` informing about all applied changes in detail, with each
30+/// [`update`][Update] corresponding to the [`fetch::Mapping`] of at the same index.
31+/// If `dry_run` is true, ref transactions won't actually be applied, but are assumed to work without error so the underlying
32+/// `repo` is not actually changed. Also it won't perform an 'object exists' check as these are likely not to exist as the pack
33+/// wasn't fetched either.
34+/// `action` is the prefix used for reflog entries, and is typically "fetch".
35+///
36+/// It can be used to produce typical information that one is used to from `git fetch`.
37+pub(crate) fn update(
38+ repo: &Repository,
39+ action: &str,
40+ mappings: &[fetch::Mapping],
41+ refspecs: &[git_refspec::RefSpec],
42+ dry_run: fetch::DryRun,
43+) -> Result<update::Outcome, update::Error> {
44+ let mut edits = Vec::new();
45+ let mut updates = Vec::new();
46+
47+ for fetch::Mapping {
48+ remote,
49+ local,
50+ spec_index,
51+ } in mappings
52+ {
53+ let remote_id = remote.as_id();
54+ if dry_run == fetch::DryRun::No && !repo.objects.contains(remote_id) {
55+ updates.push(update::Mode::RejectedSourceObjectNotFound { id: remote_id.into() }.into());
56+ continue;
57+ }
58+ let checked_out_branches = worktree_branches(repo)?;
59+ let (mode, edit_index) = match local {
60+ Some(name) => {
61+ let (mode, reflog_message, name) = match repo.try_find_reference(name)? {
62+ Some(existing) => {
63+ if let Some(wt_dir) = checked_out_branches.get(existing.name()) {
64+ let mode = update::Mode::RejectedCurrentlyCheckedOut {
65+ worktree_dir: wt_dir.to_owned(),
66+ };
67+ updates.push(mode.into());
68+ continue;
69+ }
70+ match existing.target() {
71+ TargetRef::Symbolic(_) => {
72+ updates.push(update::Mode::RejectedSymbolic.into());
73+ continue;
74+ }
75+ TargetRef::Peeled(local_id) => {
76+ let (mode, reflog_message) = if local_id == remote_id {
77+ (update::Mode::NoChangeNeeded, "no update will be performed")
78+ } else if refspecs[*spec_index].allow_non_fast_forward() {
79+ let reflog_msg = match existing.name().category() {
80+ Some(git_ref::Category::Tag) => "updating tag",
81+ _ => "forced-update",
82+ };
83+ (update::Mode::Forced, reflog_msg)
84+ } else if let Some(git_ref::Category::Tag) = existing.name().category() {
85+ updates.push(update::Mode::RejectedTagUpdate.into());
86+ continue;
87+ } else {
88+ todo!("check for fast-forward (is local an ancestor of remote?)")
89+ };
90+ (mode, reflog_message, existing.name().to_owned())
91+ }
92+ }
93+ }
94+ None => {
95+ let name: git_ref::FullName = name.try_into()?;
96+ let reflog_msg = match name.category() {
97+ Some(git_ref::Category::Tag) => "storing tag",
98+ Some(git_ref::Category::LocalBranch) => "storing head",
99+ _ => "storing ref",
100+ };
101+ (update::Mode::New, reflog_msg, name)
102+ }
103+ };
104+ let edit = RefEdit {
105+ change: Change::Update {
106+ log: LogChange {
107+ mode: RefLog::AndReference,
108+ force_create_reflog: false,
109+ message: format!("{}: {}", action, reflog_message).into(),
110+ },
111+ expected: PreviousValue::ExistingMustMatch(Target::Peeled(remote_id.into())),
112+ new: Target::Peeled(remote_id.into()),
113+ },
114+ name,
115+ deref: false,
116+ };
117+ let edit_index = edits.len();
118+ edits.push(edit);
119+ (mode, Some(edit_index))
120+ }
121+ None => (update::Mode::NoChangeNeeded, None),
122+ };
123+ updates.push(Update { mode, edit_index })
124+ }
125+
126+ let edits = match dry_run {
127+ fetch::DryRun::No => repo.edit_references(edits)?,
128+ fetch::DryRun::Yes => edits,
129+ };
130+
131+ Ok(update::Outcome { edits, updates })
132+}
133+
134+fn worktree_branches(repo: &Repository) -> Result<BTreeMap<git_ref::FullName, PathBuf>, update::Error> {
135+ let mut map = BTreeMap::new();
136+ if let Some((wt_dir, head_ref)) = repo.work_dir().zip(repo.head_ref().ok().flatten()) {
137+ map.insert(head_ref.inner.name, wt_dir.to_owned());
138+ }
139+ for proxy in repo.worktrees()? {
140+ let repo = proxy.into_repo_with_possibly_inaccessible_worktree()?;
141+ if let Some((wt_dir, head_ref)) = repo.work_dir().zip(repo.head_ref().ok().flatten()) {
142+ map.insert(head_ref.inner.name, wt_dir.to_owned());
143+ }
144+ }
145+ Ok(map)
146+}
147+
148+#[cfg(test)]
149+mod tests;
git-repository/src/remote/connection/fetch/update_refs/tests.rs+263−0
@@ -0,0 +1,263 @@
1+mod update {
2+ use crate as git;
3+ use git_testtools::{hex_to_id, Result};
4+
5+ fn base_repo_path() -> String {
6+ git::path::realpath(
7+ git_testtools::scripted_fixture_repo_read_only("make_remote_repos.sh")
8+ .unwrap()
9+ .join("base"),
10+ )
11+ .unwrap()
12+ .to_string_lossy()
13+ .into_owned()
14+ }
15+
16+ fn repo(name: &str) -> git::Repository {
17+ let dir = git_testtools::scripted_fixture_repo_read_only_with_args("make_fetch_repos.sh", [base_repo_path()])
18+ .unwrap();
19+ git::open_opts(dir.join(name), git::open::Options::isolated()).unwrap()
20+ }
21+ use crate::remote::fetch;
22+ use git_ref::transaction::Change;
23+ use git_ref::TargetRef;
24+
25+ #[test]
26+ fn various_valid_updates() {
27+ let repo = repo("two-origins");
28+ // TODO: test reflog message (various cases if it's new)
29+ for (spec, expected_mode, reflog_message, detail) in [
30+ (
31+ "refs/heads/main:refs/remotes/origin/main",
32+ fetch::refs::update::Mode::NoChangeNeeded,
33+ Some("no update will be performed"),
34+ "these refs are en-par since the initial clone",
35+ ),
36+ (
37+ "refs/heads/main",
38+ fetch::refs::update::Mode::NoChangeNeeded,
39+ None,
40+ "without local destination ref there is nothing to do for us, ever (except for FETCH_HEADs) later",
41+ ),
42+ (
43+ "refs/heads/main:refs/remotes/origin/new-main",
44+ fetch::refs::update::Mode::New,
45+ Some("storing ref"),
46+ "the destination branch doesn't exist and needs to be created",
47+ ),
48+ (
49+ "refs/heads/main:refs/heads/feature",
50+ fetch::refs::update::Mode::New,
51+ Some("storing head"),
52+ "reflog messages are specific to the type of branch stored, to some limited extend",
53+ ),
54+ (
55+ "refs/heads/main:refs/tags/new-tag",
56+ fetch::refs::update::Mode::New,
57+ Some("storing tag"),
58+ "reflog messages are specific to the type of branch stored, to some limited extend",
59+ ),
60+ (
61+ "+refs/heads/main:refs/remotes/origin/new-main",
62+ fetch::refs::update::Mode::New,
63+ Some("storing ref"),
64+ "just to validate that we really are in dry-run mode, or else this ref would be present now",
65+ ),
66+ (
67+ "+refs/heads/main:refs/remotes/origin/g",
68+ fetch::refs::update::Mode::Forced,
69+ Some("forced-update"),
70+ "a forced non-fastforward (main goes backwards)",
71+ ),
72+ (
73+ "+refs/heads/main:refs/tags/b-tag",
74+ fetch::refs::update::Mode::Forced,
75+ Some("updating tag"),
76+ "tags can only be forced",
77+ ),
78+ (
79+ "refs/heads/main:refs/tags/b-tag",
80+ fetch::refs::update::Mode::RejectedTagUpdate,
81+ None,
82+ "otherwise a tag is always refusing itself to be overwritten (no-clobber)",
83+ ),
84+ (
85+ "+refs/remotes/origin/g:refs/heads/main",
86+ fetch::refs::update::Mode::RejectedCurrentlyCheckedOut {
87+ worktree_dir: repo.work_dir().expect("present").to_owned(),
88+ },
89+ None,
90+ "checked out branches cannot be written, as it requires a merge of sorts which isn't done here",
91+ ),
92+ (
93+ "ffffffffffffffffffffffffffffffffffffffff:refs/heads/invalid-source-object",
94+ fetch::refs::update::Mode::RejectedSourceObjectNotFound {
95+ id: hex_to_id("ffffffffffffffffffffffffffffffffffffffff"),
96+ },
97+ None,
98+ "checked out branches cannot be written, as it requires a merge of sorts which isn't done here",
99+ ),
100+ // ( // TODO: make fast-forwards work
101+ // "refs/remotes/origin/g:refs/heads/not-currently-checked-out",
102+ // fetch::refs::update::Mode::FastForward,
103+ // true,
104+ // "a fast-forward only fast-forward situation, all good",
105+ // ),
106+ ] {
107+ let (mapping, specs) = mapping_from_spec(spec, &repo);
108+ let out = fetch::refs::update(
109+ &repo,
110+ "action",
111+ &mapping,
112+ &specs,
113+ reflog_message.map(|_| fetch::DryRun::Yes).unwrap_or(fetch::DryRun::No),
114+ )
115+ .unwrap();
116+
117+ assert_eq!(
118+ out.updates,
119+ vec![fetch::refs::Update {
120+ mode: expected_mode.clone(),
121+ edit_index: reflog_message.map(|_| 0),
122+ }],
123+ "{spec:?}: {detail}"
124+ );
125+ assert_eq!(out.edits.len(), reflog_message.map(|_| 1).unwrap_or(0));
126+ if let Some(reflog_message) = reflog_message {
127+ let edit = &out.edits[0];
128+ match &edit.change {
129+ Change::Update { log, .. } => {
130+ assert_eq!(
131+ log.message,
132+ format!("action: {}", reflog_message),
133+ "{}: reflog messages are specific and we emulate git word for word",
134+ spec
135+ );
136+ }
137+ _ => unreachable!("only updates"),
138+ }
139+ }
140+ }
141+ }
142+
143+ #[test]
144+ fn checked_out_branches_in_worktrees_are_rejected_with_additional_infromation() -> Result {
145+ let root = git_path::realpath(git_testtools::scripted_fixture_repo_read_only_with_args(
146+ "make_fetch_repos.sh",
147+ [base_repo_path()],
148+ )?)?;
149+ let repo = root.join("worktree-root");
150+ let repo = git::open_opts(repo, git::open::Options::isolated())?;
151+ for (branch, path_from_root) in [
152+ ("main", "worktree-root"),
153+ ("wt-a-nested", "prev/wt-a-nested"),
154+ ("wt-a", "wt-a"),
155+ ("nested-wt-b", "wt-a/nested-wt-b"),
156+ ("wt-c-locked", "wt-c-locked"),
157+ ("wt-deleted", "wt-deleted"),
158+ ] {
159+ let spec = format!("refs/heads/main:refs/heads/{}", branch);
160+ let (mappings, specs) = mapping_from_spec(&spec, &repo);
161+ let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::Yes)?;
162+
163+ assert_eq!(
164+ out.updates,
165+ vec![fetch::refs::Update {
166+ mode: fetch::refs::update::Mode::RejectedCurrentlyCheckedOut {
167+ worktree_dir: root.join(path_from_root),
168+ },
169+ edit_index: None,
170+ }],
171+ "{}: checked-out checks are done before checking if a change would actually be required (here it isn't)", spec
172+ );
173+ assert_eq!(out.edits.len(), 0);
174+ }
175+ Ok(())
176+ }
177+
178+ #[test]
179+ fn symbolic_refs_are_never_written() {
180+ let repo = repo("two-origins");
181+ let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/heads/symbolic", &repo);
182+ let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::Yes).unwrap();
183+
184+ assert_eq!(
185+ out.updates,
186+ vec![fetch::refs::Update {
187+ mode: fetch::refs::update::Mode::RejectedSymbolic,
188+ edit_index: None,
189+ }],
190+ "this also protects from writing HEAD, which should in theory be impossible to get from a refspec as it normalizes partial ref names"
191+ );
192+ assert_eq!(out.edits.len(), 0);
193+ }
194+
195+ #[test]
196+ #[should_panic]
197+ fn fast_forward_is_not_implemented_yet_but_should_be_denied() {
198+ let repo = repo("two-origins");
199+ let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/remotes/origin/g", &repo);
200+ let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::Yes).unwrap();
201+
202+ assert_eq!(
203+ out.updates,
204+ vec![fetch::refs::Update {
205+ mode: fetch::refs::update::Mode::RejectedNonFastForward,
206+ edit_index: Some(0),
207+ }]
208+ );
209+ assert_eq!(out.edits.len(), 1);
210+ }
211+
212+ fn mapping_from_spec(spec: &str, repo: &git::Repository) -> (Vec<fetch::Mapping>, Vec<git::refspec::RefSpec>) {
213+ let spec = git_refspec::parse(spec.into(), git_refspec::parse::Operation::Fetch).unwrap();
214+ let group = git_refspec::MatchGroup::from_fetch_specs(Some(spec));
215+ let references = repo.references().unwrap();
216+ let references: Vec<_> = references.all().unwrap().map(|r| into_remote_ref(r.unwrap())).collect();
217+ let mappings = group
218+ .match_remotes(references.iter().map(remote_ref_to_item))
219+ .mappings
220+ .into_iter()
221+ .map(|m| fetch::Mapping {
222+ remote: m
223+ .item_index
224+ .map(|idx| fetch::Source::Ref(references[idx].clone()))
225+ .unwrap_or_else(|| match m.lhs {
226+ git_refspec::match_group::SourceRef::ObjectId(id) => fetch::Source::ObjectId(id),
227+ _ => unreachable!("not a ref, must be id: {:?}", m),
228+ }),
229+ local: m.rhs.map(|r| r.into_owned()),
230+ spec_index: m.spec_index,
231+ })
232+ .collect();
233+ (mappings, vec![spec.to_owned()])
234+ }
235+
236+ fn into_remote_ref(mut r: git::Reference<'_>) -> git_protocol::fetch::Ref {
237+ let full_ref_name = r.name().as_bstr().into();
238+ match r.target() {
239+ TargetRef::Peeled(id) => git_protocol::fetch::Ref::Direct {
240+ full_ref_name,
241+ object: id.into(),
242+ },
243+ TargetRef::Symbolic(name) => {
244+ let target = name.as_bstr().into();
245+ let id = r.peel_to_id_in_place().unwrap();
246+ git_protocol::fetch::Ref::Symbolic {
247+ full_ref_name,
248+ target,
249+ object: id.detach(),
250+ }
251+ }
252+ }
253+ }
254+
255+ fn remote_ref_to_item(r: &git_protocol::fetch::Ref) -> git_refspec::match_group::Item<'_> {
256+ let (full_ref_name, target, object) = r.unpack();
257+ git_refspec::match_group::Item {
258+ full_ref_name,
259+ target,
260+ object,
261+ }
262+ }
263+}
git-repository/src/remote/connection/fetch/update_refs/update.rs+92−0
@@ -0,0 +1,92 @@
1+use crate::remote::fetch;
2+use std::path::PathBuf;
3+
4+mod error {
5+ /// The error returned when updating references.
6+ #[derive(Debug, thiserror::Error)]
7+ #[allow(missing_docs)]
8+ pub enum Error {
9+ #[error(transparent)]
10+ FindReference(#[from] crate::reference::find::Error),
11+ #[error("A remote reference had a name that wasn't considered valid. Corrupt remote repo or insufficient checks on remote?")]
12+ InvalidRefName(#[from] git_validate::refname::Error),
13+ #[error("Failed to update references to their new position to match their remote locations")]
14+ EditReferences(#[from] crate::reference::edit::Error),
15+ #[error("Failed to read or iterate worktree dir")]
16+ WorktreeListing(#[from] std::io::Error),
17+ #[error("Could not open worktree repository")]
18+ OpenWorktreeRepo(#[from] crate::open::Error),
19+ }
20+}
21+
22+pub use error::Error;
23+
24+/// The outcome of the refs-update operation at the end of a fetch.
25+#[derive(Debug, Clone)]
26+pub struct Outcome {
27+ /// All edits that were performed to update local refs.
28+ pub edits: Vec<git_ref::transaction::RefEdit>,
29+ /// Each update provides more information about what happened to the corresponding mapping.
30+ /// Use [`iter_mapping_updates()`][Self::iter_mapping_updates()] to recombine the update information with ref-edits and their
31+ /// mapping.
32+ pub updates: Vec<super::Update>,
33+}
34+
35+/// Describe the way a ref was updated
36+#[derive(Debug, Clone, PartialEq, Eq)]
37+pub enum Mode {
38+ /// No change was attempted as the remote ref didn't change compared to the current ref, or because no remote ref was specified
39+ /// in the ref-spec.
40+ NoChangeNeeded,
41+ /// The old ref's commit was an ancestor of the new one, allowing for a fast-forward without a merge.
42+ FastForward,
43+ /// The ref was set to point to the new commit from the remote without taking into consideration its ancestry.
44+ Forced,
45+ /// A new ref has been created as there was none before.
46+ New,
47+ /// The object id to set the target reference to could not be found.
48+ RejectedSourceObjectNotFound {
49+ /// The id of the object that didn't exist in the object database, even though it should since it should be part of the pack.
50+ id: git_hash::ObjectId,
51+ },
52+ /// Tags can never be overwritten (whether the new object would be a fast-forward or not, or unchanged), unless the refspec
53+ /// specifies force.
54+ RejectedTagUpdate,
55+ /// The reference update would not have been a fast-forward, and force is not specified in the ref-spec.
56+ RejectedNonFastForward,
57+ /// The update of a local symbolic reference was rejected.
58+ RejectedSymbolic,
59+ /// The update was rejected because the branch is checked out in the given worktree_dir.
60+ ///
61+ /// Note that the check applies to any known worktree, whether it's present on disk or not.
62+ RejectedCurrentlyCheckedOut {
63+ /// The path to the worktree directory where the branch is checked out.
64+ worktree_dir: PathBuf,
65+ },
66+}
67+
68+impl Outcome {
69+ /// Produce an iterator over all information used to produce the this outcome, ref-update by ref-update, using the `mappings`
70+ /// used when producing the ref update.
71+ pub fn iter_mapping_updates<'a, 'b>(
72+ &self,
73+ mappings: &'a [fetch::Mapping],
74+ refspecs: &'b [git_refspec::RefSpec],
75+ ) -> impl Iterator<
76+ Item = (
77+ &super::Update,
78+ &'a fetch::Mapping,
79+ &'b git_refspec::RefSpec,
80+ Option<&git_ref::transaction::RefEdit>,
81+ ),
82+ > {
83+ self.updates.iter().zip(mappings.iter()).map(move |(update, mapping)| {
84+ (
85+ update,
86+ mapping,
87+ &refspecs[mapping.spec_index],
88+ update.edit_index.and_then(|idx| self.edits.get(idx)),
89+ )
90+ })
91+ }
92+}
git-repository/src/remote/connection/mod.rs+8−48
@@ -5,8 +5,8 @@ pub(crate) struct HandshakeWithRefs { · 4 unmodified lines
55 refs: Vec<git_protocol::fetch::Ref>,
66 }
77
8−/// A function that performs a given credential action.
9−pub type CredentialsFn<'a> = Box<dyn FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a>;
8+/// A function that performs a given credential action, trying to obtain credentials for an operation that needs it.
9+pub type AuthenticateFn<'a> = Box<dyn FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a>;
1010
1111 /// A type to represent an ongoing connection to a remote host, typically with the connection already established.
1212 ///
@@ -14,56 +14,16 @@ pub type CredentialsFn<'a> = Box<dyn FnMut(git_credentials::helper::Action) -> g · 1 unmodified line
1414 /// much like a remote procedure call.
1515 pub struct Connection<'a, 'repo, T, P> {
1616 pub(crate) remote: &'a Remote<'repo>,
17− pub(crate) credentials: Option<CredentialsFn<'a>>,
17+ pub(crate) authenticate: Option<AuthenticateFn<'a>>,
1818 pub(crate) transport: T,
1919 pub(crate) progress: P,
2020 }
2121
22−mod access {
23− use crate::{
24− remote::{connection::CredentialsFn, Connection},
25− Remote,
26− };
27−
28− /// Builder
29− impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> {
30− /// Set a custom credentials callback to provide credentials if the remotes require authentication.
31− ///
32− /// Otherwise we will use the git configuration to perform the same task as the `git credential` helper program,
33− /// which is calling other helper programs in succession while resorting to a prompt to obtain credentials from the
34− /// user.
35− ///
36− /// A custom function may also be used to prevent accessing resources with authentication.
37− pub fn with_credentials(
38− mut self,
39− helper: impl FnMut(git_credentials::helper::Action) -> git_credentials::protocol::Result + 'a,
40− ) -> Self {
41− self.credentials = Some(Box::new(helper));
42− self
43− }
44− }
45−
46− /// Access
47− impl<'a, 'repo, T, P> Connection<'a, 'repo, T, P> {
48− /// A utility to return a function that will use this repository's configuration to obtain credentials, similar to
49− /// what `git credential` is doing.
50− ///
51− /// It's meant to be used by users of the [`with_credentials()`][Self::with_credentials()] builder to gain access to the
52− /// default way of handling credentials, which they can call as fallback.
53− pub fn configured_credentials(
54− &self,
55− url: git_url::Url,
56− ) -> Result<CredentialsFn<'a>, crate::config::credential_helpers::Error> {
57− let (mut cascade, _action_with_normalized_url, prompt_opts) =
58− self.remote.repo.config_snapshot().credential_helpers(url)?;
59− Ok(Box::new(move |action| cascade.invoke(action, prompt_opts.clone())) as CredentialsFn<'_>)
60− }
61− /// Drop the transport and additional state to regain the original remote.
62− pub fn remote(&self) -> &Remote<'repo> {
63− self.remote
64− }
65− }
66−}
22+mod access;
6723
6824 ///
6925 pub mod ref_map;
26+
27+///
28+#[cfg(feature = "blocking-network-client")]
29+pub mod fetch;
git-repository/src/remote/connection/ref_map.rs+68−9
@@ -1,5 +1,7 @@
1+use crate::bstr::{BString, ByteVec};
12 use git_features::progress::Progress;
23 use git_protocol::transport::client::Transport;
4+use std::collections::HashSet;
35
46 use crate::remote::{connection::HandshakeWithRefs, fetch, Connection, Direction};
57
@@ -19,6 +21,27 @@ pub enum Error { · 13 unmodified lines
1921 MappingValidation(#[from] git_refspec::match_group::validate::Error),
2022 }
2123
24+/// For use in [`Connection::ref_map()`].
25+#[derive(Debug, Clone)]
26+pub struct Options {
27+ /// Use a two-component prefix derived from the ref-spec's source, like `refs/heads/` to let the server pre-filter refs
28+ /// with great potential for savings in traffic and local CPU time. Defaults to `true`.
29+ pub prefix_from_spec_as_filter_on_remote: bool,
30+ /// Parameters in the form of `(name, optional value)` to add to the handshake.
31+ ///
32+ /// This is useful in case of custom servers.
33+ pub handshake_parameters: Vec<(String, Option<String>)>,
34+}
35+
36+impl Default for Options {
37+ fn default() -> Self {
38+ Options {
39+ prefix_from_spec_as_filter_on_remote: true,
40+ handshake_parameters: Default::default(),
41+ }
42+ }
43+}
44+
2245 impl<'remote, 'repo, T, P> Connection<'remote, 'repo, T, P>
2346 where
2447 T: Transport,
@@ -31,16 +54,31 @@ where · 6 unmodified lines
3154 /// with the local tracking branch of these tips (if available).
3255 ///
3356 /// Note that this doesn't fetch the objects mentioned in the tips nor does it make any change to underlying repository.
57+ ///
58+ /// # Consumption
59+ ///
60+ /// Due to management of the transport, it's cleanest to only use it for a single interaction. Thus it's consumed along with
61+ /// the connection.
3462 #[git_protocol::maybe_async::maybe_async]
35− pub async fn ref_map(mut self) -> Result<fetch::RefMap<'remote>, Error> {
36− let res = self.ref_map_inner().await;
37− git_protocol::fetch::indicate_end_of_interaction(&mut self.transport).await?;
63+ pub async fn ref_map(mut self, options: Options) -> Result<fetch::RefMap<'remote>, Error> {
64+ let res = self.ref_map_inner(options).await;
65+ git_protocol::fetch::indicate_end_of_interaction(&mut self.transport)
66+ .await
67+ .ok();
3868 res
3969 }
4070
4171 #[git_protocol::maybe_async::maybe_async]
42− async fn ref_map_inner(&mut self) -> Result<fetch::RefMap<'remote>, Error> {
43− let remote = self.fetch_refs().await?;
72+ pub(crate) async fn ref_map_inner(
73+ &mut self,
74+ Options {
75+ prefix_from_spec_as_filter_on_remote,
76+ handshake_parameters,
77+ }: Options,
78+ ) -> Result<fetch::RefMap<'remote>, Error> {
79+ let remote = self
80+ .fetch_refs(prefix_from_spec_as_filter_on_remote, handshake_parameters)
81+ .await?;
4482 let group = git_refspec::MatchGroup::from_fetch_specs(self.remote.fetch_specs.iter().map(|s| s.to_ref()));
4583 let (res, fixes) = group
4684 .match_remotes(remote.refs.iter().map(|r| {
@@ -77,9 +115,13 @@ where · 30 unmodified lines
77115 })
78116 }
79117 #[git_protocol::maybe_async::maybe_async]
80− async fn fetch_refs(&mut self) -> Result<HandshakeWithRefs, Error> {
118+ async fn fetch_refs(
119+ &mut self,
120+ filter_by_prefix: bool,
121+ extra_parameters: Vec<(String, Option<String>)>,
122+ ) -> Result<HandshakeWithRefs, Error> {
81123 let mut credentials_storage;
82− let authenticate = match self.credentials.as_mut() {
124+ let authenticate = match self.authenticate.as_mut() {
83125 Some(f) => f,
84126 None => {
85127 let url = self
@@ -95,15 +137,32 @@ where · 9 unmodified lines
95137 }
96138 };
97139 let mut outcome =
98− git_protocol::fetch::handshake(&mut self.transport, authenticate, Vec::new(), &mut self.progress).await?;
140+ git_protocol::fetch::handshake(&mut self.transport, authenticate, extra_parameters, &mut self.progress)
141+ .await?;
99142 let refs = match outcome.refs.take() {
100143 Some(refs) => refs,
101144 None => {
145+ let specs = &self.remote.fetch_specs;
102146 git_protocol::fetch::refs(
103147 &mut self.transport,
104148 outcome.server_protocol_version,
105149 &outcome.capabilities,
106− |_a, _b, _c| Ok(git_protocol::fetch::delegate::LsRefsAction::Continue),
150+ |_capabilities, arguments, _features| {
151+ if filter_by_prefix {
152+ let mut seen = HashSet::new();
153+ for spec in specs {
154+ let spec = spec.to_ref();
155+ if seen.insert(spec.instruction()) {
156+ if let Some(prefix) = spec.prefix() {
157+ let mut arg: BString = "ref-prefix ".into();
158+ arg.push_str(prefix);
159+ arguments.push(arg)
160+ }
161+ }
162+ }
163+ }
164+ Ok(git_protocol::fetch::delegate::LsRefsAction::Continue)
165+ },
107166 &mut self.progress,
108167 )
109168 .await?
git-repository/src/remote/fetch.rs+59−0
@@ -0,0 +1,59 @@
1+use crate::bstr::BString;
2+
3+/// If `Yes`, don't really make changes but do as much as possible to get an idea of what would be done.
4+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
5+#[cfg(feature = "blocking-network-client")]
6+pub(crate) enum DryRun {
7+ /// Enable dry-run mode and don't actually change the underlying repository in any way.
8+ Yes,
9+ /// Run the operation like normal, making changes to the underlying repository.
10+ No,
11+}
12+
13+/// Information about the relationship between our refspecs, and remote references with their local counterparts.
14+#[derive(Default, Debug, Clone)]
15+pub struct RefMap<'spec> {
16+ /// A mapping between a remote reference and a local tracking branch.
17+ pub mappings: Vec<Mapping>,
18+ /// Information about the fixes applied to the `mapping` due to validation and sanitization.
19+ pub fixes: Vec<git_refspec::match_group::validate::Fix<'spec>>,
20+ /// All refs advertised by the remote.
21+ pub remote_refs: Vec<git_protocol::fetch::Ref>,
22+ /// Additional information provided by the server as part of the handshake.
23+ ///
24+ /// Note that the `refs` field is always `None` as the refs are placed in `remote_refs`.
25+ pub handshake: git_protocol::fetch::handshake::Outcome,
26+}
27+
28+/// Either an object id that the remote has or the matched remote ref itself.
29+#[derive(Debug, Clone)]
30+pub enum Source {
31+ /// An object id, as the matched ref-spec was an object id itself.
32+ ObjectId(git_hash::ObjectId),
33+ /// The remote reference that matched the ref-specs name.
34+ Ref(git_protocol::fetch::Ref),
35+}
36+
37+impl Source {
38+ /// Return either the direct object id we refer to or the direct target that a reference refers to.
39+ pub fn as_id(&self) -> &git_hash::oid {
40+ match self {
41+ Source::ObjectId(id) => id,
42+ Source::Ref(r) => r.unpack().1,
43+ }
44+ }
45+}
46+
47+/// A mapping between a single remote reference and its advertised objects to a local destination which may or may not exist.
48+#[derive(Debug, Clone)]
49+pub struct Mapping {
50+ /// The reference on the remote side, along with information about the objects they point to as advertised by the server.
51+ pub remote: Source,
52+ /// The local tracking reference to update after fetching the object visible via `remote`.
53+ pub local: Option<BString>,
54+ /// The index into the fetch ref-specs used to produce the mapping, allowing it to be recovered.
55+ pub spec_index: usize,
56+}
57+
58+#[cfg(feature = "blocking-network-client")]
59+pub use super::connection::fetch::{negotiate, refs, Error, Outcome, Prepare, Status};
git-repository/src/remote/mod.rs+1−38
@@ -27,44 +27,7 @@ pub mod init; · 26 unmodified lines
2727
2828 ///
2929 #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))]
30−pub mod fetch {
31− use crate::bstr::BString;
32−
33− /// Information about the relationship between our refspecs, and remote references with their local counterparts.
34− #[derive(Debug, Clone)]
35− pub struct RefMap<'spec> {
36− /// A mapping between a remote reference and a local tracking branch.
37− pub mappings: Vec<Mapping>,
38− /// Information about the fixes applied to the `mapping` due to validation and sanitization.
39− pub fixes: Vec<git_refspec::match_group::validate::Fix<'spec>>,
40− /// All refs advertised by the remote.
41− pub remote_refs: Vec<git_protocol::fetch::Ref>,
42− /// Additional information provided by the server as part of the handshake.
43− ///
44− /// Note that the `refs` field is always `None` as the refs are placed in `remote_refs`.
45− pub handshake: git_protocol::fetch::handshake::Outcome,
46− }
47−
48− /// Either an object id that the remote has or the matched remote ref itself.
49− #[derive(Debug, Clone)]
50− pub enum Source {
51− /// An object id, as the matched ref-spec was an object id itself.
52− ObjectId(git_hash::ObjectId),
53− /// The remote reference that matched the ref-specs name.
54− Ref(git_protocol::fetch::Ref),
55− }
56−
57− /// A mapping between a single remote reference and its advertised objects to a local destination which may or may not exist.
58− #[derive(Debug, Clone)]
59− pub struct Mapping {
60− /// The reference on the remote side, along with information about the objects they point to as advertised by the server.
61− pub remote: Source,
62− /// The local tracking reference to update after fetching the object visible via `remote`.
63− pub local: Option<BString>,
64− /// The index into the fetch ref-specs used to produce the mapping, allowing it to be recovered.
65− pub spec_index: usize,
66− }
67−}
30+pub mod fetch;
6831
6932 ///
7033 #[cfg(any(feature = "async-network-client", feature = "blocking-network-client"))]
git-repository/src/remote/url/mod.rs+2−2
@@ -1,7 +1,7 @@
11 mod rewrite;
22 ///
3−#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
3+#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
44 pub mod scheme_permission;
55 pub(crate) use rewrite::Rewrite;
6−#[cfg(any(feature = "blocking-network-client", feature = "async-network-client-async-std"))]
6+#[cfg(any(feature = "blocking-network-client", feature = "async-network-client"))]
77 pub(crate) use scheme_permission::SchemePermission;
git-repository/src/repository/config.rs+9−2
@@ -9,10 +9,17 @@ impl crate::Repository { · 8 unmodified lines
99 config::Snapshot { repo: self }
1010 }
1111
12− /// Return a mutable snapshot of the configuration as seen upon opening the repository.
12+ /// Return a mutable snapshot of the configuration as seen upon opening the repository, starting a transaction.
13+ /// When the returned instance is dropped, it is applied in full, even if the reason for the drop is an error.
14+ ///
15+ /// Note that changes to the configuration are in-memory only and are observed only the this instance
16+ /// of the [`Repository`][crate::Repository].
1317 pub fn config_snapshot_mut(&mut self) -> config::SnapshotMut<'_> {
1418 let config = self.config.resolved.as_ref().clone();
15− config::SnapshotMut { repo: self, config }
19+ config::SnapshotMut {
20+ repo: Some(self),
21+ config,
22+ }
1623 }
1724
1825 /// The options used to open the repository.
git-repository/src/repository/identity.rs+9−2
@@ -4,7 +4,8 @@ use crate::bstr::BString; · 3 unmodified lines
44
55 /// Identity handling.
66 impl crate::Repository {
7− /// Return a crate-specific constant signature with [`Time`][git_actor::Time] set to now,
7+ /// Return a crate-specific constant signature with [`Time`][git_actor::Time] set to now, or whatever
8+ /// was overridden via `GIT_COMMITTER_TIME` or `GIT_AUTHOR_TIME` if these variables are allowed to be read,
89 /// in a similar vein as the default that git chooses if there is nothing configured.
910 ///
1011 /// This can be useful as fallback for an unset `committer` or `author`.
@@ -16,7 +17,13 @@ impl crate::Repository { · 5 unmodified lines
1617 git_actor::SignatureRef {
1718 name: "gitoxide".into(),
1819 email: "gitoxide@localhost".into(),
19− time: git_date::Time::now_local_or_utc(),
20+ time: {
21+ let p = self.config.personas();
22+ p.committer
23+ .time
24+ .or(p.author.time)
25+ .unwrap_or_else(git_date::Time::now_local_or_utc)
26+ },
2027 }
2128 }
2229
git-repository/src/repository/object.rs+24−30
@@ -125,8 +125,10 @@ impl crate::Repository { · 124 unmodified lines
125125 self.tag_reference(name, tag_id, constraint).map_err(Into::into)
126126 }
127127
128− /// Create a new commit object with `author`, `committer` and `message` referring to `tree` with `parents`, and point `reference`
128+ /// Create a new commit object with `message` referring to `tree` with `parents`, and point `reference`
129129 /// to it. The commit is written without message encoding field, which can be assumed to be UTF-8.
130+ /// `author` and `committer` fields are pre-set from the configuration, which can be altered
131+ /// [temporarily][crate::Repository::config_snapshot_mut()] before the call if required.
130132 ///
131133 /// `reference` will be created if it doesn't exist, and can be `"HEAD"` to automatically write-through to the symbolic reference
132134 /// that `HEAD` points to if it is not detached. For this reason, detached head states cannot be created unless the `HEAD` is detached
@@ -139,8 +141,6 @@ impl crate::Repository { · 6 unmodified lines
139141 pub fn commit<Name, E>(
140142 &self,
141143 reference: Name,
142− author: git_actor::SignatureRef<'_>,
143− committer: git_actor::SignatureRef<'_>,
144144 message: impl AsRef<str>,
145145 tree: impl Into<ObjectId>,
146146 parents: impl IntoIterator<Item = impl Into<ObjectId>>,
@@ -157,6 +157,8 @@ impl crate::Repository { · 10 unmodified lines
157157 // TODO: possibly use CommitRef to save a few allocations (but will have to allocate for object ids anyway.
158158 // This can be made vastly more efficient though if we wanted to, so we lie in the API
159159 let reference = reference.try_into()?;
160+ let author = self.author_or_default();
161+ let committer = self.committer_or_default();
160162 let commit = git_object::Commit {
161163 message: message.as_ref().into(),
162164 tree: tree.into(),
@@ -168,36 +170,28 @@ impl crate::Repository { · 5 unmodified lines
168170 };
169171
170172 let commit_id = self.write_object(&commit)?;
171− self.edit_reference(
172− RefEdit {
173− change: Change::Update {
174− log: LogChange {
175− mode: RefLog::AndReference,
176− force_create_reflog: false,
177− message: crate::reference::log::message(
178− "commit",
179− commit.message.as_ref(),
180− commit.parents.len(),
181− ),
182− },
183− expected: match commit.parents.first().map(|p| Target::Peeled(*p)) {
184− Some(previous) => {
185− if reference.as_bstr() == "HEAD" {
186− PreviousValue::MustExistAndMatch(previous)
187− } else {
188− PreviousValue::ExistingMustMatch(previous)
189− }
173+ self.edit_reference(RefEdit {
174+ change: Change::Update {
175+ log: LogChange {
176+ mode: RefLog::AndReference,
177+ force_create_reflog: false,
178+ message: crate::reference::log::message("commit", commit.message.as_ref(), commit.parents.len()),
179+ },
180+ expected: match commit.parents.first().map(|p| Target::Peeled(*p)) {
181+ Some(previous) => {
182+ if reference.as_bstr() == "HEAD" {
183+ PreviousValue::MustExistAndMatch(previous)
184+ } else {
185+ PreviousValue::ExistingMustMatch(previous)
190186 }
191− None => PreviousValue::MustNotExist,
192− },
193− new: Target::Peeled(commit_id.inner),
187+ }
188+ None => PreviousValue::MustNotExist,
194189 },
195− name: reference,
196− deref: true,
190+ new: Target::Peeled(commit_id.inner),
197191 },
198− git_lock::acquire::Fail::Immediately,
199− commit.committer.to_ref(),
200− )?;
192+ name: reference,
193+ deref: true,
194+ })?;
201195 Ok(commit_id)
202196 }
203197 }
git-repository/src/repository/reference.rs+28−45
@@ -1,8 +1,6 @@
11 use std::convert::TryInto;
22
3−use git_actor as actor;
43 use git_hash::ObjectId;
5−use git_lock as lock;
64 use git_ref::{
75 transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog},
86 FullName, PartialNameRef, Target,
@@ -10,8 +8,6 @@ use git_ref::{ · 1 unmodified line
108
119 use crate::{bstr::BString, ext::ReferenceExt, reference, Reference};
1210
13−const DEFAULT_LOCK_MODE: git_lock::acquire::Fail = git_lock::acquire::Fail::Immediately;
14−
1511 /// Obtain and alter references comfortably
1612 impl crate::Repository {
1713 /// Create a lightweight tag with given `name` (and without `refs/tags/` prefix) pointing to the given `target`, and return it as reference.
@@ -25,19 +21,15 @@ impl crate::Repository { · 7 unmodified lines
2521 constraint: PreviousValue,
2622 ) -> Result<Reference<'_>, reference::edit::Error> {
2723 let id = target.into();
28− let mut edits = self.edit_reference(
29− RefEdit {
30− change: Change::Update {
31− log: Default::default(),
32− expected: constraint,
33− new: Target::Peeled(id),
34− },
35− name: format!("refs/tags/{}", name.as_ref()).try_into()?,
36− deref: false,
24+ let mut edits = self.edit_reference(RefEdit {
25+ change: Change::Update {
26+ log: Default::default(),
27+ expected: constraint,
28+ new: Target::Peeled(id),
3729 },
38− DEFAULT_LOCK_MODE,
39− self.committer_or_default(),
40− )?;
30+ name: format!("refs/tags/{}", name.as_ref()).try_into()?,
31+ deref: false,
32+ })?;
4133 assert_eq!(edits.len(), 1, "reference splits should ever happen");
4234 let edit = edits.pop().expect("exactly one item");
4335 Ok(Reference {
@@ -95,23 +87,19 @@ impl crate::Repository { · 51 unmodified lines
9587 {
9688 let name = name.try_into().map_err(git_validate::reference::name::Error::from)?;
9789 let id = target.into();
98− let mut edits = self.edit_reference(
99− RefEdit {
100− change: Change::Update {
101− log: LogChange {
102− mode: RefLog::AndReference,
103− force_create_reflog: false,
104− message: log_message.into(),
105− },
106− expected: constraint,
107− new: Target::Peeled(id),
90+ let mut edits = self.edit_reference(RefEdit {
91+ change: Change::Update {
92+ log: LogChange {
93+ mode: RefLog::AndReference,
94+ force_create_reflog: false,
95+ message: log_message.into(),
10896 },
109− name,
110− deref: false,
97+ expected: constraint,
98+ new: Target::Peeled(id),
11199 },
112− DEFAULT_LOCK_MODE,
113− self.committer_or_default(),
114− )?;
100+ name,
101+ deref: false,
102+ })?;
115103 assert_eq!(
116104 edits.len(),
117105 1,
@@ -126,34 +114,29 @@ impl crate::Repository { · 8 unmodified lines
126114 .attach(self))
127115 }
128116
129− /// Edit a single reference as described in `edit`, handle locks via `lock_mode` and write reference logs as `log_committer`.
117+ /// Edit a single reference as described in `edit`, and write reference logs as `log_committer`.
130118 ///
131119 /// One or more `RefEdit`s are returned - symbolic reference splits can cause more edits to be performed. All edits have the previous
132120 /// reference values set to the ones encountered at rest after acquiring the respective reference's lock.
133− pub fn edit_reference(
134− &self,
135− edit: RefEdit,
136− lock_mode: lock::acquire::Fail,
137− log_committer: actor::SignatureRef<'_>,
138− ) -> Result<Vec<RefEdit>, reference::edit::Error> {
139− self.edit_references(Some(edit), lock_mode, log_committer)
121+ pub fn edit_reference(&self, edit: RefEdit) -> Result<Vec<RefEdit>, reference::edit::Error> {
122+ self.edit_references(Some(edit))
140123 }
141124
142− /// Edit one or more references as described by their `edits`, with `lock_mode` deciding on how to handle competing
143− /// transactions. `log_committer` is the name appearing in reference logs.
125+ /// Edit one or more references as described by their `edits`.
126+ /// Note that one can set the committer name for use in the ref-log by temporarily
127+ /// [overriding the git-config][crate::Repository::config_snapshot_mut()].
144128 ///
145129 /// Returns all reference edits, which might be more than where provided due the splitting of symbolic references, and
146130 /// whose previous (_old_) values are the ones seen on in storage after the reference was locked.
147131 pub fn edit_references(
148132 &self,
149133 edits: impl IntoIterator<Item = RefEdit>,
150− lock_mode: lock::acquire::Fail,
151− log_committer: actor::SignatureRef<'_>,
152134 ) -> Result<Vec<RefEdit>, reference::edit::Error> {
135+ let (file_lock_fail, packed_refs_lock_fail) = self.config.lock_timeout()?;
153136 self.refs
154137 .transaction()
155− .prepare(edits, lock_mode)?
156− .commit(log_committer)
138+ .prepare(edits, file_lock_fail, packed_refs_lock_fail)?
139+ .commit(self.committer_or_default())
157140 .map_err(Into::into)
158141 }
159142
git-repository/src/repository/remote.rs+11−0
@@ -32,6 +32,17 @@ impl crate::Repository { · 31 unmodified lines
3232 .ok_or_else(|| find::existing::Error::NotFound { name: name.into() })??)
3333 }
3434
35+ /// Find the default remote as configured, or `None` if no such configuration could be found.
36+ ///
37+ /// See [remote_default_name()][Self::remote_default_name()] for more information on the `direction` parameter.
38+ pub fn find_default_remote(
39+ &self,
40+ direction: remote::Direction,
41+ ) -> Option<Result<Remote<'_>, find::existing::Error>> {
42+ self.remote_default_name(direction)
43+ .map(|name| self.find_remote(name.as_ref()))
44+ }
45+
3546 /// Find the remote with the given `name` or return `None` if it doesn't exist, for the purpose of fetching or pushing
3647 /// data to a remote.
3748 ///
git-repository/src/repository/worktree.rs+1−11
@@ -25,21 +25,11 @@ impl crate::Repository { · 24 unmodified lines
2525 res.sort_by(|a, b| a.git_dir.cmp(&b.git_dir));
2626 Ok(res)
2727 }
28−
29− /// Iterate all _linked_ worktrees in sort order and resolve them, ignoring those without an accessible work tree, into repositories
30− /// whose [`worktree()`][crate::Repository::worktree()] is the worktree currently being iterated.
31− ///
32− /// Note that for convenience all io errors are squelched so if there is a chance for IO errors during
33− /// traversal of an owned directory, better use `list()` directly. The latter allows to resolve repositories
34− /// even if the worktree checkout isn't accessible.
35− pub fn worktree_repos(&self) -> ! {
36− todo!()
37− }
3828 }
3929
4030 /// Interact with individual worktrees and their information.
4131 impl crate::Repository {
42− /// Return the repository owning the main worktree.
32+ /// Return the repository owning the main worktree, typically from a linked worktree.
4333 ///
4434 /// Note that it might be the one that is currently open if this repository doesn't point to a linked worktree.
4535 /// Also note that the main repo might be bare.
git-repository/src/worktree/mod.rs+3−1
@@ -112,6 +112,8 @@ pub mod excludes { · 111 unmodified lines
112112 Io(#[from] std::io::Error),
113113 #[error(transparent)]
114114 EnvironmentPermission(#[from] git_sec::permission::Error<PathBuf>),
115+ #[error("The value for `core.excludesFile` could not be read from configuration")]
116+ ExcludesFilePathInterpolation(#[from] git_config::path::interpolate::Error),
115117 }
116118
117119 impl<'repo> crate::Worktree<'repo> {
@@ -135,7 +137,7 @@ pub mod excludes { · 17 unmodified lines
135137 overrides.unwrap_or_default(),
136138 git_attributes::MatchGroup::<git_attributes::Ignore>::from_git_dir(
137139 repo.git_dir(),
138− match repo.config.excludes_file.as_ref() {
140+ match repo.config.excludes_file()?.as_ref() {
139141 Some(user_path) => Some(user_path.to_owned()),
140142 None => repo.config.xdg_config_path("ignore")?,
141143 },
git-repository/tests/fixtures/generated-archives/.gitignore+1−0
@@ -1,3 +1,4 @@
11 /make_worktree_repo.tar.xz
22 /make_remote_repos.tar.xz
3+/make_fetch_repos.tar.xz
34 /make_core_worktree_repo.tar.xz
git-repository/tests/fixtures/make_fetch_repos.sh+30−0
@@ -0,0 +1,30 @@
1+set -eu -o pipefail
2+
3+git clone --bare "${1:?First argument is the complex base repo from make_remote_repos.sh/base}" base
4+
5+git clone --shared base clone-as-base-with-changes
6+(cd clone-as-base-with-changes
7+ touch new-file
8+ git add new-file
9+ git commit -m "add new-file"
10+ git tag -m "new-file introduction" v1.0
11+)
12+
13+git clone --shared base two-origins
14+(cd two-origins
15+ git remote add changes-on-top-of-origin "$PWD/../clone-as-base-with-changes"
16+ git branch "not-currently-checked-out"
17+ git symbolic-ref refs/heads/symbolic refs/heads/main
18+)
19+
20+git clone --shared base worktree-root
21+(
22+ cd worktree-root
23+
24+ git worktree add ../wt-a
25+ git worktree add ../prev/wt-a-nested
26+ git worktree add ../wt-b
27+ git worktree add ../wt-a/nested-wt-b
28+ git worktree add --lock ../wt-c-locked
29+ git worktree add ../wt-deleted && rm -Rf ../wt-deleted
30+)
git-repository/tests/fixtures/make_remote_repos.sh+4−0
@@ -274,3 +274,7 @@ git clone --shared base credential-helpers · 273 unmodified lines
274274 baseline "git://host.org"
275275 )
276276
277+git clone --shared base detached-head
278+(cd detached-head
279+ git checkout @~1
280+)
git-repository/tests/head/mod.rs+11−1
@@ -1,4 +1,4 @@
1−mod remote {
1+mod into_remote {
22 use git_repository as git;
33
44 use crate::remote;
@@ -12,4 +12,14 @@ mod remote { · 7 unmodified lines
1212 );
1313 Ok(())
1414 }
15+
16+ #[test]
17+ fn detached_is_none() -> crate::Result {
18+ let repo = remote::repo("detached-head");
19+ assert_eq!(
20+ repo.head()?.into_remote(git::remote::Direction::Fetch).transpose()?,
21+ None
22+ );
23+ Ok(())
24+ }
1525 }
git-repository/tests/remote/fetch.rs+123−0
@@ -0,0 +1,123 @@
1+#[cfg(feature = "blocking-network-client")]
2+mod blocking_io {
3+ use crate::remote;
4+ use git_features::progress;
5+ use git_repository as git;
6+ use git_repository::remote::fetch;
7+ use git_repository::remote::Direction::Fetch;
8+ use git_testtools::hex_to_id;
9+ use std::sync::atomic::AtomicBool;
10+
11+ fn repo_rw(name: &str) -> (git::Repository, git_testtools::tempfile::TempDir) {
12+ let dir = git_testtools::scripted_fixture_repo_writable_with_args(
13+ "make_fetch_repos.sh",
14+ [git::path::realpath(remote::repo_path("base"))
15+ .unwrap()
16+ .to_string_lossy()],
17+ git_testtools::Creation::ExecuteScript,
18+ )
19+ .unwrap();
20+ let repo = git::open_opts(dir.path().join(name), git::open::Options::isolated()).unwrap();
21+ (repo, dir)
22+ }
23+
24+ #[test]
25+ fn fetch_pack() -> crate::Result {
26+ for version in [
27+ None,
28+ Some(git::protocol::transport::Protocol::V2),
29+ Some(git::protocol::transport::Protocol::V1),
30+ ] {
31+ let (mut repo, _tmp) = repo_rw("two-origins");
32+ if let Some(version) = version {
33+ repo.config_snapshot_mut().set_raw_value(
34+ "protocol",
35+ None,
36+ "version",
37+ (version as u8).to_string().as_str(),
38+ )?;
39+ }
40+
41+ // No updates
42+ {
43+ let remote = repo.find_remote("origin")?;
44+ {
45+ remote
46+ .connect(Fetch, progress::Discard)?
47+ .prepare_fetch(Default::default())?;
48+ // early drops are fine and won't block.
49+ }
50+ let outcome = remote
51+ .connect(Fetch, progress::Discard)?
52+ .prepare_fetch(Default::default())?
53+ .receive(&AtomicBool::default())?;
54+ assert!(matches!(outcome.status, git::remote::fetch::Status::NoChange));
55+ }
56+
57+ // Some updates to be fetched
58+ for dry_run in [true, false] {
59+ let remote = repo.find_remote("changes-on-top-of-origin")?;
60+ let outcome: git::remote::fetch::Outcome = remote
61+ .connect(Fetch, progress::Discard)?
62+ .prepare_fetch(Default::default())?
63+ .with_dry_run(dry_run)
64+ .receive(&AtomicBool::default())?;
65+ let refs = match outcome.status {
66+ fetch::Status::Change {
67+ write_pack_bundle,
68+ update_refs,
69+ } => {
70+ assert_eq!(write_pack_bundle.pack_kind, git::odb::pack::data::Version::V2);
71+ assert_eq!(write_pack_bundle.object_hash, repo.object_hash());
72+ assert_eq!(write_pack_bundle.index.num_objects, 3, "this value is 4 when git does it with 'consecutive' negotiation style, but could be 33 if completely naive.");
73+ assert_eq!(
74+ write_pack_bundle.index.index_version,
75+ git::odb::pack::index::Version::V2
76+ );
77+ assert_eq!(
78+ write_pack_bundle.index.index_hash,
79+ hex_to_id("c75114f60ab2c9389916f3de1082bbaa47491e3b")
80+ );
81+ assert!(write_pack_bundle.data_path.map_or(false, |f| f.is_file()));
82+ assert!(write_pack_bundle.index_path.map_or(false, |f| f.is_file()));
83+
84+ update_refs
85+ }
86+ fetch::Status::DryRun { update_refs } => update_refs,
87+ fetch::Status::NoChange => unreachable!("we firmly expect changes here"),
88+ };
89+
90+ assert_eq!(
91+ refs.updates,
92+ vec![fetch::refs::Update {
93+ mode: fetch::refs::update::Mode::New,
94+ edit_index: Some(0),
95+ }]
96+ );
97+ for (_update, mapping, _spec, edit) in
98+ refs.iter_mapping_updates(&outcome.ref_map.mappings, remote.refspecs(Fetch))
99+ {
100+ let edit = edit.expect("refedit present even if it's a no-op");
101+ if dry_run {
102+ assert_eq!(
103+ edit.change.new_value().expect("no deletions").id(),
104+ mapping.remote.as_id()
105+ );
106+ assert!(
107+ repo.try_find_reference(edit.name.as_ref())?.is_none(),
108+ "no ref created in dry-run mode"
109+ );
110+ } else {
111+ let r = repo.find_reference(edit.name.as_ref()).unwrap();
112+ assert_eq!(
113+ r.id(),
114+ *mapping.remote.as_id(),
115+ "local reference should point to remote id"
116+ );
117+ }
118+ }
119+ }
120+ }
121+ Ok(())
122+ }
123+}
git-repository/tests/remote/mod.rs+2−1
@@ -17,4 +17,5 @@ pub(crate) fn cow_str(s: &str) -> Cow<str> { · 16 unmodified lines
1717 }
1818
1919 mod connect;
20−mod list_refs;
20+mod fetch;
21+mod ref_map;
git-repository/tests/remote/ref_map.rsrenamed from git-repository/tests/remote/list_refs.rs+2−3
@@ -24,8 +24,7 @@ mod blocking_io { · 23 unmodified lines
2424 }
2525
2626 let remote = repo.find_remote("origin")?;
27− let connection = remote.connect(Fetch, progress::Discard)?;
28− let map = connection.ref_map()?;
27+ let map = remote.connect(Fetch, progress::Discard)?.ref_map(Default::default())?;
2928 assert_eq!(
3029 map.remote_refs.len(),
3130 14,
@@ -36,7 +35,7 @@ mod blocking_io { · 4 unmodified lines
3635 assert_eq!(
3736 map.mappings.len(),
3837 11,
39− "mappings are only a sub-set of all remotes due to refspec matching"
38+ "mappings are only a sub-set of all remotes due to refspec matching, tags are filtered out."
4039 );
4140 }
4241 Ok(())
git-repository/tests/repository/config/config_snapshot/mod.rs+44−0
@@ -1,5 +1,49 @@
11 use crate::named_repo;
22
3+#[test]
4+fn commit_auto_rollback() -> crate::Result {
5+ let mut repo: git_repository::Repository = named_repo("make_basic_repo.sh")?;
6+ assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189cd3");
7+
8+ {
9+ let mut config = repo.config_snapshot_mut();
10+ config.set_raw_value("core", None, "abbrev", "4")?;
11+ let repo = config.commit_auto_rollback()?;
12+ assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189");
13+ }
14+
15+ assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189cd3");
16+
17+ let repo = {
18+ let mut config = repo.config_snapshot_mut();
19+ config.set_raw_value("core", None, "abbrev", "4")?;
20+ let repo = config.commit_auto_rollback()?;
21+ assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189");
22+ repo.rollback()?
23+ };
24+ assert_eq!(repo.head_id()?.shorten()?.to_string(), "3189cd3");
25+
26+ Ok(())
27+}
28+
29+#[test]
30+fn snapshot_mut_commit_and_forget() -> crate::Result {
31+ let mut repo: git_repository::Repository = named_repo("make_basic_repo.sh")?;
32+ let repo = {
33+ let mut repo = repo.config_snapshot_mut();
34+ repo.set_raw_value("core", None, "abbrev", "4")?;
35+ repo.commit()?
36+ };
37+ assert_eq!(repo.config_snapshot().integer("core.abbrev").expect("set"), 4);
38+ {
39+ let mut repo = repo.config_snapshot_mut();
40+ repo.set_raw_value("core", None, "abbrev", "8")?;
41+ repo.forget();
42+ }
43+ assert_eq!(repo.config_snapshot().integer("core.abbrev"), Some(4));
44+ Ok(())
45+}
46+
347 #[test]
448 fn values_are_set_in_memory_only() {
549 let mut repo = named_repo("make_config_repo.sh").unwrap();
git-repository/tests/repository/mod.rs+1−1
@@ -10,7 +10,7 @@ mod worktree; · 9 unmodified lines
1010
1111 #[test]
1212 fn size_in_memory() {
13− let expected = [760, 800];
13+ let expected = [728, 744, 784];
1414 let actual_size = std::mem::size_of::<Repository>();
1515 assert!(
1616 expected.contains(&actual_size),
git-repository/tests/repository/object.rs+14−34
@@ -147,23 +147,17 @@ mod tag { · 146 unmodified lines
147147 }
148148
149149 mod commit {
150+ use crate::{freeze_time, restricted_and_git};
150151 use git_repository as git;
151152 use git_testtools::hex_to_id;
153+
152154 #[test]
153155 fn parent_in_initial_commit_causes_failure() {
154156 let tmp = tempfile::tempdir().unwrap();
155157 let repo = git::init(&tmp).unwrap();
156158 let empty_tree_id = repo.write_object(&git::objs::Tree::empty()).unwrap().detach();
157− let author = git::actor::Signature::empty();
158159 let err = repo
159− .commit(
160− "HEAD",
161− author.to_ref(),
162− author.to_ref(),
163− "initial",
164− empty_tree_id,
165− [empty_tree_id],
166− )
160+ .commit("HEAD", "initial", empty_tree_id, [empty_tree_id])
167161 .unwrap_err();
168162 assert_eq!(
169163 err.to_string(),
@@ -173,22 +167,16 @@ mod commit { · 3 unmodified lines
173167 }
174168
175169 #[test]
170+ #[serial_test::serial]
176171 fn single_line_initial_commit_empty_tree_ref_nonexisting() -> crate::Result {
172+ let _env = freeze_time();
177173 let tmp = tempfile::tempdir()?;
178− let repo = git::init(&tmp)?;
174+ let repo = git::open_opts(git::init(&tmp)?.path(), restricted_and_git())?;
179175 let empty_tree_id = repo.write_object(&git::objs::Tree::empty())?;
180− let author = git::actor::Signature::empty();
181− let commit_id = repo.commit(
182− "HEAD",
183− author.to_ref(),
184− author.to_ref(),
185− "initial",
186− empty_tree_id,
187− git::commit::NO_PARENT_IDS,
188− )?;
176+ let commit_id = repo.commit("HEAD", "initial", empty_tree_id, git::commit::NO_PARENT_IDS)?;
189177 assert_eq!(
190178 commit_id,
191− hex_to_id("302ea5640358f98ba23cda66c1e664a6f274643f"),
179+ hex_to_id("3a774843723a713a8d361b4d4d98ad4092ef05bd"),
192180 "the commit id is stable"
193181 );
194182
@@ -207,8 +195,10 @@ mod commit { · 12 unmodified lines
207195 }
208196
209197 #[test]
198+ #[serial_test::serial]
210199 fn multi_line_commit_message_uses_first_line_in_ref_log_ref_nonexisting() -> crate::Result {
211− let (repo, _keep) = crate::basic_rw_repo()?;
200+ let _env = freeze_time();
201+ let (repo, _keep) = crate::repo_rw_opts("make_basic_repo.sh", restricted_and_git())?;
212202 let parent = repo.find_reference("HEAD")?.peel_to_id_in_place()?;
213203 let empty_tree_id = parent.object()?.to_commit_ref_iter().tree_id().expect("tree to be set");
214204 assert_eq!(
@@ -221,18 +211,10 @@ mod commit { · 6 unmodified lines
221211 empty_tree_id,
222212 "try and non-try work the same"
223213 );
224− let author = git::actor::Signature::empty();
225− let first_commit_id = repo.commit(
226− "HEAD",
227− author.to_ref(),
228− author.to_ref(),
229− "hello there \r\n\nthe body",
230− empty_tree_id,
231− Some(parent),
232− )?;
214+ let first_commit_id = repo.commit("HEAD", "hello there \r\n\nthe body", empty_tree_id, Some(parent))?;
233215 assert_eq!(
234216 first_commit_id,
235− hex_to_id("1ff7decccf76bfa15bfdb0b66bac0c9144b4b083"),
217+ hex_to_id("e7c7273539cfc1a52802fa9d61aa578f6ccebcb4"),
236218 "the commit id is stable"
237219 );
238220
@@ -254,8 +236,6 @@ mod commit { · 15 unmodified lines
254236
255237 let second_commit_id = repo.commit(
256238 "refs/heads/new-branch",
257− author.to_ref(),
258− author.to_ref(),
259239 "committing into a new branch creates it",
260240 empty_tree_id,
261241 Some(first_commit_id),
@@ -263,7 +243,7 @@ mod commit { · 1 unmodified line
263243
264244 assert_eq!(
265245 second_commit_id,
266− hex_to_id("b0d041ade77e51d31c79c7147fb769336ccc77b1"),
246+ hex_to_id("e1412f169e0812eb260601bdab3854ca0f1a7b33"),
267247 "the second commit id is stable"
268248 );
269249
git-repository/tests/repository/remote.rs+19−0
@@ -271,3 +271,22 @@ mod find_remote { · 270 unmodified lines
271271 .to_string()
272272 }
273273 }
274+
275+mod find_default_remote {
276+ use crate::remote;
277+ use git_repository as git;
278+
279+ #[test]
280+ fn works_on_detached_heads() -> crate::Result {
281+ let repo = remote::repo("detached-head");
282+ assert_eq!(
283+ repo.find_default_remote(git::remote::Direction::Fetch)
284+ .transpose()?
285+ .expect("present")
286+ .name()
287+ .expect("always named"),
288+ "origin"
289+ );
290+ Ok(())
291+ }
292+}
git-repository/tests/util/mod.rs+22−2
@@ -2,6 +2,16 @@ use git_repository::{open, Repository, ThreadSafeRepository}; · 1 unmodified line
22
33 pub type Result<T = ()> = std::result::Result<T, Box<dyn std::error::Error>>;
44
5+pub fn freeze_time() -> git_testtools::Env<'static> {
6+ let frozen_time = "1979-02-26 18:30:00";
7+ git_testtools::Env::new()
8+ .unset("GIT_AUTHOR_NAME")
9+ .unset("GIT_AUTHOR_EMAIL")
10+ .set("GIT_AUTHOR_DATE", frozen_time)
11+ .unset("GIT_COMMITTER_NAME")
12+ .unset("GIT_COMMITTER_EMAIL")
13+ .set("GIT_COMMITTER_DATE", frozen_time)
14+}
515 pub fn repo(name: &str) -> Result<ThreadSafeRepository> {
616 let repo_path = git_testtools::scripted_fixture_repo_read_only(name)?;
717 Ok(ThreadSafeRepository::open_opts(repo_path, restricted())?)
@@ -16,15 +26,25 @@ pub fn restricted() -> open::Options { · 8 unmodified lines
1626 open::Options::isolated()
1727 }
1828
29+pub fn restricted_and_git() -> open::Options {
30+ let mut opts = open::Options::isolated();
31+ opts.permissions.env.git_prefix = git_sec::Permission::Allow;
32+ opts
33+}
34+
1935 pub fn repo_rw(name: &str) -> Result<(Repository, tempfile::TempDir)> {
36+ repo_rw_opts(name, restricted())
37+}
38+
39+pub fn repo_rw_opts(name: &str, opts: git_repository::open::Options) -> Result<(Repository, tempfile::TempDir)> {
2040 let repo_path = git_testtools::scripted_fixture_repo_writable(name)?;
2141 Ok((
2242 ThreadSafeRepository::discover_opts(
2343 repo_path.path(),
2444 Default::default(),
2545 git_sec::trust::Mapping {
26− full: restricted(),
27− reduced: restricted(),
46+ full: opts.clone(),
47+ reduced: opts,
2848 },
2949 )?
3050 .to_thread_local(),
git-transport/src/client/blocking_io/connect.rs+4−4
@@ -66,10 +66,10 @@ pub(crate) mod function { · 65 unmodified lines
6666 #[cfg(not(feature = "http-client-curl"))]
6767 git_url::Scheme::Https | git_url::Scheme::Http => return Err(Error::CompiledWithoutHttp(url.scheme)),
6868 #[cfg(feature = "http-client-curl")]
69− git_url::Scheme::Https | git_url::Scheme::Http => Box::new(
70− crate::client::http::connect(&url.to_bstring().to_string(), desired_version)
71− .map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)?,
72− ),
69+ git_url::Scheme::Https | git_url::Scheme::Http => Box::new(crate::client::http::connect(
70+ &url.to_bstring().to_string(),
71+ desired_version,
72+ )),
7373 })
7474 }
7575 }
git-transport/src/client/blocking_io/file.rs+6−0
@@ -1,3 +1,5 @@
1+use std::any::Any;
2+use std::error::Error;
13 use std::process::{self, Command, Stdio};
24
35 use bstr::{BString, ByteSlice};
@@ -101,6 +103,10 @@ impl client::TransportWithoutIO for SpawnProcessOnDemand { · 97 unmodified lines
101103 fn connection_persists_across_multiple_requests(&self) -> bool {
102104 true
103105 }
106+
107+ fn configure(&mut self, _config: &dyn Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
108+ Ok(())
109+ }
104110 }
105111
106112 impl client::Transport for SpawnProcessOnDemand {
git-transport/src/client/blocking_io/http/curl/mod.rs+5−0
@@ -1,3 +1,4 @@
1+use std::error::Error;
12 use std::{
23 sync::mpsc::{Receiver, SyncSender},
34 thread,
@@ -100,4 +101,8 @@ impl crate::client::http::Http for Curl { · 96 unmodified lines
100101 ) -> Result<http::PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, http::Error> {
101102 self.make_request(url, headers, true)
102103 }
104+
105+ fn configure(&mut self, _config: &dyn std::any::Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
106+ Ok(())
107+ }
103108 }
git-transport/src/client/blocking_io/http/mod.rs+9−5
@@ -1,6 +1,6 @@
1+use std::any::Any;
12 use std::{
23 borrow::Cow,
3− convert::Infallible,
44 io::{BufRead, Read},
55 };
66
@@ -169,6 +169,10 @@ impl<H: Http> client::TransportWithoutIO for Transport<H> { · 162 unmodified lines
169169 fn connection_persists_across_multiple_requests(&self) -> bool {
170170 false
171171 }
172+
173+ fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
174+ self.http.configure(config)
175+ }
172176 }
173177
174178 impl<H: Http> client::Transport for Transport<H> {
@@ -297,12 +301,12 @@ impl<H: Http, B: ExtendedBufRead + Unpin> ExtendedBufRead for HeadersThenBody<H, · 122 unmodified lines
297301
298302 /// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol, with `http` as implementation.
299303 #[cfg(all(feature = "http-client", not(feature = "http-client-curl")))]
300−pub fn connect_http<H: Http>(http: H, url: &str, desired_version: Protocol) -> Result<Transport<H>, Infallible> {
301− Ok(Transport::new_http(http, url, desired_version))
304+pub fn connect_http<H: Http>(http: H, url: &str, desired_version: Protocol) -> Transport<H> {
305+ Transport::new_http(http, url, desired_version)
302306 }
303307
304308 /// Connect to the given `url` via HTTP/S using the `desired_version` of the `git` protocol.
305309 #[cfg(feature = "http-client-curl")]
306−pub fn connect(url: &str, desired_version: Protocol) -> Result<Transport<Impl>, Infallible> {
307− Ok(Transport::new(url, desired_version))
310+pub fn connect(url: &str, desired_version: Protocol) -> Transport<Impl> {
311+ Transport::new(url, desired_version)
308312 }
git-transport/src/client/blocking_io/http/traits.rs+8−0
@@ -67,4 +67,12 @@ pub trait Http { · 66 unmodified lines
6767 url: &str,
6868 headers: impl IntoIterator<Item = impl AsRef<str>>,
6969 ) -> Result<PostResponse<Self::Headers, Self::ResponseBody, Self::PostBody>, Error>;
70+
71+ /// Pass `config` which can deserialize in the implementation's configuration, as documented separately.
72+ ///
73+ /// The caller must know how that `config` data looks like for the intended implementation.
74+ fn configure(
75+ &mut self,
76+ config: &dyn std::any::Any,
77+ ) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>;
7078 }
git-transport/src/client/capabilities.rs+1−1
@@ -23,7 +23,7 @@ pub enum Error { · 22 unmodified lines
2323 }
2424
2525 /// A structure to represent multiple [capabilities][Capability] or features supported by the server.
26−#[derive(Debug, Clone)]
26+#[derive(Debug, Clone, Default)]
2727 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
2828 pub struct Capabilities {
2929 data: BString,
git-transport/src/client/git/async_io.rs+5−0
@@ -3,6 +3,7 @@ use bstr::BString; · 2 unmodified lines
33 use futures_io::{AsyncRead, AsyncWrite};
44 use futures_lite::AsyncWriteExt;
55 use git_packetline::PacketLineRef;
6+use std::error::Error;
67
78 use crate::{
89 client::{self, capabilities, git, Capabilities, SetServiceResponse},
@@ -50,6 +51,10 @@ where · 41 unmodified lines
5051 fn connection_persists_across_multiple_requests(&self) -> bool {
5152 true
5253 }
54+
55+ fn configure(&mut self, _config: &dyn std::any::Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
56+ Ok(())
57+ }
5358 }
5459
5560 #[async_trait(?Send)]
git-transport/src/client/git/blocking_io.rs+6−0
@@ -1,3 +1,5 @@
1+use std::any::Any;
2+use std::error::Error;
13 use std::io::Write;
24
35 use bstr::BString;
@@ -52,6 +54,10 @@ where · 48 unmodified lines
5254 fn connection_persists_across_multiple_requests(&self) -> bool {
5355 true
5456 }
57+
58+ fn configure(&mut self, _config: &dyn Any) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
59+ Ok(())
60+ }
5561 }
5662
5763 impl<R, W> client::Transport for git::Connection<R, W>
git-transport/src/client/traits.rs+14−0
@@ -1,3 +1,4 @@
1+use std::any::Any;
12 use std::ops::{Deref, DerefMut};
23
34 #[cfg(any(feature = "blocking-client", feature = "async-client"))]
@@ -46,6 +47,11 @@ pub trait TransportWithoutIO { · 42 unmodified lines
4647 /// of the fetch negotiation or that the end of interaction (i.e. no further request will be made) has to be indicated
4748 /// to the server for most graceful termination of the connection.
4849 fn connection_persists_across_multiple_requests(&self) -> bool;
50+
51+ /// Pass `config` can be cast and interpreted by the implementation, as documented separately.
52+ ///
53+ /// The caller must know how that `config` data looks like for the intended implementation.
54+ fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>>;
4955 }
5056
5157 // Would be nice if the box implementation could auto-forward to all implemented traits.
@@ -70,6 +76,10 @@ impl<T: TransportWithoutIO + ?Sized> TransportWithoutIO for Box<T> { · 18 unmodified lines
7076 fn connection_persists_across_multiple_requests(&self) -> bool {
7177 self.deref().connection_persists_across_multiple_requests()
7278 }
79+
80+ fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
81+ self.deref_mut().configure(config)
82+ }
7383 }
7484
7585 impl<T: TransportWithoutIO + ?Sized> TransportWithoutIO for &mut T {
@@ -93,4 +103,8 @@ impl<T: TransportWithoutIO + ?Sized> TransportWithoutIO for &mut T { · 17 unmodified lines
93103 fn connection_persists_across_multiple_requests(&self) -> bool {
94104 self.deref().connection_persists_across_multiple_requests()
95105 }
106+
107+ fn configure(&mut self, config: &dyn Any) -> Result<(), Box<dyn std::error::Error + Send + Sync + 'static>> {
108+ self.deref_mut().configure(config)
109+ }
96110 }
git-transport/tests/client/blocking_io/http/mock.rs+1−1
@@ -104,7 +104,7 @@ pub fn serve_and_connect( · 103 unmodified lines
104104 &server.addr.port(),
105105 path
106106 );
107− let client = git_transport::client::http::connect(&url, version)?;
107+ let client = git_transport::client::http::connect(&url, version);
108108 assert_eq!(url, client.to_url());
109109 Ok((server, client))
110110 }
gitoxide-core/src/pack/index.rs+1−1
@@ -85,7 +85,7 @@ pub fn from_pack( · 84 unmodified lines
8585 let options = pack::bundle::write::Options {
8686 thread_limit: ctx.thread_limit,
8787 iteration_mode: ctx.iteration_mode.into(),
88− index_kind: pack::index::Version::default(),
88+ index_version: pack::index::Version::default(),
8989 object_hash: ctx.object_hash,
9090 };
9191 let out = ctx.out;
gitoxide-core/src/pack/receive.rs+3−3
@@ -246,7 +246,7 @@ pub use self::async_io::receive; · 245 unmodified lines
246246
247247 #[cfg_attr(feature = "serde1", derive(serde::Serialize, serde::Deserialize))]
248248 pub struct JsonBundleWriteOutcome {
249− pub index_kind: pack::index::Version,
249+ pub index_version: pack::index::Version,
250250 pub index_hash: String,
251251
252252 pub data_hash: String,
@@ -256,7 +256,7 @@ pub struct JsonBundleWriteOutcome { · 3 unmodified lines
256256 impl From<pack::index::write::Outcome> for JsonBundleWriteOutcome {
257257 fn from(v: pack::index::write::Outcome) -> Self {
258258 JsonBundleWriteOutcome {
259− index_kind: v.index_kind,
259+ index_version: v.index_version,
260260 num_objects: v.num_objects,
261261 data_hash: v.data_hash.to_string(),
262262 index_hash: v.index_hash.to_string(),
@@ -340,7 +340,7 @@ fn receive_pack_blocking<W: io::Write>( · 77 unmodified lines
340340 ) -> io::Result<()> {
341341 let options = pack::bundle::write::Options {
342342 thread_limit: ctx.thread_limit,
343− index_kind: pack::index::Version::V2,
343+ index_version: pack::index::Version::V2,
344344 iteration_mode: pack::data::input::Mode::Verify,
345345 object_hash: ctx.object_hash,
346346 };
gitoxide-core/src/repository/remote.rs+13−1
@@ -73,7 +73,10 @@ mod refs_impl { · 72 unmodified lines
7373 let map = remote
7474 .connect(git::remote::Direction::Fetch, progress)
7575 .await?
76− .ref_map()
76+ .ref_map(git::remote::ref_map::Options {
77+ prefix_from_spec_as_filter_on_remote: !matches!(kind, refs::Kind::Remote),
78+ ..Default::default()
79+ })
7780 .await?;
7881
7982 if handshake_info {
@@ -164,6 +167,15 @@ mod refs_impl { · 84 unmodified lines
164167 }
165168 }
166169 }
170+ if map.remote_refs.len() - map.mappings.len() != 0 {
171+ writeln!(
172+ err,
173+ "server sent {} tips, {} were filtered due to {} refspec(s).",
174+ map.remote_refs.len(),
175+ map.remote_refs.len() - map.mappings.len(),
176+ refspecs.len()
177+ )?;
178+ }
167179 Ok(())
168180 }
169181
src/plumbing/main.rs+4−2
@@ -14,8 +14,9 @@ use gitoxide_core as core; · 13 unmodified lines
1414 use gitoxide_core::pack::verify;
1515
1616 use crate::{
17− plumbing::options::{
18− commit, config, credential, exclude, free, mailmap, odb, remote, revision, tree, Args, Subcommands,
17+ plumbing::{
18+ options::{commit, config, credential, exclude, free, mailmap, odb, remote, revision, tree, Args, Subcommands},
19+ show_progress,
1920 },
2021 shared::pretty::prepare_and_run,
2122 };
@@ -111,6 +112,7 @@ pub fn main() -> Result<()> { · 89 unmodified lines
111112 })?;
112113
113114 match cmd {
115+ Subcommands::Progress => show_progress(),
114116 Subcommands::Credential(cmd) => core::repository::credential(
115117 repository(Mode::StrictWithGitInstallConfig)?,
116118 match cmd {
src/plumbing/mod.rs+4−0
@@ -1,4 +1,8 @@
11 mod main;
22 pub use main::main;
33
4+#[path = "progress.rs"]
5+mod progress_impl;
6+pub use progress_impl::show_progress;
7+
48 mod options;
src/plumbing/options.rs+0−677
@@ -1,677 +0,0 @@
1−use std::path::PathBuf;
2−
3−use git_repository as git;
4−use git_repository::bstr::BString;
5−use gitoxide_core as core;
6−
7−#[derive(Debug, clap::Parser)]
8−#[clap(name = "gix-plumbing", about = "The git underworld", version = clap::crate_version!())]
9−#[clap(subcommand_required = true)]
10−#[clap(arg_required_else_help = true)]
11−pub struct Args {
12− /// The repository to access.
13− #[clap(short = 'r', long, default_value = ".")]
14− pub repository: PathBuf,
15−
16− /// Add these values to the configuration in the form of `key=value` or `key`.
17− ///
18− /// For example, if `key` is `core.abbrev`, set configuration like `[core] abbrev = key`,
19− /// or `remote.origin.url = foo` to set `[remote "origin"] url = foo`.
20− #[clap(long, short = 'c', parse(try_from_os_str = git::env::os_str_to_bstring))]
21− pub config: Vec<BString>,
22−
23− #[clap(long, short = 't')]
24− /// The amount of threads to use for some operations.
25− ///
26− /// If unset, or the value is 0, there is no limit and all logical cores can be used.
27− pub threads: Option<usize>,
28−
29− /// Display verbose messages and progress information
30− #[clap(long, short = 'v')]
31− pub verbose: bool,
32−
33− /// Bring up a terminal user interface displaying progress visually
34− #[cfg(feature = "prodash-render-tui")]
35− #[clap(long, conflicts_with("verbose"))]
36− pub progress: bool,
37−
38− /// The progress TUI will stay up even though the work is already completed.
39− ///
40− /// Use this to be able to read progress messages or additional information visible in the TUI log pane.
41− #[cfg(feature = "prodash-render-tui")]
42− #[clap(long, conflicts_with("verbose"), requires("progress"))]
43− pub progress_keep_open: bool,
44−
45− /// Determine the format to use when outputting statistics.
46− #[clap(
47− long,
48− short = 'f',
49− default_value = "human",
50− possible_values(core::OutputFormat::variants())
51− )]
52− pub format: core::OutputFormat,
53−
54− /// The object format to assume when reading files that don't inherently know about it, or when writing files.
55− #[clap(long, default_value_t = git_repository::hash::Kind::default(), possible_values(&["SHA1"]))]
56− pub object_hash: git_repository::hash::Kind,
57−
58− #[clap(subcommand)]
59− pub cmd: Subcommands,
60−}
61−
62−#[derive(Debug, clap::Subcommand)]
63−pub enum Subcommands {
64− /// Interact with the object database.
65− #[clap(subcommand)]
66− Odb(odb::Subcommands),
67− /// Interact with tree objects.
68− #[clap(subcommand)]
69− Tree(tree::Subcommands),
70− /// Interact with commit objects.
71− #[clap(subcommand)]
72− Commit(commit::Subcommands),
73− /// Verify the integrity of the entire repository
74− Verify {
75− #[clap(flatten)]
76− args: free::pack::VerifyOptions,
77− },
78− /// Query and obtain information about revisions.
79− #[clap(subcommand)]
80− Revision(revision::Subcommands),
81− /// A program just like `git credential`.
82− #[clap(subcommand)]
83− Credential(credential::Subcommands),
84− /// Interact with the mailmap.
85− #[clap(subcommand)]
86− Mailmap(mailmap::Subcommands),
87− /// Interact with the remote hosts.
88− Remote(remote::Platform),
89− /// Interact with the exclude files like .gitignore.
90− #[clap(subcommand)]
91− Exclude(exclude::Subcommands),
92− Config(config::Platform),
93− /// Subcommands that need no git repository to run.
94− #[clap(subcommand)]
95− Free(free::Subcommands),
96−}
97−
98−pub mod config {
99− /// Print all entries in a configuration file or access other sub-commands
100− #[derive(Debug, clap::Parser)]
101− #[clap(subcommand_required(false))]
102− pub struct Platform {
103− /// The filter terms to limit the output to matching sections and subsections only.
104− ///
105− /// Typical filters are `branch` or `remote.origin` or `remote.or*` - git-style globs are supported
106− /// and comparisons are case-insensitive.
107− pub filter: Vec<String>,
108− }
109−}
110−
111−pub mod remote {
112− use git_repository as git;
113−
114− #[derive(Debug, clap::Parser)]
115− pub struct Platform {
116− /// The name of the remote to connect to.
117− ///
118− /// If unset, the current branch will determine the remote.
119− #[clap(long, short = 'n')]
120− pub name: Option<String>,
121−
122− /// Connect directly to the given URL, forgoing any configuration from the repository.
123− #[clap(long, short = 'u', conflicts_with("name"), parse(try_from_os_str = std::convert::TryFrom::try_from))]
124− pub url: Option<git::Url>,
125−
126− /// Output additional typically information provided by the server as part of the connection handshake.
127− #[clap(long)]
128− pub handshake_info: bool,
129−
130− /// Subcommands
131− #[clap(subcommand)]
132− pub cmd: Subcommands,
133− }
134−
135− #[derive(Debug, clap::Subcommand)]
136− #[clap(visible_alias = "remotes")]
137− pub enum Subcommands {
138− /// Print all references available on the remote.
139− #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
140− Refs,
141− /// Print all references available on the remote as filtered through ref-specs.
142− #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
143− RefMap {
144− /// Override the built-in and configured ref-specs with one or more of the given ones.
145− #[clap(parse(try_from_os_str = git::env::os_str_to_bstring))]
146− ref_spec: Vec<git_repository::bstr::BString>,
147− },
148− }
149−}
150−
151−pub mod mailmap {
152− #[derive(Debug, clap::Subcommand)]
153− pub enum Subcommands {
154− /// Print all entries in configured mailmaps, inform about errors as well.
155− Entries,
156− }
157−}
158−
159−pub mod odb {
160− #[derive(Debug, clap::Subcommand)]
161− pub enum Subcommands {
162− /// Print all object names.
163− Entries,
164− /// Provide general information about the object database.
165− Info,
166− }
167−}
168−
169−pub mod tree {
170− #[derive(Debug, clap::Subcommand)]
171− pub enum Subcommands {
172− /// Print entries in a given tree
173− Entries {
174− /// Traverse the entire tree and its subtrees respectively, not only this tree.
175− #[clap(long, short = 'r')]
176− recursive: bool,
177−
178− /// Provide files size as well. This is expensive as the object is decoded entirely.
179− #[clap(long, short = 'e')]
180− extended: bool,
181−
182− /// The tree to traverse, or the tree at `HEAD` if unspecified.
183− treeish: Option<String>,
184− },
185− /// Provide information about a tree.
186− Info {
187− /// Provide files size as well. This is expensive as the object is decoded entirely.
188− #[clap(long, short = 'e')]
189− extended: bool,
190− /// The tree to traverse, or the tree at `HEAD` if unspecified.
191− treeish: Option<String>,
192− },
193− }
194−}
195−
196−pub mod commit {
197− #[derive(Debug, clap::Subcommand)]
198− pub enum Subcommands {
199− /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry.
200− Describe {
201− /// Use annotated tag references only, not all tags.
202− #[clap(long, short = 't', conflicts_with("all-refs"))]
203− annotated_tags: bool,
204−
205− /// Use all references under the `ref/` namespaces, which includes tag references, local and remote branches.
206− #[clap(long, short = 'a', conflicts_with("annotated-tags"))]
207− all_refs: bool,
208−
209− /// Only follow the first parent when traversing the commit graph.
210− #[clap(long, short = 'f')]
211− first_parent: bool,
212−
213− /// Always display the long format, even if that would not be necessary as the id is located directly on a reference.
214− #[clap(long, short = 'l')]
215− long: bool,
216−
217− /// Consider only the given `n` candidates. This can take longer, but potentially produces more accurate results.
218− #[clap(long, short = 'c', default_value = "10")]
219− max_candidates: usize,
220−
221− /// Print information on stderr to inform about performance statistics
222− #[clap(long, short = 's')]
223− statistics: bool,
224−
225− #[clap(long)]
226− /// If there was no way to describe the commit, fallback to using the abbreviated input revision.
227− always: bool,
228−
229− /// A specification of the revision to use, or the current `HEAD` if unset.
230− rev_spec: Option<String>,
231− },
232− }
233−}
234−
235−pub mod credential {
236− #[derive(Debug, clap::Subcommand)]
237− pub enum Subcommands {
238− /// Get the credentials fed for `url=<url>` via STDIN.
239− #[clap(visible_alias = "get")]
240− Fill,
241− /// Approve the information piped via STDIN as obtained with last call to `fill`
242− #[clap(visible_alias = "store")]
243− Approve,
244− /// Try to resolve the given revspec and print the object names.
245− #[clap(visible_alias = "erase")]
246− Reject,
247− }
248−}
249−
250−pub mod revision {
251− #[derive(Debug, clap::Subcommand)]
252− #[clap(visible_alias = "rev", visible_alias = "r")]
253− pub enum Subcommands {
254− /// List all commits reachable from the given rev-spec.
255− #[clap(visible_alias = "l")]
256− List { spec: std::ffi::OsString },
257− /// Provide the revision specification like `@~1` to explain.
258− #[clap(visible_alias = "e")]
259− Explain { spec: std::ffi::OsString },
260− /// Try to resolve the given revspec and print the object names.
261− #[clap(visible_alias = "query", visible_alias = "parse", visible_alias = "p")]
262− Resolve {
263− /// Instead of resolving a rev-spec, explain what would be done for the first spec.
264− ///
265− /// Equivalent to the `explain` subcommand.
266− #[clap(short = 'e', long)]
267− explain: bool,
268− /// Show the first resulting object similar to how `git cat-file` would, but don't show the resolved spec.
269− #[clap(short = 'c', long, conflicts_with = "explain")]
270− cat_file: bool,
271− /// rev-specs like `@`, `@~1` or `HEAD^2`.
272− #[clap(required = true)]
273− specs: Vec<std::ffi::OsString>,
274− },
275− /// Return the names and hashes of all previously checked-out branches.
276− #[clap(visible_alias = "prev")]
277− PreviousBranches,
278− }
279−}
280−
281−///
282−pub mod free {
283− #[derive(Debug, clap::Subcommand)]
284− #[clap(visible_alias = "no-repo")]
285− pub enum Subcommands {
286− /// Subcommands for interacting with commit-graphs
287− #[clap(subcommand)]
288− CommitGraph(commitgraph::Subcommands),
289− /// Subcommands for interacting with mailmaps
290− Mailmap {
291− #[clap(flatten)]
292− cmd: mailmap::Platform,
293− },
294− /// Subcommands for interacting with pack files and indices
295− #[clap(subcommand)]
296− Pack(pack::Subcommands),
297− /// Subcommands for interacting with a worktree index, typically at .git/index
298− Index(index::Platform),
299− }
300−
301− ///
302− pub mod commitgraph {
303− use std::path::PathBuf;
304−
305− #[derive(Debug, clap::Subcommand)]
306− pub enum Subcommands {
307− /// Verify the integrity of a commit graph
308− Verify {
309− /// The path to '.git/objects/info/', '.git/objects/info/commit-graphs/', or '.git/objects/info/commit-graph' to validate.
310− path: PathBuf,
311− /// output statistical information about the pack
312− #[clap(long, short = 's')]
313− statistics: bool,
314− },
315− }
316− }
317−
318− pub mod index {
319− use std::path::PathBuf;
320−
321− #[derive(Debug, clap::Parser)]
322− pub struct Platform {
323− /// The object format to assume when reading files that don't inherently know about it, or when writing files.
324− #[clap(long, default_value_t = git_repository::hash::Kind::default(), possible_values(&["SHA1"]))]
325− pub object_hash: git_repository::hash::Kind,
326−
327− /// The path to the index file.
328− #[clap(short = 'i', long, default_value = ".git/index")]
329− pub index_path: PathBuf,
330−
331− /// Subcommands
332− #[clap(subcommand)]
333− pub cmd: Subcommands,
334− }
335−
336− #[derive(Debug, clap::Subcommand)]
337− pub enum Subcommands {
338− /// Validate constraints and assumptions of an index along with its integrity.
339− Verify,
340− /// Print all entries to standard output
341− Entries,
342− /// Print information about the index structure
343− Info {
344− /// Do not extract specific extension information to gain only a superficial idea of the index's composition.
345− #[clap(long)]
346− no_details: bool,
347− },
348− /// Checkout the index into a directory with exclusive write access, similar to what would happen during clone.
349− CheckoutExclusive {
350− /// The path to `.git` repository from which objects can be obtained to write the actual files referenced
351− /// in the index. Use this measure the impact on extracting objects on overall performance.
352− #[clap(long, short = 'r')]
353− repository: Option<PathBuf>,
354− /// Ignore errors and keep checking out as many files as possible, and report all errors at the end of the operation.
355− #[clap(long, short = 'k')]
356− keep_going: bool,
357− /// Enable to query the object database yet write only empty files. This is useful to measure the overhead of ODB query
358− /// compared to writing the bytes to disk.
359− #[clap(long, short = 'e', requires = "repository")]
360− empty_files: bool,
361− /// The directory into which to write all index entries.
362− directory: PathBuf,
363− },
364− }
365− }
366−
367− ///
368− pub mod pack {
369− use std::{ffi::OsString, path::PathBuf};
370−
371− use gitoxide_core as core;
372−
373− #[derive(Debug, clap::Subcommand)]
374− pub enum Subcommands {
375− /// Subcommands for interacting with pack indices (.idx)
376− #[clap(subcommand)]
377− Index(index::Subcommands),
378− /// Subcommands for interacting with multi-pack indices (named "multi-pack-index")
379− MultiIndex(multi_index::Platform),
380− /// Create a new pack with a set of objects.
381− Create {
382− #[clap(long, short = 'r')]
383− /// the directory containing the '.git' repository from which objects should be read.
384− repository: Option<PathBuf>,
385−
386− #[clap(long, short = 'e', possible_values(core::pack::create::ObjectExpansion::variants()))]
387− /// the way objects are expanded. They differ in costs.
388− ///
389− /// Possible values are "none" and "tree-traversal". Default is "none".
390− expansion: Option<core::pack::create::ObjectExpansion>,
391−
392− #[clap(long, default_value_t = 3, requires = "nondeterministic-count")]
393− /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting
394− /// to the globally configured threads.
395− ///
396− /// Use it to have different trade-offs between counting performance and cost in terms of CPU, as the scaling
397− /// here is everything but linear. The effectiveness of each core seems to be no more than 30%.
398− counting_threads: usize,
399−
400− #[clap(long)]
401− /// if set, the counting phase may be accelerated using multithreading.
402− ///
403− /// On the flip side, however, one will loose deterministic counting results which affects the
404− /// way the resulting pack is structured.
405− nondeterministic_count: bool,
406−
407− #[clap(long, short = 's')]
408− /// If set statistical information will be presented to inform about pack creation details.
409− /// It's a form of instrumentation for developers to help improve pack generation.
410− statistics: bool,
411−
412− #[clap(long)]
413− /// The size in megabytes for a cache to speed up pack access for packs with long delta chains.
414− /// It is shared among all threads, so 4 threads would use their own cache 1/4th of the size.
415− ///
416− /// If unset, no cache will be used.
417− pack_cache_size_mb: Option<usize>,
418−
419− #[clap(long)]
420− /// The size in megabytes for a cache to speed up accessing entire objects, bypassing object database access when hit.
421− /// It is shared among all threads, so 4 threads would use their own cache 1/4th of the size.
422− ///
423− /// This cache type is currently only effective when using the 'diff-tree' object expansion.
424− ///
425− /// If unset, no cache will be used.
426− object_cache_size_mb: Option<usize>,
427−
428− #[clap(long)]
429− /// if set, delta-objects whose base object wouldn't be in the pack will not be recompressed as base object, but instead
430− /// refer to its base object using its object id.
431− ///
432− /// This allows for smaller packs but requires the receiver of the pack to resolve these ids before storing the pack.
433− /// Packs produced with this option enabled are only valid in transit, but not at rest.
434− thin: bool,
435−
436− /// The directory into which to write the pack file.
437− #[clap(long, short = 'o')]
438− output_directory: Option<PathBuf>,
439−
440− /// The tips from which to start the commit graph iteration, either as fully qualified commit hashes
441− /// or as branch names.
442− ///
443− /// If empty, we expect to read objects on stdin and default to 'none' as expansion mode.
444− /// Otherwise the expansion mode is 'tree-traversal' by default.
445− tips: Vec<OsString>,
446− },
447− /// Use the git-protocol to receive a pack, emulating a clone.
448− #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
449− Receive {
450− /// The protocol version to use. Valid values are 1 and 2
451− #[clap(long, short = 'p')]
452− protocol: Option<core::net::Protocol>,
453−
454− /// the directory into which to write references. Existing files will be overwritten.
455− ///
456− /// Note that the directory will be created if needed.
457− #[clap(long, short = 'd')]
458− refs_directory: Option<PathBuf>,
459−
460− /// The URLs or path from which to receive the pack.
461− ///
462− /// See here for a list of supported URLs: <https://www.git-scm.com/docs/git-clone#_git_urls>
463− url: String,
464−
465− /// If set once or more times, these references will be fetched instead of all advertised ones.
466− ///
467− /// Note that this requires a reasonably modern git server.
468− #[clap(long = "reference", short = 'r')]
469− refs: Vec<String>,
470−
471− /// The directory into which to write the received pack and index.
472− ///
473− /// If unset, they will be discarded.
474− directory: Option<PathBuf>,
475− },
476− /// Dissolve a pack into its loose objects.
477− ///
478− /// Note that this effectively removes delta compression for an average compression of 2x, creating one file per object in the process.
479− /// Thus this should only be done to dissolve small packs after a fetch.
480− Explode {
481− #[clap(long)]
482− /// Read written objects back and assert they match their source. Fail the operation otherwise.
483− ///
484− /// Only relevant if an object directory is set.
485− verify: bool,
486−
487− /// delete the pack and index file after the operation is successful
488− #[clap(long)]
489− delete_pack: bool,
490−
491− /// The amount of checks to run
492− #[clap(
493− long,
494− short = 'c',
495− default_value = "all",
496− possible_values(core::pack::explode::SafetyCheck::variants())
497− )]
498− check: core::pack::explode::SafetyCheck,
499−
500− /// Compress bytes even when using the sink, i.e. no object directory is specified
501− ///
502− /// This helps to determine overhead related to compression. If unset, the sink will
503− /// only create hashes from bytes, which is usually limited by the speed at which input
504− /// can be obtained.
505− #[clap(long)]
506− sink_compress: bool,
507−
508− /// The '.pack' or '.idx' file to explode into loose objects
509− pack_path: PathBuf,
510−
511− /// The path into which all objects should be written. Commonly '.git/objects'
512− object_path: Option<PathBuf>,
513− },
514− /// Verify the integrity of a pack, index or multi-index file
515− Verify {
516− #[clap(flatten)]
517− args: VerifyOptions,
518−
519− /// The '.pack', '.idx' or 'multi-pack-index' file to validate.
520− path: PathBuf,
521− },
522− }
523−
524− #[derive(Debug, clap::Parser)]
525− pub struct VerifyOptions {
526− /// output statistical information
527− #[clap(long, short = 's')]
528− pub statistics: bool,
529− /// The algorithm used to verify packs. They differ in costs.
530− #[clap(
531− long,
532− short = 'a',
533− default_value = "less-time",
534− possible_values(core::pack::verify::Algorithm::variants())
535− )]
536− pub algorithm: core::pack::verify::Algorithm,
537−
538− #[clap(long, conflicts_with("re-encode"))]
539− /// Decode and parse tags, commits and trees to validate their correctness beyond hashing correctly.
540− ///
541− /// Malformed objects should not usually occur, but could be injected on purpose or accident.
542− /// This will reduce overall performance.
543− pub decode: bool,
544−
545− #[clap(long)]
546− /// Decode and parse tags, commits and trees to validate their correctness, and re-encode them.
547− ///
548− /// This flag is primarily to test the implementation of encoding, and requires to decode the object first.
549− /// Encoding an object after decoding it should yield exactly the same bytes.
550− /// This will reduce overall performance even more, as re-encoding requires to transform zero-copy objects into
551− /// owned objects, causing plenty of allocation to occur.
552− pub re_encode: bool,
553− }
554−
555− ///
556− pub mod multi_index {
557− use std::path::PathBuf;
558−
559− #[derive(Debug, clap::Parser)]
560− pub struct Platform {
561− /// The path to the index file.
562− #[clap(short = 'i', long, default_value = ".git/objects/pack/multi-pack-index")]
563− pub multi_index_path: PathBuf,
564−
565− /// Subcommands
566− #[clap(subcommand)]
567− pub cmd: Subcommands,
568− }
569−
570− #[derive(Debug, clap::Subcommand)]
571− pub enum Subcommands {
572− /// Display all entries of a multi-index: <oid> <pack-id> <pack-offset>
573− Entries,
574− /// Print general information about a multi-index file
575− Info,
576− /// Verify a multi-index quickly without inspecting objects themselves
577− Verify,
578− /// Create a multi-pack index from one or more pack index files, overwriting possibloy existing files.
579− Create {
580− /// Paths to the pack index files to read (with .idx extension).
581− ///
582− /// Note for the multi-index to be useful, it should be side-by-side with the supplied `.idx` files.
583− #[clap(required = true)]
584− index_paths: Vec<PathBuf>,
585− },
586− }
587− }
588−
589− ///
590− pub mod index {
591− use std::path::PathBuf;
592−
593− use gitoxide_core as core;
594−
595− #[derive(Debug, clap::Subcommand)]
596− pub enum Subcommands {
597− /// create a pack index from a pack data file.
598− Create {
599− /// Specify how to iterate the pack, defaults to 'verify'
600− ///
601− /// Valid values are
602− ///
603− /// **as-is** do not do anything and expect the pack file to be valid as per the trailing hash,
604− /// **verify** the input ourselves and validate that it matches with the hash provided in the pack,
605− /// **restore** hash the input ourselves and ignore failing entries, instead finish the pack with the hash we computed
606− /// to keep as many objects as possible.
607− #[clap(
608− long,
609− short = 'i',
610− default_value = "verify",
611− possible_values(core::pack::index::IterationMode::variants())
612− )]
613− iteration_mode: core::pack::index::IterationMode,
614−
615− /// Path to the pack file to read (with .pack extension).
616− ///
617− /// If unset, the pack file is expected on stdin.
618− #[clap(long, short = 'p')]
619− pack_path: Option<PathBuf>,
620−
621− /// The folder into which to place the pack and the generated index file
622− ///
623− /// If unset, only informational output will be provided to standard output.
624− directory: Option<PathBuf>,
625− },
626− }
627− }
628− }
629−
630− ///
631− pub mod mailmap {
632− use std::path::PathBuf;
633−
634− #[derive(Debug, clap::Parser)]
635− pub struct Platform {
636− /// The path to the mailmap file.
637− #[clap(short = 'p', long, default_value = ".mailmap")]
638− pub path: PathBuf,
639−
640− /// Subcommands
641− #[clap(subcommand)]
642− pub cmd: Subcommands,
643− }
644−
645− #[derive(Debug, clap::Subcommand)]
646− pub enum Subcommands {
647− /// Parse all entries in the mailmap and report malformed lines or collisions.
648− Verify,
649− }
650− }
651−}
652−
653−pub mod exclude {
654− use std::ffi::OsString;
655−
656− use git_repository as git;
657−
658− #[derive(Debug, clap::Subcommand)]
659− pub enum Subcommands {
660− /// Check if path-specs are excluded and print the result similar to `git check-ignore`.
661− Query {
662− /// Show actual ignore patterns instead of un-excluding an entry.
663− ///
664− /// That way one can understand why an entry might not be excluded.
665− #[clap(long, short = 'i')]
666− show_ignore_patterns: bool,
667− /// Additional patterns to use for exclusions. They have the highest priority.
668− ///
669− /// Useful for undoing previous patterns using the '!' prefix.
670− #[clap(long, short = 'p')]
671− patterns: Vec<OsString>,
672− /// The git path specifications to check for exclusion, or unset to read from stdin one per line.
673− #[clap(parse(try_from_os_str = std::convert::TryFrom::try_from))]
674− pathspecs: Vec<git::path::Spec>,
675− },
676− }
677−}
src/plumbing/options/free.rs+368−0
@@ -0,0 +1,368 @@
1+#[derive(Debug, clap::Subcommand)]
2+#[clap(visible_alias = "no-repo")]
3+pub enum Subcommands {
4+ /// Subcommands for interacting with commit-graphs
5+ #[clap(subcommand)]
6+ CommitGraph(commitgraph::Subcommands),
7+ /// Subcommands for interacting with mailmaps
8+ Mailmap {
9+ #[clap(flatten)]
10+ cmd: mailmap::Platform,
11+ },
12+ /// Subcommands for interacting with pack files and indices
13+ #[clap(subcommand)]
14+ Pack(pack::Subcommands),
15+ /// Subcommands for interacting with a worktree index, typically at .git/index
16+ Index(index::Platform),
17+}
18+
19+///
20+pub mod commitgraph {
21+ use std::path::PathBuf;
22+
23+ #[derive(Debug, clap::Subcommand)]
24+ pub enum Subcommands {
25+ /// Verify the integrity of a commit graph
26+ Verify {
27+ /// The path to '.git/objects/info/', '.git/objects/info/commit-graphs/', or '.git/objects/info/commit-graph' to validate.
28+ path: PathBuf,
29+ /// output statistical information about the pack
30+ #[clap(long, short = 's')]
31+ statistics: bool,
32+ },
33+ }
34+}
35+
36+pub mod index {
37+ use std::path::PathBuf;
38+
39+ #[derive(Debug, clap::Parser)]
40+ pub struct Platform {
41+ /// The object format to assume when reading files that don't inherently know about it, or when writing files.
42+ #[clap(long, default_value_t = git_repository::hash::Kind::default(), possible_values(&["SHA1"]))]
43+ pub object_hash: git_repository::hash::Kind,
44+
45+ /// The path to the index file.
46+ #[clap(short = 'i', long, default_value = ".git/index")]
47+ pub index_path: PathBuf,
48+
49+ /// Subcommands
50+ #[clap(subcommand)]
51+ pub cmd: Subcommands,
52+ }
53+
54+ #[derive(Debug, clap::Subcommand)]
55+ pub enum Subcommands {
56+ /// Validate constraints and assumptions of an index along with its integrity.
57+ Verify,
58+ /// Print all entries to standard output
59+ Entries,
60+ /// Print information about the index structure
61+ Info {
62+ /// Do not extract specific extension information to gain only a superficial idea of the index's composition.
63+ #[clap(long)]
64+ no_details: bool,
65+ },
66+ /// Checkout the index into a directory with exclusive write access, similar to what would happen during clone.
67+ CheckoutExclusive {
68+ /// The path to `.git` repository from which objects can be obtained to write the actual files referenced
69+ /// in the index. Use this measure the impact on extracting objects on overall performance.
70+ #[clap(long, short = 'r')]
71+ repository: Option<PathBuf>,
72+ /// Ignore errors and keep checking out as many files as possible, and report all errors at the end of the operation.
73+ #[clap(long, short = 'k')]
74+ keep_going: bool,
75+ /// Enable to query the object database yet write only empty files. This is useful to measure the overhead of ODB query
76+ /// compared to writing the bytes to disk.
77+ #[clap(long, short = 'e', requires = "repository")]
78+ empty_files: bool,
79+ /// The directory into which to write all index entries.
80+ directory: PathBuf,
81+ },
82+ }
83+}
84+
85+///
86+pub mod pack {
87+ use std::{ffi::OsString, path::PathBuf};
88+
89+ use gitoxide_core as core;
90+
91+ #[derive(Debug, clap::Subcommand)]
92+ pub enum Subcommands {
93+ /// Subcommands for interacting with pack indices (.idx)
94+ #[clap(subcommand)]
95+ Index(index::Subcommands),
96+ /// Subcommands for interacting with multi-pack indices (named "multi-pack-index")
97+ MultiIndex(multi_index::Platform),
98+ /// Create a new pack with a set of objects.
99+ Create {
100+ #[clap(long, short = 'r')]
101+ /// the directory containing the '.git' repository from which objects should be read.
102+ repository: Option<PathBuf>,
103+
104+ #[clap(long, short = 'e', possible_values(core::pack::create::ObjectExpansion::variants()))]
105+ /// the way objects are expanded. They differ in costs.
106+ ///
107+ /// Possible values are "none" and "tree-traversal". Default is "none".
108+ expansion: Option<core::pack::create::ObjectExpansion>,
109+
110+ #[clap(long, default_value_t = 3, requires = "nondeterministic-count")]
111+ /// The amount of threads to use when counting and the `--nondeterminisitc-count` flag is set, defaulting
112+ /// to the globally configured threads.
113+ ///
114+ /// Use it to have different trade-offs between counting performance and cost in terms of CPU, as the scaling
115+ /// here is everything but linear. The effectiveness of each core seems to be no more than 30%.
116+ counting_threads: usize,
117+
118+ #[clap(long)]
119+ /// if set, the counting phase may be accelerated using multithreading.
120+ ///
121+ /// On the flip side, however, one will loose deterministic counting results which affects the
122+ /// way the resulting pack is structured.
123+ nondeterministic_count: bool,
124+
125+ #[clap(long, short = 's')]
126+ /// If set statistical information will be presented to inform about pack creation details.
127+ /// It's a form of instrumentation for developers to help improve pack generation.
128+ statistics: bool,
129+
130+ #[clap(long)]
131+ /// The size in megabytes for a cache to speed up pack access for packs with long delta chains.
132+ /// It is shared among all threads, so 4 threads would use their own cache 1/4th of the size.
133+ ///
134+ /// If unset, no cache will be used.
135+ pack_cache_size_mb: Option<usize>,
136+
137+ #[clap(long)]
138+ /// The size in megabytes for a cache to speed up accessing entire objects, bypassing object database access when hit.
139+ /// It is shared among all threads, so 4 threads would use their own cache 1/4th of the size.
140+ ///
141+ /// This cache type is currently only effective when using the 'diff-tree' object expansion.
142+ ///
143+ /// If unset, no cache will be used.
144+ object_cache_size_mb: Option<usize>,
145+
146+ #[clap(long)]
147+ /// if set, delta-objects whose base object wouldn't be in the pack will not be recompressed as base object, but instead
148+ /// refer to its base object using its object id.
149+ ///
150+ /// This allows for smaller packs but requires the receiver of the pack to resolve these ids before storing the pack.
151+ /// Packs produced with this option enabled are only valid in transit, but not at rest.
152+ thin: bool,
153+
154+ /// The directory into which to write the pack file.
155+ #[clap(long, short = 'o')]
156+ output_directory: Option<PathBuf>,
157+
158+ /// The tips from which to start the commit graph iteration, either as fully qualified commit hashes
159+ /// or as branch names.
160+ ///
161+ /// If empty, we expect to read objects on stdin and default to 'none' as expansion mode.
162+ /// Otherwise the expansion mode is 'tree-traversal' by default.
163+ tips: Vec<OsString>,
164+ },
165+ /// Use the git-protocol to receive a pack, emulating a clone.
166+ #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
167+ Receive {
168+ /// The protocol version to use. Valid values are 1 and 2
169+ #[clap(long, short = 'p')]
170+ protocol: Option<core::net::Protocol>,
171+
172+ /// the directory into which to write references. Existing files will be overwritten.
173+ ///
174+ /// Note that the directory will be created if needed.
175+ #[clap(long, short = 'd')]
176+ refs_directory: Option<PathBuf>,
177+
178+ /// The URLs or path from which to receive the pack.
179+ ///
180+ /// See here for a list of supported URLs: <https://www.git-scm.com/docs/git-clone#_git_urls>
181+ url: String,
182+
183+ /// If set once or more times, these references will be fetched instead of all advertised ones.
184+ ///
185+ /// Note that this requires a reasonably modern git server.
186+ #[clap(long = "reference", short = 'r')]
187+ refs: Vec<String>,
188+
189+ /// The directory into which to write the received pack and index.
190+ ///
191+ /// If unset, they will be discarded.
192+ directory: Option<PathBuf>,
193+ },
194+ /// Dissolve a pack into its loose objects.
195+ ///
196+ /// Note that this effectively removes delta compression for an average compression of 2x, creating one file per object in the process.
197+ /// Thus this should only be done to dissolve small packs after a fetch.
198+ Explode {
199+ #[clap(long)]
200+ /// Read written objects back and assert they match their source. Fail the operation otherwise.
201+ ///
202+ /// Only relevant if an object directory is set.
203+ verify: bool,
204+
205+ /// delete the pack and index file after the operation is successful
206+ #[clap(long)]
207+ delete_pack: bool,
208+
209+ /// The amount of checks to run
210+ #[clap(
211+ long,
212+ short = 'c',
213+ default_value = "all",
214+ possible_values(core::pack::explode::SafetyCheck::variants())
215+ )]
216+ check: core::pack::explode::SafetyCheck,
217+
218+ /// Compress bytes even when using the sink, i.e. no object directory is specified
219+ ///
220+ /// This helps to determine overhead related to compression. If unset, the sink will
221+ /// only create hashes from bytes, which is usually limited by the speed at which input
222+ /// can be obtained.
223+ #[clap(long)]
224+ sink_compress: bool,
225+
226+ /// The '.pack' or '.idx' file to explode into loose objects
227+ pack_path: PathBuf,
228+
229+ /// The path into which all objects should be written. Commonly '.git/objects'
230+ object_path: Option<PathBuf>,
231+ },
232+ /// Verify the integrity of a pack, index or multi-index file
233+ Verify {
234+ #[clap(flatten)]
235+ args: VerifyOptions,
236+
237+ /// The '.pack', '.idx' or 'multi-pack-index' file to validate.
238+ path: PathBuf,
239+ },
240+ }
241+
242+ #[derive(Debug, clap::Parser)]
243+ pub struct VerifyOptions {
244+ /// output statistical information
245+ #[clap(long, short = 's')]
246+ pub statistics: bool,
247+ /// The algorithm used to verify packs. They differ in costs.
248+ #[clap(
249+ long,
250+ short = 'a',
251+ default_value = "less-time",
252+ possible_values(core::pack::verify::Algorithm::variants())
253+ )]
254+ pub algorithm: core::pack::verify::Algorithm,
255+
256+ #[clap(long, conflicts_with("re-encode"))]
257+ /// Decode and parse tags, commits and trees to validate their correctness beyond hashing correctly.
258+ ///
259+ /// Malformed objects should not usually occur, but could be injected on purpose or accident.
260+ /// This will reduce overall performance.
261+ pub decode: bool,
262+
263+ #[clap(long)]
264+ /// Decode and parse tags, commits and trees to validate their correctness, and re-encode them.
265+ ///
266+ /// This flag is primarily to test the implementation of encoding, and requires to decode the object first.
267+ /// Encoding an object after decoding it should yield exactly the same bytes.
268+ /// This will reduce overall performance even more, as re-encoding requires to transform zero-copy objects into
269+ /// owned objects, causing plenty of allocation to occur.
270+ pub re_encode: bool,
271+ }
272+
273+ ///
274+ pub mod multi_index {
275+ use std::path::PathBuf;
276+
277+ #[derive(Debug, clap::Parser)]
278+ pub struct Platform {
279+ /// The path to the index file.
280+ #[clap(short = 'i', long, default_value = ".git/objects/pack/multi-pack-index")]
281+ pub multi_index_path: PathBuf,
282+
283+ /// Subcommands
284+ #[clap(subcommand)]
285+ pub cmd: Subcommands,
286+ }
287+
288+ #[derive(Debug, clap::Subcommand)]
289+ pub enum Subcommands {
290+ /// Display all entries of a multi-index: <oid> <pack-id> <pack-offset>
291+ Entries,
292+ /// Print general information about a multi-index file
293+ Info,
294+ /// Verify a multi-index quickly without inspecting objects themselves
295+ Verify,
296+ /// Create a multi-pack index from one or more pack index files, overwriting possibloy existing files.
297+ Create {
298+ /// Paths to the pack index files to read (with .idx extension).
299+ ///
300+ /// Note for the multi-index to be useful, it should be side-by-side with the supplied `.idx` files.
301+ #[clap(required = true)]
302+ index_paths: Vec<PathBuf>,
303+ },
304+ }
305+ }
306+
307+ ///
308+ pub mod index {
309+ use std::path::PathBuf;
310+
311+ use gitoxide_core as core;
312+
313+ #[derive(Debug, clap::Subcommand)]
314+ pub enum Subcommands {
315+ /// create a pack index from a pack data file.
316+ Create {
317+ /// Specify how to iterate the pack, defaults to 'verify'
318+ ///
319+ /// Valid values are
320+ ///
321+ /// **as-is** do not do anything and expect the pack file to be valid as per the trailing hash,
322+ /// **verify** the input ourselves and validate that it matches with the hash provided in the pack,
323+ /// **restore** hash the input ourselves and ignore failing entries, instead finish the pack with the hash we computed
324+ /// to keep as many objects as possible.
325+ #[clap(
326+ long,
327+ short = 'i',
328+ default_value = "verify",
329+ possible_values(core::pack::index::IterationMode::variants())
330+ )]
331+ iteration_mode: core::pack::index::IterationMode,
332+
333+ /// Path to the pack file to read (with .pack extension).
334+ ///
335+ /// If unset, the pack file is expected on stdin.
336+ #[clap(long, short = 'p')]
337+ pack_path: Option<PathBuf>,
338+
339+ /// The folder into which to place the pack and the generated index file
340+ ///
341+ /// If unset, only informational output will be provided to standard output.
342+ directory: Option<PathBuf>,
343+ },
344+ }
345+ }
346+}
347+
348+///
349+pub mod mailmap {
350+ use std::path::PathBuf;
351+
352+ #[derive(Debug, clap::Parser)]
353+ pub struct Platform {
354+ /// The path to the mailmap file.
355+ #[clap(short = 'p', long, default_value = ".mailmap")]
356+ pub path: PathBuf,
357+
358+ /// Subcommands
359+ #[clap(subcommand)]
360+ pub cmd: Subcommands,
361+ }
362+
363+ #[derive(Debug, clap::Subcommand)]
364+ pub enum Subcommands {
365+ /// Parse all entries in the mailmap and report malformed lines or collisions.
366+ Verify,
367+ }
368+}
src/plumbing/options/mod.rs+310−0
@@ -0,0 +1,310 @@
1+use std::path::PathBuf;
2+
3+use git_repository as git;
4+use git_repository::bstr::BString;
5+use gitoxide_core as core;
6+
7+#[derive(Debug, clap::Parser)]
8+#[clap(name = "gix-plumbing", about = "The git underworld", version = clap::crate_version!())]
9+#[clap(subcommand_required = true)]
10+#[clap(arg_required_else_help = true)]
11+pub struct Args {
12+ /// The repository to access.
13+ #[clap(short = 'r', long, default_value = ".")]
14+ pub repository: PathBuf,
15+
16+ /// Add these values to the configuration in the form of `key=value` or `key`.
17+ ///
18+ /// For example, if `key` is `core.abbrev`, set configuration like `[core] abbrev = key`,
19+ /// or `remote.origin.url = foo` to set `[remote "origin"] url = foo`.
20+ #[clap(long, short = 'c', parse(try_from_os_str = git::env::os_str_to_bstring))]
21+ pub config: Vec<BString>,
22+
23+ #[clap(long, short = 't')]
24+ /// The amount of threads to use for some operations.
25+ ///
26+ /// If unset, or the value is 0, there is no limit and all logical cores can be used.
27+ pub threads: Option<usize>,
28+
29+ /// Display verbose messages and progress information
30+ #[clap(long, short = 'v')]
31+ pub verbose: bool,
32+
33+ /// Bring up a terminal user interface displaying progress visually
34+ #[cfg(feature = "prodash-render-tui")]
35+ #[clap(long, conflicts_with("verbose"))]
36+ pub progress: bool,
37+
38+ /// The progress TUI will stay up even though the work is already completed.
39+ ///
40+ /// Use this to be able to read progress messages or additional information visible in the TUI log pane.
41+ #[cfg(feature = "prodash-render-tui")]
42+ #[clap(long, conflicts_with("verbose"), requires("progress"))]
43+ pub progress_keep_open: bool,
44+
45+ /// Determine the format to use when outputting statistics.
46+ #[clap(
47+ long,
48+ short = 'f',
49+ default_value = "human",
50+ possible_values(core::OutputFormat::variants())
51+ )]
52+ pub format: core::OutputFormat,
53+
54+ /// The object format to assume when reading files that don't inherently know about it, or when writing files.
55+ #[clap(long, default_value_t = git_repository::hash::Kind::default(), possible_values(&["SHA1"]))]
56+ pub object_hash: git_repository::hash::Kind,
57+
58+ #[clap(subcommand)]
59+ pub cmd: Subcommands,
60+}
61+
62+#[derive(Debug, clap::Subcommand)]
63+pub enum Subcommands {
64+ /// Interact with the object database.
65+ #[clap(subcommand)]
66+ Odb(odb::Subcommands),
67+ /// Interact with tree objects.
68+ #[clap(subcommand)]
69+ Tree(tree::Subcommands),
70+ /// Interact with commit objects.
71+ #[clap(subcommand)]
72+ Commit(commit::Subcommands),
73+ /// Verify the integrity of the entire repository
74+ Verify {
75+ #[clap(flatten)]
76+ args: free::pack::VerifyOptions,
77+ },
78+ /// Query and obtain information about revisions.
79+ #[clap(subcommand)]
80+ Revision(revision::Subcommands),
81+ /// A program just like `git credential`.
82+ #[clap(subcommand)]
83+ Credential(credential::Subcommands),
84+ /// Interact with the mailmap.
85+ #[clap(subcommand)]
86+ Mailmap(mailmap::Subcommands),
87+ /// Interact with the remote hosts.
88+ Remote(remote::Platform),
89+ /// Interact with the exclude files like .gitignore.
90+ #[clap(subcommand)]
91+ Exclude(exclude::Subcommands),
92+ /// Display overall progress of the gitoxide project as seen from the perspective of git-config.
93+ Progress,
94+ Config(config::Platform),
95+ /// Subcommands that need no git repository to run.
96+ #[clap(subcommand)]
97+ Free(free::Subcommands),
98+}
99+
100+pub mod config {
101+ /// Print all entries in a configuration file or access other sub-commands
102+ #[derive(Debug, clap::Parser)]
103+ #[clap(subcommand_required(false))]
104+ pub struct Platform {
105+ /// The filter terms to limit the output to matching sections and subsections only.
106+ ///
107+ /// Typical filters are `branch` or `remote.origin` or `remote.or*` - git-style globs are supported
108+ /// and comparisons are case-insensitive.
109+ pub filter: Vec<String>,
110+ }
111+}
112+
113+pub mod remote {
114+ use git_repository as git;
115+
116+ #[derive(Debug, clap::Parser)]
117+ pub struct Platform {
118+ /// The name of the remote to connect to.
119+ ///
120+ /// If unset, the current branch will determine the remote.
121+ #[clap(long, short = 'n')]
122+ pub name: Option<String>,
123+
124+ /// Connect directly to the given URL, forgoing any configuration from the repository.
125+ #[clap(long, short = 'u', conflicts_with("name"), parse(try_from_os_str = std::convert::TryFrom::try_from))]
126+ pub url: Option<git::Url>,
127+
128+ /// Output additional typically information provided by the server as part of the connection handshake.
129+ #[clap(long)]
130+ pub handshake_info: bool,
131+
132+ /// Subcommands
133+ #[clap(subcommand)]
134+ pub cmd: Subcommands,
135+ }
136+
137+ #[derive(Debug, clap::Subcommand)]
138+ #[clap(visible_alias = "remotes")]
139+ pub enum Subcommands {
140+ /// Print all references available on the remote.
141+ #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
142+ Refs,
143+ /// Print all references available on the remote as filtered through ref-specs.
144+ #[cfg(any(feature = "gitoxide-core-async-client", feature = "gitoxide-core-blocking-client"))]
145+ RefMap {
146+ /// Override the built-in and configured ref-specs with one or more of the given ones.
147+ #[clap(parse(try_from_os_str = git::env::os_str_to_bstring))]
148+ ref_spec: Vec<git_repository::bstr::BString>,
149+ },
150+ }
151+}
152+
153+pub mod mailmap {
154+ #[derive(Debug, clap::Subcommand)]
155+ pub enum Subcommands {
156+ /// Print all entries in configured mailmaps, inform about errors as well.
157+ Entries,
158+ }
159+}
160+
161+pub mod odb {
162+ #[derive(Debug, clap::Subcommand)]
163+ pub enum Subcommands {
164+ /// Print all object names.
165+ Entries,
166+ /// Provide general information about the object database.
167+ Info,
168+ }
169+}
170+
171+pub mod tree {
172+ #[derive(Debug, clap::Subcommand)]
173+ pub enum Subcommands {
174+ /// Print entries in a given tree
175+ Entries {
176+ /// Traverse the entire tree and its subtrees respectively, not only this tree.
177+ #[clap(long, short = 'r')]
178+ recursive: bool,
179+
180+ /// Provide files size as well. This is expensive as the object is decoded entirely.
181+ #[clap(long, short = 'e')]
182+ extended: bool,
183+
184+ /// The tree to traverse, or the tree at `HEAD` if unspecified.
185+ treeish: Option<String>,
186+ },
187+ /// Provide information about a tree.
188+ Info {
189+ /// Provide files size as well. This is expensive as the object is decoded entirely.
190+ #[clap(long, short = 'e')]
191+ extended: bool,
192+ /// The tree to traverse, or the tree at `HEAD` if unspecified.
193+ treeish: Option<String>,
194+ },
195+ }
196+}
197+
198+pub mod commit {
199+ #[derive(Debug, clap::Subcommand)]
200+ pub enum Subcommands {
201+ /// Describe the current commit or the given one using the name of the closest annotated tag in its ancestry.
202+ Describe {
203+ /// Use annotated tag references only, not all tags.
204+ #[clap(long, short = 't', conflicts_with("all-refs"))]
205+ annotated_tags: bool,
206+
207+ /// Use all references under the `ref/` namespaces, which includes tag references, local and remote branches.
208+ #[clap(long, short = 'a', conflicts_with("annotated-tags"))]
209+ all_refs: bool,
210+
211+ /// Only follow the first parent when traversing the commit graph.
212+ #[clap(long, short = 'f')]
213+ first_parent: bool,
214+
215+ /// Always display the long format, even if that would not be necessary as the id is located directly on a reference.
216+ #[clap(long, short = 'l')]
217+ long: bool,
218+
219+ /// Consider only the given `n` candidates. This can take longer, but potentially produces more accurate results.
220+ #[clap(long, short = 'c', default_value = "10")]
221+ max_candidates: usize,
222+
223+ /// Print information on stderr to inform about performance statistics
224+ #[clap(long, short = 's')]
225+ statistics: bool,
226+
227+ #[clap(long)]
228+ /// If there was no way to describe the commit, fallback to using the abbreviated input revision.
229+ always: bool,
230+
231+ /// A specification of the revision to use, or the current `HEAD` if unset.
232+ rev_spec: Option<String>,
233+ },
234+ }
235+}
236+
237+pub mod credential {
238+ #[derive(Debug, clap::Subcommand)]
239+ pub enum Subcommands {
240+ /// Get the credentials fed for `url=<url>` via STDIN.
241+ #[clap(visible_alias = "get")]
242+ Fill,
243+ /// Approve the information piped via STDIN as obtained with last call to `fill`
244+ #[clap(visible_alias = "store")]
245+ Approve,
246+ /// Try to resolve the given revspec and print the object names.
247+ #[clap(visible_alias = "erase")]
248+ Reject,
249+ }
250+}
251+
252+pub mod revision {
253+ #[derive(Debug, clap::Subcommand)]
254+ #[clap(visible_alias = "rev", visible_alias = "r")]
255+ pub enum Subcommands {
256+ /// List all commits reachable from the given rev-spec.
257+ #[clap(visible_alias = "l")]
258+ List { spec: std::ffi::OsString },
259+ /// Provide the revision specification like `@~1` to explain.
260+ #[clap(visible_alias = "e")]
261+ Explain { spec: std::ffi::OsString },
262+ /// Try to resolve the given revspec and print the object names.
263+ #[clap(visible_alias = "query", visible_alias = "parse", visible_alias = "p")]
264+ Resolve {
265+ /// Instead of resolving a rev-spec, explain what would be done for the first spec.
266+ ///
267+ /// Equivalent to the `explain` subcommand.
268+ #[clap(short = 'e', long)]
269+ explain: bool,
270+ /// Show the first resulting object similar to how `git cat-file` would, but don't show the resolved spec.
271+ #[clap(short = 'c', long, conflicts_with = "explain")]
272+ cat_file: bool,
273+ /// rev-specs like `@`, `@~1` or `HEAD^2`.
274+ #[clap(required = true)]
275+ specs: Vec<std::ffi::OsString>,
276+ },
277+ /// Return the names and hashes of all previously checked-out branches.
278+ #[clap(visible_alias = "prev")]
279+ PreviousBranches,
280+ }
281+}
282+
283+pub mod exclude {
284+ use std::ffi::OsString;
285+
286+ use git_repository as git;
287+
288+ #[derive(Debug, clap::Subcommand)]
289+ pub enum Subcommands {
290+ /// Check if path-specs are excluded and print the result similar to `git check-ignore`.
291+ Query {
292+ /// Show actual ignore patterns instead of un-excluding an entry.
293+ ///
294+ /// That way one can understand why an entry might not be excluded.
295+ #[clap(long, short = 'i')]
296+ show_ignore_patterns: bool,
297+ /// Additional patterns to use for exclusions. They have the highest priority.
298+ ///
299+ /// Useful for undoing previous patterns using the '!' prefix.
300+ #[clap(long, short = 'p')]
301+ patterns: Vec<OsString>,
302+ /// The git path specifications to check for exclusion, or unset to read from stdin one per line.
303+ #[clap(parse(try_from_os_str = std::convert::TryFrom::try_from))]
304+ pathspecs: Vec<git::path::Spec>,
305+ },
306+ }
307+}
308+
309+///
310+pub mod free;
src/plumbing/progress.rs+305−0
@@ -0,0 +1,305 @@
1+use crosstermion::crossterm::style::Stylize;
2+use owo_colors::OwoColorize;
3+use std::fmt::{Display, Formatter};
4+use tabled::{Style, TableIteratorExt, Tabled};
5+
6+#[derive(Clone)]
7+enum Usage {
8+ NotApplicable,
9+ Planned {
10+ note: Option<&'static str>,
11+ },
12+ InModule {
13+ name: &'static str,
14+ deviation: Option<&'static str>,
15+ },
16+ /// Needs analysis
17+ Puzzled,
18+}
19+use Usage::*;
20+
21+impl Display for Usage {
22+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
23+ match self {
24+ Puzzled => f.write_str("❓")?,
25+ NotApplicable => f.write_str("not applicable")?,
26+ Planned { note } => {
27+ write!(f, "{}", "planned".blink())?;
28+ if let Some(note) = note {
29+ write!(f, " ℹ {} ℹ", note.bright_white())?;
30+ }
31+ }
32+ InModule { name, deviation } => {
33+ write!(f, "mod {name}")?;
34+ if let Some(deviation) = deviation {
35+ write!(f, "{}", format!(" ❗️{deviation}❗️").bright_white())?
36+ }
37+ }
38+ }
39+ Ok(())
40+ }
41+}
42+
43+impl Usage {
44+ pub fn icon(&self) -> &'static str {
45+ match self {
46+ Puzzled => "?",
47+ NotApplicable => "❌",
48+ Planned { .. } => "🕒",
49+ InModule { deviation, .. } => deviation.is_some().then(|| "👌️").unwrap_or("✅"),
50+ }
51+ }
52+}
53+
54+#[derive(Clone)]
55+struct Record {
56+ config: &'static str,
57+ usage: Usage,
58+}
59+
60+impl Tabled for Record {
61+ const LENGTH: usize = 3;
62+
63+ fn fields(&self) -> Vec<String> {
64+ let mut tokens = self.config.split('.');
65+ let mut buf = vec![tokens.next().expect("present").bold().to_string()];
66+ buf.extend(tokens.map(ToOwned::to_owned));
67+
68+ vec![self.usage.icon().into(), buf.join("."), self.usage.to_string()]
69+ }
70+
71+ fn headers() -> Vec<String> {
72+ vec![]
73+ }
74+}
75+
76+static GIT_CONFIG: &[Record] = &[
77+ Record {
78+ config: "core.bare",
79+ usage: InModule {
80+ name: "config::cache",
81+ deviation: None,
82+ },
83+ },
84+ Record {
85+ config: "core.excludesFile",
86+ usage: InModule {
87+ name: "config::cache",
88+ deviation: None,
89+ },
90+ },
91+ Record {
92+ config: "core.abbrev",
93+ usage: InModule {
94+ name: "config::cache",
95+ deviation: None,
96+ },
97+ },
98+ Record {
99+ config: "core.ignoreCase",
100+ usage: InModule {
101+ name: "config::cache",
102+ deviation: None,
103+ },
104+ },
105+ Record {
106+ config: "core.multiPackIndex",
107+ usage: InModule {
108+ name: "config::cache",
109+ deviation: None,
110+ },
111+ },
112+ Record {
113+ config: "core.disambiguate",
114+ usage: InModule {
115+ name: "config::cache",
116+ deviation: None,
117+ },
118+ },
119+ Record {
120+ config: "core.filesRefLockTimeout",
121+ usage: InModule {name: "config::cache::access", deviation: None},
122+ },
123+ Record {
124+ config: "core.packedRefsTimeout",
125+ usage: InModule {name: "config::cache::access", deviation: None},
126+ },
127+ Record {
128+ config: "core.logAllRefUpdates",
129+ usage: InModule {
130+ name: "config::cache",
131+ deviation: None,
132+ },
133+ },
134+ Record {
135+ config: "core.repositoryFormatVersion",
136+ usage: InModule {
137+ name: "config::cache::incubate",
138+ deviation: None,
139+ },
140+ },
141+ Record {
142+ config: "extensions.objectFormat",
143+ usage: InModule {
144+ name: "config::cache::incubate",
145+ deviation: Some(
146+ "Support for SHA256 is prepared but not fully implemented yet. For now we abort when encountered.",
147+ ),
148+ },
149+ },
150+ Record {
151+ config: "committer.name",
152+ usage: InModule {
153+ name: "repository::identity",
154+ deviation: None,
155+ },
156+ },
157+ Record {
158+ config: "committer.email",
159+ usage: InModule {
160+ name: "repository::identity",
161+ deviation: None,
162+ },
163+ },
164+ Record {
165+ config: "author.name",
166+ usage: InModule {
167+ name: "repository::identity",
168+ deviation: None,
169+ },
170+ },
171+ Record {
172+ config: "author.email",
173+ usage: InModule {
174+ name: "repository::identity",
175+ deviation: None,
176+ },
177+ },
178+ Record {
179+ config: "user.name",
180+ usage: InModule {
181+ name: "repository::identity",
182+ deviation: Some("defaults to 'gitoxide'"),
183+ },
184+ },
185+ Record {
186+ config: "user.email",
187+ usage: InModule {
188+ name: "repository::identity",
189+ deviation: Some("defaults to 'gitoxide@localhost'"),
190+ },
191+ },
192+ Record {
193+ config: "fetch.recurseSubmodules",
194+ usage: Planned {
195+ note: Some("Seems useful for cargo as well"),
196+ },
197+ },
198+ Record {
199+ config: "fetch.fsckObjects",
200+ usage: Puzzled,
201+ },
202+ Record {
203+ config: "fetch.fsck.<msg-id>",
204+ usage: Puzzled,
205+ },
206+ Record {
207+ config: "fetch.fsck.skipList",
208+ usage: Puzzled,
209+ },
210+ Record {
211+ config: "fetch.unpackLimit",
212+ usage: Planned { note: None },
213+ },
214+ Record {
215+ config: "fetch.prune",
216+ usage: Planned { note: None },
217+ },
218+ Record {
219+ config: "fetch.pruneTags",
220+ usage: Planned { note: None },
221+ },
222+ Record {
223+ config: "fetch.writeCommitGraph",
224+ usage: Planned { note: None },
225+ },
226+ Record {
227+ config: "fetch.parallel",
228+ usage: Planned { note: None },
229+ },
230+ Record {
231+ config: "fetch.showForcedUpdates",
232+ usage: NotApplicable,
233+ },
234+ Record {
235+ config: "fetch.output",
236+ usage: NotApplicable,
237+ },
238+ Record {
239+ config: "fetch.negotiationAlgorithm",
240+ usage: Planned {
241+ note: Some("Implements our own 'naive' algorithm, only"),
242+ },
243+ },
244+ Record {
245+ config: "pack.threads",
246+ usage: InModule {
247+ name: "remote::connection::fetch",
248+ deviation: Some("if unset, it uses all threads as opposed to just 1"),
249+ },
250+ },
251+ Record {
252+ config: "pack.indexVersion",
253+ usage: InModule {
254+ name: "remote::connection::fetch",
255+ deviation: None,
256+ },
257+ },
258+ Record {
259+ config: "protocol.allow",
260+ usage: InModule {
261+ name: "remote::url::scheme_permission",
262+ deviation: None,
263+ },
264+ },
265+ Record {
266+ config: "protocol.<name>.allow",
267+ usage: InModule {
268+ name: "remote::url::scheme_permission",
269+ deviation: None,
270+ },
271+ },
272+ Record {
273+ config: "remotes.<group>",
274+ usage: Planned {
275+ note: Some("useful for multi-remote fetches as part of the standard API, maybe just `group(name) -> Option<Vec<Remote>>`"),
276+ },
277+ },
278+ Record {
279+ config: "url.<base>.insteadOf",
280+ usage: InModule {
281+ name: "remote::url::rewrite",
282+ deviation: None,
283+ },
284+ },
285+ Record {
286+ config: "url.<base>.pushInsteadOf",
287+ usage: InModule {
288+ name: "remote::url::rewrite",
289+ deviation: None,
290+ },
291+ },
292+];
293+
294+/// A programmatic way to record and display progress.
295+pub fn show_progress() -> anyhow::Result<()> {
296+ let sorted = {
297+ let mut v: Vec<_> = GIT_CONFIG.into();
298+ v.sort_by_key(|r| r.config);
299+ v
300+ };
301+
302+ println!("{}", sorted.table().with(Style::blank()));
303+ println!("\nTotal records: {}", GIT_CONFIG.len());
304+ Ok(())
305+}
tests/snapshots/plumbing/no-repo/pack/index/create/no-output-dir-as-json-success+1−1
@@ -1,6 +1,6 @@
11 {
22 "index": {
3− "index_kind": "V2",
3+ "index_version": "V2",
44 "index_hash": {
55 "Sha1": [
66 86,
tests/snapshots/plumbing/no-repo/pack/index/create/output-dir-restore-as-json-success+1−1
@@ -1,6 +1,6 @@
11 {
22 "index": {
3− "index_kind": "V2",
3+ "index_version": "V2",
44 "index_hash": {
55 "Sha1": [
66 44,
tests/snapshots/plumbing/no-repo/pack/receive/file-v-any-no-output-json+1−1
@@ -1,6 +1,6 @@
11 {
22 "index": {
3− "index_kind": "V2",
3+ "index_version": "V2",
44 "index_hash": "c787de2aafb897417ca8167baeb146eabd18bc5f",
55 "data_hash": "346574b7331dc3a1724da218d622c6e1b6c66a57",
66 "num_objects": 9
tests/tools/src/lib.rs+8−0
@@ -535,6 +535,14 @@ impl<'a> Env<'a> { · 534 unmodified lines
535535 self.altered_vars.push((var, prev));
536536 self
537537 }
538+
539+ /// Set `var` to `value`.
540+ pub fn unset(mut self, var: &'a str) -> Self {
541+ let prev = std::env::var_os(var);
542+ std::env::remove_var(var);
543+ self.altered_vars.push((var, prev));
544+ self
545+ }
538546 }
539547
540548 impl<'a> Drop for Env<'a> {