The larger the project, the more things tend to spiral into chaos. Branches and tags reign the chaos back in, keeping development in established lanes for branching and merging code changes.
There are myriad ways of branching and merging. Document how contributors should interact with branches, or adopt Git Flow, the de facto standard for working with branches on git projects.
For each feature, issue, or ticket, dedicate a specific branch; don't just work on trunk or master. In a large, many-developer, many-feature project, feature branches isolate changes, making features easier to test, maintain, deploy, and roll back. Feature branches are especially good for git users, as git branches are computationally inexpensive.
Name feature branches after the feature, issue, or ticket number. For example:
When the feature/issue/ticket is resolved, merge the code into a more official branch such as dev
for development, or master
if you're using an unstable master branch workflow.
When a milestone has been reached, like a set of features and bugfixes implemented, create a version control tag. For example, engineers may want to compare code between versions to diagnose a bug. Or, libraries that depend on one another can use a tagged, non-SNAPSHOT version to avoid API fluctuation.
Tagging records the code at a specific point in time, in terms of a given version number and commit ID, in a way that helps answer these sorts of questions about a codebase.
Example:
$ echo '{ "version": "1.0" }' > version.json
$ git commit -am 'bump to v1.0'
$ git tag -a 'v1.0' -m 'v1.0'
$ git push --tags