Skip to content

Commit ce72370

Browse files
skt041959dlyongemallo
authored andcommitted
feat: add Perforce (P4) VCS adapter
Cherry-picked from skt041959/diffview.nvim (commit 0aae525).
1 parent cef1454 commit ce72370

7 files changed

Lines changed: 1360 additions & 1 deletion

File tree

doc/diffview_defaults.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ DEFAULT CONFIG *diffview.defaults*
77
enhanced_diff_hl = false, -- See |diffview-config-enhanced_diff_hl|
88
git_cmd = { "git" }, -- The git executable followed by default args.
99
hg_cmd = { "hg" }, -- The hg executable followed by default args.
10+
p4_cmd = { "p4" }, -- The p4 executable followed by default args.
1011
rename_threshold = nil, -- Integer 0-100 for rename detection similarity. Nil uses git default (50%). Invalid values are ignored.
1112
use_icons = true, -- Requires nvim-web-devicons or mini.icons
1213
show_help_hints = true, -- Show hints for how to open the help panel
@@ -107,6 +108,10 @@ DEFAULT CONFIG *diffview.defaults*
107108
single_file = {},
108109
multi_file = {},
109110
},
111+
p4 = {
112+
single_file = {},
113+
multi_file = {},
114+
},
110115
},
111116
win_config = { -- See |diffview-config-win_config|
112117
position = "bottom",

lua/diffview/config.lua

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,6 +619,10 @@ function M.setup(user_config)
619619
M._config.git_cmd = M.defaults.git_cmd
620620
end
621621

622+
if #M._config.p4_cmd == 0 then
623+
M._config.p4_cmd = M.defaults.p4_cmd
624+
end
625+
622626
do
623627
local rename_threshold = M._config.rename_threshold
624628

lua/diffview/health.lua

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ function M.check()
6868
local adapter_kinds = {
6969
{ class = require("diffview.vcs.adapters.git").GitAdapter, name = "Git" },
7070
{ class = require("diffview.vcs.adapters.hg").HgAdapter, name = "Mercurial" },
71+
{ class = require("diffview.vcs.adapters.p4").P4Adapter, name = "Perforce" },
7172
}
7273

7374
for _, kind in ipairs(adapter_kinds) do
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
local lazy = require("diffview.lazy")
2+
local oop = require('diffview.oop')
3+
local Commit = require('diffview.vcs.commit').Commit
4+
local RevType = require('diffview.vcs.rev').RevType
5+
local utils = require('diffview.utils')
6+
7+
local M = {}
8+
9+
---@class P4Commit : Commit
10+
local P4Commit = oop.create_class('P4Commit', Commit)
11+
12+
---P4Commit constructor
13+
---@param opt table
14+
function P4Commit:init(opt)
15+
self:super(opt) -- Calls base Commit init
16+
17+
-- Perforce describe output gives Unix timestamp directly.
18+
-- Timezone handling might require parsing the date string or relying on system locale.
19+
-- Let's use the raw timestamp and format it simply for now.
20+
if self.time then
21+
self.iso_date = os.date("%Y-%m-%d %H:%M:%S", self.time) -- Simple local time formatting
22+
self.rel_date = utils.relative_time(self.time) -- Use existing utility if possible
23+
end
24+
self.hash = opt.changelist -- Store CL number as 'hash' for consistency
25+
end
26+
27+
-- Helper to parse p4 describe output
28+
local function parse_describe_output(output_lines)
29+
local data = { files = {} }
30+
local reading_files = false
31+
local reading_diff = false
32+
local current_file_diff = {}
33+
34+
for i, line in ipairs(output_lines) do
35+
if not reading_files and not reading_diff then
36+
local cl, date_str, user_at_client, desc_start = line:match("^Change (%d+) on ([^%s]+) by ([^@]+@[^%s]+) (.*)")
37+
if cl then
38+
data.changelist = cl
39+
-- Attempt to parse date - p4 dates can be yyyy/mm/dd:hh:mm:ss
40+
-- Let's stick to the Unix timestamp provided later for accuracy
41+
data.user = user_at_client:match("^(.*)@")
42+
data.client = user_at_client:match("@(.*)$")
43+
data.description = desc_start .. "\n"
44+
elseif line:match("^Description:") then
45+
-- Ignore, handled above or description continues
46+
elseif line:match("^Affected files ...") or line:match("^Differences ...") then
47+
reading_files = true
48+
else -- Continue capturing multi-line description
49+
if data.description and line:match("^ ") then -- Indented lines are part of desc
50+
data.description = data.description .. line:sub(2) .. "\n"
51+
end
52+
end
53+
elseif reading_files then
54+
local path, rev, action = line:match("^... (%S+)#(%d+) (%S+)")
55+
if path then
56+
table.insert(data.files, { path = path, rev = rev, action = action })
57+
elseif line:match("^Differences ...$") then
58+
reading_files = false
59+
reading_diff = true
60+
elseif line == "" then -- End of file list section
61+
reading_files = false
62+
end
63+
-- elseif reading_diff then
64+
-- Diff parsing is handled separately if needed, `p4 describe -du` gives unified diff
65+
end
66+
-- Look for the timestamp which is usually near the end
67+
local time_unix = line:match("^\*edit @ (%d+)") or line:match("^\*add @ (%d+)") or line:match("^\*delete @ (%d+)") or line:match("^\*branch @ (%d+)") or line:match("^\*integrate @ (%d+)")
68+
if time_unix then
69+
data.time = tonumber(time_unix)
70+
end
71+
end
72+
-- Clean up description whitespace
73+
if data.description then
74+
data.description = vim.trim(data.description)
75+
end
76+
77+
-- Simple relative date calculation based on timestamp
78+
if data.time then
79+
data.rel_date = utils.relative_time(data.time)
80+
else
81+
-- Fallback if timestamp wasn't found (unlikely for submitted CLs)
82+
data.rel_date = "unknown"
83+
end
84+
85+
return data
86+
end
87+
88+
89+
---@param rev_arg string # Changelist number or specifier like @CL
90+
---@param adapter P4Adapter
91+
---@return P4Commit?
92+
function P4Commit.from_rev_arg(rev_arg, adapter)
93+
local cl_num = rev_arg:match("^@(%d+)$") or rev_arg:match("^(%d+)$")
94+
if not cl_num then
95+
-- Handle other revspecs? For now, only support CL numbers for commit details.
96+
-- Could potentially support labels or #head, but describe works best with CLs.
97+
-- Maybe run 'p4 changes -m1 rev_arg' first to resolve to a CL number.
98+
local resolve_out, resolve_code = adapter:exec_sync({ "changes", "-m1", rev_arg }, adapter.ctx.toplevel)
99+
if resolve_code ~= 0 or #resolve_out == 0 then return nil end
100+
cl_num = resolve_out[1]:match("^Change (%d+)")
101+
if not cl_num then return nil end
102+
end
103+
104+
-- Fetch changelist details using p4 describe
105+
local out, code = adapter:exec_sync({ "describe", cl_num }, adapter.ctx.toplevel)
106+
107+
if code ~= 0 or #out == 0 then
108+
return nil
109+
end
110+
111+
local data = parse_describe_output(out)
112+
if not data.changelist then return nil end
113+
114+
return P4Commit({
115+
changelist = data.changelist, -- Store the actual CL number
116+
hash = data.changelist, -- Use CL number as 'hash' for internal consistency
117+
author = data.user,
118+
time = data.time, -- Unix timestamp
119+
subject = data.description:match("^[^\n]*") or ("Changelist " .. data.changelist), -- First line of description
120+
body = data.description,
121+
rel_date = data.rel_date, -- Calculated relative date
122+
-- Perforce doesn't have direct ref names like git branches/tags in describe output
123+
})
124+
end
125+
126+
M.P4Commit = P4Commit
127+
M.parse_describe_output = parse_describe_output
128+
return M

0 commit comments

Comments
 (0)