Skip to content

Commit

Permalink
feat: add flag to pass repo config as json string
Browse files Browse the repository at this point in the history
  • Loading branch information
alexbrdn committed Nov 14, 2024
1 parent 4c9a7b8 commit dc8d6af
Show file tree
Hide file tree
Showing 12 changed files with 216 additions and 219 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
- Added possibility to import file attachments to TMs and TM names
- `export`: added flag to export attachments together with TMs
- added setting/storing/detecting of attachment media types
- `check`: added `.tmc/.tmcignore` file to explicitly exclude files from being validated by `check`
- `check`: added `.tmc/.tmcignore` file to explicitly exclude files from being validated by `check`
- `repo add`, `repo config set`: added flag to pass repo config json as string

### Changed

Expand Down
37 changes: 16 additions & 21 deletions cmd/repo/repo_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,34 +13,28 @@ import (

// repoAddCmd represents the 'repo add' command
var repoAddCmd = &cobra.Command{
Use: "add [--type <type>] <name> (<config> | --file <config-file>)",
Use: "add <name> [--type <type>] ((<location> [--description <description>]) | --file <config-file> | --json <config-json>)",
Short: "Add a named repository",
Long: `Add a named repository to the tmc configuration file. Depending on the repository type,
the config may be a simple string, like directory path or a URL, or a json file. See online user documentation for details on json config file format.
--type is optional only if --file is used and the type is specified in the config file.
Long: `Add a named repository to the tmc configuration file. Using <location> is equivalent to passing a json config file with the following content:
{"type": "<type>", "loc": "<location>", "description": "<description>"}. See online user documentation for details on json config file format.
--type is optional only if --file or --json is used and the type is specified in the config file.
--file and --json override --description.
`,
Args: cobra.RangeArgs(1, 2),
Run: func(cmd *cobra.Command, args []string) {
typ, err := cmd.Flags().GetString("type")
if err != nil {
cli.Stderrf("internal error: %v", err)
os.Exit(1)
}
typ := cmd.Flag("type").Value.String()
name := args[0]
confStr := ""
if len(args) > 1 {
confStr = args[1]
}

confFile, err := cmd.Flags().GetString("file")
if err != nil {
cli.Stderrf("internal error: %v", err)
os.Exit(1)
}
confFile := cmd.Flag("file").Value.String()
jsonConf := cmd.Flag("json").Value.String()
descr := cmd.Flag("description").Value.String()

descr, _ := cmd.Flags().GetString("description")
locStr := ""
if len(args) > 1 {
locStr = args[1]
}

err = cli.RepoAdd(name, typ, confStr, confFile, descr)
err := cli.RepoAdd(name, typ, locStr, descr, jsonConf, confFile)
if err != nil {
_ = cmd.Usage()
os.Exit(1)
Expand All @@ -52,6 +46,7 @@ func init() {
repoCmd.AddCommand(repoAddCmd)
repoAddCmd.Flags().StringP("type", "t", "", fmt.Sprintf("type of repo to add. One of [%s]", strings.Join(repos.SupportedTypes, ", ")))
_ = repoAddCmd.RegisterFlagCompletionFunc("type", completion.CompleteRepoTypes)
repoAddCmd.Flags().StringP("file", "f", "", "name of the file to read repo config from")
repoAddCmd.Flags().StringP("file", "f", "", "name of the file containing the repo config")
repoAddCmd.Flags().StringP("json", "j", "", "repo config in json format")
repoAddCmd.Flags().StringP("description", "d", "", "description of the repo")
}
22 changes: 10 additions & 12 deletions cmd/repo/repo_config_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,22 @@ import (

// repoConfigSetCmd represents the 'repo config set' command
var repoConfigSetCmd = &cobra.Command{
Use: "set <name> (<config> | --file <config-file>)",
Use: "set <name> (<location> | --file <config-file> | --json <config-json>)",
Short: "Set config for a repository",
Long: `Set config of a repository. Depending on the repository type, overrides either the location string,
or the complete config in JSON format as displayed by 'repo show''.`,
Long: `Set config of a repository. Overrides either just the location string, or the entire config in JSON format as displayed by 'repo show'', except type.
The repository's type cannot be changed.`,
Args: cobra.RangeArgs(1, 2),
Run: func(cmd *cobra.Command, args []string) {
name := args[0]
confStr := ""
locStr := ""
if len(args) > 1 {
confStr = args[1]
locStr = args[1]
}

confFile, err := cmd.Flags().GetString("file")
if err != nil {
cli.Stderrf("internal error: %v", err)
os.Exit(1)
}
confFile := cmd.Flag("file").Value.String()
jsonConf := cmd.Flag("json").Value.String()

err = cli.RepoSetConfig(name, confStr, confFile)
err := cli.RepoSetConfig(name, locStr, jsonConf, confFile)
if err != nil {
_ = cmd.Usage()
os.Exit(1)
Expand All @@ -44,5 +41,6 @@ or the complete config in JSON format as displayed by 'repo show''.`,

func init() {
repoConfigCmd.AddCommand(repoConfigSetCmd)
repoConfigSetCmd.Flags().StringP("file", "f", "", "name of the file to read repo config from")
repoConfigSetCmd.Flags().StringP("file", "f", "", "name of the file containing repo config")
repoConfigSetCmd.Flags().StringP("json", "j", "", "repo config in json format")
}
15 changes: 7 additions & 8 deletions docs/_pages/50-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ all the necessary information.
## `repo add`

> Usage:
>
> tmc repo add [--type &lt;type>] <name> (&lt;config> | --file &lt;config-file-name>)
>
> tmc repo add <name> [--type &lt;type>] ((&lt;location> [--description &lt;description>]) | --file &lt;config-file> |
> --json &lt;config-json>)
>
All repos have two mandatory fields: 'type' and 'loc' (short for location), and an optional 'description' field.
The 'type' is assigned from '--type' flag and the 'loc' is assigned from \<config> argument.
All repos have two mandatory fields: 'type' and 'loc' (short for location), and an optional 'description' field.
The 'type' is assigned from '--type' flag and the 'loc' is assigned from \<location> argument.
Depending on the repo type, the exact meaning of 'loc' field is different and also other fields may be provided or may be mandatory.

When adding a repository, the entire config may be passed in form of a JSON file. See `repo show` for example.
When adding a repository, the entire config may be provided in JSON form, either by giving a file name in
\<config-file> or the entire JSON as string in \<config-json. See `repo show` for example.

### File Repositories

Expand All @@ -46,9 +48,6 @@ the requested file's path anywhere other than at the end of the URL, you can use
E.g. for an HTTP repo served by a GitLab installation, you may use ```https://example.com/api/v4/projects/<project-id>/repository/files/{% raw %}{{ID}}{% endraw %}?ref=main```
as the URL.

HTTP and TMC repositories can have authentication configured in an 'auth' field. The exact value of the field depends on the chosed authentication scheme.
Use `repo config auth` command to set the authentication parameters after a repo has been created.

### TMC Repositories

TMC repo allows to access a remotely hosted repository via TMC REST API, which is provided by `tmc serve`.
Expand Down
81 changes: 60 additions & 21 deletions internal/app/cli/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,39 +45,59 @@ func RepoList() error {
return nil
}

func RepoAdd(name, typ, confStr, confFile, descr string) error {
func RepoAdd(name, typ string, locStr, descr, jsonConf, confFile string) error {
if !repos.ValidRepoNameRegex.MatchString(name) {
Stderrf("invalid name: %v", name)
return ErrInvalidArgs
}
if confStr != "" && confFile != "" {
Stderrf("specify either <config> or --file=<configFileName>, not both")
return ErrInvalidArgs
}
if confStr == "" && confFile == "" {
Stderrf("must specify either <config> or --file=<configFileName>")
return ErrInvalidArgs
var config []byte
if locStr != "" {
if confFile != "" || jsonConf != "" {
Stderrf("<location>, --file=<config-file>, and --json=<config-json> are mutually exclusive")
return ErrInvalidArgs
}
cm := map[string]string{
repos.KeyRepoType: typ,
repos.KeyRepoLoc: locStr,
repos.KeyRepoDescription: descr,
}
config, _ = json.Marshal(cm)
}

var bytes []byte
if confFile != "" {
if locStr != "" || jsonConf != "" {
Stderrf("<location>, --file=<config-file>, and --json=<config-json> are mutually exclusive")
return ErrInvalidArgs
}
var err error
_, bytes, err = utils.ReadRequiredFile(confFile)
_, config, err = utils.ReadRequiredFile(confFile)
if err != nil {
Stderrf("cannot read file: %v", confFile)
return err
}
}
if jsonConf != "" {
if locStr != "" || confFile != "" {
Stderrf("<location>, --file=<config-file>, and --json=<config-json> are mutually exclusive")
return ErrInvalidArgs
}
config = []byte(jsonConf)
}

typ = inferType(typ, bytes)
if config == nil {
Stderrf("must specify one of: <location>, --file=<config-file>, or --json=<config-json>")
return ErrInvalidArgs
}

typ = inferType(typ, config)

if !isValidType(typ) {
Stderrf("invalid type: %v. Valid types are: %v", typ, repos.SupportedTypes)
return ErrInvalidArgs
}

rc, err := repos.NewRepoConfig(typ, confStr, bytes, descr)
rc, err := repos.NewRepoConfig(typ, config)
if err != nil {
Stderrf("cannot create repo config: %v", err)
return err
}

Expand All @@ -88,24 +108,43 @@ func RepoAdd(name, typ, confStr, confFile, descr string) error {
return err
}

func RepoSetConfig(name, confStr, confFile string) error {
func RepoSetConfig(name, locStr, jsonConf, confFile string) error {
return updateRepoConfig(name, func(conf map[string]any) (map[string]any, error) {
var bytes []byte
var configBytes []byte
if locStr != "" {
if confFile != "" || jsonConf != "" {
Stderrf("<location>, --file=<config-file>, and --json=<config-json> are mutually exclusive")
return nil, ErrInvalidArgs
}
conf[repos.KeyRepoLoc] = locStr
configBytes, _ = json.Marshal(conf)
}
if confFile != "" {
if locStr != "" || jsonConf != "" {
Stderrf("<location>, --file=<config-file>, and --json=<config-json> are mutually exclusive")
return nil, ErrInvalidArgs
}
var err error
_, bytes, err = utils.ReadRequiredFile(confFile)
_, configBytes, err = utils.ReadRequiredFile(confFile)
if err != nil {
Stderrf("cannot read file: %v", confFile)
return nil, err
}
}
if jsonConf != "" {
if locStr != "" || confFile != "" {
Stderrf("<location>, --file=<config-file>, and --json=<config-json> are mutually exclusive")
return nil, ErrInvalidArgs
}
configBytes = []byte(jsonConf)
}

rs, err := repos.ReadConfig()
if err != nil {
return nil, err
if configBytes == nil {
Stderrf("must specify one of: <location>, --file=<config-file>, or --json=<config-json>")
return nil, ErrInvalidArgs
}
oldConf := rs[name]
newConf, err := repos.NewRepoConfig(utils.JsGetStringOrEmpty(oldConf, repos.KeyRepoType), confStr, bytes, utils.JsGetStringOrEmpty(oldConf, repos.KeyRepoDescription))
typ := utils.JsGetStringOrEmpty(conf, repos.KeyRepoType)
newConf, err := repos.NewRepoConfig(typ, configBytes)
return newConf, err
})
}
Expand Down
50 changes: 19 additions & 31 deletions internal/repos/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -608,39 +608,27 @@ func (f *FileRepo) checkRootValid() error {
return nil
}

func createFileRepoConfig(dirName string, bytes []byte, descr string) (map[string]any, error) {
if dirName != "" {
absDir, err := makeAbs(dirName)
if err != nil {
return nil, err
}
return map[string]any{
KeyRepoType: RepoTypeFile,
KeyRepoLoc: absDir,
KeyRepoDescription: descr,
}, nil
} else {
rc, err := AsRepoConfig(bytes)
if err != nil {
return nil, err
}
if rType := utils.JsGetString(rc, KeyRepoType); rType != nil {
if *rType != RepoTypeFile {
return nil, fmt.Errorf("invalid json config. type must be \"file\" or absent")
}
}
rc[KeyRepoType] = RepoTypeFile
l := utils.JsGetString(rc, KeyRepoLoc)
if l == nil {
return nil, fmt.Errorf("invalid json config. must have string \"loc\"")
}
la, err := makeAbs(*l)
if err != nil {
return nil, err
func createFileRepoConfig(bytes []byte) (map[string]any, error) {
rc, err := AsRepoConfig(bytes)
if err != nil {
return nil, err
}
if rType := utils.JsGetString(rc, KeyRepoType); rType != nil {
if *rType != RepoTypeFile {
return nil, fmt.Errorf("invalid json config. type must be \"file\" or absent")
}
rc[KeyRepoLoc] = la
return rc, nil
}
rc[KeyRepoType] = RepoTypeFile
l := utils.JsGetString(rc, KeyRepoLoc)
if l == nil {
return nil, fmt.Errorf("invalid json config. must have string \"loc\"")
}
la, err := makeAbs(*l)
if err != nil {
return nil, err
}
rc[KeyRepoLoc] = la
return rc, nil
}

func makeAbs(dir string) (string, error) {
Expand Down
43 changes: 20 additions & 23 deletions internal/repos/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,44 +83,41 @@ func TestNewFileRepo(t *testing.T) {
func TestCreateFileRepoConfig(t *testing.T) {
wd, _ := os.Getwd()

descr := "test description"
tests := []struct {
strConf string
fileConf string
expRoot string
expErr bool
expDescr string
}{
{"../dir/repoName", "", filepath.Join(filepath.Dir(wd), "/dir/repoName"), false, descr},
{"./dir/repoName", "", filepath.Join(wd, "dir/repoName"), false, descr},
{"dir/repoName", "", filepath.Join(wd, "dir/repoName"), false, descr},
{"/dir/repoName", "", filepath.Join(filepath.VolumeName(wd), "/dir/repoName"), false, descr},
{".", "", filepath.Join(wd), false, descr},
{filepath.Join(wd, "dir/repoName"), "", filepath.Join(wd, "dir/repoName"), false, descr},
{"~/dir/repoName", "", "~/dir/repoName", false, descr},
{"", ``, "", true, ""},
{"", `[]`, "", true, ""},
{"", `{}`, "", true, ""},
{"", `{"loc":{}}`, "", true, ""},
{"", `{"loc":"dir/repoName"}`, filepath.Join(wd, "dir/repoName"), false, ""},
{"", `{"loc":"/dir/repoName", "description": "some description"}`, filepath.Join(filepath.VolumeName(wd), "/dir/repoName"), false, "some description"},
{"", `{"loc":"dir/repoName", "type":"http"}`, "", true, ""},
{`{"loc":"../dir/repoName"}`, filepath.Join(filepath.Dir(wd), "/dir/repoName"), false, ""},
{`{"loc":"./dir/repoName"}`, filepath.Join(wd, "dir/repoName"), false, ""},
{`{"loc":"/dir/repoName"}`, filepath.Join(filepath.VolumeName(wd), "/dir/repoName"), false, ""},
{`{"loc":"."}`, filepath.Join(wd), false, ""},
{`{"loc":"` + filepath.Join(wd, "dir/repoName") + `"}`, filepath.Join(wd, "dir/repoName"), false, ""},
{`{"loc":"~/dir/repoName"}`, "~/dir/repoName", false, ""},
{``, "", true, ""},
{`[]`, "", true, ""},
{`{}`, "", true, ""},
{`{"loc":{}}`, "", true, ""},
{`{"loc":"dir/repoName"}`, filepath.Join(wd, "dir/repoName"), false, ""},
{`{"loc":"/dir/repoName", "description": "some description"}`, filepath.Join(filepath.VolumeName(wd), "/dir/repoName"), false, "some description"},
{`{"loc":"dir/repoName", "type":"http"}`, "", true, ""},
}

for i, test := range tests {
cf, err := createFileRepoConfig(test.strConf, []byte(test.fileConf), descr)
cf, err := createFileRepoConfig([]byte(test.fileConf))
if test.expErr {
assert.Error(t, err, "error expected in test %d for %s %s", i, test.strConf, test.fileConf)
assert.Error(t, err, "error expected in test %d for %s", i, test.fileConf)
continue
} else {
assert.NoError(t, err, "no error expected in test %d for %s %s", i, test.strConf, test.fileConf)
assert.NoError(t, err, "no error expected in test %d for %s", i, test.fileConf)
}
assert.Equalf(t, "file", cf[KeyRepoType], "in test %d for %s %s", i, test.strConf, test.fileConf)
assert.Equalf(t, test.expRoot, cf[KeyRepoLoc], "in test %d for %s %s", i, test.strConf, test.fileConf)
assert.Equalf(t, "file", cf[KeyRepoType], "in test %d for %s", i, test.fileConf)
assert.Equalf(t, test.expRoot, cf[KeyRepoLoc], "in test %d for %s", i, test.fileConf)
if test.expDescr != "" {
assert.Equal(t, test.expDescr, cf[KeyRepoDescription], "in test %d for %s %s", i, test.strConf, test.fileConf)
assert.Equal(t, test.expDescr, cf[KeyRepoDescription], "in test %d for %s", i, test.fileConf)
} else {
assert.Nil(t, cf[KeyRepoDescription], "in test %d for %s %s", i, test.strConf, test.fileConf)
assert.Nil(t, cf[KeyRepoDescription], "in test %d for %s", i, test.fileConf)
}
}
}
Expand Down
Loading

0 comments on commit dc8d6af

Please sign in to comment.