Newer
Older
dotfiles / .config / lite-xl / plugins / scm / backend / git.lua
-- Backend implementation for Git.
-- More details at: https://git-scm.com/

local common = require "core.common"
local Backend = require "plugins.scm.backend"

---@class plugins.scm.backend.git : plugins.scm.backend
---@field super plugins.scm.backend
local Git = Backend:extend()

function Git:new()
  self.super.new(self, "Git", "git")
end

function Git:detect(directory)
  local list = system.list_dir(directory)
  if list then
    for _, file in ipairs(list) do
      if file == ".git" then
        return true
      end
    end
  end
  return false
end

---@return boolean
function Git:has_staging()
  return true
end

---@param file string Absolute path to file
---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Git:stage_file(file, directory, callback)
  self:execute(function(proc)
    local success = false
    local errmsg = ""
    local stdout = self:get_process_output(proc, "stdout")
    local stderr = self:get_process_output(proc, "stderr")
    if proc:returncode() == 0 then
      success = true
    else
      if stderr ~= "" then
        errmsg = stderr
      elseif stdout ~= "" then
        errmsg = stdout
      end
    end
    callback(success, errmsg)
  end, directory, "add", common.relative_path(directory, file))
end

---@param file string Absolute path to file
---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Git:unstage_file(file, directory, callback)
  self:execute(function(proc)
    local success = false
    local errmsg = ""
    local stdout = self:get_process_output(proc, "stdout")
    local stderr = self:get_process_output(proc, "stderr")
    if proc:returncode() == 0 then
      success = true
    else
      if stderr ~= "" then
        errmsg = stderr
      elseif stdout ~= "" then
        errmsg = stdout
      end
    end
    callback(success, errmsg)
  end, directory, "restore", "--staged", common.relative_path(directory, file))
end

---@param directory string Project directory
---@param callback plugins.scm.backend.ongetstaged
function Git:get_staged(directory, callback)
  directory = directory:gsub("[/\\]$", "/")
  local cached = self:get_from_cache("get_staged", directory)
  if cached then callback(cached, true) return end
  self:execute(function(proc)
    ---@type table<string,boolean>
    local staged = {}
    for idx, line in self:get_process_lines(proc, "stdout") do
      if line ~= "" then
        local trimmed_file = line:gsub("^%s+", ""):gsub("%s+$", "")
        staged[trimmed_file] = true
      end
      if idx % 50 == 0 then
        self:yield()
      end
    end
    self:add_to_cache("get_staged", staged, directory)
    callback(staged)
  end, directory, "diff", "--name-only", "--cached")
end

---@param callback plugins.scm.backend.ongetbranch
function Git:get_branch(directory, callback)
  self:execute(function(proc)
    local branch = nil
    for idx, line in self:get_process_lines(proc, "stdout") do
      local result = line:match("^[^%s]+")
      if result then
        branch = result
        break
      end
      if idx % 50 == 0 then
        self:yield()
      end
    end
    callback(branch)
  end, directory, "--no-optional-locks", "rev-parse", "--abbrev-ref", "HEAD")
end

---@param directory string
---@param callback plugins.scm.backend.ongetchanges
function Git:get_changes(directory, callback)
  directory = directory:gsub("[/\\]$", "/")
  local cached = self:get_from_cache("get_changes", directory)
  if cached then callback(cached, true) return end
  self:get_staged(directory, function(staged_files)
    self:execute(function(proc)
      ---@type plugins.scm.backend.filechange[]
      local changes = {}
      local added = {}
      for idx, line in self:get_process_lines(proc, "stdout") do
        if line ~= "" then
          local status, path = line:match("%s*(%S+)%s+(%S+)")
          local new_path = nil
          if status and path and not added[path] then
            if status == "A" then
              status = "added"
            elseif status == "D" then
              status = "deleted"
            elseif status == "M" then
              status = "edited"
            elseif status == "R" then
              status = "renamed"
              new_path = line:match("%s*%S+%s+%S+%s*%S+%s*(%S+)")
            elseif status == "??" then
              status = "untracked"
            end
            table.insert(changes, {
              status = status,
              staged = staged_files[path] or nil,
              path = directory .. PATHSEP .. path,
              new_path = new_path and (directory .. PATHSEP .. new_path) or nil
            })
            added[path] = true
          end
        end
        if idx % 50 == 0 then
          self:yield()
        end
      end
      self:add_to_cache("get_changes", changes, directory)
      callback(changes)
    end, directory, "--no-optional-locks", "status", "--short")
  end)
end

---@param directory string
---@param callback plugins.scm.backend.ongetdiff
function Git:get_diff(directory, callback)
  self:execute(function(proc)
    local diff = self:get_process_output(proc, "stdout")
    callback(diff)
  end, directory, "diff")
end

---@param file string
---@param callback plugins.scm.backend.ongetdiff
function Git:get_file_diff(file, directory, callback)
  local cached = self:get_from_cache("get_file_diff", file)
  if cached then callback(cached, true) return end
  self:execute(function(proc)
    local diff = self:get_process_output(proc, "stdout")
    self:add_to_cache("get_file_diff", diff, directory, 1)
    callback(diff)
  end, directory, "diff", common.relative_path(directory, file))
