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/push-interactive-file-select.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@tktco/create-devenv": minor
---

push コマンドに unified diff を見ながらファイルを選択できる機能を追加

- デフォルトで差分を表示しながらチェックボックスでファイル選択が可能に
- `--no-interactive` オプションで従来の確認プロンプトに切り替え可能
- `--force` オプションは引き続き確認なしで全ファイルを push
1 change: 1 addition & 0 deletions packages/create-devenv/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"citty": "^0.1.6",
"consola": "^3.4.2",
"defu": "^6.1.4",
"diff": "^8.0.2",
"giget": "^2.0.0",
"ignore": "^7.0.5",
"pathe": "^2.0.3",
Expand Down
20 changes: 17 additions & 3 deletions packages/create-devenv/src/commands/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
promptPrBody,
promptPrTitle,
promptPushConfirm,
promptSelectFilesWithDiff,
} from "../prompts/push";
import { detectDiff, formatDiff, getPushableFiles } from "../utils/diff";
import { createPullRequest, getGitHubToken } from "../utils/github";
Expand Down Expand Up @@ -45,6 +46,12 @@ export const pushCommand = defineCommand({
description: "確認プロンプトをスキップ",
default: false,
},
interactive: {
type: "boolean",
alias: "i",
description: "差分を確認しながらファイルを選択(デフォルト有効)",
default: true,
},
},
async run({ args }) {
const targetDir = resolve(args.dir);
Expand Down Expand Up @@ -100,7 +107,7 @@ export const pushCommand = defineCommand({
});

// push 対象ファイルを取得
const pushableFiles = getPushableFiles(diff);
let pushableFiles = getPushableFiles(diff);

if (pushableFiles.length === 0) {
consola.info("push するファイルがありません。");
Expand All @@ -119,8 +126,15 @@ export const pushCommand = defineCommand({
return;
}

// 確認プロンプト
if (!args.force) {
// ファイル選択(デフォルト動作)
if (args.interactive && !args.force) {
pushableFiles = await promptSelectFilesWithDiff(pushableFiles);
if (pushableFiles.length === 0) {
consola.info("ファイルが選択されませんでした。キャンセルします。");
return;
}
} else if (!args.force) {
// --no-interactive 時は従来の確認プロンプト
const confirmed = await promptPushConfirm(diff);
if (!confirmed) {
consola.info("キャンセルしました。");
Expand Down
42 changes: 39 additions & 3 deletions packages/create-devenv/src/prompts/push.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { confirm, input, password } from "@inquirer/prompts";
import type { DiffResult } from "../modules/schemas";
import { formatDiff } from "../utils/diff";
import { checkbox, confirm, input, password } from "@inquirer/prompts";
import type { DiffResult, FileDiff } from "../modules/schemas";
import {
colorizeUnifiedDiff,
formatDiff,
generateUnifiedDiff,
} from "../utils/diff";

/**
* push 実行前の確認プロンプト
Expand Down Expand Up @@ -78,3 +82,35 @@ export async function promptGitHubToken(): Promise<string> {
},
});
}

/**
* diff を表示しながらファイルを選択するプロンプト
*/
export async function promptSelectFilesWithDiff(
pushableFiles: FileDiff[],
): Promise<FileDiff[]> {
if (pushableFiles.length === 0) {
return [];
}

// 各ファイルの unified diff を表示
console.log("\n=== 変更内容(unified diff)===\n");
for (const file of pushableFiles) {
const icon = file.type === "added" ? "[+]" : "[~]";
console.log(`--- ${icon} ${file.path} ---`);
console.log(colorizeUnifiedDiff(generateUnifiedDiff(file)));
console.log();
}

// チェックボックスでファイル選択
const choices = pushableFiles.map((file) => ({
name: `[${file.type === "added" ? "+" : "~"}] ${file.path}`,
value: file,
checked: true,
}));

return checkbox<FileDiff>({
message: "PR に含めるファイルを選択してください",
choices,
});
}
188 changes: 188 additions & 0 deletions packages/create-devenv/src/utils/__tests__/diff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import { describe, expect, it } from "vitest";
import type { FileDiff } from "../../modules/schemas";
import { colorizeUnifiedDiff, generateUnifiedDiff } from "../diff";

describe("diff", () => {
describe("generateUnifiedDiff", () => {
it("added タイプのファイルで unified diff を生成する", () => {
const fileDiff: FileDiff = {
path: "new-file.txt",
type: "added",
localContent: "line1\nline2\nline3\n",
templateContent: undefined,
};

const result = generateUnifiedDiff(fileDiff);

expect(result).toContain("--- new-file.txt");
expect(result).toContain("+++ new-file.txt");
expect(result).toContain("+line1");
expect(result).toContain("+line2");
expect(result).toContain("+line3");
});

it("modified タイプのファイルで unified diff を生成する", () => {
const fileDiff: FileDiff = {
path: "existing-file.txt",
type: "modified",
localContent: "line1\nmodified line\nline3\n",
templateContent: "line1\noriginal line\nline3\n",
};

const result = generateUnifiedDiff(fileDiff);

expect(result).toContain("--- existing-file.txt");
expect(result).toContain("+++ existing-file.txt");
expect(result).toContain("-original line");
expect(result).toContain("+modified line");
});

it("deleted タイプのファイルでは空文字列を返す", () => {
const fileDiff: FileDiff = {
path: "deleted-file.txt",
type: "deleted",
localContent: undefined,
templateContent: "content\n",
};

const result = generateUnifiedDiff(fileDiff);

expect(result).toBe("");
});

it("unchanged タイプのファイルでは空文字列を返す", () => {
const fileDiff: FileDiff = {
path: "unchanged-file.txt",
type: "unchanged",
localContent: "same content\n",
templateContent: "same content\n",
};

const result = generateUnifiedDiff(fileDiff);

expect(result).toBe("");
});

it("空のファイルを追加する場合", () => {
const fileDiff: FileDiff = {
path: "empty-file.txt",
type: "added",
localContent: "",
templateContent: undefined,
};

const result = generateUnifiedDiff(fileDiff);

expect(result).toContain("--- empty-file.txt");
expect(result).toContain("+++ empty-file.txt");
});

it("内容が undefined の場合でも正しく処理する", () => {
const fileDiff: FileDiff = {
path: "file.txt",
type: "added",
localContent: undefined,
templateContent: undefined,
};

const result = generateUnifiedDiff(fileDiff);

// エラーなく空の diff が生成される
expect(result).toContain("--- file.txt");
});

it("複数行の変更を含む diff を生成する", () => {
const fileDiff: FileDiff = {
path: "config.json",
type: "modified",
localContent: `{
"name": "new-name",
"version": "2.0.0",
"description": "updated"
}`,
templateContent: `{
"name": "old-name",
"version": "1.0.0",
"description": "original"
}`,
};

const result = generateUnifiedDiff(fileDiff);

expect(result).toContain('- "name": "old-name"');
expect(result).toContain('+ "name": "new-name"');
expect(result).toContain('- "version": "1.0.0"');
expect(result).toContain('+ "version": "2.0.0"');
});
});

describe("colorizeUnifiedDiff", () => {
it("追加行を緑色にする", () => {
const diff = "+added line";

const result = colorizeUnifiedDiff(diff);

expect(result).toBe("\x1b[32m+added line\x1b[0m");
});

it("削除行を赤色にする", () => {
const diff = "-removed line";

const result = colorizeUnifiedDiff(diff);

expect(result).toBe("\x1b[31m-removed line\x1b[0m");
});

it("ハンク行をシアン色にする", () => {
const diff = "@@ -1,3 +1,4 @@";

const result = colorizeUnifiedDiff(diff);

expect(result).toBe("\x1b[36m@@ -1,3 +1,4 @@\x1b[0m");
});

it("ヘッダー行をボールドにする", () => {
const diff = "--- file.txt\n+++ file.txt";

const result = colorizeUnifiedDiff(diff);

expect(result).toContain("\x1b[1m--- file.txt\x1b[0m");
expect(result).toContain("\x1b[1m+++ file.txt\x1b[0m");
});

it("コンテキスト行はそのまま", () => {
const diff = " unchanged line";

const result = colorizeUnifiedDiff(diff);

expect(result).toBe(" unchanged line");
});

it("複数行の diff を正しくカラー化する", () => {
const diff = `--- file.txt
+++ file.txt
@@ -1,3 +1,3 @@
line1
-old line
+new line
line3`;

const result = colorizeUnifiedDiff(diff);

const lines = result.split("\n");
expect(lines[0]).toBe("\x1b[1m--- file.txt\x1b[0m");
expect(lines[1]).toBe("\x1b[1m+++ file.txt\x1b[0m");
expect(lines[2]).toBe("\x1b[36m@@ -1,3 +1,3 @@\x1b[0m");
expect(lines[3]).toBe(" line1");
expect(lines[4]).toBe("\x1b[31m-old line\x1b[0m");
expect(lines[5]).toBe("\x1b[32m+new line\x1b[0m");
expect(lines[6]).toBe(" line3");
});

it("空の diff を処理する", () => {
const result = colorizeUnifiedDiff("");

expect(result).toBe("");
});
});
});
41 changes: 41 additions & 0 deletions packages/create-devenv/src/utils/diff.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { existsSync } from "node:fs";
import { readFile } from "node:fs/promises";
import consola from "consola";
import { createPatch } from "diff";
import { join } from "pathe";
import { getModuleById } from "../modules";
import type {
Expand Down Expand Up @@ -174,3 +175,43 @@ export function hasDiff(diff: DiffResult): boolean {
diff.summary.deleted > 0
);
}

/**
* FileDiff から unified diff 形式の文字列を生成
*/
export function generateUnifiedDiff(fileDiff: FileDiff): string {
const { path, type, localContent, templateContent } = fileDiff;

switch (type) {
case "added":
return createPatch(path, "", localContent || "", "template", "local");
case "modified":
return createPatch(
path,
templateContent || "",
localContent || "",
"template",
"local",
);
default:
return "";
}
}

/**
* unified diff にカラーを適用
*/
export function colorizeUnifiedDiff(diff: string): string {
return diff
.split("\n")
.map((line) => {
if (line.startsWith("+++") || line.startsWith("---")) {
return `\x1b[1m${line}\x1b[0m`; // ボールド
}
if (line.startsWith("+")) return `\x1b[32m${line}\x1b[0m`; // 緑
if (line.startsWith("-")) return `\x1b[31m${line}\x1b[0m`; // 赤
if (line.startsWith("@@")) return `\x1b[36m${line}\x1b[0m`; // シアン
return line;
})
.join("\n");
}
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.