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
16 changes: 16 additions & 0 deletions NAMING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Naming things

Naming things is hard, so we loosely document decisions in naming while we go.
This might go poorly.

### Branch or branch name?

Branches mean lots of things, for clarity, anything that is a string of a branch
name will be called a "branchName" and a branch implies an object containing
more data than that (the repo is not currently like this but will be rewritten
to adhere to it).

### Put some respect on the name

Capitalize Gumption in comments so it's obvious we mean Gumption, the project,
and not whimsically using the word gumption for delight.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ CLI for stacked Git commands

3. Link the package to your global npm packages:
```bash
$ npm link && npm install --global gumption
$ npm link & npm install --global gumption
```
463 changes: 434 additions & 29 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"node": ">=16"
},
"scripts": {
"postinstall": "patch-package",
"test": "vitest",
"build": "tsc",
"dev": "tsc --watch",
Expand All @@ -25,6 +26,7 @@
"ink": "^4.1.0",
"ink-select-input": "^5.0.0",
"meow": "^11.0.0",
"patch-package": "^8.0.0",
"react": "^18.2.0",
"simple-git": "^3.24.0"
},
Expand Down
42 changes: 42 additions & 0 deletions patches/ink+4.4.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
diff --git a/node_modules/ink/build/hooks/use-input.js b/node_modules/ink/build/hooks/use-input.js
index 38af918..d1764f0 100644
--- a/node_modules/ink/build/hooks/use-input.js
+++ b/node_modules/ink/build/hooks/use-input.js
@@ -1,4 +1,4 @@
-import { useEffect } from 'react';
+import { useEffect, useState } from 'react';
import { isUpperCase } from 'is-upper-case';
import parseKeypress, { nonAlphanumericKeys } from '../parse-keypress.js';
import reconciler from '../reconciler.js';
@@ -30,6 +30,14 @@ import useStdin from './use-stdin.js';
const useInput = (inputHandler, options = {}) => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const { stdin, setRawMode, internal_exitOnCtrlC, internal_eventEmitter } = useStdin();
+
+ const [errorState, setErrorState] = useState({ hasError: false });
+
+ useEffect(() => {
+ if (!errorState.hasError) return;
+ throw errorState.error;
+ }, [errorState])
+
useEffect(() => {
if (options.isActive === false) {
return;
@@ -83,9 +91,13 @@ const useInput = (inputHandler, options = {}) => {
if (!(input === 'c' && key.ctrl) || !internal_exitOnCtrlC) {
// @ts-expect-error TypeScript types for `batchedUpdates` require an argument, but React's codebase doesn't provide it and it works without it as exepected.
reconciler.batchedUpdates(() => {
- inputHandler(input, key);
- });
- }
+ try {
+ inputHandler(input, key);
+ } catch (e) {
+ setErrorState({ hasError: true, error: e })
+ }
+ });
+ }
};
internal_eventEmitter?.on('input', handleData);
return () => {
7 changes: 6 additions & 1 deletion src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import { Box, Text } from 'ink';
import { GumptionErrorBoundary } from './components/gumption-error-boundary.js';
import { type Result } from 'meow';
import { findCommand, getCli } from './utils/commands.js';

Expand Down Expand Up @@ -50,5 +51,9 @@ export default function App({ cli: _cli }: Props) {

const CommandHandlerComponent = command.component;

return <CommandHandlerComponent cli={cli} input={cli.input} />;
return (
<GumptionErrorBoundary>
<CommandHandlerComponent cli={cli} input={cli.input} />
</GumptionErrorBoundary>
);
}
121 changes: 0 additions & 121 deletions src/commands/branch/new.test.tsx

This file was deleted.

99 changes: 29 additions & 70 deletions src/commands/branch/new.tsx
Original file line number Diff line number Diff line change
@@ -1,98 +1,57 @@
import ErrorDisplay from '../../components/error-display.js';
import React, { useCallback } from 'react';
import { Action, useAction } from '../../hooks/use-action.js';
import React from 'react';
import {
CommandConfig,
CommandProps,
PropSanitationResult,
Valid,
} from '../../types.js';
import { Loading } from '../../components/loading.js';
import { SelectRootBranch } from '../../components/select-root-branch.js';
import { Text } from 'ink';
import { UntrackedBranch } from '../../components/untracked-branch.js';
import {
assertBranchNameExists,
assertCurrentHasDiff,
} from '../../modules/branch/assertions.js';
import { engine } from '../../modules/engine.js';
import { git } from '../../modules/git.js';
import { safeBranchNameFromCommitMessage } from '../../utils/naming.js';
import { useGit } from '../../hooks/use-git.js';
import { useTree } from '../../hooks/use-tree.js';
import { useAction } from '../../hooks/use-action.js';

const BranchNew = (props: CommandProps) => {
const { rootBranchName, isCurrentBranchTracked } = useTree();

if (!rootBranchName) {
return <SelectRootBranch />;
}

if (!isCurrentBranchTracked) {
return <UntrackedBranch />;
}

return <DoBranchNew {...props} />;
};

const DoBranchNew = (props: CommandProps) => {
const args = branchNewConfig.getProps(props) as Valid<
PropSanitationResult<CommandArgs>
>;
const { commitMessage } = args.props;

const result = useBranchNew({
message: commitMessage,
const result = useAction<{ newBranchName: string }>({
func: () => {
const newBranchName =
safeBranchNameFromCommitMessage(commitMessage);
const branchBeforeName = git.getCurrentBranchName();
assertCurrentHasDiff();
git.createBranch({ branchName: newBranchName });
assertBranchNameExists(newBranchName);
git.checkoutBranch(newBranchName);
git.stageAllChanges();
git.commit({ message: commitMessage });

engine.trackBranch({
branchName: newBranchName,
parentBranchName: branchBeforeName,
});

return { newBranchName };
},
});

if (result.isError) {
return <ErrorDisplay error={result.error} />;
}

if (result.isLoading) {
return <Loading />;
}
if (!result.isComplete) return <Loading />;

return (
<Text color="green">
New branch created - <Text bold>{result.branchName}</Text>
New branch created - <Text bold>{result.data.newBranchName}</Text>
</Text>
);
};

type UseBranchNewAction = Action & {
branchName: string;
};

const useBranchNew = ({ message }: { message: string }): UseBranchNewAction => {
const git = useGit();
const { attachTo } = useTree();

const branchName = safeBranchNameFromCommitMessage(message);

const performGitActions = useCallback(async () => {
const branchBefore = await git.currentBranch();

await git.createBranch({ branchName });
await git.checkout(branchName);
await git.addAllFiles();
await git.commit({ message });

return branchBefore;
}, [branchName]);

const performAction = useCallback(async () => {
await performGitActions().then((prevBranch) => {
attachTo({ newBranch: branchName, parent: prevBranch });
});
}, [branchName]);

const action = useAction({
asyncAction: performAction,
});

return {
isLoading: action.isLoading,
isError: action.isError,
error: action.error,
branchName,
} as UseBranchNewAction;
};

interface CommandArgs {
commitMessage: string;
}
Expand Down
Loading