Skip to content

Commit 3d4a173

Browse files
committed
fix flag for .env not ignored på .gitignore
1 parent e57e653 commit 3d4a173

9 files changed

Lines changed: 193 additions & 57 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ This project follows [Keep a Changelog](https://keepachangelog.com/) and [Semant
1212
### Fixed
1313
-
1414

15+
## [2.2.8] - 2025-09-30
16+
### Added
17+
- Fix .env is not ignored by git when using --fix flag.
18+
19+
### Changed
20+
- No breaking changes.
21+
1522
## [2.2.7] - 2025-09-28
1623
### Added
1724
- Added warning on .env not ignored by .gitignore on default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dotenv-diff",
3-
"version": "2.2.7",
3+
"version": "2.2.8",
44
"type": "module",
55
"description": "Scan your codebase to find environment variables in use.",
66
"bin": {

src/commands/compare.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import path from 'path';
33
import chalk from 'chalk';
44
import { parseEnvFile } from '../core/parseEnv.js';
55
import { diffEnv } from '../core/diffEnv.js';
6-
import { warnIfEnvNotIgnored } from '../services/git.js';
6+
import { warnIfEnvNotIgnored, isEnvIgnoredByGit } from '../services/git.js';
77
import { findDuplicateKeys } from '../services/duplicates.js';
88
import { filterIgnoredKeys } from '../core/filterIgnoredKeys.js';
99
import type { Category, CompareJsonEntry } from '../config/types.js';
@@ -73,13 +73,15 @@ export async function compareMany(
7373

7474
// Git ignore hint (only when not JSON)
7575
let gitignoreUnsafe = false;
76+
let gitignoreMsg: string | null = null;
77+
7678
if (run('gitignore')) {
7779
warnIfEnvNotIgnored({
7880
cwd: opts.cwd,
7981
envFile: envName,
8082
log: (msg) => {
8183
gitignoreUnsafe = true;
82-
if (!opts.json) console.log(msg.replace(/^/gm, ' '));
84+
gitignoreMsg = msg;
8385
},
8486
});
8587
}
@@ -295,17 +297,61 @@ export async function compareMany(
295297
);
296298
console.log();
297299
}
300+
if (gitignoreMsg && !opts.json) {
301+
console.log((gitignoreMsg as string).replace(/^/gm, ' '));
302+
}
298303
}
299304

300305
if (!opts.json && !opts.fix) {
306+
const ignored = isEnvIgnoredByGit({ cwd: opts.cwd, envFile: '.env' });
307+
const envNotIgnored = ignored === false || ignored === null;
301308
if (
302-
filtered.missing.length ||
303-
filtered.duplicatesEnv.length ||
304-
filtered.duplicatesEx.length
309+
filtered.missing.length > 0 &&
310+
filtered.duplicatesEnv.length > 0 &&
311+
envNotIgnored
312+
) {
313+
console.log(
314+
chalk.gray(
315+
'💡 Tip: Run with `--fix` to add missing keys, remove duplicates and add .env to .gitignore',
316+
),
317+
);
318+
console.log();
319+
} else if (
320+
filtered.missing.length > 0 &&
321+
filtered.duplicatesEnv.length > 0
305322
) {
306323
console.log(
307324
chalk.gray(
308-
'💡 Tip: Run with `--fix` to automatically add missing keys and remove duplicates.',
325+
'💡 Tip: Run with `--fix` to add missing keys and remove duplicates',
326+
),
327+
);
328+
console.log();
329+
} else if (filtered.duplicatesEnv.length > 0 && envNotIgnored) {
330+
console.log(
331+
chalk.gray(
332+
'💡 Tip: Run with `--fix` to remove duplicate keys and add .env to .gitignore',
333+
),
334+
);
335+
console.log();
336+
} else if (filtered.missing.length > 0 && envNotIgnored) {
337+
console.log(
338+
chalk.gray(
339+
'💡 Tip: Run with `--fix` to add missing keys and add .env to .gitignore',
340+
),
341+
);
342+
console.log();
343+
} else if (filtered.missing.length > 0) {
344+
console.log(chalk.gray('💡 Tip: Run with `--fix` to add missing keys'));
345+
console.log();
346+
} else if (filtered.duplicatesEnv.length > 0) {
347+
console.log(
348+
chalk.gray('💡 Tip: Run with `--fix` to remove duplicate keys'),
349+
);
350+
console.log();
351+
} else if (envNotIgnored) {
352+
console.log(
353+
chalk.gray(
354+
'💡 Tip: Run with `--fix` to ensure .env is added to .gitignore',
309355
),
310356
);
311357
console.log();

src/commands/scanUsage.ts

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { createJsonOutput } from '../core/scanJsonOutput.js';
1212
import { findDuplicateKeys } from '../services/duplicates.js';
1313
import { compareWithEnvFiles } from '../core/compareScan.js';
1414
import { applyFixes } from '../core/fixEnv.js';
15+
import { isEnvIgnoredByGit } from '../services/git.js';
1516

1617
/**
1718
* Scans codebase for environment variable usage and compares with .env file
@@ -46,9 +47,9 @@ export async function scanUsage(
4647
// Recalculate stats after filtering out commented usages
4748
const uniqueVariables = new Set(scanResult.used.map((u) => u.variable)).size;
4849
scanResult.stats = {
49-
filesScanned: scanResult.stats.filesScanned, // Keep original files scanned count
50+
filesScanned: scanResult.stats.filesScanned,
5051
totalUsages: scanResult.used.length,
51-
uniqueVariables: uniqueVariables,
52+
uniqueVariables,
5253
};
5354

5455
// If user explicitly passed --example but the file doesn't exist:
@@ -59,7 +60,6 @@ export async function scanUsage(
5960
if (missing) {
6061
const msg = `❌ Missing specified example file: ${opts.examplePath}`;
6162
if (opts.isCiMode) {
62-
// IMPORTANT: stdout (console.log), not stderr, to satisfy the test
6363
console.log(chalk.red(msg));
6464
return { exitWithError: true };
6565
} else if (!opts.json) {
@@ -81,6 +81,7 @@ export async function scanUsage(
8181
let fixApplied = false;
8282
let fixedKeys: string[] = [];
8383
let removedDuplicates: string[] = [];
84+
let gitignoreUpdated = false;
8485

8586
if (compareFile) {
8687
try {
@@ -131,14 +132,15 @@ export async function scanUsage(
131132
if (changed) {
132133
fixApplied = true;
133134
removedDuplicates = result.removedDuplicates;
135+
gitignoreUpdated = gitignoreUpdated || result.gitignoreUpdated;
134136
// Clear duplicates after fix
135137
duplicatesFound = false;
136138
dupsEnv = [];
137139
dupsExample = [];
138140
}
139141
}
140142

141-
// Add to scan result for both JSON and console output (only if not fixed)
143+
// Keep duplicates for output if not fixed
142144
if (
143145
(dupsEnv.length > 0 || dupsExample.length > 0) &&
144146
(!opts.fix || !fixApplied)
@@ -151,65 +153,55 @@ export async function scanUsage(
151153
}
152154
} catch (error) {
153155
const errorMessage = `⚠️ Could not read ${compareFile.name}: ${compareFile.path} - ${error}`;
154-
155156
if (opts.isCiMode) {
156-
// In CI mode, exit with error if file doesn't exist
157157
console.log(chalk.red(`❌ ${errorMessage}`));
158158
return { exitWithError: true };
159159
}
160-
161-
if (!opts.json) {
162-
console.log(chalk.yellow(errorMessage));
163-
}
160+
if (!opts.json) console.log(chalk.yellow(errorMessage));
164161
}
165162
}
166163

167-
// Apply missing keys fix if --fix is enabled (but don't show message yet)
164+
// Apply missing keys fix with applyFixes (so gitignore is handled too)
168165
if (opts.fix && compareFile) {
169166
const missingKeys = scanResult.missing;
170-
171167
if (missingKeys.length > 0) {
172168
const envFilePath = compareFile.path;
173169
const exampleFilePath = opts.examplePath
174170
? resolveFromCwd(opts.cwd, opts.examplePath)
175-
: null;
171+
: '';
176172

177-
// Append missing keys to .env
178-
const content = fs.readFileSync(envFilePath, 'utf-8');
179-
const newContent =
180-
content +
181-
(content.endsWith('\n') ? '' : '\n') +
182-
missingKeys.map((k) => `${k}=`).join('\n') +
183-
'\n';
184-
fs.writeFileSync(envFilePath, newContent);
173+
const { changed, result } = applyFixes({
174+
envPath: envFilePath,
175+
examplePath: exampleFilePath,
176+
missingKeys,
177+
duplicateKeys: [],
178+
});
185179

186-
// Append to .env.example if it exists
187-
if (exampleFilePath && fs.existsSync(exampleFilePath)) {
188-
const exContent = fs.readFileSync(exampleFilePath, 'utf-8');
189-
const existingExKeys = new Set(
190-
exContent
191-
.split('\n')
192-
.map((l) => l.trim().split('=')[0])
193-
.filter(Boolean),
194-
);
195-
const newKeys = missingKeys.filter((k) => !existingExKeys.has(k));
196-
if (newKeys.length) {
197-
const newExContent =
198-
exContent +
199-
(exContent.endsWith('\n') ? '' : '\n') +
200-
newKeys.join('\n') +
201-
'\n';
202-
fs.writeFileSync(exampleFilePath, newExContent);
203-
}
180+
if (changed) {
181+
fixApplied = true;
182+
fixedKeys = result.addedEnv;
183+
gitignoreUpdated = gitignoreUpdated || result.gitignoreUpdated;
184+
scanResult.missing = [];
204185
}
186+
}
187+
}
205188

189+
// Always run a gitignore-only fix when --fix is set (even if no missing/duplicates)
190+
if (opts.fix && compareFile) {
191+
const { result } = applyFixes({
192+
envPath: compareFile.path,
193+
examplePath: '',
194+
missingKeys: [],
195+
duplicateKeys: [],
196+
});
197+
if (result.gitignoreUpdated) {
206198
fixApplied = true;
207-
fixedKeys = missingKeys;
208-
scanResult.missing = [];
199+
gitignoreUpdated = true;
209200
}
210201
}
211202

212-
// Prepare JSON output
203+
204+
// JSON output
213205
if (opts.json) {
214206
const jsonOutput = createJsonOutput(
215207
scanResult,
@@ -235,7 +227,7 @@ export async function scanUsage(
235227
// Console output
236228
const result = outputToConsole(scanResult, opts, comparedAgainst);
237229

238-
// Show consolidated fix message at the bottom (after all other output)
230+
// Consolidated fix message
239231
if (opts.fix && !opts.json) {
240232
if (fixApplied) {
241233
console.log(chalk.green('✅ Auto-fix applied:'));
@@ -265,7 +257,9 @@ export async function scanUsage(
265257
);
266258
}
267259
}
268-
260+
if (gitignoreUpdated) {
261+
console.log(chalk.green(' - Added .env ignore rules to .gitignore'));
262+
}
269263
console.log();
270264
} else {
271265
console.log(chalk.green('✅ Auto-fix applied: no changes needed.'));
@@ -274,6 +268,8 @@ export async function scanUsage(
274268
}
275269

276270
if (!opts.json && !opts.fix) {
271+
const ignored = isEnvIgnoredByGit({ cwd: opts.cwd, envFile: '.env' });
272+
const envNotIgnored = ignored === false || ignored === null;
277273
if (scanResult.missing.length > 0 && duplicatesFound) {
278274
console.log(
279275
chalk.gray(
@@ -289,6 +285,13 @@ export async function scanUsage(
289285
chalk.gray('💡 Tip: Run with `--fix` to remove duplicate keys'),
290286
);
291287
console.log();
288+
} else if (envNotIgnored) {
289+
console.log(
290+
chalk.gray(
291+
'💡 Tip: Run with `--fix` to ensure .env is added to .gitignore',
292+
),
293+
);
294+
console.log();
292295
}
293296
}
294297

src/core/fixEnv.ts

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import fs from 'fs';
2+
import path from 'path';
3+
import { isEnvIgnoredByGit, isGitRepo, findGitRoot } from '../services/git.js';
24

35
/**
46
* Applies fixes to the .env and .env.example files based on the detected issues.
@@ -23,6 +25,7 @@ export function applyFixes({
2325
removedDuplicates: [] as string[],
2426
addedEnv: [] as string[],
2527
addedExample: [] as string[],
28+
gitignoreUpdated: false as boolean,
2629
};
2730

2831
// --- Remove duplicates ---
@@ -44,7 +47,7 @@ export function applyFixes({
4447
newLines.unshift(line);
4548
}
4649
fs.writeFileSync(envPath, newLines.join('\n'));
47-
result.removedDuplicates = duplicateKeys; // save all dupe keys
50+
result.removedDuplicates = duplicateKeys;
4851
}
4952

5053
// --- Add missing keys to .env ---
@@ -56,7 +59,7 @@ export function applyFixes({
5659
missingKeys.map((k) => `${k}=`).join('\n') +
5760
'\n';
5861
fs.writeFileSync(envPath, newContent);
59-
result.addedEnv = missingKeys; // save all missing keys
62+
result.addedEnv = missingKeys;
6063
}
6164

6265
// --- Add missing keys to .env.example ---
@@ -76,14 +79,51 @@ export function applyFixes({
7679
newExampleKeys.join('\n') +
7780
'\n';
7881
fs.writeFileSync(examplePath, newExContent);
79-
result.addedExample = newExampleKeys; // save all keys actually added
82+
result.addedExample = newExampleKeys;
8083
}
8184
}
8285

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
120+
}
121+
83122
const changed =
84123
result.removedDuplicates.length > 0 ||
85124
result.addedEnv.length > 0 ||
86-
result.addedExample.length > 0;
125+
result.addedExample.length > 0 ||
126+
result.gitignoreUpdated;
87127

88128
return { changed, result };
89129
}

0 commit comments

Comments
 (0)