~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to patches/symlink-support.patch

  • Committer: Martin Pool
  • Date: 2005-05-17 06:51:31 UTC
  • Revision ID: mbp@sourcefrog.net-20050517065131-96113900760a324f
doc

Show diffs side-by-side

added added

removed removed

Lines of Context:
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
16
 
 Obscurity, linux)
17
 
MIME-Version: 1.0
18
 
Content-Type: multipart/mixed; boundary="=-=-="
19
 
X-Spam-Checker-Version: SpamAssassin 3.0.3 (2005-04-27) on ozlabs.org
20
 
X-Spam-Level: 
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
23
 
Content-Length: 19688
24
 
Lines: 604
25
 
 
26
 
--=-=-=
27
 
 
28
 
I'm not sending this to the list as it is pretty large. 
29
 
 
30
 
Let me know if its usefull and if I should rework anything.
31
 
 
32
 
Erik
33
 
 
34
 
 
35
 
Overview:
36
 
 
37
 
        (Bugfix: in class TreeDelta I've moved kind= one level up, since
38
 
        kind is also used in the else part)
39
 
 
40
 
        Since both the InventoryEntry and stat cache is changed, perhaps
41
 
        the branch format number should be increased?
42
 
 
43
 
        Added test cases for symlinks to testbzr
44
 
 
45
 
        Cannot use realpath since it expands path/L to path/LinkTarget
46
 
        Cannot use exists, use new osutils.lexists
47
 
 
48
 
        I'm overloading the text_modified to signify that a link        
49
 
        target is changed. Perhaps text_modified should be renamed
50
 
        content_modified?
51
 
 
52
 
        InventoryEntry has a new member "symlink_target", 
53
 
 
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
56
 
        file.
57
 
 
58
 
 
59
 
 
60
 
--=-=-=
61
 
Content-Type: text/x-patch
62
 
Content-Disposition: inline; filename=symlinksupport.patch
63
 
 
64
 
*** modified file 'bzrlib/add.py'
65
 
--- bzrlib/add.py 
66
 
+++ bzrlib/add.py 
67
 
@@ -38,7 +38,7 @@
68
 
     for f in file_list:
69
 
         kind = bzrlib.osutils.file_kind(f)
70
 
 
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)
75
 
                 continue
76
 
@@ -56,7 +56,7 @@
77
 
 
78
 
         kind = bzrlib.osutils.file_kind(f)
79
 
 
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))
83
 
             
84
 
         versioned = (inv.path2id(rf) != None)
85
 
 
86
 
*** modified file 'bzrlib/branch.py'
87
 
--- bzrlib/branch.py 
88
 
+++ bzrlib/branch.py 
89
 
@@ -58,11 +58,9 @@
90
 
     run into the root."""
91
 
     if f == None:
92
 
         f = os.getcwd()
93
 
-    elif hasattr(os.path, 'realpath'):
94
 
-        f = os.path.realpath(f)
95
 
     else:
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)
101
 
         
102
 
 
103
 
@@ -189,7 +187,7 @@
104
 
         """Return path relative to this branch of something inside it.
