shadow-git 0.1.0 → 1.0.2
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/.claude/settings.local.json +5 -1
- package/.temp/.sgit/index +0 -0
- package/.temp/.sgit/info/exclude +16 -7
- package/.temp/.sgit/objects/03/7f4e37a02573e40ab78b35dd4b442cc6110bae +0 -0
- package/.temp/.sgit/objects/2f/f0fd85dec20039eef6ff63ead905778a88843f +0 -0
- package/.temp/.sgit/objects/c3/2432f724b666f647fe21a1ce16535cd7693f74 +0 -0
- package/.temp/.sgit/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391 +0 -0
- package/.temp/.sgit/objects/ee/9dd32cca3b134623d184eb4de5805704c493e3 +0 -0
- package/.temp/.sgit.json +6 -2
- package/.temp/README.md +0 -3
- package/.temp/rec1/rec2/test +0 -0
- package/.temp/temp/shouldnt-track +0 -0
- package/.temp/test/1 +0 -0
- package/CLAUDE.md +10 -2
- package/README.md +16 -29
- package/dist/cli.js +810 -119
- package/package.json +1 -1
- package/src/cli.ts +50 -19
- package/src/commands/debug/gitignores.ts +28 -0
- package/src/commands/debug/index.ts +32 -0
- package/src/commands/track/add.ts +11 -0
- package/src/commands/track/clone.ts +213 -0
- package/src/commands/track/completion.ts +50 -15
- package/src/commands/track/destroy.ts +97 -19
- package/src/commands/track/ignore.ts +55 -0
- package/src/commands/track/index.ts +23 -1
- package/src/commands/track/init.ts +5 -2
- package/src/commands/track/sync.ts +1 -1
- package/src/commands/track/unignore.ts +50 -0
- package/src/commands/track/verify.ts +93 -20
- package/src/core/errors.ts +23 -2
- package/src/core/exclude.ts +40 -43
- package/src/core/gitignore.ts +242 -0
- package/src/core/hooks.ts +19 -0
- package/src/core/manifest.ts +33 -1
- package/src/core/paths.ts +21 -3
- package/src/core/repository.ts +17 -2
- package/tests/commands/track/add.test.ts +31 -1
- package/tests/commands/track/clone.test.ts +315 -0
- package/tests/commands/track/completion.test.ts +1 -1
- package/tests/commands/track/destroy.test.ts +68 -2
- package/tests/commands/track/ignore.test.ts +109 -0
- package/tests/commands/track/init.test.ts +27 -4
- package/tests/commands/track/remove.test.ts +1 -1
- package/tests/commands/track/sync.test.ts +3 -3
- package/tests/commands/track/unignore.test.ts +72 -0
- package/tests/commands/track/verify.test.ts +132 -1
- package/tests/core/exclude.test.ts +107 -19
- package/tests/core/gitignore.test.ts +561 -0
- package/tests/core/hooks.test.ts +34 -0
- package/tests/core/paths.test.ts +24 -0
- package/tests/core/repository.test.ts +39 -5
- package/tests/helpers/guard-child.mjs +125 -0
- package/tests/helpers.ts +37 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -2,31 +2,57 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { findRoot, validateShadowRepo } from './core/repository.js';
|
|
3
3
|
import { forwardToGit } from './commands/forward.js';
|
|
4
4
|
import { registerTrackCommands } from './commands/track/index.js';
|
|
5
|
+
import { registerDebugCommands } from './commands/debug/index.js';
|
|
5
6
|
import { SgitError } from './core/errors.js';
|
|
7
|
+
import { readManifest } from './core/manifest.js';
|
|
8
|
+
import { setupGitignoreGuard } from './core/gitignore.js';
|
|
6
9
|
import * as logger from './utils/logger.js';
|
|
7
10
|
|
|
11
|
+
const COMMANDER_COMMANDS = new Set(['track', 'debug']);
|
|
12
|
+
|
|
8
13
|
async function run(): Promise<void> {
|
|
9
14
|
const args = process.argv.slice(2);
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
args[0] === 'track' ||
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
// Skip guard for commands that manage gitignores themselves
|
|
17
|
+
const skipGuard =
|
|
18
|
+
(args[0] === 'track' && (args[1] === 'init' || args[1] === 'clone')) || args[0] === 'debug';
|
|
19
|
+
|
|
20
|
+
// Set up gitignore guard
|
|
21
|
+
let guardCleanup: (() => void) | undefined;
|
|
22
|
+
if (!skipGuard) {
|
|
23
|
+
try {
|
|
24
|
+
const root = await findRoot();
|
|
25
|
+
const manifest = await readManifest(root);
|
|
26
|
+
guardCleanup = setupGitignoreGuard(root, manifest.paths);
|
|
27
|
+
} catch {
|
|
28
|
+
// No git repo, no .sgit, or no manifest — proceed without guard
|
|
29
|
+
}
|
|
30
|
+
}
|
|
19
31
|
|
|
20
|
-
|
|
32
|
+
try {
|
|
33
|
+
if (
|
|
34
|
+
args.length === 0 ||
|
|
35
|
+
COMMANDER_COMMANDS.has(args[0]) ||
|
|
36
|
+
args[0].startsWith('-')
|
|
37
|
+
) {
|
|
38
|
+
const program = new Command('sgit')
|
|
39
|
+
.description('Shadow Git — a secondary git repository for your project')
|
|
40
|
+
.version('0.1.0');
|
|
21
41
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
42
|
+
registerTrackCommands(program);
|
|
43
|
+
registerDebugCommands(program);
|
|
44
|
+
|
|
45
|
+
program.addHelpText(
|
|
46
|
+
'after',
|
|
47
|
+
`
|
|
25
48
|
Track commands:
|
|
26
49
|
sgit track init Initialize shadow repository
|
|
50
|
+
sgit track clone Clone shadow repository from remote URL
|
|
27
51
|
sgit track add Start shadow-tracking paths (--adopt to move from main git)
|
|
28
52
|
sgit track remove Stop shadow-tracking paths
|
|
29
53
|
sgit track list List all shadow-tracked paths
|
|
54
|
+
sgit track ignore Ignore paths within tracked directories
|
|
55
|
+
sgit track unignore Stop ignoring paths
|
|
30
56
|
sgit track sync Regenerate exclude files
|
|
31
57
|
sgit track verify Validate staged files
|
|
32
58
|
sgit track destroy Remove shadow repository
|
|
@@ -34,14 +60,19 @@ Track commands:
|
|
|
34
60
|
|
|
35
61
|
All other commands are forwarded to git in the shadow repository:
|
|
36
62
|
sgit status, sgit commit, sgit log, sgit diff, ...`,
|
|
37
|
-
|
|
63
|
+
);
|
|
38
64
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
65
|
+
await program.parseAsync(process.argv);
|
|
66
|
+
} else {
|
|
67
|
+
const root = await findRoot();
|
|
68
|
+
await validateShadowRepo(root);
|
|
69
|
+
const exitCode = forwardToGit(root, args);
|
|
70
|
+
guardCleanup?.();
|
|
71
|
+
guardCleanup = undefined;
|
|
72
|
+
process.exit(exitCode);
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
guardCleanup?.();
|
|
45
76
|
}
|
|
46
77
|
}
|
|
47
78
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { findRoot, validateShadowRepo } from '../../core/repository.js';
|
|
2
|
+
import { readManifest } from '../../core/manifest.js';
|
|
3
|
+
import { setupGitignoreGuard } from '../../core/gitignore.js';
|
|
4
|
+
import * as logger from '../../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
export async function debugGitignores(): Promise<void> {
|
|
7
|
+
const root = await findRoot();
|
|
8
|
+
await validateShadowRepo(root);
|
|
9
|
+
const manifest = await readManifest(root);
|
|
10
|
+
|
|
11
|
+
const cleanup = setupGitignoreGuard(root, manifest.paths);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
logger.info('gitignores disabled — waiting 10 seconds…');
|
|
15
|
+
for (let i = 10; i > 0; i--) {
|
|
16
|
+
process.stdout.write(`\r ${i}s remaining `);
|
|
17
|
+
await sleep(1000);
|
|
18
|
+
}
|
|
19
|
+
process.stdout.write('\r \r');
|
|
20
|
+
logger.success('re-enabling gitignores');
|
|
21
|
+
} finally {
|
|
22
|
+
cleanup();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sleep(ms: number): Promise<void> {
|
|
27
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { SgitError } from '../../core/errors.js';
|
|
3
|
+
import * as logger from '../../utils/logger.js';
|
|
4
|
+
import { debugGitignores } from './gitignores.js';
|
|
5
|
+
|
|
6
|
+
export function registerDebugCommands(program: Command): void {
|
|
7
|
+
const debug = new Command('debug')
|
|
8
|
+
.description('Debug utilities');
|
|
9
|
+
|
|
10
|
+
debug
|
|
11
|
+
.command('gitignores')
|
|
12
|
+
.description('Disable .gitignore files for 10 seconds, then re-enable')
|
|
13
|
+
.action(wrapAction(debugGitignores));
|
|
14
|
+
|
|
15
|
+
program.addCommand(debug, { hidden: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function wrapAction(
|
|
19
|
+
fn: (...args: any[]) => Promise<void>,
|
|
20
|
+
): (...args: any[]) => Promise<void> {
|
|
21
|
+
return async (...args: any[]) => {
|
|
22
|
+
try {
|
|
23
|
+
await fn(...args);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
if (err instanceof SgitError) {
|
|
26
|
+
logger.error(err.message);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
throw err;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -9,9 +9,12 @@ import { syncExcludes } from './sync.js';
|
|
|
9
9
|
import {
|
|
10
10
|
PathNotFoundError,
|
|
11
11
|
PathTrackedByMainError,
|
|
12
|
+
SgitError,
|
|
12
13
|
} from '../../core/errors.js';
|
|
13
14
|
import * as logger from '../../utils/logger.js';
|
|
14
15
|
|
|
16
|
+
const BLOCKED_BASENAMES = new Set(['.gitignore', '.gitignore-sgit.bat']);
|
|
17
|
+
|
|
15
18
|
export async function trackAdd(paths: string[], opts: { adopt?: boolean } = {}): Promise<void> {
|
|
16
19
|
const root = await findRoot();
|
|
17
20
|
await validateShadowRepo(root);
|
|
@@ -22,6 +25,14 @@ export async function trackAdd(paths: string[], opts: { adopt?: boolean } = {}):
|
|
|
22
25
|
for (const inputPath of paths) {
|
|
23
26
|
const normalized = await normalizePath(inputPath, root);
|
|
24
27
|
|
|
28
|
+
// .gitignore files are managed by sgit and cannot be tracked
|
|
29
|
+
const basename = normalized.split('/').pop() ?? '';
|
|
30
|
+
if (BLOCKED_BASENAMES.has(basename)) {
|
|
31
|
+
throw new SgitError(
|
|
32
|
+
`cannot track ${normalized}: .gitignore files are managed by sgit and cannot be tracked directly`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
// Must exist on disk
|
|
26
37
|
const checkPath = normalized.endsWith('/')
|
|
27
38
|
? normalized.slice(0, -1)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { join, extname } from 'node:path';
|
|
2
|
+
import { access, readFile, writeFile, rename, mkdir, rm } from 'node:fs/promises';
|
|
3
|
+
import { constants } from 'node:fs';
|
|
4
|
+
import { createInterface } from 'node:readline';
|
|
5
|
+
import { randomBytes } from 'node:crypto';
|
|
6
|
+
import {
|
|
7
|
+
findRoot,
|
|
8
|
+
validateNotWorktree,
|
|
9
|
+
validateNotSubmodule,
|
|
10
|
+
validateGitIdentity,
|
|
11
|
+
} from '../../core/repository.js';
|
|
12
|
+
import { execGit } from '../../core/git.js';
|
|
13
|
+
import { installMainHook, installShadowHook, installShadowSyncHooks } from '../../core/hooks.js';
|
|
14
|
+
import { syncExcludes } from './sync.js';
|
|
15
|
+
import { AlreadyInitializedError, SgitError } from '../../core/errors.js';
|
|
16
|
+
import type { Manifest } from '../../core/manifest.js';
|
|
17
|
+
import * as logger from '../../utils/logger.js';
|
|
18
|
+
|
|
19
|
+
export async function trackClone(
|
|
20
|
+
url: string,
|
|
21
|
+
opts: { onConflict?: 1 | 2 | 3 } = {},
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
const root = await findRoot();
|
|
24
|
+
await validateNotWorktree(root);
|
|
25
|
+
await validateNotSubmodule(root);
|
|
26
|
+
await validateGitIdentity(root);
|
|
27
|
+
|
|
28
|
+
const sgitDir = join(root, '.sgit');
|
|
29
|
+
|
|
30
|
+
// Check if already initialized
|
|
31
|
+
try {
|
|
32
|
+
await access(sgitDir, constants.F_OK);
|
|
33
|
+
throw new AlreadyInitializedError();
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (err instanceof AlreadyInitializedError) throw err;
|
|
36
|
+
// Not found — continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Clone bare
|
|
40
|
+
try {
|
|
41
|
+
await execGit(['clone', '--bare', url, sgitDir]);
|
|
42
|
+
} catch {
|
|
43
|
+
// Clean up partial clone
|
|
44
|
+
try { await rm(sgitDir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
45
|
+
throw new SgitError(`failed to clone from ${url}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Configure as non-bare with worktree
|
|
49
|
+
await execGit(['config', 'core.bare', 'false'], { gitDir: sgitDir });
|
|
50
|
+
await execGit(['config', 'core.worktree', root], { gitDir: sgitDir });
|
|
51
|
+
|
|
52
|
+
// Copy user identity from main repo
|
|
53
|
+
try {
|
|
54
|
+
const name = await execGit(['config', 'user.name'], { cwd: root });
|
|
55
|
+
if (name) {
|
|
56
|
+
await execGit(['config', 'user.name', name], { gitDir: sgitDir });
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// Not configured in main repo
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
const email = await execGit(['config', 'user.email'], { cwd: root });
|
|
63
|
+
if (email) {
|
|
64
|
+
await execGit(['config', 'user.email', email], { gitDir: sgitDir });
|
|
65
|
+
}
|
|
66
|
+
} catch {
|
|
67
|
+
// Not configured in main repo
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Read manifest from cloned repo
|
|
71
|
+
let manifest: Manifest;
|
|
72
|
+
try {
|
|
73
|
+
const manifestJson = await execGit(['show', 'HEAD:.sgit.json'], { gitDir: sgitDir });
|
|
74
|
+
manifest = JSON.parse(manifestJson) as Manifest;
|
|
75
|
+
} catch {
|
|
76
|
+
manifest = { paths: [], ignore: [] };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Get list of tracked files
|
|
80
|
+
let trackedFiles: string[] = [];
|
|
81
|
+
try {
|
|
82
|
+
const output = await execGit(['ls-tree', '-r', '--name-only', 'HEAD'], { gitDir: sgitDir });
|
|
83
|
+
if (output.trim()) {
|
|
84
|
+
trackedFiles = output.trim().split('\n');
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// Empty repo — nothing to checkout
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Detect conflicts
|
|
91
|
+
const conflicts: string[] = [];
|
|
92
|
+
for (const file of trackedFiles) {
|
|
93
|
+
try {
|
|
94
|
+
await access(join(root, file), constants.F_OK);
|
|
95
|
+
conflicts.push(file);
|
|
96
|
+
} catch {
|
|
97
|
+
// No conflict
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Resolve conflicts
|
|
102
|
+
let choice: 1 | 2 | 3 = 1;
|
|
103
|
+
if (conflicts.length > 0) {
|
|
104
|
+
if (opts.onConflict) {
|
|
105
|
+
choice = opts.onConflict;
|
|
106
|
+
} else {
|
|
107
|
+
logger.warn(`${conflicts.length} file(s) already exist on disk:`);
|
|
108
|
+
for (const f of conflicts) {
|
|
109
|
+
logger.info(` ${f}`);
|
|
110
|
+
}
|
|
111
|
+
choice = await promptConflictResolution();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (choice === 2) {
|
|
115
|
+
// Save conflicting files before checkout
|
|
116
|
+
const savedContent = new Map<string, Buffer>();
|
|
117
|
+
for (const file of conflicts) {
|
|
118
|
+
savedContent.set(file, await readFile(join(root, file)));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Checkout all files
|
|
122
|
+
if (trackedFiles.length > 0) {
|
|
123
|
+
await execGit(['checkout', 'HEAD', '--', '.'], { gitDir: sgitDir, workTree: root });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Restore originals
|
|
127
|
+
for (const [file, content] of savedContent) {
|
|
128
|
+
await mkdir(join(root, file, '..'), { recursive: true });
|
|
129
|
+
await writeFile(join(root, file), content);
|
|
130
|
+
}
|
|
131
|
+
} else if (choice === 3) {
|
|
132
|
+
// Rename conflicting files
|
|
133
|
+
for (const file of conflicts) {
|
|
134
|
+
const renamed = generateOriginalName(file);
|
|
135
|
+
await rename(join(root, file), join(root, renamed));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Checkout all files
|
|
139
|
+
if (trackedFiles.length > 0) {
|
|
140
|
+
await execGit(['checkout', 'HEAD', '--', '.'], { gitDir: sgitDir, workTree: root });
|
|
141
|
+
}
|
|
142
|
+
} else {
|
|
143
|
+
// Option 1: just checkout (overwrites)
|
|
144
|
+
if (trackedFiles.length > 0) {
|
|
145
|
+
await execGit(['checkout', 'HEAD', '--', '.'], { gitDir: sgitDir, workTree: root });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// No conflicts — checkout directly
|
|
150
|
+
if (trackedFiles.length > 0) {
|
|
151
|
+
await execGit(['checkout', 'HEAD', '--', '.'], { gitDir: sgitDir, workTree: root });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Sync exclude files
|
|
156
|
+
await syncExcludes(root);
|
|
157
|
+
|
|
158
|
+
// Install hooks
|
|
159
|
+
await installMainHook(root);
|
|
160
|
+
await installShadowHook(sgitDir);
|
|
161
|
+
await installShadowSyncHooks(sgitDir);
|
|
162
|
+
|
|
163
|
+
// Success message
|
|
164
|
+
const fileCount = trackedFiles.filter(f => f !== '.sgit.json').length;
|
|
165
|
+
logger.success(`Shadow repository cloned from ${url}`);
|
|
166
|
+
logger.info(` ${fileCount} file(s) checked out`);
|
|
167
|
+
if (conflicts.length > 0) {
|
|
168
|
+
switch (choice) {
|
|
169
|
+
case 1:
|
|
170
|
+
logger.info(` ${conflicts.length} conflicting file(s) overwritten`);
|
|
171
|
+
break;
|
|
172
|
+
case 2:
|
|
173
|
+
logger.info(` ${conflicts.length} local file(s) kept (shadow repo shows them as modified)`);
|
|
174
|
+
break;
|
|
175
|
+
case 3:
|
|
176
|
+
logger.info(` ${conflicts.length} local file(s) renamed with -original~ suffix`);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function promptConflictResolution(): Promise<1 | 2 | 3> {
|
|
183
|
+
const rl = createInterface({
|
|
184
|
+
input: process.stdin,
|
|
185
|
+
output: process.stdout,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return new Promise((resolve) => {
|
|
189
|
+
process.stdout.write('\nHow should conflicts be resolved?\n');
|
|
190
|
+
process.stdout.write(' 1 - Keep sgit (overwrite local files)\n');
|
|
191
|
+
process.stdout.write(' 2 - Keep originals (local files stay, sgit shows them as modified)\n');
|
|
192
|
+
process.stdout.write(' 3 - Rename originals (add -original~ suffix, checkout sgit versions)\n');
|
|
193
|
+
rl.question('\nChoice [1/2/3]: ', (answer) => {
|
|
194
|
+
rl.close();
|
|
195
|
+
const num = parseInt(answer.trim(), 10);
|
|
196
|
+
if (num === 1 || num === 2 || num === 3) {
|
|
197
|
+
resolve(num as 1 | 2 | 3);
|
|
198
|
+
} else {
|
|
199
|
+
resolve(2); // Default to safest option
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function generateOriginalName(filePath: string): string {
|
|
206
|
+
const suffix = randomBytes(2).toString('hex');
|
|
207
|
+
const ext = extname(filePath);
|
|
208
|
+
if (ext) {
|
|
209
|
+
const base = filePath.slice(0, filePath.length - ext.length);
|
|
210
|
+
return `${base}-original~${suffix}${ext}`;
|
|
211
|
+
}
|
|
212
|
+
return `${filePath}-original~${suffix}`;
|
|
213
|
+
}
|
|
@@ -11,7 +11,7 @@ _sgit() {
|
|
|
11
11
|
|
|
12
12
|
if [[ \${words[1]} == "track" ]]; then
|
|
13
13
|
if [[ $cword -eq 2 ]]; then
|
|
14
|
-
COMPREPLY=($(compgen -W "init add remove list sync verify destroy completion" -- "$cur"))
|
|
14
|
+
COMPREPLY=($(compgen -W "init clone add remove list sync verify ignore unignore destroy completion" -- "$cur"))
|
|
15
15
|
return
|
|
16
16
|
fi
|
|
17
17
|
|
|
@@ -24,24 +24,34 @@ _sgit() {
|
|
|
24
24
|
_filedir
|
|
25
25
|
fi
|
|
26
26
|
;;
|
|
27
|
+
clone)
|
|
28
|
+
;;
|
|
27
29
|
remove)
|
|
28
30
|
local paths
|
|
29
31
|
paths=$(sgit track list 2>/dev/null)
|
|
30
32
|
COMPREPLY=($(compgen -W "$paths" -- "$cur"))
|
|
31
33
|
;;
|
|
34
|
+
ignore)
|
|
35
|
+
_filedir
|
|
36
|
+
;;
|
|
37
|
+
unignore)
|
|
38
|
+
local ignored
|
|
39
|
+
ignored=$(python3 -c "import json; d=json.load(open('.sgit.json')); print(' '.join(d.get('ignore',[])))" 2>/dev/null)
|
|
40
|
+
COMPREPLY=($(compgen -W "$ignored" -- "$cur"))
|
|
41
|
+
;;
|
|
32
42
|
verify)
|
|
33
43
|
COMPREPLY=($(compgen -W "--main --shadow" -- "$cur"))
|
|
34
44
|
;;
|
|
35
45
|
destroy)
|
|
36
|
-
COMPREPLY=($(compgen -W "--force" -- "$cur"))
|
|
46
|
+
COMPREPLY=($(compgen -W "--force --keep-files" -- "$cur"))
|
|
37
47
|
;;
|
|
38
48
|
completion)
|
|
39
49
|
if [[ $prev == "--shell" ]]; then
|
|
40
50
|
COMPREPLY=($(compgen -W "bash zsh fish" -- "$cur"))
|
|
41
|
-
elif [[ $
|
|
42
|
-
COMPREPLY=($(compgen -W "--shell" -- "$cur"))
|
|
51
|
+
elif [[ $cword -eq 3 ]]; then
|
|
52
|
+
COMPREPLY=($(compgen -W "install --shell" -- "$cur"))
|
|
43
53
|
else
|
|
44
|
-
COMPREPLY=($(compgen -W "
|
|
54
|
+
COMPREPLY=($(compgen -W "--shell" -- "$cur"))
|
|
45
55
|
fi
|
|
46
56
|
;;
|
|
47
57
|
esac
|
|
@@ -75,11 +85,14 @@ _sgit_track() {
|
|
|
75
85
|
local -a subcmds
|
|
76
86
|
subcmds=(
|
|
77
87
|
'init:Initialize shadow repository'
|
|
88
|
+
'clone:Clone shadow repository from remote URL'
|
|
78
89
|
'add:Start shadow-tracking paths'
|
|
79
90
|
'remove:Stop shadow-tracking paths'
|
|
80
91
|
'list:List all shadow-tracked paths'
|
|
81
92
|
'sync:Regenerate exclude files'
|
|
82
93
|
'verify:Validate staged files'
|
|
94
|
+
'ignore:Ignore paths within tracked directories'
|
|
95
|
+
'unignore:Stop ignoring paths within tracked directories'
|
|
83
96
|
'destroy:Remove shadow repository'
|
|
84
97
|
'completion:Install shell completions'
|
|
85
98
|
)
|
|
@@ -90,6 +103,9 @@ _sgit_track() {
|
|
|
90
103
|
fi
|
|
91
104
|
|
|
92
105
|
case "\${words[2]}" in
|
|
106
|
+
clone)
|
|
107
|
+
_arguments '1:url:'
|
|
108
|
+
;;
|
|
93
109
|
add)
|
|
94
110
|
_arguments '--adopt[Move main-git-tracked files to shadow tracking]' '*:file:_files'
|
|
95
111
|
;;
|
|
@@ -98,11 +114,19 @@ _sgit_track() {
|
|
|
98
114
|
paths=(\${(f)"$(sgit track list 2>/dev/null)"})
|
|
99
115
|
_describe 'tracked path' paths
|
|
100
116
|
;;
|
|
117
|
+
ignore)
|
|
118
|
+
_files
|
|
119
|
+
;;
|
|
120
|
+
unignore)
|
|
121
|
+
local -a ignored
|
|
122
|
+
ignored=(\${(f)"$(python3 -c "import json; d=json.load(open('.sgit.json')); print('\\\\n'.join(d.get('ignore',[])))" 2>/dev/null)"})
|
|
123
|
+
_describe 'ignored path' ignored
|
|
124
|
+
;;
|
|
101
125
|
verify)
|
|
102
126
|
_arguments '--main[Check main repo]' '--shadow[Check shadow repo]'
|
|
103
127
|
;;
|
|
104
128
|
destroy)
|
|
105
|
-
_arguments '--force[Skip confirmation]'
|
|
129
|
+
_arguments '--force[Skip confirmation]' '--keep-files[Keep tracked files on disk]'
|
|
106
130
|
;;
|
|
107
131
|
completion)
|
|
108
132
|
_arguments '--shell[Shell type]:shell:(bash zsh fish)' '1:action:(install)'
|
|
@@ -148,14 +172,18 @@ complete -c sgit -w git
|
|
|
148
172
|
complete -c sgit -n '__fish_use_subcommand' -a track -d 'Manage shadow-tracked paths'
|
|
149
173
|
|
|
150
174
|
# Track subcommands
|
|
151
|
-
|
|
152
|
-
complete -c sgit -n
|
|
153
|
-
complete -c sgit -n
|
|
154
|
-
complete -c sgit -n
|
|
155
|
-
complete -c sgit -n
|
|
156
|
-
complete -c sgit -n
|
|
157
|
-
complete -c sgit -n
|
|
158
|
-
complete -c sgit -n
|
|
175
|
+
set -l __sgit_track_cmds init clone add remove list sync verify ignore unignore destroy completion
|
|
176
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from \$__sgit_track_cmds" -a init -d 'Initialize shadow repository'
|
|
177
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from \$__sgit_track_cmds" -a clone -d 'Clone shadow repository from remote URL'
|
|
178
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from \$__sgit_track_cmds" -a add -d 'Start shadow-tracking paths'
|
|
179
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from \$__sgit_track_cmds" -a remove -d 'Stop shadow-tracking paths'
|
|
180
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from \$__sgit_track_cmds" -a list -d 'List all shadow-tracked paths'
|
|
181
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from \$__sgit_track_cmds" -a sync -d 'Regenerate exclude files'
|
|
182
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from \$__sgit_track_cmds" -a verify -d 'Validate staged files'
|
|
183
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from \$__sgit_track_cmds" -a ignore -d 'Ignore paths within tracked directories'
|
|
184
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from \$__sgit_track_cmds" -a unignore -d 'Stop ignoring paths within tracked directories'
|
|
185
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from \$__sgit_track_cmds" -a destroy -d 'Remove shadow repository'
|
|
186
|
+
complete -c sgit -n "__fish_seen_subcommand_from track; and not __fish_seen_subcommand_from \$__sgit_track_cmds" -a completion -d 'Install shell completions'
|
|
159
187
|
|
|
160
188
|
# Track add options and file completions
|
|
161
189
|
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from add' -l adopt -d 'Move main-git-tracked files'
|
|
@@ -164,15 +192,22 @@ complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcomma
|
|
|
164
192
|
# Track remove — complete from manifest
|
|
165
193
|
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from remove' -a '(sgit track list 2>/dev/null)'
|
|
166
194
|
|
|
195
|
+
# Track ignore — file completions
|
|
196
|
+
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from ignore' -F
|
|
197
|
+
|
|
198
|
+
# Track unignore — complete from .sgit.json ignore list
|
|
199
|
+
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from unignore' -a "(python3 -c \\"import json; d=json.load(open('.sgit.json')); print(' '.join(d.get('ignore',[])))\\\" 2>/dev/null)"
|
|
200
|
+
|
|
167
201
|
# Track verify options
|
|
168
202
|
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from verify' -l main -d 'Check main repo'
|
|
169
203
|
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from verify' -l shadow -d 'Check shadow repo'
|
|
170
204
|
|
|
171
205
|
# Track destroy options
|
|
172
206
|
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from destroy' -l force -d 'Skip confirmation'
|
|
207
|
+
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from destroy' -l keep-files -d 'Keep tracked files on disk'
|
|
173
208
|
|
|
174
209
|
# Track completion options
|
|
175
|
-
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from completion' -a install -d 'Install completions'
|
|
210
|
+
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from completion; and not __fish_seen_subcommand_from install' -a install -d 'Install completions'
|
|
176
211
|
complete -c sgit -n '__fish_seen_subcommand_from track; and __fish_seen_subcommand_from completion' -l shell -d 'Shell type' -ra 'bash zsh fish'
|
|
177
212
|
`;
|
|
178
213
|
}
|