Using Git in Laravel Team: Branches, Pull Requests, Conflicts

Git is an essential tool for every developer. In this tutorial, I will explain everything you need to know about branches and conflicts while working in a team, with Laravel examples.

In fact, this article is not about Laravel, it's just that example code will be with Laravel framework, but you can apply Git knowledge from here to other coding languages/frameworks, too.

Notice: in this tutorial, we will use Terminal and not use visual tools like Sourcetree/GitHub Desktop or PhpStorm/VSCode editors. Those tools can help but I want you to learn principles and the syntax if you do need to work with the Terminal.


Start with Branches: Main/Master and Develop

Branches are probably the foundation when working with the team. But even when working solo, you may use branches to work on separate features simultaneously, so you may have branches called "feature-payment", "feature-user-update", etc.

Also, you may use them for testing features from branches without deploying them to live. So, your testing server would get the code from "develop" branch, for example, and you can play around there, until you're sure it works, and then merge the code into the "main" branch for deployment.

Every team choose their own branch names and branch logic, but there's a most typical behavior, here's how it goes.

When the repository is created, it is created with the branch called main.

Notice: GitHub changed its default from master to main for new repositories in 2020.

git main branch

Next, let's create a new fresh Laravel project and push its code to GitHub.

laravel new laravel
cd laravel

Now we can push to that main branch:

git init
git add .
git commit -m "Fresh Laravel"
git branch -M main
git remote add origin [email protected]:krekas/Git-Example.git
git push -u origin main

What every command here does?

  • Creates an empty Git repository.
  • Adds all files to the repository.
  • Makes a commit with the message "Fresh Laravel".
  • Sets branch to main.
  • Adds an origin where to push.
  • Pushes code to the main branch.

If you refresh the GitHub repository page, you will see the code was pushed to the main branch.

fresh code in the main branch

Next step: usually the main branch is only for the "finalized" features to deploy to live. And the work is being done in other branches. So, from that main branch, someone creates a develop branch where the work is being done. Here's how you can do it on GitHub directly in the browser.

create develop branch

At this point, the develop code is identical to the main branch.

Then, whenever a developer starts working with the project, they clone the repository, checkout the develop branch and starts the work.

The main branch is only for deployment to the live server which happens from time to time, but more rarely than everyday work.

You can even protect the branches on GitHub, to restrict pushing to the main branch, to avoid accidentally pushing wrong things to live server.


Routine Work: Checkout Develop and Feature Branches

At any point, you are working with one specific branch. To check what branch are you on, you do git status.

On branch main
Your branch is up to date with 'origin/main'.
 
nothing to commit, working tree clean

And you see On branch main which means that you are on the main branch. Next, you need to do git fetch: it downloads information about newer changes (but not the code itself, yet). And we see that the new change is about a new branch.

From github.com:krekas/Git-Example
* [new branch] develop -> origin/develop

Now you can switch into the develop branch by doing git checkout develop.

Branch 'develop' set up to track remote branch 'develop' from 'origin'.
Switched to a new branch 'develop'

The develop branch code will be downloaded at this point.

So this is how you need to start with every new feature: checking out the latest version of develop and start coding.

From here, there are two ways how teams agree to work:

  • Work directly on the develop branch: generally, it happens when there's only one developer and no team
  • Or create a thing called feature branches. So whenever someone starts a new feature, they need to create their branch, branching from develop, naming the branch accordingly to the feature (like "feature-payments"), and then whenever they are ready they need to merge into the develop branch, not into main.

Personally, I recommend using feature branches even if you're solo, because it allows you to work on multiple features at the same time, choosing which one(s) to merge/push into the next live deployment.

The main branch still remains kinda like a sacred thing where only the repository owner or person who will deploy the code is responsible for this branch. Everyone else works on develop or feature branches.


Finished the Task? Pull Request from Feature Branch

Let's look at a real scenario how work would happen.

