complete fgj → fj rename: env vars, config migration, docs
Some checks are pending
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / functional (push) Blocked by required conditions

- Rename env vars: FGJ_HOST → FJ_HOST, FGJ_TOKEN → FJ_TOKEN,
  FGJ_FORCE_TTY → FJ_FORCE_TTY, FGJ_PAGER → FJ_PAGER,
  FGJ_BINARY_PATH → FJ_BINARY_PATH (all with legacy fallback)
- Auto-migrate ~/.config/fgj/ → ~/.config/fj/ on first run
- Update man page title, README, CHANGELOG
- Update test fixture labels from [FGJ E2E Test] to [FJ E2E Test]
This commit is contained in:
sid 2026-04-26 08:23:48 -06:00
parent cf7c0e0878
commit c3e8ad67ed
9 changed files with 94 additions and 33 deletions

View file

@ -189,7 +189,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
#### Authentication
- `fj auth login` - Interactive authentication with Forgejo instances
- `fj auth status` - Check authentication status
- Environment variable support (`FGJ_HOST`, `FGJ_TOKEN`)
- Environment variable support (`FJ_HOST`, `FJ_TOKEN`)
#### Development
- Comprehensive unit test suite

View file

@ -494,12 +494,12 @@ hosts:
### Environment Variables
- `FGJ_HOST`: Override the default instance (auto-detected from git remote if not set)
- `FGJ_TOKEN`: Provide authentication token
- `FJ_HOST`: Override the default instance (auto-detected from git remote if not set)
- `FJ_TOKEN`: Provide authentication token
Hostname is resolved in this priority order:
1. Command-specific flags (e.g., `--hostname`)
2. `FGJ_HOST` environment variable
2. `FJ_HOST` environment variable
3. Auto-detected from git remote URL
4. `match_dirs` lookup (longest prefix match against current directory)
5. Default to `codeberg.org`

View file

@ -188,7 +188,7 @@ func resolveAuthHostname(cfg *config.Config, hostname string) (string, error) {
hostname = viper.GetString("hostname")
}
if hostname == "" {
hostname = os.Getenv("FGJ_HOST")
hostname = config.EnvWithFallback("FJ_HOST", "FGJ_HOST")
}
if hostname == "" {
hostname = getDetectedHost()

View file

@ -29,7 +29,7 @@ var manpagesCmd = &cobra.Command{
}
header := &doc.GenManHeader{
Title: "FGJ",
Title: "FJ",
Section: "1",
}

View file

@ -2,7 +2,9 @@ package cmd
import (
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
@ -52,6 +54,18 @@ func initConfig() {
}
configDir := home + "/.config/fj"
legacyDir := home + "/.config/fgj"
// Migrate from ~/.config/fgj/ if the new dir doesn't exist yet.
if _, err := os.Stat(configDir); os.IsNotExist(err) {
if info, err := os.Stat(legacyDir); err == nil && info.IsDir() {
if copyErr := migrateConfigDir(legacyDir, configDir); copyErr == nil {
fmt.Fprintln(ios.ErrOut, "notice: migrated config from ~/.config/fgj/ to ~/.config/fj/")
fmt.Fprintln(ios.ErrOut, " you can remove ~/.config/fgj/ when ready")
}
}
}
_ = os.MkdirAll(configDir, 0755)
viper.AddConfigPath(configDir)
@ -60,7 +74,7 @@ func initConfig() {
}
viper.AutomaticEnv()
viper.SetEnvPrefix("FGJ")
viper.SetEnvPrefix("FJ")
_ = viper.ReadInConfig()
}
@ -127,3 +141,35 @@ func parseIssueArg(arg string) (int64, error) {
}
return strconv.ParseInt(arg, 10, 64)
}
// migrateConfigDir copies all files from src to dst (one level, no subdirs).
func migrateConfigDir(src, dst string) error {
if err := os.MkdirAll(dst, 0755); err != nil {
return err
}
entries, err := os.ReadDir(src)
if err != nil {
return err
}
for _, e := range entries {
if e.IsDir() {
continue
}
in, err := os.Open(filepath.Join(src, e.Name()))
if err != nil {
return err
}
out, err := os.OpenFile(filepath.Join(dst, e.Name()), os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
in.Close()
return err
}
_, err = io.Copy(out, in)
in.Close()
out.Close()
if err != nil {
return err
}
}
return nil
}

View file

