DEV Community

Cover image for A detailed description of the pull request process
Paul Cochrane 🇪🇺
Paul Cochrane 🇪🇺

Posted on • Originally published at peateasea.de on

A detailed description of the pull request process

A while ago, when sharing some code with friends, I asked for feedback, when possible, as a pull request to the repository I’d shared. One friend replied along the lines of “can do, if you show me how to make a pull request first”. This is my attempt to address that remark.

What is a pull request?

Before we can get to the details of an explanation, what is a pull request, and why would anyone want to make one?

A pull request (also known as a merge request) is a way to contribute to a software-related project.1 This could be by providing bug fixes or new features to code, by updating or extending documentation, or by providing some other kind of improvement to the project. The pull request workflow is one way (of many) to contribute to a project. Even so, it has become a very common way to work on both open- and closed-source projects. For instance, Linux kernel development uses a generalised version of this model. It has gained enormously in popularity through GitHub, which automated the workflow and integrated it into the service’s code contribution process. The pull request model process for contributing to a project is now so common that many (most?) other Git service providers (such as GitLab, BitBucket, Gitea, Codeberg, etc.) provide almost identical workflows to those that one can use on GitHub.

Before Git became popular as a version control system, it was common to send code changes to Open-Source Software projects as patches. These are files describing the changes to make to the source code so that it would, after the patches have been applied, contain the proposed new behaviour. With the advent of Git and distributed version control systems, it became possible to share the online location of a source code contribution. This was much simpler than emailing around files of those contributions. The online location is, for example, a publicly-accessible Git branch which the maintainer can pull to their computer. Thus, the act of telling the maintainer that a patch or set of patches was available in a Git repo became known as a “pull request”. GitHub then standardised and automated much of this process, effectively wrapping it into a simple web form. Consequently, GitHub is now strongly associated with the concept of pull requests.

High level overview

Let’s look at the process at a high level. We’ll see how everything works in more detail with a worked example later.

Let’s say you’ve been working on a software project. You’ve found it useful for the things you need to get done, and you think it’d be handy for others to use as well. You decide to make the code publicly available so that others can download it and run it on their computers. We’ll assume that you’ve kept the code in a Git repository on your local computer. This could be your laptop, a desktop PC, or maybe a Raspberry Pi on your home network. Whatever. The point is that the code is where you are, hence the idea that it is somehow “local” to you.

You have an account on an online Git service (such as GitHub, GitLab, and friends) and use that service to create an empty repository, ready to contain the code you wish to share. You then push your code into this empty upstream repository, making it available for anyone to download and use.

Pushing code to a remote Git service

I’ve used the term “upstream” here to refer to the online copy of your repository. This is because it becomes the central point where everyone (including you) can get the project’s code. Anyone fetching code from this location is thus “downstream” from it, which makes it “upstream” for everyone.

Time passes. Your project has been available for a while now, and people have been happily using it. One of your users happens to be a software developer and has spotted a bug. They find the root cause in the code and fix it. But how can they send this fix to you? They might not know you personally and hence won’t be able to send you fixes directly, say via email. If there’s no previous connection between the author and contributor, how can complete strangers collaborate on an Open-Source Software project? One possibility (of many) is to use an intermediary of some kind, such as an online Git service.

Fortunately, they also use the online Git service that you’re using. They fork your repository, allowing them to make and publish code changes to a copy of your code.

Forking an upstream repository within an online Git service

A fork is a simple copy of your online repository. But instead of being owned by you, it’s owned by them, hence they can push code changes to it. They clone their fork of your upstream repository to their computer. This way, they can make changes to the code.

Cloning a forked online Git service repository

There, they fix the bug in a branch and push that branch back upstream to their fork.2

Pushing pull request changes to an online fork

The fix is now available online for you to access, review, and (if so desired) incorporate into your code.

The thing is, you don’t know about it yet. But the Git service has this cool feature: through a simple web form, the other user submits their contribution to the Git service, which in turn notifies you about the bug fix. This notification is a pull request.

Creating a pull request to original project from an online fork

You can pull this new branch to your local Git repository, study the code and verify that it does in fact fix a bug. If you’re happy with the code changes, you can merge these into your main branch.3 Of course, if you’re not happy with the changes, you can ask (via a web form on the online Git service) the other person to make certain updates or ask for feedback/explanations about the submission. Usually, things get ironed out after a while, and you merge the fix from their branch into your main branch.

You then push the latest version of your main branch up to the online repository of your project. Now, anyone using your program can take advantage of the updated and fixed code. Yay!

The Git service even provides a way for you to review and merge the code into your upstream repository without having to explicitly pull the code to your local computer. Either way, it gets merged into main, and everyone wins. Cool!

That’s it. Through millions of such interactions daily, much of the software we use every day becomes better, bit by bit. Well, that’s the theory, anyway. And for the most part, that’s the case.

A detailed worked example

Let’s take these general concepts and overall process and make them more concrete via a detailed worked example.

Let’s imagine two people. One–whom we’ll call Eck–has written a program and wants to share it with the world. The other–called Alice–uses the program and wants to contribute changes back to the original project. We’ll now look over their respective shoulders as they go through the motions of the pull request process.

Setting the stage

Eck has written a simple fortune cookie application. The program prints a randomly-selected phrase from a list of fortune-cookie-like messages. Here’s the code:

# -*- coding: utf-8 -*-

import random

def fortune():
    messages = [
        "The early bird can kiss my backside",
        "Don't give up on your dreams: stay asleep",
        "All good ends must come to a thing",
        "People who live on trampolines should not throw bowling balls",
        "The second mouse gets the cheese",
        "Do not eat prunes when you are famished",
    ]

    print(random.choice(messages))

