From bb5f06e62a8511da48e750024cb7cbc5b02c53e6 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 30 Jul 2026 16:13:56 +0200 Subject: [PATCH 1/3] Bump Ark to posit-dev/ark#1369 --- extensions/positron-r/ark | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/positron-r/ark b/extensions/positron-r/ark index 37d3db0074e..ba9db11725f 160000 --- a/extensions/positron-r/ark +++ b/extensions/positron-r/ark @@ -1 +1 @@ -Subproject commit 37d3db0074e01a184d67b7a85faf9a3492836422 +Subproject commit ba9db11725f0f3312f4de1df6b1b5d86fa844c13 From 21274169f626a1e54082a64652967c30a6a580fb Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Thu, 30 Jul 2026 15:23:00 +0200 Subject: [PATCH 2/3] Unskip tests Closes https://github.com/posit-dev/positron/issues/15211 --- test/e2e/tests/autocomplete/autocomplete.test.ts | 4 +--- test/e2e/tests/code-actions/r.test.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/test/e2e/tests/autocomplete/autocomplete.test.ts b/test/e2e/tests/autocomplete/autocomplete.test.ts index cd60cd9125c..a82d5f83433 100644 --- a/test/e2e/tests/autocomplete/autocomplete.test.ts +++ b/test/e2e/tests/autocomplete/autocomplete.test.ts @@ -87,9 +87,7 @@ test.describe('Autocomplete', { await console.expectSuggestionListToContain('abspath, def abspath(path)'); }); - // Skipped: the Ark LSP can hang, so completion requests go unanswered and - // the suggestion list stays empty. Un-skip when #15211 is fixed. - test.skip('R - Verify autocomplete suggestions in Console and Editor', { + test('R - Verify autocomplete suggestions in Console and Editor', { tag: [tags.ARK], annotation: [{ type: 'issue', description: 'https://github.com/posit-dev/positron/issues/15211' }] }, async function ({ app, runCommand, sessions, hotKeys }) { diff --git a/test/e2e/tests/code-actions/r.test.ts b/test/e2e/tests/code-actions/r.test.ts index f1910a9af4c..aaa44a40f10 100644 --- a/test/e2e/tests/code-actions/r.test.ts +++ b/test/e2e/tests/code-actions/r.test.ts @@ -19,9 +19,7 @@ test.describe('R Code Actions', { tag: [tags.EDITOR, tags.WIN, tags.WEB, tags.AR }); - // Skipped: the Ark LSP can hang, so the statement range request never - // resolves and nothing is sent to the console. Un-skip when #15211 is fixed. - test.skip('R - Can execute code in untitled file with Ctrl+Enter', { + test('R - Can execute code in untitled file with Ctrl+Enter', { annotation: [ { type: 'issue', description: 'https://github.com/posit-dev/positron/issues/11533' }, { type: 'issue', description: 'https://github.com/posit-dev/positron/issues/15211' } From 001e342e00ebda78523f7aebdd9767cdfdd646b6 Mon Sep 17 00:00:00 2001 From: Lionel Henry Date: Wed, 29 Jul 2026 09:46:31 +0200 Subject: [PATCH 3/3] Handle conflicting submodule pointer --- .claude/skills/bump-ark/bump_ark.py | 80 ++++++++++++++++-------- .claude/skills/bump-ark/test_bump_ark.py | 17 +++++ 2 files changed, 71 insertions(+), 26 deletions(-) diff --git a/.claude/skills/bump-ark/bump_ark.py b/.claude/skills/bump-ark/bump_ark.py index 93903db958d..cf0a417b8e9 100644 --- a/.claude/skills/bump-ark/bump_ark.py +++ b/.claude/skills/bump-ark/bump_ark.py @@ -510,9 +510,11 @@ def open_or_update_tracked_bump( tip = branch_head_sha(branch) if tip is None: ensure_branch(target_sha, title, branch) - elif branch_submodule_sha(branch) != target_sha: - eprint(f"Advancing {branch} to {target_sha}") - stack_submodule_commit(branch, target_sha, title, tip) + else: + main_tip = main_tip_sha() + if branch_submodule_sha(branch) != target_sha or missing_main_tip(tip, main_tip): + eprint(f"Advancing {branch} to {target_sha}") + stack_submodule_commit(branch, target_sha, title, tip, main_tip) if pr: gh( @@ -575,10 +577,8 @@ def ensure_branch(target_sha: str, title: str, branch: str): ): return - base_commit = gh_json("api", f"repos/{POSITRON_REPO}/git/ref/heads/{BASE_BRANCH}")[ - "object" - ]["sha"] - new_commit = make_bump_commit(target_sha, title, base_commit) + base_commit = main_tip_sha() + new_commit = make_bump_commit(target_sha, title, base_commit, [base_commit]) gh( "api", "-X", @@ -591,20 +591,20 @@ def ensure_branch(target_sha: str, title: str, branch: str): # Create a commit that sets the submodule gitlink (mode 160000, type commit) to -# `target_sha`, parented on `parent`. The tree is `parent`'s own tree with only the -# gitlink swapped, so the commit is a pure one-file change. Returns the new commit -# sha. +# `target_sha`, on top of `parents`. The tree is `tree_source`'s own tree with only +# the gitlink swapped, so the commit differs from `tree_source` by exactly that one +# file. Returns the new commit sha. # -# Why parent's tree and not current main's tree: a PR's diff is measured from its -# merge base, the commit the branch was cut from, which stacking never moves. If we -# rebuilt the tree from current main, every file main touched since the fork would -# land in the bump PR's diff, and a later main edit to one of them would conflict on -# merge. A pure gitlink delta keeps the PR touching only the submodule, however far -# main has drifted. -def make_bump_commit(target_sha: str, title: str, parent: str) -> str: - base_tree = gh_json("api", f"repos/{POSITRON_REPO}/git/commits/{parent}")["tree"][ - "sha" - ] +# `tree_source` must be one of `parents`, normally the newest one (main's tip when +# merging main in, otherwise the sole parent). A PR's diff is measured from its +# merge base, so building the tree from that same parent keeps it as the merge base +# and the diff scoped to the gitlink alone. Building the tree from a commit outside +# `parents` would leave everything that commit added since the real merge base +# looking like part of this change. +def make_bump_commit(target_sha: str, title: str, tree_source: str, parents: list[str]) -> str: + base_tree = gh_json("api", f"repos/{POSITRON_REPO}/git/commits/{tree_source}")[ + "tree" + ]["sha"] new_tree = gh_json( "api", "-X", @@ -631,7 +631,7 @@ def make_bump_commit(target_sha: str, title: str, parent: str) -> str: f"repos/{POSITRON_REPO}/git/commits", "--input", "-", - input_obj={"message": title, "tree": new_tree, "parents": [parent]}, + input_obj={"message": title, "tree": new_tree, "parents": parents}, )["sha"] @@ -647,12 +647,40 @@ def branch_submodule_sha(branch: str) -> Optional[str]: return json.loads(proc.stdout)["sha"] +def main_tip_sha() -> str: + return gh_json("api", f"repos/{POSITRON_REPO}/git/ref/heads/{BASE_BRANCH}")[ + "object" + ]["sha"] + + +# True when `main_tip` isn't reachable from `tip` yet, i.e. this branch's last bump +# commit predates `main_tip` and still needs a merge commit to pull it in. Left +# unmerged, the branch and main have each changed the same gitlink path since their +# last common ancestor, which GitHub can't auto-resolve and shows as a conflict. +def stale_against_main(compare_status: str) -> bool: + return compare_status != "behind" + + +def missing_main_tip(tip: str, main_tip: str) -> bool: + if tip == main_tip: + return False + status = gh_json("api", f"repos/{POSITRON_REPO}/compare/{tip}...{main_tip}")[ + "status" + ] + return stale_against_main(status) + + # Advance an existing branch to a new commit that sets the submodule to -# `target_sha`, stacked on the current tip so the ref move is a fast-forward. The -# ref PATCH omits `force`, so a non-fast-forward (someone advanced the branch since -# `tip` was read) is rejected rather than clobbering their work. -def stack_submodule_commit(branch: str, target_sha: str, title: str, tip: str): - new_commit = make_bump_commit(target_sha, title, tip) +# `target_sha`. Always merges in main's current tip as a second parent (unless it's +# already the branch's tip), so a bump branch whose gitlink change would otherwise +# conflict with one already merged into main gets folded in before it can ever show +# as conflicted on GitHub. The ref PATCH omits `force`; since `tip` is always one of +# the new commit's parents, moving the ref there is still a fast-forward, so a +# non-fast-forward (someone advanced the branch since `tip` was read) is rejected +# rather than clobbering their work. +def stack_submodule_commit(branch: str, target_sha: str, title: str, tip: str, main_tip: str): + parents = [tip] if tip == main_tip else [tip, main_tip] + new_commit = make_bump_commit(target_sha, title, main_tip, parents) gh( "api", "-X", diff --git a/.claude/skills/bump-ark/test_bump_ark.py b/.claude/skills/bump-ark/test_bump_ark.py index 44474ade4d1..9eafe405fb4 100644 --- a/.claude/skills/bump-ark/test_bump_ark.py +++ b/.claude/skills/bump-ark/test_bump_ark.py @@ -26,6 +26,7 @@ first_parent_commits, parse_args, pr_resolution, + stale_against_main, tag_line, walk_first_parents, ) @@ -248,6 +249,22 @@ def test_foreign_pr_refuses(self): self.assertTrue(blocked_by_pr_owner(True, True, "someone", "me", False)) +class StaleAgainstMainTest(unittest.TestCase): + def test_behind_means_already_merged(self): + self.assertFalse(stale_against_main("behind")) + + def test_ahead_means_still_missing(self): + self.assertTrue(stale_against_main("ahead")) + + def test_diverged_means_still_missing(self): + self.assertTrue(stale_against_main("diverged")) + + def test_identical_means_still_missing(self): + # Unreachable via missing_main_tip's tip == main_tip shortcut, but the pure + # function itself has no such guard, so cover it directly. + self.assertTrue(stale_against_main("identical")) + + class BuildBodyTest(unittest.TestCase): def test_omits_closes_block_when_empty(self): body = build_body("", "@:ark", "### Release Notes\n\n- x", "- commit")