diff --git a/internal/backend/storage/gitfs/git.go b/internal/backend/storage/gitfs/git.go index 35f0208ddc..1ee08ba5b1 100644 --- a/internal/backend/storage/gitfs/git.go +++ b/internal/backend/storage/gitfs/git.go @@ -7,6 +7,7 @@ import ( "fmt" "os/exec" "path/filepath" + "regexp" "strconv" "strings" "time" @@ -190,6 +191,27 @@ func (g *Git) Version(ctx context.Context) semver.Version { return sv } +var reLeadingNumber = regexp.MustCompile(`^\s*(\d+)\D*`) + +func parseVersion(sv string) (semver.Version, error) { + if sv, err := semver.ParseTolerant(sv); err == nil { + return sv, nil + } + + parts := strings.SplitN(sv, ".", 3) + if len(parts) == 3 { + // try to extract the number-only prefix from the patch level + // e.g. "windwows.2" from "2.42.0.windows.2" + matches := reLeadingNumber.FindStringSubmatch(parts[2]) + if len(matches) > 0 { + parts[2] = matches[1] + sv = strings.Join(parts, ".") + } + } + + return semver.ParseTolerant(sv) +} + // IsInitialized returns true if this stores has an (probably) initialized .git folder. func (g *Git) IsInitialized() bool { return fsutil.IsFile(filepath.Join(g.fs.Path(), ".git", "config")) diff --git a/internal/backend/storage/gitfs/git_test.go b/internal/backend/storage/gitfs/git_test.go index b64acd47c7..95c6b6d443 100644 --- a/internal/backend/storage/gitfs/git_test.go +++ b/internal/backend/storage/gitfs/git_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "testing" + "github.com/blang/semver/v4" "github.com/gopasspw/gopass/internal/out" "github.com/gopasspw/gopass/pkg/ctxutil" "github.com/stretchr/testify/assert" @@ -80,3 +81,43 @@ func TestGit(t *testing.T) { assert.Equal(t, "foobar", string(content)) }) } + +func TestParseVersion(t *testing.T) { + for _, tc := range []struct { + name string + in string + sv semver.Version + wantErr bool + }{ + { + name: "empty", + in: "", + wantErr: true, + }, + { + name: "invalid", + in: "foo", + wantErr: true, + }, + { + name: "valid", + in: "2.30.0", + sv: semver.MustParse("2.30.0"), + }, + { + name: "invalid-recovered", // GH-2686 + in: "2.42.0.windows.2", + sv: semver.MustParse("2.42.0"), + }, + } { + t.Run(tc.name, func(t *testing.T) { + sv, err := parseVersion(tc.in) + assert.Equal(t, tc.sv, sv) + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/gitconfig/configs.go b/pkg/gitconfig/configs.go index 4ef859c8b9..0b0a1aa833 100644 --- a/pkg/gitconfig/configs.go +++ b/pkg/gitconfig/configs.go @@ -58,21 +58,26 @@ func (cs *Configs) Reload() { cs.LoadAll(cs.workdir) } +// String implements fmt.Stringer. +func (cs *Configs) String() string { + return fmt.Sprintf("GitConfigs{Env: %s - System: %s - Global: %s - Local: %s - Worktree: %s}", cs.EnvPrefix, cs.SystemConfig, cs.GlobalConfig, cs.LocalConfig, cs.WorktreeConfig) +} + // LoadAll tries to load all known config files. Missing or invalid files are // silently ignored. It never fails. The workdir is optional. If non-empty // this method will try to load a local config from this location. func (cs *Configs) LoadAll(workdir string) *Configs { cs.workdir = workdir - debug.Log("Loading gitconfigs for %+v ...", cs) + debug.Log("Loading gitconfigs for %s ...", cs) // load the system config, if any if os.Getenv(cs.EnvPrefix+"_NOSYSTEM") == "" { c, err := LoadConfig(cs.SystemConfig) if err != nil { - debug.Log("failed to load system config: %s", err) + debug.Log("[%s] failed to load system config: %s", cs.EnvPrefix, err) } else { - debug.Log("loaded system config from %s", cs.SystemConfig) + debug.Log("[%s] loaded system config from %s", cs.EnvPrefix, cs.SystemConfig) cs.system = c // the system config should generally not be written from gopass. // in almost any scenario gopass shouldn't have write access @@ -91,11 +96,11 @@ func (cs *Configs) LoadAll(workdir string) *Configs { localConfigPath := filepath.Join(workdir, cs.LocalConfig) c, err := LoadConfig(localConfigPath) if err != nil { - debug.Log("failed to load local config from %s: %s", localConfigPath, err) + debug.Log("[%s] failed to load local config from %s: %s", cs.EnvPrefix, localConfigPath, err) // set the path just in case we want to modify / write to it later cs.local.path = localConfigPath } else { - debug.Log("loaded local config from %s", localConfigPath) + debug.Log("[%s] loaded local config from %s", cs.EnvPrefix, localConfigPath) cs.local = c } } @@ -106,11 +111,11 @@ func (cs *Configs) LoadAll(workdir string) *Configs { worktreeConfigPath := filepath.Join(workdir, cs.WorktreeConfig) c, err := LoadConfig(worktreeConfigPath) if err != nil { - debug.Log("failed to load worktree config from %s: %s", worktreeConfigPath, err) + debug.Log("[%s] failed to load worktree config from %s: %s", cs.EnvPrefix, worktreeConfigPath, err) // set the path just in case we want to modify / write to it later cs.worktree.path = worktreeConfigPath } else { - debug.Log("loaded local config from %s", worktreeConfigPath) + debug.Log("[%s] loaded local config from %s", cs.EnvPrefix, worktreeConfigPath) cs.worktree = c } } @@ -130,14 +135,14 @@ func globalConfigFile() string { // loadGlobalConfigs will try to load the per-user (Git calls them "global") configs. // Since we might need to try different locations but only want to use the first one // it's easier to handle this in its own method. -func (c *Configs) loadGlobalConfigs() string { +func (cs *Configs) loadGlobalConfigs() string { locs := []string{ globalConfigFile(), } - if c.GlobalConfig != "" { + if cs.GlobalConfig != "" { // ~/.gitconfig - locs = append(locs, filepath.Join(appdir.UserHome(), c.GlobalConfig)) + locs = append(locs, filepath.Join(appdir.UserHome(), cs.GlobalConfig)) } for _, p := range locs { @@ -148,21 +153,21 @@ func (c *Configs) loadGlobalConfigs() string { } cfg, err := LoadConfig(p) if err != nil { - debug.Log("failed to load global config from %s", p) + debug.Log("[%s] failed to load global config from %s", cs.EnvPrefix, p) continue } - debug.Log("loaded global config from %s", p) - c.global = cfg + debug.Log("[%s] loaded global config from %s", cs.EnvPrefix, p) + cs.global = cfg return p } - debug.Log("no global config found") + debug.Log("[%s] no global config found", cs.EnvPrefix) // set the path in case we want to write to it (create it) later - c.global = &Config{ + cs.global = &Config{ path: globalConfigFile(), } @@ -170,20 +175,20 @@ func (c *Configs) loadGlobalConfigs() string { } // HasGlobalConfig indicates if a per-user config can be found. -func (c *Configs) HasGlobalConfig() bool { - return c.loadGlobalConfigs() != "" +func (cs *Configs) HasGlobalConfig() bool { + return cs.loadGlobalConfigs() != "" } // Get returns the value for the given key from the first location that is found. // Lookup order: env, worktree, local, global, system and presets. -func (c *Configs) Get(key string) string { +func (cs *Configs) Get(key string) string { for _, cfg := range []*Config{ - c.env, - c.worktree, - c.local, - c.global, - c.system, - c.Preset, + cs.env, + cs.worktree, + cs.local, + cs.global, + cs.system, + cs.Preset, } { if cfg == nil || cfg.vars == nil { continue @@ -193,21 +198,21 @@ func (c *Configs) Get(key string) string { } } - debug.Log("no value for %s found", key) + debug.Log("[%s] no value for %s found", cs.EnvPrefix, key) return "" } // GetAll returns all values for the given key from the first location that is found. // See the description of Get for more details. -func (c *Configs) GetAll(key string) []string { +func (cs *Configs) GetAll(key string) []string { for _, cfg := range []*Config{ - c.env, - c.worktree, - c.local, - c.global, - c.system, - c.Preset, + cs.env, + cs.worktree, + cs.local, + cs.global, + cs.system, + cs.Preset, } { if cfg == nil || cfg.vars == nil { continue @@ -217,50 +222,50 @@ func (c *Configs) GetAll(key string) []string { } } - debug.Log("no value for %s found", key) + debug.Log("[%s] no value for %s found", cs.EnvPrefix, key) return nil } // GetGlobal specifically ask the per-user (global) config for a key. -func (c *Configs) GetGlobal(key string) string { - if c.global == nil { +func (cs *Configs) GetGlobal(key string) string { + if cs.global == nil { return "" } - if v, found := c.global.Get(key); found { + if v, found := cs.global.Get(key); found { return v } - debug.Log("no value for %s found", key) + debug.Log("[%s] no value for %s found", cs.EnvPrefix, key) return "" } // GetLocal specifically asks the per-directory (local) config for a key. -func (c *Configs) GetLocal(key string) string { - if c.local == nil { +func (cs *Configs) GetLocal(key string) string { + if cs.local == nil { return "" } - if v, found := c.local.Get(key); found { + if v, found := cs.local.Get(key); found { return v } - debug.Log("no value for %s found", key) + debug.Log("[%s] no value for %s found", cs.EnvPrefix, key) return "" } // IsSet returns true if this key is set in any of our configs. -func (c *Configs) IsSet(key string) bool { +func (cs *Configs) IsSet(key string) bool { for _, cfg := range []*Config{ - c.env, - c.worktree, - c.local, - c.global, - c.system, - c.Preset, + cs.env, + cs.worktree, + cs.local, + cs.global, + cs.system, + cs.Preset, } { if cfg != nil && cfg.IsSet(key) { return true @@ -271,58 +276,58 @@ func (c *Configs) IsSet(key string) bool { } // SetLocal sets (or adds) a key only in the per-directory (local) config. -func (c *Configs) SetLocal(key, value string) error { - if c.local == nil { - if c.workdir == "" { +func (cs *Configs) SetLocal(key, value string) error { + if cs.local == nil { + if cs.workdir == "" { return fmt.Errorf("no workdir set") } - c.local = &Config{ - path: filepath.Join(c.workdir, c.LocalConfig), + cs.local = &Config{ + path: filepath.Join(cs.workdir, cs.LocalConfig), } } - return c.local.Set(key, value) + return cs.local.Set(key, value) } // SetGlobal sets (or adds) a key only in the per-user (global) config. -func (c *Configs) SetGlobal(key, value string) error { - if c.global == nil { - c.global = &Config{ +func (cs *Configs) SetGlobal(key, value string) error { + if cs.global == nil { + cs.global = &Config{ path: globalConfigFile(), } } - return c.global.Set(key, value) + return cs.global.Set(key, value) } // SetEnv sets (or adds) a key in the per-process (env) config. Useful // for persisting flags during the invocation. -func (c *Configs) SetEnv(key, value string) error { - if c.env == nil { - c.env = &Config{ +func (cs *Configs) SetEnv(key, value string) error { + if cs.env == nil { + cs.env = &Config{ noWrites: true, } } - return c.env.Set(key, value) + return cs.env.Set(key, value) } // UnsetLocal deletes a key from the local config. -func (c *Configs) UnsetLocal(key string) error { - if c.local == nil { +func (cs *Configs) UnsetLocal(key string) error { + if cs.local == nil { return nil } - return c.local.Unset(key) + return cs.local.Unset(key) } // UnsetGlobal deletes a key from the global config. -func (c *Configs) UnsetGlobal(key string) error { - if c.global == nil { +func (cs *Configs) UnsetGlobal(key string) error { + if cs.global == nil { return nil } - return c.global.Unset(key) + return cs.global.Unset(key) } // Keys returns a list of all keys from all available scopes. Every key has section and possibly @@ -332,16 +337,16 @@ func (c *Configs) UnsetGlobal(key string) error { // Examples // - remote.gist.gopass.pw.path -> section: remote, subsection: gist.gopass.pw, key: path // - core.timeout -> section: core, key: timeout -func (c *Configs) Keys() []string { +func (cs *Configs) Keys() []string { keys := make([]string, 0, 128) for _, cfg := range []*Config{ - c.Preset, - c.system, - c.global, - c.local, - c.worktree, - c.env, + cs.Preset, + cs.system, + cs.global, + cs.local, + cs.worktree, + cs.env, } { if cfg == nil { continue @@ -356,15 +361,15 @@ func (c *Configs) Keys() []string { // List returns all keys matching the given prefix. The prefix can be empty, // then this is identical to Keys(). -func (c *Configs) List(prefix string) []string { - return set.SortedFiltered(c.Keys(), func(k string) bool { +func (cs *Configs) List(prefix string) []string { + return set.SortedFiltered(cs.Keys(), func(k string) bool { return strings.HasPrefix(k, prefix) }) } // ListSections returns a sorted list of all sections. -func (c *Configs) ListSections() []string { - return set.Sorted(set.Apply(c.Keys(), func(k string) string { +func (cs *Configs) ListSections() []string { + return set.Sorted(set.Apply(cs.Keys(), func(k string) string { section, _, _ := splitKey(k) return section @@ -373,11 +378,11 @@ func (c *Configs) ListSections() []string { // ListSubsections returns a sorted list of all subsections // in the given section. -func (c *Configs) ListSubsections(wantSection string) []string { +func (cs *Configs) ListSubsections(wantSection string) []string { // apply extracts the subsection and matches it to the empty string // if it doesn't belong to the section we're looking for. Then the // filter func filters out any empty string. - return set.SortedFiltered(set.Apply(c.Keys(), func(k string) string { + return set.SortedFiltered(set.Apply(cs.Keys(), func(k string) string { section, subsection, _ := splitKey(k) if section != wantSection { return ""