##// END OF EJS Templates
extdiff: move from dict() construction to {} literals...
Augie Fackler -
r20674:2aafd585 default
parent child Browse files
Show More
@@ -1,333 +1,333
1 1 # extdiff.py - external diff program support for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''command to allow external programs to compare revisions
9 9
10 10 The extdiff Mercurial extension allows you to use external programs
11 11 to compare revisions, or revision with working directory. The external
12 12 diff programs are called with a configurable set of options and two
13 13 non-option arguments: paths to directories containing snapshots of
14 14 files to compare.
15 15
16 16 The extdiff extension also allows you to configure new diff commands, so
17 17 you do not need to type :hg:`extdiff -p kdiff3` always. ::
18 18
19 19 [extdiff]
20 20 # add new command that runs GNU diff(1) in 'context diff' mode
21 21 cdiff = gdiff -Nprc5
22 22 ## or the old way:
23 23 #cmd.cdiff = gdiff
24 24 #opts.cdiff = -Nprc5
25 25
26 26 # add new command called vdiff, runs kdiff3
27 27 vdiff = kdiff3
28 28
29 29 # add new command called meld, runs meld (no need to name twice)
30 30 meld =
31 31
32 32 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
33 33 # (see http://www.vim.org/scripts/script.php?script_id=102) Non
34 34 # English user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
35 35 # your .vimrc
36 36 vimdiff = gvim -f "+next" \\
37 37 "+execute 'DirDiff' fnameescape(argv(0)) fnameescape(argv(1))"
38 38
39 39 Tool arguments can include variables that are expanded at runtime::
40 40
41 41 $parent1, $plabel1 - filename, descriptive label of first parent
42 42 $child, $clabel - filename, descriptive label of child revision
43 43 $parent2, $plabel2 - filename, descriptive label of second parent
44 44 $root - repository root
45 45 $parent is an alias for $parent1.
46 46
47 47 The extdiff extension will look in your [diff-tools] and [merge-tools]
48 48 sections for diff tool arguments, when none are specified in [extdiff].
49 49
50 50 ::
51 51
52 52 [extdiff]
53 53 kdiff3 =
54 54
55 55 [diff-tools]
56 56 kdiff3.diffargs=--L1 '$plabel1' --L2 '$clabel' $parent $child
57 57
58 58 You can use -I/-X and list of file or directory names like normal
59 59 :hg:`diff` command. The extdiff extension makes snapshots of only
60 60 needed files, so running the external diff program will actually be
61 61 pretty fast (at least faster than having to compare the entire tree).
62 62 '''
63 63
64 64 from mercurial.i18n import _
65 65 from mercurial.node import short, nullid
66 66 from mercurial import scmutil, scmutil, util, commands, encoding
67 67 import os, shlex, shutil, tempfile, re
68 68
69 69 testedwith = 'internal'
70 70
71 71 def snapshot(ui, repo, files, node, tmproot):
72 72 '''snapshot files as of some revision
73 73 if not using snapshot, -I/-X does not work and recursive diff
74 74 in tools like kdiff3 and meld displays too many files.'''
75 75 dirname = os.path.basename(repo.root)
76 76 if dirname == "":
77 77 dirname = "root"
78 78 if node is not None:
79 79 dirname = '%s.%s' % (dirname, short(node))
80 80 base = os.path.join(tmproot, dirname)
81 81 os.mkdir(base)
82 82 if node is not None:
83 83 ui.note(_('making snapshot of %d files from rev %s\n') %
84 84 (len(files), short(node)))
85 85 else:
86 86 ui.note(_('making snapshot of %d files from working directory\n') %
87 87 (len(files)))
88 88 wopener = scmutil.opener(base)
89 89 fns_and_mtime = []
90 90 ctx = repo[node]
91 91 for fn in files:
92 92 wfn = util.pconvert(fn)
93 93 if wfn not in ctx:
94 94 # File doesn't exist; could be a bogus modify
95 95 continue
96 96 ui.note(' %s\n' % wfn)
97 97 dest = os.path.join(base, wfn)
98 98 fctx = ctx[wfn]
99 99 data = repo.wwritedata(wfn, fctx.data())
100 100 if 'l' in fctx.flags():
101 101 wopener.symlink(data, wfn)
102 102 else:
103 103 wopener.write(wfn, data)
104 104 if 'x' in fctx.flags():
105 105 util.setflags(dest, False, True)
106 106 if node is None:
107 107 fns_and_mtime.append((dest, repo.wjoin(fn),
108 108 os.lstat(dest).st_mtime))
109 109 return dirname, fns_and_mtime
110 110
111 111 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
112 112 '''Do the actual diff:
113 113
114 114 - copy to a temp structure if diffing 2 internal revisions
115 115 - copy to a temp structure if diffing working revision with
116 116 another one and more than 1 file is changed
117 117 - just invoke the diff for a single file in the working dir
118 118 '''
119 119
120 120 revs = opts.get('rev')
121 121 change = opts.get('change')
122 122 args = ' '.join(diffopts)
123 123 do3way = '$parent2' in args
124 124
125 125 if revs and change:
126 126 msg = _('cannot specify --rev and --change at the same time')
127 127 raise util.Abort(msg)
128 128 elif change:
129 129 node2 = scmutil.revsingle(repo, change, None).node()
130 130 node1a, node1b = repo.changelog.parents(node2)
131 131 else:
132 132 node1a, node2 = scmutil.revpair(repo, revs)
133 133 if not revs:
134 134 node1b = repo.dirstate.p2()
135 135 else:
136 136 node1b = nullid
137 137
138 138 # Disable 3-way merge if there is only one parent
139 139 if do3way:
140 140 if node1b == nullid:
141 141 do3way = False
142 142
143 143 matcher = scmutil.match(repo[node2], pats, opts)
144 144 mod_a, add_a, rem_a = map(set, repo.status(node1a, node2, matcher)[:3])
145 145 if do3way:
146 146 mod_b, add_b, rem_b = map(set, repo.status(node1b, node2, matcher)[:3])
147 147 else:
148 148 mod_b, add_b, rem_b = set(), set(), set()
149 149 modadd = mod_a | add_a | mod_b | add_b
150 150 common = modadd | rem_a | rem_b
151 151 if not common:
152 152 return 0
153 153
154 154 tmproot = tempfile.mkdtemp(prefix='extdiff.')
155 155 try:
156 156 # Always make a copy of node1a (and node1b, if applicable)
157 157 dir1a_files = mod_a | rem_a | ((mod_b | add_b) - add_a)
158 158 dir1a = snapshot(ui, repo, dir1a_files, node1a, tmproot)[0]
159 159 rev1a = '@%d' % repo[node1a].rev()
160 160 if do3way:
161 161 dir1b_files = mod_b | rem_b | ((mod_a | add_a) - add_b)
162 162 dir1b = snapshot(ui, repo, dir1b_files, node1b, tmproot)[0]
163 163 rev1b = '@%d' % repo[node1b].rev()
164 164 else:
165 165 dir1b = None
166 166 rev1b = ''
167 167
168 168 fns_and_mtime = []
169 169
170 170 # If node2 in not the wc or there is >1 change, copy it
171 171 dir2root = ''
172 172 rev2 = ''
173 173 if node2:
174 174 dir2 = snapshot(ui, repo, modadd, node2, tmproot)[0]
175 175 rev2 = '@%d' % repo[node2].rev()
176 176 elif len(common) > 1:
177 177 #we only actually need to get the files to copy back to
178 178 #the working dir in this case (because the other cases
179 179 #are: diffing 2 revisions or single file -- in which case
180 180 #the file is already directly passed to the diff tool).
181 181 dir2, fns_and_mtime = snapshot(ui, repo, modadd, None, tmproot)
182 182 else:
183 183 # This lets the diff tool open the changed file directly
184 184 dir2 = ''
185 185 dir2root = repo.root
186 186
187 187 label1a = rev1a
188 188 label1b = rev1b
189 189 label2 = rev2
190 190
191 191 # If only one change, diff the files instead of the directories
192 192 # Handle bogus modifies correctly by checking if the files exist
193 193 if len(common) == 1:
194 194 common_file = util.localpath(common.pop())
195 195 dir1a = os.path.join(tmproot, dir1a, common_file)
196 196 label1a = common_file + rev1a
197 197 if not os.path.isfile(dir1a):
198 198 dir1a = os.devnull
199 199 if do3way:
200 200 dir1b = os.path.join(tmproot, dir1b, common_file)
201 201 label1b = common_file + rev1b
202 202 if not os.path.isfile(dir1b):
203 203 dir1b = os.devnull
204 204 dir2 = os.path.join(dir2root, dir2, common_file)
205 205 label2 = common_file + rev2
206 206
207 207 # Function to quote file/dir names in the argument string.
208 208 # When not operating in 3-way mode, an empty string is
209 209 # returned for parent2
210 replace = dict(parent=dir1a, parent1=dir1a, parent2=dir1b,
211 plabel1=label1a, plabel2=label1b,
212 clabel=label2, child=dir2,
213 root=repo.root)
210 replace = {'parent': dir1a, 'parent1': dir1a, 'parent2': dir1b,
211 'plabel1': label1a, 'plabel2': label1b,
212 'clabel': label2, 'child': dir2,
213 'root': repo.root}
214 214 def quote(match):
215 215 key = match.group()[1:]
216 216 if not do3way and key == 'parent2':
217 217 return ''
218 218 return util.shellquote(replace[key])
219 219
220 220 # Match parent2 first, so 'parent1?' will match both parent1 and parent
221 221 regex = '\$(parent2|parent1?|child|plabel1|plabel2|clabel|root)'
222 222 if not do3way and not re.search(regex, args):
223 223 args += ' $parent1 $child'
224 224 args = re.sub(regex, quote, args)
225 225 cmdline = util.shellquote(diffcmd) + ' ' + args
226 226
227 227 ui.debug('running %r in %s\n' % (cmdline, tmproot))
228 228 util.system(cmdline, cwd=tmproot, out=ui.fout)
229 229
230 230 for copy_fn, working_fn, mtime in fns_and_mtime:
231 231 if os.lstat(copy_fn).st_mtime != mtime:
232 232 ui.debug('file changed while diffing. '
233 233 'Overwriting: %s (src: %s)\n' % (working_fn, copy_fn))
234 234 util.copyfile(copy_fn, working_fn)
235 235
236 236 return 1
237 237 finally:
238 238 ui.note(_('cleaning up temp directory\n'))
239 239 shutil.rmtree(tmproot)
240 240
241 241 def extdiff(ui, repo, *pats, **opts):
242 242 '''use external program to diff repository (or selected files)
243 243
244 244 Show differences between revisions for the specified files, using
245 245 an external program. The default program used is diff, with
246 246 default options "-Npru".
247 247
248 248 To select a different program, use the -p/--program option. The
249 249 program will be passed the names of two directories to compare. To
250 250 pass additional options to the program, use -o/--option. These
251 251 will be passed before the names of the directories to compare.
252 252
253 253 When two revision arguments are given, then changes are shown
254 254 between those revisions. If only one revision is specified then
255 255 that revision is compared to the working directory, and, when no
256 256 revisions are specified, the working directory files are compared
257 257 to its parent.'''
258 258 program = opts.get('program')
259 259 option = opts.get('option')
260 260 if not program:
261 261 program = 'diff'
262 262 option = option or ['-Npru']
263 263 return dodiff(ui, repo, program, option, pats, opts)
264 264
265 265 cmdtable = {
266 266 "extdiff":
267 267 (extdiff,
268 268 [('p', 'program', '',
269 269 _('comparison program to run'), _('CMD')),
270 270 ('o', 'option', [],
271 271 _('pass option to comparison program'), _('OPT')),
272 272 ('r', 'rev', [],
273 273 _('revision'), _('REV')),
274 274 ('c', 'change', '',
275 275 _('change made by revision'), _('REV')),
276 276 ] + commands.walkopts,
277 277 _('hg extdiff [OPT]... [FILE]...')),
278 278 }
279 279
280 280 def uisetup(ui):
281 281 for cmd, path in ui.configitems('extdiff'):
282 282 if cmd.startswith('cmd.'):
283 283 cmd = cmd[4:]
284 284 if not path:
285 285 path = cmd
286 286 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
287 287 diffopts = diffopts and [diffopts] or []
288 288 elif cmd.startswith('opts.'):
289 289 continue
290 290 else:
291 291 # command = path opts
292 292 if path:
293 293 diffopts = shlex.split(path)
294 294 path = diffopts.pop(0)
295 295 else:
296 296 path, diffopts = cmd, []
297 297 # look for diff arguments in [diff-tools] then [merge-tools]
298 298 if diffopts == []:
299 299 args = ui.config('diff-tools', cmd+'.diffargs') or \
300 300 ui.config('merge-tools', cmd+'.diffargs')
301 301 if args:
302 302 diffopts = shlex.split(args)
303 303 def save(cmd, path, diffopts):
304 304 '''use closure to save diff command to use'''
305 305 def mydiff(ui, repo, *pats, **opts):
306 306 return dodiff(ui, repo, path, diffopts + opts['option'],
307 307 pats, opts)
308 308 doc = _('''\
309 309 use %(path)s to diff repository (or selected files)
310 310
311 311 Show differences between revisions for the specified files, using
312 312 the %(path)s program.
313 313
314 314 When two revision arguments are given, then changes are shown
315 315 between those revisions. If only one revision is specified then
316 316 that revision is compared to the working directory, and, when no
317 317 revisions are specified, the working directory files are compared
318 318 to its parent.\
319 ''') % dict(path=util.uirepr(path))
319 ''') % {'path': util.uirepr(path)}
320 320
321 321 # We must translate the docstring right away since it is
322 322 # used as a format string. The string will unfortunately
323 323 # be translated again in commands.helpcmd and this will
324 324 # fail when the docstring contains non-ASCII characters.
325 325 # Decoding the string to a Unicode string here (using the
326 326 # right encoding) prevents that.
327 327 mydiff.__doc__ = doc.decode(encoding.encoding)
328 328 return mydiff
329 329 cmdtable[cmd] = (save(cmd, path, diffopts),
330 330 cmdtable['extdiff'][1][1:],
331 331 _('hg %s [OPTION]... [FILE]...') % cmd)
332 332
333 333 commands.inferrepo += " extdiff"
General Comments 0
You need to be logged in to leave comments. Login now