changeset 49609 | 9cac281eb9c0 |
parent 49284 | d44e3c45f0e4 |
child 49610 | 35d4c2124073 |
49608:78ba41878f2e | 49609:9cac281eb9c0 |
---|---|
867 ellipses=False, |
867 ellipses=False, |
868 clrevtolocalrev=None, |
868 clrevtolocalrev=None, |
869 fullclnodes=None, |
869 fullclnodes=None, |
870 precomputedellipsis=None, |
870 precomputedellipsis=None, |
871 sidedata_helpers=None, |
871 sidedata_helpers=None, |
872 debug_info=None, |
|
872 ): |
873 ): |
873 """Calculate deltas for a set of revisions. |
874 """Calculate deltas for a set of revisions. |
874 |
875 |
875 Is a generator of ``revisiondelta`` instances. |
876 Is a generator of ``revisiondelta`` instances. |
876 |
877 |
976 nodesorder=nodesorder, |
977 nodesorder=nodesorder, |
977 revisiondata=True, |
978 revisiondata=True, |
978 assumehaveparentrevisions=not ellipses, |
979 assumehaveparentrevisions=not ellipses, |
979 deltamode=deltamode, |
980 deltamode=deltamode, |
980 sidedata_helpers=sidedata_helpers, |
981 sidedata_helpers=sidedata_helpers, |
982 debug_info=debug_info, |
|
981 ) |
983 ) |
982 |
984 |
983 for i, revision in enumerate(revisions): |
985 for i, revision in enumerate(revisions): |
984 if progress: |
986 if progress: |
985 progress.update(i + 1) |
987 progress.update(i + 1) |
999 revision.linknode = linknode |
1001 revision.linknode = linknode |
1000 yield revision |
1002 yield revision |
1001 |
1003 |
1002 if progress: |
1004 if progress: |
1003 progress.complete() |
1005 progress.complete() |
1006 |
|
1007 |
|
1008 def make_debug_info(): |
|
1009 """ "build a "new" debug_info dictionnary |
|
1010 |
|
1011 That dictionnary can be used to gather information about the bundle process |
|
1012 """ |
|
1013 return { |
|
1014 'revision-total': 0, |
|
1015 'revision-changelog': 0, |
|
1016 'revision-manifest': 0, |
|
1017 'revision-files': 0, |
|
1018 'file-count': 0, |
|
1019 'merge-total': 0, |
|
1020 'available-delta': 0, |
|
1021 'available-full': 0, |
|
1022 'delta-against-prev': 0, |
|
1023 'delta-full': 0, |
|
1024 'delta-against-p1': 0, |
|
1025 'denied-delta-candeltafn': 0, |
|
1026 'denied-base-not-available': 0, |
|
1027 'reused-storage-delta': 0, |
|
1028 'computed-delta': 0, |
|
1029 } |
|
1030 |
|
1031 |
|
1032 def merge_debug_info(base, other): |
|
1033 """merge the debug information from <other> into <base> |
|
1034 |
|
1035 This function can be used to gather lower level information into higher level ones. |
|
1036 """ |
|
1037 for key in ( |
|
1038 'revision-total', |
|
1039 'revision-changelog', |
|
1040 'revision-manifest', |
|
1041 'revision-files', |
|
1042 'merge-total', |
|
1043 'available-delta', |
|
1044 'available-full', |
|
1045 'delta-against-prev', |
|
1046 'delta-full', |
|
1047 'delta-against-p1', |
|
1048 'denied-delta-candeltafn', |
|
1049 'denied-base-not-available', |
|
1050 'reused-storage-delta', |
|
1051 'computed-delta', |
|
1052 ): |
|
1053 base[key] += other[key] |
|
1054 |
|
1055 |
|
1056 _KEY_PART_WIDTH = 17 |
|
1057 |
|
1058 |
|
1059 def _dbg_bdl_line( |
|
1060 ui, |
|
1061 indent, |
|
1062 key, |
|
1063 base_value=None, |
|
1064 percentage_base=None, |
|
1065 percentage_key=None, |
|
1066 percentage_ref=None, |
|
1067 extra=None, |
|
1068 ): |
|
1069 """Print one line of debug_bundle_debug_info""" |
|
1070 line = b"DEBUG-BUNDLING: " |
|
1071 line += b' ' * (2 * indent) |
|
1072 key += b":" |
|
1073 if base_value is not None: |
|
1074 assert len(key) + 1 + (2 * indent) <= _KEY_PART_WIDTH |
|
1075 line += key.ljust(_KEY_PART_WIDTH - (2 * indent)) |
|
1076 line += b"%10d" % base_value |
|
1077 else: |
|
1078 line += key |
|
1079 |
|
1080 if percentage_base is not None: |
|
1081 assert base_value is not None |
|
1082 percentage = base_value * 100 // percentage_base |
|
1083 if percentage_key is not None: |
|
1084 line += b" (%d%% of %s %d)" % ( |
|
1085 percentage, |
|
1086 percentage_key, |
|
1087 percentage_ref, |
|
1088 ) |
|
1089 else: |
|
1090 line += b" (%d%%)" % percentage |
|
1091 |
|
1092 if extra: |
|
1093 line += b" " |
|
1094 line += extra |
|
1095 |
|
1096 line += b'\n' |
|
1097 ui.write_err(line) |
|
1098 |
|
1099 |
|
1100 def display_bundling_debug_info( |
|
1101 ui, |
|
1102 debug_info, |
|
1103 cl_debug_info, |
|
1104 mn_debug_info, |
|
1105 fl_debug_info, |
|
1106 ): |
|
1107 """display debug information gathered during a bundling through `ui`""" |
|
1108 d = debug_info |
|
1109 c = cl_debug_info |
|
1110 m = mn_debug_info |
|
1111 f = fl_debug_info |
|
1112 all_info = [ |
|
1113 (b"changelog", b"cl", c), |
|
1114 (b"manifests", b"mn", m), |
|
1115 (b"files", b"fl", f), |
|
1116 ] |
|
1117 _dbg_bdl_line(ui, 0, b'revisions', d['revision-total']) |
|
1118 _dbg_bdl_line(ui, 1, b'changelog', d['revision-changelog']) |
|
1119 _dbg_bdl_line(ui, 1, b'manifest', d['revision-manifest']) |
|
1120 extra = b'(for %d revlogs)' % d['file-count'] |
|
1121 _dbg_bdl_line(ui, 1, b'files', d['revision-files'], extra=extra) |
|
1122 if d['merge-total']: |
|
1123 _dbg_bdl_line(ui, 1, b'merge', d['merge-total'], d['revision-total']) |
|
1124 for k, __, v in all_info: |
|
1125 if v['merge-total']: |
|
1126 _dbg_bdl_line(ui, 2, k, v['merge-total'], v['revision-total']) |
|
1127 |
|
1128 _dbg_bdl_line(ui, 0, b'deltas') |
|
1129 _dbg_bdl_line( |
|
1130 ui, |
|
1131 1, |
|
1132 b'from-storage', |
|
1133 d['reused-storage-delta'], |
|
1134 percentage_base=d['available-delta'], |
|
1135 percentage_key=b"available", |
|
1136 percentage_ref=d['available-delta'], |
|
1137 ) |
|
1138 |
|
1139 if d['denied-delta-candeltafn']: |
|
1140 _dbg_bdl_line(ui, 2, b'denied-fn', d['denied-delta-candeltafn']) |
|
1141 for __, k, v in all_info: |
|
1142 if v['denied-delta-candeltafn']: |
|
1143 _dbg_bdl_line(ui, 3, k, v['denied-delta-candeltafn']) |
|
1144 |
|
1145 if d['denied-base-not-available']: |
|
1146 _dbg_bdl_line(ui, 2, b'denied-nb', d['denied-base-not-available']) |
|
1147 for k, __, v in all_info: |
|
1148 if v['denied-base-not-available']: |
|
1149 _dbg_bdl_line(ui, 3, k, v['denied-base-not-available']) |
|
1150 |
|
1151 if d['computed-delta']: |
|
1152 _dbg_bdl_line(ui, 1, b'computed', d['computed-delta']) |
|
1153 |
|
1154 if d['available-full']: |
|
1155 _dbg_bdl_line( |
|
1156 ui, |
|
1157 2, |
|
1158 b'full', |
|
1159 d['delta-full'], |
|
1160 percentage_base=d['available-full'], |
|
1161 percentage_key=b"native", |
|
1162 percentage_ref=d['available-full'], |
|
1163 ) |
|
1164 for k, __, v in all_info: |
|
1165 if v['available-full']: |
|
1166 _dbg_bdl_line( |
|
1167 ui, |
|
1168 3, |
|
1169 k, |
|
1170 v['delta-full'], |
|
1171 percentage_base=v['available-full'], |
|
1172 percentage_key=b"native", |
|
1173 percentage_ref=v['available-full'], |
|
1174 ) |
|
1175 |
|
1176 if d['delta-against-prev']: |
|
1177 _dbg_bdl_line(ui, 2, b'previous', d['delta-against-prev']) |
|
1178 for k, __, v in all_info: |
|
1179 if v['delta-against-prev']: |
|
1180 _dbg_bdl_line(ui, 3, k, v['delta-against-prev']) |
|
1181 |
|
1182 if d['delta-against-p1']: |
|
1183 _dbg_bdl_line(ui, 2, b'parent-1', d['delta-against-prev']) |
|
1184 for k, __, v in all_info: |
|
1185 if v['delta-against-p1']: |
|
1186 _dbg_bdl_line(ui, 3, k, v['delta-against-p1']) |
|
1004 |
1187 |
1005 |
1188 |
1006 class cgpacker: |
1189 class cgpacker: |
1007 def __init__( |
1190 def __init__( |
1008 self, |
1191 self, |
1084 self._verbosenote = self._repo.ui.note |
1267 self._verbosenote = self._repo.ui.note |
1085 else: |
1268 else: |
1086 self._verbosenote = lambda s: None |
1269 self._verbosenote = lambda s: None |
1087 |
1270 |
1088 def generate( |
1271 def generate( |
1089 self, commonrevs, clnodes, fastpathlinkrev, source, changelog=True |
1272 self, |
1273 commonrevs, |
|
1274 clnodes, |
|
1275 fastpathlinkrev, |
|
1276 source, |
|
1277 changelog=True, |
|
1090 ): |
1278 ): |
1091 """Yield a sequence of changegroup byte chunks. |
1279 """Yield a sequence of changegroup byte chunks. |
1092 If changelog is False, changelog data won't be added to changegroup |
1280 If changelog is False, changelog data won't be added to changegroup |
1093 """ |
1281 """ |
1094 |
1282 |
1283 debug_info = None |
|
1095 repo = self._repo |
1284 repo = self._repo |
1285 if repo.ui.configbool(b'debug', b'bundling-stats'): |
|
1286 debug_info = make_debug_info() |
|
1096 cl = repo.changelog |
1287 cl = repo.changelog |
1097 |
1288 |
1098 self._verbosenote(_(b'uncompressed size of bundle content:\n')) |
1289 self._verbosenote(_(b'uncompressed size of bundle content:\n')) |
1099 size = 0 |
1290 size = 0 |
1100 |
1291 |
1105 # We're our own remote when stripping, get the no-op helpers |
1296 # We're our own remote when stripping, get the no-op helpers |
1106 # TODO a better approach would be for the strip bundle to |
1297 # TODO a better approach would be for the strip bundle to |
1107 # correctly advertise its sidedata categories directly. |
1298 # correctly advertise its sidedata categories directly. |
1108 remote_sidedata = repo._wanted_sidedata |
1299 remote_sidedata = repo._wanted_sidedata |
1109 sidedata_helpers = sidedatamod.get_sidedata_helpers( |
1300 sidedata_helpers = sidedatamod.get_sidedata_helpers( |
1110 repo, remote_sidedata |
1301 repo, |
1302 remote_sidedata, |
|
1111 ) |
1303 ) |
1112 |
1304 |
1305 cl_debug_info = None |
|
1306 if debug_info is not None: |
|
1307 cl_debug_info = make_debug_info() |
|
1113 clstate, deltas = self._generatechangelog( |
1308 clstate, deltas = self._generatechangelog( |
1114 cl, |
1309 cl, |
1115 clnodes, |
1310 clnodes, |
1116 generate=changelog, |
1311 generate=changelog, |
1117 sidedata_helpers=sidedata_helpers, |
1312 sidedata_helpers=sidedata_helpers, |
1313 debug_info=cl_debug_info, |
|
1118 ) |
1314 ) |
1119 for delta in deltas: |
1315 for delta in deltas: |
1120 for chunk in _revisiondeltatochunks( |
1316 for chunk in _revisiondeltatochunks( |
1121 self._repo, delta, self._builddeltaheader |
1317 self._repo, delta, self._builddeltaheader |
1122 ): |
1318 ): |
1124 yield chunk |
1320 yield chunk |
1125 |
1321 |
1126 close = closechunk() |
1322 close = closechunk() |
1127 size += len(close) |
1323 size += len(close) |
1128 yield closechunk() |
1324 yield closechunk() |
1325 if debug_info is not None: |
|
1326 merge_debug_info(debug_info, cl_debug_info) |
|
1327 debug_info['revision-changelog'] = cl_debug_info['revision-total'] |
|
1129 |
1328 |
1130 self._verbosenote(_(b'%8.i (changelog)\n') % size) |
1329 self._verbosenote(_(b'%8.i (changelog)\n') % size) |
1131 |
1330 |
1132 clrevorder = clstate[b'clrevorder'] |
1331 clrevorder = clstate[b'clrevorder'] |
1133 manifests = clstate[b'manifests'] |
1332 manifests = clstate[b'manifests'] |
1134 changedfiles = clstate[b'changedfiles'] |
1333 changedfiles = clstate[b'changedfiles'] |
1334 |
|
1335 if debug_info is not None: |
|
1336 debug_info['file-count'] = len(changedfiles) |
|
1135 |
1337 |
1136 # We need to make sure that the linkrev in the changegroup refers to |
1338 # We need to make sure that the linkrev in the changegroup refers to |
1137 # the first changeset that introduced the manifest or file revision. |
1339 # the first changeset that introduced the manifest or file revision. |
1138 # The fastpath is usually safer than the slowpath, because the filelogs |
1340 # The fastpath is usually safer than the slowpath, because the filelogs |
1139 # are walked in revlog order. |
1341 # are walked in revlog order. |
1154 fastpathlinkrev = fastpathlinkrev and not scmutil.istreemanifest(repo) |
1356 fastpathlinkrev = fastpathlinkrev and not scmutil.istreemanifest(repo) |
1155 |
1357 |
1156 fnodes = {} # needed file nodes |
1358 fnodes = {} # needed file nodes |
1157 |
1359 |
1158 size = 0 |
1360 size = 0 |
1361 mn_debug_info = None |
|
1362 if debug_info is not None: |
|
1363 mn_debug_info = make_debug_info() |
|
1159 it = self.generatemanifests( |
1364 it = self.generatemanifests( |
1160 commonrevs, |
1365 commonrevs, |
1161 clrevorder, |
1366 clrevorder, |
1162 fastpathlinkrev, |
1367 fastpathlinkrev, |
1163 manifests, |
1368 manifests, |
1164 fnodes, |
1369 fnodes, |
1165 source, |
1370 source, |
1166 clstate[b'clrevtomanifestrev'], |
1371 clstate[b'clrevtomanifestrev'], |
1167 sidedata_helpers=sidedata_helpers, |
1372 sidedata_helpers=sidedata_helpers, |
1373 debug_info=mn_debug_info, |
|
1168 ) |
1374 ) |
1169 |
1375 |
1170 for tree, deltas in it: |
1376 for tree, deltas in it: |
1171 if tree: |
1377 if tree: |
1172 assert self.version in (b'03', b'04') |
1378 assert self.version in (b'03', b'04') |
1183 yield chunk |
1389 yield chunk |
1184 |
1390 |
1185 close = closechunk() |
1391 close = closechunk() |
1186 size += len(close) |
1392 size += len(close) |
1187 yield close |
1393 yield close |
1394 if debug_info is not None: |
|
1395 merge_debug_info(debug_info, mn_debug_info) |
|
1396 debug_info['revision-manifest'] = mn_debug_info['revision-total'] |
|
1188 |
1397 |
1189 self._verbosenote(_(b'%8.i (manifests)\n') % size) |
1398 self._verbosenote(_(b'%8.i (manifests)\n') % size) |
1190 yield self._manifestsend |
1399 yield self._manifestsend |
1191 |
1400 |
1192 mfdicts = None |
1401 mfdicts = None |
1197 ] |
1406 ] |
1198 |
1407 |
1199 manifests.clear() |
1408 manifests.clear() |
1200 clrevs = {cl.rev(x) for x in clnodes} |
1409 clrevs = {cl.rev(x) for x in clnodes} |
1201 |
1410 |
1411 fl_debug_info = None |
|
1412 if debug_info is not None: |
|
1413 fl_debug_info = make_debug_info() |
|
1202 it = self.generatefiles( |
1414 it = self.generatefiles( |
1203 changedfiles, |
1415 changedfiles, |
1204 commonrevs, |
1416 commonrevs, |
1205 source, |
1417 source, |
1206 mfdicts, |
1418 mfdicts, |
1207 fastpathlinkrev, |
1419 fastpathlinkrev, |
1208 fnodes, |
1420 fnodes, |
1209 clrevs, |
1421 clrevs, |
1210 sidedata_helpers=sidedata_helpers, |
1422 sidedata_helpers=sidedata_helpers, |
1423 debug_info=fl_debug_info, |
|
1211 ) |
1424 ) |
1212 |
1425 |
1213 for path, deltas in it: |
1426 for path, deltas in it: |
1214 h = _fileheader(path) |
1427 h = _fileheader(path) |
1215 size = len(h) |
1428 size = len(h) |
1228 yield close |
1441 yield close |
1229 |
1442 |
1230 self._verbosenote(_(b'%8.i %s\n') % (size, path)) |
1443 self._verbosenote(_(b'%8.i %s\n') % (size, path)) |
1231 |
1444 |
1232 yield closechunk() |
1445 yield closechunk() |
1446 if debug_info is not None: |
|
1447 merge_debug_info(debug_info, fl_debug_info) |
|
1448 debug_info['revision-files'] = fl_debug_info['revision-total'] |
|
1449 |
|
1450 if debug_info is not None: |
|
1451 display_bundling_debug_info( |
|
1452 repo.ui, |
|
1453 debug_info, |
|
1454 cl_debug_info, |
|
1455 mn_debug_info, |
|
1456 fl_debug_info, |
|
1457 ) |
|
1233 |
1458 |
1234 if clnodes: |
1459 if clnodes: |
1235 repo.hook(b'outgoing', node=hex(clnodes[0]), source=source) |
1460 repo.hook(b'outgoing', node=hex(clnodes[0]), source=source) |
1236 |
1461 |
1237 def _generatechangelog( |
1462 def _generatechangelog( |
1238 self, cl, nodes, generate=True, sidedata_helpers=None |
1463 self, |
1464 cl, |
|
1465 nodes, |
|
1466 generate=True, |
|
1467 sidedata_helpers=None, |
|
1468 debug_info=None, |
|
1239 ): |
1469 ): |
1240 """Generate data for changelog chunks. |
1470 """Generate data for changelog chunks. |
1241 |
1471 |
1242 Returns a 2-tuple of a dict containing state and an iterable of |
1472 Returns a 2-tuple of a dict containing state and an iterable of |
1243 byte chunks. The state will not be fully populated until the |
1473 byte chunks. The state will not be fully populated until the |
1330 topic=_(b'changesets'), |
1560 topic=_(b'changesets'), |
1331 clrevtolocalrev={}, |
1561 clrevtolocalrev={}, |
1332 fullclnodes=self._fullclnodes, |
1562 fullclnodes=self._fullclnodes, |
1333 precomputedellipsis=self._precomputedellipsis, |
1563 precomputedellipsis=self._precomputedellipsis, |
1334 sidedata_helpers=sidedata_helpers, |
1564 sidedata_helpers=sidedata_helpers, |
1565 debug_info=debug_info, |
|
1335 ) |
1566 ) |
1336 |
1567 |
1337 return state, gen |
1568 return state, gen |
1338 |
1569 |
1339 def generatemanifests( |
1570 def generatemanifests( |
1344 manifests, |
1575 manifests, |
1345 fnodes, |
1576 fnodes, |
1346 source, |
1577 source, |
1347 clrevtolocalrev, |
1578 clrevtolocalrev, |
1348 sidedata_helpers=None, |
1579 sidedata_helpers=None, |
1580 debug_info=None, |
|
1349 ): |
1581 ): |
1350 """Returns an iterator of changegroup chunks containing manifests. |
1582 """Returns an iterator of changegroup chunks containing manifests. |
1351 |
1583 |
1352 `source` is unused here, but is used by extensions like remotefilelog to |
1584 `source` is unused here, but is used by extensions like remotefilelog to |
1353 change what is sent based in pulls vs pushes, etc. |
1585 change what is sent based in pulls vs pushes, etc. |
1442 topic=_(b'manifests'), |
1674 topic=_(b'manifests'), |
1443 clrevtolocalrev=clrevtolocalrev, |
1675 clrevtolocalrev=clrevtolocalrev, |
1444 fullclnodes=self._fullclnodes, |
1676 fullclnodes=self._fullclnodes, |
1445 precomputedellipsis=self._precomputedellipsis, |
1677 precomputedellipsis=self._precomputedellipsis, |
1446 sidedata_helpers=sidedata_helpers, |
1678 sidedata_helpers=sidedata_helpers, |
1679 debug_info=debug_info, |
|
1447 ) |
1680 ) |
1448 |
1681 |
1449 if not self._oldmatcher.visitdir(store.tree[:-1]): |
1682 if not self._oldmatcher.visitdir(store.tree[:-1]): |
1450 yield tree, deltas |
1683 yield tree, deltas |
1451 else: |
1684 else: |
1481 mfdicts, |
1714 mfdicts, |
1482 fastpathlinkrev, |
1715 fastpathlinkrev, |
1483 fnodes, |
1716 fnodes, |
1484 clrevs, |
1717 clrevs, |
1485 sidedata_helpers=None, |
1718 sidedata_helpers=None, |
1719 debug_info=None, |
|
1486 ): |
1720 ): |
1487 changedfiles = [ |
1721 changedfiles = [ |
1488 f |
1722 f |
1489 for f in changedfiles |
1723 for f in changedfiles |
1490 if self._matcher(f) and not self._oldmatcher(f) |
1724 if self._matcher(f) and not self._oldmatcher(f) |
1576 ellipses=self._ellipses, |
1810 ellipses=self._ellipses, |
1577 clrevtolocalrev=clrevtolocalrev, |
1811 clrevtolocalrev=clrevtolocalrev, |
1578 fullclnodes=self._fullclnodes, |
1812 fullclnodes=self._fullclnodes, |
1579 precomputedellipsis=self._precomputedellipsis, |
1813 precomputedellipsis=self._precomputedellipsis, |
1580 sidedata_helpers=sidedata_helpers, |
1814 sidedata_helpers=sidedata_helpers, |
1815 debug_info=debug_info, |
|
1581 ) |
1816 ) |
1582 |
1817 |
1583 yield fname, deltas |
1818 yield fname, deltas |
1584 |
1819 |
1585 progress.complete() |
1820 progress.complete() |
1865 for node in nodes: |
2100 for node in nodes: |
1866 repo.ui.debug(b"%s\n" % hex(node)) |
2101 repo.ui.debug(b"%s\n" % hex(node)) |
1867 |
2102 |
1868 |
2103 |
1869 def makechangegroup( |
2104 def makechangegroup( |
1870 repo, outgoing, version, source, fastpath=False, bundlecaps=None |
2105 repo, |
2106 outgoing, |
|
2107 version, |
|
2108 source, |
|
2109 fastpath=False, |
|
2110 bundlecaps=None, |
|
1871 ): |
2111 ): |
1872 cgstream = makestream( |
2112 cgstream = makestream( |
1873 repo, |
2113 repo, |
1874 outgoing, |
2114 outgoing, |
1875 version, |
2115 version, |
1915 repo.filtername is None and heads == sorted(repo.heads()) |
2155 repo.filtername is None and heads == sorted(repo.heads()) |
1916 ) |
2156 ) |
1917 |
2157 |
1918 repo.hook(b'preoutgoing', throw=True, source=source) |
2158 repo.hook(b'preoutgoing', throw=True, source=source) |
1919 _changegroupinfo(repo, csets, source) |
2159 _changegroupinfo(repo, csets, source) |
1920 return bundler.generate(commonrevs, csets, fastpathlinkrev, source) |
2160 return bundler.generate( |
2161 commonrevs, |
|
2162 csets, |
|
2163 fastpathlinkrev, |
|
2164 source, |
|
2165 ) |
|
1921 |
2166 |
1922 |
2167 |
1923 def _addchangegroupfiles( |
2168 def _addchangegroupfiles( |
1924 repo, |
2169 repo, |
1925 source, |
2170 source, |