Code Review with Git Worktree

I’ve been using a little known feature for some time now and thought it might be worthy of a post for those (like myself) who prefer the Console world of GIT.

Scenario

Para-phrasing from the Git Documentation:

You are in the middle of a refactoring session, unable to commit and you need to checkout another branch to code review, reference or something else. You might typically use git-stash to store your changes away temporarily, however, your working tree is in such a state of disarray that you don’t want to risk disturbing any of it. Use a Git Worktree.

I think we can all agree that this is an all too common scenerio?

Worktree’s are also extremely useful during Code Review. You don’t want to commit your changes, but need to checkout someone elses branch to ensure the work they’ve done actually works. Or perhaps you need to checkout the code to better understand how it works.

Worktree’s

In simple terms, a worktree represents a linked clone of your master worktree. This means its extremely fast to work with and because its generally transient/temporary, you can simply delete it once you’re done.

Workflow

So lets take a look at a typical workflow.

# git worktree add $PATH $BRANCH
# Adds a new worktree at 'path' and performs a checkout of 'branch'
# path:   prs/5.2.0
# branch: release/5.2.0

git worktree add prs/5.2.0 release/5.2.0

Hack away

Simply `cd` into the directory and hack away. The sub-folder is a complete checkout of the branch you specified and has its own `.git` folder. So any changes you make here are affected only on that branch – leaving your original branch untouched.

cd prs/5.2.0``open .

Push and Prune

If you made changes to your worktree, simply push those changes back up to your origin as usual. Then you can remove the sub-folder and prune your worktree from Git.

git push # optional
cd ../..
rm -rf prs # you can remove just the 5.2.0 folder if you have multiple worktree's
git worktree prune

List

Its important to remove your Worktree when done, because Git only allows 1 worktree per branch. If you need to remember where you created your Worktree, you can list them using:

git worktree list

Conclusion

Worktree’s are a great tool to have in your bag. Using this approach I found I’m more likely to checkout someone’s branch during Code Review to ensure it works.

View Descriptors

When building our apps, its fairly well known that passage models into our views leads to bad things. I don’t want to say its a black and white POV but its rarely a good idea or even necessary to begin with.

An often recommended approach is to use a ViewModel or Extension to add properties that provide UI representations of our models data. I’d like to suggest a slight alternative that testable and scalable even across large projects.

Example

Lets say we have a simple type to represent a Product. We might have a Model similar to the following:

struct Product {
    let title: String
    let price: Decimal
}

A product can have a title and a price. We don’t want to lose our underlying types, but at some point we need to present the price to the user in their local currency.

View Model

One solution to this problem is to introduce a View Model.

struct ProductViewModel {
    let title: String
    let price: String
}

While this approach is highly testable and allows you to abstract away the presence of the original Model, it also comes with some caveats.

  1. The name of the ViewModel is tightly coupled to the original Model itself, and so refactoring could be painful to keep these names in sync. This is the #1 issue I’ve faced myself.
  2. Often leads to some duplication where the value can be mapped directly.

Swift Extension

Another solution is to simply extend the Model itself.

extension Product {
    var priceString: String? {
        let decimal = NSDecimalNumber(decimal: product.price)
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        return formatter.string(from: decimal)
    }
}

While this approach isn’t inherently bad – as long as you keep it immutable – one of the biggest issues arises when you start naming things. You can already see that we now have a price as well as a priceString variable to make it clear that the latter returns a string representation of the price.

Descriptors

An alternative approach I’ve come up with that I’ve battle tested and had proven success across even large projects, is something I call View Descriptors.

Its basically a merge of the above concepts. Where the Descriptor is a dedicated type that’s added to your model type through an extension.

extension Product {
    struct Descriptor {
        private let product: Product

        init(product: Product) {
            self.product = product
        }
    }

    var descriptor: Descriptor {
        return Descriptor(product: self)
    }
}

Now we have a dedicated type that can better describe our model to the UI. Lets add a currency specific price value to our descriptor.

extension Product.Descriptor {
    var price: String? {
        let decimal = NSDecimalNumber(decimal: product.price)
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        return formatter.string(from: decimal)
    }
}

We can now access our model’s values as such:

let product = Product(title: "iPhone X", price: 1_000)
product.price
product.descriptor.price

// prints
// 1000
// £1,000.00

Conclusion

Compared to the approaches above, I feel this solution provides the best of both worlds, with almost none of the caveats.

  1. No need to sync type names (i.e. Product ‣ ProductVM)
  2. No unnecessary duplication
  3. API is much more descriptive – allowing variable name reuse
  4. Still highly testable API
  5. Really easy to migrate/refactor existing models in an incremental fashion

My favourite thing about this solution is that the model and the descriptors are implicitly tied together, while still providing unique interfaces for their specific domain.