diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-29 17:02:21 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-29 17:02:21 +0300 |
| commit | a956a672859e92190149506197ad0fa682e964e2 (patch) | |
| tree | a5f168aef3302ad9d633f1bca56f16b320fe3e19 | |
| parent | 649a41dc0f0cbfa697408db2f006d106ba655933 (diff) | |
feat(showcase): uq move cgit host and output dir into config
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | doc/api-reference.md | 2 | ||||
| -rw-r--r-- | doc/configuration.md | 28 | ||||
| -rw-r--r-- | internal/config/config.go | 60 | ||||
| -rw-r--r-- | internal/config/config_test.go | 49 | ||||
| -rw-r--r-- | internal/showcase/showcase.go | 33 | ||||
| -rw-r--r-- | internal/showcase/showcase_test.go | 64 |
7 files changed, 221 insertions, 21 deletions
@@ -66,6 +66,8 @@ Create a configuration file at `~/.config/gitsyncer/config.json` (or specify a c "repo1", "repo2" ], + "showcase_output_dir": "~/git/foo.zone-content/gemtext/about", + "showcase_cgit_host": "https://cgit.f3s.buetow.org", "showcase_stats_branches": { "foo.zone": "content-gemtext" } @@ -453,7 +455,9 @@ Weekly rank snapshots are written on full showcase runs (all repositories), incl ### Configuration -The showcase output is written to `~/git/foo.zone-content/gemtext/about/showcase.gmi.tpl` by default (currently hardcoded). +The showcase output defaults to `~/git/foo.zone-content/gemtext/about/showcase.gmi.tpl`. You can override the output directory with `showcase_output_dir`. + +cgit links in project sections default to `https://cgit.f3s.buetow.org/<repo>/`. You can override the cgit host with `showcase_cgit_host`. You can override the branch used for showcase stats and cached code snippets on a per-repository basis with `showcase_stats_branches`. For example, `foo.zone` can use `content-gemtext` while the rest of the repos continue to use their current checkout branch. diff --git a/doc/api-reference.md b/doc/api-reference.md index 7ad41b1..0c9812e 100644 --- a/doc/api-reference.md +++ b/doc/api-reference.md @@ -187,6 +187,8 @@ type Config struct { Organizations []Organization `json:"organizations"` // List of git organizations Repositories []string `json:"repositories"` // Specific repos to sync ExcludeBranches []string `json:"exclude_branches"` // Regex patterns for branch exclusion + ShowcaseOutputDir string `json:"showcase_output_dir"` // Showcase output directory + ShowcaseCgitHost string `json:"showcase_cgit_host"` // Base URL for showcase cgit links ShowcaseStatsBranches map[string]string `json:"showcase_stats_branches"` // Per-repo branch overrides for showcase stats/code snippets } ``` diff --git a/doc/configuration.md b/doc/configuration.md index ce1f917..a8ac398 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -38,6 +38,8 @@ GitSyncer looks for configuration files in the following order: "^temp-", "-wip$" ], + "showcase_output_dir": "~/git/foo.zone-content/gemtext/about", + "showcase_cgit_host": "https://cgit.f3s.buetow.org", "showcase_stats_branches": { "foo.zone": "content-gemtext" } @@ -93,6 +95,30 @@ Example: } ``` +#### showcase_output_dir (optional) +Directory where showcase files are written (`showcase.gmi.tpl`, `showcase-rank-history.svg`, and extracted images). + +Default: `~/git/foo.zone-content/gemtext/about` + +Example: +```json +{ + "showcase_output_dir": "~/git/foo.zone-content/gemtext/about" +} +``` + +#### showcase_cgit_host (optional) +Base URL used to generate cgit links in showcase project sections. + +Default: `https://cgit.f3s.buetow.org` + +Example: +```json +{ + "showcase_cgit_host": "https://cgit.example.net/git" +} +``` + ## Examples ### Minimal Configuration @@ -139,6 +165,8 @@ Sync between GitHub and Codeberg: "-wip$", "^old-" ], + "showcase_output_dir": "~/git/foo.zone-content/gemtext/about", + "showcase_cgit_host": "https://cgit.f3s.buetow.org", "showcase_stats_branches": { "foo.zone": "content-gemtext" } diff --git a/internal/config/config.go b/internal/config/config.go index c2084b1..afccf33 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,6 +8,10 @@ import ( "strings" ) +const ( + defaultShowcaseCgitHost = "https://cgit.f3s.buetow.org" +) + // Organization represents a git organization with its host and name type Organization struct { Host string `json:"host"` @@ -27,6 +31,8 @@ type Config struct { WorkDir string `json:"work_dir,omitempty"` // Working directory for cloning repositories ExcludeFromShowcase []string `json:"exclude_from_showcase,omitempty"` // Repository names to exclude from showcase ShowcaseStatsBranches map[string]string `json:"showcase_stats_branches,omitempty"` // Repository names mapped to the branch used for showcase stats/code snippets + ShowcaseOutputDir string `json:"showcase_output_dir,omitempty"` // Directory where showcase files and assets are written + ShowcaseCgitHost string `json:"showcase_cgit_host,omitempty"` // Base URL for cgit links in showcase output // SkipReleases maps a repository name to a list of tag names for which // releases should NOT be created on any platform (GitHub/Codeberg) SkipReleases map[string][]string `json:"skip_releases,omitempty"` @@ -77,13 +83,20 @@ func Load(path string) (*Config, error) { cfg.WorkDir = filepath.Join(home, "git", "gitsyncer-workdir") } - // Expand home directory in WorkDir if needed - if strings.HasPrefix(cfg.WorkDir, "~/") { - home, err := os.UserHomeDir() + // Expand home directory in WorkDir if needed. + expandedWorkDir, err := expandHomePath(cfg.WorkDir) + if err != nil { + return nil, err + } + cfg.WorkDir = expandedWorkDir + + // Expand home directory in showcase output directory if configured. + if strings.TrimSpace(cfg.ShowcaseOutputDir) != "" { + expandedShowcaseOutputDir, err := expandHomePath(cfg.ShowcaseOutputDir) if err != nil { - return nil, fmt.Errorf("failed to get home directory: %w", err) + return nil, err } - cfg.WorkDir = filepath.Join(home, cfg.WorkDir[2:]) + cfg.ShowcaseOutputDir = expandedShowcaseOutputDir } return &cfg, nil @@ -195,3 +208,40 @@ func (o *Organization) IsSSH() bool { return !o.IsGitHub() && !o.IsCodeberg() && !strings.HasPrefix(o.Host, "file://") && (strings.Contains(o.Host, "@") || strings.Contains(o.Host, ":")) } + +// GetShowcaseOutputDir returns the configured showcase output directory when +// present, otherwise the default output location. +func (c *Config) GetShowcaseOutputDir() (string, error) { + if c != nil && strings.TrimSpace(c.ShowcaseOutputDir) != "" { + return expandHomePath(c.ShowcaseOutputDir) + } + + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get home directory: %w", err) + } + return filepath.Join(home, "git", "foo.zone-content", "gemtext", "about"), nil +} + +// GetShowcaseCgitHost returns the configured cgit host, or the default host. +func (c *Config) GetShowcaseCgitHost() string { + if c != nil { + host := strings.TrimSpace(c.ShowcaseCgitHost) + if host != "" { + return strings.TrimRight(host, "/") + } + } + return defaultShowcaseCgitHost +} + +func expandHomePath(path string) (string, error) { + if strings.HasPrefix(path, "~/") { + home, err := os.UserHomeDir() + if err != nil { + return "", fmt.Errorf("failed to get home directory: %w", err) + } + return filepath.Join(home, path[2:]), nil + } + + return path, nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 45b8024..eb81626 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "path/filepath" "strings" "testing" ) @@ -68,3 +69,51 @@ func TestFindOrganization_ReturnsPointerToStoredElement(t *testing.T) { t.Fatalf("Organizations[0].Name = %q, want %q", cfg.Organizations[0].Name, "after") } } + +func TestGetShowcaseOutputDir_Default(t *testing.T) { + homeDir := t.TempDir() + t.Setenv("HOME", homeDir) + + cfg := &Config{} + got, err := cfg.GetShowcaseOutputDir() + if err != nil { + t.Fatalf("GetShowcaseOutputDir() error = %v", err) + } + + want := filepath.Join(homeDir, "git", "foo.zone-content", "gemtext", "about") + if got != want { + t.Fatalf("GetShowcaseOutputDir() = %q, want %q", got, want) + } +} + +func TestGetShowcaseOutputDir_OverrideAndExpandHome(t *testing.T) { + homeDir := t.TempDir() + t.Setenv("HOME", homeDir) + + cfg := &Config{ + ShowcaseOutputDir: "~/custom/showcase", + } + got, err := cfg.GetShowcaseOutputDir() + if err != nil { + t.Fatalf("GetShowcaseOutputDir() error = %v", err) + } + + want := filepath.Join(homeDir, "custom", "showcase") + if got != want { + t.Fatalf("GetShowcaseOutputDir() = %q, want %q", got, want) + } +} + +func TestGetShowcaseCgitHost_DefaultAndOverride(t *testing.T) { + t.Parallel() + + defaultCfg := &Config{} + if got := defaultCfg.GetShowcaseCgitHost(); got != "https://cgit.f3s.buetow.org" { + t.Fatalf("GetShowcaseCgitHost() default = %q, want %q", got, "https://cgit.f3s.buetow.org") + } + + customCfg := &Config{ShowcaseCgitHost: "https://example.test/cgit/"} + if got := customCfg.GetShowcaseCgitHost(); got != "https://example.test/cgit" { + t.Fatalf("GetShowcaseCgitHost() override = %q, want %q", got, "https://example.test/cgit") + } +} diff --git a/internal/showcase/showcase.go b/internal/showcase/showcase.go index e32d144..ace9227 100644 --- a/internal/showcase/showcase.go +++ b/internal/showcase/showcase.go @@ -302,15 +302,20 @@ func (g *Generator) getRepositories() ([]string, error) { } func (g *Generator) buildProjectLinks(repoName string) (string, string, string) { + cfg := g.config + if cfg == nil { + cfg = &config.Config{} + } + codebergURL := "" githubURL := "" - cgitURL := fmt.Sprintf("https://cgit.f3s.buetow.org/%s/", repoName) + cgitURL := fmt.Sprintf("%s/%s/", cfg.GetShowcaseCgitHost(), repoName) - if codebergOrg := g.config.FindCodebergOrg(); codebergOrg != nil { + if codebergOrg := cfg.FindCodebergOrg(); codebergOrg != nil { codebergURL = fmt.Sprintf("https://codeberg.org/%s/%s", codebergOrg.Name, repoName) } - if githubOrg := g.config.FindGitHubOrg(); githubOrg != nil { + if githubOrg := cfg.FindGitHubOrg(); githubOrg != nil { githubURL = fmt.Sprintf("https://github.com/%s/%s", githubOrg.Name, repoName) } @@ -519,7 +524,7 @@ func (g *Generator) resolveSummary( func (g *Generator) collectAssets(repoName, repoPath, statsRepoPath string, metadata *RepoMetadata) ([]string, string, string, error) { // Always extract images from README (not cached) fmt.Printf("Extracting images from README...\n") - showcaseDir, err := showcaseOutputDir() + showcaseDir, err := g.showcaseOutputDir() if err != nil { return nil, "", "", err } @@ -545,20 +550,18 @@ func (g *Generator) collectAssets(repoName, repoPath, statsRepoPath string, meta return images, codeSnippet, codeLanguage, nil } -// showcaseOutputDir returns the canonical directory where showcase output files -// (Gemtext, SVG, images) are written. Centralised here so all writers agree on -// the path and a future change only needs to touch one place. -func showcaseOutputDir() (string, error) { - home, err := os.UserHomeDir() - if err != nil { - return "", fmt.Errorf("failed to get home directory: %w", err) +// showcaseOutputDir returns the configured showcase output directory, +// falling back to defaults when not configured. +func (g *Generator) showcaseOutputDir() (string, error) { + if g.config == nil { + return (&config.Config{}).GetShowcaseOutputDir() } - return filepath.Join(home, "git", "foo.zone-content", "gemtext", "about"), nil + return g.config.GetShowcaseOutputDir() } // writeShowcaseFile writes the showcase content to the target file func (g *Generator) writeShowcaseFile(content string) error { - targetDir, err := showcaseOutputDir() + targetDir, err := g.showcaseOutputDir() if err != nil { return err } @@ -582,7 +585,7 @@ func (g *Generator) writeShowcaseFile(content string) error { // writeRankHistorySVGFile generates an interactive SVG rank history graph and // writes it to the same directory as the showcase Gemtext file. func (g *Generator) writeRankHistorySVGFile(summaries []ProjectSummary) error { - targetDir, err := showcaseOutputDir() + targetDir, err := g.showcaseOutputDir() if err != nil { return err } @@ -706,7 +709,7 @@ func (g *Generator) verifyImages(summary *ProjectSummary) error { return nil } - showcaseDir, err := showcaseOutputDir() + showcaseDir, err := g.showcaseOutputDir() if err != nil { return err } diff --git a/internal/showcase/showcase_test.go b/internal/showcase/showcase_test.go index dac32b5..09d707f 100644 --- a/internal/showcase/showcase_test.go +++ b/internal/showcase/showcase_test.go @@ -167,6 +167,32 @@ func TestFormatGemtext_IncludesCgitLink(t *testing.T) { } } +func TestBuildProjectLinks_DefaultCgitHost(t *testing.T) { + t.Parallel() + + g := &Generator{config: &config.Config{}} + _, _, cgitURL := g.buildProjectLinks("cpuinfo") + + if cgitURL != "https://cgit.f3s.buetow.org/cpuinfo/" { + t.Fatalf("buildProjectLinks() cgit URL = %q, want %q", cgitURL, "https://cgit.f3s.buetow.org/cpuinfo/") + } +} + +func TestBuildProjectLinks_ConfiguredCgitHost(t *testing.T) { + t.Parallel() + + g := &Generator{ + config: &config.Config{ + ShowcaseCgitHost: "https://cgit.example.net/git/", + }, + } + _, _, cgitURL := g.buildProjectLinks("cpuinfo") + + if cgitURL != "https://cgit.example.net/git/cpuinfo/" { + t.Fatalf("buildProjectLinks() cgit URL = %q, want %q", cgitURL, "https://cgit.example.net/git/cpuinfo/") + } +} + func TestFormatGemtext_ZeroProjectsReleasePercentagesAreZero(t *testing.T) { t.Parallel() @@ -366,6 +392,44 @@ func TestCollectAssets_ContinuesWhenSnippetExtractionFails(t *testing.T) { } } +func TestCollectAssets_UsesConfiguredShowcaseOutputDir(t *testing.T) { + repoName := "demo" + repoPath := filepath.Join(t.TempDir(), repoName) + if err := os.MkdirAll(repoPath, 0755); err != nil { + t.Fatalf("mkdir repo: %v", err) + } + + if err := os.WriteFile(filepath.Join(repoPath, "README.md"), []byte(""), 0644); err != nil { + t.Fatalf("write README.md: %v", err) + } + if err := os.WriteFile(filepath.Join(repoPath, "shot.png"), []byte("png"), 0644); err != nil { + t.Fatalf("write shot.png: %v", err) + } + + customOutputDir := filepath.Join(t.TempDir(), "custom-showcase-output") + g := &Generator{ + config: &config.Config{ + ShowcaseOutputDir: customOutputDir, + }, + } + + images, snippet, language, err := g.collectAssets(repoName, repoPath, repoPath, nil) + if err != nil { + t.Fatalf("collectAssets() error = %v", err) + } + if len(images) != 1 || images[0] != filepath.Join("showcase", repoName, "image-1.png") { + t.Fatalf("collectAssets() images = %#v", images) + } + if snippet != "" || language != "" { + t.Fatalf("collectAssets() snippet/language = %q/%q, want empty", snippet, language) + } + + copiedPath := filepath.Join(customOutputDir, images[0]) + if _, err := os.Stat(copiedPath); err != nil { + t.Fatalf("expected copied image at %s: %v", copiedPath, err) + } +} + func TestSelectSummaryTool_DefaultPrefersOpencode(t *testing.T) { t.Parallel() |
