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("", 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 }