#!/opt/Lua51/bin/lua -- Created by jiaqiang.ye@lnxall.com -- Simple Git Repository Branch-Tag Management -- 2025/07/28 local cjson = require "cjson" local invoker = require "invoker" local gfmt = string.format -- name of environment variable local TAGMAN_CONF = "TAGMAN_CONF" local TAGMAN_CLEAN = "LNXCLEAN" local TAGMAN_MAXSIZE = 0x100000 local FT_BUILD_SH = "./build.bash" -- table holding Git repository branch/tag information local g_repinfo = {} local g_pathidx = {} -- directory path for `wlos_core local g_topdir = os.getenv("FTOPDIR") -- configuration Lua script local g_cfgfile = nil local function goodstr(s) if type(s) ~= "string" then return false end if string.len(s) == 0 then return false end return true end -- returns: FTOPDIR, Relative path for `build.bash local function find_topdir(bpath) local dirstr, regdir = "directory", "^(.+)/+[^/]+$" if not bpath then bpath = invoker.getcwd() elseif invoker.statfile(bpath) ~= dirstr then bpath = string.match(bpath, regdir) end -- check again for initial path if invoker.statfile(bpath) ~= dirstr then if not goodstr(bpath) then bpath = "unknown" end io.stderr:write(gfmt("Error, not a directory: %s\n", bpath)) io.stderr:flush() return nil, nil end -- relative path for `build.bash local reltab = 0 while true do local bsh = gfmt("%s/build.bash", bpath) if invoker.statfile(bsh) == "regular" then break end reltab = reltab + 1 bpath = string.match(bpath, regdir) if not goodstr(bpath) then break end end if goodstr(bpath) then if reltab == 0 then return bpath, FT_BUILD_SH end return bpath, gfmt("%s/build.bash", string.rep("../", reltab)) end io.stderr:write("Error, FTOPDIR not found!\n") io.stderr:flush() return nil, nil end local function clean_checkout(path_g, btag) -- clean repository first local topdir, buildsh = find_topdir(nil) if topdir ~= g_topdir then io.stderr:write(gfmt("Error, invalid TOPDIR: %s <=> %s\n", topdir, g_topdir)) io.stderr:flush() return false end local iflag = invoker.NOSTDIO if os.getenv('TAGMAN_VERBOSE') == "1" then iflag = 0 end if buildsh ~= FT_BUILD_SH then io.stdout:write(gfmt("INFO: cleaning repository: %s\n", path_g)) io.stdout:flush() invoker.invoke(iflag, "bash", buildsh, "clean") end if not btag then return true end io.stdout:write(gfmt("INFO: updating repository: %s, branch/tag: %s\n", path_g, btag)) io.stdout:flush() invoker.invoke(iflag, "git", "fetch", "--all", "--tags") io.stdout:write(gfmt("INFO: checking out branch/tag in %s: %s\n", path_g, btag)) io.stdout:flush() if invoker.invoke(iflag, "git", "checkout", btag) == 0 then return true end return false end local function tagman_checkout() local repidx, oknum = 0, 0 local dummystr, errmsg0, errmsg1 = "HELLO WOLRD", "to be committed", "not staged for commit" local inf0, inf1, inf2, wtclean = "On branch", "on any branch", "detached at", "working tree clean" local cmd_status = gfmt("set -o pipefail ; (echo '%s' && git status -uno) |& grep -e '%s' -e '%s' -e '%s' -e '%s' -e '%s' -e '%s' -e '%s'", dummystr, dummystr, inf0, inf1, inf2, wtclean, errmsg0, errmsg1) while true do repidx = repidx + 1 local rinfo = g_repinfo[repidx] if not rinfo then break end -- Goto TOPDIR directory if invoker.chdir(g_topdir) ~= 0 then io.stderr:write(gfmt("Error, failed to go back to TOPDIR: %s\n", g_topdir)) io.stderr:flush() return false end -- Then goto git repository if invoker.chdir(rinfo.path) ~= 0 then io.stderr:write(gfmt("Error, failed to goto directory: %s\n", rinfo.path)) io.stderr:flush() return false end -- get repository status local okay, output = invoker.invoke(invoker.OUTPUT, "/bin/bash", "-c", cmd_status) if type(output) ~= "string" or string.len(output) == 0 then okay, output = -1, "" io.stderr:write(gfmt("Warning: empty output from repository: %s\n", rinfo.path)) io.stderr:flush() end if string.find(output, wtclean, 1, true) then okay = 0 end if okay ~= 0 or string.find(output, errmsg0, 1, true) or string.find(output, errmsg1, 1, true) then io.stderr:write(gfmt("Error, working tree not clean: %s\n", rinfo.path)) io.stderr:flush() return false end local res, git_status = true, output if rinfo.tag == TAGMAN_CLEAN then res = clean_checkout(rinfo.path) elseif rinfo.tag then okay, output = invoker.invoke(invoker.OUTPUT, "/bin/bash", "-c", "exec git log -1 2>&1") if okay ~= 0 then io.stderr:write(gfmt("Error, git log has failed in %s\n", rinfo.path)) io.stderr:flush() return false end if type(output) ~= "string" then output = "" end local git_log = output okay, output = invoker.invoke(invoker.OUTPUT + invoker.TRIMEND, "/bin/bash", "-c", gfmt("exec git rev-list --max-count=1 '%s' 2>&1", rinfo.tag)) if okay == 0 and goodstr(output) and string.find(git_log, output, 1, true) then io.stdout:write(gfmt("Repository '%s' already at TAG: %s\n", rinfo.path, rinfo.tag)) io.stdout:flush() else res = clean_checkout(rinfo.path, rinfo.tag) end else if not string.find(git_status, gfmt("On branch %s\n", rinfo.branch), 1, true) then res = clean_checkout(rinfo.path, rinfo.branch) else io.stdout:write(gfmt("Repository '%s' already at branch: %s\n", rinfo.path, rinfo.branch)) io.stdout:flush() end if res then io.stdout:write(gfmt("pull from branch: %s => %s\n", rinfo.path, rinfo.branch)) io.stdout:flush() invoker.invoke(0, "git", "pull", "--rebase", "origin", rinfo.branch) end end if not res then io.stderr:write(gfmt("Error, failed to checkout branch/tag for '%s' => %s\n", rinfo.path, rinfo.tag or rinfo.branch)) io.stderr:flush() return false end oknum = oknum + 1 end return oknum end local function tagman_load() local cfgd = invoker.readfile(g_cfgfile, TAGMAN_MAXSIZE) if not cfgd then io.stderr:write(gfmt("Error, failed to read file: %s\n", g_cfgfile)) io.stderr:flush() return false end local okay, jdat = pcall(cjson.decode, cfgd) cfgd = nil -- free memory early if not okay or type(jdat) ~= "table" then io.stderr:write(gfmt("Error, failed to decode as JSON: %s\n", g_cfgfile)) io.stderr:flush() return false end local repos = jdat["repolist"] if type(repos) ~= "table" then io.stderr:write(gfmt("Error, repolist not found in %s\n", g_cfgfile)) io.stderr:flush() return false end local repidx = 0 while true do repidx = repidx + 1 local rinfo = repos[repidx] if type(rinfo) ~= "table" then break end -- extract Git repo information local path_g, branch, tagv = rinfo['path'], rinfo['branch'], rinfo['tag'] if not goodstr(path_g) then io.stderr:write(gfmt("Error, invalid git repository information at index: %d\n", repidx)) io.stderr:flush() return false end local dotgit = gfmt("%s/.git", path_g) if invoker.statfile(path_g) ~= "directory" or invoker.statfile(dotgit) ~= "directory" then io.stderr:write(gfmt("Error, invalid git repository path: %s\n", path_g)) io.stderr:flush() return false end if g_pathidx[path_g] then -- check for git duplication io.stderr:write(gfmt("Error, duplicated git repository: %s\n", path_g)) io.stderr:flush() return false end if not goodstr(branch) then io.stderr:write(gfmt("Error, branch not specified for %s\n", path_g)) io.stderr:flush() return false end if not goodstr(tagv) then tagv = nil end g_pathidx[path_g] = repidx g_repinfo[repidx] = { ["path"] = path_g, ["branch"] = branch, ["tag"] = tagv, } end return true end local function tagman_init(topdir, cfgjson) invoker.setenv("FTOPDIR", nil) invoker.setenv("LANGUAGE", nil) invoker.setenv("LANG", "en_US.UTF-8") -- transform relative path for `tagman to full path: topdir = invoker.realpath(topdir) if not goodstr(topdir) then io.stderr:write("Error, failed to determine script path.\n") io.stderr:flush() return false end -- split path until we have found `build.bash: topdir = find_topdir(topdir) if not goodstr(topdir) then return false end if goodstr(g_topdir) then if g_topdir ~= topdir then io.stderr:write(gfmt("Warning, build.bash found more than once: %s <=> %s\n", g_topdir, topdir)) io.stderr:flush() end else -- `g_topdir is nil, set as `topdir: g_topdir = topdir end io.stdout:write(gfmt("Non-openwrt TOPDIR: %s\n", topdir)) io.stdout:flush() -- determine configuration file local cfgfile, cfgpath = os.getenv(TAGMAN_CONF), nil invoker.setenv(TAGMAN_CONF, nil) if not goodstr(cfgfile) then if not goodstr(cfgjson) then io.stderr:write("Error, environment variable not found: `TAGMAN_CONF\n") io.stderr:flush() return false end cfgfile = cfgjson end if invoker.statfile(cfgfile) == "regular" then cfgpath = invoker.realpath(cfgfile) else cfgpath = gfmt("%s/tagman/%s", topdir, cfgfile) end -- make sure that the configuration file exists local ftype, fsize = invoker.statfile(cfgpath) if ftype ~= "regular" then io.stderr:write(gfmt("Error, tagman config file not found: %s\n", cfgfile)) io.stderr:flush() return false end if fsize == 0 or fsize > TAGMAN_MAXSIZE then io.stderr:write(gfmt("Error, invalid file size: %s\n", cfgfile)) io.stderr:flush() return false end g_cfgfile = cfgpath io.stdout:write(gfmt("INFO: using branch/tag-config file: %s\n", cfgpath)) io.stdout:flush() -- Go to TOPDIR if invoker.chdir(topdir) ~= 0 then io.stdout:write(gfmt("Error, Failed to goto top-directory: %s\n", topdir)) io.stdout:flush() return false end -- load the configuration file return tagman_load() end if not tagman_init(arg[0], arg[1]) then os.exit(1) end local numok = tagman_checkout() if numok then io.stdout:write(gfmt("Number of repositories processed: %d\n", numok)) io.stdout:flush() os.exit(0) end os.exit(2)