From e42a157dab6d2e9ece223d8ab4ed2576d79d78aa Mon Sep 17 00:00:00 2001 From: Stucki Date: Mon, 7 Jan 2019 11:00:50 -0600 Subject: [PATCH 1/9] implement --remote flag for open #460 --- cmd/open.go | 96 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/cmd/open.go b/cmd/open.go index 4d04245e8..3f4576b35 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -1,9 +1,19 @@ package cmd import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + "github.com/exercism/cli/api" "github.com/exercism/cli/browser" + "github.com/exercism/cli/config" "github.com/exercism/cli/workspace" "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" ) // openCmd opens the designated exercise in the browser. @@ -17,15 +27,95 @@ Pass the path to the directory that contains the solution you want to see on the `, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { + cfg := config.NewConfig() + + v := viper.New() + v.AddConfigPath(cfg.Dir) + v.SetConfigName("user") + v.SetConfigType("json") + // Ignore error. If the file doesn't exist, that is fine. + _ = v.ReadInConfig() + cfg.UserViperConfig = v + + return runOpen(cfg, cmd.Flags(), args) + }, +} + +func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { + var url string + + if remote, _ := flags.GetBool("remote"); remote { + usrCfg := cfg.UserViperConfig + + apiUrl := fmt.Sprintf("%s/solutions/latest", usrCfg.GetString("apibaseurl")) + + client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) + if err != nil { + return err + } + + req, err := client.NewRequest("GET", apiUrl, nil) + if err != nil { + return err + } + + q := req.URL.Query() + q.Add("exercise_id", args[0]) + if track, _ := flags.GetString("track"); track != "" { + q.Add("track_id", track) + } + req.URL.RawQuery = q.Encode() + + res, err := client.Do(req) + if err != nil { + return err + } + + defer res.Body.Close() + var payload openPayload + if err := json.NewDecoder(res.Body).Decode(&payload); err != nil { + return fmt.Errorf("unable to parse API response - %s", err) + } + + if res.StatusCode != http.StatusOK { + switch payload.Error.Type { + case "track_ambiguous": + return fmt.Errorf("%s: %s", payload.Error.Message, strings.Join(payload.Error.PossibleTrackIDs, ", ")) + default: + return errors.New(payload.Error.Message) + } + } + + url = payload.Solution.URL + } else { metadata, err := workspace.NewExerciseMetadata(args[0]) if err != nil { return err } - browser.Open(metadata.URL) - return nil - }, + url = metadata.URL + } + browser.Open(url) + return nil +} + +type openPayload struct { + Solution struct { + ID string `json:"id"` + URL string `json:"url"` + } `json:"solution"` + Error struct { + Type string `json:"type"` + Message string `json:"message"` + PossibleTrackIDs []string `json:"possible_track_ids"` + } `json:"error,omitempty"` +} + +func setupOpenFlags(flags *pflag.FlagSet) { + flags.BoolP("remote", "r", false, "checks for remote solutions") + flags.StringP("track", "t", "", "the track id") } func init() { RootCmd.AddCommand(openCmd) + setupOpenFlags(openCmd.Flags()) } From 4a7c94f1930bd12d1098bfb72f6513b4c4b5c8f6 Mon Sep 17 00:00:00 2001 From: Stucki Date: Mon, 7 Jan 2019 13:16:42 -0600 Subject: [PATCH 2/9] open command is aware of the exercism directory now you just give the exercise name instead of path --- cmd/open.go | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/cmd/open.go b/cmd/open.go index 3f4576b35..d11137508 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -43,10 +43,10 @@ Pass the path to the directory that contains the solution you want to see on the func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { var url string + usrCfg := cfg.UserViperConfig + track, _ := flags.GetString("track") if remote, _ := flags.GetBool("remote"); remote { - usrCfg := cfg.UserViperConfig - apiUrl := fmt.Sprintf("%s/solutions/latest", usrCfg.GetString("apibaseurl")) client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) @@ -61,7 +61,7 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { q := req.URL.Query() q.Add("exercise_id", args[0]) - if track, _ := flags.GetString("track"); track != "" { + if track != "" { q.Add("track_id", track) } req.URL.RawQuery = q.Encode() @@ -88,11 +88,46 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { url = payload.Solution.URL } else { - metadata, err := workspace.NewExerciseMetadata(args[0]) + ws, err := workspace.New(usrCfg.GetString("workspace")) + if err != nil { + return err + } + exercises, err := ws.Exercises() if err != nil { return err } - url = metadata.URL + + matchingExercises := make([]workspace.Exercise, 0, len(exercises)) + for _, exercise := range exercises { + if track != "" { + if exercise.Track == track && exercise.Slug == args[0] { + matchingExercises = append(matchingExercises, exercise) + } + } else if exercise.Slug == args[0] { + matchingExercises = append(matchingExercises, exercise) + } + } + + switch len(matchingExercises) { + case 0: + return fmt.Errorf("No matching exercise found") + case 1: + metaDir := matchingExercises[0].MetadataDir() + meta, err := workspace.NewExerciseMetadata(metaDir) + + if err != nil { + return err + } + + url = meta.URL + break + default: + tracks := make([]string, 0, len(matchingExercises)) + for _, exercise := range matchingExercises { + tracks = append(tracks, exercise.Track) + } + return fmt.Errorf("Please specify a track ID: %s", strings.Join(tracks, ", ")) + } } browser.Open(url) return nil From 24d789a6286257bcb50e873c0ea6e3b6235d7548 Mon Sep 17 00:00:00 2001 From: Stucki Date: Mon, 7 Jan 2019 13:22:43 -0600 Subject: [PATCH 3/9] open uses --exercise --- cmd/open.go | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/cmd/open.go b/cmd/open.go index d11137508..8cf3e5751 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -25,7 +25,6 @@ var openCmd = &cobra.Command{ Pass the path to the directory that contains the solution you want to see on the website. `, - Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { cfg := config.NewConfig() @@ -44,7 +43,12 @@ Pass the path to the directory that contains the solution you want to see on the func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { var url string usrCfg := cfg.UserViperConfig - track, _ := flags.GetString("track") + trackID, _ := flags.GetString("track") + exerciseID, _ := flags.GetString("exercise") + + if exerciseID == "" { + return fmt.Errorf("Must provide an `--exercise`") + } if remote, _ := flags.GetBool("remote"); remote { apiUrl := fmt.Sprintf("%s/solutions/latest", usrCfg.GetString("apibaseurl")) @@ -60,9 +64,9 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { } q := req.URL.Query() - q.Add("exercise_id", args[0]) - if track != "" { - q.Add("track_id", track) + q.Add("exercise_id", exerciseID) + if trackID != "" { + q.Add("track_id", trackID) } req.URL.RawQuery = q.Encode() @@ -99,11 +103,11 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { matchingExercises := make([]workspace.Exercise, 0, len(exercises)) for _, exercise := range exercises { - if track != "" { - if exercise.Track == track && exercise.Slug == args[0] { + if trackID != "" { + if exercise.Track == trackID && exercise.Slug == exerciseID { matchingExercises = append(matchingExercises, exercise) } - } else if exercise.Slug == args[0] { + } else if exercise.Slug == exerciseID { matchingExercises = append(matchingExercises, exercise) } } @@ -148,6 +152,7 @@ type openPayload struct { func setupOpenFlags(flags *pflag.FlagSet) { flags.BoolP("remote", "r", false, "checks for remote solutions") flags.StringP("track", "t", "", "the track id") + flags.StringP("exercise", "e", "", "the exercise slug") } func init() { From d3d82c475345bd975cf545adc0b9170bcd0e2a29 Mon Sep 17 00:00:00 2001 From: Stucki Date: Tue, 8 Jan 2019 09:35:27 -0600 Subject: [PATCH 4/9] open: support for --team --- cmd/open.go | 47 ++++++++++++++++++++++++++---------------- cmd/open_test.go | 1 + workspace/workspace.go | 10 +++++++++ 3 files changed, 40 insertions(+), 18 deletions(-) create mode 100644 cmd/open_test.go diff --git a/cmd/open.go b/cmd/open.go index 8cf3e5751..e3e70a4df 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -45,6 +45,7 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { usrCfg := cfg.UserViperConfig trackID, _ := flags.GetString("track") exerciseID, _ := flags.GetString("exercise") + teamID, _ := flags.GetString("team") if exerciseID == "" { return fmt.Errorf("Must provide an `--exercise`") @@ -68,6 +69,9 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { if trackID != "" { q.Add("track_id", trackID) } + if teamID != "" { + q.Add("team_id", teamID) + } req.URL.RawQuery = q.Encode() res, err := client.Do(req) @@ -101,35 +105,41 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - matchingExercises := make([]workspace.Exercise, 0, len(exercises)) + matchingExerciseMeta := make([]*workspace.ExerciseMetadata, 0, len(exercises)) for _, exercise := range exercises { - if trackID != "" { - if exercise.Track == trackID && exercise.Slug == exerciseID { - matchingExercises = append(matchingExercises, exercise) - } - } else if exercise.Slug == exerciseID { - matchingExercises = append(matchingExercises, exercise) + metaDir := exercise.MetadataDir() + meta, err := workspace.NewExerciseMetadata(metaDir) + if err != nil { + return err + } + + if meta.Exercise != exerciseID { + continue + } + + if trackID != "" && meta.Track != trackID { + continue + } + + if meta.Team != teamID { + continue } + + matchingExerciseMeta = append(matchingExerciseMeta, meta) } - switch len(matchingExercises) { + switch len(matchingExerciseMeta) { case 0: return fmt.Errorf("No matching exercise found") case 1: - metaDir := matchingExercises[0].MetadataDir() - meta, err := workspace.NewExerciseMetadata(metaDir) - - if err != nil { - return err - } - - url = meta.URL + url = matchingExerciseMeta[0].URL break default: - tracks := make([]string, 0, len(matchingExercises)) - for _, exercise := range matchingExercises { + tracks := make([]string, 0, len(matchingExerciseMeta)) + for _, exercise := range matchingExerciseMeta { tracks = append(tracks, exercise.Track) } + return fmt.Errorf("Please specify a track ID: %s", strings.Join(tracks, ", ")) } } @@ -153,6 +163,7 @@ func setupOpenFlags(flags *pflag.FlagSet) { flags.BoolP("remote", "r", false, "checks for remote solutions") flags.StringP("track", "t", "", "the track id") flags.StringP("exercise", "e", "", "the exercise slug") + flags.StringP("team", "T", "", "the team slug") } func init() { diff --git a/cmd/open_test.go b/cmd/open_test.go new file mode 100644 index 000000000..1d619dd05 --- /dev/null +++ b/cmd/open_test.go @@ -0,0 +1 @@ +package cmd diff --git a/workspace/workspace.go b/workspace/workspace.go index 71036f511..a5b6f6345 100644 --- a/workspace/workspace.go +++ b/workspace/workspace.go @@ -79,6 +79,16 @@ func (ws Workspace) PotentialExercises() ([]Exercise, error) { continue } + if topInfo.Name() == "teams" { + subInfos, _ := ioutil.ReadDir(filepath.Join(ws.Dir, "teams")) + for _, subInfo := range subInfos { + teamWs, _ := New(filepath.Join(ws.Dir, "teams", subInfo.Name())) + teamExercises, _ := teamWs.PotentialExercises() + exercises = append(exercises, teamExercises...) + } + continue + } + subInfos, err := ioutil.ReadDir(filepath.Join(ws.Dir, topInfo.Name())) if err != nil { return nil, err From 49c072deafcbe337d1e892d0117b0881a58f9e07 Mon Sep 17 00:00:00 2001 From: Stucki Date: Thu, 28 Feb 2019 08:29:59 -0600 Subject: [PATCH 5/9] open: original functionality works --- cmd/open.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/open.go b/cmd/open.go index e3e70a4df..08a3664b6 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -48,7 +48,14 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { teamID, _ := flags.GetString("team") if exerciseID == "" { - return fmt.Errorf("Must provide an `--exercise`") + // if no --exercise is given, use original functionality + metadata, err := workspace.NewExerciseMetadata(args[0]) + if err != nil { + return err + } + browser.Open(metadata.URL) + return nil + //return fmt.Errorf("Must provide an `--exercise`") } if remote, _ := flags.GetBool("remote"); remote { From a80741cbcdd3b65f3ee92b5e362155901f126fb1 Mon Sep 17 00:00:00 2001 From: Stucki Date: Thu, 28 Feb 2019 11:40:13 -0600 Subject: [PATCH 6/9] open: change meta.Exercise to meta.ExerciseSlug --- cmd/open.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/open.go b/cmd/open.go index 08a3664b6..62eb8293b 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -120,7 +120,7 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - if meta.Exercise != exerciseID { + if meta.ExerciseSlug != exerciseID { continue } From d0d0655543ad919fca43b79805102a4204ff34b4 Mon Sep 17 00:00:00 2001 From: Stucki Date: Tue, 5 Mar 2019 08:59:38 -0600 Subject: [PATCH 7/9] open: implement suggestions --- cmd/open.go | 121 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 74 insertions(+), 47 deletions(-) diff --git a/cmd/open.go b/cmd/open.go index 62eb8293b..3074eb730 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -43,36 +43,59 @@ Pass the path to the directory that contains the solution you want to see on the func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { var url string usrCfg := cfg.UserViperConfig - trackID, _ := flags.GetString("track") - exerciseID, _ := flags.GetString("exercise") - teamID, _ := flags.GetString("team") + trackID, err := flags.GetString("track") + if err != nil { + return err + } + + exerciseSlug, err := flags.GetString("exercise") + if err != nil { + return err + } + + teamID, err := flags.GetString("team") + if err != nil { + return err + } + + remote, err := flags.GetBool("remote") + if err != nil { + return err + } + + var path string + if len(args) > 0 { + path = args[0] + } - if exerciseID == "" { + if exerciseSlug == "" { + if path == "" { + return fmt.Errorf("must provide an --exercise slug or an exercise path") + } // if no --exercise is given, use original functionality - metadata, err := workspace.NewExerciseMetadata(args[0]) + metadata, err := workspace.NewExerciseMetadata(path) if err != nil { return err } browser.Open(metadata.URL) return nil - //return fmt.Errorf("Must provide an `--exercise`") } - if remote, _ := flags.GetBool("remote"); remote { - apiUrl := fmt.Sprintf("%s/solutions/latest", usrCfg.GetString("apibaseurl")) + if remote { + apiURL := fmt.Sprintf("%s/solutions/latest", usrCfg.GetString("apibaseurl")) client, err := api.NewClient(usrCfg.GetString("token"), usrCfg.GetString("apibaseurl")) if err != nil { return err } - req, err := client.NewRequest("GET", apiUrl, nil) + req, err := client.NewRequest("GET", apiURL, nil) if err != nil { return err } q := req.URL.Query() - q.Add("exercise_id", exerciseID) + q.Add("exercise_id", exerciseSlug) if trackID != "" { q.Add("track_id", trackID) } @@ -102,54 +125,58 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { } url = payload.Solution.URL - } else { - ws, err := workspace.New(usrCfg.GetString("workspace")) - if err != nil { - return err - } - exercises, err := ws.Exercises() + + browser.Open(url) + return nil + } + + ws, err := workspace.New(usrCfg.GetString("workspace")) + if err != nil { + return err + } + exercises, err := ws.Exercises() + if err != nil { + return err + } + + matchingExerciseMeta := make([]*workspace.ExerciseMetadata, 0, len(exercises)) + for _, exercise := range exercises { + metaDir := exercise.MetadataDir() + meta, err := workspace.NewExerciseMetadata(metaDir) if err != nil { return err } - matchingExerciseMeta := make([]*workspace.ExerciseMetadata, 0, len(exercises)) - for _, exercise := range exercises { - metaDir := exercise.MetadataDir() - meta, err := workspace.NewExerciseMetadata(metaDir) - if err != nil { - return err - } - - if meta.ExerciseSlug != exerciseID { - continue - } - - if trackID != "" && meta.Track != trackID { - continue - } + if meta.ExerciseSlug != exerciseSlug { + continue + } - if meta.Team != teamID { - continue - } + if trackID != "" && meta.Track != trackID { + continue + } - matchingExerciseMeta = append(matchingExerciseMeta, meta) + if meta.Team != teamID { + continue } - switch len(matchingExerciseMeta) { - case 0: - return fmt.Errorf("No matching exercise found") - case 1: - url = matchingExerciseMeta[0].URL - break - default: - tracks := make([]string, 0, len(matchingExerciseMeta)) - for _, exercise := range matchingExerciseMeta { - tracks = append(tracks, exercise.Track) - } + matchingExerciseMeta = append(matchingExerciseMeta, meta) + } - return fmt.Errorf("Please specify a track ID: %s", strings.Join(tracks, ", ")) + switch len(matchingExerciseMeta) { + case 0: + return fmt.Errorf("No matching exercise found") + case 1: + url = matchingExerciseMeta[0].URL + break + default: + tracks := make([]string, 0, len(matchingExerciseMeta)) + for _, exercise := range matchingExerciseMeta { + tracks = append(tracks, exercise.Track) } + + return fmt.Errorf("Please specify a track ID: %s", strings.Join(tracks, ", ")) } + browser.Open(url) return nil } From 76cf131ef1f406d162dc289cb86fd399f55a675e Mon Sep 17 00:00:00 2001 From: Logan Stucki Date: Wed, 21 Oct 2020 19:17:03 -0600 Subject: [PATCH 8/9] open: clean up logic and improve description --- cmd/open.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/open.go b/cmd/open.go index 3f77d1616..b1a7a55d3 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -23,7 +23,8 @@ var openCmd = &cobra.Command{ Short: "Open an exercise on the website.", Long: `Open the specified exercise to the solution page on the Exercism website. -Pass the path to the directory that contains the solution you want to see on the website. +Find local exercises by slug or team. You can also check for remote exercises. +Alternatively, you can pass a local exercise directory. `, RunE: func(cmd *cobra.Command, args []string) error { cfg := config.NewConfig() @@ -68,10 +69,11 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { path = args[0] } + if exerciseSlug == "" && path == "" { + return fmt.Errorf("must provide an --exercise slug or an exercise path") + } + if exerciseSlug == "" { - if path == "" { - return fmt.Errorf("must provide an --exercise slug or an exercise path") - } // if no --exercise is given, use original functionality metadata, err := workspace.NewExerciseMetadata(path) if err != nil { From ce6efb2c115fe23d34e49b588c84414955692b05 Mon Sep 17 00:00:00 2001 From: Logan Stucki Date: Wed, 21 Oct 2020 19:41:39 -0600 Subject: [PATCH 9/9] open: remove teams support. ref #956 --- cmd/open.go | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/cmd/open.go b/cmd/open.go index b1a7a55d3..908b81d96 100644 --- a/cmd/open.go +++ b/cmd/open.go @@ -23,8 +23,7 @@ var openCmd = &cobra.Command{ Short: "Open an exercise on the website.", Long: `Open the specified exercise to the solution page on the Exercism website. -Find local exercises by slug or team. You can also check for remote exercises. -Alternatively, you can pass a local exercise directory. +Open local or remote exercises by slug. Alternatively, you can pass a local exercise directory. `, RunE: func(cmd *cobra.Command, args []string) error { cfg := config.NewConfig() @@ -54,11 +53,6 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { return err } - teamID, err := flags.GetString("team") - if err != nil { - return err - } - remote, err := flags.GetBool("remote") if err != nil { return err @@ -100,9 +94,6 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { if trackID != "" { q.Add("track_id", trackID) } - if teamID != "" { - q.Add("team_id", teamID) - } req.URL.RawQuery = q.Encode() res, err := client.Do(req) @@ -155,10 +146,6 @@ func runOpen(cfg config.Config, flags *pflag.FlagSet, args []string) error { continue } - if meta.Team != teamID { - continue - } - matchingExerciseMeta = append(matchingExerciseMeta, meta) } @@ -196,7 +183,6 @@ func setupOpenFlags(flags *pflag.FlagSet) { flags.BoolP("remote", "r", false, "checks for remote solutions") flags.StringP("track", "t", "", "the track id") flags.StringP("exercise", "e", "", "the exercise slug") - flags.StringP("team", "T", "", "the team slug") } func init() {