Skip to content

Instantly share code, notes, and snippets.

@jderusse
Last active August 29, 2015 14:10
Show Gist options
  • Save jderusse/ad8094ab40a81d4656cd to your computer and use it in GitHub Desktop.
Save jderusse/ad8094ab40a81d4656cd to your computer and use it in GitHub Desktop.
git summary
#!/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