##// 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 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(ui, repo, files, node, tmproot):
53 def snapshot(ui, repo, files, node, tmproot):
54 '''snapshot files as of some revision
54 '''snapshot files as of some revision
55 if not using snapshot, -I/-X does not work and recursive diff
55 if not using snapshot, -I/-X does not work and recursive diff
56 in tools like kdiff3 and meld displays too many files.'''
56 in tools like kdiff3 and meld displays too many files.'''
57 dirname = os.path.basename(repo.root)
57 dirname = os.path.basename(repo.root)
58 if dirname == "":
58 if dirname == "":
59 dirname = "root"
59 dirname = "root"
60 if node is not None:
60 if node is not None:
61 dirname = '%s.%s' % (dirname, short(node))
61 dirname = '%s.%s' % (dirname, short(node))
62 base = os.path.join(tmproot, dirname)
62 base = os.path.join(tmproot, dirname)
63 os.mkdir(base)
63 os.mkdir(base)
64 if node is not None:
64 if node is not None:
65 ui.note(_('making snapshot of %d files from rev %s\n') %
65 ui.note(_('making snapshot of %d files from rev %s\n') %
66 (len(files), short(node)))
66 (len(files), short(node)))
67 else:
67 else:
68 ui.note(_('making snapshot of %d files from working dir\n') %
68 ui.note(_('making snapshot of %d files from working dir\n') %
69 (len(files)))
69 (len(files)))
70
70 wopener = util.opener(base)
71 fns_and_mtime = []
71 fns_and_mtime = []
72 ctx = repo[node]
72 ctx = repo[node]
73 for fn in files:
73 for fn in files:
74 wfn = util.pconvert(fn)
74 wfn = util.pconvert(fn)
75 if not wfn in ctx:
75 if not wfn in ctx:
76 # skipping new file after a merge ?
76 # skipping new file after a merge ?
77 continue
77 continue
78 ui.note(' %s\n' % wfn)
78 ui.note(' %s\n' % wfn)
79 dest = os.path.join(base, wfn)
79 dest = os.path.join(base, wfn)
80 destdir = os.path.dirname(dest)
80 fctx = ctx[wfn]
81 if not os.path.isdir(destdir):
81 data = repo.wwritedata(wfn, fctx.data())
82 os.makedirs(destdir)
82 if 'l' in fctx.flags():
83 data = repo.wwritedata(wfn, ctx[wfn].data())
83 wopener.symlink(data, wfn)
84 open(dest, 'wb').write(data)
84 else:
85 wopener(wfn, 'w').write(data)
86 if 'x' in fctx.flags():
87 util.set_flags(dest, False, True)
85 if node is None:
88 if node is None:
86 fns_and_mtime.append((dest, repo.wjoin(fn), os.path.getmtime(dest)))
89 fns_and_mtime.append((dest, repo.wjoin(fn), os.path.getmtime(dest)))
87 return dirname, fns_and_mtime
90 return dirname, fns_and_mtime
88
91
89 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
92 def dodiff(ui, repo, diffcmd, diffopts, pats, opts):
90 '''Do the actuall diff:
93 '''Do the actuall diff:
91
94
92 - copy to a temp structure if diffing 2 internal revisions
95 - copy to a temp structure if diffing 2 internal revisions
93 - copy to a temp structure if diffing working revision with
96 - copy to a temp structure if diffing working revision with
94 another one and more than 1 file is changed
97 another one and more than 1 file is changed
95 - just invoke the diff for a single file in the working dir
98 - just invoke the diff for a single file in the working dir
96 '''
99 '''
97
100
98 revs = opts.get('rev')
101 revs = opts.get('rev')
99 change = opts.get('change')
102 change = opts.get('change')
100
103
101 if revs and change:
104 if revs and change:
102 msg = _('cannot specify --rev and --change at the same time')
105 msg = _('cannot specify --rev and --change at the same time')
103 raise util.Abort(msg)
106 raise util.Abort(msg)
104 elif change:
107 elif change:
105 node2 = repo.lookup(change)
108 node2 = repo.lookup(change)
106 node1 = repo[node2].parents()[0].node()
109 node1 = repo[node2].parents()[0].node()
107 else:
110 else:
108 node1, node2 = cmdutil.revpair(repo, revs)
111 node1, node2 = cmdutil.revpair(repo, revs)
109
112
110 matcher = cmdutil.match(repo, pats, opts)
113 matcher = cmdutil.match(repo, pats, opts)
111 modified, added, removed = repo.status(node1, node2, matcher)[:3]
114 modified, added, removed = repo.status(node1, node2, matcher)[:3]
112 if not (modified or added or removed):
115 if not (modified or added or removed):
113 return 0
116 return 0
114
117
115 tmproot = tempfile.mkdtemp(prefix='extdiff.')
118 tmproot = tempfile.mkdtemp(prefix='extdiff.')
116 dir2root = ''
119 dir2root = ''
117 try:
120 try:
118 # Always make a copy of node1
121 # Always make a copy of node1
119 dir1 = snapshot(ui, repo, modified + removed, node1, tmproot)[0]
122 dir1 = snapshot(ui, repo, modified + removed, node1, tmproot)[0]
120 changes = len(modified) + len(removed) + len(added)
123 changes = len(modified) + len(removed) + len(added)
121
124
122 # If node2 in not the wc or there is >1 change, copy it
125 # If node2 in not the wc or there is >1 change, copy it
123 if node2 or changes > 1:
126 if node2 or changes > 1:
124 dir2, fns_and_mtime = snapshot(ui, repo, modified + added, node2, tmproot)
127 dir2, fns_and_mtime = snapshot(ui, repo, modified + added, node2, tmproot)
125 else:
128 else:
126 # This lets the diff tool open the changed file directly
129 # This lets the diff tool open the changed file directly
127 dir2 = ''
130 dir2 = ''
128 dir2root = repo.root
131 dir2root = repo.root
129 fns_and_mtime = []
132 fns_and_mtime = []
130
133
131 # If only one change, diff the files instead of the directories
134 # If only one change, diff the files instead of the directories
132 if changes == 1 :
135 if changes == 1 :
133 if len(modified):
136 if len(modified):
134 dir1 = os.path.join(dir1, util.localpath(modified[0]))
137 dir1 = os.path.join(dir1, util.localpath(modified[0]))
135 dir2 = os.path.join(dir2root, dir2, util.localpath(modified[0]))
138 dir2 = os.path.join(dir2root, dir2, util.localpath(modified[0]))
136 elif len(removed) :
139 elif len(removed) :
137 dir1 = os.path.join(dir1, util.localpath(removed[0]))
140 dir1 = os.path.join(dir1, util.localpath(removed[0]))
138 dir2 = os.devnull
141 dir2 = os.devnull
139 else:
142 else:
140 dir1 = os.devnull
143 dir1 = os.devnull
141 dir2 = os.path.join(dir2root, dir2, util.localpath(added[0]))
144 dir2 = os.path.join(dir2root, dir2, util.localpath(added[0]))
142
145
143 cmdline = ('%s %s %s %s' %
146 cmdline = ('%s %s %s %s' %
144 (util.shellquote(diffcmd), ' '.join(diffopts),
147 (util.shellquote(diffcmd), ' '.join(diffopts),
145 util.shellquote(dir1), util.shellquote(dir2)))
148 util.shellquote(dir1), util.shellquote(dir2)))
146 ui.debug(_('running %r in %s\n') % (cmdline, tmproot))
149 ui.debug(_('running %r in %s\n') % (cmdline, tmproot))
147 util.system(cmdline, cwd=tmproot)
150 util.system(cmdline, cwd=tmproot)
148
151
149 for copy_fn, working_fn, mtime in fns_and_mtime:
152 for copy_fn, working_fn, mtime in fns_and_mtime:
150 if os.path.getmtime(copy_fn) != mtime:
153 if os.path.getmtime(copy_fn) != mtime:
151 ui.debug(_('file changed while diffing. '
154 ui.debug(_('file changed while diffing. '
152 'Overwriting: %s (src: %s)\n') % (working_fn, copy_fn))
155 'Overwriting: %s (src: %s)\n') % (working_fn, copy_fn))
153 util.copyfile(copy_fn, working_fn)
156 util.copyfile(copy_fn, working_fn)
154
157
155 return 1
158 return 1
156 finally:
159 finally:
157 ui.note(_('cleaning up temp directory\n'))
160 ui.note(_('cleaning up temp directory\n'))
158 shutil.rmtree(tmproot)
161 shutil.rmtree(tmproot)
159
162
160 def extdiff(ui, repo, *pats, **opts):
163 def extdiff(ui, repo, *pats, **opts):
161 '''use external program to diff repository (or selected files)
164 '''use external program to diff repository (or selected files)
162
165
163 Show differences between revisions for the specified files, using
166 Show differences between revisions for the specified files, using
164 an external program. The default program used is diff, with
167 an external program. The default program used is diff, with
165 default options "-Npru".
168 default options "-Npru".
166
169
167 To select a different program, use the -p option. The program
170 To select a different program, use the -p option. The program
168 will be passed the names of two directories to compare. To pass
171 will be passed the names of two directories to compare. To pass
169 additional options to the program, use the -o option. These will
172 additional options to the program, use the -o option. These will
170 be passed before the names of the directories to compare.
173 be passed before the names of the directories to compare.
171
174
172 When two revision arguments are given, then changes are
175 When two revision arguments are given, then changes are
173 shown between those revisions. If only one revision is
176 shown between those revisions. If only one revision is
174 specified then that revision is compared to the working
177 specified then that revision is compared to the working
175 directory, and, when no revisions are specified, the
178 directory, and, when no revisions are specified, the
176 working directory files are compared to its parent.'''
179 working directory files are compared to its parent.'''
177 program = opts['program'] or 'diff'
180 program = opts['program'] or 'diff'
178 if opts['program']:
181 if opts['program']:
179 option = opts['option']
182 option = opts['option']
180 else:
183 else:
181 option = opts['option'] or ['-Npru']
184 option = opts['option'] or ['-Npru']
182 return dodiff(ui, repo, program, option, pats, opts)
185 return dodiff(ui, repo, program, option, pats, opts)
183
186
184 cmdtable = {
187 cmdtable = {
185 "extdiff":
188 "extdiff":
186 (extdiff,
189 (extdiff,
187 [('p', 'program', '', _('comparison program to run')),
190 [('p', 'program', '', _('comparison program to run')),
188 ('o', 'option', [], _('pass option to comparison program')),
191 ('o', 'option', [], _('pass option to comparison program')),
189 ('r', 'rev', [], _('revision')),
192 ('r', 'rev', [], _('revision')),
190 ('c', 'change', '', _('change made by revision')),
193 ('c', 'change', '', _('change made by revision')),
191 ] + commands.walkopts,
194 ] + commands.walkopts,
192 _('hg extdiff [OPT]... [FILE]...')),
195 _('hg extdiff [OPT]... [FILE]...')),
193 }
196 }
194
197
195 def uisetup(ui):
198 def uisetup(ui):
196 for cmd, path in ui.configitems('extdiff'):
199 for cmd, path in ui.configitems('extdiff'):
197 if cmd.startswith('cmd.'):
200 if cmd.startswith('cmd.'):
198 cmd = cmd[4:]
201 cmd = cmd[4:]
199 if not path: path = cmd
202 if not path: path = cmd
200 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
203 diffopts = ui.config('extdiff', 'opts.' + cmd, '')
201 diffopts = diffopts and [diffopts] or []
204 diffopts = diffopts and [diffopts] or []
202 elif cmd.startswith('opts.'):
205 elif cmd.startswith('opts.'):
203 continue
206 continue
204 else:
207 else:
205 # command = path opts
208 # command = path opts
206 if path:
209 if path:
207 diffopts = shlex.split(path)
210 diffopts = shlex.split(path)
208 path = diffopts.pop(0)
211 path = diffopts.pop(0)
209 else:
212 else:
210 path, diffopts = cmd, []
213 path, diffopts = cmd, []
211 def save(cmd, path, diffopts):
214 def save(cmd, path, diffopts):
212 '''use closure to save diff command to use'''
215 '''use closure to save diff command to use'''
213 def mydiff(ui, repo, *pats, **opts):
216 def mydiff(ui, repo, *pats, **opts):
214 return dodiff(ui, repo, path, diffopts, pats, opts)
217 return dodiff(ui, repo, path, diffopts, pats, opts)
215 mydiff.__doc__ = '''use %(path)s to diff repository (or selected files)
218 mydiff.__doc__ = '''use %(path)s to diff repository (or selected files)
216
219
217 Show differences between revisions for the specified
220 Show differences between revisions for the specified
218 files, using the %(path)s program.
221 files, using the %(path)s program.
219
222
220 When two revision arguments are given, then changes are
223 When two revision arguments are given, then changes are
221 shown between those revisions. If only one revision is
224 shown between those revisions. If only one revision is
222 specified then that revision is compared to the working
225 specified then that revision is compared to the working
223 directory, and, when no revisions are specified, the
226 directory, and, when no revisions are specified, the
224 working directory files are compared to its parent.''' % {
227 working directory files are compared to its parent.''' % {
225 'path': util.uirepr(path),
228 'path': util.uirepr(path),
226 }
229 }
227 return mydiff
230 return mydiff
228 cmdtable[cmd] = (save(cmd, path, diffopts),
231 cmdtable[cmd] = (save(cmd, path, diffopts),
229 cmdtable['extdiff'][1][1:],
232 cmdtable['extdiff'][1][1:],
230 _('hg %s [OPTION]... [FILE]...') % cmd)
233 _('hg %s [OPTION]... [FILE]...') % cmd)
@@ -1,45 +1,66 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 -C 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 merge 1
35 hg merge 1
36 # should diff cloned file against wc file
36 # should diff cloned file against wc file
37 hg falabala > out
37 hg falabala > out
38 # cleanup the output since the wc is a tmp directory
38 # cleanup the output since the wc is a tmp directory
39 sed 's:\(.* \).*\(\/test-extdiff\):\1[tmp]\2:' out
39 sed 's:\(.* \).*\(\/test-extdiff\):\1[tmp]\2:' out
40 # test --change option
40 # test --change option
41 hg ci -d '2 0' -mtest3
41 hg ci -d '2 0' -mtest3
42 hg falabala -c 1
42 hg falabala -c 1
43 # check diff are made from the first parent
43 # check diff are made from the first parent
44 hg falabala -c 3 || echo "diff-like tools yield a non-zero exit code"
44 hg falabala -c 3 || echo "diff-like tools yield a non-zero exit code"
45 #hg log
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 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 -c --change change made by revision
24 -I --include include names matching the given patterns
24 -I --include include names matching the given patterns
25 -X --exclude exclude names matching the given patterns
25 -X --exclude exclude names matching the given patterns
26
26
27 use "hg -v help falabala" to show global options
27 use "hg -v help falabala" to show global options
28 diffing a.8a5febb7f867/a a.34eed99112ab/a
28 diffing a.8a5febb7f867/a a.34eed99112ab/a
29 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
30 created new head
30 created new head
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
33 diffing a.2a13a4d2da36/a [tmp]/test-extdiff/a/a
34 diffing a.8a5febb7f867/a a.34eed99112ab/a
34 diffing a.8a5febb7f867/a a.34eed99112ab/a
35 diffing a.2a13a4d2da36/a a.46c0e4daeb72/a
35 diffing a.2a13a4d2da36/a a.46c0e4daeb72/a
36 diff-like tools yield a non-zero exit code
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