A driving force behind this site is to be more socially and ethically just by promoting better ways
to be kind and supportive to one another. One way to bring those ethics into the software itself is by not using racist and derogatory terminology by avoiding the use of master
for the default branch in Git repositories, instead using main
as your primary branch. For more context as to how master
ended up as the default branch despite the word’s racist past, you might want to read
Git Rev News, Edition 65 for details.
In this article, I’ll walk you through how to automate these necessary changes for your own projects so, you too, can make the world a little better.
New Repositories
To ensure new repositories pick up your preferred default branch name all you need to do is define this setting in your Git global configuration:
git config --global init.defaultBranch main
For GitHub users, GitHub defaulted to using the main
branch on October 1st, 2020 but you might
want to double check your repository settings to
confirm.
Existing Repositories
In my case, when dealing with existing repositories, I needed to tackle the following objectives:
-
Craft a script which is idempotent so it could reused multiple times if necessary.
-
Gracefully handle projects that have no remotes (i.e. local only).
-
Rename both local and remote default branches from
master
tomain
. -
Update remote GitHub repository and protected branch settings accordingly.
My solution was to leverage a Bash script for this work.
Script
Your situation might vary but I found it convenient to implement a Bash function/script to automate the entire process because I definitely didn’t desire to click through the GitHub UI for 30+ projects. 😅 Below is the script I used which I’ll deconstruct for you in more detail shortly:
# Label: Git Main
# Description: Defaults existing project's primary branch to "main" branch.
# Parameters: $1 (required) - Project name.
gain() {
local project="${1:-${PWD##*/}}"
printf "Analyzing 33[36m${project}33[m configuration...\n"
if [[ -z "$project" ]]; then
printf "%s\n" "ERROR: Project must be supplied."
return 1
fi
if [[ ! -d ".git" ]]; then
printf "%s\n" "ERROR: Project must be a Git repository."
return 1
fi
if [[ "$(git branch --show-current | tr -d '\n')" == "main" ]]; then
printf "%s\n" "Project has main branch. Skipping..."
return
fi
printf "%s\n" "Renaming master branch as main branch..."
git branch --move master main
if git config --get remote.origin.url > /dev/null; then
git push --set-upstream origin main
else
printf "%s\n" "Skipped remote configuration since remote doesn't exist yet."
return
fi
printf "%s\n" "Setting GitHub main branch..."
curl --request "PATCH" "https://api.github.com/repos/$GITHUB_API_LOGIN/$project" \
--header "Accept: application/vnd.github.v3+json" \
--header "Content-Type: application/json; charset=utf-8" \
--user "$GITHUB_API_LOGIN:$GITHUB_API_TOKEN" \
--progress-bar \
--output /dev/null \
--data $'{
"default_branch": "main"
}'
printf "%s\n" "Adding GitHub main branch protection..."
curl --request "PUT" "https://api.github.com/repos/$GITHUB_API_LOGIN/$project/branches/main/protection" \
--header "Accept: application/vnd.github.luke-cage-preview+json" \
--header "Content-Type: application/json; charset=utf-8" \
--user "$GITHUB_API_LOGIN:$GITHUB_API_TOKEN" \
--progress-bar \
--output /dev/null \
--data $'{
"required_status_checks": {
"strict": true,
"contexts": [
"ci/circleci: build"
]
},
"required_pull_request_reviews": {
"dismiss_stale_reviews": true,
"require_code_owner_reviews": true,
"required_approving_review_count": 1
},
"enforce_admins": true,
"required_linear_history": true,
"restrictions": null
}'
printf "%s\n" "Deleting GitHub master branch protection..."
curl --request "DELETE" "https://api.github.com/repos/$GITHUB_API_LOGIN/$project/branches/master/protection" \
--header "Accept: application/vnd.github.v3+json" \
--header "Content-Type: application/json; charset=utf-8" \
--user "$GITHUB_API_LOGIN:$GITHUB_API_TOKEN" \
--progress-bar \
--output /dev/null \
--data $'{}'
printf "%s\n" "Deleting remote master branch..."
git push --delete origin master
printf "%s\n" "Success!"
}
To run the above function, you’d only need to change to your project’s root directory and run the
gain
function. Example:
cd example
gain
Output
For a project that wasn’t converted yet, you’d end up with the following informational output:
Analyzing example configuration... Renaming master branch as main branch... Total 0 (delta 0), reused 0 (delta 0), pack-reused 0 remote: remote: Create a pull request for 'main' on GitHub by visiting: remote: https://github.com/bkuhlmann/example/pull/new/main remote: To github.com:bkuhlmann/example.git * [new branch] main -> main Branch 'main' set up to track remote branch 'main' from 'origin' by rebasing. Setting GitHub main branch... ######################################################################## 100.0% Adding GitHub main branch protection... ######################################################################## 100.0% Deleting GitHub master branch protection... ######################################################################## 100.0% Deleting remote master branch... To github.com:bkuhlmann/example.git - [deleted] master Success!
In the case where a project had no remote configured, you’d get the following output:
Analyzing example configuration... Renaming master branch as main branch... Skipped remote configuration since remote doesn't exist yet.
Finally, for projects that already had a main
branch, you’d see the following:
Analyzing example configuration... Project has main branch. Skipping...
A little bit of Bash scripting can go a long way and, in this case, was a huge help in making my
transition from the master
to main
branch smooth. 🎉
Breakdown
Earlier, I promised I’d break down the above script and explain how it works so let’s begin at the top starting with these lines:
local project="${1:-${PWD##*/}}"
printf "Analyzing 33[36m${project}33[m configuration...\n"
The first line sets a local variable, project
, which defaults to the basename (i.e. last directory
name) of the current path. This means you can supply a path or let the function figure it out. I’ll
touch upon this more later. The next line prints an informational message where the project
currently being analyzed is printed in a cyan color for improved readability.
Next is a block of conditionals which guard against if the project name is missing, if the project
is not a Git repository, and if the main
branch already exists so the function can return
immediately if requirements are not met:
if [[ -z "$project" ]]; then
printf "%s\n" "ERROR: Project must be supplied."
return 1
fi
if [[ ! -d ".git" ]]; then
printf "%s\n" "ERROR: Project must be a Git repository."
return 1
fi
if [[ "$(git branch --show-current | tr -d '\n')" == "main" ]]; then
printf "%s\n" "Project has main branch. Skipping..."
return
fi
The next line moves/renames our master
branch as the main
branch:
git branch --move master main
To close the loop, we also need to ensure the corresponding upstream (origin) branch is configured
as the main
branch — but only if the value of git config --get remote.origin.url
is empty,
which is what these lines accomplish:
if git config --get remote.origin.url > /dev/null; then
git push --set-upstream origin main
else
printf "%s\n" "Skipped remote configuration since remote doesn't exist yet."
return
fi
At this point in the script, we start making API calls to GitHub. The first API call ensures main
is registered as the default branch:
printf "%s\n" "Setting GitHub main branch..."
curl --request "PATCH" "https://api.github.com/repos/$GITHUB_API_LOGIN/$project" \
--header "Accept: application/vnd.github.v3+json" \
--header "Content-Type: application/json; charset=utf-8" \
--user "$GITHUB_API_LOGIN:$GITHUB_API_TOKEN" \
--progress-bar \
--output /dev/null \
--data $'{
"default_branch": "main"
}'
For more on this API endpoint, see the GitHub documentation on updating a repository.
I want to highlight that the global constants are used to define my credentials which break down as follows:
-
GITHUB_API_LOGIN
- Equates to my GitHub username/handle. -
GITHUB_API_TOKEN
- You’ll need to configure a Personal Access Token (PSA) especially when using Two-Factor Authentication (2FA). You are using 2FA, right? Right!?! As for the PSA scopes, you’ll need to enable all of the repo section.
The next API call is to configure branch protection for the new main
branch to use a
Git Rebase Workflow. I recommend reading the
GitHub API
Documentation on how to use this endpoint. Be aware that this endpoint requires a special header
since it’s still in preview: application/vnd.github.luke-cage-preview+json
. OK, so here’s the API
request with all of my desired settings:
curl --request "PUT" "https://api.github.com/repos/$GITHUB_API_LOGIN/$project/branches/main/protection" \
--header "Accept: application/vnd.github.luke-cage-preview+json" \
--header "Content-Type: application/json; charset=utf-8" \
--user "$GITHUB_API_LOGIN:$GITHUB_API_TOKEN" \
--progress-bar \
--output /dev/null \
--data $'{
"allow_deletions": true,
"required_status_checks": {
"strict": true,
"contexts": [
"ci/circleci: build"
]
},
"allow_force_pushes": true,
"required_pull_request_reviews": {
"dismiss_stale_reviews": true,
"require_code_owner_reviews": true,
"required_approving_review_count": 1
},
"enforce_admins": true,
"required_linear_history": true,
"restrictions": null
}'
The final API request, and
associated
documentation, is necessary for deleting exsiting branch protection rules from the now useless
master
branch:
curl --request "DELETE" "https://api.github.com/repos/$GITHUB_API_LOGIN/$project/branches/master/protection" \
--header "Accept: application/vnd.github.v3+json" \
--header "Content-Type: application/json; charset=utf-8" \
--user "$GITHUB_API_LOGIN:$GITHUB_API_TOKEN" \
--progress-bar \
--output /dev/null \
--data $'{}'
The above API request is critical to successfully executing the final line in the function to delete
the remote master
branch since the main
branch is your new default branch now:
git push --delete origin master
Additional Automation
As I mentioned earlier, the above function can take a path because you can wrap this function in another function to perform this work on multiple projects/repositories within the same directory. Here’s the function for doing exactly that:
# Label: Git Main (all)
# Description: Defaults all projects to "main" branch.
# Parameters: None.
gaina() {
while read -r project; do
(
cd "$project"
gain "$project"
)
done < <(ls -A1)
}
Now you can use either the gain
function for updating a single project or gaina
to update all
projects within the same directory.
Clones
In the situation where you have a cloned project with main
already set as the default branch, you
can use the following script to synchronize with the upstream changes:
# Label: Git Main Update
# Description: Updates cloned projects to use "main" as the default branch.
# Parameters: None.
gainu() {
while read -r project; do
(
cd "$project"
if [[ -d ".git" ]]; then
git switch master
git branch --move master main
git fetch
git branch --unset-upstream
git branch --set-upstream-to origin/main
git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main
# Print project (cyan) and message (white).
printf "33[36m${project}33[m: Updated.\n"
fi
)
done < <(ls -A1)
}
The above function doesn’t have all of the safe guards like the earlier function for dealing with existing repositories does. I’ll leave that up to you use as is or modify further if desired.
Dotfiles
For my Dotfiles, I introduced the following private function:
# Label: Git Branch Default
# Description: Print Git default branch.
_git_branch_default() {
local default="$(git config --get init.defaultBranch)"
printf "${default:-main}"
}
The above was necessary in order to calculate the default branch based on the global (or local) Git
configuration for defaultBranch
. Should a default branch not be found, it’ll fallback to the
main
branch. I ended up using this function to refactor many of my aliases and functions without
breaking my workflow when working in projects with different default branches.
Heroku
Heroku, currently doesn’t support a default branch configuration, so you’ll need to update your scripts to avoid the following:
git push heroku master
Instead, you’ll want to use:
git push heroku main:master
This is a bummer but only requires an extra bit of syntax to use properly.
Conclusion
Changing your language choices from master
to main
is absolutely worth the effort. I hope the
above is of use to you, especially if you haven’t made the transition yet. Enjoy!