Newer
Older
dotfiles / .config / lite-xl / plugins / scm / backend / init.lua
local core = require "core"
local Object = require "core.object"

---@alias plugins.scm.backend.filestatus
---| "added"
---| "edited"
---| "deleted"
---| "renamed"
---| "unchanged"
---| "untracked"

---@class plugins.scm.backend.filechange
---@field status plugins.scm.backend.filestatus
---@field staged boolean
---@field path string
---@field new_path string?

---@class plugins.scm.backend.stats
---@field inserts integer
---@field deletes integer

---@class plugins.scm.backend.cache
---@field name string
---@field path string
---@field expires number
---@field value any

---@alias plugins.scm.backend.onexecute fun(proc?:process, errmsg?:string, errcode?:number)
---@alias plugins.scm.backend.ongetdiff fun(diff?:string, cached?:boolean)
---@alias plugins.scm.backend.ongetbranch fun(branch?:string, cached?:boolean)
---@alias plugins.scm.backend.ongetchanges fun(changes:plugins.scm.backend.filechange[], cached?:boolean)
---@alias plugins.scm.backend.ongetfilestatus fun(status?:plugins.scm.backend.filestatus, cached?:boolean)
---@alias plugins.scm.backend.ongetstaged fun(files?:table<string,boolean>, cached?:boolean)
---@alias plugins.scm.backend.ongetstats fun(stats?:plugins.scm.backend.stats, cached?:boolean)
---@alias plugins.scm.backend.ongetstatus fun(status?:string, cached?:boolean)
---@alias plugins.scm.backend.onexecstatus fun(success:boolean, errmsg?:string)

---Base functionality to implement a SCM backend with async support.
---@class plugins.scm.backend : core.object
---@field name string
---@field blocking boolean
---@field command string
---@field cache plugins.scm.backend.cache[]
---@field super plugins.scm.backend
local Backend = Object:extend()

---Constructor
---@param name string
---@param command string
function Backend:new(name, command)
  self.name = name
  self.command = command
  self.cache = {}
  self.blocking = false
end

---Execute coroutine.yield if blocking mode is disabled.
---@param wait? number
function Backend:yield(wait)
  if not self.blocking then
    coroutine.yield(wait)
  end
end

---Enable or disable coroutine execution of process calls.
---@param enabled boolean
function Backend:set_blocking_mode(enabled)
  self.blocking = enabled
end

---Add a value into a temporary cache, this is useful to cache the ouput
---of commands for faster retrieveal until the given expire period.
---@param name string
---@param value any
---@param path string
---@param expires? integer Amount of seconds to expire, defaults to 5
function Backend:add_to_cache(name, value, path, expires)
  local found = nil
  for i, cache in ipairs(self.cache) do
    if cache.name == name and cache.path == path then
      found = i
      break
    end
  end

  if found then
    self.cache[found].value = value
    self.cache[found].expires = os.time() + (expires or 5)
  else
    table.insert(self.cache, {
      name = name,
      value = value,
      path = path,
      expires = os.time() + (expires or 5)
    })
  end
end

---Get a value that was previously stored on the cache.
---@param name string
---@param path string
function Backend:get_from_cache(name, path)
  local found = nil
  for i, cache in ipairs(self.cache) do
    if cache.name == name and cache.path == path then
      found = i
      break
    end
  end

  if found then
    if self.cache[found].expires >= os.time() then
      return self.cache[found].value
    end
    table.remove(self.cache, found)
  end

  return nil
end

---Iterates over all the lines that the running process is outputting.
---The iterator can return an empty line while the process gets ready to output.
---@param proc process
---@param from? string | "stdout" | "stderr"
---@return fun():integer,string
function Backend:get_process_lines(proc, from)
  local output = self:get_process_output(proc, from)
  return coroutine.wrap(function()
    local line_num = 1
    for line in (output.."\n"):gmatch("(.-)".."\n") do
      coroutine.yield(line_num, line)
      line_num = line_num + 1
    end
  end)
end

