14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
from __future__ import absolute_import
19
19
from .lazy_import import lazy_import
20
20
lazy_import(globals(), """
23
21
from breezy import (
24
22
branch as _mod_branch,
25
24
conflicts as _mod_conflicts,
27
26
graph as _mod_graph,
30
30
revision as _mod_revision,
38
39
from breezy.bzr import (
39
conflicts as _mod_bzr_conflicts,
43
43
from breezy.i18n import gettext
45
from breezy.bzr.conflicts import Conflict as BzrConflict
53
54
# TODO: Report back as changes are merged in
222
223
There are some fields hooks can access:
225
:ivar file_id: the file ID of the file being merged
224
226
:ivar base_path: Path in base tree
225
227
:ivar other_path: Path in other tree
226
228
:ivar this_path: Path in this tree
227
229
:ivar trans_id: the transform ID for the merge of this file
228
:ivar this_kind: kind of file in 'this' tree
229
:ivar other_kind: kind of file in 'other' tree
230
:ivar this_kind: kind of file_id in 'this' tree
231
:ivar other_kind: kind of file_id in 'other' tree
230
232
:ivar winner: one of 'this', 'other', 'conflict'
233
def __init__(self, merger, paths, trans_id, this_kind, other_kind,
235
def __init__(self, merger, file_id, paths, trans_id, this_kind, other_kind,
235
237
self._merger = merger
238
self.file_id = file_id
236
239
self.paths = paths
237
240
self.base_path, self.other_path, self.this_path = paths
238
241
self.trans_id = trans_id
445
448
def _add_parent(self):
446
449
new_parents = self.this_tree.get_parent_ids() + [self.other_rev_id]
447
450
new_parent_trees = []
448
with contextlib.ExitStack() as stack:
449
for revision_id in new_parents:
451
tree = self.revision_tree(revision_id)
452
except errors.NoSuchRevision:
455
stack.enter_context(tree.lock_read())
456
new_parent_trees.append((revision_id, tree))
457
self.this_tree.set_parent_trees(new_parent_trees, allow_leftmost_as_ghost=True)
451
operation = cleanup.OperationWithCleanups(
452
self.this_tree.set_parent_trees)
453
for revision_id in new_parents:
455
tree = self.revision_tree(revision_id)
456
except errors.NoSuchRevision:
460
operation.add_cleanup(tree.unlock)
461
new_parent_trees.append((revision_id, tree))
462
operation.run_simple(new_parent_trees, allow_leftmost_as_ghost=True)
459
464
def set_other(self, other_revision, possible_transports=None):
460
465
"""Set the revision and tree to merge from.
654
660
def do_merge(self):
655
with contextlib.ExitStack() as stack:
656
stack.enter_context(self.this_tree.lock_tree_write())
657
if self.base_tree is not None:
658
stack.enter_context(self.base_tree.lock_read())
659
if self.other_tree is not None:
660
stack.enter_context(self.other_tree.lock_read())
661
merge = self._do_merge_to()
661
operation = cleanup.OperationWithCleanups(self._do_merge_to)
662
self.this_tree.lock_tree_write()
663
operation.add_cleanup(self.this_tree.unlock)
664
if self.base_tree is not None:
665
self.base_tree.lock_read()
666
operation.add_cleanup(self.base_tree.unlock)
667
if self.other_tree is not None:
668
self.other_tree.lock_read()
669
operation.add_cleanup(self.other_tree.unlock)
670
merge = operation.run_simple()
662
671
if len(merge.cooked_conflicts) == 0:
663
672
if not self.ignore_zero and not trace.is_quiet():
664
673
trace.note(gettext("All changes applied successfully."))
757
763
def do_merge(self):
758
with contextlib.ExitStack() as stack:
759
stack.enter_context(self.working_tree.lock_tree_write())
760
stack.enter_context(self.this_tree.lock_read())
761
stack.enter_context(self.base_tree.lock_read())
762
stack.enter_context(self.other_tree.lock_read())
763
self.tt = self.working_tree.transform()
764
stack.enter_context(self.tt)
765
self._compute_transform()
766
results = self.tt.apply(no_conflicts=True)
767
self.write_modified(results)
769
self.working_tree.add_conflicts(self.cooked_conflicts)
770
except errors.UnsupportedOperation:
764
operation = cleanup.OperationWithCleanups(self._do_merge)
765
self.working_tree.lock_tree_write()
766
operation.add_cleanup(self.working_tree.unlock)
767
self.this_tree.lock_read()
768
operation.add_cleanup(self.this_tree.unlock)
769
self.base_tree.lock_read()
770
operation.add_cleanup(self.base_tree.unlock)
771
self.other_tree.lock_read()
772
operation.add_cleanup(self.other_tree.unlock)
775
def _do_merge(self, operation):
776
self.tt = transform.TreeTransform(self.working_tree, None)
777
operation.add_cleanup(self.tt.finalize)
778
self._compute_transform()
779
results = self.tt.apply(no_conflicts=True)
780
self.write_modified(results)
782
self.working_tree.add_conflicts(self.cooked_conflicts)
783
except errors.UnsupportedOperation:
773
786
def make_preview_transform(self):
774
787
with self.base_tree.lock_read(), self.other_tree.lock_read():
775
self.tt = self.working_tree.preview_transform()
788
self.tt = transform.TransformPreview(self.working_tree)
776
789
self._compute_transform()
779
792
def _compute_transform(self):
780
793
if self._lca_trees is None:
781
entries = list(self._entries3())
794
entries = self._entries3()
782
795
resolver = self._three_way
784
entries = list(self._entries_lca())
797
entries = self._entries_lca()
785
798
resolver = self._lca_multi_way
786
799
# Prepare merge hooks
787
800
factories = Merger.hooks['merge_file_content']
790
803
self.active_hooks = [hook for hook in hooks if hook is not None]
791
804
with ui.ui_factory.nested_progress_bar() as child_pb:
792
805
for num, (file_id, changed, paths3, parents3, names3,
793
executable3, copied) in enumerate(entries):
795
# Treat copies as simple adds for now
796
paths3 = (None, paths3[1], None)
797
parents3 = (None, parents3[1], None)
798
names3 = (None, names3[1], None)
799
executable3 = (None, executable3[1], None)
802
trans_id = self.tt.trans_id_file_id(file_id)
806
executable3) in enumerate(entries):
803
807
# Try merging each entry
804
808
child_pb.update(gettext('Preparing file merge'),
805
809
num, len(entries))
806
self._merge_names(trans_id, file_id, paths3, parents3,
810
self._merge_names(file_id, paths3, parents3,
807
811
names3, resolver=resolver)
809
file_status = self._do_merge_contents(paths3, trans_id, file_id)
813
file_status = self._do_merge_contents(paths3, file_id)
811
815
file_status = 'unmodified'
812
self._merge_executable(paths3, trans_id, executable3,
816
self._merge_executable(paths3, file_id, executable3,
813
817
file_status, resolver=resolver)
814
818
self.tt.fixup_new_roots()
815
819
self._finish_computing_transform()
847
851
self.interesting_files, trees=[self.other_tree])
848
852
this_entries = dict(self.this_tree.iter_entries_by_dir(
849
853
specific_files=this_interesting_files))
850
for change in iterator:
851
if change.path[0] is not None:
854
for (file_id, paths, changed, versioned, parents, names, kind,
855
executable) in iterator:
856
if paths[0] is not None:
852
857
this_path = _mod_tree.find_previous_path(
853
self.base_tree, self.this_tree, change.path[0])
858
self.base_tree, self.this_tree, paths[0])
855
860
this_path = _mod_tree.find_previous_path(
856
self.other_tree, self.this_tree, change.path[1])
861
self.other_tree, self.this_tree, paths[1])
857
862
this_entry = this_entries.get(this_path)
858
863
if this_entry is not None:
859
864
this_name = this_entry.name
864
869
this_parent = None
865
870
this_executable = None
866
parents3 = change.parent_id + (this_parent,)
867
names3 = change.name + (this_name,)
868
paths3 = change.path + (this_path, )
869
executable3 = change.executable + (this_executable,)
871
(change.file_id, change.changed_content, paths3,
872
parents3, names3, executable3, change.copied))
871
parents3 = parents + (this_parent,)
872
names3 = names + (this_name,)
873
paths3 = paths + (this_path, )
874
executable3 = executable + (this_executable,)
875
result.append((file_id, changed, paths3,
876
parents3, names3, executable3))
874
879
def _entries_lca(self):
875
880
"""Gather data about files modified between multiple trees.
880
885
For the multi-valued entries, the format will be (BASE, [lca1, lca2])
882
:return: [(file_id, changed, paths, parents, names, executable, copied)], where:
887
:return: [(file_id, changed, paths, parents, names, executable)], where:
884
889
* file_id: Simple file_id of the entry
885
890
* changed: Boolean, True if the kind or contents changed else False
898
903
self.interesting_files, lookup_trees)
900
905
interesting_files = None
901
from .multiwalker import MultiWalker
902
walker = MultiWalker(self.other_tree, self._lca_trees)
907
walker = _mod_tree.MultiWalker(self.other_tree, self._lca_trees)
904
for other_path, file_id, other_ie, lca_values in walker.iter_all():
909
base_inventory = self.base_tree.root_inventory
910
this_inventory = self.this_tree.root_inventory
911
for path, file_id, other_ie, lca_values in walker.iter_all():
905
912
# Is this modified at all from any of the other trees?
906
913
if other_ie is None:
907
914
other_ie = _none_entry
908
915
other_path = None
917
other_path = self.other_tree.id2path(file_id)
909
918
if interesting_files is not None and other_path not in interesting_files:
915
924
# we know that the ancestry is linear, and that OTHER did not
916
925
# modify anything
917
926
# See doc/developers/lca_merge_resolution.txt for details
918
# We can't use this shortcut when other_revision is None,
919
# because it may be None because things are WorkingTrees, and
920
# not because it is *actually* None.
921
is_unmodified = False
922
for lca_path, ie in lca_values:
923
if ie is not None and other_ie.is_unmodified(ie):
927
other_revision = other_ie.revision
928
if other_revision is not None:
929
# We can't use this shortcut when other_revision is None,
930
# because it may be None because things are WorkingTrees, and
931
# not because it is *actually* None.
932
is_unmodified = False
933
for lca_path, ie in lca_values:
934
if ie is not None and ie.revision == other_revision:
1049
1060
((base_ie.name, lca_names),
1050
1061
other_ie.name, this_ie.name),
1051
1062
((base_ie.executable, lca_executable),
1052
other_ie.executable, this_ie.executable),
1053
# Copy detection is not yet supported, so nothing is
1063
other_ie.executable, this_ie.executable)
1058
1067
def write_modified(self, results):
1059
1068
if not self.working_tree.supports_merge_modified():
1175
1185
# At this point, the lcas disagree, and the tip disagree
1176
1186
return 'conflict'
1178
def _merge_names(self, trans_id, file_id, paths, parents, names, resolver):
1179
"""Perform a merge on file names and parents"""
1188
def _merge_names(self, file_id, paths, parents, names, resolver):
1189
"""Perform a merge on file_id names and parents"""
1180
1190
base_name, other_name, this_name = names
1181
1191
base_parent, other_parent, this_parent = parents
1182
1192
unused_base_path, other_path, this_path = paths
1217
1228
parent_trans_id = transform.ROOT_PARENT
1219
1230
parent_trans_id = self.tt.trans_id_file_id(parent_id)
1220
self.tt.adjust_path(name, parent_trans_id, trans_id)
1231
self.tt.adjust_path(name, parent_trans_id,
1232
self.tt.trans_id_file_id(file_id))
1222
def _do_merge_contents(self, paths, trans_id, file_id):
1234
def _do_merge_contents(self, paths, file_id):
1223
1235
"""Performs a merge on file_id contents."""
1224
1236
def contents_pair(tree, path):
1225
1237
if path is None:
1263
1275
return "unmodified"
1264
1276
# We have a hypothetical conflict, but if we have files, then we
1265
1277
# can try to merge the content
1278
trans_id = self.tt.trans_id_file_id(file_id)
1266
1279
params = MergeFileHookParams(
1267
self, (base_path, other_path, this_path), trans_id, this_pair[0],
1280
self, file_id, (base_path, other_path,
1281
this_path), trans_id, this_pair[0],
1268
1282
other_pair[0], winner)
1269
1283
hooks = self.active_hooks
1270
1284
hook_status = 'not_applicable'
1292
1306
keep_this = True
1293
1307
# versioning the merged file will trigger a duplicate
1295
self.tt.version_file(trans_id, file_id=file_id)
1309
self.tt.version_file(file_id, trans_id)
1296
1310
transform.create_from_tree(
1297
1311
self.tt, trans_id, self.other_tree,
1299
filter_tree_path=self._get_filter_tree_path(other_path))
1312
other_path, file_id=file_id,
1313
filter_tree_path=self._get_filter_tree_path(file_id))
1300
1314
inhibit_content_conflict = True
1301
1315
elif params.other_kind is None: # file_id is not in OTHER
1302
1316
# Is the name used for a different file_id ?
1315
1329
# This is a contents conflict, because none of the available
1316
1330
# functions could merge it.
1317
1331
file_group = self._dump_conflicts(
1318
name, (base_path, other_path, this_path), parent_id)
1319
for tid in file_group:
1320
self.tt.version_file(tid, file_id=file_id)
1332
name, (base_path, other_path, this_path), parent_id,
1333
file_id, set_version=True)
1322
1334
self._raw_conflicts.append(('contents conflict', file_group))
1323
1335
elif hook_status == 'success':
1324
1336
self.tt.create_file(lines, trans_id)
1351
1363
def _default_other_winner_merge(self, merge_hook_params):
1352
1364
"""Replace this contents with other."""
1365
file_id = merge_hook_params.file_id
1353
1366
trans_id = merge_hook_params.trans_id
1354
1367
if merge_hook_params.other_path is not None:
1355
1368
# OTHER changed the file
1356
1369
transform.create_from_tree(
1357
1370
self.tt, trans_id, self.other_tree,
1358
merge_hook_params.other_path,
1359
filter_tree_path=self._get_filter_tree_path(merge_hook_params.other_path))
1371
merge_hook_params.other_path, file_id=file_id,
1372
filter_tree_path=self._get_filter_tree_path(file_id))
1360
1373
return 'done', None
1361
1374
elif merge_hook_params.this_path is not None:
1362
1375
# OTHER deleted the file
1363
1376
return 'delete', None
1365
1378
raise AssertionError(
1366
'winner is OTHER, but file %r not in THIS or OTHER tree'
1367
% (merge_hook_params.base_path,))
1379
'winner is OTHER, but file_id %r not in THIS or OTHER tree'
1369
1382
def merge_contents(self, merge_hook_params):
1370
1383
"""Fallback merge logic after user installed hooks."""
1436
1449
self._raw_conflicts.append(('text conflict', trans_id))
1437
1450
name = self.tt.final_name(trans_id)
1438
1451
parent_id = self.tt.final_parent(trans_id)
1439
file_group = self._dump_conflicts(
1440
name, paths, parent_id,
1441
lines=(base_lines, other_lines, this_lines))
1452
file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1453
this_lines, base_lines,
1442
1455
file_group.append(trans_id)
1444
def _get_filter_tree_path(self, path):
1457
def _get_filter_tree_path(self, file_id):
1445
1458
if self.this_tree.supports_content_filtering():
1446
1459
# We get the path from the working tree if it exists.
1447
1460
# That fails though when OTHER is adding a file, so
1448
1461
# we fall back to the other tree to find the path if
1449
1462
# it doesn't exist locally.
1450
filter_path = _mod_tree.find_previous_path(
1451
self.other_tree, self.working_tree, path)
1452
if filter_path is None:
1455
# Skip the lookup for older formats
1464
return self.this_tree.id2path(file_id)
1465
except errors.NoSuchId:
1466
return self.other_tree.id2path(file_id)
1467
# Skip the id2path lookup for older formats
1458
def _dump_conflicts(self, name, paths, parent_id, lines=None,
1470
def _dump_conflicts(self, name, paths, parent_id, file_id, this_lines=None,
1471
base_lines=None, other_lines=None, set_version=False,
1459
1472
no_base=False):
1460
1473
"""Emit conflict files.
1461
1474
If this_lines, base_lines, or other_lines are omitted, they will be
1463
1476
or .BASE (in that order) will be created as versioned files.
1465
1478
base_path, other_path, this_path = paths
1467
base_lines, other_lines, this_lines = lines
1469
base_lines = other_lines = this_lines = None
1470
1479
data = [('OTHER', self.other_tree, other_path, other_lines),
1471
1480
('THIS', self.this_tree, this_path, this_lines)]
1472
1481
if not no_base:
1473
1482
data.append(('BASE', self.base_tree, base_path, base_lines))
1475
1484
# We need to use the actual path in the working tree of the file here,
1476
if self.this_tree.supports_content_filtering():
1477
filter_tree_path = this_path
1485
# ignoring the conflict suffixes
1487
if wt.supports_content_filtering():
1489
filter_tree_path = wt.id2path(file_id)
1490
except errors.NoSuchId:
1491
# file has been deleted
1492
filter_tree_path = None
1494
# Skip the id2path lookup for older formats
1479
1495
filter_tree_path = None
1481
1498
file_group = []
1482
1499
for suffix, tree, path, lines in data:
1483
1500
if path is not None:
1484
1501
trans_id = self._conflict_file(
1485
name, parent_id, path, tree, suffix, lines,
1502
name, parent_id, path, tree, file_id, suffix, lines,
1486
1503
filter_tree_path)
1487
1504
file_group.append(trans_id)
1505
if set_version and not versioned:
1506
self.tt.version_file(file_id, trans_id)
1488
1508
return file_group
1490
def _conflict_file(self, name, parent_id, path, tree, suffix,
1510
def _conflict_file(self, name, parent_id, path, tree, file_id, suffix,
1491
1511
lines=None, filter_tree_path=None):
1492
1512
"""Emit a single conflict file."""
1493
1513
name = name + '.' + suffix
1494
1514
trans_id = self.tt.create_path(name, parent_id)
1495
1515
transform.create_from_tree(
1496
1516
self.tt, trans_id, tree, path,
1517
file_id=file_id, chunks=lines,
1498
1518
filter_tree_path=filter_tree_path)
1499
1519
return trans_id
1501
def _merge_executable(self, paths, trans_id, executable, file_status,
1521
def merge_executable(self, paths, file_id, file_status):
1522
"""Perform a merge on the execute bit."""
1523
executable = [self.executable(t, p, file_id)
1524
for t, p in zip([self.base_tree, self.other_tree, self.this_tree], paths)]
1525
self._merge_executable(paths, file_id, executable, file_status,
1526
resolver=self._three_way)
1528
def _merge_executable(self, paths, file_id, executable, file_status,
1503
1530
"""Perform a merge on the execute bit."""
1504
1531
base_executable, other_executable, this_executable = executable
1527
1555
elif base_path is not None:
1528
1556
executability = base_executable
1529
1557
if executability is not None:
1558
trans_id = self.tt.trans_id_file_id(file_id)
1530
1559
self.tt.set_executability(executability, trans_id)
1532
1561
def cook_conflicts(self, fs_conflicts):
1533
1562
"""Convert all conflicts into a form that doesn't depend on trans_id"""
1534
self.cooked_conflicts = list(self.tt.cook_conflicts(
1535
list(fs_conflicts) + self._raw_conflicts))
1563
content_conflict_file_ids = set()
1564
cooked_conflicts = transform.cook_conflicts(fs_conflicts, self.tt)
1565
fp = transform.FinalPaths(self.tt)
1566
for conflict in self._raw_conflicts:
1567
conflict_type = conflict[0]
1568
if conflict_type == 'path conflict':
1570
this_parent, this_name,
1571
other_parent, other_name) = conflict[1:]
1572
if this_parent is None or this_name is None:
1573
this_path = '<deleted>'
1575
parent_path = fp.get_path(
1576
self.tt.trans_id_file_id(this_parent))
1577
this_path = osutils.pathjoin(parent_path, this_name)
1578
if other_parent is None or other_name is None:
1579
other_path = '<deleted>'
1581
if other_parent == self.other_tree.get_root_id():
1582
# The tree transform doesn't know about the other root,
1583
# so we special case here to avoid a NoFinalPath
1587
parent_path = fp.get_path(
1588
self.tt.trans_id_file_id(other_parent))
1589
other_path = osutils.pathjoin(parent_path, other_name)
1590
c = _mod_conflicts.Conflict.factory(
1591
'path conflict', path=this_path,
1592
conflict_path=other_path,
1594
elif conflict_type == 'contents conflict':
1595
for trans_id in conflict[1]:
1596
file_id = self.tt.final_file_id(trans_id)
1597
if file_id is not None:
1598
# Ok we found the relevant file-id
1600
path = fp.get_path(trans_id)
1601
for suffix in ('.BASE', '.THIS', '.OTHER'):
1602
if path.endswith(suffix):
1603
# Here is the raw path
1604
path = path[:-len(suffix)]
1606
c = _mod_conflicts.Conflict.factory(conflict_type,
1607
path=path, file_id=file_id)
1608
content_conflict_file_ids.add(file_id)
1609
elif conflict_type == 'text conflict':
1610
trans_id = conflict[1]
1611
path = fp.get_path(trans_id)
1612
file_id = self.tt.final_file_id(trans_id)
1613
c = _mod_conflicts.Conflict.factory(conflict_type,
1614
path=path, file_id=file_id)
1616
raise AssertionError('bad conflict type: %r' % (conflict,))
1617
cooked_conflicts.append(c)
1619
self.cooked_conflicts = []
1620
# We want to get rid of path conflicts when a corresponding contents
1621
# conflict exists. This can occur when one branch deletes a file while
1622
# the other renames *and* modifies it. In this case, the content
1623
# conflict is enough.
1624
for c in cooked_conflicts:
1625
if (c.typestring == 'path conflict'
1626
and c.file_id in content_conflict_file_ids):
1628
self.cooked_conflicts.append(c)
1629
self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1538
1632
class WeaveMerger(Merge3Merger):
1543
1637
history_based = True
1544
1638
requires_file_merge_plan = True
1546
def _generate_merge_plan(self, this_path, base):
1547
return self.this_tree.plan_file_merge(this_path, self.other_tree,
1640
def _generate_merge_plan(self, file_id, base):
1641
return self.this_tree.plan_file_merge(file_id, self.other_tree,
1550
def _merged_lines(self, this_path):
1644
def _merged_lines(self, file_id):
1551
1645
"""Generate the merged lines.
1552
1646
There is no distinction between lines that are meant to contain <<<<<<<
1572
1666
base_lines = None
1573
1667
return lines, base_lines
1575
def text_merge(self, trans_id, paths):
1669
def text_merge(self, trans_id, paths, file_id):
1576
1670
"""Perform a (weave) text merge for a given file and file-id.
1577
1671
If conflicts are encountered, .THIS and .OTHER files will be emitted,
1578
1672
and a conflict will be noted.
1580
1674
base_path, other_path, this_path = paths
1581
lines, base_lines = self._merged_lines(this_path)
1675
lines, base_lines = self._merged_lines(file_id)
1582
1676
lines = list(lines)
1583
1677
# Note we're checking whether the OUTPUT is binary in this case,
1584
1678
# because we don't want to get into weave merge guts.
1589
1683
self._raw_conflicts.append(('text conflict', trans_id))
1590
1684
name = self.tt.final_name(trans_id)
1591
1685
parent_id = self.tt.final_parent(trans_id)
1592
file_group = self._dump_conflicts(name, paths, parent_id,
1593
(base_lines, None, None),
1686
file_group = self._dump_conflicts(name, paths, parent_id, file_id,
1688
base_lines=base_lines)
1595
1689
file_group.append(trans_id)
1772
1866
def _entries_to_incorporate(self):
1773
1867
"""Yields pairs of (inventory_entry, new_parent)."""
1774
subdir_id = self.other_tree.path2id(self._source_subpath)
1868
other_inv = self.other_tree.root_inventory
1869
subdir_id = other_inv.path2id(self._source_subpath)
1775
1870
if subdir_id is None:
1776
1871
# XXX: The error would be clearer if it gave the URL of the source
1777
1872
# branch, but we don't have a reference to that here.
1778
1873
raise PathNotInTree(self._source_subpath, "Source tree")
1779
subdir = next(self.other_tree.iter_entries_by_dir(
1780
specific_files=[self._source_subpath]))[1]
1874
subdir = other_inv.get_entry(subdir_id)
1781
1875
parent_in_target = osutils.dirname(self._target_subdir)
1782
1876
target_id = self.this_tree.path2id(parent_in_target)
1783
1877
if target_id is None: