prime8dev

prime8dev
git clone https://git.prime8.dev/prime8dev.git
Log | Files | Refs | README | LICENSE

gitlab.go (4649B)


      1 package external
      2 
      3 import (
      4 	"context"
      5 	"encoding/json"
      6 	"fmt"
      7 	"io"
      8 	"net/http"
      9 	"net/url"
     10 	"os"
     11 	"regexp"
     12 	"strings"
     13 	"time"
     14 
     15 	"git.prime8.dev/moncheeta/prime8dev/internal/config"
     16 	"git.prime8.dev/moncheeta/prime8dev/internal/utils"
     17 )
     18 
     19 type GitLab struct {
     20 	httpClient *http.Client
     21 	apiBaseURL string
     22 }
     23 
     24 func NewGitLab() *GitLab {
     25 	return &GitLab{
     26 		httpClient: &http.Client{
     27 			Timeout: 10 * time.Second,
     28 		},
     29 		apiBaseURL: strings.TrimSpace(os.Getenv("GITLAB_HOST")),
     30 	}
     31 }
     32 
     33 type GitMetadata struct {
     34 	Readme       string
     35 	DateCreated  time.Time
     36 	DateModified time.Time
     37 }
     38 
     39 func (s *GitLab) FetchMetadata(ctx context.Context, project config.Project) (*GitMetadata, error) {
     40 	if ctx == nil {
     41 		ctx = context.Background()
     42 	}
     43 
     44 	return s.fetchFromGitLab(ctx, project)
     45 }
     46 
     47 func (s *GitLab) RefreshMetadata(ctx context.Context, project config.Project) error {
     48 	if ctx == nil {
     49 		ctx = context.Background()
     50 	}
     51 
     52 	_, err := s.fetchFromGitLab(ctx, project)
     53 	return err
     54 }
     55 
     56 func (s *GitLab) fetchFromGitLab(ctx context.Context, project config.Project) (*GitMetadata, error) {
     57 	apiURL := s.rewriteGitLabURL(project.APIURL)
     58 
     59 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
     60 	if err != nil {
     61 		return nil, err
     62 	}
     63 	req.Header.Set("User-Agent", "prime8dev-portfolio")
     64 
     65 	resp, err := s.httpClient.Do(req)
     66 	if err != nil {
     67 		return nil, err
     68 	}
     69 	defer resp.Body.Close()
     70 
     71 	if resp.StatusCode != http.StatusOK {
     72 		return nil, fmt.Errorf("gitlab API returned %d", resp.StatusCode)
     73 	}
     74 
     75 	var repo struct {
     76 		WebURL         string `json:"web_url"`
     77 		DefaultBranch  string `json:"default_branch"`
     78 		CreatedAt      string `json:"created_at"`
     79 		LastActivityAt string `json:"last_activity_at"`
     80 		ReadmeURL      string `json:"readme_url"`
     81 	}
     82 	if err := json.NewDecoder(resp.Body).Decode(&repo); err != nil {
     83 		return nil, err
     84 	}
     85 
     86 	webURL := utils.EnsureHTTPS(repo.WebURL)
     87 
     88 	dateCreated, _ := time.Parse(time.RFC3339, repo.CreatedAt)
     89 	dateModified, _ := time.Parse(time.RFC3339, repo.LastActivityAt)
     90 
     91 	readme := ""
     92 	if repo.ReadmeURL != "" {
     93 		readmeURL := s.rewriteGitLabURL(strings.Replace(repo.ReadmeURL, "/-/blob/", "/-/raw/", 1))
     94 		readme = s.fetchReadme(ctx, webURL, repo.DefaultBranch, readmeURL)
     95 	}
     96 
     97 	return &GitMetadata{
     98 		Readme:       readme,
     99 		DateCreated:  dateCreated,
    100 		DateModified: dateModified,
    101 	}, nil
    102 }
    103 
    104 func (s *GitLab) rewriteGitLabURL(rawURL string) string {
    105 	if s.apiBaseURL == "" {
    106 		return rawURL
    107 	}
    108 
    109 	base, err := url.Parse(s.apiBaseURL)
    110 	if err != nil || base.Scheme == "" || base.Host == "" {
    111 		return rawURL
    112 	}
    113 
    114 	target, err := url.Parse(rawURL)
    115 	if err != nil || target.Scheme == "" || target.Host == "" {
    116 		return rawURL
    117 	}
    118 
    119 	targetOrigin := fmt.Sprintf("%s://%s", target.Scheme, target.Host)
    120 	suffix := strings.TrimPrefix(rawURL, targetOrigin)
    121 	if suffix == "" {
    122 		suffix = "/"
    123 	}
    124 
    125 	basePath := strings.TrimSuffix(base.EscapedPath(), "/")
    126 	if basePath != "" {
    127 		if !strings.HasPrefix(suffix, "/") {
    128 			suffix = fmt.Sprintf("/%s", suffix)
    129 		}
    130 		suffix = fmt.Sprintf("%s%s", basePath, suffix)
    131 	}
    132 
    133 	return fmt.Sprintf("%s://%s%s", base.Scheme, base.Host, suffix)
    134 }
    135 
    136 func (s *GitLab) fetchReadme(ctx context.Context, webURL, branch, readmeURL string) string {
    137 	req, err := http.NewRequestWithContext(ctx, http.MethodGet, readmeURL, nil)
    138 	if err != nil {
    139 		return ""
    140 	}
    141 
    142 	resp, err := s.httpClient.Do(req)
    143 	if err != nil {
    144 		return ""
    145 	}
    146 	defer resp.Body.Close()
    147 
    148 	if resp.StatusCode != http.StatusOK {
    149 		return ""
    150 	}
    151 
    152 	contentBytes, err := io.ReadAll(resp.Body)
    153 	if err != nil {
    154 		return ""
    155 	}
    156 	content := string(contentBytes)
    157 
    158 	return fixRelativePaths(webURL, branch, content)
    159 }
    160 
    161 var linkRegex = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
    162 var imageRegex = regexp.MustCompile(`!\[([^\]]*)\]\(([^)]+)\)`)
    163 
    164 func fixRelativePaths(webURL, branch, markdown string) string {
    165 	blob := fmt.Sprintf("%s/-/blob/%s", webURL, branch)
    166 	raw := fmt.Sprintf("%s/-/raw/%s", webURL, branch)
    167 
    168 	markdown = imageRegex.ReplaceAllStringFunc(markdown, func(match string) string {
    169 		parts := imageRegex.FindStringSubmatch(match)
    170 		if len(parts) != 3 {
    171 			return match
    172 		}
    173 		alt, path := parts[1], parts[2]
    174 		if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
    175 			return match
    176 		}
    177 		return fmt.Sprintf("![%s](%s/%s)", alt, raw, path)
    178 	})
    179 
    180 	markdown = linkRegex.ReplaceAllStringFunc(markdown, func(match string) string {
    181 		parts := linkRegex.FindStringSubmatch(match)
    182 		if len(parts) != 3 {
    183 			return match
    184 		}
    185 		text, path := parts[1], parts[2]
    186 		if strings.HasPrefix(path, "http://") || strings.HasPrefix(path, "https://") {
    187 			return match
    188 		}
    189 		return fmt.Sprintf("[%s](%s/%s)", text, blob, path)
    190 	})
    191 	return markdown
    192 }