diff --git a/src/commands/branch/new.test.tsx b/src/commands/branch/new.test.tsx index a519c1f..aebb851 100644 --- a/src/commands/branch/new.test.tsx +++ b/src/commands/branch/new.test.tsx @@ -18,6 +18,11 @@ const mocks = vi.hoisted(() => { setTimeout(resolve, ARBITRARY_DELAY / 4) ); }, + currentBranch: async () => { + return new Promise((resolve) => + setTimeout(() => resolve('root'), ARBITRARY_DELAY / 4) + ); + }, createBranch: async () => { return new Promise((resolve) => setTimeout(resolve, ARBITRARY_DELAY / 4) diff --git a/src/commands/branch/new.tsx b/src/commands/branch/new.tsx index fccae74..c9adbc0 100644 --- a/src/commands/branch/new.tsx +++ b/src/commands/branch/new.tsx @@ -10,17 +10,22 @@ import { 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 { safeBranchNameFromCommitMessage } from '../../utils/naming.js'; import { useGit } from '../../hooks/use-git.js'; import { useTree } from '../../hooks/use-tree.js'; const BranchNew = (props: CommandProps) => { - const { rootBranchName } = useTree(); + const { rootBranchName, isCurrentBranchTracked } = useTree(); if (!rootBranchName) { return ; } + if (!isCurrentBranchTracked) { + return ; + } + return ; }; diff --git a/src/commands/changes/commit.test.tsx b/src/commands/changes/commit.test.tsx index 39a378d..9a44757 100644 --- a/src/commands/changes/commit.test.tsx +++ b/src/commands/changes/commit.test.tsx @@ -12,15 +12,30 @@ const mocks = vi.hoisted(() => { return { createGitService: vi.fn(({}) => { return { + checkout: async () => { + return new Promise((resolve) => + setTimeout(resolve, ARBITRARY_DELAY / 4) + ); + }, + currentBranch: async () => { + return new Promise((resolve) => + setTimeout(() => resolve('root'), ARBITRARY_DELAY / 4) + ); + }, + createBranch: async () => { + return new Promise((resolve) => + setTimeout(resolve, ARBITRARY_DELAY / 4) + ); + }, addAllFiles: async () => { return new Promise((resolve) => - setTimeout(resolve, ARBITRARY_DELAY / 2) + setTimeout(resolve, ARBITRARY_DELAY / 4) ); }, commit: async ({ message }: { message: string }) => { console.log(message); return new Promise((resolve) => - setTimeout(resolve, ARBITRARY_DELAY / 2) + setTimeout(resolve, ARBITRARY_DELAY / 4) ); }, }; @@ -101,7 +116,7 @@ describe('correctly renders changes commit UI', () => { const expected = render(); - await delay(ARBITRARY_DELAY / 2); + await delay(ARBITRARY_DELAY / 4); expect(actual1.lastFrame()).to.equal(expected.lastFrame()); expect(actual2.lastFrame()).to.equal(expected.lastFrame()); }); diff --git a/src/components/untracked-branch.tsx b/src/components/untracked-branch.tsx new file mode 100644 index 0000000..5aa8441 --- /dev/null +++ b/src/components/untracked-branch.tsx @@ -0,0 +1,31 @@ +import React, { useCallback } from 'react'; +import { Box, Text, useInput } from 'ink'; +import { useAsyncValue } from '../hooks/use-async-value.js'; +import { useGit } from '../hooks/use-git.js'; + +const TRACK_BRANCH_COMMAND = 'gum branch track'; + +export const UntrackedBranch = () => { + const git = useGit(); + + const getCurrentBranch = useCallback(async () => { + return await git.currentBranch(); + }, [git.currentBranch]); + + const { value: currentBranch } = useAsyncValue({ + getValue: getCurrentBranch, + }); + + return ( + + + Cannot perform this operation on untracked branch{' '} + {currentBranch}. + + + You can start tracking it with{' '} + {TRACK_BRANCH_COMMAND}. + + + ); +}; diff --git a/src/hooks/use-async-value.ts b/src/hooks/use-async-value.ts index 47dd589..5e7cefe 100644 --- a/src/hooks/use-async-value.ts +++ b/src/hooks/use-async-value.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; type State = { type: 'LOADING' } | { type: 'COMPLETE'; value: T }; type Result = { value?: T }; + export const useAsyncValue = ({ getValue, }: { diff --git a/src/hooks/use-tree.ts b/src/hooks/use-tree.ts index abcdc33..e3038e8 100644 --- a/src/hooks/use-tree.ts +++ b/src/hooks/use-tree.ts @@ -1,13 +1,27 @@ import { Tree, TreeService, createTreeService } from '../services/tree.js'; -import { useMemo, useState } from 'react'; +import { useAsyncValue } from './use-async-value.js'; +import { useCallback, useMemo, useState } from 'react'; +import { useGit } from './use-git.js'; interface UseTreeResult extends TreeService { currentTree: Tree; rootBranchName: string | undefined; + isCurrentBranchTracked: boolean; + isLoading: boolean; } export const useTree = (): UseTreeResult => { const [currentTree, setCurrentTree] = useState([]); + const git = useGit(); + + const getCurrentBranchTracked = useCallback(async () => { + const currentBranch = await git.currentBranch(); + return Boolean(currentTree.find((b) => b.key === currentBranch)); + }, [currentTree, git.currentBranch]); + + const currentBranchTrackedResult = useAsyncValue({ + getValue: getCurrentBranchTracked, + }); const service = useMemo(() => createTreeService({ setCurrentTree }), []); @@ -21,5 +35,7 @@ export const useTree = (): UseTreeResult => { currentTree, ...computed, ...service, + isCurrentBranchTracked: Boolean(currentBranchTrackedResult.value), + isLoading: !('value' in currentBranchTrackedResult), }; }; diff --git a/src/services/git.ts b/src/services/git.ts index de38dd7..e8a6d8a 100644 --- a/src/services/git.ts +++ b/src/services/git.ts @@ -10,6 +10,7 @@ export const DEFAULT_OPTIONS: Partial = { export interface GitService { _git: SimpleGit; branchLocal: () => Promise>; + currentBranch: () => Promise; checkout: (branch: string) => Promise>; addAllFiles: () => Promise; commit: (args: { message: string }) => Promise; @@ -25,13 +26,17 @@ export const createGitService = ({ return { _git: gitEngine, // @ts-expect-error - being weird about the return type - checkout: async (branch: string) => { - return gitEngine.checkout(branch); - }, - // @ts-expect-error - being weird about the return type branchLocal: async () => { return gitEngine.branchLocal(); }, + currentBranch: async () => { + const { current } = await gitEngine.branchLocal(); + return current; + }, + // @ts-expect-error - being weird about the return type + checkout: async (branch: string) => { + return gitEngine.checkout(branch); + }, addAllFiles: async () => { await gitEngine.add('.'); },