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'