The letter A styled as Alchemists logo. lchemists
Published March 25, 2021 Updated September 28, 2022
Cover
Git Default Branch

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 to main.

  • 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!