1 # Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. 2 # DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 3 # 4 # This code is free software; you can redistribute it and/or modify it 5 # under the terms of the GNU General Public License version 2 only, as 6 # published by the Free Software Foundation. 7 # 8 # This code is distributed in the hope that it will be useful, but WITHOUT 9 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 10 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 11 # version 2 for more details (a copy is included in the LICENSE file that 12 # accompanied this code). 13 # 14 # You should have received a copy of the GNU General Public License version 15 # 2 along with this work; if not, write to the Free Software Foundation, 16 # Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 17 # 18 # Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 19 # or visit www.oracle.com if you need additional information or have any 20 # questions. 21 22 import mercurial 23 import mercurial.patch 24 import mercurial.mdiff 25 import mercurial.util 26 import difflib 27 import sys 28 29 # space separated version list 30 testedwith = '4.9.2 5.0.2' 31 32 def mode(fctx): 33 flags = fctx.flags() 34 if flags == '': return '100644' 35 if flags == 'x': return '100755' 36 if flags == 'l': return '120000' 37 38 def ratio(a, b, threshold): 39 s = difflib.SequenceMatcher(None, a, b) 40 if s.real_quick_ratio() < threshold: 41 return 0 42 if s.quick_ratio() < threshold: 43 return 0 44 ratio = s.ratio() 45 if ratio < threshold: 46 return 0 47 return ratio 48 49 def encode(s): 50 return s.decode('utf-8').encode('utf-8') 51 52 def write(s): 53 sys.stdout.write(encode(s)) 54 55 def writeln(s): 56 write(s) 57 sys.stdout.write(encode('\n')) 58 59 def _match_exact(root, cwd, files, badfn=None): 60 """ 61 Wrapper for mercurial.match.exact that ignores some arguments based on the used version 62 """ 63 if mercurial.util.version().startswith("5"): 64 return mercurial.match.exact(files, badfn) 65 else: 66 return mercurial.match.exact(root, cwd, files, badfn) 67 68 def _diff_git_raw(repo, ctx1, ctx2, modified, added, removed, showPatch): 69 nullHash = '0' * 40 70 removed_copy = set(removed) 71 72 for path in added: 73 fctx = ctx2.filectx(path) 74 if fctx.renamed(): 75 parent = fctx.p1() 76 old_path, _ = fctx.renamed() 77 if old_path in removed: 78 removed_copy.discard(old_path) 79 80 for path in sorted(modified | added | removed_copy): 81 if path in modified: 82 fctx = ctx2.filectx(path) 83 writeln(':{} {} {} {} M\t{}'.format(mode(ctx1.filectx(path)), mode(fctx), nullHash, nullHash, fctx.path())) 84 elif path in added: 85 fctx = ctx2.filectx(path) 86 if not fctx.renamed(): 87 writeln(':000000 {} {} {} A\t{}'.format(mode(fctx), nullHash, nullHash, fctx.path())) 88 else: 89 parent = fctx.p1() 90 score = int(ratio(parent.data(), fctx.data(), 0.5) * 100) 91 old_path, _ = fctx.renamed() 92 93 if old_path in removed: 94 operation = 'R' 95 else: 96 operation = 'C' 97 98 writeln(':{} {} {} {} {}{}\t{}\t{}'.format(mode(parent), mode(fctx), nullHash, nullHash, operation, score, old_path, path)) 99 elif path in removed_copy: 100 fctx = ctx1.filectx(path) 101 writeln(':{} 000000 {} {} D\t{}'.format(mode(fctx), nullHash, nullHash, path)) 102 103 if showPatch: 104 writeln('') 105 106 match = _match_exact(repo.root, repo.getcwd(), list(modified) + list(added) + list(removed_copy)) 107 opts = mercurial.mdiff.diffopts(git=True, nodates=True, context=0, showfunc=True) 108 for d in mercurial.patch.diff(repo, ctx1.node(), ctx2.node(), match=match, opts=opts): 109 sys.stdout.write(d) 110 111 def really_differs(repo, p1, p2, ctx, files): 112 # workaround bug in hg (present since forever): 113 # `hg status` can, for merge commits, report a file as modififed between one parent 114 # and the merge even though it isn't. `hg diff` works correctly, so remove any "modified" 115 # that has an empty diff against one of its parents 116 differs = set() 117 for path in files: 118 match = _match_exact(repo.root, repo.getcwd(), [path]) 119 opts = mercurial.mdiff.diffopts(git=True, nodates=True, context=0, showfunc=True) 120 121 diff1 = mercurial.patch.diff(repo, p1.node(), ctx.node(), match=match, opts=opts) 122 diff2 = mercurial.patch.diff(repo, p2.node(), ctx.node(), match=match, opts=opts) 123 if len(list(diff1)) > 0 and len(list(diff2)) > 0: 124 differs.add(path) 125 126 return differs 127 128 cmdtable = {} 129 if hasattr(mercurial, 'registrar') and hasattr(mercurial.registrar, 'command'): 130 command = mercurial.registrar.command(cmdtable) 131 elif hasattr(mercurial.cmdutil, 'command'): 132 command = mercurial.cmdutil.command(cmdtable) 133 else: 134 def command(name, options, synopsis): 135 def decorator(func): 136 cmdtable[name] = func, list(options), synopsis 137 return func 138 return decorator 139 140 if hasattr(mercurial, 'utils') and hasattr(mercurial.utils, 'dateutil'): 141 datestr = mercurial.utils.dateutil.datestr 142 else: 143 datestr = mercurial.util.datestr 144 145 if hasattr(mercurial, 'scmutil'): 146 revsingle = mercurial.scmutil.revsingle 147 revrange = mercurial.scmutil.revrange 148 else: 149 revsingle = mercurial.cmdutil.revsingle 150 revrange = mercurial.cmdutil.revrange 151 152 @command('diff-git-raw', [('', 'patch', False, '')], 'hg diff-git-raw rev1 [rev2]') 153 def diff_git_raw(ui, repo, rev1, rev2=None, **opts): 154 ctx1 = revsingle(repo, rev1) 155 156 if rev2 != None: 157 ctx2 = revsingle(repo, rev2) 158 status = repo.status(ctx1, ctx2) 159 else: 160 ctx2 = mercurial.context.workingctx(repo) 161 status = repo.status(ctx1) 162 163 modified, added, removed = [set(l) for l in status[:3]] 164 _diff_git_raw(repo, ctx1, ctx2, modified, added, removed, opts['patch']) 165 166 @command('log-git', [('', 'reverse', False, ''), ('l', 'limit', -1, '')], 'hg log-git <revisions>') 167 def log_git(ui, repo, revs=None, **opts): 168 if len(repo) == 0: 169 return 170 171 if revs == None: 172 if opts['reverse']: 173 revs = '0:tip' 174 else: 175 revs = 'tip:0' 176 177 limit = opts['limit'] 178 i = 0 179 for r in revrange(repo, [revs]): 180 ctx = repo[r] 181 182 __dump_metadata(ctx) 183 parents = ctx.parents() 184 185 if len(parents) == 1: 186 modified, added, removed = [set(l) for l in repo.status(parents[0], ctx)[:3]] 187 _diff_git_raw(repo, parents[0], ctx, modified, added, removed, True) 188 else: 189 p1 = parents[0] 190 p2 = parents[1] 191 192 modified_p1, added_p1, removed_p1 = [set(l) for l in repo.status(p1, ctx)[:3]] 193 modified_p2, added_p2, removed_p2 = [set(l) for l in repo.status(p2, ctx)[:3]] 194 195 added_both = added_p1 & added_p2 196 modified_both = modified_p1 & modified_p2 197 removed_both = removed_p1 & removed_p2 198 199 combined_modified_p1 = modified_both | (modified_p1 & added_p2) 200 combined_added_p1 = added_both | (added_p1 & modified_p2) 201 combined_modified_p2 = modified_both | (modified_p2 & added_p1) 202 combined_added_p2 = added_both | (added_p2 & modified_p1) 203 204 combined_modified_p1 = really_differs(repo, p1, p2, ctx, combined_modified_p1) 205 combined_added_p1 = really_differs(repo, p1, p2, ctx, combined_added_p1) 206 combined_modified_p2 = really_differs(repo, p1, p2, ctx, combined_modified_p2) 207 combined_added_p2 = really_differs(repo, p1, p2, ctx, combined_added_p2) 208 209 _diff_git_raw(repo, p1, ctx, combined_modified_p1, combined_added_p1, removed_both, True) 210 writeln('#@!_-=&') 211 _diff_git_raw(repo, p2, ctx, combined_modified_p2, combined_added_p2, removed_both, True) 212 213 i += 1 214 if i == limit: 215 break 216 217 def __dump_metadata(ctx): 218 writeln('#@!_-=&') 219 writeln(ctx.hex()) 220 writeln(str(ctx.rev())) 221 writeln(ctx.branch()) 222 223 parents = ctx.parents() 224 writeln(' '.join([str(p.hex()) for p in parents])) 225 writeln(' '.join([str(p.rev()) for p in parents])) 226 227 writeln(ctx.user()) 228 date = datestr(ctx.date(), format='%Y-%m-%d %H:%M:%S%z') 229 writeln(date) 230 231 description = encode(ctx.description()) 232 writeln(str(len(description))) 233 write(description) 234 235 def __dump(repo, start, end): 236 for rev in xrange(start, end): 237 ctx = revsingle(repo, rev) 238 239 __dump_metadata(ctx) 240 parents = ctx.parents() 241 242 modified, added, removed = repo.status(parents[0], ctx)[:3] 243 writeln(str(len(modified))) 244 writeln(str(len(added))) 245 writeln(str(len(removed))) 246 247 for filename in added + modified: 248 fctx = ctx.filectx(filename) 249 250 writeln(filename) 251 writeln(' '.join(fctx.flags())) 252 253 content = fctx.data() 254 writeln(str(len(content))) 255 sys.stdout.write(content) 256 257 for filename in removed: 258 writeln(filename) 259 260 def pretxnclose(ui, repo, **kwargs): 261 start = revsingle(repo, kwargs['node']) 262 end = revsingle(repo, kwargs['node_last']) 263 __dump(repo, start.rev(), end.rev() + 1) 264 265 @command('dump', [], 'hg dump') 266 def dump(ui, repo, **opts): 267 __dump(repo, 0, len(repo)) 268 269 @command('metadata', [], 'hg metadata') 270 def dump(ui, repo, revs=None, **opts): 271 if revs == None: 272 revs = "0:tip" 273 274 for r in revrange(repo, [revs]): 275 ctx = repo[r] 276 __dump_metadata(ctx) 277 278 @command('ls-tree', [], 'hg ls-tree') 279 def ls_tree(ui, repo, rev, **opts): 280 nullHash = '0' * 40 281 ctx = revsingle(repo, rev) 282 for filename in ctx.manifest(): 283 fctx = ctx.filectx(filename) 284 if 'x' in fctx.flags(): 285 write('100755 blob ') 286 else: 287 write('100644 blob ') 288 write(nullHash) 289 write('\t') 290 writeln(filename)