@halcyonlabs/app 0.47.0-halcyon.331 → 0.47.0-halcyon.333

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -316,8 +316,8 @@ declare const appKeybindingOverridesSchema: z$1.ZodArray<z$1.ZodObject<{
316
316
  shift: z$1.ZodBoolean;
317
317
  }, z$1.core.$strict>>;
318
318
  platform: z$1.ZodOptional<z$1.ZodEnum<{
319
- windows: "windows";
320
319
  mac: "mac";
320
+ windows: "windows";
321
321
  linux: "linux";
322
322
  }>>;
323
323
  }, z$1.core.$strict>>;
@@ -524,8 +524,8 @@ declare const environmentSchema: z$1.ZodObject<{
524
524
  retireAt: z$1.ZodNullable<z$1.ZodNumber>;
525
525
  teardown: z$1.ZodNullable<z$1.ZodObject<{
526
526
  status: z$1.ZodEnum<{
527
- running: "running";
528
527
  failed: "failed";
528
+ running: "running";
529
529
  removed: "removed";
530
530
  }>;
531
531
  attempt: z$1.ZodNumber;
@@ -620,8 +620,8 @@ declare const hostSchema: z$1.ZodObject<{
620
620
  pendingLog: z$1.ZodString;
621
621
  teardown: z$1.ZodNullable<z$1.ZodObject<{
622
622
  status: z$1.ZodEnum<{
623
- running: "running";
624
623
  failed: "failed";
624
+ running: "running";
625
625
  removed: "removed";
626
626
  }>;
627
627
  attempt: z$1.ZodNumber;
@@ -732,8 +732,8 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
732
732
  providerThreadId: z$1.ZodString;
733
733
  objective: z$1.ZodString;
734
734
  status: z$1.ZodEnum<{
735
- paused: "paused";
736
735
  active: "active";
736
+ paused: "paused";
737
737
  budgetLimited: "budgetLimited";
738
738
  complete: "complete";
739
739
  }>;
@@ -1304,9 +1304,9 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
1304
1304
  step: z$1.ZodString;
1305
1305
  status: z$1.ZodOptional<z$1.ZodEnum<{
1306
1306
  pending: "pending";
1307
+ active: "active";
1307
1308
  completed: "completed";
1308
1309
  failed: "failed";
1309
- active: "active";
1310
1310
  }>>;
1311
1311
  }, z$1.core.$strip>>;
1312
1312
  explanation: z$1.ZodOptional<z$1.ZodString>;
@@ -1385,10 +1385,10 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
1385
1385
  }>;
1386
1386
  taskStatus: z$1.ZodEnum<{
1387
1387
  pending: "pending";
1388
- running: "running";
1389
- paused: "paused";
1390
1388
  completed: "completed";
1391
1389
  failed: "failed";
1390
+ paused: "paused";
1391
+ running: "running";
1392
1392
  killed: "killed";
1393
1393
  stopped: "stopped";
1394
1394
  }>;
@@ -1405,8 +1405,8 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
1405
1405
  index: z$1.ZodNumber;
1406
1406
  label: z$1.ZodString;
1407
1407
  state: z$1.ZodEnum<{
1408
- running: "running";
1409
1408
  failed: "failed";
1409
+ running: "running";
1410
1410
  queued: "queued";
1411
1411
  done: "done";
1412
1412
  skipped: "skipped";
@@ -2076,9 +2076,9 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
2076
2076
  step: z$1.ZodString;
2077
2077
  status: z$1.ZodOptional<z$1.ZodEnum<{
2078
2078
  pending: "pending";
2079
+ active: "active";
2079
2080
  completed: "completed";
2080
2081
  failed: "failed";
2081
- active: "active";
2082
2082
  }>>;
2083
2083
  }, z$1.core.$strip>>;
2084
2084
  explanation: z$1.ZodOptional<z$1.ZodString>;
@@ -2157,10 +2157,10 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
2157
2157
  }>;
2158
2158
  taskStatus: z$1.ZodEnum<{
2159
2159
  pending: "pending";
2160
- running: "running";
2161
- paused: "paused";
2162
2160
  completed: "completed";
2163
2161
  failed: "failed";
2162
+ paused: "paused";
2163
+ running: "running";
2164
2164
  killed: "killed";
2165
2165
  stopped: "stopped";
2166
2166
  }>;
@@ -2177,8 +2177,8 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
2177
2177
  index: z$1.ZodNumber;
2178
2178
  label: z$1.ZodString;
2179
2179
  state: z$1.ZodEnum<{
2180
- running: "running";
2181
2180
  failed: "failed";
2181
+ running: "running";
2182
2182
  queued: "queued";
2183
2183
  done: "done";
2184
2184
  skipped: "skipped";
@@ -2389,10 +2389,10 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
2389
2389
  }>;
2390
2390
  taskStatus: z$1.ZodEnum<{
2391
2391
  pending: "pending";
2392
- running: "running";
2393
- paused: "paused";
2394
2392
  completed: "completed";
2395
2393
  failed: "failed";
2394
+ paused: "paused";
2395
+ running: "running";
2396
2396
  killed: "killed";
2397
2397
  stopped: "stopped";
2398
2398
  }>;
@@ -2409,8 +2409,8 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
2409
2409
  index: z$1.ZodNumber;
2410
2410
  label: z$1.ZodString;
2411
2411
  state: z$1.ZodEnum<{
2412
- running: "running";
2413
2412
  failed: "failed";
2413
+ running: "running";
2414
2414
  queued: "queued";
2415
2415
  done: "done";
2416
2416
  skipped: "skipped";
@@ -2488,10 +2488,10 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
2488
2488
  }>;
2489
2489
  taskStatus: z$1.ZodEnum<{
2490
2490
  pending: "pending";
2491
- running: "running";
2492
- paused: "paused";
2493
2491
  completed: "completed";
2494
2492
  failed: "failed";
2493
+ paused: "paused";
2494
+ running: "running";
2495
2495
  killed: "killed";
2496
2496
  stopped: "stopped";
2497
2497
  }>;
@@ -2508,8 +2508,8 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
2508
2508
  index: z$1.ZodNumber;
2509
2509
  label: z$1.ZodString;
2510
2510
  state: z$1.ZodEnum<{
2511
- running: "running";
2512
2511
  failed: "failed";
2512
+ running: "running";
2513
2513
  queued: "queued";
2514
2514
  done: "done";
2515
2515
  skipped: "skipped";
@@ -2704,9 +2704,9 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
2704
2704
  step: z$1.ZodString;
2705
2705
  status: z$1.ZodOptional<z$1.ZodEnum<{
2706
2706
  pending: "pending";
2707
+ active: "active";
2707
2708
  completed: "completed";
2708
2709
  failed: "failed";
2709
- active: "active";
2710
2710
  }>>;
2711
2711
  }, z$1.core.$strip>>;
2712
2712
  explanation: z$1.ZodOptional<z$1.ZodString>;
@@ -2715,8 +2715,8 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
2715
2715
  threadId: z$1.ZodString;
2716
2716
  providerThreadId: z$1.ZodString;
2717
2717
  state: z$1.ZodEnum<{
2718
- running: "running";
2719
2718
  idle: "idle";
2719
+ running: "running";
2720
2720
  }>;
2721
2721
  }, z$1.core.$strip>, z$1.ZodObject<{
2722
2722
  type: z$1.ZodLiteral<"turn/diff/updated">;
@@ -3578,9 +3578,9 @@ declare const threadEventSchema: z$1.ZodUnion<readonly [z$1.ZodIntersection<z$1.
3578
3578
  threadId: z$1.ZodString;
3579
3579
  provisioningId: z$1.ZodString;
3580
3580
  status: z$1.ZodEnum<{
3581
+ active: "active";
3581
3582
  completed: "completed";
3582
3583
  failed: "failed";
3583
- active: "active";
3584
3584
  cancelled: "cancelled";
3585
3585
  }>;
3586
3586
  environmentId: z$1.ZodNullable<z$1.ZodString>;
@@ -5766,9 +5766,9 @@ declare const hostDaemonCommandRegistry: {
5766
5766
  }, z$1.core.$strict>, z$1.ZodDiscriminatedUnion<[z$1.ZodObject<{
5767
5767
  kind: z$1.ZodLiteral<"wording">;
5768
5768
  status: z$1.ZodEnum<{
5769
- unavailable: "unavailable";
5770
5769
  pass: "pass";
5771
5770
  revise: "revise";
5771
+ unavailable: "unavailable";
5772
5772
  }>;
5773
5773
  }, z$1.core.$strict>, z$1.ZodObject<{
5774
5774
  kind: z$1.ZodLiteral<"review">;
@@ -6129,8 +6129,8 @@ declare const hostDaemonCommandRegistry: {
6129
6129
  skillFilePath: z$1.ZodString;
6130
6130
  }, z$1.core.$strict>], "kind">>;
6131
6131
  instructionMode: z$1.ZodEnum<{
6132
- append: "append";
6133
6132
  replace: "replace";
6133
+ append: "append";
6134
6134
  }>;
6135
6135
  type: z$1.ZodLiteral<"thread.rewind.prepare">;
6136
6136
  leaseId: z$1.ZodString;
@@ -6278,8 +6278,8 @@ declare const hostDaemonCommandRegistry: {
6278
6278
  skillFilePath: z$1.ZodString;
6279
6279
  }, z$1.core.$strict>], "kind">>;
6280
6280
  instructionMode: z$1.ZodEnum<{
6281
- append: "append";
6282
6281
  replace: "replace";
6282
+ append: "append";
6283
6283
  }>;
6284
6284
  type: z$1.ZodLiteral<"thread.start">;
6285
6285
  requestId: z$1.ZodString;
@@ -6742,8 +6742,8 @@ declare const hostDaemonCommandRegistry: {
6742
6742
  envPassthrough: z$1.ZodArray<z$1.ZodString>;
6743
6743
  }, z$1.core.$strict>;
6744
6744
  instructionMode: z$1.ZodEnum<{
6745
- append: "append";
6746
6745
  replace: "replace";
6746
+ append: "append";
6747
6747
  }>;
6748
6748
  projectId: z$1.ZodString;
6749
6749
  providerId: z$1.ZodString;
@@ -6958,8 +6958,8 @@ declare const hostDaemonCommandRegistry: {
6958
6958
  envPassthrough: z$1.ZodArray<z$1.ZodString>;
6959
6959
  }, z$1.core.$strict>;
6960
6960
  instructionMode: z$1.ZodEnum<{
6961
- append: "append";
6962
6961
  replace: "replace";
6962
+ append: "append";
6963
6963
  }>;
6964
6964
  projectId: z$1.ZodString;
6965
6965
  providerId: z$1.ZodString;
@@ -7860,13 +7860,13 @@ declare const hostDaemonCommandRegistry: {
7860
7860
  modifiedAtMs: z$1.ZodOptional<z$1.ZodNumber>;
7861
7861
  sha256: z$1.ZodString;
7862
7862
  }, z$1.core.$strip>, z$1.ZodObject<{
7863
+ path: z$1.ZodString;
7863
7864
  contentEncoding: z$1.ZodEnum<{
7864
7865
  base64: "base64";
7865
7866
  utf8: "utf8";
7866
7867
  }>;
7867
7868
  sha256: z$1.ZodString;
7868
7869
  sizeBytes: z$1.ZodNumber;
7869
- path: z$1.ZodString;
7870
7870
  mimeType: z$1.ZodOptional<z$1.ZodString>;
7871
7871
  modifiedAtMs: z$1.ZodOptional<z$1.ZodNumber>;
7872
7872
  notModified: z$1.ZodLiteral<true>;
@@ -8131,8 +8131,8 @@ declare const hostDaemonCommandRegistry: {
8131
8131
  supported: z$1.ZodLiteral<true>;
8132
8132
  health: z$1.ZodObject<{
8133
8133
  status: z$1.ZodEnum<{
8134
- ready: "ready";
8135
8134
  unknown: "unknown";
8135
+ ready: "ready";
8136
8136
  not_installed: "not_installed";
8137
8137
  unauthenticated: "unauthenticated";
8138
8138
  expired: "expired";
@@ -8184,8 +8184,8 @@ declare const hostDaemonCommandRegistry: {
8184
8184
  installed: z$1.ZodBoolean;
8185
8185
  installSource: z$1.ZodEnum<{
8186
8186
  notInstalled: "notInstalled";
8187
- external: "external";
8188
8187
  npmGlobal: "npmGlobal";
8188
+ external: "external";
8189
8189
  }>;
8190
8190
  currentVersion: z$1.ZodNullable<z$1.ZodString>;
8191
8191
  latestVersion: z$1.ZodNullable<z$1.ZodString>;
@@ -8735,8 +8735,8 @@ declare const providerCliStatusResponseSchema: z$1.ZodRecord<z$1.ZodString, z$1.
8735
8735
  installed: z$1.ZodBoolean;
8736
8736
  installSource: z$1.ZodEnum<{
8737
8737
  notInstalled: "notInstalled";
8738
- external: "external";
8739
8738
  npmGlobal: "npmGlobal";
8739
+ external: "external";
8740
8740
  }>;
8741
8741
  currentVersion: z$1.ZodNullable<z$1.ZodString>;
8742
8742
  latestVersion: z$1.ZodNullable<z$1.ZodString>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halcyonlabs/app",
3
- "version": "0.47.0-halcyon.331",
3
+ "version": "0.47.0-halcyon.333",
4
4
  "description": "bb app launcher for npx",
5
5
  "keywords": [
6
6
  "bb",
@@ -18,7 +18,9 @@ import subprocess
18
18
  RESULT_SPACE_FLOOR = 1_000_000
19
19
  MAIN_FREE_FLOOR = 15_000_000_000
20
20
  START_HEADROOM = 5_000_000_000
21
- EXPECTED_BUILD_BYTES = 5_500_000_000
21
+ EXPECTED_JOB_BYTES = 1_600_000_000
22
+ MIN_JOB_BYTES = 200_000_000
23
+ JOB_HISTORY_LENGTH = 16
22
24
  # Many tiny files can exhaust inodes while bytes remain: the same rule applies
23
25
  # to free file entries on filesystems that have a fixed number of them.
24
26
  INODE_FLOOR = 500_000
@@ -389,6 +391,7 @@ class Workspaces:
389
391
  f"{gigabytes(settings['disk_free_bytes'] + retained)} GB there. Free space on it first.")
390
392
  # Persist ownership first. A crash before mkdir still leaves a reserved
391
393
  # attempt which restart must explicitly finish, never silently forget.
394
+ write_record(self.state, manifest['id'] + '.expected', self.expected_job_bytes())
392
395
  write_record(self.state, manifest['id'] + '.json', manifest, exclusive=True)
393
396
  if not workspace:
394
397
  return manifest
@@ -404,28 +407,57 @@ class Workspaces:
404
407
  self.environment(manifest['id'])
405
408
  return manifest
406
409
 
410
+ def job_history(self):
411
+ raw = ledger_setting(self.state, 'job-footprints', 4096)
412
+ history = [] if raw is None else json.loads(raw)
413
+ if (not isinstance(history, list) or len(history) > JOB_HISTORY_LENGTH
414
+ or any(type(n) is not int or not 0 <= n <= 2**63 - 1 for n in history)):
415
+ raise ValueError('Invalid finished job footprints.')
416
+ return history
417
+
418
+ def expected_job_bytes(self, job_id=None):
419
+ if job_id is not None:
420
+ raw = ledger_setting(self.state, job_id + '.expected', 32)
421
+ if raw is not None:
422
+ value = json.loads(raw)
423
+ if type(value) is not int or not MIN_JOB_BYTES <= value <= 2**63 - 1:
424
+ raise ValueError('Invalid job growth reservation.')
425
+ return value
426
+ history = self.job_history()
427
+ return min(EXPECTED_JOB_BYTES, max(MIN_JOB_BYTES, max(history) * 3 // 2)) if history else EXPECTED_JOB_BYTES
428
+
429
+ def remember_footprint(self, manifest):
430
+ if not has_workspace(manifest):
431
+ return
432
+ try:
433
+ usage = self.usage(manifest['id'])
434
+ if usage['complete']:
435
+ history = self.job_history()
436
+ write_record(self.state, 'job-footprints', (history + [usage['bytes']])[-JOB_HISTORY_LENGTH:])
437
+ except (OSError, ValueError):
438
+ return
439
+
407
440
  def start_space(self, records, settings, keep=None):
408
- raw = ledger_setting(self.state, 'expected-build-bytes', 32)
409
- previous = EXPECTED_BUILD_BYTES if raw is None else int(raw)
410
- if not EXPECTED_BUILD_BYTES <= previous <= 2**63 - 1:
411
- raise ValueError('Invalid measured build footprint.')
412
- expected = max(previous, settings['disk_start_headroom_bytes'])
413
- sizes = {}
441
+ expected = self.expected_job_bytes(keep)
442
+ outstanding = 0
443
+ kept_size = 0
444
+ deadline = time.monotonic() + 5
414
445
  for record in records:
415
446
  if not holds_slot(record) and record['id'] != keep:
416
447
  continue
417
- if has_workspace(record):
418
- usage = self.usage(record['id'])
419
- if not usage['complete']:
420
- raise CapacityFull('Expected build growth cannot be checked; waiting for a complete workspace measurement.')
421
- sizes[record['id']] = usage['bytes']
422
- expected = max(expected, usage['bytes'])
448
+ size = 0
449
+ if has_workspace(record) and time.monotonic() < deadline:
450
+ try:
451
+ usage = self.usage(record['id'], deadline=deadline)
452
+ if usage['complete']:
453
+ size = usage['bytes']
454
+ except (OSError, ValueError):
455
+ pass
456
+ if record['id'] == keep:
457
+ kept_size = size
423
458
  else:
424
- sizes[record['id']] = 0
425
- if expected > previous:
426
- write_record(self.state, 'expected-build-bytes', expected)
427
- outstanding = sum(max(0, expected - size) for job, size in sizes.items() if job != keep)
428
- new_growth = max(0, expected - sizes.get(keep, 0))
459
+ outstanding += max(0, self.expected_job_bytes(record['id']) - size)
460
+ new_growth = max(settings['disk_start_headroom_bytes'], expected - kept_size)
429
461
  return settings['disk_free_bytes'] + outstanding + new_growth
430
462
 
431
463
  def require_admission(self, records):
@@ -492,7 +524,7 @@ class Workspaces:
492
524
 
493
525
 
494
526
 
495
- def usage(self, job_id):
527
+ def usage(self, job_id, *, deadline=None):
496
528
  """Size a workspace to choose which job to stop when the disk is low.
497
529
 
498
530
  The walk is bounded to five seconds. A walk cut short by the time limit
@@ -505,23 +537,24 @@ class Workspaces:
505
537
  self.read(job_id)
506
538
  root = self.open_workspace(job_id)
507
539
  device = os.fstat(root).st_dev
508
- stack = [(root, os.scandir(root))]
509
- deadline = time.monotonic() + 5
540
+ stack = [(root, os.scandir(root), '')]
541
+ deadline = min(deadline, time.monotonic() + 5) if deadline is not None else time.monotonic() + 5
510
542
  try:
511
543
  return self._walk(stack, device, deadline)
512
544
  finally:
513
- for fd, entries in reversed(stack):
545
+ for fd, entries, _ in reversed(stack):
514
546
  entries.close()
515
547
  os.close(fd)
516
548
 
517
549
  def _walk(self, stack, device, deadline):
518
550
  files = size = 0
551
+ complete = True
519
552
  seen = set()
520
553
  try:
521
554
  while stack:
522
555
  if time.monotonic() > deadline:
523
556
  return dict(files=files, bytes=size, complete=False)
524
- current, entries = stack[-1]
557
+ current, entries, parent = stack[-1]
525
558
  entry = next(entries, None)
526
559
  if entry is None:
527
560
  entries.close()
@@ -540,6 +573,15 @@ class Workspaces:
540
573
  size += metadata.st_blocks * 512
541
574
  seen.add(identity)
542
575
  if stat.S_ISDIR(metadata.st_mode):
576
+ if entry.name in MEASUREMENT_SKIPPED or (entry.name == 'build' and parent == 'results'):
577
+ child = os.open(entry.name, DIR_FLAGS, dir_fd=current)
578
+ try:
579
+ with os.scandir(child) as skipped:
580
+ if next(skipped, None) is not None:
581
+ complete = False
582
+ finally:
583
+ os.close(child)
584
+ continue
543
585
  try:
544
586
  child = os.open(entry.name, DIR_FLAGS, dir_fd=current)
545
587
  except OSError as error:
@@ -549,8 +591,8 @@ class Workspaces:
549
591
  if os.fstat(child).st_dev != device:
550
592
  os.close(child)
551
593
  raise ValueError('A workspace cannot contain another filesystem.')
552
- stack.append((child, os.scandir(child)))
553
- return dict(files=files, bytes=size, complete=True)
594
+ stack.append((child, os.scandir(child), entry.name))
595
+ return dict(files=files, bytes=size, complete=complete)
554
596
  except OSError:
555
597
  return dict(files=files, bytes=size, complete=False)
556
598
 
@@ -784,6 +826,8 @@ class Workspaces:
784
826
  return self.save_transition(manifest, 'closed')
785
827
 
786
828
  def save_transition(self, manifest, state, reason=None):
829
+ if state == 'stopping' and reason == 'finished':
830
+ self.remember_footprint(manifest)
787
831
  updated = transition(manifest, state, reason)
788
832
  write_record(self.state, manifest['id'] + '.json', updated)
789
833
  return updated
@@ -1577,6 +1621,7 @@ def darwin_workspace_users(workspace):
1577
1621
 
1578
1622
  UNPUSHED_SKIPPED = frozenset(('.git', 'node_modules', 'target', '.orbit-build', 'cargo-home', 'cargo-target',
1579
1623
  'npm-cache', 'pnpm-store', 'pnpm-home', 'cache'))
1624
+ MEASUREMENT_SKIPPED = frozenset(('node_modules', 'target', 'cargo-target', 'pnpm-store', 'npm-cache'))
1580
1625
  UNPUSHED_DEPTH = 6
1581
1626
  UNPUSHED_SECONDS = 30
1582
1627
  GIT_SECONDS = 20
@@ -1586,40 +1631,58 @@ GIT_SAFE = ('-c', 'core.fsmonitor=false', '-c', 'core.hooksPath=/dev/null', '-c'
1586
1631
 
1587
1632
 
1588
1633
  def git_checkouts(root, deadline, include_build=False):
1589
- def refuse(error):
1590
- raise error
1591
-
1592
1634
  found = []
1593
- for directory, folders, files in os.walk(root, onerror=refuse):
1635
+ pending = [Path(root)]
1636
+ while pending:
1594
1637
  if time.monotonic() > deadline:
1595
1638
  raise ValueError('Searching the work folder for Git checkouts took too long.')
1596
- here = Path(directory)
1597
- checkout = '.git' in folders or '.git' in files or (include_build and {'HEAD', 'objects', 'config'}.issubset(set(folders) | set(files)))
1598
- if '.git' in folders and not (here / '.git').is_symlink():
1599
- with os.scandir(here / '.git') as entries:
1600
- checkout = next(entries, None) is not None
1601
- if checkout:
1639
+ here = pending.pop()
1640
+ folders = []
1641
+ markers = set()
1642
+ checkout = False
1643
+ with os.scandir(here) as entries:
1644
+ for entry in entries:
1645
+ if time.monotonic() > deadline:
1646
+ raise ValueError('Searching the work folder for Git checkouts took too long.')
1647
+ if entry.name in ('HEAD', 'objects', 'config'):
1648
+ markers.add(entry.name)
1649
+ if entry.name == '.git':
1650
+ if entry.is_dir(follow_symlinks=False):
1651
+ with os.scandir(entry.path) as metadata:
1652
+ checkout = next(metadata, None) is not None
1653
+ else:
1654
+ checkout = True
1655
+ if not entry.is_dir(follow_symlinks=False):
1656
+ continue
1657
+ if entry.name in UNPUSHED_SKIPPED and not (include_build and entry.name == '.orbit-build'):
1658
+ continue
1659
+ if here.name == 'results' and entry.name == 'build':
1660
+ continue
1661
+ if include_build or len(here.relative_to(root).parts) < UNPUSHED_DEPTH:
1662
+ folders.append(entry.name)
1663
+ if checkout or (include_build and {'HEAD', 'objects', 'config'}.issubset(markers)):
1602
1664
  found.append(here)
1603
- if not include_build and len(here.relative_to(root).parts) >= UNPUSHED_DEPTH:
1604
- folders[:] = []
1605
- else:
1606
- folders[:] = sorted(name for name in folders if name not in UNPUSHED_SKIPPED or (include_build and name == '.orbit-build'))
1665
+ pending.extend(here / name for name in sorted(folders, reverse=True))
1607
1666
  return found
1608
1667
 
1609
1668
 
1610
- def git_output(account, checkout, name, *args, allowed=(0,), stdin='', offline=False):
1669
+ def git_output(account, checkout, name, *args, allowed=(0,), stdin='', offline=False, deadline=None):
1611
1670
  import shutil
1612
1671
  import subprocess
1613
1672
  program = shutil.which('git', path=GIT_PATH)
1614
1673
  if program is None:
1615
1674
  raise ValueError('Git is not installed, so unpushed work cannot be checked.')
1616
1675
  environment = dict(PATH=GIT_PATH, HOME=account.pw_dir, LC_ALL='C', GIT_CONFIG_NOSYSTEM='1',
1617
- GIT_CONFIG_GLOBAL='/dev/null', GIT_OPTIONAL_LOCKS='0', GIT_TERMINAL_PROMPT='0')
1676
+ GIT_CONFIG_GLOBAL='/dev/null', GIT_OPTIONAL_LOCKS='0', GIT_TERMINAL_PROMPT='0',
1677
+ GIT_CEILING_DIRECTORIES=str(Path(checkout).parent))
1618
1678
  if offline:
1619
1679
  environment.update(GIT_NO_LAZY_FETCH='1', GIT_ALLOW_PROTOCOL='', GIT_PAGER='cat')
1680
+ timeout = GIT_SECONDS if deadline is None else min(GIT_SECONDS, deadline - time.monotonic())
1681
+ if timeout <= 0:
1682
+ raise ValueError('Checking the work folder for unpushed commits took too long.')
1620
1683
  try:
1621
1684
  result = subprocess.run([program, '-C', str(checkout), *GIT_SAFE, *args], cwd='/', env=environment, input=stdin,
1622
- capture_output=True, text=True, errors='replace', timeout=GIT_SECONDS)
1685
+ capture_output=True, text=True, errors='replace', timeout=timeout)
1623
1686
  except subprocess.TimeoutExpired:
1624
1687
  raise ValueError(f'Git took too long to read {name}.')
1625
1688
  if result.returncode not in allowed:
@@ -1628,9 +1691,9 @@ def git_output(account, checkout, name, *args, allowed=(0,), stdin='', offline=F
1628
1691
  return result
1629
1692
 
1630
1693
 
1631
- def unpushed_checkout(account, checkout, name):
1694
+ def unpushed_checkout(account, checkout, name, deadline=None):
1632
1695
  def git(*args, allowed=(0,)):
1633
- return git_output(account, checkout, name, *args, allowed=allowed)
1696
+ return git_output(account, checkout, name, *args, allowed=allowed, deadline=deadline)
1634
1697
 
1635
1698
  def count(*revisions):
1636
1699
  return int(git('rev-list', '--count', *revisions).stdout.strip() or '0')
@@ -1666,7 +1729,7 @@ def search_unpushed(root, account, seconds, include_build=False):
1666
1729
  if time.monotonic() > deadline:
1667
1730
  raise ValueError('Checking the work folder for unpushed commits took too long.')
1668
1731
  name = str(checkout.relative_to(root))
1669
- work = unpushed_checkout(account, checkout, name)
1732
+ work = unpushed_checkout(account, checkout, name, deadline)
1670
1733
  if work['branches'] or work['detached'] or work['uncommitted'] or work['untracked'] or work['stashes']:
1671
1734
  found.append(dict(path=name, **work))
1672
1735
  return dict(checkouts=found)
@@ -1 +1 @@
1
- 7ba07c6b01118e277b923dec7e5cb77ce7a86a89
1
+ 55a68a0b352242b1c815daadd214812f7764a927