· 4 years ago · Mar 26, 2021, 08:36 AM
1HomePath = package.path
2package.path = "./.git/?.lua"
3Json = require "json"
4Base64 = require "base64"
5
6API = {user=1, repo=2, }
7Settings = {
8 User = {},
9 Cache = {},
10 Category={all=0, user=1, cache=2}
11 }
12
13--Functions related to shell
14------------------------------------------------------------------------------------------------------------------
15function newScreen(prompt)
16 term.clear()
17 term.setCursorPos(1,1)
18 --center new prompt
19 local x,y = term.getSize()
20 local x2,y2 = term.getCursorPos()
21 term.setCursorPos(math.ceil((x / 2) - (prompt:len() / 2)), y2)
22 print(tostring(prompt) .. "\n")
23end
24
25function getCursorY()
26 local X,Y = term.getCursorPos()
27 return Y
28end
29
30function setCursorLine(y)
31 term.setCursorPos(1, y)
32end
33
34function ask(question)
35 print(question)
36 return io.read()
37end
38
39function redrawLine(line, text)
40 local oldX,OldY = term.getCursorPos()
41 setCursorLine(line)
42 term.clearLine()
43 print(tostring(text))
44 term.setCursorPos(oldX, OldY)
45end
46
47function printCommands()
48 print("git Will sync the local files with the remote")
49 print("run [fileName] Will run the script based on relative path of the repo")
50 print("git setup reruns this setup")
51 print("git token runs the token setup")
52 print("git repo allows you to change the repository")
53 print("git uninstall removes turtle git")
54 print("git clean removes all scripts")
55end
56
57
58--Functions related to settings
59------------------------------------------------------------------------------------------------------------------
60function Settings.get(category)
61 local filePath = ".git/settings.json"
62 category = category or Settings.Category.all
63 if fs.exists(filePath) then
64 local settingsFile = fs.open(filePath, "r")
65 local settings = Json.decode(settingsFile.readAll())
66 settingsFile.close()
67 if category == Settings.Category.user then
68 return settings.User
69 elseif category == Settings.Category.cache then
70 return settings.Cache
71 elseif category == Settings.Category.all then
72 return settings
73 end
74 end
75 return nil
76end
77
78function Settings.save(category)
79 local cachedSettings = Settings.get(Settings.Category.all) or {}
80 if category == Settings.Category.user then
81 cachedSettings.User = Settings.User
82 elseif category == Settings.Category.cache then
83 cachedSettings.Cache = Settings.Cache
84 else
85 return false --escape early to avoid saving code
86 end
87
88 local file = fs.open(".git/settings.json", "w")
89 file.write(Json.encode(cachedSettings))
90 file.close()
91end
92
93--Functions related to http calls
94------------------------------------------------------------------------------------------------------------------
95function exists(api, user)
96 local github = "https://github.com/"
97 if api == API.user then
98 return http.get(github .. user.name) ~= nil
99 elseif api == API.repo then
100 return http.get(github .. user.name .. "/" .. user.repo) ~= nil
101 end
102 return false
103end
104
105function createAuth(username, password) --creates an auth header
106 return "Basic " .. Base64.encode(username .. ":" .. password)
107end
108
109function getJson(url, headers)
110 headers = headers or {}
111 local response = http.get(url, headers)
112 if response ~= nil then
113 return Json.decode(response.readAll())
114 end
115 return false
116end
117
118function gitApiHeader(user)
119 local header = {Accept="application/vnd.github.v3+json"}
120 if user.useToken then
121 header.Authorization = user.token
122 end
123 return header
124end
125
126--Functions relate to git specifically
127------------------------------------------------------------------------------------------------------------------
128function getRateLimit(headers)
129 local response = getJson("https://api.github.com/rate_limit", headers)
130 return response.resources.core
131end
132
133function checkUpdate(user)
134 local url = "https://api.github.com/repos/" .. user.name .. "/" .. user.repo
135 return getJson(url, gitApiHeader(user)).pushed_at
136end
137
138function getFiles(user, path)
139 local files = {}
140 path = path or ""
141 local url = "https://api.github.com/repos/" .. user.name .. "/" .. user.repo .. "/contents/" .. (path)
142 local response = getJson(url, gitApiHeader(user))
143 if response == false then
144 return false
145 end
146 for _key, file in pairs(response) do
147 if file.type == "dir" then
148 local subFiles = getFiles(user, file.path)
149 if subFiles == false then
150 return false
151 end
152 for _path, _sha in pairs(subFiles) do
153 files[path] = sha
154 end
155 elseif file.type == "file" then
156 if string.sub(file.name, 1,1) ~= "." then
157 files[file.path] = file.sha
158 end
159 end
160 end
161 return files
162end
163
164function downloadFile(path, sha, user)
165 local url = "https://api.github.com/repos/" .. user.name .. "/" .. user.repo .. "/git/blobs/" .. sha
166 local response = getJson(url, gitApiHeader(user))
167 local file = fs.open(path, "w")
168 file.write(Base64.decode(response.content))
169 file.close()
170end
171
172--Functions related to the program
173------------------------------------------------------------------------------------------------------------------
174function compareTables(cachedFiles, remoteFiles)
175 local filesToRemove = {}
176 local filesToDownload = {}
177
178 for path, sha in pairs(cachedFiles) do --check what we have that remote does not
179 if remoteFiles[path] == nil then
180 filesToRemove[path] = true
181 cachedFiles[path] = nil
182 end
183 end
184
185 for path, sha in pairs(remoteFiles) do
186 if not cachedFiles[path] then --check what remote has we do not
187 filesToDownload[path] = sha
188 else
189 if cachedFiles[path] ~= sha then --check if sha on remote differs from what we stored
190 filesToDownload[path] = sha
191 end
192 end
193 end
194
195 return {Remove=filesToRemove, Download=filesToDownload}
196end
197
198function countEntries(table)
199 local i = 0
200 for _k, _v in pairs(table) do
201 i = i+1
202 end
203 return i
204end
205
206function deleteFiles(files, startingDir, line, total)
207 local count = 0
208 for path, _v in pairs(files) do
209 fs.delete(fs.combine(startingDir, path))
210 if line ~= nil and total ~= nil then
211 count = count + 1
212 redrawLine(line, "Deleted "..tostring(count) .. "/" .. tostring(total))
213 end
214 end
215end
216
217function deleteEmpty(dirPath)
218 local list = fs.list(dirPath)
219 for i = 1, #list, 1 do
220 local newPath = fs.combine(dirPath, list[i])
221 if fs.isDir(newPath) then
222 deleteEmpty(newPath)
223 end
224 end
225 list = fs.list(dirPath) --refresh the list incase empty directories were all that were inside this directory
226 if #list == 0 then
227 fs.delete(dirPath)
228 end
229end
230
231function downloadFiles(files, user, toDir, line, total)
232 local count = 0
233 for path, sha in pairs(files) do
234 downloadFile(fs.combine(toDir, path), sha, user)
235 if line ~= nil and total ~= nil then
236 count = count + 1
237 redrawLine(line, "Downloaded "..tostring(count) .. "/" .. tostring(total))
238 end
239 end
240end
241
242function setupUser(error)
243 newScreen("Turtle Git > Setup")
244 if error then
245 print(tostring(error) .. "\n")
246 end
247 local user = ask("What is your Git Username?")
248 if user == "" then
249 return false
250 elseif exists(API.user, {name=user}) then
251 Settings.User.name = user
252 return true
253 else
254 return setupUser("Could not find " .. user .. "!\nCheck the name and try again.")
255 end
256end
257
258function setupRepo(error)
259 newScreen("Turtle Git > Setup")
260 if error then
261 print(tostring(error) .. "\n")
262 end
263
264 local repo = ask("What is the Repository Name?")
265 if repo == "" then
266 return false
267 elseif exists(API.repo, {name=Settings.User.name, repo=repo}) then
268 Settings.User.repo = repo
269 return true
270 else
271 return setupRepo("Could not find " .. repo .. "!\nCheck the name and try again.")
272 end
273end
274
275function setupUseToken()
276 newScreen("Turtle Git > Token Setup")
277 print("Would you like to use a personal token?\nPersonal access tokens allow you to use the github api 5000 times per hour versus the normal 60 per hour\n")
278 print("If your project is large or if you update your code frequenty this could cause the program to not function so use of a token is highly encouraged but not required")
279 print("\nIf you need to create a token go to:\nhttps://github.com/settings/tokens\n\n")
280 local useToken = ask("Will you be using a token?")
281 useToken = useToken:lower()
282 if useToken == "y" or useToken == "yes" or useToken == "true" or useToken == "1" then
283 setupToken()
284 else
285 print "selected not to use token"
286 end
287end
288
289function setupToken(error)
290 newScreen("Turtle Git > Token Setup")
291 if error then
292 print(tostring(error) .. "\n")
293 end
294 print("\nIf you need to create a token go to:\nhttps://github.com/settings/tokens\n\n")
295 local token = ask("What is your token?")
296 if token == "" then
297 return false
298 end
299
300 local testUser = Settings.User
301 testUser.useToken = true
302 testUser.token = createAuth(testUser.name, token)
303 local rate = getRateLimit(gitApiHeader(testUser))
304 if rate.limit > 60 then
305 newScreen("Turtle Git > Token")
306 print("Your token was successful!\nYour rate limit is: " .. rate.limit)
307 print("\nGit Turtle uses one each time it navigate a folder or subfolder, and then one for each file it downloads")
308 print("\nWhile Git Turtle tries to calculate if it's able to make a call, should you hit your limit you will have to wait to resume using Turtle Git")
309 ask("\nPress enter to continue")
310 Settings.User.useToken = true
311 Settings.User.token = testUser.token
312 return true
313 else
314 setupToken("Username Token pair did not work! Please check your token and try again!\nRemember you can paste the token into the input!")
315 end
316end
317
318function setupComplete()
319 newScreen("Turtle Git > Setup Complete")
320 Settings.save(Settings.Category.user)
321 Settings.Cache = {}
322 Settings.save(Settings.Category.cache)
323 print("Your Turtle Git is now ready to use!\nGit stores files in .scripts/[reponame]\n\nHere is some basics to using git:\n")
324 printCommands()
325 ask("\nPress Enter To Complete Setup")
326end
327
328function setup()
329 if setupUser() then
330 if setupRepo() then
331 if setupUseToken() then
332 if setupToken() then
333 setupComplete()
334 end
335 else
336 setupComplete()
337 end
338 end
339 end
340end
341
342function sync()
343 local files = getFiles(Settings.User)
344 if files == false then
345 newScreen("Turtle Git > Rate Limit Exhausted")
346 print("It looks like we failed to grab all files from the repo\nThis usually means your rate limit is gone")
347 print("\nYour Rate Limit Remaining: " .. getRateLimit(gitApiHeader(Settings.User)))
348 print("\nIf your limit is not 0 then another error has occured, try running git again")
349 ask("\nPress enter to continue")
350 return 0
351 else
352 --newScreen("Turtle Git > Syncinc Files")
353 print("\nFiles on remote: " .. tostring(countEntries(files)))
354
355 local changesLine = getCursorY()
356 print("Calculating Number of Changes to Make...")
357 local changes = compareTables(Settings.Cache, files)
358 local removeCount = countEntries(changes.Remove)
359 local downloadCount = countEntries(changes.Download)
360 redrawLine(changesLine, "Changes to process "..tostring(removeCount + downloadCount))
361 print("")print("")
362
363 local deleteLine = getCursorY()
364 print("Deleted 0/" .. tostring(removeCount))
365 deleteFiles(changes.Remove, ".scripts/" .. Settings.User.repo, deleteLine, removeCount)
366
367 local downloadLine = getCursorY()
368 print("Downloaded 0/" .. tostring(downloadCount))
369 downloadFiles(changes.Download, Settings.User, ".scripts/" .. Settings.User.repo, downloadLine, downloadCount)
370
371 Settings.Cache = files
372 Settings.save(Settings.Category.cache)
373
374 print("")print("")
375 print("Removing empty directories...")
376 deleteEmpty(".scripts/" .. Settings.User.repo)
377 print("\nFiles Synced!")
378 end
379end
380
381function main()
382 if #arg == 0 then
383 local cachedSettings = Settings.get(Settings.Category.all)
384 if cachedSettings then
385 Settings.User = cachedSettings.User or {}
386 Settings.Cache = cachedSettings.Cache or {}
387 end
388 if Settings.User.name and Settings.User.repo and exists(API.repo, Settings.User) then
389 sync()
390 elseif Settings.User.name then
391 print("Could not find repo or user! re-run setup or change repo!")
392 else
393 setup()
394 end
395 else
396 if arg[1] == "setup" then
397 setup()
398 elseif arg[1] == "token" then
399 if setupUseToken() then
400 if setupToken() then
401 setupComplete()
402 end
403 else
404 setupComplete()
405 end
406 elseif arg[1] == "repo" then
407 setupRepo()
408 elseif arg[1] == "uninstall" then
409 fs.delete(".scripts")
410 fs.delete(".git")
411 fs.delete("run")
412 fs.delete("git")
413 elseif arg[1] == "clean" then
414 fs.delete(".scripts")
415 Settings.Cache = {}
416 Settings.save(Settings.Category.cache)
417 elseif arg[1] == "help" then
418 printCommands()
419 else
420 print("Invalid command! Type git help to see availible commands")
421 end
422 end
423end
424
425main()