github copilot(gpt5mini)が吐いたコード。
まだ理解できてない。後追いでやる。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
class DiffMethodMapper
{
static int Main(string[] args)
{
if (args.Length < 2)
{
Console.Error.WriteLine("Usage: DiffMethodMapper <commitA> <commitB>");
return 2;
}
string commitA = args[0];
string commitB = args[1];
// 変更された C# ファイル一覧を取得
var filesTxt = RunGit("diff --name-only --no-color " + commitA + " " + commitB + " -- \"*.cs\"");
if (filesTxt == null) return 1;
var files = filesTxt.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
.Select(f => f.Trim())
.Where(f => f.Length > 0).ToArray();
if (files.Length == 0)
{
Console.WriteLine("No C# files changed.");
return 0;
}
foreach (var file in files)
{
Console.WriteLine("File: " + file);
// diff の hunk を取得 (対象ファイルのみ)
var diff = RunGit("diff --no-color -U0 " + commitA + " " + commitB + " -- \"" + file + "\"");
if (diff == null)
{
Console.WriteLine(" (failed to get diff)");
continue;
}
var ranges = ParseAddedLineRanges(diff);
if (ranges.Count == 0)
{
Console.WriteLine(" (no added/changed lines)");
continue;
}
// ファイルの新しい内容を取得(commitB の版)
var fileContent = RunGitQuiet("show " + commitB + ":" + EscapePathForGitShow(file));
if (fileContent == null)
{
Console.WriteLine(" (file not present in commit " + commitB + ")");
continue;
}
var tree = CSharpSyntaxTree.ParseText(fileContent);
var root = tree.GetRoot();
var text = tree.GetText();
// 各範囲について、範囲内の行の代表(開始行)を使ってメンバーを求める
var reported = new HashSet<string>();
foreach (var r in ranges)
{
for (int ln = r.Start; ln <= r.End; ln++)
{
if (ln < 1 || ln > text.Lines.Count) continue;
var pos = text.Lines[ln - 1].Start;
var token = root.FindToken(pos);
var node = token.Parent;
var member = GetEnclosingMemberFullName(node);
if (member == null) member = $"<global line {ln}>";
if (reported.Add(member))
{
Console.WriteLine($" {member} (lines {r.Start}-{r.End})");
}
// ひとつの範囲から重複出力を避けるため break して次の範囲へするか、
// ここはラップポリシーに依存。今回は範囲内全行で検出するが、
// 同じメンバーは reported によって一度だけ表示される。
}
}
}
return 0;
}
// git コマンドを実行して stdout を返す。失敗時は null。
static string RunGit(string args)
{
try
{
var psi = new ProcessStartInfo("git", args)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = System.Text.Encoding.UTF8,
StandardErrorEncoding = System.Text.Encoding.UTF8
};
using (var p = Process.Start(psi))
{
string outp = p.StandardOutput.ReadToEnd();
string err = p.StandardError.ReadToEnd();
p.WaitForExit();
if (p.ExitCode != 0)
{
Console.Error.WriteLine("git failed: " + err.Trim());
return null;
}
return outp;
}
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to run git: " + ex.Message);
return null;
}
}
// git show は二重引用やスペースがあると失敗するので、パスをコロン区切りで安全に渡すための簡易エスケープ
static string EscapePathForGitShow(string path)
{
// git show <commit>:<path> — path がリポジトリ内相対パスである前提
// Windows のパス区切りを / に変換
return path.Replace('\\', '/');
}
// git show を実行して出力を返す(NULL で失敗)
static string RunGitQuiet(string args)
{
return RunGit(args);
}
// diff の hunk から +側行範囲を抽出する
static List<(int Start, int End)> ParseAddedLineRanges(string diff)
{
var list = new List<(int Start, int End)>();
var lines = diff.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var regex = new Regex(@"^\@\@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@");
foreach (var l in lines)
{
var m = regex.Match(l);
if (m.Success)
{
int start = int.Parse(m.Groups[1].Value);
int count = 1;
if (m.Groups[2].Success) count = int.Parse(m.Groups[2].Value);
int end = start + Math.Max(0, count - 1);
list.Add((start, end));
}
}
return list;
}
// Roslyn のノードから、包含する最上位のメンバー名(Namespace.Type...Member)を組み立てて返す
// ...existing code...
static string GetEnclosingMemberFullName(SyntaxNode node)
{
if (node == null) return null;
// メソッド系(BaseMethodDeclaration: メソッド/コンストラクタ/デストラクタ)かローカル関数のみを探す
SyntaxNode methodNode = node;
while (methodNode != null &&
!(methodNode is BaseMethodDeclarationSyntax) &&
!(methodNode is LocalFunctionStatementSyntax))
{
methodNode = methodNode.Parent;
}
if (methodNode == null) return null;
string memberName = null;
if (methodNode is MethodDeclarationSyntax md) memberName = md.Identifier.Text;
else if (methodNode is ConstructorDeclarationSyntax cd) memberName = cd.Identifier.Text;
else if (methodNode is DestructorDeclarationSyntax dd) memberName = dd.Identifier.Text;
else if (methodNode is LocalFunctionStatementSyntax lf) memberName = lf.Identifier.Text;
else return null; // 念のため、それ以外は無視
// 型名チェーン(メソッドを含む最上位の型まで遡る)
var typeNames = new List<string>();
var n = methodNode;
while (n != null)
{
if (n is TypeDeclarationSyntax tds)
{
typeNames.Insert(0, tds.Identifier.Text);
}
n = n.Parent;
}
// namespace(ファイルスコープ namespace 対応)
string ns = null;
var p = methodNode;
while (p != null && !(p is NamespaceDeclarationSyntax))
p = p.Parent;
if (p is NamespaceDeclarationSyntax nds)
{
ns = nds.Name.ToString();
}
else
{
var root = methodNode.SyntaxTree.GetRoot();
var fileScoped = root.DescendantNodes().OfType<FileScopedNamespaceDeclarationSyntax>().FirstOrDefault();
if (fileScoped != null) ns = fileScoped.Name.ToString();
}
var fullnameParts = new List<string>();
if (!string.IsNullOrEmpty(ns)) fullnameParts.Add(ns);
if (typeNames.Count > 0) fullnameParts.Add(string.Join(".", typeNames));
if (!string.IsNullOrEmpty(memberName)) fullnameParts.Add(memberName);
if (fullnameParts.Count == 0) return null;
return string.Join(".", fullnameParts);
}
// ...existing code...
}
ビルドして出来上がったexeをa.exeとすると、
c#を含むリポジトリのtop階層にa.exeをおいて、
a.exe d50bc98bbb0268c3679b0ebd8f5a3b4691eb44e6 cddf9746145a626e906a2832800e6a5bb15a60f8
などとすると、そのコミット間の変化点が属するメソッドを抜き出せる。
※第一引数が変更前、第二が変更後。
ファイル名とメソッド名、メソッドのコードを出すようにした
すごいなコパイロット、、、
// ...existing code...
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
class DiffMethodMapper
{
static int Main(string[] args)
{
if (args.Length < 2)
{
Console.Error.WriteLine("Usage: DiffMethodMapper <commitA> <commitB> [repoPath]");
return 2;
}
string commitA = args[0];
string commitB = args[1];
string repoPath = args.Length >= 3 ? args[2] : null;
var filesTxt = RunGit($"diff --name-only --no-color {commitA} {commitB} -- \"*.cs\"", repoPath);
if (filesTxt == null) return 1;
var files = filesTxt.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
.Select(f => f.Trim())
.Where(f => f.Length > 0).ToArray();
if (files.Length == 0)
{
Console.WriteLine("No C# files changed.");
return 0;
}
foreach (var file in files)
{
Console.WriteLine("File: " + file);
var diff = RunGit($"diff --no-color -U0 {commitA} {commitB} -- \"{file}\"", repoPath);
if (diff == null)
{
Console.WriteLine(" (failed to get diff)");
continue;
}
var ranges = ParseAddedLineRanges(diff);
if (ranges.Count == 0)
{
Console.WriteLine(" (no added/changed lines)");
continue;
}
var fileContent = RunGitQuiet($"show {commitB}:{EscapePathForGitShow(file)}", repoPath);
if (fileContent == null)
{
Console.WriteLine(" (file not present in commit " + commitB + ")");
continue;
}
var tree = CSharpSyntaxTree.ParseText(fileContent);
var root = tree.GetRoot();
var text = tree.GetText();
var reported = new HashSet<string>();
foreach (var r in ranges)
{
// 代表行だけでなく範囲内をスキャンしてメソッドを拾う
for (int ln = r.Start; ln <= r.End; ln++)
{
if (ln < 1 || ln > text.Lines.Count) continue;
var pos = text.Lines[ln - 1].Start;
var token = root.FindToken(pos);
var node = token.Parent;
var methodNode = FindEnclosingMethodNode(node);
if (methodNode == null) continue;
var memberFullName = GetEnclosingMemberFullName(methodNode);
if (string.IsNullOrEmpty(memberFullName)) memberFullName = $"<method at line {ln}>";
if (reported.Add(memberFullName))
{
Console.WriteLine($" Member: {memberFullName} (lines {r.Start}-{r.End})");
var src = ExtractMemberSource(methodNode);
Console.WriteLine("---- source ----");
Console.WriteLine(src.TrimEnd());
Console.WriteLine("----------------");
}
}
}
}
return 0;
}
static string RunGit(string args, string repoPath = null)
{
try
{
var psi = new ProcessStartInfo("git", args)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = System.Text.Encoding.UTF8,
StandardErrorEncoding = System.Text.Encoding.UTF8
};
if (!string.IsNullOrEmpty(repoPath))
{
psi.WorkingDirectory = repoPath;
}
using (var p = Process.Start(psi))
{
string outp = p.StandardOutput.ReadToEnd();
string err = p.StandardError.ReadToEnd();
p.WaitForExit();
if (p.ExitCode != 0)
{
Console.Error.WriteLine("git failed: " + err.Trim());
return null;
}
return outp;
}
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to run git: " + ex.Message);
return null;
}
}
static string RunGitQuiet(string args, string repoPath = null)
{
return RunGit(args, repoPath);
}
static string EscapePathForGitShow(string path)
{
return path.Replace('\\', '/');
}
static List<(int Start, int End)> ParseAddedLineRanges(string diff)
{
var list = new List<(int Start, int End)>();
var lines = diff.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var regex = new Regex(@"^\@\@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@");
foreach (var l in lines)
{
var m = regex.Match(l);
if (m.Success)
{
int start = int.Parse(m.Groups[1].Value);
int count = 1;
if (m.Groups[2].Success) count = int.Parse(m.Groups[2].Value);
int end = start + Math.Max(0, count - 1);
list.Add((start, end));
}
}
return list;
}
static SyntaxNode FindEnclosingMethodNode(SyntaxNode node)
{
if (node == null) return null;
SyntaxNode n = node;
while (n != null &&
!(n is BaseMethodDeclarationSyntax) &&
!(n is LocalFunctionStatementSyntax))
{
n = n.Parent;
}
return n;
}
static string ExtractMemberSource(SyntaxNode methodNode)
{
if (methodNode == null) return null;
return methodNode.ToFullString();
}
static string GetEnclosingMemberFullName(SyntaxNode node)
{
if (node == null) return null;
string memberName = null;
if (node is MethodDeclarationSyntax md) memberName = md.Identifier.Text;
else if (node is ConstructorDeclarationSyntax cd) memberName = cd.Identifier.Text;
else if (node is DestructorDeclarationSyntax dd) memberName = dd.Identifier.Text;
else if (node is LocalFunctionStatementSyntax lf) memberName = lf.Identifier.Text;
else memberName = "<unknown>";
var typeNames = new List<string>();
var n = node;
while (n != null)
{
if (n is TypeDeclarationSyntax tds)
{
typeNames.Insert(0, tds.Identifier.Text);
}
n = n.Parent;
}
string ns = null;
var p = node;
while (p != null && !(p is NamespaceDeclarationSyntax))
p = p.Parent;
if (p is NamespaceDeclarationSyntax nds)
{
ns = nds.Name.ToString();
}
else
{
var root = node.SyntaxTree.GetRoot();
var fileScoped = root.DescendantNodes().OfType<FileScopedNamespaceDeclarationSyntax>().FirstOrDefault();
if (fileScoped != null) ns = fileScoped.Name.ToString();
}
var parts = new List<string>();
if (!string.IsNullOrEmpty(ns)) parts.Add(ns);
if (typeNames.Count > 0) parts.Add(string.Join(".", typeNames));
if (!string.IsNullOrEmpty(memberName)) parts.Add(memberName);
return parts.Count == 0 ? null : string.Join(".", parts);
}
}
// ...existing code...
ビルドして出来上がったexeをa.exeとすると、
c#を含むリポジトリのtop階層にa.exeをおいて、
a.exe d50bc98bbb0268c3679b0ebd8f5a3b4691eb44e6 cddf9746145a626e906a2832800e6a5bb15a60f8 C:\git\FileVerUpTool
などとすると、そのコミット間の変化点が属するメソッドを抜き出せる。
※第一引数が変更前コミット、第二が変更後。第三引数は、対象のリポジトリの先頭パス。
差分のあるファイル名と変更メソッド名をcsv形式で列挙するようにした
a.exeの呼び方は同じ
// ...existing code...
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
class DiffMethodMapper
{
static int Main(string[] args)
{
if (args.Length < 2)
{
Console.Error.WriteLine("Usage: DiffMethodMapper <commitA> <commitB> [repoPath]");
return 2;
}
string commitA = args[0];
string commitB = args[1];
string repoPath = args.Length >= 3 ? args[2] : null;
var filesTxt = RunGit($"diff --name-only --no-color {commitA} {commitB} -- \"*.cs\"", repoPath);
if (filesTxt == null) return 1;
var files = filesTxt.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
.Select(f => f.Trim())
.Where(f => f.Length > 0).ToArray();
if (files.Length == 0)
{
// 出力はCSVのみにするため何も出さず正常終了
return 0;
}
foreach (var file in files)
{
var diff = RunGit($"diff --no-color -U0 {commitA} {commitB} -- \"{file}\"", repoPath);
if (diff == null) continue;
var ranges = ParseAddedLineRanges(diff);
if (ranges.Count == 0) continue;
var fileContent = RunGitQuiet($"show {commitB}:{EscapePathForGitShow(file)}", repoPath);
if (fileContent == null) continue;
var tree = CSharpSyntaxTree.ParseText(fileContent);
var root = tree.GetRoot();
var text = tree.GetText();
var reported = new HashSet<string>(StringComparer.Ordinal);
foreach (var r in ranges)
{
for (int ln = r.Start; ln <= r.End; ln++)
{
if (ln < 1 || ln > text.Lines.Count) continue;
var pos = text.Lines[ln - 1].Start;
var token = root.FindToken(pos);
var node = token.Parent;
var methodNode = FindEnclosingMethodNode(node);
if (methodNode == null) continue;
var memberFullName = GetEnclosingMemberFullName(methodNode);
if (string.IsNullOrEmpty(memberFullName)) memberFullName = $"<method at line {ln}>";
// ユニークキーはファイルパス + メンバ名
var key = file.Replace('\\', '/') + "|" + memberFullName;
if (reported.Add(key))
{
// CSV形式: path (forward-slash), comma-space, fully-qualified-member-name
Console.WriteLine($"{file.Replace('\\', '/')}, {memberFullName}");
//Console.WriteLine("copilotへの指示文言をクリップボードにコピーしました。");
Console.ReadLine();
}
}
}
}
return 0;
}
static string RunGit(string args, string repoPath = null)
{
try
{
var psi = new ProcessStartInfo("git", args)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = System.Text.Encoding.UTF8,
StandardErrorEncoding = System.Text.Encoding.UTF8
};
if (!string.IsNullOrEmpty(repoPath))
{
psi.WorkingDirectory = repoPath;
}
using (var p = Process.Start(psi))
{
string outp = p.StandardOutput.ReadToEnd();
string err = p.StandardError.ReadToEnd();
p.WaitForExit();
if (p.ExitCode != 0)
{
Console.Error.WriteLine("git failed: " + err.Trim());
return null;
}
return outp;
}
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to run git: " + ex.Message);
return null;
}
}
static string RunGitQuiet(string args, string repoPath = null)
{
return RunGit(args, repoPath);
}
static string EscapePathForGitShow(string path)
{
return path.Replace('\\', '/');
}
static List<(int Start, int End)> ParseAddedLineRanges(string diff)
{
var list = new List<(int Start, int End)>();
var lines = diff.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var regex = new Regex(@"^\@\@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@");
foreach (var l in lines)
{
var m = regex.Match(l);
if (m.Success)
{
int start = int.Parse(m.Groups[1].Value);
int count = 1;
if (m.Groups[2].Success) count = int.Parse(m.Groups[2].Value);
int end = start + Math.Max(0, count - 1);
list.Add((start, end));
}
}
return list;
}
static SyntaxNode FindEnclosingMethodNode(SyntaxNode node)
{
if (node == null) return null;
SyntaxNode n = node;
while (n != null &&
!(n is BaseMethodDeclarationSyntax) &&
!(n is LocalFunctionStatementSyntax) &&
!(n is AccessorDeclarationSyntax)) // プロパティ/インデクサの get/set もメソッド扱いする
{
n = n.Parent;
}
return n;
}
static string ExtractMemberSource(SyntaxNode methodNode)
{
if (methodNode == null) return null;
return methodNode.ToFullString();
}
static string GetEnclosingMemberFullName(SyntaxNode node)
{
if (node == null) return null;
string memberName = null;
if (node is MethodDeclarationSyntax md) memberName = md.Identifier.Text;
else if (node is ConstructorDeclarationSyntax cd) memberName = cd.Identifier.Text;
else if (node is DestructorDeclarationSyntax dd) memberName = dd.Identifier.Text;
else if (node is LocalFunctionStatementSyntax lf) memberName = lf.Identifier.Text;
else if (node is AccessorDeclarationSyntax ad)
{
// プロパティ/イベントの accessor は親のプロパティ名を付加
var parentProp = ad.Parent?.Parent as BasePropertyDeclarationSyntax;
var propName = parentProp is PropertyDeclarationSyntax pd ? pd.Identifier.Text :
parentProp is IndexerDeclarationSyntax ? "this" : "<prop>";
memberName = propName + "." + ad.Keyword.Text; // e.g. MyProp.get
}
else memberName = "<unknown>";
var typeNames = new List<string>();
var n = node;
while (n != null)
{
if (n is TypeDeclarationSyntax tds)
{
typeNames.Insert(0, tds.Identifier.Text);
}
n = n.Parent;
}
string ns = null;
var p = node;
while (p != null && !(p is NamespaceDeclarationSyntax))
p = p.Parent;
if (p is NamespaceDeclarationSyntax nds)
{
ns = nds.Name.ToString();
}
else
{
var root = node.SyntaxTree.GetRoot();
var fileScoped = root.DescendantNodes().OfType<FileScopedNamespaceDeclarationSyntax>().FirstOrDefault();
if (fileScoped != null) ns = fileScoped.Name.ToString();
}
var parts = new List<string>();
if (!string.IsNullOrEmpty(ns)) parts.Add(ns);
if (typeNames.Count > 0) parts.Add(string.Join(".", typeNames));
if (!string.IsNullOrEmpty(memberName)) parts.Add(memberName);
return parts.Count == 0 ? null : string.Join(".", parts);
}
}
// ...existing code...
変更のあったメソッドをAIにレビューしてもらうための指示文言をクリップボードにコピーする
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Windows.Forms;
class DiffMethodMapper
{
[STAThread]
static int Main(string[] args)
{
if (args.Length < 2)
{
Console.Error.WriteLine("Usage: DiffMethodMapper <commitA> <commitB> [repoPath]");
return 2;
}
string commitA = args[0];
string commitB = args[1];
string repoPath = args.Length >= 3 ? args[2] : null;
var filesTxt = RunGit($"diff --name-only --no-color {commitA} {commitB} -- \"*.cs\"", repoPath);
if (filesTxt == null) return 1;
var files = filesTxt.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries)
.Select(f => f.Trim())
.Where(f => f.Length > 0).ToArray();
if (files.Length == 0)
{
// 出力はCSVのみにするため何も出さず正常終了
return 0;
}
foreach (var file in files)
{
// ファイル全文を読み込む処理を追加
string fileFullContent = null;
try
{
fileFullContent = File.ReadAllText(file, System.Text.Encoding.UTF8);
}
catch (Exception ex)
{
Console.Error.WriteLine($"ファイルの読み込みに失敗しました: {file} - {ex.Message}");
continue;
}
var diff = RunGit($"diff --no-color -U0 {commitA} {commitB} -- \"{file}\"", repoPath);
if (diff == null) continue;
var ranges = ParseAddedLineRanges(diff);
if (ranges.Count == 0) continue;
var fileContent = RunGitQuiet($"show {commitB}:{EscapePathForGitShow(file)}", repoPath);
if (fileContent == null) continue;
var tree = CSharpSyntaxTree.ParseText(fileContent);
var root = tree.GetRoot();
var text = tree.GetText();
var reported = new HashSet<string>(StringComparer.Ordinal);
foreach (var r in ranges)
{
for (int ln = r.Start; ln <= r.End; ln++)
{
if (ln < 1 || ln > text.Lines.Count) continue;
var pos = text.Lines[ln - 1].Start;
var token = root.FindToken(pos);
var node = token.Parent;
var methodNode = FindEnclosingMethodNode(node);
if (methodNode == null) continue;
var memberFullName = GetEnclosingMemberFullName(methodNode);
if (string.IsNullOrEmpty(memberFullName)) memberFullName = $"<method at line {ln}>";
// ユニークキーはファイルパス + メンバ名
var key = file.Replace('\\', '/') + "|" + memberFullName;
if (reported.Add(key))
{
// CSV形式: path (forward-slash), comma-space, fully-qualified-member-name
Console.WriteLine($"{file.Replace('\\', '/')}, {memberFullName}");
var orderString = "";
orderString += $"以下のコードの「{memberFullName}」メソッドをレビューしてください。";
orderString += "レビュー結果には、レビューしたメソッド名を明記してください。";
orderString += "";
orderString += fileFullContent;
// クリップボードにコピー
try
{
Clipboard.SetText(orderString);
Console.WriteLine(" orderStringをクリップボードにコピーしました。");
Console.WriteLine(" AIチャットクライアントに貼り付けてレビュー依頼してください。");
Console.WriteLine(" レビューが終わったら、なにかキーを押してください。");
}
catch (Exception ex)
{
Console.Error.WriteLine("クリップボードへのコピーに失敗しました: " + ex.Message);
}
//Console.WriteLine("copilotへの指示文言をクリップボードにコピーしました。");
Console.ReadLine();
}
}
}
}
return 0;
}
static string RunGit(string args, string repoPath = null)
{
try
{
var psi = new ProcessStartInfo("git", args)
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardOutputEncoding = System.Text.Encoding.UTF8,
StandardErrorEncoding = System.Text.Encoding.UTF8
};
if (!string.IsNullOrEmpty(repoPath))
{
psi.WorkingDirectory = repoPath;
}
using (var p = Process.Start(psi))
{
string outp = p.StandardOutput.ReadToEnd();
string err = p.StandardError.ReadToEnd();
p.WaitForExit();
if (p.ExitCode != 0)
{
Console.Error.WriteLine("git failed: " + err.Trim());
return null;
}
return outp;
}
}
catch (Exception ex)
{
Console.Error.WriteLine("Failed to run git: " + ex.Message);
return null;
}
}
static string RunGitQuiet(string args, string repoPath = null)
{
return RunGit(args, repoPath);
}
static string EscapePathForGitShow(string path)
{
return path.Replace('\\', '/');
}
static List<(int Start, int End)> ParseAddedLineRanges(string diff)
{
var list = new List<(int Start, int End)>();
var lines = diff.Split(new[] { '\n', '\r' }, StringSplitOptions.RemoveEmptyEntries);
var regex = new Regex(@"^\@\@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@");
foreach (var l in lines)
{
var m = regex.Match(l);
if (m.Success)
{
int start = int.Parse(m.Groups[1].Value);
int count = 1;
if (m.Groups[2].Success) count = int.Parse(m.Groups[2].Value);
int end = start + Math.Max(0, count - 1);
list.Add((start, end));
}
}
return list;
}
static SyntaxNode FindEnclosingMethodNode(SyntaxNode node)
{
if (node == null) return null;
SyntaxNode n = node;
while (n != null &&
!(n is BaseMethodDeclarationSyntax) &&
!(n is LocalFunctionStatementSyntax) &&
!(n is AccessorDeclarationSyntax)) // プロパティ/インデクサの get/set もメソッド扱いする
{
n = n.Parent;
}
return n;
}
static string ExtractMemberSource(SyntaxNode methodNode)
{
if (methodNode == null) return null;
return methodNode.ToFullString();
}
static string GetEnclosingMemberFullName(SyntaxNode node)
{
if (node == null) return null;
string memberName = null;
if (node is MethodDeclarationSyntax md) memberName = md.Identifier.Text;
else if (node is ConstructorDeclarationSyntax cd) memberName = cd.Identifier.Text;
else if (node is DestructorDeclarationSyntax dd) memberName = dd.Identifier.Text;
else if (node is LocalFunctionStatementSyntax lf) memberName = lf.Identifier.Text;
else if (node is AccessorDeclarationSyntax ad)
{
// プロパティ/イベントの accessor は親のプロパティ名を付加
var parentProp = ad.Parent?.Parent as BasePropertyDeclarationSyntax;
var propName = parentProp is PropertyDeclarationSyntax pd ? pd.Identifier.Text :
parentProp is IndexerDeclarationSyntax ? "this" : "<prop>";
memberName = propName + "." + ad.Keyword.Text; // e.g. MyProp.get
}
else memberName = "<unknown>";
var typeNames = new List<string>();
var n = node;
while (n != null)
{
if (n is TypeDeclarationSyntax tds)
{
typeNames.Insert(0, tds.Identifier.Text);
}
n = n.Parent;
}
string ns = null;
var p = node;
while (p != null && !(p is NamespaceDeclarationSyntax))
p = p.Parent;
if (p is NamespaceDeclarationSyntax nds)
{
ns = nds.Name.ToString();
}
else
{
var root = node.SyntaxTree.GetRoot();
var fileScoped = root.DescendantNodes().OfType<FileScopedNamespaceDeclarationSyntax>().FirstOrDefault();
if (fileScoped != null) ns = fileScoped.Name.ToString();
}
var parts = new List<string>();
if (!string.IsNullOrEmpty(ns)) parts.Add(ns);
if (typeNames.Count > 0) parts.Add(string.Join(".", typeNames));
if (!string.IsNullOrEmpty(memberName)) parts.Add(memberName);
return parts.Count == 0 ? null : string.Join(".", parts);
}
}