- Move version out of cmd/root.go hardcode into an ldflags-injected var. Makefile derives from 'git describe --tags --always --dirty'; plain 'go build' / 'go run' get 'dev'. Release builds will get the tag via goreleaser. - Add .goreleaser.yaml: multi-platform (linux/darwin/windows/freebsd × amd64/arm64/arm) builds with SHA256 checksums, tar.gz/zip archives, forgejo release publishing. No GPG/S3 yet — deferred until a key is provisioned. - Add .gitea/workflows/release.yml to run goreleaser on tag push. Uses built-in GITEA_TOKEN with override via RELEASE_TOKEN secret. - Align CI Go version with go.mod (1.24). Previously CI ran 1.21, which would have silently missed any 1.22+ feature use. - Move itchyny/gojq from indirect to direct (it's used in api.go). Drop stale x/sys v0.33.0 entry from go.sum. - Ignore dist/ and bin/ in .gitignore. - CHANGELOG: document v0.3.1 fix and Unreleased development changes.
133 lines
3.5 KiB
Go
133 lines
3.5 KiB
Go
package cmd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"forgejo.zerova.net/public/fgj-sid/internal/git"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
var cfgFile string
|
|
var jsonErrors bool
|
|
|
|
// version is set at build time via -ldflags "-X .../cmd.version=...".
|
|
// Defaults to "dev" for plain `go build` / `go run`.
|
|
var version = "dev"
|
|
|
|
var rootCmd = &cobra.Command{
|
|
Use: "fgj",
|
|
Short: "Forgejo CLI tool - work seamlessly with Forgejo from the command line",
|
|
Long: `fgj is a command line tool for Forgejo instances (including Codeberg).
|
|
It brings pull requests, issues, and other Forgejo concepts to the terminal.`,
|
|
Version: version,
|
|
SilenceErrors: true,
|
|
}
|
|
|
|
// JSONErrors reports whether the --json-errors flag is set.
|
|
func JSONErrors() bool {
|
|
return jsonErrors
|
|
}
|
|
|
|
func Execute() error {
|
|
return rootCmd.Execute()
|
|
}
|
|
|
|
func init() {
|
|
cobra.OnInitialize(initConfig)
|
|
|
|
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.config/fgj/config.yaml)")
|
|
rootCmd.PersistentFlags().BoolVar(&jsonErrors, "json-errors", false, "output errors as structured JSON to stderr")
|
|
rootCmd.PersistentFlags().String("hostname", "", "Forgejo instance hostname")
|
|
_ = viper.BindPFlag("hostname", rootCmd.PersistentFlags().Lookup("hostname"))
|
|
}
|
|
|
|
func initConfig() {
|
|
if cfgFile != "" {
|
|
viper.SetConfigFile(cfgFile)
|
|
} else {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
fmt.Fprintln(ios.ErrOut, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
configDir := home + "/.config/fgj"
|
|
_ = os.MkdirAll(configDir, 0755)
|
|
|
|
viper.AddConfigPath(configDir)
|
|
viper.SetConfigType("yaml")
|
|
viper.SetConfigName("config")
|
|
}
|
|
|
|
viper.AutomaticEnv()
|
|
viper.SetEnvPrefix("FGJ")
|
|
|
|
_ = viper.ReadInConfig()
|
|
}
|
|
|
|
// parseRepo parses the repository string in the format "owner/name".
|
|
// If not provided, it attempts to auto-detect from the git repository.
|
|
func parseRepo(repo string) (string, string, error) {
|
|
// If repo flag is provided, use it
|
|
if repo != "" {
|
|
parts := strings.Split(repo, "/")
|
|
if len(parts) != 2 {
|
|
return "", "", fmt.Errorf("invalid repository format: %s (expected: owner/name)", repo)
|
|
}
|
|
return parts[0], parts[1], nil
|
|
}
|
|
|
|
// Try to auto-detect from git
|
|
owner, name, err := git.DetectRepo()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("repository flag is required (use -R owner/name) or run from a git repository: %w", err)
|
|
}
|
|
|
|
return owner, name, nil
|
|
}
|
|
|
|
// getDetectedHost attempts to auto-detect the Forgejo instance hostname.
|
|
// Returns empty string if detection fails, which will fall back to other methods.
|
|
func getDetectedHost() string {
|
|
host, err := git.DetectHost()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return host
|
|
}
|
|
|
|
// getCwd returns the current working directory, or "" on error.
|
|
func getCwd() string {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return cwd
|
|
}
|
|
|
|
// promptLine prints a prompt to stderr and reads a line from stdin.
|
|
func promptLine(prompt string) (string, error) {
|
|
fmt.Fprint(ios.ErrOut, prompt)
|
|
var buf [1024]byte
|
|
n, err := ios.In.Read(buf[:])
|
|
if err != nil {
|
|
return "", fmt.Errorf("reading input: %w", err)
|
|
}
|
|
return strings.TrimSpace(string(buf[:n])), nil
|
|
}
|
|
|
|
// parseIssueArg parses an issue/PR number from various formats:
|
|
// "123", "#123", "https://host/owner/repo/pulls/123", "https://host/owner/repo/issues/123"
|
|
func parseIssueArg(arg string) (int64, error) {
|
|
arg = strings.TrimPrefix(arg, "#")
|
|
// Try URL format
|
|
if strings.HasPrefix(arg, "http") {
|
|
parts := strings.Split(strings.TrimRight(arg, "/"), "/")
|
|
arg = parts[len(parts)-1]
|
|
}
|
|
return strconv.ParseInt(arg, 10, 64)
|
|
}
|