1# rewriteutil.py - utility functions for rewriting changesets 2# 3# Copyright 2017 Octobus <contact@octobus.net> 4# 5# This software may be used and distributed according to the terms of the 6# GNU General Public License version 2 or any later version. 7 8from __future__ import absolute_import 9 10import re 11 12from .i18n import _ 13from .node import ( 14 hex, 15 nullrev, 16) 17 18from . import ( 19 error, 20 node, 21 obsolete, 22 obsutil, 23 revset, 24 scmutil, 25 util, 26) 27 28 29NODE_RE = re.compile(br'\b[0-9a-f]{6,64}\b') 30 31 32def _formatrevs(repo, revs, maxrevs=4): 33 """returns a string summarizing revisions in a decent size 34 35 If there are few enough revisions, we list them all. Otherwise we display a 36 summary of the form: 37 38 1ea73414a91b and 5 others 39 """ 40 tonode = repo.changelog.node 41 numrevs = len(revs) 42 if numrevs < maxrevs: 43 shorts = [node.short(tonode(r)) for r in revs] 44 summary = b', '.join(shorts) 45 else: 46 first = revs.first() 47 summary = _(b'%s and %d others') 48 summary %= (node.short(tonode(first)), numrevs - 1) 49 return summary 50 51 52def precheck(repo, revs, action=b'rewrite'): 53 """check if revs can be rewritten 54 action is used to control the error message. 55 56 Make sure this function is called after taking the lock. 57 """ 58 if nullrev in revs: 59 msg = _(b"cannot %s the null revision") % action 60 hint = _(b"no changeset checked out") 61 raise error.InputError(msg, hint=hint) 62 63 if any(util.safehasattr(r, 'rev') for r in revs): 64 repo.ui.develwarn(b"rewriteutil.precheck called with ctx not revs") 65 revs = (r.rev() for r in revs) 66 67 if len(repo[None].parents()) > 1: 68 raise error.StateError( 69 _(b"cannot %s changesets while merging") % action 70 ) 71 72 publicrevs = repo.revs(b'%ld and public()', revs) 73 if publicrevs: 74 summary = _formatrevs(repo, publicrevs) 75 msg = _(b"cannot %s public changesets: %s") % (action, summary) 76 hint = _(b"see 'hg help phases' for details") 77 raise error.InputError(msg, hint=hint) 78 79 newunstable = disallowednewunstable(repo, revs) 80 if newunstable: 81 hint = _(b"see 'hg help evolution.instability'") 82 raise error.InputError( 83 _(b"cannot %s changeset, as that will orphan %d descendants") 84 % (action, len(newunstable)), 85 hint=hint, 86 ) 87 88 if not obsolete.isenabled(repo, obsolete.allowdivergenceopt): 89 new_divergence = _find_new_divergence(repo, revs) 90 if new_divergence: 91 local_ctx, other_ctx, base_ctx = new_divergence 92 msg = _( 93 b'cannot %s %s, as that creates content-divergence with %s' 94 ) % ( 95 action, 96 local_ctx, 97 other_ctx, 98 ) 99 if local_ctx.rev() != base_ctx.rev(): 100 msg += _(b', from %s') % base_ctx 101 if repo.ui.verbose: 102 if local_ctx.rev() != base_ctx.rev(): 103 msg += _( 104 b'\n changeset %s is a successor of ' b'changeset %s' 105 ) % (local_ctx, base_ctx) 106 msg += _( 107 b'\n changeset %s already has a successor in ' 108 b'changeset %s\n' 109 b' rewriting changeset %s would create ' 110 b'"content-divergence"\n' 111 b' set experimental.evolution.allowdivergence=True to ' 112 b'skip this check' 113 ) % (base_ctx, other_ctx, local_ctx) 114 raise error.InputError( 115 msg, 116 hint=_( 117 b"see 'hg help evolution.instability' for details on content-divergence" 118 ), 119 ) 120 else: 121 raise error.InputError( 122 msg, 123 hint=_( 124 b"add --verbose for details or see " 125 b"'hg help evolution.instability'" 126 ), 127 ) 128 129 130def disallowednewunstable(repo, revs): 131 """Checks whether editing the revs will create new unstable changesets and 132 are we allowed to create them. 133 134 To allow new unstable changesets, set the config: 135 `experimental.evolution.allowunstable=True` 136 """ 137 allowunstable = obsolete.isenabled(repo, obsolete.allowunstableopt) 138 if allowunstable: 139 return revset.baseset() 140 return repo.revs(b"(%ld::) - %ld", revs, revs) 141 142 143def _find_new_divergence(repo, revs): 144 obsrevs = repo.revs(b'%ld and obsolete()', revs) 145 for r in obsrevs: 146 div = find_new_divergence_from(repo, repo[r]) 147 if div: 148 return (repo[r], repo[div[0]], repo.unfiltered()[div[1]]) 149 return None 150 151 152def find_new_divergence_from(repo, ctx): 153 """return divergent revision if rewriting an obsolete cset (ctx) will 154 create divergence 155 156 Returns (<other node>, <common ancestor node>) or None 157 """ 158 if not ctx.obsolete(): 159 return None 160 # We need to check two cases that can cause divergence: 161 # case 1: the rev being rewritten has a non-obsolete successor (easily 162 # detected by successorssets) 163 sset = obsutil.successorssets(repo, ctx.node()) 164 if sset: 165 return (sset[0][0], ctx.node()) 166 else: 167 # case 2: one of the precursors of the rev being revived has a 168 # non-obsolete successor (we need divergentsets for this) 169 divsets = obsutil.divergentsets(repo, ctx) 170 if divsets: 171 nsuccset = divsets[0][b'divergentnodes'] 172 prec = divsets[0][b'commonpredecessor'] 173 return (nsuccset[0], prec) 174 return None 175 176 177def skip_empty_successor(ui, command): 178 empty_successor = ui.config(b'rewrite', b'empty-successor') 179 if empty_successor == b'skip': 180 return True 181 elif empty_successor == b'keep': 182 return False 183 else: 184 raise error.ConfigError( 185 _( 186 b"%s doesn't know how to handle config " 187 b"rewrite.empty-successor=%s (only 'skip' and 'keep' are " 188 b"supported)" 189 ) 190 % (command, empty_successor) 191 ) 192 193 194def update_hash_refs(repo, commitmsg, pending=None): 195 """Replace all obsolete commit hashes in the message with the current hash. 196 197 If the obsolete commit was split or is divergent, the hash is not replaced 198 as there's no way to know which successor to choose. 199 200 For commands that update a series of commits in the current transaction, the 201 new obsolete markers can be considered by setting ``pending`` to a mapping 202 of ``pending[oldnode] = [successor_node1, successor_node2,..]``. 203 """ 204 if not pending: 205 pending = {} 206 cache = {} 207 hashes = re.findall(NODE_RE, commitmsg) 208 unfi = repo.unfiltered() 209 for h in hashes: 210 try: 211 fullnode = scmutil.resolvehexnodeidprefix(unfi, h) 212 except error.WdirUnsupported: 213 # Someone has an fffff... in a commit message we're 214 # rewriting. Don't try rewriting that. 215 continue 216 if fullnode is None: 217 continue 218 ctx = unfi[fullnode] 219 if not ctx.obsolete(): 220 successors = pending.get(fullnode) 221 if successors is None: 222 continue 223 # obsutil.successorssets() returns a list of list of nodes 224 successors = [successors] 225 else: 226 successors = obsutil.successorssets(repo, ctx.node(), cache=cache) 227 228 # We can't make any assumptions about how to update the hash if the 229 # cset in question was split or diverged. 230 if len(successors) == 1 and len(successors[0]) == 1: 231 successor = successors[0][0] 232 if successor is not None: 233 newhash = hex(successor) 234 commitmsg = commitmsg.replace(h, newhash[: len(h)]) 235 else: 236 repo.ui.note( 237 _( 238 b'The stale commit message reference to %s could ' 239 b'not be updated\n(The referenced commit was dropped)\n' 240 ) 241 % h 242 ) 243 else: 244 repo.ui.note( 245 _( 246 b'The stale commit message reference to %s could ' 247 b'not be updated\n' 248 ) 249 % h 250 ) 251 252 return commitmsg 253