conflict-split 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,36 @@
1
+ Metadata-Version: 2.4
2
+ Name: conflict-split
3
+ Version: 0.1.0
4
+ Summary: Resolve git conflict markers in any file by keeping ours, theirs, both or the diff3 base. Stdlib only.
5
+ Author-email: agent-one <rafal.baranowski.iot@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/rafal-one/conflict-split
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Dynamic: license-file
14
+
15
+ # conflict-split
16
+
17
+ Resolve git conflict markers (`<<<<<<<`, `|||||||`, `=======`, `>>>>>>>`) in **any** text file
18
+ after the fact, by keeping one side for every conflict. Handles diff3/zdiff3 style, CRLF files,
19
+ multiple conflicts. Python 3.9+, stdlib only.
20
+
21
+ ```
22
+ pip install conflict-split
23
+ conflict-split data.json --take theirs -i # keep the incoming side, rewrite the file
24
+ conflict-split notes.txt --take both # keep both sides, print to stdout
25
+ conflict-split f.txt --count # number of conflicts
26
+ ```
27
+
28
+ `--take ours|theirs|both|base` (`base` needs diff3 markers). Exit codes: 0 ok, 1 malformed markers
29
+ (unterminated/nested), 2 file or encoding error. For cell-wise merging of CSV files use
30
+ [csv-conflict](https://github.com/rafal-one/csv-conflict).
31
+
32
+ ## Tests
33
+ `pytest` (15 tests; run on Python 3.9 and 3.14).
34
+
35
+ ## Support
36
+ Free, MIT licensed. If it saved you time: https://paypal.me/RafalBaranowski
@@ -0,0 +1,7 @@
1
+ conflict_split.py,sha256=LPHbA9U2_d5uEazd4ErGGE8V2dy2tPIUGnGLkKJwFUo,3815
2
+ conflict_split-0.1.0.dist-info/licenses/LICENSE,sha256=-hZriFiksFsGGM2WbhdnQzoXUOFuMGve5P3y4pvA0Ws,1066
3
+ conflict_split-0.1.0.dist-info/METADATA,sha256=liIgNchWZmeCcE9Vr3AtJviTRj_BDEkVB1W0ROm2tb0,1437
4
+ conflict_split-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ conflict_split-0.1.0.dist-info/entry_points.txt,sha256=tGotNWkTyq6lnpdOUOYSY_Db9UrIGsFco3KfP9DsQ5M,55
6
+ conflict_split-0.1.0.dist-info/top_level.txt,sha256=5hCl57uWjA2lhSQ4Psy1hPVxGdh0oavtTNV_14pTXbY,15
7
+ conflict_split-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ conflict-split = conflict_split:main
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 agent-one
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1 @@
1
+ conflict_split
conflict_split.py ADDED
@@ -0,0 +1,111 @@
1
+ """Resolve git conflict markers in any text file by keeping one side. Stdlib only."""
2
+ import argparse
3
+ import re
4
+ import sys
5
+
6
+ __version__ = "0.1.0"
7
+ START = re.compile(r"^<{7}(?: .*)?$")
8
+ BASE = re.compile(r"^\|{7}(?: .*)?$")
9
+ MID = re.compile(r"^={7}$")
10
+ END = re.compile(r"^>{7}(?: .*)?$")
11
+ CHOICES = ("ours", "theirs", "both", "base")
12
+
13
+
14
+ class ConflictError(Exception):
15
+ pass
16
+
17
+
18
+ def parse(text):
19
+ """Return list of segments: str (plain text) or dict(ours, base, theirs) of line lists."""
20
+ segs, plain = [], []
21
+ state, cur, n = None, None, 0
22
+ for n, line in enumerate(text.splitlines(keepends=True), 1):
23
+ body = line.rstrip("\r\n")
24
+ if state is None:
25
+ if START.match(body):
26
+ if plain:
27
+ segs.append("".join(plain))
28
+ plain = []
29
+ cur = {"ours": [], "base": None, "theirs": [], "line": n}
30
+ state = "ours"
31
+ else:
32
+ plain.append(line)
33
+ elif state == "ours" and BASE.match(body):
34
+ cur["base"], state = [], "base"
35
+ elif state in ("ours", "base") and MID.match(body):
36
+ state = "theirs"
37
+ elif state == "theirs" and END.match(body):
38
+ segs.append(cur)
39
+ state = None
40
+ elif START.match(body):
41
+ raise ConflictError(f"line {n}: nested conflict marker")
42
+ else:
43
+ cur["base" if state == "base" else state].append(line)
44
+ if state is not None:
45
+ raise ConflictError(f"conflict starting at line {cur['line']} is not terminated")
46
+ if plain:
47
+ segs.append("".join(plain))
48
+ return segs
49
+
50
+
51
+ def resolve(text, take="ours"):
52
+ if take not in CHOICES:
53
+ raise ValueError(f"take must be one of {CHOICES}")
54
+ out = []
55
+ for s in parse(text):
56
+ if isinstance(s, str):
57
+ out.append(s)
58
+ elif take == "base":
59
+ if s["base"] is None:
60
+ raise ConflictError(f"conflict at line {s['line']} has no base (not diff3 style)")
61
+ out.extend(s["base"])
62
+ elif take == "both":
63
+ out.extend(s["ours"] + s["theirs"])
64
+ else:
65
+ out.extend(s[take])
66
+ return "".join(out)
67
+
68
+
69
+ def count(text):
70
+ return sum(1 for s in parse(text) if isinstance(s, dict))
71
+
72
+
73
+ def main(argv=None):
74
+ p = argparse.ArgumentParser(prog="conflict-split",
75
+ description="Resolve git conflict markers by keeping one side.")
76
+ p.add_argument("file", help="file with markers, or - for stdin")
77
+ p.add_argument("--take", choices=CHOICES, default="ours", help="side to keep (default ours)")
78
+ p.add_argument("--count", action="store_true", help="only print the number of conflicts")
79
+ p.add_argument("-o", "--output", help="write here instead of stdout")
80
+ p.add_argument("-i", "--in-place", action="store_true", help="rewrite the input file")
81
+ p.add_argument("--version", action="version", version=__version__)
82
+ a = p.parse_args(argv)
83
+ if a.in_place and a.file == "-":
84
+ p.error("--in-place needs a file")
85
+ try:
86
+ if a.file == "-":
87
+ text = sys.stdin.read()
88
+ else:
89
+ with open(a.file, newline="", encoding="utf-8") as f:
90
+ text = f.read()
91
+ if a.count:
92
+ print(count(text))
93
+ return 0
94
+ res = resolve(text, a.take)
95
+ except (OSError, UnicodeDecodeError) as e:
96
+ print(f"conflict-split: {e}", file=sys.stderr)
97
+ return 2
98
+ except ConflictError as e:
99
+ print(f"conflict-split: {e}", file=sys.stderr)
100
+ return 1
101
+ target = a.file if a.in_place else a.output
102
+ if target:
103
+ with open(target, "w", newline="", encoding="utf-8") as f:
104
+ f.write(res)
105
+ else:
106
+ sys.stdout.write(res)
107
+ return 0
108
+
109
+
110
+ if __name__ == "__main__":
111
+ sys.exit(main())