1use std::collections::HashMap;
49use std::io::Write;
50use std::path::{Path, PathBuf};
51use std::process::Command;
52use std::sync::atomic::{AtomicBool, Ordering};
53use std::sync::{Arc, Mutex};
54use std::time::{Duration, Instant};
55
56use serde_json::{json, Value};
57use weave_core::host::Host;
58
59use crate::gitscan;
60use crate::onto::{self, Options, Prep};
61
62type R<T> = Result<T, Box<dyn std::error::Error>>;
63
64const EMPTY_TREE: &str = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
65
66pub struct QueueOptions {
68 pub onto: Options,
69 pub lease_ttl: Duration,
71 pub wait_timeout: Duration,
73 pub poll: Duration,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct TicketResult {
80 pub ticket: u64,
81 pub landed: Option<String>,
82 pub reason: String,
83 pub files: Vec<String>,
84 pub regions: Vec<String>,
85 pub report: String,
86 pub tip: String,
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum QueueOutcome {
92 Landed(String),
93 Refused(String),
94}
95
96struct Ctx {
98 dir: PathBuf,
99 remote: String,
100 branch: String,
101 me: String,
102}
103
104fn cmd_git(dir: &Path, args: &[&str]) -> Command {
105 let mut c = Command::new("git");
106 c.args(args)
107 .current_dir(dir)
108 .env("GIT_AUTHOR_NAME", "weave-queue")
109 .env("GIT_AUTHOR_EMAIL", "weave-queue@localhost")
110 .env("GIT_COMMITTER_NAME", "weave-queue")
111 .env("GIT_COMMITTER_EMAIL", "weave-queue@localhost");
112 c
113}
114
115fn run(dir: &Path, args: &[&str], limit: Duration) -> R<(bool, String, String)> {
117 let (status, out, err) =
118 gitscan::run_bounded(cmd_git(dir, args), &format!("git {}", args[0]), None, limit)?;
119 Ok((
120 status.success(),
121 String::from_utf8_lossy(&out).to_string(),
122 String::from_utf8_lossy(&err).to_string(),
123 ))
124}
125
126fn git(dir: &Path, args: &[&str]) -> R<String> {
127 match run(dir, args, Duration::from_secs(300))? {
128 (true, out, _) => Ok(out.trim().to_string()),
129 (false, _, err) => Err(format!("git {} failed: {}", args.join(" "), err.trim()).into()),
130 }
131}
132
133fn lost_race(err: &str) -> bool {
136 if ["hook declined", "denied", "deny", "not allowed"]
139 .iter()
140 .any(|w| err.contains(w))
141 {
142 return false;
143 }
144 [
145 "incorrect old value",
146 "stale info",
147 "rejected",
148 "fetch first",
149 "non-fast-forward",
150 "atomic push failed",
151 "failed to update ref",
152 "cannot lock ref",
153 "already exists",
154 ]
155 .iter()
156 .any(|w| err.contains(w))
157}
158
159fn jitter(base: Duration) -> Duration {
160 let n = std::time::SystemTime::now()
161 .duration_since(std::time::UNIX_EPOCH)
162 .map(|d| d.subsec_nanos())
163 .unwrap_or(0) as u64;
164 let pid = std::process::id() as u64;
165 let ms = base.as_millis() as u64;
166 Duration::from_millis(ms + (n ^ pid.wrapping_mul(2654435761)) % (ms.max(1)))
167}
168
169impl Ctx {
170 fn r(&self, name: &str) -> String {
171 format!("refs/weave/{}/{name}", self.branch)
172 }
173 fn local(&self, name: &str) -> String {
174 format!("refs/weave-seen/{}/{}/{name}", self.remote, self.branch)
175 }
176
177 fn heads(&self) -> R<HashMap<String, String>> {
179 let main = format!("refs/heads/{}", self.branch);
180 let pat = format!("refs/weave/{}/*", self.branch);
181 let (ok, out, err) = run(
182 &self.dir,
183 &["ls-remote", &self.remote, &pat, &main],
184 Duration::from_secs(120),
185 )?;
186 if !ok {
187 return Err(format!("git ls-remote {} failed: {}", self.remote, err.trim()).into());
188 }
189 let mut m = HashMap::new();
190 for line in out.lines() {
191 if let Some((sha, name)) = line.split_once('\t') {
192 let key = if name == main {
193 "tip".to_string()
194 } else {
195 name.rsplit('/').next().unwrap_or(name).to_string()
196 };
197 m.insert(key, sha.to_string());
198 }
199 }
200 Ok(m)
201 }
202
203 fn fetch(&self) -> R<()> {
206 let specs = [
207 format!(
208 "+refs/weave/{}/*:refs/weave-seen/{}/{}/*",
209 self.branch, self.remote, self.branch
210 ),
211 format!("+refs/heads/{}:{}", self.branch, self.local("tip")),
212 ];
213 let mut args = vec![
214 "fetch",
215 "-q",
216 "--no-tags",
217 "--no-write-fetch-head",
218 &self.remote,
219 ];
220 args.extend(specs.iter().map(String::as_str));
221 let (ok, _, err) = run(&self.dir, &args, Duration::from_secs(300))?;
222 if !ok {
223 return Err(format!("git fetch {} failed: {}", self.remote, err.trim()).into());
224 }
225 Ok(())
226 }
227
228 fn message(&self, sha: &str) -> R<Value> {
229 let body = git(&self.dir, &["show", "-s", "--format=%B", sha])?;
230 Ok(serde_json::from_str(body.trim()).unwrap_or(Value::Null))
231 }
232
233 fn chain_commit(&self, parents: &[&str], msg: &Value) -> R<String> {
235 let _ = git(&self.dir, &["mktree"]); let text = msg.to_string();
237 let mut args = vec!["commit-tree", EMPTY_TREE];
238 for p in parents {
239 args.push("-p");
240 args.push(p);
241 }
242 args.push("-m");
243 args.push(&text);
244 git(&self.dir, &args)
245 }
246
247 fn genesis(&self, chain: &str) -> R<String> {
250 self.chain_commit(&[], &json!({"kind": "genesis", "chain": chain}))
251 }
252
253 fn cas(&self, updates: &[(&str, String, Option<&str>)]) -> R<bool> {
256 let mut args: Vec<String> = vec![
257 "push".into(),
258 "-q".into(),
259 "--atomic".into(),
260 self.remote.clone(),
261 ];
262 for (new, refname, old) in updates {
263 args.push(format!(
264 "--force-with-lease={refname}:{}",
265 old.unwrap_or("")
266 ));
267 args.push(format!("{new}:{refname}"));
268 }
269 let argv: Vec<&str> = args.iter().map(String::as_str).collect();
270 let (ok, _, err) = run(&self.dir, &argv, Duration::from_secs(300))?;
271 if ok {
272 return Ok(true);
273 }
274 if lost_race(&err) {
275 return Ok(false);
276 }
277 Err(format!("git push to {} failed: {}", self.remote, err.trim()).into())
278 }
279
280 fn ticket_of(&self, sha: &str) -> R<u64> {
281 self.message(sha)?
282 .get("ticket")
283 .and_then(Value::as_u64)
284 .ok_or_else(|| format!("{sha} is not a weave queue entry").into())
285 }
286
287 fn enqueue(&self, candidate: &str, meta: &Value) -> R<u64> {
289 loop {
290 let heads = self.heads()?;
291 let old = heads.get("queue").cloned();
292 let n = match &old {
293 Some(q) => {
294 self.fetch()?;
295 self.ticket_of(q)? + 1
296 }
297 None => 0,
298 };
299 let mut msg = meta.clone();
300 msg["kind"] = json!("ticket");
301 msg["ticket"] = json!(n);
302 msg["candidate"] = json!(candidate);
303 msg["submitter"] = json!(self.me);
304 let first = match &old {
305 Some(q) => q.clone(),
306 None => self.genesis("queue")?,
307 };
308 let entry = self.chain_commit(&[&first, candidate], &msg)?;
309 if self.cas(&[(&entry, self.r("queue"), old.as_deref())])? {
310 return Ok(n);
311 }
312 std::thread::sleep(jitter(Duration::from_millis(50)));
313 }
314 }
315
316 fn entry(&self, head: &str, head_n: u64, n: u64) -> R<String> {
319 if n > head_n {
320 return Err(format!("ticket {n} is not in the chain (head is {head_n})").into());
321 }
322 let back = head_n - n;
323 let e = git(&self.dir, &["rev-parse", &format!("{head}~{back}")])?;
324 let got = self.ticket_of(&e)?;
325 if got != n {
326 return Err(format!("chain entry {e} is ticket {got}, expected {n}").into());
327 }
328 Ok(e)
329 }
330
331 fn result(&self, n: u64, heads: &HashMap<String, String>) -> R<Option<TicketResult>> {
333 let Some(done) = heads.get("done") else {
334 return Ok(None);
335 };
336 if git(&self.dir, &["cat-file", "-e", done]).is_err() {
337 self.fetch()?;
338 }
339 let dn = self.ticket_of(done)?;
340 if dn < n {
341 return Ok(None);
342 }
343 let e = self.entry(done, dn, n)?;
344 let m = self.message(&e)?;
345 let strs = |k: &str| -> Vec<String> {
346 m.get(k)
347 .and_then(Value::as_array)
348 .map(|a| {
349 a.iter()
350 .filter_map(|v| v.as_str().map(String::from))
351 .collect()
352 })
353 .unwrap_or_default()
354 };
355 let s = |k: &str| m.get(k).and_then(Value::as_str).unwrap_or("").to_string();
356 Ok(Some(TicketResult {
357 ticket: n,
358 landed: m.get("landed").and_then(Value::as_str).map(String::from),
359 reason: s("reason"),
360 files: strs("files"),
361 regions: strs("regions"),
362 report: s("report"),
363 tip: s("tip"),
364 }))
365 }
366}
367
368struct Held {
372 head: Mutex<String>,
373 lost: AtomicBool,
374 stop: AtomicBool,
375}
376
377impl Held {
378 fn is_lost(&self) -> bool {
379 self.lost.load(Ordering::SeqCst)
380 }
381}
382
383fn lock_msg(ctx: &Ctx, holder: Option<&str>) -> Value {
384 json!({"kind": "lock", "holder": holder, "by": ctx.me,
385 "at": std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH)
386 .map(|d| d.as_millis() as u64).unwrap_or(0)})
387}
388
389fn lock_is_free(ctx: &Ctx, head: Option<&String>) -> R<bool> {
390 match head {
391 None => Ok(true),
392 Some(h) => {
393 if git(&ctx.dir, &["cat-file", "-e", h]).is_err() {
394 ctx.fetch()?;
395 }
396 Ok(ctx.message(h)?.get("holder").is_none_or(Value::is_null))
397 }
398 }
399}
400
401fn acquire(ctx: &Ctx, expected: Option<&str>) -> R<Option<Arc<Held>>> {
403 let parents: Vec<&str> = expected.into_iter().collect();
404 let new = ctx.chain_commit(&parents, &lock_msg(ctx, Some(&ctx.me)))?;
405 if !ctx.cas(&[(&new, ctx.r("lock"), expected)])? {
406 return Ok(None);
407 }
408 Ok(Some(Arc::new(Held {
409 head: Mutex::new(new),
410 lost: AtomicBool::new(false),
411 stop: AtomicBool::new(false),
412 })))
413}
414
415fn heartbeat(
417 dir: PathBuf,
418 remote: String,
419 branch: String,
420 me: String,
421 held: Arc<Held>,
422 ttl: Duration,
423) {
424 let ctx = Ctx {
425 dir,
426 remote,
427 branch,
428 me,
429 };
430 let every = ttl / 3;
431 loop {
432 let mut slept = Duration::ZERO;
433 while slept < every {
434 if held.stop.load(Ordering::SeqCst) || held.is_lost() {
435 return;
436 }
437 let step = Duration::from_millis(100).min(every - slept);
438 std::thread::sleep(step);
439 slept += step;
440 }
441 let mut head = held.head.lock().unwrap();
442 if held.stop.load(Ordering::SeqCst) || held.is_lost() {
443 return;
444 }
445 let renewed = ctx
446 .chain_commit(&[&head], &lock_msg(&ctx, Some(&ctx.me)))
447 .and_then(|new| Ok((ctx.cas(&[(&new, ctx.r("lock"), Some(&head))])?, new)));
448 match renewed {
449 Ok((true, new)) => *head = new,
450 Ok((false, _)) => held.lost.store(true, Ordering::SeqCst),
451 Err(_) => {} }
453 }
454}
455
456fn release(ctx: &Ctx, held: &Held) {
457 held.stop.store(true, Ordering::SeqCst);
458 let head = held.head.lock().unwrap();
459 if held.is_lost() {
460 return;
461 }
462 if let Ok(new) = ctx.chain_commit(&[&head], &lock_msg(ctx, None)) {
463 let _ = ctx.cas(&[(&new, ctx.r("lock"), Some(&head))]);
464 }
465}
466
467fn lander_tree(ctx: &Ctx, candidate: &str) -> R<PathBuf> {
470 let common = PathBuf::from(git(
471 &ctx.dir,
472 &["rev-parse", "--path-format=absolute", "--git-common-dir"],
473 )?);
474 let safe: String = ctx
475 .branch
476 .chars()
477 .map(|c| if c.is_ascii_alphanumeric() { c } else { '-' })
478 .collect();
479 let wt = common
480 .join("weave-queue")
481 .join(format!("{}-{safe}", ctx.remote));
482 if !wt.join(".git").exists() {
483 let _ = git(&ctx.dir, &["worktree", "prune"]);
484 std::fs::create_dir_all(wt.parent().unwrap())?;
485 git(
486 &ctx.dir,
487 &[
488 "worktree",
489 "add",
490 "-q",
491 "-f",
492 "--detach",
493 wt.to_str().unwrap(),
494 candidate,
495 ],
496 )?;
497 return Ok(wt);
498 }
499 let wgd = PathBuf::from(git(&wt, &["rev-parse", "--absolute-git-dir"])?);
503 let mut said = Vec::new();
504 if let Some(why) = crate::mergestate::clear_stale_lock(&wgd, &mut said)? {
505 return Err(format!("the lander's worktree {}: {why}", wt.display()).into());
506 }
507 if wgd.join("MERGE_HEAD").exists() {
508 let _ = git(&wt, &["merge", "--abort"]);
509 }
510 git(&wt, &["reset", "-q", "--hard"])?;
511 crate::mergestate::clear_record(&wgd);
512 git(&wt, &["checkout", "-q", "-f", "--detach", candidate])?;
513 git(&wt, &["clean", "-q", "-f", "-d"])?;
514 Ok(wt)
515}
516
517fn land_tickets(
521 ctx: &Ctx,
522 q: &QueueOptions,
523 host: &Host,
524 held: &Held,
525 mine: u64,
526 log: &mut dyn Write,
527) -> R<()> {
528 loop {
529 if held.is_lost() {
530 return Ok(());
531 }
532 ctx.fetch()?;
533 let seen =
534 |name: &str| git(&ctx.dir, &["rev-parse", "-q", "--verify", &ctx.local(name)]).ok();
535 let (Some(queue), Some(tip)) = (seen("queue"), seen("tip")) else {
536 return Ok(());
537 };
538 let done = git(
539 &ctx.dir,
540 &["rev-parse", "-q", "--verify", &ctx.local("done")],
541 )
542 .ok();
543 let qn = ctx.ticket_of(&queue)?;
544 let next = match &done {
545 Some(d) => ctx.ticket_of(d)? + 1,
546 None => 0,
547 };
548 if next > qn {
549 return Ok(());
550 }
551 let entry = ctx.entry(&queue, qn, next)?;
552 let meta = ctx.message(&entry)?;
553 let candidate = git(&ctx.dir, &["rev-parse", &format!("{entry}^2")])?;
554 let submitter = meta
555 .get("submitter")
556 .and_then(Value::as_str)
557 .unwrap_or("?")
558 .to_string();
559
560 let opts = Options {
561 remote: ctx.remote.clone(),
562 branch: ctx.branch.clone(),
563 resolver: None,
564 verify_cmd: meta
565 .get("verify_cmd")
566 .and_then(Value::as_str)
567 .map(String::from),
568 verify_timeout: Duration::from_secs(
569 meta.get("verify_timeout")
570 .and_then(Value::as_u64)
571 .unwrap_or(1800),
572 ),
573 check: (meta.get("check").and_then(Value::as_str) == Some("sem")).then(|| {
575 crate::semcheck::Check {
576 checkers: meta
577 .get("checkers")
578 .and_then(Value::as_str)
579 .map(String::from),
580 }
581 }),
582 attempts: 1,
583 certificate_dir: q.onto.certificate_dir.clone(),
584 };
585 let mut report: Vec<u8> = Vec::new();
586 let mut checked: Option<Value> = None;
587 let prepared = lander_tree(ctx, &candidate).and_then(|wt| {
588 let n = next.to_string();
589 let env = [
590 ("WEAVE_QUEUE_TICKET", n.as_str()),
591 ("WEAVE_QUEUE_CANDIDATE", candidate.as_str()),
592 ("WEAVE_LAND_REPO", ctx.dir.to_str().unwrap_or(".")),
593 ];
594 let p = onto::prepare(&wt, &opts, host, &mut report, &tip, &env);
595 if opts.check.is_some() {
596 if let Ok(tree) = git(&wt, &["rev-parse", "HEAD^{tree}"]) {
597 checked = onto::recorded_check(&wt, &tree);
598 }
599 }
600 p
601 });
602 let report_text = String::from_utf8_lossy(&report).to_string();
603 let tail: String = {
604 let lines: Vec<&str> = report_text.lines().collect();
605 lines[lines.len().saturating_sub(200)..].join("\n")
606 };
607 let mut result = json!({
608 "kind": "result", "ticket": next, "candidate": candidate, "submitter": submitter,
609 "lander": ctx.me, "tip": tip, "report": tail,
610 });
611 if let Some(c) = checked {
612 result["check"] = json!({
615 "verdict": c["verdict"], "reason": c["reason"], "tree": c["tree"],
616 "certificate": c["sem"]["certificate"],
617 });
618 }
619 let landed = match prepared {
620 Ok(Prep::Ready(head)) => {
621 result["landed"] = json!(head);
622 result["reason"] = json!("landed");
623 Some(head)
624 }
625 Ok(Prep::Refused(r)) => {
626 result["landed"] = Value::Null;
627 result["reason"] = json!(r.reason);
628 result["files"] = json!(r.files);
629 result["regions"] = json!(r.regions);
630 None
631 }
632 Err(e) => {
635 result["landed"] = Value::Null;
636 result["reason"] = json!("land failed");
637 result["report"] = json!(format!("{tail}\nland: the checks could not run: {e}"));
638 None
639 }
640 };
641 let first = match &done {
642 Some(d) => d.clone(),
643 None => ctx.genesis("done")?,
644 };
645 let mut parents = vec![first.as_str()];
646 if let Some(h) = &landed {
647 parents.push(h.as_str());
648 }
649 let done_new = ctx.chain_commit(&parents, &result)?;
650
651 let pushed = {
652 let mut head = held.head.lock().unwrap();
653 if held.is_lost() {
654 return Ok(());
655 }
656 let lock_new = ctx.chain_commit(&[&head], &lock_msg(ctx, Some(&ctx.me)))?;
657 let mut ups: Vec<(&str, String, Option<&str>)> = vec![
658 (&lock_new, ctx.r("lock"), Some(head.as_str())),
659 (&done_new, ctx.r("done"), done.as_deref()),
660 ];
661 let main_ref = format!("refs/heads/{}", ctx.branch);
662 if let Some(h) = &landed {
663 ups.push((h, main_ref, Some(&tip)));
664 }
665 let ok = ctx.cas(&ups)?;
666 if ok {
667 *head = lock_new;
668 }
669 ok
670 };
671 if !pushed {
672 let h = ctx.heads()?;
675 if h.get("lock") != Some(&held.head.lock().unwrap().clone()) {
676 held.lost.store(true, Ordering::SeqCst);
677 return Ok(());
678 }
679 continue;
680 }
681 let _ = writeln!(
682 log,
683 "queue: ticket {next} ({submitter}): {}",
684 match &landed {
685 Some(h) => format!("landed {}", &h[..h.len().min(12)]),
686 None => format!("refused ({})", result["reason"].as_str().unwrap_or("?")),
687 }
688 );
689 if next >= mine {
690 return Ok(());
691 }
692 }
693}
694
695pub fn submit(dir: &Path, q: &QueueOptions, host: &Host, out: &mut dyn Write) -> R<QueueOutcome> {
698 let top = PathBuf::from(gitscan::git(dir, &["rev-parse", "--show-toplevel"])?.trim());
699 let gd = PathBuf::from(gitscan::git(&top, &["rev-parse", "--absolute-git-dir"])?.trim());
700 let nanos = std::time::SystemTime::now()
701 .duration_since(std::time::UNIX_EPOCH)
702 .map(|d| d.as_nanos())
703 .unwrap_or(0);
704 let host_name = std::env::var("HOSTNAME").unwrap_or_else(|_| "host".into());
705 let ctx = Ctx {
706 dir: top.clone(),
707 remote: q.onto.remote.clone(),
708 branch: q.onto.branch.clone(),
709 me: format!("{host_name}-{}-{nanos}", std::process::id()),
710 };
711 let target = format!("{}/{}", ctx.remote, ctx.branch);
712
713 if gd.join("rebase-merge").exists() || gd.join("rebase-apply").exists() {
714 writeln!(
715 out,
716 "land: a rebase is in progress; finish it first. land merges; it does not rebase."
717 )?;
718 return Ok(QueueOutcome::Refused("rebase in progress".into()));
719 }
720 if gd.join("MERGE_HEAD").exists() {
723 writeln!(
724 out,
725 "land: checking your resolution of the merge in progress …"
726 )?;
727 match onto::check_resolution(&top, &q.onto, host, out)? {
728 true => {}
729 false => return Ok(QueueOutcome::Refused("resolution refused".into())),
730 }
731 }
732 if !git(&top, &["status", "--porcelain", "--untracked-files=no"])?.is_empty() {
733 writeln!(
734 out,
735 "land: you have uncommitted changes to tracked files; commit them first."
736 )?;
737 return Ok(QueueOutcome::Refused("dirty".into()));
738 }
739
740 for attempt in 1..=q.onto.attempts.max(1) {
741 let candidate = git(&top, &["rev-parse", "HEAD"])?;
742 let (ok, _, err) = run(
743 &top,
744 &["fetch", "-q", &ctx.remote, &ctx.branch],
745 Duration::from_secs(300),
746 )?;
747 if !ok {
748 return Err(format!("git fetch {target} failed: {}", err.trim()).into());
749 }
750 if git(&top, &["rev-list", "FETCH_HEAD..HEAD"])?.is_empty() {
751 writeln!(out, "land: nothing to land: your branch has no commits that are not already on {target}.")?;
752 return Ok(QueueOutcome::Refused("nothing to land".into()));
753 }
754 let meta = json!({
755 "verify_cmd": q.onto.verify_cmd,
756 "verify_timeout": q.onto.verify_timeout.as_secs(),
757 "check": q.onto.check.as_ref().map(|_| "sem"),
758 "checkers": q.onto.check.as_ref().and_then(|c| c.checkers.clone()),
759 });
760 let ticket = ctx.enqueue(&candidate, &meta)?;
761 writeln!(
762 out,
763 "land: queued {} for {target} as ticket {ticket}; waiting for it to land …",
764 &candidate[..12]
765 )?;
766 let res = wait(&ctx, q, host, ticket, out)?;
767 let Some(res) = res else {
768 writeln!(
769 out,
770 "land: ticket {ticket} is still queued after {}s; it will still be landed or \
771 refused. Run `weave land --queue` again later to see the result.",
772 q.wait_timeout.as_secs()
773 )?;
774 return Ok(QueueOutcome::Refused("still queued".into()));
775 };
776 if let Some(h) = &res.landed {
777 if !res.report.is_empty() {
778 writeln!(out, "{}", res.report)?;
779 }
780 writeln!(
781 out,
782 "land: LANDED {} on {target} (ticket {ticket}).",
783 &h[..h.len().min(12)]
784 )?;
785 return Ok(QueueOutcome::Landed(h.clone()));
786 }
787 writeln!(out, "{}", res.report)?;
788 writeln!(
789 out,
790 "land: ticket {ticket} REFUSED ({}); nothing was published.",
791 res.reason
792 )?;
793 if !res.files.is_empty() {
794 writeln!(out, "land: refused files:\n{}", indent(&res.files))?;
795 }
796 if !res.regions.is_empty() {
797 writeln!(
798 out,
799 "land: conflict regions (merged onto {}):\n{}",
800 &res.tip[..res.tip.len().min(12)],
801 indent(&res.regions)
802 )?;
803 }
804 if res.reason != "merge refused" {
805 return Ok(QueueOutcome::Refused(res.reason));
806 }
807 let (ok, _, err) = run(
810 &top,
811 &["fetch", "-q", &ctx.remote, &ctx.branch],
812 Duration::from_secs(300),
813 )?;
814 if !ok {
815 return Err(format!("git fetch {target} failed: {}", err.trim()).into());
816 }
817 let now = git(&top, &["rev-parse", "FETCH_HEAD"])?;
818 let mut local_opts = clone_opts(&q.onto);
819 local_opts.verify_cmd = None; local_opts.check = None;
821 match onto::prepare(&top, &local_opts, host, out, &now, &[])? {
822 Prep::Refused(r) => {
823 if r.reason == "merge refused" {
824 writeln!(
825 out,
826 "\nland: the merge with {target} is left in progress in your tree. Resolve \
827 the files above in place (keep both sides' changes; remove every conflict \
828 marker), then run `weave land --queue` again — it checks your resolution, \
829 commits the merge and queues it."
830 )?;
831 }
832 return Ok(QueueOutcome::Refused(r.reason.into()));
833 }
834 Prep::Ready(_) => {
836 writeln!(out, "land: the merge with the newer {target} passes; queueing again (attempt {attempt}).")?;
837 }
838 }
839 }
840 writeln!(
841 out,
842 "land: still refused after {} attempts.",
843 q.onto.attempts.max(1)
844 )?;
845 Ok(QueueOutcome::Refused("attempts exhausted".into()))
846}
847
848fn clone_opts(o: &Options) -> Options {
849 Options {
850 remote: o.remote.clone(),
851 branch: o.branch.clone(),
852 resolver: o.resolver.clone(),
853 verify_cmd: o.verify_cmd.clone(),
854 verify_timeout: o.verify_timeout,
855 check: o.check.clone(),
856 attempts: o.attempts,
857 certificate_dir: o.certificate_dir.clone(),
858 }
859}
860
861fn indent(lines: &[String]) -> String {
862 lines
863 .iter()
864 .map(|l| format!(" {l}"))
865 .collect::<Vec<_>>()
866 .join("\n")
867}
868
869fn wait(
872 ctx: &Ctx,
873 q: &QueueOptions,
874 host: &Host,
875 n: u64,
876 out: &mut dyn Write,
877) -> R<Option<TicketResult>> {
878 let start = Instant::now();
879 let mut seen: Option<(String, Instant)> = None;
880 loop {
881 let heads = ctx.heads()?;
882 if let Some(r) = ctx.result(n, &heads)? {
883 return Ok(Some(r));
884 }
885 if start.elapsed() > q.wait_timeout {
886 return Ok(None);
887 }
888 let lock = heads.get("lock").cloned();
889 let take = if lock_is_free(ctx, lock.as_ref())? {
890 true
891 } else {
892 let l = lock.clone().unwrap_or_default();
893 match &seen {
894 Some((v, t)) if *v == l => t.elapsed() >= q.lease_ttl,
895 _ => {
896 seen = Some((l, Instant::now()));
897 false
898 }
899 }
900 };
901 if take {
902 let stale = !lock_is_free(ctx, lock.as_ref())?;
903 if let Some(held) = acquire(ctx, lock.as_deref())? {
904 if stale {
905 writeln!(
906 out,
907 "land: the lander holding the queue went quiet for {}s; taking over.",
908 q.lease_ttl.as_secs()
909 )?;
910 }
911 let hb = {
912 let (d, r, b, m, h, t) = (
913 ctx.dir.clone(),
914 ctx.remote.clone(),
915 ctx.branch.clone(),
916 ctx.me.clone(),
917 held.clone(),
918 q.lease_ttl,
919 );
920 std::thread::spawn(move || heartbeat(d, r, b, m, h, t))
921 };
922 let res = land_tickets(ctx, q, host, &held, n, out);
923 release(ctx, &held);
924 let _ = hb.join();
925 res?;
926 seen = None;
927 continue;
928 }
929 }
930 std::thread::sleep(jitter(q.poll));
931 }
932}