History

proper reflog entries for all other updated refs during cloning (#450)

Sebastian Thiel committed 4 years ago

Commit ff4412e1 parent c25cb007 files · +119 −48Browse files

Unified · Split

7 files
  1. git-repository/src/clone/fetch.rs +9 −4
  2. git-repository/src/remote/connection/fetch/error.rs +31 −0
  3. git-repository/src/remote/connection/fetch/mod.rs +35 −33
  4. git-repository/src/remote/connection/fetch/update_refs/mod.rs +3 −2
  5. git-repository/src/remote/connection/fetch/update_refs/tests.rs +30 −7
  6. git-repository/src/remote/fetch.rs +1 −1
  7. git-repository/tests/clone/mod.rs +10 −1
git-repository/src/clone/fetch.rs+9−4
@@ -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 b215 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))
git-repository/src/remote/connection/fetch/error.rs+31−0
@@ -0,0 +1,31 @@
1/// The error returned by [`receive()`](super::Prepare::receive()).
2#[derive(Debug, thiserror::Error)]
3#[allow(missing_docs)]
4pub 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}
git-repository/src/remote/connection/fetch/mod.rs+35−33
@@ -1,5 +1,6 @@
1use std::sync::atomic::AtomicBool;1use std::sync::atomic::AtomicBool;
2 2
3use crate::bstr::BString;
3use git_odb::FindExt;4use git_odb::FindExt;
4use git_protocol::transport::client::Transport;5use git_protocol::transport::client::Transport;
5 6
@@ -13,40 +14,31 @@ use crate::{ · 7 unmodified lines
13 Progress,14 Progress,
14};15};
15 16
16mod error {17mod error;
17 /// The error returned by [`receive()`](super::Prepare::receive()).18pub 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 {21pub 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 },34impl 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}
49pub use error::Error;
50 42
51/// The status of the repository after the fetch operation43/// 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/// Builder324/// 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 self334 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
342impl<'remote, 'repo, T, P> Drop for Prepare<'remote, 'repo, T, P>344impl<'remote, 'repo, T, P> Drop for Prepare<'remote, 'repo, T, P>
git-repository/src/remote/connection/fetch/update_refs/mod.rs+3−2
@@ -7,6 +7,7 @@ use git_ref::{ · 6 unmodified lines
7 Target, TargetRef,7 Target, TargetRef,
8};8};
9 9
10use crate::remote::fetch::RefLogMessage;
10use crate::{11use 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`.
42pub(crate) fn update(43pub(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()),
git-repository/src/remote/connection/fetch/update_refs/tests.rs+30−7
@@ -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}
git-repository/src/remote/fetch.rs+1−1
@@ -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")]
63pub use super::connection::fetch::{negotiate, prepare, refs, Error, Outcome, Prepare, Status};63pub use super::connection::fetch::{negotiate, prepare, refs, Error, Outcome, Prepare, RefLogMessage, Status};
git-repository/tests/clone/mod.rs+10−1
@@ -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 }