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