if __name__ == " __main__":
    fortune()

# vim: expandtab shiftwidth=4 softtabstop=4
Enter fullscreen mode Exit fullscreen mode

You can verify that this code works by saving it into a file called fortune-cookie.py and running it with the python3 interpreter. For example:

$ python3 fortune-cookie.py
Don't give up on your dreams: stay asleep
Enter fullscreen mode Exit fullscreen mode

Eck has been using this program daily and has found it to be really useful. So useful, in fact, that he thinks other people might also want to use it. Thus, he decides to make the source code available online for others to download and run on their computers. He has an account on GitLab (a common cloud-based Git service with free access for Open-Source Software) and decides to make his program available there. From previous experience, he knows that he can share Git repositories (and hence code) with other people by publishing them on GitLab. But first, he needs to create a local Git repository to hold his program’s source code.

Storing code in a local Git repository

He puts his program into its own directory by creating a folder called fortune-cookie. From the command-line, he did something like this:

$ mkdir fortune-cookie
$ mv $HOME/fortune-cookie.py fortune-cookie/ # everyone dumps their stuff in $HOME, right? Right?
Enter fullscreen mode Exit fullscreen mode

Since Eck knows a bit of Git and has made repositories before, he enters the fortune-cookie directory and initialises a Git repository there:

$ cd fortune-cookie
$ git init .
Initialized empty Git repository in /home/eckzampill/fortune-cookie/.git/
Enter fullscreen mode Exit fullscreen mode

With the local repository created, he adds the fortune-cookie.py source code file to it and commits that change:

$ git add fortune-cookie.py
$ git commit
Enter fullscreen mode Exit fullscreen mode

At this point, Git opens an editor for Eck to write a commit message. Knowing that documentation is a love letter written to his future self, Eck takes a bit of time to compose an informative commit message. He ends up writing this:

Initial import of fortune-cookie code

... which is a program to randomly display a proverb or message similar
to that which one gets in a fortune cookie.
Enter fullscreen mode Exit fullscreen mode

This text is better than the “Initial commit” string often found in first commit messages.

Saving this text and quitting the editor commits the newly added file to the repository:

[main (root-commit) 9771905] Initial import of fortune-cookie code
 1 file changed, 23 insertions(+)
 create mode 100644 fortune-cookie.py
Enter fullscreen mode Exit fullscreen mode

Eck can now push his repository to GitLab. Before he can do that, he has to create a GitLab project.

Creating an empty online Git repository

With the code safely stored in his local Git repository, it’s time for Eck to create an empty repository on GitLab.4 There, he can publish a copy of his repository so that others can access it.

He logs in to GitLab and is presented with his home dashboard.

Eck Zampill's home dashboard view on GitLab

Within the GitLab parlance, if one wants to create a new online repository, one first has to create a project. To do that, Eck clicks on the “Projects” link in the navigation sidebar on the left-hand side of the page.

View of Eck Zampill's projects page on GitLab before adding the fortune cookie project

Now he clicks on the “New Project” button at the top right-hand side of the page. This shows him a selection of ways to create a new project.

He then selects the option “Create blank project”. Doing so, he doesn’t fall into the trap of using the “Import project” option, although it might sound like what one might want to do. One would think the task is to import the repository from your local computer into GitLab. That’s not what the “Import project” option does, though: it imports projects from other online Git service providers. That’s why Eck uses the “Create blank project” in this case. Clicking on the button for this option, he’s presented with a form asking for details about the project to create:

Empty

Now he fills out the form details.5

In the “Project name” field, Eck adds the text “Fortune cookie”. Doing so creates the project slug with the correct name of fortune-cookie. This is handy, because Eck wants it called fortune-cookie to match the directory name on his computer.

For the “Project URL” field, he selects the name eckzampill under the “Users” section of the drop-down menu.

The next option is to set a “Project deployment target”. Right now, Eck is only interested in getting his code online. Hence, he isn’t worried about setting a deployment target and leaves this option unset.

However, he notices that the “Visibility Level” is set to “Private” by default and thus changes its value to “Public”; he wants to share his program with the rest of the world after all!

The last thing he needs to do is to uncheck the “Initialize repository with a README” option under the “Project Configuration” heading. As the text for this option mentions:

Skip this if you plan to push up an existing repository.

Since Eck already has an existing repository, he doesn’t want to initialise one on GitLab.

The form now looks like this:

Filled in

Once that’s all done, he scrolls down to the bottom of the page and clicks on the “Create project” button.

GitLab presents him with the initial project page for his fortune cookie program. This page helpfully includes instructions in the “Command line instructions” section on how to push new code to this project.

Fortune cookie project initial project page on GitLab

For those following along at home: Note that you might be asked to set up SSH keys if you haven’t done so already. I won’t go into setting up SSH keys in this worked example because that would be too much of a tangent. So, authentication with the upstream Git service (in the rest of this story) will take place over HTTPS instead. Just so you know. 🙂

The project’s initial page mentions that:

The repository for this project is empty

To get started, clone the repository or upload some files.

GitLab has provided Eck with instructions on how to do this. He scrolls down the page a bit to the “Add files” section.

He notes that the default option is to use SSH for communication between his computer and the upstream Git service. Since he hasn’t set up SSH with GitLab yet, he instead clicks on the “HTTPS” tab to show the instructions for that authentication mechanism.

Pushing the local repository upstream

Since Eck already has a repository to push, he needs to use the “Push an existing Git repository” instructions. He scrolls further down the page to see them in full.

