Files
rikkei_simple_care/server/internal/handlers/handlers_github_git.go
2026-06-30 15:20:26 +07:00

290 lines
8.1 KiB
Go

package handlers
import (
"bytes"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
)
const githubBlobMaxBytes = 100 * 1024 * 1024
func collectDirFiles(rootDir string) ([]gitPathFile, []string) {
cleanRoot := filepath.Clean(rootDir)
byPath := map[string]gitPathFile{}
var warnings []string
_ = filepath.Walk(cleanRoot, func(path string, info os.FileInfo, walkErr error) error {
if walkErr != nil {
warnings = append(warnings, path+": "+walkErr.Error())
return nil
}
if info.IsDir() {
if shouldSkipBundledPath(filepath.Base(path)) && path != cleanRoot {
return filepath.SkipDir
}
return nil
}
rel, err := filepath.Rel(cleanRoot, path)
if err != nil {
warnings = append(warnings, path+": "+err.Error())
return nil
}
rel = filepath.ToSlash(rel)
if rel == "" || strings.HasPrefix(rel, "..") || shouldSkipBundledPath(rel) {
return nil
}
if info.Size() > githubBlobMaxBytes {
warnings = append(warnings, rel+": file quá lớn (>100MB), bỏ qua")
return nil
}
data, err := os.ReadFile(path)
if err != nil {
warnings = append(warnings, rel+": "+err.Error())
return nil
}
byPath[rel] = gitPathFile{path: rel, content: data}
return nil
})
out := make([]gitPathFile, 0, len(byPath))
for _, f := range byPath {
out = append(out, f)
}
return out, warnings
}
type gitPathFile struct {
path string
content []byte
}
func githubRepoAPI(owner, repo, suffix string) string {
return fmt.Sprintf("https://api.github.com/repos/%s/%s%s",
url.PathEscape(strings.TrimSpace(owner)),
url.PathEscape(strings.TrimSpace(repo)),
suffix,
)
}
func resolveGitPushTarget(token, fallbackOwner, repoName, repoHTML string) (owner, repo string) {
if o, r, err := parseGitHubRepo(repoHTML); err == nil && o != "" && r != "" {
return o, r
}
owner = strings.TrimSpace(fallbackOwner)
if live, err := githubGetUser(token); err == nil && live != "" {
owner = live
}
return owner, trimGitHubRepoName(repoName)
}
func githubCreateBlob(token, owner, repo string, content []byte) (string, error) {
apiURL := githubRepoAPI(owner, repo, "/git/blobs")
body := map[string]string{
"content": base64.StdEncoding.EncodeToString(content),
"encoding": "base64",
}
var resp struct {
SHA string `json:"sha"`
}
if err := githubJSON(token, http.MethodPost, apiURL, body, &resp); err != nil {
return "", err
}
if resp.SHA == "" {
return "", errors.New("GitHub không trả blob sha")
}
return resp.SHA, nil
}
func githubCreateTree(token, owner, repo, baseTreeSHA string, entries []map[string]string) (string, error) {
apiURL := githubRepoAPI(owner, repo, "/git/trees")
body := map[string]any{"tree": entries}
if baseTreeSHA != "" {
body["base_tree"] = baseTreeSHA
}
var resp struct {
SHA string `json:"sha"`
}
if err := githubJSON(token, http.MethodPost, apiURL, body, &resp); err != nil {
return "", err
}
if resp.SHA == "" {
return "", errors.New("GitHub không trả tree sha")
}
return resp.SHA, nil
}
func githubCreateCommit(token, owner, repo, treeSHA, parentSHA, message string) (string, error) {
apiURL := githubRepoAPI(owner, repo, "/git/commits")
body := map[string]any{
"message": message,
"tree": treeSHA,
}
if parentSHA != "" {
body["parents"] = []string{parentSHA}
}
var resp struct {
SHA string `json:"sha"`
}
if err := githubJSON(token, http.MethodPost, apiURL, body, &resp); err != nil {
return "", err
}
if resp.SHA == "" {
return "", errors.New("GitHub không trả commit sha")
}
return resp.SHA, nil
}
func githubUpdateBranchRef(token, owner, repo, branch, commitSHA string) error {
apiURL := githubRepoAPI(owner, repo, "/git/refs/heads/"+url.PathEscape(branch))
body := map[string]any{"sha": commitSHA, "force": false}
err := githubJSON(token, http.MethodPatch, apiURL, body, nil)
if err == nil {
return nil
}
// Branch chưa tồn tại — tạo mới
createURL := githubRepoAPI(owner, repo, "/git/refs")
createBody := map[string]string{
"ref": "refs/heads/" + branch,
"sha": commitSHA,
}
if createErr := githubJSON(token, http.MethodPost, createURL, createBody, nil); createErr == nil {
return nil
} else if !strings.Contains(strings.ToLower(createErr.Error()), "already exists") {
return createErr
}
// Ref đã tồn tại nhưng PATCH thất bại (non-fast-forward) — cập nhật force
forceBody := map[string]any{"sha": commitSHA, "force": true}
return githubJSON(token, http.MethodPatch, apiURL, forceBody, nil)
}
func githubBranchTip(token, owner, repo, branch string) (commitSHA, treeSHA string, err error) {
apiURL := githubRepoAPI(owner, repo, "/git/ref/heads/"+url.PathEscape(branch))
var refResp struct {
Object struct {
SHA string `json:"sha"`
} `json:"object"`
}
if err := githubJSON(token, http.MethodGet, apiURL, nil, &refResp); err != nil {
return "", "", err
}
commitSHA = refResp.Object.SHA
if commitSHA == "" {
return "", "", errors.New("không lấy được commit hiện tại")
}
commitURL := githubRepoAPI(owner, repo, "/git/commits/"+url.PathEscape(commitSHA))
var commitResp struct {
Tree struct {
SHA string `json:"sha"`
} `json:"tree"`
}
if err := githubJSON(token, http.MethodGet, commitURL, nil, &commitResp); err != nil {
return "", "", err
}
return commitSHA, commitResp.Tree.SHA, nil
}
func githubJSON(token, method, apiURL string, body any, out any) error {
var reader io.Reader
if body != nil {
payload, err := json.Marshal(body)
if err != nil {
return err
}
reader = bytes.NewReader(payload)
}
req, err := http.NewRequest(method, apiURL, reader)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("X-GitHub-Api-Version", "2022-11-28")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
raw, _ := io.ReadAll(resp.Body)
if resp.StatusCode >= 300 {
var errBody struct {
Message string `json:"message"`
}
_ = json.Unmarshal(raw, &errBody)
if errBody.Message != "" {
return errors.New(errBody.Message)
}
return fmt.Errorf("GitHub API lỗi (%d)", resp.StatusCode)
}
if out != nil && len(raw) > 0 {
if err := json.Unmarshal(raw, out); err != nil {
return err
}
}
return nil
}
// githubPushFiles — đẩy danh sách file lên repo; file lỗi được bỏ qua.
func githubPushFiles(token, owner, repo, branch string, files []gitPathFile, message string) (string, []string, error) {
warnings := []string{}
if len(files) == 0 {
return "", warnings, errors.New("không có file để đẩy")
}
parentSHA, baseTreeSHA, tipErr := githubBranchTip(token, owner, repo, branch)
if tipErr != nil {
parentSHA = ""
baseTreeSHA = ""
}
entries := make([]map[string]string, 0, len(files))
for _, f := range files {
blobSHA, err := githubCreateBlob(token, owner, repo, f.content)
if err != nil {
warnings = append(warnings, f.path+": "+err.Error()+" (bỏ qua)")
continue
}
entries = append(entries, map[string]string{
"path": f.path,
"mode": "100644",
"type": "blob",
"sha": blobSHA,
})
}
if len(entries) == 0 {
return "", warnings, errors.New("không đẩy được file nào lên GitHub")
}
treeSHA, err := githubCreateTree(token, owner, repo, baseTreeSHA, entries)
if err != nil {
return "", warnings, err
}
commitSHA, err := githubCreateCommit(token, owner, repo, treeSHA, parentSHA, message)
if err != nil {
return "", warnings, err
}
if err := githubUpdateBranchRef(token, owner, repo, branch, commitSHA); err != nil {
return "", warnings, err
}
return fmt.Sprintf("https://github.com/%s/%s/tree/%s", owner, repo, branch), warnings, nil
}
// githubPushDirectory — đẩy file lên repo; file lỗi được bỏ qua, không chặn cả lần push.
func githubPushDirectory(token, owner, repo, branch, localRoot, message string) (string, []string, error) {
files, collectWarn := collectDirFiles(localRoot)
warnings := append([]string{}, collectWarn...)
url, pushWarn, err := githubPushFiles(token, owner, repo, branch, files, message)
return url, append(warnings, pushWarn...), err
}