From 86a7b4812cbf79b7320b9075f0c4707c56a2df1e Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 30 Apr 2024 16:40:27 +0100 Subject: [PATCH 1/5] Refactor test command and add submission handler utility --- internal/commands/test.go | 111 +---------------------- internal/utils/submission_handler.go | 126 +++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 110 deletions(-) create mode 100644 internal/utils/submission_handler.go diff --git a/internal/commands/test.go b/internal/commands/test.go index 45af818..507b0ef 100644 --- a/internal/commands/test.go +++ b/internal/commands/test.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "io" "io/ioutil" "os" "os/exec" @@ -13,8 +12,6 @@ import ( "time" "github.com/codecrafters-io/cli/internal/utils" - logstream_consumer "github.com/codecrafters-io/logstream/consumer" - "github.com/fatih/color" "github.com/getsentry/sentry-go" cp "github.com/otiai10/copy" "github.com/rs/zerolog" @@ -115,99 +112,7 @@ func TestCommand(ctx context.Context) (err error) { logger.Debug().Msgf("submission created: %v", createSubmissionResponse.Id) - for _, message := range createSubmissionResponse.OnInitMessages { - fmt.Println("") - message.Print() - } - - if createSubmissionResponse.BuildLogstreamURL != "" { - logger.Debug().Msgf("streaming build logs from %s", createSubmissionResponse.BuildLogstreamURL) - - fmt.Println("") - err = streamLogs(createSubmissionResponse.BuildLogstreamURL) - if err != nil { - return fmt.Errorf("stream build logs: %w", err) - } - - logger.Debug().Msg("Finished streaming build logs") - logger.Debug().Msg("fetching build") - - fetchBuildResponse, err := codecraftersClient.FetchBuild(createSubmissionResponse.BuildID) - if err != nil { - // TODO: Notify sentry - red := color.New(color.FgRed).SprintFunc() - fmt.Fprintln(os.Stderr, red(err.Error())) - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, red("We couldn't fetch the results of your submission. Please try again?")) - fmt.Fprintln(os.Stderr, red("Let us know at hello@codecrafters.io if this error persists.")) - return err - } - - logger.Debug().Msgf("finished fetching build: %v", fetchBuildResponse) - red := color.New(color.FgRed).SprintFunc() - - switch fetchBuildResponse.Status { - case "failure": - fmt.Fprintln(os.Stderr, red("")) - fmt.Fprintln(os.Stderr, red("Looks like your codebase failed to build.")) - fmt.Fprintln(os.Stderr, red("If you think this is a CodeCrafters error, please let us know at hello@codecrafters.io.")) - fmt.Fprintln(os.Stderr, red("")) - os.Exit(0) - case "success": - time.Sleep(1 * time.Second) // The delay in-between build and test logs is usually 5-10 seconds, so let's buy some time - default: - red := color.New(color.FgRed).SprintFunc() - - fmt.Fprintln(os.Stderr, red("We couldn't fetch the results of your build. Please try again?")) - fmt.Fprintln(os.Stderr, red("Let us know at hello@codecrafters.io if this error persists.")) - os.Exit(1) - } - } - - fmt.Println("") - fmt.Println("Running tests. Logs should appear shortly...") - fmt.Println("") - - err = streamLogs(createSubmissionResponse.LogstreamURL) - if err != nil { - return fmt.Errorf("stream logs: %w", err) - } - - logger.Debug().Msgf("fetching submission %s", createSubmissionResponse.Id) - - fetchSubmissionResponse, err := codecraftersClient.FetchSubmission(createSubmissionResponse.Id) - if err != nil { - // TODO: Notify sentry - red := color.New(color.FgRed).SprintFunc() - fmt.Fprintln(os.Stderr, red(err.Error())) - fmt.Fprintln(os.Stderr, "") - fmt.Fprintln(os.Stderr, red("We couldn't fetch the results of your submission. Please try again?")) - fmt.Fprintln(os.Stderr, red("Let us know at hello@codecrafters.io if this error persists.")) - return err - } - - logger.Debug().Msgf("finished fetching submission, status: %s", fetchSubmissionResponse.Status) - - switch fetchSubmissionResponse.Status { - case "failure": - for _, message := range createSubmissionResponse.OnFailureMessages { - fmt.Println("") - message.Print() - } - case "success": - for _, message := range createSubmissionResponse.OnSuccessMessages { - fmt.Println("") - message.Print() - } - default: - fmt.Println("") - } - - if fetchSubmissionResponse.IsError { - return fmt.Errorf("%s", fetchSubmissionResponse.ErrorMessage) - } - - return nil + return utils.HandleSubmission(createSubmissionResponse, ctx, codecraftersClient) } func copyRepositoryDirToTempDir(repoDir string) (string, error) { @@ -272,20 +177,6 @@ func pushBranchToRemote(tmpDir string, remoteName string) error { return nil } -func streamLogs(logstreamUrl string) error { - consumer, err := logstream_consumer.NewConsumer(logstreamUrl, func(message string) {}) - if err != nil { - return fmt.Errorf("new log consumer: %w", err) - } - - _, err = io.Copy(os.Stdout, consumer) - if err != nil { - return fmt.Errorf("stream data: %w", err) - } - - return nil -} - func wrapError(err error, output []byte, msg string) error { if _, ok := err.(*exec.ExitError); ok { return fmt.Errorf("add all files: %s", output) diff --git a/internal/utils/submission_handler.go b/internal/utils/submission_handler.go new file mode 100644 index 0000000..6d600fc --- /dev/null +++ b/internal/utils/submission_handler.go @@ -0,0 +1,126 @@ +package utils + +import ( + "context" + "fmt" + "io" + "os" + "time" + + logstream_consumer "github.com/codecrafters-io/logstream/consumer" + + "github.com/fatih/color" + "github.com/rs/zerolog" +) + +func HandleSubmission(createSubmissionResponse CreateSubmissionResponse, ctx context.Context, codecraftersClient CodecraftersClient) (err error) { + logger := zerolog.Ctx(ctx) + + for _, message := range createSubmissionResponse.OnInitMessages { + fmt.Println("") + message.Print() + } + + if createSubmissionResponse.BuildLogstreamURL != "" { + logger.Debug().Msgf("streaming build logs from %s", createSubmissionResponse.BuildLogstreamURL) + + fmt.Println("") + err = streamLogs(createSubmissionResponse.BuildLogstreamURL) + if err != nil { + return fmt.Errorf("stream build logs: %w", err) + } + + logger.Debug().Msg("Finished streaming build logs") + logger.Debug().Msg("fetching build") + + fetchBuildResponse, err := codecraftersClient.FetchBuild(createSubmissionResponse.BuildID) + if err != nil { + // TODO: Notify sentry + red := color.New(color.FgRed).SprintFunc() + fmt.Fprintln(os.Stderr, red(err.Error())) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, red("We couldn't fetch the results of your submission. Please try again?")) + fmt.Fprintln(os.Stderr, red("Let us know at hello@codecrafters.io if this error persists.")) + return err + } + + logger.Debug().Msgf("finished fetching build: %v", fetchBuildResponse) + red := color.New(color.FgRed).SprintFunc() + + switch fetchBuildResponse.Status { + case "failure": + fmt.Fprintln(os.Stderr, red("")) + fmt.Fprintln(os.Stderr, red("Looks like your codebase failed to build.")) + fmt.Fprintln(os.Stderr, red("If you think this is a CodeCrafters error, please let us know at hello@codecrafters.io.")) + fmt.Fprintln(os.Stderr, red("")) + os.Exit(0) + case "success": + time.Sleep(1 * time.Second) // The delay in-between build and test logs is usually 5-10 seconds, so let's buy some time + default: + red := color.New(color.FgRed).SprintFunc() + + fmt.Fprintln(os.Stderr, red("We couldn't fetch the results of your build. Please try again?")) + fmt.Fprintln(os.Stderr, red("Let us know at hello@codecrafters.io if this error persists.")) + os.Exit(1) + } + } + + fmt.Println("") + fmt.Println("Running tests. Logs should appear shortly...") + fmt.Println("") + + err = streamLogs(createSubmissionResponse.LogstreamURL) + if err != nil { + return fmt.Errorf("stream logs: %w", err) + } + + logger.Debug().Msgf("fetching submission %s", createSubmissionResponse.Id) + + fetchSubmissionResponse, err := codecraftersClient.FetchSubmission(createSubmissionResponse.Id) + if err != nil { + // TODO: Notify sentry + red := color.New(color.FgRed).SprintFunc() + fmt.Fprintln(os.Stderr, red(err.Error())) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, red("We couldn't fetch the results of your submission. Please try again?")) + fmt.Fprintln(os.Stderr, red("Let us know at hello@codecrafters.io if this error persists.")) + return err + } + + logger.Debug().Msgf("finished fetching submission, status: %s", fetchSubmissionResponse.Status) + + switch fetchSubmissionResponse.Status { + case "failure": + for _, message := range createSubmissionResponse.OnFailureMessages { + fmt.Println("") + message.Print() + } + case "success": + for _, message := range createSubmissionResponse.OnSuccessMessages { + fmt.Println("") + message.Print() + } + default: + fmt.Println("") + } + + if fetchSubmissionResponse.IsError { + return fmt.Errorf("%s", fetchSubmissionResponse.ErrorMessage) + } + + return nil +} + +func streamLogs(logstreamUrl string) error { + consumer, err := logstream_consumer.NewConsumer(logstreamUrl, func(message string) {}) + if err != nil { + return fmt.Errorf("new log consumer: %w", err) + } + + _, err = io.Copy(os.Stdout, consumer) + if err != nil { + return fmt.Errorf("stream data: %w", err) + } + + return nil +} From f77e998156ccd0099a455831c98787ff02c4e9d2 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 30 Apr 2024 16:49:50 +0100 Subject: [PATCH 2/5] Commit message: "Add submit command and staticcheck.conf" --- internal/commands/submit.go | 111 ++++++++++++++++++++++++++++++++++++ staticcheck.conf | 4 ++ 2 files changed, 115 insertions(+) create mode 100644 internal/commands/submit.go create mode 100644 staticcheck.conf diff --git a/internal/commands/submit.go b/internal/commands/submit.go new file mode 100644 index 0000000..78ff9e9 --- /dev/null +++ b/internal/commands/submit.go @@ -0,0 +1,111 @@ +package commands + +import ( + "context" + "errors" + "fmt" + "os/exec" + "strings" + + "github.com/codecrafters-io/cli/internal/utils" + "github.com/getsentry/sentry-go" + "github.com/rs/zerolog" +) + +func SubmitCommand(ctx context.Context) (err error) { + logger := zerolog.Ctx(ctx) + + logger.Debug().Msg("submit command starts") + defer func() { + logger.Debug().Err(err).Msg("submit command ends") + }() + + defer func() { + if p := recover(); p != nil { + logger.Panic().Str("panic", fmt.Sprintf("%v", p)).Stack().Msg("panic") + sentry.CurrentHub().Recover(p) + + panic(p) + } + + if err == nil { + return + } + + var noRepo utils.NoCodecraftersRemoteFoundError + if errors.Is(err, &noRepo) { + // ignore + return + } + + sentry.CurrentHub().CaptureException(err) + }() + + logger.Debug().Msg("computing repository directory") + + repoDir, err := utils.GetRepositoryDir() + if err != nil { + return err + } + + logger.Debug().Msgf("found repository directory: %s", repoDir) + + logger.Debug().Msg("identifying remotes") + + codecraftersRemote, err := utils.IdentifyGitRemote(repoDir) + if err != nil { + return err + } + + logger.Debug().Msgf("identified remote: %s, %s", codecraftersRemote.Name, codecraftersRemote.Url) + + currentBranchName, err := getCurrentBranch(repoDir) + if err != nil { + return fmt.Errorf("get current branch: %w", err) + } + + defaultBranchName := "master" // TODO: Change when we allow customizing the defaultBranch + + if currentBranchName != defaultBranchName { + return fmt.Errorf("You need to be on the `%s` branch to run this command.", defaultBranchName) + } + + logger.Debug().Msgf("committing changes to %s", defaultBranchName) + + commitSha, err := commitChanges(repoDir, "codecrafters submit [skip ci]") + if err != nil { + return fmt.Errorf("commit changes: %w", err) + } + + // Place this before the push so that it "feels" fast + fmt.Printf("Submitting changes (commit: %s)...\n", commitSha[:7]) + + err = pushBranchToRemote(repoDir, codecraftersRemote.Name) + if err != nil { + return fmt.Errorf("push changes: %w", err) + } + + logger.Debug().Msgf("pushed changes to remote branch %s", defaultBranchName) + + codecraftersClient := utils.NewCodecraftersClient(codecraftersRemote.CodecraftersServerURL()) + + logger.Debug().Msgf("creating submission for %s", commitSha) + + createSubmissionResponse, err := codecraftersClient.CreateSubmission(codecraftersRemote.CodecraftersRepositoryId(), tempCommitSha) + if err != nil { + return fmt.Errorf("create submission: %w", err) + } + + logger.Debug().Msgf("submission created: %v", createSubmissionResponse.Id) + + return utils.HandleSubmission(createSubmissionResponse, ctx, codecraftersClient) +} + +func getCurrentBranch(repoDir string) (string, error) { + outputBytes, err := exec.Command("git", "-C", repoDir, "rev-parse", "--abbrev-ref", "HEAD").CombinedOutput() + if err != nil { + return "", wrapError(err, outputBytes, "get current branch") + } + + return strings.TrimSpace(string(outputBytes)), nil +} diff --git a/staticcheck.conf b/staticcheck.conf new file mode 100644 index 0000000..4e590f6 --- /dev/null +++ b/staticcheck.conf @@ -0,0 +1,4 @@ +checks = [ + "all", + "-ST1005" # We're okay with capitalized errors +] \ No newline at end of file From f028cd3fce0e720f901656019c8abd5dffd3f428 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 30 Apr 2024 16:50:11 +0100 Subject: [PATCH 3/5] Fix submission creation with correct commit SHA The commit message should be concise and describe the purpose of the changes. Here's a suggestion: "Fix submission creation with correct commit SHA" --- internal/commands/submit.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/commands/submit.go b/internal/commands/submit.go index 78ff9e9..02d4c19 100644 --- a/internal/commands/submit.go +++ b/internal/commands/submit.go @@ -91,7 +91,7 @@ func SubmitCommand(ctx context.Context) (err error) { logger.Debug().Msgf("creating submission for %s", commitSha) - createSubmissionResponse, err := codecraftersClient.CreateSubmission(codecraftersRemote.CodecraftersRepositoryId(), tempCommitSha) + createSubmissionResponse, err := codecraftersClient.CreateSubmission(codecraftersRemote.CodecraftersRepositoryId(), commitSha) if err != nil { return fmt.Errorf("create submission: %w", err) } From 2e6dba973337bdb50c718d21c79f3b6b6ba154f7 Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 30 Apr 2024 16:53:11 +0100 Subject: [PATCH 4/5] Refactor codecrafters command usage and add submit command --- cmd/codecrafters/main.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cmd/codecrafters/main.go b/cmd/codecrafters/main.go index 17134a4..fe91915 100644 --- a/cmd/codecrafters/main.go +++ b/cmd/codecrafters/main.go @@ -23,11 +23,13 @@ USAGE $ codecrafters [command] EXAMPLES - $ codecrafters test # Run tests without committing changes + $ codecrafters test # Run tests without committing changes + $ codecrafters submit # Commit changes & submit to move to next step COMMANDS - test: Run tests without committing changes - help: Show usage instructions + test: Run tests without committing changes + submit: Commit changes & submit to move to next step + help: Show usage instructions VERSION %s From 4d186c48349b5fce8bbd084d3775ff1aa2b6724c Mon Sep 17 00:00:00 2001 From: Paul Kuruvilla Date: Tue, 30 Apr 2024 16:53:32 +0100 Subject: [PATCH 5/5] Add test and submit commands to codecrafters main.go --- cmd/codecrafters/main.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/codecrafters/main.go b/cmd/codecrafters/main.go index fe91915..ffc77b8 100644 --- a/cmd/codecrafters/main.go +++ b/cmd/codecrafters/main.go @@ -77,6 +77,8 @@ func run() error { switch cmd { case "test": return commands.TestCommand(ctx) + case "submit": + return commands.SubmitCommand(ctx) case "help", "": // no argument flag.Usage()