devlaunch 0.0.27__tar.gz → 0.0.28__tar.gz
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.
- {devlaunch-0.0.27 → devlaunch-0.0.28}/PKG-INFO +1 -1
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/worktree/workspace_clone.py +79 -7
- {devlaunch-0.0.27 → devlaunch-0.0.28}/pyproject.toml +1 -1
- {devlaunch-0.0.27 → devlaunch-0.0.28}/.gitignore +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/LICENSE +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/README.md +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/__init__.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/aid.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/completion.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/completion_loader.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/completions/__init__.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/completions/dl.bash +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/devpod_provider.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/devpod_ssh.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/disk_usage.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/dl.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/gh_auth.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/timing.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/tools.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/tty_session.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/workspace_id.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/workspace_state.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/worktree/__init__.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/worktree/branch_manager.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/worktree/config.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/worktree/git_errors.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/worktree/locks.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/worktree/migration.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/worktree/models.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/worktree/repo_manager.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/worktree/storage.py +0 -0
- {devlaunch-0.0.27 → devlaunch-0.0.28}/devlaunch/xdg.py +0 -0
|
@@ -19,8 +19,9 @@ import logging
|
|
|
19
19
|
import os
|
|
20
20
|
import shutil
|
|
21
21
|
import subprocess
|
|
22
|
+
from dataclasses import dataclass
|
|
22
23
|
from pathlib import Path
|
|
23
|
-
from typing import Optional
|
|
24
|
+
from typing import Optional, Union
|
|
24
25
|
|
|
25
26
|
from .. import timing
|
|
26
27
|
from ..workspace_id import WorkspaceId, validate_ref_name
|
|
@@ -44,6 +45,42 @@ _LFS_POINTER_PREFIX = b"version https://git-lfs"
|
|
|
44
45
|
logger = logging.getLogger(__name__)
|
|
45
46
|
|
|
46
47
|
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class FreshBase:
|
|
50
|
+
"""The ref this launch runs on was fetched from the remote this call.
|
|
51
|
+
|
|
52
|
+
Either the requested branch itself was refreshed, or the branch is new and
|
|
53
|
+
the base it was cut from was. Carries no payload: "your tip is current" is
|
|
54
|
+
the whole message.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class StaleBase:
|
|
60
|
+
"""The launch proceeds on a ref nothing refreshed this call.
|
|
61
|
+
|
|
62
|
+
*base* names that ref — the default branch a new branch was cut from, the
|
|
63
|
+
cache's own ``HEAD`` when no default branch could even be named, or the
|
|
64
|
+
requested branch itself when its fetch failed. *reason* is why nothing
|
|
65
|
+
refreshed it, carried rather than reconstructed at the print site for the
|
|
66
|
+
same reason :class:`devlaunch.worktree.repo_manager.FetchFailed` carries
|
|
67
|
+
its own.
|
|
68
|
+
|
|
69
|
+
Deliberately not an error (devlaunch#144: launch-from-cache is the offline
|
|
70
|
+
contract) and deliberately not only a log line (devlaunch#245: the caller —
|
|
71
|
+
and wf, reading dl's output — must be able to tell a fresh base from a
|
|
72
|
+
stale one).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
base: str
|
|
76
|
+
reason: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Two arms and no third: every path through ensure_branch answers one of these,
|
|
80
|
+
# so "the fetch quietly did not back the base" cannot exist as an unnamed state.
|
|
81
|
+
BranchBase = Union[FreshBase, StaleBase]
|
|
82
|
+
|
|
83
|
+
|
|
47
84
|
def _on_disk(path: Path) -> Optional[bool]:
|
|
48
85
|
"""Whether *path* is there: True, False, or None for "could not look".
|
|
49
86
|
|
|
@@ -460,7 +497,17 @@ class WorkspaceCloneManager:
|
|
|
460
497
|
workspace = WorkspaceId(owner, repo, branch)
|
|
461
498
|
with self.repo_manager.hold_repo_lock(owner, repo) as lock:
|
|
462
499
|
self.repo_manager.clone_if_missing(lock, owner, repo, remote_url)
|
|
463
|
-
self.ensure_branch(lock, owner, repo, branch)
|
|
500
|
+
base = self.ensure_branch(lock, owner, repo, branch)
|
|
501
|
+
if isinstance(base, StaleBase):
|
|
502
|
+
# The one consequence-stating line for the whole degraded
|
|
503
|
+
# family, in dl's own output because wf reads that output: the
|
|
504
|
+
# fetch-level warnings above it say what failed, this says what
|
|
505
|
+
# it means for the tree the agent is about to work on.
|
|
506
|
+
logger.warning(
|
|
507
|
+
f"Prepared '{owner}/{repo}@{branch}' from the cache's '{base.base}', "
|
|
508
|
+
f"which could not be refreshed ({base.reason}); "
|
|
509
|
+
f"it may be behind the remote."
|
|
510
|
+
)
|
|
464
511
|
return self._prepare_workspace(
|
|
465
512
|
lock,
|
|
466
513
|
workspace,
|
|
@@ -469,9 +516,15 @@ class WorkspaceCloneManager:
|
|
|
469
516
|
remote_url,
|
|
470
517
|
)
|
|
471
518
|
|
|
472
|
-
def ensure_branch(self, lock: RepoLock, owner: str, repo: str, branch: str) ->
|
|
519
|
+
def ensure_branch(self, lock: RepoLock, owner: str, repo: str, branch: str) -> BranchBase:
|
|
473
520
|
"""Ensure a branch exists in the bare repo, at the remote's current tip.
|
|
474
521
|
|
|
522
|
+
Returns which of those two things it actually delivered: a
|
|
523
|
+
:class:`FreshBase` when the tip the launch runs on was fetched this
|
|
524
|
+
call, a :class:`StaleBase` naming the unrefreshed ref and the reason
|
|
525
|
+
when it was not (devlaunch#245). The degraded arms all proceed — that
|
|
526
|
+
is the devlaunch#144 contract — but they no longer proceed silently.
|
|
527
|
+
|
|
475
528
|
This is the whole of the launch path's network use, and the staleness
|
|
476
529
|
contract devlaunch#144 settled is stated here because this is the code
|
|
477
530
|
that keeps it:
|
|
@@ -505,14 +558,17 @@ class WorkspaceCloneManager:
|
|
|
505
558
|
|
|
506
559
|
try:
|
|
507
560
|
default_branch = self.repo_manager.get_default_branch(owner, repo)
|
|
561
|
+
default_branch_error = None
|
|
508
562
|
except (RuntimeError, subprocess.CalledProcessError, OSError) as e:
|
|
509
563
|
logger.warning(f"Failed to resolve default branch: {e}")
|
|
510
564
|
default_branch = None
|
|
565
|
+
default_branch_error = str(e)
|
|
511
566
|
|
|
512
567
|
# Each arm named, so a fourth one cannot be silently read as one of
|
|
513
568
|
# these three.
|
|
569
|
+
base: BranchBase
|
|
514
570
|
if isinstance(outcome, Updated):
|
|
515
|
-
|
|
571
|
+
base = FreshBase()
|
|
516
572
|
elif isinstance(outcome, RefMissingOnRemote):
|
|
517
573
|
# The remote answered, so the branch really is new: base it on the
|
|
518
574
|
# default branch, and fetch *that* so it is based on something
|
|
@@ -529,18 +585,23 @@ class WorkspaceCloneManager:
|
|
|
529
585
|
# dl's launch path guards it with (RuntimeError, OSError),
|
|
530
586
|
# so a ValueError here would surface as a traceback.
|
|
531
587
|
logger.warning(f"Cannot fetch recorded default branch: {e}")
|
|
588
|
+
base = StaleBase(base=default_branch, reason=str(e))
|
|
532
589
|
else:
|
|
533
590
|
# Named arm by arm for the same reason as the dispatch
|
|
534
591
|
# around it: the sum exists so a fourth outcome cannot be
|
|
535
592
|
# quietly read as one of these three.
|
|
536
593
|
if isinstance(base_outcome, Updated):
|
|
537
|
-
|
|
594
|
+
base = FreshBase()
|
|
538
595
|
elif isinstance(base_outcome, RefMissingOnRemote):
|
|
539
596
|
# The remote has no default branch under that name
|
|
540
597
|
# either. Nothing more to try -- there is no third ref
|
|
541
598
|
# -- and the branch creation below still has the cache's
|
|
542
|
-
# own default branch to start from.
|
|
543
|
-
|
|
599
|
+
# own default branch to start from. Unverifiable is
|
|
600
|
+
# stale here: nothing fetched backs it.
|
|
601
|
+
base = StaleBase(
|
|
602
|
+
base=default_branch,
|
|
603
|
+
reason=f"the remote has no branch '{default_branch}' to refresh from",
|
|
604
|
+
)
|
|
544
605
|
elif isinstance(base_outcome, FetchFailed):
|
|
545
606
|
# Same condition as the arm below, warned the same
|
|
546
607
|
# way: the new branch is about to be cut from a
|
|
@@ -550,13 +611,23 @@ class WorkspaceCloneManager:
|
|
|
550
611
|
f"Could not fetch {default_branch} for {owner}/{repo}: "
|
|
551
612
|
f"{base_outcome.reason}"
|
|
552
613
|
)
|
|
614
|
+
base = StaleBase(base=default_branch, reason=base_outcome.reason)
|
|
553
615
|
else:
|
|
554
616
|
unhandled_fetch_outcome(base_outcome)
|
|
617
|
+
else:
|
|
618
|
+
# No default branch could even be named, so nothing was
|
|
619
|
+
# fetched and the creation below starts from the bare cache's
|
|
620
|
+
# own HEAD -- a ref of unbounded age, and the report says so.
|
|
621
|
+
base = StaleBase(
|
|
622
|
+
base="HEAD",
|
|
623
|
+
reason=default_branch_error or "no default branch is recorded",
|
|
624
|
+
)
|
|
555
625
|
elif isinstance(outcome, FetchFailed):
|
|
556
626
|
# Not an error here: a cached branch still launches. Deliberately
|
|
557
627
|
# no default-branch fetch -- nothing was learned about the remote,
|
|
558
628
|
# so nothing licenses treating this branch as new.
|
|
559
629
|
logger.warning(f"Could not fetch {branch} for {owner}/{repo}: {outcome.reason}")
|
|
630
|
+
base = StaleBase(base=branch, reason=outcome.reason)
|
|
560
631
|
else:
|
|
561
632
|
unhandled_fetch_outcome(outcome)
|
|
562
633
|
|
|
@@ -567,6 +638,7 @@ class WorkspaceCloneManager:
|
|
|
567
638
|
start_point=default_branch or "HEAD",
|
|
568
639
|
use_local_refs=True,
|
|
569
640
|
)
|
|
641
|
+
return base
|
|
570
642
|
|
|
571
643
|
def _prepare_workspace(
|
|
572
644
|
self,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|