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'