feat: git credential backend for credential storage (#371)
## Problem
Credentials were stored as plaintext JSON in
`stdpath('data')/cp-nvim.json`, with no integration with system
credential managers.
## Solution
Replace file-based credential storage with `git credential
fill/approve/reject`, delegating to whatever credential helper the user
has configured (`cache`, `store`, `libsecret`, macOS Keychain, etc.).
- New `lua/cp/git_credential.lua` module wrapping the git credential
protocol
- All credential consumers (`credentials.lua`, `submit.lua`,
`scraper.lua`) use `git_credential` directly — `cache.lua` no longer
handles credentials
- CSES API token packed into the password field (`password<TAB>token`)
so it works with helpers that ignore the `path` field
- `has_helper()` guard on `:CP login`, `:CP logout`, and `:CP submit`
with an error message if no helper is configured
- Healthcheck split into `[required]`/`[optional]` sections; git version
and credential helper status shown
- `git` checked at startup in `check_required_runtime()`
- Cache version system (`CACHE_VERSION`, v1→v2 migration) removed — the
cache file is now a plain JSON blob
- `:CP` command gets `bar = true`
This commit is contained in:
parent
27d7a4e6b5
commit
da4e2ebeba
12 changed files with 283 additions and 150 deletions
107
lua/cp/git_credential.lua
Normal file
107
lua/cp/git_credential.lua
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
---@class cp.Credentials
|
||||
---@field username string
|
||||
---@field password string
|
||||
|
||||
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 _helper_checked = false
|
||||
local _helper_ok = false
|
||||
|
||||
---@return boolean
|
||||
function M.has_helper()
|
||||
if not _helper_checked then
|
||||
local r = vim
|
||||
.system({ 'git', 'config', 'credential.helper' }, { text = true, timeout = 5000 })
|
||||
:wait()
|
||||
_helper_ok = r.code == 0 and r.stdout ~= nil and vim.trim(r.stdout) ~= ''
|
||||
_helper_checked = true
|
||||
end
|
||||
return _helper_ok
|
||||
end
|
||||
|
||||
---@param host string
|
||||
---@param extra? table<string, string>
|
||||
---@return string
|
||||
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
|
||||
|
||||
---@param stdout string
|
||||
---@return table<string, string>
|
||||
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
|
||||
|
||||
---@param platform string
|
||||
---@return cp.Credentials?
|
||||
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
|
||||
|
||||
return { username = parsed.username, password = parsed.password }
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param creds cp.Credentials
|
||||
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()
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param creds cp.Credentials
|
||||
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()
|
||||
end
|
||||
|
||||
return M
|
||||
Loading…
Add table
Add a link
Reference in a new issue