Imagine we have a task to install Laravel Breeze. First, we need to create (checkout) the new feature branch.

git checkout -b breeze-install -╯
Switched to a new branch 'breeze-install'

This command does two things:

  • creates the new branch
  • switches to it

So our code changes will be saved on the breeze-install branch and not develop, until we decide to push and merge into develop.

Now, let's install the package.

composer require laravel/breeze --dev
php artisan breeze:install blade

Ok, so our code task is done. Now let's push new code to the feature branch.

git add .
git commit -m "Laravel Breeze"
git push --set-upstream origin breeze-install

And now in the GitHub repository, we have three branches.

three branches in the repository

The next step is to create a thing called Pull Request. This is a request for other team members (usually your senior developer) to check that code and approve that it should go into the develop branch.


GitHub web version can help you with suggestions for the pull request. So you can click Compare & pull request.

github offers pr

Name the pull request with the feature name. And the most important part is to change where to merge to the develop branch.

pr to develop branch

It will check if there are any code conflicts, and if you see Able to merge you can just Create pull request.

And that's it for regular developer workflow: the task is over for now until it gets checked. Next, someone who is responsible for the code review will check it, and if everything is correct, it will be merged into the develop, and later deployed to server(s).

Or, if the teammates ask you to make changes in your code, you do that in the same feature branch, just do git push and your changes will automatically applied to the same Pull Request.


Avoid Conflicts with Feature Branches

Often you have a situation: you work on one feature, then you make a Pull Request, and then until someone approves that, you need to start a new feature. So how to avoid conflicts?

Let's demonstrate it be working a new feature: for example, add a surname to the user table.

First, a new branch should be made from the develop branch.

git checkout develop
git checkout -b feature/user-surname

Next, quickly we add the surname.

php artisan make:migration "add surname to users table"

database/migrations/xxxx_add_surname_to_users_table.php

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->string('surname');
});
}
};

app/Models/User.php:

class User extends Authenticatable
{
// ...
protected $fillable = [
'name',
'email',
'password',
'surname',
];
// ...
}

And now we are ready to commit the code.

With git status we can check what files were changed, and two files are changed:

On branch feature/user-surname
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: app/Models/User.php
 
Untracked files:
(use "git add <file>..." to include in what will be committed)
database/migrations/2023_03_26_140758_add_surname_to_users_table.php
 
no changes added to commit (use "git add" and/or "git commit -a")

Now let's add files and commit:

git add .
git commit -m "Surname for user"
git push --set-upstream origin feature/user-surname

Now we are ready to create a Pull Request.

Go to GitHub, create a Pull Request, and don't forget to change the base branch to the develop branch.

create pr

Now, what happens until someone approves this feature? We receive a new task also related to that new user, what do we do?

A typical mistake that I saw from junior developers or from those who are new to git, is to continue working on the same feature branch.

That's why those branches are called feature branches, because every branch is related to only one feature. So if you have a new feature, for example adding another field into the user's table, then you need to check out the develop branch again, then create another feature branch from develop and continue on that separate branch.

While approving the first task, the person who is approving that will have a much better experient if you stick to only one feature and the code in the pull request will be related only to that feature. It will be easier to read, understand, test and comment/approve.

If you add unrelated code to the same branch, then it will be automatically added to that pull request and will confuse the person who is approving. So one feature branch, only code for that branch, and you continue.

Next, let's create another pull request. First, we need to switch to the develop branch, and only then create a new feature branch.

git checkout develop
git checkout -b feature/verify-email

And quickly let's add the MustVerifyEmail interface to the User Model.

app/Models/User.php:

// use Illuminate\Contracts\Auth\MustVerifyEmail; //
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
 
class User extends Authenticatable
class User extends Authenticatable implements MustVerifyEmail
{
// ...
}

Now, we need to commit and make a pull request, don't forget to set the base branch to develop:

git add .
git commit -m "Verify user email"
git push --set-upstream origin feature/verify-email

