Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/gitignore-exclude.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@tktco/create-devenv": minor
---

.gitignore に記載されたファイルを自動的に除外する機能を追加

- init, diff, push の全コマンドで .gitignore にマッチするファイルを除外
- ローカルディレクトリとテンプレートリポジトリ両方の .gitignore をチェック
- クレデンシャル等の機密情報の誤流出を防止
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "tktcorporation-github",
"private": true,
"packageManager": "pnpm@10.0.0",
"packageManager": "pnpm@10.25.0",
"scripts": {
"version": "changeset version",
"release": "changeset publish"
},
"devDependencies": {
"@changesets/changelog-github": "^0.5.2",
"@changesets/cli": "^2.29.8",
"rollup": "^4.40.0"
"rollup": "^4.53.3"
}
}
9 changes: 5 additions & 4 deletions packages/create-devenv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,20 @@
},
"dependencies": {
"@inquirer/prompts": "^8.0.2",
"@octokit/rest": "^21.0.0",
"@octokit/rest": "^22.0.1",
"citty": "^0.1.6",
"consola": "^3.4.2",
"defu": "^6.1.4",
"giget": "^2.0.0",
"ignore": "^7.0.5",
"pathe": "^2.0.3",
"tinyglobby": "^0.2.0",
"tinyglobby": "^0.2.15",
"ts-pattern": "^5.9.0",
"zod": "^4.1.13"
},
"devDependencies": {
"@biomejs/biome": "^2.3.8",
"@types/node": "^22.19.1",
"@types/node": "^22.19.2",
"@vitest/coverage-v8": "^4.0.15",
"memfs": "^4.51.1",
"typescript": "^5.9.3",
Expand All @@ -52,6 +53,6 @@
"provenance": true
},
"engines": {
"node": ">=18.0.0"
"node": ">=20.0.0"
}
}
177 changes: 177 additions & 0 deletions packages/create-devenv/src/utils/__tests__/gitignore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import { vol } from "memfs";
import { beforeEach, describe, expect, it, vi } from "vitest";

// fs モジュールをモック
vi.mock("node:fs", async () => {
const memfs = await import("memfs");
return memfs.fs;
});

vi.mock("node:fs/promises", async () => {
const memfs = await import("memfs");
return memfs.fs.promises;
});

// モック後にインポート
const { loadMergedGitignore, filterByGitignore } = await import("../gitignore");

describe("gitignore", () => {
beforeEach(() => {
vol.reset();
});

describe("loadMergedGitignore", () => {
it(".gitignore が存在しない場合は空の Ignore を返す", async () => {
vol.fromJSON({});

const ig = await loadMergedGitignore(["/project"]);

// 何もフィルタリングされない
const files = ["file.txt", "secret.env"];
expect(ig.filter(files)).toEqual(files);
});

it("単一ディレクトリの .gitignore を読み込む", async () => {
vol.fromJSON({
"/project/.gitignore": "*.env\nnode_modules/",
});

const ig = await loadMergedGitignore(["/project"]);

expect(ig.filter(["app.ts", "secret.env", "node_modules/pkg"])).toEqual([
"app.ts",
]);
});

it("複数ディレクトリの .gitignore をマージする", async () => {
vol.fromJSON({
"/local/.gitignore": "*.env",
"/template/.gitignore": "*.secret",
});

const ig = await loadMergedGitignore(["/local", "/template"]);

// 両方の .gitignore ルールが適用される
expect(
ig.filter(["app.ts", "config.env", "api.secret", "readme.md"]),
).toEqual(["app.ts", "readme.md"]);
});

it("片方のディレクトリにのみ .gitignore がある場合", async () => {
vol.fromJSON({
"/local/.gitignore": "*.env",
// /template には .gitignore がない
});

const ig = await loadMergedGitignore(["/local", "/template"]);

expect(ig.filter(["app.ts", "config.env"])).toEqual(["app.ts"]);
});

it("空の .gitignore ファイルを正しく処理する", async () => {
vol.fromJSON({
"/project/.gitignore": "",
});

const ig = await loadMergedGitignore(["/project"]);

const files = ["file.txt", "secret.env"];
expect(ig.filter(files)).toEqual(files);
});

it("コメント行のみの .gitignore を正しく処理する", async () => {
vol.fromJSON({
"/project/.gitignore": "# This is a comment\n# Another comment",
});

const ig = await loadMergedGitignore(["/project"]);

const files = ["file.txt", "secret.env"];
expect(ig.filter(files)).toEqual(files);
});

it("複雑な gitignore パターンを処理する", async () => {
vol.fromJSON({
"/project/.gitignore": `
# 環境変数ファイル
*.env
.env.*

# ビルド成果物
dist/
build/

# 依存関係
node_modules/

# IDE
.vscode/
.idea/

# ネゲーション(除外から除外)
!.env.example
`,
});

const ig = await loadMergedGitignore(["/project"]);

const files = [
"src/app.ts",
".env",
".env.local",
".env.example",
"dist/bundle.js",
"node_modules/pkg/index.js",
".vscode/settings.json",
"README.md",
];

expect(ig.filter(files)).toEqual([
"src/app.ts",
".env.example", // ネゲーションで除外から復帰
"README.md",
]);
});
});

describe("filterByGitignore", () => {
it("gitignore ルールに従ってファイルをフィルタリングする", async () => {
vol.fromJSON({
"/project/.gitignore": "*.env\n*.secret",
});

const ig = await loadMergedGitignore(["/project"]);
const files = ["app.ts", "config.env", "api.secret", "readme.md"];

expect(filterByGitignore(files, ig)).toEqual(["app.ts", "readme.md"]);
});

it("空のファイルリストを正しく処理する", async () => {
vol.fromJSON({
"/project/.gitignore": "*.env",
});

const ig = await loadMergedGitignore(["/project"]);

expect(filterByGitignore([], ig)).toEqual([]);
});

it("ディレクトリパターンを正しく処理する", async () => {
vol.fromJSON({
"/project/.gitignore": ".devcontainer/",
});

const ig = await loadMergedGitignore(["/project"]);

const files = [
".devcontainer/devcontainer.json",
".devcontainer/setup.sh",
".github/workflows/ci.yml",
];

expect(filterByGitignore(files, ig)).toEqual([
".github/workflows/ci.yml",
]);
});
});
});
19 changes: 15 additions & 4 deletions packages/create-devenv/src/utils/diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
DiffType,
FileDiff,
} from "../modules/schemas";
import { filterByGitignore, loadMergedGitignore } from "./gitignore";
import { getEffectivePatterns, resolvePatterns } from "./patterns";

