Today We Rewrite History!

TheoForger - Oct 12 - - Dev Community

I promise you I'm not being dramatic. I've been literally rewriting history - more specifically, commit history! This week, we were continuing on the work on our personal project. We did some code refactoring and learned a few tricks with git rebase.

Refactor

I was extremely happy to hear that our task involved a ton of refactoring - As someone who's very particular about keeping things efficiently organized, I've come to enjoy refactoring code. I've done it myself a couple of times throughout the month of work on the mastermind project. And here are some of the things I did this week:

Extract Function

In the api::Instance module, I used to have very similar code to the API key and the base URL. And with the recent inclusion of a config file, the logic of this part got more complicated. So I decided to rework on the pattern matching syntax, since now I am much more comfortable with it. After that I extracted it as a separate function:


    fn read_from_env_or_config_file(
        envvar: &str,
        config_value: Option<&str>,
    ) -> Result<String, Box<dyn std::error::Error>> {
        match env::var(envvar) {
            Ok(key) => Ok(key),
            Err(_) => {
                if let Some(config_key) = config_value {
                    Ok(config_key.to_string())
                } else {
                    Err(format!(
                        "Could not find environment variable '{envvar}' or any related configuration"
                    )
                    .into())
                }
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

Restructure Modules

To be honest, my project was already modularized even before this week's work. However, I decided to take it one step further - In certain modules, I had a couple of related structs in one file. For example, in the clue module, I used to have:

struct Clue {
    clue_word: String,
    count: usize,
    linked_words: Vec<String>,
    source: String,
}

pub struct ClueCollection {
    clues: Vec<Clue>,
    usage: Usage,
}

impl Clue {
//  ...
}

impl ClueCollection {
//  ...
}
Enter fullscreen mode Exit fullscreen mode

To make the code even more modular and the structs more discoverable, in this example, I separated Clue and ClueCollection into two sub-modules and put them under the same parent-module.

I did the same thing for the config module, which used to contain Config and ConfigError. Eventually, I ended up with a more organized file structure:

src
├── api
│   ├── chat_completions.rs
│   ├── models.rs
│   └── mod.rs
├── clues
│   ├── clue_collection.rs
│   ├── clue.rs
│   └── mod.rs
├── configs
│   ├── config_error.rs
│   ├── config.rs
│   └── mod.rs
├── json
│   ├── chat_completions.rs
│   ├── models.rs
│   └── mod.rs
├── lib.rs
├── main.rs
├── model_collection.rs
└── tests
    ├── expected_outputs
    │   ├── chat_completions.txt
    │   └── models.txt
    ├── mock_responses
    │   ├── chat_completions.json
    │   └── models.json
    └── mod.rs

Enter fullscreen mode Exit fullscreen mode

Organize use Keywords

I used to rely on the IDE to automatically import the correct things for me. It does work, but the result was a quite messy spread of use statements. So I gave them a makeover. For example:

// External/Built-in library
use std::env;
use std::error::Error;
use clap::Parser;
use dotenv::dotenv;

// Modules from my library crate
use mastermind::{
    api, clues, configs, model_collection, read_words_from_file, write_content_to_file, Args,
};

// Components from my modules
use clues::clue_collection::ClueCollection;
use configs::config::Config;
use model_collection::ModelCollection;
Enter fullscreen mode Exit fullscreen mode

Other Chore

clippy is a fantastic Rust linter and also helps identify common mistakes. However, I only recently heard about this clippy::pedantic option you can add to it. As soon as I turned it on, I saw warnings all over my projects. Some are quite unnecessary, while others really useful. I followed the useful tips and learned a lot about standard practices in Rust. For example:

  • let x = if let Some(y) = z { y } else { ... } can simply be written as let Some(y) = z else { ... }.
  • Always prefer references for arguments than transferring ownership. If the function doesn't need ownership of an argument, take it as a reference.
  • Calling .to_string() on a String or .to_vec() on a Vec to make a clone of them technically works, but it's better to clone them explicitly using .clone()

Rebase

I really wish I knew this earlier. There have been so many situations where I messed up on my commits and thought I had to remove my whole branch and start over. rebase could've been the perfect remedy for that.

Ever since we started this course, the professor kept assuring us that our work never gets lost in git. Learning about rebase really make that idea click for me: It was fascinating to see commit history being rewritten, yet we could still find our way back to those squashed commits.

As for this week's work, it was very straightforward - I simply ran git rebase main -i on my working branch, and squashed all the little commits into one. I also used git commit --amend to refine the commit message before pushing it to GitHub.

. . . . . . . . . . . . . .