Skip to content

CLI-1535 Interactive project folder selection for sonar link - #1254

Open
georgii-borovinskikh-sonarsource wants to merge 2 commits into
gb/sonar-config-linksfrom
gb/interactive-link
Open

georgii-borovinskikh-sonarsource wants to merge 2 commits into
gb/sonar-config-linksfrom
gb/interactive-link

Conversation

@georgii-borovinskikh-sonarsource

Copy link
Copy Markdown
Member

Summary

Without a project key or --path, an interactive sonar link now lets the user pick the project folders, then the project for each.

  • The folders under the repository root show as a tree that expands lazily. The root and folders at any depth can be ticked with Tab.
  • Typing searches folder names across the repository, ignoring case and whitespace. Hidden and git-ignored folders are left out; empty folders are kept.
  • Folders already linked show linked to <key>; folders containing linked projects show how many lie deeper.
  • One ticked folder is linked as a single project; several, or --monorepo, become monorepo mappings merged into the existing links. Each folder then gets the existing project suggestion or search. Nothing is written until every folder has a project, and one summary is printed at the end.

.sonar-config.json is now written from the full list of links: one entry as project, several as projects. --monorepo, the folder picker and sonar init merge into what is already linked. A plain sonar link <key> replaces it and warns which mappings it removed, so a projects array can no longer make a link ineffective.

The work is in three commits:

  1. A searchable tree prompt (Console.treePrompt / treeMultiSelectPrompt) in src/core/ui/tree/, split into model, view model, view and clack glue. It keeps a fixed height while scrolling, searching and loading. calculateViewport no longer hides a single row behind "1 more", which the multi-select prompt benefits from too.
  2. The shared config split into a file layer (readEntries / writeEntries) and a repository that resolves entries for discovery, plus linkProjectDirectories with merge or replace.
  3. The folder picker behind a reusable FolderPicker interface (TreeFolderPicker), used by sonar link.

Testing

  • Link integration tests for the picker: listing, expanding, ticking the root and nested folders, search (including ignored and empty folders, spaces, and keeping ticks across searches), linked-folder marks, merging into existing links, relink prompts, cancelling, and one summary at the end
  • Link and init integration tests for merge and replace writes
  • Unit tests for the tree view model (commands, lazy loading, selection, debounced search and stale responses) and view (fixed height, scroll window), the folder picker and search, the config file layer and repository, and calculateViewport
  • Typecheck, lint and formatting

🤖 Generated with Claude Code

@netlify

netlify Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for sonarqube-cli canceled.

Name Link
🔨 Latest commit 4ec7bf0
🔍 Latest deploy log https://app.netlify.com/projects/sonarqube-cli/deploys/6af5d7a373f4f5184bf32506

@hashicorp-vault-sonar-prod

hashicorp-vault-sonar-prod Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

CLI-1535

Comment thread src/commands/link/folder-picker.ts Resolved
Comment thread src/commands/command-tree.ts Resolved
@georgii-borovinskikh-sonarsource
georgii-borovinskikh-sonarsource force-pushed the gb/interactive-link branch 3 times, most recently from 73867c5 to d54f446 Compare November 12, 2026 12:14
@georgii-borovinskikh-sonarsource
georgii-borovinskikh-sonarsource force-pushed the gb/interactive-link branch 5 times, most recently from 7cf6db9 to 4ec7bf0 Compare November 12, 2026 15:13
Console.treePrompt and treeMultiSelectPrompt show a lazily expanded tree:
arrows move, right expands or steps in, left collapses or steps out, typing
searches the whole tree through the caller's search function, and Tab ticks
entries in the multi-select variant. The prompt keeps a fixed height while
scrolling, searching and loading.

It lives in src/core/ui/tree/ as model (tree-source), view model (state and
commands, with an injectable scheduler for the search debounce), view (pure
rendering) and the clack glue. calculateViewport now keeps a long list at the
viewport height and never hides a single row behind a '1 more' line, which
the multi-select prompt benefits from too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without a project key or --path, an interactive 'sonar link' shows the folders
under the repository root in the tree prompt. The root and any folder at any
depth can be ticked with Tab; typing searches folder names across the
repository, case- and whitespace-insensitively. Hidden and git-ignored folders
are left out, empty ones are kept. Folders already linked show 'linked to
<key>', and folders holding linked projects show how many lie deeper.

One ticked folder is linked as a single project, several (or --monorepo) as
monorepo mappings, merged into what is already linked. Each folder then gets
the existing project suggestion or search; nothing is written until every
folder has its project, and one summary is printed at the end.

The picker sits behind a reusable FolderPicker interface (TreeFolderPicker),
with sonar link adding its linked-project marks through annotate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gitar-bot

