@senad-d/branchme 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2 -2
- package/README.md +34 -18
- package/SECURITY.md +12 -2
- package/docs/SMOKE_TEST.md +2 -2
- package/docs/STRUCTURE.md +9 -6
- package/docs/TUI_CAPTURE.md +29 -7
- package/package.json +1 -1
- package/src/commands/branchme-command.ts +11 -5
- package/src/constants.ts +9 -0
- package/src/git.ts +210 -24
- package/src/tools/branchme-tools.ts +67 -0
- package/src/types.ts +29 -0
- package/src/ui/branchme-panel.ts +6 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
## 0.1.0 - Unreleased
|
|
4
4
|
|
|
5
5
|
- Implemented the `branchme` informational slash command with help aliases.
|
|
6
|
-
- Added strict BranchMe tools: `branch_status`, `change_branch`, `create_branch`, `push_branch`, and `pull_request`.
|
|
7
|
-
- Added argv-style git helpers for repository status, branch validation/creation/switching, clean-worktree preflight, upstream detection, and current-branch push/publish.
|
|
6
|
+
- Added strict BranchMe tools: `branch_status`, `change_branch`, `fetch_branch`, `pull_branch`, `rebase_branch`, `create_branch`, `push_branch`, and `pull_request`.
|
|
7
|
+
- Added argv-style git helpers for repository status, branch validation/creation/switching, clean-worktree preflight, upstream detection, configured-upstream fetch, fast-forward-only current-branch pull, current-branch rebase with automatic abort on failure, and current-branch push/publish.
|
|
8
8
|
- Added GitHub repository resolution, environment-token and local `.env` token fallback handling, REST pull request creation, response validation, and token redaction.
|
|
9
9
|
- Added bounded automatic Git context before each agent run with branch/upstream state, working-tree counts and unstaged paths, authenticated related-open-PR lookup, and recent commits.
|
|
10
10
|
- Expanded `branch_status` into a shared, explicit, read-only context refresh for state that may change during a run.
|
package/README.md
CHANGED
|
@@ -11,12 +11,12 @@
|
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
13
|
Current-repository branch and pull request tools for <a href="https://pi.dev">pi</a>.
|
|
14
|
-
<br />Inspect branch state, switch/create branches, push, and open GitHub PRs from pi prompts.
|
|
14
|
+
<br />Inspect branch state, fetch, switch/create/pull/rebase branches, push, and open GitHub PRs from pi prompts.
|
|
15
15
|
</p>
|
|
16
16
|
|
|
17
17
|
---
|
|
18
18
|
|
|
19
|
-
BranchMe is a Pi extension for safe branch workflow automation. Before each agent run, it appends a bounded, read-only snapshot of the current Git repository to the system prompt. It also adds an informational `/branchme` command and
|
|
19
|
+
BranchMe is a Pi extension for safe branch workflow automation. Before each agent run, it appends a bounded, read-only snapshot of the current Git repository to the system prompt. It also adds an informational `/branchme` command and eight agent-callable tools that explicitly refresh state, switch to an existing local branch, fetch its configured upstream remote, fast-forward or rebase the clean current branch onto its upstream, create a branch from the current `HEAD`, push the current branch, and create a GitHub pull request.
|
|
20
20
|
|
|
21
21
|
<table align="center">
|
|
22
22
|
<tr>
|
|
@@ -30,12 +30,13 @@ BranchMe is a Pi extension for safe branch workflow automation. Before each agen
|
|
|
30
30
|
</table>
|
|
31
31
|
|
|
32
32
|
- **Context-aware:** every agent run starts with bounded branch, working-tree, related-PR, and recent-commit metadata; repository metadata is untrusted data, not instructions.
|
|
33
|
-
- **Current-repository only:** Git and GitHub operations are scoped to the checkout where pi is running.
|
|
34
|
-
- **
|
|
33
|
+
- **Current-repository only:** Git and GitHub operations are scoped to the checkout where pi is running; fetch, pull, and rebase resolve the current branch's configured upstream.
|
|
34
|
+
- **Explicit history rewrites:** `rebase_branch` runs only when explicitly requested, requires a clean current branch with an upstream, disables autostash and multi-ref updates, and automatically attempts to abort on failure.
|
|
35
|
+
- **Commit-safe:** context collection is read-only, and BranchMe never stages files, creates user-authored commits, generates commit messages, force-pushes, creates merge commits, resets, or edits files directly.
|
|
35
36
|
- **Strict tools:** tool schemas reject extra properties such as `force`, `stash`, `discard`, `owner`, `repo`, `path`, or `baseRef`.
|
|
36
37
|
- **PR-ready:** create GitHub pull requests from existing local branches after verifying the `headBranch` matches GitHub and the base is visible, with explicit PR fields and `GITHUB_TOKEN` or `GH_TOKEN` from the process environment or a local `.env` fallback.
|
|
37
38
|
|
|
38
|
-
> **Security:** pi packages run with your full system permissions. BranchMe runs local `git` commands, may make an automatic authenticated GitHub request to find a related open pull request, can switch/create branches
|
|
39
|
+
> **Security:** pi packages run with your full system permissions. BranchMe runs local `git` commands, may make an automatic authenticated GitHub request to find a related open pull request, can fetch remotes, switch/create/pull/rebase branches, push the current branch, and create GitHub pull requests. Read [`SECURITY.md`](SECURITY.md).
|
|
39
40
|
|
|
40
41
|
## Table of Contents
|
|
41
42
|
|
|
@@ -81,12 +82,16 @@ A typical BranchMe flow is:
|
|
|
81
82
|
|
|
82
83
|
1. Use the automatic snapshot to understand state at the start of the agent run.
|
|
83
84
|
2. Use `branch_status` when you explicitly need fresh state, especially after a Git mutation in the same run.
|
|
84
|
-
3. Switch
|
|
85
|
-
4.
|
|
86
|
-
5.
|
|
87
|
-
6.
|
|
85
|
+
3. Switch to the local base branch with `change_branch`.
|
|
86
|
+
4. Update it from its configured upstream with `pull_branch`, which requires a clean worktree and uses fast-forward-only semantics.
|
|
87
|
+
5. Create from the updated `HEAD` with `create_branch`.
|
|
88
|
+
6. Make edits and commit outside BranchMe.
|
|
89
|
+
7. Push the current branch with `push_branch`.
|
|
90
|
+
8. After `push_branch` completes and GitHub can see the branches, create a pull request with `pull_request`.
|
|
88
91
|
|
|
89
|
-
|
|
92
|
+
To refresh the current branch's configured remote-tracking ref without changing the local branch or working tree, use `fetch_branch`. To reconcile the clean current branch by rewriting its local commits, run `fetch_branch`, wait for it to complete, and then run `rebase_branch`. Both tools require a configured upstream; `rebase_branch` automatically attempts `git rebase --abort` if rebasing fails.
|
|
93
|
+
|
|
94
|
+
BranchMe is tool-based. The slash command is informational only and never changes or updates branches, fetches, rebases, pushes, commits, stages, edits files, or opens pull requests.
|
|
90
95
|
|
|
91
96
|
---
|
|
92
97
|
|
|
@@ -156,7 +161,7 @@ $EDITOR .env
|
|
|
156
161
|
pi
|
|
157
162
|
```
|
|
158
163
|
|
|
159
|
-
`push_branch`
|
|
164
|
+
`fetch_branch`, `pull_branch`, and `push_branch` use your normal Git remote credentials. BranchMe does not inject `GITHUB_TOKEN` into `git fetch`, `git pull`, or `git push`. `rebase_branch` operates on the locally available configured upstream ref and makes no network request itself.
|
|
160
165
|
When the current branch already has an upstream, BranchMe pushes an explicit `HEAD:<upstream-branch-ref>` refspec to the configured upstream remote instead of relying on a bare `git push`.
|
|
161
166
|
Run `pull_request` only after `push_branch` has completed; `pull_request` preflights the GitHub `headBranch` and `baseBranch` before creating the PR and fails with retry guidance if a branch is not visible yet or the GitHub `headBranch` commit does not match the local branch.
|
|
162
167
|
|
|
@@ -197,11 +202,14 @@ Commands are informational only. BranchMe actions are performed by agent-callabl
|
|
|
197
202
|
| --- | --- | --- |
|
|
198
203
|
| `branch_status` | `{}` | Explicitly refreshes the same bounded context used at agent start: repo root in structured details, branch/detached state, upstream and ahead/behind counts, working-tree counts and unstaged/untracked paths, related open PR, and recent commits. It is read-only. |
|
|
199
204
|
| `change_branch` | `{ "branchName": string }` | Validates `branchName`, requires `refs/heads/<branchName>` to exist locally, rejects dirty worktrees, and runs `git switch <branchName>`. |
|
|
205
|
+
| `fetch_branch` | `{}` | Requires a current branch with a configured upstream and runs `git fetch --no-tags --no-recurse-submodules <upstream-remote> <upstream-branch-ref>:<remote-tracking-ref>`; only that tracking ref is refreshed without changing local branches or working-tree files. |
|
|
206
|
+
| `pull_branch` | `{}` | Requires a clean current branch with a configured upstream and runs `git pull --ff-only --no-rebase --no-autostash <upstream-remote> <upstream-branch-ref>`; divergence fails without rebasing or creating a merge commit. |
|
|
207
|
+
| `rebase_branch` | `{}` | Requires a clean current branch with a configured upstream and runs `git rebase --no-autostash --no-update-refs <upstream>`; it rewrites local commits and automatically attempts `git rebase --abort` on failure. |
|
|
200
208
|
| `create_branch` | `{ "branchName": string }` | Validates `branchName`, rejects existing local branches, and runs `git switch -c <branchName>` from current `HEAD`. |
|
|
201
209
|
| `push_branch` | `{}` | Pushes the current branch to its configured upstream remote with an explicit `HEAD:<upstream-branch-ref>` refspec, or publishes it with `git push --set-upstream origin <currentBranch>` when no upstream exists. |
|
|
202
210
|
| `pull_request` | `{ "headBranch": string, "baseBranch": string, "title": string, "body": string, "draft": boolean }` | Preflights GitHub branch visibility and verifies the GitHub `headBranch` commit matches the local branch, then creates a pull request in the resolved current repository via `POST /repos/{owner}/{repo}/pulls`; branch refs must exist locally and cannot use `owner:branch`. |
|
|
203
211
|
|
|
204
|
-
All schemas reject additional properties. `change_branch` never accepts `baseRef`, `force`, `stash`, `discard`, `create`, `owner`, `repo`, or path inputs. `pull_request` never accepts `owner`, `repo`, or owner-prefixed branch refs; BranchMe resolves the repository from local `origin` and/or matching `GITHUB_REPOSITORY`.
|
|
212
|
+
All schemas reject additional properties. `change_branch` never accepts `baseRef`, `force`, `stash`, `discard`, `create`, `owner`, `repo`, or path inputs. `fetch_branch`, `pull_branch`, and `rebase_branch` have strict empty schemas and never accept a branch, remote, refspec, force, autostash, or arbitrary rebase target. `pull_request` never accepts `owner`, `repo`, or owner-prefixed branch refs; BranchMe resolves the repository from local `origin` and/or matching `GITHUB_REPOSITORY`.
|
|
205
213
|
|
|
206
214
|
---
|
|
207
215
|
|
|
@@ -219,7 +227,7 @@ Before each agent run, BranchMe appends an **Automatic Git Context** snapshot to
|
|
|
219
227
|
|
|
220
228
|
Collection defaults are a 5-second timeout per local Git command, a 4-second related-PR lookup timeout, at most 512 characters per metadata value, and at most 4,000 characters for the rendered snapshot. GitHub response bodies are limited to 64 KiB. The formatter can further shorten values or omit entries to stay within the total limit.
|
|
221
229
|
|
|
222
|
-
The snapshot is fresh at agent start but is not live. A branch switch, commit, file change, push, or other mutation later in the same run can make it stale. `branch_status` performs an explicit current-state refresh through the same shared collector and remains read-only; it does not mutate files, Git state, or GitHub state.
|
|
230
|
+
The snapshot is fresh at agent start but is not live. A fetch, branch switch, pull, rebase, commit, file change, push, or other mutation later in the same run can make it stale. `branch_status` performs an explicit current-state refresh through the same shared collector and remains read-only; it does not mutate files, Git state, or GitHub state.
|
|
223
231
|
|
|
224
232
|
Related-PR metadata does not come from Git alone. When repository, branch, and credentials resolve, automatic collection and explicit `branch_status` may make an authenticated `GET /repos/{owner}/{repo}/pulls?state=open&head={owner}:{branch}&per_page=1` request. Without a token there is no unauthenticated fallback or GitHub request; the PR field is reported as unavailable while local Git context remains usable.
|
|
225
233
|
|
|
@@ -229,7 +237,9 @@ Use BranchMe from pi prompts or automation that drives pi with explicit tool cal
|
|
|
229
237
|
|
|
230
238
|
```text
|
|
231
239
|
Use branch_status.
|
|
232
|
-
|
|
240
|
+
Switch to the base branch with change_branch, then use pull_branch after the switch completes.
|
|
241
|
+
Fetch the current branch upstream with fetch_branch, wait for it to complete, then rebase with rebase_branch.
|
|
242
|
+
Create branch feature/docs-refresh from the updated current HEAD with create_branch.
|
|
233
243
|
Push the current branch with push_branch.
|
|
234
244
|
After push_branch completes, create a draft pull request from feature/docs-refresh to main titled "Refresh docs" with this body: "...".
|
|
235
245
|
```
|
|
@@ -239,12 +249,15 @@ BranchMe operates only on the repository where pi is running:
|
|
|
239
249
|
- Automatic collection and `branch_status` run bounded, read-only Git commands from the verified git root.
|
|
240
250
|
- Git commands use `pi.exec("git", args, { cwd, signal, timeout })` with argv arrays; repository mutations run from the verified git root.
|
|
241
251
|
- `change_branch` switches only to existing local branches and has no `force`, `stash`, `discard`, remote, or path input.
|
|
252
|
+
- `fetch_branch` requires a configured upstream, uses an explicit source-to-remote-tracking refspec with tags and submodule recursion disabled, and does not change local branches or working-tree files.
|
|
253
|
+
- `pull_branch` requires a clean worktree and configured upstream, updates only the current branch with `git pull --ff-only --no-rebase --no-autostash`, and has no branch, remote, force, or rebase input.
|
|
254
|
+
- `rebase_branch` requires a clean worktree and configured upstream, rewrites only the current branch onto the locally available upstream with autostash and multi-ref updates disabled, and automatically attempts to abort on failure.
|
|
242
255
|
- `create_branch` creates from the current `HEAD` only and has no `baseRef` input.
|
|
243
256
|
- `push_branch` pushes only the current branch, uses no bare upstream `git push`, and has no `branchName` input.
|
|
244
257
|
- `pull_request` creates PRs only for the resolved current GitHub repository, requires `headBranch` and `baseBranch` to exist locally, requires the GitHub `headBranch` commit to match the local branch, queues behind in-flight same-repository git mutation windows when possible, and rejects `owner:branch` head refs.
|
|
245
258
|
- If local `origin` and `GITHUB_REPOSITORY` both resolve but disagree, `pull_request` fails closed.
|
|
246
259
|
|
|
247
|
-
BranchMe intentionally does **not** stage files, create commits, force checkout, stash changes, discard changes, edit files directly, or generate commit messages.
|
|
260
|
+
BranchMe intentionally does **not** stage files, create user-authored commits, force checkout, stash changes, discard changes, force-push, create merge commits, edit files directly, or generate commit messages. Rebase-driven commit rewriting occurs only through explicit `rebase_branch` calls.
|
|
248
261
|
|
|
249
262
|
---
|
|
250
263
|
|
|
@@ -287,10 +300,13 @@ Ensure the token and Git credentials have permission for the branch and pull req
|
|
|
287
300
|
| Problem | Try |
|
|
288
301
|
| --- | --- |
|
|
289
302
|
| Not a git repository | Start pi from inside a git checkout. |
|
|
290
|
-
| Detached `HEAD` | Use `change_branch` to switch to an existing local branch, or checkout a branch before `
|
|
303
|
+
| Detached `HEAD` | Use `change_branch` to switch to an existing local branch, or checkout a branch before `fetch_branch`, `pull_branch`, `rebase_branch`, `create_branch`, or `push_branch`. |
|
|
291
304
|
| Branch already exists | Choose a new local branch name for `create_branch`, or use `change_branch` to switch to it. |
|
|
292
305
|
| Branch does not exist locally | Create a local branch first; `change_branch` does not checkout remote branches. |
|
|
293
|
-
| Dirty worktree before branch switch | Commit, stash, or discard changes outside BranchMe before using `change_branch`. |
|
|
306
|
+
| Dirty worktree before branch switch, pull, or rebase | Commit, stash, or discard changes outside BranchMe before using `change_branch`, `pull_branch`, or `rebase_branch`. |
|
|
307
|
+
| Fetch, pull, or rebase has no upstream | Configure the current branch upstream outside BranchMe, then retry the tool. |
|
|
308
|
+
| Pull is not a fast-forward | Run `fetch_branch`, wait for it to complete, then explicitly run `rebase_branch` if rewriting local commits is intended; otherwise reconcile outside BranchMe. |
|
|
309
|
+
| Rebase fails or conflicts | `rebase_branch` automatically attempts `git rebase --abort`. Inspect repository state before continuing if automatic cleanup also fails. |
|
|
294
310
|
| Push fails | Confirm the current branch is correct and your normal Git remote credentials can push. |
|
|
295
311
|
| Related PR is unavailable | Set `GITHUB_TOKEN` or `GH_TOKEN` before starting pi if related-PR context is wanted. Without a token, BranchMe keeps local context and intentionally makes no unauthenticated GitHub request. |
|
|
296
312
|
| PR auth fails | Set `GITHUB_TOKEN` or `GH_TOKEN` before starting pi, or copy `.env.example` to `.env` and fill in one token. |
|
|
@@ -312,7 +328,7 @@ npm run check:pack
|
|
|
312
328
|
printf '/branchme help\n/quit\n' | pi --no-extensions -e .
|
|
313
329
|
```
|
|
314
330
|
|
|
315
|
-
Validation covers TypeScript typechecking, formatting checks, automatic context collection and prompt injection, mocked GitHub lookup, unit tests, package checks, checkout Pi runtime smoke, and package-content verification. The checkout smoke loads BranchMe through Pi, then uses a temporary verifier command to confirm all
|
|
331
|
+
Validation covers TypeScript typechecking, formatting checks, automatic context collection and prompt injection, mocked GitHub lookup, unit tests, package checks, checkout Pi runtime smoke, and package-content verification. The checkout smoke loads BranchMe through Pi, then uses a temporary verifier command to confirm all eight BranchMe tools are visible through `pi.getAllTools()` with strict schemas and prompt metadata. Smoke-test notes are recorded in [`docs/SMOKE_TEST.md`](docs/SMOKE_TEST.md), and TUI/help captures are stored in [`docs/TUI_CAPTURE.md`](docs/TUI_CAPTURE.md).
|
|
316
332
|
|
|
317
333
|
Refresh TUI captures intentionally with:
|
|
318
334
|
|
package/SECURITY.md
CHANGED
|
@@ -17,14 +17,19 @@ Implemented git mutations are limited to:
|
|
|
17
17
|
|
|
18
18
|
- `change_branch`: `git switch <branchName>` after branch-name validation, local `refs/heads/<branchName>` verification, and clean-worktree preflight.
|
|
19
19
|
- `create_branch`: `git switch -c <branchName>` from current `HEAD` after branch-name validation and existing-branch checks.
|
|
20
|
+
- `fetch_branch`: `git fetch --no-tags --no-recurse-submodules <upstreamRemote> <upstreamBranchRef>:<remoteTrackingRef>` after validating the current branch's configured upstream target. The explicit destination is limited to that upstream's remote-tracking ref, so local branches and working-tree files are not changed.
|
|
21
|
+
- `pull_branch`: `git pull --ff-only --no-rebase --no-autostash <upstreamRemote> <upstreamBranchRef>` for the clean current branch after validating its configured upstream target.
|
|
22
|
+
- `rebase_branch`: `git rebase --no-autostash --no-update-refs <upstream>` for the clean current branch after validating its configured upstream target. It rewrites local commits and automatically attempts `git rebase --abort` without the cancelled caller signal if the rebase fails or is killed.
|
|
20
23
|
- `push_branch`: `git push <upstreamRemote> HEAD:<upstreamBranchRef>` for the current branch when an upstream exists, or `git push --set-upstream origin <currentBranch>` when no upstream exists.
|
|
21
24
|
|
|
22
|
-
Before each agent run, BranchMe also runs bounded, read-only Git commands to collect branch/upstream/ahead-behind state, working-tree counts, up to 20 unstaged or untracked path entries, and up to 5 recent commits. The same collector runs when `branch_status` explicitly refreshes context. Collection does not run `switch`, `push`, `add`, `commit`, or any other mutation, and it never reads diffs or file contents.
|
|
25
|
+
Before each agent run, BranchMe also runs bounded, read-only Git commands to collect branch/upstream/ahead-behind state, working-tree counts, up to 20 unstaged or untracked path entries, and up to 5 recent commits. The same collector runs when `branch_status` explicitly refreshes context. Collection does not run `fetch`, `switch`, `pull`, `rebase`, `push`, `add`, `commit`, or any other mutation, and it never reads diffs or file contents.
|
|
23
26
|
|
|
24
|
-
Branch switching can update working-tree files as normal Git
|
|
27
|
+
Branch switching, fast-forward pulls, and successful rebases can update working-tree files as normal Git behavior; fetch updates one validated remote-tracking ref without changing local branches or the working tree. Mutating branch operations for the same repository are serialized to avoid same-turn branch races. `pull_request` also uses the same repository queue around PR preflight and creation so it can wait behind an already-started same-repository mutation. BranchMe rejects dirty worktrees before `change_branch`, `pull_branch`, and `rebase_branch`. It does not force checkout, stash, stage files, create user-authored commits, reset, force-push, create merge commits, or edit files directly. Rebase-driven commit rewriting occurs only through an explicit `rebase_branch` call.
|
|
25
28
|
|
|
26
29
|
## Network behavior
|
|
27
30
|
|
|
31
|
+
`fetch_branch`, `pull_branch`, and `push_branch` contact the configured Git remote through the user's normal Git transport and credentials. They do not use or inject `GITHUB_TOKEN` or `GH_TOKEN`. `rebase_branch` uses the locally available upstream ref and makes no network request.
|
|
32
|
+
|
|
28
33
|
BranchMe's GitHub helpers use these REST API requests:
|
|
29
34
|
|
|
30
35
|
```text
|
|
@@ -45,12 +50,17 @@ BranchMe operates on the current repository only.
|
|
|
45
50
|
- The GitHub repository is inferred from local `origin` and/or `GITHUB_REPOSITORY`.
|
|
46
51
|
- Tool inputs never accept filesystem paths, `owner`, `repo`, or owner-prefixed `owner:branch` PR refs.
|
|
47
52
|
- `change_branch` accepts only `branchName` and never creates branches, checks out remote branches, forces, stashes, or discards changes.
|
|
53
|
+
- `fetch_branch` accepts no parameters, resolves the current branch's configured upstream remote and branch, constructs a source-to-remote-tracking refspec internally, disables tag fetching and submodule recursion, and does not prune or accept arbitrary refspecs.
|
|
54
|
+
- `pull_branch` accepts no parameters, updates only the clean current branch from its configured upstream, and uses fast-forward-only semantics.
|
|
55
|
+
- `rebase_branch` accepts no parameters, rebases only the clean current branch onto its configured upstream, disables autostash and multi-ref updates, never pushes, and attempts to abort on failure.
|
|
48
56
|
- If local `origin` and `GITHUB_REPOSITORY` both resolve but disagree, PR creation and related-PR lookup fail closed.
|
|
49
57
|
- PR branch inputs are validated as existing local branch-name refs; missing local branches and cross-repository `head` values are rejected before token lookup or any GitHub request.
|
|
50
58
|
- PR branch inputs must also be visible on GitHub before the PR is created, and `headBranch` must match the local branch commit; unpublished or stale `headBranch` values fail with guidance to run `push_branch`, wait for it to complete, and retry `pull_request`.
|
|
51
59
|
|
|
52
60
|
## Credentials
|
|
53
61
|
|
|
62
|
+
Git fetch, pull, and push authentication is handled by the user's configured Git credential and transport setup. BranchMe never passes GitHub API tokens to Git commands.
|
|
63
|
+
|
|
54
64
|
`pull_request` and related-PR lookup check `process.env.GITHUB_TOKEN`, then `process.env.GH_TOKEN`. If neither process token is set, BranchMe reads a local `.env` file from the verified git root and checks:
|
|
55
65
|
|
|
56
66
|
- `GITHUB_TOKEN` (preferred)
|
package/docs/SMOKE_TEST.md
CHANGED
|
@@ -18,7 +18,7 @@ pi --no-extensions -e .
|
|
|
18
18
|
## Automated smoke behavior
|
|
19
19
|
|
|
20
20
|
- `npm run smoke:pi` first runs isolated checkout Pi processes from a temporary non-Git working directory: one with `pi --no-extensions -e <package> -e <temporary verifier>` and `/branchmeverify verify`, then one with `pi --no-extensions -e <package>` and `/branchme help`.
|
|
21
|
-
- The temporary command verifier calls `pi.getAllTools()` after BranchMe loads and confirms `branch_status`, `change_branch`, `create_branch`, `push_branch`, and `pull_request` are active runtime tools with strict schemas, prompt guidelines, descriptions, and extension source metadata.
|
|
21
|
+
- The temporary command verifier calls `pi.getAllTools()` after BranchMe loads and confirms `branch_status`, `change_branch`, `fetch_branch`, `pull_branch`, `rebase_branch`, `create_branch`, `push_branch`, and `pull_request` are active runtime tools with strict schemas, prompt guidelines, descriptions, and extension source metadata.
|
|
22
22
|
- A second temporary verifier registers a deterministic local smoke model, blocks `fetch`, and runs normal prompts through real Pi lifecycle handling. It verifies automatic no-tool Git context in a temporary repository, one real `branch_status` tool refresh after a verifier-created local change, safe credential-free related-PR status with no request, and non-Git startup fallback.
|
|
23
23
|
- The checkout command smoke accepts either `/branchme help` text or the read-only BranchMe status fallback as equivalent non-mutating command output.
|
|
24
24
|
- `npm run smoke:pi:packed` creates an npm tarball under a temporary directory, installs that tarball into a separate temporary package with `npm install --omit=dev`, and runs pi against the installed package instead of the source checkout.
|
|
@@ -32,7 +32,7 @@ pi --no-extensions -e .
|
|
|
32
32
|
## Result
|
|
33
33
|
|
|
34
34
|
- `npm run validate` passed.
|
|
35
|
-
- `npm run smoke:pi` loaded BranchMe through Pi, verified all
|
|
35
|
+
- `npm run smoke:pi` loaded BranchMe through Pi, verified all eight BranchMe tools through the real `pi.getAllTools()` runtime surface, and confirmed non-mutating BranchMe command output.
|
|
36
36
|
- The isolated Git-context prompt smoke observed the `before_agent_start` snapshot, answered branch and dirty-tree state without a tool call, refreshed a verifier-created local change through one real `branch_status` call, returned safe unavailable context without credentials or outside Git, and attempted no network request.
|
|
37
37
|
- `npm run smoke:pi:packed` packed BranchMe outside the repository, installed the artifact in a temporary production workspace, loaded the installed package through Pi, and confirmed non-mutating BranchMe command output.
|
|
38
38
|
- `npm run check:pack` confirmed the package contents are limited to public docs, images, source, license, package metadata, `.env.example`, and `tsconfig.json`.
|
package/docs/STRUCTURE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# BranchMe Structure Guide
|
|
2
2
|
|
|
3
|
-
BranchMe is a TypeScript Pi extension package for current-repository git branch, push, and GitHub pull request workflows.
|
|
3
|
+
BranchMe is a TypeScript Pi extension package for current-repository git branch fetch/update/rebase, push, and GitHub pull request workflows.
|
|
4
4
|
|
|
5
5
|
## Source layout
|
|
6
6
|
|
|
@@ -13,7 +13,7 @@ src/
|
|
|
13
13
|
├── commands/
|
|
14
14
|
│ └── branchme-command.ts # /branchme status/help command; informational only
|
|
15
15
|
├── tools/
|
|
16
|
-
│ └── branchme-tools.ts #
|
|
16
|
+
│ └── branchme-tools.ts # registration for eight branch/GitHub workflow tools
|
|
17
17
|
├── git.ts # argv-style git helpers and per-repo workflow queue
|
|
18
18
|
├── github.ts # GitHub repo resolution, env/.env tokens, branch preflight, REST calls, redaction
|
|
19
19
|
└── ui/
|
|
@@ -22,11 +22,11 @@ src/
|
|
|
22
22
|
|
|
23
23
|
## Module boundaries
|
|
24
24
|
|
|
25
|
-
1. `src/extension.ts` stays small and registers the command,
|
|
25
|
+
1. `src/extension.ts` stays small and registers the command, eight tools, and one `before_agent_start` context hook.
|
|
26
26
|
2. `src/git-context.ts` owns the shared read-only collector, escaped/bounded formatter, automatic system-prompt append, and the current-state output used by `branch_status`.
|
|
27
27
|
3. `src/commands/branchme-command.ts` parses `/branchme`, `/branchme help`, `--help`, and `-h`; it never performs git or GitHub mutations and avoids raw stdout in JSON mode.
|
|
28
28
|
4. `src/tools/branchme-tools.ts` owns TypeBox schemas, prompt metadata, tool content, and safe structured details. `branch_status` delegates to the shared context collector for an explicit refresh.
|
|
29
|
-
5. `src/git.ts` owns current-repository git behavior: root detection, branch/upstream/ahead-behind inspection, working-tree parsing, recent-commit collection, branch validation, branch creation, existing-local-branch switching, clean-worktree preflight, current-branch push/publish, and the per-repository workflow queue.
|
|
29
|
+
5. `src/git.ts` owns current-repository git behavior: root detection, branch/upstream/ahead-behind inspection, working-tree parsing, recent-commit collection, branch validation, branch creation, existing-local-branch switching, configured-upstream fetch, clean-worktree preflight, fast-forward-only current-branch pull, current-branch rebase with automatic abort on failure, current-branch push/publish, and the per-repository workflow queue.
|
|
30
30
|
6. `src/github.ts` owns GitHub `owner/repo` parsing, repository boundary checks, `GITHUB_TOKEN`/`GH_TOKEN` process-env and hardened git-root `.env` fallback resolution, authenticated related-open-PR lookup, PR branch-name syntax validation, GitHub branch visibility/commit preflight, PR REST calls, bounded response validation, and redacted errors.
|
|
31
31
|
7. `src/redaction.ts` owns shared credential redaction for Git, GitHub, and prompt-bound metadata.
|
|
32
32
|
8. `src/types.ts` keeps serializable details shared by helpers, context, and tools.
|
|
@@ -36,7 +36,7 @@ src/
|
|
|
36
36
|
|
|
37
37
|
- No long-lived processes, watchers, timers, sockets, or background jobs start in the extension factory.
|
|
38
38
|
- A single `before_agent_start` handler synchronously collects a fresh snapshot for each agent run and appends it to the existing system prompt; failures degrade to bounded unavailable context rather than blocking startup.
|
|
39
|
-
- Slash commands are informational; tools perform branch, push, and PR actions. There is no context command
|
|
39
|
+
- Slash commands are informational; tools perform branch fetch/update/rebase, push, and PR actions. There is no context command.
|
|
40
40
|
- Every tool uses a strict TypeBox object schema with `additionalProperties: false`.
|
|
41
41
|
- Every tool defines a description, `promptSnippet`, and tool-specific `promptGuidelines` that explicitly name the tool.
|
|
42
42
|
- Git commands use `pi.exec("git", args, { cwd, signal, timeout })` with argv arrays; repository mutations run from the verified git root and same-repository mutation/PR windows are serialized per repository.
|
|
@@ -51,11 +51,14 @@ src/
|
|
|
51
51
|
- Related-PR lookup may issue an authenticated `GET /pulls` before every agent run and on explicit refresh. It has a 4-second timeout and 64 KiB response limit, and makes no unauthenticated fallback request.
|
|
52
52
|
- The start-of-run snapshot may be stale after a mutation in that same run; `branch_status` is the explicit read-only refresh.
|
|
53
53
|
- `change_branch` mutates local HEAD and working-tree files only through `git switch <branchName>` for existing local branches after a clean-worktree preflight.
|
|
54
|
+
- `fetch_branch` requires a configured upstream and runs `git fetch --no-tags --no-recurse-submodules <remote> <remote-ref>:<remote-tracking-ref>`; its explicit refspec updates only that tracking ref without changing local branches or working-tree files.
|
|
55
|
+
- `pull_branch` requires a clean worktree and configured upstream, then updates only the current branch with an explicit `git pull --ff-only --no-rebase --no-autostash <remote> <remote-ref>` command; divergence fails without a rebase or merge commit.
|
|
56
|
+
- `rebase_branch` requires a clean worktree and configured upstream, then rebases only the current branch with `git rebase --no-autostash --no-update-refs <upstream>`; it rewrites local commits and automatically attempts `git rebase --abort` on failure.
|
|
54
57
|
- `create_branch` mutates local branch/HEAD only with `git switch -c`.
|
|
55
58
|
- `push_branch` mutates remote refs only for the current branch and uses an explicit upstream remote/refspec instead of bare `git push` when an upstream exists.
|
|
56
59
|
- `pull_request` requires `headBranch` and `baseBranch` to exist locally, requires `headBranch` to match the GitHub-visible branch commit, queues behind already-started same-repository git mutation windows, makes GitHub REST API calls for the resolved current repository only, and rejects missing, owner-prefixed, or unsafe branch refs before token lookup or the request.
|
|
57
60
|
- `pull_request` reads `GITHUB_TOKEN` or `GH_TOKEN` from process environment first; only when neither process token is set does it read those token keys from a small regular `.env` file in the verified git root as a fallback.
|
|
58
|
-
- BranchMe does not force checkout, stash, stage,
|
|
61
|
+
- BranchMe does not force checkout, stash, stage, create user-authored commits, reset, force-push, create merge commits, directly edit files, read non-token `.env` keys, follow unsafe `.env` file types, depend on GitHub CLI, or collect telemetry. Rebase-driven commit rewriting occurs only through explicit `rebase_branch` calls.
|
|
59
62
|
|
|
60
63
|
## Documentation
|
|
61
64
|
|
package/docs/TUI_CAPTURE.md
CHANGED
|
@@ -23,16 +23,22 @@ Commands only show info; BranchMe tools perform actions.
|
|
|
23
23
|
## Workflow
|
|
24
24
|
|
|
25
25
|
1. `branch_status` — inspect repo and branch state.
|
|
26
|
-
2. `change_branch`
|
|
27
|
-
3.
|
|
28
|
-
4. `
|
|
29
|
-
5. `
|
|
26
|
+
2. `change_branch` — switch to a clean existing local branch.
|
|
27
|
+
3. `fetch_branch` — fetch its configured upstream remote without changing local files.
|
|
28
|
+
4. `pull_branch` — fast-forward from upstream, or `rebase_branch` — rebase local commits onto upstream.
|
|
29
|
+
5. `create_branch` — create a new branch from the updated `HEAD`.
|
|
30
|
+
6. Commit outside BranchMe.
|
|
31
|
+
7. `push_branch` — push the current branch.
|
|
32
|
+
8. `pull_request` — open a PR after `push_branch` completes and GitHub sees the branches.
|
|
30
33
|
|
|
31
34
|
## Requirements
|
|
32
35
|
|
|
33
36
|
- Run inside a Git repo with `git` available.
|
|
34
37
|
- For PRs: GitHub `origin` and `GITHUB_TOKEN` or `GH_TOKEN` (environment or `.env`).
|
|
35
|
-
-
|
|
38
|
+
- `fetch_branch`, `pull_branch`, and `rebase_branch` require a configured upstream.
|
|
39
|
+
- `pull_branch` and `rebase_branch` require a clean working tree.
|
|
40
|
+
- `rebase_branch` rewrites local commits only when explicitly requested and auto-aborts on failure.
|
|
41
|
+
- BranchMe never stages, creates user-authored commits, force-pushes, or creates merge commits.
|
|
36
42
|
```
|
|
37
43
|
|
|
38
44
|
## Panel: Tiny mode: clean branch with token
|
|
@@ -62,6 +68,8 @@ Width: 40
|
|
|
62
68
|
│ │
|
|
63
69
|
│ │
|
|
64
70
|
│ │
|
|
71
|
+
│ │
|
|
72
|
+
│ │
|
|
65
73
|
├──────────────────────────────────────┤
|
|
66
74
|
│ 1/2 • status • current repository on…│
|
|
67
75
|
╰──────────────────────────────────────╯
|
|
@@ -82,6 +90,8 @@ Width: 80
|
|
|
82
90
|
│ │ │
|
|
83
91
|
│ │ │
|
|
84
92
|
│ │ │
|
|
93
|
+
│ │ │
|
|
94
|
+
│ │ │
|
|
85
95
|
├─────────────────────┴────────────────────────────────────────────────────────┤
|
|
86
96
|
│ 1/2 • status • current repository only • tools perform actions │
|
|
87
97
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
@@ -98,12 +108,14 @@ Width: 80
|
|
|
98
108
|
│ Status │ WORKFLOW │
|
|
99
109
|
│▶ Workflow │ branch_status -> inspect │
|
|
100
110
|
│ │ change_branch -> existing local │
|
|
111
|
+
│ │ fetch_branch -> upstream remote │
|
|
112
|
+
│ │ pull_branch -> fast-forward │
|
|
113
|
+
│ │ rebase_branch -> onto upstream │
|
|
101
114
|
│ │ create_branch -> from HEAD │
|
|
102
115
|
│ │ push_branch -> current branch │
|
|
103
116
|
│ │ pull_request -> after push │
|
|
104
|
-
│ │ │
|
|
105
117
|
├─────────────────────┴────────────────────────────────────────────────────────┤
|
|
106
|
-
│ 2/2 • workflow • inspect → change/create → push → PR
|
|
118
|
+
│ 2/2 • workflow • inspect → change → fetch/pull/rebase → create → push → PR │
|
|
107
119
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
108
120
|
```
|
|
109
121
|
|
|
@@ -122,6 +134,8 @@ Width: 112
|
|
|
122
134
|
│ │ │
|
|
123
135
|
│ │ │
|
|
124
136
|
│ │ │
|
|
137
|
+
│ │ │
|
|
138
|
+
│ │ │
|
|
125
139
|
├──────────────────────┴───────────────────────────────────────────────────────────────────────┤
|
|
126
140
|
│ 1/2 • status • current repository only • tools perform actions │
|
|
127
141
|
╰──────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
@@ -143,6 +157,8 @@ Width: 50
|
|
|
143
157
|
│ │
|
|
144
158
|
│ │
|
|
145
159
|
│ │
|
|
160
|
+
│ │
|
|
161
|
+
│ │
|
|
146
162
|
├────────────────────────────────────────────────┤
|
|
147
163
|
│ 1/2 • warning • Unable to resolve a GitHub rep…│
|
|
148
164
|
╰────────────────────────────────────────────────╯
|
|
@@ -163,6 +179,8 @@ Width: 80
|
|
|
163
179
|
│ │ │
|
|
164
180
|
│ │ │
|
|
165
181
|
│ │ │
|
|
182
|
+
│ │ │
|
|
183
|
+
│ │ │
|
|
166
184
|
├─────────────────────┴────────────────────────────────────────────────────────┤
|
|
167
185
|
│ 1/2 • warning • Repository boundary mismatch: local origin resolves to senad…│
|
|
168
186
|
╰──────────────────────────────────────────────────────────────────────────────╯
|
|
@@ -183,6 +201,8 @@ Width: 72
|
|
|
183
201
|
│ │ │
|
|
184
202
|
│ │ │
|
|
185
203
|
│ │ │
|
|
204
|
+
│ │ │
|
|
205
|
+
│ │ │
|
|
186
206
|
├───────────────────┴──────────────────────────────────────────────────┤
|
|
187
207
|
│ 1/2 • warning • Unable to read .env file for GitHub token fallback: …│
|
|
188
208
|
╰──────────────────────────────────────────────────────────────────────╯
|
|
@@ -203,6 +223,8 @@ Width: 72
|
|
|
203
223
|
│ │ │
|
|
204
224
|
│ │ │
|
|
205
225
|
│ │ │
|
|
226
|
+
│ │ │
|
|
227
|
+
│ │ │
|
|
206
228
|
├───────────────────┴──────────────────────────────────────────────────┤
|
|
207
229
|
│ 1/2 • warning • This deliberately long status note is captured to de…│
|
|
208
230
|
╰──────────────────────────────────────────────────────────────────────╯
|
package/package.json
CHANGED
|
@@ -32,16 +32,22 @@ export function getBranchMeHelpText(): string {
|
|
|
32
32
|
"## Workflow",
|
|
33
33
|
"",
|
|
34
34
|
"1. `branch_status` — inspect repo and branch state.",
|
|
35
|
-
"2. `change_branch`
|
|
36
|
-
"3.
|
|
37
|
-
"4. `
|
|
38
|
-
"5. `
|
|
35
|
+
"2. `change_branch` — switch to a clean existing local branch.",
|
|
36
|
+
"3. `fetch_branch` — fetch its configured upstream remote without changing local files.",
|
|
37
|
+
"4. `pull_branch` — fast-forward from upstream, or `rebase_branch` — rebase local commits onto upstream.",
|
|
38
|
+
"5. `create_branch` — create a new branch from the updated `HEAD`.",
|
|
39
|
+
"6. Commit outside BranchMe.",
|
|
40
|
+
"7. `push_branch` — push the current branch.",
|
|
41
|
+
"8. `pull_request` — open a PR after `push_branch` completes and GitHub sees the branches.",
|
|
39
42
|
"",
|
|
40
43
|
"## Requirements",
|
|
41
44
|
"",
|
|
42
45
|
"- Run inside a Git repo with `git` available.",
|
|
43
46
|
"- For PRs: GitHub `origin` and `GITHUB_TOKEN` or `GH_TOKEN` (environment or `.env`).",
|
|
44
|
-
"-
|
|
47
|
+
"- `fetch_branch`, `pull_branch`, and `rebase_branch` require a configured upstream.",
|
|
48
|
+
"- `pull_branch` and `rebase_branch` require a clean working tree.",
|
|
49
|
+
"- `rebase_branch` rewrites local commits only when explicitly requested and auto-aborts on failure.",
|
|
50
|
+
"- BranchMe never stages, creates user-authored commits, force-pushes, or creates merge commits.",
|
|
45
51
|
].join("\n");
|
|
46
52
|
}
|
|
47
53
|
|
package/src/constants.ts
CHANGED
|
@@ -4,6 +4,9 @@ export const BRANCHME_COMMAND_NAME = "branchme";
|
|
|
4
4
|
export const BRANCH_STATUS_TOOL_NAME = "branch_status";
|
|
5
5
|
export const CREATE_BRANCH_TOOL_NAME = "create_branch";
|
|
6
6
|
export const CHANGE_BRANCH_TOOL_NAME = "change_branch";
|
|
7
|
+
export const FETCH_BRANCH_TOOL_NAME = "fetch_branch";
|
|
8
|
+
export const PULL_BRANCH_TOOL_NAME = "pull_branch";
|
|
9
|
+
export const REBASE_BRANCH_TOOL_NAME = "rebase_branch";
|
|
7
10
|
export const PUSH_BRANCH_TOOL_NAME = "push_branch";
|
|
8
11
|
export const PULL_REQUEST_TOOL_NAME = "pull_request";
|
|
9
12
|
|
|
@@ -11,12 +14,18 @@ export const BRANCHME_TOOL_NAMES = [
|
|
|
11
14
|
BRANCH_STATUS_TOOL_NAME,
|
|
12
15
|
CREATE_BRANCH_TOOL_NAME,
|
|
13
16
|
CHANGE_BRANCH_TOOL_NAME,
|
|
17
|
+
FETCH_BRANCH_TOOL_NAME,
|
|
18
|
+
PULL_BRANCH_TOOL_NAME,
|
|
19
|
+
REBASE_BRANCH_TOOL_NAME,
|
|
14
20
|
PUSH_BRANCH_TOOL_NAME,
|
|
15
21
|
PULL_REQUEST_TOOL_NAME,
|
|
16
22
|
] as const;
|
|
17
23
|
|
|
18
24
|
export const GIT_STATUS_TIMEOUT_MS = 5_000;
|
|
19
25
|
export const GIT_MUTATION_TIMEOUT_MS = 30_000;
|
|
26
|
+
export const GIT_FETCH_TIMEOUT_MS = 120_000;
|
|
27
|
+
export const GIT_PULL_TIMEOUT_MS = 120_000;
|
|
28
|
+
export const GIT_REBASE_TIMEOUT_MS = 120_000;
|
|
20
29
|
export const GIT_PUSH_TIMEOUT_MS = 120_000;
|
|
21
30
|
export const GITHUB_RELATED_PR_TIMEOUT_MS = 4_000;
|
|
22
31
|
|
package/src/git.ts
CHANGED
|
@@ -3,8 +3,11 @@ import {
|
|
|
3
3
|
GIT_CONTEXT_CHANGE_LIMIT,
|
|
4
4
|
GIT_CONTEXT_RECENT_COMMIT_LIMIT,
|
|
5
5
|
GIT_CONTEXT_VALUE_LIMIT_CHARS,
|
|
6
|
+
GIT_FETCH_TIMEOUT_MS,
|
|
6
7
|
GIT_MUTATION_TIMEOUT_MS,
|
|
8
|
+
GIT_PULL_TIMEOUT_MS,
|
|
7
9
|
GIT_PUSH_TIMEOUT_MS,
|
|
10
|
+
GIT_REBASE_TIMEOUT_MS,
|
|
8
11
|
GIT_STATUS_TIMEOUT_MS,
|
|
9
12
|
MAX_SUMMARY_OUTPUT_CHARS,
|
|
10
13
|
} from "./constants.ts";
|
|
@@ -15,10 +18,13 @@ import type {
|
|
|
15
18
|
ChangeBranchDetails,
|
|
16
19
|
CreateBranchDetails,
|
|
17
20
|
CurrentBranchInfo,
|
|
21
|
+
FetchBranchDetails,
|
|
18
22
|
GitExecResult,
|
|
19
23
|
GitFileChange,
|
|
20
24
|
GitFileChangeSummary,
|
|
25
|
+
PullBranchDetails,
|
|
21
26
|
PushBranchDetails,
|
|
27
|
+
RebaseBranchDetails,
|
|
22
28
|
RecentCommit,
|
|
23
29
|
WorkingTreeDetails,
|
|
24
30
|
} from "./types.ts";
|
|
@@ -182,6 +188,12 @@ export async function getUpstreamBranch(
|
|
|
182
188
|
return upstream || null;
|
|
183
189
|
}
|
|
184
190
|
|
|
191
|
+
interface ConfiguredUpstreamTarget {
|
|
192
|
+
upstream: string;
|
|
193
|
+
remote: string;
|
|
194
|
+
remoteRef: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
185
197
|
interface PushTarget {
|
|
186
198
|
upstream: string | null;
|
|
187
199
|
mode: "push" | "publish";
|
|
@@ -191,21 +203,23 @@ interface PushTarget {
|
|
|
191
203
|
args: string[];
|
|
192
204
|
}
|
|
193
205
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (remote
|
|
206
|
+
type UpstreamOperation = "fetch" | "pull" | "push" | "rebase";
|
|
207
|
+
|
|
208
|
+
function validateRemoteName(remote: string, operation: UpstreamOperation): void {
|
|
209
|
+
if (!remote) throw new Error(`Unable to ${operation} current branch: upstream remote is missing.`);
|
|
210
|
+
if (remote === ".") throw new Error(`Unable to ${operation} current branch: upstream is a local branch, not a remote.`);
|
|
211
|
+
if (remote.startsWith("-")) throw new Error(`Unable to ${operation} current branch: upstream remote cannot start with '-'.`);
|
|
198
212
|
if (remote.includes(":") || remote.includes("@")) {
|
|
199
|
-
throw new Error(
|
|
213
|
+
throw new Error(`Unable to ${operation} current branch: upstream remote name cannot be a URL or user-prefixed target.`);
|
|
200
214
|
}
|
|
201
215
|
if (/[\u0000-\u001f\u007f]/u.test(remote) || /\s/u.test(remote)) {
|
|
202
|
-
throw new Error(
|
|
216
|
+
throw new Error(`Unable to ${operation} current branch: upstream remote contains whitespace or control characters.`);
|
|
203
217
|
}
|
|
204
218
|
}
|
|
205
219
|
|
|
206
|
-
function normalizeRemoteHeadRef(mergeRef: string): string {
|
|
220
|
+
function normalizeRemoteHeadRef(mergeRef: string, operation: UpstreamOperation): string {
|
|
207
221
|
if (!mergeRef.startsWith("refs/heads/")) {
|
|
208
|
-
throw new Error(
|
|
222
|
+
throw new Error(`Unable to ${operation} current branch: upstream merge ref is not a branch ref.`);
|
|
209
223
|
}
|
|
210
224
|
|
|
211
225
|
const branchName = mergeRef.slice("refs/heads/".length);
|
|
@@ -231,15 +245,50 @@ async function getBranchConfigValue(
|
|
|
231
245
|
return value || null;
|
|
232
246
|
}
|
|
233
247
|
|
|
234
|
-
|
|
248
|
+
function remoteTrackingRef(upstream: string, remote: string): string {
|
|
249
|
+
const prefix = `${remote}/`;
|
|
250
|
+
if (!upstream.startsWith(prefix)) {
|
|
251
|
+
throw new Error("Unable to fetch current branch: configured upstream does not match its remote.");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const trackingBranch = upstream.slice(prefix.length);
|
|
255
|
+
validateBranchNameInput(trackingBranch, "Upstream branch name");
|
|
256
|
+
return `refs/remotes/${upstream}`;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function resolveConfiguredUpstreamTarget(
|
|
235
260
|
pi: Pick<ExtensionAPI, "exec">,
|
|
236
261
|
ctx: GitCommandContext,
|
|
237
262
|
currentBranch: string,
|
|
263
|
+
operation: UpstreamOperation,
|
|
238
264
|
signal?: AbortSignal,
|
|
239
|
-
): Promise<
|
|
265
|
+
): Promise<ConfiguredUpstreamTarget | null> {
|
|
240
266
|
validateBranchNameInput(currentBranch);
|
|
241
267
|
const upstream = await getUpstreamBranch(pi, ctx, signal);
|
|
242
|
-
if (!upstream)
|
|
268
|
+
if (!upstream) return null;
|
|
269
|
+
|
|
270
|
+
const remote = await getBranchConfigValue(pi, ctx, currentBranch, "remote", signal);
|
|
271
|
+
const mergeRef = await getBranchConfigValue(pi, ctx, currentBranch, "merge", signal);
|
|
272
|
+
if (!remote || !mergeRef) {
|
|
273
|
+
throw new Error(`Unable to ${operation} current branch: upstream exists but branch remote/merge configuration is incomplete.`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
validateRemoteName(remote, operation);
|
|
277
|
+
return {
|
|
278
|
+
upstream,
|
|
279
|
+
remote,
|
|
280
|
+
remoteRef: normalizeRemoteHeadRef(mergeRef, operation),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function resolvePushTarget(
|
|
285
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
286
|
+
ctx: GitCommandContext,
|
|
287
|
+
currentBranch: string,
|
|
288
|
+
signal?: AbortSignal,
|
|
289
|
+
): Promise<PushTarget> {
|
|
290
|
+
const upstreamTarget = await resolveConfiguredUpstreamTarget(pi, ctx, currentBranch, "push", signal);
|
|
291
|
+
if (!upstreamTarget) {
|
|
243
292
|
const remoteRef = `refs/heads/${currentBranch}`;
|
|
244
293
|
return {
|
|
245
294
|
upstream: null,
|
|
@@ -251,22 +300,12 @@ async function resolvePushTarget(
|
|
|
251
300
|
};
|
|
252
301
|
}
|
|
253
302
|
|
|
254
|
-
const
|
|
255
|
-
const mergeRef = await getBranchConfigValue(pi, ctx, currentBranch, "merge", signal);
|
|
256
|
-
if (!remote || !mergeRef) {
|
|
257
|
-
throw new Error("Unable to push current branch: upstream exists but branch remote/merge configuration is incomplete.");
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
validateRemoteName(remote);
|
|
261
|
-
const remoteRef = normalizeRemoteHeadRef(mergeRef);
|
|
262
|
-
const refspec = `HEAD:${remoteRef}`;
|
|
303
|
+
const refspec = `HEAD:${upstreamTarget.remoteRef}`;
|
|
263
304
|
return {
|
|
264
|
-
|
|
305
|
+
...upstreamTarget,
|
|
265
306
|
mode: "push",
|
|
266
|
-
remote,
|
|
267
|
-
remoteRef,
|
|
268
307
|
refspec,
|
|
269
|
-
args: ["push", remote, refspec],
|
|
308
|
+
args: ["push", upstreamTarget.remote, refspec],
|
|
270
309
|
};
|
|
271
310
|
}
|
|
272
311
|
|
|
@@ -676,6 +715,153 @@ export async function changeExistingLocalBranch(
|
|
|
676
715
|
});
|
|
677
716
|
}
|
|
678
717
|
|
|
718
|
+
export async function fetchCurrentBranch(
|
|
719
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
720
|
+
ctx: GitCommandContext,
|
|
721
|
+
signal?: AbortSignal,
|
|
722
|
+
): Promise<FetchBranchDetails> {
|
|
723
|
+
const repoRoot = await getGitRoot(pi, ctx, signal);
|
|
724
|
+
const rootCtx = { cwd: repoRoot };
|
|
725
|
+
|
|
726
|
+
return withRepositoryMutationQueue(repoRoot, async () => {
|
|
727
|
+
const currentBranch = await requireCurrentBranch(pi, rootCtx, signal);
|
|
728
|
+
const target = await resolveConfiguredUpstreamTarget(pi, rootCtx, currentBranch, "fetch", signal);
|
|
729
|
+
if (!target) {
|
|
730
|
+
throw new Error(`Unable to fetch current branch '${safeDetail(currentBranch)}': no upstream is configured.`);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const trackingRef = remoteTrackingRef(target.upstream, target.remote);
|
|
734
|
+
const refspec = `${target.remoteRef}:${trackingRef}`;
|
|
735
|
+
const result = await runGit(
|
|
736
|
+
pi,
|
|
737
|
+
rootCtx,
|
|
738
|
+
["fetch", "--no-tags", "--no-recurse-submodules", target.remote, refspec],
|
|
739
|
+
{
|
|
740
|
+
signal,
|
|
741
|
+
timeout: GIT_FETCH_TIMEOUT_MS,
|
|
742
|
+
},
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
return {
|
|
746
|
+
repoRoot,
|
|
747
|
+
currentBranch: safeDetail(currentBranch),
|
|
748
|
+
upstream: safeDetail(target.upstream),
|
|
749
|
+
remote: safeDetail(target.remote),
|
|
750
|
+
remoteRef: safeDetail(target.remoteRef),
|
|
751
|
+
remoteTrackingRef: safeDetail(trackingRef),
|
|
752
|
+
refspec: safeDetail(refspec),
|
|
753
|
+
output: safeOutput(result.stdout || result.stderr),
|
|
754
|
+
};
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
export async function pullCurrentBranch(
|
|
759
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
760
|
+
ctx: GitCommandContext,
|
|
761
|
+
signal?: AbortSignal,
|
|
762
|
+
): Promise<PullBranchDetails> {
|
|
763
|
+
const repoRoot = await getGitRoot(pi, ctx, signal);
|
|
764
|
+
const rootCtx = { cwd: repoRoot };
|
|
765
|
+
|
|
766
|
+
return withRepositoryMutationQueue(repoRoot, async () => {
|
|
767
|
+
const currentBranch = await requireCurrentBranch(pi, rootCtx, signal);
|
|
768
|
+
if (await hasWorkingTreeChanges(pi, rootCtx, signal)) {
|
|
769
|
+
throw new Error("Working tree has uncommitted changes; clean it before pulling the current branch.");
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
const target = await resolveConfiguredUpstreamTarget(pi, rootCtx, currentBranch, "pull", signal);
|
|
773
|
+
if (!target) {
|
|
774
|
+
throw new Error(`Unable to pull current branch '${safeDetail(currentBranch)}': no upstream is configured.`);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
const result = await runGit(pi, rootCtx, ["pull", "--ff-only", "--no-rebase", "--no-autostash", target.remote, target.remoteRef], {
|
|
778
|
+
signal,
|
|
779
|
+
timeout: GIT_PULL_TIMEOUT_MS,
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
return {
|
|
783
|
+
repoRoot,
|
|
784
|
+
currentBranch: safeDetail(currentBranch),
|
|
785
|
+
upstream: safeDetail(target.upstream),
|
|
786
|
+
remote: safeDetail(target.remote),
|
|
787
|
+
remoteRef: safeDetail(target.remoteRef),
|
|
788
|
+
output: safeOutput(result.stdout || result.stderr),
|
|
789
|
+
};
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
async function abortFailedRebase(
|
|
794
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
795
|
+
ctx: GitCommandContext,
|
|
796
|
+
failureMessage: string,
|
|
797
|
+
): Promise<never> {
|
|
798
|
+
const abortArgs = ["rebase", "--abort"];
|
|
799
|
+
let abortResult: GitExecResult;
|
|
800
|
+
try {
|
|
801
|
+
abortResult = await runGit(pi, ctx, abortArgs, {
|
|
802
|
+
timeout: GIT_MUTATION_TIMEOUT_MS,
|
|
803
|
+
allowFailure: true,
|
|
804
|
+
});
|
|
805
|
+
} catch (error) {
|
|
806
|
+
const reason = safeOutput(error instanceof Error ? error.message : String(error)) || "git rebase --abort failed";
|
|
807
|
+
throw new Error(`${failureMessage}. Automatic rebase cleanup failed: ${reason}. Inspect the repository before continuing.`);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (abortResult.code === 0) {
|
|
811
|
+
throw new Error(`${failureMessage}. Rebase was aborted and the current branch was restored.`);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const reason = safeOutput(abortResult.stderr || abortResult.stdout) || `exit code ${abortResult.code}`;
|
|
815
|
+
throw new Error(`${failureMessage}. Automatic git rebase --abort did not complete: ${reason}. Inspect the repository before continuing.`);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export async function rebaseCurrentBranch(
|
|
819
|
+
pi: Pick<ExtensionAPI, "exec">,
|
|
820
|
+
ctx: GitCommandContext,
|
|
821
|
+
signal?: AbortSignal,
|
|
822
|
+
): Promise<RebaseBranchDetails> {
|
|
823
|
+
const repoRoot = await getGitRoot(pi, ctx, signal);
|
|
824
|
+
const rootCtx = { cwd: repoRoot };
|
|
825
|
+
|
|
826
|
+
return withRepositoryMutationQueue(repoRoot, async () => {
|
|
827
|
+
const currentBranch = await requireCurrentBranch(pi, rootCtx, signal);
|
|
828
|
+
if (await hasWorkingTreeChanges(pi, rootCtx, signal)) {
|
|
829
|
+
throw new Error("Working tree has uncommitted changes; clean it before rebasing the current branch.");
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const target = await resolveConfiguredUpstreamTarget(pi, rootCtx, currentBranch, "rebase", signal);
|
|
833
|
+
if (!target) {
|
|
834
|
+
throw new Error(`Unable to rebase current branch '${safeDetail(currentBranch)}': no upstream is configured.`);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
const args = ["rebase", "--no-autostash", "--no-update-refs", target.upstream];
|
|
838
|
+
let result: GitExecResult;
|
|
839
|
+
try {
|
|
840
|
+
result = await runGit(pi, rootCtx, args, {
|
|
841
|
+
signal,
|
|
842
|
+
timeout: GIT_REBASE_TIMEOUT_MS,
|
|
843
|
+
allowFailure: true,
|
|
844
|
+
});
|
|
845
|
+
} catch (error) {
|
|
846
|
+
const failureMessage = safeOutput(error instanceof Error ? error.message : String(error)) || "git rebase failed";
|
|
847
|
+
return abortFailedRebase(pi, rootCtx, failureMessage);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (result.code !== 0) {
|
|
851
|
+
return abortFailedRebase(pi, rootCtx, formatGitFailure(args, result));
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return {
|
|
855
|
+
repoRoot,
|
|
856
|
+
currentBranch: safeDetail(currentBranch),
|
|
857
|
+
upstream: safeDetail(target.upstream),
|
|
858
|
+
remote: safeDetail(target.remote),
|
|
859
|
+
remoteRef: safeDetail(target.remoteRef),
|
|
860
|
+
output: safeOutput(result.stdout || result.stderr),
|
|
861
|
+
};
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
|
|
679
865
|
export async function pushCurrentBranch(
|
|
680
866
|
pi: Pick<ExtensionAPI, "exec">,
|
|
681
867
|
ctx: GitCommandContext,
|
|
@@ -4,16 +4,22 @@ import {
|
|
|
4
4
|
BRANCH_STATUS_TOOL_NAME,
|
|
5
5
|
CHANGE_BRANCH_TOOL_NAME,
|
|
6
6
|
CREATE_BRANCH_TOOL_NAME,
|
|
7
|
+
FETCH_BRANCH_TOOL_NAME,
|
|
8
|
+
PULL_BRANCH_TOOL_NAME,
|
|
7
9
|
PULL_REQUEST_TOOL_NAME,
|
|
8
10
|
PUSH_BRANCH_TOOL_NAME,
|
|
11
|
+
REBASE_BRANCH_TOOL_NAME,
|
|
9
12
|
} from "../constants.ts";
|
|
10
13
|
import {
|
|
11
14
|
changeExistingLocalBranch,
|
|
12
15
|
createLocalBranch,
|
|
16
|
+
fetchCurrentBranch,
|
|
13
17
|
getGitRoot,
|
|
14
18
|
getLocalBranchCommit,
|
|
15
19
|
localBranchExists,
|
|
20
|
+
pullCurrentBranch,
|
|
16
21
|
pushCurrentBranch,
|
|
22
|
+
rebaseCurrentBranch,
|
|
17
23
|
validateBranchName,
|
|
18
24
|
withRepositoryMutationQueue,
|
|
19
25
|
} from "../git.ts";
|
|
@@ -197,6 +203,67 @@ export function registerBranchMeTools(pi: Pick<ExtensionAPI, "registerTool" | "e
|
|
|
197
203
|
},
|
|
198
204
|
});
|
|
199
205
|
|
|
206
|
+
pi.registerTool({
|
|
207
|
+
name: FETCH_BRANCH_TOOL_NAME,
|
|
208
|
+
label: "Fetch Branch",
|
|
209
|
+
description: "fetch_branch fetches the current branch's configured upstream branch into its remote-tracking ref with an explicit git fetch --no-tags --no-recurse-submodules refspec. fetch_branch does not change local branches or working-tree files and never prunes, rebases, merges, stashes, stages, commits, or pushes.",
|
|
210
|
+
promptSnippet: "fetch_branch: fetch the current branch's configured upstream into its remote-tracking ref without changing local branches or files",
|
|
211
|
+
promptGuidelines: [
|
|
212
|
+
"Use fetch_branch only when the user explicitly wants to fetch the configured upstream branch for the current branch.",
|
|
213
|
+
"Use fetch_branch only on a current branch with an upstream; fetch_branch has no branchName, remote, tags, prune, force, or refspec parameters.",
|
|
214
|
+
"Call fetch_branch and wait for it to complete before rebase_branch when the user wants the latest upstream state; do not batch fetch_branch with rebase_branch.",
|
|
215
|
+
],
|
|
216
|
+
parameters: EmptyParametersSchema,
|
|
217
|
+
async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
|
|
218
|
+
const details = await fetchCurrentBranch(pi, ctx, signal);
|
|
219
|
+
return {
|
|
220
|
+
content: [{ type: "text", text: `Fetched configured upstream remote ${details.remote} for current branch ${details.currentBranch}.` }],
|
|
221
|
+
details,
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
pi.registerTool({
|
|
227
|
+
name: PULL_BRANCH_TOOL_NAME,
|
|
228
|
+
label: "Pull Branch",
|
|
229
|
+
description: "pull_branch updates the current branch from its configured upstream with git pull --ff-only --no-rebase --no-autostash. pull_branch requires a clean working tree and never rebases, creates merge commits, force-updates, stashes, stages, or commits.",
|
|
230
|
+
promptSnippet: "pull_branch: fast-forward the clean current branch from its configured upstream without rebasing",
|
|
231
|
+
promptGuidelines: [
|
|
232
|
+
"Use pull_branch only when the user explicitly wants to update the current branch from its configured upstream.",
|
|
233
|
+
"Use pull_branch only on a clean working tree with an upstream; pull_branch has no branchName, remote, force, or rebase parameters.",
|
|
234
|
+
"Do not call pull_branch in the same tool batch as change_branch or create_branch; call pull_branch after change_branch completes, then call create_branch after pull_branch completes.",
|
|
235
|
+
],
|
|
236
|
+
parameters: EmptyParametersSchema,
|
|
237
|
+
async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
|
|
238
|
+
const details = await pullCurrentBranch(pi, ctx, signal);
|
|
239
|
+
return {
|
|
240
|
+
content: [{ type: "text", text: `Pulled current branch ${details.currentBranch} with fast-forward-only semantics.` }],
|
|
241
|
+
details,
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
pi.registerTool({
|
|
247
|
+
name: REBASE_BRANCH_TOOL_NAME,
|
|
248
|
+
label: "Rebase Branch",
|
|
249
|
+
description: "rebase_branch rebases the clean current branch onto its configured upstream with git rebase --no-autostash --no-update-refs. rebase_branch rewrites local commits, never pushes or force-pushes, and automatically attempts git rebase --abort if the rebase fails.",
|
|
250
|
+
promptSnippet: "rebase_branch: rebase the clean current branch onto its configured upstream with automatic abort on failure",
|
|
251
|
+
promptGuidelines: [
|
|
252
|
+
"Use rebase_branch only when the user explicitly requests rebasing and accepts that rebase_branch rewrites local commit history.",
|
|
253
|
+
"Use rebase_branch only on a clean current branch with an upstream; rebase_branch has no branchName, base, remote, force, autostash, continue, or abort parameters.",
|
|
254
|
+
"Call fetch_branch and wait for it to complete before rebase_branch when the latest upstream state is required; do not batch rebase_branch with fetch_branch, change_branch, pull_branch, create_branch, push_branch, or pull_request.",
|
|
255
|
+
"If rebase_branch fails or conflicts, rebase_branch automatically attempts git rebase --abort and never leaves conflict resolution to another parallel tool call.",
|
|
256
|
+
],
|
|
257
|
+
parameters: EmptyParametersSchema,
|
|
258
|
+
async execute(_toolCallId, _params, signal, _onUpdate, ctx) {
|
|
259
|
+
const details = await rebaseCurrentBranch(pi, ctx, signal);
|
|
260
|
+
return {
|
|
261
|
+
content: [{ type: "text", text: `Rebased current branch ${details.currentBranch} onto ${details.upstream}.` }],
|
|
262
|
+
details,
|
|
263
|
+
};
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
|
|
200
267
|
pi.registerTool({
|
|
201
268
|
name: PUSH_BRANCH_TOOL_NAME,
|
|
202
269
|
label: "Push Branch",
|
package/src/types.ts
CHANGED
|
@@ -87,6 +87,35 @@ export interface ChangeBranchDetails {
|
|
|
87
87
|
hasChangesBeforeSwitch: false;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
export interface FetchBranchDetails {
|
|
91
|
+
repoRoot: string;
|
|
92
|
+
currentBranch: string;
|
|
93
|
+
upstream: string;
|
|
94
|
+
remote: string;
|
|
95
|
+
remoteRef: string;
|
|
96
|
+
remoteTrackingRef: string;
|
|
97
|
+
refspec: string;
|
|
98
|
+
output: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface PullBranchDetails {
|
|
102
|
+
repoRoot: string;
|
|
103
|
+
currentBranch: string;
|
|
104
|
+
upstream: string;
|
|
105
|
+
remote: string;
|
|
106
|
+
remoteRef: string;
|
|
107
|
+
output: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface RebaseBranchDetails {
|
|
111
|
+
repoRoot: string;
|
|
112
|
+
currentBranch: string;
|
|
113
|
+
upstream: string;
|
|
114
|
+
remote: string;
|
|
115
|
+
remoteRef: string;
|
|
116
|
+
output: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
90
119
|
export interface PushBranchDetails {
|
|
91
120
|
repoRoot: string;
|
|
92
121
|
currentBranch: string;
|
package/src/ui/branchme-panel.ts
CHANGED
|
@@ -29,8 +29,8 @@ export type BranchMePanelSection = "status" | "workflow";
|
|
|
29
29
|
const NARROW_MIN_WIDTH = 24;
|
|
30
30
|
const WIDE_MIN_WIDTH = 72;
|
|
31
31
|
const MAX_PANEL_WIDTH = 96;
|
|
32
|
-
const MAX_PANEL_HEIGHT =
|
|
33
|
-
const BODY_HEIGHT =
|
|
32
|
+
const MAX_PANEL_HEIGHT = 16;
|
|
33
|
+
const BODY_HEIGHT = 9;
|
|
34
34
|
const PANEL_SECTIONS: BranchMePanelSection[] = ["status", "workflow"];
|
|
35
35
|
const ANSI_RESET = "\u001b[0m";
|
|
36
36
|
|
|
@@ -177,7 +177,7 @@ function sectionFooter(section: BranchMePanelSection, data: BranchMePanelData):
|
|
|
177
177
|
case "status":
|
|
178
178
|
return "status • current repository only • tools perform actions";
|
|
179
179
|
case "workflow":
|
|
180
|
-
return "workflow • inspect → change/create → push → PR";
|
|
180
|
+
return "workflow • inspect → change → fetch/pull/rebase → create → push → PR";
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
@@ -197,6 +197,9 @@ function sectionRows(section: BranchMePanelSection, data: BranchMePanelData): Pa
|
|
|
197
197
|
heading("WORKFLOW"),
|
|
198
198
|
workflowDetailRow("branch_status", "inspect"),
|
|
199
199
|
workflowDetailRow("change_branch", "existing local"),
|
|
200
|
+
workflowDetailRow("fetch_branch", "upstream remote"),
|
|
201
|
+
workflowDetailRow("pull_branch", "fast-forward"),
|
|
202
|
+
workflowDetailRow("rebase_branch", "onto upstream"),
|
|
200
203
|
workflowDetailRow("create_branch", "from HEAD"),
|
|
201
204
|
workflowDetailRow("push_branch", "current branch"),
|
|
202
205
|
workflowDetailRow("pull_request", "after push"),
|