Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,10 +376,10 @@ Usage: `git-sim check-ignore [-v] <path>...`
<summary><b><code>git checkout</code></b>: switch branches, or create one</summary>
<br>

Usage: `git-sim checkout [-b] <branch>`
Usage: `git-sim checkout [-b] <branch> [<start-point>]`

- Checks out `<branch>` into the working directory, i.e. moves `HEAD` to the specified `<branch>`
- The `-b` flag creates a new branch with the specified name `<branch>` and checks it out, assuming it doesn't already exist
- The `-b` flag creates a new branch with the specified name `<branch>` and checks it out, assuming it doesn't already exist. An optional `<start-point>` (a branch, tag, or commit) starts it there instead of at `HEAD`; a remote-tracking branch such as `origin/x` becomes its upstream

[![git-sim checkout fix/order-totals](https://raw.githubusercontent.com/initialcommit-com/git-sim/main/docs/img/checkout.svg)](https://initialcommit.com/tools/git-sim/viewer?demo=checkout)

Expand Down
44 changes: 42 additions & 2 deletions src/git_sim/checkout.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ def __init__(self, paths):


class Checkout(GitSimBaseCommand):
def __init__(self, branch: str, b: bool):
def __init__(self, branch: str, b: bool, start_point: str = None):
super().__init__()
self.branch = branch
self.b = b
self.start_point = start_point
self.track = None

if self.b:
if self.branch in self.repo.heads:
Expand All @@ -79,6 +81,14 @@ def __init__(self, branch: str, b: bool):
+ "', it already exists"
)
sys.exit(1)
if self.start_point:
try:
self.repo.commit(self.start_point)
except Exception:
print(f"git-sim error: '{self.start_point}' is not a valid commit to start the branch at")
sys.exit(1)
if self.start_point in self.get_remote_tracking_branches():
self.track = self.start_point
else:
try:
git.repo.fun.rev_parse(self.repo, self.branch)
Expand Down Expand Up @@ -116,6 +126,7 @@ def __init__(self, branch: str, b: bool):

self.cmd += (
f"{type(self).__name__.lower()}{' -b' if self.b else ''} {self.branch}"
f"{' ' + self.start_point if self.start_point else ''}"
)

def construct(self):
Expand All @@ -125,8 +136,10 @@ def construct(self):
self.show_intro()
head_commit = self.get_commit()

if self.b and self.start_point:
self.create_elsewhere(head_commit)
# using -b flag, create new branch label and exit
if self.b:
elif self.b:
self.parse_commits(head_commit)
self.recenter_frame()
self.scale_frame()
Expand Down Expand Up @@ -176,3 +189,30 @@ def construct(self):
self.fadeout()
self.show_command_as_title()
self.show_outro()

def create_elsewhere(self, head_commit):
"""Create the branch at its start point and move HEAD onto it."""
target = self.repo.commit(self.start_point)
self.parse_commits(head_commit)
if target.hexsha not in self.drawnCommits:
# Leave room for HEAD, the new branch and any remote label below
# the lowest row already drawn by HEAD's history.
gap = 4 if self.compact else 5
top = self.drawnCommits[head_commit.hexsha].get_center()[1] if head_commit.hexsha in self.drawnCommits else 0.0
lowest = min((c.get_center()[1] for c in self.drawnCommits.values()), default=top)
self.parse_commits(target, shift=(top - lowest + gap) * m.DOWN)
drawn = target.hexsha in self.drawnCommits
if drawn and self.track and self.track not in self.drawnRefs:
self.draw_ref(target, self.stack_top(target.hexsha), text=self.track, color=self.theme.remote, kind="remote", phase="before")
self.recenter_frame()
self.scale_frame()
if drawn:
self.draw_ref(target, self.stack_top(target.hexsha), text=self.branch, color=self.theme.branch)
if target.hexsha != head_commit.hexsha:
self.reset_head(target.hexsha)
if not self.repo.head.is_detached:
self.reset_branch(head_commit.hexsha)
notes = [f"Creates {self.branch} at {self.start_point} ({target.hexsha[:6]}) and checks it out."]
if self.track:
notes.append(f"{self.track} is a remote-tracking branch, so it becomes {self.branch}'s upstream.")
self.add_notes(notes)
11 changes: 7 additions & 4 deletions src/git_sim/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,12 +137,12 @@ def branch(
def checkout(
branch: List[str] = typer.Argument(
default=None,
help="The name of the branch to checkout, or after -- the files whose changes to discard (git checkout -- <paths>)",
help="The branch to checkout, with an optional start point (branch, tag, or commit) after the new branch name with -b, or after -- the files whose changes to discard",
),
b: bool = typer.Option(
False,
"-b",
help="Create the specified branch if it doesn't already exist",
help="Create and check out a new branch at HEAD or the optional start point",
),
):
import sys
Expand All @@ -168,10 +168,13 @@ def checkout(
scene = CheckoutFiles(paths=paths)
else:
_need(revisions, "name the branch to check out, or the files after --")
if len(revisions) > 1:
if b and after is None and len(revisions) > 2:
print("git-sim error: git checkout -b takes a new branch and at most one start point")
raise typer.Exit(1)
if len(revisions) > 1 and not (b and after is None):
print("git-sim error: git checkout takes one branch or commit")
raise typer.Exit(1)
scene = Checkout(branch=revisions[0], b=b)
scene = Checkout(branch=revisions[0], b=b, start_point=revisions[1] if len(revisions) == 2 else None)
handle_animations(scene=scene)


Expand Down
13 changes: 13 additions & 0 deletions tests/unit_tests/test_scenes.py
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,19 @@ def test_switch_create_at_a_start_point(repo, tmp_path):
Switch(branch="topic", c=True, detach=False, start_point="nope")


def test_checkout_create_at_a_start_point(repo):
from git_sim.checkout import Checkout

head = run_git(repo, "rev-parse", "HEAD").strip()
target = run_git(repo, "rev-parse", "HEAD~3").strip()
short = run_git(repo, "rev-parse", "--short=8", "HEAD~3").strip()
scene = Checkout(branch="topic", b=True, start_point=short)
assert scene.cmd == f"git checkout -b topic {short}"
scene.construct()
assert ref_commit(scene, "topic") == ref_commit(scene, "HEAD") == target
assert ref_commit(scene, "main") == head


def test_annotated_tag_draws_the_tag_object(repo):
from git_sim.tag import Tag

Expand Down
4 changes: 4 additions & 0 deletions tests/validation/cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,10 @@ def check(m, repo):
# checkout
Case("checkout-branch", "classic", ["checkout", "branch2"], all_of(title("git checkout branch2"), relabelled("HEAD"))),
Case("checkout-new", "classic", ["checkout", "-b", "topic"], ref_after("topic", "branch")),
Case("checkout-new-at", "classic", ["checkout", "-b", "topic", "branch2"], all_of(title("git checkout -b topic branch2"), ref_after("topic", "branch"), relabelled("HEAD"))),
Case("checkout-new-tracking", "remote-branch", ["checkout", "-b", "idea", "origin/old-idea"], all_of(title("git checkout -b idea origin/old-idea"), ref_after("idea", "branch"), relabelled("HEAD"), texts("becomes idea's upstream"))),
Case("checkout-new-bad-start", "classic", ["checkout", "-b", "topic", "nope"], error="not a valid commit"),
Case("checkout-new-two-starts", "classic", ["checkout", "-b", "topic", "main", "branch2"], error="at most one start point"),
Case("checkout-tag", "release", ["checkout", "v1.0.0"], relabelled("HEAD")),
Case("checkout-commit", "classic", ["checkout", "HEAD~2"], relabelled("HEAD")),
Case("checkout-missing", "classic", ["checkout", "nope"], error="git-sim error"),
Expand Down
175 changes: 175 additions & 0 deletions tests/validation/golden/checkout-new-at.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
{
"commits": {
"Dummy commit #10 on branch1": {
"kind": "commit",
"parents": [
"Dummy commit #9 on branch1"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Dummy commit #10 on branch2": {
"kind": "commit",
"parents": [
"Dummy commit #9 on branch2"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Dummy commit #10 on main": {
"kind": "commit",
"parents": [
"Dummy commit #9 on main"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Dummy commit #6 on branch2": {
"kind": "commit",
"parents": [
"?"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Dummy commit #7 on branch1": {
"kind": "commit",
"parents": [
"?"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Dummy commit #7 on branch2": {
"kind": "commit",
"parents": [
"Dummy commit #6 on branch2"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Dummy commit #7 on main": {
"kind": "commit",
"parents": [
"?"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Dummy commit #8 on branch1": {
"kind": "commit",
"parents": [
"Dummy commit #7 on branch1"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Dummy commit #8 on branch2": {
"kind": "commit",
"parents": [
"Dummy commit #7 on branch2"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Dummy commit #8 on main": {
"kind": "commit",
"parents": [
"Dummy commit #7 on main"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Dummy commit #9 on branch1": {
"kind": "commit",
"parents": [
"Dummy commit #8 on branch1"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Dummy commit #9 on branch2": {
"kind": "commit",
"parents": [
"Dummy commit #8 on branch2"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Dummy commit #9 on main": {
"kind": "commit",
"parents": [
"Dummy commit #8 on main"
],
"phase": "before",
"recolored": false,
"step": 0
},
"Merge branch1 into main": {
"kind": "merge",
"parents": [
"Dummy commit #10 on branch1",
"Dummy commit #10 on main"
],
"phase": "before",
"recolored": false,
"step": 0
}
},
"edges": {
"after": 0,
"before": 24,
"origin": 0,
"removed": 0
},
"files": [],
"notes": [
"Creates topic at branch2 (573d8f) and checks it out."
],
"refs": {
"HEAD": {
"count": 2,
"kind": "head",
"moved": true,
"phase": "before"
},
"branch1": {
"count": 2,
"kind": "branch",
"moved": false,
"phase": "before"
},
"branch2": {
"count": 2,
"kind": "branch",
"moved": false,
"phase": "before"
},
"main": {
"count": 2,
"kind": "branch",
"moved": true,
"phase": "before"
},
"topic": {
"count": 2,
"kind": "branch",
"moved": false,
"phase": "after"
}
},
"steps": 2,
"title": "git checkout -b topic branch2"
}
Loading