Practical Session — Notes

Intro

In the theoretical session, we went from local/offline all the way to online and shared — from your machine to your GitLab to others' GitLab.

Here we go the other way around:

  1. The repo lives on my GitLab — that's where we start
  2. You bring it to your GitLab (fork)
  3. Then you bring it to your PC, locally, offline
  4. And then we start having fun

From my GitLab to yours — Fork

The repo is hosted on my GitLab. You don't have write access to it — and that's normal, you're not supposed to push directly to someone else's repo.

What you do instead is fork it: you make your own copy of it on your GitLab account. Again a copy, and you guessed it, it's not a dumb/classic copy, it's a smart one. ;)

That copy is yours — you can do whatever you want with it.

Steps:

  1. Go to the repo: https://gitlab.com/thibaultcimic/git_tutorial

  2. Click the Fork button (top right)

  3. Select your own namespace (your GitLab account)

  4. GitLab creates a copy of the repo under your account — done

You now have gitlab.com/yourusername/git_tutorial with the full history of the project.


From your GitLab to your PC — Clone

Now you bring your fork down to your machine. This is called cloning.

Get the SSH URL:

  1. Go to your fork on GitLab

  2. Click the Clone button

  3. Copy the SSH URL (looks like git@gitlab.com:yourusername/git_tutorial.git)

Clone it:

git clone git@gitlab.com:yourusername/git_tutorial.git

This creates a folder git_tutorial/ on your machine with the full repo inside. If you wanted a different folder name, you'd just add it at the end: git clone <url> my-folder — but git_tutorial is fine here.

Now, to be ready to work, run these commands:

cd git_tutorial
git config core.hooksPath hooks
git config merge.ff false
git config --global user.name "Any Name"
git config --global user.email "any@email.com"

Git signs every commit with your identity — these two lines tell it who you are. It's just metadata in the commit history, not your GitLab login credentials — you can put anything. --global means one identity per machine, not one per repo.

If you use the same email as your GitLab account, GitLab will recognise your commits and link them to your profile. Not required — just a nice touch.


On your PC — Working locally

Now the fun part.


Exercises

Exercise 1 — Set up your workspace

Following the branch hierarchy guideline from the theory session, the first thing we do is create our branches.

main
└── dev-yourname
        ├── dev-yourname-feature1
        └── dev-yourname-feature2
  • main — never edit files directly on this branch. Changes come in by merging.
  • dev-yourname — your personal dev branch, branched from main
  • dev-yourname-feature1, dev-yourname-feature2, ... — one feature branch per task. Here we'll use dev-yourname-participant for the exercise, but in a real project you'd have as many as you need.

Advanced: this "never edit main directly" is a guideline here — nothing is technically stopping you. But you can go further: git hooks (pre-commit, pre-receive) can be written to reject direct commits to main at the local or server level. And on GitLab/GitHub, you can enable branch protection to enforce this remotely — no direct pushes to main allowed, changes must go through an MR. That's how most real projects work.

Create your dev branch and switch to it:

git branch dev-yourname main    # create dev-yourname branching from main
git checkout dev-yourname       # switch to it

Branch out from it for the actual work:

git branch dev-yourname-participant    # create the feature branch — base is dev-yourname, since that's where you are
git checkout dev-yourname-participant  # switch to it

The default base for git branch is whichever branch you're currently on — so you don't need to specify it explicitly. And you can collapse the two commands into one:

git checkout -b dev-yourname-participant   # create AND switch in one go

And that's it — we're set and ready to start working. Might feel like a lot of setup, but for what you gain in terms of power, it's worth it. That's why the whole world is doing it — and so do you now!


Exercise 2 — Write your participant file

Copy the participant template and rename it:

cp participants/TEMPLATE.md participants/firstname-lastname.md

Now run:

git status

This tells you the current state of your repo — what branch you're on, what files have changed, what's staged, what's not. Understanding everything it outputs on first try can be a little daunting — that's fine, we just mention it here, have a look, try to make sense of what you can.

Fill in your file — open it in your editor and fill in the fields. And deliberately introduce a spelling mistake somewhere — a typo in a word, a misspelled name, anything obvious. Don't fix it yet. We'll use it in Exercise 4.

Run git status again — see the difference? Git now sees your file as modified.

Stage your file (tell git you want to include it in the next commit):

git add participants/firstname-lastname.md

Run git status again — see the difference? Your file moved from "not staged" to "staged for commit".

Commit:

git commit -m "Add participant file for Firstname Lastname"

