KhueApps
Home/DevOps/Using a Makefile to Shorten and Simplify Git Commands

Using a Makefile to Shorten and Simplify Git Commands

Last updated: October 07, 2025

Why use a Makefile for Git?

Short Make targets reduce typing, enforce consistent flags, and provide a discoverable, project-local interface for frequent Git tasks. This is handy for DevOps teams standardizing workflows across repositories.

  • One-liners for common actions
  • Self-documenting help target
  • Consistent flags and safety checks
  • Works everywhere Git and make are available

Minimal working example

Save this Makefile at the root of a Git repository.

SHELL := /usr/bin/env bash
.SHELLFLAGS := -eu -o pipefail -c

GIT ?= git
REPO_ROOT := $(shell $(GIT) rev-parse --show-toplevel 2>/dev/null)
BRANCH := $(shell $(GIT) rev-parse --abbrev-ref HEAD 2>/dev/null)

# Commit message; can be overridden: make commit M="fix: message"
M ?= chore: update
export M

.PHONY: help status lg commit push pull sync switch

help: ## Show available targets
	@grep -E '^[a-zA-Z0-9_-]+:.*?##' $(MAKEFILE_LIST) | awk -F':|##' '{printf "  %-12s %s\n", $$1, $$NF}'

status: ## Git status summary (short)
	@$(GIT) -C "$(REPO_ROOT)" status -sb

lg: ## Pretty one-line log (recent)
	@$(GIT) -C "$(REPO_ROOT)" log --oneline --graph --decorate -n 20

commit: ## Commit staged changes with message M="..."
	@$(GIT) -C "$(REPO_ROOT)" commit -m "$$M"

push: ## Push current branch to origin
	@$(GIT) -C "$(REPO_ROOT)" push -u origin "$(BRANCH)"

pull: ## Fast-forward only pull
	@$(GIT) -C "$(REPO_ROOT)" pull --ff-only

sync: ## Rebase onto origin/BRANCH and push
	@$(GIT) -C "$(REPO_ROOT)" fetch origin
	@$(GIT) -C "$(REPO_ROOT)" rebase "origin/$(BRANCH)"
	@$(GIT) -C "$(REPO_ROOT)" push -u origin "$(BRANCH)"

switch: ## Switch to branch B="name" (create with -c via GITFLAGS)
	@: $${B?Set B="branch-name"}
	@$(GIT) -C "$(REPO_ROOT)" switch "$$B"

Notes:

  • Use a real tab before each recipe line.
  • M is exported so quotes and spaces in commit messages work: make commit M="fix: handle spaces".
  • sync uses rebase; adjust to your policy.

Quickstart

  1. Create a file named Makefile in your repo with the example above.
  2. Run make help to see available targets.
  3. Stage changes normally (git add -p or git add .).
  4. Commit: make commit M="feat: add health endpoint".
  5. Push: make push.
  6. Sync with remote (fetch, rebase, push): make sync.
  7. Switch branch: make switch B=feature/authz.

Common targets at a glance

TargetWhat it doesExample usage
helpList documented targetsmake help
statusShort status with branch infomake status
lgOne-line decorated logmake lg
commitCommit staged changes with message Mmake commit M="fix: flaky test"
pushPush current branch to originmake push
pullPull with fast-forward onlymake pull
syncFetch, rebase current branch on origin, then pushmake sync
switchSwitch to branch Bmake switch B=main

Extending the Makefile

Add more repo-specific helpers to standardize your Git flow.

.PHONY: new-branch amend tag prune-local

new-branch: ## Create and switch to new branch B="name"
	@: $${B?Set B="branch-name"}
	@$(GIT) -C "$(REPO_ROOT)" switch -c "$$B"

amend: ## Amend last commit with staged changes (keep message)
	@$(GIT) -C "$(REPO_ROOT)" commit --amend --no-edit

tag: ## Create annotated tag T and push (T="v1.2.3")
	@: $${T?Set T="vX.Y.Z"}
	@$(GIT) -C "$(REPO_ROOT)" tag -a "$$T" -m "$$T"
	@$(GIT) -C "$(REPO_ROOT)" push origin "$$T"

prune-local: ## Remove local branches merged into origin/main (danger)
	@main=main; \
	merged=$$($(GIT) -C "$(REPO_ROOT)" branch --merged origin/$$main | grep -v "\*" | grep -v " $$main$$" || true); \
	[ -z "$$merged" ] || xargs -r -n1 $(GIT) -C "$(REPO_ROOT)" branch -d <<< "$$merged"

Tips:

  • Add CI-safe variations (e.g., read-only targets for logging or validation).
  • Use environment-gated behavior, e.g., ifeq ($(CI),true) to avoid destructive actions in pipelines.

Pitfalls and how to avoid them

  • Tabs vs spaces: Make recipes must start with a tab. Many editors can insert tabs automatically for Makefiles.
  • Quoting commit messages: Export M and reference it as "$${M}" in recipes; avoid -m $(M) which breaks on spaces.
  • Running outside a Git repo: Commands using -C "$(REPO_ROOT)" fail gracefully. Optionally add a guard line: test -n "$(REPO_ROOT)" || { echo "Not a git repo"; exit 1; }.
  • Destructive targets: Mark dangerous commands clearly and add confirmation prompts or no-op dry runs. Prefer --ff-only for pulls; be explicit about rebase vs merge policies.
  • Cross-platform shells: GNU make defaults to /bin/sh; set SHELL and .SHELLFLAGS for predictable behavior. On Windows, use Git Bash or WSL.
  • Target/file name clashes: Use .PHONY for all command targets so make doesn’t think a file named “push” satisfies the rule.

Performance notes

  • $(shell ...) calls run at parse time. Use := for single evaluation (as shown) so you don’t repeatedly call Git.
  • The overhead of make is tiny compared to Git. Group related operations (e.g., fetch + rebase) to avoid multiple invocations where helpful.
  • Avoid complex loops when native Git subcommands exist (e.g., use git worktree, git for-each-ref) to keep recipes fast and robust.
  • Use -C "$(REPO_ROOT)" to avoid subshell cd overhead and to ensure commands run at the repo root.

Tiny FAQ

  • Can I alias make targets to shorter names? Yes. Add short aliases, e.g., st: status; with an empty recipe and a dependency on status.
  • Why not Git aliases? Git aliases are great but local to a user. Makefiles travel with the repo, enabling team-wide conventions and CI reuse.
  • How do I pass variables with spaces? Quote them at the shell: make commit M="fix: handle edge case". M is exported and read as one argument.
  • Should sync use rebase or merge? Choose per team policy. Rebase creates a linear history; merge preserves merge commits. Adjust the sync target accordingly.

Series: Git

DevOps