Unified · Split
| @@ -52,7 +52,9 @@ impl PrepareFetch { · 51 unmodified lines | |||
| 52 | P::SubProgress: 'static, | 52 | P::SubProgress: 'static, |
| 53 | { | 53 | { |
| 54 | use crate::bstr::{BStr, ByteVec}; | 54 | use crate::bstr::{BStr, ByteVec}; |
| 55 | use crate::remote::fetch::RefLogMessage; | ||
| 55 | use git_ref::transaction::{LogChange, RefLog}; | 56 | use git_ref::transaction::{LogChange, RefLog}; |
| 57 | |||
| 56 | fn write_remote_to_local_config_file( | 58 | fn write_remote_to_local_config_file( |
| 57 | remote: &mut crate::Remote<'_>, | 59 | remote: &mut crate::Remote<'_>, |
| 58 | remote_name: String, | 60 | remote_name: String, |
| @@ -207,15 +209,18 @@ impl PrepareFetch { · 148 unmodified lines | |||
| 207 | if pending_pack.ref_map().object_hash != repo.object_hash() { | 209 | if pending_pack.ref_map().object_hash != repo.object_hash() { |
| 208 | unimplemented!("configure repository to expect a different object hash as advertised by the server") | 210 | unimplemented!("configure repository to expect a different object hash as advertised by the server") |
| 209 | } | 211 | } |
| 210 | let outcome = pending_pack.receive(should_interrupt)?; | ||
| 211 | |||
| 212 | replace_changed_local_config_file(repo, config); | ||
| 213 | |||
| 214 | let reflog_message = { | 212 | let reflog_message = { |
| 215 | let mut b = self.url.to_bstring(); | 213 | let mut b = self.url.to_bstring(); |
| 216 | b.insert_str(0, "clone: from "); | 214 | b.insert_str(0, "clone: from "); |
| 217 | b | 215 | b |
| 218 | }; | 216 | }; |
| 217 | let outcome = pending_pack | ||
| 218 | .with_reflog_message(RefLogMessage::Override { | ||
| 219 | message: reflog_message.clone(), | ||
| 220 | }) | ||
| 221 | .receive(should_interrupt)?; | ||
| 222 | |||
| 223 | replace_changed_local_config_file(repo, config); | ||
| 219 | update_head(repo, &outcome.ref_map.remote_refs, reflog_message.as_ref())?; | 224 | update_head(repo, &outcome.ref_map.remote_refs, reflog_message.as_ref())?; |
| 220 | 225 | ||
| 221 | Ok((self.repo.take().expect("still present"), outcome)) | 226 | Ok((self.repo.take().expect("still present"), outcome)) |
| @@ -0,0 +1,31 @@ | |||
| 1 | /// The error returned by [`receive()`](super::Prepare::receive()). | ||
| 2 | #[derive(Debug, thiserror::Error)] | ||
| 3 | #[allow(missing_docs)] | ||
| 4 | pub enum Error { | ||
| 5 | #[error("{message}{}", desired.map(|n| format!(" (got {})", n)).unwrap_or_default())] | ||
| 6 | Configuration { | ||
| 7 | message: &'static str, | ||
| 8 | desired: Option<i64>, | ||
| 9 | source: Option<git_config::value::Error>, | ||
| 10 | }, | ||
| 11 | #[error("Could not decode server reply")] | ||
| 12 | FetchResponse(#[from] git_protocol::fetch::response::Error), | ||
| 13 | #[error("Cannot fetch from a remote that uses {remote} while local repository uses {local} for object hashes")] | ||
| 14 | IncompatibleObjectHash { | ||
| 15 | local: git_hash::Kind, | ||
| 16 | remote: git_hash::Kind, | ||
| 17 | }, | ||
| 18 | #[error(transparent)] | ||
| 19 | Negotiate(#[from] super::negotiate::Error), | ||
| 20 | #[error(transparent)] | ||
| 21 | Client(#[from] git_protocol::transport::client::Error), | ||
| 22 | #[error(transparent)] | ||
| 23 | WritePack(#[from] git_pack::bundle::write::Error), | ||
| 24 | #[error(transparent)] | ||
| 25 | UpdateRefs(#[from] super::refs::update::Error), | ||
| 26 | #[error("Failed to remove .keep file at \"{}\"", path.display())] | ||
| 27 | RemovePackKeepFile { | ||
| 28 | path: std::path::PathBuf, | ||
| 29 | source: std::io::Error, | ||
| 30 | }, | ||
| 31 | } | ||
| @@ -1,5 +1,6 @@ | |||
| 1 | use std::sync::atomic::AtomicBool; | 1 | use std::sync::atomic::AtomicBool; |
| 2 | 2 | ||
| 3 | use crate::bstr::BString; | ||
| 3 | use git_odb::FindExt; | 4 | use git_odb::FindExt; |
| 4 | use git_protocol::transport::client::Transport; | 5 | use git_protocol::transport::client::Transport; |
| 5 | 6 | ||
| @@ -13,40 +14,31 @@ use crate::{ · 7 unmodified lines | |||
| 13 | Progress, | 14 | Progress, |
| 14 | }; | 15 | }; |
| 15 | 16 | ||
| 16 | mod error { | 17 | mod error; |
| 17 | /// The error returned by [`receive()`](super::Prepare::receive()). | 18 | pub use error::Error; |
| 18 | #[derive(Debug, thiserror::Error)] | 19 | |
| 19 | #[allow(missing_docs)] | 20 | /// The way reflog messages should be composed whenever a ref is written with recent objects from a remote. |
| 20 | pub enum Error { | 21 | pub enum RefLogMessage { |
| 21 | #[error("{message}{}", desired.map(|n| format!(" (got {})", n)).unwrap_or_default())] | 22 | /// Prefix the log with `action` and generate the typical suffix as `git` would. |
| 22 | Configuration { | 23 | Prefixed { |
| 23 | message: &'static str, | 24 | /// The action to use, like `fetch` or `pull`. |
| 24 | desired: Option<i64>, | 25 | action: String, |
| 25 | source: Option<git_config::value::Error>, | 26 | }, |
| 26 | }, | 27 | /// Control the entire message, using `message` verbatim. |
| 27 | #[error("Could not decode server reply")] | 28 | Override { |
| 28 | FetchResponse(#[from] git_protocol::fetch::response::Error), | 29 | /// The complete reflog message. |
| 29 | #[error("Cannot fetch from a remote that uses {remote} while local repository uses {local} for object hashes")] | 30 | message: BString, |
| 30 | IncompatibleObjectHash { | 31 | }, |
| 31 | local: git_hash::Kind, | 32 | } |
| 32 | remote: git_hash::Kind, | 33 | |
| 33 | }, | 34 | impl RefLogMessage { |
| 34 | #[error(transparent)] | 35 | pub(crate) fn compose(&self, context: &str) -> BString { |
| 35 | Negotiate(#[from] super::negotiate::Error), | 36 | match self { |
| 36 | #[error(transparent)] | 37 | RefLogMessage::Prefixed { action } => format!("{}: {}", action, context).into(), |
| 37 | Client(#[from] git_protocol::transport::client::Error), | 38 | RefLogMessage::Override { message } => message.to_owned(), |
| 38 | #[error(transparent)] | 39 | } |
| 39 | WritePack(#[from] git_pack::bundle::write::Error), | ||
| 40 | #[error(transparent)] | ||
| 41 | UpdateRefs(#[from] super::refs::update::Error), | ||
| 42 | #[error("Failed to remove .keep file at \"{}\"", path.display())] | ||
| 43 | RemovePackKeepFile { | ||
| 44 | path: std::path::PathBuf, | ||
| 45 | source: std::io::Error, | ||
| 46 | }, | ||
| 47 | } | 40 | } |
| 48 | } | 41 | } |
| 49 | pub use error::Error; | ||
| 50 | 42 | ||
| 51 | /// The status of the repository after the fetch operation | 43 | /// The status of the repository after the fetch operation |
| 52 | #[derive(Debug, Clone)] | 44 | #[derive(Debug, Clone)] |
| @@ -118,6 +110,7 @@ where · 65 unmodified lines | |||
| 118 | con: Some(self), | 110 | con: Some(self), |
| 119 | ref_map, | 111 | ref_map, |
| 120 | dry_run: DryRun::No, | 112 | dry_run: DryRun::No, |
| 113 | reflog_message: None, | ||
| 121 | }) | 114 | }) |
| 122 | } | 115 | } |
| 123 | } | 116 | } |
| @@ -267,7 +260,9 @@ where · 143 unmodified lines | |||
| 267 | 260 | ||
| 268 | let update_refs = refs::update( | 261 | let update_refs = refs::update( |
| 269 | repo, | 262 | repo, |
| 270 | "fetch", | 263 | self.reflog_message |
| 264 | .take() | ||
| 265 | .unwrap_or_else(|| RefLogMessage::Prefixed { action: "fetch".into() }), | ||
| 271 | &self.ref_map.mappings, | 266 | &self.ref_map.mappings, |
| 272 | con.remote.refspecs(remote::Direction::Fetch), | 267 | con.remote.refspecs(remote::Direction::Fetch), |
| 273 | self.dry_run, | 268 | self.dry_run, |
| @@ -323,6 +318,7 @@ where · 49 unmodified lines | |||
| 323 | con: Option<Connection<'remote, 'repo, T, P>>, | 318 | con: Option<Connection<'remote, 'repo, T, P>>, |
| 324 | ref_map: RefMap, | 319 | ref_map: RefMap, |
| 325 | dry_run: DryRun, | 320 | dry_run: DryRun, |
| 321 | reflog_message: Option<RefLogMessage>, | ||
| 326 | } | 322 | } |
| 327 | 323 | ||
| 328 | /// Builder | 324 | /// Builder |
| @@ -337,6 +333,12 @@ where · 8 unmodified lines | |||
| 337 | self.dry_run = enabled.then(|| DryRun::Yes).unwrap_or(DryRun::No); | 333 | self.dry_run = enabled.then(|| DryRun::Yes).unwrap_or(DryRun::No); |
| 338 | self | 334 | self |
| 339 | } | 335 | } |
| 336 | |||
| 337 | /// Set the reflog message to use when updating refs after fetching a pack. | ||
| 338 | pub fn with_reflog_message(mut self, reflog_message: RefLogMessage) -> Self { | ||
| 339 | self.reflog_message = reflog_message.into(); | ||
| 340 | self | ||
| 341 | } | ||
| 340 | } | 342 | } |
| 341 | 343 | ||
| 342 | impl<'remote, 'repo, T, P> Drop for Prepare<'remote, 'repo, T, P> | 344 | impl<'remote, 'repo, T, P> Drop for Prepare<'remote, 'repo, T, P> |
| @@ -7,6 +7,7 @@ use git_ref::{ · 6 unmodified lines | |||
| 7 | Target, TargetRef, | 7 | Target, TargetRef, |
| 8 | }; | 8 | }; |
| 9 | 9 | ||
| 10 | use crate::remote::fetch::RefLogMessage; | ||
| 10 | use crate::{ | 11 | use crate::{ |
| 11 | ext::ObjectIdExt, | 12 | ext::ObjectIdExt, |
| 12 | remote::{fetch, fetch::refs::update::Mode}, | 13 | remote::{fetch, fetch::refs::update::Mode}, |
| @@ -41,7 +42,7 @@ impl From<update::Mode> for Update { · 28 unmodified lines | |||
| 41 | /// It can be used to produce typical information that one is used to from `git fetch`. | 42 | /// It can be used to produce typical information that one is used to from `git fetch`. |
| 42 | pub(crate) fn update( | 43 | pub(crate) fn update( |
| 43 | repo: &Repository, | 44 | repo: &Repository, |
| 44 | action: &str, | 45 | message: RefLogMessage, |
| 45 | mappings: &[fetch::Mapping], | 46 | mappings: &[fetch::Mapping], |
| 46 | refspecs: &[git_refspec::RefSpec], | 47 | refspecs: &[git_refspec::RefSpec], |
| 47 | dry_run: fetch::DryRun, | 48 | dry_run: fetch::DryRun, |
| @@ -161,7 +162,7 @@ pub(crate) fn update( · 113 unmodified lines | |||
| 161 | log: LogChange { | 162 | log: LogChange { |
| 162 | mode: RefLog::AndReference, | 163 | mode: RefLog::AndReference, |
| 163 | force_create_reflog: false, | 164 | force_create_reflog: false, |
| 164 | message: format!("{}: {}", action, reflog_message).into(), | 165 | message: message.compose(reflog_message), |
| 165 | }, | 166 | }, |
| 166 | expected: previous_value, | 167 | expected: previous_value, |
| 167 | new: Target::Peeled(remote_id.into()), | 168 | new: Target::Peeled(remote_id.into()), |
| @@ -29,9 +29,11 @@ mod update { · 28 unmodified lines | |||
| 29 | let repo = git::open_opts(dir.path().join(name), git::open::Options::isolated()).unwrap(); | 29 | let repo = git::open_opts(dir.path().join(name), git::open::Options::isolated()).unwrap(); |
| 30 | (repo, dir) | 30 | (repo, dir) |
| 31 | } | 31 | } |
| 32 | use crate::bstr::BString; | ||
| 32 | use git_ref::{transaction::Change, TargetRef}; | 33 | use git_ref::{transaction::Change, TargetRef}; |
| 33 | 34 | ||
| 34 | use crate::remote::fetch; | 35 | use crate::remote::fetch; |
| 36 | use crate::remote::fetch::RefLogMessage; | ||
| 35 | 37 | ||
| 36 | #[test] | 38 | #[test] |
| 37 | fn various_valid_updates() { | 39 | fn various_valid_updates() { |
| @@ -117,7 +119,7 @@ mod update { · 79 unmodified lines | |||
| 117 | let (mapping, specs) = mapping_from_spec(spec, &repo); | 119 | let (mapping, specs) = mapping_from_spec(spec, &repo); |
| 118 | let out = fetch::refs::update( | 120 | let out = fetch::refs::update( |
| 119 | &repo, | 121 | &repo, |
| 120 | "action", | 122 | prefixed("action"), |
| 121 | &mapping, | 123 | &mapping, |
| 122 | &specs, | 124 | &specs, |
| 123 | reflog_message.map(|_| fetch::DryRun::Yes).unwrap_or(fetch::DryRun::No), | 125 | reflog_message.map(|_| fetch::DryRun::Yes).unwrap_or(fetch::DryRun::No), |
| @@ -176,7 +178,7 @@ mod update { · 52 unmodified lines | |||
| 176 | ] { | 178 | ] { |
| 177 | let spec = format!("refs/heads/main:refs/heads/{}", branch); | 179 | let spec = format!("refs/heads/main:refs/heads/{}", branch); |
| 178 | let (mappings, specs) = mapping_from_spec(&spec, &repo); | 180 | let (mappings, specs) = mapping_from_spec(&spec, &repo); |
| 179 | let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::Yes)?; | 181 | let out = fetch::refs::update(&repo, prefixed("action"), &mappings, &specs, fetch::DryRun::Yes)?; |
| 180 | 182 | ||
| 181 | assert_eq!( | 183 | assert_eq!( |
| 182 | out.updates, | 184 | out.updates, |
| @@ -197,7 +199,7 @@ mod update { · 14 unmodified lines | |||
| 197 | fn symbolic_refs_are_never_written() { | 199 | fn symbolic_refs_are_never_written() { |
| 198 | let repo = repo("two-origins"); | 200 | let repo = repo("two-origins"); |
| 199 | let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/heads/symbolic", &repo); | 201 | let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/heads/symbolic", &repo); |
| 200 | let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::Yes).unwrap(); | 202 | let out = fetch::refs::update(&repo, prefixed("action"), &mappings, &specs, fetch::DryRun::Yes).unwrap(); |
| 201 | 203 | ||
| 202 | assert_eq!( | 204 | assert_eq!( |
| 203 | out.updates, | 205 | out.updates, |
| @@ -214,7 +216,17 @@ mod update { · 10 unmodified lines | |||
| 214 | fn non_fast_forward_is_rejected_but_appears_to_be_fast_forward_in_dryrun_mode() { | 216 | fn non_fast_forward_is_rejected_but_appears_to_be_fast_forward_in_dryrun_mode() { |
| 215 | let repo = repo("two-origins"); | 217 | let repo = repo("two-origins"); |
| 216 | let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/remotes/origin/g", &repo); | 218 | let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/remotes/origin/g", &repo); |
| 217 | let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::Yes).unwrap(); | 219 | let reflog_message: BString = "very special".into(); |
| 220 | let out = fetch::refs::update( | ||
| 221 | &repo, | ||
| 222 | RefLogMessage::Override { | ||
| 223 | message: reflog_message.clone(), | ||
| 224 | }, | ||
| 225 | &mappings, | ||
| 226 | &specs, | ||
| 227 | fetch::DryRun::Yes, | ||
| 228 | ) | ||
| 229 | .unwrap(); | ||
| 218 | 230 | ||
| 219 | assert_eq!( | 231 | assert_eq!( |
| 220 | out.updates, | 232 | out.updates, |
| @@ -225,13 +237,20 @@ mod update { · 4 unmodified lines | |||
| 225 | "The caller has to be aware and note that dry-runs can't know about fast-forwards as they don't have remote objects" | 237 | "The caller has to be aware and note that dry-runs can't know about fast-forwards as they don't have remote objects" |
| 226 | ); | 238 | ); |
| 227 | assert_eq!(out.edits.len(), 1); | 239 | assert_eq!(out.edits.len(), 1); |
| 240 | let edit = &out.edits[0]; | ||
| 241 | match &edit.change { | ||
| 242 | Change::Update { log, .. } => { | ||
| 243 | assert_eq!(log.message, reflog_message); | ||
| 244 | } | ||
| 245 | _ => unreachable!("only updates"), | ||
| 246 | } | ||
| 228 | } | 247 | } |
| 229 | 248 | ||
| 230 | #[test] | 249 | #[test] |
| 231 | fn non_fast_forward_is_rejected_if_dry_run_is_disabled() { | 250 | fn non_fast_forward_is_rejected_if_dry_run_is_disabled() { |
| 232 | let (repo, _tmp) = repo_rw("two-origins"); | 251 | let (repo, _tmp) = repo_rw("two-origins"); |
| 233 | let (mappings, specs) = mapping_from_spec("refs/remotes/origin/g:refs/heads/not-currently-checked-out", &repo); | 252 | let (mappings, specs) = mapping_from_spec("refs/remotes/origin/g:refs/heads/not-currently-checked-out", &repo); |
| 234 | let out = fetch::refs::update(&repo, "action", &mappings, &specs, fetch::DryRun::No).unwrap(); | 253 | let out = fetch::refs::update(&repo, prefixed("action"), &mappings, &specs, fetch::DryRun::No).unwrap(); |
| 235 | 254 | ||
| 236 | assert_eq!( | 255 | assert_eq!( |
| 237 | out.updates, | 256 | out.updates, |
| @@ -243,7 +262,7 @@ mod update { · 5 unmodified lines | |||
| 243 | assert_eq!(out.edits.len(), 0); | 262 | assert_eq!(out.edits.len(), 0); |
| 244 | 263 | ||
| 245 | let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/remotes/origin/g", &repo); | 264 | let (mappings, specs) = mapping_from_spec("refs/heads/main:refs/remotes/origin/g", &repo); |
| 246 | let out = fetch::refs::update(&repo, "prefix", &mappings, &specs, fetch::DryRun::No).unwrap(); | 265 | let out = fetch::refs::update(&repo, prefixed("prefix"), &mappings, &specs, fetch::DryRun::No).unwrap(); |
| 247 | 266 | ||
| 248 | assert_eq!( | 267 | assert_eq!( |
| 249 | out.updates, | 268 | out.updates, |
| @@ -266,7 +285,7 @@ mod update { · 16 unmodified lines | |||
| 266 | fn fast_forwards_are_called_out_even_if_force_is_given() { | 285 | fn fast_forwards_are_called_out_even_if_force_is_given() { |
| 267 | let (repo, _tmp) = repo_rw("two-origins"); | 286 | let (repo, _tmp) = repo_rw("two-origins"); |
| 268 | let (mappings, specs) = mapping_from_spec("+refs/heads/main:refs/remotes/origin/g", &repo); | 287 | let (mappings, specs) = mapping_from_spec("+refs/heads/main:refs/remotes/origin/g", &repo); |
| 269 | let out = fetch::refs::update(&repo, "prefix", &mappings, &specs, fetch::DryRun::No).unwrap(); | 288 | let out = fetch::refs::update(&repo, prefixed("prefix"), &mappings, &specs, fetch::DryRun::No).unwrap(); |
| 270 | 289 | ||
| 271 | assert_eq!( | 290 | assert_eq!( |
| 272 | out.updates, | 291 | out.updates, |
| @@ -336,4 +355,8 @@ mod update { · 63 unmodified lines | |||
| 336 | object, | 355 | object, |
| 337 | } | 356 | } |
| 338 | } | 357 | } |
| 358 | |||
| 359 | fn prefixed(action: &str) -> RefLogMessage { | ||
| 360 | RefLogMessage::Prefixed { action: action.into() } | ||
| 361 | } | ||
| 339 | } | 362 | } |
| @@ -60,4 +60,4 @@ pub struct Mapping { · 59 unmodified lines | |||
| 60 | } | 60 | } |
| 61 | 61 | ||
| 62 | #[cfg(feature = "blocking-network-client")] | 62 | #[cfg(feature = "blocking-network-client")] |
| 63 | pub use super::connection::fetch::{negotiate, prepare, refs, Error, Outcome, Prepare, Status}; | 63 | pub use super::connection::fetch::{negotiate, prepare, refs, Error, Outcome, Prepare, RefLogMessage, Status}; |
| @@ -70,6 +70,11 @@ mod blocking_io { · 69 unmodified lines | |||
| 70 | repo.objects.contains(edit.change.new_value().expect("always set").id()), | 70 | repo.objects.contains(edit.change.new_value().expect("always set").id()), |
| 71 | "part of the fetched pack" | 71 | "part of the fetched pack" |
| 72 | ); | 72 | ); |
| 73 | let r = repo.find_reference(edit.name.as_ref()).expect("created"); | ||
| 74 | if r.name().category().expect("known") != git_ref::Category::Tag { | ||
| 75 | let mut logs = r.log_iter(); | ||
| 76 | assert_reflog(logs.all()); | ||
| 77 | } | ||
| 73 | } | 78 | } |
| 74 | } | 79 | } |
| 75 | _ => unreachable!("clones are always causing changes and dry-runs aren't possible"), | 80 | _ => unreachable!("clones are always causing changes and dry-runs aren't possible"), |
| @@ -107,7 +112,11 @@ mod blocking_io { · 31 unmodified lines | |||
| 107 | .unwrap(); | 112 | .unwrap(); |
| 108 | assert_eq!(logs.len(), 1, "just created"); | 113 | assert_eq!(logs.len(), 1, "just created"); |
| 109 | let line = &logs[0]; | 114 | let line = &logs[0]; |
| 110 | assert!(line.message.starts_with(b"clone: from "), "{:?} unexpected", line); | 115 | assert!( |
| 116 | line.message.starts_with(b"clone: from "), | ||
| 117 | "{:?} unexpected", | ||
| 118 | line.message | ||
| 119 | ); | ||
| 111 | let path = git_path::from_bstr(line.message.rsplit(|b| *b == b' ').next().expect("path").as_bstr()); | 120 | let path = git_path::from_bstr(line.message.rsplit(|b| *b == b' ').next().expect("path").as_bstr()); |
| 112 | assert!(path.is_absolute(), "{:?} must be absolute", path); | 121 | assert!(path.is_absolute(), "{:?} must be absolute", path); |
| 113 | } | 122 | } |