2017-06-15 14:02:58 +03:00
|
|
|
# obsutil.py - utility functions for obsolescence
|
|
|
|
#
|
|
|
|
# Copyright 2017 Boris Feld <boris.feld@octobus.net>
|
|
|
|
#
|
|
|
|
# This software may be used and distributed according to the terms of the
|
|
|
|
# GNU General Public License version 2 or any later version.
|
|
|
|
|
|
|
|
from __future__ import absolute_import
|
|
|
|
|
2017-07-06 15:58:44 +03:00
|
|
|
import re
|
|
|
|
|
2017-06-28 04:54:19 +03:00
|
|
|
from . import (
|
|
|
|
phases,
|
2017-08-02 20:20:59 +03:00
|
|
|
util
|
2017-06-28 04:54:19 +03:00
|
|
|
)
|
|
|
|
|
2017-06-27 02:51:40 +03:00
|
|
|
class marker(object):
|
|
|
|
"""Wrap obsolete marker raw data"""
|
|
|
|
|
|
|
|
def __init__(self, repo, data):
|
|
|
|
# the repo argument will be used to create changectx in later version
|
|
|
|
self._repo = repo
|
|
|
|
self._data = data
|
|
|
|
self._decodedmeta = None
|
|
|
|
|
|
|
|
def __hash__(self):
|
|
|
|
return hash(self._data)
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
if type(other) != type(self):
|
|
|
|
return False
|
|
|
|
return self._data == other._data
|
|
|
|
|
|
|
|
def precnode(self):
|
2017-08-02 20:20:59 +03:00
|
|
|
msg = ("'marker.precnode' is deprecated, "
|
2017-08-16 11:18:57 +03:00
|
|
|
"use 'marker.prednode'")
|
2017-08-02 20:20:59 +03:00
|
|
|
util.nouideprecwarn(msg, '4.4')
|
|
|
|
return self.prednode()
|
|
|
|
|
|
|
|
def prednode(self):
|
|
|
|
"""Predecessor changeset node identifier"""
|
2017-06-27 02:51:40 +03:00
|
|
|
return self._data[0]
|
|
|
|
|
|
|
|
def succnodes(self):
|
|
|
|
"""List of successor changesets node identifiers"""
|
|
|
|
return self._data[1]
|
|
|
|
|
|
|
|
def parentnodes(self):
|
2017-08-02 20:20:59 +03:00
|
|
|
"""Parents of the predecessors (None if not recorded)"""
|
2017-06-27 02:51:40 +03:00
|
|
|
return self._data[5]
|
|
|
|
|
|
|
|
def metadata(self):
|
|
|
|
"""Decoded metadata dictionary"""
|
|
|
|
return dict(self._data[3])
|
|
|
|
|
|
|
|
def date(self):
|
|
|
|
"""Creation date as (unixtime, offset)"""
|
|
|
|
return self._data[4]
|
|
|
|
|
|
|
|
def flags(self):
|
|
|
|
"""The flags field of the marker"""
|
|
|
|
return self._data[2]
|
|
|
|
|
2017-06-27 03:06:15 +03:00
|
|
|
def getmarkers(repo, nodes=None, exclusive=False):
|
|
|
|
"""returns markers known in a repository
|
|
|
|
|
|
|
|
If <nodes> is specified, only markers "relevant" to those nodes are are
|
|
|
|
returned"""
|
|
|
|
if nodes is None:
|
|
|
|
rawmarkers = repo.obsstore
|
|
|
|
elif exclusive:
|
|
|
|
rawmarkers = exclusivemarkers(repo, nodes)
|
|
|
|
else:
|
|
|
|
rawmarkers = repo.obsstore.relevantmarkers(nodes)
|
|
|
|
|
|
|
|
for markerdata in rawmarkers:
|
|
|
|
yield marker(repo, markerdata)
|
|
|
|
|
2017-06-15 14:02:58 +03:00
|
|
|
def closestpredecessors(repo, nodeid):
|
|
|
|
"""yield the list of next predecessors pointing on visible changectx nodes
|
|
|
|
|
|
|
|
This function respect the repoview filtering, filtered revision will be
|
|
|
|
considered missing.
|
|
|
|
"""
|
|
|
|
|
2017-08-02 20:39:08 +03:00
|
|
|
precursors = repo.obsstore.predecessors
|
2017-06-15 14:02:58 +03:00
|
|
|
stack = [nodeid]
|
|
|
|
seen = set(stack)
|
|
|
|
|
|
|
|
while stack:
|
|
|
|
current = stack.pop()
|
|
|
|
currentpreccs = precursors.get(current, ())
|
|
|
|
|
|
|
|
for prec in currentpreccs:
|
|
|
|
precnodeid = prec[0]
|
|
|
|
|
|
|
|
# Basic cycle protection
|
|
|
|
if precnodeid in seen:
|
|
|
|
continue
|
|
|
|
seen.add(precnodeid)
|
|
|
|
|
|
|
|
if precnodeid in repo:
|
|
|
|
yield precnodeid
|
|
|
|
else:
|
|
|
|
stack.append(precnodeid)
|
2017-06-27 02:03:01 +03:00
|
|
|
|
2017-08-02 20:49:57 +03:00
|
|
|
def allprecursors(*args, **kwargs):
|
|
|
|
""" (DEPRECATED)
|
|
|
|
"""
|
|
|
|
msg = ("'obsutil.allprecursors' is deprecated, "
|
|
|
|
"use 'obsutil.allpredecessors'")
|
|
|
|
util.nouideprecwarn(msg, '4.4')
|
|
|
|
|
|
|
|
return allpredecessors(*args, **kwargs)
|
|
|
|
|
|
|
|
def allpredecessors(obsstore, nodes, ignoreflags=0):
|
2017-06-27 02:31:18 +03:00
|
|
|
"""Yield node for every precursors of <nodes>.
|
|
|
|
|
|
|
|
Some precursors may be unknown locally.
|
|
|
|
|
|
|
|
This is a linear yield unsuited to detecting folded changesets. It includes
|
|
|
|
initial nodes too."""
|
|
|
|
|
|
|
|
remaining = set(nodes)
|
|
|
|
seen = set(remaining)
|
|
|
|
while remaining:
|
|
|
|
current = remaining.pop()
|
|
|
|
yield current
|
2017-08-02 20:39:08 +03:00
|
|
|
for mark in obsstore.predecessors.get(current, ()):
|
2017-06-27 02:31:18 +03:00
|
|
|
# ignore marker flagged with specified flag
|
|
|
|
if mark[2] & ignoreflags:
|
|
|
|
continue
|
|
|
|
suc = mark[0]
|
|
|
|
if suc not in seen:
|
|
|
|
seen.add(suc)
|
|
|
|
remaining.add(suc)
|
2017-06-27 02:36:20 +03:00
|
|
|
|
|
|
|
def allsuccessors(obsstore, nodes, ignoreflags=0):
|
|
|
|
"""Yield node for every successor of <nodes>.
|
|
|
|
|
|
|
|
Some successors may be unknown locally.
|
|
|
|
|
|
|
|
This is a linear yield unsuited to detecting split changesets. It includes
|
|
|
|
initial nodes too."""
|
|
|
|
remaining = set(nodes)
|
|
|
|
seen = set(remaining)
|
|
|
|
while remaining:
|
|
|
|
current = remaining.pop()
|
|
|
|
yield current
|
|
|
|
for mark in obsstore.successors.get(current, ()):
|
|
|
|
# ignore marker flagged with specified flag
|
|
|
|
if mark[2] & ignoreflags:
|
|
|
|
continue
|
|
|
|
for suc in mark[1]:
|
|
|
|
if suc not in seen:
|
|
|
|
seen.add(suc)
|
|
|
|
remaining.add(suc)
|
2017-06-27 02:31:18 +03:00
|
|
|
|
2017-06-27 02:11:56 +03:00
|
|
|
def _filterprunes(markers):
|
|
|
|
"""return a set with no prune markers"""
|
|
|
|
return set(m for m in markers if m[1])
|
|
|
|
|
|
|
|
def exclusivemarkers(repo, nodes):
|
|
|
|
"""set of markers relevant to "nodes" but no other locally-known nodes
|
|
|
|
|
|
|
|
This function compute the set of markers "exclusive" to a locally-known
|
|
|
|
node. This means we walk the markers starting from <nodes> until we reach a
|
|
|
|
locally-known precursors outside of <nodes>. Element of <nodes> with
|
|
|
|
locally-known successors outside of <nodes> are ignored (since their
|
|
|
|
precursors markers are also relevant to these successors).
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
|
|
|
# (A0 rewritten as A1)
|
|
|
|
#
|
|
|
|
# A0 <-1- A1 # Marker "1" is exclusive to A1
|
|
|
|
|
|
|
|
or
|
|
|
|
|
|
|
|
# (A0 rewritten as AX; AX rewritten as A1; AX is unkown locally)
|
|
|
|
#
|
|
|
|
# <-1- A0 <-2- AX <-3- A1 # Marker "2,3" are exclusive to A1
|
|
|
|
|
|
|
|
or
|
|
|
|
|
|
|
|
# (A0 has unknown precursors, A0 rewritten as A1 and A2 (divergence))
|
|
|
|
#
|
|
|
|
# <-2- A1 # Marker "2" is exclusive to A0,A1
|
|
|
|
# /
|
|
|
|
# <-1- A0
|
|
|
|
# \
|
|
|
|
# <-3- A2 # Marker "3" is exclusive to A0,A2
|
|
|
|
#
|
|
|
|
# in addition:
|
|
|
|
#
|
|
|
|
# Markers "2,3" are exclusive to A1,A2
|
|
|
|
# Markers "1,2,3" are exclusive to A0,A1,A2
|
|
|
|
|
|
|
|
See test/test-obsolete-bundle-strip.t for more examples.
|
|
|
|
|
|
|
|
An example usage is strip. When stripping a changeset, we also want to
|
|
|
|
strip the markers exclusive to this changeset. Otherwise we would have
|
|
|
|
"dangling"" obsolescence markers from its precursors: Obsolescence markers
|
|
|
|
marking a node as obsolete without any successors available locally.
|
|
|
|
|
|
|
|
As for relevant markers, the prune markers for children will be followed.
|
|
|
|
Of course, they will only be followed if the pruned children is
|
|
|
|
locally-known. Since the prune markers are relevant to the pruned node.
|
|
|
|
However, while prune markers are considered relevant to the parent of the
|
|
|
|
pruned changesets, prune markers for locally-known changeset (with no
|
|
|
|
successors) are considered exclusive to the pruned nodes. This allows
|
|
|
|
to strip the prune markers (with the rest of the exclusive chain) alongside
|
|
|
|
the pruned changesets.
|
|
|
|
"""
|
|
|
|
# running on a filtered repository would be dangerous as markers could be
|
|
|
|
# reported as exclusive when they are relevant for other filtered nodes.
|
|
|
|
unfi = repo.unfiltered()
|
|
|
|
|
|
|
|
# shortcut to various useful item
|
|
|
|
nm = unfi.changelog.nodemap
|
2017-08-02 20:39:08 +03:00
|
|
|
precursorsmarkers = unfi.obsstore.predecessors
|
2017-06-27 02:11:56 +03:00
|
|
|
successormarkers = unfi.obsstore.successors
|
|
|
|
childrenmarkers = unfi.obsstore.children
|
|
|
|
|
|
|
|
# exclusive markers (return of the function)
|
|
|
|
exclmarkers = set()
|
|
|
|
# we need fast membership testing
|
|
|
|
nodes = set(nodes)
|
|
|
|
# looking for head in the obshistory
|
|
|
|
#
|
|
|
|
# XXX we are ignoring all issues in regard with cycle for now.
|
|
|
|
stack = [n for n in nodes if not _filterprunes(successormarkers.get(n, ()))]
|
|
|
|
stack.sort()
|
|
|
|
# nodes already stacked
|
|
|
|
seennodes = set(stack)
|
|
|
|
while stack:
|
|
|
|
current = stack.pop()
|
|
|
|
# fetch precursors markers
|
|
|
|
markers = list(precursorsmarkers.get(current, ()))
|
|
|
|
# extend the list with prune markers
|
|
|
|
for mark in successormarkers.get(current, ()):
|
|
|
|
if not mark[1]:
|
|
|
|
markers.append(mark)
|
|
|
|
# and markers from children (looking for prune)
|
|
|
|
for mark in childrenmarkers.get(current, ()):
|
|
|
|
if not mark[1]:
|
|
|
|
markers.append(mark)
|
|
|
|
# traverse the markers
|
|
|
|
for mark in markers:
|
|
|
|
if mark in exclmarkers:
|
|
|
|
# markers already selected
|
|
|
|
continue
|
|
|
|
|
|
|
|
# If the markers is about the current node, select it
|
|
|
|
#
|
|
|
|
# (this delay the addition of markers from children)
|
|
|
|
if mark[1] or mark[0] == current:
|
|
|
|
exclmarkers.add(mark)
|
|
|
|
|
|
|
|
# should we keep traversing through the precursors?
|
|
|
|
prec = mark[0]
|
|
|
|
|
|
|
|
# nodes in the stack or already processed
|
|
|
|
if prec in seennodes:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# is this a locally known node ?
|
|
|
|
known = prec in nm
|
|
|
|
# if locally-known and not in the <nodes> set the traversal
|
|
|
|
# stop here.
|
|
|
|
if known and prec not in nodes:
|
|
|
|
continue
|
|
|
|
|
|
|
|
# do not keep going if there are unselected markers pointing to this
|
|
|
|
# nodes. If we end up traversing these unselected markers later the
|
|
|
|
# node will be taken care of at that point.
|
|
|
|
precmarkers = _filterprunes(successormarkers.get(prec))
|
|
|
|
if precmarkers.issubset(exclmarkers):
|
|
|
|
seennodes.add(prec)
|
|
|
|
stack.append(prec)
|
|
|
|
|
|
|
|
return exclmarkers
|
|
|
|
|
2017-06-27 02:40:34 +03:00
|
|
|
def foreground(repo, nodes):
|
|
|
|
"""return all nodes in the "foreground" of other node
|
|
|
|
|
|
|
|
The foreground of a revision is anything reachable using parent -> children
|
|
|
|
or precursor -> successor relation. It is very similar to "descendant" but
|
|
|
|
augmented with obsolescence information.
|
|
|
|
|
|
|
|
Beware that possible obsolescence cycle may result if complex situation.
|
|
|
|
"""
|
|
|
|
repo = repo.unfiltered()
|
|
|
|
foreground = set(repo.set('%ln::', nodes))
|
|
|
|
if repo.obsstore:
|
|
|
|
# We only need this complicated logic if there is obsolescence
|
|
|
|
# XXX will probably deserve an optimised revset.
|
|
|
|
nm = repo.changelog.nodemap
|
|
|
|
plen = -1
|
|
|
|
# compute the whole set of successors or descendants
|
|
|
|
while len(foreground) != plen:
|
|
|
|
plen = len(foreground)
|
|
|
|
succs = set(c.node() for c in foreground)
|
|
|
|
mutable = [c.node() for c in foreground if c.mutable()]
|
|
|
|
succs.update(allsuccessors(repo.obsstore, mutable))
|
|
|
|
known = (n for n in succs if n in nm)
|
|
|
|
foreground = set(repo.set('%ln::', known))
|
|
|
|
return set(c.node() for c in foreground)
|
|
|
|
|
2017-08-24 19:40:30 +03:00
|
|
|
# effectflag field
|
|
|
|
#
|
|
|
|
# Effect-flag is a 1-byte bit field used to store what changed between a
|
|
|
|
# changeset and its successor(s).
|
|
|
|
#
|
|
|
|
# The effect flag is stored in obs-markers metadata while we iterate on the
|
|
|
|
# information design. That's why we have the EFFECTFLAGFIELD. If we come up
|
|
|
|
# with an incompatible design for effect flag, we can store a new design under
|
|
|
|
# another field name so we don't break readers. We plan to extend the existing
|
|
|
|
# obsmarkers bit-field when the effect flag design will be stabilized.
|
|
|
|
#
|
|
|
|
# The effect-flag is placed behind an experimental flag
|
|
|
|
# `effect-flags` set to off by default.
|
|
|
|
#
|
|
|
|
|
effectflag: store an empty effect flag for the moment
The idea behind effect flag is to store additional information in obs-markers
about what changed between a changeset and its successor(s). It's a low-level
information that comes without guarantees.
This information can be computed a posteriori, but only if we have all
changesets locally. This is not the case with distributed workflows where you
work with several people or on several computers (eg: laptop + build server).
Storing the effect-flag as a bitfield has several advantages:
- It's compact, we are using one byte per obs-marker at most for the effect-
flag.
- It's compoundable, the obsfate log approach needs to display evolve history
that could spans several obs-markers. Computing the effect-flag between a
changeset and its grand-grand-grand-successor is simple thanks to the
bitfield.
The effect-flag design has also some limitations:
- Evolving a changeset and reverting these changes just after would lead to
two obs-markers with the same effect-flag without information that the first
and third changesets are the same.
The effect-flag current design is a trade-off between compactness and
usefulness.
Storing this information helps commands to display a more complete and
understandable evolve history. For example, obslog (an Evolve command) use it
to improve its output:
x 62206adfd571 (34302) obscache: skip updating outdated obscache...
| rewritten(parent) by Matthieu Laneuville <matthieu.laneuville@octobus...
| rewritten(content) by Boris Feld <boris.feld@octobus.net>
The effect flag is stored in obs-markers metadata while we iterate on the
information we want to store. We plan to extend the existing obsmarkers
bit-field when the effect flag design will be stabilized.
It's different from the CommitCustody concept, effect-flag are not signed and
can be forged. It's also different from the operation metadata as the command
name (for example: amend) could alter a changeset in different ways (changing
the content with hg amend, changing the description with hg amend -e, changing
the user with hg amend -U). Also it's compatible with every custom command
that writes obs-markers without needing to be updated.
The effect-flag is placed behind an experimental flag set to off by default.
Hook the saving of effect flag in create markers, but store only an empty one
for the moment, I will refine the values in effect flag in following patches.
For more information, see:
https://www.mercurial-scm.org/wiki/ChangesetEvolutionDevel#Record_types_of_operation
Differential Revision: https://phab.mercurial-scm.org/D533
2017-07-06 15:50:17 +03:00
|
|
|
EFFECTFLAGFIELD = "ef1"
|
|
|
|
|
2017-07-06 15:52:34 +03:00
|
|
|
DESCCHANGED = 1 << 0 # action changed the description
|
2017-07-06 15:58:44 +03:00
|
|
|
METACHANGED = 1 << 1 # action change the meta
|
2017-07-06 16:00:07 +03:00
|
|
|
DIFFCHANGED = 1 << 3 # action change diff introduced by the changeset
|
2017-07-06 15:56:16 +03:00
|
|
|
PARENTCHANGED = 1 << 2 # action change the parent
|
2017-07-06 15:53:48 +03:00
|
|
|
USERCHANGED = 1 << 4 # the user changed
|
2017-07-06 15:54:22 +03:00
|
|
|
DATECHANGED = 1 << 5 # the date changed
|
2017-07-06 15:55:12 +03:00
|
|
|
BRANCHCHANGED = 1 << 6 # the branch changed
|
2017-07-06 15:52:34 +03:00
|
|
|
|
2017-07-06 15:58:44 +03:00
|
|
|
METABLACKLIST = [
|
|
|
|
re.compile('^branch$'),
|
|
|
|
re.compile('^.*-source$'),
|
|
|
|
re.compile('^.*_source$'),
|
|
|
|
re.compile('^source$'),
|
|
|
|
]
|
|
|
|
|
|
|
|
def metanotblacklisted(metaitem):
|
|
|
|
""" Check that the key of a meta item (extrakey, extravalue) does not
|
|
|
|
match at least one of the blacklist pattern
|
|
|
|
"""
|
|
|
|
metakey = metaitem[0]
|
|
|
|
|
|
|
|
return not any(pattern.match(metakey) for pattern in METABLACKLIST)
|
|
|
|
|
2017-07-06 16:00:07 +03:00
|
|
|
def _prepare_hunk(hunk):
|
|
|
|
"""Drop all information but the username and patch"""
|
|
|
|
cleanhunk = []
|
|
|
|
for line in hunk.splitlines():
|
|
|
|
if line.startswith(b'# User') or not line.startswith(b'#'):
|
|
|
|
if line.startswith(b'@@'):
|
|
|
|
line = b'@@\n'
|
|
|
|
cleanhunk.append(line)
|
|
|
|
return cleanhunk
|
|
|
|
|
|
|
|
def _getdifflines(iterdiff):
|
|
|
|
"""return a cleaned up lines"""
|
|
|
|
lines = next(iterdiff, None)
|
|
|
|
|
|
|
|
if lines is None:
|
|
|
|
return lines
|
|
|
|
|
|
|
|
return _prepare_hunk(lines)
|
|
|
|
|
|
|
|
def _cmpdiff(leftctx, rightctx):
|
|
|
|
"""return True if both ctx introduce the "same diff"
|
|
|
|
|
|
|
|
This is a first and basic implementation, with many shortcoming.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Leftctx or right ctx might be filtered, so we need to use the contexts
|
|
|
|
# with an unfiltered repository to safely compute the diff
|
|
|
|
leftunfi = leftctx._repo.unfiltered()[leftctx.rev()]
|
|
|
|
leftdiff = leftunfi.diff(git=1)
|
|
|
|
rightunfi = rightctx._repo.unfiltered()[rightctx.rev()]
|
|
|
|
rightdiff = rightunfi.diff(git=1)
|
|
|
|
|
|
|
|
left, right = (0, 0)
|
|
|
|
while None not in (left, right):
|
|
|
|
left = _getdifflines(leftdiff)
|
|
|
|
right = _getdifflines(rightdiff)
|
|
|
|
|
|
|
|
if left != right:
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
effectflag: store an empty effect flag for the moment
The idea behind effect flag is to store additional information in obs-markers
about what changed between a changeset and its successor(s). It's a low-level
information that comes without guarantees.
This information can be computed a posteriori, but only if we have all
changesets locally. This is not the case with distributed workflows where you
work with several people or on several computers (eg: laptop + build server).
Storing the effect-flag as a bitfield has several advantages:
- It's compact, we are using one byte per obs-marker at most for the effect-
flag.
- It's compoundable, the obsfate log approach needs to display evolve history
that could spans several obs-markers. Computing the effect-flag between a
changeset and its grand-grand-grand-successor is simple thanks to the
bitfield.
The effect-flag design has also some limitations:
- Evolving a changeset and reverting these changes just after would lead to
two obs-markers with the same effect-flag without information that the first
and third changesets are the same.
The effect-flag current design is a trade-off between compactness and
usefulness.
Storing this information helps commands to display a more complete and
understandable evolve history. For example, obslog (an Evolve command) use it
to improve its output:
x 62206adfd571 (34302) obscache: skip updating outdated obscache...
| rewritten(parent) by Matthieu Laneuville <matthieu.laneuville@octobus...
| rewritten(content) by Boris Feld <boris.feld@octobus.net>
The effect flag is stored in obs-markers metadata while we iterate on the
information we want to store. We plan to extend the existing obsmarkers
bit-field when the effect flag design will be stabilized.
It's different from the CommitCustody concept, effect-flag are not signed and
can be forged. It's also different from the operation metadata as the command
name (for example: amend) could alter a changeset in different ways (changing
the content with hg amend, changing the description with hg amend -e, changing
the user with hg amend -U). Also it's compatible with every custom command
that writes obs-markers without needing to be updated.
The effect-flag is placed behind an experimental flag set to off by default.
Hook the saving of effect flag in create markers, but store only an empty one
for the moment, I will refine the values in effect flag in following patches.
For more information, see:
https://www.mercurial-scm.org/wiki/ChangesetEvolutionDevel#Record_types_of_operation
Differential Revision: https://phab.mercurial-scm.org/D533
2017-07-06 15:50:17 +03:00
|
|
|
def geteffectflag(relation):
|
|
|
|
""" From an obs-marker relation, compute what changed between the
|
|
|
|
predecessor and the successor.
|
|
|
|
"""
|
|
|
|
effects = 0
|
|
|
|
|
|
|
|
source = relation[0]
|
|
|
|
|
2017-07-06 15:52:34 +03:00
|
|
|
for changectx in relation[1]:
|
|
|
|
# Check if description has changed
|
|
|
|
if changectx.description() != source.description():
|
|
|
|
effects |= DESCCHANGED
|
|
|
|
|
2017-07-06 15:53:48 +03:00
|
|
|
# Check if user has changed
|
|
|
|
if changectx.user() != source.user():
|
|
|
|
effects |= USERCHANGED
|
|
|
|
|
2017-07-06 15:54:22 +03:00
|
|
|
# Check if date has changed
|
|
|
|
if changectx.date() != source.date():
|
|
|
|
effects |= DATECHANGED
|
|
|
|
|
2017-07-06 15:55:12 +03:00
|
|
|
# Check if branch has changed
|
|
|
|
if changectx.branch() != source.branch():
|
|
|
|
effects |= BRANCHCHANGED
|
|
|
|
|
2017-07-06 15:56:16 +03:00
|
|
|
# Check if at least one of the parent has changed
|
|
|
|
if changectx.parents() != source.parents():
|
|
|
|
effects |= PARENTCHANGED
|
|
|
|
|
2017-07-06 15:58:44 +03:00
|
|
|
# Check if other meta has changed
|
|
|
|
changeextra = changectx.extra().items()
|
|
|
|
ctxmeta = filter(metanotblacklisted, changeextra)
|
|
|
|
|
|
|
|
sourceextra = source.extra().items()
|
|
|
|
srcmeta = filter(metanotblacklisted, sourceextra)
|
|
|
|
|
|
|
|
if ctxmeta != srcmeta:
|
|
|
|
effects |= METACHANGED
|
|
|
|
|
2017-07-06 16:00:07 +03:00
|
|
|
# Check if the diff has changed
|
|
|
|
if not _cmpdiff(source, changectx):
|
|
|
|
effects |= DIFFCHANGED
|
|
|
|
|
effectflag: store an empty effect flag for the moment
The idea behind effect flag is to store additional information in obs-markers
about what changed between a changeset and its successor(s). It's a low-level
information that comes without guarantees.
This information can be computed a posteriori, but only if we have all
changesets locally. This is not the case with distributed workflows where you
work with several people or on several computers (eg: laptop + build server).
Storing the effect-flag as a bitfield has several advantages:
- It's compact, we are using one byte per obs-marker at most for the effect-
flag.
- It's compoundable, the obsfate log approach needs to display evolve history
that could spans several obs-markers. Computing the effect-flag between a
changeset and its grand-grand-grand-successor is simple thanks to the
bitfield.
The effect-flag design has also some limitations:
- Evolving a changeset and reverting these changes just after would lead to
two obs-markers with the same effect-flag without information that the first
and third changesets are the same.
The effect-flag current design is a trade-off between compactness and
usefulness.
Storing this information helps commands to display a more complete and
understandable evolve history. For example, obslog (an Evolve command) use it
to improve its output:
x 62206adfd571 (34302) obscache: skip updating outdated obscache...
| rewritten(parent) by Matthieu Laneuville <matthieu.laneuville@octobus...
| rewritten(content) by Boris Feld <boris.feld@octobus.net>
The effect flag is stored in obs-markers metadata while we iterate on the
information we want to store. We plan to extend the existing obsmarkers
bit-field when the effect flag design will be stabilized.
It's different from the CommitCustody concept, effect-flag are not signed and
can be forged. It's also different from the operation metadata as the command
name (for example: amend) could alter a changeset in different ways (changing
the content with hg amend, changing the description with hg amend -e, changing
the user with hg amend -U). Also it's compatible with every custom command
that writes obs-markers without needing to be updated.
The effect-flag is placed behind an experimental flag set to off by default.
Hook the saving of effect flag in create markers, but store only an empty one
for the moment, I will refine the values in effect flag in following patches.
For more information, see:
https://www.mercurial-scm.org/wiki/ChangesetEvolutionDevel#Record_types_of_operation
Differential Revision: https://phab.mercurial-scm.org/D533
2017-07-06 15:50:17 +03:00
|
|
|
return effects
|
|
|
|
|
2017-06-28 04:54:19 +03:00
|
|
|
def getobsoleted(repo, tr):
|
|
|
|
"""return the set of pre-existing revisions obsoleted by a transaction"""
|
|
|
|
torev = repo.unfiltered().changelog.nodemap.get
|
|
|
|
phase = repo._phasecache.phase
|
|
|
|
succsmarkers = repo.obsstore.successors.get
|
|
|
|
public = phases.public
|
|
|
|
addedmarkers = tr.changes.get('obsmarkers')
|
|
|
|
addedrevs = tr.changes.get('revs')
|
|
|
|
seenrevs = set(addedrevs)
|
|
|
|
obsoleted = set()
|
|
|
|
for mark in addedmarkers:
|
|
|
|
node = mark[0]
|
|
|
|
rev = torev(node)
|
|
|
|
if rev is None or rev in seenrevs:
|
|
|
|
continue
|
|
|
|
seenrevs.add(rev)
|
|
|
|
if phase(repo, rev) == public:
|
|
|
|
continue
|
2017-07-24 18:29:51 +03:00
|
|
|
if set(succsmarkers(node) or []).issubset(addedmarkers):
|
2017-06-28 04:54:19 +03:00
|
|
|
obsoleted.add(rev)
|
|
|
|
return obsoleted
|
|
|
|
|
2017-07-03 01:53:55 +03:00
|
|
|
class _succs(list):
|
|
|
|
"""small class to represent a successors with some metadata about it"""
|
|
|
|
|
2017-07-03 04:27:58 +03:00
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
super(_succs, self).__init__(*args, **kwargs)
|
|
|
|
self.markers = set()
|
|
|
|
|
2017-07-03 04:13:17 +03:00
|
|
|
def copy(self):
|
2017-07-03 04:27:58 +03:00
|
|
|
new = _succs(self)
|
|
|
|
new.markers = self.markers.copy()
|
|
|
|
return new
|
2017-07-03 04:13:17 +03:00
|
|
|
|
2017-07-03 04:54:24 +03:00
|
|
|
@util.propertycache
|
|
|
|
def _set(self):
|
|
|
|
# immutable
|
|
|
|
return set(self)
|
|
|
|
|
|
|
|
def canmerge(self, other):
|
|
|
|
return self._set.issubset(other._set)
|
|
|
|
|
2017-06-30 16:27:19 +03:00
|
|
|
def successorssets(repo, initialnode, closest=False, cache=None):
|
2017-06-27 02:03:01 +03:00
|
|
|
"""Return set of all latest successors of initial nodes
|
|
|
|
|
|
|
|
The successors set of a changeset A are the group of revisions that succeed
|
|
|
|
A. It succeeds A as a consistent whole, each revision being only a partial
|
2017-06-30 16:27:19 +03:00
|
|
|
replacement. By default, the successors set contains non-obsolete
|
|
|
|
changesets only, walking the obsolescence graph until reaching a leaf. If
|
|
|
|
'closest' is set to True, closest successors-sets are return (the
|
|
|
|
obsolescence walk stops on known changesets).
|
2017-06-27 02:03:01 +03:00
|
|
|
|
|
|
|
This function returns the full list of successor sets which is why it
|
|
|
|
returns a list of tuples and not just a single tuple. Each tuple is a valid
|
|
|
|
successors set. Note that (A,) may be a valid successors set for changeset A
|
|
|
|
(see below).
|
|
|
|
|
|
|
|
In most cases, a changeset A will have a single element (e.g. the changeset
|
|
|
|
A is replaced by A') in its successors set. Though, it is also common for a
|
|
|
|
changeset A to have no elements in its successor set (e.g. the changeset
|
|
|
|
has been pruned). Therefore, the returned list of successors sets will be
|
|
|
|
[(A',)] or [], respectively.
|
|
|
|
|
|
|
|
When a changeset A is split into A' and B', however, it will result in a
|
|
|
|
successors set containing more than a single element, i.e. [(A',B')].
|
|
|
|
Divergent changesets will result in multiple successors sets, i.e. [(A',),
|
|
|
|
(A'')].
|
|
|
|
|
|
|
|
If a changeset A is not obsolete, then it will conceptually have no
|
|
|
|
successors set. To distinguish this from a pruned changeset, the successor
|
|
|
|
set will contain itself only, i.e. [(A,)].
|
|
|
|
|
2017-06-30 14:47:24 +03:00
|
|
|
Finally, final successors unknown locally are considered to be pruned
|
|
|
|
(pruned: obsoleted without any successors). (Final: successors not affected
|
|
|
|
by markers).
|
|
|
|
|
2017-06-30 16:27:19 +03:00
|
|
|
The 'closest' mode respect the repoview filtering. For example, without
|
|
|
|
filter it will stop at the first locally known changeset, with 'visible'
|
|
|
|
filter it will stop on visible changesets).
|
|
|
|
|
2017-06-30 14:47:24 +03:00
|
|
|
The optional `cache` parameter is a dictionary that may contains
|
|
|
|
precomputed successors sets. It is meant to reuse the computation of a
|
|
|
|
previous call to `successorssets` when multiple calls are made at the same
|
|
|
|
time. The cache dictionary is updated in place. The caller is responsible
|
|
|
|
for its life span. Code that makes multiple calls to `successorssets`
|
|
|
|
*should* use this cache mechanism or risk a performance hit.
|
2017-06-30 16:27:19 +03:00
|
|
|
|
|
|
|
Since results are different depending of the 'closest' most, the same cache
|
|
|
|
cannot be reused for both mode.
|
2017-06-27 02:03:01 +03:00
|
|
|
"""
|
|
|
|
|
|
|
|
succmarkers = repo.obsstore.successors
|
|
|
|
|
|
|
|
# Stack of nodes we search successors sets for
|
|
|
|
toproceed = [initialnode]
|
|
|
|
# set version of above list for fast loop detection
|
|
|
|
# element added to "toproceed" must be added here
|
|
|
|
stackedset = set(toproceed)
|
|
|
|
if cache is None:
|
|
|
|
cache = {}
|
|
|
|
|
|
|
|
# This while loop is the flattened version of a recursive search for
|
|
|
|
# successors sets
|
|
|
|
#
|
|
|
|
# def successorssets(x):
|
|
|
|
# successors = directsuccessors(x)
|
|
|
|
# ss = [[]]
|
|
|
|
# for succ in directsuccessors(x):
|
|
|
|
# # product as in itertools cartesian product
|
|
|
|
# ss = product(ss, successorssets(succ))
|
|
|
|
# return ss
|
|
|
|
#
|
|
|
|
# But we can not use plain recursive calls here:
|
|
|
|
# - that would blow the python call stack
|
|
|
|
# - obsolescence markers may have cycles, we need to handle them.
|
|
|
|
#
|
|
|
|
# The `toproceed` list act as our call stack. Every node we search
|
|
|
|
# successors set for are stacked there.
|
|
|
|
#
|
|
|
|
# The `stackedset` is set version of this stack used to check if a node is
|
|
|
|
# already stacked. This check is used to detect cycles and prevent infinite
|
|
|
|
# loop.
|
|
|
|
#
|
|
|
|
# successors set of all nodes are stored in the `cache` dictionary.
|
|
|
|
#
|
|
|
|
# After this while loop ends we use the cache to return the successors sets
|
|
|
|
# for the node requested by the caller.
|
|
|
|
while toproceed:
|
|
|
|
# Every iteration tries to compute the successors sets of the topmost
|
|
|
|
# node of the stack: CURRENT.
|
|
|
|
#
|
|
|
|
# There are four possible outcomes:
|
|
|
|
#
|
|
|
|
# 1) We already know the successors sets of CURRENT:
|
|
|
|
# -> mission accomplished, pop it from the stack.
|
2017-06-30 16:27:19 +03:00
|
|
|
# 2) Stop the walk:
|
|
|
|
# default case: Node is not obsolete
|
|
|
|
# closest case: Node is known at this repo filter level
|
|
|
|
# -> the node is its own successors sets. Add it to the cache.
|
2017-06-27 02:03:01 +03:00
|
|
|
# 3) We do not know successors set of direct successors of CURRENT:
|
|
|
|
# -> We add those successors to the stack.
|
|
|
|
# 4) We know successors sets of all direct successors of CURRENT:
|
|
|
|
# -> We can compute CURRENT successors set and add it to the
|
|
|
|
# cache.
|
|
|
|
#
|
|
|
|
current = toproceed[-1]
|
2017-06-30 16:27:19 +03:00
|
|
|
|
|
|
|
# case 2 condition is a bit hairy because of closest,
|
|
|
|
# we compute it on its own
|
|
|
|
case2condition = ((current not in succmarkers)
|
|
|
|
or (closest and current != initialnode
|
|
|
|
and current in repo))
|
|
|
|
|
2017-06-27 02:03:01 +03:00
|
|
|
if current in cache:
|
|
|
|
# case (1): We already know the successors sets
|
|
|
|
stackedset.remove(toproceed.pop())
|
2017-06-30 16:27:19 +03:00
|
|
|
elif case2condition:
|
|
|
|
# case (2): end of walk.
|
2017-06-27 02:03:01 +03:00
|
|
|
if current in repo:
|
2017-06-30 16:27:19 +03:00
|
|
|
# We have a valid successors.
|
2017-07-03 01:53:55 +03:00
|
|
|
cache[current] = [_succs((current,))]
|
2017-06-27 02:03:01 +03:00
|
|
|
else:
|
|
|
|
# Final obsolete version is unknown locally.
|
|
|
|
# Do not count that as a valid successors
|
|
|
|
cache[current] = []
|
|
|
|
else:
|
|
|
|
# cases (3) and (4)
|
|
|
|
#
|
|
|
|
# We proceed in two phases. Phase 1 aims to distinguish case (3)
|
|
|
|
# from case (4):
|
|
|
|
#
|
|
|
|
# For each direct successors of CURRENT, we check whether its
|
|
|
|
# successors sets are known. If they are not, we stack the
|
|
|
|
# unknown node and proceed to the next iteration of the while
|
|
|
|
# loop. (case 3)
|
|
|
|
#
|
|
|
|
# During this step, we may detect obsolescence cycles: a node
|
|
|
|
# with unknown successors sets but already in the call stack.
|
|
|
|
# In such a situation, we arbitrary set the successors sets of
|
|
|
|
# the node to nothing (node pruned) to break the cycle.
|
|
|
|
#
|
|
|
|
# If no break was encountered we proceed to phase 2.
|
|
|
|
#
|
|
|
|
# Phase 2 computes successors sets of CURRENT (case 4); see details
|
|
|
|
# in phase 2 itself.
|
|
|
|
#
|
|
|
|
# Note the two levels of iteration in each phase.
|
|
|
|
# - The first one handles obsolescence markers using CURRENT as
|
|
|
|
# precursor (successors markers of CURRENT).
|
|
|
|
#
|
|
|
|
# Having multiple entry here means divergence.
|
|
|
|
#
|
|
|
|
# - The second one handles successors defined in each marker.
|
|
|
|
#
|
|
|
|
# Having none means pruned node, multiple successors means split,
|
|
|
|
# single successors are standard replacement.
|
|
|
|
#
|
|
|
|
for mark in sorted(succmarkers[current]):
|
|
|
|
for suc in mark[1]:
|
|
|
|
if suc not in cache:
|
|
|
|
if suc in stackedset:
|
|
|
|
# cycle breaking
|
|
|
|
cache[suc] = []
|
|
|
|
else:
|
|
|
|
# case (3) If we have not computed successors sets
|
|
|
|
# of one of those successors we add it to the
|
|
|
|
# `toproceed` stack and stop all work for this
|
|
|
|
# iteration.
|
|
|
|
toproceed.append(suc)
|
|
|
|
stackedset.add(suc)
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
# case (4): we know all successors sets of all direct
|
|
|
|
# successors
|
|
|
|
#
|
|
|
|
# Successors set contributed by each marker depends on the
|
|
|
|
# successors sets of all its "successors" node.
|
|
|
|
#
|
|
|
|
# Each different marker is a divergence in the obsolescence
|
|
|
|
# history. It contributes successors sets distinct from other
|
|
|
|
# markers.
|
|
|
|
#
|
|
|
|
# Within a marker, a successor may have divergent successors
|
|
|
|
# sets. In such a case, the marker will contribute multiple
|
|
|
|
# divergent successors sets. If multiple successors have
|
|
|
|
# divergent successors sets, a Cartesian product is used.
|
|
|
|
#
|
|
|
|
# At the end we post-process successors sets to remove
|
|
|
|
# duplicated entry and successors set that are strict subset of
|
|
|
|
# another one.
|
|
|
|
succssets = []
|
|
|
|
for mark in sorted(succmarkers[current]):
|
|
|
|
# successors sets contributed by this marker
|
2017-07-03 04:27:58 +03:00
|
|
|
base = _succs()
|
|
|
|
base.markers.add(mark)
|
|
|
|
markss = [base]
|
2017-06-27 02:03:01 +03:00
|
|
|
for suc in mark[1]:
|
|
|
|
# cardinal product with previous successors
|
|
|
|
productresult = []
|
|
|
|
for prefix in markss:
|
|
|
|
for suffix in cache[suc]:
|
2017-07-03 04:13:17 +03:00
|
|
|
newss = prefix.copy()
|
2017-07-03 04:27:58 +03:00
|
|
|
newss.markers.update(suffix.markers)
|
2017-06-27 02:03:01 +03:00
|
|
|
for part in suffix:
|
|
|
|
# do not duplicated entry in successors set
|
|
|
|
# first entry wins.
|
|
|
|
if part not in newss:
|
|
|
|
newss.append(part)
|
|
|
|
productresult.append(newss)
|
|
|
|
markss = productresult
|
|
|
|
succssets.extend(markss)
|
|
|
|
# remove duplicated and subset
|
|
|
|
seen = []
|
|
|
|
final = []
|
2017-07-03 04:56:53 +03:00
|
|
|
candidates = sorted((s for s in succssets if s),
|
|
|
|
key=len, reverse=True)
|
|
|
|
for cand in candidates:
|
2017-07-03 04:54:24 +03:00
|
|
|
for seensuccs in seen:
|
|
|
|
if cand.canmerge(seensuccs):
|
|
|
|
seensuccs.markers.update(cand.markers)
|
2017-06-27 02:03:01 +03:00
|
|
|
break
|
|
|
|
else:
|
2017-07-03 04:54:24 +03:00
|
|
|
final.append(cand)
|
|
|
|
seen.append(cand)
|
2017-06-27 02:03:01 +03:00
|
|
|
final.reverse() # put small successors set first
|
|
|
|
cache[current] = final
|
|
|
|
return cache[initialnode]
|
2017-08-17 19:26:11 +03:00
|
|
|
|
|
|
|
def successorsandmarkers(repo, ctx):
|
|
|
|
"""compute the raw data needed for computing obsfate
|
|
|
|
Returns a list of dict, one dict per successors set
|
|
|
|
"""
|
|
|
|
if not ctx.obsolete():
|
|
|
|
return None
|
|
|
|
|
|
|
|
ssets = successorssets(repo, ctx.node(), closest=True)
|
|
|
|
|
2017-07-03 18:38:56 +03:00
|
|
|
# closestsuccessors returns an empty list for pruned revisions, remap it
|
|
|
|
# into a list containing an empty list for future processing
|
|
|
|
if ssets == []:
|
|
|
|
ssets = [[]]
|
|
|
|
|
|
|
|
# Try to recover pruned markers
|
|
|
|
succsmap = repo.obsstore.successors
|
|
|
|
fullsuccessorsets = [] # successor set + markers
|
2017-08-17 19:26:11 +03:00
|
|
|
for sset in ssets:
|
2017-07-03 18:38:56 +03:00
|
|
|
if sset:
|
|
|
|
fullsuccessorsets.append(sset)
|
|
|
|
else:
|
|
|
|
# successorsset return an empty set() when ctx or one of its
|
|
|
|
# successors is pruned.
|
|
|
|
# In this case, walk the obs-markers tree again starting with ctx
|
|
|
|
# and find the relevant pruning obs-makers, the ones without
|
|
|
|
# successors.
|
|
|
|
# Having these markers allow us to compute some information about
|
|
|
|
# its fate, like who pruned this changeset and when.
|
|
|
|
|
|
|
|
# XXX we do not catch all prune markers (eg rewritten then pruned)
|
|
|
|
# (fix me later)
|
|
|
|
foundany = False
|
|
|
|
for mark in succsmap.get(ctx.node(), ()):
|
|
|
|
if not mark[1]:
|
|
|
|
foundany = True
|
|
|
|
sset = _succs()
|
|
|
|
sset.markers.add(mark)
|
|
|
|
fullsuccessorsets.append(sset)
|
|
|
|
if not foundany:
|
|
|
|
fullsuccessorsets.append(_succs())
|
|
|
|
|
|
|
|
values = []
|
|
|
|
for sset in fullsuccessorsets:
|
2017-08-17 19:26:11 +03:00
|
|
|
values.append({'successors': sset, 'markers': sset.markers})
|
|
|
|
|
|
|
|
return values
|
template: compute verb in obsfateverb
Add a template function obsfateverb which use the markers information to
compute a better obsfate verb.
The current logic behind the obsfate verb is simple for the moment:
- If the successorsets is empty, the changeset has been pruned, for example:
Obsfate: pruned
- If the successorsets length is 1, the changeset has been rewritten without
divergence, for example:
Obsfate: rewritten as 2:337fec4d2edc, 3:f257fde29c7a
- If the successorsets length is more than 1, the changeset has diverged, for
example:
Obsfate: split as 2:337fec4d2edc, 3:f257fde29c7a
As the divergence might occurs on a subset of successors, we might see some
successors twice:
Obsfate: split as 9:0b997eb7ceee, 5:dd800401bd8c, 10:eceed8f98ffc; split
as 8:b18bc8331526, 5:dd800401bd8c, 10:eceed8f98ffc
2017-07-03 16:33:27 +03:00
|
|
|
|
2017-10-19 13:35:47 +03:00
|
|
|
def obsfateverb(successorset, markers):
|
|
|
|
""" Return the verb summarizing the successorset and potentially using
|
|
|
|
information from the markers
|
template: compute verb in obsfateverb
Add a template function obsfateverb which use the markers information to
compute a better obsfate verb.
The current logic behind the obsfate verb is simple for the moment:
- If the successorsets is empty, the changeset has been pruned, for example:
Obsfate: pruned
- If the successorsets length is 1, the changeset has been rewritten without
divergence, for example:
Obsfate: rewritten as 2:337fec4d2edc, 3:f257fde29c7a
- If the successorsets length is more than 1, the changeset has diverged, for
example:
Obsfate: split as 2:337fec4d2edc, 3:f257fde29c7a
As the divergence might occurs on a subset of successors, we might see some
successors twice:
Obsfate: split as 9:0b997eb7ceee, 5:dd800401bd8c, 10:eceed8f98ffc; split
as 8:b18bc8331526, 5:dd800401bd8c, 10:eceed8f98ffc
2017-07-03 16:33:27 +03:00
|
|
|
"""
|
|
|
|
if not successorset:
|
|
|
|
verb = 'pruned'
|
|
|
|
elif len(successorset) == 1:
|
|
|
|
verb = 'rewritten'
|
|
|
|
else:
|
|
|
|
verb = 'split'
|
|
|
|
return verb
|
2017-07-03 16:34:00 +03:00
|
|
|
|
2017-07-03 16:34:10 +03:00
|
|
|
def markersdates(markers):
|
|
|
|
"""returns the list of dates for a list of markers
|
|
|
|
"""
|
|
|
|
return [m[4] for m in markers]
|
|
|
|
|
2017-07-03 16:34:00 +03:00
|
|
|
def markersusers(markers):
|
|
|
|
""" Returns a sorted list of markers users without duplicates
|
|
|
|
"""
|
|
|
|
markersmeta = [dict(m[3]) for m in markers]
|
|
|
|
users = set(meta.get('user') for meta in markersmeta if meta.get('user'))
|
|
|
|
|
|
|
|
return sorted(users)
|
2017-09-15 20:43:22 +03:00
|
|
|
|
|
|
|
def markersoperations(markers):
|
|
|
|
""" Returns a sorted list of markers operations without duplicates
|
|
|
|
"""
|
|
|
|
markersmeta = [dict(m[3]) for m in markers]
|
|
|
|
operations = set(meta.get('operation') for meta in markersmeta
|
|
|
|
if meta.get('operation'))
|
|
|
|
|
|
|
|
return sorted(operations)
|
2017-10-05 18:42:56 +03:00
|
|
|
|
|
|
|
def obsfateprinter(successors, markers, ui):
|
|
|
|
""" Build a obsfate string for a single successorset using all obsfate
|
|
|
|
related function defined in obsutil
|
|
|
|
"""
|
2017-10-09 16:34:12 +03:00
|
|
|
quiet = ui.quiet
|
|
|
|
verbose = ui.verbose
|
|
|
|
normal = not verbose and not quiet
|
|
|
|
|
2017-10-05 18:42:56 +03:00
|
|
|
line = []
|
|
|
|
|
|
|
|
# Verb
|
2017-10-19 13:35:47 +03:00
|
|
|
line.append(obsfateverb(successors, markers))
|
2017-10-05 18:42:56 +03:00
|
|
|
|
|
|
|
# Operations
|
|
|
|
operations = markersoperations(markers)
|
|
|
|
if operations:
|
|
|
|
line.append(" using %s" % ", ".join(operations))
|
|
|
|
|
|
|
|
# Successors
|
|
|
|
if successors:
|
|
|
|
fmtsuccessors = [successors.joinfmt(succ) for succ in successors]
|
|
|
|
line.append(" as %s" % ", ".join(fmtsuccessors))
|
|
|
|
|
|
|
|
# Users
|
|
|
|
users = markersusers(markers)
|
2017-10-09 16:34:12 +03:00
|
|
|
# Filter out current user in not verbose mode to reduce amount of
|
|
|
|
# information
|
|
|
|
if not verbose:
|
|
|
|
currentuser = ui.username(acceptempty=True)
|
|
|
|
if len(users) == 1 and currentuser in users:
|
|
|
|
users = None
|
|
|
|
|
|
|
|
if (verbose or normal) and users:
|
2017-10-05 18:42:56 +03:00
|
|
|
line.append(" by %s" % ", ".join(users))
|
|
|
|
|
|
|
|
# Date
|
|
|
|
dates = markersdates(markers)
|
|
|
|
|
2017-10-18 16:38:51 +03:00
|
|
|
if dates and verbose:
|
2017-10-09 16:34:26 +03:00
|
|
|
min_date = min(dates)
|
|
|
|
max_date = max(dates)
|
2017-10-05 18:42:56 +03:00
|
|
|
|
2017-10-09 16:34:26 +03:00
|
|
|
if min_date == max_date:
|
|
|
|
fmtmin_date = util.datestr(min_date, '%Y-%m-%d %H:%M %1%2')
|
|
|
|
line.append(" (at %s)" % fmtmin_date)
|
|
|
|
else:
|
|
|
|
fmtmin_date = util.datestr(min_date, '%Y-%m-%d %H:%M %1%2')
|
|
|
|
fmtmax_date = util.datestr(max_date, '%Y-%m-%d %H:%M %1%2')
|
|
|
|
line.append(" (between %s and %s)" % (fmtmin_date, fmtmax_date))
|
2017-10-05 18:42:56 +03:00
|
|
|
|
|
|
|
return "".join(line)
|