This is Ruby script I found way back and was using it since.

The credits for creation goes to Zach Dennis. The script is decribed in his blog post

The source is here

Example

This is beginning of very long output from Groovy source

Groovy source

The code

#!/usr/bin/env ruby

require 'rubygems'

gem 'term-ansicolor', '>=1.0.5'
require 'term/ansicolor'

class GitCommit
  attr_reader :content

  def initialize(content)
    @content = content
  end

  def sha
    @content.split[1]
  end

  def to_s
    `git log --pretty=format:"%h %ad %an - %s" #{sha}~1..#{sha}`
  end

  def unmerged?
    content =~ /^\+/
  end

  def equivalent?
    content =~ /^\-/
  end
end

class GitBranch
  attr_reader :name, :commits

  def initialize(name, commits)
    @name = name
    @commits = commits
  end

  # Returns origin from origin/some/branch/here
  def repository
    name.split("/", 2).first
  end

  # Returns some/branch/here from origin/some/branch/here
  def branch_name
    name.split("/", 2).last
  end

  def unmerged_commits
    commits.select{ |commit| commit.unmerged? }
  end

  def equivalent_commits
    commits.select{ |commit| commit.equivalent? }
  end

end

class GitBranches < Array
  def self.clean_branch_output(str)
    str.split(/\n/).map{ |e| e.strip.gsub(/\*\s+/, '') }.reject{ |branch| branch =~ /\b#{Regexp.escape(UPSTREAM)}\b/ }.sort
  end

  def self.local_branches
    clean_branch_output `git branch`
  end

  def self.remote_branches
    clean_branch_output `git branch -r`
  end

  def self.load(options)
    git_branches = new
    branches = if options[:local]
      local_branches
    elsif options[:remote]
      remote_branches
    end

    branches.each do |branch|
      raw_commits = `git cherry -v #{UPSTREAM} #{branch}`.split(/\n/).map{ |c| GitCommit.new(c) }
      git_branches << GitBranch.new(branch, raw_commits)
    end
    git_branches
  end

  def unmerged
    reject{ |branch| branch.commits.empty? }.sort_by{ |branch| branch.name }
  end

  def any_missing_commits?
    select{ |branch| branch.commits.any? }.any?
  end
end

class GitUnmerged
  VERSION = "1.1"

  include Term::ANSIColor

  attr_reader :branches

  def initialize(args)
    @options = {}
    @branches_to_prune = []
    extract_options_from_args(args)
  end

  def load
    @branches ||= GitBranches.load(:local => local?, :remote => remote?)
    @branches.reject!{|b| @options[:exclude].include?(b.name)} if @options[:exclude].is_a?(Array)
    @branches.select!{|b| @options[:only].include?(b.name)} if @options[:only].is_a?(Array)
  end

  def print_overview
    load
    if @options[:exclude] && @options[:exclude].length > 0
      puts "The following branches have been excluded"
      @options[:exclude].each do |branch_name|
        puts "  #{branch_name}"
      end
      puts
    end

    if @options[:only] && @options[:only].length > 0
      puts "The following branches will be compared against:"
      @options[:only].each do |branch_name|
        puts "  #{branch_name}"
      end
      puts
    end

    if branches.any_missing_commits?
      puts "The following branches possibly have commits not merged to #{upstream}:"
      branches.each do |branch|
        num_unmerged = yellow(branch.unmerged_commits.size.to_s)
        num_equivalent = green(branch.equivalent_commits.size.to_s)
        puts %|  #{branch.name} (#{num_unmerged}/#{num_equivalent} commits)|
      end
    end
  end

  def print_help
    puts <<-EOT.gsub(/^\s+\|/, '')
      |Usage: #{$0} [-a] [--upstream <branch>] [--remote] [--prune]
      |
      |This script wraps the "git cherry" command. It reports the commits from all local
      |branches which have not been merged into an upstream branch.
      |
      |  #{yellow("yellow")} commits have not been merged
      |  #{green("green")} commits have equivalent changes in <upstream> but different SHAs
      |
      |The default upstream is 'master' (or 'origin/master' if running with --remote)
      |
      |OPTIONS:
      |  -a   display all unmerged commits (verbose)
      |  --remote (-r)  compare remote branches instead of local branches
      |  --upstream <branch>   specify a specific upstream branch (defaults to master)
      |  --exclude <branch>[,<branch>,...]   specify a comma-separated list of branches to exclude
      |  --only <branch>[,<branch>,...]   specify a comma-separated list of branches to include
      |  --prune  prompts user to delete branches which have no differences with the upstream
      |
      |EXAMPLE: check for all unmerged commits
      |  #{$0}
      |
      |EXAMPLE: check for all unmerged commits and merged commits (but with a different SHA)
      |  #{$0} -a
      |
      |EXAMPLE: use a different upstream than master
      |  #{$0} --upstream otherbranch
      |
      |EXAMPLE: compare remote branches against origin/master
      |  #{$0} --remote (-r)
      |
      |EXAMPLE: delete branches without unmerged commits
      |  #{$0} --prune
      |
      |EXAMPLE: delete remote branches without unmerged commits
      |  #{$0} --remote --prune
      |
      |GITCONFIG:
      |  If you name this file git-unmerged and place it somewhere in your PATH
      |  you will be able to type "git unmerged" to use it. If you'd like to name
      |  it something else and still refer to it with "git unmerged" then you'll
      |  need to set up an alias:
      |      git config --global alias.unmerged \\!#{$0}
      |
      |Version: #{VERSION}
      |Author: Zach Dennis <zdennis@mutuallyhuman.com>
    EOT
    exit
  end

  def print_version
    puts "#{VERSION}"
  end

  def branch_description
    local? ? "local" : "remote"
  end

  def print_specifics
    load

    if branches.any_missing_commits?
      print_breakdown
    else
      puts "There are no #{branch_description} branches out of sync with #{upstream}"
    end
  end

  def print_breakdown
    puts "Below is a breakdown for each branch. Here's a legend:"
    puts
    print_legend
    branches.each do |branch|
      puts
      print "#{branch.name}:"
      if branch.unmerged_commits.empty? && !show_equivalent_commits?
        print "(no unmerged commits"
        if prune?
          print ",", red(" this will be pruned")
          @branches_to_prune << branch
        end
        print ")\n"
      else
        puts
      end
      branch.unmerged_commits.each { |commit| puts yellow(commit.to_s) }

      if show_equivalent_commits?
        branch.equivalent_commits.each do |commit|
          puts green(commit.to_s)
        end
      end
    end
  end

  def print_legend
    load
    puts "  " + yellow("yellow") + " commits have not been merged"
    puts "  " + green("green") + " commits have equivalent changes in #{UPSTREAM} but different SHAs" if show_equivalent_commits?
  end

  def prune
    return unless prune?
    if @branches_to_prune.empty?
      puts "", "There are no branches to prune."
    else
      # Protects Heroku repo's
      rejected_master_branches = @branches_to_prune.reject!{|branch| branch.branch_name =~ /master/}
      puts "", "Are you sure you want to prune the following #{@branches_to_prune.size} branches?", ""
      puts red("(Keep in mind this will remove these branches from the remote repository)") if @remote
      @branches_to_prune.each do |branch|
        puts red(" #{branch.name}")
      end
      puts "(omitting branches named master)" if rejected_master_branches
      puts
      print "y or n: "
      if STDIN.gets=~/y/i
        @branches_to_prune.each do |branch|
          if local?
            `git branch -D #{branch.name}`
          elsif remote?
            puts "pruning: #{branch.branch_name} with 'git push #{branch.repository} :#{branch.branch_name}'"
            `git push #{branch.repository} :#{branch.branch_name}`
          end
        end
        puts "", "Pruned #{@branches_to_prune.size} branches."
      else
        puts "", "Pruning aborted. All branches were left unharmed."
      end
    end
  end

  def prune? ; @options[:prune ] ; end
  def show_help? ; @options[:show_help] ; end
  def show_equivalent_commits? ; @options[:show_equivalent_commits] ; end
  def show_version? ; @options[:show_version] ; end

  def upstream
    if @options[:upstream]
      @options[:upstream]
    elsif local?
      "master"
    elsif remote?
      "origin/master"
    end
  end

  private

  def extract_options_from_args(args)
    if args.include?("--remote") || args.include?("-r")
      @options[:remote] = true
    else
      @options[:local] = true
    end

    @options[:prune] = true if args.include?("--prune")
    @options[:show_help] = true if args.include?("-h") || args.include?("--help")
    @options[:show_equivalent_commits] = true if args.include?("-a")
    @options[:show_version] = true if args.include?("-v") || args.include?("--version")

    if index=args.index("--upstream")
      @options[:upstream] = args[index+1]
    end

    if index=args.index("--exclude")
      @options[:exclude] = args[index+1].split(',')
    end

    if index=args.index("--only")
      @options[:only] = args[index+1].split(',')
    end
  end

  def local? ; @options[:local] ; end
  def remote? ; @options[:remote] ; end
end


unmerged = GitUnmerged.new ARGV
UPSTREAM = unmerged.upstream
if unmerged.show_help?
  unmerged.print_help
  exit
elsif unmerged.show_version?
  unmerged.print_version
  exit
else
  unmerged.print_overview
  puts
  unmerged.print_specifics
  unmerged.prune
end