Always write a commit message. Git technically lets you skip it with --allow-empty-message, but don't. A good commit message is context that lives alongside the code forever — six months from now, on someone else's machine, in a PR review, in git log. It's close to mandatory in any real project, and a habit worth building from day one.

Run git status again — your working tree is clean, the commit exists locally.

Have a look at your commit history:

git log --oneline

You'll see your commit(s) listed with their short ID and message — this is your branch's history so far.


Oops — undoing commits

Made a mistake? Forgot to include a change? Here's how to fix it, depending on where you are.

The commit hasn't been pushed yet — last commit only:

Forgot to include a change, or want to fix the commit message:

git add participants/firstname-lastname.md   # stage the missing change (if any)
git commit --amend                          # fold it into the last commit, also lets you edit the message

Want to uncommit entirely and redo it:

git reset HEAD~1          # --mixed by default: uncommit, unstage changes, keep the files
git reset --soft HEAD~1   # uncommit, keep changes staged — ready to re-commit immediately
git reset --hard HEAD~1   # uncommit AND discard the changes entirely — dangerous, can't undo

These work cleanly only when the commit hasn't been pushed. Once it's on the remote, you're rewriting history — see below.

Advanced — commit already pushed or on other branches:

If the commit is already shared, rewriting it would break everyone else's history. Two options:

  • git revert <commit-id> — safe. Creates a new commit that undoes the changes. Doesn't rewrite history. The right tool for shared branches.

  • git rebase -i HEAD~N — interactive rebase: reorder, squash, edit, or drop the last N commits. Rewrites history — only use on commits that haven't been pushed to shared branches yet.

If you've already pushed and need to rebase anyway:

bash git push --force-with-lease origin branch-name

--force-with-lease is safer than --force: it refuses if someone else pushed in the meantime. Still destructive — coordinate with your team first.


Oops — switching branches mid-work

You're in the middle of something — you have ongoing changes that aren't ready to commit yet — and you need to jump somewhere else for a moment. Switch to another branch to check something, or go back to a previous commit to run a test or look at how a function was written.

Git will try to be nice about it. But the cleanest way to handle this is to stash your work first:

git stash                        # park your ongoing changes — working tree is clean
git checkout dev-yourname        # switch to another branch
# or: git checkout <commit-id>   # go back in time to a specific commit
# ... check, run, look around ...
git checkout dev-yourname-participant   # come back
git stash pop                    # restore everything exactly as you left it

git stash is a temporary shelf. It tells git: "these changes aren't ready, but I don't want to lose them — hold on to them for me." git stash pop puts them back. If you stash multiple times without popping, git stash list shows the stack.


Exercise 3 — Merge your way up to main

Merge your feature branch onto your dev branch:

git checkout dev-yourname
git merge dev-yourname-participant

Merge your dev branch onto your local main:

git checkout main
git merge dev-yourname

Did anyone get kicked out?

(keep the suspense a moment...)

If you did — it's because main is protected by a local spell check that runs automatically when you try to merge onto it. It caught the spelling mistake in your participant file.

Don't panic — main is untouched. The hook aborted the merge before the commit was created. You're sitting on main, clean, as if nothing happened.

Go work on your English :) — go back to your feature branch and fix the mistake. But this time, instead of creating a new commit, amend it onto the previous one — the fix belongs to the same logical change:

git checkout dev-yourname-participant
# fix the typo in your file
git add participants/firstname-lastname.md
git commit --amend   # fold the fix into the last commit

Now merge your feature branch back onto your dev branch:

git checkout dev-yourname
git merge dev-yourname-participant

You'll hit a conflict. This is expected — and this is what we wanted to show you.

Here's why it happened: dev-yourname already had your participant file from the first merge. When you amended the commit on dev-yourname-participant, that commit got a new identity — so now the two branches have diverged with different versions of the same file. Git can't decide which one to keep, so it asks you.

Open your participant file — you'll see conflict markers:

<<<<<<< HEAD
... the line with the typo (from dev-yourname) ...
=======
... the line with the fix (from dev-yourname-participant) ...
>>>>>>> dev-yourname-participant

Edit the file: keep the correct version, delete the markers and the wrong line. Then:

git add participants/firstname-lastname.md
git merge --continue   # finalize the merge

If you want to bail out instead of resolving — no problem, nothing is broken: bash git merge --abort # cancels the merge entirely, puts you back where you were

Now merge up to main — the spell check will pass this time:

git checkout main
git merge dev-yourname