@ -131,7 +131,7 @@ func (c *Config) SaveToPath(path string) error {
// Priority order:
// 1. Explicitly provided hostname parameter
// 2. CLI flag (--hostname)
// 3. Environment variable (FGJ_HOST)
// 3. Environment variable (FJ_HOST, with FGJ_HOST fallback)
// 4. Auto-detected hostname from git remote
// 5. match_dirs lookup (longest prefix match)
// 6. Default to codeberg.org
@ -141,7 +141,7 @@ func (c *Config) GetHost(hostname string, detectedHost string, cwd string) (Host
}
if hostname == "" {
hostname = os.Getenv("FGJ_HOST")
hostname = EnvWithFallback("FJ_HOST", "FGJ_HOST")
}
if hostname == "" {
@ -228,6 +228,15 @@ func (c *Config) ResolveHostByPath(cwd string) string {
}
// expandHome replaces a leading ~ with the user's home directory.
// EnvWithFallback returns the value of the primary env var, falling back to
// the legacy name if the primary is unset. This eases the FGJ_ → FJ_ rename.
func EnvWithFallback(primary, legacy string) string {
if v := os.Getenv(primary); v != "" {
return v
}
return os.Getenv(legacy)
}
func expandHome(path string) string {
if path == "~" || strings.HasPrefix(path, "~/") {
home, err := os.UserHomeDir()

View file

@ -37,10 +37,10 @@ type IOStreams struct {
}
// New creates an IOStreams wired to the real os.Stdin, os.Stdout, and os.Stderr,
// with TTY status auto-detected. Setting FGJ_FORCE_TTY=1 forces all streams to
// be treated as TTYs.
// with TTY status auto-detected. Setting FJ_FORCE_TTY=1 (or legacy FGJ_FORCE_TTY=1)
// forces all streams to be treated as TTYs.
func New() *IOStreams {
forceTTY := os.Getenv("FGJ_FORCE_TTY") != ""
forceTTY := os.Getenv("FJ_FORCE_TTY") != "" || os.Getenv("FGJ_FORCE_TTY") != ""
stdinTTY := forceTTY || (isTerminal(os.Stdin.Fd()))
stdoutTTY := forceTTY || (isTerminal(os.Stdout.Fd()))
@ -118,14 +118,17 @@ func (s *IOStreams) ColorScheme() *ColorScheme {
}
// StartPager starts an external pager process and redirects Out to its stdin.
// It checks FGJ_PAGER, then PAGER, then defaults to "less". If LESS is not
// already set, it is set to "FRX" for a good default experience.
// It checks FJ_PAGER (or legacy FGJ_PAGER), then PAGER, then defaults to "less".
// If LESS is not already set, it is set to "FRX" for a good default experience.
func (s *IOStreams) StartPager() error {
if !s.isStdoutTTY {
return nil
}
pagerCmd := os.Getenv("FGJ_PAGER")
pagerCmd := os.Getenv("FJ_PAGER")
if pagerCmd == "" {
pagerCmd = os.Getenv("FGJ_PAGER")
}
if pagerCmd == "" {
pagerCmd = os.Getenv("PAGER")
}

View file

@ -230,7 +230,10 @@ func (env *TestEnv) CleanupRepo(owner, repoName string) {
// GetBinaryPath returns the path to the built fj binary
func (env *TestEnv) GetBinaryPath() string {
binaryPath := os.Getenv("FGJ_BINARY_PATH")
binaryPath := os.Getenv("FJ_BINARY_PATH")
if binaryPath == "" {
binaryPath = os.Getenv("FGJ_BINARY_PATH")
}
if binaryPath == "" {
// Look for the binary in common locations
candidates := []string{

View file

@ -86,7 +86,7 @@ func TestCLIIssueList(t *testing.T) {
env := NewTestEnv(t)
// Create a test issue so the list is not empty
issueNum := env.CreateTestIssue("[FGJ E2E Test] Issue List", "For issue list test")
issueNum := env.CreateTestIssue("[FJ E2E Test] Issue List", "For issue list test")
defer env.CleanupIssue(issueNum)
result := env.RunCLI(
@ -109,7 +109,7 @@ func TestCLIIssueList(t *testing.T) {
func TestCLIIssueListJSON(t *testing.T) {
env := NewTestEnv(t)
issueNum := env.CreateTestIssue("[FGJ E2E Test] JSON List", "For JSON output test")
issueNum := env.CreateTestIssue("[FJ E2E Test] JSON List", "For JSON output test")
defer env.CleanupIssue(issueNum)
result := env.RunCLI(
@ -137,7 +137,7 @@ func TestCLIIssueListJSON(t *testing.T) {
func TestCLIIssueView(t *testing.T) {
env := NewTestEnv(t)
issueNum := env.CreateTestIssue("[FGJ E2E Test] View Test", "Testing issue view")
issueNum := env.CreateTestIssue("[FJ E2E Test] View Test", "Testing issue view")
defer env.CleanupIssue(issueNum)
result := env.RunCLI(
@ -160,7 +160,7 @@ func TestCLIIssueView(t *testing.T) {
func TestCLIIssueViewJSON(t *testing.T) {
env := NewTestEnv(t)
issueNum := env.CreateTestIssue("[FGJ E2E Test] JSON View", "Testing JSON view")
issueNum := env.CreateTestIssue("[FJ E2E Test] JSON View", "Testing JSON view")
defer env.CleanupIssue(issueNum)
result := env.RunCLI(
@ -198,7 +198,7 @@ func TestCLIIssueCreate(t *testing.T) {
"--hostname", env.Hostname,
"-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName),
"issue", "create",
"-t", "[FGJ E2E Test] CLI Created Issue",
"-t", "[FJ E2E Test] CLI Created Issue",
"-b", "Created directly via fj CLI",
)
@ -229,7 +229,7 @@ func TestCLIIssueCreateWithLabels(t *testing.T) {
"--hostname", env.Hostname,
"-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName),
"issue", "create",
"-t", "[FGJ E2E Test] Issue with Labels",
"-t", "[FJ E2E Test] Issue with Labels",
"-b", "This issue was created with labels",
"-l", "bug",
"-l", "enhancement",
@ -275,7 +275,7 @@ func TestCLIIssueCreateWithLabels(t *testing.T) {
func TestCLIIssueComment(t *testing.T) {
env := NewTestEnv(t)
issueNum := env.CreateTestIssue("[FGJ E2E Test] Comment Test", "Testing comment via CLI")
issueNum := env.CreateTestIssue("[FJ E2E Test] Comment Test", "Testing comment via CLI")
defer env.CleanupIssue(issueNum)
result := env.RunCLI(
@ -313,7 +313,7 @@ func TestCLIIssueComment(t *testing.T) {
func TestCLIIssueClose(t *testing.T) {
env := NewTestEnv(t)
issueNum := env.CreateTestIssue("[FGJ E2E Test] Close Test", "Will be closed via CLI")
issueNum := env.CreateTestIssue("[FJ E2E Test] Close Test", "Will be closed via CLI")
result := env.RunCLI(
"--hostname", env.Hostname,
@ -341,7 +341,7 @@ func TestCLIIssueClose(t *testing.T) {
func TestCLIIssueCloseWithComment(t *testing.T) {
env := NewTestEnv(t)
issueNum := env.CreateTestIssue("[FGJ E2E Test] Close with comment", "Will be closed with a comment")
issueNum := env.CreateTestIssue("[FJ E2E Test] Close with comment", "Will be closed with a comment")
commentText := "Fixed in v2.0 - closing via functional test"
@ -389,7 +389,7 @@ func TestCLIIssueCloseWithComment(t *testing.T) {
func TestCLIIssueEditTitle(t *testing.T) {
env := NewTestEnv(t)
issueNum := env.CreateTestIssue("[FGJ E2E Test] Original Title", "Will be edited")
issueNum := env.CreateTestIssue("[FJ E2E Test] Original Title", "Will be edited")
defer env.CleanupIssue(issueNum)
result := env.RunCLI(
@ -397,7 +397,7 @@ func TestCLIIssueEditTitle(t *testing.T) {
"-R", fmt.Sprintf("%s/%s", env.Owner, env.RepoName),
"issue", "edit",
fmt.Sprintf("%d", issueNum),
"-t", "[FGJ E2E Test] Updated Title",
"-t", "[FJ E2E Test] Updated Title",
)
if result.ExitCode != 0 {
@ -409,7 +409,7 @@ func TestCLIIssueEditTitle(t *testing.T) {
t.Fatalf("failed to get issue: %v", err)
}
if issue.Title != "[FGJ E2E Test] Updated Title" {
if issue.Title != "[FJ E2E Test] Updated Title" {
t.Fatalf("expected updated title, got '%s'", issue.Title)
}
@ -421,7 +421,7 @@ func TestCLIIssueEditAddLabels(t *testing.T) {
env.EnsureTestLabels()
issueNum := env.CreateTestIssue("[FGJ E2E Test] Add Labels", "Will have labels added")
issueNum := env.CreateTestIssue("[FJ E2E Test] Add Labels", "Will have labels added")
defer env.CleanupIssue(issueNum)
result := env.RunCLI(
@ -464,7 +464,7 @@ func TestCLIIssueEditRemoveLabels(t *testing.T) {
env.EnsureTestLabels()
issue, _, err := env.Client.CreateIssue(env.Owner, env.RepoName, gitea.CreateIssueOption{
Title: "[FGJ E2E Test] Remove Labels",
Title: "[FJ E2E Test] Remove Labels",
Body: "Will have labels removed",
})
if err != nil {
@ -616,7 +616,7 @@ func TestCLIPRComment(t *testing.T) {
env := NewTestEnv(t)
// PRs share the comment API with issues
issueNum := env.CreateTestIssue("[FGJ E2E Test] PR Comment Test", "Testing pr comment command")
issueNum := env.CreateTestIssue("[FJ E2E Test] PR Comment Test", "Testing pr comment command")
defer env.CleanupIssue(issueNum)
result := env.RunCLI(
@ -800,7 +800,7 @@ func TestCLIReleaseCreateUploadDelete(t *testing.T) {
env := NewTestEnv(t)
tag := fmt.Sprintf("fj-test-%d", time.Now().UnixNano())
title := "FGJ CLI Release Test"
title := "FJ CLI Release Test"
notes := "Release created by functional tests"
tmpDir := t.TempDir()
@ -1161,7 +1161,7 @@ func TestCLIAPIPostAndDelete(t *testing.T) {
"--hostname", env.Hostname,
"api", endpoint,
"-X", "POST",
"-f", "title=[FGJ E2E Test] API Post Test",
"-f", "title=[FJ E2E Test] API Post Test",
"-f", "body=Created via fj api command",
)