From 72c72fbc411e83c45695afec401bce2905938c0a Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 7 Mar 2026 19:46:22 -0500 Subject: [PATCH] feat(credentials): add `git_credential` module Problem: credentials were stored as plaintext JSON in the cache file, with no integration with system credential managers. Solution: add `lua/cp/git_credential.lua` wrapping the `git credential fill/approve/reject` protocol. Maps each platform to its host, handles CSES token as a second entry with `path=api-token`. --- lua/cp/git_credential.lua | 104 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 lua/cp/git_credential.lua diff --git a/lua/cp/git_credential.lua b/lua/cp/git_credential.lua new file mode 100644 index 0000000..c6fbf01 --- /dev/null +++ b/lua/cp/git_credential.lua @@ -0,0 +1,104 @@ +local M = {} + +local HOSTS = { + atcoder = 'atcoder.jp', + codechef = 'www.codechef.com', + codeforces = 'codeforces.com', + cses = 'cses.fi', + kattis = 'open.kattis.com', + usaco = 'usaco.org', +} + +local function _build_input(host, extra) + local lines = { 'protocol=https', 'host=' .. host } + if extra then + for k, v in pairs(extra) do + table.insert(lines, k .. '=' .. v) + end + end + table.insert(lines, '') + table.insert(lines, '') + return table.concat(lines, '\n') +end + +local function _parse_output(stdout) + local result = {} + for line in stdout:gmatch('[^\n]+') do + local k, v = line:match('^(%S+)=(.+)$') + if k and v then + result[k] = v + end + end + return result +end + +function M.get(platform) + local host = HOSTS[platform] + if not host then + return nil + end + + local input = _build_input(host) + local obj = vim + .system({ 'git', 'credential', 'fill' }, { stdin = input, text = true, timeout = 5000 }) + :wait() + if obj.code ~= 0 then + return nil + end + + local parsed = _parse_output(obj.stdout or '') + if not parsed.username or not parsed.password then + return nil + end + + local creds = { username = parsed.username, password = parsed.password } + + if platform == 'cses' then + local token_input = _build_input(host, { path = 'api-token' }) + local token_obj = vim + .system({ 'git', 'credential', 'fill' }, { stdin = token_input, text = true, timeout = 5000 }) + :wait() + if token_obj.code == 0 then + local token_parsed = _parse_output(token_obj.stdout or '') + if token_parsed.password then + creds.token = token_parsed.password + end + end + end + + return creds +end + +function M.store(platform, creds) + local host = HOSTS[platform] + if not host then + return + end + + local input = _build_input(host, { username = creds.username, password = creds.password }) + vim.system({ 'git', 'credential', 'approve' }, { stdin = input, text = true }):wait() + + if platform == 'cses' and creds.token then + local token_input = + _build_input(host, { path = 'api-token', username = creds.username, password = creds.token }) + vim.system({ 'git', 'credential', 'approve' }, { stdin = token_input, text = true }):wait() + end +end + +function M.reject(platform, creds) + local host = HOSTS[platform] + if not host or not creds then + return + end + + local input = _build_input(host, { username = creds.username, password = creds.password }) + vim.system({ 'git', 'credential', 'reject' }, { stdin = input, text = true }):wait() + + if platform == 'cses' and creds.token then + local token_input = + _build_input(host, { path = 'api-token', username = creds.username, password = creds.token }) + vim.system({ 'git', 'credential', 'reject' }, { stdin = token_input, text = true }):wait() + end +end + +return M