Advanced: remember that git config core.hooksPath hooks you ran right after cloning? That's what made this work. By default, git looks for hooks in .git/hooks/ — a folder that is never cloned, never shared. By pointing it to hooks/ instead, git uses the hooks that live in the repo itself, version-controlled alongside the code. Have a look at hooks/pre-merge-commit and scripts/check_spelling.py to see exactly how the spell check is wired up. This is the pattern for any automated check you'd want to enforce at merge time.

The spell check here is a stand-in for something you'll encounter on any real project: a test suite. When you work on code — fix a bug, add a feature — automated tests run when you merge onto main. They check that your changes do what they're supposed to, and more importantly, that they haven't broken anything that was already working. The hook is the mechanism; the tests are the content. Here, the spell check plays that role.


Exercise 4 — Push and open a Merge Request

Push your branches to your GitLab fork:

git push origin dev-yourname
git push origin main

Where did origin come from? You didn't create it — git clone did. When you cloned the repo, git automatically set up a remote called origin pointing to the URL you cloned from, i.e. your GitLab fork. It's just a name, a shortcut to a URL.

By convention: origin is the remote pointing to your own copy of the project (your fork), and upstream is the remote pointing to the original repo — here, my GitLab. You can add it with git remote add upstream git@gitlab.com:thibaultcimic/git_tutorial.git if you ever need to pull updates from my repo.

For those of you starting from scratch — your own code, no fork, no clone: you'll create a new project on GitLab from scratch, and you'll need to set up the remote yourself, push your first commit, create branches, etc. GitLab actually explains all of this very clearly when you create a new empty project — it gives you the exact commands to run. Follow that, it's well done.

Run git status one last time — your local branch is now in sync with the remote one.

Open a Merge Request (on GitHub this is called a Pull Request — PR):

  1. Go to your fork on GitLab — you'll see a banner suggesting to open an MR for your freshly pushed branch

  2. Click Create merge request

  3. Set the target to my repo / main branch

  4. Give it a title and a short description — something like "Created my participant file from the template with my info"

  5. Submit

Did anyone get kicked out again?

(keep the suspense...)

If you did — it's because CI tests are running automatically on GitLab's servers when an MR targets main. They check the structure of your participant file: are all the required sections there? Are all the fields filled in properly?

This is the same concept as the local spell check, but running remotely — and protecting my repo instead of your local machine.

These are what's called CI tests (Continuous Integration) — automated checks that run on every MR to make sure nothing broken makes it to the main branch.

Fix your file, commit, push — the CI will re-run automatically.

Notice the difference in perspective from Exercise 3. There, the hook was protecting your main — your machine, your repo, your rules. Here it's the other way around: this CI runs on my repo, against your proposed changes.

That's exactly the point of the MR flow. Imagine a real project: you've spent a week on a feature. You rewrote a function to be cleaner — but you didn't know that somewhere else in the codebase, another module was relying on it behaving in a very specific way. Your local tests pass. You open an MR. But the moment I run the full test suite against your branch on my side — something breaks. You didn't break it on purpose; you just didn't know. That's what CI catches: it runs the project's tests against your changes, on a neutral machine, before anything gets merged. My main stays clean.

Advanced: the CI pipeline is defined in .gitlab-ci.yml at the root of the repo — have a look at it. Each job maps to a stage: check-participant-file runs on MRs targeting main, pages runs on pushes to main to rebuild the workshop website. GitLab spins up a Docker container (the image: line), runs the script: commands inside it, and reports pass or fail back on the MR. Any repo on GitLab can have a CI pipeline — it's just a YAML file next to your code.


Exercise 5 — Open an issue

You've opened a Merge Request — that's you proposing a change to the repo. But there's another way to interact with a repo you don't own: issues.

An issue is how you communicate with a repo owner publicly and asynchronously. You don't need write access to the code — anyone can open an issue. It's how you: - Report a bug - Ask a question - Suggest a feature - Introduce yourself

This is how open source works. Most of your interactions with projects you don't contribute to happen through issues.

MR = you propose code changes. Issue = you raise a point.

Open an issue on the original repo (not your fork — the real one):

  1. Go to gitlab.com/thibaultcimic/git_tutorial
  2. Click Issues → New issue
  3. Write a short intro or a question about the session
  4. Submit

Exercise 6 — Bonus: Binary files and .gitignore


Demo — binary files in git (instructor, on projector)

Prep: requires pandocsudo apt install pandoc. Use --pdf-engine=xelatex to handle emoji in the file (warnings are fine, PDF still generates).

First, let's look at what a clean diff looks like — on the markdown participant file you just committed:

git diff HEAD~1 participants/thibault-cimic.md

Line by line, human-readable, you can review exactly what changed.

Now let's generate a PDF from that same file:

