1
Return-Path: <erik@tntech.dk>
2
X-Original-To: mbp@sourcefrog.net
3
Delivered-To: mbp@ozlabs.org
4
X-Greylist: delayed 1826 seconds by postgrey-1.21 at ozlabs; Sun, 15 May 2005 06:59:11 EST
5
Received: from upstroke.tntech.dk (cpe.atm2-0-1041078.0x503eaf62.odnxx4.customer.tele.dk [80.62.175.98])
6
by ozlabs.org (Postfix) with ESMTP id B968E679EA
7
for <mbp@sourcefrog.net>; Sun, 15 May 2005 06:59:11 +1000 (EST)
8
Received: by upstroke.tntech.dk (Postfix, from userid 1001)
9
id 63F83542FF; Sat, 14 May 2005 22:28:37 +0200 (CEST)
10
To: Martin Pool <mbp@sourcefrog.net>
11
Subject: [PATCH] symlink support patch
12
From: Erik Toubro Nielsen <erik@tntech.dk>
13
Date: Sat, 14 May 2005 22:28:37 +0200
14
Message-ID: <86u0l57dsa.fsf@upstroke.tntech.dk>
15
User-Agent: Gnus/5.1006 (Gnus v5.10.6) XEmacs/21.4 (Security Through
18
Content-Type: multipart/mixed; boundary="=-=-="
19
X-Spam-Checker-Version: SpamAssassin 3.0.3 (2005-04-27) on ozlabs.org
21
X-Spam-Status: No, score=-1.4 required=3.2 tests=BAYES_00,NO_MORE_FUNN,
22
RCVD_IN_BLARS_RBL autolearn=no version=3.0.3
28
I'm not sending this to the list as it is pretty large.
30
Let me know if its usefull and if I should rework anything.
37
(Bugfix: in class TreeDelta I've moved kind= one level up, since
38
kind is also used in the else part)
40
Since both the InventoryEntry and stat cache is changed, perhaps
41
the branch format number should be increased?
43
Added test cases for symlinks to testbzr
45
Cannot use realpath since it expands path/L to path/LinkTarget
46
Cannot use exists, use new osutils.lexists
48
I'm overloading the text_modified to signify that a link
49
target is changed. Perhaps text_modified should be renamed
52
InventoryEntry has a new member "symlink_target",
54
The stat cache entry has been extended to contain the symlink
55
target and the st_mode. I try to ignore an old format cache
61
Content-Type: text/x-patch
62
Content-Disposition: inline; filename=symlinksupport.patch
64
*** modified file 'bzrlib/add.py'
69
kind = bzrlib.osutils.file_kind(f)
71
- if kind != 'file' and kind != 'directory':
72
+ if kind != 'file' and kind != 'directory' and kind != 'symlink':
73
if f not in user_list:
74
print "Skipping %s (can't add file of kind '%s')" % (f, kind)
78
kind = bzrlib.osutils.file_kind(f)
80
- if kind != 'file' and kind != 'directory':
81
+ if kind != 'file' and kind != 'directory' and kind != 'symlink':
82
bailout("can't add file '%s' of kind %r" % (f, kind))
84
versioned = (inv.path2id(rf) != None)
86
*** modified file 'bzrlib/branch.py'
93
- elif hasattr(os.path, 'realpath'):
94
- f = os.path.realpath(f)
96
- f = os.path.abspath(f)
97
- if not os.path.exists(f):
98
+ f = bzrlib.osutils.normalizepath(f)
99
+ if not bzrlib.osutils.lexists(f):
100
raise BzrError('%r does not exist' % f)
104
"""Return path relative to this branch of something inside it.
106
Raises an error if path is not in this branch."""
107
- rp = os.path.realpath(path)
108
+ rp = bzrlib.osutils.normalizepath(path)
110
if not rp.startswith(self.base):
111
bailout("path %r is not within branch %r" % (rp, self.base))
113
file_id = entry.file_id
114
mutter('commit prep file %s, id %r ' % (p, file_id))
116
- if not os.path.exists(p):
117
+ # it should be enough to use os.lexists instead of exists
118
+ # but lexists in an 2.4 function
119
+ if not bzrlib.osutils.lexists(p):
120
mutter(" file is missing, removing from inventory")
122
show_status('D', entry.kind, quotefn(path))
124
if entry.kind == 'directory':
126
bailout("%s is entered as directory but not a directory" % quotefn(p))
127
+ elif entry.kind == 'symlink':
128
+ if not os.path.islink(p):
129
+ bailout("%s is entered as symbolic link but is not a symbolic link" % quotefn(p))
130
+ entry.read_symlink_target(p)
131
elif entry.kind == 'file':
133
bailout("%s is entered as file but is not a file" % quotefn(p))
135
*** modified file 'bzrlib/diff.py'
139
print >>to_file, "\\ No newline at end of file"
142
+def _diff_symlink(old_tree, new_tree, file_id):
143
+ t1 = old_tree.get_symlink_target(file_id)
144
+ t2 = new_tree.get_symlink_target(file_id)
145
+ print '*** *** target changed %r => %r' % (t1, t2)
148
def show_diff(b, revision, specific_files):
150
@@ -112,12 +117,15 @@
152
for old_path, new_path, file_id, kind, text_modified in delta.renamed:
153
print '*** renamed %s %r => %r' % (kind, old_path, new_path)
155
+ if kind == 'file' and text_modified:
156
_diff_one(old_tree.get_file(file_id).readlines(),
157
new_tree.get_file(file_id).readlines(),
159
fromfile=old_label + old_path,
160
tofile=new_label + new_path)
162
+ elif kind == 'symlink' and text_modified:
163
+ _diff_symlink(old_tree, new_tree, file_id)
165
for path, file_id, kind in delta.modified:
166
print '*** modified %s %r' % (kind, path)
168
fromfile=old_label + path,
169
tofile=new_label + path)
171
+ elif kind == 'symlink':
172
+ _diff_symlink(old_tree, new_tree, file_id)
177
Each id is listed only once.
179
Files that are both modified and renamed are listed only in
180
- renamed, with the text_modified flag true.
181
+ renamed, with the text_modified flag true. The text_modified
182
+ applies either to the the content of the file or the target of the
183
+ symbolic link, depending of the kind of file.
185
The lists are normally sorted when the delta is created.
188
specific_files = ImmutableSet(specific_files)
190
for file_id in old_tree:
191
+ kind = old_inv.get_file_kind(file_id)
192
if file_id in new_tree:
193
- kind = old_inv.get_file_kind(file_id)
194
assert kind == new_inv.get_file_kind(file_id)
196
assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
198
old_sha1 = old_tree.get_file_sha1(file_id)
199
new_sha1 = new_tree.get_file_sha1(file_id)
200
text_modified = (old_sha1 != new_sha1)
201
+ elif kind == 'symlink':
202
+ t1 = old_tree.get_symlink_target(file_id)
203
+ t2 = new_tree.get_symlink_target(file_id)
205
+ mutter(" symlink target changed")
206
+ text_modified = True
208
+ text_modified = False
210
## mutter("no text to check for %r %r" % (file_id, kind))
211
text_modified = False
213
*** modified file 'bzrlib/inventory.py'
214
--- bzrlib/inventory.py
215
+++ bzrlib/inventory.py
216
@@ -125,14 +125,22 @@
218
self.text_id = text_id
219
self.parent_id = parent_id
220
+ self.symlink_target = None
221
if kind == 'directory':
225
+ elif kind == 'symlink':
228
raise BzrError("unhandled entry kind %r" % kind)
231
+ def read_symlink_target(self, path):
232
+ if self.kind == 'symlink':
234
+ self.symlink_target = os.readlink(path)
236
+ raise BzrError("os.readlink error, %s" % e)
238
def sorted_children(self):
239
l = self.children.items()
241
self.parent_id, text_id=self.text_id)
242
other.text_sha1 = self.text_sha1
243
other.text_size = self.text_size
244
+ other.symlink_target = self.symlink_target
249
if self.text_size != None:
250
e.set('text_size', '%d' % self.text_size)
252
- for f in ['text_id', 'text_sha1']:
253
+ for f in ['text_id', 'text_sha1', 'symlink_target']:
258
self = cls(elt.get('file_id'), elt.get('name'), elt.get('kind'), parent_id)
259
self.text_id = elt.get('text_id')
260
self.text_sha1 = elt.get('text_sha1')
261
+ self.symlink_target = elt.get('symlink_target')
263
## mutter("read inventoryentry: %r" % (elt.attrib))
266
*** modified file 'bzrlib/osutils.py'
267
--- bzrlib/osutils.py
268
+++ bzrlib/osutils.py
271
raise BzrError("can't handle file kind with mode %o of %r" % (mode, f))
276
+ if hasattr(os, 'lstat'):
282
+ if e.errno == errno.ENOENT:
285
+ raise BzrError("lstat/stat of (%r): %r" % (f, e))
287
+def normalizepath(f):
288
+ if hasattr(os.path, 'realpath'):
289
+ F = os.path.realpath
291
+ F = os.path.abspath
292
+ [p,e] = os.path.split(f)
293
+ if e == "" or e == "." or e == "..":
296
+ return os.path.join(F(p), e)
300
"""True if f is an accessible directory."""
302
*** modified file 'bzrlib/statcache.py'
303
--- bzrlib/statcache.py
304
+++ bzrlib/statcache.py
306
to use a tdb instead.
308
The cache is represented as a map from file_id to a tuple of (file_id,
309
-sha1, path, size, mtime, ctime, ino, dev).
310
+sha1, path, symlink target, size, mtime, ctime, ino, dev, mode).
324
+SC_SYMLINK_TARGET = 3
326
+CACHE_ENTRY_SIZE = 10
328
def fingerprint(abspath):
333
return (fs.st_size, fs.st_mtime,
334
- fs.st_ctime, fs.st_ino, fs.st_dev)
335
+ fs.st_ctime, fs.st_ino, fs.st_dev, fs.st_mode)
338
def _write_cache(basedir, entry_iter, dangerfiles):
341
outf.write(entry[0] + ' ' + entry[1] + ' ')
342
outf.write(b2a_qp(entry[2], True))
343
- outf.write(' %d %d %d %d %d\n' % entry[3:])
345
+ outf.write(b2a_qp(entry[3], True)) # symlink_target
346
+ outf.write(' %d %d %d %d %d %d\n' % entry[4:])
350
@@ -114,10 +118,13 @@
354
+ if len(f) != CACHE_ENTRY_SIZE:
355
+ mutter("cache is in old format, must recreate it")
359
raise BzrError("duplicated file_id in cache: {%s}" % file_id)
360
- cache[file_id] = (f[0], f[1], a2b_qp(f[2])) + tuple([long(x) for x in f[3:]])
361
+ cache[file_id] = (f[0], f[1], a2b_qp(f[2]), a2b_qp(f[3])) + tuple([long(x) for x in f[4:]])
367
def _files_from_inventory(inv):
368
for path, ie in inv.iter_entries():
369
- if ie.kind != 'file':
370
+ if ie.kind != 'file' and ie.kind != 'symlink':
372
yield ie.file_id, path
374
@@ -190,17 +197,24 @@
375
if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now):
376
dangerfiles.add(file_id)
378
- if cacheentry and (cacheentry[3:] == fp):
379
+ if cacheentry and (cacheentry[4:] == fp):
380
continue # all stat fields unchanged
384
- dig = sha.new(file(abspath, 'rb').read()).hexdigest()
386
+ mode = fp[FP_ST_MODE]
387
+ if stat.S_ISREG(mode):
388
+ link_target = '-' # can be anything, but must be non-empty
389
+ dig = sha.new(file(abspath, 'rb').read()).hexdigest()
390
+ elif stat.S_ISLNK(mode):
391
+ link_target = os.readlink(abspath)
392
+ dig = sha.new(link_target).hexdigest()
394
+ raise BzrError("file %r: unknown file stat mode: %o"%(abspath,mode))
395
if cacheentry == None or dig != cacheentry[1]:
396
# if there was no previous entry for this file, or if the
397
# SHA has changed, then update the cache
398
- cacheentry = (file_id, dig, path) + fp
399
+ cacheentry = (file_id, dig, path, link_target) + fp
400
cache[file_id] = cacheentry
404
*** modified file 'bzrlib/tree.py'
410
pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb'))
411
+ elif kind == 'symlink':
413
+ os.symlink(ie.symlink_target, fullpath)
415
+ bailout("Failed to create symlink %r -> %r, error: %s" % (fullpath, ie.symlink_target, e))
417
bailout("don't know how to export {%s} of kind %r" % (ie.file_id, kind))
418
mutter(" export {%s} kind %s to %s" % (ie.file_id, kind, fullpath))
420
for path, entry in self.inventory.iter_entries():
421
yield path, 'V', entry.kind, entry.file_id
423
+ def get_symlink_target(self, file_id):
424
+ ie = self._inventory[file_id]
425
+ return ie.symlink_target;
427
class EmptyTree(Tree):
430
if False: # just to make it a generator
434
+ def get_symlink_target(self, file_id):
437
######################################################################
440
if old_name != new_name:
441
yield (old_name, new_name)
444
+ def get_symlink_target(self, file_id):
445
+ ie = self._inventory[file_id]
446
+ return ie.symlink_target
448
*** modified file 'bzrlib/workingtree.py'
449
--- bzrlib/workingtree.py
450
+++ bzrlib/workingtree.py
453
if ie.kind == 'file':
454
if ((file_id in self._statcache)
455
- or (os.path.exists(self.abspath(inv.id2path(file_id))))):
456
+ or (bzrlib.osutils.lexists(self.abspath(inv.id2path(file_id))))):
461
return os.path.join(self.basedir, filename)
463
def has_filename(self, filename):
464
- return os.path.exists(self.abspath(filename))
465
+ return bzrlib.osutils.lexists(self.abspath(filename))
467
def get_file(self, file_id):
468
return self.get_file_byname(self.id2path(file_id))
470
self._update_statcache()
471
if file_id in self._statcache:
473
- return os.path.exists(self.abspath(self.id2path(file_id)))
474
+ return bzrlib.osutils.lexists(self.abspath(self.id2path(file_id)))
477
__contains__ = has_id
479
return self._statcache[file_id][statcache.SC_SHA1]
482
+ def get_symlink_target(self, file_id):
484
+ self._update_statcache()
485
+ target = self._statcache[file_id][statcache.SC_SYMLINK_TARGET]
488
def file_class(self, filename):
489
if self.path2id(filename):
492
*** modified file 'testbzr'
497
+#! /usr/bin/env python
499
# Copyright (C) 2005 Canonical Ltd
502
logfile.write(' at %s:%d\n' % stack[:2])
507
+ if hasattr(os, 'symlink'):
512
+def listdir_sorted(dir):
513
+ L = os.listdir(dir)
517
# prepare an empty scratch directory
518
if os.path.exists(TESTDIR):
519
@@ -320,8 +331,105 @@
520
runcmd('bzr ignore *.blah')
521
assert backtick('bzr unknowns') == ''
522
assert file('.bzrignore', 'rt').read() == '*.blah\n'
528
+ progress("symlinks")
532
+ os.symlink("NOWHERE1", "link1")
533
+ runcmd('bzr add link1')
534
+ assert backtick('bzr unknowns') == ''
535
+ runcmd(['bzr', 'commit', '-m', '1: added symlink link1'])
538
+ runcmd('bzr add d1')
539
+ assert backtick('bzr unknowns') == ''
540
+ os.symlink("NOWHERE2", "d1/link2")
541
+ assert backtick('bzr unknowns') == 'd1/link2\n'
542
+ # is d1/link2 found when adding d1
543
+ runcmd('bzr add d1')
544
+ assert backtick('bzr unknowns') == ''
545
+ os.symlink("NOWHERE3", "d1/link3")
546
+ assert backtick('bzr unknowns') == 'd1/link3\n'
547
+ runcmd(['bzr', 'commit', '-m', '2: added dir, symlink'])
549
+ runcmd('bzr rename d1 d2')
550
+ runcmd('bzr move d2/link2 .')
551
+ runcmd('bzr move link1 d2')
552
+ assert os.readlink("./link2") == "NOWHERE2"
553
+ assert os.readlink("d2/link1") == "NOWHERE1"
554
+ runcmd('bzr add d2/link3')
556
+ runcmd(['bzr', 'commit', '-m', '3: rename of dir, move symlinks, add link3'])
559
+ os.symlink("TARGET 2", "link2")
560
+ os.unlink("d2/link1")
561
+ os.symlink("TARGET 1", "d2/link1")
563
+ assert backtick("bzr relpath d2/link1") == "d2/link1\n"
564
+ runcmd(['bzr', 'commit', '-m', '4: retarget of two links'])
566
+ runcmd('bzr remove d2/link1')
567
+ assert backtick('bzr unknowns') == 'd2/link1\n'
568
+ runcmd(['bzr', 'commit', '-m', '5: remove d2/link1'])
571
+ runcmd('bzr add d1')
572
+ runcmd('bzr rename d2/link3 d1/link3new')
573
+ assert backtick('bzr unknowns') == 'd2/link1\n'
574
+ runcmd(['bzr', 'commit', '-m', '6: remove d2/link1, move/rename link3'])
576
+ runcmd(['bzr', 'check'])
578
+ runcmd(['bzr', 'export', '-r', '1', 'exp1.tmp'])
580
+ assert listdir_sorted(".") == [ "link1" ]
581
+ assert os.readlink("link1") == "NOWHERE1"
584
+ runcmd(['bzr', 'export', '-r', '2', 'exp2.tmp'])
586
+ assert listdir_sorted(".") == [ "d1", "link1" ]
589
+ runcmd(['bzr', 'export', '-r', '3', 'exp3.tmp'])
591
+ assert listdir_sorted(".") == [ "d2", "link2" ]
592
+ assert listdir_sorted("d2") == [ "link1", "link3" ]
593
+ assert os.readlink("d2/link1") == "NOWHERE1"
594
+ assert os.readlink("link2") == "NOWHERE2"
597
+ runcmd(['bzr', 'export', '-r', '4', 'exp4.tmp'])
599
+ assert listdir_sorted(".") == [ "d2", "link2" ]
600
+ assert os.readlink("d2/link1") == "TARGET 1"
601
+ assert os.readlink("link2") == "TARGET 2"
602
+ assert listdir_sorted("d2") == [ "link1", "link3" ]
605
+ runcmd(['bzr', 'export', '-r', '5', 'exp5.tmp'])
607
+ assert listdir_sorted(".") == [ "d2", "link2" ]
608
+ assert os.path.islink("link2")
609
+ assert listdir_sorted("d2")== [ "link3" ]
612
+ runcmd(['bzr', 'export', '-r', '6', 'exp6.tmp'])
614
+ assert listdir_sorted(".") == [ "d1", "d2", "link2" ]
615
+ assert listdir_sorted("d1") == [ "link3new" ]
616
+ assert listdir_sorted("d2") == []
617
+ assert os.readlink("d1/link3new") == "NOWHERE3"
622
+ progress("skipping symlink tests")
624
progress("all tests passed!")
626
sys.stderr.write('*' * 50 + '\n'