diff --git a/abspath.go b/abspath.go new file mode 100644 index 0000000..66468f8 --- /dev/null +++ b/abspath.go @@ -0,0 +1,89 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "os" + "path/filepath" + "strings" +) + +// absPath is an absolute path string. This type is intended to make it clear +// when strings are absolute paths vs something else. This does not verify or +// mutate the input, so careless callers could make instances of this type that +// are not actually absolute paths, or even "". +type absPath string + +// String returns abs as a string. +func (abs absPath) String() string { + return string(abs) +} + +// Canonical returns a canonicalized form of abs, similar to filepath.Abs +// (including filepath.Clean). Unlike filepath.Clean, this preserves "" as a +// special case. +func (abs absPath) Canonical() (absPath, error) { + if abs == "" { + return abs, nil + } + + result, err := filepath.Abs(abs.String()) + if err != nil { + return "", err + } + return absPath(result), nil +} + +// Join appends more path elements to abs, like filepath.Join. +func (abs absPath) Join(elems ...string) absPath { + all := make([]string, 0, 1+len(elems)) + all = append(all, abs.String()) + all = append(all, elems...) + return absPath(filepath.Join(all...)) +} + +// Split breaks abs into stem and leaf parts (often directory and file, but not +// necessarily), similar to filepath.Split. Unlike filepath.Split, the +// resulting stem part does not have any trailing path separators. +func (abs absPath) Split() (string, string) { + if abs == "" { + return "", "" + } + + // filepath.Split promises that dir+base == input, but trailing slashes on + // the dir is confusing and ugly. + pathSep := string(os.PathSeparator) + dir, base := filepath.Split(strings.TrimRight(abs.String(), pathSep)) + dir = strings.TrimRight(dir, pathSep) + if len(dir) == 0 { + dir = string(os.PathSeparator) + } + + return dir, base +} + +// Dir returns the stem part of abs without the leaf, like filepath.Dir. +func (abs absPath) Dir() string { + dir, _ := abs.Split() + return dir +} + +// Base returns the leaf part of abs without the stem, like filepath.Base. +func (abs absPath) Base() string { + _, base := abs.Split() + return base +} diff --git a/abspath_test.go b/abspath_test.go new file mode 100644 index 0000000..6136bc6 --- /dev/null +++ b/abspath_test.go @@ -0,0 +1,233 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "testing" +) + +func TestAbsPathString(t *testing.T) { + testCases := []string{ + "", + "/", + "//", + "/dir", + "/dir/", + "/dir//", + "/dir/sub", + "/dir/sub/", + "/dir//sub", + "/dir//sub/", + "dir", + "dir/sub", + } + + for _, tc := range testCases { + if want, got := tc, absPath(tc).String(); want != got { + t.Errorf("expected %q, got %q", want, got) + } + } +} + +func TestAbsPathCanonical(t *testing.T) { + testCases := []struct { + in absPath + exp absPath + }{{ + in: "", + exp: "", + }, { + in: "/", + exp: "/", + }, { + in: "/one", + exp: "/one", + }, { + in: "/one/two", + exp: "/one/two", + }, { + in: "/one/two/", + exp: "/one/two", + }, { + in: "/one//two", + exp: "/one/two", + }, { + in: "/one/two/../three", + exp: "/one/three", + }} + + for _, tc := range testCases { + want := tc.exp + got, err := tc.in.Canonical() + if err != nil { + t.Errorf("%q: unexpected error: %v", tc.in, err) + } else if want != got { + t.Errorf("%q: expected %q, got %q", tc.in, want, got) + } + } +} + +func TestAbsPathJoin(t *testing.T) { + testCases := []struct { + base absPath + more []string + expect absPath + }{{ + base: "/dir", + more: nil, + expect: "/dir", + }, { + base: "/dir", + more: []string{"one"}, + expect: "/dir/one", + }, { + base: "/dir", + more: []string{"one", "two"}, + expect: "/dir/one/two", + }, { + base: "/dir", + more: []string{"one", "two", "three"}, + expect: "/dir/one/two/three", + }, { + base: "/dir", + more: []string{"with/slash"}, + expect: "/dir/with/slash", + }, { + base: "/dir", + more: []string{"with/trailingslash/"}, + expect: "/dir/with/trailingslash", + }, { + base: "/dir", + more: []string{"with//twoslash"}, + expect: "/dir/with/twoslash", + }, { + base: "/dir", + more: []string{"one/1", "two/2", "three/3"}, + expect: "/dir/one/1/two/2/three/3", + }} + + for _, tc := range testCases { + if want, got := tc.expect, tc.base.Join(tc.more...); want != got { + t.Errorf("(%q, %q): expected %q, got %q", tc.base, tc.more, want, got) + } + } +} + +func TestAbsPathSplit(t *testing.T) { + testCases := []struct { + in absPath + expDir string + expBase string + }{{ + in: "", + expDir: "", + expBase: "", + }, { + in: "/", + expDir: "/", + expBase: "", + }, { + in: "//", + expDir: "/", + expBase: "", + }, { + in: "/one", + expDir: "/", + expBase: "one", + }, { + in: "/one/two", + expDir: "/one", + expBase: "two", + }, { + in: "/one/two/", + expDir: "/one", + expBase: "two", + }, { + in: "/one//two", + expDir: "/one", + expBase: "two", + }} + + for _, tc := range testCases { + wantDir, wantBase := tc.expDir, tc.expBase + if gotDir, gotBase := tc.in.Split(); wantDir != gotDir || wantBase != gotBase { + t.Errorf("%q: expected (%q, %q), got (%q, %q)", tc.in, wantDir, wantBase, gotDir, gotBase) + } + } +} + +func TestAbsPathDir(t *testing.T) { + testCases := []struct { + in absPath + exp string + }{{ + in: "", + exp: "", + }, { + in: "/", + exp: "/", + }, { + in: "/one", + exp: "/", + }, { + in: "/one/two", + exp: "/one", + }, { + in: "/one/two/", + exp: "/one", + }, { + in: "/one//two", + exp: "/one", + }} + + for _, tc := range testCases { + if want, got := tc.exp, tc.in.Dir(); want != got { + t.Errorf("%q: expected %q, got %q", tc.in, want, got) + } + } +} + +func TestAbsPathBase(t *testing.T) { + testCases := []struct { + in absPath + exp string + }{{ + in: "", + exp: "", + }, { + in: "/", + exp: "", + }, { + in: "/one", + exp: "one", + }, { + in: "/one/two", + exp: "two", + }, { + in: "/one/two/", + exp: "two", + }, { + in: "/one//two", + exp: "two", + }} + + for _, tc := range testCases { + if want, got := tc.exp, tc.in.Base(); want != got { + t.Errorf("%q: expected %q, got %q", tc.in, want, got) + } + } +} diff --git a/credential.go b/credential.go new file mode 100644 index 0000000..523aff3 --- /dev/null +++ b/credential.go @@ -0,0 +1,165 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/spf13/pflag" +) + +type credential struct { + URL string `json:"url"` + Username string `json:"username"` + Password string `json:"password,omitempty"` + PasswordFile string `json:"password-file,omitempty"` +} + +func (c credential) String() string { + jb, err := json.Marshal(c) + if err != nil { + return fmt.Sprintf("", err) + } + return string(jb) +} + +// credentialSliceValue is for flags. +type credentialSliceValue struct { + value []credential + changed bool +} + +var _ pflag.Value = &credentialSliceValue{} +var _ pflag.SliceValue = &credentialSliceValue{} + +// pflagCredentialSlice is like pflag.StringSlice() +func pflagCredentialSlice(name, def, usage string) *[]credential { + p := &credentialSliceValue{} + _ = p.Set(def) + pflag.Var(p, name, usage) + return &p.value +} + +// unmarshal is like json.Unmarshal, but fails on unknown fields. +func (cs credentialSliceValue) unmarshal(val string, out any) error { + dec := json.NewDecoder(strings.NewReader(val)) + dec.DisallowUnknownFields() + return dec.Decode(out) +} + +// decodeList handles a string-encoded JSON object. +func (cs credentialSliceValue) decodeObject(val string) (credential, error) { + var cred credential + if err := cs.unmarshal(val, &cred); err != nil { + return credential{}, err + } + return cred, nil +} + +// decodeList handles a string-encoded JSON list. +func (cs credentialSliceValue) decodeList(val string) ([]credential, error) { + var creds []credential + if err := cs.unmarshal(val, &creds); err != nil { + return nil, err + } + return creds, nil +} + +// decode handles a string-encoded JSON object or list. +func (cs credentialSliceValue) decode(val string) ([]credential, error) { + s := strings.TrimSpace(val) + if s == "" { + return nil, nil + } + // If it tastes like an object... + if s[0] == '{' { + cred, err := cs.decodeObject(s) + return []credential{cred}, err + } + // If it tastes like a list... + if s[0] == '[' { + return cs.decodeList(s) + } + // Otherwise, bad + return nil, fmt.Errorf("not a JSON object or list") +} + +func (cs *credentialSliceValue) Set(val string) error { + v, err := cs.decode(val) + if err != nil { + return err + } + + if !cs.changed { + cs.value = v + } else { + cs.value = append(cs.value, v...) + } + cs.changed = true + + return nil +} + +func (cs credentialSliceValue) Type() string { + return "credentialSlice" +} + +func (cs credentialSliceValue) String() string { + if len(cs.value) == 0 { + return "[]" + } + jb, err := json.Marshal(cs.value) + if err != nil { + return fmt.Sprintf("", err) + } + return string(jb) +} + +func (cs *credentialSliceValue) Append(val string) error { + v, err := cs.decodeObject(val) + if err != nil { + return err + } + cs.value = append(cs.value, v) + return nil +} + +func (cs *credentialSliceValue) Replace(val []string) error { + creds := []credential{} + for _, s := range val { + v, err := cs.decodeObject(s) + if err != nil { + return err + } + creds = append(creds, v) + } + cs.value = creds + return nil +} + +func (cs credentialSliceValue) GetSlice() []string { + if len(cs.value) == 0 { + return nil + } + ret := []string{} + for _, cred := range cs.value { + ret = append(ret, cred.String()) + } + return ret +} diff --git a/env.go b/env.go new file mode 100644 index 0000000..52c653c --- /dev/null +++ b/env.go @@ -0,0 +1,175 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "os" + "strconv" + "strings" + "time" +) + +func envString(def string, key string, alts ...string) string { + if val := os.Getenv(key); val != "" { + return val + } + for _, alt := range alts { + if val := os.Getenv(alt); val != "" { + fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) + return val + } + } + return def +} + +func envStringArray(def string, key string, alts ...string) []string { + parse := func(s string) []string { + return strings.Split(s, ":") + } + + if val := os.Getenv(key); val != "" { + return parse(val) + } + for _, alt := range alts { + if val := os.Getenv(alt); val != "" { + fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) + return parse(val) + } + } + return parse(def) +} + +func envBoolOrError(def bool, key string, alts ...string) (bool, error) { + parse := func(key, val string) (bool, error) { + parsed, err := strconv.ParseBool(val) + if err == nil { + return parsed, nil + } + return false, fmt.Errorf("ERROR: invalid bool env %s=%q: %w", key, val, err) + } + + if val := os.Getenv(key); val != "" { + return parse(key, val) + } + for _, alt := range alts { + if val := os.Getenv(alt); val != "" { + fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) + return parse(alt, val) + } + } + return def, nil +} +func envBool(def bool, key string, alts ...string) bool { + val, err := envBoolOrError(def, key, alts...) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + return false + } + return val +} + +func envIntOrError(def int, key string, alts ...string) (int, error) { + parse := func(key, val string) (int, error) { + parsed, err := strconv.ParseInt(val, 0, 0) + if err == nil { + return int(parsed), nil + } + return 0, fmt.Errorf("ERROR: invalid int env %s=%q: %w", key, val, err) + } + + if val := os.Getenv(key); val != "" { + return parse(key, val) + } + for _, alt := range alts { + if val := os.Getenv(alt); val != "" { + fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) + return parse(alt, val) + } + } + return def, nil +} +func envInt(def int, key string, alts ...string) int { + val, err := envIntOrError(def, key, alts...) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + return 0 + } + return val +} + +func envFloatOrError(def float64, key string, alts ...string) (float64, error) { + parse := func(key, val string) (float64, error) { + parsed, err := strconv.ParseFloat(val, 64) + if err == nil { + return parsed, nil + } + return 0, fmt.Errorf("ERROR: invalid float env %s=%q: %w", key, val, err) + } + + if val := os.Getenv(key); val != "" { + return parse(key, val) + } + for _, alt := range alts { + if val := os.Getenv(alt); val != "" { + fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) + return parse(alt, val) + } + } + return def, nil +} +func envFloat(def float64, key string, alts ...string) float64 { + val, err := envFloatOrError(def, key, alts...) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + return 0 + } + return val +} + +func envDurationOrError(def time.Duration, key string, alts ...string) (time.Duration, error) { + parse := func(key, val string) (time.Duration, error) { + parsed, err := time.ParseDuration(val) + if err == nil { + return parsed, nil + } + return 0, fmt.Errorf("ERROR: invalid duration env %s=%q: %w", key, val, err) + } + + if val := os.Getenv(key); val != "" { + return parse(key, val) + } + for _, alt := range alts { + if val := os.Getenv(alt); val != "" { + fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) + return parse(alt, val) + } + } + return def, nil +} +func envDuration(def time.Duration, key string, alts ...string) time.Duration { + val, err := envDurationOrError(def, key, alts...) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + return 0 + } + return val +} diff --git a/env_test.go b/env_test.go new file mode 100644 index 0000000..b369db2 --- /dev/null +++ b/env_test.go @@ -0,0 +1,171 @@ +/* +Copyright 2015 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "os" + "testing" + "time" +) + +const ( + testKey = "KEY" +) + +func TestEnvBool(t *testing.T) { + cases := []struct { + value string + def bool + exp bool + err bool + }{ + {"true", true, true, false}, + {"true", false, true, false}, + {"", true, true, false}, + {"", false, false, false}, + {"false", true, false, false}, + {"false", false, false, false}, + {"", true, true, false}, + {"", false, false, false}, + {"no true", false, false, true}, + {"no false", false, false, true}, + } + + for _, testCase := range cases { + os.Setenv(testKey, testCase.value) + val, err := envBoolOrError(testCase.def, testKey) + if err != nil && !testCase.err { + t.Fatalf("%q: unexpected error: %v", testCase.value, err) + } + if err == nil && testCase.err { + t.Fatalf("%q: unexpected success", testCase.value) + } + if val != testCase.exp { + t.Fatalf("%q: expected %v but %v returned", testCase.value, testCase.exp, val) + } + } +} + +func TestEnvString(t *testing.T) { + cases := []struct { + value string + def string + exp string + }{ + {"true", "true", "true"}, + {"true", "false", "true"}, + {"", "true", "true"}, + {"", "false", "false"}, + {"false", "true", "false"}, + {"false", "false", "false"}, + {"", "true", "true"}, + {"", "false", "false"}, + } + + for _, testCase := range cases { + os.Setenv(testKey, testCase.value) + val := envString(testCase.def, testKey) + if val != testCase.exp { + t.Fatalf("%q: expected %v but %v returned", testCase.value, testCase.exp, val) + } + } +} + +func TestEnvInt(t *testing.T) { + cases := []struct { + value string + def int + exp int + err bool + }{ + {"0", 1, 0, false}, + {"", 0, 0, false}, + {"-1", 0, -1, false}, + {"abcd", 0, 0, true}, + {"abcd", 0, 0, true}, + } + + for _, testCase := range cases { + os.Setenv(testKey, testCase.value) + val, err := envIntOrError(testCase.def, testKey) + if err != nil && !testCase.err { + t.Fatalf("%q: unexpected error: %v", testCase.value, err) + } + if err == nil && testCase.err { + t.Fatalf("%q: unexpected success", testCase.value) + } + if val != testCase.exp { + t.Fatalf("%q: expected %v but %v returned", testCase.value, testCase.exp, val) + } + } +} + +func TestEnvFloat(t *testing.T) { + cases := []struct { + value string + def float64 + exp float64 + err bool + }{ + {"0.5", 0, 0.5, false}, + {"", 0.5, 0.5, false}, + {"-0.5", 0, -0.5, false}, + {"abcd", 0, 0, true}, + } + + for _, testCase := range cases { + os.Setenv(testKey, testCase.value) + val, err := envFloatOrError(testCase.def, testKey) + if err != nil && !testCase.err { + t.Fatalf("%q: unexpected error: %v", testCase.value, err) + } + if err == nil && testCase.err { + t.Fatalf("%q: unexpected success", testCase.value) + } + if val != testCase.exp { + t.Fatalf("%q: expected %v but %v returned", testCase.value, testCase.exp, val) + } + } +} + +func TestEnvDuration(t *testing.T) { + cases := []struct { + value string + def time.Duration + exp time.Duration + err bool + }{ + {"1s", 0, time.Second, false}, + {"", time.Minute, time.Minute, false}, + {"1h", 0, time.Hour, false}, + {"abcd", 0, 0, true}, + } + + for _, testCase := range cases { + os.Setenv(testKey, testCase.value) + val, err := envDurationOrError(testCase.def, testKey) + if err != nil && !testCase.err { + t.Fatalf("%q: unexpected error: %v", testCase.value, err) + } + if err == nil && testCase.err { + t.Fatalf("%q: unexpected success", testCase.value) + } + if val != testCase.exp { + t.Fatalf("%q: expected %v but %v returned", testCase.value, testCase.exp, val) + } + } +} diff --git a/main.go b/main.go index fdad469..009082f 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,6 @@ package main // import "k8s.io/git-sync/cmd/git-sync" import ( "context" "crypto/md5" - "encoding/json" "errors" "fmt" "io" @@ -106,362 +105,6 @@ const ( const defaultDirMode = os.FileMode(0775) // subject to umask -type credential struct { - URL string `json:"url"` - Username string `json:"username"` - Password string `json:"password,omitempty"` - PasswordFile string `json:"password-file,omitempty"` -} - -func (c credential) String() string { - jb, err := json.Marshal(c) - if err != nil { - return fmt.Sprintf("", err) - } - return string(jb) -} - -// credentialSliceValue is for flags. -type credentialSliceValue struct { - value []credential - changed bool -} - -var _ pflag.Value = &credentialSliceValue{} -var _ pflag.SliceValue = &credentialSliceValue{} - -// pflagCredentialSlice is like pflag.StringSlice() -func pflagCredentialSlice(name, def, usage string) *[]credential { - p := &credentialSliceValue{} - _ = p.Set(def) - pflag.Var(p, name, usage) - return &p.value -} - -// unmarshal is like json.Unmarshal, but fails on unknown fields. -func (cs credentialSliceValue) unmarshal(val string, out any) error { - dec := json.NewDecoder(strings.NewReader(val)) - dec.DisallowUnknownFields() - return dec.Decode(out) -} - -// decodeList handles a string-encoded JSON object. -func (cs credentialSliceValue) decodeObject(val string) (credential, error) { - var cred credential - if err := cs.unmarshal(val, &cred); err != nil { - return credential{}, err - } - return cred, nil -} - -// decodeList handles a string-encoded JSON list. -func (cs credentialSliceValue) decodeList(val string) ([]credential, error) { - var creds []credential - if err := cs.unmarshal(val, &creds); err != nil { - return nil, err - } - return creds, nil -} - -// decode handles a string-encoded JSON object or list. -func (cs credentialSliceValue) decode(val string) ([]credential, error) { - s := strings.TrimSpace(val) - if s == "" { - return nil, nil - } - // If it tastes like an object... - if s[0] == '{' { - cred, err := cs.decodeObject(s) - return []credential{cred}, err - } - // If it tastes like a list... - if s[0] == '[' { - return cs.decodeList(s) - } - // Otherwise, bad - return nil, fmt.Errorf("not a JSON object or list") -} - -func (cs *credentialSliceValue) Set(val string) error { - v, err := cs.decode(val) - if err != nil { - return err - } - - if !cs.changed { - cs.value = v - } else { - cs.value = append(cs.value, v...) - } - cs.changed = true - - return nil -} - -func (cs credentialSliceValue) Type() string { - return "credentialSlice" -} - -func (cs credentialSliceValue) String() string { - if len(cs.value) == 0 { - return "[]" - } - jb, err := json.Marshal(cs.value) - if err != nil { - return fmt.Sprintf("", err) - } - return string(jb) -} - -func (cs *credentialSliceValue) Append(val string) error { - v, err := cs.decodeObject(val) - if err != nil { - return err - } - cs.value = append(cs.value, v) - return nil -} - -func (cs *credentialSliceValue) Replace(val []string) error { - creds := []credential{} - for _, s := range val { - v, err := cs.decodeObject(s) - if err != nil { - return err - } - creds = append(creds, v) - } - cs.value = creds - return nil -} - -func (cs credentialSliceValue) GetSlice() []string { - if len(cs.value) == 0 { - return nil - } - ret := []string{} - for _, cred := range cs.value { - ret = append(ret, cred.String()) - } - return ret -} - -func envString(def string, key string, alts ...string) string { - if val := os.Getenv(key); val != "" { - return val - } - for _, alt := range alts { - if val := os.Getenv(alt); val != "" { - fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) - return val - } - } - return def -} - -func envStringArray(def string, key string, alts ...string) []string { - parse := func(s string) []string { - return strings.Split(s, ":") - } - - if val := os.Getenv(key); val != "" { - return parse(val) - } - for _, alt := range alts { - if val := os.Getenv(alt); val != "" { - fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) - return parse(val) - } - } - return parse(def) -} - -func envBoolOrError(def bool, key string, alts ...string) (bool, error) { - parse := func(key, val string) (bool, error) { - parsed, err := strconv.ParseBool(val) - if err == nil { - return parsed, nil - } - return false, fmt.Errorf("ERROR: invalid bool env %s=%q: %w", key, val, err) - } - - if val := os.Getenv(key); val != "" { - return parse(key, val) - } - for _, alt := range alts { - if val := os.Getenv(alt); val != "" { - fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) - return parse(alt, val) - } - } - return def, nil -} -func envBool(def bool, key string, alts ...string) bool { - val, err := envBoolOrError(def, key, alts...) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - return false - } - return val -} - -func envIntOrError(def int, key string, alts ...string) (int, error) { - parse := func(key, val string) (int, error) { - parsed, err := strconv.ParseInt(val, 0, 0) - if err == nil { - return int(parsed), nil - } - return 0, fmt.Errorf("ERROR: invalid int env %s=%q: %w", key, val, err) - } - - if val := os.Getenv(key); val != "" { - return parse(key, val) - } - for _, alt := range alts { - if val := os.Getenv(alt); val != "" { - fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) - return parse(alt, val) - } - } - return def, nil -} -func envInt(def int, key string, alts ...string) int { - val, err := envIntOrError(def, key, alts...) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - return 0 - } - return val -} - -func envFloatOrError(def float64, key string, alts ...string) (float64, error) { - parse := func(key, val string) (float64, error) { - parsed, err := strconv.ParseFloat(val, 64) - if err == nil { - return parsed, nil - } - return 0, fmt.Errorf("ERROR: invalid float env %s=%q: %w", key, val, err) - } - - if val := os.Getenv(key); val != "" { - return parse(key, val) - } - for _, alt := range alts { - if val := os.Getenv(alt); val != "" { - fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) - return parse(alt, val) - } - } - return def, nil -} -func envFloat(def float64, key string, alts ...string) float64 { - val, err := envFloatOrError(def, key, alts...) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - return 0 - } - return val -} - -func envDurationOrError(def time.Duration, key string, alts ...string) (time.Duration, error) { - parse := func(key, val string) (time.Duration, error) { - parsed, err := time.ParseDuration(val) - if err == nil { - return parsed, nil - } - return 0, fmt.Errorf("ERROR: invalid duration env %s=%q: %w", key, val, err) - } - - if val := os.Getenv(key); val != "" { - return parse(key, val) - } - for _, alt := range alts { - if val := os.Getenv(alt); val != "" { - fmt.Fprintf(os.Stderr, "env %s has been deprecated, use %s instead\n", alt, key) - return parse(alt, val) - } - } - return def, nil -} -func envDuration(def time.Duration, key string, alts ...string) time.Duration { - val, err := envDurationOrError(def, key, alts...) - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - return 0 - } - return val -} - -// absPath is an absolute path string. This type is intended to make it clear -// when strings are absolute paths vs something else. This does not verify or -// mutate the input, so careless callers could make instances of this type that -// are not actually absolute paths, or even "". -type absPath string - -// String returns abs as a string. -func (abs absPath) String() string { - return string(abs) -} - -// Canonical returns a canonicalized form of abs, similar to filepath.Abs -// (including filepath.Clean). Unlike filepath.Clean, this preserves "" as a -// special case. -func (abs absPath) Canonical() (absPath, error) { - if abs == "" { - return abs, nil - } - - result, err := filepath.Abs(abs.String()) - if err != nil { - return "", err - } - return absPath(result), nil -} - -// Join appends more path elements to abs, like filepath.Join. -func (abs absPath) Join(elems ...string) absPath { - all := make([]string, 0, 1+len(elems)) - all = append(all, abs.String()) - all = append(all, elems...) - return absPath(filepath.Join(all...)) -} - -// Split breaks abs into stem and leaf parts (often directory and file, but not -// necessarily), similar to filepath.Split. Unlike filepath.Split, the -// resulting stem part does not have any trailing path separators. -func (abs absPath) Split() (string, string) { - if abs == "" { - return "", "" - } - - // filepath.Split promises that dir+base == input, but trailing slashes on - // the dir is confusing and ugly. - pathSep := string(os.PathSeparator) - dir, base := filepath.Split(strings.TrimRight(abs.String(), pathSep)) - dir = strings.TrimRight(dir, pathSep) - if len(dir) == 0 { - dir = string(os.PathSeparator) - } - - return dir, base -} - -// Dir returns the stem part of abs without the leaf, like filepath.Dir. -func (abs absPath) Dir() string { - dir, _ := abs.Split() - return dir -} - -// Base returns the leaf part of abs without the stem, like filepath.Base. -func (abs absPath) Base() string { - _, base := abs.Split() - return base -} - // repoSync represents the remote repo and the local sync of it. type repoSync struct { cmd string // the git command to run diff --git a/main_test.go b/main_test.go index 3f85bdb..a67d59d 100644 --- a/main_test.go +++ b/main_test.go @@ -27,154 +27,6 @@ import ( "go.uber.org/goleak" ) -const ( - testKey = "KEY" -) - -func TestEnvBool(t *testing.T) { - cases := []struct { - value string - def bool - exp bool - err bool - }{ - {"true", true, true, false}, - {"true", false, true, false}, - {"", true, true, false}, - {"", false, false, false}, - {"false", true, false, false}, - {"false", false, false, false}, - {"", true, true, false}, - {"", false, false, false}, - {"no true", false, false, true}, - {"no false", false, false, true}, - } - - for _, testCase := range cases { - os.Setenv(testKey, testCase.value) - val, err := envBoolOrError(testCase.def, testKey) - if err != nil && !testCase.err { - t.Fatalf("%q: unexpected error: %v", testCase.value, err) - } - if err == nil && testCase.err { - t.Fatalf("%q: unexpected success", testCase.value) - } - if val != testCase.exp { - t.Fatalf("%q: expected %v but %v returned", testCase.value, testCase.exp, val) - } - } -} - -func TestEnvString(t *testing.T) { - cases := []struct { - value string - def string - exp string - }{ - {"true", "true", "true"}, - {"true", "false", "true"}, - {"", "true", "true"}, - {"", "false", "false"}, - {"false", "true", "false"}, - {"false", "false", "false"}, - {"", "true", "true"}, - {"", "false", "false"}, - } - - for _, testCase := range cases { - os.Setenv(testKey, testCase.value) - val := envString(testCase.def, testKey) - if val != testCase.exp { - t.Fatalf("%q: expected %v but %v returned", testCase.value, testCase.exp, val) - } - } -} - -func TestEnvInt(t *testing.T) { - cases := []struct { - value string - def int - exp int - err bool - }{ - {"0", 1, 0, false}, - {"", 0, 0, false}, - {"-1", 0, -1, false}, - {"abcd", 0, 0, true}, - {"abcd", 0, 0, true}, - } - - for _, testCase := range cases { - os.Setenv(testKey, testCase.value) - val, err := envIntOrError(testCase.def, testKey) - if err != nil && !testCase.err { - t.Fatalf("%q: unexpected error: %v", testCase.value, err) - } - if err == nil && testCase.err { - t.Fatalf("%q: unexpected success", testCase.value) - } - if val != testCase.exp { - t.Fatalf("%q: expected %v but %v returned", testCase.value, testCase.exp, val) - } - } -} - -func TestEnvFloat(t *testing.T) { - cases := []struct { - value string - def float64 - exp float64 - err bool - }{ - {"0.5", 0, 0.5, false}, - {"", 0.5, 0.5, false}, - {"-0.5", 0, -0.5, false}, - {"abcd", 0, 0, true}, - } - - for _, testCase := range cases { - os.Setenv(testKey, testCase.value) - val, err := envFloatOrError(testCase.def, testKey) - if err != nil && !testCase.err { - t.Fatalf("%q: unexpected error: %v", testCase.value, err) - } - if err == nil && testCase.err { - t.Fatalf("%q: unexpected success", testCase.value) - } - if val != testCase.exp { - t.Fatalf("%q: expected %v but %v returned", testCase.value, testCase.exp, val) - } - } -} - -func TestEnvDuration(t *testing.T) { - cases := []struct { - value string - def time.Duration - exp time.Duration - err bool - }{ - {"1s", 0, time.Second, false}, - {"", time.Minute, time.Minute, false}, - {"1h", 0, time.Hour, false}, - {"abcd", 0, 0, true}, - } - - for _, testCase := range cases { - os.Setenv(testKey, testCase.value) - val, err := envDurationOrError(testCase.def, testKey) - if err != nil && !testCase.err { - t.Fatalf("%q: unexpected error: %v", testCase.value, err) - } - if err == nil && testCase.err { - t.Fatalf("%q: unexpected success", testCase.value) - } - if val != testCase.exp { - t.Fatalf("%q: expected %v but %v returned", testCase.value, testCase.exp, val) - } - } -} - func TestMakeAbsPath(t *testing.T) { cases := []struct { path string @@ -396,218 +248,6 @@ func TestParseGitConfigs(t *testing.T) { } } -func TestAbsPathString(t *testing.T) { - testCases := []string{ - "", - "/", - "//", - "/dir", - "/dir/", - "/dir//", - "/dir/sub", - "/dir/sub/", - "/dir//sub", - "/dir//sub/", - "dir", - "dir/sub", - } - - for _, tc := range testCases { - if want, got := tc, absPath(tc).String(); want != got { - t.Errorf("expected %q, got %q", want, got) - } - } -} - -func TestAbsPathCanonical(t *testing.T) { - testCases := []struct { - in absPath - exp absPath - }{{ - in: "", - exp: "", - }, { - in: "/", - exp: "/", - }, { - in: "/one", - exp: "/one", - }, { - in: "/one/two", - exp: "/one/two", - }, { - in: "/one/two/", - exp: "/one/two", - }, { - in: "/one//two", - exp: "/one/two", - }, { - in: "/one/two/../three", - exp: "/one/three", - }} - - for _, tc := range testCases { - want := tc.exp - got, err := tc.in.Canonical() - if err != nil { - t.Errorf("%q: unexpected error: %v", tc.in, err) - } else if want != got { - t.Errorf("%q: expected %q, got %q", tc.in, want, got) - } - } -} - -func TestAbsPathJoin(t *testing.T) { - testCases := []struct { - base absPath - more []string - expect absPath - }{{ - base: "/dir", - more: nil, - expect: "/dir", - }, { - base: "/dir", - more: []string{"one"}, - expect: "/dir/one", - }, { - base: "/dir", - more: []string{"one", "two"}, - expect: "/dir/one/two", - }, { - base: "/dir", - more: []string{"one", "two", "three"}, - expect: "/dir/one/two/three", - }, { - base: "/dir", - more: []string{"with/slash"}, - expect: "/dir/with/slash", - }, { - base: "/dir", - more: []string{"with/trailingslash/"}, - expect: "/dir/with/trailingslash", - }, { - base: "/dir", - more: []string{"with//twoslash"}, - expect: "/dir/with/twoslash", - }, { - base: "/dir", - more: []string{"one/1", "two/2", "three/3"}, - expect: "/dir/one/1/two/2/three/3", - }} - - for _, tc := range testCases { - if want, got := tc.expect, tc.base.Join(tc.more...); want != got { - t.Errorf("(%q, %q): expected %q, got %q", tc.base, tc.more, want, got) - } - } -} - -func TestAbsPathSplit(t *testing.T) { - testCases := []struct { - in absPath - expDir string - expBase string - }{{ - in: "", - expDir: "", - expBase: "", - }, { - in: "/", - expDir: "/", - expBase: "", - }, { - in: "//", - expDir: "/", - expBase: "", - }, { - in: "/one", - expDir: "/", - expBase: "one", - }, { - in: "/one/two", - expDir: "/one", - expBase: "two", - }, { - in: "/one/two/", - expDir: "/one", - expBase: "two", - }, { - in: "/one//two", - expDir: "/one", - expBase: "two", - }} - - for _, tc := range testCases { - wantDir, wantBase := tc.expDir, tc.expBase - if gotDir, gotBase := tc.in.Split(); wantDir != gotDir || wantBase != gotBase { - t.Errorf("%q: expected (%q, %q), got (%q, %q)", tc.in, wantDir, wantBase, gotDir, gotBase) - } - } -} - -func TestAbsPathDir(t *testing.T) { - testCases := []struct { - in absPath - exp string - }{{ - in: "", - exp: "", - }, { - in: "/", - exp: "/", - }, { - in: "/one", - exp: "/", - }, { - in: "/one/two", - exp: "/one", - }, { - in: "/one/two/", - exp: "/one", - }, { - in: "/one//two", - exp: "/one", - }} - - for _, tc := range testCases { - if want, got := tc.exp, tc.in.Dir(); want != got { - t.Errorf("%q: expected %q, got %q", tc.in, want, got) - } - } -} - -func TestAbsPathBase(t *testing.T) { - testCases := []struct { - in absPath - exp string - }{{ - in: "", - exp: "", - }, { - in: "/", - exp: "", - }, { - in: "/one", - exp: "one", - }, { - in: "/one/two", - exp: "two", - }, { - in: "/one/two/", - exp: "two", - }, { - in: "/one//two", - exp: "two", - }} - - for _, tc := range testCases { - if want, got := tc.exp, tc.in.Base(); want != got { - t.Errorf("%q: expected %q, got %q", tc.in, want, got) - } - } -} - func TestDirIsEmpty(t *testing.T) { root := absPath(t.TempDir())