Repository: rust-lang/rustlings (Rust)

Issue:
Suggestion: Support reseting an exercise
It might be nice to be able to "reset" an exercise back to the initial state. This could be useful, for example, for people who want to use some of these exercises for "dojo-style" repetitive practice to, e.g., explore different ways of approaching a particular problem.

This could be handled outside of Rustlings through, e.g., your IDE or version control, but in both cases it would be messy to go back and reset an early exercise after having done twenty or thirty subsequent exercises.

I have no idea how complex this would be, but I assume it wouldn't be too bad? Presumably some `git` fun plus some kind of appropriate warning that you'll lose the work you have, etc., etc.

Would anyone else find this useful? Or is this just me being weird… 😜


Discussion:
This would be interesting, I imagine the API could be something like `rustlings reset errors5` or something like that. Under the hood, `git stash push <filename>` or something could be applied. 

Seems like a handy feature 
I implemented the suggestion above! Please take a look at #1151 🚀

@exdx 
@NicMcPhee 
That's awesome @magnusrodseth – thanks for adding that!
I wonder if it is possible to reset the whole project. Like rustlings reset all or smth...
> I wonder if it is possible to reset the whole project. Like rustlings reset all or smth...

`git checkout tags/LATEST_VERSION` should do the trick, no?



Patch:
diff --git a/src/main.rs b/src/main.rs
index 531533ea39..9bf75e38db 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,6 @@
 use crate::exercise::{Exercise, ExerciseList};
 use crate::project::RustAnalyzerProject;
-use crate::run::run;
+use crate::run::{reset, run};
 use crate::verify::verify;
 use argh::FromArgs;
 use console::Emoji;
