From c005e5d6e2a7fac44be9eccfc549aad1bf6ebd50 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Tue, 1 Nov 2022 10:12:53 +0000 Subject: [PATCH 01/27] feat: implement FileHistory for hg --- lua/diffview/config.lua | 6 +- .../scene/views/file_history/listeners.lua | 2 +- lua/diffview/vcs/adapters/hg/commit.lua | 41 + lua/diffview/vcs/adapters/hg/init.lua | 714 +++++++++++++++++- lua/diffview/vcs/adapters/hg/rev.lua | 37 + 5 files changed, 793 insertions(+), 7 deletions(-) create mode 100644 lua/diffview/vcs/adapters/hg/commit.lua create mode 100644 lua/diffview/vcs/adapters/hg/rev.lua diff --git a/lua/diffview/config.lua b/lua/diffview/config.lua index 844351bb..77528acf 100644 --- a/lua/diffview/config.lua +++ b/lua/diffview/config.lua @@ -38,6 +38,7 @@ M.defaults = { diff_binaries = false, enhanced_diff_hl = false, git_cmd = { "git" }, + hg_cmd = { "hg" }, use_icons = true, show_help_hints = true, watch_index = true, @@ -273,7 +274,10 @@ M.log_option_defaults = { path_args = {}, }, ---@type HgLogOptions - hg = {}, + hg = { + limit = 256, + user = nil, + }, } ---@return DiffviewConfig diff --git a/lua/diffview/scene/views/file_history/listeners.lua b/lua/diffview/scene/views/file_history/listeners.lua index 32f90c01..6bb38394 100644 --- a/lua/diffview/scene/views/file_history/listeners.lua +++ b/lua/diffview/scene/views/file_history/listeners.lua @@ -33,7 +33,7 @@ return function(view) local log_options = view.panel:get_log_options() local cur = view.panel:cur_file() - if log_options.L[1] and bufnr == cur.layout:get_main_win().file.bufnr then + if log_options.L and log_options.L[1] and bufnr == cur.layout:get_main_win().file.bufnr then for _, value in ipairs(log_options.L) do local l1, lpath = value:match("^(%d+),.*:(.*)") diff --git a/lua/diffview/vcs/adapters/hg/commit.lua b/lua/diffview/vcs/adapters/hg/commit.lua new file mode 100644 index 00000000..bf2235b3 --- /dev/null +++ b/lua/diffview/vcs/adapters/hg/commit.lua @@ -0,0 +1,41 @@ +local lazy = require("diffview.lazy") +local oop = require('diffview.oop') +local utils = require("diffview.utils") +local Commit = require('diffview.vcs.commit').Commit + +local M = {} + +local HgCommit = oop.create_class('HgCommit', Commit) + +function HgCommit:init(opt) + HgCommit:super().init(self, opt) + + if opt.time_offset then + self.time_offset = HgCommit.parse_time_offset(opt.time_offset) + self.time = self.time - self.time_offset + else + self.time_offset = 0 + end + + self.iso_date = Commit.time_to_iso(self.time, self.time_offset) +end + +---@param iso_date string? +function HgCommit.parse_time_offset(iso_date) + if not iso_date or iso_date == "" then + return 0 + end + + local sign, offset = vim.trim(iso_date):match("([+-])(%d+)") + + offset = tonumber(offset) + + if sign == "-" then + offset = -offset + end + + return offset +end + +M.HgCommit = HgCommit +return M diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 8866427e..42dbe411 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -1,18 +1,722 @@ local oop = require('diffview.oop') local VCSAdapter = require('diffview.vcs.adapter').VCSAdapter +local arg_parser = require('diffview.arg_parser') +local utils = require('diffview.utils') +local lazy = require('diffview.lazy') +local config = require('diffview.config') +local async = require("plenary.async") +local logger = require('diffview.logger') +local JobStatus = require('diffview.vcs.utils').JobStatus +local Commit = require("diffview.vcs.adapters.hg.commit").HgCommit +local RevType = require("diffview.vcs.rev").RevType +local HgRev = require('diffview.vcs.adapters.hg.rev').HgRev +local Job = require("plenary.job") +local CountDownLatch = require("diffview.control").CountDownLatch +local FileEntry = require("diffview.scene.file_entry").FileEntry +local Diff2Hor = require("diffview.scene.layouts.diff_2_hor").Diff2Hor +local LogEntry = require("diffview.vcs.log_entry").LogEntry + +---@type PathLib +local pl = lazy.access(utils, "path") local M = {} +---@class HgAdapter : VCSAdapter local HgAdapter = oop.create_class('HgAdapter', VCSAdapter) -function M.get_repo_paths(args) - -- TODO: implement - return nil +HgAdapter.Rev = HgRev +HgAdapter.config_key = "hg" + +function M.get_repo_paths(path_args, cpath) + local paths = {} + local top_indicators = {} + + for _, path_arg in ipairs(path_args) do + for _, path in ipairs(pl:vim_expand(path_arg, false, true)) do + path = pl:readlink(path) or path + table.insert(paths, path) + end + end + + local cfile = pl:vim_expand("%") + cfile = pl:readlink(cfile) or cfile + + for _, path in ipairs(paths) do + table.insert(top_indicators, pl:absolute(path, cpath)) + break + end + + table.insert(top_indicators, cpath and pl:realpath(cpath) or ( + vim.bo.buftype == "" + and pl:absolute(cfile) + or nil + )) + + if not cpath then + table.insert(top_indicators, pl:realpath(".")) + end + + return paths, top_indicators +end + +---Get the git toplevel directory from a path to file or directory +---@param path string +---@return string? +local function get_toplevel(path) + local out, code = utils.system_list(vim.tbl_flatten({config.get_config().hg_cmd, {"root"}}), path) + if code ~= 0 then + return nil + end + return out[1] and vim.trim(out[1]) end function M.find_toplevel(top_indicators) - -- TODO: implement - return "", nil + local toplevel + + for _, p in ipairs(top_indicators) do + if not pl:is_dir(p) then + p = pl:parent(p) + end + + if p and pl:readable(p) then + toplevel = get_toplevel(p) + if toplevel then + return nil, toplevel + end + end + end + + return ( + ("Path not a mercurial repo (or any parent): %s") + :format(table.concat(vim.tbl_map(function(v) + local rel_path = pl:relative(v, ".") + return utils.str_quote(rel_path == "" and "." or rel_path) + end, top_indicators) --[[@as vector ]], ", ")) + ), nil +end + +function M.create(toplevel, path_args, cpath) + return HgAdapter({ + toplevel = toplevel, + path_args = path_args, + cpath = cpath, + }) +end + +function HgAdapter:init(opt) + opt = opt or {} + HgAdapter:super().init(self, opt) + + self.ctx = { + toplevel = opt.toplevel, + dir = opt.toplevel, + path_args = opt.path_args or {}, + } + + self:init_completion() +end + +function HgAdapter:get_command() + return config.get_config().hg_cmd +end + +function HgAdapter:get_show_args(path, rev) + return utils.vec_join(self:args(), "cat", "--rev", rev:object_name(), "--", path) +end + +function HgAdapter:file_history_options(range, paths, args) + local default_args = config.get_config().default_args.DiffviewFileHistory + local argo = arg_parser.parse(vim.tbl_flatten({ default_args, args })) + local rel_paths + + local cpath = argo:get_flag("C", { no_empty = true, expand = true }) + local cfile = pl:vim_expand("%") + cfile = pl:readlink(cfile) or cfile + + rel_paths = vim.tbl_map(function(v) + return v == "." and "." or pl:relative(v, ".") + end, paths) + + local cwd = cpath or vim.loop.cwd() + + local range_arg = argo:get_flag('rev', { no_empty = true }) + if range_arg then + -- TODO: check if range is valid + end + + if range then + utils.err( + "Line ranges are not supported for hg!" + ) + return + end + + local log_flag_names = { + { "rev", "r" }, + { "follow", "f" }, + { "no-merges", "M" }, + { "limit", "l" }, + { "user", "u" }, + { "keyword", "k" }, + { "include", "I" }, + { "exclude", "X" }, + } + + ---@type LogOptions + local log_options = { rev_range = range_arg } + for _, names in ipairs(log_flag_names) do + local key, _ = names[1]:gsub("%-", "_") + local v = argo:get_flag(names, { + expect_string = type(config.log_option_defaults[self.config_key][key]) ~= "boolean", + expect_list = names[1] == "L", + }) + log_options[key] = v + end + + log_options.path_args = paths + + local ok, opt_description = self:file_history_dry_run(log_options) + + if not ok then + utils.info({ + ("No hg history for the target(s) given the current options! Targets: %s") + :format(#rel_paths == 0 and "':(top)'" or table.concat(vim.tbl_map(function(v) + return "'" .. v .. "'" + end, rel_paths) --[[@as vector ]], ", ")), + ("Current options: [ %s ]"):format(opt_description) + }) + return + end + + return log_options +end + +local function prepare_fh_options(adapter, log_options, single_file) + local o = log_options + local rev_range, base + + if log_options.rev then + rev_range = log_options.rev + end + + if log_options.base then + -- TODO + end + + return { + rev_range = rev_range, + base = base, + path_args = log_options.path_args, + flags = utils.vec_join( + (o.follow and single_file) and { "--follow" } or nil, + o.user and { "--user=" .. o.user } or nil, + o.limit and { "--limit=" .. o.limit } or nil + ), + } +end + +---@param log_opt LogOptions +---@return boolean ok, string description +function HgAdapter:file_history_dry_run(log_opt) + local single_file = self:is_single_file(log_opt.path_args) + local log_options = config.get_log_options(single_file, log_opt, self.config_key) + + local options = vim.tbl_map(function(v) + return vim.fn.shellescape(v) + end, prepare_fh_options(self, log_options, single_file).flags) -- [[@as vector]] + + local description = utils.vec_join( + ("Top-level path: '%s'"):format(utils.path:vim_fnamemodify(self.ctx.toplevel, ":~")), + log_options.rev_range and ("Revision range: '%s'"):format(log_options.rev_range) or nil, + ("Flags: %s"):format(table.concat(options, " ")) + ) + + log_options = utils.tbl_clone(log_options) --[[@as LogOptions ]] + log_options.limit = 1 + -- TODO + options = prepare_fh_options(self, log_options, single_file).flags + + local context = "HgAdapter.file_history_dry_run()" + local cmd = utils.vec_join( + "log", + log_options.rev_range and "--rev=" .. log_options.rev_range or nil, + options, + log_options.path_args + ) + + local out, code = self:exec_sync(cmd, { + cwd = self.ctx.toplevel, + debug_opt = { + context = context, + no_stdout = true, + }, + }) + + local ok = code == 0 and #out > 0 + + if not ok then + logger.lvl(1).s_debug(("[%s] Dry run failed."):format(context)) + end + + return ok, table.concat(description, ", ") +end + +local function structure_fh_data(namestat_data, numstat_data) + local right_hash, left_hash, merge_hash = unpack(utils.str_split(namestat_data[1])) + local time, time_offset = namestat_data[3]:match('(%d+.%d*)([-+]?%d*)') + + return { + left_hash = left_hash ~= "" and left_hash or nil, + right_hash = right_hash, + merge_hash = merge_hash, + author = namestat_data[2], + time = tonumber(time), + time_offset = time_offset, + rel_date = namestat_data[4], + ref_names = namestat_data[5]:sub(3), + subject = namestat_data[6]:sub(3), + namestat = utils.vec_slice(namestat_data, 7), + numstat = numstat_data, + } +end + + +---@param state HgAdapter.FHState +---@param callback fun(status: JobStatus, data?: table, msg?: string[]) +local incremental_fh_data = async.void(function(state, callback) + local raw = {} + local namestat_job, numstat_job, shutdown + + local namestat_state = { + data = {}, + key = "namestat", + idx = 0, + } + local numstat_state = { + data = {}, + key = "numstat", + idx = 0, + } + + local function on_stdout(_, line, j) + local handler_state = j == namestat_job and namestat_state or numstat_state + + if line == "\0" then + if handler_state.idx > 0 then + if not raw[handler_state.idx] then + raw[handler_state.idx] = {} + end + + raw[handler_state.idx][handler_state.key] = handler_state.data + + if not shutdown and raw[handler_state.idx].namestat and raw[handler_state.idx].numstat then + shutdown = callback( + JobStatus.PROGRESS, + structure_fh_data(raw[handler_state.idx].namestat, raw[handler_state.idx].numstat) + ) + + if shutdown then + logger.lvl(1).debug("Killing file history jobs...") + -- NOTE: The default `Job:shutdown` methods use `vim.wait` which + -- causes a segfault when called here. + namestat_job:_shutdown(64) + numstat_job:_shutdown(64) + end + end + end + handler_state.idx = handler_state.idx + 1 + handler_state.data = {} + elseif line ~= "" then + table.insert(handler_state.data, line) + end + end + + ---@type CountDownLatch + local latch = CountDownLatch(2) + + local function on_exit(j, code) + if code == 0 then + on_stdout(nil, "\0", j) + end + latch:count_down() + end + + local rev_range = state.prepared_log_opts.rev_range and '--rev=' .. state.prepared_log_opts.rev_range or nil + + namestat_job = Job:new({ + command = state.adapter:bin(), + args = utils.vec_join( + state.adapter:args(), + "log", + rev_range, + '--template=\\x00\n{node} {p1.node} {ifeq(p2.rev, -1 ,\"\", \"{p2.node}\")}\n{author|person}\n{date}\n{date|age}\n {separate(", ", tags, topics)}\n {desc|firstline}\n', + state.prepared_log_opts.flags, + "--", + state.path_args + ), + cwd = state.adapter.ctx.toplevel, + on_stdout = on_stdout, + on_exit = on_exit, + }) + + numstat_job = Job:new({ + command = state.adapter:bin(), + args = utils.vec_join( + state.adapter:args(), + "log", + rev_range, + "--template=\\x00\n", + '--stat', + state.prepared_log_opts.flags, + "--", + state.path_args + ), + cwd = state.adapter.ctx.toplevel, + on_stdout = on_stdout, + on_exit = on_exit, + }) + + namestat_job:start() + numstat_job:start() + + latch:await() + + local debug_opt = { + context = "HgAdapter>incremental_fh_data()", + func = "s_info", + no_stdout = true, + } + utils.handle_job(namestat_job, { debug_opt = debug_opt }) + utils.handle_job(numstat_job, { debug_opt = debug_opt }) + + if shutdown then + callback(JobStatus.KILLED) + elseif namestat_job.code ~= 0 or numstat_job.code ~= 0 then + callback(JobStatus.ERROR, nil, utils.vec_join( + namestat_job:stderr_result(), + numstat_job:stderr_result()) + ) + else + callback(JobStatus.SUCCESS) + end +end) + +local function parse_fh_data(state) + local cur = state.cur + + if cur.merge_hash and cur.numstat[1] and #cur.numstat ~= #cur.namestat then + local job + local job_spec = { + command = state.adapter:bin(), + args = utils.vec_join( + state.adapter:args(), + "status", + "--change", + cur.right_hash, + "--", + state.old_path or state.path_args + ), + cwd = state.adapter.ctx.toplevel, + on_exit = function(j) + if j.code == 0 then + cur.namestat = j:result() + end + state.adapter:handle_co(state.thread, coroutine.resume(state.thread)) + end, + } + + local max_retries = 2 + local context = "HgAdapter.file_history_worker()" + state.resume_lock = true + + for i = 0, max_retries do + -- Git sometimes fails this job silently (exit code 0). Not sure why, + -- possibly because we are running multiple git opeartions on the same + -- repo concurrently. Retrying the job usually solves this. + job = Job:new(job_spec) + job:start() + coroutine.yield() + utils.handle_job(job, { fail_on_empty = true, context = context, log_func = logger.warn }) + + if #cur.namestat == 0 then + if i < max_retries then + logger.warn(("[%s] Retrying %d more time(s)."):format(context, max_retries - i)) + end + else + if i > 0 then + logger.info(("[%s] Retry successful!"):format(context)) + end + break + end + end + + state.resume_lock = false + + if job.code ~= 0 then + state.callback({}, JobStatus.ERROR, job:stderr_result()) + return false, JobStatus.FATAL + end + + if #cur.namestat == 0 then + -- Give up: something has been renamed. We can no longer track the + -- history. + logger.warn(("[%s] Giving up."):format(context)) + utils.warn("Displayed history may be incomplete. Check ':DiffviewLog' for details.", true) + return false + end + end + + local files = {} + for i = 1, #cur.numstat - 1 do + local status = cur.namestat[i]:sub(1, 1):gsub("%s", " ") + local name = cur.namestat[i]:match("[%a%s]%s*(.*)") + local oldname + + local stats = {} + local changes, diffstats = cur.numstat[i]:match(".*|%s+(%d+)%s+([+-]+)") + if changes and diffstats then + local _, adds = diffstats:gsub("+", "") + + stats = { + additions = tonumber(adds), + deletions = tonumber(changes) - tonumber(adds), + } + end + + if not stats.additions or not stats.deletions then + stats = nil + end + + table.insert(files, FileEntry.with_layout(state.opt.default_layout or Diff2Hor, { + adapter = state.adapter, + path = name, + oldpath = oldname, + status = status, + stats = stats, + kind = "working", + commit = state.commit, + revs = { + a = cur.left_hash and HgRev(RevType.COMMIT, cur.left_hash) or HgRev.new_null_tree(), + b = state.prepared_log_opts.base or HgRev(RevType.COMMIT, cur.right_hash), + } + })) + end + + if files[1] then + table.insert( + state.entries, + LogEntry({ + path_args = state.path_args, + commit = state.commit, + files = files, + single_file = state.single_file, + }) + ) + + state.callback(state.entries, JobStatus.PROGRESS) + end + + return true +end + +function HgAdapter:is_single_file(path_args, lflags) + if path_args and self.ctx.toplevel then + return #path_args == 1 + and not utils.path:is_dir(path_args[1]) + and #self:exec_sync({ "files", "--", path_args }, self.ctx.toplevel) < 2 + end + return true +end + +function HgAdapter:file_history_worker(thread, log_opt, opt, co_state, callback) + ---@type LogEntry[] + local entries = {} + local data = {} + local data_idx = 1 + local last_status + local err_msg + + local single_file = self:is_single_file(log_opt.single_file.path_args, {}) + + ---@type LogOptions + local log_options = config.get_log_options( + single_file, + single_file and log_opt.single_file or log_opt.multi_file, + "hg" + ) + + local state = { + thread = thread, + adapter = self, + path_args = log_opt.single_file.path_args, + log_options = log_options, + prepared_log_opts = prepare_fh_options(self, log_options, single_file), + opt = opt, + callback = callback, + entries = entries, + single_file = single_file, + resume_lock = false, + } + + local function data_callback(status, d, msg) + if status == JobStatus.PROGRESS then + data[#data+1] = d + end + + last_status = status + if msg then + err_msg = msg + end + if not state.resume_lock and coroutine.status(thread) == "suspended" then + self:handle_co(thread, coroutine.resume(thread)) + end + + if co_state.shutdown then + return true + end + end + + incremental_fh_data(state, data_callback) + + while true do + if not vim.tbl_contains({ JobStatus.SUCCESS, JobStatus.ERROR, JobStatus.KILLED }, last_status) + and not data[data_idx] then + coroutine.yield() + end + + if last_status == JobStatus.KILLED then + logger.warn("File history processing was killed.") + return + elseif last_status == JobStatus.ERROR then + callback(entries, JobStatus.ERROR, err_msg) + return + elseif last_status == JobStatus.SUCCESS and data_idx > #data then + break + end + + state.cur = data[data_idx] + + state.commit = Commit({ + hash = state.cur.right_hash, + author = state.cur.author, + time = tonumber(state.cur.time), + time_offset = state.cur.time_offset, + rel_date = state.cur.rel_date, + ref_names = state.cur.ref_names, + subject = state.cur.subject, + }) + + local ok, status = parse_fh_data(state) + + if not ok then + if status == JobStatus.FATAL then + return + end + break + end + + data_idx = data_idx + 1 + end + + callback(entries, JobStatus.SUCCESS) +end + +HgAdapter.flags = { + ---@type FlagOption[] + switches = { + {'--follow', '-f', 'Follow renames'}, + {'--no-merges', '-M', 'List no merge changesets'}, + }, + ---@type FlagOption[] + options = { + { '=r', '--rev=', 'Revspec', prompt_label = "(Revspec)" }, + { '=l', '--limit=', 'Limit the number of changesets' }, + { '=u', '--user=', 'Filter on user' }, + { '=k', '--keyword=', 'Filter by keyword' }, + { '=b', '--branch=', 'Filter by branch' }, + { '=B', '--bookmark=', 'Filter by bookmark' }, + { '=I', '--include=', 'Include files' }, + { '=E', '--exclude=', 'Exclude files' }, + }, +} + +for _, list in pairs(HgAdapter.flags) do + for i, option in ipairs(list) do + option = vim.tbl_extend("keep", option, { + prompt_fmt = "${label}${flag_name}", + + key = option.key or utils.str_match(option[2], { + "^%-%-?([^=]+)=?", + "^%+%+?([^=]+)=?", + }):gsub("%-", "_"), + + ---@param self FlagOption + ---@param value string|string[] + render_value = function(self, value) + local quoted + + if type(value) == "table" then + quoted = table.concat(vim.tbl_map(function(v) + return self[2] .. utils.str_quote(v, { only_if_whitespace = true }) + end, value), " ") + else + quoted = self[2] .. utils.str_quote(value, { only_if_whitespace = true }) + end + + return value == "", quoted + end, + + ---@param value string|string[] + render_default = function(_, value) + if value == nil then + return "" + elseif type(value) == "table" then + return table.concat(vim.tbl_map(function(v) + v = select(1, v:gsub("\\", "\\\\")) + return utils.str_quote(v, { only_if_whitespace = true }) + end, value), " ") + end + return utils.str_quote(value, { only_if_whitespace = true }) + end, + }) + + list[i] = option + list[option.key] = option + end +end + +function HgAdapter:is_binary(path, rev) + -- TODO + return false +end + +-- TODO: implement completion +function HgAdapter:rev_completion(arg_lead, opt) + return { } +end + +function HgAdapter:init_completion() + self.comp.file_history:put({"--rev", "-r"}, function(_, arg_lead) + return self:rev_completion(arg_lead, { accept_range = true }) + end) + + self.comp.file_history:put({ "--follow", "-f" }) + self.comp.file_history:put({ "--no-merges", "-M" }) + self.comp.file_history:put({ "--limit", "-l" }, {}) + + self.comp.file_history:put({ "--user", "-u" }, {}) + self.comp.file_history:put({ "--keyword", "-k" }, {}) + + self.comp.file_history:put({ "--branch", "-b" }, {}) -- TODO: completion + self.comp.file_history:put({ "--bookmark", "-B" }, {}) -- TODO: completion + + self.comp.file_history:put({"--include", "-I"}, function (_, arg_lead) + return vim.fn.getcompletion(arg_lead, "dir") + end) + self.comp.file_history:put({"--exclude", "-X"}, function (_, arg_lead) + return vim.fn.getcompletion(arg_lead, "dir") + end) + end M.HgAdapter = HgAdapter diff --git a/lua/diffview/vcs/adapters/hg/rev.lua b/lua/diffview/vcs/adapters/hg/rev.lua new file mode 100644 index 00000000..53c09d58 --- /dev/null +++ b/lua/diffview/vcs/adapters/hg/rev.lua @@ -0,0 +1,37 @@ +local oop = require("diffview.oop") +local Rev = require('diffview.vcs.rev').Rev +local RevType = require('diffview.vcs.rev').RevType + +local M = {} + +---@class HgRev : Rev +local HgRev = oop.create_class("HgRev", Rev) + +HgRev.NULL_TREE_SHA = "000000000000000000000000000000000000000" + +function HgRev:init(rev_type, revision, track_head) + local t = type(revision) + + assert( + revision == nil or t == "string" or t == "number", + "'revision' must be one of: nil, string, number!" + ) + if t == "string" then + assert(revision ~= "", "'revision' cannot be an empty string!") + end + + t = type(track_head) + assert(t == "boolean" or t == "nil", "'track_head' must be of type boolean!") + + self.type = rev_type + self.track_head = track_head or false + + self.commit = revision +end + +function HgRev:object_name() + return self.commit +end + +M.HgRev = HgRev +return M From 89c9556a62f93d0e394f42d5ad2b127d85b49285 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Sat, 19 Nov 2022 14:14:54 +0000 Subject: [PATCH 02/27] feat: implement diffview for hg --- lua/diffview/vcs/adapters/hg/init.lua | 279 +++++++++++++++++++++++++- 1 file changed, 278 insertions(+), 1 deletion(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 42dbe411..bbe5cbb5 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -15,6 +15,7 @@ local CountDownLatch = require("diffview.control").CountDownLatch local FileEntry = require("diffview.scene.file_entry").FileEntry local Diff2Hor = require("diffview.scene.layouts.diff_2_hor").Diff2Hor local LogEntry = require("diffview.vcs.log_entry").LogEntry +local vcs_utils = require("diffview.vcs.utils") ---@type PathLib local pl = lazy.access(utils, "path") @@ -469,7 +470,7 @@ local function parse_fh_data(state) local files = {} for i = 1, #cur.numstat - 1 do local status = cur.namestat[i]:sub(1, 1):gsub("%s", " ") - local name = cur.namestat[i]:match("[%a%s]%s*(.*)") + local name = vim.trim(cur.namestat[i]:match("[%a%s]%s*(.*)")) local oldname local stats = {} @@ -621,6 +622,282 @@ function HgAdapter:file_history_worker(thread, log_opt, opt, co_state, callback) callback(entries, JobStatus.SUCCESS) end +function HgAdapter:diffview_options(args) + local default_args = config.get_config().default_args.DiffviewOpen + local argo = arg_parser.parse(vim.tbl_flatten({ default_args, args })) + local rev_args = argo:get_flag({'rev'}) + + local head = self:head_rev() + local left = head or HgRev.new_null_tree() + local right = HgRev(RevType.LOCAL) + + local options = { + show_untracked = true, -- TODO: extract from hg config + selected_file = argo:get_flag("selected-file", { no_empty = true, expand = true }) + or (vim.bo.buftype == "" and pl:vim_expand("%:p")) + or nil, + } + + return {left = left, right = right, options = options} +end + +function VCSAdapter:rev_to_pretty_string(left, right) + if left.track_head and right.type == RevType.LOCAL then + return nil + elseif left.commit and right.type == RevType.LOCAL then + return left:abbrev() + elseif left.commit and right.commit then + return left:abbrev() .. "::" .. right:abbrev() + end + return nil +end + +function HgAdapter:head_rev() + local out, code = self:exec_sync({ "log", "--template={node}", "--limit=1", "--"}, {cwd = self.ctx.toplevel, retry_on_empty = 2}) + if code ~= 0 then + return + end + + local s = vim.trim(out[1]):gsub("^%^", "") + + return HgRev(RevType.COMMIT, s, true) +end + +function HgAdapter:rev_to_args(left, right) + assert( + not (left.type == RevType.LOCAL and right.type == RevType.LOCAL), + "Can't diff LOCAL against LOCAL!" + ) + if left.type == RevType.COMMIT and right.type == RevType.COMMIT then + return { '--rev="' .. left.commit .. '::' .. right.commit .. '"' } + elseif left.type == RevType.STAGE and right.type == RevType.LOCAL then + return {} + else + return { '--rev=' .. left.commit } + end +end + + +function HgAdapter:get_files_args(args) + return utils.vec_join(self:args(), "status", "--print0", "--unknown", "--no-status", "--template={path}\\n", args) +end + +HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, opt, callback) + ---@type FileEntry[] + local files = {} + ---@type FileEntry[] + local conflicts = {} + ---@type CountDownLatch + local latch = CountDownLatch(3) + local debug_opt = { + context = "HgAdapter>tracked_files()", + func = "s_debug", + debug_level = 1, + no_stdout = true, + } + + ---@param job Job + local function on_exit(job) + utils.handle_job(job, { debug_opt = debug_opt }) + latch:count_down() + end + + local namestat_job = Job:new({ + command = self:bin(), + args = utils.vec_join( + self:args(), + "status", + "--modified", + "--added", + "--removed", + "--deleted", + "--template={status} {path}\n", + args + ), + cwd = self.ctx.toplevel, + on_exit = on_exit, + }) + local mergestate_job = Job:new({ + command = self:bin(), + args = utils.vec_join(self:args(), "debugmergestate", "-Tjson"), + cwd = self.ctx.toplevel, + on_exit = on_exit, + }) + local numstat_job = Job:new({ + command = self:bin(), + args = utils.vec_join(self:args(), "diff", "--stat", args), + cwd = self.ctx.toplevel, + on_exit = on_exit, + }) + + namestat_job:start() + mergestate_job:start() + numstat_job:start() + latch:await() + local out_status + if not (#namestat_job:result() == #numstat_job:result() - 1) then + out_status = vcs_utils.ensure_output(2, { namestat_job, numstat_job }, "HgAdapter>tracked_files()") + end + + if out_status == JobStatus.ERROR or not (namestat_job.code == 0 and numstat_job.code == 0 and mergestate_job.code == 0) then + callback(utils.vec_join(namestat_job:stderr_result(), numstat_job:stderr_result(), mergestate_job:stderr_result()), nil) + return + end + + local numstat_out = numstat_job:result() + local namestat_out = namestat_job:result() + local mergestate_out = mergestate_job:result() + + local data = {} + local conflict_map = {} + local file_info = {} + + -- Last line in numstat is a summary and should not be used + table.remove(numstat_out, -1) + + for i, s in ipairs(namestat_out) do + local status = s:sub(1, 1):gsub("%s", " ") + local name = vim.trim(s:match("[%a%s]%s*(.*)")) + + local stats = {} + local changes, diffstats = numstat_out[i]:match(".*|%s+(%d+)%s+([+-]+)") + if changes and diffstats then + local _, adds = diffstats:gsub("+", "") + + stats = { + additions = tonumber(adds), + deletions = tonumber(changes) - tonumber(adds), + } + end + + if not (kind == "staged") then + file_info[name] = { + status = status, + name = name, + oldname = name, -- TODO + stats = stats, + } + end + end + + local find_key = function (t, key, value) + for _, v in ipairs(t) do + if v[key] == value then + return v + end + end + end + + local mergestate = vim.json.decode(table.concat(mergestate_out, '')) + for _, file in ipairs(mergestate[1].files) do + local base = find_key(file.extras, 'key', 'ancestorlinknode') + if file.state == 'u' then + file_info[file.path].status = 'U' + file_info[file.path].oldname = file.other_path + file_info[file.path].base = base and base.value or nil + conflict_map[file.path] = file_info[file.path] + end + end + local ours_node = find_key(mergestate[1].commits, 'name', 'local').node + local theirs_node = find_key(mergestate[1].commits, 'name', 'other').node + + for _, f in pairs(file_info) do + if f.status ~= "U" then + table.insert(data, f) + end + end + + if kind == "working" and next(conflict_map) then + -- TODO: read and parse content of .hg/merge/state2 + -- O( : others + -- L( : local + -- ancestorlinknode : base + -- HASH is 40 hex characters + -- hg debugmergestate -Tjson + for _, v in pairs(conflict_map) do + table.insert(conflicts, FileEntry.with_layout(opt.merge_layout, { + adapter = self, + path = v.name, + oldpath = v.oldname, + status = "U", + kind = "conflicting", + revs = { + a = self.Rev(RevType.COMMIT, ours_node), + b = self.Rev(RevType.LOCAL), + c = self.Rev(RevType.COMMIT, theirs_node), + d = self.Rev(RevType.COMMIT, v.base), + } + })) + end + end + + for _, v in ipairs(data) do + table.insert(files, FileEntry.with_layout(opt.default_layout, { + adapter = self, + path = v.name, + oldpath = v.oldname, + status = v.status, + stats = v.stats, + kind = kind, + revs = { + a = left, + b = right, + } + })) + end + + callback(nil, files, conflicts) +end, 7) + +HgAdapter.untracked_files = async.wrap(function(self, left, right, opt, callback) + Job:new({ + command = self:bin(), + args = utils.vec_join( + self:args(), + "status", + "--print0", + "--unknown", + "--no-status", + "--template={path}\\n" + ), + cwd = self.ctx.toplevel, + ---@type Job + on_exit = function(j) + utils.handle_job(j, { + debug_opt = { + context = "HgAdapter>untracked_files()", + func = "s_debug", + debug_level = 1, + no_stdout = true, + }, + }) + + if j.code ~= 0 then + callback(j:stderr_result() or {}, nil) + return + end + + local files = {} + for _, s in ipairs(j:result()) do + table.insert( + files, + FileEntry.with_layout(opt.default_layout, { + adapter = self, + path = s, + status = "?", + kind = "working", + revs = { + a = left, + b = right, + } + }) + ) + end + callback(nil, files) + end, + }):start() +end, 5) + HgAdapter.flags = { ---@type FlagOption[] switches = { From 3410b7039a918f10d7550651c5fef9bf5ca01f66 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Wed, 21 Dec 2022 07:47:27 +0100 Subject: [PATCH 03/27] fix: handle no tracked files changed --- lua/diffview/vcs/adapters/hg/init.lua | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index bbe5cbb5..84706679 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -698,7 +698,7 @@ HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, op ---@param job Job local function on_exit(job) - utils.handle_job(job, { debug_opt = debug_opt }) + utils.handle_job(job, { debug_opt = debug_opt, fail_on_empty = false }) latch:count_down() end @@ -735,8 +735,12 @@ HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, op numstat_job:start() latch:await() local out_status - if not (#namestat_job:result() == #numstat_job:result() - 1) then - out_status = vcs_utils.ensure_output(2, { namestat_job, numstat_job }, "HgAdapter>tracked_files()") + if + not (#namestat_job:result() == 0 and #numstat_job:result() == 0) + and not (#namestat_job:result() == #numstat_job:result() - 1) + then + out_status = + vcs_utils.ensure_output(2, { namestat_job, numstat_job }, "HgAdapter>tracked_files()") end if out_status == JobStatus.ERROR or not (namestat_job.code == 0 and numstat_job.code == 0 and mergestate_job.code == 0) then @@ -798,8 +802,12 @@ HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, op conflict_map[file.path] = file_info[file.path] end end - local ours_node = find_key(mergestate[1].commits, 'name', 'local').node - local theirs_node = find_key(mergestate[1].commits, 'name', 'other').node + local ours_node + local theirs_node + if #mergestate[1].commits > 0 then + ours_node = find_key(mergestate[1].commits, 'name', 'local').node + theirs_node = find_key(mergestate[1].commits, 'name', 'other').node + end for _, f in pairs(file_info) do if f.status ~= "U" then From 71f0518d0d4115fe2f24c96e21fb65238eb06427 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Wed, 21 Dec 2022 08:45:20 +0100 Subject: [PATCH 04/27] fix: handle null base rev --- lua/diffview/vcs/adapter.lua | 2 +- lua/diffview/vcs/adapters/hg/init.lua | 56 ++++++++++++++++++++++++--- lua/diffview/vcs/adapters/hg/rev.lua | 2 +- 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/lua/diffview/vcs/adapter.lua b/lua/diffview/vcs/adapter.lua index 96fcd718..c08fba92 100644 --- a/lua/diffview/vcs/adapter.lua +++ b/lua/diffview/vcs/adapter.lua @@ -329,7 +329,7 @@ VCSAdapter.show = async.wrap(function(self, path, rev, callback) cwd = self.ctx.toplevel, ---@type Job on_exit = async.void(function(j) - local context = "vcs.utils.show()" + local context = "VCSAdapter.show()" utils.handle_job(j, { fail_on_empty = true, context = context, diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 84706679..8c4cad53 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -816,12 +816,6 @@ HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, op end if kind == "working" and next(conflict_map) then - -- TODO: read and parse content of .hg/merge/state2 - -- O( : others - -- L( : local - -- ancestorlinknode : base - -- HASH is 40 hex characters - -- hg debugmergestate -Tjson for _, v in pairs(conflict_map) do table.insert(conflicts, FileEntry.with_layout(opt.merge_layout, { adapter = self, @@ -906,6 +900,56 @@ HgAdapter.untracked_files = async.wrap(function(self, left, right, opt, callback }):start() end, 5) +---@param self HgAdapter +---@param path string +---@param rev? Rev +---@param callback fun(stderr: string[]?, stdout: string[]?) +HgAdapter.show = async.wrap(function(self, path, rev, callback) + -- File did not exist, need to return an empty buffer + if not(rev) or (rev:object_name() == self.Rev.NULL_TREE_SHA) then + callback(nil, {}) + return + end + + local job = Job:new({ + command = self:bin(), + args = self:get_show_args(path, rev), + cwd = self.ctx.toplevel, + ---@type Job + on_exit = async.void(function(j) + local context = "HgAdapter.show()" + utils.handle_job(j, { + fail_on_empty = true, + context = context, + debug_opt = { no_stdout = true, context = context }, + }) + + if j.code ~= 0 then + callback(j:stderr_result() or {}, nil) + return + end + + local out_status + + if #j:result() == 0 then + async.util.scheduler() + out_status = vcs_utils.ensure_output(2, { j }, context) + end + + if out_status == JobStatus.ERROR then + callback(j:stderr_result() or {}, nil) + return + end + + callback(nil, j:result()) + end), + }) + -- Problem: Running multiple 'show' jobs simultaneously may cause them to fail + -- silently. + -- Solution: queue them and run them one after another. + vcs_utils.queue_sync_job(job) +end, 4) + HgAdapter.flags = { ---@type FlagOption[] switches = { diff --git a/lua/diffview/vcs/adapters/hg/rev.lua b/lua/diffview/vcs/adapters/hg/rev.lua index 53c09d58..d066b2b1 100644 --- a/lua/diffview/vcs/adapters/hg/rev.lua +++ b/lua/diffview/vcs/adapters/hg/rev.lua @@ -7,7 +7,7 @@ local M = {} ---@class HgRev : Rev local HgRev = oop.create_class("HgRev", Rev) -HgRev.NULL_TREE_SHA = "000000000000000000000000000000000000000" +HgRev.NULL_TREE_SHA = "0000000000000000000000000000000000000000" function HgRev:init(rev_type, revision, track_head) local t = type(revision) From 58501368401095e3425c796279095439a11faffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sindre=20T=2E=20Str=C3=B8m?= Date: Wed, 21 Dec 2022 17:09:42 +0100 Subject: [PATCH 05/27] WIP: Amend docs --- doc/diffview.txt | 117 +++++++++++++++++++++++++++++------------------ 1 file changed, 73 insertions(+), 44 deletions(-) diff --git a/doc/diffview.txt b/doc/diffview.txt index 9357877a..29126fb6 100644 --- a/doc/diffview.txt +++ b/doc/diffview.txt @@ -179,75 +179,97 @@ COMMANDS *diffview-commands* :'<,'>DiffviewFileHistory < - Options: ~ + What options are available will be determined by what type of repository + you're targeting. You will find the available options for the supported + VCS tools below. The command completion will do it's best to suggest the + appropriate options. + + Git Options: ~ --base={git-rev} - Specify a base git rev from which the right side of - the diff will be created. Use the special value - `LOCAL` to use the local version of the file. + Specify a base git rev from which the right side of the diff + will be created. Use the special value `LOCAL` to use the + local version of the file. --range={git-rev} - Show only commits in the specified revision range. + Show only commits in the specified revision range. - -C{path} Run as if git was started in {path} instead of the - current working directory. + -C{path} + Run as if git was started in {path} instead of the current + working directory. NOTE: All following options are described in far more detail in the man page for git-log. See `:Man git-log(1)` for more information. - --follow Follow renames (only for single file). + --follow + Follow renames (only for single file). - --first-parent Follow only the first parent upon seeing a merge - commit. + --first-parent + Follow only the first parent upon seeing a merge commit. - --show-pulls Show merge commits that are not TREESAME to its first - parent, but are to a later parent. + --show-pulls + Show merge commits that are not TREESAME to its first parent, + but are to a later parent. - --reflog Include all reachable objects mentioned by reflogs. + --reflog + Include all reachable objects mentioned by reflogs. - --all Include all refs. + --all Include all refs. - --merges List only merge commits. + --merges + List only merge commits. - --no-merges List no merge commits. + --no-merges + List no merge commits. - --reverse List commits in reverse order. + --reverse + List commits in reverse order. -n{n}, --max-count={n} - Limit the number of commits. + Limit the number of commits. -L{start},{end}:{file}, -L:{funcname}:{file} - Trace the evolution of the line range given by - {start},{end}, or by the function name regex - {funcname}, within the {file}. You can specify this - option more than once. + Trace the evolution of the line range given by {start},{end}, + or by the function name regex {funcname}, within the {file}. + You can specify this option more than once. --diff-merges={value} - Determines how merge commits are treated. {value} can - be one of: - • `off` - • `on` - • `first-parent` - • `separate` - • `combined` - • `dense-combined` - • `remerge` + Determines how merge commits are treated. {value} can + be one of: + • `off` + • `on` + • `first-parent` + • `separate` + • `combined` + • `dense-combined` + • `remerge` --author={pattern} - Limit the commits output to ones with author/committer - header lines that match the specified {pattern} - (regular expression). + Limit the commits output to ones with author/committer header + lines that match the specified {pattern} (regular expression). --grep={pattern} - Limit the commits output to ones with log message that - matches the specified {pattern} (regular expression). + Limit the commits output to ones with log message that matches + the specified {pattern} (regular expression). + + -G{pattern} + Look for differences whose patch text contains added/removed + lines that match {pattern} (extended regular expression). + + -S{pattern} + Look for differences that change the number of occurences of + the specified {pattern} (extended regular expression) in a + file. + + Mercurial Options: ~ - -G{pattern} Look for differences whose patch text contains - added/removed lines that match {pattern} (extended - regular expression). + --rev={rev} + Show only commits in the specified revision range. - -S{pattern} Look for differences that change the number of - occurences of the specified {pattern} (extended - regular expression) in a file. + -f, --follow + Follow renames (only for single file). + + -l{n}, --limit={n} + Limit the number of commits. *:DiffviewClose* :DiffviewClose Close the active Diffview. @@ -285,10 +307,17 @@ enhanced_diff_hl *diffview-config-enhanced_diff_h git_cmd *diffview-config-git_cmd* Type: `string[]`, Default: `{ "git" }` - This table forms the first part of all git commands used internally in - the plugin. The first element should be the git binary. The subsequent + This table forms the first part of all Git commands used internally in + the plugin. The first element should be the Git binary. The subsequent elements are passed as arguments. +hg_cmd *diffview-config-hg_cmd* + Type: `string[]`, Default: `{ "hg" }` + + This table forms the first part of all Mercurial commands used + internally in the plugin. The first element should be the Mercurial + binary. The subsequent elements are passed as arguments. + view.x.layout *diffview-config-view.x.layout* Type: > "diff1_plain" From b5c8a0d8811891bfba580e7dbdc764323fb8dc4e Mon Sep 17 00:00:00 2001 From: Zeger Van de Vannet <747627+zegervdv@users.noreply.github.com> Date: Wed, 21 Dec 2022 17:49:06 +0100 Subject: [PATCH 06/27] fix: correct order of flag options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Sindre T. Strøm --- lua/diffview/vcs/adapters/hg/init.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 8c4cad53..02536dc0 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -953,8 +953,8 @@ end, 4) HgAdapter.flags = { ---@type FlagOption[] switches = { - {'--follow', '-f', 'Follow renames'}, - {'--no-merges', '-M', 'List no merge changesets'}, + { '-f', '--follow', 'Follow renames' }, + { '-M', '--no-merges', 'List no merge changesets' }, }, ---@type FlagOption[] options = { From 463f60a38fb9636127c1ed9d426275c6597bc837 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Wed, 21 Dec 2022 17:44:37 +0100 Subject: [PATCH 07/27] fix: only set oldname when a file is renamed --- lua/diffview/vcs/adapters/hg/init.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 02536dc0..649dce97 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -778,7 +778,6 @@ HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, op file_info[name] = { status = status, name = name, - oldname = name, -- TODO stats = stats, } end @@ -797,7 +796,9 @@ HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, op local base = find_key(file.extras, 'key', 'ancestorlinknode') if file.state == 'u' then file_info[file.path].status = 'U' - file_info[file.path].oldname = file.other_path + if file.other_path ~= file.path then + file_info[file.path].oldname = file.other_path + end file_info[file.path].base = base and base.value or nil conflict_map[file.path] = file_info[file.path] end From 264929ea3ac1b796234929ad33a87170632986f7 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Wed, 21 Dec 2022 18:18:21 +0100 Subject: [PATCH 08/27] fix: only walk the mergestate data once --- lua/diffview/vcs/adapters/hg/init.lua | 30 ++++++++++++--------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 649dce97..55dd5f7a 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -783,31 +783,27 @@ HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, op end end - local find_key = function (t, key, value) - for _, v in ipairs(t) do - if v[key] == value then - return v - end - end - end - local mergestate = vim.json.decode(table.concat(mergestate_out, '')) for _, file in ipairs(mergestate[1].files) do - local base = find_key(file.extras, 'key', 'ancestorlinknode') + local base = nil + for _, extra in ipairs(file.extras) do + if extra.key == 'ancestorlinknode' then + base = extra.value + end + end if file.state == 'u' then file_info[file.path].status = 'U' + file_info[file.path].base = base if file.other_path ~= file.path then file_info[file.path].oldname = file.other_path end - file_info[file.path].base = base and base.value or nil conflict_map[file.path] = file_info[file.path] end end - local ours_node - local theirs_node - if #mergestate[1].commits > 0 then - ours_node = find_key(mergestate[1].commits, 'name', 'local').node - theirs_node = find_key(mergestate[1].commits, 'name', 'other').node + + local nodes = {} + for _, commit in ipairs(mergestate[1].commits) do + nodes[commit.name] = commit.node end for _, f in pairs(file_info) do @@ -825,9 +821,9 @@ HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, op status = "U", kind = "conflicting", revs = { - a = self.Rev(RevType.COMMIT, ours_node), + a = self.Rev(RevType.COMMIT, nodes['local']), b = self.Rev(RevType.LOCAL), - c = self.Rev(RevType.COMMIT, theirs_node), + c = self.Rev(RevType.COMMIT, nodes.other), d = self.Rev(RevType.COMMIT, v.base), } })) From 0ed37cb7d3111bb229e1cccbd35039a0e6d5ced4 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Wed, 21 Dec 2022 18:47:21 +0100 Subject: [PATCH 09/27] feat: add log args for hg --- lua/diffview/vcs/adapters/hg/init.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 55dd5f7a..88736f65 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -125,6 +125,11 @@ function HgAdapter:get_show_args(path, rev) return utils.vec_join(self:args(), "cat", "--rev", rev:object_name(), "--", path) end +function HgAdapter:get_log_args(args) +print(" args:", vim.inspect(args)) -- __AUTO_GENERATED_PRINT_VAR__ + return utils.vec_join(self:args(), "log", "--stat", args) +end + function HgAdapter:file_history_options(range, paths, args) local default_args = config.get_config().default_args.DiffviewFileHistory local argo = arg_parser.parse(vim.tbl_flatten({ default_args, args })) From 6ee4efa4c0e4ce7ba5a0492300858bb26df04144 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Wed, 4 Jan 2023 12:03:15 +0100 Subject: [PATCH 10/27] feat: add to_range for hg revs --- lua/diffview/vcs/adapters/hg/rev.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lua/diffview/vcs/adapters/hg/rev.lua b/lua/diffview/vcs/adapters/hg/rev.lua index d066b2b1..0bffd8b4 100644 --- a/lua/diffview/vcs/adapters/hg/rev.lua +++ b/lua/diffview/vcs/adapters/hg/rev.lua @@ -33,5 +33,12 @@ function HgRev:object_name() return self.commit end +function HgRev.to_range(rev_from, rev_to) + if rev_to then + return rev_from .. "::" .. rev_to + end + return rev_from +end + M.HgRev = HgRev return M From eec216e0e00835597ce8587b343aad9dd6cc6f27 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Tue, 17 Jan 2023 17:32:07 +0100 Subject: [PATCH 11/27] fixup! feat: add log args for hg --- lua/diffview/vcs/adapters/hg/init.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 88736f65..90c00bd5 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -126,8 +126,7 @@ function HgAdapter:get_show_args(path, rev) end function HgAdapter:get_log_args(args) -print(" args:", vim.inspect(args)) -- __AUTO_GENERATED_PRINT_VAR__ - return utils.vec_join(self:args(), "log", "--stat", args) + return utils.vec_join("log", "--stat", '--rev', args) end function HgAdapter:file_history_options(range, paths, args) From bbca98d8642e2f55ad6a8c9662c857923ae6bd22 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Tue, 17 Jan 2023 18:07:44 +0100 Subject: [PATCH 12/27] feat: add get_merge_context for hg --- lua/diffview/vcs/adapters/hg/init.lua | 32 +++++++++++++++++++++++++++ lua/diffview/vcs/adapters/hg/rev.lua | 12 ++++++++-- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 90c00bd5..3ffa3141 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -129,6 +129,38 @@ function HgAdapter:get_log_args(args) return utils.vec_join("log", "--stat", '--rev', args) end +function HgAdapter:get_merge_context() + local ret = {} + + local out, code = self:exec_sync({ "debugmergestate", "-Tjson" }, self.ctx.toplevel) + + if code ~= 0 then + return {} + end + + local data = vim.json.decode(table.concat(out, "")) + for _, commit in ipairs(data[1].commits) do + if commit.name == "other" then + ret.theirs = { hash = commit.node } + out, code = self:exec_sync({ "log", "--template={branch}", "--rev", commit.node }) + if code == 0 then + ret.theirs.ref_name = out[1] + end + elseif commit.name == "local" then + ret.ours = { hash = commit.node } + out, code = self:exec_sync({ "log", "--template={branch}", "--rev", commit.node }) + if code == 0 then + ret.ours.ref_name = out[1] + end + end + end + + -- Base is ancestorlinknode (per file) + ret.base = { hash = "" } + + return ret +end + function HgAdapter:file_history_options(range, paths, args) local default_args = config.get_config().default_args.DiffviewFileHistory local argo = arg_parser.parse(vim.tbl_flatten({ default_args, args })) diff --git a/lua/diffview/vcs/adapters/hg/rev.lua b/lua/diffview/vcs/adapters/hg/rev.lua index 0bffd8b4..ed47d544 100644 --- a/lua/diffview/vcs/adapters/hg/rev.lua +++ b/lua/diffview/vcs/adapters/hg/rev.lua @@ -29,8 +29,16 @@ function HgRev:init(rev_type, revision, track_head) self.commit = revision end -function HgRev:object_name() - return self.commit +function HgRev:object_name(abbrev_len) + if self.commit then + if abbrev_len then + return self.commit:sub(1, abbrev_len) + end + + return self.commit + end + + return "UNKNOWN" end function HgRev.to_range(rev_from, rev_to) From 7298a7f509e38de36877d415438c8d8c7ecb6d79 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Tue, 17 Jan 2023 18:26:38 +0100 Subject: [PATCH 13/27] doc: list hg options and refer to documentation where possible --- doc/diffview.txt | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/doc/diffview.txt b/doc/diffview.txt index 29126fb6..788f59dc 100644 --- a/doc/diffview.txt +++ b/doc/diffview.txt @@ -263,7 +263,9 @@ COMMANDS *diffview-commands* Mercurial Options: ~ --rev={rev} - Show only commits in the specified revision range. + Show only commits for the specified revset {rev}. + See `hg help revsets` for the full list of keywords and + constructs. -f, --follow Follow renames (only for single file). @@ -271,6 +273,27 @@ COMMANDS *diffview-commands* -l{n}, --limit={n} Limit the number of commits. + -M, --no-merges + Do not list merge commits. + + --user={text} + Limit the commits to ones with user as specified. + + --keyword={text} + Limit commits to ones matching the {text} in the log message. + + --branch={text} + Limit commits to a specified branch. + + --bookmark={text} + Limit commits to a specified bookmark + + --include={pattern} + Include commits touching files as specified in the {pattern}. + + --exclude={pattern} + Exclude commits touching files as specified in the {pattern}. + *:DiffviewClose* :DiffviewClose Close the active Diffview. @@ -318,6 +341,9 @@ hg_cmd *diffview-config-hg_cmd* internally in the plugin. The first element should be the Mercurial binary. The subsequent elements are passed as arguments. + If your Mercurial install bundles the `chg` binary, this can be + configured here to have a significant performance boost. + view.x.layout *diffview-config-view.x.layout* Type: > "diff1_plain" From 5175646d97e3ad9b14d605ee8fc21d859c64a6ab Mon Sep 17 00:00:00 2001 From: zegervdv Date: Tue, 17 Jan 2023 18:38:28 +0100 Subject: [PATCH 14/27] feat: add file_restore for hg --- lua/diffview/vcs/adapters/hg/init.lua | 57 +++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 3ffa3141..16789b42 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -713,6 +713,63 @@ function HgAdapter:rev_to_args(left, right) end end +function HgAdapter:file_restore(path, kind, commit) + local out, code + local abs_path = utils.path:join(self.ctx.toplevel, path) + local rel_path = utils.path:vim_fnamemodify(abs_path, ":~") + + _, code = self:exec_sync({"cat", "--", path}, self.ctx.toplevel) + + local exists_hg = code == 0 + local exists_local = utils.path:readable(abs_path) + + local undo + + if not exists_hg then + local bn = utils.find_file_buffer(abs_path) + if bn then + async.util.scheduler() + local ok, err = utils.remove_buffer(false, bn) + if not ok then + utils.err({ + ("Failed to delete buffer '%d'! Aborting file restoration. Error message:") + :format(bn), + err + }, true) + return false + end + end + + if kind == "working" or kind == "conflicting" then + -- File is untracked and has no history: delete it from fs. + local ok, err = utils.path:unlink(abs_path) + if not ok then + utils.err({ + ("Failed to delete file '%s'! Aborting file restoration. Error message:") + :format(abs_path), + err + }, true) + return false + end + else + -- File only exists in index + out, code = self:exec_sync( + { "rm", "-f", "--", path }, + self.ctx.toplevel + ) + end + else + -- File exists in history: revert + out, code = self:exec_sync( + utils.vec_join("revert", commit or (kind == "staged" and "HEAD" or nil), "--", path), + self.ctx.toplevel + ) + end + + return true, undo + +end + function HgAdapter:get_files_args(args) return utils.vec_join(self:args(), "status", "--print0", "--unknown", "--no-status", "--template={path}\\n", args) From 4a5b609be1938c46262c3873cd36f1e996d397db Mon Sep 17 00:00:00 2001 From: zegervdv Date: Wed, 25 Jan 2023 07:53:16 +0100 Subject: [PATCH 15/27] fix review comments --- lua/diffview/config.lua | 12 ++++++ lua/diffview/vcs/adapters/hg/commit.lua | 1 + lua/diffview/vcs/adapters/hg/init.lua | 50 +++++++++++++++++++------ 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/lua/diffview/config.lua b/lua/diffview/config.lua index 77528acf..0a1cbd4f 100644 --- a/lua/diffview/config.lua +++ b/lua/diffview/config.lua @@ -248,6 +248,13 @@ M._config = M.defaults ---@field path_args string[] ---@class HgLogOptions +---@field limit integer +---@field user string +---@field no_merges boolean +---@field rev string +---@field keyword string +---@field include string +---@field exclude string ---@alias LogOptions GitLogOptions|HgLogOptions @@ -277,6 +284,11 @@ M.log_option_defaults = { hg = { limit = 256, user = nil, + no_merges = false, + rev = nil, + keyword = nil, + include = nil, + exclude = nil, }, } diff --git a/lua/diffview/vcs/adapters/hg/commit.lua b/lua/diffview/vcs/adapters/hg/commit.lua index bf2235b3..3168aaf8 100644 --- a/lua/diffview/vcs/adapters/hg/commit.lua +++ b/lua/diffview/vcs/adapters/hg/commit.lua @@ -5,6 +5,7 @@ local Commit = require('diffview.vcs.commit').Commit local M = {} +---@class HgCommit : Commit local HgCommit = oop.create_class('HgCommit', Commit) function HgCommit:init(opt) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 16789b42..86d27cf3 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -135,7 +135,7 @@ function HgAdapter:get_merge_context() local out, code = self:exec_sync({ "debugmergestate", "-Tjson" }, self.ctx.toplevel) if code ~= 0 then - return {} + return {ours = {}, theirs = {}, base = {}} end local data = vim.json.decode(table.concat(out, "")) @@ -144,13 +144,13 @@ function HgAdapter:get_merge_context() ret.theirs = { hash = commit.node } out, code = self:exec_sync({ "log", "--template={branch}", "--rev", commit.node }) if code == 0 then - ret.theirs.ref_name = out[1] + ret.theirs.ref_names = out[1] end elseif commit.name == "local" then ret.ours = { hash = commit.node } out, code = self:exec_sync({ "log", "--template={branch}", "--rev", commit.node }) if code == 0 then - ret.ours.ref_name = out[1] + ret.ours.ref_names = out[1] end end end @@ -174,8 +174,6 @@ function HgAdapter:file_history_options(range, paths, args) return v == "." and "." or pl:relative(v, ".") end, paths) - local cwd = cpath or vim.loop.cwd() - local range_arg = argo:get_flag('rev', { no_empty = true }) if range_arg then -- TODO: check if range is valid @@ -205,7 +203,6 @@ function HgAdapter:file_history_options(range, paths, args) local key, _ = names[1]:gsub("%-", "_") local v = argo:get_flag(names, { expect_string = type(config.log_option_defaults[self.config_key][key]) ~= "boolean", - expect_list = names[1] == "L", }) log_options[key] = v end @@ -228,6 +225,26 @@ function HgAdapter:file_history_options(range, paths, args) return log_options end +---@class HgAdapter.PreparedLogOpts +---@field rev_range string +---@field base Rev +---@field path_args string[] +---@field flags string[] +-- +---@class HgAdapter.FHState +---@field thread thread +---@field adapter HgAdapter +---@field path_args string[] +---@field log_options HgLogOptions +---@field prepared_log_opts HgAdapter.PreparedLogOpts +---@field opt vcs.adapter.FileHistoryWorkerSpec +---@field single_file boolean +---@field resume_lock boolean +---@field cur table +---@field commit Commit +---@field entries LogEntry[] +---@field callback function + local function prepare_fh_options(adapter, log_options, single_file) local o = log_options local rev_range, base @@ -246,8 +263,15 @@ local function prepare_fh_options(adapter, log_options, single_file) path_args = log_options.path_args, flags = utils.vec_join( (o.follow and single_file) and { "--follow" } or nil, + o.no_merges and { "--no-merges" } or nil, + o.rev and { "--rev=" .. o.rev } or nil, + o.limit and { "--limit=" .. o.limit } or nil, o.user and { "--user=" .. o.user } or nil, - o.limit and { "--limit=" .. o.limit } or nil + o.keyword and { "--keyword=" .. o.keyword } or nil, + o.branch and { "--branch=" .. o.branch } or nil, + o.bookmark and { "--bookmark=" .. o.bookmark } or nil, + o.include and { "--include=" .. o.include } or nil, + o.exclude and { "--exclude=" .. o.exclude } or nil ), } end @@ -677,7 +701,7 @@ function HgAdapter:diffview_options(args) return {left = left, right = right, options = options} end -function VCSAdapter:rev_to_pretty_string(left, right) +function HgAdapter:rev_to_pretty_string(left, right) if left.track_head and right.type == RevType.LOCAL then return nil elseif left.commit and right.type == RevType.LOCAL then @@ -689,7 +713,11 @@ function VCSAdapter:rev_to_pretty_string(left, right) end function HgAdapter:head_rev() - local out, code = self:exec_sync({ "log", "--template={node}", "--limit=1", "--"}, {cwd = self.ctx.toplevel, retry_on_empty = 2}) + local out, code = self:exec_sync( { "log", "--template={node}", "--limit=1", "--" }, { + cwd = self.ctx.toplevel, + retry_on_empty = 2, + }) + if code ~= 0 then return end @@ -1110,13 +1138,13 @@ function HgAdapter:is_binary(path, rev) end -- TODO: implement completion -function HgAdapter:rev_completion(arg_lead, opt) +function HgAdapter:rev_candidates(arg_lead, opt) return { } end function HgAdapter:init_completion() self.comp.file_history:put({"--rev", "-r"}, function(_, arg_lead) - return self:rev_completion(arg_lead, { accept_range = true }) + return self:rev_candidates(arg_lead, { accept_range = true }) end) self.comp.file_history:put({ "--follow", "-f" }) From b267549d38918dc7636c4c8b02b03aff36d41475 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Wed, 25 Jan 2023 07:58:49 +0100 Subject: [PATCH 16/27] fix: update flags --- lua/diffview/vcs/adapters/hg/init.lua | 60 ++++++--------------------- 1 file changed, 12 insertions(+), 48 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 86d27cf3..3ea6ddd8 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -15,6 +15,7 @@ local CountDownLatch = require("diffview.control").CountDownLatch local FileEntry = require("diffview.scene.file_entry").FileEntry local Diff2Hor = require("diffview.scene.layouts.diff_2_hor").Diff2Hor local LogEntry = require("diffview.vcs.log_entry").LogEntry +local FlagOption = require("diffview.vcs.flag_option").FlagOption local vcs_utils = require("diffview.vcs.utils") ---@type PathLib @@ -1071,62 +1072,25 @@ end, 4) HgAdapter.flags = { ---@type FlagOption[] switches = { - { '-f', '--follow', 'Follow renames' }, - { '-M', '--no-merges', 'List no merge changesets' }, + FlagOption('-f', '--follow', 'Follow renames'), + FlagOption('-M', '--no-merges', 'List no merge changesets'), }, ---@type FlagOption[] options = { - { '=r', '--rev=', 'Revspec', prompt_label = "(Revspec)" }, - { '=l', '--limit=', 'Limit the number of changesets' }, - { '=u', '--user=', 'Filter on user' }, - { '=k', '--keyword=', 'Filter by keyword' }, - { '=b', '--branch=', 'Filter by branch' }, - { '=B', '--bookmark=', 'Filter by bookmark' }, - { '=I', '--include=', 'Include files' }, - { '=E', '--exclude=', 'Exclude files' }, + FlagOption('=r', '--rev=', 'Revspec', {prompt_label = "(Revspec)"}), + FlagOption('=l', '--limit=', 'Limit the number of changesets'), + FlagOption('=u', '--user=', 'Filter on user'), + FlagOption('=k', '--keyword=', 'Filter by keyword'), + FlagOption('=b', '--branch=', 'Filter by branch'), + FlagOption('=B', '--bookmark=', 'Filter by bookmark'), + FlagOption('=I', '--include=', 'Include files'), + FlagOption('=E', '--exclude=', 'Exclude files'), }, } +-- Add reverse lookups for _, list in pairs(HgAdapter.flags) do for i, option in ipairs(list) do - option = vim.tbl_extend("keep", option, { - prompt_fmt = "${label}${flag_name}", - - key = option.key or utils.str_match(option[2], { - "^%-%-?([^=]+)=?", - "^%+%+?([^=]+)=?", - }):gsub("%-", "_"), - - ---@param self FlagOption - ---@param value string|string[] - render_value = function(self, value) - local quoted - - if type(value) == "table" then - quoted = table.concat(vim.tbl_map(function(v) - return self[2] .. utils.str_quote(v, { only_if_whitespace = true }) - end, value), " ") - else - quoted = self[2] .. utils.str_quote(value, { only_if_whitespace = true }) - end - - return value == "", quoted - end, - - ---@param value string|string[] - render_default = function(_, value) - if value == nil then - return "" - elseif type(value) == "table" then - return table.concat(vim.tbl_map(function(v) - v = select(1, v:gsub("\\", "\\\\")) - return utils.str_quote(v, { only_if_whitespace = true }) - end, value), " ") - end - return utils.str_quote(value, { only_if_whitespace = true }) - end, - }) - list[i] = option list[option.key] = option end From 9c6c81de1f471e2229c262364dca4a698f168211 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Fri, 27 Jan 2023 07:56:00 +0100 Subject: [PATCH 17/27] fix: return first ancestorlinknode as merge-base --- lua/diffview/vcs/adapters/hg/init.lua | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 3ea6ddd8..c0c70ec3 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -140,6 +140,9 @@ function HgAdapter:get_merge_context() end local data = vim.json.decode(table.concat(out, "")) + + ret.base = { hash = "" } + for _, commit in ipairs(data[1].commits) do if commit.name == "other" then ret.theirs = { hash = commit.node } @@ -156,8 +159,14 @@ function HgAdapter:get_merge_context() end end - -- Base is ancestorlinknode (per file) - ret.base = { hash = "" } + for _, file in ipairs(data[1].files) do + for _, extra in ipairs(file.extras) do + if (extra.key == 'ancestorlinknode' and extra.value ~= HgAdapter.Rev.NULL_TREE_SHA) then + ret.base.hash = extra.value + break + end + end + end return ret end From dafbf1aba3ad7b7df5df2b4ff13b90c5c2d14274 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Fri, 27 Jan 2023 08:47:01 +0100 Subject: [PATCH 18/27] fix: use direct arguments to indicate range (WIP) --- lua/diffview/vcs/adapters/hg/init.lua | 57 ++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index c0c70ec3..26df2451 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -695,11 +695,9 @@ end function HgAdapter:diffview_options(args) local default_args = config.get_config().default_args.DiffviewOpen local argo = arg_parser.parse(vim.tbl_flatten({ default_args, args })) - local rev_args = argo:get_flag({'rev'}) + local rev_args = argo.args[1] - local head = self:head_rev() - local left = head or HgRev.new_null_tree() - local right = HgRev(RevType.LOCAL) + local left, right = self:parse_revs(rev_args, {}) local options = { show_untracked = true, -- TODO: extract from hg config @@ -743,7 +741,7 @@ function HgAdapter:rev_to_args(left, right) "Can't diff LOCAL against LOCAL!" ) if left.type == RevType.COMMIT and right.type == RevType.COMMIT then - return { '--rev="' .. left.commit .. '::' .. right.commit .. '"' } + return { '--rev=' .. left.commit .. '::' .. right.commit} elseif left.type == RevType.STAGE and right.type == RevType.LOCAL then return {} else @@ -751,6 +749,55 @@ function HgAdapter:rev_to_args(left, right) end end + +---Determine whether a rev arg is a range. +---@param rev_arg string +---@return boolean +function HgAdapter:is_rev_arg_range(rev_arg) + return utils.str_match(rev_arg, { + "%:", + "%:%:", + }) ~= nil +end + +---Parse a given rev arg. +---@param rev_arg string +---@param opt table +---@return Rev? left +---@return Rev? right +function HgAdapter:parse_revs(rev_arg, opt) + ---@type Rev? + local left + ---@type Rev? + local right + + local head = self:head_rev() + ---@cast head Rev + + if not rev_arg then + left = head or HgRev.new_null_tree() + right = HgRev(RevType.LOCAL) + else + local from, to = rev_arg:match("([^:]*)%:%:?(.*)$") + + if from and from ~= "" and to and to ~= "" then + left = HgRev(RevType.COMMIT, from) + right = HgRev(RevType.COMMIT, to) + elseif from and from ~= "" then + left = HgRev(RevType.COMMIT, from) + right = head + elseif to and to ~= "" then + left = HgRev.new_null_tree() + right = HgRev(RevType.COMMIT, to) + else + utils.err(("Failed to parse rev %s"):format(utils.str_quote(rev_arg))) + return + end + end + + return left, right +end + function HgAdapter:file_restore(path, kind, commit) local out, code local abs_path = utils.path:join(self.ctx.toplevel, path) From c0e80ed3fab06d2d637b72879b6ba1f93e1ab0d9 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Thu, 2 Feb 2023 08:40:02 +0100 Subject: [PATCH 19/27] fixup! fix: use direct arguments to indicate range (WIP) --- lua/diffview/vcs/adapters/hg/rev.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lua/diffview/vcs/adapters/hg/rev.lua b/lua/diffview/vcs/adapters/hg/rev.lua index ed47d544..58178361 100644 --- a/lua/diffview/vcs/adapters/hg/rev.lua +++ b/lua/diffview/vcs/adapters/hg/rev.lua @@ -29,6 +29,10 @@ function HgRev:init(rev_type, revision, track_head) self.commit = revision end +function HgRev:new_null_tree() + return HgRev(RevType.COMMIT, HgRev.NULL_TREE_SHA) +end + function HgRev:object_name(abbrev_len) if self.commit then if abbrev_len then From 20f9698db46bdf6ecb035f244f61b9bde29bd967 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Thu, 2 Feb 2023 08:46:55 +0100 Subject: [PATCH 20/27] fixup! fix: use direct arguments to indicate range (WIP) --- lua/diffview/vcs/adapters/hg/init.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 26df2451..b81b24f3 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -698,6 +698,9 @@ function HgAdapter:diffview_options(args) local rev_args = argo.args[1] local left, right = self:parse_revs(rev_args, {}) + if not (left and right) then + return + end local options = { show_untracked = true, -- TODO: extract from hg config @@ -778,7 +781,7 @@ function HgAdapter:parse_revs(rev_arg, opt) left = head or HgRev.new_null_tree() right = HgRev(RevType.LOCAL) else - local from, to = rev_arg:match("([^:]*)%:%:?(.*)$") + local from, to = rev_arg:match("([^:]*)%:?%:?(.*)$") if from and from ~= "" and to and to ~= "" then left = HgRev(RevType.COMMIT, from) From e3db8f4cff0ec0bbc477e7c089e1a150c9559f25 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Thu, 2 Feb 2023 08:52:34 +0100 Subject: [PATCH 21/27] fixup! fix: use direct arguments to indicate range (WIP) --- lua/diffview/vcs/adapters/hg/init.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index b81b24f3..09126f4e 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -793,8 +793,13 @@ function HgAdapter:parse_revs(rev_arg, opt) left = HgRev.new_null_tree() right = HgRev(RevType.COMMIT, to) else - utils.err(("Failed to parse rev %s"):format(utils.str_quote(rev_arg))) - return + local _, code, stderr = self:exec_sync({"log", "--rev=" .. rev_arg}, selc.ctx.toplevel) + if code ~= 0 then + utils.err(("Failed to parse rev %s"):format(utils.str_quote(rev_arg))) + return + end + -- Revset parsed correctly + return rev_arg, nil end end From 868090ecb513a181611760b68c16e98336cacc0f Mon Sep 17 00:00:00 2001 From: zegervdv Date: Wed, 8 Feb 2023 08:30:50 +0100 Subject: [PATCH 22/27] fix: handle empty files --- lua/diffview/vcs/adapters/hg/init.lua | 54 ++++++++++++++++----------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 09126f4e..706464e4 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -713,11 +713,9 @@ function HgAdapter:diffview_options(args) end function HgAdapter:rev_to_pretty_string(left, right) - if left.track_head and right.type == RevType.LOCAL then - return nil - elseif left.commit and right.type == RevType.LOCAL then - return left:abbrev() - elseif left.commit and right.commit then + if left.type == RevType.CUSTOM then + return left.commit + elseif right and right.commit then return left:abbrev() .. "::" .. right:abbrev() end return nil @@ -781,7 +779,7 @@ function HgAdapter:parse_revs(rev_arg, opt) left = head or HgRev.new_null_tree() right = HgRev(RevType.LOCAL) else - local from, to = rev_arg:match("([^:]*)%:?%:?(.*)$") + local from, to = rev_arg:match("([^:]*)%:%:?(.*)$") if from and from ~= "" and to and to ~= "" then left = HgRev(RevType.COMMIT, from) @@ -793,13 +791,20 @@ function HgAdapter:parse_revs(rev_arg, opt) left = HgRev.new_null_tree() right = HgRev(RevType.COMMIT, to) else - local _, code, stderr = self:exec_sync({"log", "--rev=" .. rev_arg}, selc.ctx.toplevel) - if code ~= 0 then - utils.err(("Failed to parse rev %s"):format(utils.str_quote(rev_arg))) + local node, code, stderr = self:exec_sync({"log", "--limit=1", "--template={node}", "--rev=" .. rev_arg}, self.ctx.toplevel) + if code ~= 0 and node then + utils.err(("Failed to parse rev %s: %s"):format(utils.str_quote(rev_arg), stderr)) return end - -- Revset parsed correctly - return rev_arg, nil + left = HgRev(RevType.COMMIT, node[1]) + + node, code, stderr = self:exec_sync({"log", "--limit=1", "--template={node}", "--rev=reverse(" .. rev_arg .. ")"}, self.ctx.toplevel) + if code ~= 0 and node then + utils.err(("Failed to parse rev %s: %s"):format(utils.str_quote(rev_arg), stderr)) + return + end + + right = HgRev(RevType.COMMIT, node[1]) end end @@ -938,6 +943,7 @@ HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, op local namestat_out = namestat_job:result() local mergestate_out = mergestate_job:result() + local data = {} local conflict_map = {} local file_info = {} @@ -945,20 +951,24 @@ HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, op -- Last line in numstat is a summary and should not be used table.remove(numstat_out, -1) - for i, s in ipairs(namestat_out) do + local numstat_info = {} + for _, s in ipairs(numstat_out) do + local name, changes, diffstats = s:match("(%s*)|%s+(%d+)%s+([+-]+)") + if changes and diffstats then + local _, adds = diffstats:gsub("+", "") + + numstat_info[name] = { + additions = tonumber(adds), + deletions = tonumber(changes) - tonumber(adds), + } + end + end + + for _, s in ipairs(namestat_out) do local status = s:sub(1, 1):gsub("%s", " ") local name = vim.trim(s:match("[%a%s]%s*(.*)")) - local stats = {} - local changes, diffstats = numstat_out[i]:match(".*|%s+(%d+)%s+([+-]+)") - if changes and diffstats then - local _, adds = diffstats:gsub("+", "") - - stats = { - additions = tonumber(adds), - deletions = tonumber(changes) - tonumber(adds), - } - end + local stats = numstat_info[name] or {} if not (kind == "staged") then file_info[name] = { From e84d44f88bdd01d7e8f4acb3827006de64e28287 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Tue, 14 Feb 2023 07:36:37 +0100 Subject: [PATCH 23/27] fixup! fix: use direct arguments to indicate range (WIP) --- lua/diffview/vcs/adapters/hg/init.lua | 4 ++++ lua/diffview/vcs/adapters/hg/rev.lua | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 706464e4..6cfe12ce 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -805,6 +805,10 @@ function HgAdapter:parse_revs(rev_arg, opt) end right = HgRev(RevType.COMMIT, node[1]) + -- If we refer to a single revision, show diff with working directory + if node[1] == left.commit then + right = HgRev(RevType.LOCAL ) + end end end diff --git a/lua/diffview/vcs/adapters/hg/rev.lua b/lua/diffview/vcs/adapters/hg/rev.lua index 58178361..ccb6e3e6 100644 --- a/lua/diffview/vcs/adapters/hg/rev.lua +++ b/lua/diffview/vcs/adapters/hg/rev.lua @@ -46,10 +46,12 @@ function HgRev:object_name(abbrev_len) end function HgRev.to_range(rev_from, rev_to) - if rev_to then - return rev_from .. "::" .. rev_to + if rev_from and rev_to then + return rev_from.commit .. "::" .. rev_to.commit + elseif rev_to then + return "::" .. rev_to.commit end - return rev_from + return rev_from.commit .. "::" end M.HgRev = HgRev From f03052e41e0e2849043f2816017e6a181af8bf38 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Tue, 14 Feb 2023 07:48:55 +0100 Subject: [PATCH 24/27] feat: add rev completion --- lua/diffview/vcs/adapters/hg/init.lua | 33 ++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 6cfe12ce..245177bf 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -1181,7 +1181,38 @@ end -- TODO: implement completion function HgAdapter:rev_candidates(arg_lead, opt) - return { } + opt = vim.tbl_extend("keep", opt or {}, { accept_range = false }) --[[@as RevCompletionSpec ]] + logger.lvl(1).debug("[completion] Revision candidates requested") + + local branches = self:exec_sync( + { "branches", "--template={branch}\n" }, + { cwd = self.ctx.toplevel, silent = true } + ) + + local heads = self:exec_sync( + { "heads", "--template={node|short}\n" }, + { cwd = self.ctx.toplevel, silent = true } + ) + + local ret = utils.vec_join(heads, branches) + + if opt.accept_range then + local _, range_end = utils.str_match(arg_lead, { + "^(%:%:?)()$", + "^(%:%:?)()[^:]", + "[^:](%:%:?)()$", + "[^:](%:%:?)()[^:]", + }) + + if range_end then + local range_lead = arg_lead:sub(1, range_end - 1) + ret = vim.tbl_map(function(v) + return range_lead .. v + end, ret) + end + end + + return ret end function HgAdapter:init_completion() From 0cd8fb814b635c8178a10ac5bce81d7a44884b1d Mon Sep 17 00:00:00 2001 From: zegervdv Date: Wed, 15 Feb 2023 19:08:18 +0100 Subject: [PATCH 25/27] fix: match file names from numstat output correctly --- lua/diffview/vcs/adapters/hg/init.lua | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 245177bf..a30e6ae8 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -953,11 +953,11 @@ HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, op local file_info = {} -- Last line in numstat is a summary and should not be used - table.remove(numstat_out, -1) + table.remove(numstat_out, #numstat_out) local numstat_info = {} for _, s in ipairs(numstat_out) do - local name, changes, diffstats = s:match("(%s*)|%s+(%d+)%s+([+-]+)") + local name, changes, diffstats = s:match("%s*([^|]*)%s+|%s+(%d+)%s+([+-]+)") if changes and diffstats then local _, adds = diffstats:gsub("+", "") @@ -969,17 +969,19 @@ HgAdapter.tracked_files = async.wrap(function (self, left, right, args, kind, op end for _, s in ipairs(namestat_out) do - local status = s:sub(1, 1):gsub("%s", " ") - local name = vim.trim(s:match("[%a%s]%s*(.*)")) + if s ~= " " then + local status = s:sub(1, 1):gsub("%s", " ") + local name = vim.trim(s:match("[%a%s]%s*(.*)")) - local stats = numstat_info[name] or {} + local stats = numstat_info[name] or {} - if not (kind == "staged") then - file_info[name] = { - status = status, - name = name, - stats = stats, - } + if not (kind == "staged") then + file_info[name] = { + status = status, + name = name, + stats = stats, + } + end end end From 673b58fe1a0b13f4a4fdcca5166ac41a2156c5c9 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Thu, 16 Feb 2023 22:04:33 +0100 Subject: [PATCH 26/27] fix: return error message from HgAdapter.create --- lua/diffview/vcs/adapters/hg/init.lua | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index a30e6ae8..74744afa 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -94,15 +94,29 @@ function M.find_toplevel(top_indicators) local rel_path = pl:relative(v, ".") return utils.str_quote(rel_path == "" and "." or rel_path) end, top_indicators) --[[@as vector ]], ", ")) - ), nil + ), "" end +---@param toplevel string +---@param path_args string[] +---@param cpath string? +---@return string? err +---@return HgAdapter function M.create(toplevel, path_args, cpath) - return HgAdapter({ + local err + local adapter = HgAdapter({ toplevel = toplevel, path_args = path_args, cpath = cpath, }) + + if not adapter.ctx.toplevel then + err = "Could not file top-level of the repository!" + elseif not pl:is_dir(adapter.ctx.toplevel) then + err = "The top-level is not a readable directory: " .. adapter.ctx.toplevel + end + + return err, adapter end function HgAdapter:init(opt) From 660ec6988dfae88a123c7cd0c93d6eb80dda3b53 Mon Sep 17 00:00:00 2001 From: zegervdv Date: Sun, 19 Feb 2023 17:52:37 +0100 Subject: [PATCH 27/27] fix: address review comments --- lua/diffview/scene/views/diff/diff_view.lua | 3 ++- lua/diffview/vcs/adapters/hg/init.lua | 6 ++++-- lua/diffview/vcs/adapters/hg/rev.lua | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lua/diffview/scene/views/diff/diff_view.lua b/lua/diffview/scene/views/diff/diff_view.lua index fe04da15..dd7935a3 100644 --- a/lua/diffview/scene/views/diff/diff_view.lua +++ b/lua/diffview/scene/views/diff/diff_view.lua @@ -17,6 +17,7 @@ local debounce = lazy.require("diffview.debounce") ---@module "diffview.debounce local logger = lazy.require("diffview.logger") ---@module "diffview.logger" local utils = lazy.require("diffview.utils") ---@module "diffview.utils" local vcs_utils = lazy.require("diffview.vcs.utils") ---@module "diffview.vcs.utils" +local GitAdapter = lazy.access("diffview.vcs.adapters.git", "GitAdapter") ---@type GitAdapter|LazyModule local api = vim.api local M = {} @@ -125,7 +126,7 @@ function DiffView:post_open() name = ("diffview://%s/log/%d/%s"):format(self.adapter.ctx.dir, self.tabpage, "commit_log"), }) - if config.get_config().watch_index then + if config.get_config().watch_index and self.adapter:instanceof(GitAdapter.__get()) then self.watcher = vim.loop.new_fs_poll() ---@diagnostic disable-next-line: unused-local self.watcher:start(self.adapter.ctx.dir .. "/index", 1000, function(err, prev, cur) diff --git a/lua/diffview/vcs/adapters/hg/init.lua b/lua/diffview/vcs/adapters/hg/init.lua index 74744afa..8e259a80 100644 --- a/lua/diffview/vcs/adapters/hg/init.lua +++ b/lua/diffview/vcs/adapters/hg/init.lua @@ -727,8 +727,10 @@ function HgAdapter:diffview_options(args) end function HgAdapter:rev_to_pretty_string(left, right) - if left.type == RevType.CUSTOM then - return left.commit + if left.track_head and right.type == RevType.LOCAL then + return nil + elseif left.commit and right.type == RevType.LOCAL then + return left:abbrev() elseif right and right.commit then return left:abbrev() .. "::" .. right:abbrev() end diff --git a/lua/diffview/vcs/adapters/hg/rev.lua b/lua/diffview/vcs/adapters/hg/rev.lua index ccb6e3e6..f539831c 100644 --- a/lua/diffview/vcs/adapters/hg/rev.lua +++ b/lua/diffview/vcs/adapters/hg/rev.lua @@ -29,7 +29,7 @@ function HgRev:init(rev_type, revision, track_head) self.commit = revision end -function HgRev:new_null_tree() +function HgRev.new_null_tree() return HgRev(RevType.COMMIT, HgRev.NULL_TREE_SHA) end