end

---@param file string
---@param directory string
---@param callback plugins.scm.backend.ongetfilestatus
function Git:get_file_status(file, directory, callback)
  local cached = self:get_from_cache("get_file_status", file)
  if cached then callback(cached, true) return end
  self:execute(function(proc)
    local status = "unchanged"
    local output = self:get_process_output(proc, "stdout")
    for line in output:gmatch("[^\n]+") do
      if line ~= "" then
        status = line:match("^%s*(%S+)")
        if status then
          if status == "A" then
            status = "added"
          elseif status == "D" then
            status = "deleted"
          elseif status == "M" then
            status = "edited"
          elseif status == "R" then
            status = "renamed"
          elseif status == "??" then
            status = "untracked"
          end
          break
        end
      end
      self:yield()
    end
    self:add_to_cache("get_file_status", status, file, 1)
    callback(status)
  end, directory, "status", "-s", common.relative_path(directory, file))
end

---@param callback plugins.scm.backend.ongetstats
function Git:get_stats(directory, callback)
  self:execute(function(proc)
    local inserts = 0
    local deletes = 0
    for idx, line in self:get_process_lines(proc, "stdout") do
      if line ~= "" then
        local i, d = line:match("%s*(%d+)%s+(%d+)")
        inserts = inserts + (tonumber(i) or 0)
        deletes = deletes + (tonumber(d) or 0)
      end
      if idx % 50 == 0 then
        self:yield()
      end
    end
    callback({inserts = inserts, deletes = deletes})
  end, directory, "--no-optional-locks", "diff", "--numstat")
end

---@param directory string Project directory
---@param callback plugins.scm.backend.ongetstatus
function Git:get_status(directory, callback)
  self:execute(function(proc)
    local status = ""
    local stdout = self:get_process_output(proc, "stdout")
    local stderr = self:get_process_output(proc, "stderr")
    if stderr ~= "" then
      status = stderr
    elseif stdout ~= "" then
      status = stdout
    end
    callback(status)
  end, directory, "status")
end

---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Git:pull(directory, callback)
  self:execute(function(proc)
    local success = false
    local errmsg = ""
    local stdout = self:get_process_output(proc, "stdout")
    local stderr = self:get_process_output(proc, "stderr")
    if proc:returncode() == 0 then
      success = true
    else
      if stderr ~= "" then
        errmsg = stderr
      elseif stdout ~= "" then
        errmsg = stdout
      end
    end
    callback(success, errmsg)
  end, directory, "pull")
end

---@param file string Absolute path to file
---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Git:revert_file(file, directory, callback)
  self:execute(function(proc)
    local success = false
    local errmsg = ""
    local stdout = self:get_process_output(proc, "stdout")
    local stderr = self:get_process_output(proc, "stderr")
    if proc:returncode() == 0 then
      success = true
    else
      if stderr ~= "" then
        errmsg = stderr
      elseif stdout ~= "" then
        errmsg = stdout
      end
    end
    callback(success, errmsg)
  end, directory, "restore", common.relative_path(directory, file))
end

---@param path string Absolute path to file
---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Git:add_path(path, directory, callback)
  self:execute(function(proc)
    local success = false
    local errmsg = ""
    local stdout = self:get_process_output(proc, "stdout")
    local stderr = self:get_process_output(proc, "stderr")
    if proc:returncode() == 0 then
      success = true
    else
      if stderr ~= "" then
        errmsg = stderr
      elseif stdout ~= "" then
        errmsg = stdout
      end
    end
    callback(success, errmsg)
  end, directory, "add", common.relative_path(directory, path))
end

---@param path string Absolute path to file
---@param directory string Project directory
---@param callback plugins.scm.backend.onexecstatus
function Git:remove_path(path, directory, callback)
  self:execute(function(proc)
    local success = false
    local errmsg = ""
    local stdout = self:get_process_output(proc, "stdout")
    local stderr = self:get_process_output(proc, "stderr")
    if proc:returncode() == 0 then
      success = true
    else
      if stderr ~= "" then
        errmsg = stderr
      elseif stdout ~= "" then
        errmsg = stdout
      end
    end
    callback(success, errmsg)
  end, directory, "rm", "-r", "--cached", common.relative_path(directory, path))
end

---@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 Git:move_path(from, to, directory, callback)
  self:execute(
    function(proc)
      local success = false
      local errmsg = ""
      local stdout = self:get_process_output(proc, "stdout")
      local stderr = self:get_process_output(proc, "stderr")
      if proc:returncode() == 0 then
        success = true
      else
        if stderr ~= "" then
          errmsg = stderr
        elseif stdout ~= "" then
          errmsg = stdout
        end
      end
      callback(success, errmsg)
    end,
    directory, "mv",
    common.relative_path(directory, from),
    common.relative_path(directory, to)
  )
end


return Git