From 9f2a76d079c0b04374295c927e1ca18d2e5dfcd6 Mon Sep 17 00:00:00 2001 From: Nicolas GB Date: Tue, 16 Jun 2026 18:46:46 +0200 Subject: [PATCH] feat(push): Add the possibility to push to arbitrary remotes through the vim command (#124) --- README.md | 24 +++++++++++--- lua/jj/cmd/init.lua | 81 ++++++++++++++++++++++++++++++++++++--------- tests/run_tests.lua | 58 ++++++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index b64f76e..7e019f1 100644 --- a/README.md +++ b/README.md @@ -392,6 +392,9 @@ The plugin provides a `:J` command that accepts jj subcommands: :J new :J push " Push all changes :J push main " Push only main bookmark +:J push --remote origin " Push all changes to a specific remote +:J push main --remote origin " Push only main bookmark to a specific remote +:J push --deleted --remote origin " Push deleted bookmarks to a specific remote :J fetch " Fetch from remote :J open_pr " Open PR for current change's bookmark :J open_pr --list " Select bookmark from all and open PR @@ -788,13 +791,26 @@ The `push` function accepts an options table: ```lua local cmd = require("jj.cmd") cmd.push({ - bookmark = "main" -- Push specific bookmark (default: all changes) + bookmark = "main", -- Push specific bookmark (default: all changes) + remote = "origin", -- Optional target remote + -- deleted = true, -- Push deleted bookmarks instead of a bookmark }) -- Examples: -cmd.push() -- Push all changes -cmd.push({ bookmark = "main" }) -- Push only main bookmark -cmd.push({ bookmark = "feature" }) -- Push only feature bookmark +cmd.push() -- Push all changes +cmd.push({ bookmark = "main" }) -- Push only main bookmark +cmd.push({ bookmark = "feature" }) -- Push only feature bookmark +cmd.push({ remote = "origin" }) -- Push all changes to a specific remote +cmd.push({ bookmark = "main", remote = "origin" }) -- Push only main to a specific remote +cmd.push({ deleted = true, remote = "origin" }) -- Push deleted bookmarks to a specific remote +``` + +The `:J push` command also supports these forms: + +```sh +:J push --remote origin +:J push main --remote origin +:J push --deleted --remote origin ``` ### Bookmark Management Command Options diff --git a/lua/jj/cmd/init.lua b/lua/jj/cmd/init.lua index 87850e9..ae738bb 100644 --- a/lua/jj/cmd/init.lua +++ b/lua/jj/cmd/init.lua @@ -132,6 +132,7 @@ local split_module = require("jj.cmd.split") --- @class jj.cmd.push_opts --- @field bookmark? string Specific bookmark to push (default: all) --- @field deleted? boolean Push deleted revisions +--- @field remote? string Specific remote to push too --- @class jj.cmd.open_pr_opts --- @field list_bookmarks? boolean Whether to select from all bookmarks instead of current revision @@ -756,6 +757,56 @@ function M.fetch() end end +--- Parse args for `:J push`. +--- @param args string[] +--- @return jj.cmd.push_opts|nil opts +--- @return string|nil err +function M.parse_push_args(args) + local opts = {} --[[@as jj.cmd.push_opts]] + local already_set = { + bookmark = false, + remote = false, + } + + local i = 1 + while i <= #args do + local arg = args[i] + + if arg == "--deleted" then + opts.deleted = true + elseif arg == "--remote" then + local remote = args[i + 1] + if not remote or remote:sub(1, 2) == "--" then + return nil, "Missing remote name after --remote" + end + if already_set.remote then + return nil, "Remote already set. Cannot specify multiple remotes." + end + + opts.remote = remote + already_set.remote = true + i = i + 1 + elseif arg:sub(1, 2) == "--" then + return nil, string.format("Unknown option: %s", arg) + else + if already_set.bookmark then + return nil, "Only one bookmark can be provided" + end + + opts.bookmark = arg + already_set.bookmark = true + end + + i = i + 1 + end + + if opts.deleted and opts.bookmark then + return nil, "Cannot specify both --deleted and a bookmark" + end + + return opts, nil +end + -- Jujutsu push --- @param opts? jj.cmd.push_opts Optional push options function M.push(opts) @@ -768,17 +819,24 @@ function M.push(opts) -- Save the lop one state to refresh local log_open = terminal.is_log_buffer_open() + local notify_msg = "Pushing bookmarks `ALL` bookmarks" + local cmd = "jj git push" if opts.bookmark then - utils.notify(string.format("Pushing `%s` bookmark ...", opts.bookmark), vim.log.levels.INFO, 1000) + notify_msg = string.format("Pushing bookmark `%s`", opts.bookmark) cmd = string.format("%s --bookmark %s", cmd, opts.bookmark) elseif opts.deleted then - utils.notify("Pushing deleted bookmarks...", vim.log.levels.INFO, 1000) + notify_msg = "Pushing deleted bookmarks" cmd = cmd .. " --deleted" - else - utils.notify(string.format("Pushing `ALL` bookmarks...", opts.bookmark), vim.log.levels.INFO, 1000) end + if opts.remote then + cmd = string.format("%s --remote %s", cmd, opts.remote) + notify_msg = string.format("%s to remote `%s`", notify_msg, opts.remote) + end + + utils.notify(notify_msg .. "...", vim.log.levels.INFO, 1000) + runner.execute_command_async(cmd, function() utils.notify("Successfully pushed to remote", vim.log.levels.INFO) if log_open then @@ -1311,19 +1369,12 @@ function M.j(args) M.abandon() end, push = function() - local opts = {} - for _, arg in ipairs(remaining_args) do - if arg == "--deleted" then - opts.deleted = true - else - opts.bookmark = arg - end - end - - if opts.deleted and opts.bookmark then - utils.notify("Cannot specify both --deleted and --bookmark", vim.log.levels.ERROR) + local opts, err = M.parse_push_args(remaining_args) + if err then + utils.notify(err, vim.log.levels.ERROR) return end + M.push(opts) end, fetch = function() diff --git a/tests/run_tests.lua b/tests/run_tests.lua index 12012e6..2680c7b 100755 --- a/tests/run_tests.lua +++ b/tests/run_tests.lua @@ -399,6 +399,64 @@ run_test("parse_default_cmd: returns nil for nil", function() assert_is_nil(parser.parse_default_cmd(nil)) end) +print("\n=== Running parse_push_args tests ===\n") + +local cmd = require("jj.cmd") + +run_test("parse_push_args: parses bookmark and remote", function() + local opts, err = cmd.parse_push_args({ "my-bookmark", "--remote", "origin" }) + assert_is_nil(err) + assert_table_equals({ bookmark = "my-bookmark", remote = "origin" }, opts) +end) + +run_test("parse_push_args: parses deleted flag", function() + local opts, err = cmd.parse_push_args({ "--deleted" }) + assert_is_nil(err) + assert_table_equals({ deleted = true }, opts) +end) + +run_test("parse_push_args: allows remote before bookmark", function() + local opts, err = cmd.parse_push_args({ "--remote", "origin", "my-bookmark" }) + assert_is_nil(err) + assert_table_equals({ bookmark = "my-bookmark", remote = "origin" }, opts) +end) + +run_test("parse_push_args: errors on missing remote value", function() + local opts, err = cmd.parse_push_args({ "--remote" }) + assert_is_nil(opts) + assert_equals("Missing remote name after --remote", err) +end) + +run_test("parse_push_args: errors when remote value is another option", function() + local opts, err = cmd.parse_push_args({ "--remote", "--deleted" }) + assert_is_nil(opts) + assert_equals("Missing remote name after --remote", err) +end) + +run_test("parse_push_args: errors on duplicate remotes", function() + local opts, err = cmd.parse_push_args({ "--remote", "origin", "--remote", "upstream" }) + assert_is_nil(opts) + assert_equals("Remote already set. Cannot specify multiple remotes.", err) +end) + +run_test("parse_push_args: errors on multiple bookmarks", function() + local opts, err = cmd.parse_push_args({ "bookmark-1", "bookmark-2" }) + assert_is_nil(opts) + assert_equals("Only one bookmark can be provided", err) +end) + +run_test("parse_push_args: errors on unknown option", function() + local opts, err = cmd.parse_push_args({ "--foo" }) + assert_is_nil(opts) + assert_equals("Unknown option: --foo", err) +end) + +run_test("parse_push_args: errors on deleted with bookmark", function() + local opts, err = cmd.parse_push_args({ "--deleted", "my-bookmark" }) + assert_is_nil(opts) + assert_equals("Cannot specify both --deleted and a bookmark", err) +end) + print("\n=== Running build_log_cmd tests ===\n") local log = require("jj.cmd.log")