agi.git.go 40 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258
  1. package agi
  2. /*
  3. AGI Git Library
  4. Author: tobychui
  5. This library gives AGI scripts version control over folders inside the
  6. user's virtual file system: clone, status, stage, commit, branch, diff,
  7. fetch, pull and push, plus a per-user encrypted store for HTTPS credentials.
  8. Everything is backed by mod/git, which uses go-git — no `git` binary is
  9. required on the host, so this works identically on every platform ArozOS
  10. builds for.
  11. Usage (from an AGI script):
  12. requirelib("git");
  13. if (git.isRepo("user:/Desktop/myproject")) {
  14. var status = git.status("user:/Desktop/myproject");
  15. console.log(status.branch, status.changes.length);
  16. }
  17. Path rules:
  18. - Every path is an ArozOS virtual path and is permission checked.
  19. - Read-only calls need read permission, mutating calls need write
  20. permission on the repository path.
  21. - The storage pool must be local: network-backed pools (WebDAV, SMB, S3, …)
  22. cannot host a git working tree because git needs real random-access files.
  23. */
  24. import (
  25. "encoding/base64"
  26. "encoding/json"
  27. "errors"
  28. "fmt"
  29. "os"
  30. "path/filepath"
  31. "strings"
  32. "github.com/robertkrimen/otto"
  33. "imuslab.com/arozos/mod/agi/static"
  34. "imuslab.com/arozos/mod/filesystem"
  35. "imuslab.com/arozos/mod/git"
  36. "imuslab.com/arozos/mod/info/logger"
  37. user "imuslab.com/arozos/mod/user"
  38. )
  39. func (g *Gateway) GitLibRegister() {
  40. err := g.RegisterLib("git", g.injectGitLibFunctions)
  41. if err != nil {
  42. logger.PrintAndLog("Agi", fmt.Sprint(err), nil)
  43. os.Exit(1)
  44. }
  45. }
  46. func (g *Gateway) injectGitLibFunctions(payload *static.AgiLibInjectionPayload) {
  47. vm := payload.VM
  48. u := payload.User
  49. scriptFsh := payload.ScriptFsh
  50. manager := g.Option.GitManager
  51. // resolveRepo turns a virtual path into a real one after checking the
  52. // user's permission and that the storage pool can host a repository.
  53. resolveRepo := func(vpath string, needWrite bool) (string, error) {
  54. if manager == nil {
  55. return "", errors.New("git support is not enabled on this system")
  56. }
  57. if strings.TrimSpace(vpath) == "" {
  58. return "", errors.New("repository path cannot be empty")
  59. }
  60. vpath = static.RelativeVpathRewrite(scriptFsh, vpath, vm, u)
  61. if needWrite {
  62. if !u.CanWrite(vpath) {
  63. return "", errors.New("path access denied: " + vpath)
  64. }
  65. } else if !u.CanRead(vpath) {
  66. return "", errors.New("path access denied: " + vpath)
  67. }
  68. fsh, rpath, err := static.VirtualPathToRealPath(vpath, u)
  69. if err != nil {
  70. return "", err
  71. }
  72. if err := checkFshSupportsGit(fsh); err != nil {
  73. return "", err
  74. }
  75. return rpath, nil
  76. }
  77. // git.isRepo(vpath) -> bool
  78. vm.Set("_git_isrepo", func(call otto.FunctionCall) otto.Value {
  79. vpath, err := call.Argument(0).ToString()
  80. if err != nil {
  81. return otto.FalseValue()
  82. }
  83. rpath, err := resolveRepo(vpath, false)
  84. if err != nil {
  85. return otto.FalseValue()
  86. }
  87. if manager.IsRepo(rpath) {
  88. return otto.TrueValue()
  89. }
  90. return otto.FalseValue()
  91. })
  92. // git.repoRoot(vpath) -> virtual path of the working tree root, or false
  93. vm.Set("_git_reporoot", func(call otto.FunctionCall) otto.Value {
  94. vpath, err := call.Argument(0).ToString()
  95. if err != nil {
  96. return otto.FalseValue()
  97. }
  98. vpath = static.RelativeVpathRewrite(scriptFsh, vpath, vm, u)
  99. rpath, err := resolveRepo(vpath, false)
  100. if err != nil {
  101. return otto.FalseValue()
  102. }
  103. realRoot, err := manager.RepoRoot(rpath)
  104. if err != nil {
  105. return otto.FalseValue()
  106. }
  107. //The root is mapped back by trimming the virtual path, never by
  108. //converting the real path: RepoRoot returns an absolute OS path while
  109. //the file system abstraction strips its storage root by prefix match,
  110. //so handing it back would leave the whole absolute path embedded in
  111. //the vpath and double it on the next translation.
  112. rootVpath, err := repoRootVirtualPath(vpath, rpath, realRoot)
  113. if err != nil {
  114. g.RaiseError(err)
  115. return otto.FalseValue()
  116. }
  117. result, _ := vm.ToValue(rootVpath)
  118. return result
  119. })
  120. // git.init(vpath) -> {success, error}
  121. vm.Set("_git_init", func(call otto.FunctionCall) otto.Value {
  122. vpath, _ := call.Argument(0).ToString()
  123. rpath, err := resolveRepo(vpath, true)
  124. if err != nil {
  125. return gitOperationFailure(vm, err)
  126. }
  127. if err := manager.Init(rpath); err != nil {
  128. return gitOperationFailure(vm, err)
  129. }
  130. return gitOperationSuccess(vm, "")
  131. })
  132. // git.clone(url, destVpath, options) -> {success, error, authRequired}
  133. vm.Set("_git_clone", func(call otto.FunctionCall) otto.Value {
  134. url, _ := call.Argument(0).ToString()
  135. vpath, _ := call.Argument(1).ToString()
  136. options := exportOptions(call.Argument(2))
  137. rpath, err := resolveRepo(vpath, true)
  138. if err != nil {
  139. return gitOperationFailure(vm, err)
  140. }
  141. username, token := g.resolveGitCredential(u, url, options)
  142. err = manager.Clone(&git.CloneRequest{
  143. URL: url,
  144. Dest: rpath,
  145. Branch: optionString(options, "branch"),
  146. Depth: optionInt(options, "depth"),
  147. Username: username,
  148. Token: token,
  149. })
  150. if err != nil {
  151. return gitOperationFailure(vm, err)
  152. }
  153. //Remember the credential when the caller asked us to, now that we know
  154. //it actually works.
  155. g.rememberGitCredential(u, url, options)
  156. return gitOperationSuccess(vm, "cloned into "+vpath)
  157. })
  158. // git.status(vpath) -> status object
  159. vm.Set("_git_status", func(call otto.FunctionCall) otto.Value {
  160. vpath, _ := call.Argument(0).ToString()
  161. rpath, err := resolveRepo(vpath, false)
  162. if err != nil {
  163. return gitJSONError(vm, err)
  164. }
  165. status, err := manager.Status(rpath)
  166. if err != nil {
  167. return gitJSONError(vm, err)
  168. }
  169. status.Path = static.RelativeVpathRewrite(scriptFsh, vpath, vm, u)
  170. return gitJSONValue(vm, status)
  171. })
  172. // git.log(vpath, limit) -> [commit, ...]
  173. vm.Set("_git_log", func(call otto.FunctionCall) otto.Value {
  174. vpath, _ := call.Argument(0).ToString()
  175. limit := 0
  176. if !call.Argument(1).IsUndefined() {
  177. if parsed, err := call.Argument(1).ToInteger(); err == nil {
  178. limit = int(parsed)
  179. }
  180. }
  181. rpath, err := resolveRepo(vpath, false)
  182. if err != nil {
  183. return gitJSONError(vm, err)
  184. }
  185. commits, err := manager.Log(rpath, limit)
  186. if err != nil {
  187. return gitJSONError(vm, err)
  188. }
  189. return gitJSONValue(vm, commits)
  190. })
  191. // git.branches(vpath) -> [branch, ...]
  192. vm.Set("_git_branches", func(call otto.FunctionCall) otto.Value {
  193. vpath, _ := call.Argument(0).ToString()
  194. rpath, err := resolveRepo(vpath, false)
  195. if err != nil {
  196. return gitJSONError(vm, err)
  197. }
  198. branches, err := manager.Branches(rpath)
  199. if err != nil {
  200. return gitJSONError(vm, err)
  201. }
  202. return gitJSONValue(vm, branches)
  203. })
  204. // git.checkout(vpath, branch, create) -> {success, error}
  205. vm.Set("_git_checkout", func(call otto.FunctionCall) otto.Value {
  206. vpath, _ := call.Argument(0).ToString()
  207. branch, _ := call.Argument(1).ToString()
  208. create, _ := call.Argument(2).ToBoolean()
  209. rpath, err := resolveRepo(vpath, true)
  210. if err != nil {
  211. return gitOperationFailure(vm, err)
  212. }
  213. if err := manager.Checkout(rpath, branch, create); err != nil {
  214. return gitOperationFailure(vm, err)
  215. }
  216. return gitOperationSuccess(vm, "switched to "+branch)
  217. })
  218. // git.remotes(vpath) -> [remote, ...]
  219. vm.Set("_git_remotes", func(call otto.FunctionCall) otto.Value {
  220. vpath, _ := call.Argument(0).ToString()
  221. rpath, err := resolveRepo(vpath, false)
  222. if err != nil {
  223. return gitJSONError(vm, err)
  224. }
  225. remotes, err := manager.Remotes(rpath)
  226. if err != nil {
  227. return gitJSONError(vm, err)
  228. }
  229. return gitJSONValue(vm, remotes)
  230. })
  231. // git.addRemote(vpath, name, url) -> {success, error}
  232. vm.Set("_git_addremote", func(call otto.FunctionCall) otto.Value {
  233. vpath, _ := call.Argument(0).ToString()
  234. name, _ := call.Argument(1).ToString()
  235. url, _ := call.Argument(2).ToString()
  236. rpath, err := resolveRepo(vpath, true)
  237. if err != nil {
  238. return gitOperationFailure(vm, err)
  239. }
  240. if err := manager.AddRemote(rpath, name, url); err != nil {
  241. return gitOperationFailure(vm, err)
  242. }
  243. return gitOperationSuccess(vm, "remote "+name+" saved")
  244. })
  245. // git.removeRemote(vpath, name) -> {success, error}
  246. vm.Set("_git_removeremote", func(call otto.FunctionCall) otto.Value {
  247. vpath, _ := call.Argument(0).ToString()
  248. name, _ := call.Argument(1).ToString()
  249. rpath, err := resolveRepo(vpath, true)
  250. if err != nil {
  251. return gitOperationFailure(vm, err)
  252. }
  253. if err := manager.RemoveRemote(rpath, name); err != nil {
  254. return gitOperationFailure(vm, err)
  255. }
  256. return gitOperationSuccess(vm, "remote "+name+" removed")
  257. })
  258. // git.add(vpath, files) -> {success, error}
  259. vm.Set("_git_add", func(call otto.FunctionCall) otto.Value {
  260. vpath, _ := call.Argument(0).ToString()
  261. files := exportStringSlice(call.Argument(1))
  262. rpath, err := resolveRepo(vpath, true)
  263. if err != nil {
  264. return gitOperationFailure(vm, err)
  265. }
  266. if err := manager.Add(rpath, files); err != nil {
  267. return gitOperationFailure(vm, err)
  268. }
  269. return gitOperationSuccess(vm, "")
  270. })
  271. // git.addAll(vpath) -> {success, error}
  272. vm.Set("_git_addall", func(call otto.FunctionCall) otto.Value {
  273. vpath, _ := call.Argument(0).ToString()
  274. rpath, err := resolveRepo(vpath, true)
  275. if err != nil {
  276. return gitOperationFailure(vm, err)
  277. }
  278. if err := manager.AddAll(rpath); err != nil {
  279. return gitOperationFailure(vm, err)
  280. }
  281. return gitOperationSuccess(vm, "")
  282. })
  283. // git.unstage(vpath, files) -> {success, error}
  284. vm.Set("_git_unstage", func(call otto.FunctionCall) otto.Value {
  285. vpath, _ := call.Argument(0).ToString()
  286. files := exportStringSlice(call.Argument(1))
  287. rpath, err := resolveRepo(vpath, true)
  288. if err != nil {
  289. return gitOperationFailure(vm, err)
  290. }
  291. if err := manager.Unstage(rpath, files); err != nil {
  292. return gitOperationFailure(vm, err)
  293. }
  294. return gitOperationSuccess(vm, "")
  295. })
  296. // git.discard(vpath, files) -> {success, error}
  297. vm.Set("_git_discard", func(call otto.FunctionCall) otto.Value {
  298. vpath, _ := call.Argument(0).ToString()
  299. files := exportStringSlice(call.Argument(1))
  300. rpath, err := resolveRepo(vpath, true)
  301. if err != nil {
  302. return gitOperationFailure(vm, err)
  303. }
  304. if err := manager.Discard(rpath, files); err != nil {
  305. return gitOperationFailure(vm, err)
  306. }
  307. return gitOperationSuccess(vm, "")
  308. })
  309. // git.ignore(vpath, patterns) -> {success, error, message}
  310. vm.Set("_git_ignore", func(call otto.FunctionCall) otto.Value {
  311. vpath, _ := call.Argument(0).ToString()
  312. patterns := exportStringSlice(call.Argument(1))
  313. rpath, err := resolveRepo(vpath, true)
  314. if err != nil {
  315. return gitOperationFailure(vm, err)
  316. }
  317. added, err := manager.AddIgnoreRules(rpath, patterns)
  318. if err != nil {
  319. return gitOperationFailure(vm, err)
  320. }
  321. if len(added) == 0 {
  322. return gitOperationSuccess(vm, "already ignored")
  323. }
  324. return gitOperationSuccess(vm, "ignoring "+strings.Join(added, ", "))
  325. })
  326. // git.commit(vpath, message, files, options) -> {success, error, hash}
  327. vm.Set("_git_commit", func(call otto.FunctionCall) otto.Value {
  328. vpath, _ := call.Argument(0).ToString()
  329. message, _ := call.Argument(1).ToString()
  330. files := exportStringSlice(call.Argument(2))
  331. options := exportOptions(call.Argument(3))
  332. rpath, err := resolveRepo(vpath, true)
  333. if err != nil {
  334. return gitOperationFailure(vm, err)
  335. }
  336. authorName := optionString(options, "name")
  337. if authorName == "" {
  338. authorName = u.Username
  339. }
  340. hash, err := manager.Commit(rpath, &git.CommitRequest{
  341. Message: message,
  342. Files: files,
  343. Name: authorName,
  344. Email: optionString(options, "email"),
  345. All: optionBool(options, "all"),
  346. })
  347. if err != nil {
  348. return gitOperationFailure(vm, err)
  349. }
  350. return gitJSONValue(vm, git.OperationResult{
  351. Success: true,
  352. Hash: hash,
  353. Message: "commit created",
  354. })
  355. })
  356. // git.diff(vpath, file) -> file diff
  357. vm.Set("_git_diff", func(call otto.FunctionCall) otto.Value {
  358. vpath, _ := call.Argument(0).ToString()
  359. file, _ := call.Argument(1).ToString()
  360. rpath, err := resolveRepo(vpath, false)
  361. if err != nil {
  362. return gitJSONError(vm, err)
  363. }
  364. diff, err := manager.Diff(rpath, file)
  365. if err != nil {
  366. return gitJSONError(vm, err)
  367. }
  368. return gitJSONValue(vm, diff)
  369. })
  370. // git.diffCommit(vpath, hash, file) -> file diff
  371. vm.Set("_git_diffcommit", func(call otto.FunctionCall) otto.Value {
  372. vpath, _ := call.Argument(0).ToString()
  373. hash, _ := call.Argument(1).ToString()
  374. file, _ := call.Argument(2).ToString()
  375. rpath, err := resolveRepo(vpath, false)
  376. if err != nil {
  377. return gitJSONError(vm, err)
  378. }
  379. diff, err := manager.DiffCommit(rpath, hash, file)
  380. if err != nil {
  381. return gitJSONError(vm, err)
  382. }
  383. return gitJSONValue(vm, diff)
  384. })
  385. // git.fileBlob(vpath, file, revision) -> {success, exists, base64, mime, kind, size}
  386. // Reads a file's content at a revision so the front-end can preview the
  387. // "before" side of a change that only exists in the object database.
  388. vm.Set("_git_fileblob", func(call otto.FunctionCall) otto.Value {
  389. vpath, _ := call.Argument(0).ToString()
  390. file, _ := call.Argument(1).ToString()
  391. revision, _ := call.Argument(2).ToString()
  392. rpath, err := resolveRepo(vpath, false)
  393. if err != nil {
  394. return gitJSONError(vm, err)
  395. }
  396. content, exists, err := manager.FileBlob(rpath, file, revision)
  397. if err != nil {
  398. return gitJSONError(vm, err)
  399. }
  400. payload := map[string]interface{}{
  401. "success": true,
  402. "exists": exists,
  403. "mime": git.PreviewMimeType(file),
  404. "kind": git.PreviewKind(file),
  405. "size": len(content),
  406. }
  407. if exists {
  408. payload["base64"] = base64.StdEncoding.EncodeToString(content)
  409. }
  410. return gitJSONValue(vm, payload)
  411. })
  412. // git.commitFiles(vpath, hash) -> [file, ...]
  413. vm.Set("_git_commitfiles", func(call otto.FunctionCall) otto.Value {
  414. vpath, _ := call.Argument(0).ToString()
  415. hash, _ := call.Argument(1).ToString()
  416. rpath, err := resolveRepo(vpath, false)
  417. if err != nil {
  418. return gitJSONError(vm, err)
  419. }
  420. files, err := manager.CommitFiles(rpath, hash)
  421. if err != nil {
  422. return gitJSONError(vm, err)
  423. }
  424. return gitJSONValue(vm, files)
  425. })
  426. // git.deleteBranch(vpath, branch, force) -> {success, error, unmerged}
  427. vm.Set("_git_deletebranch", func(call otto.FunctionCall) otto.Value {
  428. vpath, _ := call.Argument(0).ToString()
  429. branch, _ := call.Argument(1).ToString()
  430. force, _ := call.Argument(2).ToBoolean()
  431. rpath, err := resolveRepo(vpath, true)
  432. if err != nil {
  433. return gitOperationFailure(vm, err)
  434. }
  435. if err := manager.DeleteBranch(rpath, branch, force); err != nil {
  436. return gitOperationFailure(vm, err)
  437. }
  438. return gitOperationSuccess(vm, "deleted branch "+branch)
  439. })
  440. // git.renameBranch(vpath, oldName, newName) -> {success, error}
  441. vm.Set("_git_renamebranch", func(call otto.FunctionCall) otto.Value {
  442. vpath, _ := call.Argument(0).ToString()
  443. oldName, _ := call.Argument(1).ToString()
  444. newName, _ := call.Argument(2).ToString()
  445. rpath, err := resolveRepo(vpath, true)
  446. if err != nil {
  447. return gitOperationFailure(vm, err)
  448. }
  449. if err := manager.RenameBranch(rpath, oldName, newName); err != nil {
  450. return gitOperationFailure(vm, err)
  451. }
  452. return gitOperationSuccess(vm, "renamed to "+newName)
  453. })
  454. // git.deleteRemoteBranch(vpath, remote, branch, options) -> {success, error, authRequired}
  455. vm.Set("_git_deleteremotebranch", func(call otto.FunctionCall) otto.Value {
  456. vpath, _ := call.Argument(0).ToString()
  457. remote, _ := call.Argument(1).ToString()
  458. branch, _ := call.Argument(2).ToString()
  459. options := exportOptions(call.Argument(3))
  460. rpath, err := resolveRepo(vpath, true)
  461. if err != nil {
  462. return gitOperationFailure(vm, err)
  463. }
  464. request, err := g.remoteBranchRequest(u, rpath, remote, options)
  465. if err != nil {
  466. return gitOperationFailure(vm, err)
  467. }
  468. if err := manager.DeleteRemoteBranch(rpath, remote, branch, request); err != nil {
  469. return gitOperationFailure(vm, err)
  470. }
  471. g.rememberGitCredentialForRemote(u, rpath, remote, options)
  472. return gitOperationSuccess(vm, "deleted "+remoteOrOrigin(remote)+"/"+branch)
  473. })
  474. // git.renameRemoteBranch(vpath, remote, oldName, newName, options) -> {success, error, authRequired}
  475. vm.Set("_git_renameremotebranch", func(call otto.FunctionCall) otto.Value {
  476. vpath, _ := call.Argument(0).ToString()
  477. remote, _ := call.Argument(1).ToString()
  478. oldName, _ := call.Argument(2).ToString()
  479. newName, _ := call.Argument(3).ToString()
  480. options := exportOptions(call.Argument(4))
  481. rpath, err := resolveRepo(vpath, true)
  482. if err != nil {
  483. return gitOperationFailure(vm, err)
  484. }
  485. request, err := g.remoteBranchRequest(u, rpath, remote, options)
  486. if err != nil {
  487. return gitOperationFailure(vm, err)
  488. }
  489. if err := manager.RenameRemoteBranch(rpath, remote, oldName, newName, request); err != nil {
  490. return gitOperationFailure(vm, err)
  491. }
  492. g.rememberGitCredentialForRemote(u, rpath, remote, options)
  493. return gitOperationSuccess(vm, "renamed to "+remoteOrOrigin(remote)+"/"+newName)
  494. })
  495. // git.checkoutCommit(vpath, hash) -> {success, error}
  496. vm.Set("_git_checkoutcommit", func(call otto.FunctionCall) otto.Value {
  497. vpath, _ := call.Argument(0).ToString()
  498. hash, _ := call.Argument(1).ToString()
  499. rpath, err := resolveRepo(vpath, true)
  500. if err != nil {
  501. return gitOperationFailure(vm, err)
  502. }
  503. if err := manager.CheckoutCommit(rpath, hash); err != nil {
  504. return gitOperationFailure(vm, err)
  505. }
  506. return gitOperationSuccess(vm, "checked out "+shortHash(hash))
  507. })
  508. // git.resetToCommit(vpath, hash, mode) -> {success, error}
  509. vm.Set("_git_resettocommit", func(call otto.FunctionCall) otto.Value {
  510. vpath, _ := call.Argument(0).ToString()
  511. hash, _ := call.Argument(1).ToString()
  512. mode, _ := call.Argument(2).ToString()
  513. rpath, err := resolveRepo(vpath, true)
  514. if err != nil {
  515. return gitOperationFailure(vm, err)
  516. }
  517. if err := manager.ResetToCommit(rpath, hash, mode); err != nil {
  518. return gitOperationFailure(vm, err)
  519. }
  520. return gitOperationSuccess(vm, "reset to "+shortHash(hash))
  521. })
  522. // git.createBranchAt(vpath, branch, hash) -> {success, error}
  523. vm.Set("_git_createbranchat", func(call otto.FunctionCall) otto.Value {
  524. vpath, _ := call.Argument(0).ToString()
  525. branch, _ := call.Argument(1).ToString()
  526. hash, _ := call.Argument(2).ToString()
  527. rpath, err := resolveRepo(vpath, true)
  528. if err != nil {
  529. return gitOperationFailure(vm, err)
  530. }
  531. if err := manager.CreateBranchAt(rpath, branch, hash); err != nil {
  532. return gitOperationFailure(vm, err)
  533. }
  534. return gitOperationSuccess(vm, "created branch "+branch)
  535. })
  536. // git.createTag(vpath, tag, hash, message) -> {success, error}
  537. vm.Set("_git_createtag", func(call otto.FunctionCall) otto.Value {
  538. vpath, _ := call.Argument(0).ToString()
  539. tag, _ := call.Argument(1).ToString()
  540. hash, _ := call.Argument(2).ToString()
  541. message, _ := call.Argument(3).ToString()
  542. rpath, err := resolveRepo(vpath, true)
  543. if err != nil {
  544. return gitOperationFailure(vm, err)
  545. }
  546. if err := manager.CreateTag(rpath, tag, hash, message, u.Username, ""); err != nil {
  547. return gitOperationFailure(vm, err)
  548. }
  549. return gitOperationSuccess(vm, "created tag "+tag)
  550. })
  551. // git.revertCommit(vpath, hash, options) -> {success, error, hash}
  552. vm.Set("_git_revertcommit", func(call otto.FunctionCall) otto.Value {
  553. vpath, _ := call.Argument(0).ToString()
  554. hash, _ := call.Argument(1).ToString()
  555. options := exportOptions(call.Argument(2))
  556. rpath, err := resolveRepo(vpath, true)
  557. if err != nil {
  558. return gitOperationFailure(vm, err)
  559. }
  560. newHash, err := manager.RevertCommit(rpath, hash, commitRequestFromOptions(u, options))
  561. if err != nil {
  562. return gitOperationFailure(vm, err)
  563. }
  564. return gitJSONValue(vm, git.OperationResult{Success: true, Hash: newHash, Message: "reverted " + shortHash(hash)})
  565. })
  566. // git.cherryPickCommit(vpath, hash, options) -> {success, error, hash}
  567. vm.Set("_git_cherrypickcommit", func(call otto.FunctionCall) otto.Value {
  568. vpath, _ := call.Argument(0).ToString()
  569. hash, _ := call.Argument(1).ToString()
  570. options := exportOptions(call.Argument(2))
  571. rpath, err := resolveRepo(vpath, true)
  572. if err != nil {
  573. return gitOperationFailure(vm, err)
  574. }
  575. newHash, err := manager.CherryPickCommit(rpath, hash, commitRequestFromOptions(u, options))
  576. if err != nil {
  577. return gitOperationFailure(vm, err)
  578. }
  579. return gitJSONValue(vm, git.OperationResult{Success: true, Hash: newHash, Message: "cherry-picked " + shortHash(hash)})
  580. })
  581. // git.amendMessage(vpath, message, options) -> {success, error, hash}
  582. vm.Set("_git_amendmessage", func(call otto.FunctionCall) otto.Value {
  583. vpath, _ := call.Argument(0).ToString()
  584. message, _ := call.Argument(1).ToString()
  585. options := exportOptions(call.Argument(2))
  586. rpath, err := resolveRepo(vpath, true)
  587. if err != nil {
  588. return gitOperationFailure(vm, err)
  589. }
  590. newHash, err := manager.AmendCommitMessage(rpath, message, commitRequestFromOptions(u, options))
  591. if err != nil {
  592. return gitOperationFailure(vm, err)
  593. }
  594. return gitJSONValue(vm, git.OperationResult{Success: true, Hash: newHash, Message: "amended commit message"})
  595. })
  596. // git.fetch(vpath, options) -> {success, error, authRequired}
  597. vm.Set("_git_fetch", func(call otto.FunctionCall) otto.Value {
  598. return g.runGitTransport(vm, u, call, resolveRepo, func(rpath string, request *git.TransportRequest) (string, error) {
  599. return "fetched", manager.Fetch(rpath, request)
  600. })
  601. })
  602. // git.pull(vpath, options) -> {success, error, authRequired}
  603. vm.Set("_git_pull", func(call otto.FunctionCall) otto.Value {
  604. return g.runGitTransport(vm, u, call, resolveRepo, func(rpath string, request *git.TransportRequest) (string, error) {
  605. return manager.Pull(rpath, request)
  606. })
  607. })
  608. // git.push(vpath, options) -> {success, error, authRequired}
  609. vm.Set("_git_push", func(call otto.FunctionCall) otto.Value {
  610. return g.runGitTransport(vm, u, call, resolveRepo, func(rpath string, request *git.TransportRequest) (string, error) {
  611. return manager.Push(rpath, request)
  612. })
  613. })
  614. // git.saveCredential(host, username, token) -> {success, error}
  615. vm.Set("_git_savecredential", func(call otto.FunctionCall) otto.Value {
  616. host, _ := call.Argument(0).ToString()
  617. remoteUser, _ := call.Argument(1).ToString()
  618. token, _ := call.Argument(2).ToString()
  619. store, err := g.gitCredentialStore()
  620. if err != nil {
  621. return gitOperationFailure(vm, err)
  622. }
  623. if err := store.Save(u.Username, host, remoteUser, token); err != nil {
  624. return gitOperationFailure(vm, err)
  625. }
  626. return gitOperationSuccess(vm, "credential saved")
  627. })
  628. // git.listCredentials() -> [{host, username}, ...]
  629. vm.Set("_git_listcredentials", func(call otto.FunctionCall) otto.Value {
  630. store, err := g.gitCredentialStore()
  631. if err != nil {
  632. return gitJSONError(vm, err)
  633. }
  634. return gitJSONValue(vm, store.List(u.Username))
  635. })
  636. // git.hasCredential(host) -> bool
  637. vm.Set("_git_hascredential", func(call otto.FunctionCall) otto.Value {
  638. host, _ := call.Argument(0).ToString()
  639. store, err := g.gitCredentialStore()
  640. if err != nil {
  641. return otto.FalseValue()
  642. }
  643. if _, found := store.Get(u.Username, host); found {
  644. return otto.TrueValue()
  645. }
  646. return otto.FalseValue()
  647. })
  648. // git.removeCredential(host) -> {success, error}
  649. vm.Set("_git_removecredential", func(call otto.FunctionCall) otto.Value {
  650. host, _ := call.Argument(0).ToString()
  651. store, err := g.gitCredentialStore()
  652. if err != nil {
  653. return gitOperationFailure(vm, err)
  654. }
  655. if err := store.Remove(u.Username, host); err != nil {
  656. return gitOperationFailure(vm, err)
  657. }
  658. return gitOperationSuccess(vm, "credential removed")
  659. })
  660. // git.remoteHost(url) -> host name used as the credential key
  661. vm.Set("_git_remotehost", func(call otto.FunctionCall) otto.Value {
  662. url, _ := call.Argument(0).ToString()
  663. result, _ := vm.ToValue(git.RemoteHost(url))
  664. return result
  665. })
  666. //Wrap the native functions into a git object. Everything that can carry
  667. //structured data crosses the boundary as JSON so the script always receives
  668. //plain JavaScript objects.
  669. vm.Run(gitLibJavaScript)
  670. }
  671. // gitLibJavaScript is the in-VM wrapper that turns the native _git_* calls into
  672. // the `git` object AGI scripts use. Kept as a package constant so a unit test
  673. // can execute it and catch a syntax error before it reaches a user's script.
  674. const gitLibJavaScript = `
  675. var git = {};
  676. var _git_parse = function(raw) {
  677. if (raw === false || raw === undefined || raw === null) {
  678. return {error: "git call failed"};
  679. }
  680. try { return JSON.parse(raw); } catch(e) { return {error: "malformed git response"}; }
  681. };
  682. git.isRepo = _git_isrepo;
  683. git.repoRoot = _git_reporoot;
  684. git.remoteHost = _git_remotehost;
  685. git.hasCredential = _git_hascredential;
  686. git.init = function(vpath) { return _git_parse(_git_init(vpath)); };
  687. git.clone = function(url, vpath, options) { return _git_parse(_git_clone(url, vpath, options || {})); };
  688. git.status = function(vpath) { return _git_parse(_git_status(vpath)); };
  689. git.log = function(vpath, limit) { return _git_parse(_git_log(vpath, limit)); };
  690. git.branches = function(vpath) { return _git_parse(_git_branches(vpath)); };
  691. git.checkout = function(vpath, branch, create) { return _git_parse(_git_checkout(vpath, branch, create === true)); };
  692. git.remotes = function(vpath) { return _git_parse(_git_remotes(vpath)); };
  693. git.addRemote = function(vpath, name, url) { return _git_parse(_git_addremote(vpath, name, url)); };
  694. git.removeRemote = function(vpath, name) { return _git_parse(_git_removeremote(vpath, name)); };
  695. git.add = function(vpath, files) { return _git_parse(_git_add(vpath, files || [])); };
  696. git.addAll = function(vpath) { return _git_parse(_git_addall(vpath)); };
  697. git.unstage = function(vpath, files) { return _git_parse(_git_unstage(vpath, files || [])); };
  698. git.discard = function(vpath, files) { return _git_parse(_git_discard(vpath, files || [])); };
  699. git.commit = function(vpath, message, files, options) { return _git_parse(_git_commit(vpath, message, files || [], options || {})); };
  700. git.ignore = function(vpath, patterns) { return _git_parse(_git_ignore(vpath, patterns || [])); };
  701. git.diff = function(vpath, file) { return _git_parse(_git_diff(vpath, file)); };
  702. git.diffCommit = function(vpath, hash, file) { return _git_parse(_git_diffcommit(vpath, hash, file)); };
  703. git.commitFiles = function(vpath, hash) { return _git_parse(_git_commitfiles(vpath, hash)); };
  704. git.fileBlob = function(vpath, file, revision) { return _git_parse(_git_fileblob(vpath, file, revision || "HEAD")); };
  705. git.deleteBranch = function(vpath, branch, force) { return _git_parse(_git_deletebranch(vpath, branch, force === true)); };
  706. git.renameBranch = function(vpath, oldName, newName) { return _git_parse(_git_renamebranch(vpath, oldName, newName)); };
  707. git.deleteRemoteBranch = function(vpath, remote, branch, options) { return _git_parse(_git_deleteremotebranch(vpath, remote || "origin", branch, options || {})); };
  708. git.renameRemoteBranch = function(vpath, remote, oldName, newName, options) { return _git_parse(_git_renameremotebranch(vpath, remote || "origin", oldName, newName, options || {})); };
  709. git.checkoutCommit = function(vpath, hash) { return _git_parse(_git_checkoutcommit(vpath, hash)); };
  710. git.resetToCommit = function(vpath, hash, mode) { return _git_parse(_git_resettocommit(vpath, hash, mode || "mixed")); };
  711. git.createBranchAt = function(vpath, branch, hash) { return _git_parse(_git_createbranchat(vpath, branch, hash)); };
  712. git.createTag = function(vpath, tag, hash, message) { return _git_parse(_git_createtag(vpath, tag, hash, message || "")); };
  713. git.revertCommit = function(vpath, hash, options) { return _git_parse(_git_revertcommit(vpath, hash, options || {})); };
  714. git.cherryPickCommit = function(vpath, hash, options) { return _git_parse(_git_cherrypickcommit(vpath, hash, options || {})); };
  715. git.amendMessage = function(vpath, message, options) { return _git_parse(_git_amendmessage(vpath, message, options || {})); };
  716. git.fetch = function(vpath, options) { return _git_parse(_git_fetch(vpath, options || {})); };
  717. git.pull = function(vpath, options) { return _git_parse(_git_pull(vpath, options || {})); };
  718. git.push = function(vpath, options) { return _git_parse(_git_push(vpath, options || {})); };
  719. git.saveCredential = function(host, username, token) { return _git_parse(_git_savecredential(host, username, token)); };
  720. git.listCredentials = function() { return _git_parse(_git_listcredentials()); };
  721. git.removeCredential = function(host) { return _git_parse(_git_removecredential(host)); };
  722. `
  723. // gitTransportRunner is the shape of the fetch / pull / push bodies, letting the
  724. // three of them share credential resolution and error reporting.
  725. type gitTransportRunner func(rpath string, request *git.TransportRequest) (string, error)
  726. // runGitTransport wires a transport call: resolve the path, work out which
  727. // credential to use, run the operation and translate the outcome.
  728. func (g *Gateway) runGitTransport(vm *otto.Otto, u *user.User, call otto.FunctionCall,
  729. resolveRepo func(string, bool) (string, error), run gitTransportRunner) otto.Value {
  730. vpath, _ := call.Argument(0).ToString()
  731. options := exportOptions(call.Argument(1))
  732. //Fetching only rewrites remote-tracking refs inside .git, but that is still
  733. //a write to the repository folder.
  734. rpath, err := resolveRepo(vpath, true)
  735. if err != nil {
  736. return gitOperationFailure(vm, err)
  737. }
  738. remote := optionString(options, "remote")
  739. remoteURL, urlErr := g.Option.GitManager.RemoteURLForName(rpath, remote)
  740. if urlErr != nil {
  741. return gitOperationFailure(vm, urlErr)
  742. }
  743. username, token := g.resolveGitCredential(u, remoteURL, options)
  744. message, err := run(rpath, &git.TransportRequest{
  745. Remote: remote,
  746. Branch: optionString(options, "branch"),
  747. Username: username,
  748. Token: token,
  749. Force: optionBool(options, "force"),
  750. SetUpstream: optionBool(options, "setUpstream"),
  751. })
  752. if err != nil {
  753. return gitOperationFailure(vm, err)
  754. }
  755. g.rememberGitCredential(u, remoteURL, options)
  756. return gitOperationSuccess(vm, message)
  757. }
  758. /*
  759. remoteBranchRequest builds the TransportRequest for a remote branch operation,
  760. resolving which credential applies from the remote's own URL.
  761. The remote-branch calls take the remote by name rather than by URL, so the URL
  762. has to be looked up before the stored credential can be found.
  763. */
  764. func (g *Gateway) remoteBranchRequest(u *user.User, rpath string, remote string, options map[string]interface{}) (*git.TransportRequest, error) {
  765. remoteURL, err := g.Option.GitManager.RemoteURLForName(rpath, remote)
  766. if err != nil {
  767. return nil, err
  768. }
  769. username, token := g.resolveGitCredential(u, remoteURL, options)
  770. return &git.TransportRequest{
  771. Remote: remote,
  772. Username: username,
  773. Token: token,
  774. }, nil
  775. }
  776. // rememberGitCredentialForRemote stores a credential that has just proven itself
  777. // against a remote addressed by name.
  778. func (g *Gateway) rememberGitCredentialForRemote(u *user.User, rpath string, remote string, options map[string]interface{}) {
  779. if !optionBool(options, "remember") {
  780. return
  781. }
  782. remoteURL, err := g.Option.GitManager.RemoteURLForName(rpath, remote)
  783. if err != nil {
  784. return
  785. }
  786. g.rememberGitCredential(u, remoteURL, options)
  787. }
  788. // remoteOrOrigin mirrors the manager's default so status messages name the same
  789. // remote the operation actually used.
  790. func remoteOrOrigin(remote string) string {
  791. remote = strings.TrimSpace(remote)
  792. if remote == "" {
  793. return "origin"
  794. }
  795. return remote
  796. }
  797. // commitRequestFromOptions builds a CommitRequest for the history actions,
  798. // defaulting the author name to the calling user so a commit always has an
  799. // identity even when the front-end sends none.
  800. func commitRequestFromOptions(u *user.User, options map[string]interface{}) *git.CommitRequest {
  801. name := optionString(options, "name")
  802. if name == "" && u != nil {
  803. name = u.Username
  804. }
  805. return &git.CommitRequest{
  806. Message: optionString(options, "message"),
  807. Name: name,
  808. Email: optionString(options, "email"),
  809. }
  810. }
  811. // shortHash abbreviates a commit hash for status messages, tolerating a hash
  812. // that is already short.
  813. func shortHash(hash string) string {
  814. hash = strings.TrimSpace(hash)
  815. if len(hash) > 7 {
  816. return hash[:7]
  817. }
  818. return hash
  819. }
  820. // gitCredentialStore returns the credential store or a readable error when git
  821. // support was started without a database.
  822. func (g *Gateway) gitCredentialStore() (*git.CredentialStore, error) {
  823. if g.Option.GitManager == nil {
  824. return nil, errors.New("git support is not enabled on this system")
  825. }
  826. store := g.Option.GitManager.Credentials()
  827. if store == nil {
  828. return nil, errors.New("git credential storage is not available")
  829. }
  830. return store, nil
  831. }
  832. // resolveGitCredential decides which username / token a transport call should
  833. // use: the ones passed in the options win, otherwise the user's stored
  834. // credential for that host is used, otherwise the call proceeds anonymously.
  835. func (g *Gateway) resolveGitCredential(u *user.User, remoteURL string, options map[string]interface{}) (string, string) {
  836. username := optionString(options, "username")
  837. token := optionString(options, "token")
  838. if token != "" {
  839. return username, token
  840. }
  841. store, err := g.gitCredentialStore()
  842. if err != nil {
  843. return username, token
  844. }
  845. credential, found := store.ResolveForRemote(u.Username, remoteURL)
  846. if !found {
  847. return username, token
  848. }
  849. if username == "" {
  850. username = credential.Username
  851. }
  852. return username, credential.Token
  853. }
  854. // rememberGitCredential persists the credential that was just proven to work,
  855. // but only when the caller explicitly asked for it (options.remember).
  856. func (g *Gateway) rememberGitCredential(u *user.User, remoteURL string, options map[string]interface{}) {
  857. if !optionBool(options, "remember") {
  858. return
  859. }
  860. token := optionString(options, "token")
  861. if token == "" {
  862. return
  863. }
  864. store, err := g.gitCredentialStore()
  865. if err != nil {
  866. return
  867. }
  868. host := git.RemoteHost(remoteURL)
  869. if host == "" {
  870. return
  871. }
  872. if err := store.Save(u.Username, host, optionString(options, "username"), token); err != nil {
  873. logger.PrintAndLog("Agi", "[AGI] Unable to store git credential for "+host, err)
  874. }
  875. }
  876. /*
  877. repoRootVirtualPath maps a working tree root back onto the caller's virtual path.
  878. It works out how many folder levels the requested path sits below the repository
  879. root on the real file system, then trims that many trailing segments off the
  880. virtual path. Doing it this way avoids a real-to-virtual conversion entirely,
  881. which matters because RepoRoot answers with an absolute OS path while the file
  882. system abstraction removes its storage root by prefix match against a relative
  883. one — the mismatch used to leave the absolute path embedded in the result.
  884. */
  885. func repoRootVirtualPath(vpath string, rpath string, realRoot string) (string, error) {
  886. absPath, err := filepath.Abs(rpath)
  887. if err != nil {
  888. return "", err
  889. }
  890. absRoot, err := filepath.Abs(realRoot)
  891. if err != nil {
  892. return "", err
  893. }
  894. relative, err := filepath.Rel(absRoot, absPath)
  895. if err != nil {
  896. return "", err
  897. }
  898. relative = filepath.ToSlash(relative)
  899. if relative == "." || relative == "" {
  900. //The requested path is the working tree root itself
  901. return vpath, nil
  902. }
  903. if relative == ".." || strings.HasPrefix(relative, "../") {
  904. return "", errors.New("repository root is not an ancestor of " + vpath)
  905. }
  906. return trimVirtualPathSegments(vpath, len(strings.Split(relative, "/")))
  907. }
  908. /*
  909. trimVirtualPathSegments removes count trailing segments from a virtual path.
  910. Virtual paths are handled as plain strings rather than through path/filepath:
  911. on Windows filepath.Clean treats the "uuid:" prefix as a volume name and
  912. mangles it.
  913. */
  914. func trimVirtualPathSegments(vpath string, count int) (string, error) {
  915. separator := strings.Index(vpath, ":/")
  916. if separator < 0 {
  917. return "", errors.New("not a virtual path: " + vpath)
  918. }
  919. root := vpath[:separator+2]
  920. body := strings.Trim(vpath[separator+2:], "/")
  921. segments := []string{}
  922. if body != "" {
  923. segments = strings.Split(body, "/")
  924. }
  925. if count > len(segments) {
  926. return "", errors.New("repository root lies outside " + vpath)
  927. }
  928. return root + strings.Join(segments[:len(segments)-count], "/"), nil
  929. }
  930. // checkFshSupportsGit rejects storage pools that cannot host a working tree.
  931. // Network-backed pools only expose streaming reads and writes, while git needs
  932. // ordinary random-access files.
  933. func checkFshSupportsGit(fsh *filesystem.FileSystemHandler) error {
  934. if fsh == nil {
  935. return errors.New("unable to resolve the storage pool for this path")
  936. }
  937. if fsh.RequireBuffer {
  938. return errors.New("git repositories must live on a local storage pool, not on " + fsh.Name)
  939. }
  940. if fsh.ReadOnly {
  941. return errors.New("storage pool " + fsh.Name + " is read only")
  942. }
  943. return nil
  944. }
  945. // gitJSONValue marshals a value and hands it to the VM as a JSON string.
  946. func gitJSONValue(vm *otto.Otto, value interface{}) otto.Value {
  947. encoded, err := json.Marshal(value)
  948. if err != nil {
  949. return gitJSONError(vm, err)
  950. }
  951. result, err := vm.ToValue(string(encoded))
  952. if err != nil {
  953. return otto.FalseValue()
  954. }
  955. return result
  956. }
  957. // gitJSONError returns an {error: "..."} object for the read-only calls, which
  958. // return their payload directly rather than a success envelope.
  959. func gitJSONError(vm *otto.Otto, err error) otto.Value {
  960. payload := map[string]interface{}{"error": err.Error()}
  961. if errors.Is(err, git.ErrAuthRequired) {
  962. payload["authRequired"] = true
  963. }
  964. encoded, _ := json.Marshal(payload)
  965. result, merr := vm.ToValue(string(encoded))
  966. if merr != nil {
  967. return otto.FalseValue()
  968. }
  969. return result
  970. }
  971. // gitOperationSuccess builds the standard success envelope.
  972. func gitOperationSuccess(vm *otto.Otto, message string) otto.Value {
  973. return gitJSONValue(vm, git.OperationResult{
  974. Success: true,
  975. Message: message,
  976. })
  977. }
  978. // gitOperationFailure builds the standard failure envelope, flagging credential
  979. // problems so the front-end knows to open its sign-in dialog, and an unmerged
  980. // branch so it can offer to delete anyway.
  981. func gitOperationFailure(vm *otto.Otto, err error) otto.Value {
  982. return gitJSONValue(vm, git.OperationResult{
  983. Success: false,
  984. Error: err.Error(),
  985. AuthRequired: errors.Is(err, git.ErrAuthRequired),
  986. Unmerged: errors.Is(err, git.ErrUnmergedBranch),
  987. })
  988. }
  989. // exportOptions converts a JavaScript options object into a Go map. A missing
  990. // or non-object argument yields an empty map so callers never need nil checks.
  991. func exportOptions(value otto.Value) map[string]interface{} {
  992. if !value.IsObject() {
  993. return map[string]interface{}{}
  994. }
  995. exported, err := value.Export()
  996. if err != nil {
  997. return map[string]interface{}{}
  998. }
  999. options, ok := exported.(map[string]interface{})
  1000. if !ok {
  1001. return map[string]interface{}{}
  1002. }
  1003. return options
  1004. }
  1005. // exportStringSlice converts a JavaScript array (or a single string) into a
  1006. // string slice.
  1007. func exportStringSlice(value otto.Value) []string {
  1008. results := []string{}
  1009. if value.IsUndefined() || value.IsNull() {
  1010. return results
  1011. }
  1012. if value.IsString() {
  1013. single, err := value.ToString()
  1014. if err == nil && single != "" {
  1015. results = append(results, single)
  1016. }
  1017. return results
  1018. }
  1019. exported, err := value.Export()
  1020. if err != nil {
  1021. return results
  1022. }
  1023. switch typed := exported.(type) {
  1024. case []string:
  1025. return typed
  1026. case []interface{}:
  1027. for _, item := range typed {
  1028. if text, ok := item.(string); ok && text != "" {
  1029. results = append(results, text)
  1030. }
  1031. }
  1032. }
  1033. return results
  1034. }
  1035. // optionString reads a string field from an options map.
  1036. func optionString(options map[string]interface{}, key string) string {
  1037. value, ok := options[key]
  1038. if !ok || value == nil {
  1039. return ""
  1040. }
  1041. if text, ok := value.(string); ok {
  1042. return strings.TrimSpace(text)
  1043. }
  1044. return ""
  1045. }
  1046. // optionBool reads a boolean field from an options map, accepting the string
  1047. // "true" as well so form values work unchanged.
  1048. func optionBool(options map[string]interface{}, key string) bool {
  1049. value, ok := options[key]
  1050. if !ok || value == nil {
  1051. return false
  1052. }
  1053. switch typed := value.(type) {
  1054. case bool:
  1055. return typed
  1056. case string:
  1057. return strings.EqualFold(strings.TrimSpace(typed), "true")
  1058. }
  1059. return false
  1060. }
  1061. // optionInt reads a numeric field from an options map. Otto hands numbers back
  1062. // as float64, and form values arrive as strings.
  1063. func optionInt(options map[string]interface{}, key string) int {
  1064. value, ok := options[key]
  1065. if !ok || value == nil {
  1066. return 0
  1067. }
  1068. switch typed := value.(type) {
  1069. case int:
  1070. return typed
  1071. case int64:
  1072. return int(typed)
  1073. case float64:
  1074. return int(typed)
  1075. case string:
  1076. parsed := 0
  1077. if _, err := fmt.Sscanf(strings.TrimSpace(typed), "%d", &parsed); err != nil {
  1078. return 0
  1079. }
  1080. return parsed
  1081. }
  1082. return 0
  1083. }