409 lines
21 KiB
C#

using System.Diagnostics;
using System.Text;
using MergeHub.Services;
// MergeHub 발표 시연 재료를 만든다. 실제 업무 소스는 열지 않는다 - 전부 지어낸 코드다.
//
// dotnet run --project demo\tools\DemoMaker -- <demo 폴더> <저장소 경로>
//
// demo 폴더 아래 (저장소에 커밋해 둔다)
// plain\left, plain\right 폴더 비교
// style\Allman.cs, style\Aurora.cs C# 스타일 맞춤
// mergetool\Weld_BASE.cs · Weld_LOCAL.cs · Weld_REMOTE.cs · Weld.cs git mergetool 네 칸
// big\Big7479_left.cs · Big7479_right.cs 큰 파일
// 저장소 경로 (기본 D:\MergeHubDemo\repo, 커밋하지 않는다 - 매번 새로 만든다)
// master · feature/mine · feature/theirs. feature/mine 에 서서 feature/theirs 를 병합하면 여러 갈래 충돌이 난다.
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var cp949 = Encoding.GetEncoding(949);
var utf8Bom = new UTF8Encoding(true);
var demoDir = Path.GetFullPath(args.Length > 0 ? args[0] : Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", ".."));
var repoRoot = Path.GetFullPath(args.Length > 1 ? args[1] : @"D:\MergeHubDemo\repo");
Console.WriteLine($"demo 폴더: {demoDir}");
Console.WriteLine($"저장소: {repoRoot}");
void WriteText(string path, string text, Encoding? encoding = null)
{
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, text, encoding ?? utf8Bom);
}
string Crlf(params string[] rows) => string.Join("\r\n", rows) + "\r\n";
void ResetFolder(string dir)
{
Directory.CreateDirectory(dir);
foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories)) File.SetAttributes(f, FileAttributes.Normal);
foreach (var f in Directory.EnumerateFiles(dir)) File.Delete(f);
foreach (var d in Directory.EnumerateDirectories(dir)) Directory.Delete(d, true);
}
// ================= 1. 폴더 비교 =================
{
var left = Path.Combine(demoDir, "plain", "left");
var right = Path.Combine(demoDir, "plain", "right");
ResetFolder(left);
ResetFolder(right);
var same = Crlf("namespace Demo.Weld", "{", " public static class Units", " {", " public const double Kilo = 1000.0;", " }", "}");
WriteText(Path.Combine(left, "Same.cs"), same);
WriteText(Path.Combine(right, "Same.cs"), same);
WriteText(Path.Combine(left, "CodeDiff.cs"), Crlf("namespace Demo.Weld", "{", " public sealed class Cooling", " {", " public int WaitMs { get; set; } = 1500;", " }", "}"));
WriteText(Path.Combine(right, "CodeDiff.cs"), Crlf("namespace Demo.Weld", "{", " public sealed class Cooling", " {", " public int WaitMs { get; set; } = 1800;", " }", "}"));
WriteText(Path.Combine(left, "CommentOnly.cs"), Crlf("namespace Demo.Weld", "{", " // 예열 단계", " public sealed class Preheat", " {", " public int Temperature { get; set; } = 180; // 섭씨", " }", "}"));
WriteText(Path.Combine(right, "CommentOnly.cs"), Crlf("namespace Demo.Weld", "{", " // 예열 단계 - 소재 두께에 따라 바뀜", " public sealed class Preheat", " {", " public int Temperature { get; set; } = 180;", " }", "}"));
WriteText(Path.Combine(left, "MergeNoteOnly.cs"), Crlf("namespace Demo.Weld", "{", " public sealed class Laser", " {", " public int Power { get; set; } = 150;", " public int Focus { get; set; } = 12;", " }", "}"));
WriteText(Path.Combine(right, "MergeNoteOnly.cs"), Crlf("namespace Demo.Weld", "{", " public sealed class Laser", " {", " // [김승준] 2026-09-10 출력 통일", " public int Power { get; set; } = 150;", " public int Focus { get; set; } = 12; // [박지훈] 2026-09-11", " }", "}"));
var styleDefault = StyleSample();
WriteText(Path.Combine(left, "StyleOnly.cs"), styleDefault);
WriteText(Path.Combine(right, "StyleOnly.cs"), CSharpStyle.Format(styleDefault, CSharpBraceStyle.Aurora));
WriteText(Path.Combine(left, "LeftOnly.cs"), Crlf("namespace Demo.Weld", "{", " public sealed class OldSensor", " {", " }", "}"));
WriteText(Path.Combine(right, "RightOnly.cs"), Crlf("namespace Demo.Weld", "{", " public sealed class NewSensor", " {", " }", "}"));
WriteText(Path.Combine(left, "Views", "Recipe.xaml"), Crlf("<UserControl x:Class=\"Demo.Views.Recipe\"", " xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"", " xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">", " <Button Content=\"시작\" Width=\"80\"/>", "</UserControl>"));
WriteText(Path.Combine(right, "Views", "Recipe.xaml"), Crlf("<UserControl x:Class=\"Demo.Views.Recipe\"", " xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\"", " xmlns:x=\"http://schemas.microsoft.com/winfx/2006/xaml\">", " <Button Content=\"실행\" Width=\"96\"/>", "</UserControl>"));
WriteText(Path.Combine(left, "한글설명.cs"), Crlf("// 용접 순서", "namespace Demo.Weld", "{", " public static class 순서", " {", " public const string 첫단계 = \"예열\";", " }", "}"), cp949);
WriteText(Path.Combine(right, "한글설명.cs"), Crlf("// 용접 순서", "namespace Demo.Weld", "{", " public static class 순서", " {", " public const string 첫단계 = \"가열\";", " }", "}"), cp949);
// 이진 파일: PNG 머리를 가진 작은 그림 두 장(색만 다름)
File.WriteAllBytes(Path.Combine(left, "Logo.png"), TinyPng(0x2E, 0x7D, 0x32));
File.WriteAllBytes(Path.Combine(right, "Logo.png"), TinyPng(0xC6, 0x28, 0x28));
Console.WriteLine(" plain\\left · plain\\right 만듦");
}
// ================= 2. C# 스타일 맞춤 =================
{
var dir = Path.Combine(demoDir, "style");
ResetFolder(dir);
var sample = StyleSample();
WriteText(Path.Combine(dir, "Allman.cs"), sample);
// AURORA 스타일로 바꾸고 진짜 변경을 한 줄 넣는다 - 스타일을 맞추면 이 한 줄만 남는다.
var aurora = CSharpStyle.Format(sample, CSharpBraceStyle.Aurora)
.Replace("public int RetryCount { get; set; } = 3;", "public int RetryCount { get; set; } = 5;");
WriteText(Path.Combine(dir, "Aurora.cs"), aurora);
Console.WriteLine($" style\\Allman.cs ({sample.Split('\n').Length - 1}줄) · style\\Aurora.cs ({aurora.Split('\n').Length - 1}줄, 진짜 변경 1줄)");
}
// ================= 3. git mergetool 네 칸 =================
{
var dir = Path.Combine(demoDir, "mergetool");
ResetFolder(dir);
var rows = new List<string> { "namespace Demo.Weld", "{", " public sealed class Sequence", " {" };
for (var i = 1; i <= 30; i++) rows.Add($" public int Step{i} {{ get; set; }} = {i * 10};");
rows.AddRange(new[] { " }", "}" });
var local = new List<string>(rows);
local[13] = " public int Step10 { get; set; } = 111; // 내 쪽";
var remote = new List<string>(rows);
remote[13] = " public int Step10 { get; set; } = 222; // 상대 쪽";
remote[28] = " public int Step25 { get; set; } = 2525; // 상대만 고침";
WriteText(Path.Combine(dir, "Weld_BASE.cs"), Crlf(rows.ToArray()));
WriteText(Path.Combine(dir, "Weld_LOCAL.cs"), Crlf(local.ToArray()));
WriteText(Path.Combine(dir, "Weld_REMOTE.cs"), Crlf(remote.ToArray()));
// git 이 결과 파일에 남기는 모양: 충돌 표시(줄끝 LF)
var merged = new List<string>(local.Take(13));
merged.AddRange(new[] { "<<<<<<< HEAD", local[13], "=======", remote[13], ">>>>>>> feature/theirs" });
merged.AddRange(local.Skip(14).Take(14));
merged.Add(remote[28]);
merged.AddRange(local.Skip(29));
WriteText(Path.Combine(dir, "Weld.cs"), string.Join("\n", merged) + "\n");
Console.WriteLine(" mergetool\\Weld_BASE · LOCAL · REMOTE · Weld.cs 만듦");
}
// ================= 4. 큰 파일 =================
{
var dir = Path.Combine(demoDir, "big");
ResetFolder(dir);
var a = new StringBuilder();
var b = new StringBuilder();
for (var i = 1; i <= 7479; i++)
{
var line = $" public int Value{i} {{ get; set; }} = {i};";
a.Append(line).Append("\r\n");
b.Append(i % 997 == 0 || i >= 7470 ? $" public int Value{i} {{ get; set; }} = {i * 2}; // 바뀜" : line).Append("\r\n");
}
WriteText(Path.Combine(dir, "Big7479_left.cs"), a.ToString());
WriteText(Path.Combine(dir, "Big7479_right.cs"), b.ToString());
Console.WriteLine(" big\\Big7479_left.cs · Big7479_right.cs 만듦");
}
// ================= 5. 병합 시연 저장소 =================
MakeRepository(repoRoot);
Console.WriteLine();
Console.WriteLine("다 만들었습니다.");
return 0;
// ---------------------------------------------------------------------------
static string StyleSample()
{
// Visual Studio 기본 스타일(모든 여는 중괄호 새 줄)로 쓴 지어낸 용접 레시피 코드.
string[] rows =
{
"using System;",
"using System.Collections.Generic;",
"using System.Linq;",
"",
"namespace Demo.Weld",
"{",
" public enum WeldMode",
" {",
" Spot,",
" Seam",
" }",
"",
" public sealed class WeldRecipe",
" {",
" private readonly List<WeldStep> _steps = new List<WeldStep>();",
"",
" public string Name",
" {",
" get;",
" set;",
" }",
"",
" public int RetryCount { get; set; } = 3;",
"",
" public IReadOnlyList<WeldStep> Steps",
" {",
" get",
" {",
" return _steps;",
" }",
" }",
"",
" public WeldRecipe(string name)",
" {",
" Name = name;",
" }",
"",
" public void Add(WeldStep step)",
" {",
" if (step == null)",
" {",
" throw new ArgumentNullException(nameof(step));",
" }",
" else if (step.Power <= 0)",
" {",
" throw new ArgumentOutOfRangeException(nameof(step));",
" }",
" else",
" {",
" _steps.Add(step);",
" }",
" }",
"",
" public bool Run(Func<WeldStep, bool> fire)",
" {",
" for (var attempt = 0; attempt < RetryCount; attempt++)",
" {",
" try",
" {",
" if (_steps.All(fire))",
" {",
" return true;",
" }",
" }",
" catch (InvalidOperationException ex)",
" {",
" Log($\"재시도 {attempt + 1}: {ex.Message}\");",
" }",
" finally",
" {",
" Cool();",
" }",
" }",
"",
" return false;",
" }",
"",
" public static WeldRecipe Sample()",
" {",
" var recipe = new WeldRecipe(\"기본\")",
" {",
" RetryCount = 2",
" };",
" var steps = new[]",
" {",
" new WeldStep { Power = 150, Millis = 40 },",
" new WeldStep { Power = 180, Millis = 55 }",
" };",
" foreach (var step in steps)",
" {",
" recipe.Add(step);",
" }",
"",
" var summary = new",
" {",
" recipe.Name,",
" Count = steps.Length",
" };",
" Log(summary.ToString());",
"",
" Action done = () =>",
" {",
" Log(\"준비 끝\");",
" };",
" done();",
" return recipe;",
" }",
"",
" private static void Cool()",
" {",
" switch (DateTime.Now.Second % 2)",
" {",
" case 0:",
" Log(\"공랭\");",
" break;",
" default:",
" Log(\"수랭\");",
" break;",
" }",
" }",
"",
" private static void Log(string text)",
" {",
" }",
" }",
"",
" public sealed class WeldStep",
" {",
" public int Power { get; set; }",
" public int Millis { get; set; }",
" }",
"}",
};
return string.Join("\r\n", rows) + "\r\n";
}
static byte[] TinyPng(byte r, byte g, byte b)
{
// 1x1 PNG. 색 세 바이트만 다르다 - CRC 는 맞추지 않아도 이진 비교 시연에는 충분하다.
var bytes = new List<byte> { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
bytes.AddRange(new byte[] { 0, 0, 0, 13, 0x49, 0x48, 0x44, 0x52, 0, 0, 0, 1, 0, 0, 0, 1, 8, 2, 0, 0, 0, 0x90, 0x77, 0x53, 0xDE });
bytes.AddRange(new byte[] { 0, 0, 0, 12, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, r, g, b, 0, 0, 0, 0, 0, 0 });
bytes.AddRange(new byte[] { 0, 0, 0, 0, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82 });
return bytes.ToArray();
}
static void MakeRepository(string root)
{
var cp949 = Encoding.GetEncoding(949);
var utf8Bom = new UTF8Encoding(true);
Directory.CreateDirectory(root);
foreach (var f in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)) File.SetAttributes(f, FileAttributes.Normal);
foreach (var f in Directory.EnumerateFiles(root)) File.Delete(f);
foreach (var d in Directory.EnumerateDirectories(root)) Directory.Delete(d, true);
void Git(params string[] a)
{
var psi = new ProcessStartInfo("git") { WorkingDirectory = root, RedirectStandardOutput = true, RedirectStandardError = true };
foreach (var x in a) psi.ArgumentList.Add(x);
using var p = Process.Start(psi)!;
var err = p.StandardError.ReadToEnd();
p.StandardOutput.ReadToEnd();
p.WaitForExit();
if (p.ExitCode != 0) throw new Exception($"git {string.Join(' ', a)}: {err}");
}
string Read(string rel) => File.ReadAllText(Path.Combine(root, rel));
void Write(string rel, string text)
{
var path = Path.Combine(root, rel);
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, text, utf8Bom);
}
string ReplaceLine(string text, int index, string line)
{
var rows = text.Split("\r\n").ToList();
rows[index] = line;
return string.Join("\r\n", rows);
}
string InsertLine(string text, int index, string line)
{
var rows = text.Split("\r\n").ToList();
rows.Insert(index, line);
return string.Join("\r\n", rows);
}
string Unit(string name, int speed) =>
Crlf("using System;", "", "namespace Demo.Line", "{", $" public sealed class {name}", " {",
" // 속도 설정", $" public int Speed {{ get; set; }} = {speed};", " public int Power { get; set; } = 150;",
" public int Focus { get; set; } = 12;", "", " public void Start()", " {", " Log(\"시작\");", " }",
"", " public void Stop()", " {", " Log(\"정지\");", " }", "",
" private static void Log(string text) { }", " }", "}");
static string Crlf(params string[] rows) => string.Join("\r\n", rows) + "\r\n";
Git("init", "-q", "-b", "master");
Git("config", "user.name", "시연");
Git("config", "user.email", "demo@localhost");
Git("config", "core.autocrlf", "false");
var names = new[] { "Conveyor", "Gripper", "Laser", "Camera", "Cooling", "Heater", "Clamp", "Feeder", "Robot", "Sensor" };
foreach (var (n, i) in names.Select((n, i) => (n, i))) Write($"Line/{n}.cs", Unit(n, 100 + i));
Write("Line/Style.cs", StyleSample());
Write("Line/ToBeDeleted.cs", Unit("OldStation", 50));
Write("Views/Main.xaml", Crlf("<UserControl x:Class=\"Demo.Views.Main\"", " xmlns=\"http://schemas.microsoft.com/winfx/2006/xaml/presentation\">", " <StackPanel>", " <Button Content=\"시작\"", " Width=\"80\"/>", " </StackPanel>", "</UserControl>"));
File.WriteAllText(Path.Combine(root, "Line/한글설명.cs"), Crlf("// 용접 순서", "namespace Demo.Line", "{", " public static class 순서", " {", " public const string 둘째단계 = \"용접\";", " }", "}"), cp949);
Git("add", "-A");
Git("commit", "-q", "-m", "바탕");
// ---- 내 쪽 ----
Git("checkout", "-q", "-b", "feature/mine");
Write("Line/Conveyor.cs", ReplaceLine(Read("Line/Conveyor.cs"), 7, " public int Speed { get; set; } = 120; // 내 쪽")); // 같은 줄 충돌
Write("Line/Gripper.cs", ReplaceLine(Read("Line/Gripper.cs"), 8, " public int Power { get; set; } = 160;")); // 자동 병합(다른 자리)
Write("Line/Laser.cs", InsertLine(Read("Line/Laser.cs"), 8, " // [김승준] 2026-09-10")); // 머지 주석만 다름(줄 위)
Write("Line/Camera.cs", ReplaceLine(Read("Line/Camera.cs"), 9, " public int Focus { get; set; } = 12; // [김승준] 2026-09-10")); // 머지 주석만 다름(줄 끝)
Write("Line/ToBeDeleted.cs", ReplaceLine(Read("Line/ToBeDeleted.cs"), 7, " public int Speed { get; set; } = 55;")); // 내가 고침 · 상대가 지움
Write("Line/NewPanel.cs", Crlf("namespace Demo.Line", "{", " public sealed class NewPanel { public int Mine = 1; }", "}")); // 양쪽 새 파일
Write("Views/Main.xaml", Read("Views/Main.xaml").Replace("Content=\"시작\"", "Content=\"가동\"")); // XAML 충돌
File.WriteAllText(Path.Combine(root, "Line/한글설명.cs"), File.ReadAllText(Path.Combine(root, "Line/한글설명.cs"), cp949).Replace("\"용접\"", "\"본용접\""), cp949); // CP949 충돌
Write("Line/Style.cs", CSharpStyle.Format(StyleSample(), CSharpBraceStyle.Aurora).Replace("RetryCount { get; set; } = 3;", "RetryCount { get; set; } = 4;")); // 스타일 다름 + 값
Git("add", "-A");
Git("commit", "-q", "-m", "내 쪽 작업");
// ---- 상대 쪽 ----
Git("checkout", "-q", "master");
Git("checkout", "-q", "-b", "feature/theirs");
Write("Line/Conveyor.cs", ReplaceLine(Read("Line/Conveyor.cs"), 7, " public int Speed { get; set; } = 140; // 상대 쪽"));
// 내 쪽(8줄)과 떨어진 자리를 고친다 - 맞붙은 줄을 고치면 git 은 자동 병합하지 않고 충돌로 본다.
Write("Line/Gripper.cs", ReplaceLine(Read("Line/Gripper.cs"), 18, " Log(\"정지 - 상대 쪽\");"));
Write("Line/Laser.cs", InsertLine(Read("Line/Laser.cs"), 8, " // [박지훈] 2026-09-11"));
Write("Line/Camera.cs", ReplaceLine(Read("Line/Camera.cs"), 9, " public int Focus { get; set; } = 12; // [박지훈] 2026-09-11"));
File.Delete(Path.Combine(root, "Line/ToBeDeleted.cs"));
Write("Line/NewPanel.cs", Crlf("namespace Demo.Line", "{", " public sealed class NewPanel { public int Theirs = 2; }", "}"));
Write("Views/Main.xaml", Read("Views/Main.xaml").Replace("Content=\"시작\"", "Content=\"운전\""));
File.WriteAllText(Path.Combine(root, "Line/한글설명.cs"), File.ReadAllText(Path.Combine(root, "Line/한글설명.cs"), cp949).Replace("\"용접\"", "\"가접\""), cp949);
Write("Line/Style.cs", StyleSample().Replace("public int RetryCount { get; set; } = 3;", "public int RetryCount { get; set; } = 6;"));
Write("Line/Heater.cs", ReplaceLine(Read("Line/Heater.cs"), 7, " public int Speed { get; set; } = 105; // 상대만 고침"));
Git("add", "-A");
Git("commit", "-q", "-m", "상대 쪽 작업");
Git("checkout", "-q", "feature/mine");
Console.WriteLine($" 저장소 {root} 만듦 - feature/mine 에서 feature/theirs 를 병합하세요");
}