Git for firmware work
Objectives
Section titled “Objectives”By the end of this lesson, you will be able to
- Create a firmware project repository whose
.gitignoreexcludes build outputs and fetched dependencies. - Use branches for new work and tags for firmware that actually ships, with commit messages that explain the reasoning.
- Check before pushing that no WiFi password, key or token is in the history, and explain why deleting it afterward isn’t enough.
Takes about 70 minutes (concepts 15 · practice 25 · lab 25 · check 5). You need Git and bash on your machine.
Before you start
Section titled “Before you start”Two review questions from lessons 2.1 and 2.2.
- Which folders appear after
make getlibsandmake build, and roughly how big are they? - If the Makefile sets
ENABLE_PAGE_EXAMPLES ?= 0and you enable examples from the command line, what extra thing does your build record need to note?
See it work first
Section titled “See it work first”In the template folder you built in lesson 2.1, have Git count how many files it would commit right now.
cd bento-firmware-template-mtb-onlygit initgit status --short --untracked-files=all | wc -ldu -sh build proj_*/build 2>/dev/nullPredict before you run it: roughly how many thousand does that first number come to? Then look at how large the build folders are combined. Every byte of those files can be recreated with make build. Committing them to history would bloat the repository with every build, and every diff would fill up with files no one reads. Now copy examples/firmware.gitignore to .gitignore and count again. The drop is what shouldn’t be in history.
Concepts
Section titled “Concepts”1. What belongs in a repository, and what doesn’t
Section titled “1. What belongs in a repository, and what doesn’t”There is one rule: keep what can’t be regenerated; don’t keep what can. The SDK’s top-level .gitignore states this rule right in its first comment.
# Build output. Every one of these is reproducible from what is committed.build/build-*/mtb_shared/*.o*.a.DS_StoreSource: tesaiot-pse84-devkit-sdk’s .gitignore lines 1-7 (Apache-2.0, tesaiot-pse84-devkit-sdk)
| Keep | Don’t keep |
|---|---|
.c / .h source, the Makefile and *.mk files |
build/ and build-*/ for every project |
deps/*.mtb files, which say what to fetch and at what version |
mtb_shared/ (re-fetchable with make getlibs) |
| BSP settings and files the Configurator generates, which the SDK’s template also keeps | .o, .elf, .hex, .map (a released hex belongs attached to a release instead) |
The template’s LICENSE and NOTICE (Apache-2.0 requires keeping them) |
Your machine’s own secret files |
Notice the *.a line in the SDK’s file: the consequence is that the public repository has no prebuilt libraries at all — they come with the release zip (lesson 2.1). Your own project has to make this decision deliberately; that’s why our example file leaves it as a choice near the end. One more thing to know: .gitignore only affects files that have never been tracked. A file that’s already been committed must be removed with git rm --cached <path>, and anyone can still force-add an ignored file with git add -f — which is why we add another layer of checking with a hook.
2. Branches for work, tags for what ships, commit messages for the reasoning
Section titled “2. Branches for work, tags for what ships, commit messages for the reasoning”- A branch separates one piece of work from the main line.
git switch -c fix/sw2-debounce, finish it, test it on the board, then merge it back — so the main line always stays in a state that builds and flashes. - A tag pins a name to the commit that became a real, shipped firmware. Use an annotated tag:
git tag -a fw-v0.1.0 -m "...", thengit push origin fw-v0.1.0. The SDK follows the same rule — its releases are named likefw-c-only-v1.10.0, with the hex andSHA256SUMS.txtattached to the release, not committed. - A commit message’s first line says what was done; the body says why. A diff can tell you what changed, but never why. Six months later, whoever runs into an odd-looking line will want the reason more than anything else.
Compare two messages for the same change (a made-up example — the numbers are illustrative, not a real measurement):
Unhelpful: fix button
Helpful: Debounce SW2 in time, not by reading the pin twice
Two reads 200 ns apart sample the same bounce, so one press counted two or three times on the bench (10 presses -> 23). Require 3 agreeing polls at 10 ms, as the SDK's io/04_gpio_led_button.c does. Evidence: 10 presses -> 10 counts, board A, commit abc1234. Not verified: long presses over 5 s.The SDK itself writes this kind of reasoning into code comments throughout every file we’ve read across module 1 — the same habit works for commit messages. A message template is in examples/commit-template.txt.
3. Secrets in history: deleting them later isn’t enough
Section titled “3. Secrets in history: deleting them later isn’t enough”Git keeps every version of every file. If you commit a WiFi password once and remove it in the next commit, it’s still there in the earlier commit. Anyone who already cloned or forked has a full copy, and their git log -p will show it. Rewriting history with a tool like git filter-repo and force-pushing only removes it from your own copy — not from anyone else’s machine. So the first thing to do once a secret has leaked is change the password or revoke that key. Cleaning up history comes after, and only prevents it from leaking again.
The right approach is to never let a secret into the source in the first place. The SDK’s WiFi example states this rule directly: “A credential compiled into an example is a credential in a public repository.” (10_wifi_join.c lines 71-84). That’s why it reads the SSID and password from storage on the board, not from a #define. For values that need to live in a file during development, use a file that’s ignored, and commit a sample file next to it holding only a placeholder, such as "<your-password>".
Before pushing, search the whole history, not just the current files.
git grep -nIiE '(pass(word)?|secret|token|api_key)' $(git rev-list --all) # every commit, every branchgit log -p --all -S 'PRIVATE KEY' # commits that added or removed this textThe TESA Open Knowledge library you’re reading right now checks every file the same way before publishing, with the secrets checker in tools/validate.py.
Worked example
Section titled “Worked example”Setting up a repository for a project forked from the template. Do this inside your own bento-firmware-template-mtb-only folder.
Part 1: start a clean repository
git initcp <this lesson's folder>/examples/firmware.gitignore .gitignoregit status --short --untracked-files=all | wc -l # must be lower than before the .gitignore existedgit add .git commit -m "Import bento-firmware-template-mtb-only from fw-c-only-v1.10.0"Part 2: work on a branch, then tag what ships
git switch -c docs/build-recordmkdir -p docs && cp <the build record you filled in during lesson 2.1> docs/build-record.mdgit add docs/build-record.mdgit commit # write the reasoning, following commit-template.txtgit switch main && git merge --no-ff docs/build-recordgit tag -a fw-v0.1.0 -m "First build of the template on board A; record in docs/build-record.md"git log --oneline --decorate --graph -5(If your main line is called master, use that name instead of main.)
Part 3: install the hook, then get it to reject something
cp <this lesson's folder>/solution/pre-commit.sh .git/hooks/pre-commitchmod +x .git/hooks/pre-commitprintf '#define WIFI_PASSWORD "not-a-placeholder"\n' > wifi_secrets_test.hgit add -f wifi_secrets_test.h && git commit -m "test" # must be rejectedgit reset -q wifi_secrets_test.h && rm wifi_secrets_test.hYou should see a message like blocked: looks like a real secret. Then try changing the value to "<your-password>" and see whether the hook allows it this time. (We use git add -f because this filename is already excluded by .gitignore — which is exactly why we need this extra layer of hook.)
Practice
Section titled “Practice”Open practice/pre-commit.sh. There are 4 gaps to fill in — more than in previous lessons, matching this module’s pace.
- Get the list of staged files.
- Reject files under
build/,build-*/,mtb_shared/, and.ofiles. - Reject newly added lines that assign a real-looking value to a secret-looking name, but allow placeholder values like
<...>and empty values. - Reject the header line of a private key file.
Check your work with a test that creates a fresh, temporary repository for every case and deletes it itself, never touching your own repository.
bash examples/test_hook.sh practice/pre-commit.shBefore filling anything in, you’ll see 3 passed, 6 failed. The three that pass are the cases the hook is supposed to “allow” — a hook that does nothing at all also passes those. If there were only those three tests, an empty hook would look like it’s working. That’s exactly why there also have to be cases the hook must “reject”.
Solution
Section titled “Solution”Try it yourself for at least 15 minutes first, then open solution/pre-commit.sh. Worth comparing:
--diff-filter=ACMskips deleted files, and this command still works even on the very first commit, beforeHEADexists.- The pattern
"[^"<]is the heart of telling a real value apart from a placeholder — a value starting with<, and an empty value, both pass. - The hook prints the line where it found a secret, so a person can fix it, but never prints the contents of a private key, because logs can get copied and pasted elsewhere too.
- A hook like this can have false positives — for example, a variable named
TEST_PASSED. Accept that limitation, and fix the name or the pattern when you hit one, rather than disabling the hook.
Check your understanding
Section titled “Check your understanding”Answer the 5 questions in quiz.yaml (shown at the bottom of this page on the website), covering all three objectives. Getting 4 or more right counts as finishing the lesson.
Task: bring a project forked from the template into Git, ready for a team to work on, with evidence that there are no build files and no secrets.
- Do parts 1 through 3 until you have a first commit, one branch merged back, and the tag
fw-v0.1.0on the commit you successfully built and flashed. - Build again with
make build -j, then rungit status --short. The result should be empty. If any file shows up, fix.gitignoreand explain what that file is. - Run both history-search commands from concept 3. Note what you find. If you find a word like
passwordin the template’s source, check whether it’s a real value or just a field name or a comment. - If you have a remote (such as a private GitHub repository), push both the branch and the tag, then check on the web page that the tag points to the right commit.
Evidence to keep in your portfolio: the output of git log --oneline --decorate --graph showing the merged branch and tag, the empty git status --short output after building, the message the hook used to reject your test commit, and the results of your history search with an explanation.
Going further
Section titled “Going further”- Read Pro Git’s “Git Branching” chapter and its “Tagging” section, then try setting a tag-naming rule for your team that states both the variant and the version, the way the SDK does.
- Try moving the hook’s checks into CI too, where they can’t be skipped with
--no-verify. That’s the subject of lesson 6.2.
Next lesson, moving into module 3: lesson 3.1, an introduction to SWD and GDB
Reflect
Section titled “Reflect”- If you discovered tomorrow that a cloud token had leaked into a commit three weeks ago, and five classmates had already cloned it, what would you do first, and why?
- Looking at your commit messages from the past week, how many of them would still make sense to you six months from now?
References
Section titled “References”- Pro Git (the open Git book)
- SDK: the mtb-only template README (what getlibs fetches, and what you must supply yourself)
- SDK: the repository’s top-level .gitignore
- SDK: cm33/connectivity/10_wifi_join.c (credentials come from storage, not a #define)
- SDK: release fw-c-only-v1.10.0 (an example of a tag name, with a hex attached alongside SHA256SUMS)
Review questions
Answer on your own first, then open the answer.
-
In a project unpacked from the mtb-only template, which should not be committed? (choose all that apply) (Objective 1)
- build/ และ proj_cm55/build/
- mtb_shared/ ที่ make getlibs ดึงมา
- ไฟล์ deps/*.mtb ของแต่ละโปรเจกต์
- ไฟล์ .o และ .elf
- ไฟล์ LICENSE และ NOTICE ของแม่แบบ
Show answer
Answer: A. build/ และ proj_cm55/build/ · B. mtb_shared/ ที่ make getlibs ดึงมา · D. ไฟล์ .o และ .elf
ผลของ build และ dependency ที่ดึงมาสร้างใหม่ได้จากสิ่งที่ commit ไว้ ส่วน deps/*.mtb คือรายการที่บอกว่าจะดึงอะไร ต้องเก็บไว้ และ LICENSE กับ NOTICE ต้องคงไว้ตามสัญญาอนุญาต Apache-2.0
-
build/ is now in .gitignore, but git status still shows files in build/ as modified. Why, and what fixes it? (Objective 1)
- .gitignore ต้องอยู่ในโฟลเดอร์ build/ เอง
- ไฟล์เหล่านั้นถูก track ไปแล้วก่อนมี .gitignore ต้องเอาออกจาก index ด้วย git rm -r --cached build แล้ว commit
- ต้องลบ repository แล้วสร้างใหม่เท่านั้น
- Git ไม่อ่าน .gitignore จนกว่าจะ push
Show answer
Answer: B. ไฟล์เหล่านั้นถูก track ไปแล้วก่อนมี .gitignore ต้องเอาออกจาก index ด้วย git rm -r --cached build แล้ว commit
.gitignore มีผลกับไฟล์ที่ยังไม่ถูก track เท่านั้น ไฟล์ที่ commit ไปแล้วต้องเอาออกจาก index โดยไม่ลบไฟล์จริง ด้วย git rm --cached
-
Firmware shipped today came from one commit. What makes that commit findable for certain a year from now? (Objective 2)
- จำชื่อ branch ที่ใช้ตอนนั้นไว้
- commit ไฟล์ .hex ลงใน repository
- สร้าง annotated tag เช่น fw-v1.0.0 บน commit นั้นแล้ว push tag และแนบ hex กับ SHA-256 ไว้ที่ release
- เขียนวันที่ไว้ในข้อความ commit
Show answer
Answer: C. สร้าง annotated tag เช่น fw-v1.0.0 บน commit นั้นแล้ว push tag และแนบ hex กับ SHA-256 ไว้ที่ release
branch ขยับไปเรื่อย ๆ แต่ tag ตรึงชื่อไว้กับ commit เดียว release ของ SDK ก็ใช้ tag แบบ fw-c-only-v1.10.0 และแนบ hex กับ SHA256SUMS ไว้ที่ release แทนการ commit hex
-
Which commit message helps a future reader most? (Objective 2)
- update
- fix bug in main.c line 212
- Debounce SW2 in time, not by reading the pin twice — พร้อมเนื้อความที่บอกอาการ หลักฐานบนบอร์ด และสิ่งที่ยังไม่ได้ตรวจ
- แก้ตามที่คุยกันเมื่อวาน
Show answer
Answer: C. Debounce SW2 in time, not by reading the pin twice — พร้อมเนื้อความที่บอกอาการ หลักฐานบนบอร์ด และสิ่งที่ยังไม่ได้ตรวจ
diff บอกได้ว่าเปลี่ยนอะไรแต่บอกไม่ได้ว่าทำไม ข้อความที่ดีบอกเหตุผล หลักฐาน และขอบเขตที่ยังไม่ได้พิสูจน์ ข้อความที่อ้างเลขบรรทัดหรือบทสนทนาจะไร้ความหมายเมื่อไฟล์เปลี่ยนหรือคนลืม
-
A real WiFi password was committed two weeks ago to a repository several people have cloned. What comes first? (Objective 3)
- commit ใหม่ที่ลบบรรทัดนั้นออก ถือว่าจบ
- เปลี่ยนรหัสของเครือข่ายนั้นทันที แล้วค่อยล้างประวัติและย้ายค่าไปไว้ในที่เก็บที่ไม่อยู่ในซอร์ส
- force push ประวัติที่เขียนใหม่ แล้วรหัสจะหายจากทุกเครื่อง
- ไม่ต้องทำอะไรถ้า repository เป็นแบบ private
Show answer
Answer: B. เปลี่ยนรหัสของเครือข่ายนั้นทันที แล้วค่อยล้างประวัติและย้ายค่าไปไว้ในที่เก็บที่ไม่อยู่ในซอร์ส
Git เก็บทุกรุ่น commit ที่ลบบรรทัดไม่ได้ลบของเดิม และ force push ไม่ได้ลบสำเนาในเครื่องคนอื่น สิ่งที่หยุดความเสียหายได้จริงคือทำให้รหัสที่หลุดใช้ไม่ได้ SDK จึงไม่ใส่ข้อมูลรับรองใน #define เลย: a credential compiled into an example is a credential in a public repository
Cite this lesson
If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.
"Git for firmware work" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
Thai attribution: "Git สำหรับงานเฟิร์มแวร์" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
This lesson adapts the source below; keep its credit too.
https://github.com/tesaiot/tesaiot-pse84-devkit-sdk/tree/ef72c1b658178eee8c38b1e47d28b006f80a59b5 · SDK examples and docs are linked at this commit, not copied into this course. Lessons quote short excerpts (at most 25 lines) with a link to the file at this commit and the credit (Apache-2.0, tesaiot-pse84-devkit-sdk).
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA