29

We have a lot of branches that are inactive (the newest is 7 months old, the oldest is two years ago).
I'd like to remove all of those branches in bulk from the remote if no PR is still open for them.

Should I be using Github's API? Should I be using git using snippets like those provided in this StackOverflow question?
Is there some Github functionality I'm not familiar with that can help organize our repository?

Community
  • 1
  • 1
the_drow
  • 18,571
  • 25
  • 126
  • 193
  • 1
    I'd love to have this feature in Github. When you fork a repo with 100+ branches that's just clutter. – Kangur Mar 26 '17 at 19:46

5 Answers5

32

you can right click on the browser after opening the stale branches and run this in the browser java script console.

For the 2023 Github's web page:

async function deleteStaleBranches(delay=500) {
    var stale_branches = document.getElementsByClassName('js-branch-delete-button');
    for (var i = 0; i < stale_branches.length; i++)
    {
        stale_branches.item(i).click();
        await new Promise(r => setTimeout(r, delay));
    }
}

(() => { deleteStaleBranches(500); })();

credit: https://gist.github.com/victorlin/9c0be8f2d3305eae4d7bb8c5907a9e17

For the 2021 Github's web page:


 var stale_branches = document.getElementsByClassName('color-text-danger');
 for (var i = 0; i < stale_branches.length; i++) {
 stale_branches.item(i).click();
 }

or as @MartinNowak has suggested

document.querySelectorAll('button[data-target="branch-filter-item.destroyButton"]').forEach(btn => btn.click())
Surya
  • 2,429
  • 1
  • 21
  • 42
  • This is cool, any way of only doing that for merged ones? – Luis Davim Nov 03 '21 at 11:37
  • 1
    you can automatically do that though -> https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-the-automatic-deletion-of-branches – Surya Nov 03 '21 at 13:09
  • 1
    `document.querySelectorAll('button[data-target="branch-filter-item.destroyButton"]').forEach(btn => btn.click())` – Martin Nowak Nov 12 '21 at 20:35
  • For those searching, this solution doesn't work for Gitlab, because after the first branch deletion, the page refreshes, and you are no longer on the Stale branches tab, but the Branches Overview tab. If that didn't happen, it would have worked with a minor modification to the string passed to 'querySelectorAll'. – ryanwebjackson Apr 29 '22 at 00:45
  • 3
    2022, need to update the selector and add a delay: `var stale_branches = document.getElementsByClassName('js-branch-delete-button'); for (var i = 0; i < stale_branches.length; i++) { stale_branches.item(i).click(); await new Promise(r => setTimeout(r, 500)); }` – victorlin May 25 '22 at 22:21
  • thanks, but I get the error `Uncaught SyntaxError: await is only valid in async functions, async generators and modules` when I tried. – Surya May 26 '22 at 17:43
  • 2
    @LunaLovegood updated to be more browser-compatible here: https://gist.github.com/victorlin/9c0be8f2d3305eae4d7bb8c5907a9e17 – victorlin Jun 06 '22 at 21:03
  • Thanks @victorlin, I have updated the answer and credited to the gist link of yours. – Surya Jun 10 '22 at 09:08
  • added a loop to automove to next page. `async function deleteStaleBranches() { var stale_branches = document.getElementsByClassName('js-branch-delete-button'); for (var i = 0; i < stale_branches.length; i++) { stale_branches.item(i).click(); await new Promise(r => setTimeout(r, 500)); } await document.evaluate("//a[text()='Next']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue.click(); await new Promise(r => setTimeout(r, 500)); } while(true){ await deleteStaleBranches(); }` – Nahum Sep 22 '22 at 10:38
  • Hi Nahum, am not a js pro, when I tried running the script that you have given i get the error "Uncaught SyntaxError: await is only valid in async functions, async generators and modules", could you please let me know how we should run this script? I tried in firefox. The other scripts works in firefox. – Surya Nov 07 '22 at 14:01
  • 1
    FYI - The 2022 version still works in 06/2023! – renegadeMind Jun 12 '23 at 15:46
11

You can certainly achieve this using the GitHub API, but you will require a little bit of fiddling to do it.

First, use the list pull requests API to obtain a list of open pull requests. Each item in this list contains a ["head"]["ref"] entry which will be the name of a branch.

Now, using the get all references API, list all of the references in your repository. Note that the syntax for branches in the Git Data API is slightly different than the one returned from the pull request API (e.g. refs/heads/topic vs. topic), so you'll have to compensate for this. The references API also returns tags unless you search just the refs/heads/ sub-namespace, as mentioned in the docs, so be aware of this.

Once you have these two lists of branch refs, it's simple enough to work out which branches have no open pull requests (don't forget to account for master or any other branch you wish to keep!).

At this point, you can use the delete reference API to remove those branch refs from the repository.

kfb
  • 6,252
  • 6
  • 40
  • 51
  • 5
    I wrote a Python script that does exactly this. It lists all existing branches that are referenced by at least 1 closed PR and in none open PRs. https://github.com/simplesurance/utils/blob/master/git/stale_github_pr_branches.py – fho Sep 12 '18 at 15:31
3

I took a different tack from the other answers and just used good ol' git (and bash) to do the work:

  1. Retrieve list of all branches using git branch -r >~/branches.txt (after setting git config --global core.pager cat)
  2. Prune ~/branches.txt of ones I want to keep
  3. Call git push <ref> --delete <branch> on each one ...
for ref_branch in $(cat ~/branches.txt | head -n 5); do
    ref="$(echo "$ref_branch" | grep -Eo '^[^/]+')"
    branch="$(echo "$ref_branch" | grep -Eo '[^/]+$')"
    git push "$ref" --delete "$branch"
done

Note the use of | head -n 5 to give it a try with only 5 at a time. Remove that to let the whole thing rip.

This will likely work on most sh shells (zsh, etc.) but not Windows; sorry.

Neil C. Obremski
  • 18,696
  • 24
  • 83
  • 112
1

More safe option - delete all merged branches:

async function deleteStaleBranches(delay=500) {
    let buttons = [];
    document.querySelectorAll(".State.State--merged").forEach((el) => {
        const butEl = el.parentElement.nextElementSibling.nextElementSibling.querySelector("button");
        buttons.push(butEl);
    });
    for (let i = 0; i < buttons.length; i++)
    {
        buttons[i].click();
        await new Promise(r => setTimeout(r, delay));
    }
}

(() => { deleteStaleBranches(500); })();
0

I used the following in the console to delete only merged and closed branches.

async function deleteMergedBranches(delay=500) {
    var stale_branches = document.getElementsByClassName('State--merged');
    for (var i = 0; i < stale_branches.length; i++)
    {
        stale_branches[i].parentElement.nextElementSibling.nextElementSibling.nextElementSibling.querySelector('.js-branch-delete-button').click()
        await new Promise(r => setTimeout(r, delay));
    }
}

(() => { deleteMergedBranches(500); })();
async function deleteClosedBranches(delay=500) {
    var stale_branches = document.getElementsByClassName('State--closed');
    for (var i = 0; i < stale_branches.length; i++)
    {
        stale_branches[i].parentElement.nextElementSibling.nextElementSibling.nextElementSibling.querySelector('.js-branch-delete-button').click()
        await new Promise(r => setTimeout(r, delay));
    }
}
Farhad Khan
  • 104
  • 2
  • 14