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
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,7 @@ An interactive git branch management CLI tool inspired by `git branch` and JetBr
## Installation

```bash
# Clone the repository
git clone <repository-url>
cd rosella-cli

# Install dependencies
yarn install

# Build the project
yarn build
npm install -g rosella-cli
```

## Usage
Expand All @@ -40,8 +32,16 @@ rosella
### Branch Actions

- **n** - Create new branch from current
- **Delete** - Safe delete branch (git branch -d)
- **Shift+Del** - Force delete branch (git branch -D)
- **Delete** - Delete branch

### Git Operations

- **f** - Fetch from remote
- **u** - Pull latest changes (on current branch)
- **p** - Push to remote (on current branch)
- **m** - Merge selected branch into current
- **r** - Rebase current branch onto selected


### Search

Expand Down
5 changes: 0 additions & 5 deletions src/__mocks__/simple-git.ts

This file was deleted.

111 changes: 111 additions & 0 deletions src/__tests__/components/BranchList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -297,4 +297,115 @@ describe('BranchList', () => {
expect(output).toContain('abc');
});
});

describe('Branch Behind Remote Indicator', () => {
it('should show arrow icon for branches behind remote', () => {
const branchesWithBehind: BranchInfo[] = [
{ name: 'main', current: true, commit: 'abc1234567', label: 'main', behindRemote: 3 },
{ name: 'feature-1', current: false, commit: 'def4567890', label: 'feature-1', behindRemote: 1 },
];

const { lastFrame } = render(
<BranchList
branches={branchesWithBehind}
selectedIndex={0}
topIndex={0}
viewportHeight={10}
/>
);

const output = lastFrame();
expect(output).toContain('↙');
expect(output).toContain('main');
expect(output).toContain('feature-1');
});

it('should not show arrow icon for branches up-to-date with remote', () => {
const branchesUpToDate: BranchInfo[] = [
{ name: 'main', current: true, commit: 'abc1234567', label: 'main' },
{ name: 'feature-1', current: false, commit: 'def4567890', label: 'feature-1' },
];

const { lastFrame } = render(
<BranchList
branches={branchesUpToDate}
selectedIndex={0}
topIndex={0}
viewportHeight={10}
/>
);

const output = lastFrame();
expect(output).not.toContain('↙');
});
});

describe('Uncommitted Changes Indicator', () => {
it('should show indicator for current branch with uncommitted changes', () => {
const branchesWithChanges: BranchInfo[] = [
{ name: 'main', current: true, commit: 'abc1234567', label: 'main', hasUncommittedChanges: true },
{ name: 'feature-1', current: false, commit: 'def4567890', label: 'feature-1' },
];

const { lastFrame } = render(
<BranchList
branches={branchesWithChanges}
selectedIndex={0}
topIndex={0}
viewportHeight={10}
/>
);

const output = lastFrame();
expect(output).toContain('●');
expect(output).toContain('main');
});

it('should not show indicator for current branch without uncommitted changes', () => {
const branchesClean: BranchInfo[] = [
{ name: 'main', current: true, commit: 'abc1234567', label: 'main', hasUncommittedChanges: false },
];

const { lastFrame } = render(
<BranchList
branches={branchesClean}
selectedIndex={0}
topIndex={0}
viewportHeight={10}
/>
);

const output = lastFrame();
expect(output).not.toContain('●');
});
});

describe('Combined Indicators', () => {
it('should show both arrow icon and uncommitted changes indicator', () => {
const branchesWithBoth: BranchInfo[] = [
{
name: 'main',
current: true,
commit: 'abc1234567',
label: 'main',
behindRemote: 2,
hasUncommittedChanges: true
},
];

const { lastFrame } = render(
<BranchList
branches={branchesWithBoth}
selectedIndex={0}
topIndex={0}
viewportHeight={10}
/>
);

const output = lastFrame();
expect(output).toContain('↙');
expect(output).toContain('●');
expect(output).toContain('main');
});
});
});
21 changes: 11 additions & 10 deletions src/__tests__/components/GitBranchUI.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('GitBranchUI - Integration Tests', () => {
isGitRepository: ReturnType<typeof vi.fn>;
createBranch: ReturnType<typeof vi.fn>;
deleteBranch: ReturnType<typeof vi.fn>;
getGitVersion: ReturnType<typeof vi.fn>;
};
let mockBranches: BranchInfo[];

Expand Down Expand Up @@ -58,6 +59,7 @@ describe('GitBranchUI - Integration Tests', () => {
isGitRepository: vi.fn().mockResolvedValue(true),
createBranch: vi.fn().mockResolvedValue(undefined),
deleteBranch: vi.fn().mockResolvedValue(undefined),
getGitVersion: vi.fn().mockResolvedValue('git version 2.39.2'),
};
});

Expand Down Expand Up @@ -152,10 +154,11 @@ describe('GitBranchUI - Integration Tests', () => {
stdin.write('\r');
await new Promise((resolve) => setTimeout(resolve, 100));

expect(mockGitManager.createBranch).toHaveBeenCalledWith('new-feature');
// Now expects to be called with the selected branch name as the second parameter
expect(mockGitManager.createBranch).toHaveBeenCalledWith('new-feature', 'main');

const output = lastFrame();
expect(output).toContain("Branch 'new-feature' created");
expect(output).toContain("Branch 'new-feature' created from 'main'");
expect(output).toContain('Checkout now?');
});

