#!/usr/bin/python
#
# Copyright (C) 2012-2024 Internet Systems Consortium, Inc. ("ISC")
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

#
# This script lists obsolete (fully merged) branches. It is useful for periodic
# maintenance of our GIT tree.
#
# It is good idea to use following command before running this script:
#
# git pull
# git remote prune origin
#
# This script requires python 3.
#
# I have limited experience in Python. If things are done in a strange or
# uncommon way, there are no obscure reasons to do it that way, just plain
# lack of experience.
#
#                                                                        tomek

import argparse

# [B404:blacklist] Consider possible security implications associated with subprocess module.
import subprocess  # nosec B404


class Branch:
    MERGED = 1
    NOTMERGED = 2
    name = None
    status = NOTMERGED
    last_commit = None


def branch_list_get(verbose):
    """ Generates a list of available remote branches and
        checks their status (merged/unmerged). A branch is merged
        if all changes on that branch are also on master. """

    # call git branch -r (list of remote branches)
    txt_list = check_output(["git", "branch", "-r"])

    txt_list = txt_list.split(b"\n")

    # we will store list of suitable branches here
    out = []
    for branch in txt_list:
        # skip empty lines
        if len(branch) == 0:
            continue

        # skip branches that are aliases (something -> something_else)
        if branch.find(b"->") != -1:
            continue

        # don't complain about master
        if branch == b"origin/master":
            continue

        branch_info = Branch()

        # get branch name
        branch_info.name = branch.strip(b" ")
        branch_info.name = branch_info.name.decode("utf-8")

        # check if branch is merged or not
        if verbose:
            print("Checking branch %s" % branch_info.name)

        # get a diff with changes that are on that branch only
        # i.e. all unmerged code.
        # Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
        cmd = ["git", "diff", "master..." + branch_info.name]
        diff = check_output(cmd)
        if len(diff) == 0:
            # No diff? Then all changes from that branch are on master as well.
            branch_info.status = Branch.MERGED

            # let's get the last contributor with extra formatting
            # see man git-log and search for PRETTY FORMATS.
            # %ai = date, %ae = author e-mail, %an = author name
            cmd = ["git", "log", "-n", "1", "--pretty=\"%ai,%ae,%an\"", branch_info.name]
            # Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted
            #        input.
            offender = check_output(cmd)
            offender = offender.strip(b"\n\"")

            # comment out this 2 lines to disable obfuscation
            offender = offender.replace(b"@", b"(at)")
            # Obfuscating a dot does not work too well for folks that use
            # initials
            # offender = offender.replace(b".", b"(dot)")

            branch_info.last_commit = offender.decode("utf-8")

        else:
            # diff is not empty, so there is something to merge
            branch_info.status = Branch.NOTMERGED

        out.append(branch_info)
    return out


def branch_print(branches, csv, print_merged, print_notmerged, print_stats):
    """ prints out list of branches with specified details (using
        human-readable (or CSV) format. It is possible to specify,
        which branches should be printed (merged, notmerged) and
        also print out summary statistics """

    # counters used for statistics
    merged = 0
    notmerged = 0

    # compact list of merged/notmerged branches
    merged_str = ""
    notmerged_str = ""
    for branch in branches:
        if branch.status == Branch.MERGED:
            merged = merged + 1
            if not print_merged:
                continue
            if csv:
                print("%s,merged,%s" % (branch.name, branch.last_commit))
            else:
                merged_str = merged_str + " " + branch.name
        else:
            # NOT MERGED
            notmerged = notmerged + 1
            if not print_notmerged:
                continue
            if csv:
                print("%s,notmerged,%s" % (branch.name, branch.last_commit))
            else:
                notmerged_str = notmerged_str + " " + branch.name

    if not csv:
        if print_merged:
            print("Merged branches    : %s" % (merged_str))
        if print_notmerged:
            print("NOT merged branches: %s" % (notmerged_str))

    if print_stats:
        print("#----------")
        print("#Merged    : %d" % merged)
        print("#Not merged: %d" % notmerged)


def check_output(cmd):
    # Issue: [B603:subprocess_without_shell_equals_true] subprocess call - check for execution of untrusted input.
    return subprocess.check_output(cmd)  # nosec B603


def parse_args():
    parser = argparse.ArgumentParser(
        description="This script prints out merged and/or unmerged branches of a GIT tree.",
        usage="""%prog
    Lists all obsolete (fully merged into master) branches.
""")

    parser.add_argument("-c", "--csv", action="store_true",
                        default=False, help="generates CSV output")
    parser.add_argument("-u", "--unmerged", action="store_true",
                        default=False, help="lists unmerged branches")
    parser.add_argument("-m", "--skip-merged", action="store_true",
                        default=False, help="omits listing merged branches")
    parser.add_argument("-s", "--stats", action="store_true",
                        default=False, help="prints also statistics")

    return parser.parse_args()


def main():
    args = parse_args()
    csv = args.csv
    merged = not args.skip_merged
    unmerged = args.unmerged
    stats = args.stats

    if csv:
        print("branch name,status,date,last commit(mail),last commit(name)")

    branch_list = branch_list_get(not csv)

    branch_print(branch_list, csv, merged, unmerged, stats)


if __name__ == '__main__':
    main()