Reviewing the instructions, Eck notices that they are very generic and include steps that he doesn’t need to take in his case. So, he follows an appropriate subset of steps to publish his code online. Let’s see those steps now.

After having made his first commit earlier, Eck’s shell session is still in the fortune-cookie directory on his computer. Thus, the step that GitLab recommends to change into the appropriate repository folder isn’t necessary. Also, because this is a completely new repository, Eck doesn’t have a pre-existing remote origin,6 and can ignore the step to rename the remote origin.

However, Eck does have to specify a new remote origin for his local repository. This is how Git knows where to push changes to. Thus, he calls his upstream (“remote”) repository origin and specifies its location as a URL as part of the git remote command:

$ git remote add origin https://gitlab.com/eckzampill/fortune-cookie.git
Enter fullscreen mode Exit fullscreen mode

One can understand this command like so: add the remote repository called origin located at the URL https://gitlab.com/eckzampill/fortune-cookie.git.

Now he’s ready to push his local repository to GitLab, which he does via git push:

$ git push --set-upstream origin --all
Username for 'https://gitlab.com': eckzampill
Password for 'https://eckzampill@gitlab.com':
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Delta compression using up to 4 threads
Compressing objects: 100% (2/2), done.
Writing objects: 100% (3/3), 634 bytes | 634.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To https://gitlab.com/eckzampill/fortune-cookie.git
 * [new branch] main -> main
 Branch 'main' set up to track remote branch 'main' from 'origin'.
Enter fullscreen mode Exit fullscreen mode

Note that he had to specify his username and password because he’s using HTTPS rather than SSH.

Since this is a completely new repository, it doesn’t have any tags. Thus, Eck realises that the step to push the tags in GitLab’s documentation isn’t necessary in this case.

Since that was the last step, he reloads the GitLab project page in his browser and can now see the content he pushed.

Fortune cookie project page after pushing initial set of commits

Great! Now the world has access to Eck’s fortune cookie program!

Forking before contributing

Time passes, and Alice, a person using the fortune-cookie program, thinks that the it would be even more useful if there were installation and usage instructions in a README file. This way, new users can get up to speed quickly, and they can get their fortune cookies more easily.

Alice is a software developer and knows that to submit changes or new code to another project on GitLab, she has to fork it first. To do that, she visits the GitLab page of the fortune cookie project and clicks on the “Fork” button in the top right-hand corner of the page.7

Alice's view of Eck's fortune cookie project page before forking the project

(Notice that this image looks almost the same as the one Eck saw after the initial push. The main difference is Alice’s user avatar in the top right hand corner of the page.)

After clicking on the “Fork” button, Alice sees a form where she can specify the fork’s details in her GitLab environment. Note that this behaviour is different to GitHub. There, forks are added to the user’s main namespace, and one doesn’t have the option of a group to fork code into. GitLab’s process has more flexibility, but that flexibility means Alice needs to make a choice here. She decides to fork the project into her user’s main namespace (alliceellis).

Alice's view of forking Eck's fortune cookie project

Alice peruses the default project metadata settings, and everything seems in order. The project name looks good, the project slug is also correct, she wants to copy all branches from the original project, and to make her fork public as well. Looking good! She clicks on the “Fork project” button at the bottom of the page to complete the fork process.

Alice's project overview page of her fork of the fortune cookie project

Because of the text

Forked from Eck Zampill / Fortune cookie

on her fortune cookie project page, Alice can see that it is a fork of another repository. Also, GitLab helpfully tells her what state her fork is in relative to the original repository:

Up to date with the upstream repository.

Because she’s just forked the project, it comes as no surprise that her fork is up to date with the original repository. Eck’s repository is–relative to hers–“upstream”, hence the phrasing used in the text here.

Cloning the fork

For Alice to work on her fork of the code, she needs to clone it to her laptop. To find out the URL to use when cloning, she clicks on the “Clone” button on her fork’s project page. This opens a pop-up window with various options.

Git clone options pop-up on Alice's fortune cookie project fork

Again, we don’t want to set up SSH for this worked example, hence we’ll use the (short-term) easier HTTPS option.

Alice copies the URL shown under the heading “Clone with HTTPS”. She uses this in her git clone command to clone her fork of the fortune cookie project repository to her laptop:8

$ git clone https://gitlab.com/aliceellis/fortune-cookie.git
Cloning into 'fortune-cookie'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Receiving objects: 100% (3/3), done.
Enter fullscreen mode Exit fullscreen mode

Alice didn’t need to authenticate because her fork is publicly available. She’ll need to authenticate when pushing changes back up to her fork, though. We’ll see this in action later.

Adding a README to the project

Alice enters her freshly cloned repository:

$ cd fortune-cookie
Enter fullscreen mode Exit fullscreen mode

and creates a Markdown-formatted README file explaining what the project is, how to set it up, and how to run the program:

# Fortune cookie

A program to print a randomly-chosen fortune cookie message from a list of
available messages.

