diffwatch 1.1.2 → 2.0.0

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/src/utils/git.ts DELETED
@@ -1,185 +0,0 @@
1
- import { simpleGit, SimpleGit, StatusResult } from 'simple-git';
2
- import fs from 'fs/promises';
3
-
4
- export interface FileStatus {
5
- path: string;
6
- status: 'modified' | 'added' | 'deleted' | 'unstaged' | 'unknown' | 'unchanged';
7
- mtime?: Date;
8
- }
9
-
10
- export class GitHandler {
11
- private git: SimpleGit;
12
-
13
- constructor(workingDir: string = process.cwd()) {
14
- this.git = simpleGit(workingDir);
15
- }
16
-
17
- async isRepo(): Promise<boolean> {
18
- try {
19
- return await this.git.checkIsRepo();
20
- } catch {
21
- return false;
22
- }
23
- }
24
-
25
- async getBranch(): Promise<string> {
26
- try {
27
- const status = await this.git.status();
28
- return status.current || 'HEAD';
29
- } catch {
30
- return 'HEAD';
31
- }
32
- }
33
-
34
- async getStatus(): Promise<FileStatus[]> {
35
- const status: StatusResult = await this.git.status();
36
- const uniqueFiles = new Map<string, FileStatus>();
37
-
38
- // Process each status category and add to map (which prevents duplicates)
39
- status.modified.forEach(path => {
40
- uniqueFiles.set(path, { path, status: 'modified' });
41
- });
42
-
43
- status.deleted.forEach(path => {
44
- // Only add if not already in the map
45
- if (!uniqueFiles.has(path)) {
46
- uniqueFiles.set(path, { path, status: 'deleted' });
47
- }
48
- });
49
-
50
- status.created.forEach(path => {
51
- // Only add if not already in the map
52
- if (!uniqueFiles.has(path)) {
53
- uniqueFiles.set(path, { path, status: 'added' });
54
- }
55
- });
56
-
57
- status.not_added.forEach(path => {
58
- // Only add if not already in the map
59
- if (!uniqueFiles.has(path)) {
60
- uniqueFiles.set(path, { path, status: 'unstaged' });
61
- }
62
- });
63
-
64
- status.renamed.forEach(r => {
65
- // Only add if not already in the map
66
- if (!uniqueFiles.has(r.to)) {
67
- uniqueFiles.set(r.to, { path: r.to, status: 'added' });
68
- }
69
- });
70
-
71
- // Add last modified time for sorting
72
- const fileArray = Array.from(uniqueFiles.values());
73
- await Promise.all(fileArray.map(async (f) => {
74
- try {
75
- const stat = await fs.stat(f.path);
76
- f.mtime = stat.mtime;
77
- } catch (error) {
78
- // For deleted or inaccessible files, use epoch time
79
- f.mtime = new Date(0);
80
- }
81
- }));
82
-
83
- // Sort by last modified descending, then by filename
84
- return fileArray.sort((a, b) => {
85
- const mtimeA = a.mtime || new Date(0);
86
- const mtimeB = b.mtime || new Date(0);
87
- const timeDiff = mtimeB.getTime() - mtimeA.getTime();
88
- if (timeDiff !== 0) {
89
- return timeDiff;
90
- }
91
- return a.path.localeCompare(b.path);
92
- });
93
- }
94
-
95
- async searchFiles(term: string): Promise<FileStatus[]> {
96
- if (!term || !term.trim()) return [];
97
-
98
- try {
99
- // Use git grep to find files containing the term
100
- // -i: ignore case
101
- // -l: list filenames only
102
- // -F: interpret pattern as a fixed string
103
- // --untracked: include untracked files
104
- const result = await this.git.raw(['grep', '-i', '-l', '-F', '--untracked', term.trim()]);
105
- const matchedPaths = [...new Set(result.split('\n').map(p => p.trim()).filter(p => p !== ''))];
106
-
107
- if (matchedPaths.length === 0) return [];
108
-
109
- // Get current status to identify which matched files are changed
110
- const changedFiles = await this.getStatus();
111
- const changedMap = new Map<string, FileStatus>();
112
- changedFiles.forEach(f => changedMap.set(f.path, f));
113
-
114
- const finalResults: FileStatus[] = [];
115
-
116
- for (const path of matchedPaths) {
117
- if (changedMap.has(path)) {
118
- finalResults.push(changedMap.get(path)!);
119
- } else {
120
- // It's an unchanged file
121
- let mtime = new Date(0);
122
- try {
123
- const stat = await fs.stat(path);
124
- mtime = stat.mtime;
125
- } catch {}
126
-
127
- finalResults.push({
128
- path,
129
- status: 'unchanged',
130
- mtime
131
- });
132
- }
133
- }
134
-
135
- // Sort results by mtime descending
136
- return finalResults.sort((a, b) => {
137
- const mtimeA = a.mtime || new Date(0);
138
- const mtimeB = b.mtime || new Date(0);
139
- const timeDiff = mtimeB.getTime() - mtimeA.getTime();
140
- if (timeDiff !== 0) return timeDiff;
141
- return a.path.localeCompare(b.path);
142
- });
143
- } catch (error: any) {
144
- // git grep returns exit code 1 if no matches are found, which simple-git might throw as an error
145
- if (error.message && (error.message.includes('exit code 1') || error.exitCode === 1)) {
146
- return [];
147
- }
148
- throw error;
149
- }
150
- }
151
-
152
- async getDiff(filePath: string): Promise<string> {
153
- try {
154
- const isUntracked = (await this.git.status()).not_added.includes(filePath);
155
-
156
- if (isUntracked) {
157
- return await this.git.raw(['diff', '--no-index', '--', '/dev/null', filePath]).catch(() => {
158
- return `New file: ${filePath}`;
159
- });
160
- }
161
-
162
- const diff = await this.git.diff(['HEAD', '--', filePath]);
163
- return diff || '';
164
- } catch (error) {
165
- return `Error getting diff: ${error}`;
166
- }
167
- }
168
-
169
- async getFileContent(filePath: string): Promise<string> {
170
- try {
171
- return await fs.readFile(filePath, 'utf8');
172
- } catch (error) {
173
- return `Error reading file: ${error}`;
174
- }
175
- }
176
-
177
- async revertFile(filePath: string): Promise<void> {
178
- try {
179
- // Restore the specific file to HEAD version
180
- await this.git.raw(['restore', filePath]);
181
- } catch (error) {
182
- throw new Error(`Could not revert file: ${error}`);
183
- }
184
- }
185
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=git.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"git.test.d.ts","sourceRoot":"","sources":["git.test.ts"],"names":[],"mappings":""}
@@ -1 +0,0 @@
1
- {"version":3,"file":"git.test.js","sourceRoot":"","sources":["git.test.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,0CAA8C;AAC9C,2CAAuC;AAEvC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAExB,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,IAAI,UAAsB,CAAC;IAC3B,IAAI,OAAY,CAAC;IAEjB,UAAU,CAAC,GAAG,EAAE;QACd,OAAO,GAAG;YACR,WAAW,EAAE,IAAI,CAAC,EAAE,EAAE;YACtB,MAAM,EAAE,IAAI,CAAC,EAAE,EAAE;YACjB,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE;YACf,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE;SAChB,CAAC;QACD,sBAAuB,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;QAClD,UAAU,GAAG,IAAI,gBAAU,EAAE,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAS,EAAE;QACzD,OAAO,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAS,EAAE;QAC9D,OAAO,CAAC,WAAW,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC,CAAC;QAC/D,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,qCAAqC,EAAE,GAAS,EAAE;QACnD,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC;YAC/B,QAAQ,EAAE,CAAC,UAAU,CAAC;YACtB,OAAO,EAAE,CAAC,UAAU,CAAC;YACrB,OAAO,EAAE,CAAC,UAAU,CAAC;YACrB,SAAS,EAAE,CAAC,UAAU,CAAC;YACvB,OAAO,EAAE,EAAE;SACZ,CAAC,CAAC;QAEH,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,SAAS,EAAE,CAAC;QAC5C,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;QACxE,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QACvE,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;QACrE,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IAC1E,CAAC,CAAA,CAAC,CAAC;IAEH,EAAE,CAAC,8BAA8B,EAAE,GAAS,EAAE;QAC5C,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,8BAA8B,CAAC,CAAC;QAC/D,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;QAClD,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAC;IACpD,CAAC,CAAA,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/tests/git.test.ts DELETED
@@ -1,148 +0,0 @@
1
- jest.mock('simple-git', () => ({
2
- simpleGit: jest.fn(),
3
- }));
4
- jest.mock('fs/promises', () => ({
5
- stat: jest.fn(),
6
- readFile: jest.fn(),
7
- }));
8
-
9
- import { simpleGit } from 'simple-git';
10
- import fs from 'fs/promises';
11
- import { GitHandler, FileStatus } from '../src/utils/git';
12
-
13
- describe('GitHandler', () => {
14
- let gitHandler: GitHandler;
15
- let mockGit: any;
16
-
17
- beforeEach(() => {
18
- mockGit = {
19
- checkIsRepo: jest.fn(),
20
- status: jest.fn(),
21
- diff: jest.fn(),
22
- show: jest.fn(),
23
- };
24
- (simpleGit as jest.Mock).mockReturnValue(mockGit);
25
- (fs.stat as jest.Mock).mockResolvedValue({ mtime: new Date() });
26
- (fs.readFile as jest.Mock).mockResolvedValue('');
27
- gitHandler = new GitHandler();
28
- });
29
-
30
- it('should return true if directory is a repo', async () => {
31
- mockGit.checkIsRepo.mockResolvedValue(true);
32
- const result = await gitHandler.isRepo();
33
- expect(result).toBe(true);
34
- });
35
-
36
- it('should return false if directory is not a repo', async () => {
37
- mockGit.checkIsRepo.mockRejectedValue(new Error('not a repo'));
38
- const result = await gitHandler.isRepo();
39
- expect(result).toBe(false);
40
- });
41
-
42
- it('should return file status correctly', async () => {
43
- mockGit.status.mockResolvedValue({
44
- modified: ['file1.ts'],
45
- deleted: ['file2.ts'],
46
- created: ['file3.ts'],
47
- not_added: ['file4.ts'],
48
- renamed: [],
49
- });
50
-
51
- const status = await gitHandler.getStatus();
52
- expect(status).toEqual(expect.arrayContaining([
53
- expect.objectContaining({ path: 'file1.ts', status: 'modified' }),
54
- expect.objectContaining({ path: 'file2.ts', status: 'deleted' }),
55
- expect.objectContaining({ path: 'file3.ts', status: 'added' }),
56
- expect.objectContaining({ path: 'file4.ts', status: 'unstaged' }),
57
- ]));
58
- });
59
-
60
- it('should return diff correctly', async () => {
61
- mockGit.status.mockResolvedValue({ not_added: [] });
62
- mockGit.diff.mockResolvedValue('+ added line\n- removed line');
63
- const diff = await gitHandler.getDiff('file1.ts');
64
- expect(diff).toBe('+ added line\n- removed line');
65
- });
66
-
67
- it('should return empty string if no diff', async () => {
68
- mockGit.status.mockResolvedValue({ not_added: [] });
69
- mockGit.diff.mockResolvedValue('');
70
- const diff = await gitHandler.getDiff('file1.ts');
71
- expect(diff).toBe('');
72
- });
73
-
74
- it('should sort files by last modified descending then filename', async () => {
75
- // Mock fs.stat to return different mtimes
76
- const mockStat = jest.fn();
77
- (fs.stat as jest.Mock) = mockStat;
78
- mockStat.mockImplementation((path: string) => {
79
- const mtimes: Record<string, Date> = {
80
- 'z-file.ts': new Date('2024-01-03'),
81
- 'a-file.ts': new Date('2024-01-01'),
82
- 'z-deleted.ts': new Date('2024-01-02'),
83
- 'z-added.ts': new Date('2024-01-04'),
84
- 'a-added.ts': new Date('2024-01-05'),
85
- 'z-unstaged.ts': new Date('2024-01-06'),
86
- 'a-unstaged.ts': new Date('2024-01-07'),
87
- };
88
- return Promise.resolve({ mtime: mtimes[path] || new Date(0) });
89
- });
90
-
91
- mockGit.status.mockResolvedValue({
92
- modified: ['z-file.ts', 'a-file.ts'],
93
- deleted: ['z-deleted.ts'],
94
- created: ['z-added.ts', 'a-added.ts'],
95
- not_added: ['z-unstaged.ts', 'a-unstaged.ts'],
96
- renamed: [],
97
- });
98
-
99
- const status = await gitHandler.getStatus();
100
- expect(status.map(f => f.path)).toEqual([
101
- 'a-unstaged.ts', // newest
102
- 'z-unstaged.ts',
103
- 'a-added.ts',
104
- 'z-added.ts',
105
- 'z-file.ts',
106
- 'z-deleted.ts',
107
- 'a-file.ts', // oldest
108
- ]);
109
- });
110
-
111
- it('should search files correctly', async () => {
112
- mockGit.raw = jest.fn().mockResolvedValue('match.ts\nunchanged.ts');
113
- mockGit.status.mockResolvedValue({
114
- modified: ['match.ts'],
115
- deleted: [],
116
- created: [],
117
- not_added: [],
118
- renamed: [],
119
- });
120
-
121
- const results = await gitHandler.searchFiles('match');
122
-
123
- expect(results).toHaveLength(2);
124
- expect(results).toEqual(expect.arrayContaining([
125
- expect.objectContaining({ path: 'match.ts', status: 'modified' }),
126
- expect.objectContaining({ path: 'unchanged.ts', status: 'unchanged' }),
127
- ]));
128
- expect(mockGit.raw).toHaveBeenCalledWith(['grep', '-i', '-l', '-F', '--untracked', 'match']);
129
- });
130
-
131
- it('should deduplicate search results', async () => {
132
- // Return duplicate paths
133
- mockGit.raw = jest.fn().mockResolvedValue('match.ts\nmatch.ts\nother.ts\nother.ts');
134
- mockGit.status.mockResolvedValue({
135
- modified: [],
136
- deleted: [],
137
- created: [],
138
- not_added: [],
139
- renamed: [],
140
- });
141
-
142
- const results = await gitHandler.searchFiles('term');
143
-
144
- expect(results).toHaveLength(2);
145
- const paths = results.map(r => r.path).sort();
146
- expect(paths).toEqual(['match.ts', 'other.ts']);
147
- });
148
- });
package/tsconfig.json DELETED
@@ -1,16 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ES6",
4
- "module": "commonjs",
5
- "rootDir": "src",
6
- "outDir": "dist",
7
- "esModuleInterop": true,
8
- "forceConsistentCasingInFileNames": true,
9
- "strict": true,
10
- "skipLibCheck": true,
11
- "sourceMap": true,
12
- "declaration": true
13
- },
14
- "include": ["src/**/*"],
15
- "exclude": ["node_modules", "dist", "sample-tui", "tests"]
16
- }