Skip to content

feat: surface github.com link in sidebar main workspace - #40

Merged
linletian merged 6 commits into
developfrom
feature/github-link
Jun 20, 2026
Merged

feat: surface github.com link in sidebar main workspace#40
linletian merged 6 commits into
developfrom
feature/github-link

Conversation

@linletian

Copy link
Copy Markdown
Owner

Summary

  • Sidebar main workspace row now renders a small GitHub Mark icon to the right of the project name when the main repo's git remote resolves to github.com. Clicking the icon opens the canonical https://github.com/<owner>/<repo> URL in a new tab.
  • The icon is rendered only when the new /api/main github_url field is non-empty; non-GitHub remotes and GitHub Enterprise intentionally do not surface a link.

Backend

  • New gitx.GitHubURL(gitRoot) resolver. Prefers origin, then iterates git remote. Normalizes SCP / HTTPS / ssh:// forms, strips .git and trailing slashes, and returns the empty string for any host other than github.com (case-insensitive). Never errors out — timeouts and unparseable URLs degrade to "" so the caller can treat the result as a pure boolean.
  • GET /api/main response gains a github_url string field (always present; empty when no link can be resolved).

Frontend

  • index.html: renderSidebarMain() conditionally appends an <a target="_blank" rel="noopener noreferrer"> GitHub icon to the .wt-name row. event.stopPropagation() prevents the icon click from triggering the row's selectWorktree(MAIN_WT_ID) handler. The URL is passed through encodeURI as defense in depth.
  • .wt-name flex layout already supports flex: 1; min-width: 0 so the project name shrinks while the icon stays right-aligned.

Tests

  • gitx_test.go: TestGitHubURL covers HTTPS / HTTP / SCP / ssh:// forms, GitHub Enterprise, .git suffix, trailing slashes, leading/trailing whitespace, non-GitHub hosts, no remote, and origin-preferred-vs-fallback ordering.
  • app_test.go: TestHandleMain asserts github_url is a string in the response and that its value matches the resolver's output for the same git root.

Docs (docs-first)

  • CHANGELOG.md — Unreleased section entry.
  • docs/API.md/api/main response shape and field semantics.
  • docs/ARCHITECTURE.md — sidebar main-workspace behavior.
  • docs/PRD.md — main workspace section.
  • README.md / README.zh-CN.md — §Features (MVP) bullet.

Verification

  • go build ./...
  • go vet ./...
  • go test ./internal/gitx/... ./internal/app/...

Notes

  • ssh://git@github.com:22/owner/repo.git (explicit SSH port) is intentionally not recognized; only the default-port ssh:// form is matched. This matches the behavior documented in docs/API.md.
  • escapeHtml was applied to the existing onclick="selectWorktree('')" interpolations as a small XSS hardening side-effect.

linletian and others added 2 commits June 19, 2026 10:24
调研 anomalyco/ghostty-web(Ghostty VT 解析器 WASM 化 + xterm.js API 兼容)
与 myworktree 的相似/差异点,以及作为 xterm.js 替代 renderer 的借鉴方案。

结论:精准命中 TERMINAL_FILTER_REVIEW.md 与 CHINESE_IME_ANALYSIS.md 已记录
的 OSC/DA 回声与复杂脚本渲染两类痛点,建议以 feature flag 灰度引入。
Render a small GitHub Mark icon to the right of the sidebar main
workspace project name when the main repo's git remote resolves to
github.com. Clicking opens the canonical https://github.com/<owner>/<repo>
URL in a new tab via <a target="_blank" rel="noopener noreferrer">,
with event.stopPropagation() so the icon click does not trigger the
row's selectWorktree handler.

Backend:
- Add gitx.GitHubURL(root) resolver. Prefers 'origin', then iterates
  'git remote'. Normalizes SCP / HTTPS / ssh:// forms, strips .git and
  trailing slashes, and returns '' for any host other than github.com
  (case-insensitive). GitHub Enterprise and non-Git remotes are
  intentionally not surfaced.
- /api/main response gains a github_url string field (always present;
  empty when no link can be resolved).

