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)