Last active
August 29, 2015 14:10
-
-
Save jderusse/ad8094ab40a81d4656cd to your computer and use it in GitHub Desktop.
git summary
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
import sys | |
import os | |
import subprocess | |
import re | |
import getopt | |
class bcolors: | |
HEADER = '\033[95m' | |
UNTRACK = '\033[94m' | |
WARNING = '\033[93m' | |
OK = '\033[92m' | |
KO = '\033[91m' | |
ENDC = '\033[0m' | |
def run_command(command, cwd=None): | |
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd) | |
while(True): | |
line = process.stdout.readline() | |
if line == '' and process.poll() != None: | |
break | |
yield line | |
def get_repositories(path): | |
for line in run_command(["find", path, "-type", "d", "-name", ".git"]): | |
yield os.path.dirname(line.strip()) | |
def get_branches(repository): | |
for line in run_command(["git", "branch", "-a", "--no-color"], repository): | |
items = re.findall('^(?:\*| ) (.+)', line) | |
if items: | |
yield items[0] | |
def get_group_branches(repository): | |
branches = { | |
"local": [], | |
"remotes": { | |
} | |
} | |
for branch in get_branches(repository): | |
if branch.startswith("remotes/"): | |
(_, remote, branch) = branch.split("/", 2) | |
if " -> " in branch: | |
(branch, _) = branch.split(' -> ', 1) | |
if remote not in branches["remotes"]: | |
branches["remotes"][remote] = [] | |
branches["remotes"][remote].append(branch) | |
else: | |
branches["local"].append(branch) | |
return branches | |
def get_remote_branch_for(local_branch, branches): | |
return [remote for remote, remote_branches in branches.items() if local_branch in remote_branches] | |
def get_branch_stats(repository, local_branch, remote): | |
ahead = 0 | |
behind = 0 | |
for line in run_command(("git", "rev-list", "--left-right", "%s...remotes/%s/%s" % (local_branch, remote, local_branch)), repository): | |
if line.startswith('<'): | |
ahead += 1 | |
elif line.startswith('>'): | |
behind += 1 | |
return (ahead, behind) | |
def uncolor_str(subject): | |
return re.sub('\033\[\d+m', '', subject.decode('utf-8')) | |
def colored_len(subject): | |
return len(uncolor_str(subject)) | |
def pprinttable(rows): | |
if len(rows) > 1: | |
headers = rows[0] | |
lens = [] | |
num_cols = max([len(row) for row in rows if row not in ("---", "")]) | |
for i in range(num_cols): | |
lens.append(max([colored_len(x) for x in [x[i] for x in rows if i < len(x) and x not in ("---", "")]])) | |
formated_headers = [] | |
separator = "-+-".join(['-' * n for n in lens]) | |
for row in rows: | |
if row == "---": | |
print " " + separator | |
elif row == "": | |
print "" | |
else: | |
formated_cells = [] | |
for i in range(len(row)): | |
formated_cells.append("%s%s" % (row[i], " " * (lens[i] - colored_len(row[i])))) | |
print " " + " | ".join(formated_cells) | |
print("") | |
elif len(rows) == 1: | |
row = rows[0] | |
print " " + " | ".join(row) | |
def fetch_repositories(path, verbose): | |
processes = [] | |
for repository in get_repositories(os.getcwd()): | |
branches = get_group_branches(repository) | |
for remote in branches['remotes'].keys(): | |
print("Fetching %s:%s" % (os.path.relpath(repository, os.getcwd()), remote)) | |
processes.append(subprocess.Popen(["git", "fetch", "-p", remote], cwd=repository)) | |
for process in processes: | |
process.communicate() | |
def has_local_changes(repository): | |
diff = 0 | |
for line in run_command(["git", "diff", "--name-status"], repository): | |
if line: | |
diff += 1 | |
for line in run_command(["git", "diff", "--staged", "--name-status"], repository): | |
if line: | |
diff += 1 | |
return diff > 0 | |
def summary_local(repository, branches, verbose): | |
table = [] | |
repo_changes = False | |
for local_branch in branches["local"]: | |
row = ["" for x in branches['remotes']] | |
row.insert(0, local_branch) | |
branche_changes = False | |
for remote in get_remote_branch_for(local_branch, branches['remotes']): | |
(ahead, behind) = get_branch_stats(repository, local_branch, remote) | |
index = branches['remotes'].keys().index(remote) + 1 | |
if ahead + behind == 0: | |
row[index] =bcolors.OK + '✔' + bcolors.ENDC | |
else: | |
row[0] = bcolors.KO + local_branch + bcolors.ENDC | |
str = "" | |
if ahead >0 : | |
str += " " + bcolors.WARNING + "↑%d" % ahead + bcolors.ENDC | |
if behind >0 : | |
str += " " + bcolors.WARNING + "↓%d" % behind + bcolors.ENDC | |
row[index] = str.strip() | |
repo_changes = True | |
branche_changes = True | |
if branche_changes or verbose > 0: | |
table.append(row) | |
if has_local_changes(repository): | |
row = ["" for x in branches['remotes']] | |
row.insert(0, bcolors.KO + "local" + bcolors.ENDC) | |
table.append(row) | |
repo_changes = True | |
return (table, repo_changes) | |
def summary_remote(repository, branches): | |
branches_seen = branches["local"] + ["HEAD"] | |
table = [] | |
unseen_branches = [] | |
for remote, remote_branches in branches['remotes'].items(): | |
for branch in remote_branches: | |
if branch not in branches_seen: | |
unseen_branches.append(branch) | |
branches_seen.append(branch) | |
for remote_branch in unseen_branches: | |
row = ["" for x in branches['remotes']] | |
row.insert(0, bcolors.UNTRACK + remote_branch + bcolors.ENDC) | |
for remote in get_remote_branch_for(remote_branch, branches['remotes']): | |
index = branches['remotes'].keys().index(remote) + 1 | |
row[index] = '⚑' | |
table.append(row) | |
return table | |
def get_summary(repository, verbose): | |
branches = get_group_branches(repository) | |
table = [] | |
headers = [x for x in branches['remotes'].keys()] | |
headers.insert(0, bcolors.HEADER + os.path.relpath(repository, os.getcwd()) + bcolors.ENDC) | |
table.append(headers) | |
table.append("---") | |
table_local, repo_changes = summary_local(repository, branches, verbose) | |
table += sorted(table_local, key=lambda row: uncolor_str(row[0])) | |
if verbose > 2: | |
table_remote = summary_remote(repository, branches) | |
if len(table_remote): | |
table.append("---") | |
table += sorted(table_remote, key=lambda row: uncolor_str(row[0])) | |
return table, repo_changes | |
def summary(repository, verbose): | |
table, repo_changes = get_summary(repository, verbose) | |
if verbose > 1 or repo_changes: | |
pprinttable(table) | |
def usage(): | |
print "Display git summary\n" | |
print 'Usage: '+sys.argv[0]+'[ -f][ -v]' | |
print ' -f: fetch remotes repository' | |
print ' -v: verbose' | |
def main(): | |
try: | |
opts, args = getopt.getopt(sys.argv[1:], "hfv", ["help", "fetch", "verbose"]) | |
except getopt.GetoptError as err: | |
print str(err) # will print something like "option -a not recognized" | |
usage() | |
sys.exit(2) | |
verbose = 0 | |
fetch = False | |
for o, a in opts: | |
if o == "-v": | |
verbose += 1 | |
elif o == "-f": | |
fetch = True | |
elif o in ("-h", "--help"): | |
usage() | |
sys.exit() | |
else: | |
assert False, "unhandled option" | |
if fetch: | |
fetch_repositories(os.getcwd(), verbose) | |
table = [] | |
for repository in get_repositories(os.getcwd()): | |
summary_table, repo_changes = get_summary(repository, verbose) | |
if verbose > 1 or repo_changes: | |
table += summary_table | |
table.append("") | |
pprinttable(table) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment