Show More
@@ -0,0 +1,118 b'' | |||||
|
1 | # v2.py - Pure-Python implementation of the dirstate-v2 file format | |||
|
2 | # | |||
|
3 | # Copyright Mercurial Contributors | |||
|
4 | # | |||
|
5 | # This software may be used and distributed according to the terms of the | |||
|
6 | # GNU General Public License version 2 or any later version. | |||
|
7 | ||||
|
8 | from __future__ import absolute_import | |||
|
9 | ||||
|
10 | import struct | |||
|
11 | ||||
|
12 | from .. import policy | |||
|
13 | ||||
|
14 | parsers = policy.importmod('parsers') | |||
|
15 | ||||
|
16 | ||||
|
17 | # Must match the constant of the same name in | |||
|
18 | # `rust/hg-core/src/dirstate_tree/on_disk.rs` | |||
|
19 | TREE_METADATA_SIZE = 44 | |||
|
20 | NODE_SIZE = 43 | |||
|
21 | ||||
|
22 | ||||
|
23 | # Must match the `TreeMetadata` Rust struct in | |||
|
24 | # `rust/hg-core/src/dirstate_tree/on_disk.rs`. See doc-comments there. | |||
|
25 | # | |||
|
26 | # * 4 bytes: start offset of root nodes | |||
|
27 | # * 4 bytes: number of root nodes | |||
|
28 | # * 4 bytes: total number of nodes in the tree that have an entry | |||
|
29 | # * 4 bytes: total number of nodes in the tree that have a copy source | |||
|
30 | # * 4 bytes: number of bytes in the data file that are not used anymore | |||
|
31 | # * 4 bytes: unused | |||
|
32 | # * 20 bytes: SHA-1 hash of ignore patterns | |||
|
33 | TREE_METADATA = struct.Struct('>LLLLL4s20s') | |||
|
34 | ||||
|
35 | ||||
|
36 | # Must match the `Node` Rust struct in | |||
|
37 | # `rust/hg-core/src/dirstate_tree/on_disk.rs`. See doc-comments there. | |||
|
38 | # | |||
|
39 | # * 4 bytes: start offset of full path | |||
|
40 | # * 2 bytes: length of the full path | |||
|
41 | # * 2 bytes: length within the full path before its "base name" | |||
|
42 | # * 4 bytes: start offset of the copy source if any, or zero for no copy source | |||
|
43 | # * 2 bytes: length of the copy source if any, or unused | |||
|
44 | # * 4 bytes: start offset of child nodes | |||
|
45 | # * 4 bytes: number of child nodes | |||
|
46 | # * 4 bytes: number of descendant nodes that have an entry | |||
|
47 | # * 4 bytes: number of descendant nodes that have a "tracked" state | |||
|
48 | # * 1 byte: flags | |||
|
49 | # * 4 bytes: expected size | |||
|
50 | # * 4 bytes: mtime seconds | |||
|
51 | # * 4 bytes: mtime nanoseconds | |||
|
52 | NODE = struct.Struct('>LHHLHLLLLBlll') | |||
|
53 | ||||
|
54 | ||||
|
55 | assert TREE_METADATA_SIZE == TREE_METADATA.size | |||
|
56 | assert NODE_SIZE == NODE.size | |||
|
57 | ||||
|
58 | ||||
|
59 | def parse_dirstate(map, copy_map, data, tree_metadata): | |||
|
60 | """parse a full v2-dirstate from a binary data into dictionnaries: | |||
|
61 | ||||
|
62 | - map: a {path: entry} mapping that will be filled | |||
|
63 | - copy_map: a {path: copy-source} mapping that will be filled | |||
|
64 | - data: a binary blob contains v2 nodes data | |||
|
65 | - tree_metadata:: a binary blob of the top level node (from the docket) | |||
|
66 | """ | |||
|
67 | ( | |||
|
68 | root_nodes_start, | |||
|
69 | root_nodes_len, | |||
|
70 | _nodes_with_entry_count, | |||
|
71 | _nodes_with_copy_source_count, | |||
|
72 | _unreachable_bytes, | |||
|
73 | _unused, | |||
|
74 | _ignore_patterns_hash, | |||
|
75 | ) = TREE_METADATA.unpack(tree_metadata) | |||
|
76 | parse_nodes(map, copy_map, data, root_nodes_start, root_nodes_len) | |||
|
77 | ||||
|
78 | ||||
|
79 | def parse_nodes(map, copy_map, data, start, len): | |||
|
80 | """parse <len> nodes from <data> starting at offset <start> | |||
|
81 | ||||
|
82 | This is used by parse_dirstate to recursively fill `map` and `copy_map`. | |||
|
83 | """ | |||
|
84 | for i in range(len): | |||
|
85 | node_start = start + NODE_SIZE * i | |||
|
86 | node_bytes = slice_with_len(data, node_start, NODE_SIZE) | |||
|
87 | ( | |||
|
88 | path_start, | |||
|
89 | path_len, | |||
|
90 | _basename_start, | |||
|
91 | copy_source_start, | |||
|
92 | copy_source_len, | |||
|
93 | children_start, | |||
|
94 | children_count, | |||
|
95 | _descendants_with_entry_count, | |||
|
96 | _tracked_descendants_count, | |||
|
97 | flags, | |||
|
98 | size, | |||
|
99 | mtime_s, | |||
|
100 | _mtime_ns, | |||
|
101 | ) = NODE.unpack(node_bytes) | |||
|
102 | ||||
|
103 | # Parse child nodes of this node recursively | |||
|
104 | parse_nodes(map, copy_map, data, children_start, children_count) | |||
|
105 | ||||
|
106 | item = parsers.DirstateItem.from_v2_data(flags, size, mtime_s) | |||
|
107 | if not item.any_tracked: | |||
|
108 | continue | |||
|
109 | path = slice_with_len(data, path_start, path_len) | |||
|
110 | map[path] = item | |||
|
111 | if copy_source_start: | |||
|
112 | copy_map[path] = slice_with_len( | |||
|
113 | data, copy_source_start, copy_source_len | |||
|
114 | ) | |||
|
115 | ||||
|
116 | ||||
|
117 | def slice_with_len(data, start, len): | |||
|
118 | return data[start : start + len] |
@@ -347,6 +347,33 b' static PyObject *dirstate_item_from_v1_m' | |||||
347 | return (PyObject *)dirstate_item_from_v1_data(state, mode, size, mtime); |
|
347 | return (PyObject *)dirstate_item_from_v1_data(state, mode, size, mtime); | |
348 | }; |
|
348 | }; | |
349 |
|
349 | |||
|
350 | static PyObject *dirstate_item_from_v2_meth(PyTypeObject *subtype, | |||
|
351 | PyObject *args) | |||
|
352 | { | |||
|
353 | dirstateItemObject *t = | |||
|
354 | PyObject_New(dirstateItemObject, &dirstateItemType); | |||
|
355 | if (!t) { | |||
|
356 | return NULL; | |||
|
357 | } | |||
|
358 | if (!PyArg_ParseTuple(args, "bii", &t->flags, &t->size, &t->mtime)) { | |||
|
359 | return NULL; | |||
|
360 | } | |||
|
361 | t->mode = 0; | |||
|
362 | if (t->flags & dirstate_flag_has_meaningful_data) { | |||
|
363 | if (t->flags & dirstate_flag_mode_exec_perm) { | |||
|
364 | t->mode = 0755; | |||
|
365 | } else { | |||
|
366 | t->mode = 0644; | |||
|
367 | } | |||
|
368 | if (t->flags & dirstate_flag_mode_is_symlink) { | |||
|
369 | t->mode |= S_IFLNK; | |||
|
370 | } else { | |||
|
371 | t->mode |= S_IFREG; | |||
|
372 | } | |||
|
373 | } | |||
|
374 | return (PyObject *)t; | |||
|
375 | }; | |||
|
376 | ||||
350 | /* This means the next status call will have to actually check its content |
|
377 | /* This means the next status call will have to actually check its content | |
351 | to make sure it is correct. */ |
|
378 | to make sure it is correct. */ | |
352 | static PyObject *dirstate_item_set_possibly_dirty(dirstateItemObject *self) |
|
379 | static PyObject *dirstate_item_set_possibly_dirty(dirstateItemObject *self) | |
@@ -413,6 +440,8 b' static PyMethodDef dirstate_item_methods' | |||||
413 | "True if the stored mtime would be ambiguous with the current time"}, |
|
440 | "True if the stored mtime would be ambiguous with the current time"}, | |
414 | {"from_v1_data", (PyCFunction)dirstate_item_from_v1_meth, |
|
441 | {"from_v1_data", (PyCFunction)dirstate_item_from_v1_meth, | |
415 | METH_VARARGS | METH_CLASS, "build a new DirstateItem object from V1 data"}, |
|
442 | METH_VARARGS | METH_CLASS, "build a new DirstateItem object from V1 data"}, | |
|
443 | {"from_v2_data", (PyCFunction)dirstate_item_from_v2_meth, | |||
|
444 | METH_VARARGS | METH_CLASS, "build a new DirstateItem object from V2 data"}, | |||
416 | {"set_possibly_dirty", (PyCFunction)dirstate_item_set_possibly_dirty, |
|
445 | {"set_possibly_dirty", (PyCFunction)dirstate_item_set_possibly_dirty, | |
417 | METH_NOARGS, "mark a file as \"possibly dirty\""}, |
|
446 | METH_NOARGS, "mark a file as \"possibly dirty\""}, | |
418 | {"set_clean", (PyCFunction)dirstate_item_set_clean, METH_VARARGS, |
|
447 | {"set_clean", (PyCFunction)dirstate_item_set_clean, METH_VARARGS, |
@@ -36,6 +36,8 b' static const unsigned char dirstate_flag' | |||||
36 | static const unsigned char dirstate_flag_p2_info = 1 << 2; |
|
36 | static const unsigned char dirstate_flag_p2_info = 1 << 2; | |
37 | static const unsigned char dirstate_flag_has_meaningful_data = 1 << 3; |
|
37 | static const unsigned char dirstate_flag_has_meaningful_data = 1 << 3; | |
38 | static const unsigned char dirstate_flag_has_meaningful_mtime = 1 << 4; |
|
38 | static const unsigned char dirstate_flag_has_meaningful_mtime = 1 << 4; | |
|
39 | static const unsigned char dirstate_flag_mode_exec_perm = 1 << 5; | |||
|
40 | static const unsigned char dirstate_flag_mode_is_symlink = 1 << 6; | |||
39 |
|
41 | |||
40 | extern PyTypeObject dirstateItemType; |
|
42 | extern PyTypeObject dirstateItemType; | |
41 | #define dirstate_tuple_check(op) (Py_TYPE(op) == &dirstateItemType) |
|
43 | #define dirstate_tuple_check(op) (Py_TYPE(op) == &dirstateItemType) |
@@ -20,6 +20,7 b' from . import (' | |||||
20 |
|
20 | |||
21 | from .dirstateutils import ( |
|
21 | from .dirstateutils import ( | |
22 | docket as docketmod, |
|
22 | docket as docketmod, | |
|
23 | v2, | |||
23 | ) |
|
24 | ) | |
24 |
|
25 | |||
25 | parsers = policy.importmod('parsers') |
|
26 | parsers = policy.importmod('parsers') |
@@ -10,14 +10,10 b' from __future__ import absolute_import' | |||||
10 | import struct |
|
10 | import struct | |
11 |
|
11 | |||
12 | from ..revlogutils import docket as docket_mod |
|
12 | from ..revlogutils import docket as docket_mod | |
13 |
|
13 | from . import v2 | ||
14 |
|
14 | |||
15 | V2_FORMAT_MARKER = b"dirstate-v2\n" |
|
15 | V2_FORMAT_MARKER = b"dirstate-v2\n" | |
16 |
|
16 | |||
17 | # Must match the constant of the same name in |
|
|||
18 | # `rust/hg-core/src/dirstate_tree/on_disk.rs` |
|
|||
19 | TREE_METADATA_SIZE = 44 |
|
|||
20 |
|
||||
21 | # * 12 bytes: format marker |
|
17 | # * 12 bytes: format marker | |
22 | # * 32 bytes: node ID of the working directory's first parent |
|
18 | # * 32 bytes: node ID of the working directory's first parent | |
23 | # * 32 bytes: node ID of the working directory's second parent |
|
19 | # * 32 bytes: node ID of the working directory's second parent | |
@@ -29,7 +25,7 b' TREE_METADATA_SIZE = 44' | |||||
29 | # Node IDs are null-padded if shorter than 32 bytes. |
|
25 | # Node IDs are null-padded if shorter than 32 bytes. | |
30 | # A data file shorter than the specified used size is corrupted (truncated) |
|
26 | # A data file shorter than the specified used size is corrupted (truncated) | |
31 | HEADER = struct.Struct( |
|
27 | HEADER = struct.Struct( | |
32 | ">{}s32s32s{}sLB".format(len(V2_FORMAT_MARKER), TREE_METADATA_SIZE) |
|
28 | ">{}s32s32s{}sLB".format(len(V2_FORMAT_MARKER), v2.TREE_METADATA_SIZE) | |
33 | ) |
|
29 | ) | |
34 |
|
30 | |||
35 |
|
31 |
@@ -7,6 +7,7 b'' | |||||
7 |
|
7 | |||
8 | from __future__ import absolute_import |
|
8 | from __future__ import absolute_import | |
9 |
|
9 | |||
|
10 | import stat | |||
10 | import struct |
|
11 | import struct | |
11 | import zlib |
|
12 | import zlib | |
12 |
|
13 | |||
@@ -43,6 +44,15 b' NONNORMAL = -1' | |||||
43 | # a special value used internally for `time` if the time is ambigeous |
|
44 | # a special value used internally for `time` if the time is ambigeous | |
44 | AMBIGUOUS_TIME = -1 |
|
45 | AMBIGUOUS_TIME = -1 | |
45 |
|
46 | |||
|
47 | # Bits of the `flags` byte inside a node in the file format | |||
|
48 | DIRSTATE_V2_WDIR_TRACKED = 1 << 0 | |||
|
49 | DIRSTATE_V2_P1_TRACKED = 1 << 1 | |||
|
50 | DIRSTATE_V2_P2_INFO = 1 << 2 | |||
|
51 | DIRSTATE_V2_HAS_MODE_AND_SIZE = 1 << 3 | |||
|
52 | DIRSTATE_V2_HAS_MTIME = 1 << 4 | |||
|
53 | DIRSTATE_V2_MODE_EXEC_PERM = 1 << 5 | |||
|
54 | DIRSTATE_V2_MODE_IS_SYMLINK = 1 << 6 | |||
|
55 | ||||
46 |
|
56 | |||
47 | @attr.s(slots=True, init=False) |
|
57 | @attr.s(slots=True, init=False) | |
48 | class DirstateItem(object): |
|
58 | class DirstateItem(object): | |
@@ -109,6 +119,30 b' class DirstateItem(object):' | |||||
109 | self._mtime = parentfiledata[2] |
|
119 | self._mtime = parentfiledata[2] | |
110 |
|
120 | |||
111 | @classmethod |
|
121 | @classmethod | |
|
122 | def from_v2_data(cls, flags, size, mtime): | |||
|
123 | """Build a new DirstateItem object from V2 data""" | |||
|
124 | has_mode_size = bool(flags & DIRSTATE_V2_HAS_MODE_AND_SIZE) | |||
|
125 | mode = None | |||
|
126 | if has_mode_size: | |||
|
127 | assert stat.S_IXUSR == 0o100 | |||
|
128 | if flags & DIRSTATE_V2_MODE_EXEC_PERM: | |||
|
129 | mode = 0o755 | |||
|
130 | else: | |||
|
131 | mode = 0o644 | |||
|
132 | if flags & DIRSTATE_V2_MODE_IS_SYMLINK: | |||
|
133 | mode |= stat.S_IFLNK | |||
|
134 | else: | |||
|
135 | mode |= stat.S_IFREG | |||
|
136 | return cls( | |||
|
137 | wc_tracked=bool(flags & DIRSTATE_V2_WDIR_TRACKED), | |||
|
138 | p1_tracked=bool(flags & DIRSTATE_V2_P1_TRACKED), | |||
|
139 | p2_info=bool(flags & DIRSTATE_V2_P2_INFO), | |||
|
140 | has_meaningful_data=has_mode_size, | |||
|
141 | has_meaningful_mtime=bool(flags & DIRSTATE_V2_HAS_MTIME), | |||
|
142 | parentfiledata=(mode, size, mtime), | |||
|
143 | ) | |||
|
144 | ||||
|
145 | @classmethod | |||
112 | def from_v1_data(cls, state, mode, size, mtime): |
|
146 | def from_v1_data(cls, state, mode, size, mtime): | |
113 | """Build a new DirstateItem object from V1 data |
|
147 | """Build a new DirstateItem object from V1 data | |
114 |
|
148 |
@@ -31,10 +31,8 b' const USED_NODE_ID_BYTES: usize = 20;' | |||||
31 | pub(super) const IGNORE_PATTERNS_HASH_LEN: usize = 20; |
|
31 | pub(super) const IGNORE_PATTERNS_HASH_LEN: usize = 20; | |
32 | pub(super) type IgnorePatternsHash = [u8; IGNORE_PATTERNS_HASH_LEN]; |
|
32 | pub(super) type IgnorePatternsHash = [u8; IGNORE_PATTERNS_HASH_LEN]; | |
33 |
|
33 | |||
34 |
/// Must match |
|
34 | /// Must match constants of the same names in `mercurial/dirstateutils/v2.py` | |
35 | /// `mercurial/dirstateutils/docket.py` |
|
|||
36 | const TREE_METADATA_SIZE: usize = 44; |
|
35 | const TREE_METADATA_SIZE: usize = 44; | |
37 |
|
||||
38 | const NODE_SIZE: usize = 43; |
|
36 | const NODE_SIZE: usize = 43; | |
39 |
|
37 | |||
40 | /// Make sure that size-affecting changes are made knowingly |
|
38 | /// Make sure that size-affecting changes are made knowingly |
General Comments 0
You need to be logged in to leave comments.
Login now