Skip to content

Commit b4e4332

Browse files
committed
Allow git push --dry-run to store metadata
1 parent b62e036 commit b4e4332

File tree

3 files changed

+153
-99
lines changed

3 files changed

+153
-99
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,11 @@ preference with one of the following values:
196196
- `always`
197197
- `never`
198198
- `phase`
199+
- `force`
199200

200201
`phase` is the default described above. `always` and `never` are
201-
self-explanatory.
202+
self-explanatory. `force` has the same meaning as `always`, but also
203+
forces `git push --dry-run` to store metadata.
202204

203205
Cinnabar clone:
204206
---------------

src/main.rs

Lines changed: 124 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -4478,6 +4478,39 @@ fn check_graft_refs() {
44784478
}
44794479
}
44804480

4481+
#[derive(PartialEq, Eq)]
4482+
enum RecordMetadata {
4483+
Never,
4484+
Phase,
4485+
Always,
4486+
// Like Always, but also applies to dry-run
4487+
Force,
4488+
}
4489+
4490+
impl TryFrom<&OsStr> for RecordMetadata {
4491+
type Error = String;
4492+
4493+
fn try_from(value: &OsStr) -> Result<RecordMetadata, String> {
4494+
value
4495+
.to_str()
4496+
.and_then(|s| {
4497+
Some(match s {
4498+
"never" => RecordMetadata::Never,
4499+
"phase" => RecordMetadata::Phase,
4500+
"always" => RecordMetadata::Always,
4501+
"force" => RecordMetadata::Force,
4502+
_ => return None,
4503+
})
4504+
})
4505+
.ok_or_else(|| {
4506+
format!(
4507+
"`{}` is not one of `never`, `phase`, `always` or `force`",
4508+
value.as_bytes().as_bstr()
4509+
)
4510+
})
4511+
}
4512+
}
4513+
44814514
fn remote_helper_push(
44824515
store: &mut Store,
44834516
conn: &mut dyn HgRepo,
@@ -4530,6 +4563,11 @@ fn remote_helper_push(
45304563
}
45314564
});
45324565

