1
# (C) 2005 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
# TODO: Maybe also keep the full path of the entry, and the children?
19
# But those depend on its position within a particular inventory, and
20
# it would be nice not to need to hold the backpointer here.
22
# TODO: Perhaps split InventoryEntry into subclasses for files,
23
# directories, etc etc.
26
# This should really be an id randomly assigned when the tree is
27
# created, but it's not for now.
31
import sys, os.path, types, re
34
from bzrlib.errors import BzrError, BzrCheckError
36
from bzrlib.osutils import uuid, quotefn, splitpath, joinpath, appendpath
37
from bzrlib.trace import mutter
38
from bzrlib.errors import NotVersionedError
41
class InventoryEntry(object):
42
"""Description of a versioned file.
44
An InventoryEntry has the following fields, which are also
45
present in the XML inventory-entry element:
50
(within the parent directory)
56
file_id of the parent directory, or ROOT_ID
59
the revision_id in which the name or parent of this file was
63
sha-1 of the text of the file
66
size in bytes of the text of the file
69
the revision_id in which the text of this file was introduced
71
(reading a version 4 tree created a text_id field.)
76
>>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
77
InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
78
>>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
79
InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
80
>>> for j in i.iter_entries():
83
('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
84
('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
85
>>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
86
Traceback (most recent call last):
88
BzrError: inventory already contains entry with id {2323}
89
>>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
90
InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
91
>>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
92
InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
93
>>> i.path2id('src/wibble')
97
>>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
98
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
100
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
101
>>> for j in i.iter_entries():
103
... assert i.path2id(j[0])
110
>>> i.id2path('2326')
111
'src/wibble/wibble.c'
114
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
115
'text_id', 'parent_id', 'children',
116
'text_version', 'name_version', ]
119
def __init__(self, file_id, name, kind, parent_id, text_id=None):
120
"""Create an InventoryEntry
122
The filename must be a single component, relative to the
123
parent directory; it cannot be a whole path or relative name.
125
>>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
130
>>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
131
Traceback (most recent call last):
132
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
134
assert isinstance(name, basestring), name
135
if '/' in name or '\\' in name:
136
raise BzrCheckError('InventoryEntry name %r is invalid' % name)
138
self.text_version = None
139
self.name_version = None
140
self.text_sha1 = None
141
self.text_size = None
142
self.file_id = file_id
145
self.text_id = text_id
146
self.parent_id = parent_id
147
if kind == 'directory':
152
raise BzrError("unhandled entry kind %r" % kind)
156
def sorted_children(self):
157
l = self.children.items()
163
other = InventoryEntry(self.file_id, self.name, self.kind,
165
other.text_id = self.text_id
166
other.text_sha1 = self.text_sha1
167
other.text_size = self.text_size
168
other.text_version = self.text_version
169
other.name_version = self.name_version
170
# note that children are *not* copied; they're pulled across when
176
return ("%s(%r, %r, kind=%r, parent_id=%r)"
177
% (self.__class__.__name__,
184
def __eq__(self, other):
185
if not isinstance(other, InventoryEntry):
186
return NotImplemented
188
return (self.file_id == other.file_id) \
189
and (self.name == other.name) \
190
and (self.text_sha1 == other.text_sha1) \
191
and (self.text_size == other.text_size) \
192
and (self.text_id == other.text_id) \
193
and (self.parent_id == other.parent_id) \
194
and (self.kind == other.kind) \
195
and (self.text_version == other.text_version) \
196
and (self.name_version == other.name_version)
199
def __ne__(self, other):
200
return not (self == other)
203
raise ValueError('not hashable')
207
class RootEntry(InventoryEntry):
208
def __init__(self, file_id):
209
self.file_id = file_id
211
self.kind = 'root_directory'
212
self.parent_id = None
215
def __eq__(self, other):
216
if not isinstance(other, RootEntry):
217
return NotImplemented
219
return (self.file_id == other.file_id) \
220
and (self.children == other.children)
224
class Inventory(object):
225
"""Inventory of versioned files in a tree.
227
This describes which file_id is present at each point in the tree,
228
and possibly the SHA-1 or other information about the file.
229
Entries can be looked up either by path or by file_id.
231
The inventory represents a typical unix file tree, with
232
directories containing files and subdirectories. We never store
233
the full path to a file, because renaming a directory implicitly
234
moves all of its contents. This class internally maintains a
235
lookup tree that allows the children under a directory to be
238
InventoryEntry objects must not be modified after they are
239
inserted, other than through the Inventory API.
241
>>> inv = Inventory()
242
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
243
InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
244
>>> inv['123-123'].name
247
May be treated as an iterator or set to look up file ids:
249
>>> bool(inv.path2id('hello.c'))
254
May also look up by name:
256
>>> [x[0] for x in inv.iter_entries()]
258
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
259
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
260
InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
262
def __init__(self, root_id=ROOT_ID):
263
"""Create or read an inventory.
265
If a working directory is specified, the inventory is read
266
from there. If the file is specified, read from that. If not,
267
the inventory is created empty.
269
The inventory is created with a default root directory, with
272
# We are letting Branch(init=True) create a unique inventory
273
# root id. Rather than generating a random one here.
275
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
276
self.root = RootEntry(root_id)
277
self._byid = {self.root.file_id: self.root}
281
other = Inventory(self.root.file_id)
282
# copy recursively so we know directories will be added before
283
# their children. There are more efficient ways than this...
284
for path, entry in self.iter_entries():
285
if entry == self.root:
287
other.add(entry.copy())
292
return iter(self._byid)
296
"""Returns number of entries."""
297
return len(self._byid)
300
def iter_entries(self, from_dir=None):
301
"""Return (path, entry) pairs, in order by name."""
305
elif isinstance(from_dir, basestring):
306
from_dir = self._byid[from_dir]
308
kids = from_dir.children.items()
310
for name, ie in kids:
312
if ie.kind == 'directory':
313
for cn, cie in self.iter_entries(from_dir=ie.file_id):
314
yield os.path.join(name, cn), cie
318
"""Return list of (path, ie) for all entries except the root.
320
This may be faster than iter_entries.
323
def descend(dir_ie, dir_path):
324
kids = dir_ie.children.items()
326
for name, ie in kids:
327
child_path = os.path.join(dir_path, name)
328
accum.append((child_path, ie))
329
if ie.kind == 'directory':
330
descend(ie, child_path)
332
descend(self.root, '')
336
def directories(self):
337
"""Return (path, entry) pairs for all directories, including the root.
340
def descend(parent_ie, parent_path):
341
accum.append((parent_path, parent_ie))
343
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
346
for name, child_ie in kids:
347
child_path = os.path.join(parent_path, name)
348
descend(child_ie, child_path)
349
descend(self.root, '')
354
def __contains__(self, file_id):
355
"""True if this entry contains a file with given id.
357
>>> inv = Inventory()
358
>>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
359
InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
365
return file_id in self._byid
368
def __getitem__(self, file_id):
369
"""Return the entry for given file_id.
371
>>> inv = Inventory()
372
>>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
373
InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
374
>>> inv['123123'].name
378
return self._byid[file_id]
381
raise BzrError("can't look up file_id None")
383
raise BzrError("file_id {%s} not in inventory" % file_id)
386
def get_file_kind(self, file_id):
387
return self._byid[file_id].kind
389
def get_child(self, parent_id, filename):
390
return self[parent_id].children.get(filename)
393
def add(self, entry):
394
"""Add entry to inventory.
396
To add a file to a branch ready to be committed, use Branch.add,
399
Returns the new entry object.
401
if entry.file_id in self._byid:
402
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
404
if entry.parent_id == ROOT_ID or entry.parent_id is None:
405
entry.parent_id = self.root.file_id
408
parent = self._byid[entry.parent_id]
410
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
412
if parent.children.has_key(entry.name):
413
raise BzrError("%s is already versioned" %
414
appendpath(self.id2path(parent.file_id), entry.name))
416
self._byid[entry.file_id] = entry
417
parent.children[entry.name] = entry
421
def add_path(self, relpath, kind, file_id=None):
422
"""Add entry from a path.
424
The immediate parent must already be versioned.
426
Returns the new entry object."""
427
from bzrlib.branch import gen_file_id
429
parts = bzrlib.osutils.splitpath(relpath)
431
raise BzrError("cannot re-add root of inventory")
434
file_id = gen_file_id(relpath)
436
parent_path = parts[:-1]
437
parent_id = self.path2id(parent_path)
438
if parent_id == None:
439
raise NotVersionedError(parent_path)
441
ie = InventoryEntry(file_id, parts[-1],
442
kind=kind, parent_id=parent_id)
446
def __delitem__(self, file_id):
447
"""Remove entry by id.
449
>>> inv = Inventory()
450
>>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
451
InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
460
assert self[ie.parent_id].children[ie.name] == ie
462
# TODO: Test deleting all children; maybe hoist to a separate
464
if ie.kind == 'directory':
465
for cie in ie.children.values():
466
del self[cie.file_id]
469
del self._byid[file_id]
470
del self[ie.parent_id].children[ie.name]
473
def __eq__(self, other):
474
"""Compare two sets by comparing their contents.
480
>>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
481
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
484
>>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
485
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
489
if not isinstance(other, Inventory):
490
return NotImplemented
492
if len(self._byid) != len(other._byid):
493
# shortcut: obviously not the same
496
return self._byid == other._byid
499
def __ne__(self, other):
500
return not self.__eq__(other)
504
raise ValueError('not hashable')
507
def get_idpath(self, file_id):
508
"""Return a list of file_ids for the path to an entry.
510
The list contains one element for each directory followed by
511
the id of the file itself. So the length of the returned list
512
is equal to the depth of the file in the tree, counting the
513
root directory as depth 1.
516
while file_id != None:
518
ie = self._byid[file_id]
520
raise BzrError("file_id {%s} not found in inventory" % file_id)
521
p.insert(0, ie.file_id)
522
file_id = ie.parent_id
526
def id2path(self, file_id):
527
"""Return as a list the path to file_id."""
529
# get all names, skipping root
530
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
531
return os.sep.join(p)
535
def path2id(self, name):
536
"""Walk down through directories to return entry of last component.
538
names may be either a list of path components, or a single
539
string, in which case it is automatically split.
541
This returns the entry of the last component in the path,
542
which may be either a file or a directory.
544
Returns None iff the path is not found.
546
if isinstance(name, types.StringTypes):
547
name = splitpath(name)
549
mutter("lookup path %r" % name)
554
cie = parent.children[f]
556
assert cie.parent_id == parent.file_id
562
return parent.file_id
565
def has_filename(self, names):
566
return bool(self.path2id(names))
569
def has_id(self, file_id):
570
return self._byid.has_key(file_id)
573
def rename(self, file_id, new_parent_id, new_name):
574
"""Move a file within the inventory.
576
This can change either the name, or the parent, or both.
578
This does not move the working file."""
579
if not is_valid_name(new_name):
580
raise BzrError("not an acceptable filename: %r" % new_name)
582
new_parent = self._byid[new_parent_id]
583
if new_name in new_parent.children:
584
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
586
new_parent_idpath = self.get_idpath(new_parent_id)
587
if file_id in new_parent_idpath:
588
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
589
% (self.id2path(file_id), self.id2path(new_parent_id)))
591
file_ie = self._byid[file_id]
592
old_parent = self._byid[file_ie.parent_id]
594
# TODO: Don't leave things messed up if this fails
596
del old_parent.children[file_ie.name]
597
new_parent.children[new_name] = file_ie
599
file_ie.name = new_name
600
file_ie.parent_id = new_parent_id
607
def is_valid_name(name):
610
_NAME_RE = re.compile(r'^[^/\\]+$')
612
return bool(_NAME_RE.match(name))