Skip to content

Commit 4b74fa1

Browse files
committed
gitignore
1 parent 39c7ac3 commit 4b74fa1

4 files changed

Lines changed: 112 additions & 60 deletions

File tree

src/commands/compare.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -215,17 +215,15 @@ export async function compareMany(
215215
});
216216
}
217217

218-
// Print fix tips if not in JSON mode and not auto-fixing
219-
if (!opts.json && !opts.fix) {
220-
const ignored = isEnvIgnoredByGit({ cwd: opts.cwd, envFile: '.env' });
221-
const envNotIgnored = ignored === false || ignored === null;
218+
const ignored = isEnvIgnoredByGit({ cwd: opts.cwd, envFile: '.env' });
219+
const envNotIgnored = ignored === false || ignored === null;
220+
222221
printFixTips(
223222
filtered,
224223
envNotIgnored,
225224
opts.json ?? false,
226225
opts.fix ?? false,
227226
);
228-
}
229227

230228
// Apply auto-fix if requested
231229
if (opts.fix) {
@@ -234,6 +232,7 @@ export async function compareMany(
234232
examplePath,
235233
missingKeys: filtered.missing,
236234
duplicateKeys: dupsEnv.map((d) => d.key),
235+
ensureGitignore: envNotIgnored,
237236
});
238237

239238
printAutoFix(changed, result, envName, exampleName, opts.json ?? false);

src/commands/scanUsage.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export async function scanUsage(
127127
: '',
128128
missingKeys: [],
129129
duplicateKeys: dupsEnv.map((d) => d.key),
130+
ensureGitignore: true,
130131
});
131132

132133
if (changed) {
@@ -175,6 +176,7 @@ export async function scanUsage(
175176
examplePath: exampleFilePath,
176177
missingKeys,
177178
duplicateKeys: [],
179+
ensureGitignore: true,
178180
});
179181