And now we have two opened Pull Requests.

two opened prs

If we merge the first Pull Request, with the feature of adding a surname.

closed first pr

And in the second feature pull request, we can see changes that are made only for this feature. The person who will review it will be able to quickly merge.

feature pr


Pull Down Develop Before Starting New Feature

Another tip to avoid conflicts.

Imagine a scenario you have to add a new field to the User Model. You add it, then make a Pull Request and call it a day.

github pr

Later reviewer merges this Pull Request. The next day you continue on this project with a new feature. Let's say you have to add the country field to the same User model.

You would checkout into the develop branch and create a new branch for a new feature like git checkout -b feature/user-country.

But STOP. In this case, a golden rule is before starting a new feature you have to pull down the changes from develop. Especially when working in a team there's a big chance that someone else had their pull request merged into the same develop branch which had changes that may conflict with the file that you want to edit now.

Or even if you don't have a conflict it's still the best-case scenario to have the latest version of the application on your local computer.

You should do git pull origin develop.

Or, in full:

git checkout develop
git pull
git checkout -b feature/your-new-feature

What If Conflict Happens?

Let's simulate the scenario that we didn't pull down develop branch, so we will have a conflict.

Let's say we missed the latest changes from the develop branch and we do git checkout -b feature/user-country. Now let's add the country field, but look at the all fields: there is no about field which was added earlier by someone else, while we were working on our tasks.

app/Models/User.php:

// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
 
class User extends Authenticatable
{
// ...
protected $fillable = [
'name',
'email',
'password',
'surname',
'country',
];
// ...
}

Now let's push changes to the new branch and let's try to create a Pull Request.

git add .
git commit -m "User country"
git push --set-upstream origin feature/user-country

Now you see the message Can't automatically merge, which is a conflict.

conflict pr message

You can still create a Pull Request, but the reviewer will see that there is a conflict.

Typically what would happen then: a reviewer will contact you and ask to resolve the conflict, because they don't know what should be the latest version of the file.

conflict pr


Resolving Conflict

So currently we are in our feature/user-country branch. We do git checkout develop and pull down the latest changes with git pull origin develop.

Then we switch back to the feature/user-country branch by doing git checkout feature/user-country.

And then we merge the develop branch into the feature/user-country branch by doing git merge develop. And of course, we get the expected conflict message:

Auto-merging app/Models/User.php
CONFLICT (content): Merge conflict in app/Models/User.php
Automatic merge failed; fix conflicts and then commit the result.

Then we go into IDE, in my case PhpStorm, and it will exactly show the conflict like this:

phpstorm conflict

With those arrows and you have HEAD which is your current version of the code and then after equals are code from another branch, in this case, develop.

You need to resolve the conflict by leaving the version you need or merging two versions like this one, and removing those conflicts. So now we have both fields, country and about.

resolve conflict in phpstorm

Now that this conflict is resolved we need to commit those changes.

git add .
git commit -m "Resolve conflict"
git push

And after visiting this Pull Request you will see the latest commit with the message Resolve conflict and the approver may approve the pull request.

pr with resolved conflict

And if you go to Files changed you will that only one file with only this feature was changed.

correct file changes after conflict

So this is how you typically resolve the conflicts. But it's better to avoid them: just before creating a new feature branch always do git pull origin develop or whatever is your main branch. Maybe you don't use develop and then do git pull origin main.


That's it for this tutorial. This is my version of using git with branches. Different teams may have different branches and different philosophies/opinions.

If you want to video version on the same topic, you can check my 2-part YouTube videos:

avatar

Hey there,

I follow your work from a long time, but honestly I don't accept every point of your article.

What you showed here, is the path from developing a new feature, to merge it into develop. What I don't see is, how you merge features safely to main? Do you merge develop to main from time to time? How can you release features to main separately? With this technique you can't, or there is only a few feature development parallel.