---Gets all the output of the process at once, this function yields
---while reading to allow async support when ran from a coroutine.
---@param proc process
---@param from? string | "stdout" | "stderr"
---@return string
function Backend:get_process_output(proc, from)
  if not proc then return "" end
  from = from and "read_" .. from or "read_stdout"
  local output = ""
  local read_size = 1024 * 10
  local read = proc[from](proc, read_size)
  repeat
    if read ~= nil and read ~= "" then
      output = output .. read
    end
    self:yield(0.01)
    read = proc[from](proc, read_size)
  until (read == nil or read == "") and not proc:running()
  return output
end

---Call the scm command with the given parameters.
---@param callback plugins.scm.backend.onexecute
---@param directory string Path of project directory
---@param ... string parameters to pass to associated command
function Backend:execute(callback, directory, ...)
  local command = table.pack(self.command, ...)
  local proc, errmsg, errcode = process.start(command, {cwd = directory})
  if not self.blocking then
    core.add_thread(function()
      callback(proc, errmsg, errcode)
      if proc and proc:running() then proc:kill() end
    end)
  else
    callback(proc, errmsg, errcode)
    if proc and proc:running() then proc:kill() end
  end
  if not proc then
    core.error(
      "[SCM error]: error while executing '%s' - %s:%s",
      table.concat(command, " "),
      errmsg,
      errcode
    )
  end
end

---Check if given directory is source controlled by current backend.
---@param directory string Project directory
---@return boolean detected
function Backend:detect(directory) return false end

---Report if the backend has a staging area.
---@return boolean
function Backend:has_staging() return false end

---Add a file path to staging area.
---@param file string Absolute path to file
---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Backend:stage_file(file, directory, callback) callback(false, "not implemented") end

---Remove a file path from staging area.
---@param file string Absolute path to file
---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Backend:unstage_file(file, directory, callback) callback(false, "not implemented") end

---Retrieve the list of all staged files.
---@param directory string Project directory
---@param callback plugins.scm.backend.ongetstaged
function Backend:get_staged(directory, callback) callback(nil) end

---Retrieve the current branch.
---@param directory string Project directory
---@param callback plugins.scm.backend.ongetbranch
function Backend:get_branch(directory, callback) callback(nil) end

---Retrieve a list of file changes.
---@param directory string Project directory
---@param callback plugins.scm.backend.ongetchanges
function Backend:get_changes(directory, callback) callback({}, false) end

---Retrieve the entire project unified diff for the given file.
---@param directory string Project directory
---@param callback plugins.scm.backend.ongetdiff
function Backend:get_diff(directory, callback) callback(nil) end

---Retrieve the unified diff for the given file.
---@param file string Absolute path to file
---@param directory string Project directory
---@param callback plugins.scm.backend.ongetdiff
function Backend:get_file_diff(file, directory, callback) callback(nil) end

---Retrieve the current status of the given file.
---@param file string Absolute path to file
---@param directory string Project directory
---@param callback plugins.scm.backend.ongetfilestatus
function Backend:get_file_status(file, directory, callback) callback("unchanged") end

---Retrieve insertion and deletion stats for an entire project.
---@param directory string Project directory
---@param callback plugins.scm.backend.ongetstats
function Backend:get_stats(directory, callback) callback({0, 0}) end

---Retrieve the status description for an entire repo.
---@param directory string Project directory
---@param callback plugins.scm.backend.ongetstatus
function Backend:get_status(directory, callback) callback(nil) end

---Pull latest changes.
---TODO: this is a WIP we should handle remote and branch
---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Backend:pull(directory, callback) callback(false, "not implemented") end

---Restore a file to its previous HEAD state before any changes.
---@param file string Absolute path to file
---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Backend:revert_file(file, directory, callback) callback(false, "not implemented") end

---Add a directory or file to repository
---@param path string Absolute path to file
---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Backend:add_path(path, directory, callback) callback(false, "not implemented") end

---Remove a file or directory from repository without deleting it.
---@param path string Absolute path to file
---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Backend:remove_path(path, directory, callback) callback(false, "not implemented") end

---Rename a path of a directory or file in the repository.
---@param from string Path to move
---@param to string Destination of from path
---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Backend:move_path(from, to, directory, callback) callback(false, "not implemented") end


return Backend