﻿# Working as a team in git: issues, branches, merge requests

> Milestone 2: the team workflow – protected main and develop, a branch per issue, commit messages checked by git hooks, labelled issues on a board, and reviewed merge requests merged with merge commits.

Source: https://laszlonemes.com/university/software-technology/version-control · Author: László Nemes

Milestone 2 · Software Technology

Milestone 2: the team workflow – protected main and develop, a branch per issue, commit messages checked by git hooks, labelled issues on a board, and reviewed merge requests merged with merge commits.

14 min read `main, develop, feature branches` `Branch and commit conventions` `Commit hooks` `Issues, labels, milestones` `Merge requests` `Common team mistakes`

Milestone 2, in week 7, is the first prototype: about 30% of the use cases work, and **version control is working and actively used** by every member. From this milestone on, your GitLab activity – issues, branches, commits, merge requests, reviews – weighs heavily in your score, because it is the record of who did what.

This page is the team workflow the course recommends, as packaged in its template repository: two protected branches, a branch per issue, a fixed commit format checked by git hooks, and merge requests reviewed by a teammate. It assumes you know commits, branches and merges; if not, read [Git and GitHub: the mental model, in pictures](https://laszlonemes.com/blog/git-and-github) first.

After this session you can

- explain what main, develop, feature, release and hotfix branches are for
- name a branch and write a commit message the hooks accept
- take an issue from the board to a merged merge request
- explain why the course uses merge commits and not squash merges
- recognise the three classic team mistakes in a commit graph

## The rules on one screen

| What | Rule |
| --- | --- |
| Branches | `main` = released state, `develop` = integration. Both protected: nobody pushes to them, code arrives only through merge requests. |
| Branch name | `feature/ABC-12-short-title` (or `bugfix/`), where ABC-12 is issue #12. One issue = one branch. Starts from `develop`, returns to it. |
| Commit | `ABC-12: add save file validation` – imperative, English, at most 72 characters. The hook adds the prefix from the branch name. |
| Issue | All work starts from an issue: template, milestone, assignee, type label. |
| Board | Todo → Doing → Review → Closed. You move the card; the merge request closes the issue. |
| Merge request | Template filled, one approval from a teammate, green pipeline, all threads resolved, merge commit. The author merges. |
| Definition of Done | Acceptance criteria met, unit tests for the model, pipeline green, MR merged, wiki updated. |

`ABC` stands for your project's 2–5 letter key; the template uses `KEY` as a placeholder that you replace once. Older course material says `master`; new GitLab projects create `main` – same role, different name.

## Branches: main, develop, one per issue

Two long-lived branches carry the project. `develop` is where finished work comes together, and it should always build. `main` only moves when the team releases a milestone. All actual work happens on short-lived branches that start from `develop` and come back through a merge request (Fig. 1).

> **Fig. 1** · Feature branches start from develop and return to it. A release branch carries a milestone into main, where it is tagged; main then goes back into develop.
>
> *Diagram:* The branch model as a commit graph. develop branches off main. Feature branches named after their issue, ABC-12 and ABC-13, start from develop and are merged back into it with merge commits. Before the milestone a release branch starts from develop, is merged into main, which is tagged v1.0, and main is merged back into develop.

| Branch | Purpose | From | Back into |
| --- | --- | --- | --- |
| `main` | Released, demoed state. Tagged after every merge. | – | – |
| `develop` | Integration. Feature MRs land here; always builds. | – | – |
| `feature/ABC-12-…` | One issue: a feature, but also a refactor, tests, docs. | `develop` | `develop` |
| `bugfix/ABC-34-…` | A bug found on develop. | `develop` | `develop` |
| `release/v1.0` | Finishing a milestone release: version, README, small fixes. | `develop` | `main`, then `main → develop` |
| `hotfix/ABC-56-…` | An urgent fix on the released version. | `main` | `main`, then `main → develop` |

Branch names

```text
feature/ABC-12-implement-save-load     ← issue #12, a new feature (also: refactor, tests, docs)
bugfix/ABC-34-fix-wall-collision       ← issue #34, a bug found on develop
release/v1.0                           ← preparing the milestone release, no issue number
hotfix/ABC-56-crash-on-empty-save      ← issue #56, urgent fix on the released version

anna-branch, peter-dev, test2, final   ← not branch names: they say who, not what
```

The issue page in GitLab has a *Create merge request / Create branch* button that generates the name for you, in exactly this form if the project's branch name template is set. And push your branch every day, finished or not: it is your backup, and it shows the team where you are.

## Issues, labels, milestones, the board

**Every piece of work is an issue** – a feature, a bug, a refactor, a test, a page of documentation, CI work. No issue, no branch, no commit. An issue is created from a template (Feature or Bug), has a short imperative title (*Implement save/load*), exactly one assignee, a milestone and a type label. If two people work on something, it is two issues.

- **`type::…`** – feature, bug, refactor, test, docs, ci – what kind of work it is.
- **`status::…`** – todo, doing, review – the board column. Scoped labels exclude each other, so an issue sits in one column.
- **`priority::high`** – Only when it matters.
- **Milestone** – M1 … M4, due on the weeks 4, 7, 10 and 13. What doesn't fit moves to the next one – it never hangs in the air.

The templates end with GitLab quick actions, which run when the issue is saved – that is how every new issue gets its labels without anyone remembering to add them:

The end of an issue description

```md
## Acceptance criteria
- [ ] Save creates a file in the user's save folder
- [ ] Error case: loading a corrupt file keeps the current game

/label ~"type::feature" ~"status::todo"
/milestone %"M2 Prototype I"
/assign me
```

The path of one issue across the board and through git (Fig. 2):

> **Fig. 2** · You move the card to Doing when you start and to Review when you open the merge request. Closing is done by the merge, through Closes #12 – not by hand.
>
> *Diagram:* An issue's path. Top lane, the issue board: Todo, Doing, Review, Closed. Bottom lane, what you do: create the issue from a template with a milestone and yourself as assignee; create the branch named after it and commit; open a merge request that says Closes #12 and has a reviewer; merge, which closes the issue. If the reviewer asks for changes, the card goes back from Review to Doing.

From issue #12 to a pushed branch

```shell
git switch develop
git pull
git switch -c feature/ABC-12-implement-save-load
# ... work, then commit in small steps:
git add -p
git commit -m "add save file serialization"      # the hook makes it "ABC-12: add save ..."
git push -u origin feature/ABC-12-implement-save-load
```

The full command sequence, including updating your branch and cleaning up after the merge, is in the [issue lifecycle cheat sheet](https://laszlonemes.com/university/software-technology/issue-lifecycle).

## Commit messages and hooks

The format is `ABC-12: imperative summary`. Imperative, in English: *add*, *fix*, *remove* – read it as "this commit will … add save file serialization". One commit is one logical step. If a reason needs explaining, add a body after a blank line: the body says *why*, the diff already says *what*.

Good and bad first lines

```text
ABC-12: add save file serialization          ✓ imperative, says what this step does
ABC-12: handle missing save directory        ✓ one logical step
ABC-34: fix crash when settings file is empty ✓

ABC-12: fixed stuff.                         ✗ past tense, says nothing, ends with a period
wip                                          ✗ no key, says nothing – the hook rejects it
ABC-12: update                               ✗ "update" what?
```

A commit with a body

```text
ABC-84: retry save when the target directory is locked

On Windows the save directory can be locked for a moment by the
antivirus; a single retry after 100 ms removes the spurious failure.
```

You don't type the prefix. Two git hooks in the repository's `githooks/` folder do the bookkeeping (Fig. 3): `prepare-commit-msg` reads the issue key from the branch name and prepends it, `commit-msg` checks the result and refuses a bad message with an explanation.

> **Fig. 3** · The branch name carries the issue key, the hook copies it into every commit, and the second hook rejects messages that would make the history useless.
>
> *Diagram:* Two git hooks run on every commit. You type a short message. prepare-commit-msg reads the issue key from the branch name, feature/ABC-12-save-load, and puts ABC-12: in front. commit-msg checks the result – prefix, at most 72 characters, no final period, not just wip – and either creates the commit or rejects it with a message saying why.

Hooks are not cloned automatically – each member switches them on once per clone. They run inside git itself, so they work from the terminal, IntelliJ, Rider, Visual Studio, VS Code and GitHub Desktop alike (on Windows through Git for Windows' bundled bash).

Once per clone, every member

```shell
git clone <repo-url>
cd <repo>
./setup-hooks.sh                 # once per clone; Windows cmd / PowerShell: .\setup-hooks.bat
# it runs: git config core.hooksPath githooks
```

The core of the two hooks

```shell
# prepare-commit-msg: take the key from the branch name ...
TICKET=$(printf '%s' "$BRANCH" | sed -nE 's#^[a-z]+/([A-Za-z]{2,5}-[0-9]+).*#\1#p')

# commit-msg: ... and reject a first line that doesn't follow the rules
printf '%s' "$FIRST_LINE" | grep -qE '^([A-Z]{2,5}-[0-9]+|release|hotfix|chore|docs|ci): .+' \
  || fail "missing or invalid prefix"
[ ${#FIRST_LINE} -gt 72 ] && fail "subject line is too long"
```

### Why ABC-12 and not #12?

- In git, `#` at the start of a line is a comment: a message written in the editor as `#12: add …` loses its first line and git aborts with "empty commit message".
- A key says which project the issue belongs to, even when the commit is quoted somewhere else. It is the form Jira and most companies use, so the habit transfers.
- GitLab links the issue through the branch name and the Closes #12 line of the merge request; the key in the message is for people reading git log and git blame.

Common mistake

**--no-verify**

`git commit --no-verify` skips the hooks. Don't. If the hook rejects a message, its output says exactly what to change – a missing prefix usually means you are on the wrong branch.

## Merge requests

A merge request (MR; on GitHub: pull request) asks the team to take your branch into `develop`. Its title is the issue's title; its description follows the template – what and why, how you tested it, and `Closes #12`. Open it early with `Draft:` in the title if you want feedback before it is ready.

The platform enforces four merge conditions:

1. at least one approval from a teammate – never from the author,
2. a green pipeline (from M3; until then the setting accepts skipped pipelines),
3. every review thread resolved,
4. no conflict with `develop` – if there is one, the author resolves it, not the reviewer.

After the approval, **the author merges**, because the author knows whether everything is done. The merge method is a **merge commit** – no squash, no fast-forward (Fig. 4).

> **Fig. 4** · A merge commit keeps every step and its author and marks where the MR ended. A squash replaces them with one commit – common in companies, switched off in this course because grading reads the individual commits.
>
> *Diagram:* Left, a merge commit: the three commits of the feature branch stay in the history and the merge commit marks where the merge request ended. Right, a squash merge: the three commits are replaced by one, so the individual steps – and who did what – disappear from the history.

With merge commits, deleting the source branch after the merge loses nothing – the commits are in `develop` – so GitLab is set to delete it automatically. How to write and answer a review is in the [code review guide](https://laszlonemes.com/university/software-technology/code-review).

## Common mistakes

Three patterns show up every semester, and all three are visible in the commit graph (Fig. 5).

> **Fig. 5** · A branch per person drifts for weeks and ends in a merge nobody can review. A branch per issue lives for days and merges cleanly.
>
> *Diagram:* Left: branches named after people, anna and peter, live for weeks next to master and end in one huge conflicting merge. Right: a branch per issue – ABC-12, ABC-13, ABC-14 – each lives a few days and merges into develop, so the history stays small and readable.

Common mistake

**Everything on master**

Commits straight onto the main branch, no MRs, no review. It works until two people push the same afternoon. Protected branches make it impossible; if you still can push to `main` or `develop`, the protection is missing – see the [repository setup guide](https://laszlonemes.com/university/software-technology/repository-setup).

Common mistake

**Branches named after people**

`anna`, `peter-dev`: a branch per person becomes a private copy of the project. It drifts, gets merged rarely and painfully, and nobody can tell which issue a commit belongs to. Name the branch after the issue.

Common mistake

**One person commits everything**

If one member pushes the whole team's work, the history says one member did the project – and that is what gets graded. Everyone commits from their own account. Check that `git config user.email` matches your GitLab account, or your commits won't appear under your name.

Common mistake

**Deleting the evidence**

If a team squashes merges anyway (it is switched off in the recommended setup), the feature branch is the only place the individual commits still exist – deleting it after the squash erases the work log. With merge commits this problem doesn't exist.

## What the M2 demo shows

In your assignment

**Prototype I**

- About 30% of the use cases work in the running game – end to end, not half of each.
- Every member has issues, branches, commits and at least one merged MR of their own; every member has reviewed.
- The board and the milestone page show the state honestly: closed issues for what is done, the rest moved to M3.
- A release: `release/v1.0` from `develop`, merged into `main`, tagged `v1.0`, merged back. The steps are on the [final product page](https://laszlonemes.com/university/software-technology/final-product).

> Show the graph in the demo. A minute on the repository – the board, the MR list, the network graph – tells the instructor more about the process than any slide.

Good to know

**Small issues, small MRs**

An issue is at most a few days of work, an MR at most a few hundred lines. If it is bigger, split it: first the model with its tests, then the view.

## Summary

- main = released, develop = integration; both protected. Work happens on one short-lived branch per issue.
- Branch: feature/ABC-12-short-title. Commit: ABC-12: imperative summary – the hooks add and check the prefix.
- Every piece of work is an issue with a template, a milestone, one assignee and a type label.
- Merge requests need a teammate's approval, a green pipeline and resolved threads; the author merges with a merge commit.
- Avoid: everything on master, branches named after people, one person committing for the team.
