Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,24 @@ Then simply run:
github-pokemon
```

#### Variables

Define reusable values under `vars` and reference them in `path` with `$NAME` or `${NAME}`:

```yaml
vars:
WORKSPACE: /my/path/here
OTHERSPACE: /testing
orgs:
- org: "my-org"
path: $WORKSPACE/$OTHERSPACE # -> /my/path/here/testing
```

- Variables can reference other variables, and fall back to environment variables (e.g. `$HOME`) if not defined under `vars`.
- A leading `~` expands to your home directory, and the result is cleaned (duplicate slashes removed).
- Undefined variables and reference cycles are reported as errors.
- Variables only apply to `path`; the file is never rewritten with expanded values.

All orgs will be processed sequentially. You can also point to a custom config file:

```bash
Expand Down
94 changes: 82 additions & 12 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ type OrgConfig struct {

// Config represents the top-level configuration file structure.
type Config struct {
Orgs []OrgConfig `yaml:"orgs"`
Parallel int `yaml:"parallel,omitempty"`
SkipUpdate bool `yaml:"skip_update,omitempty"`
Verbose bool `yaml:"verbose,omitempty"`
IncludeArchived bool `yaml:"include_archived,omitempty"`
NoColor bool `yaml:"no_color,omitempty"`
Vars map[string]string `yaml:"vars,omitempty"`
Orgs []OrgConfig `yaml:"orgs"`
Parallel int `yaml:"parallel,omitempty"`
SkipUpdate bool `yaml:"skip_update,omitempty"`
Verbose bool `yaml:"verbose,omitempty"`
IncludeArchived bool `yaml:"include_archived,omitempty"`
NoColor bool `yaml:"no_color,omitempty"`
}

// defaultConfigPath returns ~/.config/github-pokemon/config.yaml.
Expand Down Expand Up @@ -54,13 +55,19 @@ func loadConfig(path string) (Config, error) {
return Config{}, fmt.Errorf("config file %s has no orgs defined", path)
}

for i, entry := range cfg.Orgs {
for i := range cfg.Orgs {
entry := &cfg.Orgs[i]
if entry.Org == "" {
return Config{}, fmt.Errorf("config entry %d: \"org\" is required", i+1)
}
if entry.Path == "" {
return Config{}, fmt.Errorf("config entry %d: \"path\" is required", i+1)
}
expanded, err := expandPath(cfg.Vars, entry.Path)
if err != nil {
return Config{}, fmt.Errorf("config entry %d (%s): %w", i+1, entry.Org, err)
}
entry.Path = expanded
}

return cfg, nil
Expand Down Expand Up @@ -102,24 +109,87 @@ func saveConfig(path string, cfg Config) error {
return nil
}

// expandVar resolves a single variable name. Names defined in vars take
// precedence over environment variables. Variables may reference other
// variables; seen tracks the chain to detect cycles.
func expandVar(vars map[string]string, name string, seen []string) (string, error) {
for _, s := range seen {
if s == name {
return "", fmt.Errorf("variable cycle: %s -> %s", strings.Join(seen, " -> "), name)
}
}

raw, ok := vars[name]
if !ok {
if env, found := os.LookupEnv(name); found {
return env, nil
}
return "", fmt.Errorf("undefined variable %q", name)
}

return expandString(vars, raw, append(seen, name))
}

func expandString(vars map[string]string, in string, seen []string) (string, error) {
var firstErr error
out := os.Expand(in, func(name string) string {
val, err := expandVar(vars, name, seen)
if err != nil && firstErr == nil {
firstErr = err
}
return val
})
return out, firstErr
}

// expandPath expands $VAR / ${VAR} references (config vars first, then
// environment variables) and a leading ~ in a path, then cleans the result.
func expandPath(vars map[string]string, path string) (string, error) {
expanded, err := expandString(vars, path, nil)
if err != nil {
return "", fmt.Errorf("expanding path %q: %w", path, err)
}

if expanded == "~" || strings.HasPrefix(expanded, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("determining home directory: %w", err)
}
expanded = home + expanded[1:]
}

return filepath.Clean(expanded), nil
}

// configHasOrg returns true if the config already contains the given org/path pair.
// Config paths are compared after variable expansion.
func configHasOrg(cfg Config, org, path string) bool {
for _, entry := range cfg.Orgs {
if entry.Org == org && entry.Path == path {
entryPath, err := expandPath(cfg.Vars, entry.Path)
if err != nil {
entryPath = entry.Path
}
if entry.Org == org && entryPath == path {
return true
}
}
return false
}

// configLookupOrg returns the first matching OrgConfig for the given org name, or false if not found.
func configLookupOrg(cfg Config, org string) (OrgConfig, bool) {
// configLookupOrg returns the first matching OrgConfig for the given org name, with its
// path expanded, or false if not found.
func configLookupOrg(cfg Config, org string) (OrgConfig, bool, error) {
for _, entry := range cfg.Orgs {
if entry.Org == org {
return entry, true
expanded, err := expandPath(cfg.Vars, entry.Path)
if err != nil {
return OrgConfig{}, true, err
}
entry.Path = expanded
return entry, true, nil
}
}
return OrgConfig{}, false
return OrgConfig{}, false, nil
}

// promptToSaveConfig asks the user if they want to save the org/path to the config file.
Expand Down
7 changes: 5 additions & 2 deletions cmd/prune_archived.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ func runPruneRoot(cmd *cobra.Command) error {
if err != nil {
return fmt.Errorf("--path is required: could not load config: %w", err)
}
entry, found := configLookupOrg(cfg, pruneOrg)
entry, found, err := configLookupOrg(cfg, pruneOrg)
if err != nil {
return err
}
if !found {
return fmt.Errorf("org %q not found in config file %s; provide --path explicitly", pruneOrg, cfgFile)
}
Expand Down Expand Up @@ -188,4 +191,4 @@ func runPruneArchived(ctx context.Context, org string, path string) error {
}

return nil
}
}
5 changes: 4 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,10 @@ func runRoot(cmd *cobra.Command) error {
if err != nil {
return fmt.Errorf("--path is required: could not load config: %w", err)
}
entry, found := configLookupOrg(cfg, organization)
entry, found, err := configLookupOrg(cfg, organization)
if err != nil {
return err
}
if !found {
return fmt.Errorf("org %q not found in config file %s; provide --path explicitly", organization, cfgFile)
}
Expand Down
Loading