18
for k in $(git branch -r --merged origin/master | cut -d" " -f 3); do
    echo $k
done

I have a list of git projects that i want to clean up old branches in, what i want to do is list and delete all the branches that are merged to master.

Is there a way to do the above without cloning each repo locally?

A.Jac
  • 1,443
  • 3
  • 17
  • 24
  • use `git branch` and the delete with `git branch -d `. – phunsukwangdu Jan 31 '17 at 11:50
  • 1
    i will use ` git branch -d branch_name ` to delete branch yes, what i want to know is if there is a way to do it without having a local copy of the repository – A.Jac Jan 31 '17 at 11:55
  • refer this [link](http://stackoverflow.com/questions/2003505/how-to-delete-a-git-branch-both-locally-and-remotely) – phunsukwangdu Jan 31 '17 at 11:57
  • Possible i am not explaining properly, but what i need is a way to list the branches that are merged to master (like the code in OP) without the need for a local copy of the repository. – A.Jac Jan 31 '17 at 12:13

2 Answers2

31

You need git ls-remote:

NAME

git-ls-remote - List references in a remote repository

SYNOPSIS

git ls-remote [--heads] [--tags] [--refs] [--upload-pack=<exec>]
              [-q | --quiet] [--exit-code] [--get-url]
              [--symref] [<repository> [<refs>...]]

DESCRIPTION

Displays references available in a remote repository along with the associated commit IDs.

So it works like:

% git ls-remote origin
af51dfb080728117d898e1d0a10e3fe01ed67063        HEAD
6a60cc68a2953f1a62b0dca641eb29509b5b6e8c        refs/heads/expdate-fix
af51dfb080728117d898e1d0a10e3fe01ed67063        refs/heads/master
4c42e43b4ccfd37074d115f6e9a694ddb8b70d55        refs/heads/redux
fd18a67bbc5cbf8aa6cda136afa4e5c20ed2d522        refs/heads/rest
7ad17cdf8b0dcd1a29a1795a363279fb3c76ac66        refs/tags/test.key
be0b2d6881902600fb3d6686c10d0a47f1e6751a        refs/tags/test.pub

To get only branches (heads), you need to narrow the refspec down:

% git ls-remote origin 'refs/heads/*'
6a60cc68a2953f1a62b0dca641eb29509b5b6e8c        refs/heads/expdate-fix
af51dfb080728117d898e1d0a10e3fe01ed67063        refs/heads/master
4c42e43b4ccfd37074d115f6e9a694ddb8b70d55        refs/heads/redux
fd18a67bbc5cbf8aa6cda136afa4e5c20ed2d522        refs/heads/rest

Now you could script around this output like

git ls-remote origin 'refs/heads/*' | while read sha ref; do
  # test if $sha is merged
done

To delete a branch, you need to "push nothing" to it, like in

git push origin :refs/heads/feature-x

(notice an empty string to the left of ":" which defined what to push to what is on the right side).

So we get something like

#!/bin/sh
set -e -u
git ls-remote origin 'refs/heads/*' | while read sha ref; do
  # test if $sha is merged
  E=`git cat-file -t "$sha" 2>&1`
  test $? -ne 0 -a "${E#*git cat-file: *}" = "could not get object info" && continue
  git branch --merged "$sha" && printf ':%s\0' "$ref"
done | xargs -0 git push origin

Note that we're using printf shell builtin to delimit the names of the refs we output with the ASCII NUL character and then pass -0 to xargs to expect NUL-terminated input. This way we work around funky ref names (containing spaces etc).

Some explanations:

  • If git cat-file -t <object_sha1_name> fails to locate the object with the indicated SHA1 name in the local repository, it exits with a non-zero exit code and prints

    fatal: git cat-file: could not get object info

    to its stderr.

  • So to test whether the history a remote ref points at exists in the local repository we run git cat-file -t on the SHA1 name of the object it points at, grab the combined output of that command and then test whether it exited with a non-zero exit code ($? -ne 0) and whether its error message indicates a missing object (the ${VAR#PATTERN} removes the prefix matching PATTERN from the contents of the variable VAR and returns the resulting value).

  • If the history a remote ref points at does not exist in the local repository, it cannot be merged to any of the local refs by definition, so if we detect such a ref, we skip its further testing with git branch --merged.

kostix
  • 51,517
  • 14
  • 93
  • 176
  • 1
    This is exactly what i needed, i had seen the ls-remote before but didnt understand it. Thank you – A.Jac Jan 31 '17 at 12:30
  • There's one big issue here: unless your repository is up to date with that remote, `git branch --merged` cannot give you correct answers. This means that you might as well run `git fetch` first, then work locally. – torek Jan 31 '17 at 14:35
  • @torek, that's correct. On the other hand, the very question was meant to avoid downloading history (I take "without cloning" also mean "without fetching"). One way around it would be testing for the special case of "commit not in the repository". Unfortunately, handling of this case in `git branch {--contains|--merged}` is (to me) surprising: as they *assume* the object with the given name do exists, and spew a not very helpful error message if it doesn't. Supposedly running `git cat-file -t "$sha"` on the commit name first would be better as it behaves in a more predictable manner IMO. – kostix Feb 01 '17 at 06:30
  • @torek, as a corollary, if `git cat-file -t "$sha"` errors out (here, with Git 2.11.1 it says "could not get object info" and exits with code 128), this means the ref identified by that `$sha` is *not* merged and hence must not be deleted from the remote repository. – kostix Feb 01 '17 at 06:31
  • I think this is good now. There are still some corner cases (that the OP did not actually ask about) regarding branch names that are rudely moved to disjoint subgraphs, but those probably should be considered "not merged", which this will do. – torek Feb 01 '17 at 07:08
  • I know this is an old response, but I'm confused how this actually tests if the branch was merged to master? The way I read it, this will check if the remote branch exists locally (ie: compare `ls-remote` with `cat-file`, and which branches have already been merged into the remote branch name `git branch --merged`. However, this does not check if the `$sha` has been merged into `master`. – Eric B. Jan 27 '21 at 16:38
  • @EricB., the script 1) obtains the SHA-1 names of the tip commits of all the branches existing in the remote; 2) it then takes each of these SHA-1 names and tries to locate them in the local repository (`git cat-file -t`); if that fails, the object with that SHA-1 name does not exist in the local repository, and that means the histoy graph it's a vertex on surely cannot be merged in the current branch as you cannot have some history merged into an existing branch but have no commits _of_ that history. 3) If the commit exists, it's tested for being merged the regurar way—`git branch --merged`. – kostix Jan 27 '21 at 17:34
  • @EricB., to be absolutely pedantic, since in Git you can have shallow clones and grafted histories, technically it is possible to have a branch in a local repository which does not contain its full history _physically_ (has no commit objects and other objects they refer to—trees, blobs and other commits) but this case is borderline and usually if a person is aware of such stuff they supposedly have no need to ask the original question; hence I've made a reasonable assumption that the local repository of interest has no funky setup ;) – kostix Jan 27 '21 at 17:40
4

Someone with more git knowledge may be able to expand on this, but today I had a similar challenge in that I wanted to remove a branch in multiple repos but I didn't want to clone each one before removing the branch.

It turns out git will let you remove a branch in a different repo as long as you are in some repo. I'm not sure if there are any restrictions here like they have to point to the same GitHub server, etc.

But this worked for me:

cd to a repo dir you do not want to remove
git push git@server:path_to_repo.git :branch_you_want_to_delete

If you think about it, this is the same as:

git push origin :branch_you_want_delete

Except you are replacing the reference origin with the explicit path