@@ -47,6 +47,7 @@ enum Subcommands {
     Verify(VerifyArgs),
     Watch(WatchArgs),
     Run(RunArgs),
+    Reset(ResetArgs),
     Hint(HintArgs),
     List(ListArgs),
     Lsp(LspArgs),
@@ -71,6 +72,15 @@ struct RunArgs {
     name: String,
 }
 
+#[derive(FromArgs, PartialEq, Debug)]
+#[argh(subcommand, name = "reset")]
+/// Resets a single exercise using "git stash -- <filename>"
+struct ResetArgs {
+    #[argh(positional)]
+    /// the name of the exercise
+    name: String,
+}
+
 #[derive(FromArgs, PartialEq, Debug)]
 #[argh(subcommand, name = "hint")]
 /// Returns a hint for the given exercise
@@ -85,7 +95,6 @@ struct HintArgs {
 /// Enable rust-analyzer for exercises
 struct LspArgs {}
 
-
 #[derive(FromArgs, PartialEq, Debug)]
 #[argh(subcommand, name = "list")]
 /// Lists the exercises available in Rustlings
@@ -164,7 +173,9 @@ fn main() {
                     "Pending"
                 };
                 let solve_cond = {
-                    (e.looks_done() && subargs.solved) || (!e.looks_done() && subargs.unsolved) || (!subargs.solved && !subargs.unsolved)
+                    (e.looks_done() && subargs.solved)
+                        || (!e.looks_done() && subargs.unsolved)
+                        || (!subargs.solved && !subargs.unsolved)
                 };
                 if solve_cond && (filter_cond || subargs.filter.is_none()) {
                     let line = if subargs.paths {
@@ -205,6 +216,12 @@ fn main() {
             run(exercise, verbose).unwrap_or_else(|_| std::process::exit(1));
         }
 
+        Subcommands::Reset(subargs) => {
+            let exercise = find_exercise(&subargs.name, &exercises);
+
+            reset(exercise).unwrap_or_else(|_| std::process::exit(1));
+        }
+
         Subcommands::Hint(subargs) => {
             let exercise = find_exercise(&subargs.name, &exercises);
 
@@ -212,7 +229,8 @@ fn main() {
         }
 
         Subcommands::Verify(_subargs) => {
-            verify(&exercises, (0, exercises.len()), verbose).unwrap_or_else(|_| std::process::exit(1));
+            verify(&exercises, (0, exercises.len()), verbose)
+                .unwrap_or_else(|_| std::process::exit(1));
         }
 
         Subcommands::Lsp(_subargs) => {
@@ -236,12 +254,18 @@ fn main() {
 
         Subcommands::Watch(_subargs) => match watch(&exercises, verbose) {
             Err(e) => {
-                println!("Error: Could not watch your progress. Error message was {:?}.", e);
+                println!(
+                    "Error: Could not watch your progress. Error message was {:?}.",
+                    e
+                );
                 println!("Most likely you've run out of disk space or your 'inotify limit' has been reached.");
                 std::process::exit(1);
             }
             Ok(WatchStatus::Finished) => {
-                println!("{emoji} All exercises completed! {emoji}", emoji = Emoji("🎉", "★"));
+                println!(
+                    "{emoji} All exercises completed! {emoji}",
+                    emoji = Emoji("🎉", "★")
+                );
                 println!("\n{}\n", FENISH_LINE);
             }
             Ok(WatchStatus::Unfinished) => {
@@ -252,8 +276,10 @@ fn main() {
     }
 }
 
-
-fn spawn_watch_shell(failed_exercise_hint: &Arc<Mutex<Option<String>>>, should_quit: Arc<AtomicBool>) {
+fn spawn_watch_shell(
+    failed_exercise_hint: &Arc<Mutex<Option<String>>>,
+    should_quit: Arc<AtomicBool>,
+) {
     let failed_exercise_hint = Arc::clone(failed_exercise_hint);
     println!("Welcome to watch mode! You can type 'help' to get an overview of the commands you can use here.");
     thread::spawn(move || loop {
@@ -290,16 +316,22 @@ fn spawn_watch_shell(failed_exercise_hint: &Arc<Mutex<Option<String>>>, should_q
 
 fn find_exercise<'a>(name: &str, exercises: &'a [Exercise]) -> &'a Exercise {
     if name.eq("next") {
-        exercises.iter().find(|e| !e.looks_done()).unwrap_or_else(|| {
-            println!("🎉 Congratulations! You have done all the exercises!");
-            println!("🔚 There are no more exercises to do next!");
-            std::process::exit(1)
-        })
+        exercises
+            .iter()
+            .find(|e| !e.looks_done())
+            .unwrap_or_else(|| {
+                println!("🎉 Congratulations! You have done all the exercises!");
+                println!("🔚 There are no more exercises to do next!");
+                std::process::exit(1)
+            })
     } else {
-        exercises.iter().find(|e| e.name == name).unwrap_or_else(|| {
-            println!("No exercise found for '{}'!", name);
-            std::process::exit(1)
-        })
+        exercises
+            .iter()
+            .find(|e| e.name == name)
+            .unwrap_or_else(|| {
+                println!("No exercise found for '{}'!", name);
+                std::process::exit(1)
+            })
     }
 }
 
@@ -337,8 +369,13 @@ fn watch(exercises: &[Exercise], verbose: bool) -> notify::Result<WatchStatus> {
                         let filepath = b.as_path().canonicalize().unwrap();
                         let pending_exercises = exercises
                             .iter()
-                            .find(|e| filepath.ends_with(&e.path)).into_iter()
-                            .chain(exercises.iter().filter(|e| !e.looks_done() && !filepath.ends_with(&e.path)));
+                            .find(|e| filepath.ends_with(&e.path))
+                            .into_iter()
+                            .chain(
+                                exercises
+                                    .iter()
+                                    .filter(|e| !e.looks_done() && !filepath.ends_with(&e.path)),
+                            );
                         let num_done = exercises.iter().filter(|e| e.looks_done()).count();
                         clear_screen();
                         match verify(pending_exercises, (num_done, exercises.len()), verbose) {
diff --git a/src/run.rs b/src/run.rs
index eb558850fd..826f00a638 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -1,3 +1,5 @@
+use std::process::Command;
+
 use crate::exercise::{Exercise, Mode};
 use crate::verify::test;
 use indicatif::ProgressBar;
@@ -15,6 +17,19 @@ pub fn run(exercise: &Exercise, verbose: bool) -> Result<(), ()> {
     Ok(())
 }
 
+// Resets the exercise by stashing the changes.
+pub fn reset(exercise: &Exercise) -> Result<(), ()> {
+    let command = Command::new("git")
+        .args(["stash", "--"])
+        .arg(&exercise.path)
+        .spawn();
+
+    match command {
+        Ok(_) => Ok(()),
+        Err(_) => Err(()),
+    }
+}
+
 // Invoke the rust compiler on the path of the given exercise
 // and run the ensuing binary.
 // This is strictly for non-test binaries, so output is displayed


Test patch:
diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs
index 0be191f0fe..1a729232f6 100644
--- a/tests/integration_tests.rs
+++ b/tests/integration_tests.rs
@@ -110,6 +110,27 @@ fn run_single_test_no_exercise() {
         .code(1);
 }
 
+#[test]
+fn reset_single_exercise() {
+    Command::cargo_bin("rustlings")
+        .unwrap()
+        .args(&["reset", "intro1"])
+        .assert()
+        .code(0);
+}
+
+#[test]
+fn reset_no_exercise() {
+    Command::cargo_bin("rustlings")
+        .unwrap()
+        .arg("reset")
+        .assert()
+        .code(1)
+        .stderr(predicates::str::contains(
+            "positional arguments not provided",
+        ));
+}
+
 #[test]
 fn get_hint_for_single_test() {
     Command::cargo_bin("rustlings")
@@ -126,7 +147,7 @@ fn all_exercises_require_confirmation() {
     for exercise in glob("exercises/**/*.rs").unwrap() {
         let path = exercise.unwrap();
         if path.file_name().unwrap() == "mod.rs" {
-            continue
+            continue;
         }
         let source = {
             let mut file = File::open(&path).unwrap();