export interface DiffOptions {
Expand All @@ -30,6 +31,10 @@ export async function detectDiff(options: DiffOptions): Promise<DiffResult> {
let deleted = 0;
let unchanged = 0;

// ローカルとテンプレート両方の .gitignore をマージして読み込み
// クレデンシャル等の機密情報の誤流出を防止
const gitignore = await loadMergedGitignore([targetDir, templateDir]);

for (const moduleId of moduleIds) {
const mod = getModuleById(moduleId);
if (!mod) {
Expand All @@ -40,10 +45,16 @@ export async function detectDiff(options: DiffOptions): Promise<DiffResult> {
// 有効なパターンを取得(カスタムパターン考慮)
const patterns = getEffectivePatterns(moduleId, mod.patterns, config);

// テンプレート側のファイル一覧を取得
const templateFiles = resolvePatterns(templateDir, patterns);
// ローカル側のファイル一覧を取得
const localFiles = resolvePatterns(targetDir, patterns);
// テンプレート側のファイル一覧を取得し、gitignore でフィルタリング
const templateFiles = filterByGitignore(
resolvePatterns(templateDir, patterns),
gitignore,
);
// ローカル側のファイル一覧を取得し、gitignore でフィルタリング
const localFiles = filterByGitignore(
resolvePatterns(targetDir, patterns),
gitignore,
);

const allFiles = new Set([...templateFiles, ...localFiles]);

Expand Down
29 changes: 29 additions & 0 deletions packages/create-devenv/src/utils/gitignore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import ignore, { type Ignore } from "ignore";
import { join } from "pathe";

/**
* 複数ディレクトリの .gitignore をマージして読み込み
* ローカルとテンプレートの両方の .gitignore を考慮することで、
* クレデンシャル等の機密情報の誤流出を防止する
*/
export async function loadMergedGitignore(dirs: string[]): Promise<Ignore> {
const ig = ignore();
for (const dir of dirs) {
const gitignorePath = join(dir, ".gitignore");
if (existsSync(gitignorePath)) {
const content = await readFile(gitignorePath, "utf-8");
ig.add(content);
}
}
return ig;
}

/**
* gitignore ルールでファイルをフィルタリング
* gitignore に該当しないファイルのみを返す
*/
export function filterByGitignore(files: string[], ig: Ignore): string[] {
return ig.filter(files);
}
10 changes: 8 additions & 2 deletions packages/create-devenv/src/utils/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
FileOperationResult,
OverwriteStrategy,
} from "../modules/schemas";
import { filterByGitignore, loadMergedGitignore } from "./gitignore";
import { getEffectivePatterns, resolvePatterns } from "./patterns";

const TEMPLATE_SOURCE = "gh:tktcorporation/.github";
Expand Down Expand Up @@ -101,6 +102,10 @@ export async function fetchTemplates(

consola.success("テンプレートを取得しました");

// ローカルとテンプレート両方の .gitignore をマージして読み込み
// クレデンシャル等の機密情報の誤流出を防止
const gitignore = await loadMergedGitignore([targetDir, templateDir]);

// 選択されたモジュールのファイルをパターンベースでコピー
for (const moduleId of modules) {
const moduleDef = getModuleById(moduleId);
Expand All @@ -113,8 +118,9 @@ export async function fetchTemplates(
config,
);

// パターンにマッチするファイル一覧を取得
const files = resolvePatterns(templateDir, patterns);
// パターンにマッチするファイル一覧を取得し、gitignore でフィルタリング
const resolvedFiles = resolvePatterns(templateDir, patterns);
const files = filterByGitignore(resolvedFiles, gitignore);

if (files.length === 0) {
consola.warn(
Expand Down
Loading