Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .github/workflows/vsix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: Package VSIX

on:
pull_request:
push:
branches:
- main
tags:
- "v*"
workflow_dispatch:

jobs:
package:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
- run: npm run typecheck
- run: npm run package
- uses: actions/upload-artifact@v4
with:
name: wgit-vsix
path: wgit-*.vsix
if-no-files-found: error

release:
if: startsWith(github.ref, 'refs/tags/v')
needs: package
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: wgit-vsix
path: dist
- name: Require the tag to match the VSIX version
run: |
file=$(ls dist/wgit-*.vsix)
base=$(basename "$file" .vsix)
version=${base#wgit-}
tag=${GITHUB_REF_NAME#v}
test "$version" = "$tag"
- name: Publish GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
tag="$GITHUB_REF_NAME"
version="${tag#v}"
gh release create "$tag" "dist/wgit-${version}.vsix" \
--repo "$GITHUB_REPOSITORY" \
--title "WGit ${tag}" \
--notes "Install wgit-${version}.vsix in Cursor or VS Code: Command Palette, then Extensions: Install from VSIX. After reload, type \`wg st\`. The status bar shows \`WGit: N\`."
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
- Dirty working tree policy: `skip` (default), `stash`, or `fail`. One repository never aborts the batch.
- Status bar item `WGit: N` and an output channel named `WGit`.
- Commands `WG: Switch Branch (sb)`, `WG: Pull All (pa)`, `WG: Fetch All (fa)`, `WG: Status (st)`, and `WG: Refresh (rf)`.
- GitHub Actions publishes `wgit-<version>.vsix` on the matching `v*` tag.

[0.1.0]: https://github.com/alpdogan/wgit/releases/tag/v0.1.0
38 changes: 20 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ Fast multi-root Git sync. Switch, fetch, and pull across every repository in the
WGit is a VS Code and Cursor extension. You type `wg sb` and every repo moves to the branch you picked. A repo that only has `origin/dev` gets a local `dev` that tracks it. A repo that cannot move is skipped, with a reason, and the rest of the batch still runs.

```
repo | before | after | action | result | error
already-on | dev | dev | already-on | ok |
admin-panel-auth-service | master | dev | create-track | ok |
no-dev | master | master | skip: missing-remote-branch | skipped |
dirty-repo | master | master | skip: dirty | skipped |
repo | before | after | action | result | error
already-on | dev | dev | already-on | ok |
example-service | master | dev | create-track | ok |
no-dev | master | master | skip: missing-remote-branch | skipped |
dirty-repo | master | master | skip: dirty | skipped |
```

## Why

Multi-root workspaces drift. Most repos sit on `dev`. One of them, `admin-panel-auth-service`, is still on `master`. `origin/dev` exists. There is no local `dev`. “Switch all to dev” should create that tracking branch. It should not leave the repo on `master` and hide the reason in a toast.
Multi-root workspaces drift. Most repos sit on `dev`. One of them is still on `master`. `origin/dev` exists. There is no local `dev`. “Switch all to dev” should create that tracking branch. It should not leave that repo on `master` and hide the reason in a toast.

WGit plans the operation before it changes any branch, then prints one row per repository.

Expand Down Expand Up @@ -74,29 +74,31 @@ Dirty means `git status --porcelain` is not empty, including untracked files. A

WGit does not activate in an untrusted workspace, and it does not run in virtual workspaces. Git has to be on `PATH`. Fetch and pull use `GIT_TERMINAL_PROMPT=0`, so a missing credential fails that repository instead of hanging the batch on a prompt.

## Install from VSIX
## Install from GitHub

WGit targets VS Code and Cursor (`engines.vscode` `^1.85.0`). The 0.1 package is installed from a VSIX. Registry publishing is documented below and is not required to try it.

```bash
git clone https://github.com/alpdogan/wgit.git
cd wgit
npm install
npm run package
```

`npm run package` writes `wgit-0.1.0.vsix` in this directory.
Download [`wgit-0.1.0.vsix`](https://github.com/alpdogan/wgit/releases/download/v0.1.0/wgit-0.1.0.vsix) from the [v0.1.0 release](https://github.com/alpdogan/wgit/releases/tag/v0.1.0). That file is attached when the `v0.1.0` tag is pushed. GitHub Actions runs the tests, builds the VSIX, and publishes the release.

In Cursor or VS Code:

1. Open the Command Palette.
2. Run **Extensions: Install from VSIX...**.
3. Choose `wgit-0.1.0.vsix`.
3. Choose the downloaded `wgit-0.1.0.vsix`.
4. Reload the window when the editor asks.
5. Open a multi-root workspace, then run **WG: Status (st)** or type `wg st`.

Trust the workspace when asked. The status bar should show `WGit: N` for the repositories it found.

To build the same file yourself:

```bash
git clone https://github.com/alpdogan/wgit.git
cd wgit
npm install
npm run package
```

`npm run package` writes `wgit-0.1.0.vsix` in this directory. WGit targets VS Code and Cursor (`engines.vscode` `^1.85.0`).

### Open VSX, then Marketplace

Publishing is manual. Create an [Open VSX](https://open-vsx.org/) access token and, only when you intend to publish:
Expand Down
12 changes: 6 additions & 6 deletions test/acceptance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,18 @@ describe('acceptance: switch a multi-root workspace to dev', () => {
const dirty = seedDirty(root);
const repos: NamedRepo[] = [
{ path: alreadyOn, name: 'already-on' },
{ path: authService, name: 'admin-panel-auth-service' },
{ path: authService, name: 'example-service' },
{ path: missing, name: 'no-dev' },
{ path: dirty, name: 'dirty-repo' },
];

const beforeFetch = await loadRepoStatus(authService, 'admin-panel-auth-service', 'origin');
const beforeFetch = await loadRepoStatus(authService, 'example-service', 'origin');
assert.equal(beforeFetch?.remoteBranches.includes('dev'), false);
assert.equal(beforeFetch?.branch, 'master');

const fetched = await fetchRepos(repos, 'origin', config.maxParallel);
assert.equal(fetched.every((row) => row.result === 'ok'), true);
const afterFetch = await loadRepoStatus(authService, 'admin-panel-auth-service', 'origin');
const afterFetch = await loadRepoStatus(authService, 'example-service', 'origin');
assert.equal(afterFetch?.remoteBranches.includes('dev'), true);
assert.equal(afterFetch?.localBranches.includes('dev'), false);

Expand All @@ -67,7 +67,7 @@ describe('acceptance: switch a multi-root workspace to dev', () => {
await switchRepos(repos, 'dev', config),
);
assertRow(first, 'already-on', { action: 'already-on', result: 'ok', after: 'dev' });
assertRow(first, 'admin-panel-auth-service', { action: 'create-track', result: 'ok', after: 'dev' });
assertRow(first, 'example-service', { action: 'create-track', result: 'ok', after: 'dev' });
assertRow(first, 'no-dev', { action: 'skip: missing-remote-branch', result: 'skipped', after: 'master' });
assertRow(first, 'dirty-repo', { action: 'skip: dirty', result: 'skipped', after: 'master' });

Expand All @@ -82,7 +82,7 @@ describe('acceptance: switch a multi-root workspace to dev', () => {

const second = await switchRepos(repos, 'dev', config);
assertRow(second, 'already-on', { action: 'already-on', result: 'ok', after: 'dev' });
assertRow(second, 'admin-panel-auth-service', { action: 'already-on', result: 'ok', after: 'dev' });
assertRow(second, 'example-service', { action: 'already-on', result: 'ok', after: 'dev' });
assertRow(second, 'no-dev', { action: 'skip: missing-remote-branch', result: 'skipped', after: 'master' });
assertRow(second, 'dirty-repo', { action: 'skip: dirty', result: 'skipped', after: 'master' });
assert.equal(fs.readFileSync(path.join(dirty, 'readme.txt'), 'utf8'), 'local edit\n');
Expand Down Expand Up @@ -229,7 +229,7 @@ function seedRemoteOnlyDev(root: string): string {
git(seed, ['checkout', '-b', 'dev']);
commitFile(seed, 'b.txt', 'dev\n', 'dev');
git(seed, ['push', '-u', 'origin', 'dev']);
const repo = cloneFrom(root, origin, 'admin-panel-auth-service');
const repo = cloneFrom(root, origin, 'example-service');
if (currentBranch(repo) !== 'master') {
git(repo, ['checkout', 'master']);
}
Expand Down
4 changes: 2 additions & 2 deletions test/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('reports', () => {
it('renders the switch table and keeps a failed fetch on a non-ok row', () => {
const table = formatReport([
{
repo: 'admin-panel-auth-service',
repo: 'example-service',
before: 'master',
after: 'dev',
action: 'create-track',
Expand All @@ -17,7 +17,7 @@ describe('reports', () => {
},
]);
assert.match(table, /repo\s+\|\s+before\s+\|\s+after\s+\|\s+action\s+\|\s+result\s+\|\s+error/);
assert.match(table, /admin-panel-auth-service\s+\|\s+master\s+\|\s+dev\s+\|\s+create-track\s+\|\s+ok/);
assert.match(table, /example-service\s+\|\s+master\s+\|\s+dev\s+\|\s+create-track\s+\|\s+ok/);

const merged = mergeFetchErrors(
[
Expand Down
Loading