Skip to content
Merged
84 changes: 80 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,11 @@ jobs:
target="myworktree_${VERSION}_darwin_${GOARCH}"
dist_dir="dist/${target}"
mkdir -p "${dist_dir}"
ldflags="-s -w -X myworktree/internal/version.Version=${VERSION} -X myworktree/internal/version.Commit=${COMMIT} -X myworktree/internal/version.BuildDate=${build_date}"
GOOS="${GOOS}" GOARCH="${GOARCH}" go build -trimpath -ldflags "${ldflags}" -o "${dist_dir}/myworktree" ./cmd/myworktree
GOOS="${GOOS}" GOARCH="${GOARCH}" go build -trimpath -ldflags "${ldflags}" -o "${dist_dir}/mw" ./cmd/mw
# Only strip debug symbols (-s), keep symbol table (-w removed)
# to preserve macOS code signing compatibility
ldflags="-s -X myworktree/internal/version.Version=${VERSION} -X myworktree/internal/version.Commit=${COMMIT} -X myworktree/internal/version.BuildDate=${build_date}"
CGO_ENABLED=0 GOOS="${GOOS}" GOARCH="${GOARCH}" go build -trimpath -ldflags "${ldflags}" -o "${dist_dir}/myworktree" ./cmd/myworktree
CGO_ENABLED=0 GOOS="${GOOS}" GOARCH="${GOARCH}" go build -trimpath -ldflags "${ldflags}" -o "${dist_dir}/mw" ./cmd/mw
cp README.md LICENSE CHANGELOG.md "${dist_dir}/"
tar -C dist -czf "dist/${target}.tar.gz" "${target}"

