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)