Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

A push leaves out only the refs its report refused, and a pull request's head copied from someone's fork says why it is not counted

A push now drops a ref from what it records only when the report says `ng` for it, or every ref when the unpack failed. A ref the report does not mention (a 200 with just `unpack ok`, or a report cut short) is left to record_push's checks against the store, so a ref that moved still starts its event, webhooks, workflows, rules and About run. The test merge's head copy comes from the pull request's source: its working copy, counted to this repository, or a branch of someone's own fork, counted to that fork. Neither is billed to the repository the pull request is in, whose owner did not push them; the comment, the architecture page, the billing guide and Drift's list now say so. The test fixtures spell their control bytes and newlines as escapes. Part of #319

G1t-Session:
ses_01m5fv26pcecxtmdsf3k7r74vg
syntaqxcommitted Parent55bff9aBrowse files
7 files+79−600/7 viewed
+11−8
118118 moved meanwhile, or a failed unpack; for a push over git, every ref it
119119 reports on refused). Only bytes the store took reach `stored_bytes`. A
120120 push and a write of one ref read the store's report the same way, one
121−parser for both (`land.rs` `read_answer`, report-status v1 or v2), and a
122−push records only the refs its report says moved: a ref the store refused
123−starts no event, webhook or workflow, and a push refused whole records
124−nothing.
121+parser for both (`land.rs` `read_answer`, report-status v1 or v2). A push
122+records no ref its report refused: that ref starts no event, webhook or
123+workflow, and a push whose unpack failed, or whose every ref was refused,
124+records nothing. A ref the report does not mention is checked against
125+the store, as every recorded ref is.
125126
126127 g1t's own copies are not reserved or counted: a pull request's test merge
127128 (`refs/pull/N/merge`, a merge commit g1t makes), its head copied in from
128−the working copy, the head kept when a working copy is removed and the
129−branch a removed working copy is made again with. What they hold was
130−counted when it was pushed, to the repository that is the working copy's
131−root, or was written by g1t.
129+where the change is, the head kept when a working copy is removed and the
130+branch a removed working copy is made again with. Each holds what g1t
131+wrote, or what was counted when it was pushed: to the repository a working
132+copy belongs to, or, for a pull request opened from a branch of someone's
133+own fork, to that fork. The repository the pull request is in is never
134+charged for objects its owner did not push.
132135
133136 A write that stops without knowing whether the store kept it (the store
134137 answered with an error, stopped answering, answered a push with a fatal
+4−2
491491 pull requests' working copies, so what is charged is never more than what
492492 is stored. A push, merge or update that storage refused, for example
493493 because its branch moved meanwhile, is not counted. Neither are the copies
494−g1t makes for its own use, such as a pull request's test merge: what they
495−hold was counted when it was pushed, or g1t wrote it. Public repositories
494+g1t makes for its own use, such as a pull request's test merge, or its head
495+copied in from someone's fork: g1t wrote them, or they were counted where
496+they were pushed (a fork's to the fork), never to a repository whose owner
497+did not push them. Public repositories
496498 are never charged, and neither is a free workspace: its pushes to private
497499 repositories stop at 1 GB instead. On the plan they never stop.
498500
+3−2
116116 plus what each pull request's working copy holds beyond it, beside
117117 `stored_bytes`. It reads only. Above zero, g1t counts less than the store
118118 holds: pushes made with a credential straight to the store (sandboxes'
119−`git_access`), ended pushes that landed, and g1t's own refs (a pull
120−request's test merge, `refs/pull/N/merge`) are not counted. Below zero,
119+`git_access`), ended pushes that landed, g1t's own refs (a pull
120+request's test merge, `refs/pull/N/merge`), and a pull request's head
121+copied in from someone's fork are not counted. Below zero,
121122 g1t counted objects that a later push made unreachable (a force push), or
122123 packs that sent objects the store already had: g1t asks for whole packs.
123124 Before #307, g1t also counted writes the store refused (a merge or update
+9−7
279279 }
280280
281281 impl Answer {
282− /// The refs the answer says moved: none when its unpack failed, and
283− /// `None` when it says nothing either way (a fatal error, or no
284− /// report), which leaves it to the store's refs.
285− pub(crate) fn moved_refs(&self) -> Option<&[String]> {
282+ /// Whether a push records `git_ref` from this answer: not when the
283+ /// report refused it (`ng`), nor any ref when its unpack failed. A ref
284+ /// the report does not mention, or an answer that says nothing either
285+ /// way (a fatal error, no report), is left to the store's refs, which
286+ /// record_push checks (pushed.rs `pushed_to`, lib.rs `publish_deleted`):
287+ /// a report cut short never hides a ref that moved.
288+ pub(crate) fn records(&self, git_ref: &str) -> bool {
286289 match self {
287− Answer::Report(report) if report.unpack_failed => Some(&[]),
288− Answer::Report(report) => Some(&report.moved),
289− Answer::Fatal(_) | Answer::Unread => None,
290+ Answer::Report(report) => !report.unpack_failed && !report.refused.iter().any(|refused| refused == git_ref),
291+ Answer::Fatal(_) | Answer::Unread => true,
290292 }
291293 }
292294 }
+9−9
24542454 None
24552455 };
24562456 // The store's answer, read once: its reservation settles by it, and
2457− // only the refs it says moved are recorded.
2457+ // the refs it refused are not recorded.
24582458 let reply = answered.as_ref().map(|(_, report)| land::read_answer(report));
24592459 if let Some(reserved) = &public_reserved {
24602460 match public_storage::push_settlement(status, forwarded.rate_limited, reply.as_ref()) {
25302530 }
25312531 }
25322532 after.ended(status, None);
2533− // Only what the report says moved is recorded: a ref the store
2534− // refused starts no event, webhook, workflow or rule, and a push
2535− // refused whole records nothing. An answer that says neither (a
2536− // fatal error, no report) leaves each ref to record_push, which
2537− // checks where it is in the store.
2533+ // A ref the report refused is not recorded: it starts no event,
2534+ // webhook, workflow or rule, and a push whose unpack failed records
2535+ // nothing. A ref the report does not mention, or an answer that
2536+ // says neither (a fatal error, no report), is left to record_push,
2537+ // which checks where it is in the store.
25382538 let (mut pushed, mut deleted) = (forwarded.pushed, forwarded.deleted);
2539− if let Some(moved) = reply.as_ref().and_then(land::Answer::moved_refs) {
2540− pushed.retain(|pushed| moved.contains(&pushed.git_ref));
2541− deleted.retain(|(git_ref, _)| moved.contains(git_ref));
2539+ if let Some(reply) = &reply {
2540+ pushed.retain(|pushed| reply.records(&pushed.git_ref));
2541+ deleted.retain(|(git_ref, _)| reply.records(git_ref));
25422542 }
25432543 // A reserved push is counted by its reservation, settled above.
25442544 let pack_bytes = if public_reserved.is_some() { 0 } else { forwarded.pack_bytes };
+34−28
900900
901901 #[test]
902902 fn a_push_the_store_answered_with_a_fatal_error_stays_ended_for_staff() {
903− let fatal = [crate::git_http::pkt_line(b"fatal: the remote end hung up unexpectedly
904−"), b"0000".to_vec()].concat();
905− let after_progress = [crate::git_http::pkt_line(b"Receiving objects: 40%
906−"), fatal.clone()].concat();
907− let err = [crate::git_http::pkt_line(b"ERR pack exceeds the store's limit
908−"), b"0000".to_vec()].concat();
903+ let fatal = [crate::git_http::pkt_line(b"\x03fatal: the remote end hung up unexpectedly\n"), b"0000".to_vec()].concat();
904+ let after_progress = [crate::git_http::pkt_line(b"\x02Receiving objects: 40%\n"), fatal.clone()].concat();
905+ let err = [crate::git_http::pkt_line(b"ERR pack exceeds the store's limit\n"), b"0000".to_vec()].concat();
909906 for answer in [&fatal, &after_progress, &err] {
910907 let read = crate::land::read_answer(answer);
911908 assert!(matches!(read, Answer::Fatal(_)), "{read:?}");
913910 // answered so is (Unclear).
914911 assert_eq!(push_settlement(200, false, Some(&read)), Settlement::End);
915912 assert_eq!(settled(answer, "refs/heads/main"), Settlement::End);
916− // Nothing is announced from it: record_push checks the store.
917− assert_eq!(read.moved_refs(), None);
913+ // Every ref is left to record_push, which checks the store.
914+ assert!(read.records("refs/heads/main"));
918915 }
919916 let outcome = push_outcome(200, Some(&crate::land::read_answer(&fatal)));
920917 assert_eq!(outcome, "The store answered the push with 200 and a fatal error: fatal: the remote end hung up unexpectedly");
943940 let read = crate::land::read_answer(answer);
944941 let verdict = crate::land::verdict(answer, "refs/heads/main", true);
945942 let Answer::Report(report) = &read else { panic!("{read:?}") };
946− // The ref a write of one ref is counted for is the one a push
947− // records, and a push refused whole is a refusal of that ref.
948− let recorded = read.moved_refs().unwrap().iter().any(|moved| moved == "refs/heads/main");
949− assert_eq!(verdict == Verdict::Moved, recorded, "{report:?}");
943+ // A write of one ref is counted when the report says it moved,
944+ // and a ref refused there is one a push does not record.
945+ assert_eq!(verdict == Verdict::Moved, report.moved("refs/heads/main"), "{report:?}");
946+ assert_eq!(matches!(verdict, Verdict::Refused(_)), !read.records("refs/heads/main"), "{report:?}");
950947 if refused_whole(report) {
951948 assert!(matches!(verdict, Verdict::Refused(_)), "{report:?}");
952949 }
956953 assert_eq!(v2.moved, ["refs/heads/main"]);
957954 assert!(v2.refused.is_empty());
958955 // One parser: neither reads the lines itself.
959− let code = include_str!("public_storage.rs").split("#[cfg(test)]
960−mod tests").next().unwrap_or_default();
956+ let code = include_str!("public_storage.rs").split("#[cfg(test)]\nmod tests").next().unwrap_or_default();
961957 assert!(!code.contains("starts_with(\"ok \")") && !code.contains("starts_with(\"ng \")"));
962958 }
963959
964960 #[test]
965− fn a_push_records_only_the_refs_its_report_says_moved() {
966− let moved = |answer: Vec<u8>| crate::land::read_answer(&answer).moved_refs().map(<[String]>::to_vec);
967− let main = || vec!["refs/heads/main".to_owned()];
968− // Every ref refused, or the unpack failed: none.
969− assert_eq!(moved(report(&["unpack ok", "ng refs/heads/main non-fast-forward", "ng refs/tags/v1 already exists"])), Some(vec![]));
970− assert_eq!(moved(report(&["unpack error: invalid pack", "ok refs/heads/main"])), Some(vec![]));
971− // One moved beside one refused (a hidden ref): that one.
972− assert_eq!(moved(banded(&["unpack ok", "ok refs/heads/main", "ng refs/pull/1/head deny updating a hidden ref"])), Some(main()));
973− assert_eq!(moved(report(&["unpack ok", "ok refs/heads/main", "option refname refs/heads/main"])), Some(main()));
974− // No report: left to the store's refs.
975− assert_eq!(moved(Vec::new()), None);
961+ fn a_push_records_every_ref_but_those_its_report_refused() {
962+ let (main, tag, pull) = ("refs/heads/main", "refs/tags/v1", "refs/pull/1/head");
963+ let recorded = |answer: Vec<u8>| {
964+ let read = crate::land::read_answer(&answer);
965+ [main, tag, pull].into_iter().filter(|git_ref| read.records(git_ref)).collect::<Vec<_>>()
966+ };
967+ // Every ref refused, a tag among them: none.
968+ assert!(recorded(report(&["unpack ok", "ng refs/heads/main non-fast-forward", "ng refs/tags/v1 already exists", "ng refs/pull/1/head x"])).is_empty());
969+ // The unpack failed: none, whatever the refs say.
970+ assert!(recorded(report(&["unpack error: invalid pack", "ok refs/heads/main"])).is_empty());
971+ // One moved beside one refused (a hidden ref): all but the refused.
972+ assert_eq!(recorded(banded(&["unpack ok", "ok refs/heads/main", "ng refs/pull/1/head deny updating a hidden ref"])), [main, tag]);
973+ assert_eq!(recorded(report(&["unpack ok", "ok refs/heads/main", "option refname refs/heads/main", "ng refs/tags/v1 already exists"])), [main, pull]);
974+ // A report that mentions only some refs, or none (just `unpack ok`,
975+ // or cut short): the rest are left to record_push's store checks.
976+ assert_eq!(recorded(report(&["unpack ok", "ok refs/heads/main"])), [main, tag, pull]);
977+ assert_eq!(recorded(report(&["unpack ok"])), [main, tag, pull]);
978+ assert_eq!(recorded(banded(&["unpack ok", "ng refs/tags/v1 already exists"])), [main, pull]);
979+ // No report, or a fatal error: left to the store too.
980+ assert_eq!(recorded(Vec::new()), [main, tag, pull]);
981+ assert_eq!(recorded([crate::git_http::pkt_line(b"\x03fatal: hung up\n"), b"0000".to_vec()].concat()), [main, tag, pull]);
976982 // The push path filters what it records by it, pushed and deleted.
977983 let answer = body_of(include_str!("lib.rs"), "answer_git");
978− let filtered = answer.find("reply.as_ref().and_then(land::Answer::moved_refs)").expect("filtered by the report");
984+ let filtered = answer.find("if let Some(reply) = &reply {").expect("filtered by the report");
979985 let done = answer.find("after.push = Some(PushDone {").expect("recorded");
980986 assert!(filtered < done);
981− assert!(answer.contains("pushed.retain(|pushed| moved.contains(&pushed.git_ref));"));
982− assert!(answer.contains("deleted.retain(|(git_ref, _)| moved.contains(git_ref));"));
987+ assert!(answer.contains("pushed.retain(|pushed| reply.records(&pushed.git_ref));"));
988+ assert!(answer.contains("deleted.retain(|(git_ref, _)| reply.records(git_ref));"));
983989 assert!(!answer[done..].contains("forwarded.pushed") && !answer[done..].contains("forwarded.deleted"));
984990 }
985991
+9−4
354354 let head = a.head.clone();
355355
356356 // The head, kept in the repository, its objects brought from the fork.
357− // Not reserved or counted here: they are the pull request's working
358− // copy's, counted to this repository (its root, registry.rs
359− // `add_stored_bytes`) when they were pushed there, so counting the
360− // copy too would count them twice.
357+ // Not reserved or counted here. They come from the pull request's
358+ // source (contracts `Pull::source_id`): its working copy, whose
359+ // pushes were counted to this repository (its root, registry.rs
360+ // `add_stored_bytes`) when they were made, so counting the copy too
361+ // would count them twice; or a branch of someone's own fork
362+ // (`head_repo`, `fork_of` NULL), whose pushes were counted to that
363+ // fork, not to this repository. Either way this repository's owner
364+ // did not push them, so they are not billed here; Drift counts the
365+ // fork's as the store holding more than g1t counts.
361366 let held_head = find(&refs, &head_ref);
362367 if held_head.as_deref() != Some(head.as_str()) {
363368 let write = git.access(Scope::Write).await?;

Comments

Sign in to comment.