105
 
 
106
 
         Raises an error if path is not in this branch."""
107
 
-        rp = os.path.realpath(path)
108
 
+        rp = bzrlib.osutils.normalizepath(path)
109
 
         # FIXME: windows
110
 
         if not rp.startswith(self.base):
111
 
             bailout("path %r is not within branch %r" % (rp, self.base))
112
 
@@ -531,7 +529,9 @@
113
 
             file_id = entry.file_id
114
 
             mutter('commit prep file %s, id %r ' % (p, file_id))
115
 
 
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")
121
 
                 if verbose:
122
 
                     show_status('D', entry.kind, quotefn(path))
123
 
@@ -554,6 +554,10 @@
124
 
             if entry.kind == 'directory':
125
 
                 if not isdir(p):
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':
132
 
                 if not isfile(p):
133
 
                     bailout("%s is entered as file but is not a file" % quotefn(p))
134
 
 
135
 
*** modified file 'bzrlib/diff.py'
136
 
--- bzrlib/diff.py 
137
 
+++ bzrlib/diff.py 
138
 
@@ -66,6 +66,11 @@
139
 
         print >>to_file, "\\ No newline at end of file"
140
 
     print >>to_file
141
 
 
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)
146
 
+    
147
 
 
148
 
 def show_diff(b, revision, specific_files):
149
 
     import sys
150
 
@@ -112,12 +117,15 @@
151
 
 
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)
154
 
-        if text_modified:
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(),
158
 
                    sys.stdout,
159
 
                    fromfile=old_label + old_path,
160
 
                    tofile=new_label + new_path)
161
 
+
162
 
+        elif kind == 'symlink' and text_modified:
163
 
+            _diff_symlink(old_tree, new_tree, file_id)
164
 
 
165
 
     for path, file_id, kind in delta.modified:
166
 
         print '*** modified %s %r' % (kind, path)
167
 
@@ -128,6 +136,8 @@
168
 
                    fromfile=old_label + path,
169
 
                    tofile=new_label + path)
170
 
 
171
 
+        elif kind == 'symlink':
172
 
+            _diff_symlink(old_tree, new_tree, file_id)
173
 
 
174
 
 
175
 
 class TreeDelta:
176
 
@@ -149,7 +159,9 @@
177
 
     Each id is listed only once.
178
 
 
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.
184
 
 
185
 
     The lists are normally sorted when the delta is created.
186
 
     """
187
 
@@ -224,8 +236,8 @@
188
 
         specific_files = ImmutableSet(specific_files)
189
 
 
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)
195
 
             
196
 
             assert kind in ('file', 'directory', 'symlink', 'root_directory'), \
197
 
@@ -246,6 +258,14 @@
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)
204
 
+                if t1 != t2:
205
 
+                    mutter("    symlink target changed")
206
 
+                    text_modified = True
207
 
+                else:
208
 
+                    text_modified = False
209
 
             else:
210
 
                 ## mutter("no text to check for %r %r" % (file_id, kind))
211
 
                 text_modified = False
212
 
 
213
 
*** modified file 'bzrlib/inventory.py'
214
 
--- bzrlib/inventory.py 
215
 
+++ bzrlib/inventory.py 
216
 
@@ -125,14 +125,22 @@
217
 
         self.kind = kind
218
 
         self.text_id = text_id
219
 
         self.parent_id = parent_id
220
 
+        self.symlink_target = None
221
 
         if kind == 'directory':
222
 
             self.children = {}
223
 
         elif kind == 'file':
224
 
             pass
225
 
+        elif kind == 'symlink':
226
 
+            pass
227
 
         else:
228
 
             raise BzrError("unhandled entry kind %r" % kind)
229
 
 
230
 
-
231
 
+    def read_symlink_target(self, path):
232
 
+        if self.kind == 'symlink':
233
 
+            try:
234
 
+                self.symlink_target = os.readlink(path)
235
 
+            except OSError,e:
236
 
+                raise BzrError("os.readlink error, %s" % e)
237
 
 
238
 
     def sorted_children(self):
239
 
         l = self.children.items()
240
 
@@ -145,6 +153,7 @@
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
245
 
         return other
246
 
 
247
 
 
248
 
@@ -168,7 +177,7 @@
249
 
         if self.text_size != None:
250
 
             e.set('text_size', '%d' % self.text_size)
251
 
             
252
 
-        for f in ['text_id', 'text_sha1']:
253
 
+        for f in ['text_id', 'text_sha1', 'symlink_target']:
254
 
             v = getattr(self, f)
255
 
             if v != None:
256
 
                 e.set(f, v)
257
 
@@ -198,6 +207,7 @@
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')
262
 
         
263
 
         ## mutter("read inventoryentry: %r" % (elt.attrib))
264
 
 
265
 
 
266
 