Requires [Python version 3](https://www.python.org/) to be installed to run.

## Installation

Download the file with `wget` into your `~/bin/` directory:

```shell
$ wget https://gitlab.com/eckzampill/fortune-cookie/-/blob/main/fortune-cookie.py?ref_type=heads -O $HOME/bin/fortune-cookie.py
```

## Usage

Call the program with `python3`:

```shell
$ python3 ~/bin/fortune-cookie.py
```

She saves this text into a file called README.md.

Before adding and committing this file, she runs git status to see the state of her repository:

$ git status
On branch main
Your branch is up to date with 'origin/main'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        README.md

nothing added to commit but untracked files present (use "git add" to track)
Enter fullscreen mode Exit fullscreen mode

Oops! She’s still on the main branch!

Alice notices the text On branch main after the git status command. She knows that to create a pull request to another project, her changes need to be committed to their own branch. Thus, she creates a new branch and changes to it in one step with git switch:

$ git switch -c add-project-readme
Switched to a new branch 'add-project-readme'
Enter fullscreen mode Exit fullscreen mode

The -c option creates the branch before switching to it. Equivalently, Alice could have used git checkout -b add-project-readme to achieve the same effect. This older way of doing things is still a common sight in online documentation.

Alice now adds the new file to her local repository and commits the change:

$ git add README.md
$ git commit
[add-project-readme 7b8b638] Add an initial project README
 1 file changed, 22 insertions(+)
 create mode 100644 README.md
Enter fullscreen mode Exit fullscreen mode

She took the time to write a clear commit message, like this:

Add an initial project README

A README file helps introduce the project to new users and describes
what the main program does.

This change adds a README file to the project describing briefly what
the program does and showing potential users how to install and run the
`fortune-cookie` program.
Enter fullscreen mode Exit fullscreen mode

Alice checks her working directory status to make sure she’s not missed anything:

$ git status
On branch add-project-readme
nothing to commit, working tree clean
Enter fullscreen mode Exit fullscreen mode

Looking good!

Pushing proposed changes to the upstream fork

Now she’s ready to push her changes up to her upstream fork of the project on GitLab. To do this, she uses git push:

$ git push
Username for 'https://gitlab.com': aliceellis
Password for 'https://aliceellis@gitlab.com':
Enumerating objects: 4, done.
Counting objects: 100% (4/4), done.
Delta compression using up to 4 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 740 bytes | 370.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
remote:
remote: To create a merge request for add-project-readme, visit:
remote: https://gitlab.com/aliceellis/fortune-cookie/-/merge_requests/new?merge_request%5Bsource_branch%5D=add-project-readme
remote:
To https://gitlab.com/aliceellis/fortune-cookie.git
 * [new branch] add-project-readme -> add-project-readme
Enter fullscreen mode Exit fullscreen mode

The message shown as part of the git push operation (on the lines prefixed with remote:) mentions the URL Alice could use to create a merge request. Note that GitLab uses the phrase “merge request” for the same concept as what GitHub calls a “pull request”.

Visiting the provided merge request link is one way for Alice to open a merge request. Another way is for Alice to view her fork’s project page on GitLab. Let’s take that particular path now.

Creating a merge request

Reloading the project page of her GitLab fork of the fortune cookie project, she sees an invitation to create a merge request.

Alice's fortune cookie project page after pushing a branch to it, with create merge request button

Alice has seen this before when creating other merge requests, and this time she decides to click on the “Create merge request” button. Alternatively, she could copy-paste the URL from the git push message into her browser to achieve the same goal.

She’s presented with a form to specify the metadata and options of her merge request.

One can see in the form that the subject line of her commit message is used as the merge request’s title. Its description is the body text from her commit message. This has all been automatically filled out by GitLab to streamline the merge request workflow.

Alice sees at the top of the page that her add-project-readme branch is to be merged into the main branch of the original project. This confirms to her that the source and destination locations of the proposed changes are correct.

Note that for single-commit changes, the commit subject line and text are used for the merge request title and description (respectively) in GitLab and GitHub. For multi-commit changes, GitLab pre-fills the form using the commit message from the first commit in the change set. However, this is often insufficient to describe work spanning multiple commits. So, for multi-commit changes, one should delete the automatically-created text in the web form and take the time to write a clear overview of the changes being submitted. Since Alice has submitted a single-commit change, she can use the title and description as they are, extending them if she sees fit.

When Alice scrolls down on the “New merge request” form, she can see the commit included as part of this merge request.

Commit list within

Since Alice is aware that Eck doesn’t know her personally, she extends the description text to mention that if Eck wants anything changed, she’ll be happy to make any required updates. From previous experience, Alice has found that being nice and offering assistance to project maintainers is not only plain good manners but also good netiquette when contributing to Open-Source Software projects. After all, she wants to change the look and behaviour of someone else’s project, so it makes sense to be civil and offer help where one can.

Alice is now satisfied that her merge request is configured correctly and that the changes are sufficiently well described. She now clicks on the “Create merge request” button to submit her merge request.

After submitting her merge request, she’s presented with an overview of it as it appears in Eck Zampill’s project environment:

Merge request overview page after submitting a merge request

This is also what Eck will see when he views the merge request online.

Let’s now switch back to Eck’s perspective.

Reviewing proposed changes in a merge request

As it turns out, Eck only had the project notification settings in GitLab set to “Participate”. Hence, he hadn’t received any email notifying him that Alice had submitted a merge request. Had he known about this, he could have changed this default notification setting to “Watch”. This way, he would have been informed of most activity on his projects, but that hadn’t happened in this case.

What should Eck use for his notification settings in general? As with many things, that depends. For instance, when one only has a few projects, it’s probably a good idea to set the notification setting as high as possible to not miss anything. However, involvement in many projects often means a flood of messages. Hence, in that case, it’d be necessary to throttle things back if the notification load becomes too high.9

Although he didn’t hear about Alice’s merge request immediately, after a while, Eck happens to notice that there’s a number next to the “Merge requests” item in the sidebar on his fortune cookie project. It’s not obvious, so I’ve highlighted the area with a red box.

Eck's fortune cookie project overview page with one queued merge request

This is fortunate for Alice, because otherwise she could have been waiting a while before getting a reaction.10

Eck wants to see the merge request and its details, so he clicks on the “Merge requests” item in the project sidebar. He’s taken to an overview of all open merge requests for the project.

Merge requests overview page

Clicking on the single merge request item in the list, he’s taken to the details page for the one that Alice submitted.

Eck's view of merge request details page

Clicking on the “Commits” tab, he can see the commits that make up the merge request.

Eck's view of commits in merge request

Clicking on the link for the commit, he can see the commit message as well as the diff, displaying what was changed as part of the commit.

Details of a single commit in a merge request

Because Alice’s submission adds a single new file, there have only been lines added in this commit, hence all the text is shown in green. Were this a more involved change, it would likely also have had line deletions as well as additions. In such a case, deletions would be shown in red.

Explicitly pulling proposed changes by direct fetch (optional)

Now, Eck could merge this merge request from the GitLab interface by clicking on the “Overview” tab and then clicking on the “Merge” button. This is the common, streamlined workflow that both GitLab and GitHub support. However, since this process derives from the more general notion of a pull request using command-line tools, let’s see how a “pull” is involved. Ironically, we’ll see that git pull isn’t part of this process. Yet the idea of “pulling in changes from upstream” remains, even if the Git command verbiage doesn’t involve pull.

To do this on GitLab requires jumping through some hoops. By contrast, it seems to be easier to do on GitHub. Let’s try the harder path to see what it looks like.

Eck notices the link to Alice’s branch at the top of the merge request overview page:

Alice Ellis requested to merge aliceellis/fortune-cookie:add-project-readme into main

He copies this link (which in this case is https://gitlab.com/aliceellis/fortune-cookie/-/tree/add-project-readme) and uses it to create a git fetch command on his local computer. Yes, you read that correctly. One way to access the contents of an upstream branch is to fetch it to your local repository rather than pulling it, as the “pull request” name would suggest. Git terminology can be inconsistent at times. To be honest, Eck is still “pull”-ing the code to his computer. The subtlety is that he has to use the git fetch command to do it.

Here’s the command he used:

$ git fetch https://gitlab.com/aliceellis/fortune-cookie add-project-readme:add-project-readme
warning: redirecting to https://gitlab.com/aliceellis/fortune-cookie.git/
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (3/3), 718 bytes | 359.00 KiB/s, done.
From https://gitlab.com/aliceellis/fortune-cookie
 * [new branch] add-project-readme -> add-project-readme
Enter fullscreen mode Exit fullscreen mode

where the warning highlights that Eck should have appended .git to the URL to point it to Alice’s upstream repository correctly. Fortunately, GitLab knew what he meant and appended the .git part for him.

Note that Eck specifies Alice’s repository in the first argument to git fetch, not the branch. In other words, he changed the copied link to point to Alice’s repository (https://gitlab.com/aliceellis/fortune-cookie). Then, he used the syntax <their-branch>:<my-branch> to fetch the add-project-readme branch from Alice’s remote repository to the add-project-readme branch in his local repository. Running the fetch command created a new branch which he can switch to. There, he can inspect the changes that Alice is submitting.

For instance,

$ git switch add-project-readme
Switched to branch 'add-project-readme'
$ ls
fortune-cookie.py README.md
Enter fullscreen mode Exit fullscreen mode

where he can see the new README file in the directory listing.

He can also see the commit that Alice made by running git show:

$ git show
commit 7b8b638a524ffc52b7eabdec52ca8540514cf7fb (HEAD -> add-project-readme)
Author: Alice Ellis 
Date: Thu Nov 13 15:46:43 2025 +0100

    Add an initial project README

    A README file helps introduce the project to new users and describes
    what the main program does.

    This change adds a README file to the project describing briefly what
    the program does and showing potential users how to install and run the
    `fortune-cookie` program.

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4114c67
--- /dev/null
+++ b/README.md
@@ -0,0 +1,22 @@
+# Fortune cookie
+
+A program to print a randomly-chosen fortune cookie message from a list of
+available messages.
+
+Requires [Python version 3](https://www.python.org/) to be installed to run.
+
+## Installation
+
+Download the file with `wget` into your `~/bin/` directory:
+
+```shell
+$ wget https://gitlab.com/eckzampill/fortune-cookie/-/blob/main/fortune-cookie.py?ref_type=heads -O $HOME/bin/fortune-cookie.py
+```
+
+## Usage
+
+Call the program with `python3`:
+
+```shell
+$ python3 ~/bin/fortune-cookie.py
+```

We can see why the web interface is so comfortable: it wraps these Git commands and displays their output in a visually pleasing way. What’s great about Git is that if one does want to use only the command-line interface, it’s possible to do so; one isn’t restricted to a single interface for a given task.

Explicitly pulling proposed changes by adding an extra remote repo (optional)

There’s another option for Eck to access Alice’s proposed changes. For this, he needs to add Alice’s repository as a remote repository and then fetch the changes. This involves more steps than the process outlined above; however, it could be useful if Alice is a frequent contributor. This way, he won’t have to look up Alice’s repository URL for each contribution she makes. Let’s run through this variant to see what it looks like.

Eck adds Alice’s upstream repository as a “remote” of his local repository:

$ git remote add alice https://gitlab.com/aliceellis/fortune-cookie
Enter fullscreen mode Exit fullscreen mode

and then fetches it to his computer:

$ git fetch alice
warning: redirecting to https://gitlab.com/aliceellis/fortune-cookie.git/
From https://gitlab.com/aliceellis/fortune-cookie
 * [new branch] add-project-readme -> alice/add-project-readme
 * [new branch] main -> alice/main
Enter fullscreen mode Exit fullscreen mode

As before, he can switch to the branch that Alice created:

$ git switch add-project-readme
Branch 'add-project-readme' set up to track remote branch 'add-project-readme' from 'alice'.
Switched to a new branch 'add-project-readme'
Enter fullscreen mode Exit fullscreen mode

and list the contents of the directory with ls:

$ ls
fortune-cookie.py README.md
Enter fullscreen mode Exit fullscreen mode

Also, he can see what Alice committed by running git show:

$ git show
commit 7b8b638a524ffc52b7eabdec52ca8540514cf7fb (HEAD -> add-project-readme)
Author: Alice Ellis <alice@peateasea.de>
Date: Thu Nov 13 15:46:43 2025 +0100

    Add an initial project README

    A README file helps introduce the project to new users and describes
    what the main program does.

    This change adds a README file to the project describing briefly what
    the program does and showing potential users how to install and run the
    `fortune-cookie` program.

<snip> (same output as above)
Enter fullscreen mode Exit fullscreen mode

Merging merge request changes

Eck was happy with Alice’s contribution from the view he had on GitLab. Thus, he didn’t need to go through the rigmarole of fetching Alice’s changes via the command-line.

To merge Alice’s changes, he went to the GitLab overview of Alice’s merge request:

Merge request overview page before merging

and clicked on the “Merge” button. This changed the merge request’s state to “merged”. The steps that GitLab carried out on Eck’s behalf were documented in the “Activity” section on the merge request’s overview page.

Merge request overview page after merging

Eck scrolls down to the comment box at the bottom of the page and thanks Alice for her contribution.

Activity list in merge request showing thanks from maintainer for the contribution

Because Alice is participating in this discussion (she submitted the merge request), she gets an email notification that the merge request was merged, and another with Eck’s thanks.

Updating the original local Git repository after an upstream merge

There’s one step that Eck needs to perform before he’s finished: he needs to pull the merged changes in his upstream repository down to the local repository on his computer.

Assuming that he’s on the main branch, he only needs to use git pull:

$ git pull
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (1/1), 273 bytes | 273.00 KiB/s, done.
From https://gitlab.com/eckzampill/fortune-cookie
   9771905..15007da main -> origin/main
Updating 9771905..15007da
Fast-forward
 README.md | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)
 create mode 100644 README.md
Enter fullscreen mode Exit fullscreen mode

Now his local repo is up to date with his upstream repo, and he can continue developing his fortune cookie program. Yay! 🎉

Updating the fork after an upstream merge

How do things now look from Alice’s perspective? Let’s change back to Alice’s view of the world and see what’s changed.

After receiving the notification that her merge request has been merged, Alice reloads her GitLab project page.

Alice's fork after merging; now behind upstream

She sees that her fork on GitLab is now two commits behind the original repository (it is considered to be “upstream” from her online fork).

Why is her fork two commits behind? After all, she only submitted one commit as part of her merge request. The reason for the extra commit is that Eck configured his project to add a merge commit when merging changes from another branch.

A merge commit can be useful if one wants to have provenance of where a given commit or set of commits came from. For instance, the merge commit generated by GitLab when Eck accepted Alice’s merge request contains this commit message:

Merge branch 'add-project-readme' into 'main'

Add an initial project README

See merge request eckzampill/fortune-cookie!1
Enter fullscreen mode Exit fullscreen mode

Here, we can see the original branch name, the merge request’s title, and a link to the merge request page on GitLab (eckzampill/fortune-cookie!1). Depending upon one’s taste, this information can be useful to have. Opinions vary somewhat concerning what the “correct” strategy is.

There are a few merge options to choose from on both GitLab and GitHub. It’s a good idea to have a look at what’s available and think about how you want merges to be handled in your situation.

But for now, let’s get back to Alice. She wants to update her repository to the current state of Eck’s original repository. How can she do this? One option is for her to click on the “Update fork” button in GitLab. This will automatically pull in the changes from the original repository into her upstream repository. Then she can update her local repository by pulling the state of her fork from GitLab (e.g. by running git pull).

There is, however, another way. It involves only the command-line and is common in Open-Source Software development: she can make the original repository an extra remote repository of her local repository. This is in addition to her fork on GitLab, which is her “origin” remote repository.

Why might Alice want to do this? Well, she might be planning further contributions to Eck’s fortune cookie project and having his upstream remote as one of her remotes makes the command-line-based workflow smoother. In that situation, there’s no need for her to log in to the GitLab interface to pull changes from Eck’s repository. Updating via GitLab would add an extra step to her workflow: pulling changes from Eck’s repository to her upstream repository means she would then have to pull those changes from her upstream repo to her local one. Instead, she can simply pull the changes directly from Eck’s upstream repository. Much easier!

Let’s look at how Alice uses this workflow now.

To get the latest changes from Eck’s online public repository, Alice adds it as an extra remote to her local repository. To do this, she uses the following git remote command:

$ git remote add upstream https://gitlab.com/eckzampill/fortune-cookie.git
Enter fullscreen mode Exit fullscreen mode

As when Eck set up his upstream repository as his remote origin earlier, we read this command like so: add a remote repository and call it upstream, specifying its online location with the URL https://gitlab.com/eckzampill/fortune-cookie.git.

Alice now has two remotes configured in her local repository. These she can check with the git remote command:

$ git remote
origin
upstream
Enter fullscreen mode Exit fullscreen mode

The entry called “origin” is her GitLab fork of Eck’s repository. When she pushes to (and pulls from) GitLab, this is the default repository that Git will use. The “upstream” repo points directly to Eck’s GitLab repository. If she wants to pull from Eck’s upstream repository, she will have to refer to it explicitly, e.g. as part of a git fetch command.

She can see more details about the remotes she has configured by using the --verbose option to git remote:

$ git remote --verbose
origin https://gitlab.com/aliceellis/fortune-cookie.git (fetch)
origin https://gitlab.com/aliceellis/fortune-cookie.git (push)
upstream https://gitlab.com/eckzampill/fortune-cookie.git (fetch)
upstream https://gitlab.com/eckzampill/fortune-cookie.git (push)
Enter fullscreen mode Exit fullscreen mode

Now it’s much clearer that “origin” is her repo on GitLab and “upstream” is Eck’s repo on GitLab.

To get the latest changes from Eck’s GitLab repo, she fetches them to her local repository:

$ git fetch upstream main
remote: Enumerating objects: 1, done.
remote: Counting objects: 100% (1/1), done.
remote: Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
Unpacking objects: 100% (1/1), 273 bytes | 273.00 KiB/s, done.
From https://gitlab.com/eckzampill/fortune-cookie
 * branch main -> FETCH_HEAD
 * [new branch] main -> upstream/main
Enter fullscreen mode Exit fullscreen mode

where she tells git fetch to get the changes from the main branch on the repository called upstream. Had she not specified the repository name explicitly, git fetch would have used her “origin” repository by default. In this case, Git wouldn’t have fetched anything, as her local repo was up to date with that on GitLab.

The fetch operation added a new branch to her local repository: upstream/main (see the output from git fetch above). This is a copy of Eck’s main branch on GitLab, but with a different name in Alice’s repository. Git (and Alice) can thus tell it apart from her own main branch. Because Alice called Eck’s GitLab repository “upstream”, her local copies of his branches are prefixed with the string upstream/.

Alice can see a list of her local and remote branches by using the -a flag on the git branch command:

$ git branch -a
  add-project-readme
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/add-project-readme
  remotes/origin/main
  remotes/upstream/main
Enter fullscreen mode Exit fullscreen mode

The branch with the star (*) next to it is her local main branch; the star signifies that she’s currently on that branch. Alice can also see the main branch in her “origin” remote repository as well as the main branch of the “upstream” remote repository (i.e. Eck’s repository on GitLab). There are thus three things called main here which, although related, aren’t exactly equal. This is because they can all be in different states at any given time.

This is something that can make Git seem so complex: names given to branches and repositories are often context-dependent. Alice’s main is not strictly the same as Eck’s main, although they may be very strongly related. Alice’s remote origin is “upstream” from her local repository, but Eck’s remote origin is “upstream” from Alice’s “upstream”. Some of these naming conventions have evolved organically, and concepts have been reused in different situations.11 It’s not surprising people get confused.

Back to our example. Alice now wants to update her own main branch to the state of Eck’s main branch. To do this, she merges the changes from upstream/main into her main branch with git merge:

$ git switch main # make sure we're on the local main branch
$ git merge upstream/main
Updating 9771905..15007da
Fast-forward
 README.md | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)
 create mode 100644 README.md
Enter fullscreen mode Exit fullscreen mode

Running git log, she sees the same commits on her local main branch as those that exist on Eck’s GitLab repository.

Theoretically, she’s up to date and could start work on another merge request if she wanted to. There’s no real need for her to update her fork on GitLab if she doesn’t want to; all she’d change is the main branch. However, Alice likes things to be nice and tidy, so she decides to push the current state of her main branch up to her fork:

$ git push
Username for 'https://gitlab.com': alice@peateasea.de
Password for 'https://alice@peateasea.de@gitlab.com':
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 295 bytes | 295.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0
To https://gitlab.com/aliceellis/fortune-cookie.git
   9771905..15007da main -> main
Enter fullscreen mode Exit fullscreen mode

Now everything should be up to date. She returns to her web browser, and the landing page of her fork of the fortune cookie project, and reloads the page. She sees that her fork is back on par with the original project:

Project overview page of Alice's fork after updating from Eck's repository

Great! All back up to date!

Also, she can see that since the project now has a README, GitLab shows its contents (the introductory text she added) on the project’s overview page. This information should help make the project more accessible for new users. Since that was her intention with the merge request, she achieved he goal. Awesome!

Trimming unneeded branches

There’s still one thing to tidy up. Now that the add-project-readme branch has been merged into the original project and Alice’s local, origin and upstream main branches are all in sync, she no longer needs the add-project-readme branch hanging around. Remember what git branch -a showed us earlier?

$ git branch -a
  add-project-readme
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/add-project-readme
  remotes/origin/main
  remotes/upstream/main
Enter fullscreen mode Exit fullscreen mode

That list is unwieldy enough, however, imagine what things would look like with tens or even hundreds of dangling, already-merged branches. It would become very hard to see the wood for the trees! That’s why Alice now deletes her local add-project-readme branch. Since the commit on that branch is already in main, she doesn’t need the extra branch anymore:

$ git branch -d add-project-readme
Deleted branch add-project-readme (was 7b8b638).
Enter fullscreen mode Exit fullscreen mode

Passing the -d option with a branch name to the git branch command deletes the named branch.

Also, she doesn’t need the add-project-readme branch on GitLab. This is for the same reason that she no longer needs the local version. Using the GitLab web interface, she could delete this branch by clicking on the “Branches” link under the “Project information” heading on the right-hand side of her fortune cookie project page:

Alice's fortune cookie project overview page with two branches

(Equivalently, she could have clicked on the “Code” item in the sidebar on the left-hand side, and subsequently on the “Branches” sub-item.)

This will take her to the branches overview page for this project:

Branch overview page of Alice's fork

On this page, she can click on the three dots at the right on the line for the add-project-readme branch. Doing so will show a pop-up menu with the option to delete the branch:

Delete branch pop-up menu on branch overview page

Clicking on that option brings up a confirmation dialog box asking her if she is absolutely sure she wants to delete the branch.

Delete branch confirmation dialog box

Were she now to click on the “Yes, delete branch” button, GitLab would delete the add-project-readme branch in her fork of the fortune cookie project.

Of course, Alice knows of a much quicker and easier way by using the command-line. She can delete the branch on GitLab by pushing to it with the --delete option:

$ git push --delete origin add-project-readme
Username for 'https://gitlab.com': alice@peateasea.de
Password for 'https://alice@peateasea.de@gitlab.com':
To https://gitlab.com/aliceellis/fortune-cookie.git
 - [deleted] add-project-readme
Enter fullscreen mode Exit fullscreen mode

This command tells Git to push the --delete operation on the add-project-readme branch of the origin remote repository. That’s just a roundabout way of saying “delete add-project-readme on the origin repo”.

Checking the list of all branches again, Alice sees that only those branches she expects to be present are in the command output:

$ git branch -a
* main
  remotes/origin/HEAD -> origin/main
  remotes/origin/main
  remotes/upstream/main
Enter fullscreen mode Exit fullscreen mode

Yay! All cleaned up! Time for a celebratory hot beverage before digging into the next task.

Lather, rinse, repeat

Where to from here? Well, there are a few things one could do to improve the application. I’ve made a quick list of ideas below; each would be its own pull/merge request or possibly several.

  • Add new fortune cookie messages.
  • Extract messages into a data file (separate configuration from program).
  • Fix an incorrect message (if present).
  • Translate messages into another language.
  • Add documentation.

Of course, because it’s the internet, someone will notice that most of the phrases are proverbs and not fortunes and will want the name of the project changed (or similar). But ya get that.

A pull request, short and sweet

My friend had asked for a “concise” explanation12 of how to make a pull request. Did I deliver? Well, no. It wasn’t concise in the slightest. Yet, to try and achieve that aim nonetheless, here’s the shortest version I could come up with.

Assume that you have an account on GitLab and have set up SSH. You fork the repository that you want to contribute to from the web interface. You make a note of your fork’s URL from the “Code” button on your fork’s main page in GitLab. Clone this URL to your laptop:

$ git clone git@gitlab.com:aliceellis/fortune-cookie.git
Enter fullscreen mode Exit fullscreen mode

Change into the directory that Git created when cloning:

$ cd fortune-cookie
Enter fullscreen mode Exit fullscreen mode

Imagine you want to add to the list of fortune cookie messages. Create a branch for the change you wish to make:

$ git switch -b add-new-fortune-messages
Enter fullscreen mode Exit fullscreen mode

Now make the change and check that everything works as expected. Commit the change:

$ git commit fortune-cookie.py
Enter fullscreen mode Exit fullscreen mode

Enter a well-written commit message in the editor that opens. When you have finished writing the message, save the file and quit the editor. The changes will now be committed to the branch. Push the branch to your fork:

$ git push
Enter fullscreen mode Exit fullscreen mode

Note the URL mentioned in the output from the git push command and open it in a web browser. On the page that appears in your web browser, click on “Create merge request”.

Done!

Wrapping up

In the end, this wasn’t the concise explanation that my friend was wishing for; it turned into a rather detailed description instead. Nevertheless, I hope that it was helpful anyway!

  1. To be honest, this is a simplification. Not all projects where one can contribute via pull requests are software-related. However, the vast majority are, hence my simpler statement.

  2. This is why they need to own a copy of your project. They can make changes (such as pushing to branches to their fork) that they wouldn’t be able to do with the original upstream repository.

  3. Historically, the main branch was called the master branch, but this usage is becoming less common.

  4. Actually called a bare repository, but that’s not important right now.

  5. Ever notice that, in English, filling in a form is the same as filling out a form? English can be weird at times.

  6. The “remote origin” mentioned here is a Git setting which points to Eck’s upstream repository on GitLab. The repository is “remote” because it’s not on Eck’s local computer. By convention, it’s called origin, hence why it’s referred to as the “remote origin”.

  7. If you hover over the button, you’ll see the help text “Create new fork”.

  8. An interesting point here is that there are now four copies of the repository: the original project repo on Eck’s local computer, Eck’s project repo on GitLab, Alice’s fork of the project repo on GitLab, and the local clone of the fork on Alice’s laptop. This duplication highlights the distributed nature of the Git version control model. Such duplication isn’t waste or a bug: it’s a feature. I’ve worked on projects where the upstream Git service had data corruption problems, and the fact that several developers each had a copy of the repository turned out to be very useful in recovering the data and getting the project back up and running again.

  9. While developing this worked example, I found this to be an odd default notification setting on GitLab. I would have expected that project owners want to be notified of merge requests in most cases, but it seems that’s not the case. My experience from GitHub is that, as a project owner or maintainer, notifications are enabled by default. Thus, when a new pull request comes in, an email is not far away. Hence, my surprise that Eck didn’t receive a message after Alice submitted her merge request.

  10. It has sometimes taken years for some of my pull requests to be seen and merged by maintainers. I’ve long forgotten that I submitted them, but sometimes an email appears with a “Hey, thanks!” message. I’m always really stoked to get such messages, even if I have to wait a long time for them 🙂

  11. Just to be really annoying, sometimes Git terminology uses different names for the same thing. For instance, the staging area can also be referred to as the cache and as the index.

  12. The original German was “kurz und bündig”, which translates roughly to “concise”.

Top comments (0)