mirror of
https://github.com/facebook/sapling.git
synced 2024-10-09 00:14:35 +03:00
98d9269874
Summary: Create a fork of the Mercurial code that we can use to build server rpms. The hg servers will continue to exist for a few more months while we move the darkstorm and ediscovery use cases off them. In the mean time, we want to start making breaking changes to the client, so let's create a stable copy of the hg code to produce rpms for the hg servers. The fork is based off c7770c78d, the latest hg release. This copies the files as is, then adds some minor tweaks to get it to build: - Disables some lint checks that appear to be bypassed by path - sed replace eden/scm with eden/hg-server - Removed a dependency on scm/telemetry from the edenfs-client tests since scm/telemetry pulls in the original eden/scm/lib/configparser which conflicts with the hg-server conflict parser. allow-large-files Reviewed By: quark-zju Differential Revision: D27632557 fbshipit-source-id: b2f442f4ec000ea08e4d62de068750832198e1f4
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()
|