180182
if (changed) {
@@ -193,6 +195,7 @@ export async function scanUsage(
193195
examplePath: '',
194196
missingKeys: [],
195197
duplicateKeys: [],
198+
ensureGitignore: true,
196199
});
197200
if (result.gitignoreUpdated) {
198201
fixApplied = true;

src/config/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,4 +234,5 @@ export type Filtered = {
234234
mismatches: Array<{ key: string; expected: string; actual: string }>;
235235
duplicatesEnv: Array<{ key: string; count: number }>;
236236
duplicatesEx: Array<{ key: string; count: number }>;
237+
gitignoreIssue: { reason: 'no-gitignore' | 'not-ignored' } | null;
237238
};

src/core/fixEnv.ts

Lines changed: 104 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,50 +2,74 @@ import fs from 'fs';
22
import path from 'path';
33
import { isEnvIgnoredByGit, isGitRepo, findGitRoot } from '../services/git.js';
44

5-
/**
6-
* Applies fixes to the .env and .env.example files based on the detected issues.
7-
* @param envPath - The path to the .env file.
8-
* @param examplePath - The path to the .env.example file.
9-
* @param missingKeys - The list of missing keys to add.
10-
* @param duplicateKeys - The list of duplicate keys to remove.
11-
* @returns An object indicating whether changes were made and details of the changes.
12-
*/
13-
export function applyFixes({
14-
envPath,
15-
examplePath,
16-
missingKeys,
17-
duplicateKeys,
18-
}: {
5+
export type ApplyFixesOptions = {
196
envPath: string;
207
examplePath: string;
218
missingKeys: string[];
229
duplicateKeys: string[];
23-
}) {
24-
const result = {
25-
removedDuplicates: [] as string[],
26-
addedEnv: [] as string[],
27-
addedExample: [] as string[],
28-
gitignoreUpdated: false as boolean,
10+
ensureGitignore?: boolean;
11+
};
12+
13+
export type FixResult = {
14+
removedDuplicates: string[];
15+
addedEnv: string[];
16+
addedExample: string[];
17+
gitignoreUpdated: boolean;
18+
};
19+
20+
/**
21+
* Applies fixes to the .env and .env.example files based on the detected issues.
22+
*
23+
* This function will:
24+
* - Remove duplicate keys from .env (keeping the last occurrence)
25+
* - Add missing keys to .env with empty values
26+
* - Add missing keys to .env.example (if not already present)
27+
* - Ensure .env is ignored in .gitignore (if in a git repo and ensureGitignore is true)
28+
*
29+
* @param options - Fix options including file paths and keys to fix
30+
* @returns An object indicating whether changes were made and details of the changes
31+
*/
32+
export function applyFixes(options: ApplyFixesOptions): {
33+
changed: boolean;
34+
result: FixResult;
35+
} {
36+
const {
37+
envPath,
38+
examplePath,
39+
missingKeys,
40+
duplicateKeys,
41+
ensureGitignore,
42+
} = options;
43+
44+
const result: FixResult = {
45+
removedDuplicates: [],
46+
addedEnv: [],
47+
addedExample: [],
48+
gitignoreUpdated: false,
2949
};
3050

3151
// --- Remove duplicates ---
3252
if (duplicateKeys.length) {
3353
const lines = fs.readFileSync(envPath, 'utf-8').split('\n');
3454
const seen = new Set<string>();
3555
const newLines: string[] = [];
56+
57+
// Process from bottom to top, keeping last occurrence
3658
for (let i = lines.length - 1; i >= 0; i--) {
3759
const line = lines[i];
3860
if (line === undefined) continue;
61+
3962
const match = line.match(/^\s*([\w.-]+)\s*=/);
4063
if (match) {
4164
const key = match[1] || '';
4265
if (duplicateKeys.includes(key)) {
43-
if (seen.has(key)) continue; // skip duplicate
66+
if (seen.has(key)) continue; // Skip duplicate
4467
seen.add(key);
4568
}
4669
}
4770
newLines.unshift(line);
4871
}
72+
4973
fs.writeFileSync(envPath, newLines.join('\n'));
5074
result.removedDuplicates = duplicateKeys;
5175
}
@@ -72,6 +96,7 @@ export function applyFixes({
7296
.filter(Boolean),
7397
);
7498
const newExampleKeys = missingKeys.filter((k) => !existingExKeys.has(k));
99+
75100
if (newExampleKeys.length) {
76101
const newExContent =
77102
exContent +
@@ -83,40 +108,9 @@ export function applyFixes({
83108
}
84109
}
85110

86-
// --- Ensure .env is ignored in gitignore (best-effort; write at git root) ---
87-
try {
88-
const startDir = path.dirname(envPath);
89-
const gitRoot = findGitRoot(startDir);
90-
if (gitRoot && isGitRepo(gitRoot)) {
91-
const gitignorePath = path.join(gitRoot, '.gitignore');
92-
// Check against the actual file name (".env" or custom)
93-
const envFileName = path.basename(envPath);
94-
const ignored = isEnvIgnoredByGit({ cwd: gitRoot, envFile: envFileName });
95-
96-
if (ignored === false || ignored === null) {
97-
const entry = '.env\n.env.*\n';
98-
if (fs.existsSync(gitignorePath)) {
99-
const current = fs.readFileSync(gitignorePath, 'utf8');
100-
// Avoid duplicate entries
101-
const hasDotEnv = current.split(/\r?\n/).some((l) => l.trim() === '.env');
102-
const hasDotEnvStar = current.split(/\r?\n/).some((l) => l.trim() === '.env.*');
103-
const pieces: string[] = [];
104-
if (!hasDotEnv) pieces.push('.env');
105-
if (!hasDotEnvStar) pieces.push('.env.*');
106-
107-
if (pieces.length) {
108-
const toAppend = `${current.endsWith('\n') ? '' : '\n'}${pieces.join('\n')}\n`;
109-
fs.appendFileSync(gitignorePath, toAppend);
110-
result.gitignoreUpdated = true;
111-
}
112-
} else {
113-
fs.writeFileSync(gitignorePath, entry);
114-
result.gitignoreUpdated = true;
115-
}
116-
}
117-
}
118-
} catch {
119-
// ignore errors - non-blocking DX
111+
// --- Ensure .env is ignored in .gitignore ---
112+
if (ensureGitignore) {
113+
result.gitignoreUpdated = updateGitignoreForEnv(envPath);
120114
}
121115

122116
const changed =
@@ -127,3 +121,58 @@ export function applyFixes({
127121

128122
return { changed, result };
129123
}
124+
125+
/**
126+
* Ensures .env patterns are present in .gitignore at the git repository root.
127+
* This is a best-effort operation and will not throw errors.
128+
*
129+
* @param envPath - Path to the .env file to check gitignore for
130+
* @returns true if .gitignore was updated, false otherwise
131+
*/
132+
function updateGitignoreForEnv(envPath: string): boolean {
133+
try {
134+
const startDir = path.dirname(envPath);
135+
const gitRoot = findGitRoot(startDir);
136+
137+
if (!gitRoot || !isGitRepo(gitRoot)) {
138+
return false;
139+
}
140+
141+
const gitignorePath = path.join(gitRoot, '.gitignore');
142+
const envFileName = path.basename(envPath);
143+
const ignored = isEnvIgnoredByGit({ cwd: gitRoot, envFile: envFileName });
144+
145+
// Already properly ignored
146+
if (ignored === true) {
147+
return false;
148+
}
149+
150+
// Need to add patterns
151+
const patterns = ['.env', '.env.*'];
152+
153+
if (fs.existsSync(gitignorePath)) {
154+
const current = fs.readFileSync(gitignorePath, 'utf8');
155+
const existingLines = current.split(/\r?\n/).map((l) => l.trim());
156+
157+
const missingPatterns = patterns.filter(
158+
(pattern) => !existingLines.includes(pattern)
159+
);
160+
161+
if (missingPatterns.length) {
162+
const toAppend =
163+
`${current.endsWith('\n') ? '' : '\n'}${missingPatterns.join('\n')}\n`;
164+
fs.appendFileSync(gitignorePath, toAppend);
165+
return true;
166+
}
167+
} else {
168+
// Create new .gitignore
169+
fs.writeFileSync(gitignorePath, patterns.join('\n') + '\n');
170+
return true;
171+
}
172+
173+
return false;
174+
} catch {
175+
// Non-blocking: ignore errors
176+
return false;
177+
}
178+
}

0 commit comments

Comments
 (0)