refactor(git): route every git invocation through util.git
This commit is contained in:
+33
-38
@@ -22,17 +22,12 @@ local function git_cmds()
|
|||||||
if cached_cmds then
|
if cached_cmds then
|
||||||
return cached_cmds
|
return cached_cmds
|
||||||
end
|
end
|
||||||
local result = vim.system(
|
local out = util.git({ "--list-cmds=main,others,alias" })
|
||||||
{ "git", "--list-cmds=main,others,alias" },
|
if not out then
|
||||||
{ text = true }
|
|
||||||
)
|
|
||||||
:wait()
|
|
||||||
if result.code ~= 0 then
|
|
||||||
util.error("git --list-cmds failed: %s", vim.trim(result.stderr or ""))
|
|
||||||
return {}
|
return {}
|
||||||
end
|
end
|
||||||
cached_cmds = {}
|
cached_cmds = {}
|
||||||
for line in (result.stdout or ""):gmatch("[^\r\n]+") do
|
for line in out:gmatch("[^\r\n]+") do
|
||||||
if line ~= "" then
|
if line ~= "" then
|
||||||
table.insert(cached_cmds, line)
|
table.insert(cached_cmds, line)
|
||||||
end
|
end
|
||||||
@@ -191,10 +186,16 @@ end
|
|||||||
local function run_in_split(r, args, conf)
|
local function run_in_split(r, args, conf)
|
||||||
util.git(args, {
|
util.git(args, {
|
||||||
cwd = r.worktree,
|
cwd = r.worktree,
|
||||||
on_done = function(stdout)
|
on_exit = function(result)
|
||||||
if not stdout then
|
if result.code ~= 0 then
|
||||||
|
util.error(
|
||||||
|
"git %s failed: %s",
|
||||||
|
args[1] or "?",
|
||||||
|
vim.trim(result.stderr or "")
|
||||||
|
)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
local stdout = result.stdout or ""
|
||||||
local buf = place_split("[Git " .. table.concat(args, " ") .. "]")
|
local buf = place_split("[Git " .. table.concat(args, " ") .. "]")
|
||||||
repo.bind(buf, r)
|
repo.bind(buf, r)
|
||||||
object.attach_dispatch(buf)
|
object.attach_dispatch(buf)
|
||||||
@@ -216,14 +217,11 @@ end
|
|||||||
---@param r ow.Git.Repo
|
---@param r ow.Git.Repo
|
||||||
---@param args string[]
|
---@param args string[]
|
||||||
local function run_to_messages(r, args)
|
local function run_to_messages(r, args)
|
||||||
local cmd = { "git" }
|
util.git(args, {
|
||||||
vim.list_extend(cmd, args)
|
cwd = r.worktree,
|
||||||
vim.system(
|
on_exit = function(result)
|
||||||
cmd,
|
local out = vim.trim(result.stdout or "")
|
||||||
{ cwd = r.worktree, text = true },
|
local err = vim.trim(result.stderr or "")
|
||||||
vim.schedule_wrap(function(obj)
|
|
||||||
local out = vim.trim(obj.stdout or "")
|
|
||||||
local err = vim.trim(obj.stderr or "")
|
|
||||||
local chunks = {}
|
local chunks = {}
|
||||||
if out ~= "" then
|
if out ~= "" then
|
||||||
table.insert(chunks, { out })
|
table.insert(chunks, { out })
|
||||||
@@ -234,17 +232,17 @@ local function run_to_messages(r, args)
|
|||||||
end
|
end
|
||||||
table.insert(chunks, { err, "ErrorMsg" })
|
table.insert(chunks, { err, "ErrorMsg" })
|
||||||
end
|
end
|
||||||
if #chunks == 0 and obj.code ~= 0 then
|
if #chunks == 0 and result.code ~= 0 then
|
||||||
table.insert(
|
table.insert(
|
||||||
chunks,
|
chunks,
|
||||||
{ "git exited " .. tostring(obj.code), "ErrorMsg" }
|
{ "git exited " .. tostring(result.code), "ErrorMsg" }
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
if #chunks > 0 then
|
if #chunks > 0 then
|
||||||
vim.api.nvim_echo(chunks, true, {})
|
vim.api.nvim_echo(chunks, true, {})
|
||||||
end
|
end
|
||||||
end)
|
end,
|
||||||
)
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param args string[]
|
---@param args string[]
|
||||||
@@ -337,11 +335,11 @@ end
|
|||||||
---@return string[]
|
---@return string[]
|
||||||
local function list_files(r, dir)
|
local function list_files(r, dir)
|
||||||
return r:get_cached("files:" .. dir, function(self)
|
return r:get_cached("files:" .. dir, function(self)
|
||||||
local cmd = { "git", "ls-files" }
|
local args = { "ls-files" }
|
||||||
if dir ~= "" then
|
if dir ~= "" then
|
||||||
table.insert(cmd, dir)
|
table.insert(args, dir)
|
||||||
end
|
end
|
||||||
local out = util.exec(cmd, { cwd = self.worktree, silent = true })
|
local out = util.git(args, { cwd = self.worktree, silent = true })
|
||||||
return out and util.split_lines(out) or {}
|
return out and util.split_lines(out) or {}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -350,8 +348,8 @@ end
|
|||||||
---@return string[]
|
---@return string[]
|
||||||
local function list_remotes(r)
|
local function list_remotes(r)
|
||||||
return r:get_cached("remotes", function(self)
|
return r:get_cached("remotes", function(self)
|
||||||
local out = util.exec(
|
local out = util.git(
|
||||||
{ "git", "remote" },
|
{ "remote" },
|
||||||
{ cwd = self.worktree, silent = true }
|
{ cwd = self.worktree, silent = true }
|
||||||
)
|
)
|
||||||
return out and util.split_lines(out) or {}
|
return out and util.split_lines(out) or {}
|
||||||
@@ -382,13 +380,10 @@ local function fetch_completions(sub)
|
|||||||
if cached_completions[sub] then
|
if cached_completions[sub] then
|
||||||
return cached_completions[sub]
|
return cached_completions[sub]
|
||||||
end
|
end
|
||||||
local out = util.exec(
|
local out = util.git(
|
||||||
{ "git", sub, "--git-completion-helper-all" },
|
{ sub, "--git-completion-helper-all" },
|
||||||
{ silent = true }
|
{ silent = true }
|
||||||
) or util.exec(
|
) or util.git({ sub, "--git-completion-helper" }, { silent = true })
|
||||||
{ "git", sub, "--git-completion-helper" },
|
|
||||||
{ silent = true }
|
|
||||||
)
|
|
||||||
local items = {}
|
local items = {}
|
||||||
if out then
|
if out then
|
||||||
for tok in out:gmatch("%S+") do
|
for tok in out:gmatch("%S+") do
|
||||||
@@ -469,8 +464,8 @@ function M.complete_rev(arg_lead)
|
|||||||
|
|
||||||
local stage, stage_path_lead = arg_lead:match("^:([0-3]):(.*)$")
|
local stage, stage_path_lead = arg_lead:match("^:([0-3]):(.*)$")
|
||||||
if stage then
|
if stage then
|
||||||
local out = util.exec(
|
local out = util.git(
|
||||||
{ "git", "ls-files", "--stage" },
|
{ "ls-files", "--stage" },
|
||||||
{ cwd = r.worktree, silent = true }
|
{ cwd = r.worktree, silent = true }
|
||||||
)
|
)
|
||||||
if not out then
|
if not out then
|
||||||
@@ -506,11 +501,11 @@ function M.complete_rev(arg_lead)
|
|||||||
name_lead = name_lead or path_lead
|
name_lead = name_lead or path_lead
|
||||||
|
|
||||||
if rev ~= "" then
|
if rev ~= "" then
|
||||||
local cmd = { "git", "ls-tree", rev }
|
local args = { "ls-tree", rev }
|
||||||
if dir ~= "" then
|
if dir ~= "" then
|
||||||
table.insert(cmd, dir)
|
table.insert(args, dir)
|
||||||
end
|
end
|
||||||
local out = util.exec(cmd, { cwd = r.worktree, silent = true })
|
local out = util.git(args, { cwd = r.worktree, silent = true })
|
||||||
if not out then
|
if not out then
|
||||||
return {}
|
return {}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ end
|
|||||||
---@param max_count integer?
|
---@param max_count integer?
|
||||||
---@return string?
|
---@return string?
|
||||||
local function fetch(worktree, max_count)
|
local function fetch(worktree, max_count)
|
||||||
local cmd = {
|
local args = {
|
||||||
"git",
|
|
||||||
"log",
|
"log",
|
||||||
"--graph",
|
"--graph",
|
||||||
"--all",
|
"--all",
|
||||||
@@ -42,9 +41,9 @@ local function fetch(worktree, max_count)
|
|||||||
"--format=format:" .. LOG_FORMAT,
|
"--format=format:" .. LOG_FORMAT,
|
||||||
}
|
}
|
||||||
if max_count then
|
if max_count then
|
||||||
table.insert(cmd, "--max-count=" .. max_count)
|
table.insert(args, "--max-count=" .. max_count)
|
||||||
end
|
end
|
||||||
return util.exec(cmd, { cwd = worktree })
|
return util.git(args, { cwd = worktree })
|
||||||
end
|
end
|
||||||
|
|
||||||
---@type table<string, integer> -- worktree -> max_count
|
---@type table<string, integer> -- worktree -> max_count
|
||||||
|
|||||||
+7
-8
@@ -96,8 +96,8 @@ local function attach_index_writer(buf, r, path)
|
|||||||
vim.api.nvim_buf_get_lines(buf, 0, -1, false),
|
vim.api.nvim_buf_get_lines(buf, 0, -1, false),
|
||||||
"\n"
|
"\n"
|
||||||
) .. "\n"
|
) .. "\n"
|
||||||
local hash_stdout = util.exec(
|
local hash_stdout = util.git(
|
||||||
{ "git", "hash-object", "-w", "--stdin" },
|
{ "hash-object", "-w", "--stdin" },
|
||||||
{ cwd = r.worktree, stdin = body }
|
{ cwd = r.worktree, stdin = body }
|
||||||
)
|
)
|
||||||
if not hash_stdout then
|
if not hash_stdout then
|
||||||
@@ -108,8 +108,8 @@ local function attach_index_writer(buf, r, path)
|
|||||||
local mode = state and state.index_mode
|
local mode = state and state.index_mode
|
||||||
if not mode then
|
if not mode then
|
||||||
mode = "100644"
|
mode = "100644"
|
||||||
local ls = util.exec(
|
local ls = util.git(
|
||||||
{ "git", "ls-files", "-s", "--", path },
|
{ "ls-files", "-s", "--", path },
|
||||||
{ cwd = r.worktree, silent = true }
|
{ cwd = r.worktree, silent = true }
|
||||||
)
|
)
|
||||||
if ls then
|
if ls then
|
||||||
@@ -123,8 +123,7 @@ local function attach_index_writer(buf, r, path)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
if
|
if
|
||||||
not util.exec({
|
not util.git({
|
||||||
"git",
|
|
||||||
"update-index",
|
"update-index",
|
||||||
"--cacheinfo",
|
"--cacheinfo",
|
||||||
mode,
|
mode,
|
||||||
@@ -171,8 +170,8 @@ end
|
|||||||
---@return boolean ok
|
---@return boolean ok
|
||||||
local function populate(buf, r, rev, state, rev_sha)
|
local function populate(buf, r, rev, state, rev_sha)
|
||||||
local rev_str = rev:format()
|
local rev_str = rev:format()
|
||||||
local stdout = util.exec(
|
local stdout = util.git(
|
||||||
{ "git", "cat-file", "-p", rev_str },
|
{ "cat-file", "-p", rev_str },
|
||||||
{ cwd = r.worktree }
|
{ cwd = r.worktree }
|
||||||
)
|
)
|
||||||
if not stdout then
|
if not stdout then
|
||||||
|
|||||||
+21
-21
@@ -38,8 +38,7 @@ local global = util.Emitter.new()
|
|||||||
local Repo = {}
|
local Repo = {}
|
||||||
Repo.__index = Repo
|
Repo.__index = Repo
|
||||||
|
|
||||||
local STATUS_CMD = {
|
local STATUS_ARGS = {
|
||||||
"git",
|
|
||||||
"--no-optional-locks",
|
"--no-optional-locks",
|
||||||
"-c",
|
"-c",
|
||||||
"core.quotePath=false",
|
"core.quotePath=false",
|
||||||
@@ -52,20 +51,22 @@ local STATUS_CMD = {
|
|||||||
|
|
||||||
---@private
|
---@private
|
||||||
function Repo:_fetch_status()
|
function Repo:_fetch_status()
|
||||||
vim.system(
|
util.git(STATUS_ARGS, {
|
||||||
STATUS_CMD,
|
cwd = self.worktree,
|
||||||
{ cwd = self.worktree, text = true },
|
on_exit = function(result)
|
||||||
vim.schedule_wrap(function(obj)
|
|
||||||
self._cache = {}
|
self._cache = {}
|
||||||
if obj.code ~= 0 then
|
if result.code ~= 0 then
|
||||||
util.error("git status failed: %s", vim.trim(obj.stderr or ""))
|
util.error(
|
||||||
|
"git status failed: %s",
|
||||||
|
vim.trim(result.stderr or "")
|
||||||
|
)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self.status = status.parse(obj.stdout or "")
|
self.status = status.parse(result.stdout or "")
|
||||||
self._events:emit("refresh", self.status)
|
self._events:emit("refresh", self.status)
|
||||||
global:emit("refresh", self, self.status)
|
global:emit("refresh", self, self.status)
|
||||||
end)
|
end,
|
||||||
)
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
function Repo:refresh()
|
function Repo:refresh()
|
||||||
@@ -242,8 +243,7 @@ end
|
|||||||
---@return string[]
|
---@return string[]
|
||||||
function Repo:list_refs()
|
function Repo:list_refs()
|
||||||
return self:get_cached("refs", function(self)
|
return self:get_cached("refs", function(self)
|
||||||
local out = util.exec({
|
local out = util.git({
|
||||||
"git",
|
|
||||||
"for-each-ref",
|
"for-each-ref",
|
||||||
"--format=%(refname:short)",
|
"--format=%(refname:short)",
|
||||||
"refs/heads",
|
"refs/heads",
|
||||||
@@ -287,8 +287,8 @@ function Repo:list_stash_refs()
|
|||||||
return {}
|
return {}
|
||||||
end
|
end
|
||||||
local refs = { "stash" }
|
local refs = { "stash" }
|
||||||
local out = util.exec(
|
local out = util.git(
|
||||||
{ "git", "stash", "list", "--pretty=format:%gd" },
|
{ "stash", "list", "--pretty=format:%gd" },
|
||||||
{ cwd = self.worktree, silent = true }
|
{ cwd = self.worktree, silent = true }
|
||||||
)
|
)
|
||||||
if out then
|
if out then
|
||||||
@@ -304,12 +304,12 @@ end
|
|||||||
---@param short boolean
|
---@param short boolean
|
||||||
---@return string?
|
---@return string?
|
||||||
function Repo:rev_parse(rev, short)
|
function Repo:rev_parse(rev, short)
|
||||||
local cmd = { "git", "rev-parse", "--verify", "--quiet" }
|
local args = { "rev-parse", "--verify", "--quiet" }
|
||||||
if short then
|
if short then
|
||||||
table.insert(cmd, "--short")
|
table.insert(args, "--short")
|
||||||
end
|
end
|
||||||
table.insert(cmd, rev)
|
table.insert(args, rev)
|
||||||
local stdout = util.exec(cmd, { cwd = self.worktree, silent = true })
|
local stdout = util.git(args, { cwd = self.worktree, silent = true })
|
||||||
local trimmed = stdout and vim.trim(stdout) or ""
|
local trimmed = stdout and vim.trim(stdout) or ""
|
||||||
return trimmed ~= "" and trimmed or nil
|
return trimmed ~= "" and trimmed or nil
|
||||||
end
|
end
|
||||||
@@ -321,8 +321,8 @@ end
|
|||||||
---@return ow.Git.Repo.ResolveStatus
|
---@return ow.Git.Repo.ResolveStatus
|
||||||
function Repo:resolve_sha(prefix)
|
function Repo:resolve_sha(prefix)
|
||||||
local result = self:get_cached("resolve:" .. prefix, function(self)
|
local result = self:get_cached("resolve:" .. prefix, function(self)
|
||||||
local out = util.exec(
|
local out = util.git(
|
||||||
{ "git", "rev-parse", "--disambiguate=" .. prefix },
|
{ "rev-parse", "--disambiguate=" .. prefix },
|
||||||
{ cwd = self.worktree, silent = true }
|
{ cwd = self.worktree, silent = true }
|
||||||
)
|
)
|
||||||
local trimmed = out and vim.trim(out) or ""
|
local trimmed = out and vim.trim(out) or ""
|
||||||
|
|||||||
+29
-29
@@ -460,17 +460,19 @@ local function action_stage()
|
|||||||
if #paths == 0 then
|
if #paths == 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local cmd = { "git", "add", "--" }
|
local args = { "add", "--" }
|
||||||
vim.list_extend(cmd, paths)
|
vim.list_extend(args, paths)
|
||||||
vim.system(
|
util.git(args, {
|
||||||
cmd,
|
cwd = s.repo.worktree,
|
||||||
{ cwd = s.repo.worktree },
|
on_exit = function(result)
|
||||||
vim.schedule_wrap(function(obj)
|
if result.code ~= 0 then
|
||||||
if obj.code ~= 0 then
|
util.error(
|
||||||
util.error("git add failed: %s", vim.trim(obj.stderr or ""))
|
"git add failed: %s",
|
||||||
end
|
vim.trim(result.stderr or "")
|
||||||
end)
|
|
||||||
)
|
)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local function action_unstage()
|
local function action_unstage()
|
||||||
@@ -481,7 +483,7 @@ local function action_unstage()
|
|||||||
if item.kind ~= "staged" then
|
if item.kind ~= "staged" then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local cmd = { "git", "restore", "--staged", "--" }
|
local args = { "restore", "--staged", "--" }
|
||||||
local entries
|
local entries
|
||||||
if item.is_header then
|
if item.is_header then
|
||||||
entries = s.repo.status:by_kind("staged")
|
entries = s.repo.status:by_kind("staged")
|
||||||
@@ -494,22 +496,21 @@ local function action_unstage()
|
|||||||
end
|
end
|
||||||
for _, e in ipairs(entries) do
|
for _, e in ipairs(entries) do
|
||||||
if e.orig then
|
if e.orig then
|
||||||
table.insert(cmd, e.orig)
|
table.insert(args, e.orig)
|
||||||
end
|
end
|
||||||
table.insert(cmd, e.path)
|
table.insert(args, e.path)
|
||||||
end
|
end
|
||||||
vim.system(
|
util.git(args, {
|
||||||
cmd,
|
cwd = s.repo.worktree,
|
||||||
{ cwd = s.repo.worktree },
|
on_exit = function(result)
|
||||||
vim.schedule_wrap(function(obj)
|
if result.code ~= 0 then
|
||||||
if obj.code ~= 0 then
|
|
||||||
util.error(
|
util.error(
|
||||||
"git restore --staged failed: %s",
|
"git restore --staged failed: %s",
|
||||||
vim.trim(obj.stderr or "")
|
vim.trim(result.stderr or "")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end)
|
end,
|
||||||
)
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
local function action_discard()
|
local function action_discard()
|
||||||
@@ -542,18 +543,17 @@ local function action_discard()
|
|||||||
elseif item.kind == "unstaged" then
|
elseif item.kind == "unstaged" then
|
||||||
prompt = string.format("Discard changes to %s?", item.path)
|
prompt = string.format("Discard changes to %s?", item.path)
|
||||||
action = function()
|
action = function()
|
||||||
vim.system(
|
util.git({ "checkout", "--", item.path }, {
|
||||||
{ "git", "checkout", "--", item.path },
|
cwd = s.repo.worktree,
|
||||||
{ cwd = s.repo.worktree },
|
on_exit = function(result)
|
||||||
vim.schedule_wrap(function(obj)
|
if result.code ~= 0 then
|
||||||
if obj.code ~= 0 then
|
|
||||||
util.error(
|
util.error(
|
||||||
"git checkout failed: %s",
|
"git checkout failed: %s",
|
||||||
vim.trim(obj.stderr or "")
|
vim.trim(result.stderr or "")
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end)
|
end,
|
||||||
)
|
})
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
return
|
return
|
||||||
|
|||||||
+16
-26
@@ -189,16 +189,7 @@ end
|
|||||||
---@field cwd string?
|
---@field cwd string?
|
||||||
---@field stdin string?
|
---@field stdin string?
|
||||||
---@field silent boolean?
|
---@field silent boolean?
|
||||||
---@field on_done fun(stdout: string?)?
|
---@field on_exit fun(result: vim.SystemCompleted)?
|
||||||
|
|
||||||
---@param args string[]
|
|
||||||
---@param opts ow.Git.Util.ExecOpts?
|
|
||||||
---@return string?
|
|
||||||
function M.git(args, opts)
|
|
||||||
local cmd = { "git" }
|
|
||||||
vim.list_extend(cmd, args)
|
|
||||||
return M.exec(cmd, opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param cmd string[]
|
---@param cmd string[]
|
||||||
---@param opts ow.Git.Util.ExecOpts?
|
---@param opts ow.Git.Util.ExecOpts?
|
||||||
@@ -207,30 +198,29 @@ function M.exec(cmd, opts)
|
|||||||
opts = opts or {}
|
opts = opts or {}
|
||||||
local sys_opts = { cwd = opts.cwd, stdin = opts.stdin, text = true }
|
local sys_opts = { cwd = opts.cwd, stdin = opts.stdin, text = true }
|
||||||
|
|
||||||
local function handle(result)
|
if opts.on_exit then
|
||||||
|
vim.system(cmd, sys_opts, vim.schedule_wrap(opts.on_exit))
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local result = vim.system(cmd, sys_opts):wait()
|
||||||
if result.code ~= 0 then
|
if result.code ~= 0 then
|
||||||
if not opts.silent then
|
if not opts.silent then
|
||||||
local label = cmd[2] and (cmd[1] .. " " .. cmd[2])
|
local label = cmd[2] and (cmd[1] .. " " .. cmd[2]) or cmd[1] or "?"
|
||||||
or cmd[1]
|
|
||||||
or "?"
|
|
||||||
M.error("%s failed: %s", label, vim.trim(result.stderr or ""))
|
M.error("%s failed: %s", label, vim.trim(result.stderr or ""))
|
||||||
end
|
end
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
return result.stdout or ""
|
return result.stdout or ""
|
||||||
end
|
end
|
||||||
|
|
||||||
if opts.on_done then
|
---@param args string[]
|
||||||
vim.system(
|
---@param opts ow.Git.Util.ExecOpts?
|
||||||
cmd,
|
---@return string?
|
||||||
sys_opts,
|
function M.git(args, opts)
|
||||||
vim.schedule_wrap(function(result)
|
local cmd = { "git" }
|
||||||
opts.on_done(handle(result))
|
vim.list_extend(cmd, args)
|
||||||
end)
|
return M.exec(cmd, opts)
|
||||||
)
|
|
||||||
return nil
|
|
||||||
end
|
|
||||||
return handle(vim.system(cmd, sys_opts):wait())
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class ow.Git.Util.Emitter<T>
|
---@class ow.Git.Util.Emitter<T>
|
||||||
|
|||||||
Reference in New Issue
Block a user