*** modified file 'bzrlib/osutils.py'
267
 
--- bzrlib/osutils.py 
268
 
+++ bzrlib/osutils.py 
269
 
@@ -58,7 +58,30 @@
270
 
     else:
271
 
         raise BzrError("can't handle file kind with mode %o of %r" % (mode, f)) 
272
 
 
273
 
-
274
 
+def lexists(f):
275
 
+    try:
276
 
+        if hasattr(os, 'lstat'):
277
 
+            os.lstat(f)
278
 
+        else:
279
 
+            os.stat(f)
280
 
+        return True
281
 
+    except OSError,e:
282
 
+        if e.errno == errno.ENOENT:
283
 
+            return False;
284
 
+        else:
285
 
+            raise BzrError("lstat/stat of (%r): %r" % (f, e))
286
 
+
287
 
+def normalizepath(f):
288
 
+    if hasattr(os.path, 'realpath'):
289
 
+        F = os.path.realpath
290
 
+    else:
291
 
+        F = os.path.abspath
292
 
+    [p,e] = os.path.split(f)
293
 
+    if e == "" or e == "." or e == "..":
294
 
+        return F(f)
295
 
+    else:
296
 
+        return os.path.join(F(p), e)
297
 
+    
298
 
 
299
 
 def isdir(f):
300
 
     """True if f is an accessible directory."""
301
 
 
302
 
*** modified file 'bzrlib/statcache.py'
303
 
--- bzrlib/statcache.py 
304
 
+++ bzrlib/statcache.py 
305
 
@@ -53,7 +53,7 @@
306
 
 to use a tdb instead.
