-
Notifications
You must be signed in to change notification settings - Fork 44
unsloth: compare add/add conflicts on content, not on the braces around it #170
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,6 +13,12 @@ | |
| the merge base is non-empty means at least one side *edited* shared text, and | ||
| picking a side or unioning them is a guess. This script never guesses. | ||
|
|
||
| The two additions are compared on their CONTENT, not on the braces around it. | ||
| A case arm is `case X:`, a body, and `} break;`, and two arms for different | ||
| architectures share that last part whatever they do. Treating the scaffolding | ||
| as evidence that the same change was made twice refuses exactly the conflict | ||
| this script exists for; see STRUCTURAL below. | ||
|
|
||
| Reads a conflicted work tree, writes resolutions in place, exits 0 if every | ||
| conflict in every file was resolved and 1 otherwise. `--report` emits JSON | ||
| describing what it did for the caller to quote in a PR body. | ||
|
|
@@ -22,6 +28,7 @@ | |
|
|
||
| import argparse | ||
| import json | ||
| import re | ||
| import subprocess | ||
| import sys | ||
| from pathlib import Path | ||
|
|
@@ -87,6 +94,44 @@ def nonblank(lines: list[str]) -> list[str]: | |
| return [ln.strip() for ln in lines if ln.strip()] | ||
|
|
||
|
|
||
| # A line that closes or opens a block and nothing else. Two INDEPENDENT case | ||
| # arms in the same switch share these by construction -- `{`, `} break;`, `}` | ||
| # are what a case arm is made of, not what makes it that case arm -- so finding | ||
| # them on both sides says nothing about whether the two sides added the same | ||
| # construct. Matching them as "shared" is what refused the real add/add of | ||
| # PROJECTOR_TYPE_KIMIK3 next to PROJECTOR_TYPE_DEEPSEEK4V in tools/mtmd/clip.cpp | ||
| # with "one change made twice: {, } break;", when the two arms had no line of | ||
| # actual content in common. | ||
| # | ||
| # Deliberately narrow: braces, brackets, parens, semicolons and commas, around | ||
| # at most one bare block-terminating keyword. `break;` matches, `return true;` | ||
| # does not, and anything naming a type, a constant or a function does not. | ||
| STRUCTURAL = re.compile(r"^[\s{}()\[\];,]*(?:break|continue|return|pass)?[\s{}()\[\];,]*$") | ||
|
|
||
|
|
||
| def identifying(lines: list[str]) -> set[str]: | ||
| """The lines that say WHICH construct this is, ignoring block scaffolding.""" | ||
| return {ln for ln in nonblank(lines) if not STRUCTURAL.match(ln)} | ||
|
|
||
|
|
||
| # `case FOO:`, `case FOO :`, `default:`. A fallthrough label may carry no body | ||
| # at all, which is the shape the nightly hits most often. | ||
| CASE_LABEL = re.compile(r"^(?:case\s+[^:]+|default\s*):") | ||
|
|
||
|
|
||
| def case_arms(lines: list[str]) -> set[str] | None: | ||
| """The case labels this side adds, or None if it is not a run of case arms. | ||
|
|
||
| None, not an empty set: "adds no case arm" and "adds case arms, none of | ||
| which the other side adds" have to be told apart, and only the second one | ||
| licenses the union below. | ||
| """ | ||
| ident = [ln for ln in nonblank(lines) if not STRUCTURAL.match(ln)] | ||
| if not ident or not CASE_LABEL.match(ident[0]): | ||
| return None | ||
| return {ln for ln in ident if CASE_LABEL.match(ln)} | ||
|
|
||
|
|
||
| def resolve_region(ours: list[str], base: list[str], theirs: list[str]) -> list[str]: | ||
| """Return the union, or raise if this region is not a pure add/add.""" | ||
| if nonblank(base): | ||
|
|
@@ -100,14 +145,38 @@ def resolve_region(ours: list[str], base: list[str], theirs: list[str]) -> list[ | |
| if ours == theirs: | ||
| # Both sides added byte-identical text; one copy is the resolution. | ||
| return list(ours) | ||
| shared = set(nonblank(ours)) & set(nonblank(theirs)) | ||
| ours_arms, theirs_arms = case_arms(ours), case_arms(theirs) | ||
| if ours_arms and theirs_arms and ours_arms.isdisjoint(theirs_arms): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When both additions are fallthrough arms with distinct labels but a shared body line, this fast path concatenates them without checking whether either arm terminates. For example, merging Useful? React with 👍 / 👎. |
||
| # Both sides added case arms, and not one label is on both sides. Two | ||
| # arms of the same switch labelled differently are two constructs, so | ||
| # any line they happen to share is body text, not a duplicate: the real | ||
| # tools/mtmd/clip.cpp collision has a KIMIK3 arm and a DEEPSEEK4V arm | ||
| # that both set `hparams.rope_theta = 10000.0f;`, and refusing on that | ||
| # coincidence is what the shared-line check is for, backwards. | ||
| # | ||
| # The same change made twice would keep its label, so it lands in the | ||
| # check below instead. This is the one place where a shared line is | ||
| # allowed, and it is allowed because the labels prove the arms are | ||
| # distinct -- a duplicated label would not even compile. | ||
| return list(theirs) + list(ours) | ||
| shared = identifying(ours) & identifying(theirs) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When both branches add the same keyed initializer but format its opener differently—for example, ours splits Useful? React with 👍 / 👎. |
||
| if shared: | ||
| # Overlapping content is the signature of one construct added twice, | ||
| # not two independent additions. Unioning it would duplicate code. | ||
| # Scaffolding lines are excluded above, so what is left is content both | ||
| # sides genuinely wrote, which is the thing that makes this a duplicate. | ||
| raise Unresolvable( | ||
| "both sides add the same line(s), so this is one change made twice: " | ||
| + ", ".join(sorted(shared)[:3]) | ||
| ) | ||
| if not identifying(ours) or not identifying(theirs): | ||
| # Everything one side added is scaffolding, so there is no content to | ||
| # tell the two additions apart and the exclusion above has nothing left | ||
| # to work with. Refuse rather than union braces onto braces. | ||
| raise Unresolvable( | ||
| "one side adds only block scaffolding, so the two additions cannot " | ||
| "be told apart" | ||
| ) | ||
| # Upstream first, then ours: the same order a human repin produces. | ||
| return list(theirs) + list(ours) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When both branches add the same label with harmless formatting differences, such as
case FOO:versuscase FOO :,case_arms()stores the complete stripped lines, so these sets appear disjoint and this fast path stages both arms while bypassing duplicate detection. The resulting switch contains two equivalent case values and fails to compile; compare normalized label expressions rather than raw label lines before taking this path.Useful? React with 👍 / 👎.