File size: 4,558 Bytes
df41178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
difflook.py -- なにが変わったかを、行ごとに見せる

  2つの見せ方を持つ。

    ① フォルダの差   … 実行の前と後で、どのファイルが増えた・減った・動いた
    ② ファイルの中身 … 1枚のテキストの、どの行が変わったか

  ②は Python が持っている difflib をそのまま使う。
  自分で書き直す理由がない(車輪の再発明はしない)。
"""
import difflib, os

# 中身を読んで比べてよい大きさ。これ以上は行数だけ見る
MAX_READ = 2_000_000
TEXT_EXT = {".txt", ".md", ".html", ".htm", ".css", ".js", ".json", ".py",
            ".csv", ".log", ".xml", ".yml", ".yaml", ".sh", ".ini", ".cfg"}


def is_text(path):
    if os.path.splitext(path)[1].lower() in TEXT_EXT:
        return True
    try:
        with open(path, "rb") as f:
            head = f.read(2048)
        return b"\0" not in head
    except OSError:
        return False


def read(path):
    try:
        if os.path.getsize(path) > MAX_READ:
            return None
        with open(path, encoding="utf-8", errors="replace") as f:
            return f.read().splitlines()
    except OSError:
        return None


def lines(a_lines, b_lines, a_name="前", b_name="後", ctx=3):
    """行の差を [(しるし, 文)] で返す。

    しるし: "+" 増えた / "-" 減った / " " そのまま / "@" 区切り
    """
    if a_lines is None or b_lines is None:
        return [("@", "中身が読めませんでした")]
    out = []
    for ln in difflib.unified_diff(a_lines, b_lines,
                                   fromfile=a_name, tofile=b_name,
                                   lineterm="", n=ctx):
        if ln.startswith("+++") or ln.startswith("---"):
            continue
        if ln.startswith("@@"):
            out.append(("@", ln.strip()))
        elif ln.startswith("+"):
            out.append(("+", ln[1:]))
        elif ln.startswith("-"):
            out.append(("-", ln[1:]))
        else:
            out.append((" ", ln[1:] if ln.startswith(" ") else ln))
    if not out:
        out = [("@", "中身は変わっていません")]
    return out


def files(a_path, b_path, ctx=3):
    """2つのファイルの中身を比べる"""
    if not os.path.exists(a_path):
        return [("@", f"ありません: {a_path}")]
    if not os.path.exists(b_path):
        return [("@", f"ありません: {b_path}")]
    if not (is_text(a_path) and is_text(b_path)):
        sa, sb = os.path.getsize(a_path), os.path.getsize(b_path)
        return [("@", f"文字のファイルではありません({sa:,} → {sb:,} バイト)")]
    return lines(read(a_path), read(b_path),
                 os.path.basename(a_path), os.path.basename(b_path), ctx)


def folders(before, after):
    """フォルダの前と後(virtual.simulate の「前」「後」)を突き合わせる。

    戻り値: [{"場所", "増えた": [...], "減った": [...], "件数": (前, 後)}]
    """
    out = []
    for d in sorted(set(before) | set(after)):
        b = list(before.get(d, []))
        a = list(after.get(d, []))
        if b == a:
            continue
        out.append({"場所": d,
                    "増えた": [x for x in a if x not in b],
                    "減った": [x for x in b if x not in a],
                    "件数": (len(b), len(a))})
    return out


def render(rows, limit=200):
    """行の差を、人が読める文字列にする"""
    out = []
    for i, (mark, text) in enumerate(rows):
        if i >= limit:
            out.append(f"… ほか {len(rows)-limit} 行")
            break
        out.append(("  " if mark == " " else mark + " ") + text)
    return "\n".join(out)


def render_folders(rows):
    out = []
    for r in rows:
        name = os.path.basename(r["場所"]) or r["場所"]
        out.append(f"{name}/  {r['件数'][0]} 件 → {r['件数'][1]} 件")
        for x in r["減った"]:
            out.append("  - " + x)
        for x in r["増えた"]:
            out.append("  + " + x)
    return "\n".join(out) or "変わったところはありません"


if __name__ == "__main__":
    import sys
    if len(sys.argv) == 3:
        print(render(files(sys.argv[1], sys.argv[2])))
    else:
        a = ["こんにちは", "ここは変わらない", "古い行", "おわり"]
        b = ["こんにちは", "ここは変わらない", "新しい行", "足した行", "おわり"]
        print(render(lines(a, b)))