I would change one step (above in the article). Imagine a team with a real client with multiple feature request. The client wants to test feature by feature, so he/she can mark the features, which can go live. And it is really important.

Whenever you create a new feature branch, you create it from the main branch, and not from the develop. The develop branch can have unaccepted features. If you create a new feature branch from here, there is no chance, your newly created feature could be released sooner, if the client approves it. Let's say, there is an ongoing blog feature request, which some part is already in the development, but the client changes his minds from time to time. Also there is a new popup request, if the visitor wants to left site, a popup should appear. If this popup feature comes from the 'unfinished' develop branch, you can't release the popup stuff, until everything is accepted.

As always you say: it depends, of course you are right :)

What I wanted to point out: This approach (features came always from develop branch) can cause headaches and lost of hours, if there are multiple feature developments in parallel & someone create a new feature from develop branch.

It will only show something unexpected, when you want to merge the accepted feature to main and you see more changes than it was committed.

But this is my opinion & experience which I wanted to share :)

👍 8
avatar
Mehmet Nurullah Sağlam

Hey, I got your point but another view is, you can use your main/master branch for versioning and develop branch to put a last step before release.

For example, your app/package is on version 0.1.0 and want to add x, y and z features for the version 0.2.0. I think it's not a proper way to merge these features to main/master branch separately. You can combine them on develop branch and when it's merged to main/master, it will be the next version.

But, as you said, the client may wants test feature by feature or some other case. And again as we all say, it depends :)

avatar

Thanks for the very valuable and long comment Pappz. I agree, this article mostly describes the "almost ideal" world where nothing needs to be hot-fixed or deployed urgently.

For urgent deployments like these, every team have different approaches. Personally in our experience, we used to break all the rules and committed straight to "main" for very urgent things, then merging it into develop, after it all calms down. But do you really think that I need to recommend this approach? I kinda deliberately left it our of the article, pointing out only the "best" scenarios because it's best to keep it that way.

I guess there should be a separate article of "Ways to quickly deploy/launch things", will think about it.

avatar

Thanks for the reply! I know situations, teams and clients are all different. Also I think, maybe there is a little misunderstanding so I try to show it with a timeline.

I think this can be a good approach, and this is what I meant, that every new feature branch should be made from main, in my point of view:

Day 1,

  • main and develop branches are equal
  • client request feature A
  • developer007 checkout from main, create a new feature/A branch

Day2,

  • developer007 merge feature/A branch to develop
  • after it client request feature B
  • developerKing checkout from main, create a new feature/B branch

Day3,

  • developerKing merge feature/B branch to develop
  • at this time, main is not touched, but develop has feature/A and feature/B
  • client reply to feature A, it needs some modification, so developer007 will continue working on feature/A branch
  • client reply to feature B, it is ok, release it
  • now developerWhoInChargeToRelease can simply merge feature/B branch back to main
  • now main branch contains only the accepted feature

All-in-all:

What you mentioned above, always create new branch from develop is what I wanted to point out.

On Day2, if developerKing would checkout from develop branch, then his feature/B branch would contain feature/A, which was not accepted on Day3. This means, they couldn't release feature/B on Day3. Only after feature/A acceptance.

I hope it is clearer now.

Moreover, belive me, I made also some ASAP hot fixes, directly to main, so I understand, situations create the rules!

But I have to mention, I like to keep things clear. I always have fix/pappz or hotfixes branch, which is always "my-urgent-asap-fixes" branch. Whenever some hotfixes have to be done, I merge main to my hotfix branch, then after merge it to main. This make things more clear for me. (At least I try to belive in :) ).

avatar

Wow what an amazing long comment, thanks for taking the time! And yes, I do agree with the approach you described, as you explained it in more detail.

avatar

main branch at origin should reflect production-ready state.

