diff options
Diffstat (limited to '')
-rw-r--r-- | LICENSE (renamed from LISCENSE) | 2 | ||||
-rw-r--r-- | README.md | 70 | ||||
-rw-r--r-- | bootstrap.lua | 8 | ||||
-rw-r--r-- | eatit-cfg.lua | 76 | ||||
-rw-r--r-- | eatit.lua | 221 | ||||
-rw-r--r-- | main.lua | 247 | ||||
-rw-r--r-- | modules/README.md | 28 | ||||
-rw-r--r-- | modules/lssi/LSSI.md | 16 | ||||
-rw-r--r-- | modules/lssi/lssi.lua | 133 | ||||
-rw-r--r-- | proc.lua | 85 |
10 files changed, 387 insertions, 499 deletions
@@ -1,6 +1,6 @@ Eat It - a Mpv plugin manager -Copyright © 2023 squibid +Copyright (c) 2024 squibid This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -1,25 +1,63 @@ # Eat It -Eat It is a plugin manager for mpv which allows you to declare what files you -want from git repos. +Preface - eatit is a "plugin manager" for mpv it's sole purpose is to provide a +declerative way to install and update your plugins. Loading the plugins is done +by mpv itself. +## Installation +put this script into your ~/.config/mpv/scripts/ directory: +```lua +-- install eat-it on startup +local mp = require("mp") +local utils = require("mp.utils") +local path = mp.comand_native({ "expand-path", "~~/scripts/eat-it" }) -## Installing Eat It +if not utils.readdir(path) then + mp.command_native_async({ + name = "subprocess", + playback_only = false, + args = { "git", "clone", "--depth=1", "https://git.squi.bid/dep", path } + }) +end +``` +## Setup +in ~/.config/mpv/eatit-cfg.lua put: +```lua +return { + -- list of packages +} +``` +### Package Spec +```lua +{ + -- [string] Specifies the full name of the package (required) + "user/package", -### First time -To install eatit run these two commands [Make sure you know what it does](https://explainshell.com/explain?cmd=curl+-L+https%3A%2F%2Fgit.squi.bid%2Feat-it%2Fplain%2Featit.lua+-o+~%2F.config%2Fmpv%2Fscripts%2Featit.lua+curl+-L+https%3A%2F%2Fgit.squi.bid%2Feat-it%2Fplain%2Featit-cfg.lua+-o+~%2F.config%2Fmpv%2Featit-cfg.lua): - -curl -L https://git.squi.bid/eat-it/plain/eatit.lua -o ~/.config/mpv/scripts/eatit.lua -curl -L https://git.squi.bid/eat-it/plain/eatit-cfg.lua -o ~/.config/mpv/eatit-cfg.lua + -- [string] Overrides the url of the git repo to clone + -- by default eatit tries https://github.com/user/package.git + url = "", -### Bootstraping -The included bootstrap script will install and load eatit as soon as mpv starts. -## FAQ -Q: Where does the name 'Eat It' come from? -A: The plugin manager eats all the useless files and keeps the ones you want, -*and Weird Al is a funny guy* + -- [boolean] whether to ignore updates + pin = true, + + -- [string] git branch to clone + branch = "", -## TODO + -- [table] table of files to copy + files = { + [""] = "" + }, + -- [function] code to run when installing/updating the package note due to how + -- mpv works eatit cannot change the working directory while running the setup + -- function + setup = function() + end +} +``` +## FAQ +Q: Where does the name "Eat It" come from? +A: The plugin manager eats all the useless files and keeps the ones you want, +~~also a refrence to Weird Al's song "Eat It"~~ ## ALTERNATIVES -[email me](mailto:me@zacharyscheiman.com) if you know of any alternatives +[email me](mailto:me@zacharyscheiman.com) if you know of any other alternatives - [mpv_manager](https://github.com/po5/mpv_manager) diff --git a/bootstrap.lua b/bootstrap.lua deleted file mode 100644 index 89b62fb..0000000 --- a/bootstrap.lua +++ /dev/null @@ -1,8 +0,0 @@ -os.execute('git clone https://git.squi.bid/eat-it /tmp/eatit-tmp') -local a = io.open('/tmp/eatit-tmp/eatit.lua', 'r') -local b = io.open(mp.command_native({'expand-path', '~~/scripts/eatit.lua'}), 'w') -b:write(a:read('*a')) -b:close() -a:close() -dofile(mp.command_native({'expand-path', '~~/scripts/eatit.lua'})) -os.execute('rm -rf /tmp/eatit-tmp') diff --git a/eatit-cfg.lua b/eatit-cfg.lua deleted file mode 100644 index 1b28bfc..0000000 --- a/eatit-cfg.lua +++ /dev/null @@ -1,76 +0,0 @@ ---[[ -Eat It - a Mpv plugin manager - -Copyright © 2023 squibid - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. -]] - --- NOTE: The variables in this file need to be global in order to be read --- after being called with dofile() - -plugins = { -- the plugins you want to load - { 'squibid/eat-it', -- required, specifies the git repo - -- optional, sets repo link (see advanced example for more info on how to - -- use this) - url = 'https://git.squi.bid/eat-it', - - -- required, specifies the desired file from the git repo - file = 'eatit.lua', - - -- optional, sets the destination of the requested file - dir = 'scripts', - - -- optional, sets the desired branch of the git repo - branch = 'master', - - -- optional, stop the plugin from being updated - pin = false, - }, - - -- advanced example - { 'gh:po5/thumbfast', -- expands to https://github.com/po5/thumbfast - file = { -- multiple files all going to the same place - 'thumbfast.lua', - 'osc.lua' - }, - branch = 'ancient', - --[[ - no need to specify dir as it defaults to ~~/scripts - no need to specify url as it is extrapolated from name - name expansion can be configured in the opts section - ]] - }, -} - --- options for eat it -opts = { - bind = 'U', - logging = { -- options for logging - log = true, - logdate = '[%H:%M:%S]:', - logfile = '~~/eatit.log', - }, - dl = { -- options for dealing with the git repos - dir = '/tmp/mpv-eatit', - powerwash = false, -- if true the download dir gets deleted after mpv closes - }, - nameexp = { - pre = 'https://', - map = { - -- shortcut = link - gl = 'gitlab.com', - cb = 'codeberg.org', - sr = 'sr.ht', - gh = 'github.com' - } - } -} diff --git a/eatit.lua b/eatit.lua deleted file mode 100644 index b18e56d..0000000 --- a/eatit.lua +++ /dev/null @@ -1,221 +0,0 @@ ---[[ -Eat It - a Mpv plugin manager - -Copyright © 2023 squibid - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. -]] - -local mp = require('mp') - --- load the config file -dofile(mp.command_native({'expand-path', '~~/eatit-cfg.lua'})) - --- helper functions -- -local function tablelength(T) - local count = 0 - for _ in pairs(T) do count = count + 1 end - return count -end - -local function fileexists(name) - local ok, err, code = os.rename(name, name) - if not ok then - if code == 13 then - -- Permission denied, but it exists - return true - end - end - return ok, err -end - -local function testforslash(str) - if string.match(str, '/') then - return string.match(str, '/([^/]+)$') - else - return str - end -end - -local function run(cmd) - local x = io.popen(cmd) - if not x then return 1 end - local y = x:read("*a") - x:close() - return y -end - -local function cp(a, b) - local i = io.open(a, 'r') - if not i then return 1 end - local o = io.open(b, 'w') - if not o then return 2 end - o:write(i:read('*a')) - o:close() - i:close() -end - -local function getsrc(id) - local src - if string.find(id, ":") then src = id:match("^[a-z]+[^:]") end - local name = id:match([[[^:]*$]]) - if name then - local pre = opts.nameexp.pre or "https://" - local map = opts.nameexp.map or { - gl = 'gitlab.com', - cb = 'codeberg.org', - sr = 'sr.ht', - gh = 'github.com' - } - return { - link = (pre..map[src]) or false, - repo = name - } - end -end - -local function openlog() - if opts.logging.log then -- log if asked to - -- get our logfile's full path - fn = mp.command_native({'expand-path', opts.logging.logfile}) - - f = io.open(fn, 'a') -- open file buffer - if not f then return 1 end - io.output(f) -- set it as default - end -end - -local function logwrite(string) - if opts.logging.log then - io.write(os.date(opts.logging.logdate)..' '..string..'\n') - end -end - -local function closelog() - if opts.logging.log then - io.close(f) - end -end - --- get the requested git repos -local function clonegit(plugdir, url, branch) - logwrite('downloading '..url) - - -- clone the repo - -- BUG: logwriting the git command doesn't actually log the output - run('git -C '..opts.dl.dir..' clone '..url..' '..plugdir) - run('git -C '..plugdir..' checkout -q '..branch) -end - --- check for updates -local function checkupdates(plugdir) - local localhash = run('git -C '..plugdir..' log -1 --format=format:"%H"') - local remotehash = run('git -C '..plugdir..' rev-parse $(git -C '.. - plugdir..' branch -r) | tail -1') - if localhash ~= remotehash then return true else return false end -end - --- start install -local function startinstall(i) - local src = getsrc(plugins[i][1]) - local plugin = { -- plugin table spec - id = plugins[i][1] or false, - url = plugins[i]['url'] or (src['link'].."/"..src['repo']), - file = plugins[i]['file'] or false, - dir = plugins[i]['dir'] or 'scripts', - pin = plugins[i]['pin'] or false, - branch = plugins[i]['branch'] or false, - } - - -- check if the user has defined a file for the current plugin - if not plugin['file'] then - logwrite('WARNING! File not configured for '..plugin[1]) - goto continue - end - -- skip install of pinned files - if plugin['pin'] == true then goto continue end - - -- get the plugins tmp download dir - local plugdir = opts.dl.dir..'/'..testforslash(src['repo']):gsub('.git', '') - - -- if no specified branch we use the default - plugin['branch'] = plugin['branch'] or testforslash( - run('git -C '..plugdir..' symbolic-ref refs/remotes/origin/HEAD') - ) - - -- check for multiple files - if type(plugin['file']) == 'string' then - plugin['file'] = {} - table.insert(plugin['file'], plugins[i]['file']) - end - - -- get the requested file(s) - for j = 1, #plugin['file'] or 1 do - -- get the file's dir - local pluginfile = plugdir..'/'..plugin['file'][j] - - -- get the dest dir - local destfile = mp.command_native({'expand-path', '~~/'}).. - '/'..plugin['dir']..'/'..testforslash(plugin['file'][j]) - - if fileexists(plugdir..'/') then - -- if we need to update, update - if checkupdates(plugdir) then - logwrite(plugin['file'][j]..' is updating.') - -- make sure we are on the main branch - run('git -C '..plugdir..' checkout -q '..plugin['branch']) - run('git -C '..plugdir..' pull') -- get the latest commits - else - logwrite(plugin['file'][j]..' is up to date!') - end - else - clonegit(plugdir, plugin['url'], plugin['branch']) - end - - -- copy the file contents over to the desired location - cp(pluginfile, destfile) - end - ::continue:: -end - -local function initupdate() - openlog() - - logwrite('# of plugins defined in table: '..tablelength(plugins)) - os.execute('mkdir -p '..opts.dl.dir) -- make download dir - - -- start the install process - logwrite('Starting Download...') - mp.osd_message('Downloading plugins!') - - -- start iterating through plugins - for i = 1, tablelength(plugins) do - local f = coroutine.create(function() startinstall(i) end) - coroutine.resume(f) - end - - -- closing/removing everything - if opts.dl.powerwash == true then - logwrite('powerwashing the tmp dir "'..opts.dl.dir..'"') - os.execute('rm -rf '..opts.dl.dir) - end - - closelog() -end - --- remove logfile on startup -if opts.logging.log then - openlog() - os.remove(fn) - closelog() -end - -mp.add_key_binding(opts.bind, 'UpdatePlugins', initupdate) diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..83316b8 --- /dev/null +++ b/main.lua @@ -0,0 +1,247 @@ +-- Copyright (c) 2024 squibid, see LICENSE file for more info +local mp = require('mp') +local msg = require('mp.msg') +local utils = require('mp.utils') + +local proc = require('proc') + +-- load the config file +local config = dofile(mp.command_native({"expand-path", "~~/eatit-cfg.lua"})) +if not config or type(config) ~= "table" then + msg.fatal("no config provided, bailing out") + return +end + +local base_dir = mp.command_native({"expand-path", "~~cache/plugins"}) +local packages = {} + +-- make sure the base directory exists (*nix only) +if utils.file_info(base_dir) == nil then + proc.exec({ "mkdir", "-p", base_dir }, {}, function(err, message) + if err then + msg.fatal(string.format("failed to create plugin directory: %s", err)) + return + end + end) +end + +--- regiester a new package spec +---@param spec table package spec from config +---@return table package +local function register_pkg(spec) + if type(spec) ~= "table" then + spec = { spec } + end + + local id = spec[1] + local package = packages[id] + + if not package then + package = { + id = id, + exists = false, + setup = false + } + + packages[id] = package + end + + package.name = string.sub(package.id, string.find(package.id, "%/") + 1, #package.id) + package.url = spec.url or ("https://github.com/"..package.id..".git") + package.branch = spec.branch + package.files = spec.files + package.dir = package.files and utils.join_path(base_dir, package.name) or utils.join_path(mp.command_native({ 'expand-path', "~~/scripts" }), package.name) + package.pin = spec.pin + + package.exists = utils.file_info(package.dir) ~= nil + -- validate that all files have been installed + if type(package.files) == "table" then + for filename, dest in pairs(package.files) do + if not utils.file_info(utils.join_path(package.dir, filename)) or + not utils.file_info(utils.join_path(mp.command_native({ "expand-path", dest }), filename)) then + package.exists = false + break + end + end + end + + package.on_setup = spec.setup + + return package +end + +--- run package setup +---@param package table package +local function setup_package(package) + if type(package.on_setup) ~= "function" then + return + end + + local ok, err = pcall(package.on_setup, package.dir) + if not ok then + msg.warn(string.format("error when running setup on '%s': %s", package.id, err)) + return + end + package.setup = true +end + +--- copy all files according to package spec +---@param package table package +local function copy_files(package) + --- copy src to dest + ---@param src string path to src file + ---@param dest string path to dest file + local function cp(src, dest) + local i = io.open(src, 'r') + if not i then return end + local o = io.open(dest, 'w') + if not o then return end + o:write(i:read('*a')) + o:close() + i:close() + end + + if type(package.files) == "table" then + for name, loc in pairs(package.files) do + local path = mp.command_native({'expand-path', loc}) + local dest = utils.join_path(path, name) + + local src = utils.join_path(package.dir, name) + if not utils.file_info(src) then + msg.warn(string.format("file %s not found", name)) + return + end + local ok, err = pcall(cp, src, dest) + if not ok then + msg.warn(string.format("failed to copy %s: %s", name, utils.to_string(err))) + end + end + end +end + +local function validate_package() +end + +--- download or update package +---@param package table package +---@param cb function callback +local function sync(package, cb) + if package.exists then + if package.pin then + cb() + return + end + + --- generic error + ---@param err any error + local function log_err(err) + msg.error(string.format("failed to update %s; reason: %s", package.id, err)) + end + + -- get current head commit hash + proc.git_rev_parse(package.dir, "HEAD", function(err, before) + if err then + log_err(before) + cb(err) + return + end + + -- get the latest commit hash + proc.git_fetch(package.dir, "origin", package.branch or "HEAD", function(err, message) + if err then + log_err(message) + cb(err) + return + end + + -- check the latest and current against eachother + proc.git_rev_parse(package.dir, "FETCH_HEAD", function(err, after) + if err then + log_err(after) + cb(err) + return + elseif before == after then + msg.info(string.format("skipped %s", package.id)) + cb(err) + return + end + + -- switch HEAD to new commit + proc.git_reset(package.dir, after, function(err, message) + if err then + log_err(message) + return + end + setup_package(package) + copy_files(package) + msg.info(string.format("updated %s; %s -> %s", package.id, before, after)) + + cb(err) + end) + end) + end) + end) + else + -- clone repo since it doesn't exist + proc.git_clone(package.dir, package.url, package.branch, function(err, message) + if err then + msg.error(string.format("failed to install %s; reason: %s", package.id, utils.to_string(message))) + else + setup_package(package) + copy_files(package) + package.exists = true + msg.info(string.format("installed %s", package.id)) + end + + cb(err) + end) + end +end + +--- sync a list of plugins +---@param list table list of packages +---@param cb function callback +local function sync_list(list, cb) + local progress = 0 + + for i in pairs(list) do + sync(list[i], function() + progress = progress + 1 + cb() + end) + end +end + +--- check if package spec should be synced +---@param package table package +---@return boolean +local function should_sync(package) + if config.sync == "new" or config.sync == nil then + return not package.exists + else + return config.sync == "always" + end +end + +-- register all packages +for i = 1, #config do + local ok, err = pcall(register_pkg, config[i]) + if not ok then + msg.warn(string.format("%s: %s", err, config[i].as)) + end +end + +-- check for package updates +local targets = {} +for i in pairs(packages) do + if should_sync(packages[i]) then + targets[#targets + 1] = packages[i] + end +end + +sync_list(targets, function() end) + +-- register script message for keybinding +mp.register_script_message("eatit-sync", function(name, value) + sync_list(packages, function() end) +end) diff --git a/modules/README.md b/modules/README.md deleted file mode 100644 index 614a9b2..0000000 --- a/modules/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Modules -Modules are extentions to the EatIt plugin manager that aren't necessary for -running it. Every module will be in it's own sub directory in order to keep -documentation relative to the individual modules. - -## List of modules -- `lssi.lua` Lua Script Script Injector for modifing files post install - -## Installing -Installing a module can be done through EatIt like so: -```lua -plugins = { -- the plugins you want to load - { 'https://git.squi.bid/eat-it', - file = 'modules/moddir/module.lua', - dir = 'scripts', - }, -} -``` - -## For Devs -Please add a table in the global scope called 'mod' as this may be used in the -future. It should look like this: -```lua -mod = { - version = 'version', - author = 'autor name', -} -``` diff --git a/modules/lssi/LSSI.md b/modules/lssi/LSSI.md deleted file mode 100644 index 5bcd65d..0000000 --- a/modules/lssi/LSSI.md +++ /dev/null @@ -1,16 +0,0 @@ -# Using lssi -Any plugin can be modified by lssi by doing the following in the eatit-cfg.lua: -```lua -plugins = { -- the plugins you want to load - { 'https://git.squi.bid/eat-it', - file = 'eatit.lua', - dir = 'scripts', - lssi = { - -- code line - { 'print("hello world")', 'G' }, - } - }, -} -``` -The line option can be 'g' for top of file, 'G' for bottom of file, or any -number in between. diff --git a/modules/lssi/lssi.lua b/modules/lssi/lssi.lua deleted file mode 100644 index 21f9536..0000000 --- a/modules/lssi/lssi.lua +++ /dev/null @@ -1,133 +0,0 @@ ---[[ -Eat It - a Mpv plugin manager - -Copyright © 2023 squibid - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. -]] - --- NOTE: This is a POC and will most likely be reimplimented using diff files --- with git --- or we might want to generate a diff file from the requested changes --- and check if the diffs match in content - ---[[ - Lua Script Script Injector -Takes an input file and code then, it outputs a file with your code in there. -]]-- - -local mp = require('mp') - -mod = { - version = 'ALPHA 1.1', -- the current version of lssi - author = 'squibid', -} - --- load the eatit config file -dofile(mp.command_native({'expand-path', '~~/eatit-cfg.lua'})) - --- helper functions -local function tablelength(T) - local count = 0 - for _ in pairs(T) do count = count + 1 end - return count -end - -local function fileexists(name) - local f = io.open(name, 'r') - if f ~= nil then io.close(f) return true else return false end -end - -local function logwrite(string) - if opts.logging.log then - io.write(os.date(opts.logging.logdate)..' '..string..'\n') - end -end - -local function openlog() - if opts.logging.log then -- log if asked to - -- get our logfile's full path - fn = mp.command_native({'expand-path', opts.logging.logfile}) - - f = io.open(fn, 'a') -- open file buffer - if not f then return end - io.output(f) -- set it as default - end -end - -local function closelog() - if opts.logging.log then - io.close(f) - end -end - -local function inject(infile, l, outfile) - local inf = io.open(infile, 'r') - local infcont = {} - for i in inf:lines() do - table.insert(infcont, i) - end - inf:close() - - -- don't do anything if there is already code injected into the file - if string.find(infcont[1], "-- code injected by lssi") then - logwrite('code is already injected into '..infile) - return - end - logwrite('Injecting code into '..infile) - - for i in pairs(l) do - -- add requested line below existing line - if l[i][2] == 'G' then - infcont[tablelength(infcont)] = infcont[tablelength(infcont)]..'\n'..l[i][1] - elseif l[i][2] == 'g' then - infcont[1] = l[i][1]..'\n'..infcont[1] - else - infcont[l[i][2]] = (infcont[l[i][2]])..'\n'..l[i][1] - end - end - - local outf = io.open(outfile, 'w') - - -- we inject metadata to prevent writing to the file more than once - infcont[1] = "-- code injected by lssi "..mod.version..'\n'..infcont[1] - for i, v in ipairs(infcont) do - outf:write(v..'\n') - end - - io.close(outf) -end - -local function checkandinject() - openlog() - - for i = 1, tablelength(plugins) do - -- check if the plugin has been configured with lssi - if plugins[i]['lssi'] ~= nil then - -- get the file we want to inject our code into - local f = mp.command_native({'expand-path', '~~/'}) .. - '/'..(plugins[i]['dir'] or 'scripts') .. - '/'..plugins[i]['file'] - -- and the file we are trying to modify actually exists - if fileexists(f) then - -- inject it! no going back now - inject(f, plugins[i]['lssi'], f) - else - logwrite('Failed to inject code into "' .. - plugins[i]['file']..'" file does not exist') - end - end - end - - closelog() -end - -checkandinject() diff --git a/proc.lua b/proc.lua new file mode 100644 index 0000000..306cb2e --- /dev/null +++ b/proc.lua @@ -0,0 +1,85 @@ +-- Copyright (c) 2024 squibid, see LICENSE file for more info +local mp = require('mp') + +local M = {} + +--- run a system binary +---@param args table command with it's options +---@param env table key value pair of envvars +---@param cb function callback +function M.exec(args, env, cb) + local res_env = {} + for i, v in pairs(env) do + res_env[#res_env + 1] = i.."="..v + end + + --- run callback + ---@param success boolean if the command ran successfully + ---@param result table|nil results + ---@param error string|nil error string or nil + local function callback(success, result, error) + local output + if result then + -- combine both stdout and stderr + output = result.stdout..result.stderr + end + cb(not success, output) + end + + mp.command_native_async({ + name = "subprocess", + playback_only = false, + capture_stdout = true, + capture_stderr = true, + env = res_env, + args = args + }, callback) +end + +---@type table git environment +local git_env = { GIT_TERMINAL_PROMPT = 0 } + +--- git rev parse +---@param dir string directory +---@param arg string arg +---@param cb function callback +function M.git_rev_parse(dir, arg, cb) + local cmd = { "git", "-C", dir, "rev-parse", "--short", arg } + M.exec(cmd, git_env, cb) +end + +--- git clone +---@param dir string directory +---@param url string url +---@param branch string branch +---@param cb function callback +function M.git_clone(dir, url, branch, cb) + local cmd = { "git", "clone", "--depth=1", "--recurse-submodules", "--shallow-submodules", url, dir } + + if branch then + cmd[#cmd + 1] = "--branch="..branch + end + + M.exec(cmd, git_env, cb) +end + +--- git fetch +---@param dir string directory +---@param remote string remote +---@param refspec string refspec +---@param cb function callback +function M.git_fetch(dir, remote, refspec, cb) + local cmd = { "git", "-C", dir, "fetch", "--depth=1", "--recurse-submodules", remote, refspec } + M.exec(cmd, git_env, cb) +end + +--- git reset +---@param dir string dir +---@param treeish string treeish +---@param cb function callback +function M.git_reset(dir, treeish, cb) + local cmd = { "git", "-C", dir, "reset", "--hard", "--recurse-submodules", treeish, "--" } + M.exec(cmd, git_env, cb) +end + +return M |