package github import ( "context" "encoding/base64" "fmt" "io" "net/http" stdpath "path" "strings" "sync" "text/template" "github.com/ProtonMail/go-crypto/openpgp" "github.com/alist-org/alist/v3/drivers/base" "github.com/alist-org/alist/v3/internal/driver" "github.com/alist-org/alist/v3/internal/errs" "github.com/alist-org/alist/v3/internal/model" "github.com/alist-org/alist/v3/pkg/utils" "github.com/go-resty/resty/v2" "github.com/pkg/errors" log "github.com/sirupsen/logrus" ) type Github struct { model.Storage Addition client *resty.Client mkdirMsgTmpl *template.Template deleteMsgTmpl *template.Template putMsgTmpl *template.Template renameMsgTmpl *template.Template copyMsgTmpl *template.Template moveMsgTmpl *template.Template isOnBranch bool commitMutex sync.Mutex pgpEntity *openpgp.Entity } func (d *Github) Config() driver.Config { return config } func (d *Github) GetAddition() driver.Additional { return &d.Addition } func (d *Github) Init(ctx context.Context) error { d.RootFolderPath = utils.FixAndCleanPath(d.RootFolderPath) if d.CommitterName != "" && d.CommitterEmail == "" { return errors.New("committer email is required") } if d.CommitterName == "" && d.CommitterEmail != "" { return errors.New("committer name is required") } if d.AuthorName != "" && d.AuthorEmail == "" { return errors.New("author email is required") } if d.AuthorName == "" && d.AuthorEmail != "" { return errors.New("author name is required") } var err error d.mkdirMsgTmpl, err = template.New("mkdirCommitMsgTemplate").Parse(d.MkdirCommitMsg) if err != nil { return err } d.deleteMsgTmpl, err = template.New("deleteCommitMsgTemplate").Parse(d.DeleteCommitMsg) if err != nil { return err } d.putMsgTmpl, err = template.New("putCommitMsgTemplate").Parse(d.PutCommitMsg) if err != nil { return err } d.renameMsgTmpl, err = template.New("renameCommitMsgTemplate").Parse(d.RenameCommitMsg) if err != nil { return err } d.copyMsgTmpl, err = template.New("copyCommitMsgTemplate").Parse(d.CopyCommitMsg) if err != nil { return err } d.moveMsgTmpl, err = template.New("moveCommitMsgTemplate").Parse(d.MoveCommitMsg) if err != nil { return err } d.client = base.NewRestyClient(). SetHeader("Accept", "application/vnd.github.object+json"). SetHeader("X-GitHub-Api-Version", "2022-11-28"). SetLogger(log.StandardLogger()). SetDebug(false) token := strings.TrimSpace(d.Token) if token != "" { d.client = d.client.SetHeader("Authorization", "Bearer "+token) } if d.Ref == "" { repo, err := d.getRepo() if err != nil { return err } d.Ref = repo.DefaultBranch d.isOnBranch = true } else { _, err = d.getBranchHead() d.isOnBranch = err == nil } if d.GPGPrivateKey != "" { if d.CommitterName == "" || d.AuthorName == "" { user, e := d.getAuthenticatedUser() if e != nil { return e } if d.CommitterName == "" { d.CommitterName = user.Name d.CommitterEmail = user.Email } if d.AuthorName == "" { d.AuthorName = user.Name d.AuthorEmail = user.Email } } d.pgpEntity, err = loadPrivateKey(d.GPGPrivateKey, d.GPGKeyPassphrase) if err != nil { return err } } return nil } func (d *Github) Drop(ctx context.Context) error { return nil } func (d *Github) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { obj, err := d.get(dir.GetPath()) if err != nil { return nil, err } if obj.Entries == nil { return nil, errs.NotFolder } if len(obj.Entries) >= 1000 { tree, err := d.getTree(obj.Sha) if err != nil { return nil, err } if tree.Truncated { return nil, fmt.Errorf("tree %s is truncated", dir.GetPath()) } ret := make([]model.Obj, 0, len(tree.Trees)) for _, t := range tree.Trees { if t.Path != ".gitkeep" { ret = append(ret, t.toModelObj()) } } return ret, nil } else { ret := make([]model.Obj, 0, len(obj.Entries)) for _, entry := range obj.Entries { if entry.Name != ".gitkeep" { ret = append(ret, entry.toModelObj()) } } return ret, nil } } func (d *Github) Link(ctx context.Context, file model.Obj, args model.LinkArgs) (*model.Link, error) { obj, err := d.get(file.GetPath()) if err != nil { return nil, err } if obj.Type == "submodule" { return nil, errors.New("cannot download a submodule") } url := obj.DownloadURL ghProxy := strings.TrimSpace(d.Addition.GitHubProxy) if ghProxy != "" { url = strings.Replace(url, "https://raw.githubusercontent.com", ghProxy, 1) } return &model.Link{ URL: url, }, nil } func (d *Github) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { if !d.isOnBranch { return errors.New("cannot write to non-branch reference") } d.commitMutex.Lock() defer d.commitMutex.Unlock() parent, err := d.get(parentDir.GetPath()) if err != nil { return err } if parent.Entries == nil { return errs.NotFolder } subDirSha, err := d.newTree("", []interface{}{ map[string]string{ "path": ".gitkeep", "mode": "100644", "type": "blob", "content": "", }, }) if err != nil { return err } newTree := make([]interface{}, 0, 2) newTree = append(newTree, TreeObjReq{ Path: dirName, Mode: "040000", Type: "tree", Sha: subDirSha, }) if len(parent.Entries) == 1 && parent.Entries[0].Name == ".gitkeep" { newTree = append(newTree, TreeObjReq{ Path: ".gitkeep", Mode: "100644", Type: "blob", Sha: nil, }) } newSha, err := d.newTree(parent.Sha, newTree) if err != nil { return err } rootSha, err := d.renewParentTrees(parentDir.GetPath(), parent.Sha, newSha, "/") if err != nil { return err } commitMessage, err := getMessage(d.mkdirMsgTmpl, &MessageTemplateVars{ UserName: getUsername(ctx), ObjName: dirName, ObjPath: stdpath.Join(parentDir.GetPath(), dirName), ParentName: parentDir.GetName(), ParentPath: parentDir.GetPath(), }, "mkdir") if err != nil { return err } return d.commit(commitMessage, rootSha) } func (d *Github) Move(ctx context.Context, srcObj, dstDir model.Obj) error { if !d.isOnBranch { return errors.New("cannot write to non-branch reference") } if strings.HasPrefix(dstDir.GetPath(), srcObj.GetPath()) { return errors.New("cannot move parent dir to child") } d.commitMutex.Lock() defer d.commitMutex.Unlock() var rootSha string if strings.HasPrefix(dstDir.GetPath(), stdpath.Dir(srcObj.GetPath())) { // /aa/1 -> /aa/bb/ dstOldSha, dstNewSha, ancestorOldSha, srcParentTree, err := d.copyWithoutRenewTree(srcObj, dstDir) if err != nil { return err } srcParentPath := stdpath.Dir(srcObj.GetPath()) dstRest := dstDir.GetPath()[len(srcParentPath):] if dstRest[0] == '/' { dstRest = dstRest[1:] } dstNextName, _, _ := strings.Cut(dstRest, "/") dstNextPath := stdpath.Join(srcParentPath, dstNextName) dstNextTreeSha, err := d.renewParentTrees(dstDir.GetPath(), dstOldSha, dstNewSha, dstNextPath) if err != nil { return err } var delSrc, dstNextTree *TreeObjReq = nil, nil for _, t := range srcParentTree.Trees { if t.Path == dstNextName { dstNextTree = &t.TreeObjReq dstNextTree.Sha = dstNextTreeSha } if t.Path == srcObj.GetName() { delSrc = &t.TreeObjReq delSrc.Sha = nil } if delSrc != nil && dstNextTree != nil { break } } if delSrc == nil || dstNextTree == nil { return errs.ObjectNotFound } ancestorNewSha, err := d.newTree(ancestorOldSha, []interface{}{*delSrc, *dstNextTree}) if err != nil { return err } rootSha, err = d.renewParentTrees(srcParentPath, ancestorOldSha, ancestorNewSha, "/") if err != nil { return err } } else if strings.HasPrefix(srcObj.GetPath(), dstDir.GetPath()) { // /aa/bb/1 -> /aa/ srcParentPath := stdpath.Dir(srcObj.GetPath()) srcParentTree, srcParentOldSha, err := d.getTreeDirectly(srcParentPath) if err != nil { return err } var src *TreeObjReq = nil for _, t := range srcParentTree.Trees { if t.Path == srcObj.GetName() { if t.Type == "commit" { return errors.New("cannot move a submodule") } src = &t.TreeObjReq break } } if src == nil { return errs.ObjectNotFound } delSrc := *src delSrc.Sha = nil delSrcTree := make([]interface{}, 0, 2) delSrcTree = append(delSrcTree, delSrc) if len(srcParentTree.Trees) == 1 { delSrcTree = append(delSrcTree, map[string]string{ "path": ".gitkeep", "mode": "100644", "type": "blob", "content": "", }) } srcParentNewSha, err := d.newTree(srcParentOldSha, delSrcTree) if err != nil { return err } srcRest := srcObj.GetPath()[len(dstDir.GetPath()):] if srcRest[0] == '/' { srcRest = srcRest[1:] } srcNextName, _, ok := strings.Cut(srcRest, "/") if !ok { // /aa/1 -> /aa/ return errors.New("cannot move in place") } srcNextPath := stdpath.Join(dstDir.GetPath(), srcNextName) srcNextTreeSha, err := d.renewParentTrees(srcParentPath, srcParentOldSha, srcParentNewSha, srcNextPath) if err != nil { return err } ancestorTree, ancestorOldSha, err := d.getTreeDirectly(dstDir.GetPath()) if err != nil { return err } var srcNextTree *TreeObjReq = nil for _, t := range ancestorTree.Trees { if t.Path == srcNextName { srcNextTree = &t.TreeObjReq srcNextTree.Sha = srcNextTreeSha break } } if srcNextTree == nil { return errs.ObjectNotFound } ancestorNewSha, err := d.newTree(ancestorOldSha, []interface{}{*srcNextTree, *src}) if err != nil { return err } rootSha, err = d.renewParentTrees(dstDir.GetPath(), ancestorOldSha, ancestorNewSha, "/") if err != nil { return err } } else { // /aa/1 -> /bb/ // do copy dstOldSha, dstNewSha, srcParentOldSha, srcParentTree, err := d.copyWithoutRenewTree(srcObj, dstDir) if err != nil { return err } // delete src object and create new tree var srcNewTree *TreeObjReq = nil for _, t := range srcParentTree.Trees { if t.Path == srcObj.GetName() { srcNewTree = &t.TreeObjReq srcNewTree.Sha = nil break } } if srcNewTree == nil { return errs.ObjectNotFound } delSrcTree := make([]interface{}, 0, 2) delSrcTree = append(delSrcTree, *srcNewTree) if len(srcParentTree.Trees) == 1 { delSrcTree = append(delSrcTree, map[string]string{ "path": ".gitkeep", "mode": "100644", "type": "blob", "content": "", }) } srcParentNewSha, err := d.newTree(srcParentOldSha, delSrcTree) if err != nil { return err } // renew but the common ancestor of srcPath and dstPath ancestor, srcChildName, dstChildName, _, _ := getPathCommonAncestor(srcObj.GetPath(), dstDir.GetPath()) dstNextTreeSha, err := d.renewParentTrees(dstDir.GetPath(), dstOldSha, dstNewSha, stdpath.Join(ancestor, dstChildName)) if err != nil { return err } srcNextTreeSha, err := d.renewParentTrees(stdpath.Dir(srcObj.GetPath()), srcParentOldSha, srcParentNewSha, stdpath.Join(ancestor, srcChildName)) if err != nil { return err } // renew the tree of the last common ancestor ancestorTree, ancestorOldSha, err := d.getTreeDirectly(ancestor) if err != nil { return err } newTree := make([]interface{}, 2) srcBind := false dstBind := false for _, t := range ancestorTree.Trees { if t.Path == srcChildName { t.Sha = srcNextTreeSha newTree[0] = t.TreeObjReq srcBind = true } if t.Path == dstChildName { t.Sha = dstNextTreeSha newTree[1] = t.TreeObjReq dstBind = true } if srcBind && dstBind { break } } if !srcBind || !dstBind { return errs.ObjectNotFound } ancestorNewSha, err := d.newTree(ancestorOldSha, newTree) if err != nil { return err } // renew until root rootSha, err = d.renewParentTrees(ancestor, ancestorOldSha, ancestorNewSha, "/") if err != nil { return err } } // commit message, err := getMessage(d.moveMsgTmpl, &MessageTemplateVars{ UserName: getUsername(ctx), ObjName: srcObj.GetName(), ObjPath: srcObj.GetPath(), ParentName: stdpath.Base(stdpath.Dir(srcObj.GetPath())), ParentPath: stdpath.Dir(srcObj.GetPath()), TargetName: stdpath.Base(dstDir.GetPath()), TargetPath: dstDir.GetPath(), }, "move") if err != nil { return err } return d.commit(message, rootSha) } func (d *Github) Rename(ctx context.Context, srcObj model.Obj, newName string) error { if !d.isOnBranch { return errors.New("cannot write to non-branch reference") } d.commitMutex.Lock() defer d.commitMutex.Unlock() parentDir := stdpath.Dir(srcObj.GetPath()) tree, _, err := d.getTreeDirectly(parentDir) if err != nil { return err } newTree := make([]interface{}, 2) operated := false for _, t := range tree.Trees { if t.Path == srcObj.GetName() { if t.Type == "commit" { return errors.New("cannot rename a submodule") } delCopy := t.TreeObjReq delCopy.Sha = nil newTree[0] = delCopy t.Path = newName newTree[1] = t.TreeObjReq operated = true break } } if !operated { return errs.ObjectNotFound } newSha, err := d.newTree(tree.Sha, newTree) if err != nil { return err } rootSha, err := d.renewParentTrees(parentDir, tree.Sha, newSha, "/") if err != nil { return err } message, err := getMessage(d.renameMsgTmpl, &MessageTemplateVars{ UserName: getUsername(ctx), ObjName: srcObj.GetName(), ObjPath: srcObj.GetPath(), ParentName: stdpath.Base(parentDir), ParentPath: parentDir, TargetName: newName, TargetPath: stdpath.Join(parentDir, newName), }, "rename") if err != nil { return err } return d.commit(message, rootSha) } func (d *Github) Copy(ctx context.Context, srcObj, dstDir model.Obj) error { if !d.isOnBranch { return errors.New("cannot write to non-branch reference") } if strings.HasPrefix(dstDir.GetPath(), srcObj.GetPath()) { return errors.New("cannot copy parent dir to child") } d.commitMutex.Lock() defer d.commitMutex.Unlock() dstSha, newSha, _, _, err := d.copyWithoutRenewTree(srcObj, dstDir) if err != nil { return err } rootSha, err := d.renewParentTrees(dstDir.GetPath(), dstSha, newSha, "/") if err != nil { return err } message, err := getMessage(d.copyMsgTmpl, &MessageTemplateVars{ UserName: getUsername(ctx), ObjName: srcObj.GetName(), ObjPath: srcObj.GetPath(), ParentName: stdpath.Base(stdpath.Dir(srcObj.GetPath())), ParentPath: stdpath.Dir(srcObj.GetPath()), TargetName: stdpath.Base(dstDir.GetPath()), TargetPath: dstDir.GetPath(), }, "copy") if err != nil { return err } return d.commit(message, rootSha) } func (d *Github) Remove(ctx context.Context, obj model.Obj) error { if !d.isOnBranch { return errors.New("cannot write to non-branch reference") } d.commitMutex.Lock() defer d.commitMutex.Unlock() parentDir := stdpath.Dir(obj.GetPath()) tree, treeSha, err := d.getTreeDirectly(parentDir) if err != nil { return err } var del *TreeObjReq = nil for _, t := range tree.Trees { if t.Path == obj.GetName() { if t.Type == "commit" { return errors.New("cannot remove a submodule") } del = &t.TreeObjReq del.Sha = nil break } } if del == nil { return errs.ObjectNotFound } newTree := make([]interface{}, 0, 2) newTree = append(newTree, *del) if len(tree.Trees) == 1 { // completely emptying the repository will get a 404 newTree = append(newTree, map[string]string{ "path": ".gitkeep", "mode": "100644", "type": "blob", "content": "", }) } newSha, err := d.newTree(treeSha, newTree) if err != nil { return err } rootSha, err := d.renewParentTrees(parentDir, treeSha, newSha, "/") if err != nil { return err } commitMessage, err := getMessage(d.deleteMsgTmpl, &MessageTemplateVars{ UserName: getUsername(ctx), ObjName: obj.GetName(), ObjPath: obj.GetPath(), ParentName: stdpath.Base(parentDir), ParentPath: parentDir, }, "remove") if err != nil { return err } return d.commit(commitMessage, rootSha) } func (d *Github) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { if !d.isOnBranch { return errors.New("cannot write to non-branch reference") } blob, err := d.putBlob(ctx, stream, up) if err != nil { return err } d.commitMutex.Lock() defer d.commitMutex.Unlock() parent, err := d.get(dstDir.GetPath()) if err != nil { return err } if parent.Entries == nil { return errs.NotFolder } newTree := make([]interface{}, 0, 2) newTree = append(newTree, TreeObjReq{ Path: stream.GetName(), Mode: "100644", Type: "blob", Sha: blob, }) if len(parent.Entries) == 1 && parent.Entries[0].Name == ".gitkeep" { newTree = append(newTree, TreeObjReq{ Path: ".gitkeep", Mode: "100644", Type: "blob", Sha: nil, }) } newSha, err := d.newTree(parent.Sha, newTree) if err != nil { return err } rootSha, err := d.renewParentTrees(dstDir.GetPath(), parent.Sha, newSha, "/") if err != nil { return err } commitMessage, err := getMessage(d.putMsgTmpl, &MessageTemplateVars{ UserName: getUsername(ctx), ObjName: stream.GetName(), ObjPath: stdpath.Join(dstDir.GetPath(), stream.GetName()), ParentName: dstDir.GetName(), ParentPath: dstDir.GetPath(), }, "upload") if err != nil { return err } return d.commit(commitMessage, rootSha) } var _ driver.Driver = (*Github)(nil) func (d *Github) getContentApiUrl(path string) string { path = utils.FixAndCleanPath(path) return fmt.Sprintf("https://api.github.com/repos/%s/%s/contents%s", d.Owner, d.Repo, path) } func (d *Github) get(path string) (*Object, error) { res, err := d.client.R().SetQueryParam("ref", d.Ref).Get(d.getContentApiUrl(path)) if err != nil { return nil, err } if res.StatusCode() != 200 { return nil, toErr(res) } var resp Object err = utils.Json.Unmarshal(res.Body(), &resp) return &resp, err } func (d *Github) putBlob(ctx context.Context, s model.FileStreamer, up driver.UpdateProgress) (string, error) { beforeContent := "{\"encoding\":\"base64\",\"content\":\"" afterContent := "\"}" length := int64(len(beforeContent)) + calculateBase64Length(s.GetSize()) + int64(len(afterContent)) beforeContentReader := strings.NewReader(beforeContent) contentReader, contentWriter := io.Pipe() go func() { encoder := base64.NewEncoder(base64.StdEncoding, contentWriter) if _, err := utils.CopyWithBuffer(encoder, s); err != nil { _ = contentWriter.CloseWithError(err) return } _ = encoder.Close() _ = contentWriter.Close() }() afterContentReader := strings.NewReader(afterContent) req, err := http.NewRequestWithContext(ctx, http.MethodPost, fmt.Sprintf("https://api.github.com/repos/%s/%s/git/blobs", d.Owner, d.Repo), driver.NewLimitedUploadStream(ctx, &driver.ReaderUpdatingProgress{ Reader: &driver.SimpleReaderWithSize{ Reader: io.MultiReader(beforeContentReader, contentReader, afterContentReader), Size: length, }, UpdateProgress: up, })) if err != nil { return "", err } req.Header.Set("Accept", "application/vnd.github+json") req.Header.Set("X-GitHub-Api-Version", "2022-11-28") token := strings.TrimSpace(d.Token) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } req.ContentLength = length res, err := base.HttpClient.Do(req) if err != nil { return "", err } defer res.Body.Close() resBody, err := io.ReadAll(res.Body) if err != nil { return "", err } if res.StatusCode != 201 { var errMsg ErrResp if err = utils.Json.Unmarshal(resBody, &errMsg); err != nil { return "", errors.New(res.Status) } else { return "", fmt.Errorf("%s: %s", res.Status, errMsg.Message) } } var resp PutBlobResp if err = utils.Json.Unmarshal(resBody, &resp); err != nil { return "", err } return resp.Sha, nil } func (d *Github) renewParentTrees(path, prevSha, curSha, until string) (string, error) { for path != until { path = stdpath.Dir(path) tree, sha, err := d.getTreeDirectly(path) if err != nil { return "", err } var newTree *TreeObjReq = nil for _, t := range tree.Trees { if t.Sha == prevSha { newTree = &t.TreeObjReq newTree.Sha = curSha break } } if newTree == nil { return "", errs.ObjectNotFound } curSha, err = d.newTree(sha, []interface{}{*newTree}) if err != nil { return "", err } prevSha = sha } return curSha, nil } func (d *Github) getTree(sha string) (*TreeResp, error) { res, err := d.client.R().Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/git/trees/%s", d.Owner, d.Repo, sha)) if err != nil { return nil, err } if res.StatusCode() != 200 { return nil, toErr(res) } var resp TreeResp if err = utils.Json.Unmarshal(res.Body(), &resp); err != nil { return nil, err } return &resp, nil } func (d *Github) getTreeDirectly(path string) (*TreeResp, string, error) { p, err := d.get(path) if err != nil { return nil, "", err } if p.Entries == nil { return nil, "", fmt.Errorf("%s is not a folder", path) } tree, err := d.getTree(p.Sha) if err != nil { return nil, "", err } if tree.Truncated { return nil, "", fmt.Errorf("tree %s is truncated", path) } return tree, p.Sha, nil } func (d *Github) newTree(baseSha string, tree []interface{}) (string, error) { body := &TreeReq{Trees: tree} if baseSha != "" { body.BaseTree = baseSha } res, err := d.client.R().SetBody(body). Post(fmt.Sprintf("https://api.github.com/repos/%s/%s/git/trees", d.Owner, d.Repo)) if err != nil { return "", err } if res.StatusCode() != 201 { return "", toErr(res) } var resp TreeResp if err = utils.Json.Unmarshal(res.Body(), &resp); err != nil { return "", err } return resp.Sha, nil } func (d *Github) commit(message, treeSha string) error { oldCommit, err := d.getBranchHead() body := map[string]interface{}{ "message": message, "tree": treeSha, "parents": []string{oldCommit}, } d.addCommitterAndAuthor(&body) if d.pgpEntity != nil { signature, e := signCommit(&body, d.pgpEntity) if e != nil { return e } body["signature"] = signature } res, err := d.client.R().SetBody(body).Post(fmt.Sprintf("https://api.github.com/repos/%s/%s/git/commits", d.Owner, d.Repo)) if err != nil { return err } if res.StatusCode() != 201 { return toErr(res) } var resp CommitResp if err = utils.Json.Unmarshal(res.Body(), &resp); err != nil { return err } // update branch head res, err = d.client.R(). SetBody(&UpdateRefReq{ Sha: resp.Sha, Force: false, }). Patch(fmt.Sprintf("https://api.github.com/repos/%s/%s/git/refs/heads/%s", d.Owner, d.Repo, d.Ref)) if err != nil { return err } if res.StatusCode() != 200 { return toErr(res) } return nil } func (d *Github) getBranchHead() (string, error) { res, err := d.client.R().Get(fmt.Sprintf("https://api.github.com/repos/%s/%s/branches/%s", d.Owner, d.Repo, d.Ref)) if err != nil { return "", err } if res.StatusCode() != 200 { return "", toErr(res) } var resp BranchResp if err = utils.Json.Unmarshal(res.Body(), &resp); err != nil { return "", err } return resp.Commit.Sha, nil } func (d *Github) copyWithoutRenewTree(srcObj, dstDir model.Obj) (dstSha, newSha, srcParentSha string, srcParentTree *TreeResp, err error) { dst, err := d.get(dstDir.GetPath()) if err != nil { return "", "", "", nil, err } if dst.Entries == nil { return "", "", "", nil, errs.NotFolder } dstSha = dst.Sha srcParentPath := stdpath.Dir(srcObj.GetPath()) srcParentTree, srcParentSha, err = d.getTreeDirectly(srcParentPath) if err != nil { return "", "", "", nil, err } var src *TreeObjReq = nil for _, t := range srcParentTree.Trees { if t.Path == srcObj.GetName() { if t.Type == "commit" { return "", "", "", nil, errors.New("cannot copy a submodule") } src = &t.TreeObjReq break } } if src == nil { return "", "", "", nil, errs.ObjectNotFound } newTree := make([]interface{}, 0, 2) newTree = append(newTree, *src) if len(dst.Entries) == 1 && dst.Entries[0].Name == ".gitkeep" { newTree = append(newTree, TreeObjReq{ Path: ".gitkeep", Mode: "100644", Type: "blob", Sha: nil, }) } newSha, err = d.newTree(dstSha, newTree) if err != nil { return "", "", "", nil, err } return dstSha, newSha, srcParentSha, srcParentTree, nil } func (d *Github) getRepo() (*RepoResp, error) { res, err := d.client.R().Get(fmt.Sprintf("https://api.github.com/repos/%s/%s", d.Owner, d.Repo)) if err != nil { return nil, err } if res.StatusCode() != 200 { return nil, toErr(res) } var resp RepoResp if err = utils.Json.Unmarshal(res.Body(), &resp); err != nil { return nil, err } return &resp, nil } func (d *Github) getAuthenticatedUser() (*UserResp, error) { res, err := d.client.R().Get("https://api.github.com/user") if err != nil { return nil, err } if res.StatusCode() != 200 { return nil, toErr(res) } resp := &UserResp{} if err = utils.Json.Unmarshal(res.Body(), resp); err != nil { return nil, err } return resp, nil } func (d *Github) addCommitterAndAuthor(m *map[string]interface{}) { if d.CommitterName != "" { committer := map[string]string{ "name": d.CommitterName, "email": d.CommitterEmail, } (*m)["committer"] = committer } if d.AuthorName != "" { author := map[string]string{ "name": d.AuthorName, "email": d.AuthorEmail, } (*m)["author"] = author } }