Tests cover all three URL formats, malformed inputs, GitHub Enterprise,
non-GitHub hosts, .git suffix, and the /api/main field plumbing.

Docs (per docs-first convention): CHANGELOG, API, ARCHITECTURE, PRD,
README.md, README.zh-CN.md.
@linletian

Copy link
Copy Markdown
Owner Author

Code Review

Bug — SCP-style regex 硬编码 git@ 用户(中等)

internal/gitx/remote.go:88

var githubSCPRE = regexp.MustCompile(`(?i)^git@github\.com:([A-Za-z0-9._-]+)/([A-Za-z0-9._-]+?)(?:\.git)?$`)

仅匹配 git@github.com:owner/repo.git。同文件中的 githubSSHRE 故意允许任意用户((?:[^@/]+@)?),但 SCP 形式没有。如果用户 ~/.ssh/config 中配置类似:

Host github.com
    User mywork

或 CI bot 配置为 ci-bot@github.com:...,则会静默丢失 GitHub 图标,因为其 origin URL 并不以 git@ 开头。用户必须显式使用 git 用户。

严重度为中等,因为规避方式是「将 SSH 用户改回默认的 git」(原始 GitHub URL 仍然可用)。但用户完全无感知——图标只是默默消失。

remote.go:19 的 Go 文档注释将其描述为「default SSH port only」,具有误导性:端口没问题,约束在用户docs/API.md:43 也未提及此点。TestGitHubURL 测试用例只覆盖了 git@

修复建议: 改为 ^[^@/]+@github\.com:...(参照 githubSSHRE),并补充非 git 用户的测试用例。

Subprocess amplification in the fallback path(低 / 提示性)

internal/gitx/remote.go:31-51 加上第 53-79 行的辅助函数:

  • 对于 缺失 origin(不仅是「非 GitHub」)的仓库,第一次 getRemoteURL(gitRoot, "origin") 调用总会派生一个 git remote get-url origin 子进程,该进程以非零退出并被丢弃。
  • 然后回退逻辑再派生 git remote 以及每个非 origin remote 各一次 git remote get-url <name>

最坏情况(5 个 remote,都非 GitHub,没有 origin)= 每次 /api/main 请求约 7 个子进程,每个附带 2s 的 context.WithTimeout。前端会定期轮询 /api/main,开销会累积。该模式与 CurrentBranch/DefaultBranch 一致,所以并不突兀,但可以考虑改用单次 git config --get-regexp ^remote\..*\.url$ 调用代替 N+2 次调用。无需修改,仅作提示。

Inline onclick="event.stopPropagation();"(轻微 / 提示性)

internal/ui/static/index.html:1878。该写法依赖过时的全局 window.event。当前主流浏览器均支持,但更稳妥的写法是 onclick="(e) => e.stopPropagation();"(更好的做法是在渲染后绑定监听器)。非 bug。

经核查、确认无问题的项

  • event.stopPropagation() 正确阻止了图标点击触发 selectWorktree(MAIN_WT_ID)<a target="_blank" rel="noopener noreferrer"> 的选择保留了对中键点击 /「在新标签页打开」行为的支持。
  • encodeURI(githubUrl) 是无害的纵深防御,即便正则已将 owner/repo 限制为 [A-Za-z0-9._-]+
  • state.mainRepo.namestate.mainRepo.branchw.namew.branch 以及 worktree id 新增的 escapeHtml 修复了先前模板中存在的 XSS 漏洞——值得肯定。
  • GitCommand 使用 exec.CommandContext(未走 shell),因此含 shell 元字符的 remote 名称不会成为命令注入向量。
  • handleMain 没有按 loopback 限制 github_url——这是正确的,因为该 URL 是公开的。
  • 测试覆盖了 SCP / HTTPS / ssh://httphttps 升级、.git / 尾部斜杠的剥离、GitHub Enterprise、非 GitHub 主机、file://origin 被拒绝但回退接受的场景,以及 /api/main 字段透传。覆盖充分。

