~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to patches/symlink-support.patch

  • Committer: Martin Pool
  • Date: 2005-09-09 09:44:03 UTC
  • Revision ID: mbp@sourcefrog.net-20050909094403-ddad5896b0b12c68
- weave commit records per-file ancestors

 - commits of merges are currently forbidden

 - files that existed in the previous revision are recorded with that 
   parent

 'weave annotate' on woven files now gives the correct result!

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
--=-=-=--