Skip to content

Commit

Permalink
Branch new command final (#27)
Browse files Browse the repository at this point in the history
* yada

* finish branch new

* testing testing

* fixes

* fix tests

* fix formatting
  • Loading branch information
Opeyem1a authored Oct 9, 2024
1 parent 98dec78 commit 4558280
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 88 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
}
],
"@typescript-eslint/switch-exhaustiveness-check": "error",
"@typescript-eslint/no-explicit-any": "warn"
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unused-vars": "warn"
}
}
12 changes: 12 additions & 0 deletions src/command-registry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import BranchNew, { branchNewConfig } from './commands/branch/new.js';
import ChangesAdd, { changesAddConfig } from './commands/changes/add.js';
import ChangesCommit, {
changesCommitConfig,
Expand Down Expand Up @@ -39,4 +40,15 @@ export const REGISTERED_COMMANDS: CommandGroup = {
config: changesCommitConfig,
},
} as CommandGroup,
branch: {
_group: {
alias: 'b',
name: 'branch',
description: 'Commands related to branch management',
},
new: {
component: BranchNew,
config: branchNewConfig,
},
} as CommandGroup,
} as const;
77 changes: 77 additions & 0 deletions src/commands/branch/new.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import ErrorDisplay from '../../components/error-display.js';
import React, { useCallback } from 'react';
import { Action, useAction } from '../../hooks/use-action.js';
import { CommandConfig, CommandProps } from '../../types.js';
import { Text } from 'ink';
import { safeBranchNameFromCommitMessage } from '../../utils/naming.js';
import { useGit } from '../../hooks/use-git.js';

function BranchNew({ input }: CommandProps) {
const [, , message] = input;
// todo: refactor to a sanitize input pattern
const result = useBranchNew({ message: message! });

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

if (result.isLoading) {
return <Text color="cyan">Loading...</Text>;
}

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

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

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

const branchName = safeBranchNameFromCommitMessage(message);

const performAction = useCallback(async () => {
await git.createBranch({ branchName });
await git.checkout(branchName);
await git.addAllFiles();
await git.commit({ message });
}, [branchName]);

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

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

export const branchNewConfig: CommandConfig = {
description:
'Create a new branch, switch to it, and commit all current changes to it',
usage: 'branch new "<message>"',
key: 'new',
aliases: ['n'],
validateProps: (props) => {
const { input } = props;
const [, , message] = input;

if (!message)
return {
valid: false,
errors: ['Please provide a commit message'],
};

return { valid: true };
},
};

export default BranchNew;
51 changes: 8 additions & 43 deletions src/commands/changes/add.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ErrorDisplay from '../../components/error-display.js';
import React, { useEffect, useState } from 'react';
import React, { useCallback } from 'react';
import { Action, useAction } from '../../hooks/use-action.js';
import { CommandConfig, CommandProps } from '../../types.js';
import { Text } from 'ink';
import { useGit } from '../../hooks/use-git.js';
Expand All @@ -22,52 +23,16 @@ function ChangedAdd({}: CommandProps) {
);
}

type Action = { isLoading: boolean } & (
| {
isError: false;
}
| {
isError: true;
error: Error;
}
);

type State =
| {
type: 'LOADING';
}
| {
type: 'COMPLETE';
}
| {
type: 'ERROR';
error: Error;
};

const useChangesAdd = (): Action => {
const git = useGit();
const [state, setState] = useState<State>({ type: 'LOADING' });

useEffect(() => {
git.addAllFiles()
.then(() => setState({ type: 'COMPLETE' }))
.catch((e: Error) => {
setState({ type: 'ERROR', error: e });
});
}, []);

if (state.type === 'ERROR') {
return {
isLoading: false,
isError: true,
error: state.error,
};
}
const performAction = useCallback(async () => {
await git.addAllFiles();
}, [git]);

return {
isLoading: state.type === 'LOADING',
isError: false,
};
return useAction({
asyncAction: performAction,
});
};

