From a0098a3946507bb3b1640f67d14ba51361f5767d Mon Sep 17 00:00:00 2001 From: Pablu Date: Tue, 28 Jul 2026 18:03:43 +0200 Subject: [PATCH] Change name and prepare for push --- README.md | 26 +++++++++++++++----------- config.go | 38 ++++++++++++++++++++++++++++---------- config_test.go | 43 ++++++++++++++++++++++++++++++++++++------- conflicts.go | 10 +++++----- github.go | 2 +- go.mod | 2 +- main.go | 8 ++++++-- persistence.go | 2 +- tui.go | 4 ++-- 9 files changed, 95 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 694a204..cc09204 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# gh-threads +# diple A terminal UI for people receiving GitHub pull-request reviews. It shows open PRs and a scrollable PR dashboard with the description, branches, @@ -20,7 +20,7 @@ Requires Go 1.24+, Git 2.38+, and an authenticated GitHub CLI: ```sh go install . gh auth login -gh-threads +diple ``` To run directly from a source checkout instead: @@ -41,34 +41,38 @@ authenticated user and groups the results by repository. Use `--repo` to limit the picker to one repository: ```sh -gh-threads --repo owner/repository +diple --repo owner/repository ``` With a repository selected, pass `--all` to include every open PR in that repository: ```sh -gh-threads --repo owner/repository --all --poll 15s +diple --repo owner/repository --all --poll 15s ``` GitHub Enterprise Server can be used after authenticating that host: ```sh gh auth login --hostname github.example.com -gh-threads --repo owner/repository \ +diple --repo owner/repository \ --endpoint https://github.example.com/api/graphql ``` ## Configuration The optional TOML configuration is loaded from -`$GH_THREADS_CONFIG`, `$XDG_CONFIG_HOME/gh-threads/config.toml`, or the -operating system's user configuration directory at `gh-threads/config.toml`. -On Linux this is normally `~/.config/gh-threads/config.toml`. On macOS, -`~/Library/Application Support/gh-threads/config.toml` is preferred, with -`~/.config/gh-threads/config.toml` automatically used as a fallback when it +`$DIPLE_CONFIG`, `$XDG_CONFIG_HOME/diple/config.toml`, or the operating +system's user configuration directory at `diple/config.toml`. +On Linux this is normally `~/.config/diple/config.toml`. On macOS, +`~/Library/Application Support/diple/config.toml` is preferred, with +`~/.config/diple/config.toml` automatically used as a fallback when it exists. +For migration, `GH_THREADS_CONFIG` and existing `gh-threads` configuration or +cache directories remain fallback locations when their new `diple` +counterparts do not yet exist. + ```toml theme = "dark" # dark, light, high-contrast, or no-color refresh_interval = "10s" @@ -221,7 +225,7 @@ avoid synchronized clients. Opening another PR or starting another refresh cancels the superseded request. GitHub's public APIs report whether a PR conflicts but do not expose its -conflicting file paths. For conflicting PRs only, `gh-threads` performs a +conflicting file paths. For conflicting PRs only, `diple` performs a read-only `git merge-tree` analysis in a temporary bare repository. It never touches or inspects the current checkout, so Git, Jujutsu (`jj`), and directories without a local repository behave identically. The analysis fetches the exact diff --git a/config.go b/config.go index cb8bc24..642b2a1 100644 --- a/config.go +++ b/config.go @@ -96,31 +96,46 @@ func defaultConfig() Config { } func configPath() (string, error) { + if path := os.Getenv("DIPLE_CONFIG"); path != "" { + return path, nil + } + // Preserve the old override during the rename so existing scripts do not + // silently start with a fresh configuration. if path := os.Getenv("GH_THREADS_CONFIG"); path != "" { return path, nil } if base := os.Getenv("XDG_CONFIG_HOME"); base != "" { - return filepath.Join(base, "gh-threads", "config.toml"), nil + return firstExistingOrDefault( + filepath.Join(base, "diple", "config.toml"), + filepath.Join(base, "gh-threads", "config.toml"), + ), nil } base, err := os.UserConfigDir() if err != nil { return "", fmt.Errorf("find user config directory: %w", err) } - preferred := filepath.Join(base, "gh-threads", "config.toml") + preferred := filepath.Join(base, "diple", "config.toml") home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("find home directory: %w", err) } - fallback := filepath.Join(home, ".config", "gh-threads", "config.toml") - return existingConfigPath(preferred, fallback), nil + dotConfig := filepath.Join(home, ".config", "diple", "config.toml") + legacyPreferred := filepath.Join(base, "gh-threads", "config.toml") + legacyDotConfig := filepath.Join(home, ".config", "gh-threads", "config.toml") + return firstExistingOrDefault( + preferred, dotConfig, legacyPreferred, legacyDotConfig, + ), nil } func existingConfigPath(preferred, fallback string) string { - if _, err := os.Stat(preferred); err == nil || !errors.Is(err, os.ErrNotExist) { - return preferred - } - if _, err := os.Stat(fallback); err == nil { - return fallback + return firstExistingOrDefault(preferred, fallback) +} + +func firstExistingOrDefault(preferred string, alternatives ...string) string { + for _, candidate := range append([]string{preferred}, alternatives...) { + if _, err := os.Stat(candidate); err == nil || !errors.Is(err, os.ErrNotExist) { + return candidate + } } return preferred } @@ -200,7 +215,10 @@ func defaultCacheDir() (string, error) { if err != nil { return "", fmt.Errorf("find user cache directory: %w", err) } - return filepath.Join(base, "gh-threads"), nil + return firstExistingOrDefault( + filepath.Join(base, "diple"), + filepath.Join(base, "gh-threads"), + ), nil } func validateThreadStatusOrder(order []string) error { diff --git a/config_test.go b/config_test.go index 4121974..14d70bd 100644 --- a/config_test.go +++ b/config_test.go @@ -52,7 +52,7 @@ within_status = "timestamp" [cache] enabled = false max_age = "48h" -directory = "/tmp/gh-threads-cache" +directory = "/tmp/diple-cache" [editing] mode = "standard" @@ -79,7 +79,7 @@ up = ["ctrl+k"] !got.Paths.Scroll || got.Paths.ScrollInterval.Duration != 125*time.Millisecond || strings.Join(got.Threads.StatusOrder, ",") != "resolved,unresolved,outdated" || got.Threads.WithinStatus != "timestamp" || got.Cache.Enabled || - got.Cache.MaxAge.Duration != 48*time.Hour || got.Cache.Directory != "/tmp/gh-threads-cache" || + got.Cache.MaxAge.Duration != 48*time.Hour || got.Cache.Directory != "/tmp/diple-cache" || got.Editing.Mode != "standard" || strings.Join(got.KeyBindings.Navigation.Down, ",") != "ctrl+j" || strings.Join(got.KeyBindings.Navigation.Up, ",") != "ctrl+k" { @@ -149,20 +149,33 @@ func TestLoadConfigRejectsUnknownSettings(t *testing.T) { } func TestConfigPathHonorsEnvironmentOverride(t *testing.T) { - t.Setenv("GH_THREADS_CONFIG", "/tmp/custom-gh-threads.toml") + t.Setenv("DIPLE_CONFIG", "/tmp/custom-diple.toml") + t.Setenv("GH_THREADS_CONFIG", "") got, err := configPath() if err != nil { t.Fatal(err) } - if got != "/tmp/custom-gh-threads.toml" { + if got != "/tmp/custom-diple.toml" { t.Fatalf("config path = %q", got) } } +func TestConfigPathHonorsLegacyEnvironmentOverride(t *testing.T) { + t.Setenv("DIPLE_CONFIG", "") + t.Setenv("GH_THREADS_CONFIG", "/tmp/legacy-gh-threads.toml") + got, err := configPath() + if err != nil { + t.Fatal(err) + } + if got != "/tmp/legacy-gh-threads.toml" { + t.Fatalf("legacy config path = %q", got) + } +} + func TestExistingConfigPathFallsBackToDotConfig(t *testing.T) { root := t.TempDir() - preferred := filepath.Join(root, "Library", "Application Support", "gh-threads", "config.toml") - fallback := filepath.Join(root, ".config", "gh-threads", "config.toml") + preferred := filepath.Join(root, "Library", "Application Support", "diple", "config.toml") + fallback := filepath.Join(root, ".config", "diple", "config.toml") if err := os.MkdirAll(filepath.Dir(fallback), 0o700); err != nil { t.Fatal(err) } @@ -184,14 +197,30 @@ func TestExistingConfigPathFallsBackToDotConfig(t *testing.T) { } } +func TestFirstExistingConfigPathFallsBackToLegacyName(t *testing.T) { + root := t.TempDir() + current := filepath.Join(root, "diple", "config.toml") + legacy := filepath.Join(root, "gh-threads", "config.toml") + if err := os.MkdirAll(filepath.Dir(legacy), 0o700); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(legacy, []byte("theme = \"dark\"\n"), 0o600); err != nil { + t.Fatal(err) + } + if got := firstExistingOrDefault(current, legacy); got != legacy { + t.Fatalf("migration config path = %q, want %q", got, legacy) + } +} + func TestConfigPathHonorsXDGConfigHome(t *testing.T) { t.Setenv("GH_THREADS_CONFIG", "") + t.Setenv("DIPLE_CONFIG", "") t.Setenv("XDG_CONFIG_HOME", "/tmp/xdg-config") got, err := configPath() if err != nil { t.Fatal(err) } - want := "/tmp/xdg-config/gh-threads/config.toml" + want := "/tmp/xdg-config/diple/config.toml" if got != want { t.Fatalf("config path = %q, want %q", got, want) } diff --git a/conflicts.go b/conflicts.go index ad9bcd8..8e20773 100644 --- a/conflicts.go +++ b/conflicts.go @@ -57,7 +57,7 @@ func analyzeConflictFiles( if repositoryURL == "" || baseRef == "" || number <= 0 { return nil, errors.New("missing repository merge metadata") } - gitDir, err := os.MkdirTemp("", "gh-threads-conflicts-*") + gitDir, err := os.MkdirTemp("", "diple-conflicts-*") if err != nil { return nil, fmt.Errorf("create temporary merge repository: %w", err) } @@ -83,8 +83,8 @@ func analyzeConflictFiles( } refspecs := []string{ - "+refs/heads/" + baseRef + ":refs/gh-threads/base", - "+refs/pull/" + strconv.Itoa(number) + "/head:refs/gh-threads/head", + "+refs/heads/" + baseRef + ":refs/diple/base", + "+refs/pull/" + strconv.Itoa(number) + "/head:refs/diple/head", } fetch := func(depthArgs ...string) error { args := []string{"-C", gitDir, "fetch", "--quiet", "--no-tags", "--filter=blob:none"} @@ -113,7 +113,7 @@ func analyzeConflictFiles( output, runErr := run( "-C", gitDir, "merge-tree", "--write-tree", "--name-only", "--no-messages", "-z", - "refs/gh-threads/base", "refs/gh-threads/head", + "refs/diple/base", "refs/diple/head", ) if runErr == nil { return nil, nil @@ -130,7 +130,7 @@ func mergeBaseExists( gitDir string, ) bool { _, err := run( - "-C", gitDir, "merge-base", "refs/gh-threads/base", "refs/gh-threads/head", + "-C", gitDir, "merge-base", "refs/diple/base", "refs/diple/head", ) return err == nil } diff --git a/github.go b/github.go index 94761ce..673a27f 100644 --- a/github.go +++ b/github.go @@ -105,7 +105,7 @@ func (c *GitHubClient) query( } req.Header.Set("Authorization", "Bearer "+c.token) req.Header.Set("Content-Type", "application/json") - req.Header.Set("User-Agent", "gh-threads") + req.Header.Set("User-Agent", "diple") resp, err := c.http.Do(req) if err != nil { diff --git a/go.mod b/go.mod index cc6c206..b313516 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module git.pablu.de/Pablu/gh-threads +module git.pablu.de/Pablu/diple go 1.24.0 diff --git a/main.go b/main.go index 4bba5c4..09623ca 100644 --- a/main.go +++ b/main.go @@ -39,7 +39,11 @@ func main() { visited := map[string]bool{} flag.Visit(func(item *flag.Flag) { visited[item.Name] = true }) - config, err := loadConfig(*configFile, visited["config"] || os.Getenv("GH_THREADS_CONFIG") != "") + config, err := loadConfig( + *configFile, + visited["config"] || os.Getenv("DIPLE_CONFIG") != "" || + os.Getenv("GH_THREADS_CONFIG") != "", + ) if err != nil { exitf("configuration: %v", err) } @@ -167,7 +171,7 @@ func firstNonEmpty(values ...string) string { } func exitf(format string, args ...any) { - fmt.Fprintf(os.Stderr, "gh-threads: "+format+"\n", args...) + fmt.Fprintf(os.Stderr, "diple: "+format+"\n", args...) os.Exit(1) } diff --git a/persistence.go b/persistence.go index e832083..f316e81 100644 --- a/persistence.go +++ b/persistence.go @@ -19,7 +19,7 @@ func atomicWriteJSON(path string, value any, mode os.FileMode) error { if err != nil { return err } - temp, err := os.CreateTemp(dir, ".gh-threads-*") + temp, err := os.CreateTemp(dir, ".diple-*") if err != nil { return err } diff --git a/tui.go b/tui.go index e2258cb..3552765 100644 --- a/tui.go +++ b/tui.go @@ -1802,7 +1802,7 @@ func (m App) viewWritePopup() string { strings.ToLower(m.mergeMethod), )), "", - badStyle.Render("This action cannot be undone from gh-threads."), + badStyle.Render("This action cannot be undone from diple."), "", warnStyle.Render(fmt.Sprintf( "%s merge now • %s cancel", @@ -2126,7 +2126,7 @@ func paneStyle(active bool) lipgloss.Style { } func (m App) viewPRs() string { - header := titleStyle.Render("gh-threads") + header := titleStyle.Render("diple") if m.owner != "" { header += " " + m.owner + "/" + m.repo }