The githubSCPRE regex hardcoded 'git@github.com:...' as the only
recognized SCP-style prefix. Repositories whose origin URL uses a
non-'git' username (e.g. a '\~/.ssh/config' alias mapping
'github.com' to 'User mywork', or a CI bot configured as
'ci-bot@github.com:...') would silently fail to surface a GitHub
icon in the sidebar — the URL was still valid, but the resolver
returned '' so the icon was never rendered.

Loosen the regex to '[^@/]+@github\.com:...' so it accepts any
non-empty user segment, mirroring the existing githubSSHRE pattern
and bringing the two ssh-shaped forms back into symmetry.

- remote.go: relax githubSCPRE; update Go doc to mention arbitrary
  user and replace the misleading 'default SSH port only' line.
- gitx_test.go: add three new parse cases (ssh config alias user,
  CI bot user, dotted user) so the relaxation is covered.
- docs/API.md: update the supported URL formats list to call out
  the arbitrary user segment.

No behavior change for existing 'git@' URLs; tests + vet + build
all green.
@linletian

Copy link
Copy Markdown
Owner Author

Review response

Thanks for the careful review. Addressing each point:

✅ Fixed — SCP regex hardcoded git@

Pushed as 231ebf9 fix(gitx): accept arbitrary SSH user in SCP-style GitHub URLs.

  • githubSCPRE is now ^[^@/]+@github\.com:..., mirroring the existing githubSSHRE so the two ssh-shaped forms are treated symmetrically.
  • Added three new parse cases to TestGitHubURL:
    • mywork@github.com:owner/repo.git (typical ~/.ssh/config User alias)
    • ci-bot@github.com:owner/repo.git (CI bot convention)
    • first.last@github.com:owner/repo.git (dotted user)
  • remote.go Go doc no longer says "default SSH port only" — that wording was misleading (the constraint was on the user, not the port). It now explicitly notes that the user segment is arbitrary and gives the alias/CI-bot cases as examples.
  • docs/API.md §1.3 updated to call out the arbitrary user segment in the supported-formats list.

No behavior change for existing git@ URLs; full go test ./internal/gitx/... ./internal/app/... is green (28 parse/e2e subtests pass, including the 3 new ones).

ℹ️ Acknowledged — informational items (no change)

  • Subprocess amplification in fallback path: noted, intentionally not changed in this PR to keep the diff focused on the bug fix. The suggested git config --get-regexp ^remote\..*\.url$ consolidation is a good follow-up — happy to file as a separate PR if you'd like.
  • Inline onclick="event.stopPropagation();": agreed it's not a bug. Refactoring inline handlers to bound listeners is a larger UI change that belongs in a separate cleanup PR, not in this feature.

Re-requesting review.

# Conflicts:
#	CHANGELOG.md
#	docs/PRD.md
#	internal/app/app_test.go
#	internal/ui/static/index.html
PR #40 (GitHub sidebar link) and PR #42 (branch divergence detection) both
inlined a ~10-line `git remote` parser in their respective files. Git's
content-level merge cannot detect duplicate top-level symbols across files,
so the collision only surfaced as a Go compiler error after the merge. The
fix was to drop the divergent.go copy in the merge commit, leaving a single
implementation in remote.go.

This commit goes one step further: move that single implementation to a
new remotes.go file and export it as ListRemotes, so future features that
need the same helper have a clearly signposted canonical entry point
instead of being tempted to inline their own copy.

No behavior change. The two existing callers (GitHubURL in remote.go and
effectiveHead in diverged.go) now invoke ListRemotes directly.
@linletian

Copy link
Copy Markdown
Owner Author

Closing to retrigger CI — the PR head was a merge commit (846d9a6) which suppressed the synchronize event for the follow-up push. Reopening to get a fresh merge-state evaluation.

@linletian linletian closed this Jun 20, 2026
@linletian linletian reopened this Jun 20, 2026
`gofmt -l` on Linux/macOS CI rejects the file without a trailing
newline. Run gofmt to comply.
@linletian
linletian merged commit bc7a9ae into develop Jun 20, 2026
2 checks passed
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