package main import ( "fmt" "log" "os" "os/exec" "strings" ) func updateTemplates(config Config) (string, error) { log.Printf("Updating templates from Git: %s (branch: %s)", sanitizeRepoURL(config.TemplatesRepo), config.TemplatesBranch) // Check if templates directory exists if _, err := os.Stat(config.TemplatesPath); os.IsNotExist(err) { // Clone repository if err := cloneRepository(config); err != nil { return "", err } return getCurrentCommit(config.TemplatesPath), nil } // Check if it's a valid git repository if !isGitRepository(config.TemplatesPath) { log.Printf("Warning: %s exists but is not a git repository", config.TemplatesPath) log.Printf("Remove the directory or use -skip-git-pull flag") return "", fmt.Errorf("not a git repository") } // Pull latest changes and detect if there were updates if err := pullRepository(config); err != nil { return getCurrentCommit(config.TemplatesPath), err } return getCurrentCommit(config.TemplatesPath), nil } func cloneRepository(config Config) error { log.Printf("Cloning templates repository...") repoURL := buildRepoURL(config.TemplatesRepo, config.GitToken) cmd := exec.Command("git", "clone", "-b", config.TemplatesBranch, repoURL, config.TemplatesPath) cmd.Stdout = os.Stdout // Don't show stderr to avoid leaking token var stderr strings.Builder cmd.Stderr = &stderr if err := cmd.Run(); err != nil { // Sanitize error message errMsg := sanitizeGitError(stderr.String()) return fmt.Errorf("failed to clone repository: %w\n%s", err, errMsg) } log.Printf("Templates cloned successfully") showCurrentCommit(config.TemplatesPath) return nil } func pullRepository(config Config) error { // Get current commit before pull oldCommit := getCurrentCommit(config.TemplatesPath) log.Printf("Pulling latest templates...") // Update remote URL with token if provided if config.GitToken != "" { if err := updateRemoteURL(config); err != nil { log.Printf("Warning: Failed to update remote URL: %v", err) } } cmd := exec.Command("git", "-C", config.TemplatesPath, "pull", "origin", config.TemplatesBranch) var stdout, stderr strings.Builder cmd.Stdout = &stdout cmd.Stderr = &stderr if err := cmd.Run(); err != nil { errMsg := sanitizeGitError(stderr.String()) return fmt.Errorf("failed to pull repository: %w\n%s", err, errMsg) } // Get current commit after pull newCommit := getCurrentCommit(config.TemplatesPath) // Check if there were changes if oldCommit == newCommit { log.Printf("No new changes in templates (commit: %s)", truncateHash(oldCommit)) } else if oldCommit == "" { log.Printf("Successfully pulled templates") showCurrentCommit(config.TemplatesPath) } else { log.Printf("Templates updated: %s -> %s", truncateHash(oldCommit), truncateHash(newCommit)) showCommitDiff(config.TemplatesPath, oldCommit, newCommit) } return nil } func updateRemoteURL(config Config) error { repoURL := buildRepoURL(config.TemplatesRepo, config.GitToken) cmd := exec.Command("git", "-C", config.TemplatesPath, "remote", "set-url", "origin", repoURL) return cmd.Run() } func buildRepoURL(repoURL, token string) string { if token == "" { return repoURL } // For Gitea, format: https://TOKEN@svc-git.cirex.ru/... // Remove https:// prefix if present cleanURL := strings.TrimPrefix(repoURL, "https://") cleanURL = strings.TrimPrefix(cleanURL, "http://") return fmt.Sprintf("https://%s@%s", token, cleanURL) } func sanitizeRepoURL(repoURL string) string { // Remove token from URL for logging if strings.Contains(repoURL, "@") { parts := strings.SplitN(repoURL, "@", 2) if len(parts) == 2 { // Keep protocol and domain protocol := "https://" if strings.HasPrefix(repoURL, "http://") { protocol = "http://" } return protocol + "***:***@" + parts[1] } } return repoURL } func sanitizeGitError(errMsg string) string { // Remove potential token from error messages // Support both Gitea (TOKEN@) and GitLab (oauth2:TOKEN@) formats // Gitea format: https://TOKEN@host if strings.Contains(errMsg, "@") && strings.Contains(errMsg, "://") { parts := strings.Split(errMsg, "://") for i := 1; i < len(parts); i++ { atIndex := strings.Index(parts[i], "@") if atIndex > 0 { parts[i] = "***@" + parts[i][atIndex+1:] } } errMsg = strings.Join(parts, "://") } // GitLab format: https://oauth2:TOKEN@host (legacy support) if strings.Contains(errMsg, "oauth2:") { parts := strings.Split(errMsg, "oauth2:") for i := 1; i < len(parts); i++ { atIndex := strings.Index(parts[i], "@") if atIndex > 0 { parts[i] = "***" + parts[i][atIndex:] } } errMsg = strings.Join(parts, "oauth2:") } return errMsg } func isGitRepository(path string) bool { gitDir := path + "/.git" _, err := os.Stat(gitDir) return err == nil } func getCurrentCommit(templatesPath string) string { cmd := exec.Command("git", "-C", templatesPath, "rev-parse", "HEAD") output, err := cmd.Output() if err != nil { return "" } return strings.TrimSpace(string(output)) } func showCurrentCommit(templatesPath string) { cmd := exec.Command("git", "-C", templatesPath, "log", "-1", "--oneline") output, err := cmd.Output() if err == nil { log.Printf("Current template commit: %s", strings.TrimSpace(string(output))) } } func showCommitDiff(templatesPath, oldCommit, newCommit string) { // Show files changed cmd := exec.Command("git", "-C", templatesPath, "diff", "--name-only", oldCommit, newCommit) output, err := cmd.Output() if err == nil && len(output) > 0 { files := strings.Split(strings.TrimSpace(string(output)), "\n") log.Printf("Changed files:") for _, file := range files { log.Printf(" - %s", file) } } } func truncateHash(hash string) string { if len(hash) > 8 { return hash[:8] } return hash }