Expand Down Expand Up @@ -231,7 +234,7 @@ describe('GitBranchUI - Integration Tests', () => {

// Move to non-current branch
stdin.write('j');
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, 100));

// Press Delete
stdin.write('\x7F');
Expand All @@ -247,8 +250,6 @@ describe('GitBranchUI - Integration Tests', () => {
// But UI should still be interactive (branch list should still be visible)
expect(output).toContain('main');
expect(output).toContain('feature-1');
// Status bar should still be visible
expect(output).toContain('line');
});

it('should complete full branch deletion workflow', async () => {
Expand All @@ -258,7 +259,7 @@ describe('GitBranchUI - Integration Tests', () => {

// Move to non-current branch
stdin.write('j');
await new Promise((resolve) => setTimeout(resolve, 50));
await new Promise((resolve) => setTimeout(resolve, 100));

// Press Delete
stdin.write('\x7F');
Expand Down Expand Up @@ -476,8 +477,8 @@ describe('GitBranchUI - Integration Tests', () => {
const output = lastFrame();
// Should show branch list
expect(output).toContain('main');
// Should show status bar
expect(output).toContain('line 1 of 3');
// Should show hints in status bar
expect(output).toContain('h: Help');
});

it('should coordinate state across components', async () => {
Expand All @@ -490,8 +491,8 @@ describe('GitBranchUI - Integration Tests', () => {
await new Promise((resolve) => setTimeout(resolve, 50));

const output = lastFrame();
// Status bar should update
expect(output).toContain('line 2 of 3');
// Status bar should show hints
expect(output).toContain('h: Help');
});
});
});
27 changes: 21 additions & 6 deletions src/__tests__/components/Header.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Header } from '../../components/Header.js';
describe('Header', () => {
const mockProps = {
version: '1.0.0',
gitVersion: 'git version 2.39.2',
cwd: '/Users/test/project',
};

Expand All @@ -17,17 +18,25 @@ describe('Header', () => {
});

it('should display version number', () => {
const { lastFrame } = render(<Header version="1.2.3" cwd="/test" />);
const { lastFrame } = render(<Header version="1.2.3" gitVersion="git version 2.39.2" cwd="/test" />);

const output = lastFrame();
expect(output).toContain('1.2.3');
});

it('should display version with space after Rosella', () => {
it('should display version with space after app name', () => {
const { lastFrame } = render(<Header {...mockProps} />);

const output = lastFrame();
expect(output).toContain('Rosella 1.0.0');
// Should display "Rosella 1.0.0" based on config
expect(output).toContain('1.0.0');
});

it('should display git version', () => {
const { lastFrame } = render(<Header {...mockProps} />);

const output = lastFrame();
expect(output).toContain('git version 2.39.2');
});
});

Expand All @@ -41,7 +50,7 @@ describe('Header', () => {

it('should display different cwd when provided', () => {
const { lastFrame } = render(
<Header version="1.0.0" cwd="/different/path" />
<Header version="1.0.0" gitVersion="git version 2.39.2" cwd="/different/path" />
);

const output = lastFrame();
Expand All @@ -68,14 +77,20 @@ describe('Header', () => {

describe('Props Handling', () => {
it('should handle empty version string', () => {
const { lastFrame } = render(<Header version="" cwd="/test" />);
const { lastFrame } = render(<Header version="" gitVersion="git version 2.39.2" cwd="/test" />);

const output = lastFrame();
expect(output).toContain('Rosella');
});

it('should handle empty cwd string', () => {
const { lastFrame } = render(<Header version="1.0.0" cwd="" />);
const { lastFrame } = render(<Header version="1.0.0" gitVersion="git version 2.39.2" cwd="" />);

expect(() => lastFrame()).not.toThrow();
});

it('should handle empty gitVersion string', () => {
const { lastFrame } = render(<Header version="1.0.0" gitVersion="" cwd="/test" />);

expect(() => lastFrame()).not.toThrow();
});
Expand Down
18 changes: 5 additions & 13 deletions src/__tests__/components/Help.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('Help', () => {
const { lastFrame } = render(<Help />);

const output = lastFrame();
expect(output).toContain('rosella - Help');
expect(output).toContain('Rosella - Help');
});
});

Expand Down Expand Up @@ -65,17 +65,7 @@ describe('Help', () => {

const output = lastFrame();
expect(output).toContain('Delete');
expect(output).toContain('Safe delete branch');
expect(output).toContain('git branch -d');
});

it('should list force delete branch command', () => {
const { lastFrame } = render(<Help />);

const output = lastFrame();
expect(output).toContain('Shift+Del');
expect(output).toContain('Force delete branch');
expect(output).toContain('git branch -D');
expect(output).toContain('Delete branch');
});
});

Expand Down Expand Up @@ -155,6 +145,7 @@ describe('Help', () => {
// Verify all major sections are present
expect(output).toContain('Navigation');
expect(output).toContain('Branch Actions');
expect(output).toContain('Git Operations');
expect(output).toContain('Search');
expect(output).toContain('Other');
});
Expand All @@ -172,7 +163,8 @@ describe('Help', () => {

const output = lastFrame();
const shortcuts = [
'↑/↓', 'j/k', 'Enter', 'n', 'Delete', 'Shift+Del',
'↑/↓', 'j/k', 'Enter', 'n', 'Delete',
'f', 'u', 'p', 'm', 'r',
'/', ':', 'Esc', 'h', 'q'
];

Expand Down
Loading