alist/drivers/github/driver.go
KirCute cdc41595bc
feat(github): support GPG verification (#7996 close #7986)
* feat(github): support GPG verification

* chore
2025-02-24 23:12:23 +08:00

976 lines
25 KiB
Go

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
}
}