3 Git Tips to Improve Medical Device Software
Medical device software is very diverse. From low-level firmware drivers to data analysis and AI, to web development and cybersecurity, the technologies are wide ranging, each with their own quirks. In all this variety, one technology is ubiquitous: Git!
Your version control system (VCS) is always there, and it’s worth taking a bit of time to really get to know it. If you go beyond the basics, Git will help you develop medical device software faster without sacrificing traceability.
Medical device software requires extra layers of accountability above and beyond the usual software best practices. One example is related to configuration management and maintaining a record of what changed between software versions, which implies keeping a clear history of the software’s development.
You need to put in a bit of time to make your own mental model for how Git works to make the most of it. Let’s dive into three eye-opening realizations which help me use Git better.
You Don’t Have to Commit Everything!
At its best, Git gets out of the way. The place I want to be as a programmer is in “code mode” – where I lose track of time hacking away at a new feature, or bug fix, or maybe both. There is a tension here between getting the code written and building a sensible history for future reviewers. The last thing I want to do is interrupt my programming to think about VCS and how changes are going to look for a reviewer. After all, it’s good practice to make commits small and self-contained.
Say you get carried away programming and made a couple unrelated changes without committing any of them. What do you do? Git separates the “working copy” – where all local, uncommitted changes are – from the “staging area” – where you add changes you want to be committed.
Here’s an example to illustrate how to use the staging area when working on a project with an SPI driver, a display driver, and some business logic. Maybe the sources are organized like this:
|– Drivers/
| |– spi.c
| |– spi.h
| |– display.c
| |– display.h
|– App/
| |– app.c
|– …
You have been happily building out your SPI driver, but on the way, you have an idea for rendering pages more smoothly on your display. You drop in a few changes to your display driver, and now you have changes in both spi.c and display.c. If you commit it all at once (“git add .”) then you’re tangling up changes that aren’t related, which isn’t good git hygiene. What if you introduced a bug while trying to be clever with your display driver? You’d have to revert this patch and then pick out the parts that are only related to SPI to save them from being dropped. Gnarly!
Instead, use Git’s powerful staging area to tease out atomic changes from the working copy. By adding only changes related to SPI first, committing those, then adding display driver changes, you have created two commits which are nice and independent, but which come from the same working copy!
git add Drivers/spi.c
git commit …
git add Drivers/display.c
git commit …
Notice how you don’t have to think about what your next commit will be before you make the changes. Instead, you can get back to coding and build commits from your changes later! Git doesn’t punish you for getting lost in the programming; it helps you pick up the pieces after a caffeine-fuelled bug fixing session.
When the atomic changes are localized to individual files, teasing independent changes into different commits is easy. But what if there are changes in app.c which apply to both SPI and display changes? Once again, Git has it covered.
Adding Individual Changes With –patch
Learning about the staging area is one thing, but realizing that you can filter through changes from within a single file opens a new world of capability. The “git add” command has a “–patch/-p” option which takes you hunk-by-hunk through the files you’ve passed to it, letting you pull in only what you need for the commit. You can get very specific with additions, splitting provided hunks and even hand-picking lines by editing them. A bonus of building commits hunk-by-hunk is that you’ll probably write a better commit message, because the changes will be fresh in your mind.
Tip: use the “singleKey” config option to make navigating hunks faster (git config –global interactive.singleKey true). You’ll need the Term::ReadKey module for this to work.
The “–patch” option is available on more commands than just “add”. Want to toss one particularly passive aggressive comment from a file? “git reset –patch”! Or want to save just a line or two to your stash for later? “git stash –patch”!
Git History
“History will be kind to me, for I intend to write it.”
- You, opening your next PR
Now that you’re easily putting atomic commits together, you can look at the bigger picture of the story you are telling with Git history. For example, you’ve been working on a new feature for a of couple days in the familiar “two steps forward, one step back” pattern of working new functionality into the software.
You introduce a new module to your codebase, but inadvertently cause a bug somewhere else. So, you fix this bug and commit those changes. This pattern of introducing new code and then fixing bugs or regressions you previously introduced continues for a while. Finally, you’ve patched it all up and your shiny new feature is ready to be merged back into the “main” branch.
Perhaps the feature branch looks a little like this:
$ git log –oneline –graph feature_branch
* (feature_branch) Fixed boundary case issue.
* Refactor display driver.
* Page-wise rendering improvements.
* Display renders page-by-page.
* (main) …
When you merge this branch in, it will carry all this history with it. That’s history that nobody cares about. In 5 years when you’re reviewing the project’s history, no one will care about bugs that were introduced in one commit and fixed in the very next commit. You will care about these commits as checkpoints during development, but as soon as everything works, you won’t need to remember how you got there. Reviewers just want to see the minimal changeset that gets the project from pre-feature to post-feature, maybe with a couple important milestones in between. So how do you deal with this? You could wait to commit anything until the feature is 100% written and working, but that’s a perilous spot to be midway through development.
Git comes to the rescue again with “git rebase –interactive”, letting users interactively edit commit history. You can reorder, edit, drop, or “squash” commits from the history. Note that interactive rebasing changes your Git history. That makes them potentially very dangerous. You can severely damage your repo here if you’re not careful.
I recommend setting up the git remote host to only allow “rewind” permissions to developers on branches that match a template like “dev/*”. This will keep the main branch and other important checkpoints intact no matter what your devs do. On my projects, “rewinding” is only allowed on branches matching “tmp/*” and “dev/*”. Once the changes are on a shared feature branch or “main”, history won’t change anymore! Where possible, take this idea to its logical conclusion by banning any pushes to the “main” branch unless they come from an accepted pull request.
Revisiting our example feature branch, here is what the history looks like after a cleanup:
$ git rebase –interactive main..feature_branch
$ git log –oneline –graph feature_branch
* (feature_branch) Refactor display driver.
* Display renders page-by-page.
* (main) …
The two bug fix commits were squashed into their parent commits. This makes for a nice clean history that clearly shows a new feature being introduced to the codebase, without the noise of feature development errors.
Keeping a reliable history is one reason to use a VCS – configuration management is a key requirement of IEC 62304 for medical device software. Use interactive rebasing to make your history clear and easy to see how the software progressed between two releases.
Conclusion
Maintaining a clear history is a critical aspect of medical device software development. When developing exciting and wide-ranging functionality for medical device products, Git’s powerful staging area and interactive rebasing features will keep you moving fast and breaking less.
Image: Adobe Stock
Levi Puckett is a Software Engineer at Starfish Medical. With a degree in Electrical Engineering, he bridges the gap between electronics and high-quality embedded software. Levi works at all levels of medical device software, helping companies develop effective, innovative, and safe software for their new medical devices.