git-drive 0.1.6 → 0.1.7
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/.github/workflows/ci.yml +77 -0
- package/.planning/codebase/ARCHITECTURE.md +151 -0
- package/.planning/codebase/CONCERNS.md +191 -0
- package/.planning/codebase/CONVENTIONS.md +169 -0
- package/.planning/codebase/INTEGRATIONS.md +94 -0
- package/.planning/codebase/STACK.md +77 -0
- package/.planning/codebase/STRUCTURE.md +157 -0
- package/.planning/codebase/TESTING.md +156 -0
- package/Dockerfile.cli +30 -0
- package/Dockerfile.server +32 -0
- package/README.md +157 -0
- package/docker-compose.yml +48 -0
- package/package.json +20 -55
- package/packages/cli/Dockerfile +26 -0
- package/packages/cli/jest.config.js +26 -0
- package/packages/cli/package.json +65 -0
- package/packages/cli/src/__tests__/commands/companion.test.ts +152 -0
- package/packages/cli/src/__tests__/commands/init.test.ts +154 -0
- package/packages/cli/src/__tests__/commands/list.test.ts +122 -0
- package/packages/cli/src/__tests__/commands/push.test.ts +155 -0
- package/packages/cli/src/__tests__/commands/restore.test.ts +135 -0
- package/packages/cli/src/__tests__/commands/status.test.ts +199 -0
- package/packages/cli/src/__tests__/config.test.ts +198 -0
- package/packages/cli/src/__tests__/e2e.test.ts +125 -0
- package/packages/cli/src/__tests__/errors.test.ts +66 -0
- package/packages/cli/src/__tests__/git.test.ts +250 -0
- package/packages/cli/src/__tests__/server.test.ts +371 -0
- package/packages/cli/src/commands/archive.ts +39 -0
- package/packages/cli/src/commands/companion.ts +205 -0
- package/packages/cli/src/commands/init.ts +130 -0
- package/packages/cli/src/commands/link.ts +151 -0
- package/packages/cli/src/commands/list.ts +94 -0
- package/packages/cli/src/commands/push.ts +77 -0
- package/packages/cli/src/commands/restore.ts +36 -0
- package/packages/cli/src/commands/status.ts +127 -0
- package/packages/cli/src/config.ts +73 -0
- package/packages/cli/src/errors.ts +23 -0
- package/packages/cli/src/git.ts +60 -0
- package/packages/cli/src/index.ts +129 -0
- package/packages/cli/src/server.ts +700 -0
- package/packages/cli/tsconfig.json +13 -0
- package/packages/git-drive-docker/package.json +15 -0
- package/packages/server/package.json +44 -0
- package/packages/server/src/index.ts +569 -0
- package/packages/server/tsconfig.json +9 -0
- package/packages/ui/README.md +73 -0
- package/packages/ui/eslint.config.js +23 -0
- package/packages/ui/index.html +13 -0
- package/packages/ui/package.json +52 -0
- package/packages/ui/postcss.config.js +6 -0
- package/packages/ui/public/vite.svg +1 -0
- package/packages/ui/src/App.css +23 -0
- package/packages/ui/src/App.test.tsx +248 -0
- package/packages/ui/src/App.tsx +803 -0
- package/packages/ui/src/assets/react.svg +8 -0
- package/packages/ui/src/assets/vite.svg +3 -0
- package/packages/ui/src/index.css +37 -0
- package/packages/ui/src/main.tsx +14 -0
- package/packages/ui/src/test/setup.ts +1 -0
- package/packages/ui/tailwind.config.js +11 -0
- package/packages/ui/tsconfig.app.json +28 -0
- package/packages/ui/tsconfig.json +26 -0
- package/packages/ui/tsconfig.node.json +12 -0
- package/packages/ui/vite.config.ts +7 -0
- package/packages/ui/vitest.config.ts +20 -0
- package/pnpm-workspace.yaml +4 -0
- package/rewrite_app.js +731 -0
- package/tsconfig.json +14 -0
- package/dist/__tests__/commands/init.test.js +0 -123
- package/dist/__tests__/commands/list.test.js +0 -91
- package/dist/__tests__/commands/push.test.js +0 -128
- package/dist/__tests__/commands/restore.test.js +0 -99
- package/dist/__tests__/commands/status.test.js +0 -151
- package/dist/__tests__/config.test.js +0 -150
- package/dist/__tests__/e2e.test.js +0 -107
- package/dist/__tests__/errors.test.js +0 -56
- package/dist/__tests__/git.test.js +0 -184
- package/dist/__tests__/server.test.js +0 -310
- package/dist/commands/archive.js +0 -32
- package/dist/commands/init.js +0 -55
- package/dist/commands/link.js +0 -175
- package/dist/commands/list.js +0 -83
- package/dist/commands/push.js +0 -112
- package/dist/commands/restore.js +0 -30
- package/dist/commands/status.js +0 -116
- package/dist/config.js +0 -62
- package/dist/errors.js +0 -30
- package/dist/git.js +0 -67
- package/dist/index.js +0 -108
- package/dist/server.js +0 -535
- /package/{ui → packages/cli/ui}/assets/index-Br8xQbJz.js +0 -0
- /package/{ui → packages/cli/ui}/assets/index-Cc2q1t5k.js +0 -0
- /package/{ui → packages/cli/ui}/assets/index-DrL7ojPA.css +0 -0
- /package/{ui → packages/cli/ui}/index.html +0 -0
- /package/{ui → packages/cli/ui}/vite.svg +0 -0
package/package.json
CHANGED
|
@@ -1,64 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-drive",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"backup",
|
|
8
|
-
"external-drive",
|
|
9
|
-
"usb",
|
|
10
|
-
"remote",
|
|
11
|
-
"cli",
|
|
12
|
-
"docker"
|
|
3
|
+
"version": "0.1.7",
|
|
4
|
+
"description": "Use an external drive as a git bare repository remote",
|
|
5
|
+
"workspaces": [
|
|
6
|
+
"packages/*"
|
|
13
7
|
],
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
"homepage": "https://github.com/josmanvis/git-drive#readme",
|
|
25
|
-
"type": "commonjs",
|
|
26
|
-
"bin": {
|
|
27
|
-
"git-drive": "dist/index.js"
|
|
28
|
-
},
|
|
29
|
-
"main": "./dist/index.js",
|
|
30
|
-
"files": [
|
|
31
|
-
"dist",
|
|
32
|
-
"ui"
|
|
33
|
-
],
|
|
34
|
-
"dependencies": {
|
|
35
|
-
"express": "^4.19.2",
|
|
36
|
-
"node-disk-info": "^1.3.0",
|
|
37
|
-
"prompts": "^2.4.2"
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "pnpm build:cli && pnpm build:ui && pnpm build:server",
|
|
10
|
+
"build:cli": "tsc -p packages/cli/tsconfig.json",
|
|
11
|
+
"build:ui": "cd packages/ui && pnpm run build",
|
|
12
|
+
"build:server": "tsc -p packages/server/tsconfig.json",
|
|
13
|
+
"dev": "tsc --watch",
|
|
14
|
+
"test": "pnpm -r test",
|
|
15
|
+
"test:cli": "cd packages/cli && pnpm test",
|
|
16
|
+
"test:ui": "cd packages/ui && pnpm test",
|
|
17
|
+
"test:coverage": "pnpm -r test:coverage"
|
|
38
18
|
},
|
|
39
19
|
"devDependencies": {
|
|
40
|
-
"@types/express": "^4.17.21",
|
|
41
20
|
"@types/node": "^22.0.0",
|
|
42
|
-
"@types/prompts": "^2.4.9",
|
|
43
|
-
"@types/jest": "^29.5.14",
|
|
44
|
-
"@types/supertest": "^6.0.2",
|
|
45
|
-
"jest": "^29.7.0",
|
|
46
|
-
"ts-jest": "^29.2.5",
|
|
47
|
-
"supertest": "^6.3.4",
|
|
48
|
-
"memfs": "^4.14.0",
|
|
49
21
|
"typescript": "^5.7.0"
|
|
50
22
|
},
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"start": "node dist/index.js",
|
|
57
|
-
"start:server": "node dist/server.js",
|
|
58
|
-
"docker:build": "docker build -t git-drive .",
|
|
59
|
-
"docker:run": "docker run -it --rm -v /Volumes:/Volumes -p 4483:4483 git-drive",
|
|
60
|
-
"test": "jest",
|
|
61
|
-
"test:watch": "jest --watch",
|
|
62
|
-
"test:coverage": "jest --coverage"
|
|
23
|
+
"pnpm": {
|
|
24
|
+
"onlyBuiltDependencies": [
|
|
25
|
+
"drivelist",
|
|
26
|
+
"esbuild"
|
|
27
|
+
]
|
|
63
28
|
}
|
|
64
|
-
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Git Drive - Docker Image
|
|
2
|
+
# Includes CLI, server, and web UI
|
|
3
|
+
|
|
4
|
+
FROM node:20-alpine
|
|
5
|
+
|
|
6
|
+
# Install git and util-linux (for lsblk)
|
|
7
|
+
RUN apk add --no-cache git util-linux
|
|
8
|
+
|
|
9
|
+
WORKDIR /app
|
|
10
|
+
|
|
11
|
+
# Copy package files
|
|
12
|
+
COPY package.json ./
|
|
13
|
+
COPY dist ./dist/
|
|
14
|
+
COPY ui ./ui/
|
|
15
|
+
|
|
16
|
+
# Install production dependencies only
|
|
17
|
+
RUN npm install --omit=dev
|
|
18
|
+
|
|
19
|
+
# Expose the web UI port
|
|
20
|
+
EXPOSE 4483
|
|
21
|
+
|
|
22
|
+
# Set environment
|
|
23
|
+
ENV GIT_DRIVE_PORT=4483
|
|
24
|
+
|
|
25
|
+
# Default command starts the server
|
|
26
|
+
CMD ["node", "dist/server.js"]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
preset: 'ts-jest',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
roots: ['<rootDir>/src'],
|
|
6
|
+
testMatch: ['**/__tests__/**/*.ts', '**/*.test.ts', '**/*.spec.ts'],
|
|
7
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
|
8
|
+
collectCoverageFrom: [
|
|
9
|
+
'src/**/*.ts',
|
|
10
|
+
'!src/**/*.d.ts',
|
|
11
|
+
'!src/__tests__/**',
|
|
12
|
+
],
|
|
13
|
+
coverageDirectory: 'coverage',
|
|
14
|
+
coverageReporters: ['text', 'lcov', 'html'],
|
|
15
|
+
verbose: true,
|
|
16
|
+
clearMocks: true,
|
|
17
|
+
restoreMocks: true,
|
|
18
|
+
moduleNameMapper: {
|
|
19
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
20
|
+
},
|
|
21
|
+
transform: {
|
|
22
|
+
'^.+\\.tsx?$': ['ts-jest', {
|
|
23
|
+
useESM: false,
|
|
24
|
+
}],
|
|
25
|
+
},
|
|
26
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "git-drive",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "Turn any external drive into a git remote backup for your code - CLI, server, and web UI",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"git",
|
|
7
|
+
"backup",
|
|
8
|
+
"external-drive",
|
|
9
|
+
"usb",
|
|
10
|
+
"remote",
|
|
11
|
+
"cli",
|
|
12
|
+
"docker"
|
|
13
|
+
],
|
|
14
|
+
"author": "",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/josmanvis/git-drive.git",
|
|
19
|
+
"directory": "packages/cli"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/josmanvis/git-drive/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/josmanvis/git-drive#readme",
|
|
25
|
+
"type": "commonjs",
|
|
26
|
+
"bin": {
|
|
27
|
+
"git-drive": "dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"main": "./dist/index.js",
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"ui"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.json",
|
|
36
|
+
"start": "node dist/index.js",
|
|
37
|
+
"start:server": "node dist/server.js",
|
|
38
|
+
"docker:build": "docker build -t git-drive .",
|
|
39
|
+
"docker:run": "docker run -it --rm -v /Volumes:/Volumes -p 4483:4483 git-drive",
|
|
40
|
+
"prepublishOnly": "npm run build",
|
|
41
|
+
"test": "jest",
|
|
42
|
+
"test:watch": "jest --watch",
|
|
43
|
+
"test:coverage": "jest --coverage"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"express": "^4.19.2",
|
|
47
|
+
"node-disk-info": "^1.3.0",
|
|
48
|
+
"prompts": "^2.4.2"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/express": "^4.17.21",
|
|
52
|
+
"@types/node": "^22.0.0",
|
|
53
|
+
"@types/prompts": "^2.4.9",
|
|
54
|
+
"@types/jest": "^29.5.14",
|
|
55
|
+
"@types/supertest": "^6.0.2",
|
|
56
|
+
"jest": "^29.7.0",
|
|
57
|
+
"ts-jest": "^29.2.5",
|
|
58
|
+
"supertest": "^6.3.4",
|
|
59
|
+
"memfs": "^4.14.0",
|
|
60
|
+
"typescript": "^5.7.0"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=18"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { vol } from 'memfs';
|
|
2
|
+
|
|
3
|
+
// Mock fs
|
|
4
|
+
jest.mock('fs', () => {
|
|
5
|
+
const { fs } = require('memfs');
|
|
6
|
+
return fs;
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Mock node-disk-info
|
|
10
|
+
jest.mock('node-disk-info', () => ({
|
|
11
|
+
getDiskInfo: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// Mock child_process
|
|
15
|
+
jest.mock('child_process', () => ({
|
|
16
|
+
spawn: jest.fn(() => ({
|
|
17
|
+
on: jest.fn(),
|
|
18
|
+
kill: jest.fn(),
|
|
19
|
+
})),
|
|
20
|
+
execSync: jest.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Mock os
|
|
24
|
+
jest.mock('os', () => ({
|
|
25
|
+
homedir: () => '/home/testuser',
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Mock readline
|
|
29
|
+
jest.mock('readline', () => ({
|
|
30
|
+
createInterface: jest.fn(() => ({
|
|
31
|
+
question: jest.fn((_prompt, callback) => callback()),
|
|
32
|
+
close: jest.fn(),
|
|
33
|
+
})),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
// Mock prompts
|
|
37
|
+
jest.mock('prompts', () => jest.fn());
|
|
38
|
+
|
|
39
|
+
import { getDiskInfo } from 'node-disk-info';
|
|
40
|
+
import { getCompanionInfo, isPortAvailable, findAvailablePort } from '../../commands/companion.js';
|
|
41
|
+
|
|
42
|
+
const mockGetDiskInfo = getDiskInfo as jest.Mock;
|
|
43
|
+
|
|
44
|
+
// Mock fetch for port testing
|
|
45
|
+
const mockFetch = jest.fn();
|
|
46
|
+
global.fetch = mockFetch;
|
|
47
|
+
|
|
48
|
+
describe('companion command', () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
jest.clearAllMocks();
|
|
51
|
+
vol.reset();
|
|
52
|
+
mockFetch.mockReset();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('getCompanionInfo', () => {
|
|
56
|
+
it('should return installed: false when companion repo does not exist', () => {
|
|
57
|
+
vol.fromJSON({
|
|
58
|
+
'/Volumes/MyUSB/.git-drive': '',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const result = getCompanionInfo('/Volumes/MyUSB');
|
|
62
|
+
|
|
63
|
+
expect(result.installed).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should return installed: true when companion repo exists', () => {
|
|
67
|
+
vol.fromJSON({
|
|
68
|
+
'/Volumes/MyUSB/.git-drive/git-drive.git/HEAD': '',
|
|
69
|
+
'/Volumes/MyUSB/.git-drive/companion.json': JSON.stringify({
|
|
70
|
+
version: '0.1.6',
|
|
71
|
+
installedAt: '2026-02-26T00:00:00Z',
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const result = getCompanionInfo('/Volumes/MyUSB');
|
|
76
|
+
|
|
77
|
+
expect(result.installed).toBe(true);
|
|
78
|
+
expect(result.version).toBe('0.1.6');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return installed: true even without companion.json', () => {
|
|
82
|
+
vol.fromJSON({
|
|
83
|
+
'/Volumes/MyUSB/.git-drive/git-drive.git/HEAD': '',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const result = getCompanionInfo('/Volumes/MyUSB');
|
|
87
|
+
|
|
88
|
+
expect(result.installed).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should handle malformed companion.json gracefully', () => {
|
|
92
|
+
vol.fromJSON({
|
|
93
|
+
'/Volumes/MyUSB/.git-drive/git-drive.git/HEAD': '',
|
|
94
|
+
'/Volumes/MyUSB/.git-drive/companion.json': 'invalid json{',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const result = getCompanionInfo('/Volumes/MyUSB');
|
|
98
|
+
|
|
99
|
+
expect(result.installed).toBe(true);
|
|
100
|
+
expect(result.version).toBeUndefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('isPortAvailable', () => {
|
|
105
|
+
it('should return false when port is in use', async () => {
|
|
106
|
+
mockFetch.mockResolvedValueOnce({ ok: true });
|
|
107
|
+
|
|
108
|
+
const result = await isPortAvailable(4484);
|
|
109
|
+
|
|
110
|
+
expect(result).toBe(false);
|
|
111
|
+
expect(mockFetch).toHaveBeenCalledWith(
|
|
112
|
+
'http://localhost:4484/api/health',
|
|
113
|
+
expect.objectContaining({ method: 'HEAD' })
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return true when port is available', async () => {
|
|
118
|
+
mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
|
|
119
|
+
|
|
120
|
+
const result = await isPortAvailable(4484);
|
|
121
|
+
|
|
122
|
+
expect(result).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('findAvailablePort', () => {
|
|
127
|
+
it('should return the start port if available', async () => {
|
|
128
|
+
mockFetch.mockRejectedValue(new Error('Connection refused'));
|
|
129
|
+
|
|
130
|
+
const result = await findAvailablePort(4484);
|
|
131
|
+
|
|
132
|
+
expect(result).toBe(4484);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should find next available port', async () => {
|
|
136
|
+
// First port is in use
|
|
137
|
+
mockFetch.mockResolvedValueOnce({ ok: true });
|
|
138
|
+
// Second port is available
|
|
139
|
+
mockFetch.mockRejectedValueOnce(new Error('Connection refused'));
|
|
140
|
+
|
|
141
|
+
const result = await findAvailablePort(4484);
|
|
142
|
+
|
|
143
|
+
expect(result).toBe(4485);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should throw after max attempts', async () => {
|
|
147
|
+
mockFetch.mockResolvedValue({ ok: true });
|
|
148
|
+
|
|
149
|
+
await expect(findAvailablePort(4484)).rejects.toThrow('Could not find an available port');
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { init } from '../../commands/init.js';
|
|
2
|
+
import { GitDriveError } from '../../errors.js';
|
|
3
|
+
import { vol } from 'memfs';
|
|
4
|
+
|
|
5
|
+
// Mock fs and path modules
|
|
6
|
+
jest.mock('fs', () => {
|
|
7
|
+
const { fs } = require('memfs');
|
|
8
|
+
return fs;
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Mock prompts
|
|
12
|
+
jest.mock('prompts', () => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: jest.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
// Mock config
|
|
18
|
+
jest.mock('../../config.js', () => ({
|
|
19
|
+
saveConfig: jest.fn(),
|
|
20
|
+
getDriveStorePath: jest.fn((drivePath: string) => `${drivePath}/.git-drive`),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Mock git
|
|
24
|
+
jest.mock('../../git.js', () => ({
|
|
25
|
+
listDrives: jest.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Mock child_process for companion installation
|
|
29
|
+
jest.mock('child_process', () => ({
|
|
30
|
+
execSync: jest.fn(),
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
import prompts from 'prompts';
|
|
34
|
+
import { listDrives } from '../../git.js';
|
|
35
|
+
import { saveConfig, getDriveStorePath } from '../../config.js';
|
|
36
|
+
|
|
37
|
+
const mockPrompts = prompts as unknown as jest.Mock;
|
|
38
|
+
const mockListDrives = listDrives as jest.Mock;
|
|
39
|
+
const mockSaveConfig = saveConfig as jest.Mock;
|
|
40
|
+
const mockGetDriveStorePath = getDriveStorePath as jest.Mock;
|
|
41
|
+
|
|
42
|
+
describe('init command', () => {
|
|
43
|
+
let consoleSpy: jest.SpyInstance;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
jest.clearAllMocks();
|
|
47
|
+
vol.reset();
|
|
48
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
49
|
+
mockGetDriveStorePath.mockImplementation((drivePath: string) => `${drivePath}/.git-drive`);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
consoleSpy.mockRestore();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('with path argument', () => {
|
|
57
|
+
it('should initialize git-drive on specified path', async () => {
|
|
58
|
+
const drivePath = '/Volumes/TestDrive';
|
|
59
|
+
vol.fromJSON({
|
|
60
|
+
[drivePath]: null, // null creates a directory in memfs
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await init([drivePath]);
|
|
64
|
+
|
|
65
|
+
expect(mockSaveConfig).toHaveBeenCalledWith({ drivePath: expect.stringContaining('TestDrive') });
|
|
66
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Git Drive initialized'));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should throw error if path does not exist', async () => {
|
|
70
|
+
await expect(init(['/Volumes/NonExistent'])).rejects.toThrow(GitDriveError);
|
|
71
|
+
await expect(init(['/Volumes/NonExistent'])).rejects.toThrow('Path not found');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should throw error if path is not a directory', async () => {
|
|
75
|
+
vol.fromJSON({
|
|
76
|
+
'/Volumes/SomeFile': 'file content',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
await expect(init(['/Volumes/SomeFile'])).rejects.toThrow('Path is not a directory');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should resolve relative paths', async () => {
|
|
83
|
+
// Use an absolute path instead since memfs doesn't interact with process.cwd properly
|
|
84
|
+
vol.fromJSON({
|
|
85
|
+
'/Volumes/RelativeDrive': null, // null creates a directory in memfs
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
await init(['/Volumes/RelativeDrive']);
|
|
89
|
+
|
|
90
|
+
expect(mockSaveConfig).toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('without path argument (interactive)', () => {
|
|
95
|
+
it('should throw error when no drives found', async () => {
|
|
96
|
+
mockListDrives.mockResolvedValue([]);
|
|
97
|
+
|
|
98
|
+
await expect(init([])).rejects.toThrow('No external drives found');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('should prompt user to select a drive', async () => {
|
|
102
|
+
mockListDrives.mockResolvedValue([
|
|
103
|
+
{ mounted: '/Volumes/Drive1', filesystem: 'Drive1', blocks: 1000000, available: 500000 },
|
|
104
|
+
{ mounted: '/Volumes/Drive2', filesystem: 'Drive2', blocks: 2000000, available: 1000000 },
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
mockPrompts.mockResolvedValue({
|
|
108
|
+
selectedDrive: '/Volumes/Drive1',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
vol.fromJSON({
|
|
112
|
+
'/Volumes/Drive1': null, // null creates a directory in memfs
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await init([]);
|
|
116
|
+
|
|
117
|
+
expect(mockPrompts).toHaveBeenCalledWith(
|
|
118
|
+
expect.objectContaining({
|
|
119
|
+
type: 'select',
|
|
120
|
+
name: 'selectedDrive',
|
|
121
|
+
})
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should handle user cancellation', async () => {
|
|
126
|
+
mockListDrives.mockResolvedValue([
|
|
127
|
+
{ mounted: '/Volumes/Drive1', filesystem: 'Drive1', blocks: 1000000, available: 500000 },
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
mockPrompts.mockResolvedValue({
|
|
131
|
+
selectedDrive: undefined,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await init([]);
|
|
135
|
+
|
|
136
|
+
expect(consoleSpy).toHaveBeenCalledWith('Operation cancelled.');
|
|
137
|
+
expect(mockSaveConfig).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('store directory creation', () => {
|
|
142
|
+
it('should create .git-drive directory if it does not exist', async () => {
|
|
143
|
+
const drivePath = '/Volumes/TestDrive';
|
|
144
|
+
vol.fromJSON({
|
|
145
|
+
[drivePath]: null, // null creates a directory in memfs
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await init([drivePath]);
|
|
149
|
+
|
|
150
|
+
// The store path should have been requested
|
|
151
|
+
expect(mockGetDriveStorePath).toHaveBeenCalled();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { vol } from 'memfs';
|
|
2
|
+
|
|
3
|
+
// Mock fs
|
|
4
|
+
jest.mock('fs', () => {
|
|
5
|
+
const { fs } = require('memfs');
|
|
6
|
+
return fs;
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
// Mock node-disk-info
|
|
10
|
+
jest.mock('node-disk-info', () => ({
|
|
11
|
+
getDiskInfo: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// Mock server
|
|
15
|
+
jest.mock('../../server.js', () => ({
|
|
16
|
+
ensureServerRunning: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// Mock os
|
|
20
|
+
jest.mock('os', () => ({
|
|
21
|
+
homedir: () => '/home/testuser',
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
import { getDiskInfo } from 'node-disk-info';
|
|
25
|
+
import { list } from '../../commands/list.js';
|
|
26
|
+
|
|
27
|
+
const mockGetDiskInfo = getDiskInfo as jest.Mock;
|
|
28
|
+
|
|
29
|
+
describe('list command', () => {
|
|
30
|
+
let consoleSpy: jest.SpyInstance;
|
|
31
|
+
const originalPlatform = process.platform;
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
vol.reset();
|
|
36
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
37
|
+
Object.defineProperty(process, 'platform', { value: 'darwin', writable: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
consoleSpy.mockRestore();
|
|
42
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform, writable: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should display no drives message when no external drives found', async () => {
|
|
46
|
+
mockGetDiskInfo.mockResolvedValue([]);
|
|
47
|
+
|
|
48
|
+
vol.fromJSON({
|
|
49
|
+
'/home/testuser/.config/git-drive/links.json': '{}',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
await list([]);
|
|
53
|
+
|
|
54
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('No external drives detected'));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should list connected drives with git-drive status', async () => {
|
|
58
|
+
mockGetDiskInfo.mockResolvedValue([
|
|
59
|
+
{ mounted: '/Volumes/MyUSB', filesystem: 'MyUSB', blocks: 32000000, available: 16000000 },
|
|
60
|
+
{ mounted: '/Volumes/External', filesystem: 'External', blocks: 1000000000, available: 500000000 },
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
vol.fromJSON({
|
|
64
|
+
'/home/testuser/.config/git-drive/links.json': '{}',
|
|
65
|
+
'/Volumes/MyUSB/.git-drive/my-project.git/HEAD': '',
|
|
66
|
+
'/Volumes/External': '',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await list([]);
|
|
70
|
+
|
|
71
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('/Volumes/MyUSB'));
|
|
72
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('/Volumes/External'));
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should count repositories on initialized drives', async () => {
|
|
76
|
+
mockGetDiskInfo.mockResolvedValue([
|
|
77
|
+
{ mounted: '/Volumes/MyUSB', filesystem: 'MyUSB', blocks: 32000000, available: 16000000 },
|
|
78
|
+
]);
|
|
79
|
+
|
|
80
|
+
vol.fromJSON({
|
|
81
|
+
'/home/testuser/.config/git-drive/links.json': '{}',
|
|
82
|
+
'/Volumes/MyUSB/.git-drive/project1.git/HEAD': '',
|
|
83
|
+
'/Volumes/MyUSB/.git-drive/project2.git/HEAD': '',
|
|
84
|
+
'/Volumes/MyUSB/.git-drive/project3.git/HEAD': '',
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await list([]);
|
|
88
|
+
|
|
89
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Repositories: 3'));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should display drive size in GB', async () => {
|
|
93
|
+
mockGetDiskInfo.mockResolvedValue([
|
|
94
|
+
{ mounted: '/Volumes/MyUSB', filesystem: 'MyUSB', blocks: 97656250, available: 50000000 },
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
vol.fromJSON({
|
|
98
|
+
'/home/testuser/.config/git-drive/links.json': '{}',
|
|
99
|
+
'/Volumes/MyUSB': '',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
await list([]);
|
|
103
|
+
|
|
104
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('GB'));
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('should handle errors when detecting drives', async () => {
|
|
108
|
+
mockGetDiskInfo.mockRejectedValue(new Error('Failed to detect drives'));
|
|
109
|
+
|
|
110
|
+
vol.fromJSON({
|
|
111
|
+
'/home/testuser/.config/git-drive/links.json': '{}',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
115
|
+
|
|
116
|
+
await list([]);
|
|
117
|
+
|
|
118
|
+
expect(errorSpy).toHaveBeenCalledWith('Error detecting drives:', expect.any(Error));
|
|
119
|
+
|
|
120
|
+
errorSpy.mockRestore();
|
|
121
|
+
});
|
|
122
|
+
});
|