307
 
 
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).
311
 
 """
312
 
 
313
 
 
314
 
@@ -62,11 +62,13 @@
315
 
 FP_CTIME = 2
316
 
 FP_INO   = 3
317
 
 FP_DEV   = 4
318
 
-
319
 
+FP_ST_MODE=5
320
 
 
321
 
 SC_FILE_ID = 0
322
 
 SC_SHA1    = 1 
323
 
-
324
 
+SC_SYMLINK_TARGET = 3
325
 
+
326
 
+CACHE_ENTRY_SIZE = 10
327
 
 
328
 
 def fingerprint(abspath):
329
 
     try:
330
 
@@ -79,7 +81,7 @@
331
 
         return None
332
 
 
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)
336
 
 
337
 
 
338
 
 def _write_cache(basedir, entry_iter, dangerfiles):
339
 
@@ -93,7 +95,9 @@
340
 
                 continue
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:])
344
 
+            outf.write(' ')
345
 
+            outf.write(b2a_qp(entry[3], True)) # symlink_target
346
 
+            outf.write(' %d %d %d %d %d %d\n' % entry[4:])
347
 
 
348
 
         outf.commit()
349
 
     finally:
350
 
@@ -114,10 +118,13 @@
351
 
     
352
 
     for l in cachefile:
353
 
         f = l.split(' ')
354
 
+        if len(f) != CACHE_ENTRY_SIZE:
355
 
+            mutter("cache is in old format, must recreate it")
356
 
+            return {}
357
 
         file_id = f[0]
358
 
         if file_id in cache:
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:]])
362
 
     return cache
363
 
 
364
 
 
365
 
@@ -125,7 +132,7 @@
366
 
 
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':
371
 
             continue
372
 
         yield ie.file_id, path
373
 
     
374
 
@@ -190,17 +197,24 @@
375
 
         if (fp[FP_MTIME] >= now) or (fp[FP_CTIME] >= now):
376
 
             dangerfiles.add(file_id)
377
 
 
378
 
-        if cacheentry and (cacheentry[3:] == fp):
379
 
+        if cacheentry and (cacheentry[4:] == fp):
380
 
             continue                    # all stat fields unchanged
381
 
 
382
 
         hardcheck += 1
383
 
 
384
 
-        dig = sha.new(file(abspath, 'rb').read()).hexdigest()
385
 
-
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()
393
 
+        else:
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
401
 
             change_cnt += 1
402
 
 
403
 
 
404
 
*** modified file 'bzrlib/tree.py'
405
 
--- bzrlib/tree.py 
406
 
+++ bzrlib/tree.py 
407
 
@@ -125,6 +125,11 @@
408
 
                 os.mkdir(fullpath)
409
 
             elif kind == 'file':
410
 
                 pumpfile(self.get_file(ie.file_id), file(fullpath, 'wb'))
411
 
+            elif kind == 'symlink':
412
 
+                try:
413
 
+                    os.symlink(ie.symlink_target, fullpath)
414
 
+                except OSError,e:
415
 
+                    bailout("Failed to create symlink %r -> %r, error: %s" % (fullpath, ie.symlink_target, e))
416
 
             else:
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))
419
 
@@ -167,6 +172,9 @@
420
 
         for path, entry in self.inventory.iter_entries():
421
 
             yield path, 'V', entry.kind, entry.file_id
422
 
 
423
 
+    def get_symlink_target(self, file_id):
424
 
+        ie = self._inventory[file_id]
425
 
+        return ie.symlink_target;
426
 
 
427
 
 class EmptyTree(Tree):
428
 
     def __init__(self):
429
 
@@ -179,7 +187,8 @@
430
 
         if False:  # just to make it a generator
431
 
             yield None
432
 
     
433
 
-
434
 
+    def get_symlink_target(self, file_id):
435
 
+        return None
436
 
 
437
 
 ######################################################################
438
 
 # diff
439
 
@@ -245,3 +254,7 @@
440
 
         if old_name != new_name:
441
 
             yield (old_name, new_name)
442
 
             
443
 
+
444
 
+    def get_symlink_target(self, file_id):
445
 
+        ie = self._inventory[file_id]        
446
 
+        return ie.symlink_target
447
 
 
448
 
*** modified file 'bzrlib/workingtree.py'
449
 
--- bzrlib/workingtree.py 
450
 
+++ bzrlib/workingtree.py 
451
 
@@ -53,7 +53,7 @@
452
 
             ie = inv[file_id]
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))))):
457
 
                     yield file_id
458
 
 
459
 
 
460
 
@@ -66,7 +66,7 @@
461
 
         return os.path.join(self.basedir, filename)
462
 
 
463
 
     def has_filename(self, filename):
464
 
-        return os.path.exists(self.abspath(filename))
465
 
+        return bzrlib.osutils.lexists(self.abspath(filename))
466
 
 
467
 
     def get_file(self, file_id):
468
 
         return self.get_file_byname(self.id2path(file_id))
469
 
@@ -86,7 +86,7 @@
470
 
         self._update_statcache()
471
 
         if file_id in self._statcache:
472
 
             return True
473
 
-        return os.path.exists(self.abspath(self.id2path(file_id)))
474
 
+        return bzrlib.osutils.lexists(self.abspath(self.id2path(file_id)))
475
 
 
476
 
 
477
 
     __contains__ = has_id
478
 
@@ -108,6 +108,12 @@
479
 
         return self._statcache[file_id][statcache.SC_SHA1]
480
 
 
481
 
 
482
 
+    def get_symlink_target(self, file_id):
483
 
+        import statcache
484
 
+        self._update_statcache()
485
 
+        target = self._statcache[file_id][statcache.SC_SYMLINK_TARGET]
486
 
+        return target
487
 
+        
488
 
     def file_class(self, filename):
489
 
         if self.path2id(filename):
490
 
             return 'V'
491
 
 
492
 
*** modified file 'testbzr'
493
 
--- testbzr 
494
 
+++ testbzr 
495
 
@@ -1,4 +1,4 @@
496
 
-#! /usr/bin/python
497
 
+#! /usr/bin/env python
498
 
 
499
 
 # Copyright (C) 2005 Canonical Ltd
500
 
 
501
 
@@ -113,6 +113,17 @@
502
 
     logfile.write('   at %s:%d\n' % stack[:2])
503
 
 
504
 
 
505
 
+def has_symlinks():
506
 
+    import os;
507
 
+    if hasattr(os, 'symlink'):
508
 
+        return True
509
 
+    else:
510
 
+        return False
511
 
+
512
 
+def listdir_sorted(dir):
513
 
+    L = os.listdir(dir)
514
 
+    L.sort()
515
 
+    return L
516
 
 
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'
523
 
-
524
 
-
525
 
+    cd("..")
526
 
+    
527
 
+    if has_symlinks():
528
 
+        progress("symlinks")
529
 
+        mkdir('symlinks')
530
 
+        cd('symlinks')
531
 
+        runcmd('bzr init')
532
 
+        os.symlink("NOWHERE1", "link1")
533
 
+        runcmd('bzr add link1')
534
 
+        assert backtick('bzr unknowns') == ''
535
 
+        runcmd(['bzr', 'commit', '-m', '1: added symlink link1'])
536
 
+
537
 
+        mkdir('d1')
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'])
548
 
+
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')
555
 
+        runcmd('bzr diff')
556
 
+        runcmd(['bzr', 'commit', '-m', '3: rename of dir, move symlinks, add link3'])
557
 
+
558
 
+        os.unlink("link2")
559
 
+        os.symlink("TARGET 2", "link2")
560
 
+        os.unlink("d2/link1")
561
 
+        os.symlink("TARGET 1", "d2/link1")
562
 
+        runcmd('bzr diff')
563
 
+        assert backtick("bzr relpath d2/link1") == "d2/link1\n"
564
 
+        runcmd(['bzr', 'commit', '-m', '4: retarget of two links'])
565
 
+
566
 
+        runcmd('bzr remove d2/link1')
567
 
+        assert backtick('bzr unknowns') == 'd2/link1\n'
568
 
+        runcmd(['bzr', 'commit', '-m', '5: remove d2/link1'])
569
 
+
570
 
+        os.mkdir("d1")
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'])
575
 
+        
576
 
+        runcmd(['bzr', 'check'])
577
 
+        
578
 
+        runcmd(['bzr', 'export', '-r', '1', 'exp1.tmp'])
579
 
+        cd("exp1.tmp")
580
 
+        assert listdir_sorted(".") == [ "link1" ]
581
 
+        assert os.readlink("link1") == "NOWHERE1"
582
 
+        cd("..")
583
 
+        
584
 
+        runcmd(['bzr', 'export', '-r', '2', 'exp2.tmp'])
585
 
+        cd("exp2.tmp")
586
 
+        assert listdir_sorted(".") == [ "d1", "link1" ]
587
 
+        cd("..")
588
 
+        
589
 
+        runcmd(['bzr', 'export', '-r', '3', 'exp3.tmp'])
590
 
+        cd("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"
595
 
+        cd("..")
596
 
+        
597
 
+        runcmd(['bzr', 'export', '-r', '4', 'exp4.tmp'])
598
 
+        cd("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" ]
603
 
+        cd("..")
604
 
+        
605
 
+        runcmd(['bzr', 'export', '-r', '5', 'exp5.tmp'])
606
 
+        cd("exp5.tmp")
607
 
+        assert listdir_sorted(".") == [ "d2", "link2" ]
608
 
+        assert os.path.islink("link2")
609
 
+        assert listdir_sorted("d2")== [ "link3" ]
610
 
+        cd("..")
611
 
+        
612
 
+        runcmd(['bzr', 'export', '-r', '6', 'exp6.tmp'])
613
 
+        cd("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"
618
 
+        cd("..")
619
 
+
620
 
+        cd("..")
621
 
+    else:
622
 
+        progress("skipping symlink tests")
623
 
+        
624
 
     progress("all tests passed!")
625
 
 except Exception, e:
626
 
     sys.stderr.write('*' * 50 + '\n'
627
 
 
628
 
 
629
 
--=-=-=--