gitar-bot Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
CI failed: Two CI integration-test failures in the Linux shard: four auth/analyze tests failed or timed out (one with an unexplained exit-code assertion), and one `sonar link list` test timed out; 518 and 653 other tests passed, and the link-list failure is unclear relation to this PR.

Overview

Two unique failure patterns were found across 6 CI logs for commit 2ea95cd. Both are test failures in the Linux integration shard (shard 2); the Windows e2e job was skipped. The rest of the suite passed, and no build or infrastructure failure was identified.

Failures

Auth and agentic integration test failures (confidence: medium)

  • Type: test
  • Affected jobs: 131950600389 (Integration and E2E Tests - linux (shard 2))
  • Related to change: unclear
  • Root cause: Four tests failed out of 523. tests/integration/specs/auth/auth.test.ts:1358 (organization selection) timed out after 5s and its child exited with 143. The auth picker status-order test and the login connection-selection test also failed around 5.1–5.6s, consistent with stalls, but their error details are not shown. tests/integration/specs/analyze/analyze-sqaa.test.ts:2343 failed because the exhausted-retries case did not match its expected exit code (expected 1; actual value omitted). The interactive auth tests touch the sonar link/login picker area this PR is modifying, but the logs do not confirm a causal link.
  • Suggested fix: Re-run the shard to check whether the interactive picker timeouts are flaky. For the analyze-sqaa test, capture the actual exit code and stderr at analyze-sqaa.test.ts:2343 before changing the assertion. If the picker tests stall consistently, check whether the new searchable tree prompt waits on input that the test session never sends.

sonar link list > flags enclosing projects apart from nested ones timeout (confidence: medium)

  • Type: flaky_test
  • Affected jobs: 131950600389
  • Related to change: unclear
  • Root cause: The test timed out after 5 seconds with no assertion output or stack trace, so the cause is not visible in the logs. The PR touches sonar link and adds link integration tests, but the evidence does not show which change caused the stall.
  • Suggested fix: Inspect the test setup and command completion to see why it exceeded the 5s limit. If the work is legitimately slow, raise the timeout with evidence; if it hangs, investigate the new link-list path.

Summary

  • Change-related failures: None confirmed. Relationship is unclear for all failures, since the logs lack assertion details for most of them.
  • Infrastructure/flaky failures: The timeouts (including the link-list test) may be flaky and should be triaged separately; 1 dangling dbus process was also killed during cleanup.
  • Recommended action: Re-run the Linux shard once. If the same tests fail, capture full output for the timed-out auth picker tests and the analyze-sqaa exit code, and check whether the new interactive tree prompt is blocking those tests.
Code Review ✅ Approved 🟡 Medium risk 5 closed / 5 findings

Interactive folder selection and revised link merging rewrite .sonar-config.json; mistaken choices could associate folders with unintended projects.

Adds an interactive sonar link folder picker with a searchable tree prompt, merging monorepo mappings into existing .sonar-config.json links. Resolved the help text mismatch, the sonar init write path compile break, unquoted check-ignore output for non-ASCII folders, silent drops of unvalidatable config entries on merge, and inconsistent absolute-path resolution between root and written entries. No open issues remain.

✅ 5 closed
✅ Quality: sonar link help text contradicts the new write and merge behaviour

📄 src/commands/command-tree.ts:235 📄 src/commands/command-tree.ts:248 📄 src/core/project-discovery/shared-project-config.ts:155-159
linkExtraHelp still says "Linking again replaces the existing link" and "Mappings are saved in the projects array in .sonar-config.json". With this PR, the folder picker always merges into the existing links. Also, writeEntries saves a single mapping as project, so a first sonar link --monorepo on a fresh repository writes project, not a projects array. Users who read --help get the wrong picture of what is written. Update these two sentences so they say a single link is written as project and several as projects, and that the picker and --monorepo merge.

✅ Cross-PR: #1253's init write path won't compile after this API removal

📄 src/core/project-discovery/link-target.ts:127-141 📄 src/commands/init/steps/mapping.ts:82-92
Breaks when this PR and #1253 both merge, in either order. The one merged second fails to typecheck because #1253's writeMapping still calls the removed linkProjectDirectory.

How

  1. This PR replaces linkProjectDirectory and SavedLink with linkProjectDirectories(auth, targetDir, links, mode), which returns SavedLinks (src/core/project-discovery/link-target.ts:127). It also drops set and addProject from the repository.
  2. CLI-1372 Monorepo workflow for sonar init #1253's writeMapping still imports linkProjectDirectory(auth, projectKey, target, { monorepo }) and destructures { replacedProjectKey, shadowed } from its result (src/commands/init/steps/mapping.ts:100). Its new monorepo flow, bind, unbound and link-existing steps all write through that function.
  3. This PR's init write always uses 'merge' (src/commands/init/steps/mapping.ts:82-87). So CLI-1372 Monorepo workflow for sonar init #1253's monorepo flag no longer changes how the link is written. CLI-1372 Monorepo workflow for sonar init #1253 skips the re-map warning when the key is the same (replacedProjectKey !== projectKey), but this PR warns for every key in replacedProjectKeys, so that check is lost.