Expand All @@ -52,9 +54,83 @@ jobs:
path: dist/*.tar.gz
if-no-files-found: error

codesign-notarize:
runs-on: macos-latest
needs: build-darwin
if: ${{ vars.APPLE_ENABLE_CODESIGN == 'true' }}
strategy:
fail-fast: false
matrix:
goarch:
- amd64
- arm64
steps:
- name: Download archive
uses: actions/download-artifact@v4
with:
name: release-darwin-${{ matrix.goarch }}
path: dist

- name: Extract and codesign
env:
APPLE_DEVELOPER_ID_CERT_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_CERT_BASE64 }}
APPLE_DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_CERT_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
run: |
set -euo pipefail
archive=$(ls dist/*.tar.gz)
basename=$(basename "${archive}" .tar.gz)
mkdir -p dist/extracted
tar xzf "${archive}" -C dist/extracted

# Import Apple Developer signing certificate
KEYCHAIN="build.keychain"
security create-keychain -p "" "${KEYCHAIN}"
security default-keychain -s "${KEYCHAIN}"
security unlock-keychain -p "" "${KEYCHAIN}"
security set-keychain-settings -lut 21600 "${KEYCHAIN}"
echo "${APPLE_DEVELOPER_ID_CERT_BASE64}" | base64 -d > cert.p12
security import cert.p12 -k "${KEYCHAIN}" -P "${APPLE_DEVELOPER_ID_CERT_PASSWORD}" -T /usr/bin/codesign -T /usr/bin/pkgbuild
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "" "${KEYCHAIN}"

for binary in myworktree mw; do
# Sign with hardened runtime (required for notarization)
codesign --force --options runtime --timestamp --sign "${APPLE_TEAM_ID}" \
"dist/extracted/${basename}/${binary}"
done

# Notarize each binary
for binary in myworktree mw; do
zip_path="dist/${binary}.zip"
ditto -c -k --keepParent "dist/extracted/${basename}/${binary}" "${zip_path}"
xcrun notarytool submit "${zip_path}" \
--apple-id "${APPLE_ID}" \
--team-id "${APPLE_TEAM_ID}" \
--password "${APPLE_APP_SPECIFIC_PASSWORD}" \
--wait
xcrun stapler staple "dist/extracted/${basename}/${binary}"
done

# Re-pack the signed archive
tar czf "${archive}" -C dist/extracted "${basename}"

# Clean up keychain
security delete-keychain "${KEYCHAIN}"

- name: Upload signed archive
uses: actions/upload-artifact@v4
with:
name: release-darwin-${{ matrix.goarch }}
path: dist/*.tar.gz
if-no-files-found: error
overwrite: true

publish:
runs-on: ubuntu-latest
needs: build-darwin
needs: [build-darwin, codesign-notarize]
if: ${{ !failure() }}
steps:
- name: Download archives
uses: actions/download-artifact@v4
Expand Down
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,27 @@
# Changelog

## v0.3.0

Release focused on remote collaboration, build robustness, and Apple Silicon reliability.

Highlights:
- Added **Portal Dashboard** — a shared entry port with auto-discovery of running instances across repos, global auth token (HttpOnly Cookie, CSRF protection), and Tailscale serve readiness.
- Improved **remote access** — instances and portal now bind to `0.0.0.0` by default, auth token auto-generates on first run, and the login flow supports CSRF-protected forms for non-loopback clients.
- Integrated **LLM-powered branch naming** — configurable protocol (OpenAI / Anthropic), with reasoning split support and a manual override option.
- Upgraded terminal shell with **xterm.js v6.0.0** and fixed Chinese IME shift-symbol fullwidth issues.
- Enhanced the **Changes panel** with separate Staged / Unstaged accordion sections, untracked file tracking, and per-file diff stats.
- Streamlined instance lifecycle — instances can be deleted directly on stop (no archive step), with per-worktree tab reordering via optimistic locking.

Build hardening for Apple Silicon:
- Release builds now set `CGO_ENABLED=0` to guarantee pure-Go cross-compilation from Linux to Darwin.
- Removed `-w` linker flag to preserve macOS code-signing compatibility.
- Added optional macOS codesign + notarization job (enabled via repository variables/secrets) to resolve Gatekeeper blocking on Apple Silicon Macs.
- Users who still encounter "no response" on Apple Silicon can run `xattr -d com.apple.quarantine ./mw` to clear the download quarantine attribute.

Documentation and validation:
- Expanded API, architecture, and PRD docs to cover the Portal Dashboard, remote access flow, CSRF protection, and auto-auth generation.
- Release packaging continues to publish Darwin `amd64` / `arm64` archives plus SHA256 checksums via the tag-triggered GitHub Actions workflow, with an optional codesign job.

## v0.2.0

Feature release focused on workspace visibility, terminal continuity, and day-to-day usability improvements.
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ Start from `v0.2.0` or newer for public release binaries. The earlier `v0.1.0` G
Each release archive contains `mw`, `myworktree`, `README.md`, `LICENSE`, and `CHANGELOG.md`.
If there is no prerelease/release asset yet, or you need a platform we do not publish, follow the source build steps below.

**Apple Silicon troubleshooting:** macOS may quarantine downloaded binaries and silently prevent execution (Gatekeeper). If the binary does not respond or shows "cannot be opened":
```bash
xattr -d com.apple.quarantine ./mw ./myworktree
```
Or open **System Settings → Privacy & Security** and click "Allow Anyway" for the blocked binaries.

### Build & install

```bash
Expand Down
6 changes: 6 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ mw --version
每个发布压缩包内都包含 `mw`、`myworktree`、`README.md`、`LICENSE` 和 `CHANGELOG.md`。
如果当前还没有预发布/正式发布压缩包,或者你的平台暂无对应产物,就直接使用下面的源码编译步骤。

**Apple Silicon 排障提示:** macOS 会对从网络下载的二进制文件施加隔离属性(Gatekeeper),可能导致二进制无响应或提示"无法验证开发者"。可运行:
```bash
xattr -d com.apple.quarantine ./mw ./myworktree
```
或在 **系统设置 → 隐私与安全性** 中为被阻止的二进制文件点击"仍要打开"。

### Build & install

```bash
Expand Down
56 changes: 56 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,62 @@ Partial failure example (staged succeeded, unstaged failed):
```
- Returns HTTP 400 if `id` is missing or unknown.

### Get all worktrees divergence
`GET /api/worktrees/diverged`

Returns divergence information for all worktrees: whether each worktree branch is behind the main branch or develop branch.

- Called on page load and every 60 seconds.
- For each worktree whose branch is the main branch itself, returns an empty object `{}` (no divergence check needed).
- For each worktree whose branch is develop, checks only main.
- For all other worktrees, checks both main and develop (if develop exists locally).
- Uses the local vs remote effective head that is more ahead (`git rev-list --left-right --count`).
- If the worktree's current branch cannot be determined (e.g., detached HEAD), the worktree entry contains only an `error` field.

Response:
```json
{
"items": {
"wt_abc123": {
"mainBranch": {"diverged": true, "ahead": 3},
"develop": {"diverged": false}
},
"wt_def456": {
"mainBranch": {"diverged": true, "ahead": 1},
"develop": {"diverged": true, "ahead": 2}
},
"wt_detached": {
"mainBranch": {"error": "cannot determine branch: git HEAD is detached or malformed"}
},
"__main__": {}
}
}
```

- `diverged`: `true` means the upstream branch has commits not yet contained in the worktree branch HEAD.
- `ahead`: number of commits the upstream effective head is ahead of the worktree HEAD. Only present when `diverged` is `true`.
- `error`: optional string describing why the check failed (e.g., git command timeout). When present, `diverged` is `false` and `ahead` is absent.
- `mainBranch` / `develop`: each key may be absent if the check is not applicable (e.g., develop does not exist locally).

### Get single worktree divergence
`GET /api/worktree/diverged?id=<worktreeId>`

Returns divergence information for a single worktree. Same response structure as above, but only contains the requested worktree entry.

- Called immediately when the user selects a worktree in the sidebar to refresh divergence labels.
- `id` can be a managed worktree ID, or `"__main__"` for the main repo.

Response:
```json
{
"items": {
"wt_abc123": {
"mainBranch": {"diverged": true, "ahead": 3}
}
}
}
```

## 3) Branches
### List (default + top 10)
`GET /api/branches`
Expand Down
2 changes: 2 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ It does **not** analyze project code or prevent concurrent write conflicts insid
- `internal/llm/` — LLM API client(OpenAI / Anthropic / OpenAI Compatible),可选,LLM Settings 通过 Web UI 对话框配置
- `internal/config/` — global auth configuration (read/write `auth.json`)
- `internal/portal/` — Portal dashboard (port claiming, instance registry, CSRF state management, HTTP endpoints; **reverse proxy `/s/<repo-hash>/` planned but not yet implemented** — current dashboard links point to instance ports directly; tailscale serve automation code is defined but **currently unused** due to tailscale CLI bug)
- `internal/gitx/` — git CLI wrappers (branch listing, default branch detection, branch divergence detection)
- `internal/ui/` — embedded static UI.

## 3. Data & persistence
Expand Down Expand Up @@ -67,6 +68,7 @@ The sidebar shows a pinned **Main Workspace** item at the top (purple accent), f
- **Instance routing**: Use `worktree_id: "__main__"` (constant: `instance.MainWorktreeID`) in `POST /api/instances` to start an instance in the main repo root. The instance's `worktree_id` will be `"__main__"` and `worktree_name` will be the directory basename.
- **Auto-select**: On first load, the UI auto-selects the first worktree; if no worktrees exist, it selects the main repo.
- **Refresh**: All branch info (main repo + worktrees) updates via the existing 2-second polling.
- **Divergence labels**: Each non-main worktree item in the sidebar displays compact red labels (e.g. `m↑3`, `d↑1`) next to its branch name, indicating how many commits the upstream branch (main or develop) is ahead. Labels are refreshed every 60 seconds and immediately when the user selects a different worktree. This helps users verify whether their worktree base is up-to-date before starting new work. See `docs/plans/git-commit-history-graph/DESIGN.md` for details.
- **Git Changes panel**: Below the worktree list, a read-only panel shows changed files for the currently selected worktree, split into two mutually exclusive accordion sections: **Staged** (changes in the index via `git diff --cached --numstat`) and **Unstaged** (working tree changes via `git diff --numstat`). The panel auto-refreshes every 10 seconds and on worktree selection change. The main repo's changes also refresh when its branch changes. Both git commands run concurrently on the server with a 2-second timeout each. The accordion defaults to showing Unstaged; clicking either header expands that section and collapses the other. Empty sections still show their header with a "No staged changes" / "No unstaged changes" message.

## 4. Instance lifecycle & reconnect semantics
Expand Down
7 changes: 7 additions & 0 deletions docs/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@
- 双层认证架构(Portal 层 Cookie + CSRF,实例层 loopback 绕过)
- 浏览器关闭保护:前端在 `beforeunload` 事件时,无论是否存在运行中实例,均触发浏览器原生确认对话框,防止误操作关闭页面。
- **Main workspace 分支查询**:`GET /api/main` 返回 `{name, branch}`。branch 字段实时查询(`git rev-parse --abbrev-ref HEAD`),在 detached HEAD 场景(如 CI 浅克隆)下返回空字符串而非错误。
- **规划新增:分支落后检测**:
- 侧栏每个 worktree 分支名旁显示红色标签(如 `m↑3` / `d↑1`),标识当前分支是否落后于主分支或集成分支 develop。
- 如果当前 worktree 就是主分支自身,则不显示标记。
- 判断逻辑:计算当前 HEAD 到上游 effective head(本地和远端中更领先的一方)的 ahead 数量,结果 `> 0` 即落后。
- 远端发现优先 `origin`,其次取其他 remote 中领先最多的;无远端则仅用本地判断。
- 标签常驻显示,60 秒定时刷新;切换 worktree 时立即刷新。
- 详情见 `docs/plans/git-commit-history-graph/DESIGN.md`。

## 8. 验收标准(MVP)
- 可创建/列出/删除 worktree(dirty 删除被拒绝)。
Expand Down
117 changes: 117 additions & 0 deletions docs/plans/git-commit-history-graph/DESIGN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# 分支落后检测 — 需求文档

> 版本:1.0
> 日期:2026-05-26

---

## 1. 背景

myworktree 是一个基于 git worktree 的多工作区管理工具。用户在日常开发中会在同一个仓库下创建多个 worktree,每个 worktree 对应一个独立的功能分支。随着时间推移,主分支(main/master)或集成分支(develop)可能已经包含新的提交,而用户的 worktree 分支可能基于旧版本创建,导致分支落后。

在创建新 worktree 时,如果 base 不是最新的主分支或集成分支,后续合并可能产生不必要的冲突。用户需要一种直观的方式来随时了解每个 worktree 分支是否落后于上游。

---

## 2. 需求

1. 在 Web 前端左侧 worktree 列表中,每个 worktree 分支名旁边用**红色文字标签**简洁标识是否落后于主分支(main/master)或集成分支(develop)
2. 标签格式如 `m↑3`(主分支领先 3 个 commit)、`d↑1`(develop 领选 1 个 commit)
3. 如果当前分支就是主分支自身,不显示任何标记
4. 如果当前分支就是 develop 自身,仍检查是否落后于主分支
5. 本地和远端取**领先更多的那个**作为 effective head 进行比较
6. 远端优先使用 `origin`,没有 `origin` 则取其他 remote 中领先最多的,没有远端则仅用本地
7. 标记**常驻显示**,60 秒定时刷新;当用户切换 worktree 时立即刷新

---

## 3. 需求分析

### 3.1 核心判定逻辑

只用一个 git 命令即可完成判定:

```
git rev-list --count <当前分支HEAD>..<上游effective_head>
```

- 结果 `> 0` → 上游领先,显示标记
- 结果 `= 0` → 无落后,不显示标记

相比于使用 `git merge-base --is-ancestor` 额外判断,这种方式一步完成,更简洁。

### 3.2 分支关系

```
检查方 被检查方
─────────────────────
普通分支 → 主分支 (必须)
普通分支 → develop (如果 develop 存在且当前分支不是 develop)
develop → 主分支 (必须)
主分支 → 无 (不显示标记)
```

### 3.3 effective head 计算

对每个上游分支(main 或 develop):

1. `git rev-parse <branch_name>` → 本地 head
2. 远端发现:
- 优先 `git rev-parse origin/<branch_name>`
- 不存在则遍历 `git remote` 列表,取 ahead 最多的 remote tracking branch
- 没有任何远端匹配 → 仅使用本地 head
3. `git rev-list --left-right --count <branch_name>...<remote_tracking>` → 取 `ahead` 值
4. 本地和远端中,取 `ahead` 更多的一方作为 effective head

### 3.4 刷新策略

| 触发条件 | 接口 | 说明 |
|---------|------|------|
| 页面初始加载 | 批量接口 | 随 refresh() 一起调用 |
| 定时刷新 (60s) | 批量接口 | 低频轮询,避免频繁 git 操作 |
| 用户切换 worktree | 单条接口 | 立即获取最新状态 |

---

## 4. 基础规划

### 4.1 后端 API

**批量接口**:`GET /api/worktrees/diverged`

遍历所有 worktree,对每个分支执行分支关系判定,返回聚合结果。

**单条接口**:`GET /api/worktree/diverged?id=<worktree_id>`

根据指定 worktree 执行判定,用于切换 worktree 时的即时刷新。

**新增后端模块**:`internal/gitx/diverged.go`

封装三个核心工具函数:
- 计算分支的 ahead 数量
- 计算分支的 effective head(本地与远端中更领先的一方)
- 发现远端 tracking branch(优先 origin,其次其他 remote)

### 4.2 前端 UI

**渲染位置**:在 `renderSidebar()` 中,每个 worktree 行的分支名字符串后,追加红色标签。

**HTML 结构**:一个内联的 `<span>` 标签组,每个标签带 `title` 属性提供 tooltip 说明文字。

**CSS 样式**:10px 字号,红色 (`--danger-text`),与分支名字号形成对比但不过分突出。

**状态管理**:在全局 `state` 中新增 `diverged` 字段存储每个 worktree 的检测结果,由刷新函数异步更新,`renderSidebar()` 渲染时读取。

**交互**:切换 worktree 的 `selectWorktree()` 函数中追加单条接口调用。

### 4.3 涉及文件

| 文件 | 操作 | 内容 |
|------|------|------|
| `internal/gitx/diverged.go` | 新增 | 核心 git 查询函数 |
| `internal/app/app.go` | 修改 | 注册 2 个新路由 + handler |
| `internal/ui/static/index.html` | 修改 | CSS 样式 + JS 刷新逻辑 + 渲染 |

---

*本文档确认后进入实现阶段。*
Loading