diff --git a/trainings/clone.go b/trainings/clone.go index 213debd..bfcaf02 100644 --- a/trainings/clone.go +++ b/trainings/clone.go @@ -30,7 +30,7 @@ func (h *Handlers) Clone(ctx context.Context, executionID string) error { "err": err, }).Debug("Received exercise from server") - if err := h.startTraining(ctx, resp.TrainingName); err != nil { + if _, err := h.startTraining(ctx, resp.TrainingName, false); err != nil { return err } diff --git a/trainings/config/training.go b/trainings/config/training.go index 97a4b2a..f47659d 100644 --- a/trainings/config/training.go +++ b/trainings/config/training.go @@ -62,5 +62,5 @@ func (c Config) FindTrainingRoot() (string, error) { previousDir = dir } - return "", TrainingRootNotFoundError + return "", errors.WithStack(TrainingRootNotFoundError) } diff --git a/trainings/init.go b/trainings/init.go index dd8c0c9..2402894 100644 --- a/trainings/init.go +++ b/trainings/init.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path" + "path/filepath" "strings" "github.com/ThreeDotsLabs/cli/internal" @@ -23,7 +24,8 @@ func (h *Handlers) Init(ctx context.Context, trainingName string) error { "training_name": trainingName, }).Debug("Starting training") - if err := h.startTraining(ctx, trainingName); errors.Is(err, ErrInterrupted) { + trainingRoot, err := h.startTraining(ctx, trainingName, true) + if errors.Is(err, ErrInterrupted) { fmt.Println("Interrupted") return nil } else if err != nil { @@ -31,18 +33,47 @@ func (h *Handlers) Init(ctx context.Context, trainingName string) error { } // todo - handle situation when training was started but something failed here and someone is starting excersise again (because he have no local files) - _, err := h.nextExercise(ctx, "") + _, err = h.nextExercise(ctx, "", trainingRoot) if err != nil { return err } - fmt.Println("To see exercise content, please go back to " + color.CyanString(internal.WebsiteAddress)) + if !isInTrainingRoot(trainingRoot) { + fmt.Println("\nNow run " + color.CyanString("cd "+trainingName+"/") + " to enter the training workspace") + } + return nil } +func isInTrainingRoot(trainingRoot string) bool { + pwd, err := os.Getwd() + if err != nil { + logrus.WithError(err).Warn("Can't get current working directory") + return false + } + + absPwd, err := filepath.Abs(pwd) + if err != nil { + logrus.WithError(err).Warn("Can't get absolute path of current working directory") + return false + } + + absTrainingRoot, err := filepath.Abs(trainingRoot) + if err != nil { + logrus.WithError(err).Warn("Can't get absolute path of training root") + return false + } + + return absPwd == absTrainingRoot +} + var ErrInterrupted = errors.New("interrupted") -func (h *Handlers) startTraining(ctx context.Context, trainingName string) error { +func (h *Handlers) startTraining( + ctx context.Context, + trainingName string, + addTrainingNameToDir bool, +) (string, error) { var trainingRoot string alreadyExistingTrainingRoot, err := h.config.FindTrainingRoot() @@ -50,19 +81,24 @@ func (h *Handlers) startTraining(ctx context.Context, trainingName string) error fmt.Println(color.BlueString("Training was already initialised. Training root:" + alreadyExistingTrainingRoot)) trainingRoot = alreadyExistingTrainingRoot } else if !errors.Is(err, config.TrainingRootNotFoundError) { - return errors.Wrap(err, "can't check if training root exists") + return "", errors.Wrap(err, "can't check if training root exists") } else { - if err := h.showTrainingStartPrompt(); err != nil { - return err - } - wd, err := os.Getwd() if err != nil { - return errors.WithStack(err) + return "", errors.WithStack(err) + } + + if addTrainingNameToDir { + trainingRoot = path.Join(wd, trainingName) + } else { + trainingRoot = wd + } + + if err := h.showTrainingStartPrompt(trainingRoot); err != nil { + return "", err } // we will create training root in current working directory - trainingRoot = wd logrus.Debug("No training root yet") } @@ -71,13 +107,18 @@ func (h *Handlers) startTraining(ctx context.Context, trainingName string) error if alreadyExistingTrainingRoot != "" { cfg := h.config.TrainingConfig(trainingRootFs) if cfg.TrainingName != trainingName { - return fmt.Errorf( + return "", fmt.Errorf( "training %s was already started in this directory, please go to other directory and run `tdl training init`", cfg.TrainingName, ) } } else { - err = createGoWorkspace(trainingRoot) + err := os.MkdirAll(trainingRoot, 0755) + if err != nil { + return "", errors.Wrap(err, "can't create training root dir") + } + + err = createGoWorkspace(trainingRoot, trainingName) if err != nil { logrus.WithError(err).Warn("Could not create go workspace") } @@ -91,18 +132,18 @@ func (h *Handlers) startTraining(ctx context.Context, trainingName string) error }, ) if err != nil { - return errors.Wrap(err, "start training gRPC call failed") + return "", errors.Wrap(err, "start training gRPC call failed") } if err := h.config.WriteTrainingConfig(config.TrainingConfig{TrainingName: trainingName}, trainingRootFs); err != nil { - return errors.Wrap(err, "can't write training config") + return "", errors.Wrap(err, "can't write training config") } if err := writeGitignore(trainingRootFs); err != nil { - return err + return "", err } - return nil + return trainingRoot, nil } var gitignore = strings.Join( @@ -129,14 +170,19 @@ func writeGitignore(trainingRootFs *afero.BasePathFs) error { return nil } -func createGoWorkspace(trainingRoot string) error { +func createGoWorkspace(trainingRoot, trainingName string) error { cmd := exec.Command("go", "work", "init") cmd.Dir = trainingRoot - printlnCommand(".", "go work init") + printlnCommand(trainingRoot, "go work init") - if err := cmd.Run(); err != nil { - return errors.Wrap(err, "can't run go work init") + out, err := cmd.CombinedOutput() + if strings.Contains(string(out), "already exists") { + logrus.Debug("go.work already exists") + return nil + } + if err != nil { + return errors.Wrapf(err, "can't run go work init: %s", string(out)) } return nil @@ -155,7 +201,7 @@ func addModuleToWorkspace(trainingRoot string, modulePath string) error { cmd := exec.Command("go", "work", "use", modulePath) cmd.Dir = trainingRoot - printlnCommand(".", fmt.Sprintf("go work use %v", modulePath)) + printlnCommand(trainingRoot, fmt.Sprintf("go work use %v", modulePath)) if err := cmd.Run(); err != nil { return errors.Wrap(err, "can't run go work use") @@ -164,15 +210,10 @@ func addModuleToWorkspace(trainingRoot string, modulePath string) error { return nil } -func (h *Handlers) showTrainingStartPrompt() error { - pwd, err := os.Getwd() - if err != nil { - return errors.Wrap(err, "can't get wd") - } - +func (h *Handlers) showTrainingStartPrompt(trainingDir string) error { fmt.Printf( "This command will clone training source code to %s directory.\n", - pwd, + trainingDir, ) if !internal.ConfirmPromptDefaultYes("continue") { diff --git a/trainings/next.go b/trainings/next.go index 4f0692c..cce454a 100644 --- a/trainings/next.go +++ b/trainings/next.go @@ -11,14 +11,9 @@ import ( "github.com/spf13/afero" ) -func (h *Handlers) nextExercise(ctx context.Context, currentExerciseID string) (finished bool, err error) { +func (h *Handlers) nextExercise(ctx context.Context, currentExerciseID string, trainingRoot string) (finished bool, err error) { h.solutionHintDisplayed = false - trainingRoot, err := h.config.FindTrainingRoot() - if err != nil { - return false, err - } - // We should never trust the remote server. // Writing files based on external name is a vector for Path Traversal attack. // For more info please check: https://owasp.org/www-community/attacks/Path_Traversal diff --git a/trainings/reset.go b/trainings/reset.go index c30cd1d..15e23ac 100644 --- a/trainings/reset.go +++ b/trainings/reset.go @@ -5,7 +5,12 @@ import ( ) func (h *Handlers) Reset(ctx context.Context) error { - _, err := h.nextExercise(ctx, "") + trainingRoot, err := h.config.FindTrainingRoot() + if err != nil { + return err + } + + _, err = h.nextExercise(ctx, "", trainingRoot) if err != nil { return err } diff --git a/trainings/run.go b/trainings/run.go index 0fd39eb..512a3df 100644 --- a/trainings/run.go +++ b/trainings/run.go @@ -62,7 +62,12 @@ func (h *Handlers) detachedRun(ctx context.Context, trainingRootFs *afero.BasePa os.Exit(0) } - finished, err := h.nextExercise(ctx, h.config.ExerciseConfig(trainingRootFs).ExerciseID) + trainingRoot, err := h.config.FindTrainingRoot() + if err != nil { + return err + } + + finished, err := h.nextExercise(ctx, h.config.ExerciseConfig(trainingRootFs).ExerciseID, trainingRoot) if err != nil { return err } @@ -122,7 +127,12 @@ func (h *Handlers) interactiveRun(ctx context.Context, trainingRootFs *afero.Bas continue } - finished, err := h.nextExercise(ctx, h.config.ExerciseConfig(trainingRootFs).ExerciseID) + trainingRoot, err := h.config.FindTrainingRoot() + if err != nil { + return err + } + + finished, err := h.nextExercise(ctx, h.config.ExerciseConfig(trainingRootFs).ExerciseID, trainingRoot) if err != nil { return err } @@ -154,7 +164,12 @@ func (h *Handlers) run(ctx context.Context, trainingRootFs *afero.BasePathFs) (b os.Exit(0) } - _, err = h.nextExercise(ctx, "") + trainingRoot, err := h.config.FindTrainingRoot() + if err != nil { + return false, err + } + + _, err = h.nextExercise(ctx, "", trainingRoot) return true, err } @@ -271,7 +286,23 @@ func printCommand(root string, command string) { } func printlnCommand(root string, command string) { - printCommand(root, command+"\n") + pwd, err := os.Getwd() + if err != nil { + fmt.Println("Error getting current directory:", err) + return + } + + relPath, err := filepath.Rel(pwd, root) + if err != nil { + fmt.Println("Error getting relative path:", err) + return + } + + if relPath != "." && relPath != "" { + relPath = "./" + relPath + } + + printCommand(relPath, command+"\n") } func (h *Handlers) generateRunTerminalPath(trainingRootFs *afero.BasePathFs) string {