Fix

  • This PR: Optionally skip replacedProjectKeys entries equal to the newly linked key before warning in writeMapping, so a same-key re-map stays silent.
  • CLI-1372 Monorepo workflow for sonar init #1253: Rebase writeMapping and its tests onto linkProjectDirectories/SavedLinks and readEntries/writeEntries. Drop or redefine the monorepo parameter, and keep the same-key warning suppression.

Compared b382c46 with #1253 at 97f6176

✅ Edge Case: check-ignore output isn't unquoted, so ignored non-ASCII folders still show

📄 src/commands/link/folder-picker.ts:140-147 📄 src/commands/link/folder-search.ts:45
withoutIgnored compares the raw output lines of git check-ignore with the folder names. Without -z, git C-quotes paths that contain non-ASCII bytes (core.quotePath defaults to true) or characters such as " and \. For example, an ignored folder données comes back as "donn\303\251es", so it never matches and the tree keeps showing it. folder-search.ts decodes the ls-files output with decodeGitPath, so the same folder is correctly hidden from search results, and the tree and search now disagree. Fix: decode each line with decodeGitPath, or pass -z and split on NUL.

✅ Bug: Merge writes silently drop config entries the CLI cannot validate

📄 src/core/project-discovery/link-target.ts:139-153 📄 src/core/project-discovery/shared-project-config.ts:135-149
linkProjectDirectories reads the existing links with readEntries(..., { strict: true }). That call filters out every entry that fails isValidEntry and logs it only at debug level. The function then writes back just the filtered list through writeEntries, which also drops the original project/projects keys. Before this PR, addProject (--monorepo) kept every raw element of projects it did not replace, and set (sonar init) did not touch projects at all. Now sonar link --monorepo, the folder picker and sonar init (all in merge mode) permanently delete any committed entry they don't recognise. Examples: an entry written by a newer CLI with a region this version doesn't know, or one with a typo a teammate could fix. A lone project that sits next to a projects array is also discarded without a warning. In replace mode these entries are removed too, but removedProjectKeys never lists them, so the warning understates what was deleted. Fix: keep the raw unknown/invalid elements and write them back unchanged (or at least warn when any are dropped), and only replace entries whose root matches.

✅ Edge Case: Absolute-path entries resolve one way for root and get written another

📄 src/core/project-discovery/shared-project-config.ts:167-170 📄 src/core/project-discovery/shared-project-config.ts:256-258 📄 src/core/project-discovery/shared-project-config.ts:264-266 📄 src/core/project-discovery/shared-project-config.ts:273-278 📄 src/core/project-discovery/shared-project-config.ts:324-327
isValidEntry accepts an absolute path, so such an entry ends up in readItems. Its root is computed with resolve(dir, entry.path), which gives back the absolute folder. withRelativePath instead rebuilds the path with join(dir, entry.path), and join sticks the absolute path onto dir. Take {"path": "/repo/api"} in /repo: any later merge write (sonar init on another folder, --monorepo, the picker) rewrites it as repo/api, a folder that doesn't exist. The same mismatch shows up in load: an absolute entry wins the new duplicate check against a later relative entry for the same folder, and is then dropped by resolveContainedPath, which rejects absolute paths. The relative entry is discarded as "already linked", so the folder ends up with no link at all. The fix is to reject absolute paths in isValidEntry, which matches how load already treats them. At minimum, withRelativePath should use resolve (or item.root) so the written path matches the folder the item was matched on.

Verification Rules 2/2 met Repos 3 inspected · PRs 3 inspected

Functional validation No results

Rules 2 passed 2 not applicable

✅ Internal artifact registries
✅ Repository license compliance

Broader impact Repos 3 inspected PRs 3 inspected
Cross-repo 3 repositories inspected 0 open impacts

SonarSource/sonarlint-intellij Inspected
SonarSource/sonarqube-agent-plugins Inspected
sonar-solutions/sonarqube-docker-sandbox-kit Inspected

Cross-PR 3 PRs inspected 0 open impacts

PR 1259: CLI-1538 Write .sonar-config.json from the full list of links Inspected
PR 1253: CLI-1372 Monorepo workflow for sonar init Inspected
PR 1263: CLI-1540 Support stderr confirmation prompts and quiet-console spinners Inspected

Auto-approval Withheld

For more verification details, comment gitar display:verbose.

Tip

Comment Gitar fix CI or enable auto-apply: gitar auto-apply:on

Options

Auto-apply is off → Gitar will not commit updates to this branch.
Display: compact → Showing compact verification results.

Comment with these commands to change the behavior for this request:

Auto-apply Compact
gitar auto-apply:on         
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant