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(), """
37
from warnings import warn
40
from bzrlib import errors, osutils
41
from bzrlib.osutils import (pumpfile, quotefn, splitpath, joinpath,
42
pathjoin, sha_strings)
43
from bzrlib.errors import (NotVersionedError, InvalidEntryName,
44
BzrError, BzrCheckError, BinaryFile)
49
from bzrlib.errors import (
45
53
from bzrlib.trace import mutter
82
90
InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None)
83
91
>>> i.add(InventoryFile('2323', 'hello.c', parent_id='123'))
84
92
InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None)
85
>>> shouldbe = {0: '', 1: 'src', 2: pathjoin('src','hello.c')}
93
>>> shouldbe = {0: '', 1: 'src', 2: 'src/hello.c'}
86
94
>>> for ix, j in enumerate(i.iter_entries()):
87
95
... print (j[0] == shouldbe[ix], j[1])
89
(True, InventoryDirectory('TREE_ROOT', '', parent_id=None, revision=None))
97
(True, InventoryDirectory('TREE_ROOT', u'', parent_id=None, revision=None))
90
98
(True, InventoryDirectory('123', 'src', parent_id='TREE_ROOT', revision=None))
91
99
(True, InventoryFile('2323', 'hello.c', parent_id='123', sha1=None, len=None))
92
>>> i.add(InventoryFile('2323', 'bye.c', '123'))
93
Traceback (most recent call last):
95
BzrError: inventory already contains entry with id {2323}
96
100
>>> i.add(InventoryFile('2324', 'bye.c', '123'))
97
101
InventoryFile('2324', 'bye.c', parent_id='123', sha1=None, len=None)
98
102
>>> i.add(InventoryDirectory('2325', 'wibble', '123'))
189
193
if self.file_id in inv:
190
194
ie = inv[self.file_id]
191
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.
192
199
if ie.revision in candidates:
193
200
# same revision value in two different inventories:
194
201
# correct possible inconsistencies:
247
254
def get_tar_item(self, root, dp, now, tree):
248
255
"""Get a tarfile item and a file stream for its content."""
249
item = tarfile.TarInfo(pathjoin(root, dp).encode('utf8'))
256
item = tarfile.TarInfo(osutils.pathjoin(root, dp).encode('utf8'))
250
257
# TODO: would be cool to actually set it to the timestamp of the
251
258
# revision it was last changed
282
289
assert isinstance(name, basestring), name
283
290
if '/' in name or '\\' in name:
284
raise InvalidEntryName(name=name)
291
raise errors.InvalidEntryName(name=name)
285
292
self.executable = False
286
293
self.revision = None
287
294
self.text_sha1 = None
288
295
self.text_size = None
289
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)
291
300
self.text_id = text_id
292
301
self.parent_id = parent_id
293
302
self.symlink_target = None
303
self.reference_revision = None
295
305
def kind_character(self):
296
306
"""Return a short kind indicator useful for appending to names."""
312
322
This is a template method - implement _put_on_disk in subclasses.
314
fullpath = pathjoin(dest, dp)
324
fullpath = osutils.pathjoin(dest, dp)
315
325
self._put_on_disk(fullpath, tree)
316
326
# mutter(" export {%s} kind %s to %s", self.file_id,
317
327
# self.kind, fullpath)
327
337
def versionable_kind(kind):
328
return (kind in ('file', 'directory', 'symlink'))
338
return (kind in ('file', 'directory', 'symlink', 'tree-reference'))
330
340
def check(self, checker, rev_id, inv, tree):
331
341
"""Check this inventory entry is intact.
501
516
class RootEntry(InventoryEntry):
503
518
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
504
'text_id', 'parent_id', 'children', 'executable',
505
'revision', 'symlink_target']
519
'text_id', 'parent_id', 'children', 'executable',
520
'revision', 'symlink_target', 'reference_revision']
507
522
def _check(self, checker, rev_id, tree):
508
523
"""See InventoryEntry._check"""
514
529
self.parent_id = None
516
531
self.revision = None
517
warn('RootEntry is deprecated as of bzr 0.10. Please use '
518
'InventoryDirectory instead.',
519
DeprecationWarning, stacklevel=2)
532
symbol_versioning.warn('RootEntry is deprecated as of bzr 0.10.'
533
' Please use InventoryDirectory instead.',
534
DeprecationWarning, stacklevel=2)
521
536
def __eq__(self, other):
522
537
if not isinstance(other, RootEntry):
530
545
"""A directory in an inventory."""
532
547
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
533
'text_id', 'parent_id', 'children', 'executable',
534
'revision', 'symlink_target']
548
'text_id', 'parent_id', 'children', 'executable',
549
'revision', 'symlink_target', 'reference_revision']
536
551
def _check(self, checker, rev_id, tree):
537
552
"""See InventoryEntry._check"""
577
592
"""A file in an inventory."""
579
594
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
580
'text_id', 'parent_id', 'children', 'executable',
581
'revision', 'symlink_target']
595
'text_id', 'parent_id', 'children', 'executable',
596
'revision', 'symlink_target', 'reference_revision']
583
598
def _check(self, checker, tree_revision_id, tree):
584
599
"""See InventoryEntry._check"""
646
661
text_diff(to_label, to_text,
647
662
from_label, from_text, output_to)
663
except errors.BinaryFile:
650
665
label_pair = (to_label, from_label)
678
693
def _put_on_disk(self, fullpath, tree):
679
694
"""See InventoryEntry._put_on_disk."""
680
pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
695
osutils.pumpfile(tree.get_file(self.file_id), file(fullpath, 'wb'))
681
696
if tree.is_executable(self.file_id):
682
697
os.chmod(fullpath, 0755)
725
740
"""A file in an inventory."""
727
742
__slots__ = ['text_sha1', 'text_size', 'file_id', 'name', 'kind',
728
'text_id', 'parent_id', 'children', 'executable',
729
'revision', 'symlink_target']
743
'text_id', 'parent_id', 'children', 'executable',
744
'revision', 'symlink_target', 'reference_revision']
731
746
def _check(self, checker, rev_id, tree):
732
747
"""See InventoryEntry._check"""
813
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
816
858
class Inventory(object):
817
859
"""Inventory of versioned files in a tree.
850
892
>>> inv = Inventory('TREE_ROOT-12345678-12345678')
851
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'))
852
897
InventoryFile('123-123', 'hello.c', parent_id='TREE_ROOT-12345678-12345678', sha1=None, len=None)
854
899
def __init__(self, root_id=ROOT_ID, revision_id=None):
861
906
The inventory is created with a default root directory, with
864
# We are letting Branch.create() create a unique inventory
865
# root id. Rather than generating a random one here.
867
# root_id = bzrlib.branch.gen_file_id('TREE_ROOT')
868
909
if root_id is not None:
869
self._set_root(InventoryDirectory(root_id, '', None))
910
assert root_id.__class__ == str
911
self._set_root(InventoryDirectory(root_id, u'', None))
873
# FIXME: this isn't ever used, changing it to self.revision may break
874
# things. TODO make everything use self.revision_id
875
915
self.revision_id = revision_id
877
917
def _set_root(self, ie):
898
938
def iter_entries(self, from_dir=None):
899
939
"""Return (path, entry) pairs, in order by name."""
900
940
if from_dir is None:
941
if self.root is None:
902
943
from_dir = self.root
903
944
yield '', self.root
904
945
elif isinstance(from_dir, basestring):
938
979
# if we finished all children, pop it off the stack
941
def iter_entries_by_dir(self, from_dir=None):
982
def iter_entries_by_dir(self, from_dir=None, specific_file_ids=None):
942
983
"""Iterate over the entries in a directory first order.
944
985
This returns all entries for a directory before returning
949
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)
951
995
# TODO? Perhaps this should return the from_dir so that the root is
952
996
# yielded? or maybe an option?
953
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]
955
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
957
1010
elif isinstance(from_dir, basestring):
958
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)
960
1032
stack = [(u'', from_dir)]
967
1039
child_relpath = cur_relpath + child_name
969
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
971
1045
if child_ie.kind == 'directory':
972
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))
973
1048
stack.extend(reversed(child_dirs))
975
1050
def entries(self):
982
1057
kids = dir_ie.children.items()
984
1059
for name, ie in kids:
985
child_path = pathjoin(dir_path, name)
1060
child_path = osutils.pathjoin(dir_path, name)
986
1061
accum.append((child_path, ie))
987
1062
if ie.kind == 'directory':
988
1063
descend(ie, child_path)
1003
1078
for name, child_ie in kids:
1004
child_path = pathjoin(parent_path, name)
1079
child_path = osutils.pathjoin(parent_path, name)
1005
1080
descend(child_ie, child_path)
1006
1081
descend(self.root, u'')
1028
1104
>>> inv['123123'].name
1107
file_id = osutils.safe_file_id(file_id)
1032
1109
return self._byid[file_id]
1033
1110
except KeyError:
1035
raise BzrError("can't look up file_id None")
1037
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)
1039
1114
def get_file_kind(self, file_id):
1115
file_id = osutils.safe_file_id(file_id)
1040
1116
return self._byid[file_id].kind
1042
1118
def get_child(self, parent_id, filename):
1119
parent_id = osutils.safe_file_id(parent_id)
1043
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)
1045
1132
def add(self, entry):
1046
1133
"""Add entry to inventory.
1051
1138
Returns the new entry object.
1053
1140
if entry.file_id in self._byid:
1054
raise BzrError("inventory already contains entry with id {%s}" % entry.file_id)
1141
raise errors.DuplicateFileId(entry.file_id,
1142
self._byid[entry.file_id])
1056
1144
if entry.parent_id is None:
1057
1145
assert self.root is None and len(self._byid) == 0
1058
self._set_root(entry)
1060
if entry.parent_id == ROOT_ID:
1061
assert self.root is not None, self
1062
entry.parent_id = self.root.file_id
1065
parent = self._byid[entry.parent_id]
1067
raise BzrError("parent_id {%s} not in inventory" % entry.parent_id)
1069
if entry.name in parent.children:
1070
raise BzrError("%s is already versioned" %
1071
pathjoin(self.id2path(parent.file_id), entry.name))
1073
self._byid[entry.file_id] = entry
1074
parent.children[entry.name] = entry
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)
1077
1161
def add_path(self, relpath, kind, file_id=None, parent_id=None):
1078
1162
"""Add entry from a path.
1086
1170
if len(parts) == 0:
1087
1171
if file_id is None:
1088
file_id = bzrlib.workingtree.gen_root_id()
1172
file_id = generate_ids.gen_root_id()
1174
file_id = osutils.safe_file_id(file_id)
1089
1175
self.root = InventoryDirectory(file_id, '', None)
1090
1176
self._byid = {self.root.file_id: self.root}
1093
1179
parent_path = parts[:-1]
1094
1180
parent_id = self.path2id(parent_path)
1095
1181
if parent_id is None:
1096
raise NotVersionedError(path=parent_path)
1182
raise errors.NotVersionedError(path=parent_path)
1097
1183
ie = make_entry(kind, parts[-1], parent_id, file_id)
1098
1184
return self.add(ie)
1148
1235
def _iter_file_id_parents(self, file_id):
1149
1236
"""Yield the parents of file_id up to the root."""
1237
file_id = osutils.safe_file_id(file_id)
1150
1238
while file_id is not None:
1152
1240
ie = self._byid[file_id]
1153
1241
except KeyError:
1154
raise BzrError("file_id {%s} not found in inventory" % file_id)
1242
raise errors.NoSuchId(tree=None, file_id=file_id)
1156
1244
file_id = ie.parent_id
1163
1251
is equal to the depth of the file in the tree, counting the
1164
1252
root directory as depth 1.
1254
file_id = osutils.safe_file_id(file_id)
1167
1256
for parent in self._iter_file_id_parents(file_id):
1168
1257
p.insert(0, parent.file_id)
1177
1266
>>> print i.id2path('foo-id')
1269
file_id = osutils.safe_file_id(file_id)
1180
1270
# get all names, skipping root
1181
1271
return '/'.join(reversed(
1182
1272
[parent.name for parent in
1194
1284
Returns None IFF the path is not found.
1196
if isinstance(name, types.StringTypes):
1197
name = splitpath(name)
1286
if isinstance(name, basestring):
1287
name = osutils.splitpath(name)
1199
1289
# mutter("lookup path %r" % name)
1201
1291
parent = self.root
1204
cie = parent.children[f]
1296
children = getattr(parent, 'children', None)
1297
if children is None:
1205
1300
assert cie.name == f
1206
1301
assert cie.parent_id == parent.file_id
1215
1310
return bool(self.path2id(names))
1217
1312
def has_id(self, file_id):
1313
file_id = osutils.safe_file_id(file_id)
1218
1314
return (file_id in self._byid)
1220
1316
def remove_recursive_id(self, file_id):
1232
1329
for file_id in reversed(to_delete):
1233
1330
ie = self[file_id]
1234
1331
del self._byid[file_id]
1235
if ie.parent_id is not None:
1236
del self[ie.parent_id].children[ie.name]
1332
if ie.parent_id is not None:
1333
del self[ie.parent_id].children[ie.name]
1238
1337
def rename(self, file_id, new_parent_id, new_name):
1239
1338
"""Move a file within the inventory.
1241
1340
This can change either the name, or the parent, or both.
1243
This does not move the working file."""
1342
This does not move the working file.
1344
file_id = osutils.safe_file_id(file_id)
1244
1345
if not is_valid_name(new_name):
1245
1346
raise BzrError("not an acceptable filename: %r" % new_name)
1264
1365
file_ie.name = new_name
1265
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
1268
1380
def make_entry(kind, name, parent_id, file_id=None):
1269
1381
"""Create an inventory entry.
1274
1386
:param file_id: the file_id to use. if None, one will be created.
1276
1388
if file_id is None:
1277
file_id = bzrlib.workingtree.gen_file_id(name)
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.
1279
1397
norm_name, can_access = osutils.normalized_filename(name)
1280
1398
if norm_name != name:
1285
1403
# if the error was raised with the full path
1286
1404
raise errors.InvalidNormalization(name)
1288
if kind == 'directory':
1289
return InventoryDirectory(file_id, name, parent_id)
1290
elif kind == 'file':
1291
return InventoryFile(file_id, name, parent_id)
1292
elif kind == 'symlink':
1293
return InventoryLink(file_id, name, parent_id)
1407
factory = entry_factory[kind]
1295
1409
raise BzrError("unknown kind %r" % kind)
1410
return factory(file_id, name, parent_id)
1298
1413
_NAME_RE = None