遇到一個小需求,想用 .NET 出一份檔案修改前後對照表,我心中最理想的方案是用 git diff + diff2html 產生 HTML 報表,省時省力又好看。

git diff 指令跟產生 diff2html 網頁技能是現成的,最不花腦的解決方法是用 .NET 將修改前後檔案雙雙寫成檔案,呼叫外部程式 git.exe 跑 git diff a.txt b.txt 取得結果,再交給 diff2html 顯示對照表。有沒有更輕巧簡潔的解法?

libgit2 是一個用 C 開發的開源 Git 可攜程式庫,讓你不用安裝 Git 直接引用 Git 的各項功能,libgit2sharp 則為 libgit2 提供了 .NET 相容介面,會比呼叫 git.exe 更優雅。

在專案執行 dotnet add package libgit2sharp 或從 NuGet 安裝後,就可以在 .NET 程式裡直接進行各項 Git 操作。

以下是個簡單範例,示範 用 C# 建立一個 Git 儲存庫,加入 test.txt、Commit、修改 test.txt、再次 Commit、列出 Commit 記錄、顯示 Commit 的異動內容:

using LibGit2Sharp;

Func<string> CreateTempRepo = () =>
{
    var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
    Directory.CreateDirectory(path);
    Repository.Init(path, false);
    return path;
};

var userName = "Tester";
var email = "tester@mail.net";
// Signature 物件,代表 Commit 的作者或提交者
Func<Signature> getSignature = () => new Signature(userName, email, DateTimeOffset.Now);
var repoPath = CreateTempRepo();
using (var repo = new Repository(repoPath))
{
    var fileName = "test.txt";
    var path = Path.Combine(repoPath, fileName);
    File.WriteAllText(path, "Hello, World!\nFirst Commit");
    Commands.Stage(repo, fileName);
    repo.Commit("Initial commit",
        // Author       Committer
        getSignature(), getSignature());
    File.AppendAllText(path, "\nSecond Commit");
    Commands.Stage(repo, fileName);
    Thread.Sleep(1000);
    repo.Commit("Second commit", getSignature(), getSignature());
    print("**** Commit 紀錄 ****", ConsoleColor.Magenta);
    foreach (var commit in repo.Commits)
    {
        print("Commit: " + commit.Id, ConsoleColor.Cyan);
        print("  " + commit.Message, noNewLine: true);
        print($"  by {commit.Author.Name} at {commit.Author.When:HH:mm:ss}", ConsoleColor.Green);
    }
    var commit1 = repo.Commits.Last();
    var commit2 = repo.Commits.First();
    var changes = repo.Diff.Compare<Patch>(commit1.Tree, commit2.Tree);
    print("**** 異動比對 ****", ConsoleColor.Magenta);
    foreach (var change in changes)
    {
        print(change.Patch, ConsoleColor.DarkYellow);
    }
}

void print(string text, ConsoleColor color = ConsoleColor.White, bool noNewLine = false)
{
    Console.ForegroundColor = color;
    if (noNewLine)
        Console.Write(text);
    else
        Console.WriteLine(text);
    Console.ResetColor();
}

輕輕鬆鬆搞定。

最後,來看一下一開始的 XML 修改前後對照是怎麼做出來的?

public static void Run()
{
    var xml= XDocument.Parse(@"
<list>
<item id=""N1"">Item 1</item>
<item id=""N2"">Item 2</item>
<item id=""N3"">Item 3</item>
<item id=""N4"">Item 4</item>
</list>
");
    var origin = xml.ToString();
    xml.Root.Elements("item").Skip(1).First().SetAttributeValue("id", "N2-new");
    xml.Root.Elements("item").Skip(2).First().Value = "Item 3 (modified)";
    xml.Root.Add(new XElement("item", new XAttribute("id", "N5"), "Item 5"));
    var modified = xml.ToString();
    var html = GitDiffTool.GenGitDiffHtmlReport(origin, modified, "origin.xml", "modified.xml");
    File.WriteAllText("D:\\Report.html", html);
    // 用預設瀏覽器開啟
    System.Diagnostics.Process.Start("cmd", @"/c ""start D:\Report.html""");
}

關鍵的 GitDiffTool.GenGitDiffHtmlReport 如下,建立一個暫存目錄,直接將修改前後字串內容轉成 Blob 交由 Diff.Comapre() 比對。結果字串交給 diff2html 顯示,過程我用了 HTML 單檔文件挑戰 - 內嵌 .js GZIP 壓縮檔展示過的技巧,將差異結果用 Base64 內嵌在網頁裡。

using System.Text;
using LibGit2Sharp;

public class GitDiffTool
{
    static string CreateTempRepo()
    {
        var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
        Directory.CreateDirectory(path);
        Repository.Init(path, false);
        return path;
    }

    const string html = @"
<!DOCTYPE html>

<html>

<head>
    <link rel=""stylesheet"" type=""text/css""
          href=""https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css"" />
    <script src=""https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html.min.js""></script>
</head>

<body>
    <script id='diff.txt' type=""text/base64"">
[$diff.txt$]
    </script>
    <div id=report>
    </div>
    <script>
    var diffText = new TextDecoder().decode(
        new Uint8Array(
            atob(
                document.getElementById('diff.txt').innerHTML.replace(/[\n\r \t]/g, '')
            ).split('').map(c => c.charCodeAt(0))
        )
	);
    var diffHtml = Diff2Html.html(diffText, {
        drawFileList: true,
        matching: 'lines',
        outputFormat: 'side-by-side',
    });
    document.getElementById('report').innerHTML = diffHtml;
    </script>
</body>

</html>";

    public static string GenGitDiff(string oldText, string newText, string oldFile = "old", string newFile = "new")
    {
        using (var repo = new Repository(CreateTempRepo()))
        {
            var lines = new[] { "Line 1", "Line 2", "Line 3" };
            var blob1 = repo.ObjectDatabase.CreateBlob(
                // 由字串建立 Blob 物件 (也可改成讀檔案傳入 FileStream)
                new MemoryStream(Encoding.UTF8.GetBytes(oldText)));
            var blob2 = repo.ObjectDatabase.CreateBlob(
                new MemoryStream(Encoding.UTF8.GetBytes(newText)));
            // 比較兩個 Blob 物件差異
            var patch = $@"
diff --git a/{oldFile} b/{newFile}
index aaaaaaa..bbbbbbb 100644
--- a/{oldFile}
+++ b/{newFile}
{repo.Diff.Compare(blob1, blob2).Patch}";
            return patch;
        }
    }
    public static string GenGitDiffHtmlReport(string oldText, string newText, string oldFile = "old", string newFile = "new")
    {
        var diff = GenGitDiff(oldText, newText, oldFile, newFile);
        var base64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(diff));
        var pem = new StringBuilder();
        const int maxWidth = 160;
        for (int i = 0; i < base64.Length; i += maxWidth)
        {
            pem.AppendLine(base64.Substring(i, Math.Min(maxWidth, base64.Length - i)));
        }
        return html.Replace("[$diff.txt$]", pem.ToString());
    }
}

就醬,又解掉一件任務。


Comments

Be the first to post a comment

Post a comment