Break up main.go

Add new files for abspath, credential, and env functions.
This commit is contained in:
Tim Hockin 2023-10-08 14:59:50 -07:00
parent ff51ca92dc
commit 8fa652dafa
7 changed files with 833 additions and 717 deletions

89
abspath.go Normal file
View File

@ -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
}

233
abspath_test.go Normal file
View File

@ -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)
}
}
}

165
credential.go Normal file
View File

@ -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("<encoding error: %v>", 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("<encoding error: %v>", 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
}

175
env.go Normal file
View File

@ -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
}

171
env_test.go Normal file
View File

@ -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)
}
}
}

357
main.go
View File

@ -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("<encoding error: %v>", 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("<encoding error: %v>", 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

View File

@ -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())