##// END OF EJS Templates
extdiff: add --change option to display single changeset diff...
Gilles Moris -
r7758:e81e6c99 default
parent child Browse files
Show More
@@ -1,247 +1,260 b''
1 # extdiff.py - external diff program support for mercurial
1 # extdiff.py - external diff program support for mercurial
2 #
2 #
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms
5 # This software may be used and distributed according to the terms
6 # of the GNU General Public License, incorporated herein by reference.
6 # of the GNU General Public License, incorporated herein by reference.
7
7
8 '''
8 '''
9 The `extdiff' Mercurial extension allows you to use external programs
9 The `extdiff' Mercurial extension allows you to use external programs
10 to compare revisions, or revision with working dir. The external diff
10 to compare revisions, or revision with working dir. The external diff
11 programs are called with a configurable set of options and two
11 programs are called with a configurable set of options and two
12 non-option arguments: paths to directories containing snapshots of
12 non-option arguments: paths to directories containing snapshots of
13 files to compare.
13 files to compare.
14
14
15 To enable this extension:
15 To enable this extension:
16
16
17 [extensions]
17 [extensions]
18 hgext.extdiff =
18 hgext.extdiff =
19
19
20 The `extdiff' extension also allows to configure new diff commands, so
20 The `extdiff' extension also allows to configure new diff commands, so
21 you do not need to type "hg extdiff -p kdiff3" always.
21 you do not need to type "hg extdiff -p kdiff3" always.
22
22
23 [extdiff]
23 [extdiff]
24 # add new command that runs GNU diff(1) in 'context diff' mode
24 # add new command that runs GNU diff(1) in 'context diff' mode
25 cdiff = gdiff -Nprc5
25 cdiff = gdiff -Nprc5
26 ## or the old way:
26 ## or the old way:
27 #cmd.cdiff = gdiff
27 #cmd.cdiff = gdiff
28 #opts.cdiff = -Nprc5
28 #opts.cdiff = -Nprc5
29
29
30 # add new command called vdiff, runs kdiff3
30 # add new command called vdiff, runs kdiff3
31 vdiff = kdiff3
31 vdiff = kdiff3
32
32
33 # add new command called meld, runs meld (no need to name twice)
33 # add new command called meld, runs meld (no need to name twice)
34 meld =
34 meld =
35
35
36 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
36 # add new command called vimdiff, runs gvimdiff with DirDiff plugin
37 #(see http://www.vim.org/scripts/script.php?script_id=102)
37 #(see http://www.vim.org/scripts/script.php?script_id=102)
38 # Non english user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
38 # Non english user, be sure to put "let g:DirDiffDynamicDiffText = 1" in
39 # your .vimrc
39 # your .vimrc
40 vimdiff = gvim -f '+next' '+execute "DirDiff" argv(0) argv(1)'
40 vimdiff = gvim -f '+next' '+execute "DirDiff" argv(0) argv(1)'
41
41
42 You can use -I/-X and list of file or directory names like normal
42 You can use -I/-X and list of file or directory names like normal
43 "hg diff" command. The `extdiff' extension makes snapshots of only
43 "hg diff" command. The `extdiff' extension makes snapshots of only
44 needed files, so running the external diff program will actually be
44 needed files, so running the external diff program will actually be
45 pretty fast (at least faster than having to compare the entire tree).
45 pretty fast (at least faster than having to compare the entire tree).
46 '''
46 '''
47
47
48 from mercurial.i18n import _
48 from mercurial.i18n import _
49 from mercurial.node import short
49 from mercurial.node import short
50 from mercurial import cmdutil, util, commands
50 from mercurial import cmdutil, util, commands
51 import os, shlex, shutil, tempfile
51 import os, shlex, shutil, tempfile
52
52
53 def snapshot_node(ui, repo, files, node, tmproot):
53 def snapshot_node(ui, repo, files, node, tmproot):
54 '''snapshot files as of some revision'''
54 '''snapshot files as of some revision'''
55 dirname = os.path.basename(repo.root)
55 dirname = os.path.basename(repo.root)
56 if dirname == "":
56 if dirname == "":
57 dirname = "root"
57 dirname = "root"
58 dirname = '%s.%s' % (dirname, short(node))
58 dirname = '%s.%s' % (dirname, short(node))
59 base = os.path.join(tmproot, dirname)
59 base = os.path.join(tmproot, dirname)
60 os.mkdir(base)
60 os.mkdir(base)
61 ui.note(_('making snapshot of %d files from rev %s\n') %
61 ui.note(_('making snapshot of %d files from rev %s\n') %
62 (len(files), short(node)))
62 (len(files), short(node)))
63 ctx = repo[node]
63 ctx = repo[node]
64 for fn in files:
64 for fn in files:
65 wfn = util.pconvert(fn)
65 wfn = util.pconvert(fn)
66 if not wfn in ctx:
66 if not wfn in ctx:
67 # skipping new file after a merge ?
67 # skipping new file after a merge ?
68 continue
68 continue
69 ui.note(' %s\n' % wfn)
69 ui.note(' %s\n' % wfn)
70 dest = os.path.join(base, wfn)
70 dest = os.path.join(base, wfn)
71 destdir = os.path.dirname(dest)
71 destdir = os.path.dirname(dest)
72 if not os.path.isdir(destdir):
72 if not os.path.isdir(destdir):
73 os.makedirs(destdir)
73 os.makedirs(destdir)
74 data = repo.wwritedata(wfn, ctx[wfn].data())
74 data = repo.wwritedata(wfn, ctx[wfn].data())
75 open(dest, 'wb').write(data)
75 open(dest, 'wb').write(data)
76 return dirname
76 return dirname
77
77
78
78
79 def snapshot_wdir(ui, repo, files, tmproot):
79 def snapshot_wdir(ui, repo, files, tmproot):
80 '''snapshot files from working directory.
80 '''snapshot files from working directory.
81 if not using snapshot, -I/-X does not work and recursive diff
81 if not using snapshot, -I/-X does not work and recursive diff
82 in tools like kdiff3 and meld displays too many files.'''
82 in tools like kdiff3 and meld displays too many files.'''
83 dirname = os.path.basename(repo.root)
83 dirname = os.path.basename(repo.root)
84 if dirname == "":
84 if dirname == "":
85 dirname = "root"
85 dirname = "root"
86 base = os.path.join(tmproot, dirname)
86 base = os.path.join(tmproot, dirname)
87 os.mkdir(base)
87 os.mkdir(base)
88 ui.note(_('making snapshot of %d files from working dir\n') %
88 ui.note(_('making snapshot of %d files from working dir\n') %
89 (len(files)))
89 (len(files)))
90
90
91 fns_and_mtime = []
91 fns_and_mtime = []
92
92
93 for fn in files:
93 for fn in files:
94 wfn = util.pconvert(fn)
94 wfn = util.pconvert(fn)
95 ui.note(' %s\n' % wfn)
95 ui.note(' %s\n' % wfn)
96 dest = os.path.join(base, wfn)
96 dest = os.path.join(base, wfn)
97 destdir = os.path.dirname(dest)
97 destdir = os.path.dirname(dest)
98 if not os.path.isdir(destdir):
98 if not os.path.isdir(destdir):
99 os.makedirs(destdir)
99 os.makedirs(destdir)
100
100
101 fp = open(dest, 'wb')
101 fp = open(dest, 'wb')
102 for chunk in util.filechunkiter(repo.wopener(wfn)):
102 for chunk in util.filechunkiter(repo.wopener(wfn)):
103 fp.write(chunk)
103 fp.write(chunk)
104 fp.close()
104 fp.close()
105
105
106 fns_and_mtime.append((dest, repo.wjoin(fn), os.path.getmtime(dest)))
106 fns_and_mtime.append((dest, repo.wjoin(fn), os.path.getmtime(dest)))
107
107
108
108
109 return dirname, fns_and_mtime
109 return dirname, fns_and_mtime
110
110
111
111
112 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
112 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
113 '''Do the actuall diff:
113 '''Do the actuall diff:
114
114
115 - copy to a temp structure if diffing 2 internal revisions
115 - copy to a temp structure if diffing 2 internal revisions
116 - copy to a temp structure if diffing working revision with
116 - copy to a temp structure if diffing working revision with
117 another one and more than 1 file is changed
117 another one and more than 1 file is changed
118 - just invoke the diff for a single file in the working dir
118 - just invoke the diff for a single file in the working dir
119 '''
119 '''
120 node1, node2 = cmdutil.revpair(repo, opts['rev'])
120
121 revs = opts.get('rev')
122 change = opts.get('change')
123
124 if revs and change:
125 msg = _('cannot specify --rev and --change at the same time')
126 raise util.Abort(msg)
127 elif change:
128 node2 = repo.lookup(change)
129 node1 = repo[node2].parents()[0].node()
130 else:
131 node1, node2 = cmdutil.revpair(repo, revs)
132
121 matcher = cmdutil.match(repo, pats, opts)
133 matcher = cmdutil.match(repo, pats, opts)
122 modified, added, removed = repo.status(node1, node2, matcher)[:3]
134 modified, added, removed = repo.status(node1, node2, matcher)[:3]
123 if not (modified or added or removed):
135 if not (modified or added or removed):
124 return 0
136 return 0
125
137
126 tmproot = tempfile.mkdtemp(prefix='extdiff.')
138 tmproot = tempfile.mkdtemp(prefix='extdiff.')
127 dir2root = ''
139 dir2root = ''
128 try:
140 try:
129 # Always make a copy of node1
141 # Always make a copy of node1
130 dir1 = snapshot_node(ui, repo, modified + removed, node1, tmproot)
142 dir1 = snapshot_node(ui, repo, modified + removed, node1, tmproot)
131 changes = len(modified) + len(removed) + len(added)
143 changes = len(modified) + len(removed) + len(added)
132
144
133 fns_and_mtime = []
145 fns_and_mtime = []
134
146
135 # If node2 in not the wc or there is >1 change, copy it
147 # If node2 in not the wc or there is >1 change, copy it
136 if node2:
148 if node2:
137 dir2 = snapshot_node(ui, repo, modified + added, node2, tmproot)
149 dir2 = snapshot_node(ui, repo, modified + added, node2, tmproot)
138 elif changes > 1:
150 elif changes > 1:
139 #we only actually need to get the files to copy back to the working
151 #we only actually need to get the files to copy back to the working
140 #dir in this case (because the other cases are: diffing 2 revisions
152 #dir in this case (because the other cases are: diffing 2 revisions
141 #or single file -- in which case the file is already directly passed
153 #or single file -- in which case the file is already directly passed
142 #to the diff tool).
154 #to the diff tool).
143 dir2, fns_and_mtime = snapshot_wdir(ui, repo, modified + added, tmproot)
155 dir2, fns_and_mtime = snapshot_wdir(ui, repo, modified + added, tmproot)
144 else:
156 else:
145 # This lets the diff tool open the changed file directly
157 # This lets the diff tool open the changed file directly
146 dir2 = ''
158 dir2 = ''
147 dir2root = repo.root
159 dir2root = repo.root
148
160
149 # If only one change, diff the files instead of the directories
161 # If only one change, diff the files instead of the directories
150 if changes == 1 :
162 if changes == 1 :
151 if len(modified):
163 if len(modified):
152 dir1 = os.path.join(dir1, util.localpath(modified[0]))
164 dir1 = os.path.join(dir1, util.localpath(modified[0]))
153 dir2 = os.path.join(dir2root, dir2, util.localpath(modified[0]))
165 dir2 = os.path.join(dir2root, dir2, util.localpath(modified[0]))
154 elif len(removed) :
166 elif len(removed) :
155 dir1 = os.path.join(dir1, util.localpath(removed[0]))
167 dir1 = os.path.join(dir1, util.localpath(removed[0]))
156 dir2 = os.devnull
168 dir2 = os.devnull
157 else:
169 else:
158 dir1 = os.devnull
170 dir1 = os.devnull
159 dir2 = os.path.join(dir2root, dir2, util.localpath(added[0]))
171 dir2 = os.path.join(dir2root, dir2, util.localpath(added[0]))
160
172
161 cmdline = ('%s %s %s %s' %
173 cmdline = ('%s %s %s %s' %
162 (util.shellquote(diffcmd), ' '.join(diffopts),
174 (util.shellquote(diffcmd), ' '.join(diffopts),
163 util.shellquote(dir1), util.shellquote(dir2)))
175 util.shellquote(dir1), util.shellquote(dir2)))
164 ui.debug(_('running %r in %s\n') % (cmdline, tmproot))
176 ui.debug(_('running %r in %s\n') % (cmdline, tmproot))
165 util.system(cmdline, cwd=tmproot)
177 util.system(cmdline, cwd=tmproot)
166
178
167 for copy_fn, working_fn, mtime in fns_and_mtime:
179 for copy_fn, working_fn, mtime in fns_and_mtime:
168 if os.path.getmtime(copy_fn) != mtime:
180 if os.path.getmtime(copy_fn) != mtime:
169 ui.debug(_('file changed while diffing. '
181 ui.debug(_('file changed while diffing. '
170 'Overwriting: %s (src: %s)\n') % (working_fn, copy_fn))
182 'Overwriting: %s (src: %s)\n') % (working_fn, copy_fn))
171 util.copyfile(copy_fn, working_fn)
183 util.copyfile(copy_fn, working_fn)
172
184
173 return 1
185 return 1
174 finally:
186 finally:
175 ui.note(_('cleaning up temp directory\n'))
187 ui.note(_('cleaning up temp directory\n'))
176 shutil.rmtree(tmproot)
188 shutil.rmtree(tmproot)
177
189
178 def extdiff(ui, repo, *pats, **opts):
190 def extdiff(ui, repo, *pats, **opts):
179 '''use external program to diff repository (or selected files)
191 '''use external program to diff repository (or selected files)
180
192
181 Show differences between revisions for the specified files, using
193 Show differences between revisions for the specified files, using
182 an external program. The default program used is diff, with
194 an external program. The default program used is diff, with
183 default options "-Npru".
195 default options "-Npru".
184
196
185 To select a different program, use the -p option. The program
197 To select a different program, use the -p option. The program
186 will be passed the names of two directories to compare. To pass
198 will be passed the names of two directories to compare. To pass
187 additional options to the program, use the -o option. These will
199 additional options to the program, use the -o option. These will
188 be passed before the names of the directories to compare.
200 be passed before the names of the directories to compare.
189
201
190 When two revision arguments are given, then changes are
202 When two revision arguments are given, then changes are
191 shown between those revisions. If only one revision is
203 shown between those revisions. If only one revision is
192 specified then that revision is compared to the working
204 specified then that revision is compared to the working
193 directory, and, when no revisions are specified, the
205 directory, and, when no revisions are specified, the
194 working directory files are compared to its parent.'''
206 working directory files are compared to its parent.'''
195 program = opts['program'] or 'diff'
207 program = opts['program'] or 'diff'
196 if opts['program']:
208 if opts['program']:
197 option = opts['option']
209 option = opts['option']
198 else:
210 else:
199 option = opts['option'] or ['-Npru']
211 option = opts['option'] or ['-Npru']
200 return dodiff(ui, repo, program, option, pats, opts)
212 return dodiff(ui, repo, program, option, pats, opts)
201
213
202 cmdtable = {
214 cmdtable = {
203 "extdiff":
215 "extdiff":
204 (extdiff,
216 (extdiff,
205 [('p', 'program', '', _('comparison program to run')),
217 [('p', 'program', '', _('comparison program to run')),
206 ('o', 'option', [], _('pass option to comparison program')),
218 ('o', 'option', [], _('pass option to comparison program')),
207 ('r', 'rev', [], _('revision')),
219 ('r', 'rev', [], _('revision')),
220 ('c', 'change', '', _('change made by revision')),
208 ] + commands.walkopts,
221 ] + commands.walkopts,
209 _('hg extdiff [OPT]... [FILE]...')),
222 _('hg extdiff [OPT]... [FILE]...')),
210 }
223 }
211
224
212 def uisetup(ui):
225 def uisetup(ui):
213 for cmd, path in ui.configitems('extdiff'):
226 for cmd, path in ui.configitems('extdiff'):
214 if cmd.startswith('cmd.'):
227 if cmd.startswith('cmd.'):
215 cmd = cmd[4:]
228 cmd = cmd[4:]
216 if not path: path = cmd
229 if not path: path = cmd
217 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
230 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
218 diffopts = diffopts and [diffopts] or []
231 diffopts = diffopts and [diffopts] or []
219 elif cmd.startswith('opts.'):
232 elif cmd.startswith('opts.'):
220 continue
233 continue
221 else:
234 else:
222 # command = path opts
235 # command = path opts
223 if path:
236 if path:
224 diffopts = shlex.split(path)
237 diffopts = shlex.split(path)
225 path = diffopts.pop(0)
238 path = diffopts.pop(0)
226 else:
239 else:
227 path, diffopts = cmd, []
240 path, diffopts = cmd, []
228 def save(cmd, path, diffopts):
241 def save(cmd, path, diffopts):
229 '''use closure to save diff command to use'''
242 '''use closure to save diff command to use'''
230 def mydiff(ui, repo, *pats, **opts):
243 def mydiff(ui, repo, *pats, **opts):
231 return dodiff(ui, repo, path, diffopts, pats, opts)
244 return dodiff(ui, repo, path, diffopts, pats, opts)
232 mydiff.__doc__ = '''use %(path)s to diff repository (or selected files)
245 mydiff.__doc__ = '''use %(path)s to diff repository (or selected files)
233
246
234 Show differences between revisions for the specified
247 Show differences between revisions for the specified
235 files, using the %(path)s program.
248 files, using the %(path)s program.
236
249
237 When two revision arguments are given, then changes are
250 When two revision arguments are given, then changes are
238 shown between those revisions. If only one revision is
251 shown between those revisions. If only one revision is
239 specified then that revision is compared to the working
252 specified then that revision is compared to the working
240 directory, and, when no revisions are specified, the
253 directory, and, when no revisions are specified, the
241 working directory files are compared to its parent.''' % {
254 working directory files are compared to its parent.''' % {
242 'path': util.uirepr(path),
255 'path': util.uirepr(path),
243 }
256 }
244 return mydiff
257 return mydiff
245 cmdtable[cmd] = (save(cmd, path, diffopts),
258 cmdtable[cmd] = (save(cmd, path, diffopts),
246 cmdtable['extdiff'][1][1:],
259 cmdtable['extdiff'][1][1:],
247 _('hg %s [OPTION]... [FILE]...') % cmd)
260 _('hg %s [OPTION]... [FILE]...') % cmd)
@@ -1,40 +1,45 b''
1 #!/bin/sh
1 #!/bin/sh
2
2
3 echo "[extensions]" >> $HGRCPATH
3 echo "[extensions]" >> $HGRCPATH
4 echo "extdiff=" >> $HGRCPATH
4 echo "extdiff=" >> $HGRCPATH
5
5
6 hg init a
6 hg init a
7 cd a
7 cd a
8 echo a > a
8 echo a > a
9 echo b > b
9 echo b > b
10 hg add
10 hg add
11 # should diff cloned directories
11 # should diff cloned directories
12 hg extdiff -o -r $opt
12 hg extdiff -o -r $opt
13
13
14 echo "[extdiff]" >> $HGRCPATH
14 echo "[extdiff]" >> $HGRCPATH
15 echo "cmd.falabala=echo" >> $HGRCPATH
15 echo "cmd.falabala=echo" >> $HGRCPATH
16 echo "opts.falabala=diffing" >> $HGRCPATH
16 echo "opts.falabala=diffing" >> $HGRCPATH
17
17
18 hg falabala
18 hg falabala
19
19
20 hg help falabala
20 hg help falabala
21
21
22 hg ci -d '0 0' -mtest1
22 hg ci -d '0 0' -mtest1
23
23
24 echo b >> a
24 echo b >> a
25 hg ci -d '1 0' -mtest2
25 hg ci -d '1 0' -mtest2
26
26
27 # should diff cloned files directly
27 # should diff cloned files directly
28 hg falabala -r 0:1
28 hg falabala -r 0:1
29
29
30 # test diff during merge
30 # test diff during merge
31 hg update 0
31 hg update 0
32 echo c >> c
32 echo c >> c
33 hg add c
33 hg add c
34 hg ci -m "new branch" -d '1 0'
34 hg ci -m "new branch" -d '1 0'
35 hg update -C 1
35 hg merge 1
36 hg merge tip
37 # should diff cloned file against wc file
36 # should diff cloned file against wc file
38 hg falabala > out || echo "diff-like tools yield a non-zero exit code"
37 hg falabala > out
39 # cleanup the output since the wc is a tmp directory
38 # cleanup the output since the wc is a tmp directory
40 sed 's:\(.* \).*\(\/test-extdiff\):\1[tmp]\2:' out
39 sed 's:\(.* \).*\(\/test-extdiff\):\1[tmp]\2:' out
40 # test --change option
41 hg ci -d '2 0' -mtest3
42 hg falabala -c 1
43 # check diff are made from the first parent
44 hg falabala -c 3 || echo "diff-like tools yield a non-zero exit code"
45 #hg log
@@ -1,34 +1,36 b''
1 adding a
1 adding a
2 adding b
2 adding b
3 Only in a: a
3 Only in a: a
4 Only in a: b
4 Only in a: b
5 diffing a.000000000000 a
5 diffing a.000000000000 a
6 hg falabala [OPTION]... [FILE]...
6 hg falabala [OPTION]... [FILE]...
7
7
8 use 'echo' to diff repository (or selected files)
8 use 'echo' to diff repository (or selected files)
9
9
10 Show differences between revisions for the specified
10 Show differences between revisions for the specified
11 files, using the 'echo' program.
11 files, using the 'echo' program.
12
12
13 When two revision arguments are given, then changes are
13 When two revision arguments are given, then changes are
14 shown between those revisions. If only one revision is
14 shown between those revisions. If only one revision is
15 specified then that revision is compared to the working
15 specified then that revision is compared to the working
16 directory, and, when no revisions are specified, the
16 directory, and, when no revisions are specified, the
17 working directory files are compared to its parent.
17 working directory files are compared to its parent.
18
18
19 options:
19 options:
20
20
21 -o --option pass option to comparison program
21 -o --option pass option to comparison program
22 -r --rev revision
22 -r --rev revision
23 -c --change change made by revision
23 -I --include include names matching the given patterns
24 -I --include include names matching the given patterns
24 -X --exclude exclude names matching the given patterns
25 -X --exclude exclude names matching the given patterns
25
26
26 use "hg -v help falabala" to show global options
27 use "hg -v help falabala" to show global options
27 diffing a.8a5febb7f867/a a.34eed99112ab/a
28 diffing a.8a5febb7f867/a a.34eed99112ab/a
28 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
29 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
29 created new head
30 created new head
30 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
31 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
31 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
32 (branch merge, don't forget to commit)
32 (branch merge, don't forget to commit)
33 diffing a.2a13a4d2da36/a [tmp]/test-extdiff/a/a
34 diffing a.8a5febb7f867/a a.34eed99112ab/a
35 diffing a.2a13a4d2da36/a a.46c0e4daeb72/a
33 diff-like tools yield a non-zero exit code
36 diff-like tools yield a non-zero exit code
34 diffing a.34eed99112ab/c [tmp]/test-extdiff/a/c
General Comments 0
You need to be logged in to leave comments. Login now