Change name and prepare for push
This commit is contained in:
26
README.md
26
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
|
||||
|
||||
36
config.go
36
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
|
||||
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
|
||||
}
|
||||
if _, err := os.Stat(fallback); err == nil {
|
||||
return fallback
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
10
conflicts.go
10
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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
2
go.mod
2
go.mod
@@ -1,4 +1,4 @@
|
||||
module git.pablu.de/Pablu/gh-threads
|
||||
module git.pablu.de/Pablu/diple
|
||||
|
||||
go 1.24.0
|
||||
|
||||
|
||||
8
main.go
8
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
4
tui.go
4
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user