| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258 |
- package agi
- /*
- AGI Git Library
- Author: tobychui
- This library gives AGI scripts version control over folders inside the
- user's virtual file system: clone, status, stage, commit, branch, diff,
- fetch, pull and push, plus a per-user encrypted store for HTTPS credentials.
- Everything is backed by mod/git, which uses go-git — no `git` binary is
- required on the host, so this works identically on every platform ArozOS
- builds for.
- Usage (from an AGI script):
- requirelib("git");
- if (git.isRepo("user:/Desktop/myproject")) {
- var status = git.status("user:/Desktop/myproject");
- console.log(status.branch, status.changes.length);
- }
- Path rules:
- - Every path is an ArozOS virtual path and is permission checked.
- - Read-only calls need read permission, mutating calls need write
- permission on the repository path.
- - The storage pool must be local: network-backed pools (WebDAV, SMB, S3, …)
- cannot host a git working tree because git needs real random-access files.
- */
- import (
- "encoding/base64"
- "encoding/json"
- "errors"
- "fmt"
- "os"
- "path/filepath"
- "strings"
- "github.com/robertkrimen/otto"
- "imuslab.com/arozos/mod/agi/static"
- "imuslab.com/arozos/mod/filesystem"
- "imuslab.com/arozos/mod/git"
- "imuslab.com/arozos/mod/info/logger"
- user "imuslab.com/arozos/mod/user"
- )
- func (g *Gateway) GitLibRegister() {
- err := g.RegisterLib("git", g.injectGitLibFunctions)
- if err != nil {
- logger.PrintAndLog("Agi", fmt.Sprint(err), nil)
- os.Exit(1)
- }
- }
- func (g *Gateway) injectGitLibFunctions(payload *static.AgiLibInjectionPayload) {
- vm := payload.VM
- u := payload.User
- scriptFsh := payload.ScriptFsh
- manager := g.Option.GitManager
- // resolveRepo turns a virtual path into a real one after checking the
- // user's permission and that the storage pool can host a repository.
- resolveRepo := func(vpath string, needWrite bool) (string, error) {
- if manager == nil {
- return "", errors.New("git support is not enabled on this system")
- }
- if strings.TrimSpace(vpath) == "" {
- return "", errors.New("repository path cannot be empty")
- }
- vpath = static.RelativeVpathRewrite(scriptFsh, vpath, vm, u)
- if needWrite {
- if !u.CanWrite(vpath) {
- return "", errors.New("path access denied: " + vpath)
- }
- } else if !u.CanRead(vpath) {
- return "", errors.New("path access denied: " + vpath)
- }
- fsh, rpath, err := static.VirtualPathToRealPath(vpath, u)
- if err != nil {
- return "", err
- }
- if err := checkFshSupportsGit(fsh); err != nil {
- return "", err
- }
- return rpath, nil
- }
- // git.isRepo(vpath) -> bool
- vm.Set("_git_isrepo", func(call otto.FunctionCall) otto.Value {
- vpath, err := call.Argument(0).ToString()
- if err != nil {
- return otto.FalseValue()
- }
- rpath, err := resolveRepo(vpath, false)
- if err != nil {
- return otto.FalseValue()
- }
- if manager.IsRepo(rpath) {
- return otto.TrueValue()
- }
- return otto.FalseValue()
- })
- // git.repoRoot(vpath) -> virtual path of the working tree root, or false
- vm.Set("_git_reporoot", func(call otto.FunctionCall) otto.Value {
- vpath, err := call.Argument(0).ToString()
- if err != nil {
- return otto.FalseValue()
- }
- vpath = static.RelativeVpathRewrite(scriptFsh, vpath, vm, u)
- rpath, err := resolveRepo(vpath, false)
- if err != nil {
- return otto.FalseValue()
- }
- realRoot, err := manager.RepoRoot(rpath)
- if err != nil {
- return otto.FalseValue()
- }
- //The root is mapped back by trimming the virtual path, never by
- //converting the real path: RepoRoot returns an absolute OS path while
- //the file system abstraction strips its storage root by prefix match,
- //so handing it back would leave the whole absolute path embedded in
- //the vpath and double it on the next translation.
- rootVpath, err := repoRootVirtualPath(vpath, rpath, realRoot)
- if err != nil {
- g.RaiseError(err)
- return otto.FalseValue()
- }
- result, _ := vm.ToValue(rootVpath)
- return result
- })
- // git.init(vpath) -> {success, error}
- vm.Set("_git_init", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.Init(rpath); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "")
- })
- // git.clone(url, destVpath, options) -> {success, error, authRequired}
- vm.Set("_git_clone", func(call otto.FunctionCall) otto.Value {
- url, _ := call.Argument(0).ToString()
- vpath, _ := call.Argument(1).ToString()
- options := exportOptions(call.Argument(2))
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- username, token := g.resolveGitCredential(u, url, options)
- err = manager.Clone(&git.CloneRequest{
- URL: url,
- Dest: rpath,
- Branch: optionString(options, "branch"),
- Depth: optionInt(options, "depth"),
- Username: username,
- Token: token,
- })
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- //Remember the credential when the caller asked us to, now that we know
- //it actually works.
- g.rememberGitCredential(u, url, options)
- return gitOperationSuccess(vm, "cloned into "+vpath)
- })
- // git.status(vpath) -> status object
- vm.Set("_git_status", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- rpath, err := resolveRepo(vpath, false)
- if err != nil {
- return gitJSONError(vm, err)
- }
- status, err := manager.Status(rpath)
- if err != nil {
- return gitJSONError(vm, err)
- }
- status.Path = static.RelativeVpathRewrite(scriptFsh, vpath, vm, u)
- return gitJSONValue(vm, status)
- })
- // git.log(vpath, limit) -> [commit, ...]
- vm.Set("_git_log", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- limit := 0
- if !call.Argument(1).IsUndefined() {
- if parsed, err := call.Argument(1).ToInteger(); err == nil {
- limit = int(parsed)
- }
- }
- rpath, err := resolveRepo(vpath, false)
- if err != nil {
- return gitJSONError(vm, err)
- }
- commits, err := manager.Log(rpath, limit)
- if err != nil {
- return gitJSONError(vm, err)
- }
- return gitJSONValue(vm, commits)
- })
- // git.branches(vpath) -> [branch, ...]
- vm.Set("_git_branches", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- rpath, err := resolveRepo(vpath, false)
- if err != nil {
- return gitJSONError(vm, err)
- }
- branches, err := manager.Branches(rpath)
- if err != nil {
- return gitJSONError(vm, err)
- }
- return gitJSONValue(vm, branches)
- })
- // git.checkout(vpath, branch, create) -> {success, error}
- vm.Set("_git_checkout", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- branch, _ := call.Argument(1).ToString()
- create, _ := call.Argument(2).ToBoolean()
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.Checkout(rpath, branch, create); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "switched to "+branch)
- })
- // git.remotes(vpath) -> [remote, ...]
- vm.Set("_git_remotes", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- rpath, err := resolveRepo(vpath, false)
- if err != nil {
- return gitJSONError(vm, err)
- }
- remotes, err := manager.Remotes(rpath)
- if err != nil {
- return gitJSONError(vm, err)
- }
- return gitJSONValue(vm, remotes)
- })
- // git.addRemote(vpath, name, url) -> {success, error}
- vm.Set("_git_addremote", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- name, _ := call.Argument(1).ToString()
- url, _ := call.Argument(2).ToString()
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.AddRemote(rpath, name, url); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "remote "+name+" saved")
- })
- // git.removeRemote(vpath, name) -> {success, error}
- vm.Set("_git_removeremote", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- name, _ := call.Argument(1).ToString()
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.RemoveRemote(rpath, name); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "remote "+name+" removed")
- })
- // git.add(vpath, files) -> {success, error}
- vm.Set("_git_add", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- files := exportStringSlice(call.Argument(1))
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.Add(rpath, files); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "")
- })
- // git.addAll(vpath) -> {success, error}
- vm.Set("_git_addall", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.AddAll(rpath); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "")
- })
- // git.unstage(vpath, files) -> {success, error}
- vm.Set("_git_unstage", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- files := exportStringSlice(call.Argument(1))
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.Unstage(rpath, files); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "")
- })
- // git.discard(vpath, files) -> {success, error}
- vm.Set("_git_discard", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- files := exportStringSlice(call.Argument(1))
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.Discard(rpath, files); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "")
- })
- // git.ignore(vpath, patterns) -> {success, error, message}
- vm.Set("_git_ignore", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- patterns := exportStringSlice(call.Argument(1))
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- added, err := manager.AddIgnoreRules(rpath, patterns)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if len(added) == 0 {
- return gitOperationSuccess(vm, "already ignored")
- }
- return gitOperationSuccess(vm, "ignoring "+strings.Join(added, ", "))
- })
- // git.commit(vpath, message, files, options) -> {success, error, hash}
- vm.Set("_git_commit", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- message, _ := call.Argument(1).ToString()
- files := exportStringSlice(call.Argument(2))
- options := exportOptions(call.Argument(3))
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- authorName := optionString(options, "name")
- if authorName == "" {
- authorName = u.Username
- }
- hash, err := manager.Commit(rpath, &git.CommitRequest{
- Message: message,
- Files: files,
- Name: authorName,
- Email: optionString(options, "email"),
- All: optionBool(options, "all"),
- })
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitJSONValue(vm, git.OperationResult{
- Success: true,
- Hash: hash,
- Message: "commit created",
- })
- })
- // git.diff(vpath, file) -> file diff
- vm.Set("_git_diff", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- file, _ := call.Argument(1).ToString()
- rpath, err := resolveRepo(vpath, false)
- if err != nil {
- return gitJSONError(vm, err)
- }
- diff, err := manager.Diff(rpath, file)
- if err != nil {
- return gitJSONError(vm, err)
- }
- return gitJSONValue(vm, diff)
- })
- // git.diffCommit(vpath, hash, file) -> file diff
- vm.Set("_git_diffcommit", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- hash, _ := call.Argument(1).ToString()
- file, _ := call.Argument(2).ToString()
- rpath, err := resolveRepo(vpath, false)
- if err != nil {
- return gitJSONError(vm, err)
- }
- diff, err := manager.DiffCommit(rpath, hash, file)
- if err != nil {
- return gitJSONError(vm, err)
- }
- return gitJSONValue(vm, diff)
- })
- // git.fileBlob(vpath, file, revision) -> {success, exists, base64, mime, kind, size}
- // Reads a file's content at a revision so the front-end can preview the
- // "before" side of a change that only exists in the object database.
- vm.Set("_git_fileblob", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- file, _ := call.Argument(1).ToString()
- revision, _ := call.Argument(2).ToString()
- rpath, err := resolveRepo(vpath, false)
- if err != nil {
- return gitJSONError(vm, err)
- }
- content, exists, err := manager.FileBlob(rpath, file, revision)
- if err != nil {
- return gitJSONError(vm, err)
- }
- payload := map[string]interface{}{
- "success": true,
- "exists": exists,
- "mime": git.PreviewMimeType(file),
- "kind": git.PreviewKind(file),
- "size": len(content),
- }
- if exists {
- payload["base64"] = base64.StdEncoding.EncodeToString(content)
- }
- return gitJSONValue(vm, payload)
- })
- // git.commitFiles(vpath, hash) -> [file, ...]
- vm.Set("_git_commitfiles", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- hash, _ := call.Argument(1).ToString()
- rpath, err := resolveRepo(vpath, false)
- if err != nil {
- return gitJSONError(vm, err)
- }
- files, err := manager.CommitFiles(rpath, hash)
- if err != nil {
- return gitJSONError(vm, err)
- }
- return gitJSONValue(vm, files)
- })
- // git.deleteBranch(vpath, branch, force) -> {success, error, unmerged}
- vm.Set("_git_deletebranch", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- branch, _ := call.Argument(1).ToString()
- force, _ := call.Argument(2).ToBoolean()
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.DeleteBranch(rpath, branch, force); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "deleted branch "+branch)
- })
- // git.renameBranch(vpath, oldName, newName) -> {success, error}
- vm.Set("_git_renamebranch", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- oldName, _ := call.Argument(1).ToString()
- newName, _ := call.Argument(2).ToString()
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.RenameBranch(rpath, oldName, newName); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "renamed to "+newName)
- })
- // git.deleteRemoteBranch(vpath, remote, branch, options) -> {success, error, authRequired}
- vm.Set("_git_deleteremotebranch", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- remote, _ := call.Argument(1).ToString()
- branch, _ := call.Argument(2).ToString()
- options := exportOptions(call.Argument(3))
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- request, err := g.remoteBranchRequest(u, rpath, remote, options)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.DeleteRemoteBranch(rpath, remote, branch, request); err != nil {
- return gitOperationFailure(vm, err)
- }
- g.rememberGitCredentialForRemote(u, rpath, remote, options)
- return gitOperationSuccess(vm, "deleted "+remoteOrOrigin(remote)+"/"+branch)
- })
- // git.renameRemoteBranch(vpath, remote, oldName, newName, options) -> {success, error, authRequired}
- vm.Set("_git_renameremotebranch", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- remote, _ := call.Argument(1).ToString()
- oldName, _ := call.Argument(2).ToString()
- newName, _ := call.Argument(3).ToString()
- options := exportOptions(call.Argument(4))
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- request, err := g.remoteBranchRequest(u, rpath, remote, options)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.RenameRemoteBranch(rpath, remote, oldName, newName, request); err != nil {
- return gitOperationFailure(vm, err)
- }
- g.rememberGitCredentialForRemote(u, rpath, remote, options)
- return gitOperationSuccess(vm, "renamed to "+remoteOrOrigin(remote)+"/"+newName)
- })
- // git.checkoutCommit(vpath, hash) -> {success, error}
- vm.Set("_git_checkoutcommit", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- hash, _ := call.Argument(1).ToString()
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.CheckoutCommit(rpath, hash); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "checked out "+shortHash(hash))
- })
- // git.resetToCommit(vpath, hash, mode) -> {success, error}
- vm.Set("_git_resettocommit", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- hash, _ := call.Argument(1).ToString()
- mode, _ := call.Argument(2).ToString()
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.ResetToCommit(rpath, hash, mode); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "reset to "+shortHash(hash))
- })
- // git.createBranchAt(vpath, branch, hash) -> {success, error}
- vm.Set("_git_createbranchat", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- branch, _ := call.Argument(1).ToString()
- hash, _ := call.Argument(2).ToString()
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.CreateBranchAt(rpath, branch, hash); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "created branch "+branch)
- })
- // git.createTag(vpath, tag, hash, message) -> {success, error}
- vm.Set("_git_createtag", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- tag, _ := call.Argument(1).ToString()
- hash, _ := call.Argument(2).ToString()
- message, _ := call.Argument(3).ToString()
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := manager.CreateTag(rpath, tag, hash, message, u.Username, ""); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "created tag "+tag)
- })
- // git.revertCommit(vpath, hash, options) -> {success, error, hash}
- vm.Set("_git_revertcommit", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- hash, _ := call.Argument(1).ToString()
- options := exportOptions(call.Argument(2))
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- newHash, err := manager.RevertCommit(rpath, hash, commitRequestFromOptions(u, options))
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitJSONValue(vm, git.OperationResult{Success: true, Hash: newHash, Message: "reverted " + shortHash(hash)})
- })
- // git.cherryPickCommit(vpath, hash, options) -> {success, error, hash}
- vm.Set("_git_cherrypickcommit", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- hash, _ := call.Argument(1).ToString()
- options := exportOptions(call.Argument(2))
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- newHash, err := manager.CherryPickCommit(rpath, hash, commitRequestFromOptions(u, options))
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitJSONValue(vm, git.OperationResult{Success: true, Hash: newHash, Message: "cherry-picked " + shortHash(hash)})
- })
- // git.amendMessage(vpath, message, options) -> {success, error, hash}
- vm.Set("_git_amendmessage", func(call otto.FunctionCall) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- message, _ := call.Argument(1).ToString()
- options := exportOptions(call.Argument(2))
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- newHash, err := manager.AmendCommitMessage(rpath, message, commitRequestFromOptions(u, options))
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitJSONValue(vm, git.OperationResult{Success: true, Hash: newHash, Message: "amended commit message"})
- })
- // git.fetch(vpath, options) -> {success, error, authRequired}
- vm.Set("_git_fetch", func(call otto.FunctionCall) otto.Value {
- return g.runGitTransport(vm, u, call, resolveRepo, func(rpath string, request *git.TransportRequest) (string, error) {
- return "fetched", manager.Fetch(rpath, request)
- })
- })
- // git.pull(vpath, options) -> {success, error, authRequired}
- vm.Set("_git_pull", func(call otto.FunctionCall) otto.Value {
- return g.runGitTransport(vm, u, call, resolveRepo, func(rpath string, request *git.TransportRequest) (string, error) {
- return manager.Pull(rpath, request)
- })
- })
- // git.push(vpath, options) -> {success, error, authRequired}
- vm.Set("_git_push", func(call otto.FunctionCall) otto.Value {
- return g.runGitTransport(vm, u, call, resolveRepo, func(rpath string, request *git.TransportRequest) (string, error) {
- return manager.Push(rpath, request)
- })
- })
- // git.saveCredential(host, username, token) -> {success, error}
- vm.Set("_git_savecredential", func(call otto.FunctionCall) otto.Value {
- host, _ := call.Argument(0).ToString()
- remoteUser, _ := call.Argument(1).ToString()
- token, _ := call.Argument(2).ToString()
- store, err := g.gitCredentialStore()
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := store.Save(u.Username, host, remoteUser, token); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "credential saved")
- })
- // git.listCredentials() -> [{host, username}, ...]
- vm.Set("_git_listcredentials", func(call otto.FunctionCall) otto.Value {
- store, err := g.gitCredentialStore()
- if err != nil {
- return gitJSONError(vm, err)
- }
- return gitJSONValue(vm, store.List(u.Username))
- })
- // git.hasCredential(host) -> bool
- vm.Set("_git_hascredential", func(call otto.FunctionCall) otto.Value {
- host, _ := call.Argument(0).ToString()
- store, err := g.gitCredentialStore()
- if err != nil {
- return otto.FalseValue()
- }
- if _, found := store.Get(u.Username, host); found {
- return otto.TrueValue()
- }
- return otto.FalseValue()
- })
- // git.removeCredential(host) -> {success, error}
- vm.Set("_git_removecredential", func(call otto.FunctionCall) otto.Value {
- host, _ := call.Argument(0).ToString()
- store, err := g.gitCredentialStore()
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- if err := store.Remove(u.Username, host); err != nil {
- return gitOperationFailure(vm, err)
- }
- return gitOperationSuccess(vm, "credential removed")
- })
- // git.remoteHost(url) -> host name used as the credential key
- vm.Set("_git_remotehost", func(call otto.FunctionCall) otto.Value {
- url, _ := call.Argument(0).ToString()
- result, _ := vm.ToValue(git.RemoteHost(url))
- return result
- })
- //Wrap the native functions into a git object. Everything that can carry
- //structured data crosses the boundary as JSON so the script always receives
- //plain JavaScript objects.
- vm.Run(gitLibJavaScript)
- }
- // gitLibJavaScript is the in-VM wrapper that turns the native _git_* calls into
- // the `git` object AGI scripts use. Kept as a package constant so a unit test
- // can execute it and catch a syntax error before it reaches a user's script.
- const gitLibJavaScript = `
- var git = {};
- var _git_parse = function(raw) {
- if (raw === false || raw === undefined || raw === null) {
- return {error: "git call failed"};
- }
- try { return JSON.parse(raw); } catch(e) { return {error: "malformed git response"}; }
- };
- git.isRepo = _git_isrepo;
- git.repoRoot = _git_reporoot;
- git.remoteHost = _git_remotehost;
- git.hasCredential = _git_hascredential;
- git.init = function(vpath) { return _git_parse(_git_init(vpath)); };
- git.clone = function(url, vpath, options) { return _git_parse(_git_clone(url, vpath, options || {})); };
- git.status = function(vpath) { return _git_parse(_git_status(vpath)); };
- git.log = function(vpath, limit) { return _git_parse(_git_log(vpath, limit)); };
- git.branches = function(vpath) { return _git_parse(_git_branches(vpath)); };
- git.checkout = function(vpath, branch, create) { return _git_parse(_git_checkout(vpath, branch, create === true)); };
- git.remotes = function(vpath) { return _git_parse(_git_remotes(vpath)); };
- git.addRemote = function(vpath, name, url) { return _git_parse(_git_addremote(vpath, name, url)); };
- git.removeRemote = function(vpath, name) { return _git_parse(_git_removeremote(vpath, name)); };
- git.add = function(vpath, files) { return _git_parse(_git_add(vpath, files || [])); };
- git.addAll = function(vpath) { return _git_parse(_git_addall(vpath)); };
- git.unstage = function(vpath, files) { return _git_parse(_git_unstage(vpath, files || [])); };
- git.discard = function(vpath, files) { return _git_parse(_git_discard(vpath, files || [])); };
- git.commit = function(vpath, message, files, options) { return _git_parse(_git_commit(vpath, message, files || [], options || {})); };
- git.ignore = function(vpath, patterns) { return _git_parse(_git_ignore(vpath, patterns || [])); };
- git.diff = function(vpath, file) { return _git_parse(_git_diff(vpath, file)); };
- git.diffCommit = function(vpath, hash, file) { return _git_parse(_git_diffcommit(vpath, hash, file)); };
- git.commitFiles = function(vpath, hash) { return _git_parse(_git_commitfiles(vpath, hash)); };
- git.fileBlob = function(vpath, file, revision) { return _git_parse(_git_fileblob(vpath, file, revision || "HEAD")); };
- git.deleteBranch = function(vpath, branch, force) { return _git_parse(_git_deletebranch(vpath, branch, force === true)); };
- git.renameBranch = function(vpath, oldName, newName) { return _git_parse(_git_renamebranch(vpath, oldName, newName)); };
- git.deleteRemoteBranch = function(vpath, remote, branch, options) { return _git_parse(_git_deleteremotebranch(vpath, remote || "origin", branch, options || {})); };
- git.renameRemoteBranch = function(vpath, remote, oldName, newName, options) { return _git_parse(_git_renameremotebranch(vpath, remote || "origin", oldName, newName, options || {})); };
- git.checkoutCommit = function(vpath, hash) { return _git_parse(_git_checkoutcommit(vpath, hash)); };
- git.resetToCommit = function(vpath, hash, mode) { return _git_parse(_git_resettocommit(vpath, hash, mode || "mixed")); };
- git.createBranchAt = function(vpath, branch, hash) { return _git_parse(_git_createbranchat(vpath, branch, hash)); };
- git.createTag = function(vpath, tag, hash, message) { return _git_parse(_git_createtag(vpath, tag, hash, message || "")); };
- git.revertCommit = function(vpath, hash, options) { return _git_parse(_git_revertcommit(vpath, hash, options || {})); };
- git.cherryPickCommit = function(vpath, hash, options) { return _git_parse(_git_cherrypickcommit(vpath, hash, options || {})); };
- git.amendMessage = function(vpath, message, options) { return _git_parse(_git_amendmessage(vpath, message, options || {})); };
- git.fetch = function(vpath, options) { return _git_parse(_git_fetch(vpath, options || {})); };
- git.pull = function(vpath, options) { return _git_parse(_git_pull(vpath, options || {})); };
- git.push = function(vpath, options) { return _git_parse(_git_push(vpath, options || {})); };
- git.saveCredential = function(host, username, token) { return _git_parse(_git_savecredential(host, username, token)); };
- git.listCredentials = function() { return _git_parse(_git_listcredentials()); };
- git.removeCredential = function(host) { return _git_parse(_git_removecredential(host)); };
- `
- // gitTransportRunner is the shape of the fetch / pull / push bodies, letting the
- // three of them share credential resolution and error reporting.
- type gitTransportRunner func(rpath string, request *git.TransportRequest) (string, error)
- // runGitTransport wires a transport call: resolve the path, work out which
- // credential to use, run the operation and translate the outcome.
- func (g *Gateway) runGitTransport(vm *otto.Otto, u *user.User, call otto.FunctionCall,
- resolveRepo func(string, bool) (string, error), run gitTransportRunner) otto.Value {
- vpath, _ := call.Argument(0).ToString()
- options := exportOptions(call.Argument(1))
- //Fetching only rewrites remote-tracking refs inside .git, but that is still
- //a write to the repository folder.
- rpath, err := resolveRepo(vpath, true)
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- remote := optionString(options, "remote")
- remoteURL, urlErr := g.Option.GitManager.RemoteURLForName(rpath, remote)
- if urlErr != nil {
- return gitOperationFailure(vm, urlErr)
- }
- username, token := g.resolveGitCredential(u, remoteURL, options)
- message, err := run(rpath, &git.TransportRequest{
- Remote: remote,
- Branch: optionString(options, "branch"),
- Username: username,
- Token: token,
- Force: optionBool(options, "force"),
- SetUpstream: optionBool(options, "setUpstream"),
- })
- if err != nil {
- return gitOperationFailure(vm, err)
- }
- g.rememberGitCredential(u, remoteURL, options)
- return gitOperationSuccess(vm, message)
- }
- /*
- remoteBranchRequest builds the TransportRequest for a remote branch operation,
- resolving which credential applies from the remote's own URL.
- The remote-branch calls take the remote by name rather than by URL, so the URL
- has to be looked up before the stored credential can be found.
- */
- func (g *Gateway) remoteBranchRequest(u *user.User, rpath string, remote string, options map[string]interface{}) (*git.TransportRequest, error) {
- remoteURL, err := g.Option.GitManager.RemoteURLForName(rpath, remote)
- if err != nil {
- return nil, err
- }
- username, token := g.resolveGitCredential(u, remoteURL, options)
- return &git.TransportRequest{
- Remote: remote,
- Username: username,
- Token: token,
- }, nil
- }
- // rememberGitCredentialForRemote stores a credential that has just proven itself
- // against a remote addressed by name.
- func (g *Gateway) rememberGitCredentialForRemote(u *user.User, rpath string, remote string, options map[string]interface{}) {
- if !optionBool(options, "remember") {
- return
- }
- remoteURL, err := g.Option.GitManager.RemoteURLForName(rpath, remote)
- if err != nil {
- return
- }
- g.rememberGitCredential(u, remoteURL, options)
- }
- // remoteOrOrigin mirrors the manager's default so status messages name the same
- // remote the operation actually used.
- func remoteOrOrigin(remote string) string {
- remote = strings.TrimSpace(remote)
- if remote == "" {
- return "origin"
- }
- return remote
- }
- // commitRequestFromOptions builds a CommitRequest for the history actions,
- // defaulting the author name to the calling user so a commit always has an
- // identity even when the front-end sends none.
- func commitRequestFromOptions(u *user.User, options map[string]interface{}) *git.CommitRequest {
- name := optionString(options, "name")
- if name == "" && u != nil {
- name = u.Username
- }
- return &git.CommitRequest{
- Message: optionString(options, "message"),
- Name: name,
- Email: optionString(options, "email"),
- }
- }
- // shortHash abbreviates a commit hash for status messages, tolerating a hash
- // that is already short.
- func shortHash(hash string) string {
- hash = strings.TrimSpace(hash)
- if len(hash) > 7 {
- return hash[:7]
- }
- return hash
- }
- // gitCredentialStore returns the credential store or a readable error when git
- // support was started without a database.
- func (g *Gateway) gitCredentialStore() (*git.CredentialStore, error) {
- if g.Option.GitManager == nil {
- return nil, errors.New("git support is not enabled on this system")
- }
- store := g.Option.GitManager.Credentials()
- if store == nil {
- return nil, errors.New("git credential storage is not available")
- }
- return store, nil
- }
- // resolveGitCredential decides which username / token a transport call should
- // use: the ones passed in the options win, otherwise the user's stored
- // credential for that host is used, otherwise the call proceeds anonymously.
- func (g *Gateway) resolveGitCredential(u *user.User, remoteURL string, options map[string]interface{}) (string, string) {
- username := optionString(options, "username")
- token := optionString(options, "token")
- if token != "" {
- return username, token
- }
- store, err := g.gitCredentialStore()
- if err != nil {
- return username, token
- }
- credential, found := store.ResolveForRemote(u.Username, remoteURL)
- if !found {
- return username, token
- }
- if username == "" {
- username = credential.Username
- }
- return username, credential.Token
- }
- // rememberGitCredential persists the credential that was just proven to work,
- // but only when the caller explicitly asked for it (options.remember).
- func (g *Gateway) rememberGitCredential(u *user.User, remoteURL string, options map[string]interface{}) {
- if !optionBool(options, "remember") {
- return
- }
- token := optionString(options, "token")
- if token == "" {
- return
- }
- store, err := g.gitCredentialStore()
- if err != nil {
- return
- }
- host := git.RemoteHost(remoteURL)
- if host == "" {
- return
- }
- if err := store.Save(u.Username, host, optionString(options, "username"), token); err != nil {
- logger.PrintAndLog("Agi", "[AGI] Unable to store git credential for "+host, err)
- }
- }
- /*
- repoRootVirtualPath maps a working tree root back onto the caller's virtual path.
- It works out how many folder levels the requested path sits below the repository
- root on the real file system, then trims that many trailing segments off the
- virtual path. Doing it this way avoids a real-to-virtual conversion entirely,
- which matters because RepoRoot answers with an absolute OS path while the file
- system abstraction removes its storage root by prefix match against a relative
- one — the mismatch used to leave the absolute path embedded in the result.
- */
- func repoRootVirtualPath(vpath string, rpath string, realRoot string) (string, error) {
- absPath, err := filepath.Abs(rpath)
- if err != nil {
- return "", err
- }
- absRoot, err := filepath.Abs(realRoot)
- if err != nil {
- return "", err
- }
- relative, err := filepath.Rel(absRoot, absPath)
- if err != nil {
- return "", err
- }
- relative = filepath.ToSlash(relative)
- if relative == "." || relative == "" {
- //The requested path is the working tree root itself
- return vpath, nil
- }
- if relative == ".." || strings.HasPrefix(relative, "../") {
- return "", errors.New("repository root is not an ancestor of " + vpath)
- }
- return trimVirtualPathSegments(vpath, len(strings.Split(relative, "/")))
- }
- /*
- trimVirtualPathSegments removes count trailing segments from a virtual path.
- Virtual paths are handled as plain strings rather than through path/filepath:
- on Windows filepath.Clean treats the "uuid:" prefix as a volume name and
- mangles it.
- */
- func trimVirtualPathSegments(vpath string, count int) (string, error) {
- separator := strings.Index(vpath, ":/")
- if separator < 0 {
- return "", errors.New("not a virtual path: " + vpath)
- }
- root := vpath[:separator+2]
- body := strings.Trim(vpath[separator+2:], "/")
- segments := []string{}
- if body != "" {
- segments = strings.Split(body, "/")
- }
- if count > len(segments) {
- return "", errors.New("repository root lies outside " + vpath)
- }
- return root + strings.Join(segments[:len(segments)-count], "/"), nil
- }
- // checkFshSupportsGit rejects storage pools that cannot host a working tree.
- // Network-backed pools only expose streaming reads and writes, while git needs
- // ordinary random-access files.
- func checkFshSupportsGit(fsh *filesystem.FileSystemHandler) error {
- if fsh == nil {
- return errors.New("unable to resolve the storage pool for this path")
- }
- if fsh.RequireBuffer {
- return errors.New("git repositories must live on a local storage pool, not on " + fsh.Name)
- }
- if fsh.ReadOnly {
- return errors.New("storage pool " + fsh.Name + " is read only")
- }
- return nil
- }
- // gitJSONValue marshals a value and hands it to the VM as a JSON string.
- func gitJSONValue(vm *otto.Otto, value interface{}) otto.Value {
- encoded, err := json.Marshal(value)
- if err != nil {
- return gitJSONError(vm, err)
- }
- result, err := vm.ToValue(string(encoded))
- if err != nil {
- return otto.FalseValue()
- }
- return result
- }
- // gitJSONError returns an {error: "..."} object for the read-only calls, which
- // return their payload directly rather than a success envelope.
- func gitJSONError(vm *otto.Otto, err error) otto.Value {
- payload := map[string]interface{}{"error": err.Error()}
- if errors.Is(err, git.ErrAuthRequired) {
- payload["authRequired"] = true
- }
- encoded, _ := json.Marshal(payload)
- result, merr := vm.ToValue(string(encoded))
- if merr != nil {
- return otto.FalseValue()
- }
- return result
- }
- // gitOperationSuccess builds the standard success envelope.
- func gitOperationSuccess(vm *otto.Otto, message string) otto.Value {
- return gitJSONValue(vm, git.OperationResult{
- Success: true,
- Message: message,
- })
- }
- // gitOperationFailure builds the standard failure envelope, flagging credential
- // problems so the front-end knows to open its sign-in dialog, and an unmerged
- // branch so it can offer to delete anyway.
- func gitOperationFailure(vm *otto.Otto, err error) otto.Value {
- return gitJSONValue(vm, git.OperationResult{
- Success: false,
- Error: err.Error(),
- AuthRequired: errors.Is(err, git.ErrAuthRequired),
- Unmerged: errors.Is(err, git.ErrUnmergedBranch),
- })
- }
- // exportOptions converts a JavaScript options object into a Go map. A missing
- // or non-object argument yields an empty map so callers never need nil checks.
- func exportOptions(value otto.Value) map[string]interface{} {
- if !value.IsObject() {
- return map[string]interface{}{}
- }
- exported, err := value.Export()
- if err != nil {
- return map[string]interface{}{}
- }
- options, ok := exported.(map[string]interface{})
- if !ok {
- return map[string]interface{}{}
- }
- return options
- }
- // exportStringSlice converts a JavaScript array (or a single string) into a
- // string slice.
- func exportStringSlice(value otto.Value) []string {
- results := []string{}
- if value.IsUndefined() || value.IsNull() {
- return results
- }
- if value.IsString() {
- single, err := value.ToString()
- if err == nil && single != "" {
- results = append(results, single)
- }
- return results
- }
- exported, err := value.Export()
- if err != nil {
- return results
- }
- switch typed := exported.(type) {
- case []string:
- return typed
- case []interface{}:
- for _, item := range typed {
- if text, ok := item.(string); ok && text != "" {
- results = append(results, text)
- }
- }
- }
- return results
- }
- // optionString reads a string field from an options map.
- func optionString(options map[string]interface{}, key string) string {
- value, ok := options[key]
- if !ok || value == nil {
- return ""
- }
- if text, ok := value.(string); ok {
- return strings.TrimSpace(text)
- }
- return ""
- }
- // optionBool reads a boolean field from an options map, accepting the string
- // "true" as well so form values work unchanged.
- func optionBool(options map[string]interface{}, key string) bool {
- value, ok := options[key]
- if !ok || value == nil {
- return false
- }
- switch typed := value.(type) {
- case bool:
- return typed
- case string:
- return strings.EqualFold(strings.TrimSpace(typed), "true")
- }
- return false
- }
- // optionInt reads a numeric field from an options map. Otto hands numbers back
- // as float64, and form values arrive as strings.
- func optionInt(options map[string]interface{}, key string) int {
- value, ok := options[key]
- if !ok || value == nil {
- return 0
- }
- switch typed := value.(type) {
- case int:
- return typed
- case int64:
- return int(typed)
- case float64:
- return int(typed)
- case string:
- parsed := 0
- if _, err := fmt.Sscanf(strings.TrimSpace(typed), "%d", &parsed); err != nil {
- return 0
- }
- return parsed
- }
- return 0
- }
|