develop branch reflects a state with the latest delivered development changes for the next release, it may be called as "integration branch". In general not completed or not accepted feature shouldn't be merged into develop in the first place, but that's not always the case. Good strategy could be to use --no-ff flag when merging feature into develop.

git checkout develop
git merge --no-ff feature/A

This way it always creates new commit object and preserves historical changes (even if fast-forward is possible) of a feature branch, and allows to easily discard feature from the next release.

Then another dev created feature/B from develop with feature/A merged. Got work done, work was approved.

feature/A gets discarded from develop by this point.

Now feature/B has conflicts and still feature/A present. It is good to resolve that upfront (for example we do not know yet if client approved feature/B or not). So what dev can do is just to run rebase.

git checkout feature/B
git rebase develop

rebase will reapply feature's B commits on top of current develop branch and will allow to resolve conflicts in the feature branch instead of when merging into develop.

This way you can merge feature/B anytime into develop without any consequences, and ever having to deal with main to apply hotfixes.

git checkout develop
git merge --no-ff feature/B

When the code in the develop reaches a stable point, changes are merged back into main and this is a new production release by definition.

avatar
You can use Markdown
avatar

Resolving Conflict Section *I think Re consider third paragraph of this section *

we merge the develop branch into the feature/user-country branch by doing git merge develop. And of course, we get the expected conflict message:

I think, It Should be we merge the feature/user-country branch into the develop branch by doing git merge develop. And of course, we get the expected conflict message:

@Povilas

avatar

I think that one is actually correct. To resolve the conflict, we do need to merge the developer into our feature branch, then resolve the conflict in our feature branch, and push the feature branch.

And only then merge into develop.

avatar
You can use Markdown
avatar

BTW, This is The great article. Love it.

avatar
You can use Markdown
avatar

Very well layed out Povilas! I think this is the basic way to follow, then depending on different scenarios, teams might proceed as required.

avatar
You can use Markdown
avatar

I have a following problem: I have 4 laravel apps wich are very similar but not identical and mostly the difference is the design (they are made for different clients). Now I have to add some fixes and some new functionality to all of them. All the fixes and the new functionality are the same for all the apps. Is there a way to make some kind of patch to apply so I will not have to copy-paste all of this fixes and new stuff to every one of the apps separately?

avatar

Well, this is tricky.

Is there any way this could be done? Yeah! Private packages would help here. They are quite good at keeping things up to date from a single point.

Is there any other way? Sadly - no. Separate projects/repositories means separate bugfixes. And that has to be done manually.

avatar

I just found the better (I think) way:

  • change one app as you need (if the app has any irrelevant to the topic changes commit them before starting)
  • make all necessary changes

When finished the changes there are 2 ways

  1. from command line do: git diff > mypatch.patch and then commit
  2. if you use PHP Storm commit the work and right click the commit you've just created and select Create Patch... and name it as you wish eg. mypatch.patch

Now copy the mypatch.patch to other apps you want to patch and from command line do: git apply mypatch.patch

tada! you have it patched.

avatar

Yes, this is possible too, but to be fair - is really hacky :) I'd still recommend extracting common parts as a package, rather than individual stuff. That way you will have re-usable code pieces

avatar

You can not extract fixes of some errors and few new methods or changes in existing controllers or views as a package. To me that sounds crazy. And even if it was possibe then it would be really really hacky.

avatar

Oh no, not fixes! I'm talking about the case, where you just mentioned that patch was pretty much identical between all applications. This indicates, that the code behind is also identical (to some extent!). That means - it should be possible to completely move that code to a private package and maintain it via that package.

In other words:

  1. You create a common codebase as a package
  2. Use that in any number of projects
  3. Once they need identical updates - you just update the package and pull newer version in the projects

At least that's how I feel it should be done, as otherwise you will constantly have to update the same thing in 4 or more places :)

avatar

But as I said I needed fixes + some extensions to many existing apps :) Otherwise I agree

avatar
You can use Markdown
avatar
You can use Markdown

Recent New Courses