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 mercurial.hg 27 import mercurial.node 28 import difflib 29 import sys 30 31 # space separated version list 32 testedwith = '4.9.2 5.0.2 5.2.1' 33 34 def mode(fctx): 35 flags = fctx.flags() 36 if flags == b'': return b'100644' 37 if flags == b'x': return b'100755' 38 if flags == b'l': return b'120000' 39 40 def ratio(a, b, threshold): 41 s = difflib.SequenceMatcher(None, a, b) 42 if s.real_quick_ratio() < threshold: 43 return 0 44 if s.quick_ratio() < threshold: 45 return 0 46 ratio = s.ratio() 47 if ratio < threshold: 48 return 0 49 return ratio 50 51 def write(s): 52 if sys.version_info >= (3, 0): 53 sys.stdout.buffer.write(s) 54 else: 55 sys.stdout.write(s) 56 57 def writeln(s): 58 write(s) 59 write(b'\n') 60 61 def int_to_str(i): 62 return str(i).encode('ascii') 63 64 def _match_exact(root, cwd, files, badfn=None): 65 """ 66 Wrapper for mercurial.match.exact that ignores some arguments based on the used version 67 """ 68 if mercurial.util.version().startswith(b"5"): 69 return mercurial.match.exact(files, badfn) 70 else: 71 return mercurial.match.exact(root, cwd, files, badfn) 72 73 def _diff_git_raw(repo, ctx1, ctx2, modified, added, removed, showPatch): 74 nullHash = b'0' * 40 75 removed_copy = set(removed) 76 77 for path in added: 78 fctx = ctx2.filectx(path) 79 if fctx.renamed(): 80 parent = fctx.p1() 81 old_path, _ = fctx.renamed() 82 if old_path in removed: 83 removed_copy.discard(old_path) 84 85 for path in sorted(modified | added | removed_copy): 86 if path in modified: 87 fctx = ctx2.filectx(path) 88 writeln(b':' + mode(ctx1.filectx(path)) + b' ' + mode(fctx) + b' ' + nullHash + b' ' + nullHash + b' M\t' + fctx.path()) 89 elif path in added: 90 fctx = ctx2.filectx(path) 91 if not fctx.renamed(): 92 writeln(b':000000 ' + mode(fctx) + b' ' + nullHash + b' ' + nullHash + b' A\t' + fctx.path()) 93 else: 94 parent = fctx.p1() 95 score = int_to_str(int(ratio(parent.data(), fctx.data(), 0.5) * 100)) 96 old_path, _ = fctx.renamed() 97 98 if old_path in removed: 99 operation = b'R' 100 else: 101 operation = b'C' 102 103 write(b':' + mode(parent) + b' ' + mode(fctx) + b' ' + nullHash + b' ' + nullHash + b' ') 104 writeln(operation + score + b'\t' + old_path + b'\t' + path) 105 elif path in removed_copy: 106 fctx = ctx1.filectx(path) 107 writeln(b':' + mode(fctx) + b' 000000 ' + nullHash + b' ' + nullHash + b' D\t' + path) 108 109 if showPatch: 110 writeln(b'') 111 112 match = _match_exact(repo.root, repo.getcwd(), list(modified) + list(added) + list(removed_copy)) 113 opts = mercurial.mdiff.diffopts(git=True, nodates=True, context=0, showfunc=True) 114 for d in mercurial.patch.diff(repo, ctx1.node(), ctx2.node(), match=match, opts=opts): 115 write(d) 116 117 def really_differs(repo, p1, p2, ctx, files): 118 # workaround bug in hg (present since forever): 119 # `hg status` can, for merge commits, report a file as modififed between one parent 120 # and the merge even though it isn't. `hg diff` works correctly, so remove any "modified" 121 # that has an empty diff against one of its parents 122 differs = set() 123 for path in files: 124 match = _match_exact(repo.root, repo.getcwd(), [path]) 125 opts = mercurial.mdiff.diffopts(git=True, nodates=True, context=0, showfunc=True) 126 127 diff1 = mercurial.patch.diff(repo, p1.node(), ctx.node(), match=match, opts=opts) 128 diff2 = mercurial.patch.diff(repo, p2.node(), ctx.node(), match=match, opts=opts) 129 if len(list(diff1)) > 0 and len(list(diff2)) > 0: 130 differs.add(path) 131 132 return differs 133 134 cmdtable = {} 135 if hasattr(mercurial, 'registrar') and hasattr(mercurial.registrar, 'command'): 136 command = mercurial.registrar.command(cmdtable) 137 elif hasattr(mercurial.cmdutil, 'command'): 138 command = mercurial.cmdutil.command(cmdtable) 139 else: 140 def command(name, options, synopsis): 141 def decorator(func): 142 cmdtable[name] = func, list(options), synopsis 143 return func 144 return decorator 145 146 if hasattr(mercurial, 'utils') and hasattr(mercurial.utils, 'dateutil'): 147 datestr = mercurial.utils.dateutil.datestr 148 else: 149 datestr = mercurial.util.datestr 150 151 if hasattr(mercurial, 'scmutil'): 152 revsingle = mercurial.scmutil.revsingle 153 revrange = mercurial.scmutil.revrange 154 else: 155 revsingle = mercurial.cmdutil.revsingle 156 revrange = mercurial.cmdutil.revrange 157 158 @command(b'diff-git-raw', [(b'', b'patch', False, b'')], b'hg diff-git-raw rev1 [rev2]') 159 def diff_git_raw(ui, repo, rev1, rev2=None, **opts): 160 ctx1 = revsingle(repo, rev1) 161 162 if rev2 != None: 163 ctx2 = revsingle(repo, rev2) 164 status = repo.status(ctx1, ctx2) 165 else: 166 ctx2 = mercurial.context.workingctx(repo) 167 status = repo.status(ctx1) 168 169 modified, added, removed = [set(l) for l in status[:3]] 170 _diff_git_raw(repo, ctx1, ctx2, modified, added, removed, opts['patch']) 171 172 @command(b'log-git', [(b'', b'reverse', False, b''), (b'l', b'limit', -1, b'')], b'hg log-git <revisions>') 173 def log_git(ui, repo, revs=None, **opts): 174 if len(repo) == 0: 175 return 176 177 if revs == None: 178 if opts['reverse']: 179 revs = b'0:tip' 180 else: 181 revs = b'tip:0' 182 183 limit = opts['limit'] 184 i = 0 185 for r in revrange(repo, [revs]): 186 ctx = repo[r] 187 188 __dump_metadata(ctx) 189 parents = ctx.parents() 190 191 if len(parents) == 1: 192 modified, added, removed = [set(l) for l in repo.status(parents[0], ctx)[:3]] 193 _diff_git_raw(repo, parents[0], ctx, modified, added, removed, True) 194 else: 195 p1 = parents[0] 196 p2 = parents[1] 197 198 modified_p1, added_p1, removed_p1 = [set(l) for l in repo.status(p1, ctx)[:3]] 199 modified_p2, added_p2, removed_p2 = [set(l) for l in repo.status(p2, ctx)[:3]] 200 201 added_both = added_p1 & added_p2 202 modified_both = modified_p1 & modified_p2 203 removed_both = removed_p1 & removed_p2 204 205 combined_modified_p1 = modified_both | (modified_p1 & added_p2) 206 combined_added_p1 = added_both | (added_p1 & modified_p2) 207 combined_modified_p2 = modified_both | (modified_p2 & added_p1) 208 combined_added_p2 = added_both | (added_p2 & modified_p1) 209 210 combined_modified_p1 = really_differs(repo, p1, p2, ctx, combined_modified_p1) 211 combined_added_p1 = really_differs(repo, p1, p2, ctx, combined_added_p1) 212 combined_modified_p2 = really_differs(repo, p1, p2, ctx, combined_modified_p2) 213 combined_added_p2 = really_differs(repo, p1, p2, ctx, combined_added_p2) 214 215 _diff_git_raw(repo, p1, ctx, combined_modified_p1, combined_added_p1, removed_both, True) 216 writeln(b'#@!_-=&') 217 _diff_git_raw(repo, p2, ctx, combined_modified_p2, combined_added_p2, removed_both, True) 218 219 i += 1 220 if i == limit: 221 break 222 223 def __dump_metadata(ctx): 224 writeln(b'#@!_-=&') 225 writeln(ctx.hex()) 226 writeln(int_to_str(ctx.rev())) 227 writeln(ctx.branch()) 228 229 parents = ctx.parents() 230 writeln(b' '.join([p.hex() for p in parents])) 231 writeln(b' '.join([int_to_str(p.rev()) for p in parents])) 232 233 writeln(ctx.user()) 234 date = datestr(ctx.date(), format=b'%Y-%m-%d %H:%M:%S%z') 235 writeln(date) 236 237 description = ctx.description() 238 writeln(int_to_str(len(description))) 239 write(description) 240 241 def __dump(repo, start, end): 242 for rev in range(start, end): 243 ctx = revsingle(repo, rev) 244 245 __dump_metadata(ctx) 246 parents = ctx.parents() 247 248 modified, added, removed = repo.status(parents[0], ctx)[:3] 249 writeln(int_to_str(len(modified))) 250 writeln(int_to_str(len(added))) 251 writeln(int_to_str(len(removed))) 252 253 for filename in added + modified: 254 fctx = ctx.filectx(filename) 255 256 writeln(filename) 257 writeln(b' '.join(fctx.flags())) 258 259 content = fctx.data() 260 writeln(int_to_str(len(content))) 261 write(content) 262 263 for filename in removed: 264 writeln(filename) 265 266 def pretxnclose(ui, repo, **kwargs): 267 start = revsingle(repo, kwargs['node']) 268 end = revsingle(repo, kwargs['node_last']) 269 __dump(repo, start.rev(), end.rev() + 1) 270 271 @command(b'dump', [], b'hg dump') 272 def dump(ui, repo, **opts): 273 __dump(repo, 0, len(repo)) 274 275 @command(b'metadata', [], b'hg metadata') 276 def dump(ui, repo, revs=None, **opts): 277 if revs == None: 278 revs = b"0:tip" 279 280 for r in revrange(repo, [revs]): 281 ctx = repo[r] 282 __dump_metadata(ctx) 283 284 @command(b'ls-tree', [], b'hg ls-tree') 285 def ls_tree(ui, repo, rev, **opts): 286 nullHash = b'0' * 40 287 ctx = revsingle(repo, rev) 288 for filename in ctx.manifest(): 289 fctx = ctx.filectx(filename) 290 if b'x' in fctx.flags(): 291 write(b'100755 blob ') 292 else: 293 write(b'100644 blob ') 294 write(nullHash) 295 write(b'\t') 296 writeln(filename) 297 298 @command(b'ls-remote', [], b'hg ls-remote PATH') 299 def ls_remote(ui, repo, path, **opts): 300 peer = mercurial.hg.peer(ui or repo, opts, ui.expandpath(path)) 301 for branch, heads in peer.branchmap().iteritems(): 302 for head in heads: 303 write(mercurial.node.hex(head)) 304 write(b"\t") 305 writeln(branch)