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 }