A Rust .gitignore needs target/, Cargo’s build directory, and a few files tools leave beside the source: **/*.rs.bk backups from rustfmt, *.pdb debug symbols from MSVC builds, the *.profraw profiles that -C instrument-coverage writes, and the *.profdata files merged from them. With one more line that keeps a source module called target, that is the tool’s whole Rust template.
What it ignores, and what it keeps
target/ holds everything cargo build, cargo test and cargo doc produce: debug/, release/, the incremental cache and the rendered docs. It is rebuilt from source, tied to your machine and toolchain, and grows to gigabytes.
Keep Cargo.lock. In an application it pins exactly what you tested and shipped, and since 2023 the Cargo team’s advice is to commit it for libraries too. Older versions of cargo new --lib added Cargo.lock to .gitignore, so delete that line if your file still has it. In a scratch repository with this file, git check-ignore -v -n --no-index prints the file, line and pattern that matched each path, or :: when nothing did:
.gitignore:3:target/ target/debug/app
.gitignore:3:target/ fuzz/target/x
:: src/target/mod.rs
.gitignore:5:!**/src/**/target/ src/target
:: Cargo.lock
:: Cargo.toml
.gitignore:7:**/*.rs.bk src/main.rs.bk
.gitignore:11:*.profraw default_123.profraw
Why target/ has a second line
A pattern whose only slash is at the end matches at every level (gitignore(5)). That catches the target/ of a nested crate such as fuzz/, but on its own it would also hide a module directory called target, so the template re-includes any target/ under a src/ folder. cargo new takes the other route and writes /target, anchored to the root, which leaves nested crates’ build output untracked but visible.
Keeping one folder of target/
Git never looks inside a directory it has excluded, so a !target/doc/ line after target/ changes nothing: target/doc/index.html is still reported against target/ on line 3. Ignore the directory’s contents instead, then re-include the folder, with these two lines in place of target/:
/target/*
!/target/doc/
:: target/doc/index.html
.gitignore:3:/target/* target/debug/app
Already committed target/?
.gitignore only affects untracked files. If target/ was committed before the line existed, Git keeps tracking it, and git check-ignore without --no-index prints nothing for it. Untrack it, keeping the files on disk, and commit:
git rm -r --cached target
git commit -m "Stop tracking target/"
Editor and OS files
.DS_Store, Thumbs.db and Vim’s *.swp come from your machine, not from Rust, so they belong in a global ignore file rather than every project’s:
git config --global core.excludesFile ~/.gitignore_global
Without that setting, Git reads ~/.config/git/ignore, or $XDG_CONFIG_HOME/git/ignore when that variable is set.