C# 코드 스타일 맞춤(AURORA · Default)과 발표 시연 재료 demo 폴더
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
eb43385e14
commit
6bd2bb7619
49
demo/README.md
Normal file
49
demo/README.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# MergeHub 시연 재료
|
||||||
|
|
||||||
|
발표 시연에 쓰는 파일과 저장소다. **실제 업무 소스는 쓰지 않는다** - 전부 지어낸 코드다.
|
||||||
|
|
||||||
|
## 새로 만들기
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File demo\make-demo.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
- 아래 `plain` · `style` · `mergetool` · `big` 을 지우고 다시 만든다.
|
||||||
|
- 병합 시연 저장소 `D:\MergeHubDemo\repo` 를 지우고 다시 만든다(`-RepoRoot` 로 바꿀 수 있다). 시연 한 번 할 때마다 다시 만들면 된다.
|
||||||
|
- 만드는 코드는 `tools\DemoMaker` 에 있다. C# 스타일 파일은 MergeHub 의 정리기(`src\MergeHub\Services\CSharpStyle.cs`)로 만든다.
|
||||||
|
|
||||||
|
## 들어 있는 것
|
||||||
|
|
||||||
|
| 폴더 | 시연 | 볼 것 |
|
||||||
|
|---|---|---|
|
||||||
|
| `plain\left` · `plain\right` | 폴더 비교 | 같음 `Same.cs` · 다름 `CodeDiff.cs` · 주석만 다름 `CommentOnly.cs` · 머지 주석만 다름 `MergeNoteOnly.cs` · C# 스타일만 다름 `StyleOnly.cs` · 한쪽만 `LeftOnly.cs` / `RightOnly.cs` · XAML `Views\Recipe.xaml` · CP949 한글 `한글설명.cs` · 이진 `Logo.png` |
|
||||||
|
| `style\Default.cs` · `style\Aurora.cs` | C# 스타일 맞춤 | 스타일 끔 **남은 차이 39줄** → AURORA 또는 Default **1줄**(`RetryCount` 3 → 5, 진짜 변경) |
|
||||||
|
| `mergetool\` | `git mergetool` 네 칸 | BASE · LOCAL · REMOTE 세 벌, 결과 `Weld.cs` 는 git 모양 충돌 표시. 충돌 1 · 상대만 고친 줄 1 |
|
||||||
|
| `big\` | 큰 파일 | 7,479줄. 끝까지 줄 번호 · "마지막 차이" 로 멀리 건너뛰기 |
|
||||||
|
| `D:\MergeHubDemo\repo` | 병합 | 아래 |
|
||||||
|
|
||||||
|
mergetool 네 칸으로 열기:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& "D:\Sorce\MergeTool\MergeHub\src\MergeHub\bin\Debug\net48\UbisamBase.Shell.exe" --merge `
|
||||||
|
--base demo\mergetool\Weld_BASE.cs --local demo\mergetool\Weld_LOCAL.cs `
|
||||||
|
--remote demo\mergetool\Weld_REMOTE.cs --merged demo\mergetool\Weld.cs
|
||||||
|
```
|
||||||
|
|
||||||
|
## 병합 시연 저장소
|
||||||
|
|
||||||
|
`feature/mine` 에 서 있다. MergeHub Git 탭에서 `feature/theirs` 를 골라 [Merge → 현재 브랜치로].
|
||||||
|
|
||||||
|
| 파일 | 경우 | MergeHub 에서 |
|
||||||
|
|---|---|---|
|
||||||
|
| `Line/Conveyor.cs` | 같은 줄을 양쪽이 다르게 고침 | 충돌 → 네 칸에서 줄 단위로 |
|
||||||
|
| `Line/Laser.cs` | 코드 줄 위에 서로 다른 머지 주석 줄 | **자동으로 풀림**, 두 주석이 다 남음 |
|
||||||
|
| `Line/Camera.cs` | 코드 줄 끝에 서로 다른 머지 주석 | **자동으로 풀림**, 한 줄 끝에 두 주석 |
|
||||||
|
| `Line/Style.cs` | 내 쪽 AURORA + 값 4, 상대 Default + 값 6 | 충돌. C# AURORA 를 고르면 남은 차이 39줄 → 1줄 |
|
||||||
|
| `Line/NewPanel.cs` | 양쪽이 같은 이름으로 새 파일 | 충돌(BASE 없음) |
|
||||||
|
| `Line/ToBeDeleted.cs` | 내가 고침 · 상대가 지움 | [파일 남기기] / [파일 지우기] |
|
||||||
|
| `Line/한글설명.cs` | CP949 한글 파일에서 같은 줄 | 충돌, 저장해도 CP949 유지 |
|
||||||
|
| `Views/Main.xaml` | XAML 여러 줄 태그 안 | 충돌 |
|
||||||
|
| `Line/Gripper.cs` · `Line/Heater.cs` | 서로 다른 자리 · 상대만 고침 | 자동 병합 |
|
||||||
|
|
||||||
|
git 으로만 병합하면 충돌 8개, MergeHub 로 병합하면 머지 주석만 다른 2개를 스스로 풀어 **6개**가 남는다.
|
||||||
7479
demo/big/Big7479_left.cs
Normal file
7479
demo/big/Big7479_left.cs
Normal file
File diff suppressed because it is too large
Load Diff
7479
demo/big/Big7479_right.cs
Normal file
7479
demo/big/Big7479_right.cs
Normal file
File diff suppressed because it is too large
Load Diff
20
demo/make-demo.ps1
Normal file
20
demo/make-demo.ps1
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# MergeHub 발표 시연 재료를 새로 만든다. 실제 업무 소스는 쓰지 않는다.
|
||||||
|
#
|
||||||
|
# powershell -ExecutionPolicy Bypass -File demo\make-demo.ps1
|
||||||
|
# powershell -ExecutionPolicy Bypass -File demo\make-demo.ps1 -RepoRoot E:\시연\repo
|
||||||
|
#
|
||||||
|
# 만드는 것
|
||||||
|
# demo\plain\left · right 폴더 비교
|
||||||
|
# demo\style\Default.cs · Aurora.cs C# 스타일 맞춤
|
||||||
|
# demo\mergetool\... git mergetool 네 칸
|
||||||
|
# demo\big\... 7,479줄 큰 파일
|
||||||
|
# -RepoRoot (기본 D:\MergeHubDemo\repo) 병합 시연 저장소 - 매번 지우고 새로 만든다
|
||||||
|
param(
|
||||||
|
[string]$RepoRoot = "D:\MergeHubDemo\repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$project = Join-Path $PSScriptRoot "tools\DemoMaker\DemoMaker.csproj"
|
||||||
|
|
||||||
|
dotnet run --project $project -- $PSScriptRoot $RepoRoot
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "시연 재료를 만들지 못했습니다 (종료 코드 $LASTEXITCODE)" }
|
||||||
40
demo/mergetool/Weld.cs
Normal file
40
demo/mergetool/Weld.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public sealed class Sequence
|
||||||
|
{
|
||||||
|
public int Step1 { get; set; } = 10;
|
||||||
|
public int Step2 { get; set; } = 20;
|
||||||
|
public int Step3 { get; set; } = 30;
|
||||||
|
public int Step4 { get; set; } = 40;
|
||||||
|
public int Step5 { get; set; } = 50;
|
||||||
|
public int Step6 { get; set; } = 60;
|
||||||
|
public int Step7 { get; set; } = 70;
|
||||||
|
public int Step8 { get; set; } = 80;
|
||||||
|
public int Step9 { get; set; } = 90;
|
||||||
|
<<<<<<< HEAD
|
||||||
|
public int Step10 { get; set; } = 111; // 내 쪽
|
||||||
|
=======
|
||||||
|
public int Step10 { get; set; } = 222; // 상대 쪽
|
||||||
|
>>>>>>> feature/theirs
|
||||||
|
public int Step11 { get; set; } = 110;
|
||||||
|
public int Step12 { get; set; } = 120;
|
||||||
|
public int Step13 { get; set; } = 130;
|
||||||
|
public int Step14 { get; set; } = 140;
|
||||||
|
public int Step15 { get; set; } = 150;
|
||||||
|
public int Step16 { get; set; } = 160;
|
||||||
|
public int Step17 { get; set; } = 170;
|
||||||
|
public int Step18 { get; set; } = 180;
|
||||||
|
public int Step19 { get; set; } = 190;
|
||||||
|
public int Step20 { get; set; } = 200;
|
||||||
|
public int Step21 { get; set; } = 210;
|
||||||
|
public int Step22 { get; set; } = 220;
|
||||||
|
public int Step23 { get; set; } = 230;
|
||||||
|
public int Step24 { get; set; } = 240;
|
||||||
|
public int Step25 { get; set; } = 2525; // 상대만 고침
|
||||||
|
public int Step26 { get; set; } = 260;
|
||||||
|
public int Step27 { get; set; } = 270;
|
||||||
|
public int Step28 { get; set; } = 280;
|
||||||
|
public int Step29 { get; set; } = 290;
|
||||||
|
public int Step30 { get; set; } = 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
demo/mergetool/Weld_BASE.cs
Normal file
36
demo/mergetool/Weld_BASE.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public sealed class Sequence
|
||||||
|
{
|
||||||
|
public int Step1 { get; set; } = 10;
|
||||||
|
public int Step2 { get; set; } = 20;
|
||||||
|
public int Step3 { get; set; } = 30;
|
||||||
|
public int Step4 { get; set; } = 40;
|
||||||
|
public int Step5 { get; set; } = 50;
|
||||||
|
public int Step6 { get; set; } = 60;
|
||||||
|
public int Step7 { get; set; } = 70;
|
||||||
|
public int Step8 { get; set; } = 80;
|
||||||
|
public int Step9 { get; set; } = 90;
|
||||||
|
public int Step10 { get; set; } = 100;
|
||||||
|
public int Step11 { get; set; } = 110;
|
||||||
|
public int Step12 { get; set; } = 120;
|
||||||
|
public int Step13 { get; set; } = 130;
|
||||||
|
public int Step14 { get; set; } = 140;
|
||||||
|
public int Step15 { get; set; } = 150;
|
||||||
|
public int Step16 { get; set; } = 160;
|
||||||
|
public int Step17 { get; set; } = 170;
|
||||||
|
public int Step18 { get; set; } = 180;
|
||||||
|
public int Step19 { get; set; } = 190;
|
||||||
|
public int Step20 { get; set; } = 200;
|
||||||
|
public int Step21 { get; set; } = 210;
|
||||||
|
public int Step22 { get; set; } = 220;
|
||||||
|
public int Step23 { get; set; } = 230;
|
||||||
|
public int Step24 { get; set; } = 240;
|
||||||
|
public int Step25 { get; set; } = 250;
|
||||||
|
public int Step26 { get; set; } = 260;
|
||||||
|
public int Step27 { get; set; } = 270;
|
||||||
|
public int Step28 { get; set; } = 280;
|
||||||
|
public int Step29 { get; set; } = 290;
|
||||||
|
public int Step30 { get; set; } = 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
demo/mergetool/Weld_LOCAL.cs
Normal file
36
demo/mergetool/Weld_LOCAL.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public sealed class Sequence
|
||||||
|
{
|
||||||
|
public int Step1 { get; set; } = 10;
|
||||||
|
public int Step2 { get; set; } = 20;
|
||||||
|
public int Step3 { get; set; } = 30;
|
||||||
|
public int Step4 { get; set; } = 40;
|
||||||
|
public int Step5 { get; set; } = 50;
|
||||||
|
public int Step6 { get; set; } = 60;
|
||||||
|
public int Step7 { get; set; } = 70;
|
||||||
|
public int Step8 { get; set; } = 80;
|
||||||
|
public int Step9 { get; set; } = 90;
|
||||||
|
public int Step10 { get; set; } = 111; // 내 쪽
|
||||||
|
public int Step11 { get; set; } = 110;
|
||||||
|
public int Step12 { get; set; } = 120;
|
||||||
|
public int Step13 { get; set; } = 130;
|
||||||
|
public int Step14 { get; set; } = 140;
|
||||||
|
public int Step15 { get; set; } = 150;
|
||||||
|
public int Step16 { get; set; } = 160;
|
||||||
|
public int Step17 { get; set; } = 170;
|
||||||
|
public int Step18 { get; set; } = 180;
|
||||||
|
public int Step19 { get; set; } = 190;
|
||||||
|
public int Step20 { get; set; } = 200;
|
||||||
|
public int Step21 { get; set; } = 210;
|
||||||
|
public int Step22 { get; set; } = 220;
|
||||||
|
public int Step23 { get; set; } = 230;
|
||||||
|
public int Step24 { get; set; } = 240;
|
||||||
|
public int Step25 { get; set; } = 250;
|
||||||
|
public int Step26 { get; set; } = 260;
|
||||||
|
public int Step27 { get; set; } = 270;
|
||||||
|
public int Step28 { get; set; } = 280;
|
||||||
|
public int Step29 { get; set; } = 290;
|
||||||
|
public int Step30 { get; set; } = 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
demo/mergetool/Weld_REMOTE.cs
Normal file
36
demo/mergetool/Weld_REMOTE.cs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public sealed class Sequence
|
||||||
|
{
|
||||||
|
public int Step1 { get; set; } = 10;
|
||||||
|
public int Step2 { get; set; } = 20;
|
||||||
|
public int Step3 { get; set; } = 30;
|
||||||
|
public int Step4 { get; set; } = 40;
|
||||||
|
public int Step5 { get; set; } = 50;
|
||||||
|
public int Step6 { get; set; } = 60;
|
||||||
|
public int Step7 { get; set; } = 70;
|
||||||
|
public int Step8 { get; set; } = 80;
|
||||||
|
public int Step9 { get; set; } = 90;
|
||||||
|
public int Step10 { get; set; } = 222; // 상대 쪽
|
||||||
|
public int Step11 { get; set; } = 110;
|
||||||
|
public int Step12 { get; set; } = 120;
|
||||||
|
public int Step13 { get; set; } = 130;
|
||||||
|
public int Step14 { get; set; } = 140;
|
||||||
|
public int Step15 { get; set; } = 150;
|
||||||
|
public int Step16 { get; set; } = 160;
|
||||||
|
public int Step17 { get; set; } = 170;
|
||||||
|
public int Step18 { get; set; } = 180;
|
||||||
|
public int Step19 { get; set; } = 190;
|
||||||
|
public int Step20 { get; set; } = 200;
|
||||||
|
public int Step21 { get; set; } = 210;
|
||||||
|
public int Step22 { get; set; } = 220;
|
||||||
|
public int Step23 { get; set; } = 230;
|
||||||
|
public int Step24 { get; set; } = 240;
|
||||||
|
public int Step25 { get; set; } = 2525; // 상대만 고침
|
||||||
|
public int Step26 { get; set; } = 260;
|
||||||
|
public int Step27 { get; set; } = 270;
|
||||||
|
public int Step28 { get; set; } = 280;
|
||||||
|
public int Step29 { get; set; } = 290;
|
||||||
|
public int Step30 { get; set; } = 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
demo/plain/left/CodeDiff.cs
Normal file
7
demo/plain/left/CodeDiff.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public sealed class Cooling
|
||||||
|
{
|
||||||
|
public int WaitMs { get; set; } = 1500;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
demo/plain/left/CommentOnly.cs
Normal file
8
demo/plain/left/CommentOnly.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
// 예열 단계
|
||||||
|
public sealed class Preheat
|
||||||
|
{
|
||||||
|
public int Temperature { get; set; } = 180; // 섭씨
|
||||||
|
}
|
||||||
|
}
|
||||||
6
demo/plain/left/LeftOnly.cs
Normal file
6
demo/plain/left/LeftOnly.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public sealed class OldSensor
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
demo/plain/left/Logo.png
Normal file
BIN
demo/plain/left/Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 B |
8
demo/plain/left/MergeNoteOnly.cs
Normal file
8
demo/plain/left/MergeNoteOnly.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public sealed class Laser
|
||||||
|
{
|
||||||
|
public int Power { get; set; } = 150;
|
||||||
|
public int Focus { get; set; } = 12;
|
||||||
|
}
|
||||||
|
}
|
||||||
7
demo/plain/left/Same.cs
Normal file
7
demo/plain/left/Same.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public static class Units
|
||||||
|
{
|
||||||
|
public const double Kilo = 1000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
132
demo/plain/left/StyleOnly.cs
Normal file
132
demo/plain/left/StyleOnly.cs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
5
demo/plain/left/Views/Recipe.xaml
Normal file
5
demo/plain/left/Views/Recipe.xaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
||||||
8
demo/plain/left/한글설명.cs
Normal file
8
demo/plain/left/한글설명.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// 용접 순서
|
||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public static class 순서
|
||||||
|
{
|
||||||
|
public const string 첫단계 = "예열";
|
||||||
|
}
|
||||||
|
}
|
||||||
7
demo/plain/right/CodeDiff.cs
Normal file
7
demo/plain/right/CodeDiff.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public sealed class Cooling
|
||||||
|
{
|
||||||
|
public int WaitMs { get; set; } = 1800;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
demo/plain/right/CommentOnly.cs
Normal file
8
demo/plain/right/CommentOnly.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
// 예열 단계 - 소재 두께에 따라 바뀜
|
||||||
|
public sealed class Preheat
|
||||||
|
{
|
||||||
|
public int Temperature { get; set; } = 180;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
demo/plain/right/Logo.png
Normal file
BIN
demo/plain/right/Logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 65 B |
9
demo/plain/right/MergeNoteOnly.cs
Normal file
9
demo/plain/right/MergeNoteOnly.cs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
6
demo/plain/right/RightOnly.cs
Normal file
6
demo/plain/right/RightOnly.cs
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public sealed class NewSensor
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
7
demo/plain/right/Same.cs
Normal file
7
demo/plain/right/Same.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public static class Units
|
||||||
|
{
|
||||||
|
public const double Kilo = 1000.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
111
demo/plain/right/StyleOnly.cs
Normal file
111
demo/plain/right/StyleOnly.cs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
5
demo/plain/right/Views/Recipe.xaml
Normal file
5
demo/plain/right/Views/Recipe.xaml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<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>
|
||||||
8
demo/plain/right/한글설명.cs
Normal file
8
demo/plain/right/한글설명.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// 용접 순서
|
||||||
|
namespace Demo.Weld
|
||||||
|
{
|
||||||
|
public static class 순서
|
||||||
|
{
|
||||||
|
public const string 첫단계 = "가열";
|
||||||
|
}
|
||||||
|
}
|
||||||
111
demo/style/Aurora.cs
Normal file
111
demo/style/Aurora.cs
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
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; } = 5;
|
||||||
|
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
132
demo/style/Default.cs
Normal file
132
demo/style/Default.cs
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
19
demo/tools/DemoMaker/DemoMaker.csproj
Normal file
19
demo/tools/DemoMaker/DemoMaker.csproj
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<!-- 발표 시연 재료를 만든다. 실제 업무 소스는 쓰지 않는다 - 전부 지어낸 코드다. -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Program.cs" />
|
||||||
|
<!-- C# 스타일 파일은 MergeHub 의 정리기로 만든다. 시연에서 보이는 결과와 같은 규칙이다. -->
|
||||||
|
<Compile Include="..\..\..\src\MergeHub\Services\CSharpStyle.cs" />
|
||||||
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="8.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
408
demo/tools/DemoMaker/Program.cs
Normal file
408
demo/tools/DemoMaker/Program.cs
Normal file
@ -0,0 +1,408 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using MergeHub.Services;
|
||||||
|
|
||||||
|
// MergeHub 발표 시연 재료를 만든다. 실제 업무 소스는 열지 않는다 - 전부 지어낸 코드다.
|
||||||
|
//
|
||||||
|
// dotnet run --project demo\tools\DemoMaker -- <demo 폴더> <저장소 경로>
|
||||||
|
//
|
||||||
|
// demo 폴더 아래 (저장소에 커밋해 둔다)
|
||||||
|
// plain\left, plain\right 폴더 비교
|
||||||
|
// style\Default.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, "Default.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\\Default.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 를 병합하세요");
|
||||||
|
}
|
||||||
@ -691,3 +691,57 @@ MERGED 는 git 이 충돌 표시를 넣어 둔 파일이라, 두 칸으로는
|
|||||||
(자동 조작은 목록이 화면에 보이는 항목만 만들어 두는 탓에 처음에는 10개만 골랐다. 목록 항목을 모두 불러오게 고쳐 15개로 다시 쟀다.)
|
(자동 조작은 목록이 화면에 보이는 항목만 만들어 두는 탓에 처음에는 10개만 골랐다. 목록 항목을 모두 불러오게 고쳐 15개로 다시 쟀다.)
|
||||||
|
|
||||||
빌드 경고 0 · 오류 0.
|
빌드 경고 0 · 오류 0.
|
||||||
|
|
||||||
|
## E-1. C# 코드 스타일 맞춤 (2026-09-14, 추가 요청, 완료)
|
||||||
|
|
||||||
|
사용자 요청: Visual Studio [텍스트 편집기 > C# > 코드 스타일 > 서식 > 줄 바꿈] 옵션 그림과 같은 스타일(AURORA)이 아니면 diff 가 많이 뜬다. C# 이면 그 스타일로 자동으로 맞춰 달라. AURORA 와 Default 를 고를 수 있게.
|
||||||
|
|
||||||
|
### 그림에서 읽은 AURORA
|
||||||
|
|
||||||
|
| 옵션 | 켬 |
|
||||||
|
|---|---|
|
||||||
|
| 여는 중괄호 새 줄: 형식 · 메서드와 로컬 함수 | ✓ |
|
||||||
|
| 여는 중괄호 새 줄: 속성·인덱서·이벤트 · 접근자 · 익명 메서드 · 제어 블록 · 익명 형식 · 개체/컬렉션/배열 초기화 · 람다 | 끔(같은 줄) |
|
||||||
|
| "else" · "catch" · "finally" 새 줄 | 끔(`} else {`) |
|
||||||
|
| 개체 초기화 멤버 · 익명 형식 멤버 · 쿼리 절 새 줄 | ✓ (손대지 않는다) |
|
||||||
|
|
||||||
|
Default 는 Visual Studio 기본 - 모두 새 줄.
|
||||||
|
|
||||||
|
### 만든 것
|
||||||
|
|
||||||
|
| 파일 | 무엇 |
|
||||||
|
|---|---|
|
||||||
|
| `Services/CSharpStyle.cs` (새) | 중괄호 · else/catch/finally 의 **줄 위치만** 옮긴다. 주석 · 문자열 · 보간 문자열 · 전처리 줄은 가려서 본다. 중괄호 앞 머리를 거꾸로 읽어 갈래(형식 · 메서드 · 속성 · 접근자 · 익명 메서드 · 제어 · 익명 형식 · 초기화 · 람다)를 정하고 옵션표대로 올리거나 내린다. 앞 줄 끝에 주석이 있으면 올리지 않는다. 판단할 수 없는 블록은 그대로. 바꿀 것이 없으면 받은 글 그대로 |
|
||||||
|
| `ViewModels/HomeViewModel.cs` | 비교 규칙 칸에 [C# 스타일 끔 / C# AURORA / C# Default]. 고르면 원본부터 다시 읽어 BASE · LOCAL · REMOTE 를 맞춘다(Git 두 칸 · 네 칸 · 파일 두 개 · mergetool). 편집 칸 글이 바뀌면 저장할 것이 있는 상태, 저장하면 그 스타일로 파일에 쓴다. **스타일만 바뀐 편집은 파일을 바꿀 때 묻지 않는다**(묻게 두면 파일마다 창이 뜬다). 상태바 요약에 스타일 |
|
||||||
|
| `Services/FolderComparer.cs` · `FolderViewModel.cs` | 스타일만 다른 C# 파일은 **같음**. 줄끝이 다르면 가리지 않는다 |
|
||||||
|
| `MergeHubSettings.cs` | [비교 옵션] "C# 코드 스타일 맞춤", 기본 끔 |
|
||||||
|
|
||||||
|
검사: StyleTest(새) **78건** - 두 스타일 사이 왕복, 다시 맞춰도 그대로, CRLF, 주석 · 문자열 · 전처리 속 중괄호, 갈래 판단 36가지, 옵션표, 1,600줄대 글 속도. FolderTest 57 → **62**. 검사 모음 22개 **948건 통과**.
|
||||||
|
|
||||||
|
### 확인한 것
|
||||||
|
|
||||||
|
만든 시연 재료(`demo\style\Default.cs` 132줄, `Aurora.cs` 는 같은 코드를 AURORA 로 맞추고 값 한 줄 `RetryCount` 3 → 5).
|
||||||
|
|
||||||
|
| 확인 | 남은 차이 | 저장 |
|
||||||
|
|---|---|---|
|
||||||
|
| 스타일 끔 | **39줄** | 꺼짐 |
|
||||||
|
| C# AURORA | **1줄** (RetryCount) | 꺼짐 - 편집 칸이 이미 AURORA |
|
||||||
|
| C# Default | 1줄 | **켜짐** - 편집 칸이 Default 로 바뀜 |
|
||||||
|
| 다시 끔 | 39줄 | 꺼짐, **확인 창 안 뜸** |
|
||||||
|
|
||||||
|
시연 저장소(`D:\MergeHubDemo\repo`)를 MergeHub 로 병합 → `Line/Style.cs`(내 쪽 AURORA + 값 4, 상대 Default + 값 6) 네 칸: 끔 **남은 충돌 1 · 차이 39줄** → AURORA **충돌 1 · 차이 1줄**.
|
||||||
|
|
||||||
|
설정 화면에 저장된 값은 `"CSharpBraceStyle": 1`. 확인 뒤 설정 파일은 원래 바이트로 되돌렸다.
|
||||||
|
|
||||||
|
### 남긴 것
|
||||||
|
|
||||||
|
- 들여쓰기 · 공백 · 그 밖의 서식은 맞추지 않는다. 중괄호 줄 위치만.
|
||||||
|
- git 병합(libgit2) 자체는 스타일을 모른다. 스타일만 다른 충돌도 git 은 충돌로 내고, 네 칸에서 맞춰 보여 줄 뿐 자동으로 풀지는 않는다.
|
||||||
|
|
||||||
|
## E-2. 발표 시연 재료 (2026-09-14, 추가 요청, 완료)
|
||||||
|
|
||||||
|
`demo\make-demo.ps1` 한 번이면 `demo\plain` · `style` · `mergetool` · `big` 과 병합 저장소 `D:\MergeHubDemo\repo` 를 새로 만든다(`tools\DemoMaker`, 지어낸 코드). 내용과 쓰는 법은 `demo\README.md`.
|
||||||
|
|
||||||
|
시연 저장소를 git 으로 병합하면 충돌 8개(같은 줄 · 머지 주석 줄 위 · 줄 끝 · 스타일 · 양쪽 새 파일 · 한쪽 삭제 · CP949 · XAML), `Gripper.cs` · `Heater.cs` 는 자동 병합. MergeHub 로 병합하면 머지 주석만 다른 2개를 스스로 풀어 6개.
|
||||||
|
|
||||||
|
처음 만든 생성기는 CP949 파일을 UTF-8 로 읽어 충돌이 안 났고, `Gripper.cs` 는 맞붙은 줄을 고쳐 충돌이 났다. 둘 다 고쳐 다시 만들었다. `make-demo.ps1` 은 PowerShell 5.1 이 한글을 읽도록 BOM 을 붙였다.
|
||||||
|
|||||||
@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
| 날짜 | 항목 | 커밋 |
|
| 날짜 | 항목 | 커밋 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
|
| 2026-09-14 | **E-1 C# 코드 스타일 맞춤** — AURORA(형식 · 메서드만 새 줄, `} else {`) / Default(모두 새 줄)로 중괄호 줄바꿈을 맞춘 뒤 비교. 두 칸 · 네 칸 · mergetool · 폴더 비교, 설정. 스타일만 바뀐 편집은 파일을 바꿀 때 묻지 않음. StyleTest 78건 | 아래 |
|
||||||
|
| 2026-09-14 | **E-2 시연 재료** — `demo\make-demo.ps1` · `tools\DemoMaker`. 지어낸 코드로 폴더 비교 · 스타일 · mergetool · 큰 파일 · 병합 저장소(`D:\MergeHubDemo\repo`) | 아래 |
|
||||||
| 2026-09-14 | **6차 B-6** — 충돌 탭에서 여러 파일을 골라(Ctrl · Shift) Accept 를 한 번에. 작업 하나로 풀고 저장소 다시 읽기는 끝에 한 번. 같은 줄 충돌 15개 22.0초 → 1.2초 | 아래 |
|
| 2026-09-14 | **6차 B-6** — 충돌 탭에서 여러 파일을 골라(Ctrl · Shift) Accept 를 한 번에. 작업 하나로 풀고 저장소 다시 읽기는 끝에 한 번. 같은 줄 충돌 15개 22.0초 → 1.2초 | 아래 |
|
||||||
| 2026-09-14 | **6차 B-5** — 머지 주석을 비교에서 무시(주석 줄은 거르고 줄 끝 주석은 떼고 견줌). 주석만 다른 git 충돌은 양쪽 주석을 남겨 자동으로 풂. 정렬이 주석 줄과 코드 줄을 짝지으면 떼어 놓음. 폴더 비교도. 설정 [머지 주석] 에서 끔 | 아래 |
|
| 2026-09-14 | **6차 B-5** — 머지 주석을 비교에서 무시(주석 줄은 거르고 줄 끝 주석은 떼고 견줌). 주석만 다른 git 충돌은 양쪽 주석을 남겨 자동으로 풂. 정렬이 주석 줄과 코드 줄을 짝지으면 떼어 놓음. 폴더 비교도. 설정 [머지 주석] 에서 끔 | 아래 |
|
||||||
| 2026-09-14 | **6차 B-4 확인 중 발견 두 건** — "마지막 차이" 로 멀리 건너뛰면 표시층 보정이 화면 밖 행으로 재져 네 칸이 9줄 위로 밀리던 것을 고침. `git mergetool` 이 넘긴 세 벌을 두 칸이 아닌 네 칸으로 열고 결과를 MERGED 파일에 쓴다 | 아래 |
|
| 2026-09-14 | **6차 B-4 확인 중 발견 두 건** — "마지막 차이" 로 멀리 건너뛰면 표시층 보정이 화면 밖 행으로 재져 네 칸이 9줄 위로 밀리던 것을 고침. `git mergetool` 이 넘긴 세 벌을 두 칸이 아닌 네 칸으로 열고 결과를 MERGED 파일에 쓴다 | 아래 |
|
||||||
|
|||||||
@ -113,6 +113,14 @@
|
|||||||
| 대기 | B-7 상태바가 병합 중에 믿을 수 없는 값을 보인다 |
|
| 대기 | B-7 상태바가 병합 중에 믿을 수 없는 값을 보인다 |
|
||||||
| 대기 | B-8 병합 커밋 뒤 파일 비교 탭이 어정쩡하게 남는다 |
|
| 대기 | B-8 병합 커밋 뒤 파일 비교 탭이 어정쩡하게 남는다 |
|
||||||
|
|
||||||
|
### E. 추가 요청 (2026-09-14)
|
||||||
|
|
||||||
|
| 상태 | 항목 |
|
||||||
|
|---|---|
|
||||||
|
| **완료** | E-1 C# 코드 스타일 맞춤 (AURORA · Default) — 비교 전에 중괄호 줄바꿈을 맞춤. 두 칸 · 네 칸 · 폴더 비교, 설정. 스타일만 다른 파일 남은 차이 39줄 → 1줄 |
|
||||||
|
| **완료** | E-2 발표 시연 재료 `demo\` — `make-demo.ps1` 한 번으로 폴더 비교 · 스타일 · mergetool · 큰 파일 · 병합 저장소 |
|
||||||
|
| **완료** | E-3 웹 문서 — 사용자 매뉴얼 · 발표 자료 · 남은 일 · 개발 현황 · 기획안 최신화 · 시연 대본 · WinMerge 비교 |
|
||||||
|
|
||||||
### C. 사소한 것
|
### C. 사소한 것
|
||||||
|
|
||||||
| 상태 | 항목 |
|
| 상태 | 항목 |
|
||||||
|
|||||||
@ -67,6 +67,15 @@ namespace MergeHub
|
|||||||
[DisplayName("줄 필터 정규식 (한 줄에 하나)")]
|
[DisplayName("줄 필터 정규식 (한 줄에 하나)")]
|
||||||
public string LineFilters { get; set; } = string.Empty;
|
public string LineFilters { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// C# 파일을 비교하기 전에 중괄호 줄바꿈을 맞출 스타일(6차 추가).
|
||||||
|
/// AURORA = 형식 · 메서드 중괄호만 새 줄, 나머지와 else/catch/finally 는 같은 줄. Default = Visual Studio 기본(모두 새 줄).
|
||||||
|
/// 켜면 편집 칸의 결과도 그 스타일로 저장된다.
|
||||||
|
/// </summary>
|
||||||
|
[Category("비교 옵션")]
|
||||||
|
[DisplayName("C# 코드 스타일 맞춤 (None=끔 · Aurora · Default)")]
|
||||||
|
public MergeHub.Services.CSharpBraceStyle CSharpBraceStyle { get; set; } = MergeHub.Services.CSharpBraceStyle.None;
|
||||||
|
|
||||||
[Category("비교 옵션")]
|
[Category("비교 옵션")]
|
||||||
[DisplayName("단어 단위 diff")]
|
[DisplayName("단어 단위 diff")]
|
||||||
public bool WordLevelDiff { get; set; } = true;
|
public bool WordLevelDiff { get; set; } = true;
|
||||||
|
|||||||
542
src/MergeHub/Services/CSharpStyle.cs
Normal file
542
src/MergeHub/Services/CSharpStyle.cs
Normal file
@ -0,0 +1,542 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace MergeHub.Services
|
||||||
|
{
|
||||||
|
/// <summary>C# 여는 중괄호 · else/catch/finally 를 어디에 둘지.</summary>
|
||||||
|
public enum CSharpBraceStyle
|
||||||
|
{
|
||||||
|
/// <summary>맞추지 않는다. 파일 그대로 견준다.</summary>
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AURORA. Visual Studio [텍스트 편집기 > C# > 코드 스타일 > 서식 > 줄 바꿈] 에서
|
||||||
|
/// 형식 · 메서드와 로컬 함수의 여는 중괄호만 새 줄, 나머지(속성 · 접근자 · 익명 메서드 · 제어 블록 ·
|
||||||
|
/// 익명 형식 · 초기화 · 람다)는 같은 줄. else · catch · finally 는 같은 줄(<c>} else {</c>).
|
||||||
|
/// </summary>
|
||||||
|
Aurora = 1,
|
||||||
|
|
||||||
|
/// <summary>Visual Studio 기본. 여는 중괄호는 모두 새 줄, else · catch · finally 도 새 줄.</summary>
|
||||||
|
Default = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// C# 코드의 중괄호 줄바꿈을 한 가지 스타일로 맞춘다(6차 추가 - C# 코드 스타일 맞춤).
|
||||||
|
///
|
||||||
|
/// 같은 코드인데 한쪽은 <c>if (x) {</c>, 다른 쪽은 <c>if (x)</c> + 다음 줄 <c>{</c> 로 쓰면
|
||||||
|
/// 줄 단위 비교에서 그 자리마다 차이가 둘씩 뜬다. 스타일이 다른 사람 코드를 병합할 때 진짜 변경이 묻힌다.
|
||||||
|
/// 그래서 비교하기 전에 양쪽을 같은 스타일로 맞춘다.
|
||||||
|
///
|
||||||
|
/// <b>줄의 위치만</b> 옮긴다 - 여는 중괄호를 앞 줄 끝으로 올리거나 다음 줄로 내리고,
|
||||||
|
/// <c>}</c> 와 else/catch/finally 를 붙이거나 뗀다. 들여쓰기 · 공백 · 그 밖의 서식은 건드리지 않는다.
|
||||||
|
/// 주석 · 문자열 · 보간 문자열 · 전처리 줄 안의 중괄호는 코드로 보지 않는다.
|
||||||
|
/// 앞 줄 끝에 주석이 있으면 올리지 않는다(중괄호가 주석 안으로 들어가 버린다).
|
||||||
|
/// 스타일을 판단할 수 없는 중괄호(그냥 블록 · 중첩 배열 초기화)는 그대로 둔다.
|
||||||
|
/// </summary>
|
||||||
|
public static class CSharpStyle
|
||||||
|
{
|
||||||
|
/// <summary>중괄호가 여는 것의 갈래. Visual Studio 옵션의 항목과 같게 나눈다.</summary>
|
||||||
|
public enum BraceKind
|
||||||
|
{
|
||||||
|
/// <summary>판단하지 않는다(그냥 블록 · 알 수 없음). 옮기지 않는다.</summary>
|
||||||
|
Unknown,
|
||||||
|
Type,
|
||||||
|
Method,
|
||||||
|
Property,
|
||||||
|
Accessor,
|
||||||
|
AnonymousMethod,
|
||||||
|
Control,
|
||||||
|
AnonymousType,
|
||||||
|
Initializer,
|
||||||
|
Lambda,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>이 파일에 스타일 맞춤을 걸지. C# 파일(.cs)만.</summary>
|
||||||
|
public static bool Applies(string? path, CSharpBraceStyle style) =>
|
||||||
|
style != CSharpBraceStyle.None && !string.IsNullOrEmpty(path) &&
|
||||||
|
string.Equals(Path.GetExtension(path), ".cs", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public static string Label(CSharpBraceStyle style) => style switch
|
||||||
|
{
|
||||||
|
CSharpBraceStyle.Aurora => "AURORA",
|
||||||
|
CSharpBraceStyle.Default => "Default",
|
||||||
|
_ => "끔",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 글 전체를 스타일에 맞춘다. 바꿀 것이 없으면 <b>받은 글을 그대로</b> 돌려준다(줄끝도 그대로).
|
||||||
|
/// 바꾸면 줄끝은 글에 CRLF 가 하나라도 있으면 CRLF, 아니면 LF 로 잇는다.
|
||||||
|
/// </summary>
|
||||||
|
public static string Format(string text, CSharpBraceStyle style)
|
||||||
|
{
|
||||||
|
if (style == CSharpBraceStyle.None || string.IsNullOrEmpty(text)) return text;
|
||||||
|
|
||||||
|
var newLine = text.IndexOf("\r\n", StringComparison.Ordinal) >= 0 ? "\r\n" : "\n";
|
||||||
|
var endsWithNewLine = text.EndsWith("\n", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
var lines = new List<string>(text.Replace("\r\n", "\n").Split('\n'));
|
||||||
|
if (endsWithNewLine) lines.RemoveAt(lines.Count - 1);
|
||||||
|
|
||||||
|
var any = false;
|
||||||
|
for (var pass = 0; pass < 8; pass++)
|
||||||
|
{
|
||||||
|
var changed = PlaceKeywords(lines, style);
|
||||||
|
changed |= PlaceBraces(lines, style);
|
||||||
|
if (!changed) break;
|
||||||
|
any = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!any) return text;
|
||||||
|
|
||||||
|
var result = string.Join(newLine, lines);
|
||||||
|
return endsWithNewLine ? result + newLine : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>그 갈래의 여는 중괄호를 새 줄에 둘지.</summary>
|
||||||
|
public static bool OpenBraceOnNewLine(BraceKind kind, CSharpBraceStyle style) =>
|
||||||
|
style == CSharpBraceStyle.Default || kind is BraceKind.Type or BraceKind.Method;
|
||||||
|
|
||||||
|
// ================= else · catch · finally =================
|
||||||
|
|
||||||
|
private static readonly Regex KeywordLine = new Regex(@"^(else|catch|finally)\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex CloseThenKeyword = new Regex(@"^\}\s*(else|catch|finally)\b", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static bool PlaceKeywords(List<string> lines, CSharpBraceStyle style)
|
||||||
|
{
|
||||||
|
var mask = Mask(lines);
|
||||||
|
var changed = false;
|
||||||
|
|
||||||
|
for (var i = lines.Count - 1; i >= 1; i--)
|
||||||
|
{
|
||||||
|
if (mask.Inside[i] || mask.Preprocessor[i]) continue;
|
||||||
|
var code = mask.Code[i].Trim();
|
||||||
|
|
||||||
|
if (style == CSharpBraceStyle.Aurora)
|
||||||
|
{
|
||||||
|
// } → } else {
|
||||||
|
// else {
|
||||||
|
if (!KeywordLine.IsMatch(code)) continue;
|
||||||
|
|
||||||
|
var p = i - 1;
|
||||||
|
if (mask.Inside[p] || mask.Preprocessor[p]) continue;
|
||||||
|
if (lines[p].Trim() != "}" || mask.Code[p].Trim() != "}") continue;
|
||||||
|
|
||||||
|
lines[p] = lines[p].TrimEnd() + " " + lines[i].TrimStart();
|
||||||
|
lines.RemoveAt(i);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// } else { → }
|
||||||
|
// else {
|
||||||
|
if (!CloseThenKeyword.IsMatch(code) || !lines[i].TrimStart().StartsWith("}", StringComparison.Ordinal)) continue;
|
||||||
|
|
||||||
|
var indent = Indent(lines[i]);
|
||||||
|
var rest = lines[i].TrimStart().Substring(1).TrimStart();
|
||||||
|
lines[i] = indent + "}";
|
||||||
|
lines.Insert(i + 1, indent + rest);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ================= 여는 중괄호 =================
|
||||||
|
|
||||||
|
private static bool PlaceBraces(List<string> lines, CSharpBraceStyle style)
|
||||||
|
{
|
||||||
|
var mask = Mask(lines);
|
||||||
|
var changed = false;
|
||||||
|
|
||||||
|
for (var i = lines.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (mask.Inside[i] || mask.Preprocessor[i]) continue;
|
||||||
|
|
||||||
|
var code = mask.Code[i];
|
||||||
|
var first = FirstNonSpace(code);
|
||||||
|
if (first < 0) continue;
|
||||||
|
|
||||||
|
// (가) 줄이 '{' 로 시작한다 → 같은 줄 스타일이면 앞 줄 끝으로 올린다.
|
||||||
|
if (code[first] == '{')
|
||||||
|
{
|
||||||
|
if (i == 0) continue;
|
||||||
|
var p = i - 1;
|
||||||
|
if (mask.Inside[p] || mask.Preprocessor[p]) continue;
|
||||||
|
|
||||||
|
var prevCode = mask.Code[p].TrimEnd();
|
||||||
|
if (prevCode.Trim().Length == 0) continue;
|
||||||
|
|
||||||
|
// 앞 줄 끝에 주석이 있으면 올리지 않는다 - 원문 끝과 코드 끝이 같아야 한다.
|
||||||
|
if (lines[p].TrimEnd().Length != prevCode.Length) continue;
|
||||||
|
|
||||||
|
var (kind, _) = Classify(mask, i, first);
|
||||||
|
if (kind == BraceKind.Unknown || OpenBraceOnNewLine(kind, style)) continue;
|
||||||
|
|
||||||
|
lines[p] = lines[p].TrimEnd() + " " + lines[i].TrimStart();
|
||||||
|
lines.RemoveAt(i);
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (나) 코드 뒤 줄 끝에 '{' 가 있다 → 새 줄 스타일이면 다음 줄로 내린다.
|
||||||
|
var trimmed = code.TrimEnd();
|
||||||
|
if (trimmed.Length == 0 || trimmed[trimmed.Length - 1] != '{') continue;
|
||||||
|
|
||||||
|
var brace = trimmed.Length - 1;
|
||||||
|
if (code.Substring(0, brace).Trim().Length == 0) continue;
|
||||||
|
if (code.Substring(0, brace).TrimStart().StartsWith("}", StringComparison.Ordinal)) continue;
|
||||||
|
|
||||||
|
var (k, headerLine) = Classify(mask, i, brace);
|
||||||
|
if (k == BraceKind.Unknown || !OpenBraceOnNewLine(k, style)) continue;
|
||||||
|
|
||||||
|
var original = lines[i];
|
||||||
|
lines[i] = original.Substring(0, brace).TrimEnd();
|
||||||
|
lines.Insert(i + 1, Indent(lines[headerLine]) + original.Substring(brace));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// (line, col) 의 여는 중괄호가 무엇을 여는지. 앞쪽 코드를 거꾸로 읽어 머리(선언 · 제어문 등)를 모은다.
|
||||||
|
/// 머리가 시작한 줄 번호도 준다 - 중괄호를 내릴 때 그 줄의 들여쓰기를 따른다.
|
||||||
|
/// </summary>
|
||||||
|
private static (BraceKind Kind, int HeaderLine) Classify(Masked mask, int line, int col)
|
||||||
|
{
|
||||||
|
var chars = new List<char>(128);
|
||||||
|
int paren = 0, bracket = 0, angle = 0;
|
||||||
|
var start = line;
|
||||||
|
var pos = col - 1;
|
||||||
|
var l = line;
|
||||||
|
|
||||||
|
while (chars.Count < 4000)
|
||||||
|
{
|
||||||
|
if (pos < 0)
|
||||||
|
{
|
||||||
|
l--;
|
||||||
|
if (l < 0 || mask.Preprocessor[l]) break;
|
||||||
|
pos = mask.Code[l].Length - 1;
|
||||||
|
chars.Add(' ');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = mask.Code[l];
|
||||||
|
var ch = text[pos];
|
||||||
|
|
||||||
|
if (ch == ')') paren++;
|
||||||
|
else if (ch == '(') { if (paren == 0) break; paren--; }
|
||||||
|
else if (ch == ']') bracket++;
|
||||||
|
else if (ch == '[') { if (bracket == 0) break; bracket--; }
|
||||||
|
else if (ch == '>' && !(pos > 0 && (text[pos - 1] == '=' || text[pos - 1] == '-')) &&
|
||||||
|
!(pos + 1 < text.Length && text[pos + 1] == '='))
|
||||||
|
angle++;
|
||||||
|
else if (ch == '<') { if (angle > 0) angle--; }
|
||||||
|
else if (paren == 0 && bracket == 0 && (ch == ';' || ch == '{' || ch == '}')) break;
|
||||||
|
else if (paren == 0 && bracket == 0 && angle == 0 && ch == ',') break;
|
||||||
|
|
||||||
|
if (!char.IsWhiteSpace(ch)) start = l;
|
||||||
|
chars.Add(ch);
|
||||||
|
pos--;
|
||||||
|
}
|
||||||
|
|
||||||
|
chars.Reverse();
|
||||||
|
return (KindOfHeader(new string(chars.ToArray())), start);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly HashSet<string> ControlWords = new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
{
|
||||||
|
"if", "else", "for", "foreach", "while", "do", "switch", "try", "catch", "finally",
|
||||||
|
"using", "lock", "fixed", "unsafe", "checked", "unchecked",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly Regex Spaces = new Regex(@"\s+", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex CaseLabel = new Regex(@"^(case\b.*|default)\s*:$", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex AccessorHeader = new Regex(@"^((public|private|protected|internal)\s+)*(get|set|init|add|remove)$", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex TypeKeyword = new Regex(@"\b(class|struct|interface|enum|record|namespace)\b", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex WhereClause = new Regex(@"\bwhere\s+\w+\s*:", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
/// <summary>중괄호 앞 머리 글로 갈래를 정한다. 검사에서 직접 부를 수 있게 열어 둔다.</summary>
|
||||||
|
public static BraceKind KindOfHeader(string header)
|
||||||
|
{
|
||||||
|
var h = Spaces.Replace(header ?? string.Empty, " ").Trim();
|
||||||
|
|
||||||
|
// 앞에 붙은 특성([Test] 등)은 뗀다.
|
||||||
|
while (h.StartsWith("[", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var close = MatchingClose(h, 0, '[', ']');
|
||||||
|
if (close < 0) break;
|
||||||
|
h = h.Substring(close + 1).TrimStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (h.Length == 0) return BraceKind.Unknown;
|
||||||
|
if (h.EndsWith("=>", StringComparison.Ordinal)) return BraceKind.Lambda;
|
||||||
|
if (CaseLabel.IsMatch(h)) return BraceKind.Control;
|
||||||
|
|
||||||
|
var firstWord = Regex.Match(h, @"^[A-Za-z_]\w*").Value;
|
||||||
|
if (ControlWords.Contains(firstWord)) return BraceKind.Control;
|
||||||
|
if (AccessorHeader.IsMatch(h)) return BraceKind.Accessor;
|
||||||
|
|
||||||
|
var paren = h.IndexOf('(');
|
||||||
|
var type = TypeKeyword.Match(h);
|
||||||
|
if (type.Success && (paren < 0 || type.Index < paren)) return BraceKind.Type;
|
||||||
|
|
||||||
|
if (Regex.IsMatch(h, @"\bdelegate\b")) return BraceKind.AnonymousMethod;
|
||||||
|
|
||||||
|
// 제약이 붙은 제네릭 메서드: void M<T>() where T : new()
|
||||||
|
if (WhereClause.IsMatch(h) && paren >= 0) return BraceKind.Method;
|
||||||
|
|
||||||
|
if (h == "new") return BraceKind.AnonymousType;
|
||||||
|
if (EndsWithCreation(h)) return BraceKind.Initializer;
|
||||||
|
if (HasAssignment(h)) return BraceKind.Initializer;
|
||||||
|
if (h.EndsWith("switch", StringComparison.Ordinal)) return BraceKind.Initializer;
|
||||||
|
if (h.EndsWith(")", StringComparison.Ordinal)) return BraceKind.Method;
|
||||||
|
|
||||||
|
var last = h[h.Length - 1];
|
||||||
|
if (char.IsLetterOrDigit(last) || last == '_' || last == '>' || last == ']' || last == '?') return BraceKind.Property;
|
||||||
|
|
||||||
|
return BraceKind.Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 머리가 <c>new 형식</c> · <c>new 형식(인자)</c> · <c>new[]</c> · <c>new()</c> 로 끝나는지 - 개체 · 컬렉션 · 배열 초기화.
|
||||||
|
/// 멤버 숨기기 한정자(<c>public new void M()</c>)는 new 뒤에 낱말이 둘 이상이라 아니다.
|
||||||
|
/// </summary>
|
||||||
|
private static bool EndsWithCreation(string h)
|
||||||
|
{
|
||||||
|
var at = LastWord(h, "new");
|
||||||
|
if (at < 0) return false;
|
||||||
|
|
||||||
|
var rest = h.Substring(at + 3).Trim();
|
||||||
|
|
||||||
|
// 끝의 (인자) 한 덩어리를 뗀다.
|
||||||
|
if (rest.EndsWith(")", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
var open = MatchingOpen(rest, rest.Length - 1, '(', ')');
|
||||||
|
if (open < 0) return false;
|
||||||
|
rest = rest.Substring(0, open).Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rest.Length == 0) return true; // new() · new (...)
|
||||||
|
|
||||||
|
// 형식 이름 하나(점 · 제네릭 · 배열 표시 포함), 공백으로 나뉜 낱말이 없어야 한다.
|
||||||
|
var depth = 0;
|
||||||
|
foreach (var c in rest)
|
||||||
|
{
|
||||||
|
if (c == '<' || c == '[') depth++;
|
||||||
|
else if (c == '>' || c == ']') depth--;
|
||||||
|
else if (c == ' ' && depth == 0) return false;
|
||||||
|
else if (depth == 0 && !(char.IsLetterOrDigit(c) || c == '_' || c == '.' || c == '?')) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>괄호 밖에 대입 '=' 가 있는지(==, !=, <=, >=, => 는 빼고).</summary>
|
||||||
|
private static bool HasAssignment(string h)
|
||||||
|
{
|
||||||
|
var depth = 0;
|
||||||
|
for (var i = 0; i < h.Length; i++)
|
||||||
|
{
|
||||||
|
var c = h[i];
|
||||||
|
if (c == '(' || c == '[') depth++;
|
||||||
|
else if (c == ')' || c == ']') depth--;
|
||||||
|
else if (c == '=' && depth == 0)
|
||||||
|
{
|
||||||
|
var prev = i > 0 ? h[i - 1] : ' ';
|
||||||
|
var next = i + 1 < h.Length ? h[i + 1] : ' ';
|
||||||
|
if (next == '=' || next == '>' || prev == '=' || prev == '!' || prev == '<' || prev == '>') continue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int LastWord(string h, string word)
|
||||||
|
{
|
||||||
|
var at = h.Length;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
at = h.LastIndexOf(word, at - 1, StringComparison.Ordinal);
|
||||||
|
if (at < 0) return -1;
|
||||||
|
|
||||||
|
var before = at == 0 || !(char.IsLetterOrDigit(h[at - 1]) || h[at - 1] == '_');
|
||||||
|
var after = at + word.Length >= h.Length || !(char.IsLetterOrDigit(h[at + word.Length]) || h[at + word.Length] == '_');
|
||||||
|
if (before && after) return at;
|
||||||
|
if (at == 0) return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int MatchingClose(string s, int open, char o, char c)
|
||||||
|
{
|
||||||
|
var depth = 0;
|
||||||
|
for (var i = open; i < s.Length; i++)
|
||||||
|
{
|
||||||
|
if (s[i] == o) depth++;
|
||||||
|
else if (s[i] == c && --depth == 0) return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int MatchingOpen(string s, int close, char o, char c)
|
||||||
|
{
|
||||||
|
var depth = 0;
|
||||||
|
for (var i = close; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (s[i] == c) depth++;
|
||||||
|
else if (s[i] == o && --depth == 0) return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FirstNonSpace(string s)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < s.Length; i++)
|
||||||
|
if (!char.IsWhiteSpace(s[i])) return i;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Indent(string line) => line.Substring(0, line.Length - line.TrimStart().Length);
|
||||||
|
|
||||||
|
// ================= 주석 · 문자열 가리기 =================
|
||||||
|
|
||||||
|
/// <summary>줄마다 코드만 남긴 글(주석은 공백, 문자열 속은 '_'). 길이는 원문과 같다.</summary>
|
||||||
|
private sealed class Masked
|
||||||
|
{
|
||||||
|
public Masked(string[] code, bool[] inside, bool[] preprocessor)
|
||||||
|
{
|
||||||
|
Code = code;
|
||||||
|
Inside = inside;
|
||||||
|
Preprocessor = preprocessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string[] Code { get; }
|
||||||
|
|
||||||
|
/// <summary>줄이 블록 주석 · 여러 줄 문자열 안에서 시작하는지.</summary>
|
||||||
|
public bool[] Inside { get; }
|
||||||
|
|
||||||
|
public bool[] Preprocessor { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum Mode { Code, LineComment, BlockComment, Str, VerbatimStr, InterpStr, InterpVerbatimStr, Char }
|
||||||
|
|
||||||
|
private static Masked Mask(IReadOnlyList<string> lines)
|
||||||
|
{
|
||||||
|
var src = string.Join("\n", lines);
|
||||||
|
var mask = src.ToCharArray();
|
||||||
|
var modes = new Stack<Mode>();
|
||||||
|
modes.Push(Mode.Code);
|
||||||
|
var holes = new Stack<int>(); // 보간 구멍마다 그 안의 중괄호 깊이
|
||||||
|
|
||||||
|
var inside = new bool[lines.Count];
|
||||||
|
var line = 0;
|
||||||
|
var n = src.Length;
|
||||||
|
var i = 0;
|
||||||
|
|
||||||
|
while (i < n)
|
||||||
|
{
|
||||||
|
var ch = src[i];
|
||||||
|
var next = i + 1 < n ? src[i + 1] : '\0';
|
||||||
|
var next2 = i + 2 < n ? src[i + 2] : '\0';
|
||||||
|
|
||||||
|
if (ch == '\n')
|
||||||
|
{
|
||||||
|
var top = modes.Peek();
|
||||||
|
if (top is Mode.LineComment or Mode.Str or Mode.Char or Mode.InterpStr) { modes.Pop(); top = modes.Peek(); }
|
||||||
|
line++;
|
||||||
|
if (line < inside.Length) inside[line] = top != Mode.Code || holes.Count > 0;
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (modes.Peek())
|
||||||
|
{
|
||||||
|
case Mode.Code:
|
||||||
|
if (ch == '/' && next == '/') { modes.Push(Mode.LineComment); mask[i] = ' '; mask[i + 1] = ' '; i += 2; continue; }
|
||||||
|
if (ch == '/' && next == '*') { modes.Push(Mode.BlockComment); mask[i] = ' '; mask[i + 1] = ' '; i += 2; continue; }
|
||||||
|
if ((ch == '$' && next == '@' && next2 == '"') || (ch == '@' && next == '$' && next2 == '"'))
|
||||||
|
{ modes.Push(Mode.InterpVerbatimStr); Hide(mask, i, 3, holes); i += 3; continue; }
|
||||||
|
if (ch == '$' && next == '"') { modes.Push(Mode.InterpStr); Hide(mask, i, 2, holes); i += 2; continue; }
|
||||||
|
if (ch == '@' && next == '"') { modes.Push(Mode.VerbatimStr); Hide(mask, i, 2, holes); i += 2; continue; }
|
||||||
|
if (ch == '"') { modes.Push(Mode.Str); Hide(mask, i, 1, holes); i++; continue; }
|
||||||
|
if (ch == '\'') { modes.Push(Mode.Char); Hide(mask, i, 1, holes); i++; continue; }
|
||||||
|
|
||||||
|
if (holes.Count > 0)
|
||||||
|
{
|
||||||
|
if (ch == '{') holes.Push(holes.Pop() + 1);
|
||||||
|
else if (ch == '}')
|
||||||
|
{
|
||||||
|
var depth = holes.Pop();
|
||||||
|
if (depth == 0) { modes.Pop(); mask[i] = '_'; i++; continue; }
|
||||||
|
holes.Push(depth - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
mask[i] = '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case Mode.LineComment:
|
||||||
|
mask[i] = ' ';
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case Mode.BlockComment:
|
||||||
|
if (ch == '*' && next == '/') { mask[i] = ' '; mask[i + 1] = ' '; modes.Pop(); i += 2; continue; }
|
||||||
|
mask[i] = ' ';
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case Mode.Str:
|
||||||
|
case Mode.Char:
|
||||||
|
var quote = modes.Peek() == Mode.Str ? '"' : '\'';
|
||||||
|
if (ch == '\\') { mask[i] = '_'; if (i + 1 < n && next != '\n') mask[i + 1] = '_'; i += next == '\n' ? 1 : 2; continue; }
|
||||||
|
if (ch == quote) { modes.Pop(); mask[i] = '_'; i++; continue; }
|
||||||
|
mask[i] = '_';
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case Mode.VerbatimStr:
|
||||||
|
if (ch == '"' && next == '"') { mask[i] = '_'; mask[i + 1] = '_'; i += 2; continue; }
|
||||||
|
if (ch == '"') modes.Pop();
|
||||||
|
mask[i] = '_';
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case Mode.InterpStr:
|
||||||
|
case Mode.InterpVerbatimStr:
|
||||||
|
var verbatim = modes.Peek() == Mode.InterpVerbatimStr;
|
||||||
|
if ((ch == '{' && next == '{') || (ch == '}' && next == '}') || (verbatim && ch == '"' && next == '"'))
|
||||||
|
{ mask[i] = '_'; mask[i + 1] = '_'; i += 2; continue; }
|
||||||
|
if (!verbatim && ch == '\\') { mask[i] = '_'; if (next != '\n' && i + 1 < n) mask[i + 1] = '_'; i += next == '\n' ? 1 : 2; continue; }
|
||||||
|
if (ch == '"') { modes.Pop(); mask[i] = '_'; i++; continue; }
|
||||||
|
if (ch == '{') { modes.Push(Mode.Code); holes.Push(0); mask[i] = '_'; i++; continue; }
|
||||||
|
mask[i] = '_';
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var code = new string(mask).Split('\n');
|
||||||
|
var preprocessor = new bool[code.Length];
|
||||||
|
for (var k = 0; k < code.Length; k++)
|
||||||
|
preprocessor[k] = !inside[k] && code[k].TrimStart().StartsWith("#", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
return new Masked(code, inside, preprocessor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>문자열 여는 기호를 가린다. 보간 구멍 안에서 연 문자열이면 원래도 가려져야 할 자리다.</summary>
|
||||||
|
private static void Hide(char[] mask, int at, int count, Stack<int> holes)
|
||||||
|
{
|
||||||
|
for (var k = 0; k < count && at + k < mask.Length; k++) mask[at + k] = '_';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -61,6 +61,12 @@ namespace MergeHub.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IgnoreMergeNotes { get; set; }
|
public bool IgnoreMergeNotes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// C# 파일을 이 스타일로 맞춘 뒤 견줄지(6차 추가). 중괄호 줄바꿈만 다른 파일은 "같음" 이다.
|
||||||
|
/// 줄끝 · 인코딩 차이는 가리지 않는다.
|
||||||
|
/// </summary>
|
||||||
|
public CSharpBraceStyle CSharpStyle { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 수정 시각을 같다고 볼 오차. 파일 시스템마다 정밀도가 달라(FAT 는 2초) 정확히 같기를
|
/// 수정 시각을 같다고 볼 오차. 파일 시스템마다 정밀도가 달라(FAT 는 2초) 정확히 같기를
|
||||||
/// 기대하면 안 된다.
|
/// 기대하면 안 된다.
|
||||||
@ -399,7 +405,8 @@ namespace MergeHub.Services
|
|||||||
{
|
{
|
||||||
if (TextFile.IsBinary(leftPath) || TextFile.IsBinary(rightPath)) return FolderEntryStatus.Binary;
|
if (TextFile.IsBinary(leftPath) || TextFile.IsBinary(rightPath)) return FolderEntryStatus.Binary;
|
||||||
|
|
||||||
if (options.IgnoreMergeNotes && OnlyMergeNotesDiffer(leftPath, rightPath, size, options))
|
if ((options.IgnoreMergeNotes || options.CSharpStyle != CSharpBraceStyle.None) &&
|
||||||
|
OnlyMergeNotesDiffer(leftPath, rightPath, size, options))
|
||||||
return FolderEntryStatus.Same;
|
return FolderEntryStatus.Same;
|
||||||
|
|
||||||
return OnlyCommentsDiffer(leftPath, rightPath, size, options)
|
return OnlyCommentsDiffer(leftPath, rightPath, size, options)
|
||||||
@ -451,23 +458,34 @@ namespace MergeHub.Services
|
|||||||
{
|
{
|
||||||
if (size > options.FullCompareLimitBytes) return false;
|
if (size > options.FullCompareLimitBytes) return false;
|
||||||
|
|
||||||
var left = WithoutMergeNotes(leftPath, out var leftCut);
|
var left = WithoutMergeNotes(leftPath, options, out var leftCut);
|
||||||
if (left is null) return false;
|
if (left is null) return false;
|
||||||
|
|
||||||
var right = WithoutMergeNotes(rightPath, out var rightCut);
|
var right = WithoutMergeNotes(rightPath, options, out var rightCut);
|
||||||
if (right is null) return false;
|
if (right is null) return false;
|
||||||
|
|
||||||
return (leftCut || rightCut) && string.Equals(left, right, StringComparison.Ordinal);
|
return (leftCut || rightCut) && string.Equals(left, right, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>머지 주석을 걷어낸 글. 읽지 못하면 null. 하나라도 걷어냈으면 <paramref name="cut"/> 이 참.</summary>
|
/// <summary>
|
||||||
private static string? WithoutMergeNotes(string path, out bool cut)
|
/// 머지 주석을 걷어내고(켰으면) C# 스타일을 맞춘(골랐으면) 글. 읽지 못하면 null.
|
||||||
|
/// 하나라도 바꾼 것이 있으면 <paramref name="cut"/> 이 참.
|
||||||
|
/// 스타일 맞춤은 줄끝을 그 파일의 줄끝으로 다시 이으므로, 양쪽 줄끝이 다르면 여전히 다르게 나온다.
|
||||||
|
/// </summary>
|
||||||
|
private static string? WithoutMergeNotes(string path, FolderCompareOptions options, out bool cut)
|
||||||
{
|
{
|
||||||
cut = false;
|
cut = false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var text = TextFile.Read(path);
|
var original = TextFile.Read(path);
|
||||||
|
var text = Services.CSharpStyle.Applies(path, options.CSharpStyle)
|
||||||
|
? Services.CSharpStyle.Format(original, options.CSharpStyle)
|
||||||
|
: original;
|
||||||
|
cut = !ReferenceEquals(text, original);
|
||||||
|
|
||||||
|
if (!options.IgnoreMergeNotes) return text;
|
||||||
|
|
||||||
var builder = new System.Text.StringBuilder(text.Length);
|
var builder = new System.Text.StringBuilder(text.Length);
|
||||||
|
|
||||||
foreach (var raw in text.Split('\n'))
|
foreach (var raw in text.Split('\n'))
|
||||||
|
|||||||
@ -334,6 +334,7 @@ namespace MergeHub.ViewModels
|
|||||||
IncludePattern = IncludePattern,
|
IncludePattern = IncludePattern,
|
||||||
ExcludePattern = ExcludePattern,
|
ExcludePattern = ExcludePattern,
|
||||||
IgnoreMergeNotes = _workspace.Settings.IgnoreMergeNotes,
|
IgnoreMergeNotes = _workspace.Settings.IgnoreMergeNotes,
|
||||||
|
CSharpStyle = _workspace.Settings.CSharpBraceStyle,
|
||||||
};
|
};
|
||||||
|
|
||||||
var left = LeftFolder;
|
var left = LeftFolder;
|
||||||
|
|||||||
@ -198,6 +198,12 @@ namespace MergeHub.ViewModels
|
|||||||
// 행을 싣기 전에 틀부터 네 칸으로 바꾼다(LoadFileAsync 와 같은 까닭).
|
// 행을 싣기 전에 틀부터 네 칸으로 바꾼다(LoadFileAsync 와 같은 까닭).
|
||||||
SetToolThreeWay(true);
|
SetToolThreeWay(true);
|
||||||
|
|
||||||
|
// C# 스타일 맞춤(6차 추가).
|
||||||
|
var rawLocalText = localText;
|
||||||
|
baseText = Styled(baseText);
|
||||||
|
localText = Styled(localText);
|
||||||
|
remoteText = Styled(remoteText);
|
||||||
|
|
||||||
_baseTextSnapshot = baseText;
|
_baseTextSnapshot = baseText;
|
||||||
_mineTextSnapshot = localText;
|
_mineTextSnapshot = localText;
|
||||||
_remoteTextSnapshot = remoteText;
|
_remoteTextSnapshot = remoteText;
|
||||||
@ -226,7 +232,7 @@ namespace MergeHub.ViewModels
|
|||||||
|
|
||||||
_undo.Clear();
|
_undo.Clear();
|
||||||
_redo.Clear();
|
_redo.Clear();
|
||||||
HasChanges = false;
|
MarkStyleChange(rawLocalText, localText);
|
||||||
CaretRow = 0;
|
CaretRow = 0;
|
||||||
InvalidateDiffBlocks();
|
InvalidateDiffBlocks();
|
||||||
|
|
||||||
@ -584,7 +590,12 @@ namespace MergeHub.ViewModels
|
|||||||
|
|
||||||
partial void OnCommitMessageChanged(string value) => RaiseCanExecuteChanged();
|
partial void OnCommitMessageChanged(string value) => RaiseCanExecuteChanged();
|
||||||
|
|
||||||
partial void OnHasChangesChanged(bool value) => RaiseCanExecuteChanged();
|
partial void OnHasChangesChanged(bool value)
|
||||||
|
{
|
||||||
|
// 저장했거나 버렸으면 "스타일 맞춤으로 바뀐 글" 도 끝난 것이다.
|
||||||
|
if (!value) _styleRawLocalText = null;
|
||||||
|
RaiseCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>새로고침으로 목록을 다시 만들 때 선택을 잇는 중이면 재진입을 막는다.</summary>
|
/// <summary>새로고침으로 목록을 다시 만들 때 선택을 잇는 중이면 재진입을 막는다.</summary>
|
||||||
private bool _syncingSelection;
|
private bool _syncingSelection;
|
||||||
@ -1257,6 +1268,12 @@ namespace MergeHub.ViewModels
|
|||||||
// 행을 만들기 전에 언어를 정해야 한다 - 블록 주석 이어붙이기가 이 값을 본다.
|
// 행을 만들기 전에 언어를 정해야 한다 - 블록 주석 이어붙이기가 이 값을 본다.
|
||||||
RefreshLanguage();
|
RefreshLanguage();
|
||||||
|
|
||||||
|
// C# 스타일 맞춤(6차 추가). 세 벌 모두 같은 스타일로 맞춘 뒤 견준다.
|
||||||
|
var rawLocalText = localText;
|
||||||
|
baseText = Styled(baseText);
|
||||||
|
localText = Styled(localText);
|
||||||
|
remoteText = Styled(remoteText);
|
||||||
|
|
||||||
var rows = threeWay
|
var rows = threeWay
|
||||||
? BuildThreeWayLines(baseText, localText, remoteText)
|
? BuildThreeWayLines(baseText, localText, remoteText)
|
||||||
: BuildLines(baseText, localText);
|
: BuildLines(baseText, localText);
|
||||||
@ -1329,6 +1346,7 @@ namespace MergeHub.ViewModels
|
|||||||
ClearAutoCommitMessage();
|
ClearAutoCommitMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarkStyleChange(rawLocalText, localText);
|
||||||
UpdateSummaries();
|
UpdateSummaries();
|
||||||
RaiseCanExecuteChanged();
|
RaiseCanExecuteChanged();
|
||||||
});
|
});
|
||||||
@ -2117,7 +2135,7 @@ namespace MergeHub.ViewModels
|
|||||||
if (moveSelection)
|
if (moveSelection)
|
||||||
SelectedLine = Lines.FirstOrDefault(l => l.IsDifferent) ?? Lines.FirstOrDefault();
|
SelectedLine = Lines.FirstOrDefault(l => l.IsDifferent) ?? Lines.FirstOrDefault();
|
||||||
|
|
||||||
HasChanges = !string.Equals(localText, _originalLocalText, StringComparison.Ordinal);
|
HasChanges = !string.Equals(localText, _originalLocalText, StringComparison.Ordinal) || _styleRawLocalText is not null;
|
||||||
|
|
||||||
InvalidateDiffBlocks();
|
InvalidateDiffBlocks();
|
||||||
SyncEditorFromLines();
|
SyncEditorFromLines();
|
||||||
@ -2395,6 +2413,11 @@ namespace MergeHub.ViewModels
|
|||||||
// 읽어 오는 사이에 Git 모드로 넘어갔으면 버린다(위 LoadFileAsync 와 같은 이유).
|
// 읽어 오는 사이에 Git 모드로 넘어갔으면 버린다(위 LoadFileAsync 와 같은 이유).
|
||||||
if (IsGitMode) return;
|
if (IsGitMode) return;
|
||||||
|
|
||||||
|
// C# 스타일 맞춤(6차 추가).
|
||||||
|
var rawLocalText = localText;
|
||||||
|
baseText = Styled(baseText);
|
||||||
|
localText = Styled(localText);
|
||||||
|
|
||||||
_baseTextSnapshot = baseText;
|
_baseTextSnapshot = baseText;
|
||||||
_remoteTextSnapshot = string.Empty; // 파일 두 개 비교는 늘 2-Way 다
|
_remoteTextSnapshot = string.Empty; // 파일 두 개 비교는 늘 2-Way 다
|
||||||
_mineTextSnapshot = string.Empty;
|
_mineTextSnapshot = string.Empty;
|
||||||
@ -2421,7 +2444,8 @@ namespace MergeHub.ViewModels
|
|||||||
|
|
||||||
// 새로 읽은 파일에는 고친 것이 없다. 이걸 안 끄면 앞 파일의 상태가 그대로 끌려와
|
// 새로 읽은 파일에는 고친 것이 없다. 이걸 안 끄면 앞 파일의 상태가 그대로 끌려와
|
||||||
// 저장 단추가 켜진 채로 남고, 다음에 파일을 열 때마다 "저장하지 않은 편집" 을 묻는다.
|
// 저장 단추가 켜진 채로 남고, 다음에 파일을 열 때마다 "저장하지 않은 편집" 을 묻는다.
|
||||||
HasChanges = false;
|
// 스타일 맞춤으로 글이 바뀌었을 때만 저장할 것이 있다.
|
||||||
|
MarkStyleChange(rawLocalText, localText);
|
||||||
|
|
||||||
CaretRow = 0;
|
CaretRow = 0;
|
||||||
InvalidateDiffBlocks();
|
InvalidateDiffBlocks();
|
||||||
@ -2565,6 +2589,10 @@ namespace MergeHub.ViewModels
|
|||||||
if (_suspendGuard) return true;
|
if (_suspendGuard) return true;
|
||||||
if (!HasChanges) return true;
|
if (!HasChanges) return true;
|
||||||
|
|
||||||
|
// 스타일 맞춤으로만 바뀐 글은 다시 열면 똑같이 만들어진다 - 묻지 않는다.
|
||||||
|
// 묻게 두면 스타일이 다른 파일을 훑을 때마다 창이 떠서 쓸 수가 없다.
|
||||||
|
if (OnlyStyleChanged) return true;
|
||||||
|
|
||||||
// <b>고친 파일</b>의 이름을 댄다(6차 B-2). 파일을 바꿀 때는 이 창이 뜨는 순간 SelectedFile 이 이미 새 파일이라,
|
// <b>고친 파일</b>의 이름을 댄다(6차 B-2). 파일을 바꿀 때는 이 창이 뜨는 순간 SelectedFile 이 이미 새 파일이라,
|
||||||
// 예전에는 그 새 파일 이름을 댔다 - Motion/Unit0.cs 를 고쳤는데 "ToBeDeleted.cs 에 저장하지 않은 편집" 이라고 했다.
|
// 예전에는 그 새 파일 이름을 댔다 - Motion/Unit0.cs 를 고쳤는데 "ToBeDeleted.cs 에 저장하지 않은 편집" 이라고 했다.
|
||||||
// Git 모드는 저장소 안 경로로 적는다. 다른 폴더에 같은 이름 파일이 흔하다.
|
// Git 모드는 저장소 안 경로로 적는다. 다른 폴더에 같은 이름 파일이 흔하다.
|
||||||
@ -2707,6 +2735,14 @@ namespace MergeHub.ViewModels
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void RecompareIfSettingsChanged()
|
public void RecompareIfSettingsChanged()
|
||||||
{
|
{
|
||||||
|
// C# 스타일은 원본부터 다시 읽어야 맞춰진다(편집 칸만 다시 정렬하면 BASE 가 옛 스타일로 남는다).
|
||||||
|
if (_workspace.Settings.CSharpBraceStyle != _appliedStyle && Lines.Count > 0)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(CSharpStyleIndex));
|
||||||
|
if (ConfirmDiscard("설정에서 C# 스타일이 바뀌어 다시 엽니다.")) ReloadCurrentSource();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var now = SettingsSignature();
|
var now = SettingsSignature();
|
||||||
if (now == _appliedSettingsSignature) return;
|
if (now == _appliedSettingsSignature) return;
|
||||||
|
|
||||||
@ -2747,6 +2783,83 @@ namespace MergeHub.ViewModels
|
|||||||
ApplyRuleChange();
|
ApplyRuleChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= C# 코드 스타일 맞춤 (6차 추가) =================
|
||||||
|
|
||||||
|
/// <summary>콤보에 보이는 이름. 순서가 <see cref="CSharpBraceStyle"/> 값과 같다.</summary>
|
||||||
|
public IReadOnlyList<string> CSharpStyleNames { get; } = new[] { "C# 스타일 끔", "C# AURORA", "C# Default" };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 고른 스타일. 바꾸면 지금 파일을 <b>원본부터 다시 읽어</b> 그 스타일로 맞춘다.
|
||||||
|
/// 공백 · 대소문자 규칙과 달리 편집 칸 글만 다시 정렬해서는 안 된다 - BASE · REMOTE 도 다시 맞춰야 한다.
|
||||||
|
/// </summary>
|
||||||
|
public int CSharpStyleIndex
|
||||||
|
{
|
||||||
|
get => (int)_workspace.Settings.CSharpBraceStyle;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
var style = (CSharpBraceStyle)Math.Max(0, Math.Min(2, value));
|
||||||
|
if (style == _workspace.Settings.CSharpBraceStyle) return;
|
||||||
|
|
||||||
|
if (!ConfirmDiscard($"C# 스타일을 {CSharpStyle.Label(style)} 로 바꿔 다시 엽니다."))
|
||||||
|
{
|
||||||
|
// 콤보는 제 선택 처리 중에 되돌린 값을 무시한다 - 다음 차례에 알린다(6차 B-2 와 같다).
|
||||||
|
Dispatcher.CurrentDispatcher.BeginInvoke(
|
||||||
|
new Action(() => OnPropertyChanged(nameof(CSharpStyleIndex))), DispatcherPriority.Background);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_workspace.Settings.CSharpBraceStyle = style;
|
||||||
|
_workspace.SaveSettings();
|
||||||
|
OnPropertyChanged();
|
||||||
|
ReloadCurrentSource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>스타일 맞춤을 걸기 전의 편집 칸 글. 맞춤으로 글이 바뀌었을 때만 있다.</summary>
|
||||||
|
private string? _styleRawLocalText;
|
||||||
|
|
||||||
|
/// <summary>지금 불러온 글에 적용한 스타일. 설정 화면에서 바꾸고 돌아왔는지 알아보려고 둔다.</summary>
|
||||||
|
private CSharpBraceStyle _appliedStyle;
|
||||||
|
|
||||||
|
/// <summary>고친 것이 스타일 맞춤뿐인지(사람이 손대지 않았다).</summary>
|
||||||
|
private bool OnlyStyleChanged =>
|
||||||
|
_styleRawLocalText is not null && string.Equals(BuildLocalText(), _originalLocalText, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>C# 파일이고 스타일을 골랐으면 그 스타일로 맞춘 글. 아니면 그대로.</summary>
|
||||||
|
private string Styled(string text) =>
|
||||||
|
CSharpStyle.Applies(CurrentFilePath, _workspace.Settings.CSharpBraceStyle)
|
||||||
|
? CSharpStyle.Format(text, _workspace.Settings.CSharpBraceStyle)
|
||||||
|
: text;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 불러오기를 마칠 때 부른다. 스타일 맞춤으로 편집 칸 글이 바뀌었으면 <b>저장할 것이 있는</b> 상태로 둔다 -
|
||||||
|
/// 저장하면 그 스타일이 파일에 반영된다. 안 바뀌었으면 고친 것 없음.
|
||||||
|
/// </summary>
|
||||||
|
private void MarkStyleChange(string raw, string styled)
|
||||||
|
{
|
||||||
|
_appliedStyle = _workspace.Settings.CSharpBraceStyle;
|
||||||
|
_styleRawLocalText = null;
|
||||||
|
HasChanges = false;
|
||||||
|
RefreshRuleLabels(); // 상태바 "무시: …" 요약에 지금 스타일을 적는다
|
||||||
|
|
||||||
|
if (string.Equals(raw, styled, StringComparison.Ordinal)) return;
|
||||||
|
|
||||||
|
_styleRawLocalText = raw;
|
||||||
|
HasChanges = true;
|
||||||
|
|
||||||
|
var before = raw.Split('\n').Length;
|
||||||
|
var after = styled.Split('\n').Length;
|
||||||
|
_workspace.AppendLog($"C# 스타일({CSharpStyle.Label(_appliedStyle)})로 맞춤: 편집 칸 {before}줄 → {after}줄. 저장하면 파일에 반영됩니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>지금 보고 있는 파일을 원본부터 다시 읽는다. Git · 파일 두 개 · mergetool 중 지금 길로.</summary>
|
||||||
|
private void ReloadCurrentSource()
|
||||||
|
{
|
||||||
|
if (IsGitMode) _ = LoadFileAsync();
|
||||||
|
else if (_mergeTool is { } tool) _ = LoadMergeToolAsync(tool);
|
||||||
|
else if (!string.IsNullOrWhiteSpace(LeftFile) || !string.IsNullOrWhiteSpace(RightFile)) _ = LoadPlainFilesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>규칙이 바뀌면 지금 보고 있는 내용을 그대로 다시 정렬한다.</summary>
|
/// <summary>규칙이 바뀌면 지금 보고 있는 내용을 그대로 다시 정렬한다.</summary>
|
||||||
private void ApplyRuleChange()
|
private void ApplyRuleChange()
|
||||||
{
|
{
|
||||||
@ -2765,7 +2878,11 @@ namespace MergeHub.ViewModels
|
|||||||
OnPropertyChanged(nameof(WhitespaceLabel));
|
OnPropertyChanged(nameof(WhitespaceLabel));
|
||||||
OnPropertyChanged(nameof(IgnoreCaseLabel));
|
OnPropertyChanged(nameof(IgnoreCaseLabel));
|
||||||
OnPropertyChanged(nameof(LineFilterLabel));
|
OnPropertyChanged(nameof(LineFilterLabel));
|
||||||
RulesSummary = Rules.Describe() ?? string.Empty;
|
var style = _workspace.Settings.CSharpBraceStyle;
|
||||||
|
var summary = Rules.Describe() ?? string.Empty;
|
||||||
|
RulesSummary = style == CSharpBraceStyle.None
|
||||||
|
? summary
|
||||||
|
: string.IsNullOrEmpty(summary) ? $"C# {CSharpStyle.Label(style)}" : $"{summary} · C# {CSharpStyle.Label(style)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
// ================= 차이 탐색 · 이동 · 북마크 =================
|
// ================= 차이 탐색 · 이동 · 북마크 =================
|
||||||
@ -3069,7 +3186,7 @@ namespace MergeHub.ViewModels
|
|||||||
|
|
||||||
private void AfterEdit()
|
private void AfterEdit()
|
||||||
{
|
{
|
||||||
HasChanges = _undo.Count > 0;
|
HasChanges = _undo.Count > 0 || _styleRawLocalText is not null;
|
||||||
RefreshAppliedFlags();
|
RefreshAppliedFlags();
|
||||||
RefreshBlockCommentState(Lines);
|
RefreshBlockCommentState(Lines);
|
||||||
InvalidateDiffBlocks();
|
InvalidateDiffBlocks();
|
||||||
|
|||||||
@ -1665,6 +1665,11 @@
|
|||||||
MinHeight="28" FontSize="11" Margin="2,4,2,0"
|
MinHeight="28" FontSize="11" Margin="2,4,2,0"
|
||||||
ToolTip="긴 줄을 접어서 보여줍니다 (가로 스크롤 대신)"/>
|
ToolTip="긴 줄을 접어서 보여줍니다 (가로 스크롤 대신)"/>
|
||||||
</UniformGrid>
|
</UniformGrid>
|
||||||
|
<!-- C# 코드 스타일 맞춤(6차 추가). 중괄호 줄바꿈만 달라 차이가 많이 뜨는 것을 막는다. -->
|
||||||
|
<ComboBox ItemsSource="{Binding CSharpStyleNames}" SelectedIndex="{Binding CSharpStyleIndex}"
|
||||||
|
MinHeight="26" FontSize="11" Margin="2,4,2,0"
|
||||||
|
Visibility="{Binding IsRulesOpen, Converter={StaticResource BoolToVis}}"
|
||||||
|
ToolTip="C# 파일의 여는 중괄호 · else/catch/finally 줄바꿈을 이 스타일로 맞춘 뒤 비교합니다.
AURORA: 형식 · 메서드 중괄호만 새 줄, 나머지는 같은 줄, } else {
Default: Visual Studio 기본 - 모두 새 줄
편집 칸 글이 바뀌면 저장할 때 그 스타일로 저장됩니다."/>
|
||||||
<Button Content="{Binding ReadOnlyLabel}" Click="ToggleReadOnly_Click"
|
<Button Content="{Binding ReadOnlyLabel}" Click="ToggleReadOnly_Click"
|
||||||
MinHeight="28" FontSize="11" Margin="2,4,2,0"
|
MinHeight="28" FontSize="11" Margin="2,4,2,0"
|
||||||
Visibility="{Binding IsRulesOpen, Converter={StaticResource BoolToVis}}"
|
Visibility="{Binding IsRulesOpen, Converter={StaticResource BoolToVis}}"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user