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