From 505f4d313227a8a5eebdcb9d96b8026176c60fca Mon Sep 17 00:00:00 2001 From: opeyem1a Date: Thu, 17 Oct 2024 22:55:51 -0600 Subject: [PATCH 1/2] add track command --- src/command-registry.ts | 5 ++ src/commands/branch/track.tsx | 89 ++++++++++++++++++++++++++ src/components/select-root-branch.tsx | 35 ++-------- src/components/select-search-input.tsx | 55 ++++++++++++++++ src/hooks/use-git-helpers.ts | 10 +++ src/services/git.ts | 5 ++ 6 files changed, 171 insertions(+), 28 deletions(-) create mode 100644 src/commands/branch/track.tsx create mode 100644 src/components/select-search-input.tsx diff --git a/src/command-registry.ts b/src/command-registry.ts index 3bf08c6..62d3dc0 100644 --- a/src/command-registry.ts +++ b/src/command-registry.ts @@ -1,4 +1,5 @@ import BranchNew, { branchNewConfig } from './commands/branch/new.js'; +import BranchTrack, { branchTrackConfig } from './commands/branch/track.js'; import ChangesAdd, { changesAddConfig } from './commands/changes/add.js'; import ChangesCommit, { changesCommitConfig, @@ -50,6 +51,10 @@ export const REGISTERED_COMMANDS: CommandGroup = { component: BranchNew, config: branchNewConfig, }, + track: { + component: BranchTrack, + config: branchTrackConfig, + }, } as CommandGroup, } as const; diff --git a/src/commands/branch/track.tsx b/src/commands/branch/track.tsx new file mode 100644 index 0000000..e13f007 --- /dev/null +++ b/src/commands/branch/track.tsx @@ -0,0 +1,89 @@ +import React, { useCallback, useMemo, useState } from 'react'; +import { CommandConfig, CommandProps } from '../../types.js'; +import { Loading } from '../../components/loading.js'; +import { SearchSelectInput } from '../../components/select-search-input.js'; +import { SelectRootBranch } from '../../components/select-root-branch.js'; +import { Text } from 'ink'; +import { useGitHelpers } from '../../hooks/use-git-helpers.js'; +import { useTree } from '../../hooks/use-tree.js'; + +function BranchTrack(_: CommandProps) { + const { allBranches, currentBranch } = useGitHelpers(); + const { rootBranchName, isCurrentBranchTracked, attachTo, isLoading } = + useTree(); + + // either false or the name of the parent branch + const [complete, setComplete] = useState(false); + + const trackBranch = useCallback( + ({ parentBranch }: { parentBranch: string }) => { + if (currentBranch.isLoading) return; + attachTo({ newBranch: currentBranch.value, parent: parentBranch }); + setComplete(parentBranch); + }, + [attachTo, currentBranch.value, currentBranch.isLoading] + ); + + const branchItems = useMemo(() => { + if (allBranches.isLoading) return []; + + return allBranches.value.map((b) => ({ label: b, value: b })); + }, [allBranches.value, allBranches.isLoading]); + + if (isLoading || currentBranch.isLoading || allBranches.isLoading) { + return ; + } + + if (!rootBranchName) { + return ; + } + + if (!isCurrentBranchTracked) { + return ( + + + {currentBranch.value} + {' '} + is already a tracked branch + + ); + } + + if (complete) { + return ( + + + {currentBranch.value} + {' '} + tracked with parent{' '} + + {complete} + + ! + + ); + } + + return ( + trackBranch({ parentBranch: item.value })} + /> + ); +} + +export const branchTrackConfig: CommandConfig = { + description: 'Track a branch', + usage: 'branch track', + key: 'track', + aliases: ['t'], + getProps: (_) => { + return { + valid: true, + props: {}, + }; + }, +}; + +export default BranchTrack; diff --git a/src/components/select-root-branch.tsx b/src/components/select-root-branch.tsx index 31f77b8..26bc53f 100644 --- a/src/components/select-root-branch.tsx +++ b/src/components/select-root-branch.tsx @@ -7,6 +7,7 @@ import { Loading } from './loading.js'; import { useAsyncValue } from '../hooks/use-async-value.js'; import { useGit } from '../hooks/use-git.js'; import { useTree } from '../hooks/use-tree.js'; +import { SearchSelectInput } from './select-search-input.js'; export const SelectRootBranch = () => { const git = useGit(); @@ -47,13 +48,7 @@ export const SelectRootBranch = () => { if (!allBranches || isLoading) { return []; } - - const filtered = allBranches.filter((b) => - search.length > 0 - ? b.toLowerCase().includes(search.toLowerCase()) - : true - ); - return filtered.map((b) => ({ label: b, value: b })); + return allBranches.map((b) => ({ label: b, value: b })); }, [search, allBranches]); if (rootBranchName) { @@ -89,26 +84,10 @@ export const SelectRootBranch = () => { } return ( - - - Please select a root branch for Gumption before proceeding. - - - - 🔎  - {search.length ? search : '(type to search)'} - - - {items.length ? ( - - ) : ( - No results - )} - + ); }; diff --git a/src/components/select-search-input.tsx b/src/components/select-search-input.tsx new file mode 100644 index 0000000..b290038 --- /dev/null +++ b/src/components/select-search-input.tsx @@ -0,0 +1,55 @@ +import GumptionItemComponent from './gumption-item-component.js'; +import React, { useMemo, useState } from 'react'; +import SelectInput from 'ink-select-input'; +import { Box, Text, useInput } from 'ink'; + +export const SearchSelectInput = ({ + title, + items, + onSelect, +}: { + title: string; + items: { label: string; value: string }[]; + onSelect: (args: { label: string; value: string }) => void; +}) => { + const [search, setSearch] = useState(''); + + const filteredItems = useMemo(() => { + return items.filter((item) => + search.length > 0 + ? item.label.toLowerCase().includes(search.toLowerCase()) || + item.value.toLowerCase().includes(search.toLowerCase()) + : true + ); + }, [items, search]); + + useInput((input, key) => { + if (key.backspace || key.delete) { + // remove final character + return setSearch((prev) => prev.slice(0, -1)); + } + setSearch((prev) => `${prev}${input}`); + }); + + return ( + + {title} + + + 🔎  + {search.length ? search : '(type to search)'} + + + {filteredItems.length ? ( + onSelect(item)} + limit={10} + /> + ) : ( + No results + )} + + ); +}; diff --git a/src/hooks/use-git-helpers.ts b/src/hooks/use-git-helpers.ts index 8854650..5cbe4a4 100644 --- a/src/hooks/use-git-helpers.ts +++ b/src/hooks/use-git-helpers.ts @@ -5,6 +5,7 @@ import { useGit } from './use-git.js'; interface UseGitHelpersResult { currentBranch: AsyncResult; + allBranches: AsyncResult; } export const useGitHelpers = (): UseGitHelpersResult => { @@ -18,7 +19,16 @@ export const useGitHelpers = (): UseGitHelpersResult => { getValue: getCurrentBranch, }); + const getBranches = useCallback(async () => { + return git.listBranches(); + }, [git.listBranches]); + + const allBranchesResult = useAsyncValue({ + getValue: getBranches, + }); + return { currentBranch: currentBranchResult, + allBranches: allBranchesResult, }; }; diff --git a/src/services/git.ts b/src/services/git.ts index e8a6d8a..15f1832 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -11,6 +11,7 @@ export interface GitService { _git: SimpleGit; branchLocal: () => Promise>; currentBranch: () => Promise; + listBranches: () => Promise; checkout: (branch: string) => Promise>; addAllFiles: () => Promise; commit: (args: { message: string }) => Promise; @@ -33,6 +34,10 @@ export const createGitService = ({ const { current } = await gitEngine.branchLocal(); return current; }, + listBranches: async () => { + const { all } = await gitEngine.branchLocal(); + return all; + }, // @ts-expect-error - being weird about the return type checkout: async (branch: string) => { return gitEngine.checkout(branch); From 4ad9dbae6b9ce9f65eb432686a67d152bed60a62 Mon Sep 17 00:00:00 2001 From: opeyem1a Date: Thu, 17 Oct 2024 23:14:58 -0600 Subject: [PATCH 2/2] lint and tests --- src/commands/branch/new.test.tsx | 8 ++++++++ src/commands/changes/commit.test.tsx | 8 ++++++++ src/components/select-root-branch.tsx | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/commands/branch/new.test.tsx b/src/commands/branch/new.test.tsx index fb16963..a74ab76 100644 --- a/src/commands/branch/new.test.tsx +++ b/src/commands/branch/new.test.tsx @@ -23,6 +23,14 @@ const mocks = vi.hoisted(() => { setTimeout(() => resolve('root'), ARBITRARY_DELAY / 4) ); }, + listBranches: async () => { + return new Promise((resolve) => + setTimeout( + () => resolve(['root', 'branch']), + ARBITRARY_DELAY / 4 + ) + ); + }, createBranch: async () => { return new Promise((resolve) => setTimeout(resolve, ARBITRARY_DELAY / 4) diff --git a/src/commands/changes/commit.test.tsx b/src/commands/changes/commit.test.tsx index 9a44757..5bd47a7 100644 --- a/src/commands/changes/commit.test.tsx +++ b/src/commands/changes/commit.test.tsx @@ -22,6 +22,14 @@ const mocks = vi.hoisted(() => { setTimeout(() => resolve('root'), ARBITRARY_DELAY / 4) ); }, + listBranches: async () => { + return new Promise((resolve) => + setTimeout( + () => resolve(['root', 'branch']), + ARBITRARY_DELAY / 4 + ) + ); + }, createBranch: async () => { return new Promise((resolve) => setTimeout(resolve, ARBITRARY_DELAY / 4) diff --git a/src/components/select-root-branch.tsx b/src/components/select-root-branch.tsx index 26bc53f..fd16ba8 100644 --- a/src/components/select-root-branch.tsx +++ b/src/components/select-root-branch.tsx @@ -4,10 +4,10 @@ import SelectInput from 'ink-select-input'; import { Box, Text, useInput } from 'ink'; import { ConfirmStatement } from './confirm-statement.js'; import { Loading } from './loading.js'; +import { SearchSelectInput } from './select-search-input.js'; import { useAsyncValue } from '../hooks/use-async-value.js'; import { useGit } from '../hooks/use-git.js'; import { useTree } from '../hooks/use-tree.js'; -import { SearchSelectInput } from './select-search-input.js'; export const SelectRootBranch = () => { const git = useGit();