@@ -31,6 +31,81 @@ local function sanitize(s)
3131 return (string.gsub (s , " [^%w_%-]" , " _" ))
3232end
3333
34+ --- Returns "github", "gitlab", or nil if the URL is not a recognized git host.
35+ --- @param url string
36+ --- @return string ?
37+ local function isRecognizedGitHost (url )
38+ if url :match (" ^https?://github%.com/" ) then return " github" end
39+ if url :match (" ^https?://gitlab%.com/" ) then return " gitlab" end
40+ return nil
41+ end
42+
43+ --- Builds a tarball URL for a recognized git host at a given ref.
44+ --- @param url string # git clone URL (may have .git suffix or /tree/... paths)
45+ --- @param ref string # commit SHA, branch name, or tag
46+ --- @param hostType string # "github" or "gitlab"
47+ --- @return string
48+ local function buildTarballUrl (url , ref , hostType )
49+ local base = url :gsub (" %.git$" , " " )
50+ base = base :gsub (" /tree/.*$" , " " )
51+ base = base :gsub (" /$" , " " )
52+
53+ if hostType == " github" then
54+ return base .. " /archive/" .. ref .. " .tar.gz"
55+ elseif hostType == " gitlab" then
56+ local repoName = base :match (" /([^/]+)$" )
57+ return base .. " /-/archive/" .. ref .. " /" .. repoName .. " -" .. ref .. " .tar.gz"
58+ end
59+
60+ error (" Unknown host type: " .. hostType )
61+ end
62+
63+ --- Downloads and extracts a git tarball for a recognized host into repoDir.
64+ --- @param url string
65+ --- @param commit string
66+ --- @param hostType string
67+ --- @param repoDir string
68+ --- @param label string
69+ local function downloadTarball (url , commit , hostType , repoDir , label )
70+ local tarballUrl = buildTarballUrl (url , commit , hostType )
71+ local bar = lde .verbose and ansi .ProgressBar (" Downloading " .. label ) or nil
72+ fs .mkdir (repoDir )
73+
74+ local archiveFile = repoDir .. " .archive"
75+
76+ local dlOpts
77+ if bar then
78+ dlOpts = {
79+ progress = function (dltotal , dlnow )
80+ local ratio = dltotal > 0 and (dlnow / dltotal ) or nil
81+ local info = dltotal > 0
82+ and (ansi .formatBytes (dlnow ) .. " / " .. ansi .formatBytes (dltotal ))
83+ or ansi .formatBytes (dlnow )
84+ bar :update (ratio , info )
85+ end
86+ }
87+ end
88+
89+ local ok , dlErr = curl .download (tarballUrl , archiveFile , dlOpts )
90+ if not ok then
91+ fs .rmdir (repoDir )
92+ fs .delete (archiveFile )
93+ if bar then bar :fail (" Downloading " .. label ) end
94+ error (" Failed to download " .. tarballUrl .. " : " .. (dlErr or " " ))
95+ end
96+
97+ local ok2 , err2 = Archive .new (archiveFile ):extract (repoDir , { stripComponents = true })
98+ fs .delete (archiveFile )
99+
100+ if not ok2 then
101+ fs .rmdir (repoDir )
102+ if bar then bar :fail (" Downloading " .. label ) end
103+ error (" Failed to extract " .. label .. " : " .. (err2 or " " ))
104+ end
105+
106+ if bar then bar :done (" Downloaded " .. label ) end
107+ end
108+
34109--- @type string ?
35110local dirOverride = nil
36111
@@ -157,75 +232,78 @@ function global.resolveRegistryVersion(portfile, version)
157232 return latest , versions [latest ]
158233end
159234
160- --- Builds the cache directory name for a git repo.
161- --- Format: name, name-branch, or name-branch-commit
235+ --- Builds the cache directory name for a git repo: <name>-<commit>.
162236--- @param repoName string
163- --- @param branch string ?
164- --- @param commit string ?
237+ --- @param commit string
165238--- @return string
166- function global .getGitRepoDir (repoName , branch , commit )
167- local parts = { sanitize (repoName ) }
168-
169- if branch then
170- parts [# parts + 1 ] = sanitize (branch )
171- end
172-
173- if commit then
174- parts [# parts + 1 ] = sanitize (commit )
175- end
176-
177- local fullName = table.concat (parts , " -" )
178- return path .join (global .getGitCacheDir (), fullName )
239+ function global .getGitRepoDir (repoName , commit )
240+ return path .join (global .getGitCacheDir (), sanitize (repoName ) .. " -" .. sanitize (commit ))
179241end
180242
243+ --- Git clone fallback for unrecognized hosts. Always checks out the specific commit.
181244--- @param repoName string
182245--- @param repoUrl string
183- --- @param branch string ?
184- --- @param commit string ?
246+ --- @param commit string
185247--- @param progress fun ( stats : table )?
186- function global .cloneDir (repoName , repoUrl , branch , commit , progress )
187- local repoDir = global .getGitRepoDir (repoName , branch , commit )
188- local repo , err = git2 .clone (repoUrl , repoDir , branch , nil , progress )
248+ function global .cloneDir (repoName , repoUrl , commit , progress )
249+ local repoDir = global .getGitRepoDir (repoName , commit )
250+ local repo , err = git2 .clone (repoUrl , repoDir , nil , nil , progress )
189251 if not repo then return nil , err end
190252 repo :updateSubmodules (nil , progress )
191- if commit then
192- local ok , cerr = repo :checkout (commit )
193- if not ok then return nil , cerr end
194- end
253+ local ok , cerr = repo :checkout (commit )
254+ if not ok then return nil , cerr end
195255 return true
196256end
197257
258+ --- Ensures a git repo is cached locally (via tarball for GitHub/GitLab, git clone otherwise).
259+ --- Always resolves to a specific commit. Returns the cache directory and the pinned commit.
198260--- @param repoName string
199261--- @param repoUrl string
200262--- @param branch string ?
201263--- @param commit string ?
264+ --- @return string repoDir
265+ --- @return string commit
202266function global .getOrInitGitRepo (repoName , repoUrl , branch , commit )
203- local repoDir = global .getGitRepoDir (repoName , branch , commit )
267+ if not commit then
268+ local ref = branch and (" refs/heads/" .. branch ) or " HEAD"
269+ local sha , err = git2 .lsRemote (repoUrl , ref )
270+ if not sha then
271+ error (" Failed to resolve '" .. ref .. " ' for " .. repoUrl .. " : " .. (err or " " ))
272+ end
273+ commit = sha
274+ end
275+
276+ local repoDir = global .getGitRepoDir (repoName , commit )
204277 if not fs .exists (repoDir ) then
205- local progress
206- local bar = lde .verbose and ansi .ProgressBar (" Cloning " .. repoName ) or nil
207- if bar then
208- local totalObjs = 0
209- progress = function (stats )
210- if stats .total_objects > 0 then
211- totalObjs = stats .total_objects
278+ local hostType = isRecognizedGitHost (repoUrl )
279+ if hostType then
280+ downloadTarball (repoUrl , commit , hostType , repoDir , repoName )
281+ else
282+ local progress
283+ local bar = lde .verbose and ansi .ProgressBar (" Cloning " .. repoName ) or nil
284+ if bar then
285+ local totalObjs = 0
286+ progress = function (stats )
287+ if stats .total_objects > 0 then
288+ totalObjs = stats .total_objects
289+ end
290+ local ratio = totalObjs > 0 and (stats .indexed_objects / totalObjs ) or nil
291+ local info = totalObjs > 0
292+ and string.format (" %d/%d objects" , stats .indexed_objects , totalObjs )
293+ or string.format (" %d objects, %s" , stats .received_objects , ansi .formatBytes (stats .received_bytes ))
294+ bar :update (ratio , info )
212295 end
213- local ratio = totalObjs > 0 and (stats .indexed_objects / totalObjs ) or nil
214- local info = totalObjs > 0
215- and string.format (" %d/%d objects" , stats .indexed_objects , totalObjs )
216- or string.format (" %d objects, %s" , stats .received_objects , ansi .formatBytes (stats .received_bytes ))
217- bar :update (ratio , info )
218296 end
297+ local ok , err = global .cloneDir (repoName , repoUrl , commit , progress )
298+ if not ok then
299+ if bar then bar :fail (" Cloning " .. repoName ) end
300+ error (" Failed to clone git repository: " .. err )
301+ end
302+ if bar then bar :done (" Cloned " .. repoName ) end
219303 end
220- local ok , err = global .cloneDir (repoName , repoUrl , branch , commit , progress )
221- if not ok then
222- if bar then bar :fail (" Cloning " .. repoName ) end
223- error (" Failed to clone git repository: " .. err )
224- end
225- if bar then bar :done (" Cloned " .. repoName ) end
226304 end
227305
228- return repoDir
306+ return repoDir , commit
229307end
230308
231309--- Downloads and extracts an archive URL (.zip, .tar.gz, .tar.bz2, etc.) into the cache.
@@ -305,22 +383,51 @@ function global.repoNameFromUrl(url)
305383 return url :match (" ([^/]+)%.git$" ) or url :match (" ([^/]+)$" )
306384end
307385
308- --- Clones or retrieves a cached git repo directory (simple name+branch key, no commit).
386+ --- Clones or retrieves a cached git repo directory. Always resolves to a specific commit.
387+ --- Checks existing cache entries first to avoid network where possible.
309388--- @param repoName string
310389--- @param cloneUrl string
311390--- @param branch string ?
312391--- @return string repoDir
392+ --- @return string commit
313393function global .getOrCloneRepo (repoName , cloneUrl , branch )
314- local safeName = branch and (repoName .. " -" .. branch ) or repoName
315- local repoDir = global .getGitRepoDir (safeName )
394+ -- Check for any existing cache entry for this repo name
395+ local prefix = sanitize (repoName ) .. " -"
396+ local cacheDir = global .getGitCacheDir ()
397+ if fs .isdir (cacheDir ) then
398+ for entry in fs .readdir (cacheDir ) do
399+ if entry .type == " dir" and entry .name :sub (1 , # prefix ) == prefix then
400+ local commit = entry .name :sub (# prefix + 1 )
401+ return path .join (cacheDir , entry .name ), commit
402+ end
403+ end
404+ end
405+
406+ -- No cache hit, resolve and download
407+ local ref = branch and (" refs/heads/" .. branch ) or " HEAD"
408+ local commit , err = git2 .lsRemote (cloneUrl , ref )
409+ if not commit then
410+ error (" Failed to resolve ref for " .. cloneUrl .. " : " .. (err or " " ))
411+ end
412+
413+ local repoDir = global .getGitRepoDir (repoName , commit )
316414 if not fs .exists (repoDir ) then
317- local repo , err = git2 .clone (cloneUrl , repoDir , branch )
318- if not repo then
319- error (" Failed to clone git repository: " .. (err or " unknown error" ))
415+ local hostType = isRecognizedGitHost (cloneUrl )
416+ if hostType then
417+ downloadTarball (cloneUrl , commit , hostType , repoDir , repoName )
418+ else
419+ local repo , cerr = git2 .clone (cloneUrl , repoDir , branch )
420+ if not repo then
421+ error (" Failed to clone git repository: " .. (cerr or " unknown error" ))
422+ end
423+ repo :updateSubmodules ()
424+ local ok , cerr2 = repo :checkout (commit )
425+ if not ok then
426+ error (" Failed to checkout commit: " .. (cerr2 or " unknown error" ))
427+ end
320428 end
321- repo :updateSubmodules ()
322429 end
323- return repoDir
430+ return repoDir , commit
324431end
325432
326433--- Finds a named package inside a directory by scanning for lde.json files.
0 commit comments