4566+
let data = get_config_remote("data", remote)
4567+
.filter(|d| !d.is_empty())
4568+
.as_deref()
4569+
.map_or(Ok(RecordMetadata::Phase), RecordMetadata::try_from)?;
4570+
45334571
let mut pushed = ChangesetHeads::new();
45344572
let result = (|| {
45354573
let push_commits = push_refs.iter().filter_map(|(c, _, _)| *c).collect_vec();
@@ -4692,7 +4730,7 @@ fn remote_helper_push(
46924730
}
46934731

46944732
let mut result = None;
4695-
if !push_commits.is_empty() && !dry_run {
4733+
if !push_commits.is_empty() && (!dry_run || data == RecordMetadata::Force) {
46964734
conn.require_capability(b"unbundle");
46974735

46984736
let b2caps = conn
@@ -4718,6 +4756,7 @@ fn remote_helper_push(
47184756
.unwrap_or(1);
47194757
(BundleSpec::V2None, version)
47204758
};
4759+
// TODO: Ideally, for dry-run, we wouldn't even create a temporary file.
47214760
let tempfile = tempfile::Builder::new()
47224761
.prefix("hg-bundle-")
47234762
.suffix(".hg")
@@ -4734,40 +4773,43 @@ fn remote_helper_push(
47344773
version == 2,
47354774
)?;
47364775
drop(file);
4737-
let file = File::open(path).unwrap();
4738-
let empty_heads = [HgChangesetId::NULL];
4739-
let heads = if force {
4740-
None
4741-
} else if no_topological_heads {
4742-
Some(&empty_heads[..])
4743-
} else {
4744-
Some(&info.topological_heads[..])
4745-
};
4746-
let response = conn.unbundle(heads, file);
4747-
match response {
4748-
UnbundleResponse::Bundlev2(data) => {
4749-
let mut bundle = BundleReader::new(data).unwrap();
4750-
while let Some(part) = bundle.next_part().unwrap() {
4751-
match part.part_type.as_bytes() {
4752-
b"reply:changegroup" => {
4753-
// TODO: should check in-reply-to param.
4754-
let response = part.get_param("return").unwrap();
4755-
result = u32::from_str(response).ok();
4756-
}
4757-
b"error:abort" => {
4758-
let mut message = part.get_param("message").unwrap().to_string();
4759-
if let Some(hint) = part.get_param("hint") {
4760-
message.push_str("\n\n");
4761-
message.push_str(hint);
4776+
if !dry_run {
4777+
let file = File::open(path).unwrap();
4778+
let empty_heads = [HgChangesetId::NULL];
4779+
let heads = if force {
4780+
None
4781+
} else if no_topological_heads {
4782+
Some(&empty_heads[..])
4783+
} else {
4784+
Some(&info.topological_heads[..])
4785+
};
4786+
let response = conn.unbundle(heads, file);
4787+
match response {
4788+
UnbundleResponse::Bundlev2(data) => {
4789+
let mut bundle = BundleReader::new(data).unwrap();
4790+
while let Some(part) = bundle.next_part().unwrap() {
4791+
match part.part_type.as_bytes() {
4792+
b"reply:changegroup" => {
4793+
// TODO: should check in-reply-to param.
4794+
let response = part.get_param("return").unwrap();
4795+
result = u32::from_str(response).ok();
4796+
}
4797+
b"error:abort" => {
4798+
let mut message =
4799+
part.get_param("message").unwrap().to_string();
4800+
if let Some(hint) = part.get_param("hint") {
4801+
message.push_str("\n\n");
4802+
message.push_str(hint);
4803+
}
4804+
error!(target: "root", "{}", message);
47624805
}
4763-
error!(target: "root", "{}", message);
4806+
_ => {}
47644807
}
4765-
_ => {}
47664808
}
47674809
}
4768-
}
4769-
UnbundleResponse::Raw(response) => {
4770-
result = u32::from_bytes(&response).ok();
4810+
UnbundleResponse::Raw(response) => {
4811+
result = u32::from_bytes(&response).ok();
4812+
}
47714813
}
47724814
}
47734815
}
@@ -4851,76 +4893,60 @@ fn remote_helper_push(
48514893
stdout.flush().unwrap();
48524894
}
48534895

4854-
let data = get_config_remote("data", remote);
4855-
let data = data
4856-
.as_deref()
4857-
.and_then(|d| (!d.is_empty()).then_some(d))
4858-
.unwrap_or_else(|| OsStr::new("phase"));
4859-
let valid = [
4860-
OsStr::new("never"),
4861-
OsStr::new("phase"),
4862-
OsStr::new("always"),
4863-
];
4864-
if !valid.contains(&data) {
4865-
die!(
4866-
"`{}` is not one of `never`, `phase` or `always`",
4867-
data.as_bytes().as_bstr()
4868-
);
4869-
}
4870-
let rollback = if status.is_empty() || pushed.is_empty() || dry_run {
4871-
true
4872-
} else {
4873-
match data.to_str().unwrap() {
4874-
"always" => false,
4875-
"never" => true,
4876-
"phase" => {
4877-
let phases = conn.phases();
4878-
let phases = ByteSlice::lines(&*phases)
4879-
.filter_map(|l| {
4880-
l.splitn_exact(b'\t')
4881-
.map(|[k, v]| (k.as_bstr(), v.as_bstr()))
4882-
})
4883-
.collect::<HashMap<_, _>>();
4884-
let drafts = (!phases.contains_key(b"publishing".as_bstr()))
4885-
.then(|| {
4886-
phases
4887-
.into_iter()
4888-
.filter_map(|(phase, is_draft)| {
4889-
u32::from_bytes(is_draft).ok().and_then(|is_draft| {
4890-
(is_draft > 0).then(|| HgChangesetId::from_bytes(phase))
4896+
let rollback =
4897+
if status.is_empty() || pushed.is_empty() || (dry_run && data != RecordMetadata::Force) {
4898+
true
4899+
} else {
4900+
match data {
4901+
RecordMetadata::Force | RecordMetadata::Always => false,
4902+
RecordMetadata::Never => true,
4903+
RecordMetadata::Phase => {
4904+
let phases = conn.phases();
4905+
let phases = ByteSlice::lines(&*phases)
4906+
.filter_map(|l| {
4907+
l.splitn_exact(b'\t')
4908+
.map(|[k, v]| (k.as_bstr(), v.as_bstr()))
4909+
})
4910+
.collect::<HashMap<_, _>>();
4911+
let drafts = (!phases.contains_key(b"publishing".as_bstr()))
4912+
.then(|| {
4913+
phases
4914+
.into_iter()
4915+
.filter_map(|(phase, is_draft)| {
4916+
u32::from_bytes(is_draft).ok().and_then(|is_draft| {
4917+
(is_draft > 0).then(|| HgChangesetId::from_bytes(phase))
4918+
})
48914919
})
4892-
})
4893-
.collect::<Result<Vec<_>, _>>()
4894-
})
4895-
.transpose()
4896-
.unwrap()
4897-
.unwrap_or_default();
4898-
if drafts.is_empty() {
4899-
false
4900-
} else {
4901-
// Theoretically, we could have commits with no
4902-
// metadata that the remote declares are public, while
4903-
// the rest of our push is in a draft state. That is
4904-
// however so unlikely that it's not worth the effort
4905-
// to support partial metadata storage.
4906-
!reachable_subset(
4907-
pushed
4908-
.heads()
4909-
.copied()
4910-
.filter_map(|h| h.to_git(store))
4911-
.map(Into::into),
4912-
drafts
4913-
.iter()
4914-
.copied()
4915-
.filter_map(|h| h.to_git(store))
4916-
.map(Into::into),
4917-
)
4918-
.is_empty()
4920+
.collect::<Result<Vec<_>, _>>()
4921+
})
4922+
.transpose()
4923+
.unwrap()
4924+
.unwrap_or_default();
4925+
if drafts.is_empty() {
4926+
false
4927+
} else {
4928+
// Theoretically, we could have commits with no
4929+
// metadata that the remote declares are public, while
4930+
// the rest of our push is in a draft state. That is
4931+
// however so unlikely that it's not worth the effort
4932+
// to support partial metadata storage.
4933+
!reachable_subset(
4934+
pushed
4935+
.heads()
4936+
.copied()
4937+
.filter_map(|h| h.to_git(store))
4938+
.map(Into::into),
4939+
drafts
4940+
.iter()
4941+
.copied()
4942+
.filter_map(|h| h.to_git(store))
4943+
.map(Into::into),
4944+
)
4945+
.is_empty()
4946+
}
49194947
}
49204948
}
4921-
_ => unreachable!(),
4922-
}
4923-
};
4949+
};
49244950
if rollback {
49254951
unsafe {
49264952
do_cleanup(1);

tests/push.t

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,32 @@ Server is now non-publishing, so metadata is unchanged.
364364
WARNING Pushing a new root
365365
To hg::.*/push.t/xyz (re)
366366
+ 687e015...7ca6a3c 7ca6a3c32ec0dbcbcd155b2be6e2f4505012c273 -> branches/default/tip (forced update)
367+
368+
Whatever happens, --dry-run doesn't store metadata
369+
370+
$ git -C xyz-git cinnabar rollback --candidates
371+
a2341d430e5acddf9481eabcad901fda12d023d3 (current)
372+
8b8194eefb69ec89edc35dafb965311fe48c49d0
373+
2836e453f32b1ecccd3acca412f75b07c88176bf
374+
375+
There is an extra mode for cinnabar.data that forces --dry-run to store metadata
376+
377+
$ git -c cinnabar.data=force -C xyz-git push -f --dry-run origin 7ca6a3c32ec0dbcbcd155b2be6e2f4505012c273:refs/heads/branches/default/tip
378+
\r (no-eol) (esc)
379+
WARNING Pushing a new root
380+
To hg::.*/push.t/xyz (re)
381+
+ 687e015...7ca6a3c 7ca6a3c32ec0dbcbcd155b2be6e2f4505012c273 -> branches/default/tip (forced update)
382+
$ git -C xyz-git cinnabar rollback --candidates
383+
4305cef3fa610b3370f64ce10d2b50693a904278 (current)
384+
a2341d430e5acddf9481eabcad901fda12d023d3
385+
8b8194eefb69ec89edc35dafb965311fe48c49d0
386+
2836e453f32b1ecccd3acca412f75b07c88176bf
387+
$ git -C xyz-git cinnabar rollback
388+
$ git -C xyz-git cinnabar rollback --candidates
389+
a2341d430e5acddf9481eabcad901fda12d023d3 (current)
390+
8b8194eefb69ec89edc35dafb965311fe48c49d0
391+
2836e453f32b1ecccd3acca412f75b07c88176bf
392+
367393
$ git -c cinnabar.data=always -C xyz-git push -f origin 7ca6a3c32ec0dbcbcd155b2be6e2f4505012c273:refs/heads/branches/default/tip
368394
\r (no-eol) (esc)
369395
WARNING Pushing a new root

0 commit comments

Comments
 (0)