1
# Copyright (C) 2005, 2006 Canonical Ltd
1
# Copyright (C) 2005, 2006, 2007 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
27
27
# created, but it's not for now.
28
28
ROOT_ID = "TREE_ROOT"
34
from bzrlib.lazy_import import lazy_import
35
lazy_import(globals(), """
39
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
40
pathjoin, sha_strings)
41
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
42
BzrError, BzrCheckError, BinaryFile)
49
from bzrlib.errors import (
43
53
from bzrlib.trace import mutter
80
90
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
81
91
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
82
92
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
83
>>> shouldbe = {0: 'src', 1: pathjoin('src','hello.c')}
93
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
84
94
>>> for ix, j in enumerate(i.iter_entries()):
85
95
... print (j[0] == shouldbe[ix], j[1])
97
(True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
87
98
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
88
99
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
89
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
90
Traceback (most recent call last):
92
BzrError: inventory already contains entry with id {2323}
93
100
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
94
101
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
95
102
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
185
193
if self.file_id in inv:
186
194
ie = inv[self.file_id]
187
195
assert ie.file_id == self.file_id
196
if ie.kind != self.kind:
197
# Can't be a candidate if the kind has changed.
188
199
if ie.revision in candidates:
189
200
# same revision value in two different inventories:
190
201
# correct possible inconsistencies:
243
254
def get_tar_item(self, root, dp, now, tree):
244
255
"""Get a tarfile item and a file stream for its content."""
245
item = tarfile.TarInfo(pathjoin(root, dp))
256
item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
246
257
# TODO: would be cool to actually set it to the timestamp of the
247
258
# revision it was last changed
278
289
assert isinstance(name, basestring), name
279
290
if '/' in name or '\\' in name:
280
raise InvalidEntryName(name=name)
291
raise errors.InvalidEntryName(name=name)
281
292
self.executable = False
282
293
self.revision = None
283
294
self.text_sha1 = None
284
295
self.text_size = None
285
296
self.file_id = file_id
297
assert isinstance(file_id, (str, None.__class__)), \
298
'bad type %r for %r' % (type(file_id), file_id)
287
300
self.text_id = text_id
288
301
self.parent_id = parent_id
289
302
self.symlink_target = None
303
self.reference_revision = None
291
305
def kind_character(self):
292
306
"""Return a short kind indicator useful for appending to names."""
293
307
raise BzrError('unknown kind %r' % self.kind)
295
known_kinds = ('file', 'directory', 'symlink', 'root_directory')
309
known_kinds = ('file', 'directory', 'symlink')
297
311
def _put_in_tar(self, item, tree):
298
312
"""populate item for stashing in a tar, and return the content stream.
308
322
This is a template method - implement _put_on_disk in subclasses.
310
fullpath = pathjoin(dest, dp)
324
fullpath = osutils.pathjoin(dest, dp)
311
325
self._put_on_disk(fullpath, tree)
312
mutter(" export {%s} kind %s to %s", self.file_id,
326
# mutter(" export {%s} kind %s to %s", self.file_id,
327
# self.kind, fullpath)
315
329
def _put_on_disk(self, fullpath, tree):
316
330
"""Put this entry onto disk at fullpath, from tree tree."""
323
337
def versionable_kind(kind):
324
return kind in ('file', 'directory', 'symlink')
338
return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
326
340
def check(self, checker, rev_id, inv, tree):
327
341
"""Check this inventory entry is intact.
407
423
This means that all its fields are populated, that it has its
408
424
text stored in the text store or weave.
410
mutter('new parents of %s are %r', path, previous_entries)
426
# mutter('new parents of %s are %r', path, previous_entries)
411
427
self._read_tree_state(path, work_tree)
412
428
# TODO: Where should we determine whether to reuse a
413
429
# previous revision id or create a new revision? 20060606
415
431
# cannot be unchanged unless there is only one parent file rev.
416
432
parent_ie = previous_entries.values()[0]
417
433
if self._unchanged(parent_ie):
418
mutter("found unchanged entry")
434
# mutter("found unchanged entry")
419
435
self.revision = parent_ie.revision
420
436
return "unchanged"
421
437
return self._snapshot_into_revision(revision, previous_entries,
433
449
:returns: String description of the commit (e.g. "merged", "modified"), etc.
435
mutter('new revision {%s} for {%s}', revision, self.file_id)
451
# mutter('new revision {%s} for {%s}', revision, self.file_id)
436
452
self.revision = revision
437
453
self._snapshot_text(previous_entries, work_tree, commit_builder)
497
516
class RootEntry(InventoryEntry):
499
518
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
500
'text_id', 'parent_id', 'children', 'executable',
501
'revision', 'symlink_target']
519
'text_id', 'parent_id', 'children', 'executable',
520
'revision', 'symlink_target', 'reference_revision']
503
522
def _check(self, checker, rev_id, tree):
504
523
"""See InventoryEntry._check"""
506
525
def __init__(self, file_id):
507
526
self.file_id = file_id
508
527
self.children = {}
509
self.kind = 'root_directory'
528
self.kind = 'directory'
510
529
self.parent_id = None
512
531
self.revision = None
532
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
533
' Please use InventoryDirectory instead.',
534
DeprecationWarning, stacklevel=2)
514
536
def __eq__(self, other):
515
537
if not isinstance(other, RootEntry):
523
545
"""A directory in an inventory."""
525
547
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
526
'text_id', 'parent_id', 'children', 'executable',
527
'revision', 'symlink_target']
548
'text_id', 'parent_id', 'children', 'executable',
549
'revision', 'symlink_target', 'reference_revision']
529
551
def _check(self, checker, rev_id, tree):
530
552
"""See InventoryEntry._check"""
570
592
"""A file in an inventory."""
572
594
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
573
'text_id', 'parent_id', 'children', 'executable',
574
'revision', 'symlink_target']
595
'text_id', 'parent_id', 'children', 'executable',
596
'revision', 'symlink_target', 'reference_revision']
576
598
def _check(self, checker, tree_revision_id, tree):
577
599
"""See InventoryEntry._check"""
639
661
text_diff(to_label, to_text,
640
662
from_label, from_text, output_to)
663
except errors.BinaryFile:
643
665
label_pair = (to_label, from_label)
671
693
def _put_on_disk(self, fullpath, tree):
672
694
"""See InventoryEntry._put_on_disk."""
673
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
695
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
674
696
if tree.is_executable(self.file_id):
675
697
os.chmod(fullpath, 0755)
693
715
def _forget_tree_state(self):
694
716
self.text_sha1 = None
695
self.executable = None
697
718
def _snapshot_text(self, file_parents, work_tree, commit_builder):
698
719
"""See InventoryEntry._snapshot_text."""
719
740
"""A file in an inventory."""
721
742
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
722
'text_id', 'parent_id', 'children', 'executable',
723
'revision', 'symlink_target']
743
'text_id', 'parent_id', 'children', 'executable',
744
'revision', 'symlink_target', 'reference_revision']
725
746
def _check(self, checker, rev_id, tree):
726
747
"""See InventoryEntry._check"""
807
828
self.file_id, file_parents, self.symlink_target)
831
class TreeReference(InventoryEntry):
833
kind = 'tree-reference'
835
def __init__(self, file_id, name, parent_id, revision=None,
836
reference_revision=None):
837
InventoryEntry.__init__(self, file_id, name, parent_id)
838
self.revision = revision
839
self.reference_revision = reference_revision
842
return TreeReference(self.file_id, self.name, self.parent_id,
843
self.revision, self.reference_revision)
845
def _snapshot_text(self, file_parents, work_tree, commit_builder):
846
commit_builder.modified_reference(self.file_id, file_parents)
848
def _read_tree_state(self, path, work_tree):
849
"""Populate fields in the inventory entry from the given tree.
851
self.reference_revision = work_tree.get_reference_revision(
854
def _forget_tree_state(self):
855
self.reference_revision = None
810
858
class Inventory(object):
811
859
"""Inventory of versioned files in a tree.
840
888
May also look up by name:
842
890
>>> [x[0] for x in inv.iter_entries()]
844
892
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
845
893
>>> inv.add(InventoryFile('123-123', 'hello.c', ROOT_ID))
894
Traceback (most recent call last):
895
BzrError: parent_id {TREE_ROOT} not in inventory
896
>>> inv.add(InventoryFile('123-123', 'hello.c', 'TREE_ROOT-12345678-12345678'))
846
897
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
848
899
def __init__(self, root_id=ROOT_ID, revision_id=None):
855
906
The inventory is created with a default root directory, with
858
# We are letting Branch.create() create a unique inventory
859
# root id. Rather than generating a random one here.
861
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
862
self.root = RootEntry(root_id)
863
# FIXME: this isn't ever used, changing it to self.revision may break
864
# things. TODO make everything use self.revision_id
909
if root_id is not None:
910
assert root_id.__class__ == str
911
self._set_root(InventoryDirectory(root_id, u'', None))
865
915
self.revision_id = revision_id
917
def _set_root(self, ie):
866
919
self._byid = {self.root.file_id: self.root}
869
922
# TODO: jam 20051218 Should copy also copy the revision_id?
870
other = Inventory(self.root.file_id)
923
entries = self.iter_entries()
924
other = Inventory(entries.next()[1].file_id)
871
925
# copy recursively so we know directories will be added before
872
926
# their children. There are more efficient ways than this...
873
for path, entry in self.iter_entries():
874
if entry == self.root:
927
for path, entry in entries():
876
928
other.add(entry.copy())
886
938
def iter_entries(self, from_dir=None):
887
939
"""Return (path, entry) pairs, in order by name."""
888
940
if from_dir is None:
941
if self.root is None:
890
943
from_dir = self.root
891
945
elif isinstance(from_dir, basestring):
892
946
from_dir = self._byid[from_dir]
925
979
# if we finished all children, pop it off the stack
928
def iter_entries_by_dir(self, from_dir=None):
982
def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
929
983
"""Iterate over the entries in a directory first order.
931
985
This returns all entries for a directory before returning
936
990
:return: This yields (path, entry) pairs
992
if specific_file_ids:
993
safe = osutils.safe_file_id
994
specific_file_ids = set(safe(fid) for fid in specific_file_ids)
938
995
# TODO? Perhaps this should return the from_dir so that the root is
939
996
# yielded? or maybe an option?
940
997
if from_dir is None:
998
if self.root is None:
1000
# Optimize a common case
1001
if specific_file_ids is not None and len(specific_file_ids) == 1:
1002
file_id = list(specific_file_ids)[0]
1004
yield self.id2path(file_id), self[file_id]
942
1006
from_dir = self.root
1007
if (specific_file_ids is None or
1008
self.root.file_id in specific_file_ids):
1009
yield u'', self.root
943
1010
elif isinstance(from_dir, basestring):
944
1011
from_dir = self._byid[from_dir]
1013
if specific_file_ids is not None:
1014
# TODO: jam 20070302 This could really be done as a loop rather
1015
# than a bunch of recursive calls.
1018
def add_ancestors(file_id):
1019
if file_id not in byid:
1021
parent_id = byid[file_id].parent_id
1022
if parent_id is None:
1024
if parent_id not in parents:
1025
parents.add(parent_id)
1026
add_ancestors(parent_id)
1027
for file_id in specific_file_ids:
1028
add_ancestors(file_id)
946
1032
stack = [(u'', from_dir)]
953
1039
child_relpath = cur_relpath + child_name
955
yield child_relpath, child_ie
1041
if (specific_file_ids is None or
1042
child_ie.file_id in specific_file_ids):
1043
yield child_relpath, child_ie
957
1045
if child_ie.kind == 'directory':
958
child_dirs.append((child_relpath+'/', child_ie))
1046
if parents is None or child_ie.file_id in parents:
1047
child_dirs.append((child_relpath+'/', child_ie))
959
1048
stack.extend(reversed(child_dirs))
961
1050
def entries(self):
968
1057
kids = dir_ie.children.items()
970
1059
for name, ie in kids:
971
child_path = pathjoin(dir_path, name)
1060
child_path = osutils.pathjoin(dir_path, name)
972
1061
accum.append((child_path, ie))
973
1062
if ie.kind == 'directory':
974
1063
descend(ie, child_path)
989
1078
for name, child_ie in kids:
990
child_path = pathjoin(parent_path, name)
1079
child_path = osutils.pathjoin(parent_path, name)
991
1080
descend(child_ie, child_path)
992
1081
descend(self.root, u'')
1003
1092
>>> '456' in inv
1006
return file_id in self._byid
1095
file_id = osutils.safe_file_id(file_id)
1096
return (file_id in self._byid)
1008
1098
def __getitem__(self, file_id):
1009
1099
"""Return the entry for given file_id.
1014
1104
>>> inv['123123'].name
1107
file_id = osutils.safe_file_id(file_id)
1018
1109
return self._byid[file_id]
1019
1110
except KeyError:
1021
raise BzrError("can't look up file_id None")
1023
raise BzrError("file_id {%s} not in inventory" % file_id)
1111
# really we're passing an inventory, not a tree...
1112
raise errors.NoSuchId(self, file_id)
1025
1114
def get_file_kind(self, file_id):
1115
file_id = osutils.safe_file_id(file_id)
1026
1116
return self._byid[file_id].kind
1028
1118
def get_child(self, parent_id, filename):
1119
parent_id = osutils.safe_file_id(parent_id)
1029
1120
return self[parent_id].children.get(filename)
1122
def _add_child(self, entry):
1123
"""Add an entry to the inventory, without adding it to its parent"""
1124
if entry.file_id in self._byid:
1125
raise BzrError("inventory already contains entry with id {%s}" %
1127
self._byid[entry.file_id] = entry
1128
for child in getattr(entry, 'children', {}).itervalues():
1129
self._add_child(child)
1031
1132
def add(self, entry):
1032
1133
"""Add entry to inventory.
1037
1138
Returns the new entry object.
1039
1140
if entry.file_id in self._byid:
1040
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
1042
if entry.parent_id == ROOT_ID or entry.parent_id is None:
1043
entry.parent_id = self.root.file_id
1046
parent = self._byid[entry.parent_id]
1048
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1050
if entry.name in parent.children:
1051
raise BzrError("%s is already versioned" %
1052
pathjoin(self.id2path(parent.file_id), entry.name))
1054
self._byid[entry.file_id] = entry
1055
parent.children[entry.name] = entry
1141
raise errors.DuplicateFileId(entry.file_id,
1142
self._byid[entry.file_id])
1144
if entry.parent_id is None:
1145
assert self.root is None and len(self._byid) == 0
1149
parent = self._byid[entry.parent_id]
1151
raise BzrError("parent_id {%s} not in inventory" %
1154
if entry.name in parent.children:
1155
raise BzrError("%s is already versioned" %
1156
osutils.pathjoin(self.id2path(parent.file_id),
1157
entry.name).encode('utf-8'))
1158
parent.children[entry.name] = entry
1159
return self._add_child(entry)
1058
1161
def add_path(self, relpath, kind, file_id=None, parent_id=None):
1059
1162
"""Add entry from a path.
1063
1166
Returns the new entry object."""
1065
parts = bzrlib.osutils.splitpath(relpath)
1168
parts = osutils.splitpath(relpath)
1067
1170
if len(parts) == 0:
1068
1171
if file_id is None:
1069
file_id = bzrlib.workingtree.gen_root_id()
1070
self.root = RootEntry(file_id)
1172
file_id = generate_ids.gen_root_id()
1174
file_id = osutils.safe_file_id(file_id)
1175
self.root = InventoryDirectory(file_id, '', None)
1071
1176
self._byid = {self.root.file_id: self.root}
1074
1179
parent_path = parts[:-1]
1075
1180
parent_id = self.path2id(parent_path)
1076
1181
if parent_id is None:
1077
raise NotVersionedError(path=parent_path)
1182
raise errors.NotVersionedError(path=parent_path)
1078
1183
ie = make_entry(kind, parts[-1], parent_id, file_id)
1079
1184
return self.add(ie)
1129
1235
def _iter_file_id_parents(self, file_id):
1130
1236
"""Yield the parents of file_id up to the root."""
1237
file_id = osutils.safe_file_id(file_id)
1131
1238
while file_id is not None:
1133
1240
ie = self._byid[file_id]
1134
1241
except KeyError:
1135
raise BzrError("file_id {%s} not found in inventory" % file_id)
1242
raise errors.NoSuchId(tree=None, file_id=file_id)
1137
1244
file_id = ie.parent_id
1144
1251
is equal to the depth of the file in the tree, counting the
1145
1252
root directory as depth 1.
1254
file_id = osutils.safe_file_id(file_id)
1148
1256
for parent in self._iter_file_id_parents(file_id):
1149
1257
p.insert(0, parent.file_id)
1158
1266
>>> print i.id2path('foo-id')
1269
file_id = osutils.safe_file_id(file_id)
1161
1270
# get all names, skipping root
1162
1271
return '/'.join(reversed(
1163
1272
[parent.name for parent in
1175
1284
Returns None IFF the path is not found.
1177
if isinstance(name, types.StringTypes):
1178
name = splitpath(name)
1286
if isinstance(name, basestring):
1287
name = osutils.splitpath(name)
1180
1289
# mutter("lookup path %r" % name)
1182
1291
parent = self.root
1185
cie = parent.children[f]
1296
children = getattr(parent, 'children', None)
1297
if children is None:
1186
1300
assert cie.name == f
1187
1301
assert cie.parent_id == parent.file_id
1196
1310
return bool(self.path2id(names))
1198
1312
def has_id(self, file_id):
1199
return self._byid.has_key(file_id)
1313
file_id = osutils.safe_file_id(file_id)
1314
return (file_id in self._byid)
1316
def remove_recursive_id(self, file_id):
1317
"""Remove file_id, and children, from the inventory.
1319
:param file_id: A file_id to remove.
1321
file_id = osutils.safe_file_id(file_id)
1322
to_find_delete = [self._byid[file_id]]
1324
while to_find_delete:
1325
ie = to_find_delete.pop()
1326
to_delete.append(ie.file_id)
1327
if ie.kind == 'directory':
1328
to_find_delete.extend(ie.children.values())
1329
for file_id in reversed(to_delete):
1331
del self._byid[file_id]
1332
if ie.parent_id is not None:
1333
del self[ie.parent_id].children[ie.name]
1201
1337
def rename(self, file_id, new_parent_id, new_name):
1202
1338
"""Move a file within the inventory.
1204
1340
This can change either the name, or the parent, or both.
1206
This does not move the working file."""
1342
This does not move the working file.
1344
file_id = osutils.safe_file_id(file_id)
1207
1345
if not is_valid_name(new_name):
1208
1346
raise BzrError("not an acceptable filename: %r" % new_name)
1227
1365
file_ie.name = new_name
1228
1366
file_ie.parent_id = new_parent_id
1368
def is_root(self, file_id):
1369
file_id = osutils.safe_file_id(file_id)
1370
return self.root is not None and file_id == self.root.file_id
1374
'directory': InventoryDirectory,
1375
'file': InventoryFile,
1376
'symlink': InventoryLink,
1377
'tree-reference': TreeReference
1231
1380
def make_entry(kind, name, parent_id, file_id=None):
1232
1381
"""Create an inventory entry.
1237
1386
:param file_id: the file_id to use. if None, one will be created.
1239
1388
if file_id is None:
1240
file_id = bzrlib.workingtree.gen_file_id(name)
1241
if kind == 'directory':
1242
return InventoryDirectory(file_id, name, parent_id)
1243
elif kind == 'file':
1244
return InventoryFile(file_id, name, parent_id)
1245
elif kind == 'symlink':
1246
return InventoryLink(file_id, name, parent_id)
1389
file_id = generate_ids.gen_file_id(name)
1391
file_id = osutils.safe_file_id(file_id)
1393
#------- This has been copied to bzrlib.dirstate.DirState.add, please
1394
# keep them synchronised.
1395
# we dont import normalized_filename directly because we want to be
1396
# able to change the implementation at runtime for tests.
1397
norm_name, can_access = osutils.normalized_filename(name)
1398
if norm_name != name:
1402
# TODO: jam 20060701 This would probably be more useful
1403
# if the error was raised with the full path
1404
raise errors.InvalidNormalization(name)
1407
factory = entry_factory[kind]
1248
1409
raise BzrError("unknown kind %r" % kind)
1410
return factory(file_id, name, parent_id)
1252
1413
_NAME_RE = None