Lessons
1 · Java Foundations
2 · Object-Oriented Java
3 · Core Java
4 · Collections and Generics
5 · Functional Java
6 · The JVM and Concurrency
7 · Tools and Craft
- 21File I/O and Serialization
- 22Debugging: Errors, Stack Traces, the Debugger and Logging
- 23Maven: What It Does and How to Use It
- 24Git for Java Projects: Commits, Branches, Merging and Pull Requests
- 25Testing with JUnit and Mockito: Unit Tests, Mocks and Their Limits
- 26Project 1: Build an Expense Tracker CLI in Plain Java
8 · Databases and JDBC
9 · Web and Spring
10 · Persistence with JPA
11 · Secure, Tested, Production-Ready
- 36Security Fundamentals and Spring Security: Passwords, JWT, Access Control, CORS and Secrets
- 37Testing Spring Boot Applications: Slice Tests, MockMvc, Real Databases and the Test Pyramid
- 38Production Practices: Configuration, Logging, Docker and Health Checks for Spring Boot
- 39Project 3: The Job Application Tracker API, Assembled and Reviewed
Lesson 24 · Tools and Craft
Git for Java Projects: Commits, Branches, Merging and Pull Requests
A practical Git workflow for Java developers: the three areas, commits and good messages, branches, merging and resolving conflicts, pull requests, a correct .gitignore for Maven projects, and what to do when a secret is committed.
For
Learners who build Java projects and need version control for their own work and for working in a team.
Level
Before you start
A working Java project, ideally the Maven project from article 23. Git installed. The commands were verified with Git 2.55.
On this page
Key takeaways
- Git records snapshots of your project. Changes move from the working tree to the staging area (git add) to the repository (git commit), and each commit has a message that explains why.
- Branches are cheap, so do each piece of work on its own branch and merge it back through a reviewed pull request.
- A merge conflict means two branches changed the same lines. Git marks both versions in the file, you decide the final text, then add and commit.
- .gitignore keeps build output, IDE files and secrets out of the repository, but it does not untrack files that were already committed.
- A committed secret is compromised. Revoke or rotate it first, because removing it from history is secondary.
Imagine renaming a file Report-final-v2-REALLY-final.docx. Version control exists so you never need to. Git is the version control system used by almost every software team: it records the history of your project, lets you try ideas safely on separate branches, and lets many people change the same code without overwriting each other.
This article teaches the part of Git you will use every day on a Java project, with a real example (including a real merge conflict) that was run for this article.
What you will learn
#- The three areas of Git and the core commands.
- How to write a useful commit and how to work on branches.
- How to merge, and how to resolve a conflict step by step.
- How pull requests and code review fit in.
- What to put in
.gitignore, and what to do when a secret is committed.
How Git thinks
#Git stores your project as a series of commits. A commit is a snapshot of all tracked files, with an author, a time, a message, and a pointer to its parent commit(s). Together the commits form the history.
Your changes pass through three areas:
Area | What it is | Moves on with |
|---|---|---|
Working tree | The files on disk that you edit | |
Staging area (index) | The set of changes you have chosen for the next commit |
|
Repository | The saved history of commits |
|
The staging area is useful because you choose exactly what goes into each commit, so one commit can be one logical change even if you edited several things.
Git works locally: your computer has the full history. A remote (such as one on GitHub, GitLab or Bitbucket) is another copy used to share work, reached with git push and git pull.
Older style. Earlier version control systems such as CVS and Subversion were centralised: one server held the history, and you needed it to commit. You may meet Subversion in old company projects. Git is distributed, which makes branching and working offline practical.
First steps
#git config --global user.name "Your Name"git config --global user.email "you@example.com"git init # start a repository in this foldergit status # what changed? (use this constantly)git add src/ # stage changesgit commit -m "Add Greeting class"git log --oneline # compact historygit diff # unstaged changes; git diff --staged shows staged onesPrefer git add <paths> over git add -A or git add . while you are learning, so you do not stage files you did not intend to (generated output, local settings, secrets). Check git status before every commit.
Good commits
#A commit should be one logical change that leaves the project working (the build and tests pass). The message explains the why so that someone reading the history in a year understands it.
- A short summary line, in the imperative mood: "Add validation for blank names" rather than "added stuff". About 50 characters or fewer is the common convention.
- If needed, a blank line and a body saying why the change was made and what trade-offs it involves.
- Not "fix", "update" or "WIP" alone.
- Small commits are easier to review, to revert, and to understand than one giant commit at the end of the week.
Branches
#A branch is a movable pointer to a commit. Creating one costs almost nothing, so the normal workflow is: one branch per piece of work, and main always stays in a working state.
git switch -c feature/shout # create a branch and move to it# ...edit, add, commit...git switch main # go backgit branch # list branchesOlder style. Tutorials often use
git checkout -b feature/xandgit checkout main. They still work. Newer Git addedgit switch(change branch) andgit restore(discard changes to files), becausecheckoutdid too many different things.
Name branches so the purpose is obvious: feature/expense-export, fix/null-category. Teams often include a ticket number.
Merging
#When the work is finished, you merge the branch back. If main has not moved since you branched, Git just moves the pointer forward (a fast-forward). If both branches have new commits, Git creates a merge commit that has two parents.
Here is a real, run example. The repository has a Greeting class on main. One branch changes the text to "HELLO", while at the same time main changes it to "Hello, world". Because both branches changed the same line, this will conflict:
$ git log --oneline --graph --all* 7df0322 Shout the greeting| * 4078fc9 Greet the world|/* d0d33fe Add Greeting class$ git merge feature/shoutAuto-merging src/main/java/app/Greeting.javaCONFLICT (content): Merge conflict in src/main/java/app/Greeting.javaAutomatic merge failed; fix conflicts and then commit the result.(The commit hashes on your machine will be different.) The graph shows the two lines of work that split from the same starting commit.
Resolving a conflict
#A conflict is not an error in Git. It means Git cannot decide for you, because two people changed the same lines. git status --short shows UU (unmerged, changed on both sides) next to the file. Open it and you will see markers:
public class Greeting {<<<<<<< HEAD String text() { return "Hello, world"; }======= String text() { return "HELLO"; }>>>>>>> feature/shout}- Between
<<<<<<< HEADand=======is your current branch's version (heremain). - Between
=======and>>>>>>> feature/shoutis the incoming branch's version.
Your job is to edit the file to the final correct text, which might be one side, the other, or a combination, and delete all three marker lines. Here we want both changes, so the result is:
public class Greeting { String text() { return "HELLO, WORLD"; }}Then tell Git it is resolved and finish the merge:
$ git add src/main/java/app/Greeting.java$ git commit -m "Merge feature/shout"$ git log --oneline --graph --all* 9379264 Merge feature/shout|\| * 7df0322 Shout the greeting* | 4078fc9 Greet the world|/* d0d33fe Add Greeting classHow to resolve conflicts calmly:
- Understand both changes before choosing. If you do not know why the other side changed it, ask them.
- After resolving, build and run the tests. A merge can compile but be logically wrong. A conflict-free merge can also break behaviour, so tests matter even when Git raises no conflict.
- IDEs (IntelliJ IDEA, VS Code) have three-way merge tools that show both sides and the result. They are convenient, and the underlying idea is the same.
- To abandon a merge and go back, use
git merge --abort. - Keep branches short-lived and merge
maininto them (or rebase) often, so conflicts stay small.
Rebase, briefly
#git rebase main is an alternative to merging: it replays your commits on top of the latest main, giving a straight history without a merge commit. It rewrites your commits (new hashes). The rule to remember: never rebase commits that other people have already pulled, because you rewrite history they depend on. Rebasing your own unpublished branch is fine. Teams differ on merge versus rebase, so follow the team's convention.
Remotes, push, pull and pull requests
#git remote add origin <url-of-your-empty-remote-repository>git push -u origin main # first push, sets the upstreamgit pull # fetch and integrate changes from the remotegit clone <url> # get a copy of an existing repositoryOn a team, the usual flow is:
- Update
main:git switch mainthengit pull. - Create a branch:
git switch -c feature/expense-export. - Make small commits. Run the build and tests (
mvn clean verify) before you push. git push -u origin feature/expense-export.- Open a pull request (called a merge request on GitLab): a request to merge your branch into
main, with a description, a diff, and a place for discussion. - Code review: teammates read the change, comment, and approve. Automated checks (CI) run the build and tests.
- Address the comments by pushing more commits to the same branch, then merge.
- Delete the branch and update your local
main.
A good pull request is small (a reviewer can understand it in minutes), focused (one purpose), explained (why, how to test it, anything risky) and passing the automated checks before you ask for review. When you review others, comment on the code and its consequences, not the person; ask questions, and check tests, error handling, naming and security.
.gitignore for Java and Maven
#A .gitignore file lists patterns of files Git should not track. For a Maven project you should ignore:
# Build outputtarget/# IDE and editor files.idea/*.iml.vscode/.classpath.project.settings/# Local configuration and secrets.envapplication-local.properties*.pem*.keyHere Git itself confirms that the patterns work. The target/ and .env files exist on disk, and git check-ignore -v shows which rule matches each:
$ git check-ignore -v target/app.class .env.gitignore:3:target/ target/app.class.gitignore:4:.env .envDetails worth knowing, from the Git documentation:
- A pattern ending in
/(such astarget/) matches only directories. .gitignoreonly affects files that are not yet tracked. If you already committed a file, ignoring it later does nothing. Stop tracking it withgit rm --cached <file>(this removes it from Git but leaves it on disk), commit, and then ignore it.- Commit the
.gitignoreitself, so the whole team shares it. - Do not ignore things the project needs to build:
pom.xml, the source folders, and the Maven Wrapper files (mvnw,.mvn/).
Never commit secrets
#Passwords, API keys, tokens, private keys and database URLs with credentials do not belong in Git, even in a private repository. Repositories get cloned, forked, shared with contractors and made public by mistake, and history is permanent.
- Keep secrets in environment variables or a secret manager, and read them in the application. Article 38 shows how Spring Boot does this.
- Commit a template with fake values (
application-example.properties) so others know what to set. - Ignore the real local files, as above.
If a secret is committed, treat it as compromised. GitHub's documentation gives the order of response: the first step is to revoke or rotate the secret, so it can no longer be used; rewriting history to remove it may then not even be necessary, although for other sensitive data you may still choose to. Deleting the file in a new commit does not remove the secret from history, and it may already have been copied.
Undoing things safely
#Situation | Command | Notes |
|---|---|---|
Discard unstaged edits to a file |
| Destroys those edits permanently |
Unstage a file |
| Keeps the edits |
Fix the last commit message (not yet pushed) |
| Rewrites that commit |
Undo a commit that is already shared |
| Adds a new commit that reverses it; safe for shared history |
Temporarily set work aside |
| Useful before switching branch |
See who changed a line, and why |
| Read the message for the reason |
Find which commit broke something |
| Binary search through history |
The real git restore run for this article: after appending a stray line to Greeting.java, git diff --stat showed 1 file changed, 1 insertion(+), and after git restore the working tree was clean again. Be careful with commands that discard work (git restore, git reset --hard, git clean, git push --force): they cannot always be undone, so read the command twice and check git status first. Never force-push to a shared branch such as main.
A sensible daily routine for your own projects
#git status,git pull.- Branch for the task.
- Work in small steps, commit at each working step with a clear message.
- Run
mvn clean verifybefore pushing. - Push, open a pull request (even on solo projects it builds the habit), merge, delete the branch.
Common mistakes
#- Committing
target/, IDE files or local configuration. - Committing secrets, and thinking that a deleting commit fixes it.
- Using
git add .blindly and committing unrelated files. - Huge commits and vague messages ("fix", "stuff").
- Working directly on
mainwith no review. - Resolving a conflict without understanding it, or leaving marker lines (
<<<<<<<) in the file. The build usually fails, but not always when they are in comments or text. - Force-pushing shared branches.
- Never pulling, then facing a giant conflict.
- Treating Git as a backup system with no remote and no pushes.
Exercises
#Exercise 1
Staging
#You edited Greeting.java and README.md, and you want two separate commits. Which commands do you use?
Exercise 2: Read the markers
#In a conflicted file, what is between <<<<<<< HEAD and =======? What must you do before committing?
Exercise 3: Fix a tracked file
#You committed application-local.properties, then added it to .gitignore, and git status still shows changes to it. Explain and fix, and say what extra step you need if it contained a real password.
Exercise 4: Revert or reset
#You pushed a commit to a shared main that broke the build. Which command do you use to undo it safely, and why not rewrite history?
Exercise 5: Write the message
#Rewrite "fixed bug" as a good commit message for a change that stops greet(" ") from returning "Hello, !".
Show solutionHide solution
git add src/main/java/app/Greeting.javagit commit -m "Greet the user by name"git add README.mdgit commit -m "Document how to run the greeter"Staging only the files for each logical change lets you split the work into two commits. Check git status between them.
Exercise 2
Show solutionHide solution
Between <<<<<<< HEAD and ======= is the version from your current branch. The incoming branch's version is between ======= and >>>>>>>. Before committing, edit the file to the final correct text and delete all three marker lines, then git add the file, build and test, and commit.
Exercise 3
Show solutionHide solution
.gitignore only affects untracked files, and the file is already tracked. Run git rm --cached application-local.properties, commit that, and keep the ignore rule. If it contained a real password, that password is in history, so revoke or rotate it immediately; removing the file from the repository does not make the old password safe.
Exercise 4
Show solutionHide solution
Use git revert <hash>, which adds a new commit undoing the change. Others have probably already pulled the bad commit, so rewriting the shared history (reset and force push) would make their copies diverge and cause confusing conflicts or lost work. A revert keeps the history truthful and is safe for shared branches.
Exercise 5
Show solutionHide solution
For example: Reject blank names in Greeter.greet as the summary, with a body such as: Previously greet(" ") returned "Hello, !". Now it throws IllegalArgumentException, and a test covers it. The summary uses the imperative mood and says what changed, and the body says why.
Where to go next
#- Next: Testing with JUnit and Mockito gives you the safety net that makes small commits and merges less risky.
- Then: Project 1: Expense Tracker CLI is your first project to keep in its own repository.
- For secrets and configuration in a Spring Boot application: Production Practices: Configuration, Logging, Docker and Health Checks.
Sources & further reading
- Technology
- Software Engineering
- #git
- #Java
- #Java backend
- #Java interview
- #JPA
- #learn Java
- #pull requests
- #Spring Boot
- #SQL
- #version control
Connected lessons
Linked from 4 guides
Other pages in the library that point here.
Keep reading
More in Technology →Guide · Technology
Git for Beginners: The Handful of Commands You Actually Need
Guide · Technology
Heaps and Priority Queues
Course · Technology
DSA From Zero to Job-Ready: A Practical Guide with Java

