mirror of
https://github.com/facebook/sapling.git
synced 2024-10-16 19:57:18 +03:00
353 lines
12 KiB
Python
Executable File
353 lines
12 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# Copyright 2004-present Facebook. All rights reserved.
|
|
#
|
|
# Emulate the output of smartlog.py atop git, instead of Mercurial.
|
|
#
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import print_function
|
|
from __future__ import unicode_literals
|
|
|
|
import argparse
|
|
import re
|
|
import subprocess
|
|
import time
|
|
|
|
seconds_in_a_day = 60.0 * 60.0 * 24.0
|
|
|
|
class ColorOutput(object):
|
|
colors = {
|
|
'black': 90,
|
|
'red': 91,
|
|
'green': 92,
|
|
'yellow': 93,
|
|
'blue': 94,
|
|
'pink': 95,
|
|
'cyan': 96,
|
|
'under': 4,
|
|
'none': 0
|
|
}
|
|
str_pattern_color = '\33[%dm'
|
|
|
|
@classmethod
|
|
def str(cls, color, text):
|
|
return (cls.str_pattern_color % cls.colors[color]) + str(text) + \
|
|
(cls.str_pattern_color % cls.colors['none'])
|
|
|
|
class GitRevision(object):
|
|
revisionmap = {}
|
|
root = None
|
|
current_branch = None
|
|
show_all = False
|
|
origin_timestamp_threshold = 0
|
|
ignored_message = None
|
|
|
|
# ref can be a hash or a branch_name
|
|
def __init__(self, ref, hash=None):
|
|
self.hash = hash if hash is not None else git_rev_parse(ref)
|
|
self.ref = [ref]
|
|
self.ancestors = []
|
|
self.predecessors = []
|
|
self.father = self
|
|
self.other_fathers = set()
|
|
self.children = []
|
|
self.author = "AwesomeAuthor"
|
|
self.longref = "Awesome Description"
|
|
self.commit_timestamp = 0
|
|
self.between_me_and_father = []
|
|
self.evaluated = set()
|
|
self.real_father = False
|
|
self.me_head = False
|
|
self.small_hash = ""
|
|
self.get_my_info()
|
|
global origin_timestamp_threshold
|
|
self.use_me = (GitRevision.show_all or
|
|
self.commit_timestamp > GitRevision.origin_timestamp_threshold)
|
|
|
|
@classmethod
|
|
def make_unique(cls, ref, hash=None):
|
|
hash = hash if hash is not None else git_rev_parse(ref)
|
|
if hash in cls.revisionmap:
|
|
cls.revisionmap[hash].ref += [ref]
|
|
else:
|
|
cls.revisionmap[hash] = cls(ref, hash=hash)
|
|
return cls.revisionmap[hash]
|
|
|
|
@classmethod
|
|
def build_revmap(cls, reflist):
|
|
hashes = git_rev_parse_many(reflist)
|
|
for (hash, ref) in zip(hashes, reflist):
|
|
cls.make_unique(ref, hash=hash)
|
|
cls.remove_old_revs()
|
|
|
|
@staticmethod
|
|
def set_hashes(revs):
|
|
return set([x.hash for x in revs])
|
|
|
|
@classmethod
|
|
def print_ignored_if_any(cls):
|
|
if cls.ignored_message is not None:
|
|
print(cls.ignored_message)
|
|
|
|
@classmethod
|
|
def remove_old_revs(cls):
|
|
new_revisionmap = {}
|
|
ignored = []
|
|
for hash, rev in cls.revisionmap.iteritems():
|
|
if rev.use_me:
|
|
new_revisionmap[hash] = rev
|
|
else:
|
|
ignored.append(
|
|
"ignored: %s %s (%s) [%.1f days old] %s" % (
|
|
rev.small_hash,
|
|
rev.author,
|
|
", ".join(rev.ref),
|
|
(time.time() - rev.commit_timestamp) / seconds_in_a_day,
|
|
rev.longref)
|
|
)
|
|
cls.revisionmap = new_revisionmap
|
|
cls.ignored_message = "\n".join(ignored)
|
|
|
|
@classmethod
|
|
def get_rev(cls, rev_hash):
|
|
if rev_hash not in cls.revisionmap:
|
|
cls.make_unique(rev_hash)
|
|
return cls.revisionmap[rev_hash]
|
|
|
|
@classmethod
|
|
def prepare(cls, to_be_prepared=None):
|
|
refs = cls.revisionmap.keys()
|
|
all_rev = cls.revisionmap.values()
|
|
will_prepare = to_be_prepared or all_rev
|
|
for rev in will_prepare:
|
|
for rev_s in all_rev:
|
|
rev.eval_rev(rev_s)
|
|
newrefs = set(cls.revisionmap.keys()) - set(refs)
|
|
if len(newrefs) > 0:
|
|
cls.prepare(to_be_prepared=[cls.get_rev(x) for x in newrefs])
|
|
if to_be_prepared is None:
|
|
cls.find_parents_and_children()
|
|
head_hash = git_rev_parse("HEAD")
|
|
cls.get_rev(head_hash).me_head = True
|
|
|
|
@classmethod
|
|
def find_parents_and_children(cls):
|
|
for rev in cls.revisionmap.values():
|
|
rev.normalize()
|
|
for rev in cls.revisionmap.values():
|
|
rev.find_my_children()
|
|
for rev in cls.revisionmap.values():
|
|
if rev.father == rev:
|
|
cls.root = rev
|
|
for rev in cls.revisionmap.values():
|
|
key=(lambda x: time.time() if "master" in x.ref else
|
|
x.newest_timestamp())
|
|
rev.children = sorted(
|
|
rev.children,
|
|
key = key
|
|
)
|
|
|
|
def newest_timestamp(self):
|
|
ts = self.commit_timestamp
|
|
for ch in self.children:
|
|
ts = max(ts, ch.commit_timestamp)
|
|
return ts
|
|
|
|
def normalize(self):
|
|
self.ancestors = list(
|
|
dict([(x.hash, x) for x in self.ancestors]).values()
|
|
)
|
|
self.predecessors = list(
|
|
dict([(x.hash, x) for x in self.predecessors]).values()
|
|
)
|
|
|
|
def find_my_children(self):
|
|
my_ancestors_hashes = GitRevision.set_hashes(self.ancestors)
|
|
my_predecessors_hashes = GitRevision.set_hashes(self.predecessors)
|
|
for child in self.predecessors:
|
|
|
|
# / OTHER_A
|
|
# AN \ / - PARALLEL_A ----------\
|
|
# AN - ME OTHER
|
|
# AN / \ - PARALLEL_B - CHILD --/
|
|
# BR_A ------- BR_B -----/ \ OTHER_B
|
|
#
|
|
# if PARALLEL_B == {} => CHILD is child
|
|
#
|
|
#
|
|
#
|
|
# CHILD.predecessors = OTHER | OTHER_B
|
|
# CHILD.ancestors = AN | ME | PARALLEL_B | BR_A | BR_B
|
|
# ME.ancestors = AN | BR_A
|
|
# nodes := CHILD.ancestors - ME.ancestors - ME = PARALLEL_B | BR_B
|
|
# nodes & ME.predecessors = PARALLEL_B
|
|
|
|
# all operations are NlogN with very low const
|
|
child_anc = GitRevision.set_hashes(child.ancestors)
|
|
nodes = child_anc - my_ancestors_hashes
|
|
nodes = nodes - set([self.hash])
|
|
parallel_b = nodes & my_predecessors_hashes
|
|
if len(parallel_b) == 0:
|
|
# if there is no one that is between me and my child, then
|
|
# it's a 'direct child'
|
|
self.children.append(child)
|
|
direct_fathers = set(get_parents(child.hash))
|
|
fathers = child.other_fathers
|
|
if child.hash != child.father.hash:
|
|
fathers|= set([child.father.hash])
|
|
fathers|= set([self.hash])
|
|
real_fathers = fathers & direct_fathers
|
|
if len(real_fathers) > 0:
|
|
father_hash = real_fathers.pop()
|
|
child.other_fathers = fathers - set([father_hash])
|
|
child.father = GitRevision.get_rev(father_hash)
|
|
child.real_father = True
|
|
else:
|
|
father_hash = fathers.pop()
|
|
child.other_fathers = fathers - set([father_hash])
|
|
child.father = GitRevision.get_rev(father_hash)
|
|
child.real_father = False
|
|
|
|
def is_same_hash(self, hash_str):
|
|
len_hash = min(len(self.hash), len(hash_str))
|
|
if self.hash[:len_hash] == hash_str[:len_hash]:
|
|
return True
|
|
return False
|
|
|
|
def eval_rev(self, revision):
|
|
if revision.hash == self.hash:
|
|
return
|
|
if revision.hash in self.evaluated:
|
|
return
|
|
self.evaluated.add(revision.hash)
|
|
revision.evaluated.add(self.hash)
|
|
ancestor = git_common_ancestor(self.hash, revision.hash)
|
|
if self.is_same_hash(ancestor):
|
|
self.predecessors.append(revision)
|
|
revision.ancestors.append(self)
|
|
elif revision.is_same_hash(ancestor):
|
|
self.ancestors.append(revision)
|
|
revision.predecessors.append(self)
|
|
else:
|
|
anc_rev = GitRevision.get_rev(ancestor)
|
|
anc_rev.eval_rev(self)
|
|
anc_rev.eval_rev(revision)
|
|
|
|
def __str__(self):
|
|
return '\n'.join(reversed(list(self.tree())))
|
|
|
|
def tree(self):
|
|
yield " " if self.root != self else "|"
|
|
for line in self.twolines_ref():
|
|
yield line
|
|
last = self.children[-1] if len(self.children) > 0 else None
|
|
for child in self.children:
|
|
if child.father is not self:
|
|
# this guy is a child of someone else too. let he print it
|
|
continue
|
|
prefix = '|' if child == last else '|/'
|
|
for line in child.tree():
|
|
yield prefix + line
|
|
prefix = '' if child == last else '| '
|
|
|
|
def twolines_ref(self):
|
|
bullet = "@ " if self.me_head else "o "
|
|
text = self.small_hash + " " + self.author
|
|
if self.me_head:
|
|
lines = bullet + ColorOutput.str("green", text)
|
|
longref = ColorOutput.str("green", self.longref)
|
|
else:
|
|
lines = bullet + text
|
|
longref = self.longref
|
|
if self.ref != self.hash:
|
|
refv = [
|
|
x + "*" if x == GitRevision.current_branch else x
|
|
for x in self.ref
|
|
]
|
|
text = " (" + ", ".join(refv) + ")"
|
|
if self.me_head:
|
|
lines += ColorOutput.str("yellow", text)
|
|
else:
|
|
lines += ColorOutput.str("blue", text)
|
|
if self.real_father:
|
|
trace = "| "
|
|
else:
|
|
trace = ": "
|
|
if len(self.other_fathers) == 0:
|
|
return [trace + longref, lines]
|
|
else:
|
|
other = [GitRevision.get_rev(x).small_hash for x in
|
|
self.other_fathers]
|
|
other = ColorOutput.str("cyan", "Also son of: " + ", ".join(other))
|
|
return [trace + other, trace + longref, lines]
|
|
|
|
def get_my_info(self):
|
|
[name, longref, committime, small_hash] = get_info(self.hash)
|
|
self.author = name
|
|
self.longref = longref
|
|
self.commit_timestamp = int(committime)
|
|
self.small_hash = small_hash
|
|
|
|
def run_cmd(cmd, swallow_error=False):
|
|
p = subprocess.Popen(["git"] + cmd,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
out, err = p.communicate()
|
|
if p.returncode != 0:
|
|
if swallow_error:
|
|
return '', ''
|
|
print(
|
|
"git %s returned code %d:\nstdout:\n%s\nstderr:\n%s" %
|
|
(cmd, p.returncode, out, err)
|
|
)
|
|
exit(p.returncode)
|
|
return out, err
|
|
|
|
def get_info(revref):
|
|
out, _ = run_cmd(["log", revref, "--format=%ae%x00%s%x00%ct%x00%h", "-n1"])
|
|
result = out.split('\n')[0].split("\x00")
|
|
result[0] = result[0].split('@')[0]
|
|
return result
|
|
|
|
def get_parents(rev):
|
|
out, _ = run_cmd(["log", "--pretty=%P", "-n1", rev])
|
|
return out.split()
|
|
|
|
def git_rev_parse_many(refs):
|
|
out, _ = run_cmd(["rev-parse"] + refs)
|
|
return out.split()
|
|
|
|
def git_rev_parse(ref):
|
|
return git_rev_parse_many([ref])[0]
|
|
|
|
# Git Branch (git branch)
|
|
def git_branch_names():
|
|
out, _ = run_cmd(["branch"])
|
|
reg = r"\*\s*(\w*)"
|
|
GitRevision.current_branch = re.search(reg, out).groups()[0]
|
|
return list(set(out.split()) - set(["*"]))
|
|
|
|
def git_common_ancestor(branch1, branch2):
|
|
out, _ = run_cmd(["merge-base", branch1, branch2], swallow_error=True)
|
|
return out.strip()
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(description='Git Smart Log - prints a '
|
|
'tree representation of your branches')
|
|
parser.add_argument('-a', '--all', action='store_true',
|
|
help='By default it will only look into features not older than 2 weeks'
|
|
', if this is not what you want and you don\'t care about waiting '
|
|
'for 10 minutes, use this')
|
|
# defaults for
|
|
parser.add_argument('-t', '--time', metavar='FLOAT_TIME_IN_DAYS',
|
|
default=str(14), help='time in days to look for features.')
|
|
gitsl_args = parser.parse_args()
|
|
GitRevision.show_all = gitsl_args.all
|
|
GitRevision.origin_timestamp_threshold = (time.time() -
|
|
float(gitsl_args.time) * seconds_in_a_day)
|
|
GitRevision.build_revmap(git_branch_names() + ["HEAD"])
|
|
GitRevision.prepare()
|
|
print(GitRevision.root)
|
|
GitRevision.print_ignored_if_any()
|