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
# This should really be an id randomly assigned when the tree is
19
# created, but it's not for now.
29
from bzrlib.errors import BzrError, BzrCheckError
31
from bzrlib.osutils import quotefn, splitpath, joinpath, appendpath
32
from bzrlib.trace import mutter
33
from bzrlib.errors import NotVersionedError
36
class InventoryEntry(object):
37
"""Description of a versioned file.
39
An InventoryEntry has the following fields, which are also
40
present in the XML inventory-entry element:
43
* *name*: (only the basename within the directory, must not
45
* *kind*: "directory" or "file"
46
* *directory_id*: (if absent/null means the branch root directory)
47
* *text_sha1*: only for files
48
* *text_size*: in bytes, only for files
49
* *text_id*: identifier for the text version, only for files
51
InventoryEntries can also exist inside a WorkingTree
52
inventory, in which case they are not yet bound to a
53
particular revision of the file. In that case the text_sha1,
54
text_size and text_id are absent.
60
>>> i.add(InventoryEntry('123', 'src', 'directory', ROOT_ID))
61
InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT')
62
>>> i.add(InventoryEntry('2323', 'hello.c', 'file', parent_id='123'))
63
InventoryEntry('2323', 'hello.c', kind='file', parent_id='123')
64
>>> for j in i.iter_entries():
67
('src', InventoryEntry('123', 'src', kind='directory', parent_id='TREE_ROOT'))
68
('src/hello.c', InventoryEntry('2323', 'hello.c', kind='file', parent_id='123'))
69
>>> i.add(InventoryEntry('2323', 'bye.c', 'file', '123'))
70
Traceback (most recent call last):
72
BzrError: inventory already contains entry with id {2323}
73
>>> i.add(InventoryEntry('2324', 'bye.c', 'file', '123'))
74
InventoryEntry('2324', 'bye.c', kind='file', parent_id='123')
75
>>> i.add(InventoryEntry('2325', 'wibble', 'directory', '123'))
76
InventoryEntry('2325', 'wibble', kind='directory', parent_id='123')
77
>>> i.path2id('src/wibble')
81
>>> i.add(InventoryEntry('2326', 'wibble.c', 'file', '2325'))
82
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
84
InventoryEntry('2326', 'wibble.c', kind='file', parent_id='2325')
85
>>> for path, entry in i.iter_entries():
86
... print path.replace('\\\\', '/') # for win32 os.sep
87
... assert i.path2id(path)
94
>>> i.id2path('2326').replace('\\\\', '/')
97
TODO: Maybe also keep the full path of the entry, and the children?
98
But those depend on its position within a particular inventory, and
99
it would be nice not to need to hold the backpointer here.
102
# TODO: split InventoryEntry into subclasses for files,
103
# directories, etc etc.
105
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
106
'text_id', 'parent_id', 'children',
107
'text_version', 'entry_version', ]
110
def __init__(self, file_id, name, kind, parent_id, text_id=None):
111
"""Create an InventoryEntry
113
The filename must be a single component, relative to the
114
parent directory; it cannot be a whole path or relative name.
116
>>> e = InventoryEntry('123', 'hello.c', 'file', ROOT_ID)
121
>>> e = InventoryEntry('123', 'src/hello.c', 'file', ROOT_ID)
122
Traceback (most recent call last):
123
BzrCheckError: InventoryEntry name 'src/hello.c' is invalid
125
assert isinstance(name, basestring), name
126
if '/' in name or '\\' in name:
127
raise BzrCheckError('InventoryEntry name %r is invalid' % name)
129
self.text_version = None
130
self.entry_version = None
131
self.text_sha1 = None
132
self.text_size = None
133
self.file_id = file_id
136
self.text_id = text_id
137
self.parent_id = parent_id
138
if kind == 'directory':
143
raise BzrError("unhandled entry kind %r" % kind)
147
def sorted_children(self):
148
l = self.children.items()
154
other = InventoryEntry(self.file_id, self.name, self.kind,
155
self.parent_id, text_id=self.text_id)
156
other.text_sha1 = self.text_sha1
157
other.text_size = self.text_size
158
# note that children are *not* copied; they're pulled across when
164
return ("%s(%r, %r, kind=%r, parent_id=%r)"
165
% (self.__class__.__name__,
172
def __eq__(self, other):
173
if not isinstance(other, InventoryEntry):
174
return NotImplemented
176
return (self.file_id == other.file_id) \
177
and (self.name == other.name) \
178
and (self.text_sha1 == other.text_sha1) \
179
and (self.text_size == other.text_size) \
180
and (self.text_id == other.text_id) \
181
and (self.parent_id == other.parent_id) \
182
and (self.kind == other.kind) \
183
and (self.text_version == other.text_version) \
184
and (self.entry_version == other.entry_version)
187
def __ne__(self, other):
188
return not (self == other)
191
raise ValueError('not hashable')
195
class RootEntry(InventoryEntry):
196
def __init__(self, file_id):
197
self.file_id = file_id
199
self.kind = 'root_directory'
200
self.parent_id = None
203
def __eq__(self, other):
204
if not isinstance(other, RootEntry):
205
return NotImplemented
207
return (self.file_id == other.file_id) \
208
and (self.children == other.children)
212
class Inventory(object):
213
"""Inventory of versioned files in a tree.
215
This describes which file_id is present at each point in the tree,
216
and possibly the SHA-1 or other information about the file.
217
Entries can be looked up either by path or by file_id.
219
The inventory represents a typical unix file tree, with
220
directories containing files and subdirectories. We never store
221
the full path to a file, because renaming a directory implicitly
222
moves all of its contents. This class internally maintains a
223
lookup tree that allows the children under a directory to be
226
InventoryEntry objects must not be modified after they are
227
inserted, other than through the Inventory API.
229
>>> inv = Inventory()
230
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
231
InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT')
232
>>> inv['123-123'].name
235
May be treated as an iterator or set to look up file ids:
237
>>> bool(inv.path2id('hello.c'))
242
May also look up by name:
244
>>> [x[0] for x in inv.iter_entries()]
246
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
247
>>> inv.add(InventoryEntry('123-123', 'hello.c', 'file', ROOT_ID))
248
InventoryEntry('123-123', 'hello.c', kind='file', parent_id='TREE_ROOT-12345678-12345678')
250
def __init__(self, root_id=ROOT_ID):
251
"""Create or read an inventory.
253
If a working directory is specified, the inventory is read
254
from there. If the file is specified, read from that. If not,
255
the inventory is created empty.
257
The inventory is created with a default root directory, with
260
# We are letting Branch.initialize() create a unique inventory
261
# root id. Rather than generating a random one here.
263
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
264
self.root = RootEntry(root_id)
265
self._byid = {self.root.file_id: self.root}
269
return iter(self._byid)
273
"""Returns number of entries."""
274
return len(self._byid)
277
def iter_entries(self, from_dir=None):
278
"""Return (path, entry) pairs, in order by name."""
282
elif isinstance(from_dir, basestring):
283
from_dir = self._byid[from_dir]
285
kids = from_dir.children.items()
287
for name, ie in kids:
289
if ie.kind == 'directory':
290
for cn, cie in self.iter_entries(from_dir=ie.file_id):
291
yield os.path.join(name, cn), cie
295
"""Return list of (path, ie) for all entries except the root.
297
This may be faster than iter_entries.
300
def descend(dir_ie, dir_path):
301
kids = dir_ie.children.items()
303
for name, ie in kids:
304
child_path = os.path.join(dir_path, name)
305
accum.append((child_path, ie))
306
if ie.kind == 'directory':
307
descend(ie, child_path)
309
descend(self.root, '')
313
def directories(self):
314
"""Return (path, entry) pairs for all directories, including the root.
317
def descend(parent_ie, parent_path):
318
accum.append((parent_path, parent_ie))
320
kids = [(ie.name, ie) for ie in parent_ie.children.itervalues() if ie.kind == 'directory']
323
for name, child_ie in kids:
324
child_path = os.path.join(parent_path, name)
325
descend(child_ie, child_path)
326
descend(self.root, '')
331
def __contains__(self, file_id):
332
"""True if this entry contains a file with given id.
334
>>> inv = Inventory()
335
>>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
336
InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
342
return file_id in self._byid
345
def __getitem__(self, file_id):
346
"""Return the entry for given file_id.
348
>>> inv = Inventory()
349
>>> inv.add(InventoryEntry('123123', 'hello.c', 'file', ROOT_ID))
350
InventoryEntry('123123', 'hello.c', kind='file', parent_id='TREE_ROOT')
351
>>> inv['123123'].name
355
return self._byid[file_id]
358
raise BzrError("can't look up file_id None")
360
raise BzrError("file_id {%s} not in inventory" % file_id)
363
def get_file_kind(self, file_id):
364
return self._byid[file_id].kind
366
def get_child(self, parent_id, filename):
367
return self[parent_id].children.get(filename)
370
def add(self, entry):
371
"""Add entry to inventory.
373
To add a file to a branch ready to be committed, use Branch.add,
376
Returns the new entry object.
378
if entry.file_id in self._byid:
379
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
381
if entry.parent_id == ROOT_ID or entry.parent_id is None:
382
entry.parent_id = self.root.file_id
385
parent = self._byid[entry.parent_id]
387
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
389
if parent.children.has_key(entry.name):
390
raise BzrError("%s is already versioned" %
391
appendpath(self.id2path(parent.file_id), entry.name))
393
self._byid[entry.file_id] = entry
394
parent.children[entry.name] = entry
398
def add_path(self, relpath, kind, file_id=None):
399
"""Add entry from a path.
401
The immediate parent must already be versioned.
403
Returns the new entry object."""
404
from bzrlib.branch import gen_file_id
406
parts = bzrlib.osutils.splitpath(relpath)
408
raise BzrError("cannot re-add root of inventory")
411
file_id = gen_file_id(relpath)
413
parent_path = parts[:-1]
414
parent_id = self.path2id(parent_path)
415
if parent_id == None:
416
raise NotVersionedError(parent_path)
418
ie = InventoryEntry(file_id, parts[-1],
419
kind=kind, parent_id=parent_id)
423
def __delitem__(self, file_id):
424
"""Remove entry by id.
426
>>> inv = Inventory()
427
>>> inv.add(InventoryEntry('123', 'foo.c', 'file', ROOT_ID))
428
InventoryEntry('123', 'foo.c', kind='file', parent_id='TREE_ROOT')
437
assert self[ie.parent_id].children[ie.name] == ie
439
# TODO: Test deleting all children; maybe hoist to a separate
441
if ie.kind == 'directory':
442
for cie in ie.children.values():
443
del self[cie.file_id]
446
del self._byid[file_id]
447
del self[ie.parent_id].children[ie.name]
450
def __eq__(self, other):
451
"""Compare two sets by comparing their contents.
457
>>> i1.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
458
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
461
>>> i2.add(InventoryEntry('123', 'foo', 'file', ROOT_ID))
462
InventoryEntry('123', 'foo', kind='file', parent_id='TREE_ROOT')
466
if not isinstance(other, Inventory):
467
return NotImplemented
469
if len(self._byid) != len(other._byid):
470
# shortcut: obviously not the same
473
return self._byid == other._byid
476
def __ne__(self, other):
477
return not (self == other)
481
raise ValueError('not hashable')
484
def get_idpath(self, file_id):
485
"""Return a list of file_ids for the path to an entry.
487
The list contains one element for each directory followed by
488
the id of the file itself. So the length of the returned list
489
is equal to the depth of the file in the tree, counting the
490
root directory as depth 1.
493
while file_id != None:
495
ie = self._byid[file_id]
497
raise BzrError("file_id {%s} not found in inventory" % file_id)
498
p.insert(0, ie.file_id)
499
file_id = ie.parent_id
503
def id2path(self, file_id):
504
"""Return as a list the path to file_id."""
506
# get all names, skipping root
507
p = [self._byid[fid].name for fid in self.get_idpath(file_id)[1:]]
508
return os.sep.join(p)
512
def path2id(self, name):
513
"""Walk down through directories to return entry of last component.
515
names may be either a list of path components, or a single
516
string, in which case it is automatically split.
518
This returns the entry of the last component in the path,
519
which may be either a file or a directory.
521
Returns None iff the path is not found.
523
if isinstance(name, types.StringTypes):
524
name = splitpath(name)
526
mutter("lookup path %r" % name)
531
cie = parent.children[f]
533
assert cie.parent_id == parent.file_id
539
return parent.file_id
542
def has_filename(self, names):
543
return bool(self.path2id(names))
546
def has_id(self, file_id):
547
return self._byid.has_key(file_id)
550
def rename(self, file_id, new_parent_id, new_name):
551
"""Move a file within the inventory.
553
This can change either the name, or the parent, or both.
555
This does not move the working file."""
556
if not is_valid_name(new_name):
557
raise BzrError("not an acceptable filename: %r" % new_name)
559
new_parent = self._byid[new_parent_id]
560
if new_name in new_parent.children:
561
raise BzrError("%r already exists in %r" % (new_name, self.id2path(new_parent_id)))
563
new_parent_idpath = self.get_idpath(new_parent_id)
564
if file_id in new_parent_idpath:
565
raise BzrError("cannot move directory %r into a subdirectory of itself, %r"
566
% (self.id2path(file_id), self.id2path(new_parent_id)))
568
file_ie = self._byid[file_id]
569
old_parent = self._byid[file_ie.parent_id]
571
# TODO: Don't leave things messed up if this fails
573
del old_parent.children[file_ie.name]
574
new_parent.children[new_name] = file_ie
576
file_ie.name = new_name
577
file_ie.parent_id = new_parent_id
584
def is_valid_name(name):
587
_NAME_RE = re.compile(r'^[^/\\]+$')
589
return bool(_NAME_RE.match(name))