Skip to content

Commit

Permalink
merge: Add --into <ref>
Browse files Browse the repository at this point in the history
Adds the ability to merge into a branch that isn't pointed at by HEAD
  • Loading branch information
craigds committed Nov 15, 2024
1 parent 764953a commit 7889787
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 5 deletions.
30 changes: 25 additions & 5 deletions kart/merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,15 @@ def get_commit_message(


def do_merge(
repo, ff, ff_only, dry_run, commit, message, launch_editor=True, quiet=False
repo,
ff,
ff_only,
dry_run,
commit,
message,
into="HEAD",
launch_editor=True,
quiet=False,
):
"""Does a merge, but doesn't update the working copy."""
if ff_only and not ff:
Expand All @@ -69,9 +77,12 @@ def do_merge(

# accept ref-ish things (refspec, branch, commit)
theirs = CommitWithReference.resolve(repo, commit)
ours = CommitWithReference.resolve(repo, "HEAD")
ours = CommitWithReference.resolve(repo, into)
ancestor_id = repo.merge_base(theirs.id, ours.id)

if not ours.reference:
raise click.BadParameter(f"--into: Ref {into!r} doesn't exist")

if not ancestor_id:
raise InvalidOperation(f"Commits {theirs.id} and {ours.id} aren't related.")

Expand Down Expand Up @@ -110,7 +121,7 @@ def do_merge(
merge_jdict["commit"] = theirs.id.hex
merge_jdict["fastForward"] = True
if not dry_run:
repo.head.set_target(
ours.reference.set_target(
theirs.id, f"{merge_context.get_message()}: Fast-forward"
)
return merge_jdict
Expand Down Expand Up @@ -155,7 +166,7 @@ def do_merge(
quiet=quiet,
)
merge_commit_id = repo.create_commit(
repo.head.name,
ours.reference.name,
user,
user,
message,
Expand Down Expand Up @@ -329,6 +340,12 @@ def complete_merging_state(ctx):
help="Use the given message as the commit message.",
is_eager=True, # -m is eager and --continue is non-eager so we can access -m from complete_merging_state callback.
)
@click.option(
"--into",
help="Merge into the given ref instead of the currently active branch.",
hidden=True,
default="HEAD",
)
@click.option(
" /--no-editor",
"launch_editor",
Expand All @@ -351,7 +368,9 @@ def complete_merging_state(ctx):
)
@click.argument("commit", required=True, metavar="COMMIT")
@click.pass_context
def merge(ctx, ff, ff_only, dry_run, message, launch_editor, output_format, commit):
def merge(
ctx, ff, ff_only, dry_run, message, into, launch_editor, output_format, commit
):
"""Incorporates changes from the named commits (usually other branch heads) into the current branch."""

repo = ctx.obj.get_repo(
Expand All @@ -369,6 +388,7 @@ def merge(ctx, ff, ff_only, dry_run, message, launch_editor, output_format, comm
dry_run,
commit,
message,
into=into,
launch_editor=launch_editor,
quiet=do_json,
)
Expand Down
63 changes: 63 additions & 0 deletions tests/test_merge.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
import pytest

import pygit2

from kart.exceptions import SUCCESS, INVALID_OPERATION, NO_CONFLICT
from kart.merge_util import (
MergedIndex,
Expand Down Expand Up @@ -360,3 +362,64 @@ def test_merge_state_lock(data_archive, cli_runner):
assert r.exit_code == SUCCESS
r = cli_runner.invoke(["resolve", "dummy_conflict", "--with=delete"])
assert r.exit_code == NO_CONFLICT # "dummy_conflict" is not a real conflict


def test_merge_into_branch(data_archive, tmp_path, cli_runner):
with data_archive("points") as repo_path:
# create two branches and put a commit on each
r = cli_runner.invoke(["branch", "b1", "main"])
assert r.exit_code == 0, r.stderr
r = cli_runner.invoke(
["commit-files", f"--ref=refs/heads/b1", "-m", "B1", "a=1"]
)
assert r.exit_code == 0, r.stderr
r = cli_runner.invoke(["branch", "b2", "main"])
assert r.exit_code == 0, r.stderr
r = cli_runner.invoke(
["commit-files", f"--ref=refs/heads/b2", "-m", "B2", "b=1"]
)
assert r.exit_code == 0, r.stderr

# Merge b1 into b2, even though main is still checked out
r = cli_runner.invoke(["merge", "--into=b2", "b1", "-m", "merged"])
assert r.exit_code == 0, r.stderr

repo = KartRepo(repo_path)
# HEAD is unchanged
assert repo.head.name == "refs/heads/main"
head_commit = repo.head_commit
assert head_commit.message == "Improve naming on Coromandel East coast"

# b2 ref has a merge commit on it
b2_commit = repo.references["refs/heads/b2"].peel(pygit2.Commit)
assert len(b2_commit.parents) == 2
assert b2_commit.message == "merged"


def test_merge_into_branch_fastforward(data_archive, tmp_path, cli_runner):
with data_archive("points") as repo_path:
# create two branches and put a commit on one of them
r = cli_runner.invoke(["branch", "b1", "main"])
assert r.exit_code == 0, r.stderr
r = cli_runner.invoke(
["commit-files", f"--ref=refs/heads/b1", "-m", "B1", "a=1"]
)
assert r.exit_code == 0, r.stderr
r = cli_runner.invoke(["branch", "b2", "main"])
assert r.exit_code == 0, r.stderr

# Merge b1 into b2, even though main is still checked out
r = cli_runner.invoke(["merge", "--into=b2", "b1"])
assert r.exit_code == 0, r.stderr

repo = KartRepo(repo_path)
b1_commit = repo.references["refs/heads/b1"].peel(pygit2.Commit)

# HEAD is unchanged
assert repo.head.name == "refs/heads/main"
head_commit = repo.head_commit
assert head_commit.message == "Improve naming on Coromandel East coast"

# b2 ref is now the same as b1 (because it was fastforwarded)
b2_commit = repo.references["refs/heads/b2"].peel(pygit2.Commit)
assert b2_commit.hex == b1_commit.hex

0 comments on commit 7889787

Please sign in to comment.