pandoc participants/thibault-cimic.md -o participants/thibault-cimic.pdf --pdf-engine=xelatex

No pandoc? No problem — just copy any PDF you have on your laptop into the repo folder. Same effect.

git add participants/thibault-cimic.pdf
git diff --cached participants/thibault-cimic.pdf

In the terminal: "Binary files a/... and b/... differ" — that's all git can tell you. It has no idea what changed.

Now, a smart editor like VSCode might render both versions visually side by side and make it look like a decent diff. But that's VSCode doing the work, not git. On GitLab, in an MR, your reviewer sees exactly what the terminal shows — nothing useful.

And the real problem isn't one PDF. It's that you start with one, then you have more, and more — plots, reports, slides — and all of a sudden every git commit, git merge, git push takes forever. The repo is carrying dead weight it can never compress or make sense of.

Don't track PDFs. Track the .md file that generates them. The source is what matters.

git reset participants/thibault-cimic.pdf   # unstage — we don't actually want to commit this

Your turn — write a .gitignore

The repo already has a .gitignore at the root. Open it:

# MkDocs build output
public/
site/

You can see the syntax: one pattern per line, # for comments. Add *.pdf to it:

# MkDocs build output
public/
site/

# Generated files — don't track outputs, track the source
*.pdf

Now:

git add .gitignore
git commit -m "Ignore PDF files"
git status

The PDF sitting in your folder? Git doesn't see it anymore.

.git/info/exclude does the same thing but stays purely local — never committed, never shared. Useful for personal stuff you don't want to impose on the team.

Merge your new commit up:

git checkout dev-yourname
git merge dev-yourname-participant
git checkout main
git merge dev-yourname

This time there's no conflict — .gitignore is a new file, both branches agree on it.


What's next?

Once I accept your MR, your file is on my main. At that point your local main and your fork are behind mine — to get back in sync you'd need to pull my main down. That's beyond the scope of today, but know it exists: git pull upstream main. We'll cover that another time.


Homework — Build your own professional website

⚠️ This section has not been tested end-to-end. If you make it here and try it out, please send feedback — what worked, what didn't, what's unclear. Open an issue or an MR on the repo. That's exactly what those tools are for.

Everything we did today started from an existing repo. This is what starting from nothing looks like — YOUR project, initiated by you, from a blank GitLab project. Same workflow, different starting point.

The goal: build a professional/academic website in markdown, generate it with Hugo, and host it on GitLab Pages via CI/CD. The source is markdown — what you worked with today. Hugo reads it and generates HTML. The CI deploys it automatically every time you push. You never commit the generated output, only the source. Sound familiar.

Prerequisites — install locally:

Check they're there:

hugo version
go version

Step 1 — Create a new blank GitLab project

GitLab → New project → Create blank project. Name it something like my-website — it'll appear in your URL. Set it to public.

Clone it locally:

git clone git@gitlab.com:yourusername/my-website.git
cd my-website

Step 2 — Initialize Hugo

hugo new site . --force

This creates the Hugo project structure inside your (currently empty) git repo. --force because the folder already exists.


Step 3 — Add the Academic theme as a git submodule

git submodule add https://github.com/HugoBlox/theme-academic-cv.git themes/academic-cv

A git submodule is a way to include another git repo as a dependency inside yours — the theme lives in themes/ and is tracked as a reference, not copied in. This is the git-native way to manage external dependencies.

Copy the example site structure to get started:

cp -r themes/academic-cv/exampleSite/config/ .
cp -r themes/academic-cv/exampleSite/content/ .

Step 4 — Add public/ to .gitignore

echo "public/" >> .gitignore

Hugo builds the site into public/. That's generated output — it doesn't belong in git. Track the source, not the output.


Step 5 — Preview locally

hugo server

Open http://localhost:1313 in your browser. The site rebuilds live as you edit files.

Start customising: - content/authors/admin/_index.md — your name, role, bio, photo - content/ — pages for publications, talks, projects, etc.


Step 6 — Add the GitLab CI job

Create .gitlab-ci.yml at the root:

image: registry.gitlab.com/pages/hugo:latest

pages:
  script:
    - git submodule update --init --recursive
    - hugo
  artifacts:
    paths:
      - public
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

GitLab's official Hugo image has everything pre-installed. The git submodule update line fetches the theme on the CI runner.


Step 7 — Commit and push

git add .
git commit -m "Init Hugo academic website"
git push origin main

Go to Build → Pipelines and watch it build. Once the pages job passes, go to Settings → General → Visibility → Pages → Everyone. Your site is live at https://yourusername.gitlab.io/my-website.