Add gitlab provider

This commit is contained in:
2026-05-05 20:09:29 +02:00
parent 0344215b42
commit 6169dea989
2 changed files with 149 additions and 0 deletions
+6
View File
@@ -19,6 +19,12 @@ func NewProvider(serverType, protocol, baseUrl, authToken string) (GitProvider,
baseUrl: baseUrl, baseUrl: baseUrl,
authToken: authToken, authToken: authToken,
}, nil }, nil
case "gitlab":
return &GitLabProvider{
protocol: protocol,
baseUrl: baseUrl,
authToken: authToken,
}, nil
default: default:
return nil, fmt.Errorf("unsupported git provider: %s", serverType) return nil, fmt.Errorf("unsupported git provider: %s", serverType)
} }
+143
View File
@@ -0,0 +1,143 @@
package gitprovider
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
)
type GitLabProvider struct {
protocol string
baseUrl string
authToken string
}
var _ GitProvider = (*GitLabProvider)(nil)
func (p *GitLabProvider) FetchAndDeployBranch(owner, repo, branch, destPath string) (*DeployResult, error) {
apiUrl := fmt.Sprintf("%s://%s/api/v4/", p.protocol, strings.TrimSuffix(p.baseUrl, "/"))
result, err := fetchGitLabBranchHead(apiUrl, owner, repo, branch, p.authToken)
if err != nil {
return nil, fmt.Errorf("fetching branch head: %w", err)
}
if err = downloadAndExtractGitLab(apiUrl, owner, repo, branch, p.authToken, destPath); err != nil {
return nil, err
}
return result, nil
}
func fetchGitLabBranchHead(apiUrl, owner, repo, branch, token string) (*DeployResult, error) {
// GitLab identifies projects by URL-encoded "namespace/repo" path.
projectID := url.PathEscape(owner + "/" + repo)
encodedBranch := url.PathEscape(branch)
reqUrl := fmt.Sprintf("%sprojects/%s/repository/branches/%s", apiUrl, projectID, encodedBranch)
req, err := http.NewRequest(http.MethodGet, reqUrl, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
req.Header.Set("PRIVATE-TOKEN", token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("fetching branch: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitLab returned %s for %s", resp.Status, reqUrl)
}
var payload struct {
Commit struct {
ID string `json:"id"`
Title string `json:"title"` // first line of the commit message
} `json:"commit"`
}
if err = json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, fmt.Errorf("decoding branch response: %w", err)
}
return &DeployResult{
CommitHash: payload.Commit.ID,
CommitMessage: payload.Commit.Title,
}, nil
}
func downloadAndExtractGitLab(apiUrl, owner, repo, branch, token, destDir string) error {
// GET /projects/:id/repository/archive.zip?sha=<branch>
projectID := url.PathEscape(owner + "/" + repo)
archiveURL := fmt.Sprintf("%sprojects/%s/repository/archive.zip?sha=%s", apiUrl, projectID, url.QueryEscape(branch))
req, err := http.NewRequest(http.MethodGet, archiveURL, nil)
if err != nil {
return fmt.Errorf("creating request: %w", err)
}
req.Header.Set("PRIVATE-TOKEN", token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("fetching archive: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("GitLab returned %s for %s", resp.Status, archiveURL)
}
storageRoot := filepath.Dir(destDir)
tmpZip, err := os.CreateTemp(storageRoot, "gitlabbranch-*.zip")
if err != nil {
return fmt.Errorf("creating temp zip: %w", err)
}
defer os.Remove(tmpZip.Name())
if _, err = io.Copy(tmpZip, resp.Body); err != nil {
tmpZip.Close()
return fmt.Errorf("writing zip: %w", err)
}
tmpZip.Close()
tmpDir, err := os.MkdirTemp(storageRoot, "gitlabbranch-unpack-*")
if err != nil {
return fmt.Errorf("creating temp dir: %w", err)
}
defer os.RemoveAll(tmpDir)
if err = unzip(tmpZip.Name(), tmpDir); err != nil {
return fmt.Errorf("unzipping: %w", err)
}
// GitLab names the root folder {repo}-{branch}-{sha}/.
entries, err := os.ReadDir(tmpDir)
if err != nil {
return fmt.Errorf("reading temp dir: %w", err)
}
if len(entries) != 1 || !entries[0].IsDir() {
return fmt.Errorf("unexpected archive layout: expected a single root directory")
}
extractedRoot := filepath.Join(tmpDir, entries[0].Name())
if err = os.MkdirAll(filepath.Dir(destDir), 0o755); err != nil {
return fmt.Errorf("creating destination parent dirs: %w", err)
}
os.RemoveAll(destDir)
if err = os.Rename(extractedRoot, destDir); err == nil {
return nil
}
if err = copyDir(extractedRoot, destDir); err != nil {
return fmt.Errorf("cross-device copy to destination: %w", err)
}
return nil
}