Skip to content

feat: add a project tree for folders containing multiple repositories / 新增“工程树”,支持包含多个 git 仓库的上级目录 - #2887

Open
ncnnnnn wants to merge 2 commits into
sourcegit-scm:developfrom
ncnnnnn:feat/project-tree
Open

ncnnnnn wants to merge 2 commits into
sourcegit-scm:developfrom
ncnnnnn:feat/project-tree

Conversation

@ncnnnnn

@ncnnnnn ncnnnnn commented Nov 12, 2026

Copy link
Copy Markdown

Problem

Today the launcher tree is a list of repositories. It cannot represent the common
case of one parent folder that contains many git repositories — the folder itself
is not a repository, so opening it only offers "initialize here" or "trust".

TryOpenRepositoryFromPath has exactly two branches (it is a repository / show
Init or TrustRepository popup), because LauncherPage.Node is always a
RepositoryNode; there is no "directory container" dimension.

Upstream's ScanRepositories can recurse, but it is a one-shot modal import: it
leaves no container behind and registers every found repository into the global
Preferences.RepositoryNodes, so the result is indistinguishable from manually
imported repositories. Related requests exist in the tracker (#2712, #2296,
#2537, #2542, #602, #713) and were not implemented, so this PR proposes one
concrete shape and keeps it isolated.

Solution

Add a resizable "project" panel on the left of the launcher page:

  • A project is a container with a name and a list of directories.
  • Directories are scanned recursively (max depth 5) for git repositories.
  • Double-clicking a repository opens it through the existing
    OpenRepositoryInTab, so tabs, workspace restore and everything else behave
    exactly as with a repository opened from the welcome page.
  • The tree follows the active tab: switching tabs selects the matching
    repository and expands the directories on the way to it; a welcome tab clears
    the selection.
  • Scans run in the background, render results incrementally and can be cancelled.
  • Per-node expand state is persisted; the expansion caused by tab switching is not.
  • Projects are stored in their own projects.json, not in
    Preferences.RepositoryNodes.

Why the scanner is not ScanRepositories

Reusing it would have made scanning a normal folder take minutes: it runs one
git rev-parse process per directory without a .git entry. Measured on
C:\docs (5275 directories, 9 repositories within depth 5):

upstream approach this PR
one git rev-parse per directory ~5275 × 139 ms ≈ 12 min none
bare repositories one process each recognised by layout (HEAD + objects/ + refs/)
verification of real candidates serial 4-way concurrent, results read back in order
first repository visible only after the whole scan as soon as it is found

End-to-end: 12 min → 3.5 s on the folder above, with all 9 repositories listed.

Changes

New files (no upstream behaviour modified):

  • Models/Project.cs — project model and the save model for projects.json
  • Models/RepositoryScanner.cs — directory walk and repository detection
  • Models/ScanLog.cs — opt-in trace, used to diagnose scan problems
  • ViewModels/ProjectTreeNode.cs — tree node (project / directory / repository)
  • ViewModels/Projects.cs — project list, scanning, incremental tree building
  • Views/ProjectTree.axaml(.cs) — the panel, context menu, dialogs
  • Views/ProjectRename.axaml(.cs) — rename dialog, following the existing
    PushToNewBranch style (v:ChromelessWindow, v:CaptionButtons)

Two upstream files touched, both minimally:

  • Views/Launcher.axaml — the page area becomes
    Grid ColumnDefinitions="240,1,*" with the tree and a GridSplitter; the
    original ContentControl is moved unchanged into the third column.
  • App.JsonCodeGen.cs — one line registering ProjectsSaveModel, required
    because projects.json is trimmed away under PublishAot/PublishTrimmed.

Testing

  • dotnet build -c Debug — 0 warnings, 0 errors.
  • Windows, C:\docs (18 top-level folders, 5275 directories, 9 repositories at
    depth 5): all 9 repositories appear, double-click opens a normal tab, tab
    switching keeps the tree in sync.
  • Linux: self-contained linux-x64 publish succeeds; the binary is a valid ELF
    apphost with libcoreclr.so / libSkiaSharp.so / libHarfBuzzSharp.so
    present. Not launched on a Linux desktop yet.

Screenshots available on request.


问题(中文)

现在的启动页树本质是“仓库列表”,无法表达“一个上级目录里装着多个 git 仓库”这种常见情况:
这个目录本身不是仓库,打开它只会走“在此初始化”或“信任仓库”两条分支。

TryOpenRepositoryFromPath 只有两支(是仓库 / 弹 Init 或 TrustRepository),
根因是 LauncherPage.Node 恒为 RepositoryNode,缺“目录容器”这一维。

上游 ScanRepositories 虽有递归能力,但它是一次性模态导入:不留容器概念,
且会把找到的每个仓库都注册进全局 Preferences.RepositoryNodes,
结果与手工导入的仓库无法区分。仓库里的同类诉求(#2712、#2296、#2537、#2542、#602、#713)
均未实现,因此本 PR 提出一种具体形态,并把改动完全隔离。

方案

在启动页左侧新增可拖宽的“工程”面板:

  • 工程是一个容器,带名字和一组目录;
  • 目录递归扫描 git 仓库(最大深度 5);
  • 双击仓库走既有的 OpenRepositoryInTab,标签页、工作区恢复等行为与从欢迎页打开完全一致;
  • 树跟随当前标签页:切换标签时自动选中对应仓库并展开通往它的目录;切到欢迎页则清空选中;
  • 扫描在后台执行、边扫边上树、可取消;
  • 每个节点的展开状态会持久化,但由切换标签引起的展开不写入持久化;
  • 工程数据单独存 projects.json,不写入 Preferences.RepositoryNodes。

为什么不复用 ScanRepositories

复用它会让普通目录的扫描变成分钟级:它对每个“没有 .git 的目录”都起一次 git rev-parse 进程。
在 C:\docs(5275 个目录、深度 5 内 9 个仓库)实测:

上游做法 本 PR
每个空目录一次 git rev-parse 约 5275 × 139ms ≈ 12 分钟 无
裸仓库识别 每个都起进程 按目录布局判定(HEAD + objects/ + refs/)
候选校验 串行 4 并发,按序回填结果
第一个仓库出现时间 全量扫完才建树 找到即上树

端到端:同一目录 12 分钟 → 3.5 秒,9 个仓库全部列出。

改动

新增文件(不改上游既有行为):

  • Models/Project.cs — 工程模型与 projects.json 的保存模型
  • Models/RepositoryScanner.cs — 目录遍历与仓库识别
  • Models/ScanLog.cs — 可选开关的追踪日志,用于诊断扫描问题
  • ViewModels/ProjectTreeNode.cs — 树节点(工程 / 目录 / 仓库)
  • ViewModels/Projects.cs — 工程列表、扫描、增量建树
  • Views/ProjectTree.axaml(.cs) — 面板、右键菜单、对话框
  • Views/ProjectRename.axaml(.cs) — 重命名对话框,沿用既有 PushToNewBranch 的样式
    (v:ChromelessWindow、v:CaptionButtons)

只改 2 个上游文件,且都是最小改动:

  • Views/Launcher.axaml — 页面区改成 Grid ColumnDefinitions="240,1,*",放入树和 GridSplitter,
    原 ContentControl 原样移到第三列;
  • App.JsonCodeGen.cs — 加一行注册 ProjectsSaveModel。因为 projects.json 在
    PublishAot/PublishTrimmed 下会被裁掉,必须靠源生成器。

测试

  • dotnet build -c Debug — 0 警告 0 错误;
  • Windows 实测 C:\docs(18 个顶层目录、深度 5 内 5275 个目录、9 个仓库):
    9 个仓库全部显示,双击可打开与原生一致的标签页,切换标签时树保持同步;
  • Linux:linux-x64 自包含发布成功,产物是合法 ELF apphost,
    libcoreclr.so / libSkiaSharp.so / libHarfBuzzSharp.so 齐备;尚未在 Linux 桌面实跑。

需要截图可以补。

Projects are user-defined containers of directories. The tree scans each directory for git repositories (same pruning rules as ScanRepositories but without the side effect of registering nodes into Preferences), keeps only the directory chains that actually lead to a repository, and opens a repository in a regular tab on double click - opening an already opened repository activates its tab.

The panel is mounted as a new column of the existing page area in Launcher.axaml; Preferences and the repository pages are untouched. Data is persisted to a separate projects.json so the upstream Preferences class keeps its diff-free state.
Scanning a large folder such as C:\docs (5275 directories) used to take about
12 minutes, because the upstream scanner spawns one `git rev-parse` process per
subdirectory, and the tree was only built after the whole scan finished. Fixing
the performance alone did not make repositories visible: ProjectTree never set
its DataContext, so every binding resolved against Launcher's view model and the
tree rendered empty no matter what the model contained.

- RepositoryScanner: recognise bare repositories by layout (HEAD + objects/ +
  refs/) instead of one git process per folder; verify real candidates with
  4-way concurrency and read results back in index order; report every hit
  through IProgress so the caller can render it immediately
- Projects: add the directory node before scanning and insert repositories as
  they are found, reconcile against the full result list afterwards, restore
  projects sequentially (concurrent rebuilds used to cancel each other), reuse
  an existing project when the chosen directories are identical, and name the
  top-level directory node by its full path so it no longer collides with the
  project name
- ProjectTree: set DataContext to Projects.Instance; show three-state icons
  (folder for a project, open folder for an intermediate directory, repository
  stack for a git repository) taken from Resources/Icons.axaml; trim long names
- ProjectTreeNode: IsDirectory plus change notification for icon selection
- ScanLog + `--scan-log <dir>` + SOURCEGIT_SCAN_LOG: headless scanner trace,
  disabled unless explicitly requested

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