export const changesAddConfig: CommandConfig = {
Expand Down
7 changes: 6 additions & 1 deletion src/commands/changes/commit.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ const mocks = vi.hoisted(() => {
return {
createGitService: vi.fn(({}) => {
return {
addAllFiles: async () => {
return new Promise((resolve) =>
setTimeout(resolve, ARBITRARY_DELAY / 2)
);
},
commit: async ({ message }: { message: string }) => {
console.log(message);
return new Promise((resolve) =>
setTimeout(resolve, ARBITRARY_DELAY)
setTimeout(resolve, ARBITRARY_DELAY / 2)
);
},
};
Expand Down
50 changes: 8 additions & 42 deletions src/commands/changes/commit.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ErrorDisplay from '../../components/error-display.js';
import React, { useEffect, useState } from 'react';
import React, { useCallback } from 'react';
import { Action, useAction } from '../../hooks/use-action.js';
import { CommandConfig, CommandProps } from '../../types.js';
import { Text } from 'ink';
import { useGit } from '../../hooks/use-git.js';
Expand All @@ -24,52 +25,17 @@ function ChangesCommit({ input }: CommandProps) {
);
}

type Action = { isLoading: boolean } & (
| {
isError: false;
}
| {
isError: true;
error: Error;
}
);

type State =
| {
type: 'LOADING';
}
| {
type: 'COMPLETE';
}
| {
type: 'ERROR';
error: Error;
};

const useChangesCommit = ({ message }: { message: string }): Action => {
const git = useGit();
const [state, setState] = useState<State>({ type: 'LOADING' });

useEffect(() => {
git.commit({ message })
.then(() => setState({ type: 'COMPLETE' }))
.catch((e: Error) => {
setState({ type: 'ERROR', error: e });
});
const performAction = useCallback(async () => {
await git.addAllFiles();
await git.commit({ message });
}, []);

if (state.type === 'ERROR') {
return {
isLoading: false,
isError: true,
error: state.error,
};
}

return {
isLoading: state.type === 'LOADING',
isError: false,
};
return useAction({
asyncAction: performAction,
});
};

export const changesCommitConfig: CommandConfig = {
Expand Down
57 changes: 57 additions & 0 deletions src/hooks/use-action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useEffect, useState } from 'react';

export const useAction = ({
asyncAction,
}: {
asyncAction: () => Promise<void>;
}): Action => {
const [state, setState] = useState<State>({ type: 'LOADING' });

useEffect(() => {
asyncAction()
.then(() => setState({ type: 'COMPLETE' }))
.catch((e: Error) => {
setState({ type: 'ERROR', error: e });
});
}, [asyncAction]);

if (state.type === 'ERROR') {
return {
isLoading: false,
isError: true,
error: state.error,
};
}

return {
isLoading: state.type === 'LOADING',
isError: false,
error: undefined,
};
};

export type Action =
| {
isLoading: boolean;
isError: false;
error: undefined;
}
| {
isLoading: boolean;
isError: true;
error: Error;
};

// export type Action = { isLoading: boolean } & ErrorResult;

export type State =
| {
type: 'LOADING';
}
| {
type: 'COMPLETE';
}
| {
type: 'ERROR';
error: Error;
};
4 changes: 4 additions & 0 deletions src/services/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface GitService {
checkout: (branch: string) => Promise<ReturnType<SimpleGit['checkout']>>;
addAllFiles: () => Promise<void>;
commit: (args: { message: string }) => Promise<void>;
createBranch: (args: { branchName: string }) => Promise<void>;
}

export const createGitService = ({
Expand All @@ -37,5 +38,8 @@ export const createGitService = ({
commit: async ({ message }) => {
await gitEngine.commit(message);
},
createBranch: async ({ branchName }: { branchName: string }) => {
await gitEngine.branch([branchName]);
},
};
};
20 changes: 20 additions & 0 deletions src/utils/naming.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';
import { safeBranchNameFromCommitMessage } from './naming.js';

describe('safeBranchNameFromCommitMessage is working normally', () => {
it('it converts commit messages to branch names as expected', () => {
expect(
safeBranchNameFromCommitMessage(
'name/fix(module-test): do cool things [ENG-1111]'
)
).to.equal('name/fix_module-test_do_cool_things_eng-1111_');
});
it('handles special characters', () => {
// this is just an arbitrary string containing random special characters
expect(
safeBranchNameFromCommitMessage(
'!"#$%&\'()*+,.:;<=>?@[\\]^{}~‒–—―‘’“”«»…〈〉【】《》+−*=≠≤≥±∞≈×÷∑∫∂∇√$¢£€¥₣₤₹₱₩§¶©®™℅℗∴¨^`´~¯ˇ˘˙˚˛←→↑↓↔↕↖↗↘↙♪♩♫♬☼☽☾☀☁☂☃✓✔✕✖✗✘┌┐└┘├┤┬┴┼─│█▀▄▌▐░▒▓★☆☎☏☑❄❅❆☐🙂❤️👍\n'
)
).to.equal('_');
});
});
6 changes: 6 additions & 0 deletions src/utils/naming.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const safeBranchNameFromCommitMessage = (message: string): string => {
// todo: how to handle exact branch matches
// Match every non-alphanumeric character that is not "-" or "_"
const pattern = /[^a-zA-Z0-9\-_\/]+/gm;
return message.replace(pattern, '_').toLowerCase();
};
4 changes: 3 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"extends": "@sindresorhus/tsconfig",
"compilerOptions": {
"outDir": "dist",
"esModuleInterop": true
"esModuleInterop": true,
"noUnusedLocals": false,
"noUnusedParameters": false
},
"include": ["src"]
}

0 comments on commit 4558280

Please sign in to comment.