git-thing 1.0.2 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.d.ts +1 -5
- package/dist/app.js +42 -9
- package/dist/cli.js +1 -20
- package/dist/components/BranchList.d.ts +9 -0
- package/dist/components/BranchList.js +93 -0
- package/dist/components/Settings.d.ts +10 -0
- package/dist/components/Settings.js +67 -0
- package/dist/constants/theme.d.ts +45 -0
- package/dist/constants/theme.js +55 -0
- package/dist/hooks/useGitBranches.d.ts +17 -0
- package/dist/hooks/useGitBranches.js +84 -0
- package/dist/hooks/useSettings.d.ts +9 -0
- package/dist/hooks/useSettings.js +31 -0
- package/package.json +2 -5
- package/readme.md +67 -17
package/dist/app.d.ts
CHANGED
package/dist/app.js
CHANGED
|
@@ -1,15 +1,48 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { Text, useApp, useInput } from 'ink';
|
|
3
|
-
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useApp, useInput } from 'ink';
|
|
3
|
+
import { BranchList } from './components/BranchList.js';
|
|
4
|
+
import { Settings } from './components/Settings.js';
|
|
5
|
+
import { useSettings } from './hooks/useSettings.js';
|
|
6
|
+
import { theme } from './constants/theme.js';
|
|
7
|
+
export default function App() {
|
|
4
8
|
const { exit } = useApp();
|
|
9
|
+
const { settings, updateSetting } = useSettings();
|
|
10
|
+
const [activeTab, setActiveTab] = useState('branches');
|
|
11
|
+
const [statusMessage, setStatusMessage] = useState(null);
|
|
12
|
+
// Auto-clear status message after 3s
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!statusMessage) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const timer = setTimeout(() => {
|
|
18
|
+
setStatusMessage(null);
|
|
19
|
+
}, 3000);
|
|
20
|
+
return () => clearTimeout(timer);
|
|
21
|
+
}, [statusMessage]);
|
|
5
22
|
useInput((input, key) => {
|
|
6
|
-
if (
|
|
23
|
+
if (input === 'q' || (key.ctrl && input === 'c')) {
|
|
7
24
|
exit();
|
|
8
25
|
}
|
|
26
|
+
// Tab key switches tabs (but not Shift+Tab which is used for mode switching)
|
|
27
|
+
if (key.tab && !key.shift) {
|
|
28
|
+
setActiveTab((t) => (t === 'branches' ? 'settings' : 'branches'));
|
|
29
|
+
}
|
|
9
30
|
});
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
31
|
+
const handleStatusChange = (message, type) => {
|
|
32
|
+
if (message) {
|
|
33
|
+
setStatusMessage({ text: message, type });
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
setStatusMessage(null);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
40
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
41
|
+
React.createElement(Text, { bold: activeTab === 'branches', color: activeTab === 'branches' ? theme.tabs.activeColor : theme.tabs.inactiveColor }, "[Branches]"),
|
|
42
|
+
React.createElement(Text, null, " "),
|
|
43
|
+
React.createElement(Text, { bold: activeTab === 'settings', color: activeTab === 'settings' ? theme.tabs.activeColor : theme.tabs.inactiveColor }, "[Settings]")),
|
|
44
|
+
React.createElement(Box, { flexDirection: "column", flexGrow: 1 }, activeTab === 'branches' ? (React.createElement(BranchList, { baseBranch: settings.baseBranch, autoStash: settings.autoStash, isActive: activeTab === 'branches', onStatusChange: handleStatusChange })) : (React.createElement(Settings, { baseBranch: settings.baseBranch, autoStash: settings.autoStash, onBaseBranchChange: (value) => updateSetting('baseBranch', value), onAutoStashChange: (value) => updateSetting('autoStash', value), isActive: activeTab === 'settings' }))),
|
|
45
|
+
React.createElement(Box, { marginTop: 1, height: 1 }, statusMessage ? (React.createElement(Text, { color: statusMessage.type === 'success'
|
|
46
|
+
? theme.status.successColor
|
|
47
|
+
: theme.status.errorColor }, statusMessage.text)) : (React.createElement(Text, { dimColor: true }, "Press q to exit")))));
|
|
15
48
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,24 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { render } from 'ink';
|
|
4
|
-
import meow from 'meow';
|
|
5
4
|
import App from './app.js';
|
|
6
|
-
|
|
7
|
-
Usage
|
|
8
|
-
$ Git-thing
|
|
9
|
-
|
|
10
|
-
Options
|
|
11
|
-
--name Your name
|
|
12
|
-
|
|
13
|
-
Examples
|
|
14
|
-
$ Git-thing --name=Jane
|
|
15
|
-
Hello, Jane
|
|
16
|
-
`, {
|
|
17
|
-
importMeta: import.meta,
|
|
18
|
-
flags: {
|
|
19
|
-
name: {
|
|
20
|
-
type: 'string',
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
render(React.createElement(App, { name: cli.flags.name }));
|
|
5
|
+
render(React.createElement(App, null));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
type BranchListProps = {
|
|
3
|
+
baseBranch: string;
|
|
4
|
+
autoStash: boolean;
|
|
5
|
+
isActive: boolean;
|
|
6
|
+
onStatusChange: (message: string | null, type: 'success' | 'error') => void;
|
|
7
|
+
};
|
|
8
|
+
export declare function BranchList({ baseBranch, autoStash, isActive, onStatusChange, }: BranchListProps): React.JSX.Element;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { useGitBranches } from '../hooks/useGitBranches.js';
|
|
4
|
+
import { theme } from '../constants/theme.js';
|
|
5
|
+
export function BranchList({ baseBranch, autoStash, isActive, onStatusChange, }) {
|
|
6
|
+
const { branches, loading, error, checkout, rebaseMaster } = useGitBranches();
|
|
7
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
8
|
+
const [mode, setMode] = useState('selection');
|
|
9
|
+
const [rebasedBranch, setRebasedBranch] = useState(null);
|
|
10
|
+
// Clear rebased indicator after 3 seconds
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!rebasedBranch) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const timer = setTimeout(() => {
|
|
16
|
+
setRebasedBranch(null);
|
|
17
|
+
}, 3000);
|
|
18
|
+
return () => clearTimeout(timer);
|
|
19
|
+
}, [rebasedBranch]);
|
|
20
|
+
const cycleMode = () => {
|
|
21
|
+
setMode((m) => (m === 'selection' ? 'rebase' : 'selection'));
|
|
22
|
+
};
|
|
23
|
+
useInput((_input, key) => {
|
|
24
|
+
// SHIFT+TAB to switch modes within branch list
|
|
25
|
+
if (key.shift && key.tab) {
|
|
26
|
+
cycleMode();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (key.upArrow) {
|
|
30
|
+
setSelectedIndex((i) => (i > 0 ? i - 1 : branches.length - 1));
|
|
31
|
+
}
|
|
32
|
+
else if (key.downArrow) {
|
|
33
|
+
setSelectedIndex((i) => (i < branches.length - 1 ? i + 1 : 0));
|
|
34
|
+
}
|
|
35
|
+
else if (key.return) {
|
|
36
|
+
const branch = branches[selectedIndex];
|
|
37
|
+
if (branch && !branch.isCurrent) {
|
|
38
|
+
if (mode === 'selection') {
|
|
39
|
+
checkout(branch.name).catch((err) => {
|
|
40
|
+
onStatusChange(err.message, 'error');
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
else if (mode === 'rebase') {
|
|
44
|
+
const branchName = branch.name;
|
|
45
|
+
rebaseMaster({ baseBranch, autoStash })
|
|
46
|
+
.then(() => {
|
|
47
|
+
setRebasedBranch(branchName);
|
|
48
|
+
})
|
|
49
|
+
.catch((err) => {
|
|
50
|
+
onStatusChange(err.message, 'error');
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}, { isActive });
|
|
56
|
+
if (loading) {
|
|
57
|
+
return React.createElement(Text, { dimColor: true }, "Loading branches...");
|
|
58
|
+
}
|
|
59
|
+
if (error) {
|
|
60
|
+
return React.createElement(Text, { color: theme.error.color },
|
|
61
|
+
"Error: ",
|
|
62
|
+
error);
|
|
63
|
+
}
|
|
64
|
+
if (branches.length === 0) {
|
|
65
|
+
return React.createElement(Text, { dimColor: true }, "No branches found");
|
|
66
|
+
}
|
|
67
|
+
const modeConfig = theme.modes[mode];
|
|
68
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
69
|
+
React.createElement(Box, { alignSelf: "flex-start", borderStyle: theme.heading.borderStyle, borderColor: theme.heading.borderColor, paddingX: theme.heading.paddingX, paddingY: theme.heading.paddingY, marginBottom: theme.heading.marginBottom },
|
|
70
|
+
React.createElement(Text, { bold: theme.heading.bold, color: theme.heading.color }, theme.heading.text)),
|
|
71
|
+
React.createElement(Box, { marginBottom: 1 },
|
|
72
|
+
React.createElement(Text, null, "MODE: "),
|
|
73
|
+
React.createElement(Text, { bold: true, color: modeConfig.color }, modeConfig.label),
|
|
74
|
+
mode === 'rebase' && React.createElement(Text, { dimColor: true },
|
|
75
|
+
" (base: ",
|
|
76
|
+
baseBranch,
|
|
77
|
+
")")),
|
|
78
|
+
branches.map((branch, index) => {
|
|
79
|
+
const isSelected = index === selectedIndex;
|
|
80
|
+
const wasRebased = branch.name === rebasedBranch;
|
|
81
|
+
return (React.createElement(Box, { key: branch.name },
|
|
82
|
+
React.createElement(Text, { color: branch.isCurrent ? theme.branch.currentColor : undefined }, branch.isCurrent ? '* ' : ' '),
|
|
83
|
+
React.createElement(Text, { color: isSelected ? modeConfig.color : undefined, bold: isSelected && theme.branch.selectedBold }, branch.name),
|
|
84
|
+
wasRebased && (React.createElement(Text, { bold: true, color: theme.modes.rebase.color },
|
|
85
|
+
' ',
|
|
86
|
+
"REBASED"))));
|
|
87
|
+
}),
|
|
88
|
+
React.createElement(Box, { marginTop: 1 },
|
|
89
|
+
React.createElement(Text, { dimColor: theme.help.dimmed },
|
|
90
|
+
"\u2191\u2193 navigate Enter ",
|
|
91
|
+
mode === 'selection' ? 'checkout' : 'rebase',
|
|
92
|
+
" Shift+Tab switch mode Tab switch tab q quit"))));
|
|
93
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
type SettingsProps = {
|
|
3
|
+
baseBranch: string;
|
|
4
|
+
autoStash: boolean;
|
|
5
|
+
onBaseBranchChange: (value: string) => void;
|
|
6
|
+
onAutoStashChange: (value: boolean) => void;
|
|
7
|
+
isActive: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function Settings({ baseBranch, autoStash, onBaseBranchChange, onAutoStashChange, isActive, }: SettingsProps): React.JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { theme } from '../constants/theme.js';
|
|
4
|
+
export function Settings({ baseBranch, autoStash, onBaseBranchChange, onAutoStashChange, isActive, }) {
|
|
5
|
+
const [selectedItem, setSelectedItem] = useState('baseBranch');
|
|
6
|
+
const [editValue, setEditValue] = useState(baseBranch);
|
|
7
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
8
|
+
const settingItems = ['baseBranch', 'autoStash'];
|
|
9
|
+
useInput((input, key) => {
|
|
10
|
+
if (isEditing) {
|
|
11
|
+
if (key.return) {
|
|
12
|
+
if (editValue.trim()) {
|
|
13
|
+
onBaseBranchChange(editValue.trim());
|
|
14
|
+
}
|
|
15
|
+
setIsEditing(false);
|
|
16
|
+
}
|
|
17
|
+
else if (key.escape) {
|
|
18
|
+
setEditValue(baseBranch);
|
|
19
|
+
setIsEditing(false);
|
|
20
|
+
}
|
|
21
|
+
else if (key.backspace || key.delete) {
|
|
22
|
+
setEditValue((v) => v.slice(0, -1));
|
|
23
|
+
}
|
|
24
|
+
else if (input && !key.ctrl && !key.meta && !key.tab) {
|
|
25
|
+
setEditValue((v) => v + input);
|
|
26
|
+
}
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (key.upArrow) {
|
|
30
|
+
const currentIndex = settingItems.indexOf(selectedItem);
|
|
31
|
+
const newIndex = currentIndex > 0 ? currentIndex - 1 : settingItems.length - 1;
|
|
32
|
+
setSelectedItem(settingItems[newIndex]);
|
|
33
|
+
}
|
|
34
|
+
else if (key.downArrow) {
|
|
35
|
+
const currentIndex = settingItems.indexOf(selectedItem);
|
|
36
|
+
const newIndex = currentIndex < settingItems.length - 1 ? currentIndex + 1 : 0;
|
|
37
|
+
setSelectedItem(settingItems[newIndex]);
|
|
38
|
+
}
|
|
39
|
+
else if (key.return || input === ' ') {
|
|
40
|
+
if (selectedItem === 'baseBranch') {
|
|
41
|
+
setIsEditing(true);
|
|
42
|
+
setEditValue(baseBranch);
|
|
43
|
+
}
|
|
44
|
+
else if (selectedItem === 'autoStash') {
|
|
45
|
+
onAutoStashChange(!autoStash);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}, { isActive });
|
|
49
|
+
const modeConfig = theme.modes.settings;
|
|
50
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
51
|
+
React.createElement(Box, { alignSelf: "flex-start", borderStyle: theme.heading.borderStyle, borderColor: modeConfig.color, paddingX: theme.heading.paddingX, paddingY: theme.heading.paddingY, marginBottom: 1 },
|
|
52
|
+
React.createElement(Text, { bold: true, color: modeConfig.color }, "SETTINGS")),
|
|
53
|
+
React.createElement(Box, null,
|
|
54
|
+
React.createElement(Text, { color: selectedItem === 'baseBranch' ? modeConfig.color : undefined }, selectedItem === 'baseBranch' ? '▸ ' : ' '),
|
|
55
|
+
React.createElement(Text, null, "Base Branch: "),
|
|
56
|
+
isEditing ? (React.createElement(Text, { color: "yellow" },
|
|
57
|
+
editValue,
|
|
58
|
+
"\u2588")) : (React.createElement(Text, { bold: selectedItem === 'baseBranch', color: modeConfig.color }, baseBranch))),
|
|
59
|
+
React.createElement(Box, null,
|
|
60
|
+
React.createElement(Text, { color: selectedItem === 'autoStash' ? modeConfig.color : undefined }, selectedItem === 'autoStash' ? '▸ ' : ' '),
|
|
61
|
+
React.createElement(Text, null, "Auto Stash: "),
|
|
62
|
+
React.createElement(Text, { bold: selectedItem === 'autoStash', color: autoStash ? 'green' : 'red' }, autoStash ? 'ON' : 'OFF')),
|
|
63
|
+
React.createElement(Box, { marginTop: 1 },
|
|
64
|
+
React.createElement(Text, { dimColor: theme.help.dimmed }, isEditing
|
|
65
|
+
? 'Enter save Esc cancel'
|
|
66
|
+
: '↑↓ navigate Enter/Space toggle Tab switch tab q quit'))));
|
|
67
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const theme: {
|
|
2
|
+
heading: {
|
|
3
|
+
text: string;
|
|
4
|
+
color: string;
|
|
5
|
+
bold: boolean;
|
|
6
|
+
borderStyle: "double";
|
|
7
|
+
borderColor: string;
|
|
8
|
+
paddingX: number;
|
|
9
|
+
paddingY: number;
|
|
10
|
+
marginBottom: number;
|
|
11
|
+
};
|
|
12
|
+
branch: {
|
|
13
|
+
currentColor: string;
|
|
14
|
+
selectedColor: string;
|
|
15
|
+
selectedBold: boolean;
|
|
16
|
+
};
|
|
17
|
+
help: {
|
|
18
|
+
dimmed: boolean;
|
|
19
|
+
};
|
|
20
|
+
error: {
|
|
21
|
+
color: string;
|
|
22
|
+
};
|
|
23
|
+
modes: {
|
|
24
|
+
selection: {
|
|
25
|
+
label: string;
|
|
26
|
+
color: string;
|
|
27
|
+
};
|
|
28
|
+
rebase: {
|
|
29
|
+
label: string;
|
|
30
|
+
color: string;
|
|
31
|
+
};
|
|
32
|
+
settings: {
|
|
33
|
+
label: string;
|
|
34
|
+
color: string;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
status: {
|
|
38
|
+
successColor: string;
|
|
39
|
+
errorColor: string;
|
|
40
|
+
};
|
|
41
|
+
tabs: {
|
|
42
|
+
activeColor: string;
|
|
43
|
+
inactiveColor: string;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Terminal colors: black, red, green, yellow, blue, magenta, cyan, white, gray
|
|
2
|
+
// Or hex: #ff0000, rgb: rgb(255,0,0)
|
|
3
|
+
export const theme = {
|
|
4
|
+
// Heading
|
|
5
|
+
heading: {
|
|
6
|
+
text: 'BRANCHES',
|
|
7
|
+
color: 'blue',
|
|
8
|
+
bold: true,
|
|
9
|
+
// Border: 'single', 'double', 'round', 'bold', 'classic', 'singleDouble', 'doubleSingle'
|
|
10
|
+
borderStyle: 'double',
|
|
11
|
+
borderColor: 'blue',
|
|
12
|
+
paddingX: 2, // horizontal padding inside box
|
|
13
|
+
paddingY: 0, // vertical padding inside box
|
|
14
|
+
marginBottom: 0, // space below heading
|
|
15
|
+
},
|
|
16
|
+
// Branch list
|
|
17
|
+
branch: {
|
|
18
|
+
currentColor: 'green',
|
|
19
|
+
selectedColor: 'blue',
|
|
20
|
+
selectedBold: true,
|
|
21
|
+
},
|
|
22
|
+
// Help text
|
|
23
|
+
help: {
|
|
24
|
+
dimmed: true,
|
|
25
|
+
},
|
|
26
|
+
// Errors
|
|
27
|
+
error: {
|
|
28
|
+
color: 'red',
|
|
29
|
+
},
|
|
30
|
+
// Modes
|
|
31
|
+
modes: {
|
|
32
|
+
selection: {
|
|
33
|
+
label: 'Selection',
|
|
34
|
+
color: 'blue',
|
|
35
|
+
},
|
|
36
|
+
rebase: {
|
|
37
|
+
label: 'Rebase Master',
|
|
38
|
+
color: 'magenta',
|
|
39
|
+
},
|
|
40
|
+
settings: {
|
|
41
|
+
label: 'Settings',
|
|
42
|
+
color: 'cyan',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
// Status messages
|
|
46
|
+
status: {
|
|
47
|
+
successColor: 'green',
|
|
48
|
+
errorColor: 'red',
|
|
49
|
+
},
|
|
50
|
+
// Tabs
|
|
51
|
+
tabs: {
|
|
52
|
+
activeColor: 'cyan',
|
|
53
|
+
inactiveColor: 'gray',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type Branch = {
|
|
2
|
+
name: string;
|
|
3
|
+
isCurrent: boolean;
|
|
4
|
+
};
|
|
5
|
+
export type RebaseOptions = {
|
|
6
|
+
baseBranch: string;
|
|
7
|
+
autoStash: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type UseGitBranchesResult = {
|
|
10
|
+
branches: Branch[];
|
|
11
|
+
loading: boolean;
|
|
12
|
+
error: string | null;
|
|
13
|
+
refresh: () => void;
|
|
14
|
+
checkout: (branchName: string) => Promise<void>;
|
|
15
|
+
rebaseMaster: (options: RebaseOptions) => Promise<string>;
|
|
16
|
+
};
|
|
17
|
+
export declare function useGitBranches(): UseGitBranchesResult;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { execFile } from 'child_process';
|
|
3
|
+
export function useGitBranches() {
|
|
4
|
+
const [branches, setBranches] = useState([]);
|
|
5
|
+
const [loading, setLoading] = useState(true);
|
|
6
|
+
const [error, setError] = useState(null);
|
|
7
|
+
const loadBranches = useCallback(() => {
|
|
8
|
+
execFile('git', ['branch'], (err, stdout, stderr) => {
|
|
9
|
+
setLoading(false);
|
|
10
|
+
if (err) {
|
|
11
|
+
if (stderr.includes('not a git repository')) {
|
|
12
|
+
setError('Not a git repository');
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
setError(stderr || err.message);
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const parsedBranches = stdout
|
|
20
|
+
.split('\n')
|
|
21
|
+
.filter((line) => line.trim())
|
|
22
|
+
.map((line) => {
|
|
23
|
+
const isCurrent = line.startsWith('*');
|
|
24
|
+
const name = line.replace(/^\*?\s+/, '');
|
|
25
|
+
return { name, isCurrent };
|
|
26
|
+
});
|
|
27
|
+
setBranches(parsedBranches);
|
|
28
|
+
});
|
|
29
|
+
}, []);
|
|
30
|
+
const checkout = useCallback((branchName) => {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
execFile('git', ['checkout', branchName], (err, _stdout, stderr) => {
|
|
33
|
+
if (err) {
|
|
34
|
+
reject(new Error(stderr || err.message));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
loadBranches();
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}, [loadBranches]);
|
|
42
|
+
const rebaseMaster = useCallback(({ baseBranch, autoStash }) => {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const currentBranch = branches.find((b) => b.isCurrent)?.name;
|
|
45
|
+
if (currentBranch === 'master') {
|
|
46
|
+
// On master branch: just pull --rebase
|
|
47
|
+
execFile('git', ['pull', '--rebase', 'origin', 'master'], (err, stdout, stderr) => {
|
|
48
|
+
if (err) {
|
|
49
|
+
reject(new Error(stderr || err.message));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
loadBranches();
|
|
53
|
+
resolve(stdout || 'Successfully pulled and rebased master');
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// On other branch: fetch origin master:master, then rebase
|
|
58
|
+
execFile('git', ['fetch', 'origin', 'master:master'], (fetchErr, _fetchStdout, fetchStderr) => {
|
|
59
|
+
if (fetchErr) {
|
|
60
|
+
reject(new Error(fetchStderr || fetchErr.message));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const rebaseArgs = ['rebase'];
|
|
64
|
+
if (autoStash) {
|
|
65
|
+
rebaseArgs.push('--autostash');
|
|
66
|
+
}
|
|
67
|
+
rebaseArgs.push(baseBranch);
|
|
68
|
+
execFile('git', rebaseArgs, (err, stdout, stderr) => {
|
|
69
|
+
if (err) {
|
|
70
|
+
reject(new Error(stderr || err.message));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
loadBranches();
|
|
74
|
+
resolve(stdout || `Successfully rebased onto ${baseBranch}`);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}, [loadBranches, branches]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
loadBranches();
|
|
82
|
+
}, [loadBranches]);
|
|
83
|
+
return { branches, loading, error, refresh: loadBranches, checkout, rebaseMaster };
|
|
84
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
const SETTINGS_PATH = join(homedir(), '.git-thing-settings.json');
|
|
6
|
+
const DEFAULT_SETTINGS = {
|
|
7
|
+
baseBranch: 'main',
|
|
8
|
+
autoStash: true,
|
|
9
|
+
};
|
|
10
|
+
export function useSettings() {
|
|
11
|
+
const [settings, setSettings] = useState(DEFAULT_SETTINGS);
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
14
|
+
try {
|
|
15
|
+
const data = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
16
|
+
setSettings({ ...DEFAULT_SETTINGS, ...data });
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// If file is corrupted, use defaults
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}, []);
|
|
23
|
+
const updateSetting = useCallback((key, value) => {
|
|
24
|
+
setSettings((prev) => {
|
|
25
|
+
const next = { ...prev, [key]: value };
|
|
26
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(next, null, 2));
|
|
27
|
+
return next;
|
|
28
|
+
});
|
|
29
|
+
}, []);
|
|
30
|
+
return { settings, updateSetting };
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-thing",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,16 +13,13 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "tsc",
|
|
16
|
-
"dev": "tsc
|
|
17
|
-
"dev:watch": "tsx watch source/cli.tsx",
|
|
18
|
-
"test": "prettier --check . && xo && ava"
|
|
16
|
+
"dev": "tsc && node dist/cli.js"
|
|
19
17
|
},
|
|
20
18
|
"files": [
|
|
21
19
|
"dist"
|
|
22
20
|
],
|
|
23
21
|
"dependencies": {
|
|
24
22
|
"ink": "^4.1.0",
|
|
25
|
-
"meow": "^11.0.0",
|
|
26
23
|
"react": "^18.2.0"
|
|
27
24
|
},
|
|
28
25
|
"devDependencies": {
|
package/readme.md
CHANGED
|
@@ -1,25 +1,75 @@
|
|
|
1
|
-
#
|
|
1
|
+
# git-thing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A streamlined CLI tool to handle the heavy lifting of feature branch workflows.
|
|
4
|
+
Stop typing long Git commands and let `git-thing` manage your branching, switching, and rebasing.
|
|
4
5
|
|
|
5
|
-
##
|
|
6
|
+
## Description
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
$ npm install --global Git-thing
|
|
9
|
-
```
|
|
8
|
+
`git-thing` is a simple utility designed to automate the repetitive parts of a Git-based workflow. It allows you to:
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
- **Create** new feature branches instantly.
|
|
11
|
+
- **Switch** between active tasks without friction.
|
|
12
|
+
- **Rebase** your work against the main branch to keep your history clean.
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
$ Git-thing --help
|
|
14
|
+
---
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
$ Git-thing
|
|
16
|
+
## Installation
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
--name Your name
|
|
18
|
+
To use `git-thing` as a permanent command on your machine without using `npx` every time, install it globally:
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
npm install -g git-thing
|
|
21
|
+
|
|
22
|
+
## How to Update
|
|
23
|
+
|
|
24
|
+
When a new version is published to NPM, sync your local copy by running:
|
|
25
|
+
|
|
26
|
+
npm update -g git-thing
|
|
27
|
+
|
|
28
|
+
## How to Uninstall
|
|
29
|
+
|
|
30
|
+
If you no longer need the tool, remove it with:
|
|
31
|
+
|
|
32
|
+
npm uninstall -g git-thing
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Setting up an Alias (e.g., gt)
|
|
37
|
+
|
|
38
|
+
If `git-thing` is too long to type, you can set up a shorter alias like `gt`.
|
|
39
|
+
|
|
40
|
+
### macOS and Linux (Zsh or Bash)
|
|
41
|
+
|
|
42
|
+
1. Open your configuration file (usually `~/.zshrc` or `~/.bashrc`):
|
|
43
|
+
|
|
44
|
+
nano ~/.zshrc
|
|
45
|
+
|
|
46
|
+
2. Add this line at the end:
|
|
47
|
+
|
|
48
|
+
alias gt='git-thing'
|
|
49
|
+
|
|
50
|
+
3. Save, exit, and reload your shell:
|
|
51
|
+
|
|
52
|
+
source ~/.zshrc
|
|
53
|
+
|
|
54
|
+
### Windows (PowerShell)
|
|
55
|
+
|
|
56
|
+
1. Open your PowerShell profile:
|
|
57
|
+
|
|
58
|
+
notepad $PROFILE
|
|
59
|
+
|
|
60
|
+
2. Add this line:
|
|
61
|
+
|
|
62
|
+
Set-Alias -Name gt -Value git-thing
|
|
63
|
+
|
|
64
|
+
3. Save and restart your PowerShell terminal.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Usage
|
|
69
|
+
|
|
70
|
+
Once installed (and optionally aliased), you can run the tool from any directory:
|
|
71
|
+
|
|
72
|
+
git-thing
|
|
73
|
+
|
|
74
|
+
# OR, if aliased:
|
|
75
|
+
gt
|