calkit-python 0.49.4__py3-none-any.whl → 0.49.5__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.
- calkit/agent_skills/resolve-merge-conflicts/SKILL.md +72 -0
- calkit/cli/fix.py +196 -0
- calkit/cli/main/core.py +164 -0
- calkit/merge.py +636 -0
- calkit/tests/cli/main/test_core.py +78 -0
- calkit/tests/cli/test_fix.py +174 -0
- calkit/tests/test_merge.py +182 -0
- {calkit_python-0.49.4.dist-info → calkit_python-0.49.5.dist-info}/METADATA +1 -1
- {calkit_python-0.49.4.dist-info → calkit_python-0.49.5.dist-info}/RECORD +25 -20
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/etc/jupyter/jupyter_server_config.d/calkit.json +0 -0
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/share/jupyter/labextensions/calkit/package.json +0 -0
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/package.json.orig +0 -0
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/share/jupyter/labextensions/calkit/schemas/calkit/plugin.json +0 -0
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/share/jupyter/labextensions/calkit/static/506.c34b070098184e33.js +0 -0
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/share/jupyter/labextensions/calkit/static/57d13a7399cec3c5.png +0 -0
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/share/jupyter/labextensions/calkit/static/616.528427eb54a4a0c0.js +0 -0
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/share/jupyter/labextensions/calkit/static/740.6bf87276e9e5788a.js +0 -0
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/share/jupyter/labextensions/calkit/static/899.f9b9f9bf705a6493.js +0 -0
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/share/jupyter/labextensions/calkit/static/935.46ecc6bf99aa593a.js +0 -0
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/share/jupyter/labextensions/calkit/static/remoteEntry.109a3a8379365e59.js +0 -0
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/share/jupyter/labextensions/calkit/static/style.js +0 -0
- {calkit_python-0.49.4.data → calkit_python-0.49.5.data}/data/share/jupyter/labextensions/calkit/static/third-party-licenses.json +0 -0
- {calkit_python-0.49.4.dist-info → calkit_python-0.49.5.dist-info}/WHEEL +0 -0
- {calkit_python-0.49.4.dist-info → calkit_python-0.49.5.dist-info}/entry_points.txt +0 -0
- {calkit_python-0.49.4.dist-info → calkit_python-0.49.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: resolve-merge-conflicts
|
|
3
|
+
description: Resolve Git merge conflicts in a Calkit project, including
|
|
4
|
+
dvc.lock, stage run records, and pipeline outputs. Use when the user
|
|
5
|
+
invokes `/calkit:resolve-merge-conflicts`, asks to merge a branch, or a
|
|
6
|
+
merge, pull, or rebase stopped with conflicts.
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Resolve merge conflicts
|
|
10
|
+
|
|
11
|
+
Some conflicted files record what the pipeline ran rather than what anyone
|
|
12
|
+
wrote, so they're resolved per stage by Calkit, not by editing text.
|
|
13
|
+
|
|
14
|
+
## Division of labor
|
|
15
|
+
|
|
16
|
+
Done by Calkit, never by hand:
|
|
17
|
+
|
|
18
|
+
- `dvc.lock` and `.calkit/stage-runs.json`, in the project and any
|
|
19
|
+
subproject. Don't edit them or take one side whole with
|
|
20
|
+
`git checkout --ours/--theirs`: that mixes stages, or drops the records of
|
|
21
|
+
stages that only ran on the other side.
|
|
22
|
+
- Git-tracked pipeline outputs, which are checked out from the side each
|
|
23
|
+
stage's `dvc.lock` entry came from.
|
|
24
|
+
- `dvc.yaml`, which is generated from `calkit.yaml`. Once `calkit.yaml` is
|
|
25
|
+
resolved, take either side (`git checkout --ours dvc.yaml`) and run
|
|
26
|
+
`calkit status`, which rewrites it.
|
|
27
|
+
|
|
28
|
+
Done by you, with judgment: everything else, e.g., source code,
|
|
29
|
+
`calkit.yaml`, and manuscripts. When the two sides disagree about intent,
|
|
30
|
+
not just text, ask the user.
|
|
31
|
+
|
|
32
|
+
## Starting a merge
|
|
33
|
+
|
|
34
|
+
Prefer `calkit merge BRANCH` to `git merge`. It sets up the merge driver,
|
|
35
|
+
resolves what it can, and checks out DVC-tracked outputs afterwards. Most
|
|
36
|
+
stages merge without a conflict: one changed on one side only takes that
|
|
37
|
+
side.
|
|
38
|
+
|
|
39
|
+
## Stages changed on both sides
|
|
40
|
+
|
|
41
|
+
Each needs one choice, which decides its `dvc.lock` entry, run record, and
|
|
42
|
+
Git-tracked outputs together:
|
|
43
|
+
|
|
44
|
+
- `ours` or `theirs`: keep that side's result.
|
|
45
|
+
- `rerun`: keep neither, so `calkit run` reruns it.
|
|
46
|
+
|
|
47
|
+
You have no terminal for the prompt, so pass choices as options:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
calkit fix merge-conflicts --theirs fit --rerun figures
|
|
51
|
+
calkit fix merge-conflicts --prefer rerun
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Name subproject stages as `calkit check stage-runs` does, e.g.,
|
|
55
|
+
`sub/dvc.yaml:stage`. Run it without options first to list the stages that
|
|
56
|
+
need a choice.
|
|
57
|
+
|
|
58
|
+
To choose, compare each side's inputs with the merged ones. If the merged
|
|
59
|
+
inputs match one side, keep that side. If they match neither, e.g., both
|
|
60
|
+
sides edited the code, the stage will rerun either way, so choose `rerun`.
|
|
61
|
+
If a stage is expensive to run, or its result is shared with others, ask the
|
|
62
|
+
user. During a rebase, "ours" is the branch being rebased onto, and
|
|
63
|
+
"theirs" is the commit being replayed.
|
|
64
|
+
|
|
65
|
+
## Finishing
|
|
66
|
+
|
|
67
|
+
1. Resolve and `git add` the remaining files.
|
|
68
|
+
2. For a merge, run `calkit merge --continue`, with any stage choices still
|
|
69
|
+
needed. For a rebase or cherry-pick, run `calkit fix merge-conflicts`,
|
|
70
|
+
then `git rebase --continue` or `git cherry-pick --continue`.
|
|
71
|
+
3. Run `calkit status`, then `calkit run` if stages are stale.
|
|
72
|
+
4. Run `calkit check stage-runs` to confirm every stage matches its run.
|
calkit/cli/fix.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""CLI for fixing things."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import posixpath
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
import typer
|
|
10
|
+
from typing_extensions import Annotated
|
|
11
|
+
|
|
12
|
+
from calkit.cli import AliasGroup, raise_error, warn
|
|
13
|
+
|
|
14
|
+
fix_app = typer.Typer(cls=AliasGroup, no_args_is_help=True)
|
|
15
|
+
|
|
16
|
+
OursOpt = Annotated[
|
|
17
|
+
list[str],
|
|
18
|
+
typer.Option(
|
|
19
|
+
"--ours",
|
|
20
|
+
"-o",
|
|
21
|
+
help="Keep our side of this stage (repeatable).",
|
|
22
|
+
show_default=False,
|
|
23
|
+
),
|
|
24
|
+
]
|
|
25
|
+
TheirsOpt = Annotated[
|
|
26
|
+
list[str],
|
|
27
|
+
typer.Option(
|
|
28
|
+
"--theirs",
|
|
29
|
+
"-t",
|
|
30
|
+
help="Keep their side of this stage (repeatable).",
|
|
31
|
+
show_default=False,
|
|
32
|
+
),
|
|
33
|
+
]
|
|
34
|
+
RerunOpt = Annotated[
|
|
35
|
+
list[str],
|
|
36
|
+
typer.Option(
|
|
37
|
+
"--rerun",
|
|
38
|
+
"-r",
|
|
39
|
+
help="Keep neither side of this stage, so it reruns (repeatable).",
|
|
40
|
+
show_default=False,
|
|
41
|
+
),
|
|
42
|
+
]
|
|
43
|
+
PreferOpt = Annotated[
|
|
44
|
+
str | None,
|
|
45
|
+
typer.Option(
|
|
46
|
+
"--prefer",
|
|
47
|
+
help="Choice for every other stage changed on both sides "
|
|
48
|
+
"(ours, theirs, or rerun).",
|
|
49
|
+
),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def resolve_merge_conflicts(
|
|
54
|
+
ours: list[str] | None = None,
|
|
55
|
+
theirs: list[str] | None = None,
|
|
56
|
+
rerun: list[str] | None = None,
|
|
57
|
+
prefer: str | None = None,
|
|
58
|
+
) -> dict:
|
|
59
|
+
"""Resolve conflicts in the files Calkit merges, asking for choices.
|
|
60
|
+
|
|
61
|
+
Returns what ``calkit.merge.resolve_conflicts`` does.
|
|
62
|
+
"""
|
|
63
|
+
import calkit.git
|
|
64
|
+
import calkit.merge
|
|
65
|
+
|
|
66
|
+
if prefer is not None and prefer not in calkit.merge.CHOICES:
|
|
67
|
+
raise_error(
|
|
68
|
+
f"--prefer must be one of {', '.join(calkit.merge.CHOICES)}"
|
|
69
|
+
)
|
|
70
|
+
choices: dict[str, str] = {}
|
|
71
|
+
for choice, names in [
|
|
72
|
+
(calkit.merge.OURS, ours),
|
|
73
|
+
(calkit.merge.THEIRS, theirs),
|
|
74
|
+
(calkit.merge.RERUN, rerun),
|
|
75
|
+
]:
|
|
76
|
+
for name in names or []:
|
|
77
|
+
if name in choices and choices[name] != choice:
|
|
78
|
+
raise_error(f"Stage '{name}' was given more than one choice")
|
|
79
|
+
choices[name] = choice
|
|
80
|
+
repo = calkit.git.get_repo()
|
|
81
|
+
operation = calkit.merge.get_operation(repo)
|
|
82
|
+
if operation is None:
|
|
83
|
+
raise_error("No merge, rebase, or cherry-pick is in progress")
|
|
84
|
+
assert operation is not None
|
|
85
|
+
labels = {}
|
|
86
|
+
for side, ref in [("ours", "HEAD"), ("theirs", operation[1])]:
|
|
87
|
+
labels[side] = repo.git.log("-1", "--format=%h %s", ref)
|
|
88
|
+
|
|
89
|
+
def choose(name: str, info: dict) -> str:
|
|
90
|
+
if not sys.stdin.isatty():
|
|
91
|
+
raise_error(
|
|
92
|
+
f"Stage '{name}' changed on both sides; choose with --ours, "
|
|
93
|
+
"--theirs, --rerun, or --prefer"
|
|
94
|
+
)
|
|
95
|
+
typer.echo(f"\nStage '{name}' changed on both sides:")
|
|
96
|
+
for side in ("ours", "theirs"):
|
|
97
|
+
typer.echo(f" {side} ({labels[side]}): {info[side]}")
|
|
98
|
+
if info["changed_deps"]:
|
|
99
|
+
typer.echo(
|
|
100
|
+
" Inputs that differ: " + ", ".join(info["changed_deps"])
|
|
101
|
+
)
|
|
102
|
+
answer = typer.prompt(
|
|
103
|
+
"Keep [o]urs, [t]heirs, or [r]erun it",
|
|
104
|
+
type=click.Choice(["o", "t", "r"]),
|
|
105
|
+
show_choices=False,
|
|
106
|
+
)
|
|
107
|
+
return {"o": "ours", "t": "theirs", "r": "rerun"}[answer]
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
result = calkit.merge.resolve_conflicts(
|
|
111
|
+
repo, choices=choices, prefer=prefer, choose=choose
|
|
112
|
+
)
|
|
113
|
+
except calkit.merge.MergeError as e:
|
|
114
|
+
raise_error(str(e))
|
|
115
|
+
unknown = sorted(set(choices) - set(result["choices"]))
|
|
116
|
+
for name in unknown:
|
|
117
|
+
warn(
|
|
118
|
+
f"Stage '{name}' wasn't changed on both sides; ignoring its choice"
|
|
119
|
+
)
|
|
120
|
+
for path in result["resolved"]:
|
|
121
|
+
typer.echo(f"Resolved {path}")
|
|
122
|
+
rerun_names = [
|
|
123
|
+
n for n, c in result["choices"].items() if c == calkit.merge.RERUN
|
|
124
|
+
]
|
|
125
|
+
if rerun_names:
|
|
126
|
+
typer.echo(
|
|
127
|
+
"Run 'calkit run' to rerun " + ", ".join(sorted(rerun_names))
|
|
128
|
+
)
|
|
129
|
+
if result["unmerged"]:
|
|
130
|
+
typer.echo("Still in conflict:")
|
|
131
|
+
for path in result["unmerged"]:
|
|
132
|
+
typer.echo(f" {path}")
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def checkout_dvc_outputs() -> None:
|
|
137
|
+
"""Check out DVC-tracked outputs to match dvc.lock."""
|
|
138
|
+
import calkit.dvc
|
|
139
|
+
import calkit.dvc.zip
|
|
140
|
+
|
|
141
|
+
if calkit.dvc.run_dvc_command(["checkout"]) != 0:
|
|
142
|
+
warn(
|
|
143
|
+
"Some DVC-tracked outputs couldn't be checked out; run "
|
|
144
|
+
"'calkit dvc pull' to fetch them"
|
|
145
|
+
)
|
|
146
|
+
calkit.dvc.zip.sync_all(direction="to-workspace")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@fix_app.command(name="merge-conflicts")
|
|
150
|
+
def fix_merge_conflicts(
|
|
151
|
+
ours: OursOpt = [],
|
|
152
|
+
theirs: TheirsOpt = [],
|
|
153
|
+
rerun: RerunOpt = [],
|
|
154
|
+
prefer: PreferOpt = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Resolve merge conflicts in dvc.lock and stage run records.
|
|
157
|
+
|
|
158
|
+
Each stage is merged whole. A stage changed on one side takes that side,
|
|
159
|
+
and one changed on both needs a choice, which also decides its run record
|
|
160
|
+
and Git-tracked outputs. Stages are named as in 'calkit check
|
|
161
|
+
stage-runs', e.g., 'sub/dvc.yaml:stage' for a subproject's.
|
|
162
|
+
"""
|
|
163
|
+
result = resolve_merge_conflicts(
|
|
164
|
+
ours=ours, theirs=theirs, rerun=rerun, prefer=prefer
|
|
165
|
+
)
|
|
166
|
+
operation = result["operation"]
|
|
167
|
+
unmerged = result["unmerged"]
|
|
168
|
+
finish = {
|
|
169
|
+
"merge": "calkit merge --continue",
|
|
170
|
+
"squash": "calkit merge --continue",
|
|
171
|
+
"rebase": "git rebase --continue",
|
|
172
|
+
"cherry-pick": "git cherry-pick --continue",
|
|
173
|
+
}[operation]
|
|
174
|
+
# DVC can't read the pipeline while a dvc.yaml is in conflict, and
|
|
175
|
+
# 'calkit merge --continue' checks outputs out after committing anyway
|
|
176
|
+
steps = [finish]
|
|
177
|
+
if any(posixpath.basename(p) == "dvc.yaml" for p in unmerged):
|
|
178
|
+
if operation not in ("merge", "squash"):
|
|
179
|
+
steps.insert(0, "calkit dvc checkout")
|
|
180
|
+
else:
|
|
181
|
+
checkout_dvc_outputs()
|
|
182
|
+
then = " and ".join(f"'{step}'" for step in steps)
|
|
183
|
+
if unmerged:
|
|
184
|
+
raise_error(f"Resolve the rest and 'git add' them, then run {then}")
|
|
185
|
+
typer.echo(f"Run {then} to finish")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@fix_app.command(name="storage")
|
|
189
|
+
def fix_storage() -> None:
|
|
190
|
+
"""Move paths to where they're declared to be stored (Git or DVC).
|
|
191
|
+
|
|
192
|
+
Same as 'calkit check storage --fix'.
|
|
193
|
+
"""
|
|
194
|
+
from calkit.cli.check import check_storage
|
|
195
|
+
|
|
196
|
+
check_storage(fix=True, as_json=False)
|
calkit/cli/main/core.py
CHANGED
|
@@ -26,6 +26,7 @@ import typer
|
|
|
26
26
|
from typing_extensions import Annotated, Optional
|
|
27
27
|
|
|
28
28
|
import calkit
|
|
29
|
+
import calkit.merge
|
|
29
30
|
from calkit import (
|
|
30
31
|
AUTO_IGNORE_PATHS,
|
|
31
32
|
AUTO_IGNORE_PREFIXES,
|
|
@@ -54,6 +55,13 @@ from calkit.cli.config import config_app
|
|
|
54
55
|
from calkit.cli.delete import delete_app
|
|
55
56
|
from calkit.cli.describe import describe_app
|
|
56
57
|
from calkit.cli.dev import dev_app
|
|
58
|
+
from calkit.cli.fix import (
|
|
59
|
+
OursOpt,
|
|
60
|
+
PreferOpt,
|
|
61
|
+
RerunOpt,
|
|
62
|
+
TheirsOpt,
|
|
63
|
+
fix_app,
|
|
64
|
+
)
|
|
57
65
|
from calkit.cli.hub import hub_app
|
|
58
66
|
from calkit.cli.import_ import import_app
|
|
59
67
|
from calkit.cli.latex import latex_app
|
|
@@ -89,6 +97,7 @@ app.add_typer(import_app, name="import", help="Import objects.")
|
|
|
89
97
|
app.add_typer(office_app, name="office", help="Work with Microsoft Office.")
|
|
90
98
|
app.add_typer(update_app, name="update", help="Update objects.")
|
|
91
99
|
app.add_typer(check_app, name="check", help="Check things.")
|
|
100
|
+
app.add_typer(fix_app, name="fix", help="Fix things.")
|
|
92
101
|
app.add_typer(latex_app, name="latex|tex", help="Work with LaTeX.")
|
|
93
102
|
app.add_typer(overleaf_app, name="overleaf|ol", help="Interact with Overleaf.")
|
|
94
103
|
app.add_typer(hub_app, name="hub|cloud", help="Interact with a Calkit hub.")
|
|
@@ -413,6 +422,8 @@ def clone(
|
|
|
413
422
|
location = url.split("/")[-1].removesuffix(".git")
|
|
414
423
|
typer.echo(f"Moving into repo dir: {location}")
|
|
415
424
|
os.chdir(location)
|
|
425
|
+
# So Git merges dvc.lock and stage run records stage by stage
|
|
426
|
+
calkit.merge.configure_driver()
|
|
416
427
|
# Setup auth for any Calkit remotes
|
|
417
428
|
if not no_config_remote:
|
|
418
429
|
remotes = calkit.dvc.get_remotes()
|
|
@@ -855,6 +866,19 @@ def get_status(
|
|
|
855
866
|
if targets:
|
|
856
867
|
git_cmd += ["--"] + targets
|
|
857
868
|
run_cmd(git_cmd)
|
|
869
|
+
try:
|
|
870
|
+
repo = calkit.git.get_repo()
|
|
871
|
+
unmerged = calkit.merge.get_unmerged_paths(repo)
|
|
872
|
+
operation = calkit.merge.get_operation(repo)
|
|
873
|
+
except Exception:
|
|
874
|
+
unmerged, operation = [], None
|
|
875
|
+
if operation is not None and any(
|
|
876
|
+
calkit.merge.is_merged_file(p) for p in unmerged
|
|
877
|
+
):
|
|
878
|
+
typer.echo(
|
|
879
|
+
"Run 'calkit fix merge-conflicts' to resolve the conflicts "
|
|
880
|
+
"in dvc.lock and stage run records"
|
|
881
|
+
)
|
|
858
882
|
typer.echo()
|
|
859
883
|
if "dvc" in categories:
|
|
860
884
|
print_sep("DVC")
|
|
@@ -1819,12 +1843,23 @@ def pull(
|
|
|
1819
1843
|
dvc_args.append("-f")
|
|
1820
1844
|
if not no_git:
|
|
1821
1845
|
typer.echo("Git pulling")
|
|
1846
|
+
# So Git merges dvc.lock and stage run records stage by stage
|
|
1847
|
+
calkit.merge.configure_driver()
|
|
1822
1848
|
try:
|
|
1823
1849
|
git_cmd = ["git", "pull"]
|
|
1824
1850
|
if not no_recursive and "--recurse-submodules" not in git_args:
|
|
1825
1851
|
git_cmd.append("--recurse-submodules")
|
|
1826
1852
|
subprocess.check_call(git_cmd + git_args)
|
|
1827
1853
|
except subprocess.CalledProcessError:
|
|
1854
|
+
repo = calkit.git.get_repo()
|
|
1855
|
+
if calkit.merge.get_operation(repo) is not None and any(
|
|
1856
|
+
calkit.merge.is_merged_file(p)
|
|
1857
|
+
for p in calkit.merge.get_unmerged_paths(repo)
|
|
1858
|
+
):
|
|
1859
|
+
typer.echo(
|
|
1860
|
+
"Run 'calkit fix merge-conflicts' to resolve the "
|
|
1861
|
+
"conflicts in dvc.lock and stage run records"
|
|
1862
|
+
)
|
|
1828
1863
|
raise_error("Git pull failed")
|
|
1829
1864
|
if not no_dvc:
|
|
1830
1865
|
typer.echo("DVC pulling")
|
|
@@ -3133,6 +3168,8 @@ def run(
|
|
|
3133
3168
|
subprocess.check_call(["git", "init"])
|
|
3134
3169
|
except subprocess.CalledProcessError:
|
|
3135
3170
|
raise_error("Failed to initialize Git repo")
|
|
3171
|
+
# So Git merges dvc.lock and stage run records stage by stage
|
|
3172
|
+
calkit.merge.configure_driver()
|
|
3136
3173
|
# Set env vars
|
|
3137
3174
|
calkit.set_env_vars(ck_info=ck_info)
|
|
3138
3175
|
# Stages that can't run here are skipped, keeping their outputs, and
|
|
@@ -5073,6 +5110,133 @@ def switch_branch(
|
|
|
5073
5110
|
calkit.dvc.zip.sync_all(direction="to-workspace")
|
|
5074
5111
|
|
|
5075
5112
|
|
|
5113
|
+
@app.command(name="merge")
|
|
5114
|
+
def merge(
|
|
5115
|
+
branch: Annotated[
|
|
5116
|
+
str | None,
|
|
5117
|
+
typer.Argument(help="Branch or commit to merge into the current one."),
|
|
5118
|
+
] = None,
|
|
5119
|
+
cont: Annotated[
|
|
5120
|
+
bool,
|
|
5121
|
+
typer.Option(
|
|
5122
|
+
"--continue", help="Finish a merge stopped by conflicts."
|
|
5123
|
+
),
|
|
5124
|
+
] = False,
|
|
5125
|
+
abort: Annotated[
|
|
5126
|
+
bool, typer.Option("--abort", help="Abort the merge in progress.")
|
|
5127
|
+
] = False,
|
|
5128
|
+
squash: Annotated[
|
|
5129
|
+
bool,
|
|
5130
|
+
typer.Option(
|
|
5131
|
+
"--squash", help="Combine the branch's changes into one commit."
|
|
5132
|
+
),
|
|
5133
|
+
] = False,
|
|
5134
|
+
message: Annotated[
|
|
5135
|
+
str | None,
|
|
5136
|
+
typer.Option(
|
|
5137
|
+
"--message",
|
|
5138
|
+
"-m",
|
|
5139
|
+
help="Commit message (default: Git's for the merge or squash).",
|
|
5140
|
+
),
|
|
5141
|
+
] = None,
|
|
5142
|
+
ours: OursOpt = [],
|
|
5143
|
+
theirs: TheirsOpt = [],
|
|
5144
|
+
rerun: RerunOpt = [],
|
|
5145
|
+
prefer: PreferOpt = None,
|
|
5146
|
+
git_args: Annotated[
|
|
5147
|
+
list[str],
|
|
5148
|
+
typer.Option("--git-arg", help="Additional Git merge args."),
|
|
5149
|
+
] = [],
|
|
5150
|
+
) -> None:
|
|
5151
|
+
"""Merge a branch, resolving dvc.lock and stage run records per stage.
|
|
5152
|
+
|
|
5153
|
+
A stage changed on both sides needs a choice: ours, theirs, or rerun,
|
|
5154
|
+
which also decides its run record and Git-tracked outputs. DVC-tracked
|
|
5155
|
+
outputs are checked out to match afterwards. Other conflicts are left
|
|
5156
|
+
to resolve before 'calkit merge --continue'.
|
|
5157
|
+
"""
|
|
5158
|
+
from calkit.cli.fix import checkout_dvc_outputs, resolve_merge_conflicts
|
|
5159
|
+
|
|
5160
|
+
if sum([branch is not None, cont, abort]) != 1:
|
|
5161
|
+
raise_error("Give a branch, --continue, or --abort")
|
|
5162
|
+
if squash and branch is None:
|
|
5163
|
+
raise_error("--squash needs a branch")
|
|
5164
|
+
repo = calkit.git.get_repo()
|
|
5165
|
+
|
|
5166
|
+
def commit() -> None:
|
|
5167
|
+
cmd = ["git", "commit"] + (
|
|
5168
|
+
["-m", message] if message else ["--no-edit"]
|
|
5169
|
+
)
|
|
5170
|
+
if subprocess.call(cmd) != 0:
|
|
5171
|
+
raise_error("Git commit failed")
|
|
5172
|
+
checkout_dvc_outputs()
|
|
5173
|
+
|
|
5174
|
+
if abort:
|
|
5175
|
+
op = calkit.merge.get_operation(repo)
|
|
5176
|
+
# A squash merge has no merge to abort, only changes to reset
|
|
5177
|
+
if op is not None and op[0] == "squash":
|
|
5178
|
+
cmd = ["git", "reset", "--merge"]
|
|
5179
|
+
else:
|
|
5180
|
+
cmd = ["git", "merge", "--abort"]
|
|
5181
|
+
if subprocess.call(cmd) != 0:
|
|
5182
|
+
raise_error("Git merge abort failed")
|
|
5183
|
+
checkout_dvc_outputs()
|
|
5184
|
+
return
|
|
5185
|
+
if branch is not None:
|
|
5186
|
+
calkit.merge.configure_driver()
|
|
5187
|
+
cmd = ["git", "merge", "--no-edit"]
|
|
5188
|
+
if squash:
|
|
5189
|
+
cmd.append("--squash")
|
|
5190
|
+
elif message:
|
|
5191
|
+
cmd += ["-m", message]
|
|
5192
|
+
if subprocess.call(cmd + git_args + [branch]) == 0:
|
|
5193
|
+
# A squash merge stops short of committing
|
|
5194
|
+
staged = subprocess.call(["git", "diff", "--cached", "--quiet"])
|
|
5195
|
+
if squash and staged:
|
|
5196
|
+
commit()
|
|
5197
|
+
else:
|
|
5198
|
+
checkout_dvc_outputs()
|
|
5199
|
+
return
|
|
5200
|
+
op = calkit.merge.get_operation(repo)
|
|
5201
|
+
if op is None or op[0] not in ("merge", "squash"):
|
|
5202
|
+
raise_error(
|
|
5203
|
+
"Git merge failed"
|
|
5204
|
+
if branch is not None
|
|
5205
|
+
else "No merge is in progress"
|
|
5206
|
+
)
|
|
5207
|
+
if any(
|
|
5208
|
+
calkit.merge.is_merged_file(p)
|
|
5209
|
+
for p in calkit.merge.get_unmerged_paths(repo)
|
|
5210
|
+
):
|
|
5211
|
+
result = resolve_merge_conflicts(
|
|
5212
|
+
ours=ours, theirs=theirs, rerun=rerun, prefer=prefer
|
|
5213
|
+
)
|
|
5214
|
+
unmerged = result["unmerged"]
|
|
5215
|
+
else:
|
|
5216
|
+
unmerged = calkit.merge.get_unmerged_paths(repo)
|
|
5217
|
+
if unmerged:
|
|
5218
|
+
typer.echo("Still in conflict:")
|
|
5219
|
+
for path in unmerged:
|
|
5220
|
+
typer.echo(f" {path}")
|
|
5221
|
+
if unmerged:
|
|
5222
|
+
raise_error(
|
|
5223
|
+
"Resolve the conflicts, 'git add' them, then run "
|
|
5224
|
+
"'calkit merge --continue'"
|
|
5225
|
+
)
|
|
5226
|
+
commit()
|
|
5227
|
+
|
|
5228
|
+
|
|
5229
|
+
@app.command(name="merge-driver", hidden=True)
|
|
5230
|
+
def merge_driver(
|
|
5231
|
+
base: Annotated[str, typer.Argument()],
|
|
5232
|
+
ours: Annotated[str, typer.Argument()],
|
|
5233
|
+
theirs: Annotated[str, typer.Argument()],
|
|
5234
|
+
path: Annotated[str, typer.Argument()],
|
|
5235
|
+
) -> None:
|
|
5236
|
+
"""Merge a file whose content is grouped by stage, as Git's merge driver."""
|
|
5237
|
+
raise typer.Exit(calkit.merge.run_driver(base, ours, theirs, path))
|
|
5238
|
+
|
|
5239
|
+
|
|
5076
5240
|
@app.command(name="stash")
|
|
5077
5241
|
def stash(
|
|
5078
5242
|
pop: Annotated[
|