Collaborative Git Workflows⚓
When starting a project, especially a collaborative project with a lot of contributors, you need to decide how to organize the development process. If joining an existing project, you need to thumb through their documentation or contact the project maintainers to see how to contribute to their project.
This guide is oriented for the use of Gitlab as the server hosting the remote repository of a project, though it can easily be adapted for Github or any other online software forge offering Git support.
Branches and Workflows⚓
One of the thing you need to decide upon quickly, is the branching strategy you will use as a team. This is rather important, as it will determine what are the roles of each branch, how the creation and merging of branches should proceed and what development workflow you will generally use.
Fortunately, you don't have to come up with your own workflow, as git users have been experimenting on those for decades. Here are the more popular:
Workflow | Key Features | Pros | Cons |
---|---|---|---|
Trunk-Based | - Single long-lived main branch- Short-lived feature branches - Frequent merges - Often uses feature flags |
✅ Simple and fast integration ✅ Encourages CI/CD ✅ Fewer long-running branches |
❌ Requires strong CI discipline ❌ Feature flags can be complex ❌ Less release control |
Git Flow | - main + develop branches- Long-lived branches - Separate feature , release , hotfix branches |
✅ Structured process ✅ Supports release staging ✅ Isolates features and fixes |
❌ Complex to manage ❌ Slower delivery ❌ Not CI/CD-friendly without automation |
GitHub Flow | - Single main branch- All changes via PR - Rapid short-lived branches - Continuous integration/deployment |
✅ Very simple ✅ Ideal for rapid delivery ✅ Encourages review & testing |
❌ No formal release flow ❌ Less suitable for large teams ❌ Needs strict discipline |
Trunk-Based Development⚓
Principle⚓
Trunk-based development encourages all developers to work in a single shared branch—usually called main
—and to integrate small, frequent changes. Feature branches are short-lived and merged back rapidly, often multiple times per day. This approach reduces integration risk and aligns with continuous integration/deployment.
Git Graph⚓
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true}} }%%
gitGraph
commit id: "Initial commit"
commit id: "Main development"
branch feature-a
commit id: "Feature A work"
checkout main
commit id: "Minor fix"
merge feature-a id: "Merge Feature A"
branch feature-b
commit id: "Feature B work"
checkout main
merge feature-b id: "Merge Feature B"
commit id: "Release prep"
Key Features⚓
- A single long-lived branch (
main
) - Short-lived feature branches merged quickly
- Strong reliance on CI/CD and testing
- Often used with feature toggles/flags
Pros⚓
- Simplifies branching strategy
- Encourages frequent integration
- CI-friendly, ideal for DevOps and CD
- Reduces merge conflicts and drift
Cons⚓
- Requires a solid CI setup to avoid breakage
- Feature flags can add complexity
- Less structure for managing releases
Git Flow⚓
Principle⚓
Git Flow separates code into multiple branches for features, releases, and hotfixes. It uses a develop
branch for integration and a main
branch for production. Releases are prepared in a dedicated release
branch, and urgent patches are handled through hotfix
branches.
Git Graph⚓
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true}} }%%
gitGraph
commit id: "Initial commit"
branch develop order: 3
commit id: "Dev work"
branch feature-x order: 4
commit id: "Feature X"
checkout develop
merge feature-x id: "Merge Feature X"
branch release order: 1
commit id: "Release prep"
checkout main
merge release id: "Release to main" tag: "v1.0"
checkout develop
merge release id: "Merge release back"
branch hotfix order: 2
commit id: "Hotfix 1"
checkout main
merge hotfix id: "Hotfix to main" tag: "v1.0.1"
checkout develop
merge hotfix id: "Hotfix back to develop"
Key Features⚓
main
holds only production-ready codedevelop
is the integration branch- Long-lived feature, release, and hotfix branches
- Uses semantic versioning and tags
Pros⚓
- Clear structure for teams with scheduled releases
- Isolates features and bugfixes
- Suitable for large projects with QA/staging phases
Cons⚓
- Overhead of managing multiple branches
- Slower integration cycle
- Confusing for beginners
- Not ideal for continuous delivery
GitHub Flow⚓
Principle⚓
GitHub Flow focuses on continuous deployment and simplicity. All development happens in short-lived branches off main
, and every change goes through a pull request (merge request). Merges only happen when tests pass and code is reviewed.
Git Graph⚓
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true}} }%%
gitGraph
commit id: "Initial commit"
commit id: "Main updates"
branch feature-x
commit id: "Feature X work"
commit id: "Polish"
checkout main
merge feature-x id: "Merge Feature X"
branch feature-y
commit id: "Feature Y work"
checkout main
merge feature-y id: "Merge Feature Y"
commit id: "Release v1.0" tag: "v1.0"
Key Features⚓
- Always-deployable
main
- Each feature via pull request
- Continuous delivery emphasis
- Review-first approach
Pros⚓
- Simple and effective
- Encourages fast iterations
- Great for open-source and startups
- Works well with modern CI/CD tools
Cons⚓
- Can be too simplistic for enterprise workflows
- No built-in support for hotfixes or releases
- Relies heavily on discipline for versioning
Merge Strategies⚓
When collaborating on code using Git, how you integrate changes from one branch into another has a big impact on your project's commit history, traceability, and debugging ease. Git supports multiple merging strategies, each with its pros and cons depending on your project's structure and workflow.
Merge Commit (default)⚓
Command:
1 |
|
What it does: Creates a new commit that combines the changes of both branches, preserving the history of both lines.
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true}} }%%
gitGraph:
commit id: "Initial"
commit id: "Modif1"
commit id: "Modif2"
branch feature-branch
commit id: "feature: Modif1"
commit id: "feature: Modif2"
checkout main
merge feature-branch id: "Merge feature-branch" type: HIGHLIGHT
Use when:
- You want a clear record of when two branches were merged
- Preserving the full history is important
- You're following Git Flow or working in teams with multiple branches
✅ Pros:
- Maintains full history
- Easy to trace merges
- Safer in collaborative environments
❌ Cons:
- More verbose history
- Can create "merge commits noise" if overused
Fast-Forward Merge⚓
Command:
1 |
|
What it does:
Moves the target branch pointer forward to the tip of the feature branch—no new commit created.
Let's say we have created the same feature-branch
as in previous section, with the same 2 commits.
We will get the following state after fast-forward merging:
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true}} }%%
gitGraph:
commit id: "Initial"
commit id: "Modif1"
commit id: "Modif2"
commit id: "feature-branch: Modif1"
commit id: "feature-branch: Modif2" type: HIGHLIGHT
Use when:
- The target branch hasn’t diverged (e.g., working solo or rebased feature branches)
- You want a clean, linear history
✅ Pros:
- Clean, linear history
- No unnecessary commits
❌ Cons:
- History can be misleading (harder to trace features)
- Not possible if branches diverged
Squash and Merge⚓
Command:
1 |
|
Or via GitHub/GitLab UI: “Squash and Merge”
What it does: Combines all commits from the feature branch into a single commit on the target branch. Does not create a merge commit.
Let's say we have created the same feature-branch
as in Merge commit (default), with the same 2 commits.
We will get the following state after squash merging and commiting:
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true}} }%%
gitGraph:
commit id: "Initial"
commit id: "Modif1"
commit id: "Modif2"
commit id: "Squash feature-branch" type: HIGHLIGHT
Use when:
- You want to simplify history
- Feature branch has lots of small commits
- You care about what changed, not how
✅ Pros:
- Clean history
- Ideal for small features or bugfixes
- Encourages meaningful commit messages
❌ Cons:
- Loses individual commit history
- Harder to debug incremental steps
- Makes cherry-picking difficult
Rebase (alternative to merging)⚓
Command:
1 2 |
|
What it does: Moves the entire feature branch onto the tip of another branch, rewriting history.
Let's say we have the following git commit graph before rebasing:
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true}} }%%
gitGraph:
commit id: "Initial"
commit id: "Modif1"
branch feature-branch
commit id: "feature: Modif1"
commit id: "feature: Modif2" type: HIGHLIGHT
checkout main
commit id: "Modif2"
commit id: "Modif3"
We will get the following after:
%%{init: { 'logLevel': 'debug', 'theme': 'base', 'gitGraph': {'showBranches': true, 'showCommitLabel':true}} }%%
gitGraph:
commit id: "Initial"
commit id: "Modif1"
commit id: "Modif2"
commit id: "Modif3"
branch feature-branch
commit id: "feature: Modif1"
commit id: "feature: Modif2" type: HIGHLIGHT
Use when:
- You want a linear history
- You’re working solo or with rebasing discipline
✅ Pros:
- Very clean history
- Useful before merging to main
❌ Cons:
- History rewriting can be dangerous in shared branches
- Requires discipline and understanding
Summary Table⚓
Strategy | Creates New Commit? | History | Best For |
---|---|---|---|
Merge Commit | ✅ Yes | Full & branched | Team collaboration, Git Flow |
Fast-Forward | ❌ No | Linear | Solo dev, up-to-date main |
Squash and Merge | ✅ Yes (single commit) | Flattened | Simplifying noisy commit history |
Rebase | ❌ No (rewrites history) | Linear | Clean history, solo work before merging |
Protected Branches⚓
Protected branches are branches in a Git repository that have restrictions placed on them to prevent unwanted changes. This is a key feature for maintaining code quality and enforcing collaboration workflows.
What Are Protected Branches?⚓
In Git (and especially in platforms like GitLab, GitHub, Bitbucket), protected branches are used to:
- Prevent direct pushes
- Require merge requests (MRs) for changes
- Enforce successful CI before merge
- Restrict who can push, merge, or force-push
- Prevent branch deletion
By default, teams often protect shared branches like main
, develop
, or release branches to enforce safe collaboration practices.
Common Protections⚓
Feature | Description |
---|---|
Disallow force-push | Prevents rewriting history |
Prevent direct pushes | Only allows changes via Merge Requests |
Require code review | Only merges allowed after approval(s) |
Enforce status checks / CI success | Merges blocked until CI passes |
Restrict who can merge | Limit merging to Maintainers or specific users/groups |
Prevent deletion | Avoid accidental loss of important branches |
When to Use⚓
Use protected branches to:
- Protect production and release code
- Enforce team collaboration workflows
- Avoid accidental or malicious changes
- Integrate code reviews and CI as gates
Forks⚓
Whenever you are joining an existing project, especially in the open source community, the usual workflow starts by creating your own copy of the remote repository called a fork.
This simply means:
- cloning the online repository on your local machine
- create a remote repository on your forge of choice
- Rename the existing repository remote as something other than
origin
(e.gupstream
) - add your new repository as the
origin
remote of your local repository. - Make your new
origin
repository the tracked remote for the main working branch - Push to your new repository
The whole process can look like this, let's say you already created a new empty repository for your fork of another repository:
1 2 3 4 5 |
|
This means that you will have two remotes:
origin
: your forked repository which you ownupstream
: the original repository which you forked
Most forges offer a direct way to fork a repository on their platform.
A fork simply means you copy a specific version of another repository, then continue development from that point on. This will make the two repository diverge on the long run.
You can always get the latest changes from the original upstream
repository and apply them to your own forked repository. This can look like this:
1 2 3 |
|
If you want to make both repository converge, you might want to push your changes back to the other repository. Depending on the other repository rules and permissions, this might/should be done using a pull request (Github) or merge request (Gitlab).
Info
When directly pushing your branch to the upstream
remote if the latter has configured the use of pull requests/merge requests, this will fail and propose you a link to create the request on the upstream
repository.
Pull/Merge Requests⚓
A pull or merge request is an official request put on an online repository for your changes to be merged on. This can be used for instance to merge changes from a forked repository back to the original (upstream
) repository.
Though it might also be used inside a single repository to merge distributed tasks.
The creation and management of such requests is generally done directly on the software forge website. It demands filling a form to explain what your changes are, making your case for merging them back on the original repository. It will then take the shape of a git branch, on which you will push those changes, created on the original repository.
Depending on the maintainer's review process, the interaction will probably proceed on the MR page. You will need to make your case for those changes, possibly adapt and push them, until the maintainer formally accept or decline them.
Some repositories might be configured in a way that MR can only be done in association with an open issue. In that case, you should open an issue, presenting the issue your changes are solving or the new feature it introduces. Then you can create a branch for that issue, and push your changes to it. Once you are ready for review, you can then open the MR (or you can create the branch and MR at the same time). In that case, the discussions will probably take place on the issue page, though if a Continuous Integration (CI) pipeline is set up by the maintainer, it might give feedback on the MR page (e.g tests passing or failing, code bad/well formatted, etc).
Tip
Even if working with others on a single repository, it really is a good practice to enforce strict compliance rules when merging changes on a shared main branch. Those might even be automated by a Continuous Integration (CI) pipeline. Yet simply forcing you and your contributors to go through this formal process, especially if you apply a review process on MR, is a great way to work cleanly on a collaborative project.
Code Review Process⚓
Code review is a fundamental practice in professional software development. It ensures quality, consistency, and knowledge sharing across a team. Regardless of the Git forge (GitLab, GitHub, Bitbucket…), the principles remain the same, though tooling and terminology may differ slightly.
What Is Code Review?⚓
A code review is the process of systematically examining changes made in a codebase—usually through a merge request (MR) or pull request (PR)—before merging them into a shared branch (typically main
, develop
, or similar).
🔁 Code reviews are typically initiated when a developer opens a merge request or pull request on the project’s Git forge.
Code Review Flow (Generalized)⚓
-
Create a Feature Branch Work on a new feature, bugfix, or improvement in an isolated branch.
-
Push Changes Push commits to the remote repository (the forge hosts this).
-
Open a Merge Request (MR) / Pull Request (PR) This is the formal request for code review and merging. Done via the forge UI (e.g. GitLab → Merge Request, GitHub → Pull Request).
-
Review and Feedback Teammates or designated reviewers:
- Comment on specific lines or files
- Ask for clarifications or changes
- Approve or request changes
-
Iterate The author addresses feedback by pushing additional commits to the same branch.
-
Merge Once approved, the MR/PR is merged using the selected merging strategy (merge commit, squash, or rebase).
Best Practices for Code Review⚓
For Authors | For Reviewers |
---|---|
Keep changes small and focused | Review promptly and constructively |
Add clear title and description | Test changes if applicable |
Link issues or user stories | Ask questions instead of making assumptions |
Document design decisions in the MR/PR | Look for bugs, maintainability, style |
Re-request review after updates | Approve only when you're confident |
Code Review Features in Git Forges⚓
While the process is mostly the same, forges provide tooling to support it:
Feature | GitLab / GitHub / Bitbucket Notes |
---|---|
MR/PR discussions | Inline comments on specific lines or files |
Suggested changes | Direct suggestions that can be committed with one click |
Approvals | Require approval from one or more reviewers before merge |
CI status integration | Block merge if CI fails |
Reviewers assignment | Assign specific people or teams to review |
Draft MRs/PRs | Indicate work in progress to prevent early merging |
Merge checks | Enforce branch protection rules (e.g., only merge if approved) |
When Is Review Mandatory?⚓
You may enforce review as mandatory by:
- Protecting branches so only merge requests are allowed
- Requiring a minimum number of approvals before merge
- Blocking merge until CI passes
These configurations are done in the forge settings for each repository or project.
Why It Matters⚓
- Improves quality: Prevent bugs and regressions early
- Increases team knowledge: Share design decisions and context
- Encourages consistent style and practices
- Fosters collaboration: Everyone becomes familiar with the whole codebase
Code Review Checklist (example)⚓
It is a good idea to formalize the objectives of code review as a checklist, which contributions (e.g Merge Requests) must follow to be accepted. Depending on the project, those can vary, but here is a generic example of such checklist for a typical python project.
Note
This is list is rather exhaustive, and contains topics which you may not be familiar with yet. We strongly recommend you to skip it for now, and return to it once you have fully read the Python Professional Software Development guides serie.
🔍 General⚓
- [ ] Is the purpose of the change clear from the merge/pull request title and description?
- [ ] Are the changes appropriately scoped (small, focused, single concern)?
- [ ] Are unrelated changes (e.g., formatting, config tweaks) avoided?
🧠 Correctness & Logic⚓
- [ ] Does the code do what it claims to do?
- [ ] Are edge cases and error conditions handled?
- [ ] Are assumptions clearly documented or enforced?
- [ ] Are unit tests provided for new or changed logic?
🧼 Code Style & Formatting⚓
- [ ] Is the code PEP 8 compliant? (use
flake8
,black
, etc.) - [ ] Are variable and function names meaningful and consistent?
- [ ] Are docstrings (PEP 257) used for public functions, classes, and modules?
- [ ] Is code duplication avoided?
🧪 Tests⚓
- [ ] Are there sufficient tests (unit, integration, etc.)?
- [ ] Do the tests cover common and edge cases?
- [ ] Do all tests pass locally and in CI?
- [ ] Are tests fast and reliable (no random failures)?
📦 Dependencies & Environment⚓
- [ ] Are new dependencies justified and minimal?
- [ ] Are new packages added to
requirements.txt
orpyproject.toml
appropriately? - [ ] Are environments reproducible (e.g., via
venv
,pip
, orpoetry
)?
📚 Documentation⚓
- [ ] Are public APIs (functions, classes) documented?
- [ ] Is the README or usage guide updated if relevant?
- [ ] Is inline documentation used where the logic is non-obvious?
- [ ] Are any newly added configuration or CLI flags documented?
🔐 Security & Robustness (as applicable)⚓
- [ ] Are inputs validated and sanitized?
- [ ] Are secrets, passwords, or tokens avoided in code and logs?
- [ ] Are exception-handling blocks appropriate (no silent
except:
)? - [ ] Is sensitive functionality protected (e.g., file writes, subprocesses)?
🧰 Tooling & Build⚓
- [ ] Does the change pass linters (e.g.,
flake8
,pylint
)? - [ ] Does the change follow project conventions (imports, logging, typing)?
- [ ] If the project uses
pre-commit
, do all hooks pass?
Optional (but recommended)⚓
- [ ] Are type annotations (
typing
) used where helpful? - [ ] Is logging used instead of
print()
? - [ ] Are any performance-critical sections optimized or profiled?