Rules to Better Version Control with Git - 16 Rules
If you still need help, visit Application Lifecycle Management and book in a consultant.
Source control is the backup of your code, as well as your tool for tracking changes over time.
With source control, we can share project code and collaborate with other team members. Using it allows us to track changes, compare code, and even roll-back if required. Moreover, it keeps our code safe and that is the most important part.
It also helps you to do root cause analysis by finding out who made the change. Then, you can chat with them and get aligned on what caused the problem and what is the best solution moving forward. Remember, the person who made the change might have important knowledge that you don't know!
Don't just fix the problem, see who caused the problem and correct them. - Adam Cogan
Using VS Code? There are 2 awesome extensions to see history:
The .gitignore file tells Git which files to ignore and not track in your repository. Every Git project should have a .gitignore file to keep unnecessary files out of the repo. For example, cache files should not be included in the main repository.
Why Use .gitignore?
When working on projects, you often generate files that should not be committed to the version control system. These might include:
- Temporary files generated during development (e.g., .log, .tmp)
- Configuration files with sensitive data (e.g., .env)
- Build outputs (e.g., bin/, obj/ folders for .NET projects)
- Dependency caches (e.g., /node_module folder for javacript projects)
- Personal IDE config (e.g., .vscode sometimes you might want to share IDE config intentionally)
A .gitignore file tells Git which files or folders to ignore. This keeps your repository clean and ensures that only important files are tracked, improving efficiency.
Common Syntax Patterns for .gitignore
Here are some useful patterns you might need in a .gitignore file:
"*" - (e.g. *.log This will ignore all files ending in .log anywhere in the repository.)
"**" - (e.g. **/lib/name.file This pattern will ignore name.file in any lib/ directory within the repository, no matter its depth.)
" /{{name}}/ " - (e.g. /build/ This will ignore the build/ directory only at the root level.)
For more details and advanced patterns, refer to the w3schools Gitignore tutorial.
How to setup .gitignore?
- Create a .gitignore File: In your project’s root directory, create a file named .gitignore if it doesn't already exist.
- Add Files or Directories to Ignore: Open the .gitignore file and list the files or directories you want Git to ignore. Each entry should be on a new line. For example:
bash node_modules/ .env *.log
Figure: Example of gitnore file
Additional Tips
-
Use Pre-made Templates(You don’t need to write a .gitignore from scratch! ):
- Use templates based on your programming language or framework. gitignore template from github
- Use a generator like gitignore.io to create a .gitignore file tailored to your project.
- Use command dotnet new gitignore to create gitignore file for dotnet application. More details on create dotnet gitignore
- Use Comments: You can add comments in the .gitignore file to explain why certain files or directories are being ignored. Use # for comments. For example:
# Ignore environment variables for security .env # Ignore node_modules folder to avoid unnecessary files in the repo node_modules/
Figure: Example of gitignore file with comment
Do you know the best way to manage NuGet packages with Git? You can get into all sorts of trouble by including your packages in source control.
** Do not check packages into Git **
The following are a few issues that are related to having your NuGet packages in source control:
- Over time the packages will grow to be too many and cloning the repository will be slow.
- You could get duplicate NuGet packages in your packages folder as new versions are updated.
- NuGet shows packages to update that have already been updated. This can happen if you have duplicate NuGet packages but they are different versions.
- It becomes harder to "clean" your solution of any unused package folders, as you need to ensure you don't delete any package folders still in use.
Nuget will automatically restore packages with out checking them in to source control
Beginning with NuGet 2.7, the NuGet Visual Studio extension integrates into Visual Studio's build events and restores missing packages when a build begins. This feature is enabled by default and packages.config will be automatically included in souce control.
Here's how it works:
- On project or solution build, Visual Studio raises an event that a build is beginning within the solution.
- NuGet responds to this event and checks for packages.config files included in the solution.
- For each packages.config file found, its packages are enumerated and checked for existence in the solution's packages folder.
- Any missing packages are downloaded from the user's configured (and enabled) package sources, respecting the order of the package sources.
- As packages are downloaded, they are unzipped into the solution's packages folder.
Support in legacy versions of NuGet
It is highly recommended that you upgrade to the latest version of NuGet to to avoid having to configure your solution to not check in NuGet pagages to source control.
You can read more here http://blogs.msdn.com/b/dotnet/archive/2013/08/22/improved-package-restore.aspx?PageIndex=3#comments.
Using the Azure DevOps API you can programmatically get a list of commits from your repository with only a HTTP request.
Using HTTPS with basic authentication, make a GET request to a URL as below, substituting in your project details. A JSON object will be returned. To quickly create classes from a JSON response, see the rule Do you know how to easily get classes from a JSON response?
For a C# implementation, see this blog post Getting Git Commits with the VSO REST API.
Like most skills, it can take a little while to get your head around Git.We rate our devs and the devs that we mentor on the following scale.
Where are you?
** Level 1 - Understanding the basic principles
At this level, you need to have an understanding of the basic operations (including branching).
Your workflow looks like this:
- init local repository / clone
- pull
- <code>
- commit
- push
** Level 2 - Working with Git on a team
Now that you know the basic Git commands you can start working on projects with more than one developer.
You should be using local feature branches for your work.
Your workflow involves:
- pull
- merge
- push
** Level 3 - Learning to use pull requests
Pull requests can be used for code reviews within your team, or to accept suggested changes from people outside your team.
Pull requests should preferably be used with policies (TFS Git only - harder with GitHub).
** Level 4 - Working with a team advanced - Rebasing (harder, but worth it)
When working in a team, Git does a pretty good job of merging code together from different branches... but it can be very messy. True Git masters master rebasing. It lets you keep a much cleaner project history.
Git process for Git masters:
- pull master
- rebase feature branch on top of remote master
- push feature branch to remote or create pull request
Branch protection is a feature in version control software that allows teams to define rules and restrictions around who can make changes to specific branches, what types of changes are allowed, and if there are conditions that have to be met.
This can include:
- Number of reviewers
- Linked work items e.g. PBIs (super useful to track back to why the code was changed)
- Any feedback has been addressed/resolved
- Enforcing specific merge types
- Checking that builds pass
- Checking other services e.g. code quality like SonarQube
- Automatically adding specific people to review the code
When you merge a branch you end up with messy merge commits.
Rebasing might take a bit to get your head around, but you get a much cleaner project history.
- it eliminates the unnecessary merge commits required by git merge
- rebasing also results in a perfectly linear project history - you can follow the tip of feature all the way to the beginning of the project without any forks.
This makes it easier to navigate your project with commands like git log, git bisect, and gitk.
Warning: If you don’t follow the Golden Rule of Rebasing, you could end up in a world of pain.
Rebasing is great for ensuring a clean project history... but it can be dangerous in inexperienced hands.
The golden rule of git rebase is to never use it on public branches. (ie. never rebase master).
You should never rebase master onto a feature branch. This would move all of the commits in master onto the tip of the feature branch (not the other way around).
Since rebasing results in brand new commits, Git will think that your master branch’s history has diverged from everybody else’s. If you were to Push this to the server... expect lots of pain to fix it up!
Git has become the defacto standard for version control systems. It's distributed and decentralized and promotes working disconnected as default. It also takes away the pain of branching and merging and has a built in code review system with pull requests. Everybody should be using Git, and if you're not, you should be migrating the Git using one of the below tools.
- VisualStudio.com - Import Repository
- Git-Tf
- Git-Tfs (recommended)
VisualStudio.com - Import Repository
VisualStudio.com gives you the ability to import from a TFVC repository into a new Git repository.
Bad Example - Built in tool has several limitations
If you don't care about source control history, then this inbuilt tool is the easiest to use. It has the limitations of:
- 180 days of history
- No branches
TIP - Use this if you don't care about source control history
Git -Tf
Git-Tf is an open source command line tool that works cross platform and use the Java TFS SDK. This tool is useful for migration if you're not on a Windows environment. This tool is not maintained and has issues with migrating branches.
To see how to use this to migrate see "Migrate an existing project from TFS to Git with changeset history intact" from Chris Kirby
TIP - Use Git-Tf if you don't have a Windows environment
Git-Tfs ( Recommended)
Git-Tfs is an open source command line tool that uses the .NET TFS SDK to interface between Git and TFVC. It has the following advantages over the other tools:
- Actively maintained
- Good support for branches
- Author mapping
- Migrates all history
Follow the migration guide to import from TFVC to Git and then proceed with the after migration steps. To help you do a smoother migration, you can refer to this tool.
When you create a new git repository and need to push it to VSTS you need to provide login credentials.
It isn't always clear how to do this.
Instead, you should use Personal Access Token. You can do this in two ways.
The first option is to make sure your Git for Windows is up-to-date and when cloning the repository, you use Microsoft Account to log in. Personal Access Token for Git will be created for you.
Option 2 is to manually create Personal Access Token and use it as a password for Git login.
You can follow this blog post for full instructions: Using Personal Access Tokens to access Visual Studio Online.
After you use the right tool to migrate from TFVC to Git, there's a few more things you need to do to clean things up for a new user joining the project. By default, if there is a TFVC repository, that will become the default in the UI.
Unfortunately, you can't kill the TFVC repository and make Git the default one, so there's a few steps you need to follow.
Figure: Bad Example - Can't delete the now deprecated TFVC repository
Delete files from TFVC
Go into the repository, delete any existing files. Add a new document saying "_MigratedtoGit.md". This will stop people from getting the wrong code.
**Figure: Clean up TFVC so developers can't accidentally get the wrong source code
**Note : All the source code is still there, it's just flagged as being deleted.
Lock down TFVC
In the TFVC repository, click Security
Figure: Configure the security of the TFVC repository ** Then deny check-ins to **Contributors , P roject Administrators and Project Collection Administrators . This should stop anyone from committing new code to the repository.
Update the Dashboard
Next step is to update the dashboard to let new developers know.
Figure: Good example - Let new users know that the source control is now on Git
Suggestions for the VSTS team
- Give us the ability to hide a repository
- Give us the ability to set a repository as the default for all users
- Give us the ability to delete a TFVC repository
Having any of these suggestions will avoid the confusion on this screen
Figure: Bad Exmaple - This is confusing for a new dev
When starting to work on a project, it's common to wonder whether to fork an existing repository or create a new branch for it. Before making this decision, it's important to consider the key differences between the two options.
Figuring out whether to fork or branch
Generally, branching is a default option if you're working on a team developing a product. However, if you run into someone else's product and have new ideas you want to try, then forking is a good option because you can work on your ideas in isolation.
Tip: If unsure ask yourself 3 questions...
If your answer is 'no' to any of the following questions, then you should go for a fork:- Do you have access to the existing repository to clone a new branch?
- Is the change going to be part of that project and has it been approved by the Product Owner?
- Do you or anyone you're working with on that project own the existing repository?
Summary - Forking vs Branching
Fork Branch Purpose Create a separate copy of a repository for significant changes or different directions Develop new features or fix bugs without disrupting the main codebase Relationship to the original codebase Completely independent repository Linked to the original repository Ownership Owned by the user who created them Owned by the repository owner Scope of changes Typically involve significant changes Typically involve smaller changes Collaboration Used to develop ideas in isolation from the main team Used to develop ideas that the main team is working on Maintaining a clean git history is important for readability and understanding the changes that have been made to a codebase. This is especially important when working in a team, as it allows you to see the changes that have been made to a file over time, and who made them. This can be useful for debugging, and for understanding why a particular change was made.
Things that create a good git history include:
- Granularity of commits
- Descriptive commit messages
- Easy to maintain (i.e. easily revert an entire feature)
- Never lose history
Squashing Pull Requests
Video: Git MERGE vs REBASE: Everything You Need to Know (4 min)Squashing commits is the process of combining multiple commits into a single commit. This allows for the repository history to reflect changes to the application over time, rather than individual commits making up a feature which can be hard to follow as they give little context to the feature that the commit is being added for.
Good Pull Request titles
When creating a pull request, it's important to have a clear and descriptive title. This is important when looking back at the git history, as it allows you to see at a glance what changes were made in a particular pull request.
Preserving file history
Often developers will move files around and have major changes to the contents leading to the file history being lost as the the operation will be seen as a delete and add file because of the differences in contents.
This can be avoided by using the
git mv
command to move files, or by breaking up the operation into smaller chunks i.e.: change content and then move the file as separate commits.
📝 Git Tips - Making structural and content changes (4 min)Merging multiple repositories
Sometimes you have a system that was designed in multiple repositories, and you want to merge them back together. This can be a complex process, but it's important that you know what to do when this situation comes up in order to not lose history.
Source tip
The process of a source tip is to stop using source code from 1 repository as of certain date, and start using it from another repository.
This process leads to a situation where you have a couple of projects/folders of code all being added to a repository as a new commit, and developers would need to look in an alternate place to see the history of that portion of the codebase.
Figure: Bad example
Merge multiple repository histories into one
It's important when combining git repositories that you bring all the history with you. This means that even if the files were added physically to a new repository today, developers will be able to see all the commits originally created in the original repository.
Figure: Good example
Writing descriptive commit messages
It's important to have descriptive names for commits so that you can easily keep track of what was achieved after each commit was applied. Knowing what was achieved when the commits were made will make it easier to retroactively squash related commits as you'll know what work was done. In addition having descriptive commit messages makes it easier for a reviewer to see what you were trying to achieve in your pull request.
Read more about writing descriptive commit messages.
Conclusion
Maintaining a clean git history is important for readability and understanding the changes that have been made to a codebase over the lifetime of the repository.
Below is a summary of the things that create a good git history and the methods to achieve them:
Granularity of commits Descriptive commit messages Easy to maintain Never lose history Squashing Pull Requests ✅ ✅ ✅ Good Pull Request titles ✅ ✅ Preserving file history ✅ ✅ Merging multiple repositories ✅ ✅ Updating commit information can be essential for maintaining accurate project history or correcting errors. Whether you need to change a commit date for clarity, compliance, or other reasons, you have a couple of methods at your disposal.
This rule outlines how to change the date of an existing commit using both a manual CLI approach and an automated script.
Method 1 – Use CLI
-
Checkout to the branch containing the commit
git checkout -b {{ BRANCH NAME }} origin/{{ BRANCH NAME }}
-
Run git log to get the last commit hash
git log
-
Do an interactive rebase for the parent of the last commit
git rebase -i {{ COMMIT HASH }}^
-
This opens vi editor:
- Press "I" key to enter interactive mode,
- Change "pick" to "edit",
- Press "escape" to exit interactive mode,
- Type ":wq" to save and exit
-
Change the commit date
GIT_COMMITTER_DATE="{{ NEW DATE IN 'YYYY-MM-DD HH:MM:SS' FORMAT }}" GIT_AUTHOR_DATE="{{ NEW DATE IN 'YYYY-MM-DD HH:MM:SS' FORMAT }}" git commit --amend --no-edit
-
Finish the rebase
git rebase --continue
-
Force push to origin
git push origin {{ BRANCH NAME }} --force
Method 2 (recommended) – Use a script
If the date change is to be applied on several branches, it is preferable to automate the process with a script.
BRANCH=$1 DATE=$2 if [ -z "$BRANCH" ] || [ -z "$DATE" ]; then echo "Usage: $0 {{ BRANCH NAME }} {{ NEW DATE IN 'YYYY-MM-DD HH:MM:SS' FORMAT }}" exit 1 fi git checkout -b "$BRANCH" "origin/$BRANCH" LAST_COMMIT_HASH=$(git log -n 1 --pretty=format:"%H") git rebase -i "$LAST_COMMIT_HASH^" GIT_COMMITTER_DATE="$DATE" GIT_AUTHOR_DATE="$DATE" git commit --amend --no-edit git rebase --continue git push origin "$BRANCH" --force git checkout main
The script can be actioned with the following command:
./change_history.sh "{{ LOCAL PATH }}" "{{ NEW DATE IN 'YYYY-MM-DD HH:MM:SS' FORMAT }}"
-