diff --git a/lua/neogit/lib/git/status.lua b/lua/neogit/lib/git/status.lua index 2d4220bea..e83e0eaaf 100644 --- a/lua/neogit/lib/git/status.lua +++ b/lua/neogit/lib/git/status.lua @@ -153,6 +153,10 @@ local status = { unstage_all = function() git.cli.reset.call() end, + is_dirty = function() + local repo = require("neogit.lib.git.repository") + return #repo.staged.items > 0 or #repo.unstaged.items > 0 + end, } status.register = function(meta) diff --git a/lua/neogit/popups/branch/actions.lua b/lua/neogit/popups/branch/actions.lua index e7a41833f..40e751542 100644 --- a/lua/neogit/popups/branch/actions.lua +++ b/lua/neogit/popups/branch/actions.lua @@ -22,6 +22,46 @@ local function parse_remote_branch_name(ref) return remote, branch_name end +local function spin_off_branch(checkout) + if git.status.is_dirty() and not checkout then + notif.create("Staying on HEAD due to uncommitted changes", vim.log.levels.INFO) + checkout = true + end + + local name = input.get_user_input("branch > ") + if not name or name == "" then + return + end + + name, _ = name:gsub("%s", "-") + git.branch.create(name) + + local current_branch_name = git.branch.current_full_name() + + if checkout then + git.cli.checkout.branch(name).call_sync() + end + + local upstream = git.branch.upstream() + if upstream then + if checkout then + git.log.update_ref(current_branch_name, upstream) + else + git.cli.reset.hard.args(upstream).call() + end + end +end + +M.spin_off_branch = operation("spin_off_branch", function() + spin_off_branch(true) + status.refresh(true, "spin_off_branch") +end) + +M.spin_out_branch = operation("spin_out_branch", function() + spin_off_branch(false) + status.refresh(true, "spin_out_branch") +end) + M.checkout_branch_revision = operation("checkout_branch_revision", function(popup) local selected_branch = FuzzyFinderBuffer.new(git.branch.get_all_branches()):open_async() if not selected_branch then diff --git a/lua/neogit/popups/branch/init.lua b/lua/neogit/popups/branch/init.lua index 6d1047f28..f9c7780bd 100644 --- a/lua/neogit/popups/branch/init.lua +++ b/lua/neogit/popups/branch/init.lua @@ -35,11 +35,11 @@ function M.create() :action("l", "local branch", actions.checkout_local_branch) :new_action_group() :action("c", "new branch", actions.checkout_create_branch) - :action("s", "new spin-off") + :action("s", "new spin-off", actions.spin_off_branch) :action("w", "new worktree") :new_action_group("Create") :action("n", "new branch", actions.create_branch) - :action("S", "new spin-out") + :action("S", "new spin-out", actions.spin_out_branch) :action("W", "new worktree") :new_action_group("Do") :action("C", "Configure...", actions.configure_branch) diff --git a/tests/specs/neogit/operations_spec.lua b/tests/specs/neogit/operations_spec.lua index 73c8dd021..160afce99 100644 --- a/tests/specs/neogit/operations_spec.lua +++ b/tests/specs/neogit/operations_spec.lua @@ -1,4 +1,5 @@ -require("plenary.async").tests.add_to_env() +local async = require("plenary.async") +async.tests.add_to_env() local eq = assert.are.same local operations = require("neogit.operations") local harness = require("tests.util.git_harness") @@ -110,4 +111,100 @@ describe("branch popup", function() assert.False(vim.tbl_contains(get_git_branches(), "second-branch")) end) ) + + describe("spin out", function() + it( + "moves unpushed commits to a new branch unchecked out branch", + in_prepared_repo(function() + util.system([[ + git reset --hard origin/master + touch feature.js + git add . + git commit -m 'some feature' + ]]) + async.util.block_on(status.reset) + + local input_branch = "spin-out-branch" + input.values = { input_branch } + + local branch_before = get_current_branch() + local commit_before = get_git_rev(branch_before) + + local remote_commit = get_git_rev("origin/" .. branch_before) + + act("bS") + operations.wait("spin_out_branch") + + local branch_after = get_current_branch() + + eq(branch_after, branch_before) + eq(get_git_rev(input_branch), commit_before) + eq(get_git_rev(branch_before), remote_commit) + end) + ) + + it( + "checks out the new branch if uncommitted changes present", + in_prepared_repo(function() + util.system([[ + git reset --hard origin/master + touch feature.js + git add . + git commit -m 'some feature' + touch wip.js + git add . + ]]) + async.util.block_on(status.reset) + + local input_branch = "spin-out-branch" + input.values = { input_branch } + + local branch_before = get_current_branch() + local commit_before = get_git_rev(branch_before) + + local remote_commit = get_git_rev("origin/" .. branch_before) + + act("bS") + operations.wait("spin_out_branch") + + local branch_after = get_current_branch() + + eq(branch_after, input_branch) + eq(get_git_rev(branch_after), commit_before) + eq(get_git_rev(branch_before), remote_commit) + end) + ) + end) + + describe("spin off", function() + it( + "moves unpushed commits to a new checked out branch", + in_prepared_repo(function() + util.system([[ + git reset --hard origin/master + touch feature.js + git add . + git commit -m 'some feature' + ]]) + async.util.block_on(status.reset) + + local input_branch = "spin-off-branch" + input.values = { input_branch } + + local branch_before = get_current_branch() + local commit_before = get_git_rev(branch_before) + + local remote_commit = get_git_rev("origin/" .. branch_before) + + act("bs") + operations.wait("spin_off_branch") + + local branch_after = get_current_branch() + + eq(branch_after, input_branch) + eq(get_git_rev(branch_after), commit_before) + eq(get_git_rev(branch_before), remote_commit) + end) + ) + end) end)