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.
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.
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.
There, they fix the bug in a branch and push that branch back upstream to their fork.2
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.
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
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
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?
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/
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
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.
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
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.
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.
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:
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:
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.
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
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'.
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.
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
(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 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.
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.
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.
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
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)
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'
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
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.
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
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
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 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.
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:
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.
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.
Clicking on the single merge request item in the list, heās taken to the details page for the one that Alice submitted.
Clicking on the āCommitsā tab, he can see the commits that make up the 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.
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
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
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
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
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'
and list the contents of the directory with ls:
$ ls
fortune-cookie.py README.md
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)
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:
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.
Eck scrolls down to the comment box at the bottom of the page and thanks Alice for her 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
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.
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
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
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
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)
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
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
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
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
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:
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
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).
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:
(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:
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:
Clicking on that option brings up a confirmation dialog box asking her if she is absolutely sure she wants to delete the branch.
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
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
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
Change into the directory that Git created when cloning:
$ cd fortune-cookie
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
Now make the change and check that everything works as expected. Commit the change:
$ git commit fortune-cookie.py
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
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!
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. ā©
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. ā©
Historically, the
mainbranch was called themasterbranch, but this usage is becoming less common. ā©Actually called a bare repository, but thatās not important right now. ā©
Ever notice that, in English, filling in a form is the same as filling out a form? English can be weird at times. ā©
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ā. ā©If you hover over the button, youāll see the help text āCreate new forkā. ā©
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. ā©
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. ā©
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 š ā©
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. ā©
The original German was ākurz und bündigā, which translates roughly to āconciseā. ā©




































Top comments (0)