devlyn-cli 4.3.12 → 4.3.14
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/bin/cleanup-history.json +499 -1
- package/bin/devlyn.js +240 -36
- package/bin/skill-history.json +3 -0
- package/config/skills/_shared/task-complete.py +81 -19
- package/config/skills/_shared/task-completion.md +3 -1
- package/native/claude/.claude-plugin/plugin.json +1 -1
- package/native/claude/skills/_shared/task-complete.py +81 -19
- package/native/claude/skills/_shared/task-completion.md +3 -1
- package/native/codex/.agents/plugins/marketplace.json +1 -1
- package/native/codex/plugins/devlyn/.codex-plugin/plugin.json +1 -1
- package/native/codex/plugins/devlyn/skills/_shared/task-complete.py +81 -19
- package/native/codex/plugins/devlyn/skills/_shared/task-completion.md +3 -1
- package/node_modules/jsonc-parser/CHANGELOG.md +76 -0
- package/node_modules/jsonc-parser/LICENSE.md +21 -0
- package/node_modules/jsonc-parser/README.md +364 -0
- package/node_modules/jsonc-parser/SECURITY.md +41 -0
- package/node_modules/jsonc-parser/lib/esm/impl/edit.js +185 -0
- package/node_modules/jsonc-parser/lib/esm/impl/format.js +261 -0
- package/node_modules/jsonc-parser/lib/esm/impl/parser.js +659 -0
- package/node_modules/jsonc-parser/lib/esm/impl/scanner.js +443 -0
- package/node_modules/jsonc-parser/lib/esm/impl/string-intern.js +29 -0
- package/node_modules/jsonc-parser/lib/esm/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/esm/main.js +178 -0
- package/node_modules/jsonc-parser/lib/umd/impl/edit.js +201 -0
- package/node_modules/jsonc-parser/lib/umd/impl/format.js +275 -0
- package/node_modules/jsonc-parser/lib/umd/impl/parser.js +682 -0
- package/node_modules/jsonc-parser/lib/umd/impl/scanner.js +456 -0
- package/node_modules/jsonc-parser/lib/umd/impl/string-intern.js +42 -0
- package/node_modules/jsonc-parser/lib/umd/main.d.ts +351 -0
- package/node_modules/jsonc-parser/lib/umd/main.js +194 -0
- package/node_modules/jsonc-parser/package.json +37 -0
- package/package.json +3 -1
- package/release.json +110 -15
|
@@ -324,10 +324,12 @@ def allocate(args, *, exclude_receipts=(), warnings=None):
|
|
|
324
324
|
if local:
|
|
325
325
|
receipt["local_only"] = True # Local work needs no remote: nothing is fetched or pushed.
|
|
326
326
|
else:
|
|
327
|
-
|
|
327
|
+
if args.repository is None:
|
|
328
|
+
receipt["repository"] = repository_from_url(gref(receipt, "remote", "get-url", "--all", args.remote))
|
|
328
329
|
policy(receipt, None)
|
|
329
330
|
receipt["remote_url"] = remote_url(receipt)
|
|
330
331
|
require("\n" not in receipt["remote_url"]["push"] and receipt["remote_url"]["push"] == receipt["remote_url"]["fetch"], "split/multiple remote URLs are unsupported")
|
|
332
|
+
repo_policy(receipt, allocating=True)
|
|
331
333
|
target = Path(args.worktree).resolve() # A relative, `..` or symlinked (macOS /tmp) path names its real location.
|
|
332
334
|
require(not target.exists() and all(not target.is_relative_to(p) and not p.is_relative_to(target) for p in map(Path, registrations(receipt))), "linked worktree must be an absent path disjoint from every registered worktree")
|
|
333
335
|
receipt["worktree"] = str(target)
|
|
@@ -541,9 +543,17 @@ def repository_info(repository):
|
|
|
541
543
|
return info
|
|
542
544
|
|
|
543
545
|
|
|
544
|
-
def repo_policy(receipt):
|
|
546
|
+
def repo_policy(receipt, *, allocating=False):
|
|
545
547
|
info = repository_info(receipt["repository"])
|
|
546
|
-
|
|
548
|
+
default = info["defaultBranchRef"]["name"]
|
|
549
|
+
if allocating:
|
|
550
|
+
if receipt["base"] is None:
|
|
551
|
+
gref(receipt, "check-ref-format", "refs/heads/" + default)
|
|
552
|
+
receipt["base"] = default
|
|
553
|
+
require(receipt["branch"] != receipt["base"], "cannot own a base/default branch")
|
|
554
|
+
require(default == receipt["base"] and receipt["branch"] != default,
|
|
555
|
+
f"base {receipt['base']!r} differs from default branch {default!r}; the helper delivers only to the default branch; use a local allocation with --local-base for other bases"
|
|
556
|
+
if allocating else "base/default branch changed; retain resources")
|
|
547
557
|
return info
|
|
548
558
|
|
|
549
559
|
|
|
@@ -1409,7 +1419,8 @@ class CompletionTests(unittest.TestCase):
|
|
|
1409
1419
|
from unittest.mock import patch
|
|
1410
1420
|
target = self.root / "absent" / "new"
|
|
1411
1421
|
exists = Path.exists
|
|
1412
|
-
with patch.
|
|
1422
|
+
with patch.dict(os.environ, self.env), \
|
|
1423
|
+
patch.object(Path, "exists", lambda path: False if path == target or path in target.parents else exists(path)), \
|
|
1413
1424
|
patch.object(shutil, "disk_usage") as usage:
|
|
1414
1425
|
with self.assertRaisesRegex(CompletionError, f"no existing ancestor of {re.escape(str(target))} to measure free space"):
|
|
1415
1426
|
allocate(self.allocation_args("new", target))
|
|
@@ -1677,6 +1688,62 @@ class CompletionTests(unittest.TestCase):
|
|
|
1677
1688
|
self.g("--no-literal-pathspecs", "check-ignore", "-q", "--", record, work=self.task)
|
|
1678
1689
|
self.assertEqual(self.g("status", "--porcelain", "--untracked-files=all", work=self.task), "")
|
|
1679
1690
|
|
|
1691
|
+
def test_allocate_derives_repository(self):
|
|
1692
|
+
result, _ = self.cli("allocate", "--repo", self.work, "--task", "fixture",
|
|
1693
|
+
"--branch", "task/fixture", "--base", "main", "--worktree", self.root / "linked")
|
|
1694
|
+
self.assertEqual(result["status"], "ALLOCATED")
|
|
1695
|
+
self.assertEqual(read_json(Path(result["receipt"]))["repository"], "test/project")
|
|
1696
|
+
|
|
1697
|
+
def test_allocate_derives_default_base(self):
|
|
1698
|
+
self.g("push", "origin", "main:refs/heads/release")
|
|
1699
|
+
self.configure(default_branch="release")
|
|
1700
|
+
for name, flags in (("explicit", ["--repository", "test/project"]), ("derived", [])):
|
|
1701
|
+
with self.subTest(name=name):
|
|
1702
|
+
result, _ = self.cli("allocate", "--repo", self.work, "--task", name,
|
|
1703
|
+
"--branch", "task/" + name, "--worktree", self.root / name, *flags)
|
|
1704
|
+
self.assertEqual(result["status"], "ALLOCATED")
|
|
1705
|
+
receipt = read_json(Path(result["receipt"]))
|
|
1706
|
+
self.assertEqual((receipt["repository"], receipt["base"]), ("test/project", "release"))
|
|
1707
|
+
self.assertEqual(self.g("rev-parse", "HEAD", work=Path(result["worktree"])), receipt["baseline"])
|
|
1708
|
+
|
|
1709
|
+
def test_allocate_rejects_nondefault_base_before_resources(self):
|
|
1710
|
+
self.configure(default_branch="release")
|
|
1711
|
+
self.g("update-ref", "-d", "refs/remotes/origin/main")
|
|
1712
|
+
refs, worktrees = self.g("show-ref"), self.g("worktree", "list", "--porcelain")
|
|
1713
|
+
result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
|
|
1714
|
+
"--branch", "task/fixture", "--repository", "test/project", "--base", "main",
|
|
1715
|
+
"--worktree", self.root / "linked", success=False)
|
|
1716
|
+
self.assertNotEqual(process.returncode, 0)
|
|
1717
|
+
self.assertEqual(result["status"], "BLOCKED")
|
|
1718
|
+
for text in ("main", "release", "only to the default branch", "--local-base"):
|
|
1719
|
+
self.assertIn(text, result["reason"])
|
|
1720
|
+
self.assertEqual(self.g("show-ref"), refs)
|
|
1721
|
+
self.assertEqual(self.g("worktree", "list", "--porcelain"), worktrees)
|
|
1722
|
+
self.assertFalse((self.work / ".git/devlyn-completion").exists())
|
|
1723
|
+
self.assertFalse((self.root / "linked").exists())
|
|
1724
|
+
|
|
1725
|
+
def test_allocate_rejects_derived_base_as_task_branch(self):
|
|
1726
|
+
self.configure(default_branch="release")
|
|
1727
|
+
refs = self.g("show-ref")
|
|
1728
|
+
result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
|
|
1729
|
+
"--branch", "release", "--worktree", self.root / "linked", success=False)
|
|
1730
|
+
self.assertNotEqual(process.returncode, 0)
|
|
1731
|
+
self.assertEqual(result["status"], "BLOCKED")
|
|
1732
|
+
self.assertIn("cannot own a base/default branch", result["reason"])
|
|
1733
|
+
self.assertEqual(self.g("show-ref"), refs)
|
|
1734
|
+
self.assertEqual(self.g("branch", "--list", "release"), "")
|
|
1735
|
+
self.assertFalse((self.work / ".git/devlyn-completion").exists())
|
|
1736
|
+
self.assertFalse((self.root / "linked").exists())
|
|
1737
|
+
|
|
1738
|
+
def test_allocate_rejects_explicit_repository_mismatch(self):
|
|
1739
|
+
result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
|
|
1740
|
+
"--branch", "task/fixture", "--repository", "test/other", "--base", "main",
|
|
1741
|
+
"--worktree", self.root / "linked", success=False)
|
|
1742
|
+
self.assertNotEqual(process.returncode, 0)
|
|
1743
|
+
self.assertEqual(result, {"status": "BLOCKED", "reason": "repository differs from remote"})
|
|
1744
|
+
self.assertFalse((self.work / ".git/devlyn-completion").exists())
|
|
1745
|
+
self.assertFalse((self.root / "linked").exists())
|
|
1746
|
+
|
|
1680
1747
|
def test_allocate_base_with_same_named_tag(self):
|
|
1681
1748
|
self.g("tag", "main")
|
|
1682
1749
|
result = self.allocate()
|
|
@@ -1687,6 +1754,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
1687
1754
|
base = "main\u00a0"
|
|
1688
1755
|
self.g("switch", "-c", base)
|
|
1689
1756
|
self.g("push", "origin", "HEAD:refs/heads/"+base)
|
|
1757
|
+
self.configure(default_branch=base)
|
|
1690
1758
|
result = self.allocate(base=base)
|
|
1691
1759
|
self.assertEqual(result["status"], "ALLOCATED")
|
|
1692
1760
|
self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["base"], base)
|
|
@@ -1711,6 +1779,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
1711
1779
|
def test_allocate_rejects_short_base_alias(self):
|
|
1712
1780
|
self.g("branch", "heads/main")
|
|
1713
1781
|
self.g("tag", "main")
|
|
1782
|
+
self.configure(default_branch="heads/main")
|
|
1714
1783
|
self.assert_base_identity_rejected("main", "heads/main")
|
|
1715
1784
|
|
|
1716
1785
|
def test_allocate_uses_remote_base_with_dirty_foreign_anchor(self):
|
|
@@ -2295,7 +2364,8 @@ ctypes.CDLL(None).pthread_exit(None)
|
|
|
2295
2364
|
self.merge_pr()
|
|
2296
2365
|
receipt, task = self.receipt, self.task
|
|
2297
2366
|
self.configure(default_branch="changed")
|
|
2298
|
-
|
|
2367
|
+
self.g("push", "origin", "main:refs/heads/changed")
|
|
2368
|
+
result = self.allocate("next", base="changed")
|
|
2299
2369
|
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "RETAINED")])
|
|
2300
2370
|
self.assertIn("base/default branch changed", result["reconciled"][0]["reason"])
|
|
2301
2371
|
self.assertTrue(task.exists())
|
|
@@ -2398,12 +2468,13 @@ ctypes.CDLL(None).pthread_exit(None)
|
|
|
2398
2468
|
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
2399
2469
|
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
2400
2470
|
self.configure(default_branch="changed")
|
|
2471
|
+
self.g("push", "origin", "main:refs/heads/changed")
|
|
2401
2472
|
server = json.loads(self.data.read_text())
|
|
2402
|
-
result = self.allocate("retry")
|
|
2473
|
+
result = self.allocate("retry", base="changed")
|
|
2403
2474
|
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
|
|
2404
2475
|
self.assertFalse(any(scratch.iterdir()))
|
|
2405
2476
|
self.assertEqual(json.loads(self.data.read_text()), server)
|
|
2406
|
-
self.assertEqual(self.allocate("later")["reconciled"], [])
|
|
2477
|
+
self.assertEqual(self.allocate("later", base="changed")["reconciled"], [])
|
|
2407
2478
|
|
|
2408
2479
|
def test_reconcile_retries_unattempted_scratch_cleanup(self):
|
|
2409
2480
|
scratch = Path(self.allocate()["scratch"])
|
|
@@ -3158,14 +3229,10 @@ ctypes.CDLL(None).pthread_exit(None)
|
|
|
3158
3229
|
self.assertFalse((self.root / "next").exists())
|
|
3159
3230
|
|
|
3160
3231
|
def test_local_allocation_needs_no_repository(self):
|
|
3161
|
-
"""
|
|
3162
|
-
local allocation, here beside an origin that is not GitHub, is ALLOCATED and its local-only completion returns
|
|
3163
|
-
LOCAL_ONLY with nothing pushed. Before: argparse required --repository, so both failed as an argparse error."""
|
|
3232
|
+
"""A local allocation beside a non-GitHub origin needs no repository and completes without publication."""
|
|
3164
3233
|
args = ["allocate", "--repo", self.work, "--base", "main"]
|
|
3165
|
-
refused, _ = self.cli(*args, "--task", "next", "--branch", "task/next", "--worktree", self.root / "next", success=False)
|
|
3166
|
-
self.assertIn("--repository", refused["reason"])
|
|
3167
|
-
self.assertEqual((self.g("branch", "--list", "task/next"), (self.root / "next").exists()), ("", False))
|
|
3168
3234
|
self.g("remote", "set-url", "origin", "https://gitlab.com/team/project.git")
|
|
3235
|
+
(self.bin / "gh").write_text("#!/bin/sh\nexit 1\n", encoding="utf-8")
|
|
3169
3236
|
result, _ = self.cli(*args, "--task", "fixture", "--branch", "task/fixture", "--worktree", self.root / "linked",
|
|
3170
3237
|
"--local-base", self.g("rev-parse", "HEAD"))
|
|
3171
3238
|
self.assertEqual(result["status"], "ALLOCATED")
|
|
@@ -3175,13 +3242,8 @@ ctypes.CDLL(None).pthread_exit(None)
|
|
|
3175
3242
|
self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs", 0), 0)
|
|
3176
3243
|
|
|
3177
3244
|
def test_local_allocation_needs_no_base(self):
|
|
3178
|
-
"""
|
|
3179
|
-
runs, is ALLOCATED and completes LOCAL_ONLY, while a remote allocation without --base is refused naming it, with
|
|
3180
|
-
nothing created."""
|
|
3245
|
+
"""A local allocation with --local-base and no --base is ALLOCATED and completes LOCAL_ONLY."""
|
|
3181
3246
|
args = ["allocate", "--repo", self.work, "--task", "fixture", "--branch", "task/fixture", "--worktree", self.root / "linked"]
|
|
3182
|
-
refused, _ = self.cli(*args, "--repository", "test/project", success=False)
|
|
3183
|
-
self.assertIn("--base", refused["reason"])
|
|
3184
|
-
self.assertEqual((self.g("branch", "--list", "task/fixture"), (self.root / "linked").exists()), ("", False))
|
|
3185
3247
|
result, _ = self.cli(*args, "--local-base", self.g("rev-parse", "HEAD"))
|
|
3186
3248
|
self.assertEqual(result["status"], "ALLOCATED")
|
|
3187
3249
|
self.receipt, self.task = Path(result["receipt"]), Path(result["worktree"])
|
|
@@ -55,9 +55,11 @@ not write to the shared checkout.
|
|
|
55
55
|
python3 "$DEVLYN_SHARED_DIR/task-complete.py" allocate --repo . \
|
|
56
56
|
--task '<task identity>' --branch '<absent task branch>' \
|
|
57
57
|
--worktree '<repo parent>/.devlyn-worktrees/<repo name>/<task directory>' \
|
|
58
|
-
--
|
|
58
|
+
--remote origin
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
The repository comes from the remote's GitHub URL and the base from the repository's default branch; passing either flag checks it instead.
|
|
62
|
+
A non-default base is refused at allocation.
|
|
61
63
|
Every delivery allocation owns a linked worktree; `--worktree` is required and
|
|
62
64
|
must name an absent path.
|
|
63
65
|
Cleanup removes empty parents through the exact per-repo root shown above or its legacy `.devlyn.noindex`/`.devlyn` root, keeping the shared `.devlyn-worktrees` container. Allocation also sweeps empty legacy roots for this repo.
|
|
@@ -324,10 +324,12 @@ def allocate(args, *, exclude_receipts=(), warnings=None):
|
|
|
324
324
|
if local:
|
|
325
325
|
receipt["local_only"] = True # Local work needs no remote: nothing is fetched or pushed.
|
|
326
326
|
else:
|
|
327
|
-
|
|
327
|
+
if args.repository is None:
|
|
328
|
+
receipt["repository"] = repository_from_url(gref(receipt, "remote", "get-url", "--all", args.remote))
|
|
328
329
|
policy(receipt, None)
|
|
329
330
|
receipt["remote_url"] = remote_url(receipt)
|
|
330
331
|
require("\n" not in receipt["remote_url"]["push"] and receipt["remote_url"]["push"] == receipt["remote_url"]["fetch"], "split/multiple remote URLs are unsupported")
|
|
332
|
+
repo_policy(receipt, allocating=True)
|
|
331
333
|
target = Path(args.worktree).resolve() # A relative, `..` or symlinked (macOS /tmp) path names its real location.
|
|
332
334
|
require(not target.exists() and all(not target.is_relative_to(p) and not p.is_relative_to(target) for p in map(Path, registrations(receipt))), "linked worktree must be an absent path disjoint from every registered worktree")
|
|
333
335
|
receipt["worktree"] = str(target)
|
|
@@ -541,9 +543,17 @@ def repository_info(repository):
|
|
|
541
543
|
return info
|
|
542
544
|
|
|
543
545
|
|
|
544
|
-
def repo_policy(receipt):
|
|
546
|
+
def repo_policy(receipt, *, allocating=False):
|
|
545
547
|
info = repository_info(receipt["repository"])
|
|
546
|
-
|
|
548
|
+
default = info["defaultBranchRef"]["name"]
|
|
549
|
+
if allocating:
|
|
550
|
+
if receipt["base"] is None:
|
|
551
|
+
gref(receipt, "check-ref-format", "refs/heads/" + default)
|
|
552
|
+
receipt["base"] = default
|
|
553
|
+
require(receipt["branch"] != receipt["base"], "cannot own a base/default branch")
|
|
554
|
+
require(default == receipt["base"] and receipt["branch"] != default,
|
|
555
|
+
f"base {receipt['base']!r} differs from default branch {default!r}; the helper delivers only to the default branch; use a local allocation with --local-base for other bases"
|
|
556
|
+
if allocating else "base/default branch changed; retain resources")
|
|
547
557
|
return info
|
|
548
558
|
|
|
549
559
|
|
|
@@ -1409,7 +1419,8 @@ class CompletionTests(unittest.TestCase):
|
|
|
1409
1419
|
from unittest.mock import patch
|
|
1410
1420
|
target = self.root / "absent" / "new"
|
|
1411
1421
|
exists = Path.exists
|
|
1412
|
-
with patch.
|
|
1422
|
+
with patch.dict(os.environ, self.env), \
|
|
1423
|
+
patch.object(Path, "exists", lambda path: False if path == target or path in target.parents else exists(path)), \
|
|
1413
1424
|
patch.object(shutil, "disk_usage") as usage:
|
|
1414
1425
|
with self.assertRaisesRegex(CompletionError, f"no existing ancestor of {re.escape(str(target))} to measure free space"):
|
|
1415
1426
|
allocate(self.allocation_args("new", target))
|
|
@@ -1677,6 +1688,62 @@ class CompletionTests(unittest.TestCase):
|
|
|
1677
1688
|
self.g("--no-literal-pathspecs", "check-ignore", "-q", "--", record, work=self.task)
|
|
1678
1689
|
self.assertEqual(self.g("status", "--porcelain", "--untracked-files=all", work=self.task), "")
|
|
1679
1690
|
|
|
1691
|
+
def test_allocate_derives_repository(self):
|
|
1692
|
+
result, _ = self.cli("allocate", "--repo", self.work, "--task", "fixture",
|
|
1693
|
+
"--branch", "task/fixture", "--base", "main", "--worktree", self.root / "linked")
|
|
1694
|
+
self.assertEqual(result["status"], "ALLOCATED")
|
|
1695
|
+
self.assertEqual(read_json(Path(result["receipt"]))["repository"], "test/project")
|
|
1696
|
+
|
|
1697
|
+
def test_allocate_derives_default_base(self):
|
|
1698
|
+
self.g("push", "origin", "main:refs/heads/release")
|
|
1699
|
+
self.configure(default_branch="release")
|
|
1700
|
+
for name, flags in (("explicit", ["--repository", "test/project"]), ("derived", [])):
|
|
1701
|
+
with self.subTest(name=name):
|
|
1702
|
+
result, _ = self.cli("allocate", "--repo", self.work, "--task", name,
|
|
1703
|
+
"--branch", "task/" + name, "--worktree", self.root / name, *flags)
|
|
1704
|
+
self.assertEqual(result["status"], "ALLOCATED")
|
|
1705
|
+
receipt = read_json(Path(result["receipt"]))
|
|
1706
|
+
self.assertEqual((receipt["repository"], receipt["base"]), ("test/project", "release"))
|
|
1707
|
+
self.assertEqual(self.g("rev-parse", "HEAD", work=Path(result["worktree"])), receipt["baseline"])
|
|
1708
|
+
|
|
1709
|
+
def test_allocate_rejects_nondefault_base_before_resources(self):
|
|
1710
|
+
self.configure(default_branch="release")
|
|
1711
|
+
self.g("update-ref", "-d", "refs/remotes/origin/main")
|
|
1712
|
+
refs, worktrees = self.g("show-ref"), self.g("worktree", "list", "--porcelain")
|
|
1713
|
+
result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
|
|
1714
|
+
"--branch", "task/fixture", "--repository", "test/project", "--base", "main",
|
|
1715
|
+
"--worktree", self.root / "linked", success=False)
|
|
1716
|
+
self.assertNotEqual(process.returncode, 0)
|
|
1717
|
+
self.assertEqual(result["status"], "BLOCKED")
|
|
1718
|
+
for text in ("main", "release", "only to the default branch", "--local-base"):
|
|
1719
|
+
self.assertIn(text, result["reason"])
|
|
1720
|
+
self.assertEqual(self.g("show-ref"), refs)
|
|
1721
|
+
self.assertEqual(self.g("worktree", "list", "--porcelain"), worktrees)
|
|
1722
|
+
self.assertFalse((self.work / ".git/devlyn-completion").exists())
|
|
1723
|
+
self.assertFalse((self.root / "linked").exists())
|
|
1724
|
+
|
|
1725
|
+
def test_allocate_rejects_derived_base_as_task_branch(self):
|
|
1726
|
+
self.configure(default_branch="release")
|
|
1727
|
+
refs = self.g("show-ref")
|
|
1728
|
+
result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
|
|
1729
|
+
"--branch", "release", "--worktree", self.root / "linked", success=False)
|
|
1730
|
+
self.assertNotEqual(process.returncode, 0)
|
|
1731
|
+
self.assertEqual(result["status"], "BLOCKED")
|
|
1732
|
+
self.assertIn("cannot own a base/default branch", result["reason"])
|
|
1733
|
+
self.assertEqual(self.g("show-ref"), refs)
|
|
1734
|
+
self.assertEqual(self.g("branch", "--list", "release"), "")
|
|
1735
|
+
self.assertFalse((self.work / ".git/devlyn-completion").exists())
|
|
1736
|
+
self.assertFalse((self.root / "linked").exists())
|
|
1737
|
+
|
|
1738
|
+
def test_allocate_rejects_explicit_repository_mismatch(self):
|
|
1739
|
+
result, process = self.cli("allocate", "--repo", self.work, "--task", "fixture",
|
|
1740
|
+
"--branch", "task/fixture", "--repository", "test/other", "--base", "main",
|
|
1741
|
+
"--worktree", self.root / "linked", success=False)
|
|
1742
|
+
self.assertNotEqual(process.returncode, 0)
|
|
1743
|
+
self.assertEqual(result, {"status": "BLOCKED", "reason": "repository differs from remote"})
|
|
1744
|
+
self.assertFalse((self.work / ".git/devlyn-completion").exists())
|
|
1745
|
+
self.assertFalse((self.root / "linked").exists())
|
|
1746
|
+
|
|
1680
1747
|
def test_allocate_base_with_same_named_tag(self):
|
|
1681
1748
|
self.g("tag", "main")
|
|
1682
1749
|
result = self.allocate()
|
|
@@ -1687,6 +1754,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
1687
1754
|
base = "main\u00a0"
|
|
1688
1755
|
self.g("switch", "-c", base)
|
|
1689
1756
|
self.g("push", "origin", "HEAD:refs/heads/"+base)
|
|
1757
|
+
self.configure(default_branch=base)
|
|
1690
1758
|
result = self.allocate(base=base)
|
|
1691
1759
|
self.assertEqual(result["status"], "ALLOCATED")
|
|
1692
1760
|
self.assertEqual(json.loads(self.receipt.read_text(encoding="utf-8"))["base"], base)
|
|
@@ -1711,6 +1779,7 @@ class CompletionTests(unittest.TestCase):
|
|
|
1711
1779
|
def test_allocate_rejects_short_base_alias(self):
|
|
1712
1780
|
self.g("branch", "heads/main")
|
|
1713
1781
|
self.g("tag", "main")
|
|
1782
|
+
self.configure(default_branch="heads/main")
|
|
1714
1783
|
self.assert_base_identity_rejected("main", "heads/main")
|
|
1715
1784
|
|
|
1716
1785
|
def test_allocate_uses_remote_base_with_dirty_foreign_anchor(self):
|
|
@@ -2295,7 +2364,8 @@ ctypes.CDLL(None).pthread_exit(None)
|
|
|
2295
2364
|
self.merge_pr()
|
|
2296
2365
|
receipt, task = self.receipt, self.task
|
|
2297
2366
|
self.configure(default_branch="changed")
|
|
2298
|
-
|
|
2367
|
+
self.g("push", "origin", "main:refs/heads/changed")
|
|
2368
|
+
result = self.allocate("next", base="changed")
|
|
2299
2369
|
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "RETAINED")])
|
|
2300
2370
|
self.assertIn("base/default branch changed", result["reconciled"][0]["reason"])
|
|
2301
2371
|
self.assertTrue(task.exists())
|
|
@@ -2398,12 +2468,13 @@ ctypes.CDLL(None).pthread_exit(None)
|
|
|
2398
2468
|
self.assertEqual(self.g("branch", "--list", "task/fixture"), "")
|
|
2399
2469
|
self.assertEqual(self.g("ls-remote", "origin", "refs/heads/task/fixture"), "")
|
|
2400
2470
|
self.configure(default_branch="changed")
|
|
2471
|
+
self.g("push", "origin", "main:refs/heads/changed")
|
|
2401
2472
|
server = json.loads(self.data.read_text())
|
|
2402
|
-
result = self.allocate("retry")
|
|
2473
|
+
result = self.allocate("retry", base="changed")
|
|
2403
2474
|
self.assertEqual([(row["receipt"], row["status"]) for row in result["reconciled"]], [(str(receipt), "COMPLETE")])
|
|
2404
2475
|
self.assertFalse(any(scratch.iterdir()))
|
|
2405
2476
|
self.assertEqual(json.loads(self.data.read_text()), server)
|
|
2406
|
-
self.assertEqual(self.allocate("later")["reconciled"], [])
|
|
2477
|
+
self.assertEqual(self.allocate("later", base="changed")["reconciled"], [])
|
|
2407
2478
|
|
|
2408
2479
|
def test_reconcile_retries_unattempted_scratch_cleanup(self):
|
|
2409
2480
|
scratch = Path(self.allocate()["scratch"])
|
|
@@ -3158,14 +3229,10 @@ ctypes.CDLL(None).pthread_exit(None)
|
|
|
3158
3229
|
self.assertFalse((self.root / "next").exists())
|
|
3159
3230
|
|
|
3160
3231
|
def test_local_allocation_needs_no_repository(self):
|
|
3161
|
-
"""
|
|
3162
|
-
local allocation, here beside an origin that is not GitHub, is ALLOCATED and its local-only completion returns
|
|
3163
|
-
LOCAL_ONLY with nothing pushed. Before: argparse required --repository, so both failed as an argparse error."""
|
|
3232
|
+
"""A local allocation beside a non-GitHub origin needs no repository and completes without publication."""
|
|
3164
3233
|
args = ["allocate", "--repo", self.work, "--base", "main"]
|
|
3165
|
-
refused, _ = self.cli(*args, "--task", "next", "--branch", "task/next", "--worktree", self.root / "next", success=False)
|
|
3166
|
-
self.assertIn("--repository", refused["reason"])
|
|
3167
|
-
self.assertEqual((self.g("branch", "--list", "task/next"), (self.root / "next").exists()), ("", False))
|
|
3168
3234
|
self.g("remote", "set-url", "origin", "https://gitlab.com/team/project.git")
|
|
3235
|
+
(self.bin / "gh").write_text("#!/bin/sh\nexit 1\n", encoding="utf-8")
|
|
3169
3236
|
result, _ = self.cli(*args, "--task", "fixture", "--branch", "task/fixture", "--worktree", self.root / "linked",
|
|
3170
3237
|
"--local-base", self.g("rev-parse", "HEAD"))
|
|
3171
3238
|
self.assertEqual(result["status"], "ALLOCATED")
|
|
@@ -3175,13 +3242,8 @@ ctypes.CDLL(None).pthread_exit(None)
|
|
|
3175
3242
|
self.assertEqual(json.loads(self.data.read_text(encoding="utf-8")).get("pushs", 0), 0)
|
|
3176
3243
|
|
|
3177
3244
|
def test_local_allocation_needs_no_base(self):
|
|
3178
|
-
"""
|
|
3179
|
-
runs, is ALLOCATED and completes LOCAL_ONLY, while a remote allocation without --base is refused naming it, with
|
|
3180
|
-
nothing created."""
|
|
3245
|
+
"""A local allocation with --local-base and no --base is ALLOCATED and completes LOCAL_ONLY."""
|
|
3181
3246
|
args = ["allocate", "--repo", self.work, "--task", "fixture", "--branch", "task/fixture", "--worktree", self.root / "linked"]
|
|
3182
|
-
refused, _ = self.cli(*args, "--repository", "test/project", success=False)
|
|
3183
|
-
self.assertIn("--base", refused["reason"])
|
|
3184
|
-
self.assertEqual((self.g("branch", "--list", "task/fixture"), (self.root / "linked").exists()), ("", False))
|
|
3185
3247
|
result, _ = self.cli(*args, "--local-base", self.g("rev-parse", "HEAD"))
|
|
3186
3248
|
self.assertEqual(result["status"], "ALLOCATED")
|
|
3187
3249
|
self.receipt, self.task = Path(result["receipt"]), Path(result["worktree"])
|
|
@@ -55,9 +55,11 @@ not write to the shared checkout.
|
|
|
55
55
|
python3 "$DEVLYN_SHARED_DIR/task-complete.py" allocate --repo . \
|
|
56
56
|
--task '<task identity>' --branch '<absent task branch>' \
|
|
57
57
|
--worktree '<repo parent>/.devlyn-worktrees/<repo name>/<task directory>' \
|
|
58
|
-
--
|
|
58
|
+
--remote origin
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
The repository comes from the remote's GitHub URL and the base from the repository's default branch; passing either flag checks it instead.
|
|
62
|
+
A non-default base is refused at allocation.
|
|
61
63
|
Every delivery allocation owns a linked worktree; `--worktree` is required and
|
|
62
64
|
must name an absent path.
|
|
63
65
|
Cleanup removes empty parents through the exact per-repo root shown above or its legacy `.devlyn.noindex`/`.devlyn` root, keeping the shared `.devlyn-worktrees` container. Allocation also sweeps empty legacy roots for this repo.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
3.3.0 2022-06-24
|
|
2
|
+
=================
|
|
3
|
+
- `JSONVisitor.onObjectBegin` and `JSONVisitor.onArrayBegin` can now return `false` to instruct the visitor that no children should be visited.
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
3.2.0 2022-08-30
|
|
7
|
+
=================
|
|
8
|
+
- update the version of the bundled Javascript files to `es2020`.
|
|
9
|
+
- include all `const enum` values in the bundled JavaScript files (`ScanError`, `SyntaxKind`, `ParseErrorCode`).
|
|
10
|
+
|
|
11
|
+
3.1.0 2022-07-07
|
|
12
|
+
==================
|
|
13
|
+
* added new API `FormattingOptions.keepLines` : It leaves the initial line positions in the formatting.
|
|
14
|
+
|
|
15
|
+
3.0.0 2020-11-13
|
|
16
|
+
==================
|
|
17
|
+
* fixed API spec for `parseTree`. Can return `undefine` for empty input.
|
|
18
|
+
* added new API `FormattingOptions.insertFinalNewline`.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
2.3.0 2020-07-03
|
|
22
|
+
==================
|
|
23
|
+
* new API `ModificationOptions.isArrayInsertion`: If `JSONPath` refers to an index of an array and `isArrayInsertion` is `true`, then `modify` will insert a new item at that location instead of overwriting its contents.
|
|
24
|
+
* `ModificationOptions.formattingOptions` is now optional. If not set, newly inserted content will not be formatted.
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
2.2.0 2019-10-25
|
|
28
|
+
==================
|
|
29
|
+
* added `ParseOptions.allowEmptyContent`. Default is `false`.
|
|
30
|
+
* new API `getNodeType`: Returns the type of a value returned by parse.
|
|
31
|
+
* `parse`: Fix issue with empty property name
|
|
32
|
+
|
|
33
|
+
2.1.0 2019-03-29
|
|
34
|
+
==================
|
|
35
|
+
* `JSONScanner` and `JSONVisitor` return lineNumber / character.
|
|
36
|
+
|
|
37
|
+
2.0.0 2018-04-12
|
|
38
|
+
==================
|
|
39
|
+
* renamed `Node.columnOffset` to `Node.colonOffset`
|
|
40
|
+
* new API `getNodePath`: Gets the JSON path of the given JSON DOM node
|
|
41
|
+
* new API `findNodeAtOffset`: Finds the most inner node at the given offset. If `includeRightBound` is set, also finds nodes that end at the given offset.
|
|
42
|
+
|
|
43
|
+
1.0.3 2018-03-07
|
|
44
|
+
==================
|
|
45
|
+
* provide ems modules
|
|
46
|
+
|
|
47
|
+
1.0.2 2018-03-05
|
|
48
|
+
==================
|
|
49
|
+
* added the `visit.onComment` API, reported when comments are allowed.
|
|
50
|
+
* added the `ParseErrorCode.InvalidCommentToken` enum value, reported when comments are disallowed.
|
|
51
|
+
|
|
52
|
+
1.0.1
|
|
53
|
+
==================
|
|
54
|
+
* added the `format` API: computes edits to format a JSON document.
|
|
55
|
+
* added the `modify` API: computes edits to insert, remove or replace a property or value in a JSON document.
|
|
56
|
+
* added the `allyEdits` API: applies edits to a document
|
|
57
|
+
|
|
58
|
+
1.0.0
|
|
59
|
+
==================
|
|
60
|
+
* remove nls dependency (remove `getParseErrorMessage`)
|
|
61
|
+
|
|
62
|
+
0.4.2 / 2017-05-05
|
|
63
|
+
==================
|
|
64
|
+
* added `ParseError.offset` & `ParseError.length`
|
|
65
|
+
|
|
66
|
+
0.4.1 / 2017-04-02
|
|
67
|
+
==================
|
|
68
|
+
* added `ParseOptions.allowTrailingComma`
|
|
69
|
+
|
|
70
|
+
0.4.0 / 2017-02-23
|
|
71
|
+
==================
|
|
72
|
+
* fix for `getLocation`. Now `getLocation` inside an object will always return a property from inside that property. Can be empty string if the object has no properties or if the offset is before a actual property `{ "a": { | }} will return location ['a', ' ']`
|
|
73
|
+
|
|
74
|
+
0.3.0 / 2017-01-17
|
|
75
|
+
==================
|
|
76
|
+
* Updating to typescript 2.0
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) Microsoft
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|