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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ Project conventions to keep updated as the repo evolves.
worktree folder layout.
- `gw switch`, `gw clone`, and `gw pr` are the commands that need shell-side
directory changes. They communicate target directories through `GW_CWD_FILE`;
keep the shell wrappers in `src/core/shell.ts` synchronized if that command
set changes.
shell wrappers in `src/core/shell.ts` pass handoff files to every command so
newly added handoff commands do not require another static wrapper list.
- Shell integration targets `bash`, `zsh`, `fish`, and `nu`. Missing shells may
cause local smoke tests to skip, not fail.

Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ gw clone [--branch-prefix <prefix>] <project-name> <repo-url>
gw init [--branch-prefix <prefix>]
gw shell-init [--shell bash|zsh|fish|nu]
gw setup [--install] [--shell bash|zsh|fish|nu]
gw --version | gw -v
gw help
```

Expand Down Expand Up @@ -358,14 +359,19 @@ Recommended branch/PR beta workflow:
2. Share either the generated tarball or a Git install command pinned to the
tested commit SHA.
3. Have testers run `gw setup` or `eval "$(gw shell-init)"`, then verify
`gw clone`, `gw switch`, and `gw list` in a disposable worktree project.
`gw clone`, `gw switch`, `gw pr`, and `gw list` in a disposable worktree
project.

## Troubleshooting

`gw clone`, `gw switch`, or `gw pr` prints a path but does not change
directories: shell integration is not active in the current shell. Run
`gw setup`, or run the session activation command for your shell.

If this starts after updating `gw`, the current shell may still have older
generated integration loaded. Run `gw setup --install --shell <shell>`, then
open a new shell or run the session activation command printed by `gw setup`.

`command gw switch ...` or `command gw pr ...` does not change directories:
`command gw` bypasses the shell wrapper. Use `gw switch ...` or `gw pr ...`.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@amadeusdemarzi/git-gw",
"version": "1.0.0",
"version": "1.0.1",
"description": "Opinionated CLI for branch-like Git worktree workflows",
"keywords": [
"cli",
Expand Down
27 changes: 26 additions & 1 deletion scripts/pack-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,22 @@ async function createRemoteFixture(rootDir: string): Promise<string> {
);
await runGit(['push', '-u', 'origin', 'feature/test'], seedPath);

await runGit(['checkout', '-b', 'pr_123'], seedPath);
await runGit(
[
'-c',
'user.name=gw-test',
'-c',
'user.email=gw-test@example.com',
'commit',
'--allow-empty',
'-m',
'pr',
],
seedPath
);
await runGit(['push', '-u', 'origin', 'pr_123'], seedPath);

return originPath;
}

Expand All @@ -143,13 +159,22 @@ async function verifyInstalledCli(installDir: string): Promise<void> {
env,
});

const versionResult = await execa('gw', ['--version'], {
cwd: installDir,
env,
});

if (!versionResult.stdout.trim()) {
throw new Error('gw --version did not print a version');
}

await execa(
'bash',
[
'--noprofile',
'--norc',
'-c',
'set -e; source <(gw shell-init); cd "$1"; gw clone demo "$2" >/dev/null 2>/dev/null; test "$(pwd -P)" = "$1/demo/main"; gw switch feature/test >/dev/null 2>/dev/null; test "$(pwd -P)" = "$1/demo/feature~test"; gw list >/dev/null',
'set -e; source <(gw shell-init); cd "$1"; gw clone demo "$2" >/dev/null 2>/dev/null; test "$(pwd -P)" = "$1/demo/main"; gw switch feature/test >/dev/null 2>/dev/null; test "$(pwd -P)" = "$1/demo/feature~test"; cd "$1/demo/main"; gw switch pr_123 >/dev/null 2>/dev/null; test "$(pwd -P)" = "$1/demo/pr_123"; cd "$1/demo/main"; gw pr 123 >/dev/null 2>/dev/null; test "$(pwd -P)" = "$1/demo/pr_123"; gw list >/dev/null',
'_',
resolvedWorkDir,
originPath,
Expand Down
3 changes: 3 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import { registerRemoveCommand } from '@/commands/remove';
import { registerSetupCommand } from '@/commands/setup';
import { registerShellInitCommand } from '@/commands/shell-init';
import { registerSwitchCommand } from '@/commands/switch';
import { readPackageVersion } from '@/core/package-version';

const program = new Command();
const packageVersion = await readPackageVersion();

program
.name('gw')
.description('Manage git worktree projects')
.version(packageVersion, '-v, --version')
.showHelpAfterError();

registerListCommand(program);
Expand Down
51 changes: 51 additions & 0 deletions src/core/package-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { readFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

interface PackageJson {
version?: unknown;
}

function isNotFoundError(error: unknown): boolean {
return Boolean(
error &&
typeof error === 'object' &&
'code' in error &&
error.code === 'ENOENT'
);
}

export async function readPackageVersion(
startUrl = import.meta.url
): Promise<string> {
let currentDir = dirname(fileURLToPath(startUrl));

while (true) {
const packageJsonPath = join(currentDir, 'package.json');

try {
const packageJson = JSON.parse(
await readFile(packageJsonPath, 'utf8')
) as PackageJson;

if (typeof packageJson.version !== 'string') {
throw new Error(
`package.json version must be a string: ${packageJsonPath}`
);
}

return packageJson.version;
} catch (error) {
if (!isNotFoundError(error)) {
throw error;
}
}

const parentDir = dirname(currentDir);
if (parentDir === currentDir) {
throw new Error('could not find package.json');
}

currentDir = parentDir;
}
}
Loading