##// END OF EJS Templates
branching: merge stable into default
Raphaël Gomès -
r51644:0a55206c merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,90 b''
1 = Mercurial 6.5 =
2
3 As usual, a lot of patches don't make it to this list since they're more internal.
4
5 == New Features ==
6
7 * Improved Python 3.12 compatiblity
8 * configitems: enable changegroup3 by default (unless using infinitepush)
9 * extras: expose 'retained_extras' for extensions to extend
10 * stabletailgraph: implement stable-tail sort
11 * stabletailgraph: naive version of leap computation
12 * bundle: introduce a "v3" spec
13 * clone-bundles: add a basic first version of automatic bundle generation
14 * clone-bundles: garbage collect older bundle when generating new ones
15 * clone-bundles: only regenerate the clone bundle when cached ration is low
16 * clone-bundles: also control automation based on absolute number of revisions
17 * clone-bundles: add a configuration to control auto-generation on changes
18 * clone-bundles: introduce a command to refresh bundle
19 * clone-bundles: add a command to clear all bundles
20 * clone-bundles: add an option to generate bundles in the background
21 * clonebundles: add support for inline (streaming) clonebundles
22 * clonebundles: adds a auto-generate.serve-inline option
23 * match: add `filepath:` pattern to match an exact filepath relative to the root
24 * hgweb: add "children" into the JSON template for a changeset
25 * hgweb: add support to explicitly access hidden changesets
26 * pull: add --remote-hidden option and pass it through peer creation
27 * hidden: add support for --remote-hidden to HTTP peer
28 * hidden: support passing --hidden with `serve --stdio`
29 * hidden: add support to explicitly access hidden changesets with SSH peers
30 * perf: introduce a `perf::stream-locked-section` command
31 * perf: add a function to find a stream version generator
32 * perf: add support for stream-v3 during benchmark
33 * perf: add a perf::stream-generate command
34 * perf: add a perf::stream-consume
35 * cli: make debugnodemap capable of inspecting an arbitrary nodemap
36 * rust: configure MSRV in Clippy
37 * rhg: make `rhg files` work if `ui.relative-files=true` is specified
38 * rhg: support `rhg files` with `ui.relative-paths=false`
39 * rhg: support `status --print0`
40 * tree-manifest: allow `debugupgraderepo` to run on tree manifest repo
41 * library: enable runpy invocation on mercurial package
42 * library: incorporate demandimport into runpy invocation
43 * exchange: allow passing no includes/excludes to `pull()`
44
45 == New Experimental Features ==
46
47 * stream-clone: add an experimental v3 version of the protocol
48 * stream-clone: support streamv3 on the cli [hg bundle]
49
50 == Bug Fixes ==
51
52 * mail: add a missing argument to properly override starttls
53 * bundle: include required phases when saving a bundle (issue6794)
54 * outgoing: fix common-heads computation from `missingroots` argument
55 * strip: do not include internal changeset in the strip backup
56 * bundle: abort if the user request bundling of internal changesets
57 * bundle: prevent implicit bundling of internal changeset
58 * encoding: avoid quadratic time complexity when json-encoding non-UTF8 strings
59 * sha1dc: Make sure SHA1DC_BIGENDIAN is set on Darwin/PowerPC
60 * zstd: hack include order to ensure that our zstd.h is found
61 * dirstate: better error messages when dirstate is corrupted
62 * stream-clone: avoid opening a revlog in case we do not need it
63 * treemanifest: make `updatecaches` update the nodemaps for all directories
64 * rust-hg-core: move from `ouroboros` to `self_cell`
65 * rust-dependencies: switch from `users` to `whoami`
66 * dirstate-v2: actually fix the dirstate-v2 upgrade race
67 * dirstate: avoid leaking disk space in `hg debugrebuilddirstate`
68 * clonebundles: add warning if auto-generate is enabled without formats
69 * win32mbcs: unbyteify some strings for py3 support
70 * rust-revlog: fix incorrect results with NULL_NODE prefixes
71 * rust-revlog: fix RevlogEntry.data() for NULL_REVISION
72
73 == Backwards Compatibility Changes ==
74
75 * infinitepush: aggressively deprecated infinite push
76 * narrow: indicated the default of 'Yes' when confirming auto-remove-includes
77
78 == Internal API Changes ==
79
80 * Store walk was reworked to fix small race conditions in stream-clone and
81 greatly improve its API robustness and flexibility.
82
83 == Miscellaneous ==
84
85 * Typechecking support was improved in a lot of places
86 * Removed more useless compat code for now unsupported Python versions
87 * Sped up zstd usage in Rust contexts
88 * revlog: add an exception hint when processing LFS flags without the extension
89 * ui: keep the progress bar around when writing if stdout is not a tty
90 * transaction: use a ".bck" extension for all backup file
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,248 +1,250 b''
1 1 35fb62a3a673d5322f6274a44ba6456e5e4b3b37 0 iD8DBQBEYmO2ywK+sNU5EO8RAnaYAKCO7x15xUn5mnhqWNXqk/ehlhRt2QCfRDfY0LrUq2q4oK/KypuJYPHgq1A=
2 2 2be3001847cb18a23c403439d9e7d0ace30804e9 0 iD8DBQBExUbjywK+sNU5EO8RAhzxAKCtyHAQUzcTSZTqlfJ0by6vhREwWQCghaQFHfkfN0l9/40EowNhuMOKnJk=
3 3 36a957364b1b89c150f2d0e60a99befe0ee08bd3 0 iD8DBQBFfL2QywK+sNU5EO8RAjYFAKCoGlaWRTeMsjdmxAjUYx6diZxOBwCfY6IpBYsKvPTwB3oktnPt5Rmrlys=
4 4 27230c29bfec36d5540fbe1c976810aefecfd1d2 0 iD8DBQBFheweywK+sNU5EO8RAt7VAKCrqJQWT2/uo2RWf0ZI4bLp6v82jACgjrMdsaTbxRsypcmEsdPhlG6/8F4=
5 5 fb4b6d5fe100b0886f8bc3d6731ec0e5ed5c4694 0 iD8DBQBGgHicywK+sNU5EO8RAgNxAJ0VG8ixAaeudx4sZbhngI1syu49HQCeNUJQfWBgA8bkJ2pvsFpNxwYaX3I=
6 6 23889160905a1b09fffe1c07378e9fc1827606eb 0 iD8DBQBHGTzoywK+sNU5EO8RAr/UAJ0Y8s4jQtzgS+G9vM8z6CWBThZ8fwCcCT5XDj2XwxKkz/0s6UELwjsO3LU=
7 7 bae2e9c838e90a393bae3973a7850280413e091a 0 iD8DBQBH6DO5ywK+sNU5EO8RAsfrAJ0e4r9c9GF/MJsM7Xjd3NesLRC3+ACffj6+6HXdZf8cswAoFPO+DY00oD0=
8 8 d5cbbe2c49cee22a9fbeb9ea41daa0ac4e26b846 0 iD8DBQBINdwsywK+sNU5EO8RAjIUAKCPmlFJSpsPAAUKF+iNHAwVnwmzeQCdEXrL27CWclXuUKdbQC8De7LICtE=
9 9 d2375bbee6d47e62ba8e415c86e83a465dc4dce9 0 iD8DBQBIo1wpywK+sNU5EO8RAmRNAJ94x3OFt6blbqu/yBoypm/AJ44fuACfUaldXcV5z9tht97hSp22DVTEPGc=
10 10 2a67430f92f15ea5159c26b09ec4839a0c549a26 0 iEYEABECAAYFAkk1hykACgkQywK+sNU5EO85QACeNJNUanjc2tl4wUoPHNuv+lSj0ZMAoIm93wSTc/feyYnO2YCaQ1iyd9Nu
11 11 3773e510d433969e277b1863c317b674cbee2065 0 iEYEABECAAYFAklNbbAACgkQywK+sNU5EO8o+gCfeb2/lfIJZMvyDA1m+G1CsBAxfFsAoIa6iAMG8SBY7hW1Q85Yf/LXEvaE
12 12 11a4eb81fb4f4742451591489e2797dc47903277 0 iEYEABECAAYFAklcAnsACgkQywK+sNU5EO+uXwCbBVHNNsLy1g7BlAyQJwadYVyHOXoAoKvtAVO71+bv7EbVoukwTzT+P4Sx
13 13 11efa41037e280d08cfb07c09ad485df30fb0ea8 0 iEYEABECAAYFAkmvJRQACgkQywK+sNU5EO9XZwCeLMgDgPSMWMm6vgjL4lDs2pEc5+0AnRxfiFbpbBfuEFTqKz9nbzeyoBlx
14 14 02981000012e3adf40c4849bd7b3d5618f9ce82d 0 iEYEABECAAYFAknEH3wACgkQywK+sNU5EO+uXwCeI+LbLMmhjU1lKSfU3UWJHjjUC7oAoIZLvYDGOL/tNZFUuatc3RnZ2eje
15 15 196d40e7c885fa6e95f89134809b3ec7bdbca34b 0 iEYEABECAAYFAkpL2X4ACgkQywK+sNU5EO9FOwCfXJycjyKJXsvQqKkHrglwOQhEKS4An36GfKzptfN8b1qNc3+ya/5c2WOM
16 16 3ef6c14a1e8e83a31226f5881b7fe6095bbfa6f6 0 iEYEABECAAYFAkpopLIACgkQywK+sNU5EO8QSgCfZ0ztsd071rOa2lhmp9Fyue/WoI0AoLTei80/xrhRlB8L/rZEf2KBl8dA
17 17 31ec469f9b556f11819937cf68ee53f2be927ebf 0 iEYEABECAAYFAksBuxAACgkQywK+sNU5EO+mBwCfagB+A0txzWZ6dRpug3LEoK7Z1QsAoKpbk8vsLjv6/oRDicSk/qBu33+m
18 18 439d7ea6fe3aa4ab9ec274a68846779153789de9 0 iEYEABECAAYFAksVw0kACgkQywK+sNU5EO/oZwCfdfBEkgp38xq6wN2F4nj+SzofrJIAnjmxt04vaJSeOOeHylHvk6lzuQsw
19 19 296a0b14a68621f6990c54fdba0083f6f20935bf 0 iEYEABECAAYFAks+jCoACgkQywK+sNU5EO9J8wCeMUGF9E/gS2UBsqIz56WS4HMPRPUAoI5J95mwEIK8Clrl7qFRidNI6APq
20 20 4aa619c4c2c09907034d9824ebb1dd0e878206eb 0 iEYEABECAAYFAktm9IsACgkQywK+sNU5EO9XGgCgk4HclRQhexEtooPE5GcUCdB6M8EAn2ptOhMVbIoO+JncA+tNACPFXh0O
21 21 ff2704a8ded37fbebd8b6eb5ec733731d725da8a 0 iEYEABECAAYFAkuRoSQACgkQywK+sNU5EO//3QCeJDc5r2uFyFCtAlpSA27DEE5rrxAAn2FSwTy9fhrB3QAdDQlwkEZcQzDh
22 22 2b01dab594167bc0dd33331dbaa6dca3dca1b3aa 0 iEYEABECAAYFAku1IwIACgkQywK+sNU5EO9MjgCdHLVwkTZlNHxhcznZKBL1rjN+J7cAoLLWi9LTL6f/TgBaPSKOy1ublbaW
23 23 39f725929f0c48c5fb3b90c071fc3066012456ca 0 iEYEABECAAYFAkvclvsACgkQywK+sNU5EO9FSwCeL9i5x8ALW/LE5+lCX6MFEAe4MhwAn1ev5o6SX6GrNdDfKweiemfO2VBk
24 24 fdcf80f26604f233dc4d8f0a5ef9d7470e317e8a 0 iEYEABECAAYFAkvsKTkACgkQywK+sNU5EO9qEACgiSiRGvTG2vXGJ65tUSOIYihTuFAAnRzRIqEVSw8M8/RGeUXRps0IzaCO
25 25 24fe2629c6fd0c74c90bd066e77387c2b02e8437 0 iEYEABECAAYFAkwFLRsACgkQywK+sNU5EO+pJACgp13tPI+pbwKZV+LeMjcQ4H6tCZYAoJebzhd6a8yYx6qiwpJxA9BXZNXy
26 26 f786fc4b8764cd2a5526d259cf2f94d8a66924d9 0 iEYEABECAAYFAkwsyxcACgkQywK+sNU5EO+crACfUpNAF57PmClkSri9nJcBjb2goN4AniPCNaKvnki7TnUsi1u2oxltpKKL
27 27 bf1774d95bde614af3956d92b20e2a0c68c5fec7 0 iEYEABECAAYFAkxVwccACgkQywK+sNU5EO+oFQCeJzwZ+we1fIIyBGCddHceOUAN++cAnjvT6A8ZWW0zV21NXIFF1qQmjxJd
28 28 c00f03a4982e467fb6b6bd45908767db6df4771d 0 iEYEABECAAYFAkxXDqsACgkQywK+sNU5EO/GJACfT9Rz4hZOxPQEs91JwtmfjevO84gAmwSmtfo5mmWSm8gtTUebCcdTv0Kf
29 29 ff5cec76b1c5b6be9c3bb923aae8c3c6d079d6b9 0 iD8DBQBMdo+qywK+sNU5EO8RAqQpAJ975BL2CCAiWMz9SXthNQ9xG181IwCgp4O+KViHPkufZVFn2aTKMNvcr1A=
30 30 93d8bff78c96fe7e33237b257558ee97290048a4 0 iD8DBQBMpfvdywK+sNU5EO8RAsxVAJ0UaL1XB51C76JUBhafc9GBefuMxwCdEWkTOzwvE0SarJBe9i008jhbqW4=
31 31 333421b9e0f96c7bc788e5667c146a58a9440a55 0 iD8DBQBMz0HOywK+sNU5EO8RAlsEAJ0USh6yOG7OrWkADGunVt9QimBQnwCbBqeMnKgSbwEw8jZwE3Iz1mdrYlo=
32 32 4438875ec01bd0fc32be92b0872eb6daeed4d44f 0 iD8DBQBM4WYUywK+sNU5EO8RAhCVAJ0dJswachwFAHALmk1x0RJehxzqPQCbBNskP9n/X689jB+btNTZTyKU/fw=
33 33 6aff4f144ad356311318b0011df0bb21f2c97429 0 iD8DBQBM9uxXywK+sNU5EO8RAv+4AKCDj4qKP16GdPaq1tP6BUwpM/M1OACfRyzLPp/qiiN8xJTWoWYSe/XjJug=
34 34 e3bf16703e2601de99e563cdb3a5d50b64e6d320 0 iD8DBQBNH8WqywK+sNU5EO8RAiQTAJ9sBO+TeiGro4si77VVaQaA6jcRUgCfSA28dBbjj0oFoQwvPoZjANiZBH8=
35 35 a6c855c32ea081da3c3b8ff628f1847ff271482f 0 iD8DBQBNSJJ+ywK+sNU5EO8RAoJaAKCweDEF70fu+r1Zn7pYDXdlk5RuSgCeO9gK/eit8Lin/1n3pO7aYguFLok=
36 36 2b2155623ee2559caf288fd333f30475966c4525 0 iD8DBQBNSJeBywK+sNU5EO8RAm1KAJ4hW9Cm9nHaaGJguchBaPLlAr+O3wCgqgmMok8bdAS06N6PL60PSTM//Gg=
37 37 2616325766e3504c8ae7c84bd15ee610901fe91d 0 iD8DBQBNbWy9ywK+sNU5EO8RAlWCAJ4mW8HbzjJj9GpK98muX7k+7EvEHwCfaTLbC/DH3QEsZBhEP+M8tzL6RU4=
38 38 aa1f3be38ab127280761889d2dca906ca465b5f4 0 iD8DBQBNeQq7ywK+sNU5EO8RAlEOAJ4tlEDdetE9lKfjGgjbkcR8PrC3egCfXCfF3qNVvU/2YYjpgvRwevjvDy0=
39 39 b032bec2c0a651ca0ddecb65714bfe6770f67d70 0 iD8DBQBNlg5kywK+sNU5EO8RAnGEAJ9gmEx6MfaR4XcG2m/93vwtfyzs3gCgltzx8/YdHPwqDwRX/WbpYgi33is=
40 40 3cb1e95676ad089596bd81d0937cad37d6e3b7fb 0 iD8DBQBNvTy4ywK+sNU5EO8RAmp8AJ9QnxK4jTJ7G722MyeBxf0UXEdGwACgtlM7BKtNQfbEH/fOW5y+45W88VI=
41 41 733af5d9f6b22387913e1d11350fb8cb7c1487dd 0 iD8DBQBN5q/8ywK+sNU5EO8RArRGAKCNGT94GKIYtSuwZ57z1sQbcw6uLACfffpbMV4NAPMl8womAwg+7ZPKnIU=
42 42 de9eb6b1da4fc522b1cab16d86ca166204c24f25 0 iD8DBQBODhfhywK+sNU5EO8RAr2+AJ4ugbAj8ae8/K0bYZzx3sascIAg1QCeK3b+zbbVVqd3b7CDpwFnaX8kTd4=
43 43 4a43e23b8c55b4566b8200bf69fe2158485a2634 0 iD8DBQBONzIMywK+sNU5EO8RAj5SAJ0aPS3+JHnyI6bHB2Fl0LImbDmagwCdGbDLp1S7TFobxXudOH49bX45Iik=
44 44 d629f1e89021103f1753addcef6b310e4435b184 0 iD8DBQBOWAsBywK+sNU5EO8RAht4AJwJl9oNFopuGkj5m8aKuf7bqPkoAQCeNrEm7UhFsZKYT5iUOjnMV7s2LaM=
45 45 351a9292e430e35766c552066ed3e87c557b803b 0 iD8DBQBOh3zUywK+sNU5EO8RApFMAKCD3Y/u3avDFndznwqfG5UeTHMlvACfUivPIVQZyDZnhZMq0UhC6zhCEQg=
46 46 384082750f2c51dc917d85a7145748330fa6ef4d 0 iD8DBQBOmd+OywK+sNU5EO8RAgDgAJ9V/X+G7VLwhTpHrZNiOHabzSyzYQCdE2kKfIevJUYB9QLAWCWP6DPwrwI=
47 47 41453d55b481ddfcc1dacb445179649e24ca861d 0 iD8DBQBOsFhpywK+sNU5EO8RAqM6AKCyfxUae3/zLuiLdQz+JR78690eMACfQ6JTBQib4AbE+rUDdkeFYg9K/+4=
48 48 195dbd1cef0c2f9f8bcf4ea303238105f716bda3 0 iD8DBQBO1/fWywK+sNU5EO8RAmoPAKCR5lpv1D6JLURHD8KVLSV4GRVEBgCgnd0Sy78ligNfqAMafmACRDvj7vo=
49 49 6344043924497cd06d781d9014c66802285072e4 0 iD8DBQBPALgmywK+sNU5EO8RAlfhAJ9nYOdWnhfVDHYtDTJAyJtXBAQS9wCgnefoSQt7QABkbGxM+Q85UYEBuD0=
50 50 db33555eafeaf9df1e18950e29439eaa706d399b 0 iD8DBQBPGdzxywK+sNU5EO8RAppkAJ9jOXhUVE/97CPgiMA0pMGiIYnesQCfengAszcBiSiKGugiI8Okc9ghU+Y=
51 51 2aa5b51f310fb3befd26bed99c02267f5c12c734 0 iD8DBQBPKZ9bywK+sNU5EO8RAt1TAJ45r1eJ0YqSkInzrrayg4TVCh0SnQCgm0GA/Ua74jnnDwVQ60lAwROuz1Q=
52 52 53e2cd303ecf8ca7c7eeebd785c34e5ed6b0f4a4 0 iD8DBQBPT/fvywK+sNU5EO8RAnfYAKCn7d0vwqIb100YfWm1F7nFD5B+FACeM02YHpQLSNsztrBCObtqcnfod7Q=
53 53 b9bd95e61b49c221c4cca24e6da7c946fc02f992 0 iD8DBQBPeLsIywK+sNU5EO8RAvpNAKCtKe2gitz8dYn52IRF0hFOPCR7AQCfRJL/RWCFweu2T1vH/mUOCf8SXXc=
54 54 d9e2f09d5488c395ae9ddbb320ceacd24757e055 0 iD8DBQBPju/dywK+sNU5EO8RArBYAJ9xtifdbk+hCOJO8OZa4JfHX8OYZQCeKPMBaBWiT8N/WHoOm1XU0q+iono=
55 55 00182b3d087909e3c3ae44761efecdde8f319ef3 0 iD8DBQBPoFhIywK+sNU5EO8RAhzhAKCBj1n2jxPTkZNJJ5pSp3soa+XHIgCgsZZpAQxOpXwCp0eCdNGe0+pmxmg=
56 56 5983de86462c5a9f42a3ad0f5e90ce5b1d221d25 0 iD8DBQBPovNWywK+sNU5EO8RAhgiAJ980T91FdPTRMmVONDhpkMsZwVIMACgg3bKvoWSeuCW28llUhAJtUjrMv0=
57 57 85a358df5bbbe404ca25730c9c459b34263441dc 0 iD8DBQBPyZsWywK+sNU5EO8RAnpLAJ48qrGDJRT+pteS0mSQ11haqHstPwCdG4ccGbk+0JHb7aNy8/NRGAOqn9w=
58 58 b013baa3898e117959984fc64c29d8c784d2f28b 0 iD8DBQBP8QOPywK+sNU5EO8RAqimAKCFRSx0lvG6y8vne2IhNG062Hn0dACeMLI5/zhpWpHBIVeAAquYfx2XFeA=
59 59 7f5094bb3f423fc799e471aac2aee81a7ce57a0b 0 iD8DBQBQGiL8ywK+sNU5EO8RAq5oAJ4rMMCPx6O+OuzNXVOexogedWz/QgCeIiIxLd76I4pXO48tdXhr0hQcBuM=
60 60 072209ae4ddb654eb2d5fd35bff358c738414432 0 iD8DBQBQQkq0ywK+sNU5EO8RArDTAJ9nk5CySnNAjAXYvqvx4uWCw9ThZwCgqmFRehH/l+oTwj3f8nw8u8qTCdc=
61 61 b3f0f9a39c4e1d0250048cd803ab03542d6f140a 0 iD8DBQBQamltywK+sNU5EO8RAlsqAJ4qF/m6aFu4mJCOKTiAP5RvZFK02ACfawYShUZO6OXEFfveU0aAxDR0M1k=
62 62 d118a4f4fd16d9b558ec3f3e87bfee772861d2b7 0 iD8DBQBQgPV5ywK+sNU5EO8RArylAJ0abcx5NlDjyv3ZDWpAfRIHyRsJtQCgn4TMuEayqgxzrvadQZHdTEU2g38=
63 63 195ad823b5d58c68903a6153a25e3fb4ed25239d 0 iD8DBQBQkuT9ywK+sNU5EO8RAhB4AKCeerItoK2Jipm2cVf4euGofAa/WACeJj3TVd4pFILpb+ogj7ebweFLJi0=
64 64 0c10cf8191469e7c3c8844922e17e71a176cb7cb 0 iD8DBQBQvQWoywK+sNU5EO8RAnq3AJoCn98u4geFx5YaQaeh99gFhCd7bQCgjoBwBSUyOvGd0yBy60E3Vv3VZhM=
65 65 a4765077b65e6ae29ba42bab7834717b5072d5ba 0 iD8DBQBQ486sywK+sNU5EO8RAhmJAJ90aLfLKZhmcZN7kqphigQJxiFOQACeJ5IUZxjGKH4xzi3MrgIcx9n+dB0=
66 66 f5fbe15ca7449f2c9a3cf817c86d0ae68b307214 0 iD8DBQBQ+yuYywK+sNU5EO8RAm9JAJoD/UciWvpGeKBcpGtZJBFJVcL/HACghDXSgQ+xQDjB+6uGrdgAQsRR1Lg=
67 67 a6088c05e43a8aee0472ca3a4f6f8d7dd914ebbf 0 iD8DBQBRDDROywK+sNU5EO8RAh75AJ9uJCGoCWnP0Lv/+XuYs4hvUl+sAgCcD36QgAnuw8IQXrvv684BAXAnHcA=
68 68 7511d4df752e61fe7ae4f3682e0a0008573b0402 0 iD8DBQBRFYaoywK+sNU5EO8RAuErAJoDyhXn+lptU3+AevVdwAIeNFyR2gCdHzPHyWd+JDeWCUR+pSOBi8O2ppM=
69 69 5b7175377babacce80a6c1e12366d8032a6d4340 0 iD8DBQBRMCYgywK+sNU5EO8RAq1/AKCWKlt9ysibyQgYwoxxIOZv5J8rpwCcDSHQaaf1fFZUTnQsOePwcM2Y/Sg=
70 70 50c922c1b5145dab8baefefb0437d363b6a6c21c 0 iD8DBQBRWnUnywK+sNU5EO8RAuQRAJwM42cJqJPeqJ0jVNdMqKMDqr4dSACeP0cRVGz1gitMuV0x8f3mrZrqc7I=
71 71 8a7bd2dccd44ed571afe7424cd7f95594f27c092 0 iD8DBQBRXfBvywK+sNU5EO8RAn+LAKCsMmflbuXjYRxlzFwId5ptm8TZcwCdGkyLbZcASBOkzQUm/WW1qfknJHU=
72 72 292cd385856d98bacb2c3086f8897bc660c2beea 0 iD8DBQBRcM0BywK+sNU5EO8RAjp4AKCJBykQbvXhKuvLSMxKx3a2TBiXcACfbr/kLg5GlZTF/XDPmY+PyHgI/GM=
73 73 23f785b38af38d2fca6b8f3db56b8007a84cd73a 0 iD8DBQBRgZwNywK+sNU5EO8RAmO4AJ4u2ILGuimRP6MJgE2t65LZ5dAdkACgiENEstIdrlFC80p+sWKD81kKIYI=
74 74 ddc7a6be20212d18f3e27d9d7e6f079a66d96f21 0 iD8DBQBRkswvywK+sNU5EO8RAiYYAJsHTHyHbJeAgmGvBTmDrfcKu4doUgCeLm7eGBjx7yAPUvEtxef8rAkQmXI=
75 75 cceaf7af4c9e9e6fa2dbfdcfe9856c5da69c4ffd 0 iD8DBQBRqnFLywK+sNU5EO8RAsWNAJ9RR6t+y1DLFc2HeH0eN9VfZAKF9gCeJ8ezvhtKq/LMs0/nvcgKQc/d5jk=
76 76 009794acc6e37a650f0fae37872e733382ac1c0c 0 iD8DBQBR0guxywK+sNU5EO8RArNkAKCq9pMihVzP8Os5kCmgbWpe5C37wgCgqzuPZTHvAsXF5wTyaSTMVa9Ccq4=
77 77 f0d7721d7322dcfb5af33599c2543f27335334bb 0 iD8DBQBR8taaywK+sNU5EO8RAqeEAJ4idDhhDuEsgsUjeQgWNj498matHACfT67gSF5w0ylsrBx1Hb52HkGXDm0=
78 78 f37b5a17e6a0ee17afde2cdde5393dd74715fb58 0 iD8DBQBR+ymFywK+sNU5EO8RAuSdAJkBMcd9DAZ3rWE9WGKPm2YZ8LBoXACfXn/wbEsVy7ZgJoUwiWmHSnQaWCI=
79 79 335a558f81dc73afeab4d7be63617392b130117f 0 iQIVAwUAUiZrIyBXgaxoKi1yAQK2iw//cquNqqSkc8Re5/TZT9I6NH+lh6DbOKjJP0Xl1Wqq0K+KSIUgZG4G32ovaEb2l5X0uY+3unRPiZ0ebl0YSw4Fb2ZiPIADXLBTOYRrY2Wwd3tpJeGI6wEgZt3SfcITV/g7NJrCjT3FlYoSOIayrExM80InSdcEM0Q3Rx6HKzY2acyxzgZeAtAW5ohFvHilSvY6p5Gcm4+QptMxvw45GPdreUmjeXZxNXNXZ8P+MjMz/QJbai/N7PjmK8lqnhkBsT48Ng/KhhmOkGntNJ2/ImBWLFGcWngSvJ7sfWwnyhndvGhe0Hq1NcCf7I8TjNDxU5TR+m+uW7xjXdLoDbUjBdX4sKXnh8ZjbYiODKBOrrDq25cf8nA/tnpKyE/qsVy60kOk6loY4XKiYmn1V49Ta0emmDx0hqo3HgxHHsHX0NDnGdWGol7cPRET0RzVobKq1A0jnrhPooWidvLh9bPzLonrWDo+ib+DuySoRkuYUK4pgZJ2mbg6daFOBEZygkSyRB8bo1UQUP7EgQDrWe4khb/5GHEfDkrQz3qu/sXvc0Ir1mOUWBFPHC2DjjCn/oMJuUkG1SwM8l2Bfv7h67ssES6YQ2+RjOix4yid7EXS/Ogl45PzCIPSI5+BbNs10JhE0w5uErBHlF53EDTe/TSLc+GU6DB6PP6dH912Njdr3jpNSUQ=
80 80 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
@@ -1,264 +1,266 b''
1 1 d40cc5aacc31ed673d9b5b24f98bee78c283062c 0.4f
2 2 1c590d34bf61e2ea12c71738e5a746cd74586157 0.4e
3 3 7eca4cfa8aad5fce9a04f7d8acadcd0452e2f34e 0.4d
4 4 b4d0c3786ad3e47beacf8412157326a32b6d25a4 0.4c
5 5 f40273b0ad7b3a6d3012fd37736d0611f41ecf54 0.5
6 6 0a28dfe59f8fab54a5118c5be4f40da34a53cdb7 0.5b
7 7 12e0fdbc57a0be78f0e817fd1d170a3615cd35da 0.6
8 8 4ccf3de52989b14c3d84e1097f59e39a992e00bd 0.6b
9 9 eac9c8efcd9bd8244e72fb6821f769f450457a32 0.6c
10 10 979c049974485125e1f9357f6bbe9c1b548a64c3 0.7
11 11 3a56574f329a368d645853e0f9e09472aee62349 0.8
12 12 6a03cff2b0f5d30281e6addefe96b993582f2eac 0.8.1
13 13 35fb62a3a673d5322f6274a44ba6456e5e4b3b37 0.9
14 14 2be3001847cb18a23c403439d9e7d0ace30804e9 0.9.1
15 15 36a957364b1b89c150f2d0e60a99befe0ee08bd3 0.9.2
16 16 27230c29bfec36d5540fbe1c976810aefecfd1d2 0.9.3
17 17 fb4b6d5fe100b0886f8bc3d6731ec0e5ed5c4694 0.9.4
18 18 23889160905a1b09fffe1c07378e9fc1827606eb 0.9.5
19 19 bae2e9c838e90a393bae3973a7850280413e091a 1.0
20 20 d5cbbe2c49cee22a9fbeb9ea41daa0ac4e26b846 1.0.1
21 21 d2375bbee6d47e62ba8e415c86e83a465dc4dce9 1.0.2
22 22 2a67430f92f15ea5159c26b09ec4839a0c549a26 1.1
23 23 3773e510d433969e277b1863c317b674cbee2065 1.1.1
24 24 11a4eb81fb4f4742451591489e2797dc47903277 1.1.2
25 25 11efa41037e280d08cfb07c09ad485df30fb0ea8 1.2
26 26 02981000012e3adf40c4849bd7b3d5618f9ce82d 1.2.1
27 27 196d40e7c885fa6e95f89134809b3ec7bdbca34b 1.3
28 28 3ef6c14a1e8e83a31226f5881b7fe6095bbfa6f6 1.3.1
29 29 31ec469f9b556f11819937cf68ee53f2be927ebf 1.4
30 30 439d7ea6fe3aa4ab9ec274a68846779153789de9 1.4.1
31 31 296a0b14a68621f6990c54fdba0083f6f20935bf 1.4.2
32 32 4aa619c4c2c09907034d9824ebb1dd0e878206eb 1.4.3
33 33 ff2704a8ded37fbebd8b6eb5ec733731d725da8a 1.5
34 34 2b01dab594167bc0dd33331dbaa6dca3dca1b3aa 1.5.1
35 35 39f725929f0c48c5fb3b90c071fc3066012456ca 1.5.2
36 36 fdcf80f26604f233dc4d8f0a5ef9d7470e317e8a 1.5.3
37 37 24fe2629c6fd0c74c90bd066e77387c2b02e8437 1.5.4
38 38 f786fc4b8764cd2a5526d259cf2f94d8a66924d9 1.6
39 39 bf1774d95bde614af3956d92b20e2a0c68c5fec7 1.6.1
40 40 c00f03a4982e467fb6b6bd45908767db6df4771d 1.6.2
41 41 ff5cec76b1c5b6be9c3bb923aae8c3c6d079d6b9 1.6.3
42 42 93d8bff78c96fe7e33237b257558ee97290048a4 1.6.4
43 43 333421b9e0f96c7bc788e5667c146a58a9440a55 1.7
44 44 4438875ec01bd0fc32be92b0872eb6daeed4d44f 1.7.1
45 45 6aff4f144ad356311318b0011df0bb21f2c97429 1.7.2
46 46 e3bf16703e2601de99e563cdb3a5d50b64e6d320 1.7.3
47 47 a6c855c32ea081da3c3b8ff628f1847ff271482f 1.7.4
48 48 2b2155623ee2559caf288fd333f30475966c4525 1.7.5
49 49 2616325766e3504c8ae7c84bd15ee610901fe91d 1.8
50 50 aa1f3be38ab127280761889d2dca906ca465b5f4 1.8.1
51 51 b032bec2c0a651ca0ddecb65714bfe6770f67d70 1.8.2
52 52 3cb1e95676ad089596bd81d0937cad37d6e3b7fb 1.8.3
53 53 733af5d9f6b22387913e1d11350fb8cb7c1487dd 1.8.4
54 54 de9eb6b1da4fc522b1cab16d86ca166204c24f25 1.9
55 55 4a43e23b8c55b4566b8200bf69fe2158485a2634 1.9.1
56 56 d629f1e89021103f1753addcef6b310e4435b184 1.9.2
57 57 351a9292e430e35766c552066ed3e87c557b803b 1.9.3
58 58 384082750f2c51dc917d85a7145748330fa6ef4d 2.0-rc
59 59 41453d55b481ddfcc1dacb445179649e24ca861d 2.0
60 60 195dbd1cef0c2f9f8bcf4ea303238105f716bda3 2.0.1
61 61 6344043924497cd06d781d9014c66802285072e4 2.0.2
62 62 db33555eafeaf9df1e18950e29439eaa706d399b 2.1-rc
63 63 2aa5b51f310fb3befd26bed99c02267f5c12c734 2.1
64 64 53e2cd303ecf8ca7c7eeebd785c34e5ed6b0f4a4 2.1.1
65 65 b9bd95e61b49c221c4cca24e6da7c946fc02f992 2.1.2
66 66 d9e2f09d5488c395ae9ddbb320ceacd24757e055 2.2-rc
67 67 00182b3d087909e3c3ae44761efecdde8f319ef3 2.2
68 68 5983de86462c5a9f42a3ad0f5e90ce5b1d221d25 2.2.1
69 69 85a358df5bbbe404ca25730c9c459b34263441dc 2.2.2
70 70 b013baa3898e117959984fc64c29d8c784d2f28b 2.2.3
71 71 a06e2681dd1786e2354d84a5fa9c1c88dd4fa3e0 2.3-rc
72 72 7f5094bb3f423fc799e471aac2aee81a7ce57a0b 2.3
73 73 072209ae4ddb654eb2d5fd35bff358c738414432 2.3.1
74 74 b3f0f9a39c4e1d0250048cd803ab03542d6f140a 2.3.2
75 75 d118a4f4fd16d9b558ec3f3e87bfee772861d2b7 2.4-rc
76 76 195ad823b5d58c68903a6153a25e3fb4ed25239d 2.4
77 77 0c10cf8191469e7c3c8844922e17e71a176cb7cb 2.4.1
78 78 a4765077b65e6ae29ba42bab7834717b5072d5ba 2.4.2
79 79 f5fbe15ca7449f2c9a3cf817c86d0ae68b307214 2.5-rc
80 80 a6088c05e43a8aee0472ca3a4f6f8d7dd914ebbf 2.5
81 81 7511d4df752e61fe7ae4f3682e0a0008573b0402 2.5.1
82 82 5b7175377babacce80a6c1e12366d8032a6d4340 2.5.2
83 83 50c922c1b5145dab8baefefb0437d363b6a6c21c 2.5.3
84 84 8a7bd2dccd44ed571afe7424cd7f95594f27c092 2.5.4
85 85 292cd385856d98bacb2c3086f8897bc660c2beea 2.6-rc
86 86 23f785b38af38d2fca6b8f3db56b8007a84cd73a 2.6
87 87 ddc7a6be20212d18f3e27d9d7e6f079a66d96f21 2.6.1
88 88 cceaf7af4c9e9e6fa2dbfdcfe9856c5da69c4ffd 2.6.2
89 89 009794acc6e37a650f0fae37872e733382ac1c0c 2.6.3
90 90 f0d7721d7322dcfb5af33599c2543f27335334bb 2.7-rc
91 91 f37b5a17e6a0ee17afde2cdde5393dd74715fb58 2.7
92 92 335a558f81dc73afeab4d7be63617392b130117f 2.7.1
93 93 e7fa36d2ad3a7944a52dca126458d6f482db3524 2.7.2
94 94 1596f2d8f2421314b1ddead8f7d0c91009358994 2.8-rc
95 95 d825e4025e39d1c39db943cdc89818abd0a87c27 2.8
96 96 209e04a06467e2969c0cc6501335be0406d46ef0 2.8.1
97 97 ca387377df7a3a67dbb90b6336b781cdadc3ef41 2.8.2
98 98 8862469e16f9236208581b20de5f96bd13cc039d 2.9-rc
99 99 3cec5134e9c4bceab6a00c60f52a4f80677a78f2 2.9
100 100 b96cb15ec9e04d8ac5ee08b34fcbbe4200588965 2.9.1
101 101 3f83fc5cfe715d292069ee8417c83804f6c6c1e4 2.9.2
102 102 564f55b251224f16508dd1311452db7780dafe2b 3.0-rc
103 103 2195ac506c6ababe86985b932f4948837c0891b5 3.0
104 104 269c80ee5b3cb3684fa8edc61501b3506d02eb10 3.0.1
105 105 2d8cd3d0e83c7336c0cb45a9f88638363f993848 3.0.2
106 106 6c36dc6cd61a0e1b563f1d51e55bdf4dacf12162 3.1-rc
107 107 3178e49892020336491cdc6945885c4de26ffa8b 3.1
108 108 5dc91146f35369949ea56b40172308158b59063a 3.1.1
109 109 f768c888aaa68d12dd7f509dcc7f01c9584357d0 3.1.2
110 110 7f8d16af8cae246fa5a48e723d48d58b015aed94 3.2-rc
111 111 ced632394371a36953ce4d394f86278ae51a2aae 3.2
112 112 643c58303fb0ec020907af28b9e486be299ba043 3.2.1
113 113 902554884335e5ca3661d63be9978eb4aec3f68a 3.2.2
114 114 6dad422ecc5adb63d9fa649eeb8e05a5f9bc4900 3.2.3
115 115 1265a3a71d75396f5d4cf6935ae7d9ba5407a547 3.2.4
116 116 db8e3f7948b1fdeb9ad12d448fc3525759908b9f 3.3-rc
117 117 fbdd5195528fae4f41feebc1838215c110b25d6a 3.3
118 118 5b4ed033390bf6e2879c8f5c28c84e1ee3b87231 3.3.1
119 119 07a92bbd02e5e3a625e0820389b47786b02b2cea 3.3.2
120 120 2e2e9a0750f91a6fe0ad88e4de34f8efefdcab08 3.3.3
121 121 e89f909edffad558b56f4affa8239e4832f88de0 3.4-rc
122 122 8cc6036bca532e06681c5a8fa37efaa812de67b5 3.4
123 123 ed18f4acf435a2824c6f49fba40f42b9df5da7ad 3.4.1
124 124 540cd0ddac49c1125b2e013aa2ff18ecbd4dd954 3.4.2
125 125 96a38d44ba093bd1d1ecfd34119e94056030278b 3.5-rc
126 126 21aa1c313b05b1a85f8ffa1120d51579ddf6bf24 3.5
127 127 1a45e49a6bed023deb229102a8903234d18054d3 3.5.1
128 128 9a466b9f9792e3ad7ae3fc6c43c3ff2e136b718d 3.5.2
129 129 b66e3ca0b90c3095ea28dfd39aa24247bebf5c20 3.6-rc
130 130 47dd34f2e7272be9e3b2a5a83cd0d20be44293f4 3.6
131 131 1aa5083cbebbe7575c88f3402ab377539b484897 3.6.1
132 132 2d437a0f3355834a9485bbbeb30a52a052c98f19 3.6.2
133 133 ea389970c08449440587712117f178d33bab3f1e 3.6.3
134 134 158bdc8965720ca4061f8f8d806563cfc7cdb62e 3.7-rc
135 135 2408645de650d8a29a6ce9e7dce601d8dd0d1474 3.7
136 136 b698abf971e7377d9b7ec7fc8c52df45255b0329 3.7.1
137 137 d493d64757eb45ada99fcb3693e479a51b7782da 3.7.2
138 138 ae279d4a19e9683214cbd1fe8298cf0b50571432 3.7.3
139 139 740156eedf2c450aee58b1a90b0e826f47c5da64 3.8-rc
140 140 f85de28eae32e7d3064b1a1321309071bbaaa069 3.8
141 141 a56296f55a5e1038ea5016dace2076b693c28a56 3.8.1
142 142 aaabed77791a75968a12b8c43ad263631a23ee81 3.8.2
143 143 a9764ab80e11bcf6a37255db7dd079011f767c6c 3.8.3
144 144 26a5d605b8683a292bb89aea11f37a81b06ac016 3.8.4
145 145 519bb4f9d3a47a6e83c2b414d58811ed38f503c2 3.9-rc
146 146 299546f84e68dbb9bd026f0f3a974ce4bdb93686 3.9
147 147 ccd436f7db6d5d7b9af89715179b911d031d44f1 3.9.1
148 148 149433e68974eb5c63ccb03f794d8b57339a80c4 3.9.2
149 149 438173c415874f6ac653efc1099dec9c9150e90f 4.0-rc
150 150 eab27446995210c334c3d06f1a659e3b9b5da769 4.0
151 151 b3b1ae98f6a0e14c1e1ba806a6c18e193b6dae5c 4.0.1
152 152 e69874dc1f4e142746ff3df91e678a09c6fc208c 4.0.2
153 153 a1dd2c0c479e0550040542e392e87bc91262517e 4.1-rc
154 154 e1526da1e6d84e03146151c9b6e6950fe9a83d7d 4.1
155 155 25703b624d27e3917d978af56d6ad59331e0464a 4.1.1
156 156 ed5b25874d998ababb181a939dd37a16ea644435 4.1.2
157 157 77eaf9539499a1b8be259ffe7ada787d07857f80 4.1.3
158 158 616e788321cc4ae9975b7f0c54c849f36d82182b 4.2-rc
159 159 bb96d4a497432722623ae60d9bc734a1e360179e 4.2
160 160 c850f0ed54c1d42f9aa079ad528f8127e5775217 4.2.1
161 161 26c49ed51a698ec016d2b4c6b44ca3c3f73cc788 4.2.2
162 162 857876ebaed4e315f63157bd157d6ce553c7ab73 4.3-rc
163 163 5544af8622863796a0027566f6b646e10d522c4c 4.3
164 164 943c91326b23954e6e1c6960d0239511f9530258 4.2.3
165 165 3fee7f7d2da04226914c2258cc2884dc27384fd7 4.3.1
166 166 920977f72c7b70acfdaf56ab35360584d7845827 4.3.2
167 167 2f427b57bf9019c6dc3750baa539dc22c1be50f6 4.3.3
168 168 1e2454b60e5936f5e77498cab2648db469504487 4.4-rc
169 169 0ccb43d4cf01d013ae05917ec4f305509f851b2d 4.4
170 170 cabc840ffdee8a72f3689fb77dd74d04fdc2bc04 4.4.1
171 171 a92b9f8e11ba330614cdfd6af0e03b15c1ff3797 4.4.2
172 172 27b6df1b5adbdf647cf5c6675b40575e1b197c60 4.5-rc
173 173 d334afc585e29577f271c5eda03378736a16ca6b 4.5
174 174 369aadf7a3264b03c8b09efce715bc41e6ab4a9b 4.5.1
175 175 8bba684efde7f45add05f737952093bb2aa07155 4.5.2
176 176 7de7bd407251af2bc98e5b809c8598ee95830daf 4.5.3
177 177 ed5448edcbfa747b9154099e18630e49024fd47b 4.6rc0
178 178 1ec874717d8a93b19e0d50628443e0ee5efab3a9 4.6rc1
179 179 6614cac550aea66d19c601e45efd1b7bd08d7c40 4.6
180 180 9c5ced5276d6e7d54f7c3dadf5247b7ee98ec79c 4.6.1
181 181 0b63a6743010dfdbf8a8154186e119949bdaa1cc 4.6.2
182 182 e90130af47ce8dd53a3109aed9d15876b3e7dee8 4.7rc0
183 183 33ac6a72308a215e6086fbced347ec10aa963b0a 4.7
184 184 ede3bf31fe63677fdf5bd8db687977d4e3d792ed 4.7.1
185 185 5405cb1a79010ac50c58cd84e6f50c4556bf2a4c 4.7.2
186 186 956ec6f1320df26f3133ec40f3de866ea0695fd7 4.8rc0
187 187 a91a2837150bdcb27ae76b3646e6c93cd6a15904 4.8
188 188 1c8c54cf97256f4468da2eb4dbee24f7f3888e71 4.8.1
189 189 197f092b2cd9691e2a55d198f717b231af9be6f9 4.8.2
190 190 593718ff5844cad7a27ee3eb5adad89ac8550949 4.9rc0
191 191 83377b4b4ae0e9a6b8e579f7b0a693b8cf5c3b10 4.9
192 192 4ea21df312ec7159c5b3633096b6ecf68750b0dd 4.9.1
193 193 4a8d9ed864754837a185a642170cde24392f9abf 5.0rc0
194 194 07e479ef7c9639be0029f00e6a722b96dcc05fee 5.0
195 195 c3484ddbdb9621256d597ed86b90d229c59c2af9 5.0.1
196 196 97ada9b8d51bef24c5cb4cdca4243f0db694ab6e 5.0.2
197 197 e386b5f4f8360dbb43a576dd9b1368e386fefa5b 5.1rc0
198 198 e91930d712e8507d1bc1b2dffd96c83edc4cbed3 5.1
199 199 a4e32fd539ab41489a51b2aa88bda9a73b839562 5.1.1
200 200 181e52f2b62f4768aa0d988936c929dc7c4a41a0 5.1.2
201 201 59338f9561099de77c684c00f76507f11e46ebe8 5.2rc0
202 202 ca3dca416f8d5863ca6f5a4a6a6bb835dcd5feeb 5.2
203 203 a50fecefa691c9b72a99e49aa6fe9dd13943c2bf 5.2.1
204 204 b4c82b70418022e67cc0e69b1aa3c3aa43aa1d29 5.2.2
205 205 84a0102c05c7852c8215ef6cf21d809927586b69 5.3rc0
206 206 e4344e463c0c888a2f437b78b5982ecdf3f6650a 5.3rc1
207 207 7f5410dfc8a64bb587d19637deb95d378fd1eb5c 5.3
208 208 6d121acbb82e65fe4dd3c2318a1b61981b958492 5.3.1
209 209 8fca7e8449a847e3cf1054f2c07b51237699fad3 5.3.2
210 210 26ce8e7515036d3431a03aaeb7bc72dd96cb1112 5.4rc0
211 211 cf3e07d7648a4371ce584d15dd692e7a6845792f 5.4
212 212 065704cbdbdbb05dcd6bb814eb9bbdd982211b28 5.4.1
213 213 0ea9c86fac8974cd74dc12ea681c8986eb6da6c4 5.4.2
214 214 28163c5de797e5416f9b588940f4608269b4d50a 5.5rc0
215 215 7fc3c5fbc65f6fe85d70ea63923b8767dda4f2e0 5.5
216 216 f62bb5d07848ca598aa860a517394130b61bf2ee 5.5.1
217 217 07731064ac41dacdf0ec869ebd05c2e848c14fbf 5.5.2
218 218 0e06a7ab9e0d5c65af4e511aee1e0342998799df 5.6rc0
219 219 18c17d63fdabd009e70bf994e5efb7db422f4f7f 5.6
220 220 1d5189a57405ceca5aa244052c9f948977f4699b 5.6.1
221 221 9da65e3cf3706ff41e08b311381c588440c27baf 5.7rc0
222 222 0e2e7300f4302b02412b0b734717697049494c4c 5.7
223 223 d5d9177c0045d206db575bae6daa98e2cb2fe5bc 5.7.1
224 224 f67b8946bb1b6cfa8328dbf8d6a9128b69ccdcb4 5.8rc0
225 225 8d2b62d716b095507effaa8d56f87cd27ba659ab 5.8rc1
226 226 067f2c53fb24506c9e9fb4639871b13b19a85f8a 5.8
227 227 411dc27fd9fd076d6a031a08fcaace659afe2fe3 5.8.1
228 228 d7515d29761d5ada7d9c765f517db67db75dea9a 5.9rc0
229 229 2813d406b03607cdb8c06cb04c44efcc9a79d9a2 5.9rc1
230 230 53221078e0de65d1a821ce5311dec45a7a978301 5.9
231 231 86a60679cf619e14cee9442f865fcf31b142cb9f 5.9.1
232 232 750920b18aaaddd654756be40dec59d90f2643be 5.9.2
233 233 6ee0244fc1cf889ae543d2ce0ec45201ae0be6e1 5.9.3
234 234 a44bb185f6bdbecc754996d8386722e2f0123b0a 6.0rc0
235 235 5d08b289e2e526259d7d5ea32b70fe76d5b327d7 6.0
236 236 799fdf4cca80cb9ae40537a90995e6bd163ebc0b 6.0.1
237 237 75676122c2bf7594ac732b7388db4c74c648b365 6.0.2
238 238 dcec16e799ddb6d33fcd11b04af530250a417a58 6.0.3
239 239 c00d3ce4e94bb0ee8d809e25e1dcb2a5fab84e2c 6.1rc0
240 240 d4486810a1795fba9521449b8885ced034f3a6dd 6.1
241 241 5bd6bcd31dd1ebb63b8914b00064f96297267af7 6.1.1
242 242 0ddd5e1f5f67438af85d12e4ce6c39021dde9916 6.1.2
243 243 6b10151b962108f65bfa12b3918b1021ca334f73 6.1.3
244 244 0cc5f74ff7f0f4ac2427096bddbe102dbc2453ae 6.1.4
245 245 288de6f5d724bba7bf1669e2838f196962bb7528 6.2rc0
246 246 094a5fa3cf52f936e0de3f1e507c818bee5ece6b 6.2
247 247 f69bffd00abe3a1b94d1032eb2c92e611d16a192 6.2.1
248 248 b5c8524827d20fe2e0ca8fb1234a0fe35a1a36c7 6.2.2
249 249 dbdee8ac3e3fcdda1fa55b90c0a235125b7f8e6f 6.2.3
250 250 a3356ab610fc50000cf0ba55c424a4d96da11db7 6.3rc0
251 251 04f1dba53c961dfdb875c8469adc96fa999cfbed 6.3.0
252 252 04f1dba53c961dfdb875c8469adc96fa999cfbed 6.3
253 253 04f1dba53c961dfdb875c8469adc96fa999cfbed 6.3.0
254 254 0000000000000000000000000000000000000000 6.3.0
255 255 c890d8b8bc59b18e5febf60caada629df5356ee2 6.3.1
256 256 59466b13a3ae0e29a5d4f485393e516cfbb057d0 6.3.2
257 257 8830004967ad865ead89c28a410405a6e71e0796 6.3.3
258 258 05de4896508e8ec387b33eb30d8aab78d1c8e9e4 6.4rc0
259 259 f14864fffdcab725d9eac6d4f4c07be05a35f59a 6.4
260 260 83ea6ce48b4fd09fb79c4e34cc5750c805699a53 6.4.1
261 261 f952be90b0514a576dcc8bbe758ce3847faba9bb 6.4.2
262 262 fc445f8abcf90b33db7c463816a1b3560681767f 6.4.3
263 263 da372c745e0f053bb7a64e74cccd15810d96341d 6.4.4
264 264 271a4ab29605ffa0bae5d3208eaa21a95427ff92 6.4.5
265 bb42988c7e156931b0ff1e93732b98173ebbcb7f 6.5rc0
266 3ffc7209bbae5804a53084c9dc2d41139e88c867 6.5
@@ -1,1091 +1,1089 b''
1 1 #!/usr/bin/env python3
2 2 #
3 3 # check-code - a style and portability checker for Mercurial
4 4 #
5 5 # Copyright 2010 Olivia Mackall <olivia@selenic.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 """style and portability checker for Mercurial
11 11
12 12 when a rule triggers wrong, do one of the following (prefer one from top):
13 13 * do the work-around the rule suggests
14 14 * doublecheck that it is a false match
15 15 * improve the rule pattern
16 16 * add an ignore pattern to the rule (3rd arg) which matches your good line
17 17 (you can append a short comment and match this, like: #re-raises)
18 18 * change the pattern to a warning and list the exception in test-check-code-hg
19 19 * ONLY use no--check-code for skipping entire files from external sources
20 20 """
21 21
22 22 import glob
23 23 import keyword
24 24 import optparse
25 25 import os
26 26 import re
27 27 import sys
28 28
29 29 if sys.version_info[0] < 3:
30 30 opentext = open
31 31 else:
32 32
33 33 def opentext(f):
34 34 return open(f, encoding='latin1')
35 35
36 36
37 37 try:
38 38 xrange
39 39 except NameError:
40 40 xrange = range
41 41 try:
42 42 import re2
43 43 except ImportError:
44 44 re2 = None
45 45
46 46 import testparseutil
47 47
48 48
49 49 def compilere(pat, multiline=False):
50 50 if multiline:
51 51 pat = '(?m)' + pat
52 52 if re2:
53 53 try:
54 54 return re2.compile(pat)
55 55 except re2.error:
56 56 pass
57 57 return re.compile(pat)
58 58
59 59
60 60 # check "rules depending on implementation of repquote()" in each
61 61 # patterns (especially pypats), before changing around repquote()
62 62 _repquotefixedmap = {
63 63 ' ': ' ',
64 64 '\n': '\n',
65 65 '.': 'p',
66 66 ':': 'q',
67 67 '%': '%',
68 68 '\\': 'b',
69 69 '*': 'A',
70 70 '+': 'P',
71 71 '-': 'M',
72 72 }
73 73
74 74
75 75 def _repquoteencodechr(i):
76 76 if i > 255:
77 77 return 'u'
78 78 c = chr(i)
79 79 if c in _repquotefixedmap:
80 80 return _repquotefixedmap[c]
81 81 if c.isalpha():
82 82 return 'x'
83 83 if c.isdigit():
84 84 return 'n'
85 85 return 'o'
86 86
87 87
88 88 _repquotett = ''.join(_repquoteencodechr(i) for i in xrange(256))
89 89
90 90
91 91 def repquote(m):
92 92 t = m.group('text')
93 93 t = t.translate(_repquotett)
94 94 return m.group('quote') + t + m.group('quote')
95 95
96 96
97 97 def reppython(m):
98 98 comment = m.group('comment')
99 99 if comment:
100 100 l = len(comment.rstrip())
101 101 return "#" * l + comment[l:]
102 102 return repquote(m)
103 103
104 104
105 105 def repcomment(m):
106 106 return m.group(1) + "#" * len(m.group(2))
107 107
108 108
109 109 def repccomment(m):
110 110 t = re.sub(r"((?<=\n) )|\S", "x", m.group(2))
111 111 return m.group(1) + t + "*/"
112 112
113 113
114 114 def repcallspaces(m):
115 115 t = re.sub(r"\n\s+", "\n", m.group(2))
116 116 return m.group(1) + t
117 117
118 118
119 119 def repinclude(m):
120 120 return m.group(1) + "<foo>"
121 121
122 122
123 123 def rephere(m):
124 124 t = re.sub(r"\S", "x", m.group(2))
125 125 return m.group(1) + t
126 126
127 127
128 128 testpats = [
129 129 [
130 130 (r'\b(push|pop)d\b', "don't use 'pushd' or 'popd', use 'cd'"),
131 131 (r'\W\$?\(\([^\)\n]*\)\)', "don't use (()) or $(()), use 'expr'"),
132 132 (r'grep.*-q', "don't use 'grep -q', redirect to /dev/null"),
133 133 (r'(?<!hg )grep.* -a', "don't use 'grep -a', use in-line python"),
134 134 (r'sed.*-i', "don't use 'sed -i', use a temporary file"),
135 135 (r'\becho\b.*\\n', "don't use 'echo \\n', use printf"),
136 136 (r'echo -n', "don't use 'echo -n', use printf"),
137 137 (r'(^|\|\s*)\bwc\b[^|]*$\n(?!.*\(re\))', "filter wc output"),
138 138 (r'head -c', "don't use 'head -c', use 'dd'"),
139 139 (r'tail -n', "don't use the '-n' option to tail, just use '-<num>'"),
140 140 (r'sha1sum', "don't use sha1sum, use $TESTDIR/md5sum.py"),
141 141 (r'\bls\b.*-\w*R', "don't use 'ls -R', use 'find'"),
142 142 (r'printf.*[^\\]\\([1-9]|0\d)', r"don't use 'printf \NNN', use Python"),
143 143 (r'printf.*[^\\]\\x', "don't use printf \\x, use Python"),
144 144 (r'rm -rf \*', "don't use naked rm -rf, target a directory"),
145 145 (
146 146 r'\[[^\]]+==',
147 147 '[ foo == bar ] is a bashism, use [ foo = bar ] instead',
148 148 ),
149 (
150 r'(^|\|\s*)grep (-\w\s+)*[^|]*[(|]\w',
151 "use egrep for extended grep syntax",
152 ),
149 (r'(^|\|\s*)egrep', "use grep -E for extended grep syntax"),
150 (r'(^|\|\s*)fgrep', "use grep -F for fixed string grepping"),
153 151 (r'(^|\|\s*)e?grep .*\\S', "don't use \\S in regular expression"),
154 152 (r'(?<!!)/bin/', "don't use explicit paths for tools"),
155 153 (r'#!.*/bash', "don't use bash in shebang, use sh"),
156 154 (r'[^\n]\Z', "no trailing newline"),
157 155 (r'export .*=', "don't export and assign at once"),
158 156 (r'^source\b', "don't use 'source', use '.'"),
159 157 (r'touch -d', "don't use 'touch -d', use 'touch -t' instead"),
160 158 (r'\bls +[^|\n-]+ +-', "options to 'ls' must come before filenames"),
161 159 (r'[^>\n]>\s*\$HGRCPATH', "don't overwrite $HGRCPATH, append to it"),
162 160 (r'^stop\(\)', "don't use 'stop' as a shell function name"),
163 161 (r'(\[|\btest\b).*-e ', "don't use 'test -e', use 'test -f'"),
164 162 (r'\[\[\s+[^\]]*\]\]', "don't use '[[ ]]', use '[ ]'"),
165 163 (r'^alias\b.*=', "don't use alias, use a function"),
166 164 (r'if\s*!', "don't use '!' to negate exit status"),
167 165 (r'/dev/u?random', "don't use entropy, use /dev/zero"),
168 166 (r'do\s*true;\s*done', "don't use true as loop body, use sleep 0"),
169 167 (
170 168 r'sed (-e )?\'(\d+|/[^/]*/)i(?!\\\n)',
171 169 "put a backslash-escaped newline after sed 'i' command",
172 170 ),
173 171 (r'^diff *-\w*[uU].*$\n(^ \$ |^$)', "prefix diff -u/-U with cmp"),
174 172 (r'^\s+(if)? diff *-\w*[uU]', "prefix diff -u/-U with cmp"),
175 173 (r'[\s="`\']python\s(?!bindings)', "don't use 'python', use '$PYTHON'"),
176 174 (r'seq ', "don't use 'seq', use $TESTDIR/seq.py"),
177 175 (r'\butil\.Abort\b', "directly use error.Abort"),
178 176 (r'\|&', "don't use |&, use 2>&1"),
179 177 (r'\w = +\w', "only one space after = allowed"),
180 178 (
181 179 r'\bsed\b.*[^\\]\\n',
182 180 "don't use 'sed ... \\n', use a \\ and a newline",
183 181 ),
184 182 (r'env.*-u', "don't use 'env -u VAR', use 'unset VAR'"),
185 183 (r'cp.* -r ', "don't use 'cp -r', use 'cp -R'"),
186 184 (r'grep.* -[ABC]', "don't use grep's context flags"),
187 185 (
188 186 r'find.*-printf',
189 187 "don't use 'find -printf', it doesn't exist on BSD find(1)",
190 188 ),
191 189 (r'\$RANDOM ', "don't use bash-only $RANDOM to generate random values"),
192 190 ],
193 191 # warnings
194 192 [
195 193 (r'^function', "don't use 'function', use old style"),
196 194 (r'^diff.*-\w*N', "don't use 'diff -N'"),
197 195 (r'\$PWD|\${PWD}', "don't use $PWD, use `pwd`", "no-pwd-check"),
198 196 (r'^([^"\'\n]|("[^"\n]*")|(\'[^\'\n]*\'))*\^', "^ must be quoted"),
199 197 (r'kill (`|\$\()', "don't use kill, use killdaemons.py"),
200 198 ],
201 199 ]
202 200
203 201 testfilters = [
204 202 (r"( *)(#([^!][^\n]*\S)?)", repcomment),
205 203 (r"<<(\S+)((.|\n)*?\n\1)", rephere),
206 204 ]
207 205
208 206 uprefix = r"^ \$ "
209 207 utestpats = [
210 208 [
211 209 (r'^(\S.*|| [$>] \S.*)[ \t]\n', "trailing whitespace on non-output"),
212 210 (
213 211 uprefix + r'.*\|\s*sed[^|>\n]*\n',
214 212 "use regex test output patterns instead of sed",
215 213 ),
216 214 (uprefix + r'(true|exit 0)', "explicit zero exit unnecessary"),
217 215 (
218 216 uprefix + r'.*\|\| echo.*(fail|error)',
219 217 "explicit exit code checks unnecessary",
220 218 ),
221 219 (uprefix + r'set -e', "don't use set -e"),
222 220 (uprefix + r'(\s|fi\b|done\b)', "use > for continued lines"),
223 221 (
224 222 uprefix + r'.*:\.\S*/',
225 223 "x:.y in a path does not work on msys, rewrite "
226 224 "as x://.y, or see `hg log -k msys` for alternatives",
227 225 r'-\S+:\.|' '# no-msys', # -Rxxx
228 226 ), # in test-pull.t which is skipped on windows
229 227 (
230 228 r'^ [^$>].*27\.0\.0\.1',
231 229 'use $LOCALIP not an explicit loopback address',
232 230 ),
233 231 (
234 232 r'^ (?![>$] ).*\$LOCALIP.*[^)]$',
235 233 'mark $LOCALIP output lines with (glob) to help tests in BSD jails',
236 234 ),
237 235 (
238 236 r'^ (cat|find): .*: \$ENOENT\$',
239 237 'use test -f to test for file existence',
240 238 ),
241 239 (
242 240 r'^ diff -[^ -]*p',
243 241 "don't use (external) diff with -p for portability",
244 242 ),
245 243 (r' readlink ', 'use readlink.py instead of readlink'),
246 244 (
247 245 r'^ [-+][-+][-+] .* [-+]0000 \(glob\)',
248 246 "glob timezone field in diff output for portability",
249 247 ),
250 248 (
251 249 r'^ @@ -[0-9]+ [+][0-9]+,[0-9]+ @@',
252 250 "use '@@ -N* +N,n @@ (glob)' style chunk header for portability",
253 251 ),
254 252 (
255 253 r'^ @@ -[0-9]+,[0-9]+ [+][0-9]+ @@',
256 254 "use '@@ -N,n +N* @@ (glob)' style chunk header for portability",
257 255 ),
258 256 (
259 257 r'^ @@ -[0-9]+ [+][0-9]+ @@',
260 258 "use '@@ -N* +N* @@ (glob)' style chunk header for portability",
261 259 ),
262 260 (
263 261 uprefix + r'hg( +-[^ ]+( +[^ ]+)?)* +extdiff'
264 262 r'( +(-[^ po-]+|--(?!program|option)[^ ]+|[^-][^ ]*))*$',
265 263 "use $RUNTESTDIR/pdiff via extdiff (or -o/-p for false-positives)",
266 264 ),
267 265 ],
268 266 # warnings
269 267 [
270 268 (
271 269 r'^ (?!.*\$LOCALIP)[^*?/\n]* \(glob\)$',
272 270 "glob match with no glob string (?, *, /, and $LOCALIP)",
273 271 ),
274 272 ],
275 273 ]
276 274
277 275 # transform plain test rules to unified test's
278 276 for i in [0, 1]:
279 277 for tp in testpats[i]:
280 278 p = tp[0]
281 279 m = tp[1]
282 280 if p.startswith('^'):
283 281 p = "^ [$>] (%s)" % p[1:]
284 282 else:
285 283 p = "^ [$>] .*(%s)" % p
286 284 utestpats[i].append((p, m) + tp[2:])
287 285
288 286 # don't transform the following rules:
289 287 # " > \t" and " \t" should be allowed in unified tests
290 288 testpats[0].append((r'^( *)\t', "don't use tabs to indent"))
291 289 utestpats[0].append((r'^( ?)\t', "don't use tabs to indent"))
292 290
293 291 utestfilters = [
294 292 (r"<<(\S+)((.|\n)*?\n > \1)", rephere),
295 293 (r"( +)(#([^!][^\n]*\S)?)", repcomment),
296 294 ]
297 295
298 296 # common patterns to check *.py
299 297 commonpypats = [
300 298 [
301 299 (r'\\$', 'Use () to wrap long lines in Python, not \\'),
302 300 (
303 301 r'^\s*def\s*\w+\s*\(.*,\s*\(',
304 302 "tuple parameter unpacking not available in Python 3+",
305 303 ),
306 304 (
307 305 r'lambda\s*\(.*,.*\)',
308 306 "tuple parameter unpacking not available in Python 3+",
309 307 ),
310 308 (r'(?<!def)\s+(cmp)\(', "cmp is not available in Python 3+"),
311 309 (r'(?<!\.)\breduce\s*\(.*', "reduce is not available in Python 3+"),
312 310 (
313 311 r'\bdict\(.*=',
314 312 'dict() is different in Py2 and 3 and is slower than {}',
315 313 'dict-from-generator',
316 314 ),
317 315 (r'\.has_key\b', "dict.has_key is not available in Python 3+"),
318 316 (r'\s<>\s', '<> operator is not available in Python 3+, use !='),
319 317 (r'^\s*\t', "don't use tabs"),
320 318 (r'\S;\s*\n', "semicolon"),
321 319 (r'[^_]_\([ \t\n]*(?:"[^"]+"[ \t\n+]*)+%', "don't use % inside _()"),
322 320 (r"[^_]_\([ \t\n]*(?:'[^']+'[ \t\n+]*)+%", "don't use % inside _()"),
323 321 (r'(\w|\)),\w', "missing whitespace after ,"),
324 322 (r'(\w|\))[+/*\-<>]\w', "missing whitespace in expression"),
325 323 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
326 324 (
327 325 (
328 326 # a line ending with a colon, potentially with trailing comments
329 327 r':([ \t]*#[^\n]*)?\n'
330 328 # one that is not a pass and not only a comment
331 329 r'(?P<indent>[ \t]+)[^#][^\n]+\n'
332 330 # more lines at the same indent level
333 331 r'((?P=indent)[^\n]+\n)*'
334 332 # a pass at the same indent level, which is bogus
335 333 r'(?P=indent)pass[ \t\n#]'
336 334 ),
337 335 'omit superfluous pass',
338 336 ),
339 337 (r'[^\n]\Z', "no trailing newline"),
340 338 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
341 339 (
342 340 r'^\s*(if|while|def|class|except|try)\s[^[\n]*:\s*[^\\n]#\s]+',
343 341 "linebreak after :",
344 342 ),
345 343 (
346 344 r'\b(%s)\('
347 345 % '|'.join(k for k in keyword.kwlist if k not in ('print', 'exec')),
348 346 "Python keyword is not a function",
349 347 ),
350 348 # (r'class\s[A-Z][^\(]*\((?!Exception)',
351 349 # "don't capitalize non-exception classes"),
352 350 # (r'in range\(', "use xrange"),
353 351 # (r'^\s*print\s+', "avoid using print in core and extensions"),
354 352 (r'[\x80-\xff]', "non-ASCII character literal"),
355 353 (r'("\')\.format\(', "str.format() has no bytes counterpart, use %"),
356 354 (
357 355 r'([\(\[][ \t]\S)|(\S[ \t][\)\]])',
358 356 "gratuitous whitespace in () or []",
359 357 ),
360 358 # (r'\s\s=', "gratuitous whitespace before ="),
361 359 (
362 360 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
363 361 "missing whitespace around operator",
364 362 ),
365 363 (
366 364 r'[^>< ](\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\s',
367 365 "missing whitespace around operator",
368 366 ),
369 367 (
370 368 r'\s(\+=|-=|!=|<>|<=|>=|<<=|>>=|%=)\S',
371 369 "missing whitespace around operator",
372 370 ),
373 371 (r'[^^+=*/!<>&| %-](\s=|=\s)[^= ]', "wrong whitespace around ="),
374 372 (
375 373 r'raise [^,(]+, (\([^\)]+\)|[^,\(\)]+)$',
376 374 "don't use old-style two-argument raise, use Exception(message)",
377 375 ),
378 376 (r' is\s+(not\s+)?["\'0-9-]', "object comparison with literal"),
379 377 (
380 378 r' [=!]=\s+(True|False|None)',
381 379 "comparison with singleton, use 'is' or 'is not' instead",
382 380 ),
383 381 (
384 382 r'^\s*(while|if) [01]:',
385 383 "use True/False for constant Boolean expression",
386 384 ),
387 385 (r'^\s*if False(:| +and)', 'Remove code instead of using `if False`'),
388 386 (
389 387 r'(?:(?<!def)\s+|\()hasattr\(',
390 388 'hasattr(foo, bar) is broken on py2, use util.safehasattr(foo, bar) '
391 389 'instead',
392 390 r'#.*hasattr-py3-only',
393 391 ),
394 392 (r'opener\([^)]*\).read\(', "use opener.read() instead"),
395 393 (r'opener\([^)]*\).write\(', "use opener.write() instead"),
396 394 (r'(?i)descend[e]nt', "the proper spelling is descendAnt"),
397 395 (r'\.debug\(\_', "don't mark debug messages for translation"),
398 396 (r'\.strip\(\)\.split\(\)', "no need to strip before splitting"),
399 397 (r'^\s*except\s*:', "naked except clause", r'#.*re-raises'),
400 398 (
401 399 r'^\s*except\s([^\(,]+|\([^\)]+\))\s*,',
402 400 'legacy exception syntax; use "as" instead of ","',
403 401 ),
404 402 (r'release\(.*wlock, .*lock\)', "wrong lock release order"),
405 403 (r'\bdef\s+__bool__\b', "__bool__ should be __nonzero__ in Python 2"),
406 404 (
407 405 r'os\.path\.join\(.*, *(""|\'\')\)',
408 406 "use pathutil.normasprefix(path) instead of os.path.join(path, '')",
409 407 ),
410 408 (r'\s0[0-7]+\b', 'legacy octal syntax; use "0o" prefix instead of "0"'),
411 409 # XXX only catch mutable arguments on the first line of the definition
412 410 (r'def.*[( ]\w+=\{\}', "don't use mutable default arguments"),
413 411 (r'\butil\.Abort\b', "directly use error.Abort"),
414 412 (
415 413 r'^@(\w*\.)?cachefunc',
416 414 "module-level @cachefunc is risky, please avoid",
417 415 ),
418 416 (
419 417 r'^(from|import) mercurial\.(cext|pure|cffi)',
420 418 "use mercurial.policy.importmod instead",
421 419 ),
422 420 (r'\.next\(\)', "don't use .next(), use next(...)"),
423 421 (
424 422 r'([a-z]*).revision\(\1\.node\(',
425 423 "don't convert rev to node before passing to revision(nodeorrev)",
426 424 ),
427 425 (r'platform\.system\(\)', "don't use platform.system(), use pycompat"),
428 426 ],
429 427 # warnings
430 428 [],
431 429 ]
432 430
433 431 # patterns to check normal *.py files
434 432 pypats = [
435 433 [
436 434 # Ideally, these should be placed in "commonpypats" for
437 435 # consistency of coding rules in Mercurial source tree.
438 436 # But on the other hand, these are not so seriously required for
439 437 # python code fragments embedded in test scripts. Fixing test
440 438 # scripts for these patterns requires many changes, and has less
441 439 # profit than effort.
442 440 (r'raise Exception', "don't raise generic exceptions"),
443 441 (r'[\s\(](open|file)\([^)]*\)\.read\(', "use util.readfile() instead"),
444 442 (
445 443 r'[\s\(](open|file)\([^)]*\)\.write\(',
446 444 "use util.writefile() instead",
447 445 ),
448 446 (
449 447 r'^[\s\(]*(open(er)?|file)\([^)]*\)(?!\.close\(\))',
450 448 "always assign an opened file to a variable, and close it afterwards",
451 449 ),
452 450 (
453 451 r'[\s\(](open|file)\([^)]*\)\.(?!close\(\))',
454 452 "always assign an opened file to a variable, and close it afterwards",
455 453 ),
456 454 (r':\n( )*( ){1,3}[^ ]', "must indent 4 spaces"),
457 455 (r'^import atexit', "don't use atexit, use ui.atexit"),
458 456 # rules depending on implementation of repquote()
459 457 (
460 458 r' x+[xpqo%APM][\'"]\n\s+[\'"]x',
461 459 'string join across lines with no space',
462 460 ),
463 461 (
464 462 r'''(?x)ui\.(status|progress|write|note|warn)\(
465 463 [ \t\n#]*
466 464 (?# any strings/comments might precede a string, which
467 465 # contains translatable message)
468 466 b?((['"]|\'\'\'|""")[ \npq%bAPMxno]*(['"]|\'\'\'|""")[ \t\n#]+)*
469 467 (?# sequence consisting of below might precede translatable message
470 468 # - formatting string: "% 10s", "%05d", "% -3.2f", "%*s", "%%" ...
471 469 # - escaped character: "\\", "\n", "\0" ...
472 470 # - character other than '%', 'b' as '\', and 'x' as alphabet)
473 471 (['"]|\'\'\'|""")
474 472 ((%([ n]?[PM]?([np]+|A))?x)|%%|b[bnx]|[ \nnpqAPMo])*x
475 473 (?# this regexp can't use [^...] style,
476 474 # because _preparepats forcibly adds "\n" into [^...],
477 475 # even though this regexp wants match it against "\n")''',
478 476 "missing _() in ui message (use () to hide false-positives)",
479 477 ),
480 478 ]
481 479 + commonpypats[0],
482 480 # warnings
483 481 [
484 482 # rules depending on implementation of repquote()
485 483 (r'(^| )pp +xxxxqq[ \n][^\n]', "add two newlines after '.. note::'"),
486 484 ]
487 485 + commonpypats[1],
488 486 ]
489 487
490 488 # patterns to check *.py for embedded ones in test script
491 489 embeddedpypats = [
492 490 [] + commonpypats[0],
493 491 # warnings
494 492 [] + commonpypats[1],
495 493 ]
496 494
497 495 # common filters to convert *.py
498 496 commonpyfilters = [
499 497 (
500 498 r"""(?msx)(?P<comment>\#.*?$)|
501 499 ((?P<quote>('''|\"\"\"|(?<!')'(?!')|(?<!")"(?!")))
502 500 (?P<text>(([^\\]|\\.)*?))
503 501 (?P=quote))""",
504 502 reppython,
505 503 ),
506 504 ]
507 505
508 506 # pattern only for mercurial and extensions
509 507 core_py_pats = [
510 508 [
511 509 # Windows tend to get confused about capitalization of the drive letter
512 510 #
513 511 # see mercurial.windows.abspath for details
514 512 (
515 513 r'os\.path\.abspath',
516 514 "use util.abspath instead (windows)",
517 515 r'#.*re-exports',
518 516 ),
519 517 ],
520 518 # warnings
521 519 [],
522 520 ]
523 521
524 522 # filters to convert normal *.py files
525 523 pyfilters = [] + commonpyfilters
526 524
527 525 # non-filter patterns
528 526 pynfpats = [
529 527 [
530 528 (r'pycompat\.osname\s*[=!]=\s*[\'"]nt[\'"]', "use pycompat.iswindows"),
531 529 (r'pycompat\.osname\s*[=!]=\s*[\'"]posix[\'"]', "use pycompat.isposix"),
532 530 (
533 531 r'pycompat\.sysplatform\s*[!=]=\s*[\'"]darwin[\'"]',
534 532 "use pycompat.isdarwin",
535 533 ),
536 534 ],
537 535 # warnings
538 536 [],
539 537 ]
540 538
541 539 # filters to convert *.py for embedded ones in test script
542 540 embeddedpyfilters = [] + commonpyfilters
543 541
544 542 # extension non-filter patterns
545 543 pyextnfpats = [
546 544 [(r'^"""\n?[A-Z]', "don't capitalize docstring title")],
547 545 # warnings
548 546 [],
549 547 ]
550 548
551 549 txtfilters = []
552 550
553 551 txtpats = [
554 552 [
555 553 (r'\s$', 'trailing whitespace'),
556 554 ('.. note::[ \n][^\n]', 'add two newlines after note::'),
557 555 ],
558 556 [],
559 557 ]
560 558
561 559 cpats = [
562 560 [
563 561 (r'//', "don't use //-style comments"),
564 562 (r'\S\t', "don't use tabs except for indent"),
565 563 (r'(\S[ \t]+|^[ \t]+)\n', "trailing whitespace"),
566 564 (r'(while|if|do|for)\(', "use space after while/if/do/for"),
567 565 (r'return\(', "return is not a function"),
568 566 (r' ;', "no space before ;"),
569 567 (r'[^;] \)', "no space before )"),
570 568 (r'[)][{]', "space between ) and {"),
571 569 (r'\w+\* \w+', "use int *foo, not int* foo"),
572 570 (r'\W\([^\)]+\) \w+', "use (int)foo, not (int) foo"),
573 571 (r'\w+ (\+\+|--)', "use foo++, not foo ++"),
574 572 (r'\w,\w', "missing whitespace after ,"),
575 573 (r'^[^#]\w[+/*]\w', "missing whitespace in expression"),
576 574 (r'\w\s=\s\s+\w', "gratuitous whitespace after ="),
577 575 (r'^#\s+\w', "use #foo, not # foo"),
578 576 (r'[^\n]\Z', "no trailing newline"),
579 577 (r'^\s*#import\b', "use only #include in standard C code"),
580 578 (r'strcpy\(', "don't use strcpy, use strlcpy or memcpy"),
581 579 (r'strcat\(', "don't use strcat"),
582 580 # rules depending on implementation of repquote()
583 581 ],
584 582 # warnings
585 583 [
586 584 # rules depending on implementation of repquote()
587 585 ],
588 586 ]
589 587
590 588 cfilters = [
591 589 (r'(/\*)(((\*(?!/))|[^*])*)\*/', repccomment),
592 590 (r'''(?P<quote>(?<!")")(?P<text>([^"]|\\")+)"(?!")''', repquote),
593 591 (r'''(#\s*include\s+<)([^>]+)>''', repinclude),
594 592 (r'(\()([^)]+\))', repcallspaces),
595 593 ]
596 594
597 595 inutilpats = [
598 596 [
599 597 (r'\bui\.', "don't use ui in util"),
600 598 ],
601 599 # warnings
602 600 [],
603 601 ]
604 602
605 603 inrevlogpats = [
606 604 [
607 605 (r'\brepo\.', "don't use repo in revlog"),
608 606 ],
609 607 # warnings
610 608 [],
611 609 ]
612 610
613 611 webtemplatefilters = []
614 612
615 613 webtemplatepats = [
616 614 [],
617 615 [
618 616 (
619 617 r'{desc(\|(?!websub|firstline)[^\|]*)+}',
620 618 'follow desc keyword with either firstline or websub',
621 619 ),
622 620 ],
623 621 ]
624 622
625 623 allfilesfilters = []
626 624
627 625 allfilespats = [
628 626 [
629 627 (
630 628 r'(http|https)://[a-zA-Z0-9./]*selenic.com/',
631 629 'use mercurial-scm.org domain URL',
632 630 ),
633 631 (
634 632 r'mercurial@selenic\.com',
635 633 'use mercurial-scm.org domain for mercurial ML address',
636 634 ),
637 635 (
638 636 r'mercurial-devel@selenic\.com',
639 637 'use mercurial-scm.org domain for mercurial-devel ML address',
640 638 ),
641 639 ],
642 640 # warnings
643 641 [],
644 642 ]
645 643
646 644 py3pats = [
647 645 [
648 646 (
649 647 r'os\.environ',
650 648 "use encoding.environ instead (py3)",
651 649 r'#.*re-exports',
652 650 ),
653 651 (r'os\.name', "use pycompat.osname instead (py3)"),
654 652 (r'os\.getcwd', "use encoding.getcwd instead (py3)", r'#.*re-exports'),
655 653 (r'os\.sep', "use pycompat.ossep instead (py3)"),
656 654 (r'os\.pathsep', "use pycompat.ospathsep instead (py3)"),
657 655 (r'os\.altsep', "use pycompat.osaltsep instead (py3)"),
658 656 (r'sys\.platform', "use pycompat.sysplatform instead (py3)"),
659 657 (r'getopt\.getopt', "use pycompat.getoptb instead (py3)"),
660 658 (r'os\.getenv', "use encoding.environ.get instead"),
661 659 (r'os\.setenv', "modifying the environ dict is not preferred"),
662 660 (r'(?<!pycompat\.)xrange', "use pycompat.xrange instead (py3)"),
663 661 ],
664 662 # warnings
665 663 [],
666 664 ]
667 665
668 666 checks = [
669 667 ('python', r'.*\.(py|cgi)$', r'^#!.*python', pyfilters, pypats),
670 668 ('python', r'.*\.(py|cgi)$', r'^#!.*python', [], pynfpats),
671 669 ('python', r'.*hgext.*\.py$', '', [], pyextnfpats),
672 670 (
673 671 'python 3',
674 672 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
675 673 '',
676 674 pyfilters,
677 675 py3pats,
678 676 ),
679 677 (
680 678 'core files',
681 679 r'.*(hgext|mercurial)/(?!demandimport|policy|pycompat).*\.py',
682 680 '',
683 681 pyfilters,
684 682 core_py_pats,
685 683 ),
686 684 ('test script', r'(.*/)?test-[^.~]*$', '', testfilters, testpats),
687 685 ('c', r'.*\.[ch]$', '', cfilters, cpats),
688 686 ('unified test', r'.*\.t$', '', utestfilters, utestpats),
689 687 (
690 688 'layering violation repo in revlog',
691 689 r'mercurial/revlog\.py',
692 690 '',
693 691 pyfilters,
694 692 inrevlogpats,
695 693 ),
696 694 (
697 695 'layering violation ui in util',
698 696 r'mercurial/util\.py',
699 697 '',
700 698 pyfilters,
701 699 inutilpats,
702 700 ),
703 701 ('txt', r'.*\.txt$', '', txtfilters, txtpats),
704 702 (
705 703 'web template',
706 704 r'mercurial/templates/.*\.tmpl',
707 705 '',
708 706 webtemplatefilters,
709 707 webtemplatepats,
710 708 ),
711 709 ('all except for .po', r'.*(?<!\.po)$', '', allfilesfilters, allfilespats),
712 710 ]
713 711
714 712 # (desc,
715 713 # func to pick up embedded code fragments,
716 714 # list of patterns to convert target files
717 715 # list of patterns to detect errors/warnings)
718 716 embeddedchecks = [
719 717 (
720 718 'embedded python',
721 719 testparseutil.pyembedded,
722 720 embeddedpyfilters,
723 721 embeddedpypats,
724 722 )
725 723 ]
726 724
727 725
728 726 def _preparepats():
729 727 def preparefailandwarn(failandwarn):
730 728 for pats in failandwarn:
731 729 for i, pseq in enumerate(pats):
732 730 # fix-up regexes for multi-line searches
733 731 p = pseq[0]
734 732 # \s doesn't match \n (done in two steps)
735 733 # first, we replace \s that appears in a set already
736 734 p = re.sub(r'\[\\s', r'[ \\t', p)
737 735 # now we replace other \s instances.
738 736 p = re.sub(r'(?<!(\\|\[))\\s', r'[ \\t]', p)
739 737 # [^...] doesn't match newline
740 738 p = re.sub(r'(?<!\\)\[\^', r'[^\\n', p)
741 739
742 740 pats[i] = (re.compile(p, re.MULTILINE),) + pseq[1:]
743 741
744 742 def preparefilters(filters):
745 743 for i, flt in enumerate(filters):
746 744 filters[i] = re.compile(flt[0]), flt[1]
747 745
748 746 for cs in (checks, embeddedchecks):
749 747 for c in cs:
750 748 failandwarn = c[-1]
751 749 preparefailandwarn(failandwarn)
752 750
753 751 filters = c[-2]
754 752 preparefilters(filters)
755 753
756 754
757 755 class norepeatlogger:
758 756 def __init__(self):
759 757 self._lastseen = None
760 758
761 759 def log(self, fname, lineno, line, msg, blame):
762 760 """print error related a to given line of a given file.
763 761
764 762 The faulty line will also be printed but only once in the case
765 763 of multiple errors.
766 764
767 765 :fname: filename
768 766 :lineno: line number
769 767 :line: actual content of the line
770 768 :msg: error message
771 769 """
772 770 msgid = fname, lineno, line
773 771 if msgid != self._lastseen:
774 772 if blame:
775 773 print("%s:%d (%s):" % (fname, lineno, blame))
776 774 else:
777 775 print("%s:%d:" % (fname, lineno))
778 776 print(" > %s" % line)
779 777 self._lastseen = msgid
780 778 print(" " + msg)
781 779
782 780
783 781 _defaultlogger = norepeatlogger()
784 782
785 783
786 784 def getblame(f):
787 785 lines = []
788 786 for l in os.popen('hg annotate -un %s' % f):
789 787 start, line = l.split(':', 1)
790 788 user, rev = start.split()
791 789 lines.append((line[1:-1], user, rev))
792 790 return lines
793 791
794 792
795 793 def checkfile(
796 794 f,
797 795 logfunc=_defaultlogger.log,
798 796 maxerr=None,
799 797 warnings=False,
800 798 blame=False,
801 799 debug=False,
802 800 lineno=True,
803 801 ):
804 802 """checks style and portability of a given file
805 803
806 804 :f: filepath
807 805 :logfunc: function used to report error
808 806 logfunc(filename, linenumber, linecontent, errormessage)
809 807 :maxerr: number of error to display before aborting.
810 808 Set to false (default) to report all errors
811 809
812 810 return True if no error is found, False otherwise.
813 811 """
814 812 result = True
815 813
816 814 try:
817 815 with opentext(f) as fp:
818 816 try:
819 817 pre = fp.read()
820 818 except UnicodeDecodeError as e:
821 819 print("%s while reading %s" % (e, f))
822 820 return result
823 821 except IOError as e:
824 822 print("Skipping %s, %s" % (f, str(e).split(':', 1)[0]))
825 823 return result
826 824
827 825 # context information shared while single checkfile() invocation
828 826 context = {'blamecache': None}
829 827
830 828 for name, match, magic, filters, pats in checks:
831 829 if debug:
832 830 print(name, f)
833 831 if not (re.match(match, f) or (magic and re.search(magic, pre))):
834 832 if debug:
835 833 print(
836 834 "Skipping %s for %s it doesn't match %s" % (name, match, f)
837 835 )
838 836 continue
839 837 if "no-" "check-code" in pre:
840 838 # If you're looking at this line, it's because a file has:
841 839 # no- check- code
842 840 # but the reason to output skipping is to make life for
843 841 # tests easier. So, instead of writing it with a normal
844 842 # spelling, we write it with the expected spelling from
845 843 # tests/test-check-code.t
846 844 print("Skipping %s it has no-che?k-code (glob)" % f)
847 845 return "Skip" # skip checking this file
848 846
849 847 fc = _checkfiledata(
850 848 name,
851 849 f,
852 850 pre,
853 851 filters,
854 852 pats,
855 853 context,
856 854 logfunc,
857 855 maxerr,
858 856 warnings,
859 857 blame,
860 858 debug,
861 859 lineno,
862 860 )
863 861 if fc:
864 862 result = False
865 863
866 864 if f.endswith('.t') and "no-" "check-code" not in pre:
867 865 if debug:
868 866 print("Checking embedded code in %s" % f)
869 867
870 868 prelines = pre.splitlines()
871 869 embeddederros = []
872 870 for name, embedded, filters, pats in embeddedchecks:
873 871 # "reset curmax at each repetition" treats maxerr as "max
874 872 # nubmer of errors in an actual file per entry of
875 873 # (embedded)checks"
876 874 curmaxerr = maxerr
877 875
878 876 for found in embedded(f, prelines, embeddederros):
879 877 filename, starts, ends, code = found
880 878 fc = _checkfiledata(
881 879 name,
882 880 f,
883 881 code,
884 882 filters,
885 883 pats,
886 884 context,
887 885 logfunc,
888 886 curmaxerr,
889 887 warnings,
890 888 blame,
891 889 debug,
892 890 lineno,
893 891 offset=starts - 1,
894 892 )
895 893 if fc:
896 894 result = False
897 895 if curmaxerr:
898 896 if fc >= curmaxerr:
899 897 break
900 898 curmaxerr -= fc
901 899
902 900 return result
903 901
904 902
905 903 def _checkfiledata(
906 904 name,
907 905 f,
908 906 filedata,
909 907 filters,
910 908 pats,
911 909 context,
912 910 logfunc,
913 911 maxerr,
914 912 warnings,
915 913 blame,
916 914 debug,
917 915 lineno,
918 916 offset=None,
919 917 ):
920 918 """Execute actual error check for file data
921 919
922 920 :name: of the checking category
923 921 :f: filepath
924 922 :filedata: content of a file
925 923 :filters: to be applied before checking
926 924 :pats: to detect errors
927 925 :context: a dict of information shared while single checkfile() invocation
928 926 Valid keys: 'blamecache'.
929 927 :logfunc: function used to report error
930 928 logfunc(filename, linenumber, linecontent, errormessage)
931 929 :maxerr: number of error to display before aborting, or False to
932 930 report all errors
933 931 :warnings: whether warning level checks should be applied
934 932 :blame: whether blame information should be displayed at error reporting
935 933 :debug: whether debug information should be displayed
936 934 :lineno: whether lineno should be displayed at error reporting
937 935 :offset: line number offset of 'filedata' in 'f' for checking
938 936 an embedded code fragment, or None (offset=0 is different
939 937 from offset=None)
940 938
941 939 returns number of detected errors.
942 940 """
943 941 blamecache = context['blamecache']
944 942 if offset is None:
945 943 lineoffset = 0
946 944 else:
947 945 lineoffset = offset
948 946
949 947 fc = 0
950 948 pre = post = filedata
951 949
952 950 if True: # TODO: get rid of this redundant 'if' block
953 951 for p, r in filters:
954 952 post = re.sub(p, r, post)
955 953 nerrs = len(pats[0]) # nerr elements are errors
956 954 if warnings:
957 955 pats = pats[0] + pats[1]
958 956 else:
959 957 pats = pats[0]
960 958 # print post # uncomment to show filtered version
961 959
962 960 if debug:
963 961 print("Checking %s for %s" % (name, f))
964 962
965 963 prelines = None
966 964 errors = []
967 965 for i, pat in enumerate(pats):
968 966 if len(pat) == 3:
969 967 p, msg, ignore = pat
970 968 else:
971 969 p, msg = pat
972 970 ignore = None
973 971 if i >= nerrs:
974 972 msg = "warning: " + msg
975 973
976 974 pos = 0
977 975 n = 0
978 976 for m in p.finditer(post):
979 977 if prelines is None:
980 978 prelines = pre.splitlines()
981 979 postlines = post.splitlines(True)
982 980
983 981 start = m.start()
984 982 while n < len(postlines):
985 983 step = len(postlines[n])
986 984 if pos + step > start:
987 985 break
988 986 pos += step
989 987 n += 1
990 988 l = prelines[n]
991 989
992 990 if ignore and re.search(ignore, l, re.MULTILINE):
993 991 if debug:
994 992 print(
995 993 "Skipping %s for %s:%s (ignore pattern)"
996 994 % (name, f, (n + lineoffset))
997 995 )
998 996 continue
999 997 bd = ""
1000 998 if blame:
1001 999 bd = 'working directory'
1002 1000 if blamecache is None:
1003 1001 blamecache = getblame(f)
1004 1002 context['blamecache'] = blamecache
1005 1003 if (n + lineoffset) < len(blamecache):
1006 1004 bl, bu, br = blamecache[(n + lineoffset)]
1007 1005 if offset is None and bl == l:
1008 1006 bd = '%s@%s' % (bu, br)
1009 1007 elif offset is not None and bl.endswith(l):
1010 1008 # "offset is not None" means "checking
1011 1009 # embedded code fragment". In this case,
1012 1010 # "l" does not have information about the
1013 1011 # beginning of an *original* line in the
1014 1012 # file (e.g. ' > ').
1015 1013 # Therefore, use "str.endswith()", and
1016 1014 # show "maybe" for a little loose
1017 1015 # examination.
1018 1016 bd = '%s@%s, maybe' % (bu, br)
1019 1017
1020 1018 errors.append((f, lineno and (n + lineoffset + 1), l, msg, bd))
1021 1019
1022 1020 errors.sort()
1023 1021 for e in errors:
1024 1022 logfunc(*e)
1025 1023 fc += 1
1026 1024 if maxerr and fc >= maxerr:
1027 1025 print(" (too many errors, giving up)")
1028 1026 break
1029 1027
1030 1028 return fc
1031 1029
1032 1030
1033 1031 def main():
1034 1032 parser = optparse.OptionParser("%prog [options] [files | -]")
1035 1033 parser.add_option(
1036 1034 "-w",
1037 1035 "--warnings",
1038 1036 action="store_true",
1039 1037 help="include warning-level checks",
1040 1038 )
1041 1039 parser.add_option(
1042 1040 "-p", "--per-file", type="int", help="max warnings per file"
1043 1041 )
1044 1042 parser.add_option(
1045 1043 "-b",
1046 1044 "--blame",
1047 1045 action="store_true",
1048 1046 help="use annotate to generate blame info",
1049 1047 )
1050 1048 parser.add_option(
1051 1049 "", "--debug", action="store_true", help="show debug information"
1052 1050 )
1053 1051 parser.add_option(
1054 1052 "",
1055 1053 "--nolineno",
1056 1054 action="store_false",
1057 1055 dest='lineno',
1058 1056 help="don't show line numbers",
1059 1057 )
1060 1058
1061 1059 parser.set_defaults(
1062 1060 per_file=15, warnings=False, blame=False, debug=False, lineno=True
1063 1061 )
1064 1062 (options, args) = parser.parse_args()
1065 1063
1066 1064 if len(args) == 0:
1067 1065 check = glob.glob("*")
1068 1066 elif args == ['-']:
1069 1067 # read file list from stdin
1070 1068 check = sys.stdin.read().splitlines()
1071 1069 else:
1072 1070 check = args
1073 1071
1074 1072 _preparepats()
1075 1073
1076 1074 ret = 0
1077 1075 for f in check:
1078 1076 if not checkfile(
1079 1077 f,
1080 1078 maxerr=options.per_file,
1081 1079 warnings=options.warnings,
1082 1080 blame=options.blame,
1083 1081 debug=options.debug,
1084 1082 lineno=options.lineno,
1085 1083 ):
1086 1084 ret = 1
1087 1085 return ret
1088 1086
1089 1087
1090 1088 if __name__ == "__main__":
1091 1089 sys.exit(main())
@@ -1,144 +1,165 b''
1 # Don't run pipelines on branch "merge", since we're fast-forward only.
2 # Gitlab sees a new branch (since e.g. `topic/stable/my-topic` becomes
3 # `branch/stable`), but the hash hasn't changed. There is no reason to
4 # re-run the CI in our case, since we haven't built up any specific automation.
5 # Right now it's just wasted CI and developer time.
6 # One can still run the pipeline manually via the web interface,
7 # like in the case of releases, to make *extra* sure that the actual branch
8 # has succeeded.
9 workflow:
10 rules:
11 - if: $CI_COMMIT_BRANCH =~ /^branch\/.*/ && $CI_PIPELINE_SOURCE != "web"
12 when: never
13 - if: $CI_PIPELINE_SOURCE == "merge_request_event"
14 when: never
15 - if: $CI_PIPELINE_SOURCE == "push"
16 when: always
17 - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
18 when: never
19 - if: $CI_COMMIT_BRANCH
20 when: always
21
1 22 stages:
2 23 - tests
3 24
4 25 image: registry.heptapod.net/mercurial/ci-images/mercurial-core:$HG_CI_IMAGE_TAG
5 26
6 27 variables:
7 28 PYTHON: python
8 29 TEST_HGMODULEPOLICY: "allow"
9 30 HG_CI_IMAGE_TAG: "v1.0"
10 31 TEST_HGTESTS_ALLOW_NETIO: "0"
11 32
12 33 .all_template: &all
13 34 when: on_success
14 35
15 36 .runtests_template: &runtests
16 37 <<: *all
17 38 stage: tests
18 39 # The runner made a clone as root.
19 40 # We make a new clone owned by user used to run the step.
20 41 before_script:
21 42 - hg clone . /tmp/mercurial-ci/ --noupdate --config phases.publish=no
22 43 - hg -R /tmp/mercurial-ci/ update `hg log --rev '.' --template '{node}'`
23 44 - cd /tmp/mercurial-ci/
24 45 - ls -1 tests/test-check-*.* > /tmp/check-tests.txt
25 46 - black --version
26 47 - clang-format --version
27 48 script:
28 49 - echo "python used, $PYTHON"
29 50 - $PYTHON --version
30 51 - echo "$RUNTEST_ARGS"
31 52 - HGTESTS_ALLOW_NETIO="$TEST_HGTESTS_ALLOW_NETIO" HGMODULEPOLICY="$TEST_HGMODULEPOLICY" "$PYTHON" tests/run-tests.py --color=always $RUNTEST_ARGS
32 53
33 54 checks:
34 55 <<: *runtests
35 56 variables:
36 57 RUNTEST_ARGS: "--time --test-list /tmp/check-tests.txt"
37 58 PYTHON: python3
38 59 CI_CLEVER_CLOUD_FLAVOR: S
39 60
40 61 rust-cargo-test:
41 62 <<: *all
42 63 stage: tests
43 64 script:
44 65 - echo "python used, $PYTHON"
45 66 - make rust-tests
46 67 - make cargo-clippy
47 68 variables:
48 69 PYTHON: python3
49 70 CI_CLEVER_CLOUD_FLAVOR: S
50 71
51 72 test-c:
52 73 <<: *runtests
53 74 variables:
54 75 RUNTEST_ARGS: " --no-rust --blacklist /tmp/check-tests.txt"
55 76 PYTHON: python3
56 77 TEST_HGMODULEPOLICY: "c"
57 78 TEST_HGTESTS_ALLOW_NETIO: "1"
58 79
59 80 test-pure:
60 81 <<: *runtests
61 82 variables:
62 83 RUNTEST_ARGS: "--pure --blacklist /tmp/check-tests.txt"
63 84 PYTHON: python3
64 85 TEST_HGMODULEPOLICY: "py"
65 86
66 87 test-rust:
67 88 <<: *runtests
68 89 variables:
69 90 HGWITHRUSTEXT: cpython
70 91 RUNTEST_ARGS: "--rust --blacklist /tmp/check-tests.txt"
71 92 PYTHON: python3
72 93 TEST_HGMODULEPOLICY: "rust+c"
73 94
74 95 test-rhg:
75 96 <<: *runtests
76 97 variables:
77 98 HGWITHRUSTEXT: cpython
78 99 RUNTEST_ARGS: "--rust --rhg --blacklist /tmp/check-tests.txt"
79 100 PYTHON: python3
80 101 TEST_HGMODULEPOLICY: "rust+c"
81 102
82 103 test-chg:
83 104 <<: *runtests
84 105 variables:
85 106 PYTHON: python3
86 107 RUNTEST_ARGS: "--blacklist /tmp/check-tests.txt --chg"
87 108 TEST_HGMODULEPOLICY: "c"
88 109
89 110 check-pytype:
90 111 extends: .runtests_template
91 112 before_script:
92 113 - hg clone . /tmp/mercurial-ci/ --noupdate --config phases.publish=no
93 114 - hg -R /tmp/mercurial-ci/ update `hg log --rev '.' --template '{node}'`
94 115 - cd /tmp/mercurial-ci/
95 116 - make local PYTHON=$PYTHON
96 117 - $PYTHON -m pip install --user -U libcst==0.3.20 pytype==2022.11.18
97 118 - ./contrib/setup-pytype.sh
98 119 script:
99 120 - echo "Entering script section"
100 121 - sh contrib/check-pytype.sh
101 122 variables:
102 123 PYTHON: python3
103 124
104 125 # `sh.exe --login` sets a couple of extra environment variables that are defined
105 126 # in the MinGW shell, but switches CWD to /home/$username. The previous value
106 127 # is stored in OLDPWD. Of the added variables, MSYSTEM is crucial to running
107 128 # run-tests.py- it is needed to make run-tests.py generate a `python3` script
108 129 # that satisfies the various shebang lines and delegates to `py -3`.
109 130 .window_runtests_template: &windows_runtests
110 131 <<: *all
111 132 when: manual # we don't have any Windows runners anymore at the moment
112 133 stage: tests
113 134 before_script:
114 135 - C:/MinGW/msys/1.0/bin/sh.exe --login -c 'cd "$OLDPWD" && ls -1 tests/test-check-*.* > C:/Temp/check-tests.txt'
115 136 # TODO: find/install cvs, bzr, perforce, gpg, sqlite3
116 137
117 138 script:
118 139 - echo "Entering script section"
119 140 - echo "python used, $Env:PYTHON"
120 141 - Invoke-Expression "$Env:PYTHON -V"
121 142 - Invoke-Expression "$Env:PYTHON -m black --version"
122 143 - echo "$Env:RUNTEST_ARGS"
123 144 - echo "$Env:TMP"
124 145 - echo "$Env:TEMP"
125 146
126 147 - C:/MinGW/msys/1.0/bin/sh.exe --login -c 'cd "$OLDPWD" && HGTESTS_ALLOW_NETIO="$TEST_HGTESTS_ALLOW_NETIO" HGMODULEPOLICY="$TEST_HGMODULEPOLICY" $PYTHON tests/run-tests.py --color=always $RUNTEST_ARGS'
127 148
128 149 windows:
129 150 <<: *windows_runtests
130 151 tags:
131 152 - windows
132 153 variables:
133 154 TEST_HGMODULEPOLICY: "c"
134 155 RUNTEST_ARGS: "--blacklist C:/Temp/check-tests.txt"
135 156 PYTHON: py -3
136 157
137 158 windows-pyox:
138 159 <<: *windows_runtests
139 160 tags:
140 161 - windows
141 162 variables:
142 163 TEST_HGMODULEPOLICY: "c"
143 164 RUNTEST_ARGS: "--blacklist C:/Temp/check-tests.txt --pyoxidized"
144 165 PYTHON: py -3
@@ -1,22 +1,19 b''
1 1 #!/usr/bin/env python3
2 2 #
3 3 # An example FastCGI script for use with flup, edit as necessary
4 4
5 5 # Path to repo or hgweb config to serve (see 'hg help hgweb')
6 6 config = b"/path/to/repo/or/config"
7 7
8 8 # Uncomment and adjust if Mercurial is not installed system-wide
9 9 # (consult "installed modules" path from 'hg debuginstall'):
10 10 # import sys; sys.path.insert(0, "/path/to/python/lib")
11 11
12 # Uncomment to send python tracebacks to the browser if an error occurs:
13 # import cgitb; cgitb.enable()
14
15 12 from mercurial import demandimport
16 13
17 14 demandimport.enable()
18 15 from mercurial.hgweb import hgweb
19 16 from flup.server.fcgi import WSGIServer
20 17
21 18 application = hgweb(config)
22 19 WSGIServer(application).run()
@@ -1,18 +1,15 b''
1 1 # An example WSGI for use with mod_wsgi, edit as necessary
2 2 # See https://mercurial-scm.org/wiki/modwsgi for more information
3 3
4 4 # Path to repo or hgweb config to serve (see 'hg help hgweb')
5 5 config = b"/path/to/repo/or/config"
6 6
7 7 # Uncomment and adjust if Mercurial is not installed system-wide
8 8 # (consult "installed modules" path from 'hg debuginstall'):
9 9 #import sys; sys.path.insert(0, "/path/to/python/lib")
10 10
11 # Uncomment to send python tracebacks to the browser if an error occurs:
12 #import cgitb; cgitb.enable()
13
14 11 # enable demandloading to reduce startup time
15 12 from mercurial import demandimport; demandimport.enable()
16 13
17 14 from mercurial.hgweb import hgweb
18 15 application = hgweb(config)
@@ -1,83 +1,86 b''
1 1 # hgdemandimport - global demand-loading of modules for Mercurial
2 2 #
3 3 # Copyright 2017 Facebook Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''demandimport - automatic demand-loading of modules'''
9 9
10 10 # This is in a separate package from mercurial because in Python 3,
11 11 # demand loading is per-package. Keeping demandimport in the mercurial package
12 12 # would disable demand loading for any modules in mercurial.
13 13
14 14
15 15 import os
16 16 import sys
17 17
18 18 from . import demandimportpy3 as demandimport
19 19
20 20 # Full module names which can't be lazy imported.
21 21 # Extensions can add to this set.
22 22 IGNORES = {
23 23 '__future__',
24 24 '_hashlib',
25 25 # ImportError during pkg_resources/__init__.py:fixup_namespace_package
26 26 '_imp',
27 27 '_xmlplus',
28 28 'fcntl',
29 29 'nt', # pathlib2 tests the existence of built-in 'nt' module
30 30 'win32com.gen_py',
31 31 'win32com.shell', # 'appdirs' tries to import win32com.shell
32 32 '_winreg', # 2.7 mimetypes needs immediate ImportError
33 33 'pythoncom',
34 34 # imported by tarfile, not available under Windows
35 35 'pwd',
36 36 'grp',
37 37 # imported by profile, itself imported by hotshot.stats,
38 38 # not available under Windows
39 39 'resource',
40 40 # this trips up many extension authors
41 41 'gtk',
42 42 # setuptools' pkg_resources.py expects "from __main__ import x" to
43 43 # raise ImportError if x not defined
44 44 '__main__',
45 45 '_ast', # https://bugs.python.org/issue41631
46 46 '_ssl', # conditional imports in the stdlib, issue1964
47 47 '_sre', # issue4920
48 48 'rfc822',
49 49 'mimetools',
50 50 'sqlalchemy.events', # has import-time side effects (issue5085)
51 51 'sqlalchemy.dialects', # similar problems as above
52 52 # setuptools 8 expects this module to explode early when not on windows
53 53 'distutils.msvc9compiler',
54 54 '__builtin__',
55 55 'builtins',
56 56 'urwid.command_map', # for pudb
57 57 'lzma',
58 # setuptools uses this hack to inject it's own distutils at import time
59 'setuptools',
60 '_distutils_hack.override',
58 61 }
59 62
60 63 _pypy = '__pypy__' in sys.builtin_module_names
61 64
62 65 if _pypy:
63 66 # _ctypes.pointer is shadowed by "from ... import pointer" (PyPy 5)
64 67 IGNORES.add('_ctypes.pointer')
65 68 # pure Python module on PyPy, must be loaded to raise ModuleNotFoundError
66 69 # on non-Windows platforms
67 70 IGNORES.add('msvcrt')
68 71
69 72 demandimport.init(IGNORES)
70 73
71 74 # Re-export.
72 75 isenabled = demandimport.isenabled
73 76 disable = demandimport.disable
74 77 deactivated = demandimport.deactivated
75 78
76 79
77 80 def enable():
78 81 # chg pre-imports modules so do not enable demandimport for it
79 82 if (
80 83 'CHGINTERNALMARK' not in os.environ
81 84 and os.environ.get('HGDEMANDIMPORT') != 'disable'
82 85 ):
83 86 demandimport.enable()
@@ -1,1082 +1,1090 b''
1 1 # This software may be used and distributed according to the terms of the
2 2 # GNU General Public License version 2 or any later version.
3 3
4 4 """advertise pre-generated bundles to seed clones
5 5
6 6 "clonebundles" is a server-side extension used to advertise the existence
7 7 of pre-generated, externally hosted bundle files to clients that are
8 8 cloning so that cloning can be faster, more reliable, and require less
9 9 resources on the server. "pullbundles" is a related feature for sending
10 10 pre-generated bundle files to clients as part of pull operations.
11 11
12 12 Cloning can be a CPU and I/O intensive operation on servers. Traditionally,
13 13 the server, in response to a client's request to clone, dynamically generates
14 14 a bundle containing the entire repository content and sends it to the client.
15 15 There is no caching on the server and the server will have to redundantly
16 16 generate the same outgoing bundle in response to each clone request. For
17 17 servers with large repositories or with high clone volume, the load from
18 18 clones can make scaling the server challenging and costly.
19 19
20 20 This extension provides server operators the ability to offload
21 21 potentially expensive clone load to an external service. Pre-generated
22 22 bundles also allow using more CPU intensive compression, reducing the
23 23 effective bandwidth requirements.
24 24
25 25 Here's how clone bundles work:
26 26
27 27 1. A server operator establishes a mechanism for making bundle files available
28 28 on a hosting service where Mercurial clients can fetch them.
29 29 2. A manifest file listing available bundle URLs and some optional metadata
30 30 is added to the Mercurial repository on the server.
31 31 3. A client initiates a clone against a clone bundles aware server.
32 32 4. The client sees the server is advertising clone bundles and fetches the
33 33 manifest listing available bundles.
34 34 5. The client filters and sorts the available bundles based on what it
35 35 supports and prefers.
36 36 6. The client downloads and applies an available bundle from the
37 37 server-specified URL.
38 38 7. The client reconnects to the original server and performs the equivalent
39 39 of :hg:`pull` to retrieve all repository data not in the bundle. (The
40 40 repository could have been updated between when the bundle was created
41 41 and when the client started the clone.) This may use "pullbundles".
42 42
43 43 Instead of the server generating full repository bundles for every clone
44 44 request, it generates full bundles once and they are subsequently reused to
45 45 bootstrap new clones. The server may still transfer data at clone time.
46 46 However, this is only data that has been added/changed since the bundle was
47 47 created. For large, established repositories, this can reduce server load for
48 48 clones to less than 1% of original.
49 49
50 50 Here's how pullbundles work:
51 51
52 52 1. A manifest file listing available bundles and describing the revisions
53 53 is added to the Mercurial repository on the server.
54 54 2. A new-enough client informs the server that it supports partial pulls
55 55 and initiates a pull.
56 56 3. If the server has pull bundles enabled and sees the client advertising
57 57 partial pulls, it checks for a matching pull bundle in the manifest.
58 58 A bundle matches if the format is supported by the client, the client
59 59 has the required revisions already and needs something from the bundle.
60 60 4. If there is at least one matching bundle, the server sends it to the client.
61 61 5. The client applies the bundle and notices that the server reply was
62 62 incomplete. It initiates another pull.
63 63
64 64 To work, this extension requires the following of server operators:
65 65
66 66 * Generating bundle files of repository content (typically periodically,
67 67 such as once per day).
68 68 * Clone bundles: A file server that clients have network access to and that
69 69 Python knows how to talk to through its normal URL handling facility
70 70 (typically an HTTP/HTTPS server).
71 71 * A process for keeping the bundles manifest in sync with available bundle
72 72 files.
73 73
74 74 Strictly speaking, using a static file hosting server isn't required: a server
75 75 operator could use a dynamic service for retrieving bundle data. However,
76 76 static file hosting services are simple and scalable and should be sufficient
77 77 for most needs.
78 78
79 79 Bundle files can be generated with the :hg:`bundle` command. Typically
80 80 :hg:`bundle --all` is used to produce a bundle of the entire repository.
81 81
82 82 The bundlespec option `stream` (see :hg:`help bundlespec`)
83 83 can be used to produce a special *streaming clonebundle*, typically using
84 84 :hg:`bundle --all --type="none-streamv2"`.
85 85 These are bundle files that are extremely efficient
86 86 to produce and consume (read: fast). However, they are larger than
87 87 traditional bundle formats and require that clients support the exact set
88 88 of repository data store formats in use by the repository that created them.
89 89 Typically, a newer server can serve data that is compatible with older clients.
90 90 However, *streaming clone bundles* don't have this guarantee. **Server
91 91 operators need to be aware that newer versions of Mercurial may produce
92 92 streaming clone bundles incompatible with older Mercurial versions.**
93 93
94 94 A server operator is responsible for creating a ``.hg/clonebundles.manifest``
95 95 file containing the list of available bundle files suitable for seeding
96 96 clones. If this file does not exist, the repository will not advertise the
97 97 existence of clone bundles when clients connect. For pull bundles,
98 98 ``.hg/pullbundles.manifest`` is used.
99 99
100 100 The manifest file contains a newline (\\n) delimited list of entries.
101 101
102 102 Each line in this file defines an available bundle. Lines have the format:
103 103
104 104 <URL> [<key>=<value>[ <key>=<value>]]
105 105
106 106 That is, a URL followed by an optional, space-delimited list of key=value
107 107 pairs describing additional properties of this bundle. Both keys and values
108 108 are URI encoded.
109 109
110 110 For pull bundles, the URL is a path under the ``.hg`` directory of the
111 111 repository.
112 112
113 113 Keys in UPPERCASE are reserved for use by Mercurial and are defined below.
114 114 All non-uppercase keys can be used by site installations. An example use
115 115 for custom properties is to use the *datacenter* attribute to define which
116 116 data center a file is hosted in. Clients could then prefer a server in the
117 117 data center closest to them.
118 118
119 119 The following reserved keys are currently defined:
120 120
121 121 BUNDLESPEC
122 122 A "bundle specification" string that describes the type of the bundle.
123 123
124 124 These are string values that are accepted by the "--type" argument of
125 125 :hg:`bundle`.
126 126
127 127 The values are parsed in strict mode, which means they must be of the
128 128 "<compression>-<type>" form. See
129 129 mercurial.exchange.parsebundlespec() for more details.
130 130
131 131 :hg:`debugbundle --spec` can be used to print the bundle specification
132 132 string for a bundle file. The output of this command can be used verbatim
133 133 for the value of ``BUNDLESPEC`` (it is already escaped).
134 134
135 135 Clients will automatically filter out specifications that are unknown or
136 136 unsupported so they won't attempt to download something that likely won't
137 137 apply.
138 138
139 139 The actual value doesn't impact client behavior beyond filtering:
140 140 clients will still sniff the bundle type from the header of downloaded
141 141 files.
142 142
143 143 **Use of this key is highly recommended**, as it allows clients to
144 144 easily skip unsupported bundles. If this key is not defined, an old
145 145 client may attempt to apply a bundle that it is incapable of reading.
146 146
147 147 REQUIRESNI
148 148 Whether Server Name Indication (SNI) is required to connect to the URL.
149 149 SNI allows servers to use multiple certificates on the same IP. It is
150 150 somewhat common in CDNs and other hosting providers. Older Python
151 151 versions do not support SNI. Defining this attribute enables clients
152 152 with older Python versions to filter this entry without experiencing
153 153 an opaque SSL failure at connection time.
154 154
155 155 If this is defined, it is important to advertise a non-SNI fallback
156 156 URL or clients running old Python releases may not be able to clone
157 157 with the clonebundles facility.
158 158
159 159 Value should be "true".
160 160
161 161 REQUIREDRAM
162 162 Value specifies expected memory requirements to decode the payload.
163 163 Values can have suffixes for common bytes sizes. e.g. "64MB".
164 164
165 165 This key is often used with zstd-compressed bundles using a high
166 166 compression level / window size, which can require 100+ MB of memory
167 167 to decode.
168 168
169 169 heads
170 170 Used for pull bundles. This contains the ``;`` separated changeset
171 171 hashes of the heads of the bundle content.
172 172
173 173 bases
174 174 Used for pull bundles. This contains the ``;`` separated changeset
175 175 hashes of the roots of the bundle content. This can be skipped if
176 176 the bundle was created without ``--base``.
177 177
178 178 Manifests can contain multiple entries. Assuming metadata is defined, clients
179 179 will filter entries from the manifest that they don't support. The remaining
180 180 entries are optionally sorted by client preferences
181 181 (``ui.clonebundleprefers`` config option). The client then attempts
182 182 to fetch the bundle at the first URL in the remaining list.
183 183
184 184 **Errors when downloading a bundle will fail the entire clone operation:
185 185 clients do not automatically fall back to a traditional clone.** The reason
186 186 for this is that if a server is using clone bundles, it is probably doing so
187 187 because the feature is necessary to help it scale. In other words, there
188 188 is an assumption that clone load will be offloaded to another service and
189 189 that the Mercurial server isn't responsible for serving this clone load.
190 190 If that other service experiences issues and clients start mass falling back to
191 191 the original Mercurial server, the added clone load could overwhelm the server
192 192 due to unexpected load and effectively take it offline. Not having clients
193 193 automatically fall back to cloning from the original server mitigates this
194 194 scenario.
195 195
196 196 Because there is no automatic Mercurial server fallback on failure of the
197 197 bundle hosting service, it is important for server operators to view the bundle
198 198 hosting service as an extension of the Mercurial server in terms of
199 199 availability and service level agreements: if the bundle hosting service goes
200 200 down, so does the ability for clients to clone. Note: clients will see a
201 201 message informing them how to bypass the clone bundles facility when a failure
202 202 occurs. So server operators should prepare for some people to follow these
203 203 instructions when a failure occurs, thus driving more load to the original
204 204 Mercurial server when the bundle hosting service fails.
205 205
206 206
207 207 inline clonebundles
208 208 -------------------
209 209
210 210 It is possible to transmit clonebundles inline in case repositories are
211 211 accessed over SSH. This avoids having to setup an external HTTPS server
212 212 and results in the same access control as already present for the SSH setup.
213 213
214 214 Inline clonebundles should be placed into the `.hg/bundle-cache` directory.
215 215 A clonebundle at `.hg/bundle-cache/mybundle.bundle` is referred to
216 216 in the `clonebundles.manifest` file as `peer-bundle-cache://mybundle.bundle`.
217 217
218 218
219 219 auto-generation of clone bundles
220 220 --------------------------------
221 221
222 222 It is possible to set Mercurial to automatically re-generate clone bundles when
223 223 enough new content is available.
224 224
225 225 Mercurial will take care of the process asynchronously. The defined list of
226 226 bundle-type will be generated, uploaded, and advertised. Older bundles will get
227 227 decommissioned as newer ones replace them.
228 228
229 229 Bundles Generation:
230 230 ...................
231 231
232 232 The extension can generate multiple variants of the clone bundle. Each
233 233 different variant will be defined by the "bundle-spec" they use::
234 234
235 235 [clone-bundles]
236 236 auto-generate.formats= zstd-v2, gzip-v2
237 237
238 238 See `hg help bundlespec` for details about available options.
239 239
240 240 By default, new bundles are generated when 5% of the repository contents or at
241 241 least 1000 revisions are not contained in the cached bundles. This option can
242 242 be controlled by the `clone-bundles.trigger.below-bundled-ratio` option
243 243 (default 0.95) and the `clone-bundles.trigger.revs` option (default 1000)::
244 244
245 245 [clone-bundles]
246 246 trigger.below-bundled-ratio=0.95
247 247 trigger.revs=1000
248 248
249 249 This logic can be manually triggered using the `admin::clone-bundles-refresh`
250 250 command, or automatically on each repository change if
251 `clone-bundles.auto-generate.on-change` is set to `yes`.
251 `clone-bundles.auto-generate.on-change` is set to `yes`::
252 252
253 253 [clone-bundles]
254 254 auto-generate.on-change=yes
255 255 auto-generate.formats= zstd-v2, gzip-v2
256 256
257 257 Automatic Inline serving
258 258 ........................
259 259
260 260 The simplest way to serve the generated bundle is through the Mercurial
261 261 protocol. However it is not the most efficient as request will still be served
262 262 by that main server. It is useful in case where authentication is complexe or
263 263 when an efficient mirror system is already in use anyway. See the `inline
264 264 clonebundles` section above for details about inline clonebundles
265 265
266 266 To automatically serve generated bundle through inline clonebundle, simply set
267 267 the following option::
268 268
269 269 auto-generate.serve-inline=yes
270 270
271 271 Enabling this option disable the managed upload and serving explained below.
272 272
273 273 Bundles Upload and Serving:
274 274 ...........................
275 275
276 276 This is the most efficient way to serve automatically generated clone bundles,
277 277 but requires some setup.
278 278
279 279 The generated bundles need to be made available to users through a "public" URL.
280 280 This should be donne through `clone-bundles.upload-command` configuration. The
281 281 value of this command should be a shell command. It will have access to the
282 282 bundle file path through the `$HGCB_BUNDLE_PATH` variable. And the expected
283 283 basename in the "public" URL is accessible at::
284 284
285 285 [clone-bundles]
286 286 upload-command=sftp put $HGCB_BUNDLE_PATH \
287 287 sftp://bundles.host/clone-bundles/$HGCB_BUNDLE_BASENAME
288 288
289 289 If the file was already uploaded, the command must still succeed.
290 290
291 291 After upload, the file should be available at an url defined by
292 292 `clone-bundles.url-template`.
293 293
294 294 [clone-bundles]
295 295 url-template=https://bundles.host/cache/clone-bundles/{basename}
296 296
297 297 Old bundles cleanup:
298 298 ....................
299 299
300 300 When new bundles are generated, the older ones are no longer necessary and can
301 301 be removed from storage. This is done through the `clone-bundles.delete-command`
302 302 configuration. The command is given the url of the artifact to delete through
303 303 the `$HGCB_BUNDLE_URL` environment variable.
304 304
305 305 [clone-bundles]
306 306 delete-command=sftp rm sftp://bundles.host/clone-bundles/$HGCB_BUNDLE_BASENAME
307 307
308 308 If the file was already deleted, the command must still succeed.
309 309 """
310 310
311 311
312 312 import os
313 313 import weakref
314 314
315 315 from mercurial.i18n import _
316 316
317 317 from mercurial import (
318 318 bundlecaches,
319 319 commands,
320 320 error,
321 321 extensions,
322 322 localrepo,
323 323 lock,
324 324 node,
325 325 registrar,
326 326 util,
327 327 wireprotov1server,
328 328 )
329 329
330 330
331 331 from mercurial.utils import (
332 332 procutil,
333 333 )
334 334
335 335 testedwith = b'ships-with-hg-core'
336 336
337 337
338 338 def capabilities(orig, repo, proto):
339 339 caps = orig(repo, proto)
340 340
341 341 # Only advertise if a manifest exists. This does add some I/O to requests.
342 342 # But this should be cheaper than a wasted network round trip due to
343 343 # missing file.
344 344 if repo.vfs.exists(bundlecaches.CB_MANIFEST_FILE):
345 345 caps.append(b'clonebundles')
346 346 caps.append(b'clonebundles_manifest')
347 347
348 348 return caps
349 349
350 350
351 351 def extsetup(ui):
352 352 extensions.wrapfunction(wireprotov1server, b'_capabilities', capabilities)
353 353
354 354
355 355 # logic for bundle auto-generation
356 356
357 357
358 358 configtable = {}
359 359 configitem = registrar.configitem(configtable)
360 360
361 361 cmdtable = {}
362 362 command = registrar.command(cmdtable)
363 363
364 364 configitem(b'clone-bundles', b'auto-generate.on-change', default=False)
365 365 configitem(b'clone-bundles', b'auto-generate.formats', default=list)
366 366 configitem(b'clone-bundles', b'auto-generate.serve-inline', default=False)
367 367 configitem(b'clone-bundles', b'trigger.below-bundled-ratio', default=0.95)
368 368 configitem(b'clone-bundles', b'trigger.revs', default=1000)
369 369
370 370 configitem(b'clone-bundles', b'upload-command', default=None)
371 371
372 372 configitem(b'clone-bundles', b'delete-command', default=None)
373 373
374 374 configitem(b'clone-bundles', b'url-template', default=None)
375 375
376 376 configitem(b'devel', b'debug.clonebundles', default=False)
377 377
378 378
379 379 # category for the post-close transaction hooks
380 380 CAT_POSTCLOSE = b"clonebundles-autobundles"
381 381
382 382 # template for bundle file names
383 383 BUNDLE_MASK = (
384 384 b"full-%(bundle_type)s-%(revs)d_revs-%(tip_short)s_tip-%(op_id)s.hg"
385 385 )
386 386
387 387
388 388 # file in .hg/ use to track clonebundles being auto-generated
389 389 AUTO_GEN_FILE = b'clonebundles.auto-gen'
390 390
391 391
392 392 class BundleBase(object):
393 393 """represents the core of properties that matters for us in a bundle
394 394
395 395 :bundle_type: the bundlespec (see hg help bundlespec)
396 396 :revs: the number of revisions in the repo at bundle creation time
397 397 :tip_rev: the rev-num of the tip revision
398 398 :tip_node: the node id of the tip-most revision in the bundle
399 399
400 400 :ready: True if the bundle is ready to be served
401 401 """
402 402
403 403 ready = False
404 404
405 405 def __init__(self, bundle_type, revs, tip_rev, tip_node):
406 406 self.bundle_type = bundle_type
407 407 self.revs = revs
408 408 self.tip_rev = tip_rev
409 409 self.tip_node = tip_node
410 410
411 411 def valid_for(self, repo):
412 412 """is this bundle applicable to the current repository
413 413
414 414 This is useful for detecting bundles made irrelevant by stripping.
415 415 """
416 416 tip_node = node.bin(self.tip_node)
417 417 return repo.changelog.index.get_rev(tip_node) == self.tip_rev
418 418
419 419 def __eq__(self, other):
420 420 left = (self.ready, self.bundle_type, self.tip_rev, self.tip_node)
421 421 right = (other.ready, other.bundle_type, other.tip_rev, other.tip_node)
422 422 return left == right
423 423
424 424 def __neq__(self, other):
425 425 return not self == other
426 426
427 427 def __cmp__(self, other):
428 428 if self == other:
429 429 return 0
430 430 return -1
431 431
432 432
433 433 class RequestedBundle(BundleBase):
434 434 """A bundle that should be generated.
435 435
436 436 Additional attributes compared to BundleBase
437 437 :heads: list of head revisions (as rev-num)
438 438 :op_id: a "unique" identifier for the operation triggering the change
439 439 """
440 440
441 441 def __init__(self, bundle_type, revs, tip_rev, tip_node, head_revs, op_id):
442 442 self.head_revs = head_revs
443 443 self.op_id = op_id
444 444 super(RequestedBundle, self).__init__(
445 445 bundle_type,
446 446 revs,
447 447 tip_rev,
448 448 tip_node,
449 449 )
450 450
451 451 @property
452 452 def suggested_filename(self):
453 453 """A filename that can be used for the generated bundle"""
454 454 data = {
455 455 b'bundle_type': self.bundle_type,
456 456 b'revs': self.revs,
457 457 b'heads': self.head_revs,
458 458 b'tip_rev': self.tip_rev,
459 459 b'tip_node': self.tip_node,
460 460 b'tip_short': self.tip_node[:12],
461 461 b'op_id': self.op_id,
462 462 }
463 463 return BUNDLE_MASK % data
464 464
465 465 def generate_bundle(self, repo, file_path):
466 466 """generate the bundle at `filepath`"""
467 467 commands.bundle(
468 468 repo.ui,
469 469 repo,
470 470 file_path,
471 471 base=[b"null"],
472 472 rev=self.head_revs,
473 473 type=self.bundle_type,
474 474 quiet=True,
475 475 )
476 476
477 477 def generating(self, file_path, hostname=None, pid=None):
478 478 """return a GeneratingBundle object from this object"""
479 479 if pid is None:
480 480 pid = os.getpid()
481 481 if hostname is None:
482 482 hostname = lock._getlockprefix()
483 483 return GeneratingBundle(
484 484 self.bundle_type,
485 485 self.revs,
486 486 self.tip_rev,
487 487 self.tip_node,
488 488 hostname,
489 489 pid,
490 490 file_path,
491 491 )
492 492
493 493
494 494 class GeneratingBundle(BundleBase):
495 495 """A bundle being generated
496 496
497 497 extra attributes compared to BundleBase:
498 498
499 499 :hostname: the hostname of the machine generating the bundle
500 500 :pid: the pid of the process generating the bundle
501 501 :filepath: the target filename of the bundle
502 502
503 503 These attributes exist to help detect stalled generation processes.
504 504 """
505 505
506 506 ready = False
507 507
508 508 def __init__(
509 509 self, bundle_type, revs, tip_rev, tip_node, hostname, pid, filepath
510 510 ):
511 511 self.hostname = hostname
512 512 self.pid = pid
513 513 self.filepath = filepath
514 514 super(GeneratingBundle, self).__init__(
515 515 bundle_type, revs, tip_rev, tip_node
516 516 )
517 517
518 518 @classmethod
519 519 def from_line(cls, line):
520 520 """create an object by deserializing a line from AUTO_GEN_FILE"""
521 521 assert line.startswith(b'PENDING-v1 ')
522 522 (
523 523 __,
524 524 bundle_type,
525 525 revs,
526 526 tip_rev,
527 527 tip_node,
528 528 hostname,
529 529 pid,
530 530 filepath,
531 531 ) = line.split()
532 532 hostname = util.urlreq.unquote(hostname)
533 533 filepath = util.urlreq.unquote(filepath)
534 534 revs = int(revs)
535 535 tip_rev = int(tip_rev)
536 536 pid = int(pid)
537 537 return cls(
538 538 bundle_type, revs, tip_rev, tip_node, hostname, pid, filepath
539 539 )
540 540
541 541 def to_line(self):
542 542 """serialize the object to include as a line in AUTO_GEN_FILE"""
543 543 templ = b"PENDING-v1 %s %d %d %s %s %d %s"
544 544 data = (
545 545 self.bundle_type,
546 546 self.revs,
547 547 self.tip_rev,
548 548 self.tip_node,
549 549 util.urlreq.quote(self.hostname),
550 550 self.pid,
551 551 util.urlreq.quote(self.filepath),
552 552 )
553 553 return templ % data
554 554
555 555 def __eq__(self, other):
556 556 if not super(GeneratingBundle, self).__eq__(other):
557 557 return False
558 558 left = (self.hostname, self.pid, self.filepath)
559 559 right = (other.hostname, other.pid, other.filepath)
560 560 return left == right
561 561
562 562 def uploaded(self, url, basename):
563 563 """return a GeneratedBundle from this object"""
564 564 return GeneratedBundle(
565 565 self.bundle_type,
566 566 self.revs,
567 567 self.tip_rev,
568 568 self.tip_node,
569 569 url,
570 570 basename,
571 571 )
572 572
573 573
574 574 class GeneratedBundle(BundleBase):
575 575 """A bundle that is done being generated and can be served
576 576
577 577 extra attributes compared to BundleBase:
578 578
579 579 :file_url: the url where the bundle is available.
580 580 :basename: the "basename" used to upload (useful for deletion)
581 581
582 582 These attributes exist to generate a bundle manifest
583 583 (.hg/pullbundles.manifest)
584 584 """
585 585
586 586 ready = True
587 587
588 588 def __init__(
589 589 self, bundle_type, revs, tip_rev, tip_node, file_url, basename
590 590 ):
591 591 self.file_url = file_url
592 592 self.basename = basename
593 593 super(GeneratedBundle, self).__init__(
594 594 bundle_type, revs, tip_rev, tip_node
595 595 )
596 596
597 597 @classmethod
598 598 def from_line(cls, line):
599 599 """create an object by deserializing a line from AUTO_GEN_FILE"""
600 600 assert line.startswith(b'DONE-v1 ')
601 601 (
602 602 __,
603 603 bundle_type,
604 604 revs,
605 605 tip_rev,
606 606 tip_node,
607 607 file_url,
608 608 basename,
609 609 ) = line.split()
610 610 revs = int(revs)
611 611 tip_rev = int(tip_rev)
612 612 file_url = util.urlreq.unquote(file_url)
613 613 return cls(bundle_type, revs, tip_rev, tip_node, file_url, basename)
614 614
615 615 def to_line(self):
616 616 """serialize the object to include as a line in AUTO_GEN_FILE"""
617 617 templ = b"DONE-v1 %s %d %d %s %s %s"
618 618 data = (
619 619 self.bundle_type,
620 620 self.revs,
621 621 self.tip_rev,
622 622 self.tip_node,
623 623 util.urlreq.quote(self.file_url),
624 624 self.basename,
625 625 )
626 626 return templ % data
627 627
628 628 def manifest_line(self):
629 629 """serialize the object to include as a line in pullbundles.manifest"""
630 630 templ = b"%s BUNDLESPEC=%s"
631 631 if self.file_url.startswith(b'http'):
632 632 templ += b" REQUIRESNI=true"
633 633 return templ % (self.file_url, self.bundle_type)
634 634
635 635 def __eq__(self, other):
636 636 if not super(GeneratedBundle, self).__eq__(other):
637 637 return False
638 638 return self.file_url == other.file_url
639 639
640 640
641 641 def parse_auto_gen(content):
642 642 """parse the AUTO_GEN_FILE to return a list of Bundle object"""
643 643 bundles = []
644 644 for line in content.splitlines():
645 645 if line.startswith(b'PENDING-v1 '):
646 646 bundles.append(GeneratingBundle.from_line(line))
647 647 elif line.startswith(b'DONE-v1 '):
648 648 bundles.append(GeneratedBundle.from_line(line))
649 649 return bundles
650 650
651 651
652 652 def dumps_auto_gen(bundles):
653 653 """serialize a list of Bundle as a AUTO_GEN_FILE content"""
654 654 lines = []
655 655 for b in bundles:
656 656 lines.append(b"%s\n" % b.to_line())
657 657 lines.sort()
658 658 return b"".join(lines)
659 659
660 660
661 661 def read_auto_gen(repo):
662 662 """read the AUTO_GEN_FILE for the <repo> a list of Bundle object"""
663 663 data = repo.vfs.tryread(AUTO_GEN_FILE)
664 664 if not data:
665 665 return []
666 666 return parse_auto_gen(data)
667 667
668 668
669 669 def write_auto_gen(repo, bundles):
670 670 """write a list of Bundle objects into the repo's AUTO_GEN_FILE"""
671 671 assert repo._cb_lock_ref is not None
672 672 data = dumps_auto_gen(bundles)
673 673 with repo.vfs(AUTO_GEN_FILE, mode=b'wb', atomictemp=True) as f:
674 674 f.write(data)
675 675
676 676
677 677 def generate_manifest(bundles):
678 678 """write a list of Bundle objects into the repo's AUTO_GEN_FILE"""
679 679 bundles = list(bundles)
680 680 bundles.sort(key=lambda b: b.bundle_type)
681 681 lines = []
682 682 for b in bundles:
683 683 lines.append(b"%s\n" % b.manifest_line())
684 684 return b"".join(lines)
685 685
686 686
687 687 def update_ondisk_manifest(repo):
688 688 """update the clonebundle manifest with latest url"""
689 689 with repo.clonebundles_lock():
690 690 bundles = read_auto_gen(repo)
691 691
692 692 per_types = {}
693 693 for b in bundles:
694 694 if not (b.ready and b.valid_for(repo)):
695 695 continue
696 696 current = per_types.get(b.bundle_type)
697 697 if current is not None and current.revs >= b.revs:
698 698 continue
699 699 per_types[b.bundle_type] = b
700 700 manifest = generate_manifest(per_types.values())
701 701 with repo.vfs(
702 702 bundlecaches.CB_MANIFEST_FILE, mode=b"wb", atomictemp=True
703 703 ) as f:
704 704 f.write(manifest)
705 705
706 706
707 707 def update_bundle_list(repo, new_bundles=(), del_bundles=()):
708 708 """modify the repo's AUTO_GEN_FILE
709 709
710 710 This method also regenerates the clone bundle manifest when needed"""
711 711 with repo.clonebundles_lock():
712 712 bundles = read_auto_gen(repo)
713 713 if del_bundles:
714 714 bundles = [b for b in bundles if b not in del_bundles]
715 715 new_bundles = [b for b in new_bundles if b not in bundles]
716 716 bundles.extend(new_bundles)
717 717 write_auto_gen(repo, bundles)
718 718 all_changed = []
719 719 all_changed.extend(new_bundles)
720 720 all_changed.extend(del_bundles)
721 721 if any(b.ready for b in all_changed):
722 722 update_ondisk_manifest(repo)
723 723
724 724
725 725 def cleanup_tmp_bundle(repo, target):
726 726 """remove a GeneratingBundle file and entry"""
727 727 assert not target.ready
728 728 with repo.clonebundles_lock():
729 729 repo.vfs.tryunlink(target.filepath)
730 730 update_bundle_list(repo, del_bundles=[target])
731 731
732 732
733 733 def finalize_one_bundle(repo, target):
734 734 """upload a generated bundle and advertise it in the clonebundles.manifest"""
735 735 with repo.clonebundles_lock():
736 736 bundles = read_auto_gen(repo)
737 737 if target in bundles and target.valid_for(repo):
738 738 result = upload_bundle(repo, target)
739 739 update_bundle_list(repo, new_bundles=[result])
740 740 cleanup_tmp_bundle(repo, target)
741 741
742 742
743 743 def find_outdated_bundles(repo, bundles):
744 744 """finds outdated bundles"""
745 745 olds = []
746 746 per_types = {}
747 747 for b in bundles:
748 748 if not b.valid_for(repo):
749 749 olds.append(b)
750 750 continue
751 751 l = per_types.setdefault(b.bundle_type, [])
752 752 l.append(b)
753 753 for key in sorted(per_types):
754 754 all = per_types[key]
755 755 if len(all) > 1:
756 756 all.sort(key=lambda b: b.revs, reverse=True)
757 757 olds.extend(all[1:])
758 758 return olds
759 759
760 760
761 761 def collect_garbage(repo):
762 762 """finds outdated bundles and get them deleted"""
763 763 with repo.clonebundles_lock():
764 764 bundles = read_auto_gen(repo)
765 765 olds = find_outdated_bundles(repo, bundles)
766 766 for o in olds:
767 767 delete_bundle(repo, o)
768 768 update_bundle_list(repo, del_bundles=olds)
769 769
770 770
771 771 def upload_bundle(repo, bundle):
772 772 """upload the result of a GeneratingBundle and return a GeneratedBundle
773 773
774 774 The upload is done using the `clone-bundles.upload-command`
775 775 """
776 776 inline = repo.ui.config(b'clone-bundles', b'auto-generate.serve-inline')
777 777 basename = repo.vfs.basename(bundle.filepath)
778 778 if inline:
779 779 dest_dir = repo.vfs.join(bundlecaches.BUNDLE_CACHE_DIR)
780 780 repo.vfs.makedirs(dest_dir)
781 781 dest = repo.vfs.join(dest_dir, basename)
782 782 util.copyfiles(bundle.filepath, dest, hardlink=True)
783 783 url = bundlecaches.CLONEBUNDLESCHEME + basename
784 784 return bundle.uploaded(url, basename)
785 785 else:
786 786 cmd = repo.ui.config(b'clone-bundles', b'upload-command')
787 787 url = repo.ui.config(b'clone-bundles', b'url-template')
788 788 filepath = procutil.shellquote(bundle.filepath)
789 789 variables = {
790 790 b'HGCB_BUNDLE_PATH': filepath,
791 791 b'HGCB_BUNDLE_BASENAME': basename,
792 792 }
793 793 env = procutil.shellenviron(environ=variables)
794 794 ret = repo.ui.system(cmd, environ=env)
795 795 if ret:
796 796 raise error.Abort(b"command returned status %d: %s" % (ret, cmd))
797 797 url = (
798 798 url.decode('utf8')
799 799 .format(basename=basename.decode('utf8'))
800 800 .encode('utf8')
801 801 )
802 802 return bundle.uploaded(url, basename)
803 803
804 804
805 805 def delete_bundle(repo, bundle):
806 806 """delete a bundle from storage"""
807 807 assert bundle.ready
808 808
809 809 inline = bundle.file_url.startswith(bundlecaches.CLONEBUNDLESCHEME)
810 810
811 811 if inline:
812 812 msg = b'clone-bundles: deleting inline bundle %s\n'
813 813 else:
814 814 msg = b'clone-bundles: deleting bundle %s\n'
815 815 msg %= bundle.basename
816 816 if repo.ui.configbool(b'devel', b'debug.clonebundles'):
817 817 repo.ui.write(msg)
818 818 else:
819 819 repo.ui.debug(msg)
820 820
821 821 if inline:
822 822 inline_path = repo.vfs.join(
823 823 bundlecaches.BUNDLE_CACHE_DIR,
824 824 bundle.basename,
825 825 )
826 826 util.tryunlink(inline_path)
827 827 else:
828 828 cmd = repo.ui.config(b'clone-bundles', b'delete-command')
829 829 variables = {
830 830 b'HGCB_BUNDLE_URL': bundle.file_url,
831 831 b'HGCB_BASENAME': bundle.basename,
832 832 }
833 833 env = procutil.shellenviron(environ=variables)
834 834 ret = repo.ui.system(cmd, environ=env)
835 835 if ret:
836 836 raise error.Abort(b"command returned status %d: %s" % (ret, cmd))
837 837
838 838
839 839 def auto_bundle_needed_actions(repo, bundles, op_id):
840 840 """find the list of bundles that need action
841 841
842 842 returns a list of RequestedBundle objects that need to be generated and
843 843 uploaded."""
844 844 create_bundles = []
845 845 delete_bundles = []
846 846 repo = repo.filtered(b"immutable")
847 847 targets = repo.ui.configlist(b'clone-bundles', b'auto-generate.formats')
848 848 ratio = float(
849 849 repo.ui.config(b'clone-bundles', b'trigger.below-bundled-ratio')
850 850 )
851 851 abs_revs = repo.ui.configint(b'clone-bundles', b'trigger.revs')
852 852 revs = len(repo.changelog)
853 853 generic_data = {
854 854 'revs': revs,
855 855 'head_revs': repo.changelog.headrevs(),
856 856 'tip_rev': repo.changelog.tiprev(),
857 857 'tip_node': node.hex(repo.changelog.tip()),
858 858 'op_id': op_id,
859 859 }
860 860 for t in targets:
861 861 t = bundlecaches.parsebundlespec(repo, t, strict=False).as_spec()
862 862 if new_bundle_needed(repo, bundles, ratio, abs_revs, t, revs):
863 863 data = generic_data.copy()
864 864 data['bundle_type'] = t
865 865 b = RequestedBundle(**data)
866 866 create_bundles.append(b)
867 867 delete_bundles.extend(find_outdated_bundles(repo, bundles))
868 868 return create_bundles, delete_bundles
869 869
870 870
871 871 def new_bundle_needed(repo, bundles, ratio, abs_revs, bundle_type, revs):
872 872 """consider the current cached content and trigger new bundles if needed"""
873 873 threshold = max((revs * ratio), (revs - abs_revs))
874 874 for b in bundles:
875 875 if not b.valid_for(repo) or b.bundle_type != bundle_type:
876 876 continue
877 877 if b.revs > threshold:
878 878 return False
879 879 return True
880 880
881 881
882 882 def start_one_bundle(repo, bundle):
883 883 """start the generation of a single bundle file
884 884
885 885 the `bundle` argument should be a RequestedBundle object.
886 886
887 887 This data is passed to the `debugmakeclonebundles` "as is".
888 888 """
889 889 data = util.pickle.dumps(bundle)
890 890 cmd = [procutil.hgexecutable(), b'--cwd', repo.path, INTERNAL_CMD]
891 891 env = procutil.shellenviron()
892 892 msg = b'clone-bundles: starting bundle generation: %s\n'
893 893 stdout = None
894 894 stderr = None
895 895 waits = []
896 896 record_wait = None
897 897 if repo.ui.configbool(b'devel', b'debug.clonebundles'):
898 898 stdout = procutil.stdout
899 899 stderr = procutil.stderr
900 900 repo.ui.write(msg % bundle.bundle_type)
901 901 record_wait = waits.append
902 902 else:
903 903 repo.ui.debug(msg % bundle.bundle_type)
904 904 bg = procutil.runbgcommand
905 905 bg(
906 906 cmd,
907 907 env,
908 908 stdin_bytes=data,
909 909 stdout=stdout,
910 910 stderr=stderr,
911 911 record_wait=record_wait,
912 912 )
913 913 for f in waits:
914 914 f()
915 915
916 916
917 917 INTERNAL_CMD = b'debug::internal-make-clone-bundles'
918 918
919 919
920 920 @command(INTERNAL_CMD, [], b'')
921 921 def debugmakeclonebundles(ui, repo):
922 922 """Internal command to auto-generate debug bundles"""
923 923 requested_bundle = util.pickle.load(procutil.stdin)
924 924 procutil.stdin.close()
925 925
926 926 collect_garbage(repo)
927 927
928 928 fname = requested_bundle.suggested_filename
929 929 fpath = repo.vfs.makedirs(b'tmp-bundles')
930 930 fpath = repo.vfs.join(b'tmp-bundles', fname)
931 931 bundle = requested_bundle.generating(fpath)
932 932 update_bundle_list(repo, new_bundles=[bundle])
933 933
934 934 requested_bundle.generate_bundle(repo, fpath)
935 935
936 936 repo.invalidate()
937 937 finalize_one_bundle(repo, bundle)
938 938
939 939
940 940 def make_auto_bundler(source_repo):
941 941 reporef = weakref.ref(source_repo)
942 942
943 943 def autobundle(tr):
944 944 repo = reporef()
945 945 assert repo is not None
946 946 bundles = read_auto_gen(repo)
947 947 new, __ = auto_bundle_needed_actions(repo, bundles, b"%d_txn" % id(tr))
948 948 for data in new:
949 949 start_one_bundle(repo, data)
950 950 return None
951 951
952 952 return autobundle
953 953
954 954
955 955 def reposetup(ui, repo):
956 956 """install the two pieces needed for automatic clonebundle generation
957 957
958 958 - add a "post-close" hook that fires bundling when needed
959 959 - introduce a clone-bundle lock to let multiple processes meddle with the
960 960 state files.
961 961 """
962 962 if not repo.local():
963 963 return
964 964
965 965 class autobundlesrepo(repo.__class__):
966 966 def transaction(self, *args, **kwargs):
967 967 tr = super(autobundlesrepo, self).transaction(*args, **kwargs)
968 968 enabled = repo.ui.configbool(
969 969 b'clone-bundles',
970 970 b'auto-generate.on-change',
971 971 )
972 972 targets = repo.ui.configlist(
973 973 b'clone-bundles', b'auto-generate.formats'
974 974 )
975 if enabled and targets:
975 if enabled:
976 if not targets:
977 repo.ui.warn(
978 _(
979 b'clone-bundle auto-generate enabled, '
980 b'but no formats specified: disabling generation\n'
981 )
982 )
983 else:
976 984 tr.addpostclose(CAT_POSTCLOSE, make_auto_bundler(self))
977 985 return tr
978 986
979 987 @localrepo.unfilteredmethod
980 988 def clonebundles_lock(self, wait=True):
981 989 '''Lock the repository file related to clone bundles'''
982 990 if not util.safehasattr(self, '_cb_lock_ref'):
983 991 self._cb_lock_ref = None
984 992 l = self._currentlock(self._cb_lock_ref)
985 993 if l is not None:
986 994 l.lock()
987 995 return l
988 996
989 997 l = self._lock(
990 998 vfs=self.vfs,
991 999 lockname=b"clonebundleslock",
992 1000 wait=wait,
993 1001 releasefn=None,
994 1002 acquirefn=None,
995 1003 desc=_(b'repository %s') % self.origroot,
996 1004 )
997 1005 self._cb_lock_ref = weakref.ref(l)
998 1006 return l
999 1007
1000 1008 repo._wlockfreeprefix.add(AUTO_GEN_FILE)
1001 1009 repo._wlockfreeprefix.add(bundlecaches.CB_MANIFEST_FILE)
1002 1010 repo.__class__ = autobundlesrepo
1003 1011
1004 1012
1005 1013 @command(
1006 1014 b'admin::clone-bundles-refresh',
1007 1015 [
1008 1016 (
1009 1017 b'',
1010 1018 b'background',
1011 1019 False,
1012 1020 _(b'start bundle generation in the background'),
1013 1021 ),
1014 1022 ],
1015 1023 b'',
1016 1024 )
1017 1025 def cmd_admin_clone_bundles_refresh(
1018 1026 ui,
1019 1027 repo: localrepo.localrepository,
1020 1028 background=False,
1021 1029 ):
1022 1030 """generate clone bundles according to the configuration
1023 1031
1024 1032 This runs the logic for automatic generation, removing outdated bundles and
1025 1033 generating new ones if necessary. See :hg:`help -e clone-bundles` for
1026 1034 details about how to configure this feature.
1027 1035 """
1028 1036 debug = repo.ui.configbool(b'devel', b'debug.clonebundles')
1029 1037 bundles = read_auto_gen(repo)
1030 1038 op_id = b"%d_acbr" % os.getpid()
1031 1039 create, delete = auto_bundle_needed_actions(repo, bundles, op_id)
1032 1040
1033 1041 # if some bundles are scheduled for creation in the background, they will
1034 1042 # deal with garbage collection too, so no need to synchroniously do it.
1035 1043 #
1036 1044 # However if no bundles are scheduled for creation, we need to explicitly do
1037 1045 # it here.
1038 1046 if not (background and create):
1039 1047 # we clean up outdated bundles before generating new ones to keep the
1040 1048 # last two versions of the bundle around for a while and avoid having to
1041 1049 # deal with clients that just got served a manifest.
1042 1050 for o in delete:
1043 1051 delete_bundle(repo, o)
1044 1052 update_bundle_list(repo, del_bundles=delete)
1045 1053
1046 1054 if create:
1047 1055 fpath = repo.vfs.makedirs(b'tmp-bundles')
1048 1056
1049 1057 if background:
1050 1058 for requested_bundle in create:
1051 1059 start_one_bundle(repo, requested_bundle)
1052 1060 else:
1053 1061 for requested_bundle in create:
1054 1062 if debug:
1055 1063 msg = b'clone-bundles: starting bundle generation: %s\n'
1056 1064 repo.ui.write(msg % requested_bundle.bundle_type)
1057 1065 fname = requested_bundle.suggested_filename
1058 1066 fpath = repo.vfs.join(b'tmp-bundles', fname)
1059 1067 generating_bundle = requested_bundle.generating(fpath)
1060 1068 update_bundle_list(repo, new_bundles=[generating_bundle])
1061 1069 requested_bundle.generate_bundle(repo, fpath)
1062 1070 result = upload_bundle(repo, generating_bundle)
1063 1071 update_bundle_list(repo, new_bundles=[result])
1064 1072 update_ondisk_manifest(repo)
1065 1073 cleanup_tmp_bundle(repo, generating_bundle)
1066 1074
1067 1075
1068 1076 @command(b'admin::clone-bundles-clear', [], b'')
1069 1077 def cmd_admin_clone_bundles_clear(ui, repo: localrepo.localrepository):
1070 1078 """remove existing clone bundle caches
1071 1079
1072 1080 See `hg help admin::clone-bundles-refresh` for details on how to regenerate
1073 1081 them.
1074 1082
1075 1083 This command will only affect bundles currently available, it will not
1076 1084 affect bundles being asynchronously generated.
1077 1085 """
1078 1086 bundles = read_auto_gen(repo)
1079 1087 delete = [b for b in bundles if b.ready]
1080 1088 for o in delete:
1081 1089 delete_bundle(repo, o)
1082 1090 update_bundle_list(repo, del_bundles=delete)
@@ -1,222 +1,222 b''
1 1 # win32mbcs.py -- MBCS filename support for Mercurial
2 2 #
3 3 # Copyright (c) 2008 Shun-ichi Goto <shunichi.goto@gmail.com>
4 4 #
5 5 # Version: 0.3
6 6 # Author: Shun-ichi Goto <shunichi.goto@gmail.com>
7 7 #
8 8 # This software may be used and distributed according to the terms of the
9 9 # GNU General Public License version 2 or any later version.
10 10 #
11 11
12 12 '''allow the use of MBCS paths with problematic encodings
13 13
14 14 Some MBCS encodings are not good for some path operations (i.e.
15 15 splitting path, case conversion, etc.) with its encoded bytes. We call
16 16 such a encoding (i.e. shift_jis and big5) as "problematic encoding".
17 17 This extension can be used to fix the issue with those encodings by
18 18 wrapping some functions to convert to Unicode string before path
19 19 operation.
20 20
21 21 This extension is useful for:
22 22
23 23 - Japanese Windows users using shift_jis encoding.
24 24 - Chinese Windows users using big5 encoding.
25 25 - All users who use a repository with one of problematic encodings on
26 26 case-insensitive file system.
27 27
28 28 This extension is not needed for:
29 29
30 30 - Any user who use only ASCII chars in path.
31 31 - Any user who do not use any of problematic encodings.
32 32
33 33 Note that there are some limitations on using this extension:
34 34
35 35 - You should use single encoding in one repository.
36 36 - If the repository path ends with 0x5c, .hg/hgrc cannot be read.
37 37 - win32mbcs is not compatible with fixutf8 extension.
38 38
39 39 By default, win32mbcs uses encoding.encoding decided by Mercurial.
40 40 You can specify the encoding by config option::
41 41
42 42 [win32mbcs]
43 43 encoding = sjis
44 44
45 45 It is useful for the users who want to commit with UTF-8 log message.
46 46 '''
47 47
48 48 import os
49 49 import sys
50 50
51 51 from mercurial.i18n import _
52 52 from mercurial.pycompat import getattr, setattr
53 53 from mercurial import (
54 54 encoding,
55 55 error,
56 56 pycompat,
57 57 registrar,
58 58 )
59 59
60 60 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
61 61 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
62 62 # be specifying the version(s) of Mercurial they are tested with, or
63 63 # leave the attribute unspecified.
64 64 testedwith = b'ships-with-hg-core'
65 65
66 66 configtable = {}
67 67 configitem = registrar.configitem(configtable)
68 68
69 69 # Encoding.encoding may be updated by --encoding option.
70 70 # Use a lambda do delay the resolution.
71 71 configitem(
72 72 b'win32mbcs',
73 73 b'encoding',
74 74 default=lambda: encoding.encoding,
75 75 )
76 76
77 77 _encoding = None # see extsetup
78 78
79 79
80 80 def decode(arg):
81 81 if isinstance(arg, bytes):
82 82 uarg = arg.decode(_encoding)
83 83 if arg == uarg.encode(_encoding):
84 84 return uarg
85 raise UnicodeError(b"Not local encoding")
85 raise UnicodeError("Not local encoding")
86 86 elif isinstance(arg, tuple):
87 87 return tuple(map(decode, arg))
88 88 elif isinstance(arg, list):
89 89 return map(decode, arg)
90 90 elif isinstance(arg, dict):
91 91 for k, v in arg.items():
92 92 arg[k] = decode(v)
93 93 return arg
94 94
95 95
96 96 def encode(arg):
97 97 if isinstance(arg, str):
98 98 return arg.encode(_encoding)
99 99 elif isinstance(arg, tuple):
100 100 return tuple(map(encode, arg))
101 101 elif isinstance(arg, list):
102 102 return map(encode, arg)
103 103 elif isinstance(arg, dict):
104 104 for k, v in arg.items():
105 105 arg[k] = encode(v)
106 106 return arg
107 107
108 108
109 109 def appendsep(s):
110 110 # ensure the path ends with os.sep, appending it if necessary.
111 111 try:
112 112 us = decode(s)
113 113 except UnicodeError:
114 us = s
115 if us and us[-1] not in b':/\\':
114 us = s # TODO: how to handle this bytes case??
115 if us and us[-1] not in ':/\\':
116 116 s += pycompat.ossep
117 117 return s
118 118
119 119
120 120 def basewrapper(func, argtype, enc, dec, args, kwds):
121 121 # check check already converted, then call original
122 122 for arg in args:
123 123 if isinstance(arg, argtype):
124 124 return func(*args, **kwds)
125 125
126 126 try:
127 127 # convert string arguments, call func, then convert back the
128 128 # return value.
129 129 return enc(func(*dec(args), **dec(kwds)))
130 130 except UnicodeError:
131 131 raise error.Abort(
132 132 _(b"[win32mbcs] filename conversion failed with %s encoding\n")
133 133 % _encoding
134 134 )
135 135
136 136
137 137 def wrapper(func, args, kwds):
138 138 return basewrapper(func, str, encode, decode, args, kwds)
139 139
140 140
141 141 def reversewrapper(func, args, kwds):
142 142 return basewrapper(func, str, decode, encode, args, kwds)
143 143
144 144
145 145 def wrapperforlistdir(func, args, kwds):
146 146 # Ensure 'path' argument ends with os.sep to avoids
147 147 # misinterpreting last 0x5c of MBCS 2nd byte as path separator.
148 148 if args:
149 149 args = list(args)
150 150 args[0] = appendsep(args[0])
151 if b'path' in kwds:
152 kwds[b'path'] = appendsep(kwds[b'path'])
151 if 'path' in kwds:
152 kwds['path'] = appendsep(kwds['path'])
153 153 return func(*args, **kwds)
154 154
155 155
156 def wrapname(name, wrapper):
157 module, name = name.rsplit(b'.', 1)
156 def wrapname(name: str, wrapper):
157 module, name = name.rsplit('.', 1)
158 158 module = sys.modules[module]
159 159 func = getattr(module, name)
160 160
161 161 def f(*args, **kwds):
162 162 return wrapper(func, args, kwds)
163 163
164 164 f.__name__ = func.__name__
165 165 setattr(module, name, f)
166 166
167 167
168 168 # List of functions to be wrapped.
169 169 # NOTE: os.path.dirname() and os.path.basename() are safe because
170 170 # they use result of os.path.split()
171 funcs = b'''os.path.join os.path.split os.path.splitext
171 funcs = '''os.path.join os.path.split os.path.splitext
172 172 os.path.normpath os.makedirs mercurial.util.endswithsep
173 173 mercurial.util.splitpath mercurial.util.fscasesensitive
174 174 mercurial.util.fspath mercurial.util.pconvert mercurial.util.normpath
175 175 mercurial.util.checkwinfilename mercurial.util.checkosfilename
176 176 mercurial.util.split'''
177 177
178 178 # These functions are required to be called with local encoded string
179 179 # because they expects argument is local encoded string and cause
180 180 # problem with unicode string.
181 rfuncs = b'''mercurial.encoding.upper mercurial.encoding.lower
181 rfuncs = '''mercurial.encoding.upper mercurial.encoding.lower
182 182 mercurial.util._filenamebytestr'''
183 183
184 184 # List of Windows specific functions to be wrapped.
185 winfuncs = b'''os.path.splitunc'''
185 winfuncs = '''os.path.splitunc'''
186 186
187 187 # codec and alias names of sjis and big5 to be faked.
188 188 problematic_encodings = b'''big5 big5-tw csbig5 big5hkscs big5-hkscs
189 189 hkscs cp932 932 ms932 mskanji ms-kanji shift_jis csshiftjis shiftjis
190 190 sjis s_jis shift_jis_2004 shiftjis2004 sjis_2004 sjis2004
191 191 shift_jisx0213 shiftjisx0213 sjisx0213 s_jisx0213 950 cp950 ms950 '''
192 192
193 193
194 194 def extsetup(ui):
195 195 # TODO: decide use of config section for this extension
196 196 if (not os.path.supports_unicode_filenames) and (
197 197 pycompat.sysplatform != b'cygwin'
198 198 ):
199 199 ui.warn(_(b"[win32mbcs] cannot activate on this platform.\n"))
200 200 return
201 201 # determine encoding for filename
202 202 global _encoding
203 203 _encoding = ui.config(b'win32mbcs', b'encoding')
204 204 # fake is only for relevant environment.
205 205 if _encoding.lower() in problematic_encodings.split():
206 206 for f in funcs.split():
207 207 wrapname(f, wrapper)
208 208 if pycompat.iswindows:
209 209 for f in winfuncs.split():
210 210 wrapname(f, wrapper)
211 wrapname(b"mercurial.util.listdir", wrapperforlistdir)
212 wrapname(b"mercurial.windows.listdir", wrapperforlistdir)
211 wrapname("mercurial.util.listdir", wrapperforlistdir)
212 wrapname("mercurial.windows.listdir", wrapperforlistdir)
213 213 # wrap functions to be called with local byte string arguments
214 214 for f in rfuncs.split():
215 215 wrapname(f, reversewrapper)
216 216 # Check sys.args manually instead of using ui.debug() because
217 217 # command line options is not yet applied when
218 218 # extensions.loadall() is called.
219 if b'--debug' in sys.argv:
219 if '--debug' in sys.argv:
220 220 ui.writenoi18n(
221 221 b"[win32mbcs] activated with encoding: %s\n" % _encoding
222 222 )
@@ -1,22 +1,19 b''
1 1 #!/usr/bin/env python3
2 2 #
3 3 # An example hgweb CGI script, edit as necessary
4 4 # See also https://mercurial-scm.org/wiki/PublishingRepositories
5 5
6 6 # Path to repo or hgweb config to serve (see 'hg help hgweb')
7 7 config = b"/path/to/repo/or/config"
8 8
9 9 # Uncomment and adjust if Mercurial is not installed system-wide
10 10 # (consult "installed modules" path from 'hg debuginstall'):
11 11 # import sys; sys.path.insert(0, "/path/to/python/lib")
12 12
13 # Uncomment to send python tracebacks to the browser if an error occurs:
14 # import cgitb; cgitb.enable()
15
16 13 from mercurial import demandimport
17 14
18 15 demandimport.enable()
19 16 from mercurial.hgweb import hgweb, wsgicgi
20 17
21 18 application = hgweb(config)
22 19 wsgicgi.launch(application)
@@ -1,852 +1,849 b''
1 1 # dirstatemap.py
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6
7 import struct
8 7 from .i18n import _
9 8
10 9 from . import (
11 10 error,
12 11 pathutil,
13 12 policy,
14 13 testing,
15 14 txnutil,
16 15 util,
17 16 )
18 17
19 18 from .dirstateutils import (
20 19 docket as docketmod,
21 20 v2,
22 21 )
23 22
24 23 parsers = policy.importmod('parsers')
25 24 rustmod = policy.importrust('dirstate')
26 25
27 26 propertycache = util.propertycache
28 27
29 28 if rustmod is None:
30 29 DirstateItem = parsers.DirstateItem
31 30 else:
32 31 DirstateItem = rustmod.DirstateItem
33 32
34 33 rangemask = 0x7FFFFFFF
35 34
36 35 WRITE_MODE_AUTO = 0
37 36 WRITE_MODE_FORCE_NEW = 1
38 37 WRITE_MODE_FORCE_APPEND = 2
39 38
40 39
41 40 V2_MAX_READ_ATTEMPTS = 5
42 41
43 42
44 43 class _dirstatemapcommon:
45 44 """
46 45 Methods that are identical for both implementations of the dirstatemap
47 46 class, with and without Rust extensions enabled.
48 47 """
49 48
50 49 # please pytype
51 50
52 51 _map = None
53 52 copymap = None
54 53
55 54 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
56 55 self._use_dirstate_v2 = use_dirstate_v2
57 56 self._nodeconstants = nodeconstants
58 57 self._ui = ui
59 58 self._opener = opener
60 59 self._root = root
61 60 self._filename = b'dirstate'
62 61 self._nodelen = 20 # Also update Rust code when changing this!
63 62 self._parents = None
64 63 self._dirtyparents = False
65 64 self._docket = None
66 65 write_mode = ui.config(b"devel", b"dirstate.v2.data_update_mode")
67 66 if write_mode == b"auto":
68 67 self._write_mode = WRITE_MODE_AUTO
69 68 elif write_mode == b"force-append":
70 69 self._write_mode = WRITE_MODE_FORCE_APPEND
71 70 elif write_mode == b"force-new":
72 71 self._write_mode = WRITE_MODE_FORCE_NEW
73 72 else:
74 73 # unknown value, fallback to default
75 74 self._write_mode = WRITE_MODE_AUTO
76 75
77 76 # for consistent view between _pl() and _read() invocations
78 77 self._pendingmode = None
79 78
80 79 def _set_identity(self):
81 80 self.identity = self._get_current_identity()
82 81
83 82 def _get_current_identity(self):
84 83 try:
85 84 return util.cachestat(self._opener.join(self._filename))
86 85 except FileNotFoundError:
87 86 return None
88 87
89 88 def may_need_refresh(self):
90 89 if 'identity' not in vars(self):
91 90 # no existing identity, we need a refresh
92 91 return True
93 92 if self.identity is None:
94 93 return True
95 94 if not self.identity.cacheable():
96 95 # We cannot trust the entry
97 96 # XXX this is a problem on windows, NFS, or other inode less system
98 97 return True
99 98 current_identity = self._get_current_identity()
100 99 if current_identity is None:
101 100 return True
102 101 if not current_identity.cacheable():
103 102 # We cannot trust the entry
104 103 # XXX this is a problem on windows, NFS, or other inode less system
105 104 return True
106 105 return current_identity != self.identity
107 106
108 107 def preload(self):
109 108 """Loads the underlying data, if it's not already loaded"""
110 109 self._map
111 110
112 111 def get(self, key, default=None):
113 112 return self._map.get(key, default)
114 113
115 114 def __len__(self):
116 115 return len(self._map)
117 116
118 117 def __iter__(self):
119 118 return iter(self._map)
120 119
121 120 def __contains__(self, key):
122 121 return key in self._map
123 122
124 123 def __getitem__(self, item):
125 124 return self._map[item]
126 125
127 126 ### disk interaction
128 127
129 128 def _opendirstatefile(self):
130 129 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
131 130 if self._pendingmode is not None and self._pendingmode != mode:
132 131 fp.close()
133 132 raise error.Abort(
134 133 _(b'working directory state may be changed parallelly')
135 134 )
136 135 self._pendingmode = mode
137 136 return fp
138 137
139 138 def _readdirstatefile(self, size=-1):
140 139 try:
141 140 with self._opendirstatefile() as fp:
142 141 return fp.read(size)
143 142 except FileNotFoundError:
144 143 # File doesn't exist, so the current state is empty
145 144 return b''
146 145
147 146 @property
148 147 def docket(self):
149 148 if not self._docket:
150 149 if not self._use_dirstate_v2:
151 150 raise error.ProgrammingError(
152 151 b'dirstate only has a docket in v2 format'
153 152 )
154 153 self._set_identity()
155 try:
154 data = self._readdirstatefile()
155 if data == b'' or data.startswith(docketmod.V2_FORMAT_MARKER):
156 156 self._docket = docketmod.DirstateDocket.parse(
157 self._readdirstatefile(), self._nodeconstants
157 data, self._nodeconstants
158 158 )
159 except struct.error:
160 self._ui.debug(b"failed to read dirstate-v2 data")
161 raise error.CorruptedDirstate(
162 b"failed to read dirstate-v2 data"
163 )
159 else:
160 raise error.CorruptedDirstate(b"dirstate is not in v2 format")
164 161 return self._docket
165 162
166 163 def _read_v2_data(self):
167 164 data = None
168 165 attempts = 0
169 166 while attempts < V2_MAX_READ_ATTEMPTS:
170 167 attempts += 1
171 168 try:
172 169 # TODO: use mmap when possible
173 170 data = self._opener.read(self.docket.data_filename())
174 171 except FileNotFoundError:
175 172 # read race detected between docket and data file
176 173 # reload the docket and retry
177 174 self._docket = None
178 175 if data is None:
179 176 assert attempts >= V2_MAX_READ_ATTEMPTS
180 177 msg = b"dirstate read race happened %d times in a row"
181 178 msg %= attempts
182 179 raise error.Abort(msg)
183 180 return self._opener.read(self.docket.data_filename())
184 181
185 182 def write_v2_no_append(self, tr, st, meta, packed):
186 183 try:
187 184 old_docket = self.docket
188 185 except error.CorruptedDirstate:
189 186 # This means we've identified a dirstate-v1 file on-disk when we
190 187 # were expecting a dirstate-v2 docket. We've managed to recover
191 188 # from that unexpected situation, and now we want to write back a
192 189 # dirstate-v2 file to make the on-disk situation right again.
193 190 #
194 191 # This shouldn't be triggered since `self.docket` is cached and
195 192 # we would have called parents() or read() first, but it's here
196 193 # just in case.
197 194 old_docket = None
198 195
199 196 new_docket = docketmod.DirstateDocket.with_new_uuid(
200 197 self.parents(), len(packed), meta
201 198 )
202 199 if old_docket is not None and old_docket.uuid == new_docket.uuid:
203 200 raise error.ProgrammingError(b'dirstate docket name collision')
204 201 data_filename = new_docket.data_filename()
205 202 self._opener.write(data_filename, packed)
206 203 # tell the transaction that we are adding a new file
207 204 if tr is not None:
208 205 tr.addbackup(data_filename, location=b'plain')
209 206 # Write the new docket after the new data file has been
210 207 # written. Because `st` was opened with `atomictemp=True`,
211 208 # the actual `.hg/dirstate` file is only affected on close.
212 209 st.write(new_docket.serialize())
213 210 st.close()
214 211 # Remove the old data file after the new docket pointing to
215 212 # the new data file was written.
216 213 if old_docket is not None and old_docket.uuid:
217 214 data_filename = old_docket.data_filename()
218 215 if tr is not None:
219 216 tr.addbackup(data_filename, location=b'plain')
220 217 unlink = lambda _tr=None: self._opener.unlink(data_filename)
221 218 if tr:
222 219 category = b"dirstate-v2-clean-" + old_docket.uuid
223 220 tr.addpostclose(category, unlink)
224 221 else:
225 222 unlink()
226 223 self._docket = new_docket
227 224
228 225 ### reading/setting parents
229 226
230 227 def parents(self):
231 228 if not self._parents:
232 229 if self._use_dirstate_v2:
233 230 try:
234 231 self.docket
235 232 except error.CorruptedDirstate as e:
236 233 # fall back to dirstate-v1 if we fail to read v2
237 234 self._v1_parents(e)
238 235 else:
239 236 self._parents = self.docket.parents
240 237 else:
241 238 self._v1_parents()
242 239
243 240 return self._parents
244 241
245 242 def _v1_parents(self, from_v2_exception=None):
246 243 read_len = self._nodelen * 2
247 244 st = self._readdirstatefile(read_len)
248 245 l = len(st)
249 246 if l == read_len:
250 247 self._parents = (
251 248 st[: self._nodelen],
252 249 st[self._nodelen : 2 * self._nodelen],
253 250 )
254 251 elif l == 0:
255 252 self._parents = (
256 253 self._nodeconstants.nullid,
257 254 self._nodeconstants.nullid,
258 255 )
259 256 else:
260 257 hint = None
261 258 if from_v2_exception is not None:
262 259 hint = _(b"falling back to dirstate-v1 from v2 also failed")
263 260 raise error.Abort(
264 261 _(b'working directory state appears damaged!'), hint
265 262 )
266 263
267 264
268 265 class dirstatemap(_dirstatemapcommon):
269 266 """Map encapsulating the dirstate's contents.
270 267
271 268 The dirstate contains the following state:
272 269
273 270 - `identity` is the identity of the dirstate file, which can be used to
274 271 detect when changes have occurred to the dirstate file.
275 272
276 273 - `parents` is a pair containing the parents of the working copy. The
277 274 parents are updated by calling `setparents`.
278 275
279 276 - the state map maps filenames to tuples of (state, mode, size, mtime),
280 277 where state is a single character representing 'normal', 'added',
281 278 'removed', or 'merged'. It is read by treating the dirstate as a
282 279 dict. File state is updated by calling various methods (see each
283 280 documentation for details):
284 281
285 282 - `reset_state`,
286 283 - `set_tracked`
287 284 - `set_untracked`
288 285 - `set_clean`
289 286 - `set_possibly_dirty`
290 287
291 288 - `copymap` maps destination filenames to their source filename.
292 289
293 290 The dirstate also provides the following views onto the state:
294 291
295 292 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
296 293 form that they appear as in the dirstate.
297 294
298 295 - `dirfoldmap` is a dict mapping normalized directory names to the
299 296 denormalized form that they appear as in the dirstate.
300 297 """
301 298
302 299 ### Core data storage and access
303 300
304 301 @propertycache
305 302 def _map(self):
306 303 self._map = {}
307 304 self.read()
308 305 return self._map
309 306
310 307 @propertycache
311 308 def copymap(self):
312 309 self.copymap = {}
313 310 self._map
314 311 return self.copymap
315 312
316 313 def clear(self):
317 314 self._map.clear()
318 315 self.copymap.clear()
319 316 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
320 317 util.clearcachedproperty(self, b"_dirs")
321 318 util.clearcachedproperty(self, b"_alldirs")
322 319 util.clearcachedproperty(self, b"filefoldmap")
323 320 util.clearcachedproperty(self, b"dirfoldmap")
324 321
325 322 def items(self):
326 323 return self._map.items()
327 324
328 325 # forward for python2,3 compat
329 326 iteritems = items
330 327
331 328 def debug_iter(self, all):
332 329 """
333 330 Return an iterator of (filename, state, mode, size, mtime) tuples
334 331
335 332 `all` is unused when Rust is not enabled
336 333 """
337 334 for (filename, item) in self.items():
338 335 yield (filename, item.state, item.mode, item.size, item.mtime)
339 336
340 337 def keys(self):
341 338 return self._map.keys()
342 339
343 340 ### reading/setting parents
344 341
345 342 def setparents(self, p1, p2, fold_p2=False):
346 343 self._parents = (p1, p2)
347 344 self._dirtyparents = True
348 345 copies = {}
349 346 if fold_p2:
350 347 for f, s in self._map.items():
351 348 # Discard "merged" markers when moving away from a merge state
352 349 if s.p2_info:
353 350 source = self.copymap.pop(f, None)
354 351 if source:
355 352 copies[f] = source
356 353 s.drop_merge_data()
357 354 return copies
358 355
359 356 ### disk interaction
360 357
361 358 def read(self):
362 359 testing.wait_on_cfg(self._ui, b'dirstate.pre-read-file')
363 360 if self._use_dirstate_v2:
364 361 try:
365 362 self.docket
366 363 except error.CorruptedDirstate:
367 364 # fall back to dirstate-v1 if we fail to read v2
368 365 self._set_identity()
369 366 st = self._readdirstatefile()
370 367 else:
371 368 if not self.docket.uuid:
372 369 return
373 370 testing.wait_on_cfg(self._ui, b'dirstate.post-docket-read-file')
374 371 st = self._read_v2_data()
375 372 else:
376 373 self._set_identity()
377 374 st = self._readdirstatefile()
378 375
379 376 if not st:
380 377 return
381 378
382 379 # TODO: adjust this estimate for dirstate-v2
383 380 if util.safehasattr(parsers, 'dict_new_presized'):
384 381 # Make an estimate of the number of files in the dirstate based on
385 382 # its size. This trades wasting some memory for avoiding costly
386 383 # resizes. Each entry have a prefix of 17 bytes followed by one or
387 384 # two path names. Studies on various large-scale real-world repositories
388 385 # found 54 bytes a reasonable upper limit for the average path names.
389 386 # Copy entries are ignored for the sake of this estimate.
390 387 self._map = parsers.dict_new_presized(len(st) // 71)
391 388
392 389 # Python's garbage collector triggers a GC each time a certain number
393 390 # of container objects (the number being defined by
394 391 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
395 392 # for each file in the dirstate. The C version then immediately marks
396 393 # them as not to be tracked by the collector. However, this has no
397 394 # effect on when GCs are triggered, only on what objects the GC looks
398 395 # into. This means that O(number of files) GCs are unavoidable.
399 396 # Depending on when in the process's lifetime the dirstate is parsed,
400 397 # this can get very expensive. As a workaround, disable GC while
401 398 # parsing the dirstate.
402 399 #
403 400 # (we cannot decorate the function directly since it is in a C module)
404 401 if self._use_dirstate_v2:
405 402 try:
406 403 self.docket
407 404 except error.CorruptedDirstate:
408 405 # fall back to dirstate-v1 if we fail to parse v2
409 406 parse_dirstate = util.nogc(parsers.parse_dirstate)
410 407 p = parse_dirstate(self._map, self.copymap, st)
411 408 else:
412 409 p = self.docket.parents
413 410 meta = self.docket.tree_metadata
414 411 parse_dirstate = util.nogc(v2.parse_dirstate)
415 412 parse_dirstate(self._map, self.copymap, st, meta)
416 413 else:
417 414 parse_dirstate = util.nogc(parsers.parse_dirstate)
418 415 p = parse_dirstate(self._map, self.copymap, st)
419 416 if not self._dirtyparents:
420 417 self.setparents(*p)
421 418
422 419 # Avoid excess attribute lookups by fast pathing certain checks
423 420 self.__contains__ = self._map.__contains__
424 421 self.__getitem__ = self._map.__getitem__
425 422 self.get = self._map.get
426 423
427 424 def write(self, tr, st):
428 425 if self._use_dirstate_v2:
429 426 packed, meta = v2.pack_dirstate(self._map, self.copymap)
430 427 self.write_v2_no_append(tr, st, meta, packed)
431 428 else:
432 429 packed = parsers.pack_dirstate(
433 430 self._map, self.copymap, self.parents()
434 431 )
435 432 st.write(packed)
436 433 st.close()
437 434 self._dirtyparents = False
438 435
439 436 @propertycache
440 437 def identity(self):
441 438 self._map
442 439 return self.identity
443 440
444 441 ### code related to maintaining and accessing "extra" property
445 442 # (e.g. "has_dir")
446 443
447 444 def _dirs_incr(self, filename, old_entry=None):
448 445 """increment the dirstate counter if applicable"""
449 446 if (
450 447 old_entry is None or old_entry.removed
451 448 ) and "_dirs" in self.__dict__:
452 449 self._dirs.addpath(filename)
453 450 if old_entry is None and "_alldirs" in self.__dict__:
454 451 self._alldirs.addpath(filename)
455 452
456 453 def _dirs_decr(self, filename, old_entry=None, remove_variant=False):
457 454 """decrement the dirstate counter if applicable"""
458 455 if old_entry is not None:
459 456 if "_dirs" in self.__dict__ and not old_entry.removed:
460 457 self._dirs.delpath(filename)
461 458 if "_alldirs" in self.__dict__ and not remove_variant:
462 459 self._alldirs.delpath(filename)
463 460 elif remove_variant and "_alldirs" in self.__dict__:
464 461 self._alldirs.addpath(filename)
465 462 if "filefoldmap" in self.__dict__:
466 463 normed = util.normcase(filename)
467 464 self.filefoldmap.pop(normed, None)
468 465
469 466 @propertycache
470 467 def filefoldmap(self):
471 468 """Returns a dictionary mapping normalized case paths to their
472 469 non-normalized versions.
473 470 """
474 471 try:
475 472 makefilefoldmap = parsers.make_file_foldmap
476 473 except AttributeError:
477 474 pass
478 475 else:
479 476 return makefilefoldmap(
480 477 self._map, util.normcasespec, util.normcasefallback
481 478 )
482 479
483 480 f = {}
484 481 normcase = util.normcase
485 482 for name, s in self._map.items():
486 483 if not s.removed:
487 484 f[normcase(name)] = name
488 485 f[b'.'] = b'.' # prevents useless util.fspath() invocation
489 486 return f
490 487
491 488 @propertycache
492 489 def dirfoldmap(self):
493 490 f = {}
494 491 normcase = util.normcase
495 492 for name in self._dirs:
496 493 f[normcase(name)] = name
497 494 return f
498 495
499 496 def hastrackeddir(self, d):
500 497 """
501 498 Returns True if the dirstate contains a tracked (not removed) file
502 499 in this directory.
503 500 """
504 501 return d in self._dirs
505 502
506 503 def hasdir(self, d):
507 504 """
508 505 Returns True if the dirstate contains a file (tracked or removed)
509 506 in this directory.
510 507 """
511 508 return d in self._alldirs
512 509
513 510 @propertycache
514 511 def _dirs(self):
515 512 return pathutil.dirs(self._map, only_tracked=True)
516 513
517 514 @propertycache
518 515 def _alldirs(self):
519 516 return pathutil.dirs(self._map)
520 517
521 518 ### code related to manipulation of entries and copy-sources
522 519
523 520 def reset_state(
524 521 self,
525 522 filename,
526 523 wc_tracked=False,
527 524 p1_tracked=False,
528 525 p2_info=False,
529 526 has_meaningful_mtime=True,
530 527 parentfiledata=None,
531 528 ):
532 529 """Set a entry to a given state, diregarding all previous state
533 530
534 531 This is to be used by the part of the dirstate API dedicated to
535 532 adjusting the dirstate after a update/merge.
536 533
537 534 note: calling this might result to no entry existing at all if the
538 535 dirstate map does not see any point at having one for this file
539 536 anymore.
540 537 """
541 538 # copy information are now outdated
542 539 # (maybe new information should be in directly passed to this function)
543 540 self.copymap.pop(filename, None)
544 541
545 542 if not (p1_tracked or p2_info or wc_tracked):
546 543 old_entry = self._map.get(filename)
547 544 self._drop_entry(filename)
548 545 self._dirs_decr(filename, old_entry=old_entry)
549 546 return
550 547
551 548 old_entry = self._map.get(filename)
552 549 self._dirs_incr(filename, old_entry)
553 550 entry = DirstateItem(
554 551 wc_tracked=wc_tracked,
555 552 p1_tracked=p1_tracked,
556 553 p2_info=p2_info,
557 554 has_meaningful_mtime=has_meaningful_mtime,
558 555 parentfiledata=parentfiledata,
559 556 )
560 557 self._map[filename] = entry
561 558
562 559 def set_tracked(self, filename):
563 560 new = False
564 561 entry = self.get(filename)
565 562 if entry is None:
566 563 self._dirs_incr(filename)
567 564 entry = DirstateItem(
568 565 wc_tracked=True,
569 566 )
570 567
571 568 self._map[filename] = entry
572 569 new = True
573 570 elif not entry.tracked:
574 571 self._dirs_incr(filename, entry)
575 572 entry.set_tracked()
576 573 self._refresh_entry(filename, entry)
577 574 new = True
578 575 else:
579 576 # XXX This is probably overkill for more case, but we need this to
580 577 # fully replace the `normallookup` call with `set_tracked` one.
581 578 # Consider smoothing this in the future.
582 579 entry.set_possibly_dirty()
583 580 self._refresh_entry(filename, entry)
584 581 return new
585 582
586 583 def set_untracked(self, f):
587 584 """Mark a file as no longer tracked in the dirstate map"""
588 585 entry = self.get(f)
589 586 if entry is None:
590 587 return False
591 588 else:
592 589 self._dirs_decr(f, old_entry=entry, remove_variant=not entry.added)
593 590 if not entry.p2_info:
594 591 self.copymap.pop(f, None)
595 592 entry.set_untracked()
596 593 self._refresh_entry(f, entry)
597 594 return True
598 595
599 596 def set_clean(self, filename, mode, size, mtime):
600 597 """mark a file as back to a clean state"""
601 598 entry = self[filename]
602 599 size = size & rangemask
603 600 entry.set_clean(mode, size, mtime)
604 601 self._refresh_entry(filename, entry)
605 602 self.copymap.pop(filename, None)
606 603
607 604 def set_possibly_dirty(self, filename):
608 605 """record that the current state of the file on disk is unknown"""
609 606 entry = self[filename]
610 607 entry.set_possibly_dirty()
611 608 self._refresh_entry(filename, entry)
612 609
613 610 def _refresh_entry(self, f, entry):
614 611 """record updated state of an entry"""
615 612 if not entry.any_tracked:
616 613 self._map.pop(f, None)
617 614
618 615 def _drop_entry(self, f):
619 616 """remove any entry for file f
620 617
621 618 This should also drop associated copy information
622 619
623 620 The fact we actually need to drop it is the responsability of the caller"""
624 621 self._map.pop(f, None)
625 622 self.copymap.pop(f, None)
626 623
627 624
628 625 if rustmod is not None:
629 626
630 627 class dirstatemap(_dirstatemapcommon):
631 628
632 629 ### Core data storage and access
633 630
634 631 @propertycache
635 632 def _map(self):
636 633 """
637 634 Fills the Dirstatemap when called.
638 635 """
639 636 # ignore HG_PENDING because identity is used only for writing
640 637 self._set_identity()
641 638
642 639 testing.wait_on_cfg(self._ui, b'dirstate.pre-read-file')
643 640 if self._use_dirstate_v2:
644 641 try:
645 642 self.docket
646 643 except error.CorruptedDirstate as e:
647 644 # fall back to dirstate-v1 if we fail to read v2
648 645 parents = self._v1_map(e)
649 646 else:
650 647 parents = self.docket.parents
651 648 inode = (
652 649 self.identity.stat.st_ino
653 650 if self.identity is not None
654 651 and self.identity.stat is not None
655 652 else None
656 653 )
657 654 testing.wait_on_cfg(
658 655 self._ui, b'dirstate.post-docket-read-file'
659 656 )
660 657 if not self.docket.uuid:
661 658 data = b''
662 659 self._map = rustmod.DirstateMap.new_empty()
663 660 else:
664 661 data = self._read_v2_data()
665 662 self._map = rustmod.DirstateMap.new_v2(
666 663 data,
667 664 self.docket.data_size,
668 665 self.docket.tree_metadata,
669 666 self.docket.uuid,
670 667 inode,
671 668 )
672 669 parents = self.docket.parents
673 670 else:
674 671 parents = self._v1_map()
675 672
676 673 if parents and not self._dirtyparents:
677 674 self.setparents(*parents)
678 675
679 676 self.__contains__ = self._map.__contains__
680 677 self.__getitem__ = self._map.__getitem__
681 678 self.get = self._map.get
682 679 return self._map
683 680
684 681 def _v1_map(self, from_v2_exception=None):
685 682 self._set_identity()
686 683 inode = (
687 684 self.identity.stat.st_ino
688 685 if self.identity is not None and self.identity.stat is not None
689 686 else None
690 687 )
691 688 try:
692 689 self._map, parents = rustmod.DirstateMap.new_v1(
693 690 self._readdirstatefile(), inode
694 691 )
695 692 except OSError as e:
696 693 if from_v2_exception is not None:
697 694 raise e from from_v2_exception
698 695 raise
699 696 return parents
700 697
701 698 @property
702 699 def copymap(self):
703 700 return self._map.copymap()
704 701
705 702 def debug_iter(self, all):
706 703 """
707 704 Return an iterator of (filename, state, mode, size, mtime) tuples
708 705
709 706 `all`: also include with `state == b' '` dirstate tree nodes that
710 707 don't have an associated `DirstateItem`.
711 708
712 709 """
713 710 return self._map.debug_iter(all)
714 711
715 712 def clear(self):
716 713 self._map.clear()
717 714 self.setparents(
718 715 self._nodeconstants.nullid, self._nodeconstants.nullid
719 716 )
720 717 util.clearcachedproperty(self, b"_dirs")
721 718 util.clearcachedproperty(self, b"_alldirs")
722 719 util.clearcachedproperty(self, b"dirfoldmap")
723 720
724 721 def items(self):
725 722 return self._map.items()
726 723
727 724 # forward for python2,3 compat
728 725 iteritems = items
729 726
730 727 def keys(self):
731 728 return iter(self._map)
732 729
733 730 ### reading/setting parents
734 731
735 732 def setparents(self, p1, p2, fold_p2=False):
736 733 self._parents = (p1, p2)
737 734 self._dirtyparents = True
738 735 copies = {}
739 736 if fold_p2:
740 737 copies = self._map.setparents_fixup()
741 738 return copies
742 739
743 740 ### disk interaction
744 741
745 742 @propertycache
746 743 def identity(self):
747 744 self._map
748 745 return self.identity
749 746
750 747 def write(self, tr, st):
751 748 if not self._use_dirstate_v2:
752 749 p1, p2 = self.parents()
753 750 packed = self._map.write_v1(p1, p2)
754 751 st.write(packed)
755 752 st.close()
756 753 self._dirtyparents = False
757 754 return
758 755
759 756 write_mode = self._write_mode
760 757 try:
761 758 docket = self.docket
762 759 except error.CorruptedDirstate:
763 760 # fall back to dirstate-v1 if we fail to parse v2
764 761 docket = None
765 762
766 763 # We can only append to an existing data file if there is one
767 764 if docket is None or docket.uuid is None:
768 765 write_mode = WRITE_MODE_FORCE_NEW
769 766 packed, meta, append = self._map.write_v2(write_mode)
770 767 if append:
771 768 docket = self.docket
772 769 data_filename = docket.data_filename()
773 770 # We mark it for backup to make sure a future `hg rollback` (or
774 771 # `hg recover`?) call find the data it needs to restore a
775 772 # working repository.
776 773 #
777 774 # The backup can use a hardlink because the format is resistant
778 775 # to trailing "dead" data.
779 776 if tr is not None:
780 777 tr.addbackup(data_filename, location=b'plain')
781 778 with self._opener(data_filename, b'r+b') as fp:
782 779 fp.seek(docket.data_size)
783 780 assert fp.tell() == docket.data_size
784 781 written = fp.write(packed)
785 782 if written is not None: # py2 may return None
786 783 assert written == len(packed), (written, len(packed))
787 784 docket.data_size += len(packed)
788 785 docket.parents = self.parents()
789 786 docket.tree_metadata = meta
790 787 st.write(docket.serialize())
791 788 st.close()
792 789 else:
793 790 self.write_v2_no_append(tr, st, meta, packed)
794 791 # Reload from the newly-written file
795 792 util.clearcachedproperty(self, b"_map")
796 793 self._dirtyparents = False
797 794
798 795 ### code related to maintaining and accessing "extra" property
799 796 # (e.g. "has_dir")
800 797
801 798 @propertycache
802 799 def filefoldmap(self):
803 800 """Returns a dictionary mapping normalized case paths to their
804 801 non-normalized versions.
805 802 """
806 803 return self._map.filefoldmapasdict()
807 804
808 805 def hastrackeddir(self, d):
809 806 return self._map.hastrackeddir(d)
810 807
811 808 def hasdir(self, d):
812 809 return self._map.hasdir(d)
813 810
814 811 @propertycache
815 812 def dirfoldmap(self):
816 813 f = {}
817 814 normcase = util.normcase
818 815 for name in self._map.tracked_dirs():
819 816 f[normcase(name)] = name
820 817 return f
821 818
822 819 ### code related to manipulation of entries and copy-sources
823 820
824 821 def set_tracked(self, f):
825 822 return self._map.set_tracked(f)
826 823
827 824 def set_untracked(self, f):
828 825 return self._map.set_untracked(f)
829 826
830 827 def set_clean(self, filename, mode, size, mtime):
831 828 self._map.set_clean(filename, mode, size, mtime)
832 829
833 830 def set_possibly_dirty(self, f):
834 831 self._map.set_possibly_dirty(f)
835 832
836 833 def reset_state(
837 834 self,
838 835 filename,
839 836 wc_tracked=False,
840 837 p1_tracked=False,
841 838 p2_info=False,
842 839 has_meaningful_mtime=True,
843 840 parentfiledata=None,
844 841 ):
845 842 return self._map.reset_state(
846 843 filename,
847 844 wc_tracked,
848 845 p1_tracked,
849 846 p2_info,
850 847 has_meaningful_mtime,
851 848 parentfiledata,
852 849 )
@@ -1,1928 +1,1929 b''
1 1 use bytes_cast::BytesCast;
2 2 use std::borrow::Cow;
3 3 use std::path::PathBuf;
4 4
5 5 use super::on_disk;
6 6 use super::on_disk::DirstateV2ParseError;
7 7 use super::owning::OwningDirstateMap;
8 8 use super::path_with_basename::WithBasename;
9 9 use crate::dirstate::parsers::pack_entry;
10 10 use crate::dirstate::parsers::packed_entry_size;
11 11 use crate::dirstate::parsers::parse_dirstate_entries;
12 12 use crate::dirstate::CopyMapIter;
13 13 use crate::dirstate::DirstateV2Data;
14 14 use crate::dirstate::ParentFileData;
15 15 use crate::dirstate::StateMapIter;
16 16 use crate::dirstate::TruncatedTimestamp;
17 17 use crate::matchers::Matcher;
18 18 use crate::utils::filter_map_results;
19 19 use crate::utils::hg_path::{HgPath, HgPathBuf};
20 20 use crate::DirstateEntry;
21 21 use crate::DirstateError;
22 22 use crate::DirstateMapError;
23 23 use crate::DirstateParents;
24 24 use crate::DirstateStatus;
25 25 use crate::FastHashbrownMap as FastHashMap;
26 26 use crate::PatternFileWarning;
27 27 use crate::StatusError;
28 28 use crate::StatusOptions;
29 29
30 30 /// Append to an existing data file if the amount of unreachable data (not used
31 31 /// anymore) is less than this fraction of the total amount of existing data.
32 32 const ACCEPTABLE_UNREACHABLE_BYTES_RATIO: f32 = 0.5;
33 33
34 34 #[derive(Debug, PartialEq, Eq)]
35 35 /// Version of the on-disk format
36 36 pub enum DirstateVersion {
37 37 V1,
38 38 V2,
39 39 }
40 40
41 41 #[derive(Debug, PartialEq, Eq)]
42 42 pub enum DirstateMapWriteMode {
43 43 Auto,
44 44 ForceNewDataFile,
45 45 ForceAppend,
46 46 }
47 47
48 48 #[derive(Debug)]
49 49 pub struct DirstateMap<'on_disk> {
50 50 /// Contents of the `.hg/dirstate` file
51 51 pub(super) on_disk: &'on_disk [u8],
52 52
53 53 pub(super) root: ChildNodes<'on_disk>,
54 54
55 55 /// Number of nodes anywhere in the tree that have `.entry.is_some()`.
56 56 pub(super) nodes_with_entry_count: u32,
57 57
58 58 /// Number of nodes anywhere in the tree that have
59 59 /// `.copy_source.is_some()`.
60 60 pub(super) nodes_with_copy_source_count: u32,
61 61
62 62 /// See on_disk::Header
63 63 pub(super) ignore_patterns_hash: on_disk::IgnorePatternsHash,
64 64
65 65 /// How many bytes of `on_disk` are not used anymore
66 66 pub(super) unreachable_bytes: u32,
67 67
68 68 /// Size of the data used to first load this `DirstateMap`. Used in case
69 69 /// we need to write some new metadata, but no new data on disk,
70 70 /// as well as to detect writes that have happened in another process
71 71 /// since first read.
72 72 pub(super) old_data_size: usize,
73 73
74 74 /// UUID used when first loading this `DirstateMap`. Used to check if
75 75 /// the UUID has been changed by another process since first read.
76 76 /// Can be `None` if using dirstate v1 or if it's a brand new dirstate.
77 77 pub(super) old_uuid: Option<Vec<u8>>,
78 78
79 79 /// Identity of the dirstate file (for dirstate-v1) or the docket file
80 80 /// (v2). Used to detect if the file has changed from another process.
81 81 /// Since it's always written atomically, we can compare the inode to
82 82 /// check the file identity.
83 83 ///
84 84 /// TODO On non-Unix systems, something like hashing is a possibility?
85 85 pub(super) identity: Option<u64>,
86 86
87 87 pub(super) dirstate_version: DirstateVersion,
88 88
89 89 /// Controlled by config option `devel.dirstate.v2.data_update_mode`
90 90 pub(super) write_mode: DirstateMapWriteMode,
91 91 }
92 92
93 93 /// Using a plain `HgPathBuf` of the full path from the repository root as a
94 94 /// map key would also work: all paths in a given map have the same parent
95 95 /// path, so comparing full paths gives the same result as comparing base
96 96 /// names. However `HashMap` would waste time always re-hashing the same
97 97 /// string prefix.
98 98 pub(super) type NodeKey<'on_disk> = WithBasename<Cow<'on_disk, HgPath>>;
99 99
100 100 /// Similar to `&'tree Cow<'on_disk, HgPath>`, but can also be returned
101 101 /// for on-disk nodes that don’t actually have a `Cow` to borrow.
102 102 #[derive(Debug)]
103 103 pub(super) enum BorrowedPath<'tree, 'on_disk> {
104 104 InMemory(&'tree HgPathBuf),
105 105 OnDisk(&'on_disk HgPath),
106 106 }
107 107
108 108 #[derive(Debug)]
109 109 pub(super) enum ChildNodes<'on_disk> {
110 110 InMemory(FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
111 111 OnDisk(&'on_disk [on_disk::Node]),
112 112 }
113 113
114 114 #[derive(Debug)]
115 115 pub(super) enum ChildNodesRef<'tree, 'on_disk> {
116 116 InMemory(&'tree FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
117 117 OnDisk(&'on_disk [on_disk::Node]),
118 118 }
119 119
120 120 #[derive(Debug)]
121 121 pub(super) enum NodeRef<'tree, 'on_disk> {
122 122 InMemory(&'tree NodeKey<'on_disk>, &'tree Node<'on_disk>),
123 123 OnDisk(&'on_disk on_disk::Node),
124 124 }
125 125
126 126 impl<'tree, 'on_disk> BorrowedPath<'tree, 'on_disk> {
127 127 pub fn detach_from_tree(&self) -> Cow<'on_disk, HgPath> {
128 128 match *self {
129 129 BorrowedPath::InMemory(in_memory) => Cow::Owned(in_memory.clone()),
130 130 BorrowedPath::OnDisk(on_disk) => Cow::Borrowed(on_disk),
131 131 }
132 132 }
133 133 }
134 134
135 135 impl<'tree, 'on_disk> std::ops::Deref for BorrowedPath<'tree, 'on_disk> {
136 136 type Target = HgPath;
137 137
138 138 fn deref(&self) -> &HgPath {
139 139 match *self {
140 140 BorrowedPath::InMemory(in_memory) => in_memory,
141 141 BorrowedPath::OnDisk(on_disk) => on_disk,
142 142 }
143 143 }
144 144 }
145 145
146 146 impl Default for ChildNodes<'_> {
147 147 fn default() -> Self {
148 148 ChildNodes::InMemory(Default::default())
149 149 }
150 150 }
151 151
152 152 impl<'on_disk> ChildNodes<'on_disk> {
153 153 pub(super) fn as_ref<'tree>(
154 154 &'tree self,
155 155 ) -> ChildNodesRef<'tree, 'on_disk> {
156 156 match self {
157 157 ChildNodes::InMemory(nodes) => ChildNodesRef::InMemory(nodes),
158 158 ChildNodes::OnDisk(nodes) => ChildNodesRef::OnDisk(nodes),
159 159 }
160 160 }
161 161
162 162 pub(super) fn is_empty(&self) -> bool {
163 163 match self {
164 164 ChildNodes::InMemory(nodes) => nodes.is_empty(),
165 165 ChildNodes::OnDisk(nodes) => nodes.is_empty(),
166 166 }
167 167 }
168 168
169 169 fn make_mut(
170 170 &mut self,
171 171 on_disk: &'on_disk [u8],
172 172 unreachable_bytes: &mut u32,
173 173 ) -> Result<
174 174 &mut FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>,
175 175 DirstateV2ParseError,
176 176 > {
177 177 match self {
178 178 ChildNodes::InMemory(nodes) => Ok(nodes),
179 179 ChildNodes::OnDisk(nodes) => {
180 180 *unreachable_bytes +=
181 181 std::mem::size_of_val::<[on_disk::Node]>(nodes) as u32;
182 182 let nodes = nodes
183 183 .iter()
184 184 .map(|node| {
185 185 Ok((
186 186 node.path(on_disk)?,
187 187 node.to_in_memory_node(on_disk)?,
188 188 ))
189 189 })
190 190 .collect::<Result<_, _>>()?;
191 191 *self = ChildNodes::InMemory(nodes);
192 192 match self {
193 193 ChildNodes::InMemory(nodes) => Ok(nodes),
194 194 ChildNodes::OnDisk(_) => unreachable!(),
195 195 }
196 196 }
197 197 }
198 198 }
199 199 }
200 200
201 201 impl<'tree, 'on_disk> ChildNodesRef<'tree, 'on_disk> {
202 202 pub(super) fn get(
203 203 &self,
204 204 base_name: &HgPath,
205 205 on_disk: &'on_disk [u8],
206 206 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
207 207 match self {
208 208 ChildNodesRef::InMemory(nodes) => Ok(nodes
209 209 .get_key_value(base_name)
210 210 .map(|(k, v)| NodeRef::InMemory(k, v))),
211 211 ChildNodesRef::OnDisk(nodes) => {
212 212 let mut parse_result = Ok(());
213 213 let search_result = nodes.binary_search_by(|node| {
214 214 match node.base_name(on_disk) {
215 215 Ok(node_base_name) => node_base_name.cmp(base_name),
216 216 Err(e) => {
217 217 parse_result = Err(e);
218 218 // Dummy comparison result, `search_result` won’t
219 219 // be used since `parse_result` is an error
220 220 std::cmp::Ordering::Equal
221 221 }
222 222 }
223 223 });
224 224 parse_result.map(|()| {
225 225 search_result.ok().map(|i| NodeRef::OnDisk(&nodes[i]))
226 226 })
227 227 }
228 228 }
229 229 }
230 230
231 231 /// Iterate in undefined order
232 232 pub(super) fn iter(
233 233 &self,
234 234 ) -> impl Iterator<Item = NodeRef<'tree, 'on_disk>> {
235 235 match self {
236 236 ChildNodesRef::InMemory(nodes) => itertools::Either::Left(
237 237 nodes.iter().map(|(k, v)| NodeRef::InMemory(k, v)),
238 238 ),
239 239 ChildNodesRef::OnDisk(nodes) => {
240 240 itertools::Either::Right(nodes.iter().map(NodeRef::OnDisk))
241 241 }
242 242 }
243 243 }
244 244
245 245 /// Iterate in parallel in undefined order
246 246 pub(super) fn par_iter(
247 247 &self,
248 248 ) -> impl rayon::iter::ParallelIterator<Item = NodeRef<'tree, 'on_disk>>
249 249 {
250 250 use rayon::prelude::*;
251 251 match self {
252 252 ChildNodesRef::InMemory(nodes) => rayon::iter::Either::Left(
253 253 nodes.par_iter().map(|(k, v)| NodeRef::InMemory(k, v)),
254 254 ),
255 255 ChildNodesRef::OnDisk(nodes) => rayon::iter::Either::Right(
256 256 nodes.par_iter().map(NodeRef::OnDisk),
257 257 ),
258 258 }
259 259 }
260 260
261 261 pub(super) fn sorted(&self) -> Vec<NodeRef<'tree, 'on_disk>> {
262 262 match self {
263 263 ChildNodesRef::InMemory(nodes) => {
264 264 let mut vec: Vec<_> = nodes
265 265 .iter()
266 266 .map(|(k, v)| NodeRef::InMemory(k, v))
267 267 .collect();
268 268 fn sort_key<'a>(node: &'a NodeRef) -> &'a HgPath {
269 269 match node {
270 270 NodeRef::InMemory(path, _node) => path.base_name(),
271 271 NodeRef::OnDisk(_) => unreachable!(),
272 272 }
273 273 }
274 274 // `sort_unstable_by_key` doesn’t allow keys borrowing from the
275 275 // value: https://github.com/rust-lang/rust/issues/34162
276 276 vec.sort_unstable_by(|a, b| sort_key(a).cmp(sort_key(b)));
277 277 vec
278 278 }
279 279 ChildNodesRef::OnDisk(nodes) => {
280 280 // Nodes on disk are already sorted
281 281 nodes.iter().map(NodeRef::OnDisk).collect()
282 282 }
283 283 }
284 284 }
285 285 }
286 286
287 287 impl<'tree, 'on_disk> NodeRef<'tree, 'on_disk> {
288 288 pub(super) fn full_path(
289 289 &self,
290 290 on_disk: &'on_disk [u8],
291 291 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
292 292 match self {
293 293 NodeRef::InMemory(path, _node) => Ok(path.full_path()),
294 294 NodeRef::OnDisk(node) => node.full_path(on_disk),
295 295 }
296 296 }
297 297
298 298 /// Returns a `BorrowedPath`, which can be turned into a `Cow<'on_disk,
299 299 /// HgPath>` detached from `'tree`
300 300 pub(super) fn full_path_borrowed(
301 301 &self,
302 302 on_disk: &'on_disk [u8],
303 303 ) -> Result<BorrowedPath<'tree, 'on_disk>, DirstateV2ParseError> {
304 304 match self {
305 305 NodeRef::InMemory(path, _node) => match path.full_path() {
306 306 Cow::Borrowed(on_disk) => Ok(BorrowedPath::OnDisk(on_disk)),
307 307 Cow::Owned(in_memory) => Ok(BorrowedPath::InMemory(in_memory)),
308 308 },
309 309 NodeRef::OnDisk(node) => {
310 310 Ok(BorrowedPath::OnDisk(node.full_path(on_disk)?))
311 311 }
312 312 }
313 313 }
314 314
315 315 pub(super) fn base_name(
316 316 &self,
317 317 on_disk: &'on_disk [u8],
318 318 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
319 319 match self {
320 320 NodeRef::InMemory(path, _node) => Ok(path.base_name()),
321 321 NodeRef::OnDisk(node) => node.base_name(on_disk),
322 322 }
323 323 }
324 324
325 325 pub(super) fn children(
326 326 &self,
327 327 on_disk: &'on_disk [u8],
328 328 ) -> Result<ChildNodesRef<'tree, 'on_disk>, DirstateV2ParseError> {
329 329 match self {
330 330 NodeRef::InMemory(_path, node) => Ok(node.children.as_ref()),
331 331 NodeRef::OnDisk(node) => {
332 332 Ok(ChildNodesRef::OnDisk(node.children(on_disk)?))
333 333 }
334 334 }
335 335 }
336 336
337 337 pub(super) fn has_copy_source(&self) -> bool {
338 338 match self {
339 339 NodeRef::InMemory(_path, node) => node.copy_source.is_some(),
340 340 NodeRef::OnDisk(node) => node.has_copy_source(),
341 341 }
342 342 }
343 343
344 344 pub(super) fn copy_source(
345 345 &self,
346 346 on_disk: &'on_disk [u8],
347 347 ) -> Result<Option<&'tree HgPath>, DirstateV2ParseError> {
348 348 match self {
349 349 NodeRef::InMemory(_path, node) => Ok(node.copy_source.as_deref()),
350 350 NodeRef::OnDisk(node) => node.copy_source(on_disk),
351 351 }
352 352 }
353 353 /// Returns a `BorrowedPath`, which can be turned into a `Cow<'on_disk,
354 354 /// HgPath>` detached from `'tree`
355 355 pub(super) fn copy_source_borrowed(
356 356 &self,
357 357 on_disk: &'on_disk [u8],
358 358 ) -> Result<Option<BorrowedPath<'tree, 'on_disk>>, DirstateV2ParseError>
359 359 {
360 360 Ok(match self {
361 361 NodeRef::InMemory(_path, node) => {
362 362 node.copy_source.as_ref().map(|source| match source {
363 363 Cow::Borrowed(on_disk) => BorrowedPath::OnDisk(on_disk),
364 364 Cow::Owned(in_memory) => BorrowedPath::InMemory(in_memory),
365 365 })
366 366 }
367 367 NodeRef::OnDisk(node) => {
368 368 node.copy_source(on_disk)?.map(BorrowedPath::OnDisk)
369 369 }
370 370 })
371 371 }
372 372
373 373 pub(super) fn entry(
374 374 &self,
375 375 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
376 376 match self {
377 377 NodeRef::InMemory(_path, node) => {
378 378 Ok(node.data.as_entry().copied())
379 379 }
380 380 NodeRef::OnDisk(node) => node.entry(),
381 381 }
382 382 }
383 383
384 384 pub(super) fn cached_directory_mtime(
385 385 &self,
386 386 ) -> Result<Option<TruncatedTimestamp>, DirstateV2ParseError> {
387 387 match self {
388 388 NodeRef::InMemory(_path, node) => Ok(match node.data {
389 389 NodeData::CachedDirectory { mtime } => Some(mtime),
390 390 _ => None,
391 391 }),
392 392 NodeRef::OnDisk(node) => node.cached_directory_mtime(),
393 393 }
394 394 }
395 395
396 396 pub(super) fn descendants_with_entry_count(&self) -> u32 {
397 397 match self {
398 398 NodeRef::InMemory(_path, node) => {
399 399 node.descendants_with_entry_count
400 400 }
401 401 NodeRef::OnDisk(node) => node.descendants_with_entry_count.get(),
402 402 }
403 403 }
404 404
405 405 pub(super) fn tracked_descendants_count(&self) -> u32 {
406 406 match self {
407 407 NodeRef::InMemory(_path, node) => node.tracked_descendants_count,
408 408 NodeRef::OnDisk(node) => node.tracked_descendants_count.get(),
409 409 }
410 410 }
411 411 }
412 412
413 413 /// Represents a file or a directory
414 414 #[derive(Default, Debug)]
415 415 pub(super) struct Node<'on_disk> {
416 416 pub(super) data: NodeData,
417 417
418 418 pub(super) copy_source: Option<Cow<'on_disk, HgPath>>,
419 419
420 420 pub(super) children: ChildNodes<'on_disk>,
421 421
422 422 /// How many (non-inclusive) descendants of this node have an entry.
423 423 pub(super) descendants_with_entry_count: u32,
424 424
425 425 /// How many (non-inclusive) descendants of this node have an entry whose
426 426 /// state is "tracked".
427 427 pub(super) tracked_descendants_count: u32,
428 428 }
429 429
430 430 #[derive(Debug)]
431 431 pub(super) enum NodeData {
432 432 Entry(DirstateEntry),
433 433 CachedDirectory { mtime: TruncatedTimestamp },
434 434 None,
435 435 }
436 436
437 437 impl Default for NodeData {
438 438 fn default() -> Self {
439 439 NodeData::None
440 440 }
441 441 }
442 442
443 443 impl NodeData {
444 444 fn has_entry(&self) -> bool {
445 445 matches!(self, NodeData::Entry(_))
446 446 }
447 447
448 448 fn as_entry(&self) -> Option<&DirstateEntry> {
449 449 match self {
450 450 NodeData::Entry(entry) => Some(entry),
451 451 _ => None,
452 452 }
453 453 }
454 454
455 455 fn as_entry_mut(&mut self) -> Option<&mut DirstateEntry> {
456 456 match self {
457 457 NodeData::Entry(entry) => Some(entry),
458 458 _ => None,
459 459 }
460 460 }
461 461 }
462 462
463 463 impl<'on_disk> DirstateMap<'on_disk> {
464 464 pub(super) fn empty(on_disk: &'on_disk [u8]) -> Self {
465 465 Self {
466 466 on_disk,
467 467 root: ChildNodes::default(),
468 468 nodes_with_entry_count: 0,
469 469 nodes_with_copy_source_count: 0,
470 470 ignore_patterns_hash: [0; on_disk::IGNORE_PATTERNS_HASH_LEN],
471 471 unreachable_bytes: 0,
472 472 old_data_size: 0,
473 473 old_uuid: None,
474 474 identity: None,
475 475 dirstate_version: DirstateVersion::V1,
476 476 write_mode: DirstateMapWriteMode::Auto,
477 477 }
478 478 }
479 479
480 480 #[logging_timer::time("trace")]
481 481 pub fn new_v2(
482 482 on_disk: &'on_disk [u8],
483 483 data_size: usize,
484 484 metadata: &[u8],
485 485 uuid: Vec<u8>,
486 486 identity: Option<u64>,
487 487 ) -> Result<Self, DirstateError> {
488 488 if let Some(data) = on_disk.get(..data_size) {
489 489 Ok(on_disk::read(data, metadata, uuid, identity)?)
490 490 } else {
491 491 Err(DirstateV2ParseError::new("not enough bytes on disk").into())
492 492 }
493 493 }
494 494
495 495 #[logging_timer::time("trace")]
496 496 pub fn new_v1(
497 497 on_disk: &'on_disk [u8],
498 498 identity: Option<u64>,
499 499 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
500 500 let mut map = Self::empty(on_disk);
501 501 if map.on_disk.is_empty() {
502 502 return Ok((map, None));
503 503 }
504 504
505 505 let parents = parse_dirstate_entries(
506 506 map.on_disk,
507 507 |path, entry, copy_source| {
508 508 let tracked = entry.tracked();
509 509 let node = Self::get_or_insert_node_inner(
510 510 map.on_disk,
511 511 &mut map.unreachable_bytes,
512 512 &mut map.root,
513 513 path,
514 514 WithBasename::to_cow_borrowed,
515 515 |ancestor| {
516 516 if tracked {
517 517 ancestor.tracked_descendants_count += 1
518 518 }
519 519 ancestor.descendants_with_entry_count += 1
520 520 },
521 521 )?;
522 522 assert!(
523 523 !node.data.has_entry(),
524 524 "duplicate dirstate entry in read"
525 525 );
526 526 assert!(
527 527 node.copy_source.is_none(),
528 528 "duplicate dirstate entry in read"
529 529 );
530 530 node.data = NodeData::Entry(*entry);
531 531 node.copy_source = copy_source.map(Cow::Borrowed);
532 532 map.nodes_with_entry_count += 1;
533 533 if copy_source.is_some() {
534 534 map.nodes_with_copy_source_count += 1
535 535 }
536 536 Ok(())
537 537 },
538 538 )?;
539 539 let parents = Some(*parents);
540 540 map.identity = identity;
541 541
542 542 Ok((map, parents))
543 543 }
544 544
545 545 /// Assuming dirstate-v2 format, returns whether the next write should
546 546 /// append to the existing data file that contains `self.on_disk` (true),
547 547 /// or create a new data file from scratch (false).
548 548 pub(super) fn write_should_append(&self) -> bool {
549 549 match self.write_mode {
550 550 DirstateMapWriteMode::ForceAppend => true,
551 551 DirstateMapWriteMode::ForceNewDataFile => false,
552 552 DirstateMapWriteMode::Auto => {
553 553 let ratio =
554 554 self.unreachable_bytes as f32 / self.on_disk.len() as f32;
555 555 ratio < ACCEPTABLE_UNREACHABLE_BYTES_RATIO
556 556 }
557 557 }
558 558 }
559 559
560 560 fn get_node<'tree>(
561 561 &'tree self,
562 562 path: &HgPath,
563 563 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
564 564 let mut children = self.root.as_ref();
565 565 let mut components = path.components();
566 566 let mut component =
567 567 components.next().expect("expected at least one components");
568 568 loop {
569 569 if let Some(child) = children.get(component, self.on_disk)? {
570 570 if let Some(next_component) = components.next() {
571 571 component = next_component;
572 572 children = child.children(self.on_disk)?;
573 573 } else {
574 574 return Ok(Some(child));
575 575 }
576 576 } else {
577 577 return Ok(None);
578 578 }
579 579 }
580 580 }
581 581
582 582 /// Returns a mutable reference to the node at `path` if it exists
583 583 ///
584 584 /// `each_ancestor` is a callback that is called for each ancestor node
585 585 /// when descending the tree. It is used to keep the different counters
586 586 /// of the `DirstateMap` up-to-date.
587 587 fn get_node_mut<'tree>(
588 588 &'tree mut self,
589 589 path: &HgPath,
590 590 each_ancestor: impl FnMut(&mut Node),
591 591 ) -> Result<Option<&'tree mut Node<'on_disk>>, DirstateV2ParseError> {
592 592 Self::get_node_mut_inner(
593 593 self.on_disk,
594 594 &mut self.unreachable_bytes,
595 595 &mut self.root,
596 596 path,
597 597 each_ancestor,
598 598 )
599 599 }
600 600
601 601 /// Lower-level version of `get_node_mut`.
602 602 ///
603 603 /// This takes `root` instead of `&mut self` so that callers can mutate
604 604 /// other fields while the returned borrow is still valid.
605 605 ///
606 606 /// `each_ancestor` is a callback that is called for each ancestor node
607 607 /// when descending the tree. It is used to keep the different counters
608 608 /// of the `DirstateMap` up-to-date.
609 609 fn get_node_mut_inner<'tree>(
610 610 on_disk: &'on_disk [u8],
611 611 unreachable_bytes: &mut u32,
612 612 root: &'tree mut ChildNodes<'on_disk>,
613 613 path: &HgPath,
614 614 mut each_ancestor: impl FnMut(&mut Node),
615 615 ) -> Result<Option<&'tree mut Node<'on_disk>>, DirstateV2ParseError> {
616 616 let mut children = root;
617 617 let mut components = path.components();
618 618 let mut component =
619 619 components.next().expect("expected at least one components");
620 620 loop {
621 621 if let Some(child) = children
622 622 .make_mut(on_disk, unreachable_bytes)?
623 623 .get_mut(component)
624 624 {
625 625 if let Some(next_component) = components.next() {
626 626 each_ancestor(child);
627 627 component = next_component;
628 628 children = &mut child.children;
629 629 } else {
630 630 return Ok(Some(child));
631 631 }
632 632 } else {
633 633 return Ok(None);
634 634 }
635 635 }
636 636 }
637 637
638 638 /// Get a mutable reference to the node at `path`, creating it if it does
639 639 /// not exist.
640 640 ///
641 641 /// `each_ancestor` is a callback that is called for each ancestor node
642 642 /// when descending the tree. It is used to keep the different counters
643 643 /// of the `DirstateMap` up-to-date.
644 644 fn get_or_insert_node<'tree, 'path>(
645 645 &'tree mut self,
646 646 path: &'path HgPath,
647 647 each_ancestor: impl FnMut(&mut Node),
648 648 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
649 649 Self::get_or_insert_node_inner(
650 650 self.on_disk,
651 651 &mut self.unreachable_bytes,
652 652 &mut self.root,
653 653 path,
654 654 WithBasename::to_cow_owned,
655 655 each_ancestor,
656 656 )
657 657 }
658 658
659 659 /// Lower-level version of `get_or_insert_node_inner`, which is used when
660 660 /// parsing disk data to remove allocations for new nodes.
661 661 fn get_or_insert_node_inner<'tree, 'path>(
662 662 on_disk: &'on_disk [u8],
663 663 unreachable_bytes: &mut u32,
664 664 root: &'tree mut ChildNodes<'on_disk>,
665 665 path: &'path HgPath,
666 666 to_cow: impl Fn(
667 667 WithBasename<&'path HgPath>,
668 668 ) -> WithBasename<Cow<'on_disk, HgPath>>,
669 669 mut each_ancestor: impl FnMut(&mut Node),
670 670 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
671 671 let mut child_nodes = root;
672 672 let mut inclusive_ancestor_paths =
673 673 WithBasename::inclusive_ancestors_of(path);
674 674 let mut ancestor_path = inclusive_ancestor_paths
675 675 .next()
676 676 .expect("expected at least one inclusive ancestor");
677 677 loop {
678 678 let (_, child_node) = child_nodes
679 679 .make_mut(on_disk, unreachable_bytes)?
680 680 .raw_entry_mut()
681 681 .from_key(ancestor_path.base_name())
682 682 .or_insert_with(|| (to_cow(ancestor_path), Node::default()));
683 683 if let Some(next) = inclusive_ancestor_paths.next() {
684 684 each_ancestor(child_node);
685 685 ancestor_path = next;
686 686 child_nodes = &mut child_node.children;
687 687 } else {
688 688 return Ok(child_node);
689 689 }
690 690 }
691 691 }
692 692
693 693 #[allow(clippy::too_many_arguments)]
694 694 fn reset_state(
695 695 &mut self,
696 696 filename: &HgPath,
697 697 old_entry_opt: Option<DirstateEntry>,
698 698 wc_tracked: bool,
699 699 p1_tracked: bool,
700 700 p2_info: bool,
701 701 has_meaningful_mtime: bool,
702 702 parent_file_data_opt: Option<ParentFileData>,
703 703 ) -> Result<(), DirstateError> {
704 704 let (had_entry, was_tracked) = match old_entry_opt {
705 705 Some(old_entry) => (true, old_entry.tracked()),
706 706 None => (false, false),
707 707 };
708 708 let node = self.get_or_insert_node(filename, |ancestor| {
709 709 if !had_entry {
710 710 ancestor.descendants_with_entry_count += 1;
711 711 }
712 712 if was_tracked {
713 713 if !wc_tracked {
714 714 ancestor.tracked_descendants_count = ancestor
715 715 .tracked_descendants_count
716 716 .checked_sub(1)
717 717 .expect("tracked count to be >= 0");
718 718 }
719 719 } else if wc_tracked {
720 720 ancestor.tracked_descendants_count += 1;
721 721 }
722 722 })?;
723 723
724 724 let v2_data = if let Some(parent_file_data) = parent_file_data_opt {
725 725 DirstateV2Data {
726 726 wc_tracked,
727 727 p1_tracked,
728 728 p2_info,
729 729 mode_size: parent_file_data.mode_size,
730 730 mtime: if has_meaningful_mtime {
731 731 parent_file_data.mtime
732 732 } else {
733 733 None
734 734 },
735 735 ..Default::default()
736 736 }
737 737 } else {
738 738 DirstateV2Data {
739 739 wc_tracked,
740 740 p1_tracked,
741 741 p2_info,
742 742 ..Default::default()
743 743 }
744 744 };
745 745 node.data = NodeData::Entry(DirstateEntry::from_v2_data(v2_data));
746 746 if !had_entry {
747 747 self.nodes_with_entry_count += 1;
748 748 }
749 749 Ok(())
750 750 }
751 751
752 752 fn set_tracked(
753 753 &mut self,
754 754 filename: &HgPath,
755 755 old_entry_opt: Option<DirstateEntry>,
756 756 ) -> Result<bool, DirstateV2ParseError> {
757 757 let was_tracked = old_entry_opt.map_or(false, |e| e.tracked());
758 758 let had_entry = old_entry_opt.is_some();
759 759 let tracked_count_increment = if was_tracked { 0 } else { 1 };
760 760 let mut new = false;
761 761
762 762 let node = self.get_or_insert_node(filename, |ancestor| {
763 763 if !had_entry {
764 764 ancestor.descendants_with_entry_count += 1;
765 765 }
766 766
767 767 ancestor.tracked_descendants_count += tracked_count_increment;
768 768 })?;
769 769 if let Some(old_entry) = old_entry_opt {
770 770 let mut e = old_entry;
771 771 if e.tracked() {
772 772 // XXX
773 773 // This is probably overkill for more case, but we need this to
774 774 // fully replace the `normallookup` call with `set_tracked`
775 775 // one. Consider smoothing this in the future.
776 776 e.set_possibly_dirty();
777 777 } else {
778 778 new = true;
779 779 e.set_tracked();
780 780 }
781 781 node.data = NodeData::Entry(e)
782 782 } else {
783 783 node.data = NodeData::Entry(DirstateEntry::new_tracked());
784 784 self.nodes_with_entry_count += 1;
785 785 new = true;
786 786 };
787 787 Ok(new)
788 788 }
789 789
790 790 /// Set a node as untracked in the dirstate.
791 791 ///
792 792 /// It is the responsibility of the caller to remove the copy source and/or
793 793 /// the entry itself if appropriate.
794 794 ///
795 795 /// # Panics
796 796 ///
797 797 /// Panics if the node does not exist.
798 798 fn set_untracked(
799 799 &mut self,
800 800 filename: &HgPath,
801 801 old_entry: DirstateEntry,
802 802 ) -> Result<(), DirstateV2ParseError> {
803 803 let node = self
804 804 .get_node_mut(filename, |ancestor| {
805 805 ancestor.tracked_descendants_count = ancestor
806 806 .tracked_descendants_count
807 807 .checked_sub(1)
808 808 .expect("tracked_descendants_count should be >= 0");
809 809 })?
810 810 .expect("node should exist");
811 811 let mut new_entry = old_entry;
812 812 new_entry.set_untracked();
813 813 node.data = NodeData::Entry(new_entry);
814 814 Ok(())
815 815 }
816 816
817 817 /// Set a node as clean in the dirstate.
818 818 ///
819 819 /// It is the responsibility of the caller to remove the copy source.
820 820 ///
821 821 /// # Panics
822 822 ///
823 823 /// Panics if the node does not exist.
824 824 fn set_clean(
825 825 &mut self,
826 826 filename: &HgPath,
827 827 old_entry: DirstateEntry,
828 828 mode: u32,
829 829 size: u32,
830 830 mtime: TruncatedTimestamp,
831 831 ) -> Result<(), DirstateError> {
832 832 let node = self
833 833 .get_node_mut(filename, |ancestor| {
834 834 if !old_entry.tracked() {
835 835 ancestor.tracked_descendants_count += 1;
836 836 }
837 837 })?
838 838 .expect("node should exist");
839 839 let mut new_entry = old_entry;
840 840 new_entry.set_clean(mode, size, mtime);
841 841 node.data = NodeData::Entry(new_entry);
842 842 Ok(())
843 843 }
844 844
845 845 /// Set a node as possibly dirty in the dirstate.
846 846 ///
847 847 /// # Panics
848 848 ///
849 849 /// Panics if the node does not exist.
850 850 fn set_possibly_dirty(
851 851 &mut self,
852 852 filename: &HgPath,
853 853 ) -> Result<(), DirstateError> {
854 854 let node = self
855 855 .get_node_mut(filename, |_ancestor| {})?
856 856 .expect("node should exist");
857 857 let entry = node.data.as_entry_mut().expect("entry should exist");
858 858 entry.set_possibly_dirty();
859 859 node.data = NodeData::Entry(*entry);
860 860 Ok(())
861 861 }
862 862
863 863 /// Clears the cached mtime for the (potential) folder at `path`.
864 864 pub(super) fn clear_cached_mtime(
865 865 &mut self,
866 866 path: &HgPath,
867 867 ) -> Result<(), DirstateV2ParseError> {
868 868 let node = match self.get_node_mut(path, |_ancestor| {})? {
869 869 Some(node) => node,
870 870 None => return Ok(()),
871 871 };
872 872 if let NodeData::CachedDirectory { .. } = &node.data {
873 873 node.data = NodeData::None
874 874 }
875 875 Ok(())
876 876 }
877 877
878 878 /// Sets the cached mtime for the (potential) folder at `path`.
879 879 pub(super) fn set_cached_mtime(
880 880 &mut self,
881 881 path: &HgPath,
882 882 mtime: TruncatedTimestamp,
883 883 ) -> Result<(), DirstateV2ParseError> {
884 884 let node = match self.get_node_mut(path, |_ancestor| {})? {
885 885 Some(node) => node,
886 886 None => return Ok(()),
887 887 };
888 888 match &node.data {
889 889 NodeData::Entry(_) => {} // Don’t overwrite an entry
890 890 NodeData::CachedDirectory { .. } | NodeData::None => {
891 891 node.data = NodeData::CachedDirectory { mtime }
892 892 }
893 893 }
894 894 Ok(())
895 895 }
896 896
897 897 fn iter_nodes<'tree>(
898 898 &'tree self,
899 899 ) -> impl Iterator<
900 900 Item = Result<NodeRef<'tree, 'on_disk>, DirstateV2ParseError>,
901 901 > + 'tree {
902 902 // Depth first tree traversal.
903 903 //
904 904 // If we could afford internal iteration and recursion,
905 905 // this would look like:
906 906 //
907 907 // ```
908 908 // fn traverse_children(
909 909 // children: &ChildNodes,
910 910 // each: &mut impl FnMut(&Node),
911 911 // ) {
912 912 // for child in children.values() {
913 913 // traverse_children(&child.children, each);
914 914 // each(child);
915 915 // }
916 916 // }
917 917 // ```
918 918 //
919 919 // However we want an external iterator and therefore can’t use the
920 920 // call stack. Use an explicit stack instead:
921 921 let mut stack = Vec::new();
922 922 let mut iter = self.root.as_ref().iter();
923 923 std::iter::from_fn(move || {
924 924 while let Some(child_node) = iter.next() {
925 925 let children = match child_node.children(self.on_disk) {
926 926 Ok(children) => children,
927 927 Err(error) => return Some(Err(error)),
928 928 };
929 929 // Pseudo-recursion
930 930 let new_iter = children.iter();
931 931 let old_iter = std::mem::replace(&mut iter, new_iter);
932 932 stack.push((child_node, old_iter));
933 933 }
934 934 // Found the end of a `children.iter()` iterator.
935 935 if let Some((child_node, next_iter)) = stack.pop() {
936 936 // "Return" from pseudo-recursion by restoring state from the
937 937 // explicit stack
938 938 iter = next_iter;
939 939
940 940 Some(Ok(child_node))
941 941 } else {
942 942 // Reached the bottom of the stack, we’re done
943 943 None
944 944 }
945 945 })
946 946 }
947 947
948 948 fn count_dropped_path(unreachable_bytes: &mut u32, path: Cow<HgPath>) {
949 949 if let Cow::Borrowed(path) = path {
950 950 *unreachable_bytes += path.len() as u32
951 951 }
952 952 }
953 953
954 954 pub(crate) fn set_write_mode(&mut self, write_mode: DirstateMapWriteMode) {
955 955 self.write_mode = write_mode;
956 956 }
957 957 }
958 958
959 959 type DebugDirstateTuple<'a> = (&'a HgPath, (u8, i32, i32, i32));
960 960
961 961 impl OwningDirstateMap {
962 962 pub fn clear(&mut self) {
963 963 self.with_dmap_mut(|map| {
964 964 map.root = Default::default();
965 965 map.nodes_with_entry_count = 0;
966 966 map.nodes_with_copy_source_count = 0;
967 map.unreachable_bytes = map.on_disk.len() as u32;
967 968 });
968 969 }
969 970
970 971 pub fn set_tracked(
971 972 &mut self,
972 973 filename: &HgPath,
973 974 ) -> Result<bool, DirstateV2ParseError> {
974 975 let old_entry_opt = self.get(filename)?;
975 976 self.with_dmap_mut(|map| map.set_tracked(filename, old_entry_opt))
976 977 }
977 978
978 979 pub fn set_untracked(
979 980 &mut self,
980 981 filename: &HgPath,
981 982 ) -> Result<bool, DirstateError> {
982 983 let old_entry_opt = self.get(filename)?;
983 984 match old_entry_opt {
984 985 None => Ok(false),
985 986 Some(old_entry) => {
986 987 if !old_entry.tracked() {
987 988 // `DirstateMap::set_untracked` is not a noop if
988 989 // already not tracked as it will decrement the
989 990 // tracked counters while going down.
990 991 return Ok(true);
991 992 }
992 993 if old_entry.added() {
993 994 // Untracking an "added" entry will just result in a
994 995 // worthless entry (and other parts of the code will
995 996 // complain about it), just drop it entirely.
996 997 self.drop_entry_and_copy_source(filename)?;
997 998 return Ok(true);
998 999 }
999 1000 if !old_entry.p2_info() {
1000 1001 self.copy_map_remove(filename)?;
1001 1002 }
1002 1003
1003 1004 self.with_dmap_mut(|map| {
1004 1005 map.set_untracked(filename, old_entry)?;
1005 1006 Ok(true)
1006 1007 })
1007 1008 }
1008 1009 }
1009 1010 }
1010 1011
1011 1012 pub fn set_clean(
1012 1013 &mut self,
1013 1014 filename: &HgPath,
1014 1015 mode: u32,
1015 1016 size: u32,
1016 1017 mtime: TruncatedTimestamp,
1017 1018 ) -> Result<(), DirstateError> {
1018 1019 let old_entry = match self.get(filename)? {
1019 1020 None => {
1020 1021 return Err(
1021 1022 DirstateMapError::PathNotFound(filename.into()).into()
1022 1023 )
1023 1024 }
1024 1025 Some(e) => e,
1025 1026 };
1026 1027 self.copy_map_remove(filename)?;
1027 1028 self.with_dmap_mut(|map| {
1028 1029 map.set_clean(filename, old_entry, mode, size, mtime)
1029 1030 })
1030 1031 }
1031 1032
1032 1033 pub fn set_possibly_dirty(
1033 1034 &mut self,
1034 1035 filename: &HgPath,
1035 1036 ) -> Result<(), DirstateError> {
1036 1037 if self.get(filename)?.is_none() {
1037 1038 return Err(DirstateMapError::PathNotFound(filename.into()).into());
1038 1039 }
1039 1040 self.with_dmap_mut(|map| map.set_possibly_dirty(filename))
1040 1041 }
1041 1042
1042 1043 pub fn reset_state(
1043 1044 &mut self,
1044 1045 filename: &HgPath,
1045 1046 wc_tracked: bool,
1046 1047 p1_tracked: bool,
1047 1048 p2_info: bool,
1048 1049 has_meaningful_mtime: bool,
1049 1050 parent_file_data_opt: Option<ParentFileData>,
1050 1051 ) -> Result<(), DirstateError> {
1051 1052 if !(p1_tracked || p2_info || wc_tracked) {
1052 1053 self.drop_entry_and_copy_source(filename)?;
1053 1054 return Ok(());
1054 1055 }
1055 1056 self.copy_map_remove(filename)?;
1056 1057 let old_entry_opt = self.get(filename)?;
1057 1058 self.with_dmap_mut(|map| {
1058 1059 map.reset_state(
1059 1060 filename,
1060 1061 old_entry_opt,
1061 1062 wc_tracked,
1062 1063 p1_tracked,
1063 1064 p2_info,
1064 1065 has_meaningful_mtime,
1065 1066 parent_file_data_opt,
1066 1067 )
1067 1068 })
1068 1069 }
1069 1070
1070 1071 pub fn drop_entry_and_copy_source(
1071 1072 &mut self,
1072 1073 filename: &HgPath,
1073 1074 ) -> Result<(), DirstateError> {
1074 1075 let was_tracked = self.get(filename)?.map_or(false, |e| e.tracked());
1075 1076 struct Dropped {
1076 1077 was_tracked: bool,
1077 1078 had_entry: bool,
1078 1079 had_copy_source: bool,
1079 1080 }
1080 1081
1081 1082 /// If this returns `Ok(Some((dropped, removed)))`, then
1082 1083 ///
1083 1084 /// * `dropped` is about the leaf node that was at `filename`
1084 1085 /// * `removed` is whether this particular level of recursion just
1085 1086 /// removed a node in `nodes`.
1086 1087 fn recur<'on_disk>(
1087 1088 on_disk: &'on_disk [u8],
1088 1089 unreachable_bytes: &mut u32,
1089 1090 nodes: &mut ChildNodes<'on_disk>,
1090 1091 path: &HgPath,
1091 1092 ) -> Result<Option<(Dropped, bool)>, DirstateV2ParseError> {
1092 1093 let (first_path_component, rest_of_path) =
1093 1094 path.split_first_component();
1094 1095 let nodes = nodes.make_mut(on_disk, unreachable_bytes)?;
1095 1096 let node = if let Some(node) = nodes.get_mut(first_path_component)
1096 1097 {
1097 1098 node
1098 1099 } else {
1099 1100 return Ok(None);
1100 1101 };
1101 1102 let dropped;
1102 1103 if let Some(rest) = rest_of_path {
1103 1104 if let Some((d, removed)) = recur(
1104 1105 on_disk,
1105 1106 unreachable_bytes,
1106 1107 &mut node.children,
1107 1108 rest,
1108 1109 )? {
1109 1110 dropped = d;
1110 1111 if dropped.had_entry {
1111 1112 node.descendants_with_entry_count = node
1112 1113 .descendants_with_entry_count
1113 1114 .checked_sub(1)
1114 1115 .expect(
1115 1116 "descendants_with_entry_count should be >= 0",
1116 1117 );
1117 1118 }
1118 1119 if dropped.was_tracked {
1119 1120 node.tracked_descendants_count = node
1120 1121 .tracked_descendants_count
1121 1122 .checked_sub(1)
1122 1123 .expect(
1123 1124 "tracked_descendants_count should be >= 0",
1124 1125 );
1125 1126 }
1126 1127
1127 1128 // Directory caches must be invalidated when removing a
1128 1129 // child node
1129 1130 if removed {
1130 1131 if let NodeData::CachedDirectory { .. } = &node.data {
1131 1132 node.data = NodeData::None
1132 1133 }
1133 1134 }
1134 1135 } else {
1135 1136 return Ok(None);
1136 1137 }
1137 1138 } else {
1138 1139 let entry = node.data.as_entry();
1139 1140 let was_tracked = entry.map_or(false, |entry| entry.tracked());
1140 1141 let had_entry = entry.is_some();
1141 1142 if had_entry {
1142 1143 node.data = NodeData::None
1143 1144 }
1144 1145 let mut had_copy_source = false;
1145 1146 if let Some(source) = &node.copy_source {
1146 1147 DirstateMap::count_dropped_path(
1147 1148 unreachable_bytes,
1148 1149 Cow::Borrowed(source),
1149 1150 );
1150 1151 had_copy_source = true;
1151 1152 node.copy_source = None
1152 1153 }
1153 1154 dropped = Dropped {
1154 1155 was_tracked,
1155 1156 had_entry,
1156 1157 had_copy_source,
1157 1158 };
1158 1159 }
1159 1160 // After recursion, for both leaf (rest_of_path is None) nodes and
1160 1161 // parent nodes, remove a node if it just became empty.
1161 1162 let remove = !node.data.has_entry()
1162 1163 && node.copy_source.is_none()
1163 1164 && node.children.is_empty();
1164 1165 if remove {
1165 1166 let (key, _) =
1166 1167 nodes.remove_entry(first_path_component).unwrap();
1167 1168 DirstateMap::count_dropped_path(
1168 1169 unreachable_bytes,
1169 1170 Cow::Borrowed(key.full_path()),
1170 1171 )
1171 1172 }
1172 1173 Ok(Some((dropped, remove)))
1173 1174 }
1174 1175
1175 1176 self.with_dmap_mut(|map| {
1176 1177 if let Some((dropped, _removed)) = recur(
1177 1178 map.on_disk,
1178 1179 &mut map.unreachable_bytes,
1179 1180 &mut map.root,
1180 1181 filename,
1181 1182 )? {
1182 1183 if dropped.had_entry {
1183 1184 map.nodes_with_entry_count = map
1184 1185 .nodes_with_entry_count
1185 1186 .checked_sub(1)
1186 1187 .expect("nodes_with_entry_count should be >= 0");
1187 1188 }
1188 1189 if dropped.had_copy_source {
1189 1190 map.nodes_with_copy_source_count = map
1190 1191 .nodes_with_copy_source_count
1191 1192 .checked_sub(1)
1192 1193 .expect("nodes_with_copy_source_count should be >= 0");
1193 1194 }
1194 1195 } else {
1195 1196 debug_assert!(!was_tracked);
1196 1197 }
1197 1198 Ok(())
1198 1199 })
1199 1200 }
1200 1201
1201 1202 pub fn has_tracked_dir(
1202 1203 &mut self,
1203 1204 directory: &HgPath,
1204 1205 ) -> Result<bool, DirstateError> {
1205 1206 self.with_dmap_mut(|map| {
1206 1207 if let Some(node) = map.get_node(directory)? {
1207 1208 // A node without a `DirstateEntry` was created to hold child
1208 1209 // nodes, and is therefore a directory.
1209 1210 let is_dir = node.entry()?.is_none();
1210 1211 Ok(is_dir && node.tracked_descendants_count() > 0)
1211 1212 } else {
1212 1213 Ok(false)
1213 1214 }
1214 1215 })
1215 1216 }
1216 1217
1217 1218 pub fn has_dir(
1218 1219 &mut self,
1219 1220 directory: &HgPath,
1220 1221 ) -> Result<bool, DirstateError> {
1221 1222 self.with_dmap_mut(|map| {
1222 1223 if let Some(node) = map.get_node(directory)? {
1223 1224 // A node without a `DirstateEntry` was created to hold child
1224 1225 // nodes, and is therefore a directory.
1225 1226 let is_dir = node.entry()?.is_none();
1226 1227 Ok(is_dir && node.descendants_with_entry_count() > 0)
1227 1228 } else {
1228 1229 Ok(false)
1229 1230 }
1230 1231 })
1231 1232 }
1232 1233
1233 1234 #[logging_timer::time("trace")]
1234 1235 pub fn pack_v1(
1235 1236 &self,
1236 1237 parents: DirstateParents,
1237 1238 ) -> Result<Vec<u8>, DirstateError> {
1238 1239 let map = self.get_map();
1239 1240 // Optizimation (to be measured?): pre-compute size to avoid `Vec`
1240 1241 // reallocations
1241 1242 let mut size = parents.as_bytes().len();
1242 1243 for node in map.iter_nodes() {
1243 1244 let node = node?;
1244 1245 if node.entry()?.is_some() {
1245 1246 size += packed_entry_size(
1246 1247 node.full_path(map.on_disk)?,
1247 1248 node.copy_source(map.on_disk)?,
1248 1249 );
1249 1250 }
1250 1251 }
1251 1252
1252 1253 let mut packed = Vec::with_capacity(size);
1253 1254 packed.extend(parents.as_bytes());
1254 1255
1255 1256 for node in map.iter_nodes() {
1256 1257 let node = node?;
1257 1258 if let Some(entry) = node.entry()? {
1258 1259 pack_entry(
1259 1260 node.full_path(map.on_disk)?,
1260 1261 &entry,
1261 1262 node.copy_source(map.on_disk)?,
1262 1263 &mut packed,
1263 1264 );
1264 1265 }
1265 1266 }
1266 1267 Ok(packed)
1267 1268 }
1268 1269
1269 1270 /// Returns new data and metadata together with whether that data should be
1270 1271 /// appended to the existing data file whose content is at
1271 1272 /// `map.on_disk` (true), instead of written to a new data file
1272 1273 /// (false), and the previous size of data on disk.
1273 1274 #[logging_timer::time("trace")]
1274 1275 pub fn pack_v2(
1275 1276 &self,
1276 1277 write_mode: DirstateMapWriteMode,
1277 1278 ) -> Result<(Vec<u8>, on_disk::TreeMetadata, bool, usize), DirstateError>
1278 1279 {
1279 1280 let map = self.get_map();
1280 1281 on_disk::write(map, write_mode)
1281 1282 }
1282 1283
1283 1284 /// `callback` allows the caller to process and do something with the
1284 1285 /// results of the status. This is needed to do so efficiently (i.e.
1285 1286 /// without cloning the `DirstateStatus` object with its paths) because
1286 1287 /// we need to borrow from `Self`.
1287 1288 pub fn with_status<R>(
1288 1289 &mut self,
1289 1290 matcher: &(dyn Matcher + Sync),
1290 1291 root_dir: PathBuf,
1291 1292 ignore_files: Vec<PathBuf>,
1292 1293 options: StatusOptions,
1293 1294 callback: impl for<'r> FnOnce(
1294 1295 Result<(DirstateStatus<'r>, Vec<PatternFileWarning>), StatusError>,
1295 1296 ) -> R,
1296 1297 ) -> R {
1297 1298 self.with_dmap_mut(|map| {
1298 1299 callback(super::status::status(
1299 1300 map,
1300 1301 matcher,
1301 1302 root_dir,
1302 1303 ignore_files,
1303 1304 options,
1304 1305 ))
1305 1306 })
1306 1307 }
1307 1308
1308 1309 pub fn copy_map_len(&self) -> usize {
1309 1310 let map = self.get_map();
1310 1311 map.nodes_with_copy_source_count as usize
1311 1312 }
1312 1313
1313 1314 pub fn copy_map_iter(&self) -> CopyMapIter<'_> {
1314 1315 let map = self.get_map();
1315 1316 Box::new(filter_map_results(map.iter_nodes(), move |node| {
1316 1317 Ok(if let Some(source) = node.copy_source(map.on_disk)? {
1317 1318 Some((node.full_path(map.on_disk)?, source))
1318 1319 } else {
1319 1320 None
1320 1321 })
1321 1322 }))
1322 1323 }
1323 1324
1324 1325 pub fn copy_map_contains_key(
1325 1326 &self,
1326 1327 key: &HgPath,
1327 1328 ) -> Result<bool, DirstateV2ParseError> {
1328 1329 let map = self.get_map();
1329 1330 Ok(if let Some(node) = map.get_node(key)? {
1330 1331 node.has_copy_source()
1331 1332 } else {
1332 1333 false
1333 1334 })
1334 1335 }
1335 1336
1336 1337 pub fn copy_map_get(
1337 1338 &self,
1338 1339 key: &HgPath,
1339 1340 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
1340 1341 let map = self.get_map();
1341 1342 if let Some(node) = map.get_node(key)? {
1342 1343 if let Some(source) = node.copy_source(map.on_disk)? {
1343 1344 return Ok(Some(source));
1344 1345 }
1345 1346 }
1346 1347 Ok(None)
1347 1348 }
1348 1349
1349 1350 pub fn copy_map_remove(
1350 1351 &mut self,
1351 1352 key: &HgPath,
1352 1353 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1353 1354 self.with_dmap_mut(|map| {
1354 1355 let count = &mut map.nodes_with_copy_source_count;
1355 1356 let unreachable_bytes = &mut map.unreachable_bytes;
1356 1357 Ok(DirstateMap::get_node_mut_inner(
1357 1358 map.on_disk,
1358 1359 unreachable_bytes,
1359 1360 &mut map.root,
1360 1361 key,
1361 1362 |_ancestor| {},
1362 1363 )?
1363 1364 .and_then(|node| {
1364 1365 if let Some(source) = &node.copy_source {
1365 1366 *count = count
1366 1367 .checked_sub(1)
1367 1368 .expect("nodes_with_copy_source_count should be >= 0");
1368 1369 DirstateMap::count_dropped_path(
1369 1370 unreachable_bytes,
1370 1371 Cow::Borrowed(source),
1371 1372 );
1372 1373 }
1373 1374 node.copy_source.take().map(Cow::into_owned)
1374 1375 }))
1375 1376 })
1376 1377 }
1377 1378
1378 1379 pub fn copy_map_insert(
1379 1380 &mut self,
1380 1381 key: &HgPath,
1381 1382 value: &HgPath,
1382 1383 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1383 1384 self.with_dmap_mut(|map| {
1384 1385 let node = map.get_or_insert_node(key, |_ancestor| {})?;
1385 1386 let had_copy_source = node.copy_source.is_none();
1386 1387 let old = node
1387 1388 .copy_source
1388 1389 .replace(value.to_owned().into())
1389 1390 .map(Cow::into_owned);
1390 1391 if had_copy_source {
1391 1392 map.nodes_with_copy_source_count += 1
1392 1393 }
1393 1394 Ok(old)
1394 1395 })
1395 1396 }
1396 1397
1397 1398 pub fn len(&self) -> usize {
1398 1399 let map = self.get_map();
1399 1400 map.nodes_with_entry_count as usize
1400 1401 }
1401 1402
1402 1403 pub fn is_empty(&self) -> bool {
1403 1404 self.len() == 0
1404 1405 }
1405 1406
1406 1407 pub fn contains_key(
1407 1408 &self,
1408 1409 key: &HgPath,
1409 1410 ) -> Result<bool, DirstateV2ParseError> {
1410 1411 Ok(self.get(key)?.is_some())
1411 1412 }
1412 1413
1413 1414 pub fn get(
1414 1415 &self,
1415 1416 key: &HgPath,
1416 1417 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
1417 1418 let map = self.get_map();
1418 1419 Ok(if let Some(node) = map.get_node(key)? {
1419 1420 node.entry()?
1420 1421 } else {
1421 1422 None
1422 1423 })
1423 1424 }
1424 1425
1425 1426 pub fn iter(&self) -> StateMapIter<'_> {
1426 1427 let map = self.get_map();
1427 1428 Box::new(filter_map_results(map.iter_nodes(), move |node| {
1428 1429 Ok(if let Some(entry) = node.entry()? {
1429 1430 Some((node.full_path(map.on_disk)?, entry))
1430 1431 } else {
1431 1432 None
1432 1433 })
1433 1434 }))
1434 1435 }
1435 1436
1436 1437 pub fn iter_tracked_dirs(
1437 1438 &mut self,
1438 1439 ) -> Result<
1439 1440 Box<
1440 1441 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>>
1441 1442 + Send
1442 1443 + '_,
1443 1444 >,
1444 1445 DirstateError,
1445 1446 > {
1446 1447 let map = self.get_map();
1447 1448 let on_disk = map.on_disk;
1448 1449 Ok(Box::new(filter_map_results(
1449 1450 map.iter_nodes(),
1450 1451 move |node| {
1451 1452 Ok(if node.tracked_descendants_count() > 0 {
1452 1453 Some(node.full_path(on_disk)?)
1453 1454 } else {
1454 1455 None
1455 1456 })
1456 1457 },
1457 1458 )))
1458 1459 }
1459 1460
1460 1461 /// Only public because it needs to be exposed to the Python layer.
1461 1462 /// It is not the full `setparents` logic, only the parts that mutate the
1462 1463 /// entries.
1463 1464 pub fn setparents_fixup(
1464 1465 &mut self,
1465 1466 ) -> Result<Vec<(HgPathBuf, HgPathBuf)>, DirstateV2ParseError> {
1466 1467 // XXX
1467 1468 // All the copying and re-querying is quite inefficient, but this is
1468 1469 // still a lot better than doing it from Python.
1469 1470 //
1470 1471 // The better solution is to develop a mechanism for `iter_mut`,
1471 1472 // which will be a lot more involved: we're dealing with a lazy,
1472 1473 // append-mostly, tree-like data structure. This will do for now.
1473 1474 let mut copies = vec![];
1474 1475 let mut files_with_p2_info = vec![];
1475 1476 for res in self.iter() {
1476 1477 let (path, entry) = res?;
1477 1478 if entry.p2_info() {
1478 1479 files_with_p2_info.push(path.to_owned())
1479 1480 }
1480 1481 }
1481 1482 self.with_dmap_mut(|map| {
1482 1483 for path in files_with_p2_info.iter() {
1483 1484 let node = map.get_or_insert_node(path, |_| {})?;
1484 1485 let entry =
1485 1486 node.data.as_entry_mut().expect("entry should exist");
1486 1487 entry.drop_merge_data();
1487 1488 if let Some(source) = node.copy_source.take().as_deref() {
1488 1489 copies.push((path.to_owned(), source.to_owned()));
1489 1490 }
1490 1491 }
1491 1492 Ok(copies)
1492 1493 })
1493 1494 }
1494 1495
1495 1496 pub fn debug_iter(
1496 1497 &self,
1497 1498 all: bool,
1498 1499 ) -> Box<
1499 1500 dyn Iterator<Item = Result<DebugDirstateTuple, DirstateV2ParseError>>
1500 1501 + Send
1501 1502 + '_,
1502 1503 > {
1503 1504 let map = self.get_map();
1504 1505 Box::new(filter_map_results(map.iter_nodes(), move |node| {
1505 1506 let debug_tuple = if let Some(entry) = node.entry()? {
1506 1507 entry.debug_tuple()
1507 1508 } else if !all {
1508 1509 return Ok(None);
1509 1510 } else if let Some(mtime) = node.cached_directory_mtime()? {
1510 1511 (b' ', 0, -1, mtime.truncated_seconds() as i32)
1511 1512 } else {
1512 1513 (b' ', 0, -1, -1)
1513 1514 };
1514 1515 Ok(Some((node.full_path(map.on_disk)?, debug_tuple)))
1515 1516 }))
1516 1517 }
1517 1518 }
1518 1519 #[cfg(test)]
1519 1520 mod tests {
1520 1521 use super::*;
1521 1522
1522 1523 /// Shortcut to return tracked descendants of a path.
1523 1524 /// Panics if the path does not exist.
1524 1525 fn tracked_descendants(map: &OwningDirstateMap, path: &[u8]) -> u32 {
1525 1526 let path = dbg!(HgPath::new(path));
1526 1527 let node = map.get_map().get_node(path);
1527 1528 node.unwrap().unwrap().tracked_descendants_count()
1528 1529 }
1529 1530
1530 1531 /// Shortcut to return descendants with an entry.
1531 1532 /// Panics if the path does not exist.
1532 1533 fn descendants_with_an_entry(map: &OwningDirstateMap, path: &[u8]) -> u32 {
1533 1534 let path = dbg!(HgPath::new(path));
1534 1535 let node = map.get_map().get_node(path);
1535 1536 node.unwrap().unwrap().descendants_with_entry_count()
1536 1537 }
1537 1538
1538 1539 fn assert_does_not_exist(map: &OwningDirstateMap, path: &[u8]) {
1539 1540 let path = dbg!(HgPath::new(path));
1540 1541 let node = map.get_map().get_node(path);
1541 1542 assert!(node.unwrap().is_none());
1542 1543 }
1543 1544
1544 1545 /// Shortcut for path creation in tests
1545 1546 fn p(b: &[u8]) -> &HgPath {
1546 1547 HgPath::new(b)
1547 1548 }
1548 1549
1549 1550 /// Test the very simple case a single tracked file
1550 1551 #[test]
1551 1552 fn test_tracked_descendants_simple() -> Result<(), DirstateError> {
1552 1553 let mut map = OwningDirstateMap::new_empty(vec![]);
1553 1554 assert_eq!(map.len(), 0);
1554 1555
1555 1556 map.set_tracked(p(b"some/nested/path"))?;
1556 1557
1557 1558 assert_eq!(map.len(), 1);
1558 1559 assert_eq!(tracked_descendants(&map, b"some"), 1);
1559 1560 assert_eq!(tracked_descendants(&map, b"some/nested"), 1);
1560 1561 assert_eq!(tracked_descendants(&map, b"some/nested/path"), 0);
1561 1562
1562 1563 map.set_untracked(p(b"some/nested/path"))?;
1563 1564 assert_eq!(map.len(), 0);
1564 1565 assert!(map.get_map().get_node(p(b"some"))?.is_none());
1565 1566
1566 1567 Ok(())
1567 1568 }
1568 1569
1569 1570 /// Test the simple case of all tracked, but multiple files
1570 1571 #[test]
1571 1572 fn test_tracked_descendants_multiple() -> Result<(), DirstateError> {
1572 1573 let mut map = OwningDirstateMap::new_empty(vec![]);
1573 1574
1574 1575 map.set_tracked(p(b"some/nested/path"))?;
1575 1576 map.set_tracked(p(b"some/nested/file"))?;
1576 1577 // one layer without any files to test deletion cascade
1577 1578 map.set_tracked(p(b"some/other/nested/path"))?;
1578 1579 map.set_tracked(p(b"root_file"))?;
1579 1580 map.set_tracked(p(b"some/file"))?;
1580 1581 map.set_tracked(p(b"some/file2"))?;
1581 1582 map.set_tracked(p(b"some/file3"))?;
1582 1583
1583 1584 assert_eq!(map.len(), 7);
1584 1585 assert_eq!(tracked_descendants(&map, b"some"), 6);
1585 1586 assert_eq!(tracked_descendants(&map, b"some/nested"), 2);
1586 1587 assert_eq!(tracked_descendants(&map, b"some/other"), 1);
1587 1588 assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1);
1588 1589 assert_eq!(tracked_descendants(&map, b"some/nested/path"), 0);
1589 1590
1590 1591 map.set_untracked(p(b"some/nested/path"))?;
1591 1592 assert_eq!(map.len(), 6);
1592 1593 assert_eq!(tracked_descendants(&map, b"some"), 5);
1593 1594 assert_eq!(tracked_descendants(&map, b"some/nested"), 1);
1594 1595 assert_eq!(tracked_descendants(&map, b"some/other"), 1);
1595 1596 assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1);
1596 1597
1597 1598 map.set_untracked(p(b"some/nested/file"))?;
1598 1599 assert_eq!(map.len(), 5);
1599 1600 assert_eq!(tracked_descendants(&map, b"some"), 4);
1600 1601 assert_eq!(tracked_descendants(&map, b"some/other"), 1);
1601 1602 assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1);
1602 1603 assert_does_not_exist(&map, b"some_nested");
1603 1604
1604 1605 map.set_untracked(p(b"some/other/nested/path"))?;
1605 1606 assert_eq!(map.len(), 4);
1606 1607 assert_eq!(tracked_descendants(&map, b"some"), 3);
1607 1608 assert_does_not_exist(&map, b"some/other");
1608 1609
1609 1610 map.set_untracked(p(b"root_file"))?;
1610 1611 assert_eq!(map.len(), 3);
1611 1612 assert_eq!(tracked_descendants(&map, b"some"), 3);
1612 1613 assert_does_not_exist(&map, b"root_file");
1613 1614
1614 1615 map.set_untracked(p(b"some/file"))?;
1615 1616 assert_eq!(map.len(), 2);
1616 1617 assert_eq!(tracked_descendants(&map, b"some"), 2);
1617 1618 assert_does_not_exist(&map, b"some/file");
1618 1619
1619 1620 map.set_untracked(p(b"some/file2"))?;
1620 1621 assert_eq!(map.len(), 1);
1621 1622 assert_eq!(tracked_descendants(&map, b"some"), 1);
1622 1623 assert_does_not_exist(&map, b"some/file2");
1623 1624
1624 1625 map.set_untracked(p(b"some/file3"))?;
1625 1626 assert_eq!(map.len(), 0);
1626 1627 assert_does_not_exist(&map, b"some/file3");
1627 1628
1628 1629 Ok(())
1629 1630 }
1630 1631
1631 1632 /// Check with a mix of tracked and non-tracked items
1632 1633 #[test]
1633 1634 fn test_tracked_descendants_different() -> Result<(), DirstateError> {
1634 1635 let mut map = OwningDirstateMap::new_empty(vec![]);
1635 1636
1636 1637 // A file that was just added
1637 1638 map.set_tracked(p(b"some/nested/path"))?;
1638 1639 // This has no information, the dirstate should ignore it
1639 1640 map.reset_state(p(b"some/file"), false, false, false, false, None)?;
1640 1641 assert_does_not_exist(&map, b"some/file");
1641 1642
1642 1643 // A file that was removed
1643 1644 map.reset_state(
1644 1645 p(b"some/nested/file"),
1645 1646 false,
1646 1647 true,
1647 1648 false,
1648 1649 false,
1649 1650 None,
1650 1651 )?;
1651 1652 assert!(!map.get(p(b"some/nested/file"))?.unwrap().tracked());
1652 1653 // Only present in p2
1653 1654 map.reset_state(p(b"some/file3"), false, false, true, false, None)?;
1654 1655 assert!(!map.get(p(b"some/file3"))?.unwrap().tracked());
1655 1656 // A file that was merged
1656 1657 map.reset_state(p(b"root_file"), true, true, true, false, None)?;
1657 1658 assert!(map.get(p(b"root_file"))?.unwrap().tracked());
1658 1659 // A file that is added, with info from p2
1659 1660 // XXX is that actually possible?
1660 1661 map.reset_state(p(b"some/file2"), true, false, true, false, None)?;
1661 1662 assert!(map.get(p(b"some/file2"))?.unwrap().tracked());
1662 1663 // A clean file
1663 1664 // One layer without any files to test deletion cascade
1664 1665 map.reset_state(
1665 1666 p(b"some/other/nested/path"),
1666 1667 true,
1667 1668 true,
1668 1669 false,
1669 1670 false,
1670 1671 None,
1671 1672 )?;
1672 1673 assert!(map.get(p(b"some/other/nested/path"))?.unwrap().tracked());
1673 1674
1674 1675 assert_eq!(map.len(), 6);
1675 1676 assert_eq!(tracked_descendants(&map, b"some"), 3);
1676 1677 assert_eq!(descendants_with_an_entry(&map, b"some"), 5);
1677 1678 assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1);
1678 1679 assert_eq!(descendants_with_an_entry(&map, b"some/other/nested"), 1);
1679 1680 assert_eq!(tracked_descendants(&map, b"some/other/nested/path"), 0);
1680 1681 assert_eq!(
1681 1682 descendants_with_an_entry(&map, b"some/other/nested/path"),
1682 1683 0
1683 1684 );
1684 1685 assert_eq!(tracked_descendants(&map, b"some/nested"), 1);
1685 1686 assert_eq!(descendants_with_an_entry(&map, b"some/nested"), 2);
1686 1687
1687 1688 // might as well check this
1688 1689 map.set_untracked(p(b"path/does/not/exist"))?;
1689 1690 assert_eq!(map.len(), 6);
1690 1691
1691 1692 map.set_untracked(p(b"some/other/nested/path"))?;
1692 1693 // It is set untracked but not deleted since it held other information
1693 1694 assert_eq!(map.len(), 6);
1694 1695 assert_eq!(tracked_descendants(&map, b"some"), 2);
1695 1696 assert_eq!(descendants_with_an_entry(&map, b"some"), 5);
1696 1697 assert_eq!(descendants_with_an_entry(&map, b"some/other"), 1);
1697 1698 assert_eq!(descendants_with_an_entry(&map, b"some/other/nested"), 1);
1698 1699 assert_eq!(tracked_descendants(&map, b"some/nested"), 1);
1699 1700 assert_eq!(descendants_with_an_entry(&map, b"some/nested"), 2);
1700 1701
1701 1702 map.set_untracked(p(b"some/nested/path"))?;
1702 1703 // It is set untracked *and* deleted since it was only added
1703 1704 assert_eq!(map.len(), 5);
1704 1705 assert_eq!(tracked_descendants(&map, b"some"), 1);
1705 1706 assert_eq!(descendants_with_an_entry(&map, b"some"), 4);
1706 1707 assert_eq!(tracked_descendants(&map, b"some/nested"), 0);
1707 1708 assert_eq!(descendants_with_an_entry(&map, b"some/nested"), 1);
1708 1709 assert_does_not_exist(&map, b"some/nested/path");
1709 1710
1710 1711 map.set_untracked(p(b"root_file"))?;
1711 1712 // Untracked but not deleted
1712 1713 assert_eq!(map.len(), 5);
1713 1714 assert!(map.get(p(b"root_file"))?.is_some());
1714 1715
1715 1716 map.set_untracked(p(b"some/file2"))?;
1716 1717 assert_eq!(map.len(), 5);
1717 1718 assert_eq!(tracked_descendants(&map, b"some"), 0);
1718 1719 assert!(map.get(p(b"some/file2"))?.is_some());
1719 1720
1720 1721 map.set_untracked(p(b"some/file3"))?;
1721 1722 assert_eq!(map.len(), 5);
1722 1723 assert_eq!(tracked_descendants(&map, b"some"), 0);
1723 1724 assert!(map.get(p(b"some/file3"))?.is_some());
1724 1725
1725 1726 Ok(())
1726 1727 }
1727 1728
1728 1729 /// Check that copies counter is correctly updated
1729 1730 #[test]
1730 1731 fn test_copy_source() -> Result<(), DirstateError> {
1731 1732 let mut map = OwningDirstateMap::new_empty(vec![]);
1732 1733
1733 1734 // Clean file
1734 1735 map.reset_state(p(b"files/clean"), true, true, false, false, None)?;
1735 1736 // Merged file
1736 1737 map.reset_state(p(b"files/from_p2"), true, true, true, false, None)?;
1737 1738 // Removed file
1738 1739 map.reset_state(p(b"removed"), false, true, false, false, None)?;
1739 1740 // Added file
1740 1741 map.reset_state(p(b"files/added"), true, false, false, false, None)?;
1741 1742 // Add copy
1742 1743 map.copy_map_insert(p(b"files/clean"), p(b"clean_copy_source"))?;
1743 1744 assert_eq!(map.copy_map_len(), 1);
1744 1745
1745 1746 // Copy override
1746 1747 map.copy_map_insert(p(b"files/clean"), p(b"other_clean_copy_source"))?;
1747 1748 assert_eq!(map.copy_map_len(), 1);
1748 1749
1749 1750 // Multiple copies
1750 1751 map.copy_map_insert(p(b"removed"), p(b"removed_copy_source"))?;
1751 1752 assert_eq!(map.copy_map_len(), 2);
1752 1753
1753 1754 map.copy_map_insert(p(b"files/added"), p(b"added_copy_source"))?;
1754 1755 assert_eq!(map.copy_map_len(), 3);
1755 1756
1756 1757 // Added, so the entry is completely removed
1757 1758 map.set_untracked(p(b"files/added"))?;
1758 1759 assert_does_not_exist(&map, b"files/added");
1759 1760 assert_eq!(map.copy_map_len(), 2);
1760 1761
1761 1762 // Removed, so the entry is kept around, so is its copy
1762 1763 map.set_untracked(p(b"removed"))?;
1763 1764 assert!(map.get(p(b"removed"))?.is_some());
1764 1765 assert_eq!(map.copy_map_len(), 2);
1765 1766
1766 1767 // Clean, so the entry is kept around, but not its copy
1767 1768 map.set_untracked(p(b"files/clean"))?;
1768 1769 assert!(map.get(p(b"files/clean"))?.is_some());
1769 1770 assert_eq!(map.copy_map_len(), 1);
1770 1771
1771 1772 map.copy_map_insert(p(b"files/from_p2"), p(b"from_p2_copy_source"))?;
1772 1773 assert_eq!(map.copy_map_len(), 2);
1773 1774
1774 1775 // Info from p2, so its copy source info is kept around
1775 1776 map.set_untracked(p(b"files/from_p2"))?;
1776 1777 assert!(map.get(p(b"files/from_p2"))?.is_some());
1777 1778 assert_eq!(map.copy_map_len(), 2);
1778 1779
1779 1780 Ok(())
1780 1781 }
1781 1782
1782 1783 /// Test with "on disk" data. For the sake of this test, the "on disk" data
1783 1784 /// does not actually come from the disk, but it's opaque to the code being
1784 1785 /// tested.
1785 1786 #[test]
1786 1787 fn test_on_disk() -> Result<(), DirstateError> {
1787 1788 // First let's create some data to put "on disk"
1788 1789 let mut map = OwningDirstateMap::new_empty(vec![]);
1789 1790
1790 1791 // A file that was just added
1791 1792 map.set_tracked(p(b"some/nested/added"))?;
1792 1793 map.copy_map_insert(p(b"some/nested/added"), p(b"added_copy_source"))?;
1793 1794
1794 1795 // A file that was removed
1795 1796 map.reset_state(
1796 1797 p(b"some/nested/removed"),
1797 1798 false,
1798 1799 true,
1799 1800 false,
1800 1801 false,
1801 1802 None,
1802 1803 )?;
1803 1804 // Only present in p2
1804 1805 map.reset_state(
1805 1806 p(b"other/p2_info_only"),
1806 1807 false,
1807 1808 false,
1808 1809 true,
1809 1810 false,
1810 1811 None,
1811 1812 )?;
1812 1813 map.copy_map_insert(
1813 1814 p(b"other/p2_info_only"),
1814 1815 p(b"other/p2_info_copy_source"),
1815 1816 )?;
1816 1817 // A file that was merged
1817 1818 map.reset_state(p(b"merged"), true, true, true, false, None)?;
1818 1819 // A file that is added, with info from p2
1819 1820 // XXX is that actually possible?
1820 1821 map.reset_state(
1821 1822 p(b"other/added_with_p2"),
1822 1823 true,
1823 1824 false,
1824 1825 true,
1825 1826 false,
1826 1827 None,
1827 1828 )?;
1828 1829 // One layer without any files to test deletion cascade
1829 1830 // A clean file
1830 1831 map.reset_state(
1831 1832 p(b"some/other/nested/clean"),
1832 1833 true,
1833 1834 true,
1834 1835 false,
1835 1836 false,
1836 1837 None,
1837 1838 )?;
1838 1839
1839 1840 let (packed, metadata, _should_append, _old_data_size) =
1840 1841 map.pack_v2(DirstateMapWriteMode::ForceNewDataFile)?;
1841 1842 let packed_len = packed.len();
1842 1843 assert!(packed_len > 0);
1843 1844
1844 1845 // Recreate "from disk"
1845 1846 let mut map = OwningDirstateMap::new_v2(
1846 1847 packed,
1847 1848 packed_len,
1848 1849 metadata.as_bytes(),
1849 1850 vec![],
1850 1851 None,
1851 1852 )?;
1852 1853
1853 1854 // Check that everything is accounted for
1854 1855 assert!(map.contains_key(p(b"some/nested/added"))?);
1855 1856 assert!(map.contains_key(p(b"some/nested/removed"))?);
1856 1857 assert!(map.contains_key(p(b"merged"))?);
1857 1858 assert!(map.contains_key(p(b"other/p2_info_only"))?);
1858 1859 assert!(map.contains_key(p(b"other/added_with_p2"))?);
1859 1860 assert!(map.contains_key(p(b"some/other/nested/clean"))?);
1860 1861 assert_eq!(
1861 1862 map.copy_map_get(p(b"some/nested/added"))?,
1862 1863 Some(p(b"added_copy_source"))
1863 1864 );
1864 1865 assert_eq!(
1865 1866 map.copy_map_get(p(b"other/p2_info_only"))?,
1866 1867 Some(p(b"other/p2_info_copy_source"))
1867 1868 );
1868 1869 assert_eq!(tracked_descendants(&map, b"some"), 2);
1869 1870 assert_eq!(descendants_with_an_entry(&map, b"some"), 3);
1870 1871 assert_eq!(tracked_descendants(&map, b"other"), 1);
1871 1872 assert_eq!(descendants_with_an_entry(&map, b"other"), 2);
1872 1873 assert_eq!(tracked_descendants(&map, b"some/other"), 1);
1873 1874 assert_eq!(descendants_with_an_entry(&map, b"some/other"), 1);
1874 1875 assert_eq!(tracked_descendants(&map, b"some/other/nested"), 1);
1875 1876 assert_eq!(descendants_with_an_entry(&map, b"some/other/nested"), 1);
1876 1877 assert_eq!(tracked_descendants(&map, b"some/nested"), 1);
1877 1878 assert_eq!(descendants_with_an_entry(&map, b"some/nested"), 2);
1878 1879 assert_eq!(map.len(), 6);
1879 1880 assert_eq!(map.get_map().unreachable_bytes, 0);
1880 1881 assert_eq!(map.copy_map_len(), 2);
1881 1882
1882 1883 // Shouldn't change anything since it's already not tracked
1883 1884 map.set_untracked(p(b"some/nested/removed"))?;
1884 1885 assert_eq!(map.get_map().unreachable_bytes, 0);
1885 1886
1886 1887 if let ChildNodes::InMemory(_) = map.get_map().root {
1887 1888 panic!("root should not have been mutated")
1888 1889 }
1889 1890 // We haven't mutated enough (nothing, actually), we should still be in
1890 1891 // the append strategy
1891 1892 assert!(map.get_map().write_should_append());
1892 1893
1893 1894 // But this mutates the structure, so there should be unreachable_bytes
1894 1895 assert!(map.set_untracked(p(b"some/nested/added"))?);
1895 1896 let unreachable_bytes = map.get_map().unreachable_bytes;
1896 1897 assert!(unreachable_bytes > 0);
1897 1898
1898 1899 if let ChildNodes::OnDisk(_) = map.get_map().root {
1899 1900 panic!("root should have been mutated")
1900 1901 }
1901 1902
1902 1903 // This should not mutate the structure either, since `root` has
1903 1904 // already been mutated along with its direct children.
1904 1905 map.set_untracked(p(b"merged"))?;
1905 1906 assert_eq!(map.get_map().unreachable_bytes, unreachable_bytes);
1906 1907
1907 1908 if let NodeRef::InMemory(_, _) =
1908 1909 map.get_map().get_node(p(b"other/added_with_p2"))?.unwrap()
1909 1910 {
1910 1911 panic!("'other/added_with_p2' should not have been mutated")
1911 1912 }
1912 1913 // But this should, since it's in a different path
1913 1914 // than `<root>some/nested/add`
1914 1915 map.set_untracked(p(b"other/added_with_p2"))?;
1915 1916 assert!(map.get_map().unreachable_bytes > unreachable_bytes);
1916 1917
1917 1918 if let NodeRef::OnDisk(_) =
1918 1919 map.get_map().get_node(p(b"other/added_with_p2"))?.unwrap()
1919 1920 {
1920 1921 panic!("'other/added_with_p2' should have been mutated")
1921 1922 }
1922 1923
1923 1924 // We have rewritten most of the tree, we should create a new file
1924 1925 assert!(!map.get_map().write_should_append());
1925 1926
1926 1927 Ok(())
1927 1928 }
1928 1929 }
@@ -1,341 +1,343 b''
1 1 use crate::errors::HgError;
2 use crate::revlog::Revision;
2 3 use crate::revlog::{Node, NodePrefix};
3 use crate::revlog::{Revision, NULL_REVISION};
4 4 use crate::revlog::{Revlog, RevlogEntry, RevlogError};
5 5 use crate::utils::hg_path::HgPath;
6 6 use crate::vfs::Vfs;
7 7 use itertools::Itertools;
8 8 use std::ascii::escape_default;
9 9 use std::borrow::Cow;
10 10 use std::fmt::{Debug, Formatter};
11 11
12 12 /// A specialized `Revlog` to work with changelog data format.
13 13 pub struct Changelog {
14 14 /// The generic `revlog` format.
15 15 pub(crate) revlog: Revlog,
16 16 }
17 17
18 18 impl Changelog {
19 19 /// Open the `changelog` of a repository given by its root.
20 20 pub fn open(store_vfs: &Vfs, use_nodemap: bool) -> Result<Self, HgError> {
21 21 let revlog =
22 22 Revlog::open(store_vfs, "00changelog.i", None, use_nodemap)?;
23 23 Ok(Self { revlog })
24 24 }
25 25
26 26 /// Return the `ChangelogRevisionData` for the given node ID.
27 27 pub fn data_for_node(
28 28 &self,
29 29 node: NodePrefix,
30 30 ) -> Result<ChangelogRevisionData, RevlogError> {
31 31 let rev = self.revlog.rev_from_node(node)?;
32 32 self.data_for_rev(rev)
33 33 }
34 34
35 35 /// Return the [`ChangelogEntry`] for the given revision number.
36 36 pub fn entry_for_rev(
37 37 &self,
38 38 rev: Revision,
39 39 ) -> Result<ChangelogEntry, RevlogError> {
40 40 let revlog_entry = self.revlog.get_entry(rev)?;
41 41 Ok(ChangelogEntry { revlog_entry })
42 42 }
43 43
44 44 /// Return the [`ChangelogRevisionData`] for the given revision number.
45 45 ///
46 46 /// This is a useful shortcut in case the caller does not need the
47 47 /// generic revlog information (parents, hashes etc). Otherwise
48 48 /// consider taking a [`ChangelogEntry`] with
49 49 /// [entry_for_rev](`Self::entry_for_rev`) and doing everything from there.
50 50 pub fn data_for_rev(
51 51 &self,
52 52 rev: Revision,
53 53 ) -> Result<ChangelogRevisionData, RevlogError> {
54 if rev == NULL_REVISION {
55 return Ok(ChangelogRevisionData::null());
56 }
57 54 self.entry_for_rev(rev)?.data()
58 55 }
59 56
60 57 pub fn node_from_rev(&self, rev: Revision) -> Option<&Node> {
61 58 self.revlog.node_from_rev(rev)
62 59 }
63 60
64 61 pub fn rev_from_node(
65 62 &self,
66 63 node: NodePrefix,
67 64 ) -> Result<Revision, RevlogError> {
68 65 self.revlog.rev_from_node(node)
69 66 }
70 67 }
71 68
72 69 /// A specialized `RevlogEntry` for `changelog` data format
73 70 ///
74 71 /// This is a `RevlogEntry` with the added semantics that the associated
75 72 /// data should meet the requirements for `changelog`, materialized by
76 73 /// the fact that `data()` constructs a `ChangelogRevisionData`.
77 74 /// In case that promise would be broken, the `data` method returns an error.
78 75 #[derive(Clone)]
79 76 pub struct ChangelogEntry<'changelog> {
80 77 /// Same data, as a generic `RevlogEntry`.
81 78 pub(crate) revlog_entry: RevlogEntry<'changelog>,
82 79 }
83 80
84 81 impl<'changelog> ChangelogEntry<'changelog> {
85 82 pub fn data<'a>(
86 83 &'a self,
87 84 ) -> Result<ChangelogRevisionData<'changelog>, RevlogError> {
88 85 let bytes = self.revlog_entry.data()?;
89 86 if bytes.is_empty() {
90 87 Ok(ChangelogRevisionData::null())
91 88 } else {
92 89 Ok(ChangelogRevisionData::new(bytes).map_err(|err| {
93 90 RevlogError::Other(HgError::CorruptedRepository(format!(
94 91 "Invalid changelog data for revision {}: {:?}",
95 92 self.revlog_entry.revision(),
96 93 err
97 94 )))
98 95 })?)
99 96 }
100 97 }
101 98
102 99 /// Obtain a reference to the underlying `RevlogEntry`.
103 100 ///
104 101 /// This allows the caller to access the information that is common
105 102 /// to all revlog entries: revision number, node id, parent revisions etc.
106 103 pub fn as_revlog_entry(&self) -> &RevlogEntry {
107 104 &self.revlog_entry
108 105 }
109 106
110 107 pub fn p1_entry(&self) -> Result<Option<ChangelogEntry>, RevlogError> {
111 108 Ok(self
112 109 .revlog_entry
113 110 .p1_entry()?
114 111 .map(|revlog_entry| Self { revlog_entry }))
115 112 }
116 113
117 114 pub fn p2_entry(&self) -> Result<Option<ChangelogEntry>, RevlogError> {
118 115 Ok(self
119 116 .revlog_entry
120 117 .p2_entry()?
121 118 .map(|revlog_entry| Self { revlog_entry }))
122 119 }
123 120 }
124 121
125 122 /// `Changelog` entry which knows how to interpret the `changelog` data bytes.
126 123 #[derive(PartialEq)]
127 124 pub struct ChangelogRevisionData<'changelog> {
128 125 /// The data bytes of the `changelog` entry.
129 126 bytes: Cow<'changelog, [u8]>,
130 127 /// The end offset for the hex manifest (not including the newline)
131 128 manifest_end: usize,
132 129 /// The end offset for the user+email (not including the newline)
133 130 user_end: usize,
134 131 /// The end offset for the timestamp+timezone+extras (not including the
135 132 /// newline)
136 133 timestamp_end: usize,
137 134 /// The end offset for the file list (not including the newline)
138 135 files_end: usize,
139 136 }
140 137
141 138 impl<'changelog> ChangelogRevisionData<'changelog> {
142 139 fn new(bytes: Cow<'changelog, [u8]>) -> Result<Self, HgError> {
143 140 let mut line_iter = bytes.split(|b| b == &b'\n');
144 141 let manifest_end = line_iter
145 142 .next()
146 143 .expect("Empty iterator from split()?")
147 144 .len();
148 145 let user_slice = line_iter.next().ok_or_else(|| {
149 146 HgError::corrupted("Changeset data truncated after manifest line")
150 147 })?;
151 148 let user_end = manifest_end + 1 + user_slice.len();
152 149 let timestamp_slice = line_iter.next().ok_or_else(|| {
153 150 HgError::corrupted("Changeset data truncated after user line")
154 151 })?;
155 152 let timestamp_end = user_end + 1 + timestamp_slice.len();
156 153 let mut files_end = timestamp_end + 1;
157 154 loop {
158 155 let line = line_iter.next().ok_or_else(|| {
159 156 HgError::corrupted("Changeset data truncated in files list")
160 157 })?;
161 158 if line.is_empty() {
162 159 if files_end == bytes.len() {
163 160 // The list of files ended with a single newline (there
164 161 // should be two)
165 162 return Err(HgError::corrupted(
166 163 "Changeset data truncated after files list",
167 164 ));
168 165 }
169 166 files_end -= 1;
170 167 break;
171 168 }
172 169 files_end += line.len() + 1;
173 170 }
174 171
175 172 Ok(Self {
176 173 bytes,
177 174 manifest_end,
178 175 user_end,
179 176 timestamp_end,
180 177 files_end,
181 178 })
182 179 }
183 180
184 181 fn null() -> Self {
185 182 Self::new(Cow::Borrowed(
186 183 b"0000000000000000000000000000000000000000\n\n0 0\n\n",
187 184 ))
188 185 .unwrap()
189 186 }
190 187
191 188 /// Return an iterator over the lines of the entry.
192 189 pub fn lines(&self) -> impl Iterator<Item = &[u8]> {
193 190 self.bytes.split(|b| b == &b'\n')
194 191 }
195 192
196 193 /// Return the node id of the `manifest` referenced by this `changelog`
197 194 /// entry.
198 195 pub fn manifest_node(&self) -> Result<Node, HgError> {
199 196 let manifest_node_hex = &self.bytes[..self.manifest_end];
200 197 Node::from_hex_for_repo(manifest_node_hex)
201 198 }
202 199
203 200 /// The full user string (usually a name followed by an email enclosed in
204 201 /// angle brackets)
205 202 pub fn user(&self) -> &[u8] {
206 203 &self.bytes[self.manifest_end + 1..self.user_end]
207 204 }
208 205
209 206 /// The full timestamp line (timestamp in seconds, offset in seconds, and
210 207 /// possibly extras)
211 208 // TODO: We should expose this in a more useful way
212 209 pub fn timestamp_line(&self) -> &[u8] {
213 210 &self.bytes[self.user_end + 1..self.timestamp_end]
214 211 }
215 212
216 213 /// The files changed in this revision.
217 214 pub fn files(&self) -> impl Iterator<Item = &HgPath> {
218 215 self.bytes[self.timestamp_end + 1..self.files_end]
219 216 .split(|b| b == &b'\n')
220 217 .map(HgPath::new)
221 218 }
222 219
223 220 /// The change description.
224 221 pub fn description(&self) -> &[u8] {
225 222 &self.bytes[self.files_end + 2..]
226 223 }
227 224 }
228 225
229 226 impl Debug for ChangelogRevisionData<'_> {
230 227 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
231 228 f.debug_struct("ChangelogRevisionData")
232 229 .field("bytes", &debug_bytes(&self.bytes))
233 230 .field("manifest", &debug_bytes(&self.bytes[..self.manifest_end]))
234 231 .field(
235 232 "user",
236 233 &debug_bytes(
237 234 &self.bytes[self.manifest_end + 1..self.user_end],
238 235 ),
239 236 )
240 237 .field(
241 238 "timestamp",
242 239 &debug_bytes(
243 240 &self.bytes[self.user_end + 1..self.timestamp_end],
244 241 ),
245 242 )
246 243 .field(
247 244 "files",
248 245 &debug_bytes(
249 246 &self.bytes[self.timestamp_end + 1..self.files_end],
250 247 ),
251 248 )
252 249 .field(
253 250 "description",
254 251 &debug_bytes(&self.bytes[self.files_end + 2..]),
255 252 )
256 253 .finish()
257 254 }
258 255 }
259 256
260 257 fn debug_bytes(bytes: &[u8]) -> String {
261 258 String::from_utf8_lossy(
262 259 &bytes.iter().flat_map(|b| escape_default(*b)).collect_vec(),
263 260 )
264 261 .to_string()
265 262 }
266 263
267 264 #[cfg(test)]
268 265 mod tests {
269 266 use super::*;
270 267 use crate::vfs::Vfs;
271 268 use crate::NULL_REVISION;
272 269 use pretty_assertions::assert_eq;
273 270
274 271 #[test]
275 272 fn test_create_changelogrevisiondata_invalid() {
276 273 // Completely empty
277 274 assert!(ChangelogRevisionData::new(Cow::Borrowed(b"abcd")).is_err());
278 275 // No newline after manifest
279 276 assert!(ChangelogRevisionData::new(Cow::Borrowed(b"abcd")).is_err());
280 277 // No newline after user
281 278 assert!(ChangelogRevisionData::new(Cow::Borrowed(b"abcd\n")).is_err());
282 279 // No newline after timestamp
283 280 assert!(
284 281 ChangelogRevisionData::new(Cow::Borrowed(b"abcd\n\n0 0")).is_err()
285 282 );
286 283 // Missing newline after files
287 284 assert!(ChangelogRevisionData::new(Cow::Borrowed(
288 285 b"abcd\n\n0 0\nfile1\nfile2"
289 286 ))
290 287 .is_err(),);
291 288 // Only one newline after files
292 289 assert!(ChangelogRevisionData::new(Cow::Borrowed(
293 290 b"abcd\n\n0 0\nfile1\nfile2\n"
294 291 ))
295 292 .is_err(),);
296 293 }
297 294
298 295 #[test]
299 296 fn test_create_changelogrevisiondata() {
300 297 let data = ChangelogRevisionData::new(Cow::Borrowed(
301 298 b"0123456789abcdef0123456789abcdef01234567
302 299 Some One <someone@example.com>
303 300 0 0
304 301 file1
305 302 file2
306 303
307 304 some
308 305 commit
309 306 message",
310 307 ))
311 308 .unwrap();
312 309 assert_eq!(
313 310 data.manifest_node().unwrap(),
314 311 Node::from_hex("0123456789abcdef0123456789abcdef01234567")
315 312 .unwrap()
316 313 );
317 314 assert_eq!(data.user(), b"Some One <someone@example.com>");
318 315 assert_eq!(data.timestamp_line(), b"0 0");
319 316 assert_eq!(
320 317 data.files().collect_vec(),
321 318 vec![HgPath::new("file1"), HgPath::new("file2")]
322 319 );
323 320 assert_eq!(data.description(), b"some\ncommit\nmessage");
324 321 }
325 322
326 323 #[test]
327 324 fn test_data_from_rev_null() -> Result<(), RevlogError> {
328 325 // an empty revlog will be enough for this case
329 326 let temp = tempfile::tempdir().unwrap();
330 327 let vfs = Vfs { base: temp.path() };
331 328 std::fs::write(temp.path().join("foo.i"), b"").unwrap();
332 329 let revlog = Revlog::open(&vfs, "foo.i", None, false).unwrap();
333 330
334 331 let changelog = Changelog { revlog };
335 332 assert_eq!(
336 333 changelog.data_for_rev(NULL_REVISION)?,
337 334 ChangelogRevisionData::null()
338 335 );
336 // same with the intermediate entry object
337 assert_eq!(
338 changelog.entry_for_rev(NULL_REVISION)?.data()?,
339 ChangelogRevisionData::null()
340 );
339 341 Ok(())
340 342 }
341 343 }
@@ -1,743 +1,829 b''
1 1 // Copyright 2018-2023 Georges Racinet <georges.racinet@octobus.net>
2 2 // and Mercurial contributors
3 3 //
4 4 // This software may be used and distributed according to the terms of the
5 5 // GNU General Public License version 2 or any later version.
6 6 //! Mercurial concepts for handling revision history
7 7
8 8 pub mod node;
9 9 pub mod nodemap;
10 10 mod nodemap_docket;
11 11 pub mod path_encode;
12 12 pub use node::{FromHexError, Node, NodePrefix};
13 13 pub mod changelog;
14 14 pub mod filelog;
15 15 pub mod index;
16 16 pub mod manifest;
17 17 pub mod patch;
18 18
19 19 use std::borrow::Cow;
20 20 use std::io::Read;
21 21 use std::ops::Deref;
22 22 use std::path::Path;
23 23
24 24 use flate2::read::ZlibDecoder;
25 25 use sha1::{Digest, Sha1};
26 26 use std::cell::RefCell;
27 27 use zstd;
28 28
29 29 use self::node::{NODE_BYTES_LENGTH, NULL_NODE};
30 30 use self::nodemap_docket::NodeMapDocket;
31 31 use super::index::Index;
32 32 use super::nodemap::{NodeMap, NodeMapError};
33 33 use crate::errors::HgError;
34 34 use crate::vfs::Vfs;
35 35
36 36 /// Mercurial revision numbers
37 37 ///
38 38 /// As noted in revlog.c, revision numbers are actually encoded in
39 39 /// 4 bytes, and are liberally converted to ints, whence the i32
40 40 pub type Revision = i32;
41 41
42 42 /// Marker expressing the absence of a parent
43 43 ///
44 44 /// Independently of the actual representation, `NULL_REVISION` is guaranteed
45 45 /// to be smaller than all existing revisions.
46 46 pub const NULL_REVISION: Revision = -1;
47 47
48 48 /// Same as `mercurial.node.wdirrev`
49 49 ///
50 50 /// This is also equal to `i32::max_value()`, but it's better to spell
51 51 /// it out explicitely, same as in `mercurial.node`
52 52 #[allow(clippy::unreadable_literal)]
53 53 pub const WORKING_DIRECTORY_REVISION: Revision = 0x7fffffff;
54 54
55 55 pub const WORKING_DIRECTORY_HEX: &str =
56 56 "ffffffffffffffffffffffffffffffffffffffff";
57 57
58 58 /// The simplest expression of what we need of Mercurial DAGs.
59 59 pub trait Graph {
60 60 /// Return the two parents of the given `Revision`.
61 61 ///
62 62 /// Each of the parents can be independently `NULL_REVISION`
63 63 fn parents(&self, rev: Revision) -> Result<[Revision; 2], GraphError>;
64 64 }
65 65
66 66 #[derive(Clone, Debug, PartialEq)]
67 67 pub enum GraphError {
68 68 ParentOutOfRange(Revision),
69 69 WorkingDirectoryUnsupported,
70 70 }
71 71
72 72 /// The Mercurial Revlog Index
73 73 ///
74 74 /// This is currently limited to the minimal interface that is needed for
75 75 /// the [`nodemap`](nodemap/index.html) module
76 76 pub trait RevlogIndex {
77 77 /// Total number of Revisions referenced in this index
78 78 fn len(&self) -> usize;
79 79
80 80 fn is_empty(&self) -> bool {
81 81 self.len() == 0
82 82 }
83 83
84 84 /// Return a reference to the Node or `None` if rev is out of bounds
85 85 ///
86 86 /// `NULL_REVISION` is not considered to be out of bounds.
87 87 fn node(&self, rev: Revision) -> Option<&Node>;
88 88 }
89 89
90 90 const REVISION_FLAG_CENSORED: u16 = 1 << 15;
91 91 const REVISION_FLAG_ELLIPSIS: u16 = 1 << 14;
92 92 const REVISION_FLAG_EXTSTORED: u16 = 1 << 13;
93 93 const REVISION_FLAG_HASCOPIESINFO: u16 = 1 << 12;
94 94
95 95 // Keep this in sync with REVIDX_KNOWN_FLAGS in
96 96 // mercurial/revlogutils/flagutil.py
97 97 const REVIDX_KNOWN_FLAGS: u16 = REVISION_FLAG_CENSORED
98 98 | REVISION_FLAG_ELLIPSIS
99 99 | REVISION_FLAG_EXTSTORED
100 100 | REVISION_FLAG_HASCOPIESINFO;
101 101
102 102 const NULL_REVLOG_ENTRY_FLAGS: u16 = 0;
103 103
104 104 #[derive(Debug, derive_more::From)]
105 105 pub enum RevlogError {
106 106 InvalidRevision,
107 107 /// Working directory is not supported
108 108 WDirUnsupported,
109 109 /// Found more than one entry whose ID match the requested prefix
110 110 AmbiguousPrefix,
111 111 #[from]
112 112 Other(HgError),
113 113 }
114 114
115 115 impl From<NodeMapError> for RevlogError {
116 116 fn from(error: NodeMapError) -> Self {
117 117 match error {
118 118 NodeMapError::MultipleResults => RevlogError::AmbiguousPrefix,
119 119 NodeMapError::RevisionNotInIndex(rev) => RevlogError::corrupted(
120 120 format!("nodemap point to revision {} not in index", rev),
121 121 ),
122 122 }
123 123 }
124 124 }
125 125
126 126 fn corrupted<S: AsRef<str>>(context: S) -> HgError {
127 127 HgError::corrupted(format!("corrupted revlog, {}", context.as_ref()))
128 128 }
129 129
130 130 impl RevlogError {
131 131 fn corrupted<S: AsRef<str>>(context: S) -> Self {
132 132 RevlogError::Other(corrupted(context))
133 133 }
134 134 }
135 135
136 136 /// Read only implementation of revlog.
137 137 pub struct Revlog {
138 138 /// When index and data are not interleaved: bytes of the revlog index.
139 139 /// When index and data are interleaved: bytes of the revlog index and
140 140 /// data.
141 141 index: Index,
142 142 /// When index and data are not interleaved: bytes of the revlog data
143 143 data_bytes: Option<Box<dyn Deref<Target = [u8]> + Send>>,
144 144 /// When present on disk: the persistent nodemap for this revlog
145 145 nodemap: Option<nodemap::NodeTree>,
146 146 }
147 147
148 148 impl Revlog {
149 149 /// Open a revlog index file.
150 150 ///
151 151 /// It will also open the associated data file if index and data are not
152 152 /// interleaved.
153 153 pub fn open(
154 154 store_vfs: &Vfs,
155 155 index_path: impl AsRef<Path>,
156 156 data_path: Option<&Path>,
157 157 use_nodemap: bool,
158 158 ) -> Result<Self, HgError> {
159 159 let index_path = index_path.as_ref();
160 160 let index = {
161 161 match store_vfs.mmap_open_opt(&index_path)? {
162 162 None => Index::new(Box::new(vec![])),
163 163 Some(index_mmap) => {
164 164 let index = Index::new(Box::new(index_mmap))?;
165 165 Ok(index)
166 166 }
167 167 }
168 168 }?;
169 169
170 170 let default_data_path = index_path.with_extension("d");
171 171
172 172 // type annotation required
173 173 // won't recognize Mmap as Deref<Target = [u8]>
174 174 let data_bytes: Option<Box<dyn Deref<Target = [u8]> + Send>> =
175 175 if index.is_inline() {
176 176 None
177 177 } else {
178 178 let data_path = data_path.unwrap_or(&default_data_path);
179 179 let data_mmap = store_vfs.mmap_open(data_path)?;
180 180 Some(Box::new(data_mmap))
181 181 };
182 182
183 183 let nodemap = if index.is_inline() || !use_nodemap {
184 184 None
185 185 } else {
186 186 NodeMapDocket::read_from_file(store_vfs, index_path)?.map(
187 187 |(docket, data)| {
188 188 nodemap::NodeTree::load_bytes(
189 189 Box::new(data),
190 190 docket.data_length,
191 191 )
192 192 },
193 193 )
194 194 };
195 195
196 196 Ok(Revlog {
197 197 index,
198 198 data_bytes,
199 199 nodemap,
200 200 })
201 201 }
202 202
203 203 /// Return number of entries of the `Revlog`.
204 204 pub fn len(&self) -> usize {
205 205 self.index.len()
206 206 }
207 207
208 208 /// Returns `true` if the `Revlog` has zero `entries`.
209 209 pub fn is_empty(&self) -> bool {
210 210 self.index.is_empty()
211 211 }
212 212
213 213 /// Returns the node ID for the given revision number, if it exists in this
214 214 /// revlog
215 215 pub fn node_from_rev(&self, rev: Revision) -> Option<&Node> {
216 216 if rev == NULL_REVISION {
217 217 return Some(&NULL_NODE);
218 218 }
219 219 Some(self.index.get_entry(rev)?.hash())
220 220 }
221 221
222 222 /// Return the revision number for the given node ID, if it exists in this
223 223 /// revlog
224 224 pub fn rev_from_node(
225 225 &self,
226 226 node: NodePrefix,
227 227 ) -> Result<Revision, RevlogError> {
228 let looked_up = if let Some(nodemap) = &self.nodemap {
229 nodemap
230 .find_bin(&self.index, node)?
231 .ok_or(RevlogError::InvalidRevision)
232 } else {
233 self.rev_from_node_no_persistent_nodemap(node)
234 };
235
228 236 if node.is_prefix_of(&NULL_NODE) {
229 return Ok(NULL_REVISION);
237 return match looked_up {
238 Ok(_) => Err(RevlogError::AmbiguousPrefix),
239 Err(RevlogError::InvalidRevision) => Ok(NULL_REVISION),
240 res => res,
241 };
242 };
243
244 looked_up
230 245 }
231 246
232 if let Some(nodemap) = &self.nodemap {
233 return nodemap
234 .find_bin(&self.index, node)?
235 .ok_or(RevlogError::InvalidRevision);
236 }
237
238 // Fallback to linear scan when a persistent nodemap is not present.
239 // This happens when the persistent-nodemap experimental feature is not
240 // enabled, or for small revlogs.
241 //
247 /// Same as `rev_from_node`, without using a persistent nodemap
248 ///
249 /// This is used as fallback when a persistent nodemap is not present.
250 /// This happens when the persistent-nodemap experimental feature is not
251 /// enabled, or for small revlogs.
252 fn rev_from_node_no_persistent_nodemap(
253 &self,
254 node: NodePrefix,
255 ) -> Result<Revision, RevlogError> {
256 // Linear scan of the revlog
242 257 // TODO: consider building a non-persistent nodemap in memory to
243 258 // optimize these cases.
244 259 let mut found_by_prefix = None;
245 260 for rev in (0..self.len() as Revision).rev() {
246 261 let index_entry = self.index.get_entry(rev).ok_or_else(|| {
247 262 HgError::corrupted(
248 263 "revlog references a revision not in the index",
249 264 )
250 265 })?;
251 266 if node == *index_entry.hash() {
252 267 return Ok(rev);
253 268 }
254 269 if node.is_prefix_of(index_entry.hash()) {
255 270 if found_by_prefix.is_some() {
256 271 return Err(RevlogError::AmbiguousPrefix);
257 272 }
258 273 found_by_prefix = Some(rev)
259 274 }
260 275 }
261 276 found_by_prefix.ok_or(RevlogError::InvalidRevision)
262 277 }
263 278
264 279 /// Returns whether the given revision exists in this revlog.
265 280 pub fn has_rev(&self, rev: Revision) -> bool {
266 281 self.index.get_entry(rev).is_some()
267 282 }
268 283
269 284 /// Return the full data associated to a revision.
270 285 ///
271 286 /// All entries required to build the final data out of deltas will be
272 287 /// retrieved as needed, and the deltas will be applied to the inital
273 288 /// snapshot to rebuild the final data.
274 289 pub fn get_rev_data(
275 290 &self,
276 291 rev: Revision,
277 292 ) -> Result<Cow<[u8]>, RevlogError> {
278 293 if rev == NULL_REVISION {
279 294 return Ok(Cow::Borrowed(&[]));
280 295 };
281 296 Ok(self.get_entry(rev)?.data()?)
282 297 }
283 298
284 299 /// Check the hash of some given data against the recorded hash.
285 300 pub fn check_hash(
286 301 &self,
287 302 p1: Revision,
288 303 p2: Revision,
289 304 expected: &[u8],
290 305 data: &[u8],
291 306 ) -> bool {
292 307 let e1 = self.index.get_entry(p1);
293 308 let h1 = match e1 {
294 309 Some(ref entry) => entry.hash(),
295 310 None => &NULL_NODE,
296 311 };
297 312 let e2 = self.index.get_entry(p2);
298 313 let h2 = match e2 {
299 314 Some(ref entry) => entry.hash(),
300 315 None => &NULL_NODE,
301 316 };
302 317
303 318 hash(data, h1.as_bytes(), h2.as_bytes()) == expected
304 319 }
305 320
306 321 /// Build the full data of a revision out its snapshot
307 322 /// and its deltas.
308 323 fn build_data_from_deltas(
309 324 snapshot: RevlogEntry,
310 325 deltas: &[RevlogEntry],
311 326 ) -> Result<Vec<u8>, HgError> {
312 327 let snapshot = snapshot.data_chunk()?;
313 328 let deltas = deltas
314 329 .iter()
315 330 .rev()
316 331 .map(RevlogEntry::data_chunk)
317 332 .collect::<Result<Vec<_>, _>>()?;
318 333 let patches: Vec<_> =
319 334 deltas.iter().map(|d| patch::PatchList::new(d)).collect();
320 335 let patch = patch::fold_patch_lists(&patches);
321 336 Ok(patch.apply(&snapshot))
322 337 }
323 338
324 339 /// Return the revlog data.
325 340 fn data(&self) -> &[u8] {
326 341 match &self.data_bytes {
327 342 Some(data_bytes) => data_bytes,
328 343 None => panic!(
329 344 "forgot to load the data or trying to access inline data"
330 345 ),
331 346 }
332 347 }
333 348
334 349 pub fn make_null_entry(&self) -> RevlogEntry {
335 350 RevlogEntry {
336 351 revlog: self,
337 352 rev: NULL_REVISION,
338 353 bytes: b"",
339 354 compressed_len: 0,
340 355 uncompressed_len: 0,
341 356 base_rev_or_base_of_delta_chain: None,
342 357 p1: NULL_REVISION,
343 358 p2: NULL_REVISION,
344 359 flags: NULL_REVLOG_ENTRY_FLAGS,
345 360 hash: NULL_NODE,
346 361 }
347 362 }
348 363
349 364 /// Get an entry of the revlog.
350 365 pub fn get_entry(
351 366 &self,
352 367 rev: Revision,
353 368 ) -> Result<RevlogEntry, RevlogError> {
354 369 if rev == NULL_REVISION {
355 370 return Ok(self.make_null_entry());
356 371 }
357 372 let index_entry = self
358 373 .index
359 374 .get_entry(rev)
360 375 .ok_or(RevlogError::InvalidRevision)?;
361 376 let start = index_entry.offset();
362 377 let end = start + index_entry.compressed_len() as usize;
363 378 let data = if self.index.is_inline() {
364 379 self.index.data(start, end)
365 380 } else {
366 381 &self.data()[start..end]
367 382 };
368 383 let entry = RevlogEntry {
369 384 revlog: self,
370 385 rev,
371 386 bytes: data,
372 387 compressed_len: index_entry.compressed_len(),
373 388 uncompressed_len: index_entry.uncompressed_len(),
374 389 base_rev_or_base_of_delta_chain: if index_entry
375 390 .base_revision_or_base_of_delta_chain()
376 391 == rev
377 392 {
378 393 None
379 394 } else {
380 395 Some(index_entry.base_revision_or_base_of_delta_chain())
381 396 },
382 397 p1: index_entry.p1(),
383 398 p2: index_entry.p2(),
384 399 flags: index_entry.flags(),
385 400 hash: *index_entry.hash(),
386 401 };
387 402 Ok(entry)
388 403 }
389 404
390 405 /// when resolving internal references within revlog, any errors
391 406 /// should be reported as corruption, instead of e.g. "invalid revision"
392 407 fn get_entry_internal(
393 408 &self,
394 409 rev: Revision,
395 410 ) -> Result<RevlogEntry, HgError> {
396 411 self.get_entry(rev)
397 412 .map_err(|_| corrupted(format!("revision {} out of range", rev)))
398 413 }
399 414 }
400 415
401 416 /// The revlog entry's bytes and the necessary informations to extract
402 417 /// the entry's data.
403 418 #[derive(Clone)]
404 419 pub struct RevlogEntry<'revlog> {
405 420 revlog: &'revlog Revlog,
406 421 rev: Revision,
407 422 bytes: &'revlog [u8],
408 423 compressed_len: u32,
409 424 uncompressed_len: i32,
410 425 base_rev_or_base_of_delta_chain: Option<Revision>,
411 426 p1: Revision,
412 427 p2: Revision,
413 428 flags: u16,
414 429 hash: Node,
415 430 }
416 431
417 432 thread_local! {
418 433 // seems fine to [unwrap] here: this can only fail due to memory allocation
419 434 // failing, and it's normal for that to cause panic.
420 435 static ZSTD_DECODER : RefCell<zstd::bulk::Decompressor<'static>> =
421 436 RefCell::new(zstd::bulk::Decompressor::new().ok().unwrap());
422 437 }
423 438
424 439 fn zstd_decompress_to_buffer(
425 440 bytes: &[u8],
426 441 buf: &mut Vec<u8>,
427 442 ) -> Result<usize, std::io::Error> {
428 443 ZSTD_DECODER
429 444 .with(|decoder| decoder.borrow_mut().decompress_to_buffer(bytes, buf))
430 445 }
431 446
432 447 impl<'revlog> RevlogEntry<'revlog> {
433 448 pub fn revision(&self) -> Revision {
434 449 self.rev
435 450 }
436 451
437 452 pub fn node(&self) -> &Node {
438 453 &self.hash
439 454 }
440 455
441 456 pub fn uncompressed_len(&self) -> Option<u32> {
442 457 u32::try_from(self.uncompressed_len).ok()
443 458 }
444 459
445 460 pub fn has_p1(&self) -> bool {
446 461 self.p1 != NULL_REVISION
447 462 }
448 463
449 464 pub fn p1_entry(
450 465 &self,
451 466 ) -> Result<Option<RevlogEntry<'revlog>>, RevlogError> {
452 467 if self.p1 == NULL_REVISION {
453 468 Ok(None)
454 469 } else {
455 470 Ok(Some(self.revlog.get_entry(self.p1)?))
456 471 }
457 472 }
458 473
459 474 pub fn p2_entry(
460 475 &self,
461 476 ) -> Result<Option<RevlogEntry<'revlog>>, RevlogError> {
462 477 if self.p2 == NULL_REVISION {
463 478 Ok(None)
464 479 } else {
465 480 Ok(Some(self.revlog.get_entry(self.p2)?))
466 481 }
467 482 }
468 483
469 484 pub fn p1(&self) -> Option<Revision> {
470 485 if self.p1 == NULL_REVISION {
471 486 None
472 487 } else {
473 488 Some(self.p1)
474 489 }
475 490 }
476 491
477 492 pub fn p2(&self) -> Option<Revision> {
478 493 if self.p2 == NULL_REVISION {
479 494 None
480 495 } else {
481 496 Some(self.p2)
482 497 }
483 498 }
484 499
485 500 pub fn is_censored(&self) -> bool {
486 501 (self.flags & REVISION_FLAG_CENSORED) != 0
487 502 }
488 503
489 504 pub fn has_length_affecting_flag_processor(&self) -> bool {
490 505 // Relevant Python code: revlog.size()
491 506 // note: ELLIPSIS is known to not change the content
492 507 (self.flags & (REVIDX_KNOWN_FLAGS ^ REVISION_FLAG_ELLIPSIS)) != 0
493 508 }
494 509
495 510 /// The data for this entry, after resolving deltas if any.
496 511 pub fn rawdata(&self) -> Result<Cow<'revlog, [u8]>, HgError> {
497 512 let mut entry = self.clone();
498 513 let mut delta_chain = vec![];
499 514
500 515 // The meaning of `base_rev_or_base_of_delta_chain` depends on
501 516 // generaldelta. See the doc on `ENTRY_DELTA_BASE` in
502 517 // `mercurial/revlogutils/constants.py` and the code in
503 518 // [_chaininfo] and in [index_deltachain].
504 519 let uses_generaldelta = self.revlog.index.uses_generaldelta();
505 520 while let Some(base_rev) = entry.base_rev_or_base_of_delta_chain {
506 521 let base_rev = if uses_generaldelta {
507 522 base_rev
508 523 } else {
509 524 entry.rev - 1
510 525 };
511 526 delta_chain.push(entry);
512 527 entry = self.revlog.get_entry_internal(base_rev)?;
513 528 }
514 529
515 530 let data = if delta_chain.is_empty() {
516 531 entry.data_chunk()?
517 532 } else {
518 533 Revlog::build_data_from_deltas(entry, &delta_chain)?.into()
519 534 };
520 535
521 536 Ok(data)
522 537 }
523 538
524 539 fn check_data(
525 540 &self,
526 541 data: Cow<'revlog, [u8]>,
527 542 ) -> Result<Cow<'revlog, [u8]>, HgError> {
528 543 if self.revlog.check_hash(
529 544 self.p1,
530 545 self.p2,
531 546 self.hash.as_bytes(),
532 547 &data,
533 548 ) {
534 549 Ok(data)
535 550 } else {
536 551 if (self.flags & REVISION_FLAG_ELLIPSIS) != 0 {
537 552 return Err(HgError::unsupported(
538 553 "ellipsis revisions are not supported by rhg",
539 554 ));
540 555 }
541 556 Err(corrupted(format!(
542 557 "hash check failed for revision {}",
543 558 self.rev
544 559 )))
545 560 }
546 561 }
547 562
548 563 pub fn data(&self) -> Result<Cow<'revlog, [u8]>, HgError> {
549 564 let data = self.rawdata()?;
565 if self.rev == NULL_REVISION {
566 return Ok(data);
567 }
550 568 if self.is_censored() {
551 569 return Err(HgError::CensoredNodeError);
552 570 }
553 571 self.check_data(data)
554 572 }
555 573
556 574 /// Extract the data contained in the entry.
557 575 /// This may be a delta. (See `is_delta`.)
558 576 fn data_chunk(&self) -> Result<Cow<'revlog, [u8]>, HgError> {
559 577 if self.bytes.is_empty() {
560 578 return Ok(Cow::Borrowed(&[]));
561 579 }
562 580 match self.bytes[0] {
563 581 // Revision data is the entirety of the entry, including this
564 582 // header.
565 583 b'\0' => Ok(Cow::Borrowed(self.bytes)),
566 584 // Raw revision data follows.
567 585 b'u' => Ok(Cow::Borrowed(&self.bytes[1..])),
568 586 // zlib (RFC 1950) data.
569 587 b'x' => Ok(Cow::Owned(self.uncompressed_zlib_data()?)),
570 588 // zstd data.
571 589 b'\x28' => Ok(Cow::Owned(self.uncompressed_zstd_data()?)),
572 590 // A proper new format should have had a repo/store requirement.
573 591 format_type => Err(corrupted(format!(
574 592 "unknown compression header '{}'",
575 593 format_type
576 594 ))),
577 595 }
578 596 }
579 597
580 598 fn uncompressed_zlib_data(&self) -> Result<Vec<u8>, HgError> {
581 599 let mut decoder = ZlibDecoder::new(self.bytes);
582 600 if self.is_delta() {
583 601 let mut buf = Vec::with_capacity(self.compressed_len as usize);
584 602 decoder
585 603 .read_to_end(&mut buf)
586 604 .map_err(|e| corrupted(e.to_string()))?;
587 605 Ok(buf)
588 606 } else {
589 607 let cap = self.uncompressed_len.max(0) as usize;
590 608 let mut buf = vec![0; cap];
591 609 decoder
592 610 .read_exact(&mut buf)
593 611 .map_err(|e| corrupted(e.to_string()))?;
594 612 Ok(buf)
595 613 }
596 614 }
597 615
598 616 fn uncompressed_zstd_data(&self) -> Result<Vec<u8>, HgError> {
599 617 let cap = self.uncompressed_len.max(0) as usize;
600 618 if self.is_delta() {
601 619 // [cap] is usually an over-estimate of the space needed because
602 620 // it's the length of delta-decoded data, but we're interested
603 621 // in the size of the delta.
604 622 // This means we have to [shrink_to_fit] to avoid holding on
605 623 // to a large chunk of memory, but it also means we must have a
606 624 // fallback branch, for the case when the delta is longer than
607 625 // the original data (surprisingly, this does happen in practice)
608 626 let mut buf = Vec::with_capacity(cap);
609 627 match zstd_decompress_to_buffer(self.bytes, &mut buf) {
610 628 Ok(_) => buf.shrink_to_fit(),
611 629 Err(_) => {
612 630 buf.clear();
613 631 zstd::stream::copy_decode(self.bytes, &mut buf)
614 632 .map_err(|e| corrupted(e.to_string()))?;
615 633 }
616 634 };
617 635 Ok(buf)
618 636 } else {
619 637 let mut buf = Vec::with_capacity(cap);
620 638 let len = zstd_decompress_to_buffer(self.bytes, &mut buf)
621 639 .map_err(|e| corrupted(e.to_string()))?;
622 640 if len != self.uncompressed_len as usize {
623 641 Err(corrupted("uncompressed length does not match"))
624 642 } else {
625 643 Ok(buf)
626 644 }
627 645 }
628 646 }
629 647
630 648 /// Tell if the entry is a snapshot or a delta
631 649 /// (influences on decompression).
632 650 fn is_delta(&self) -> bool {
633 651 self.base_rev_or_base_of_delta_chain.is_some()
634 652 }
635 653 }
636 654
637 655 /// Calculate the hash of a revision given its data and its parents.
638 656 fn hash(
639 657 data: &[u8],
640 658 p1_hash: &[u8],
641 659 p2_hash: &[u8],
642 660 ) -> [u8; NODE_BYTES_LENGTH] {
643 661 let mut hasher = Sha1::new();
644 662 let (a, b) = (p1_hash, p2_hash);
645 663 if a > b {
646 664 hasher.update(b);
647 665 hasher.update(a);
648 666 } else {
649 667 hasher.update(a);
650 668 hasher.update(b);
651 669 }
652 670 hasher.update(data);
653 671 *hasher.finalize().as_ref()
654 672 }
655 673
656 674 #[cfg(test)]
657 675 mod tests {
658 676 use super::*;
659 677 use crate::index::{IndexEntryBuilder, INDEX_ENTRY_SIZE};
660 678 use itertools::Itertools;
661 679
662 680 #[test]
663 681 fn test_empty() {
664 682 let temp = tempfile::tempdir().unwrap();
665 683 let vfs = Vfs { base: temp.path() };
666 684 std::fs::write(temp.path().join("foo.i"), b"").unwrap();
667 685 let revlog = Revlog::open(&vfs, "foo.i", None, false).unwrap();
668 686 assert!(revlog.is_empty());
669 687 assert_eq!(revlog.len(), 0);
670 688 assert!(revlog.get_entry(0).is_err());
671 689 assert!(!revlog.has_rev(0));
690 assert_eq!(
691 revlog.rev_from_node(NULL_NODE.into()).unwrap(),
692 NULL_REVISION
693 );
694 let null_entry = revlog.get_entry(NULL_REVISION).ok().unwrap();
695 assert_eq!(null_entry.revision(), NULL_REVISION);
696 assert!(null_entry.data().unwrap().is_empty());
672 697 }
673 698
674 699 #[test]
675 700 fn test_inline() {
676 701 let temp = tempfile::tempdir().unwrap();
677 702 let vfs = Vfs { base: temp.path() };
678 703 let node0 = Node::from_hex("2ed2a3912a0b24502043eae84ee4b279c18b90dd")
679 704 .unwrap();
680 705 let node1 = Node::from_hex("b004912a8510032a0350a74daa2803dadfb00e12")
681 706 .unwrap();
682 707 let node2 = Node::from_hex("dd6ad206e907be60927b5a3117b97dffb2590582")
683 708 .unwrap();
684 709 let entry0_bytes = IndexEntryBuilder::new()
685 710 .is_first(true)
686 711 .with_version(1)
687 712 .with_inline(true)
688 713 .with_offset(INDEX_ENTRY_SIZE)
689 714 .with_node(node0)
690 715 .build();
691 716 let entry1_bytes = IndexEntryBuilder::new()
692 717 .with_offset(INDEX_ENTRY_SIZE)
693 718 .with_node(node1)
694 719 .build();
695 720 let entry2_bytes = IndexEntryBuilder::new()
696 721 .with_offset(INDEX_ENTRY_SIZE)
697 722 .with_p1(0)
698 723 .with_p2(1)
699 724 .with_node(node2)
700 725 .build();
701 726 let contents = vec![entry0_bytes, entry1_bytes, entry2_bytes]
702 727 .into_iter()
703 728 .flatten()
704 729 .collect_vec();
705 730 std::fs::write(temp.path().join("foo.i"), contents).unwrap();
706 731 let revlog = Revlog::open(&vfs, "foo.i", None, false).unwrap();
707 732
708 733 let entry0 = revlog.get_entry(0).ok().unwrap();
709 734 assert_eq!(entry0.revision(), 0);
710 735 assert_eq!(*entry0.node(), node0);
711 736 assert!(!entry0.has_p1());
712 737 assert_eq!(entry0.p1(), None);
713 738 assert_eq!(entry0.p2(), None);
714 739 let p1_entry = entry0.p1_entry().unwrap();
715 740 assert!(p1_entry.is_none());
716 741 let p2_entry = entry0.p2_entry().unwrap();
717 742 assert!(p2_entry.is_none());
718 743
719 744 let entry1 = revlog.get_entry(1).ok().unwrap();
720 745 assert_eq!(entry1.revision(), 1);
721 746 assert_eq!(*entry1.node(), node1);
722 747 assert!(!entry1.has_p1());
723 748 assert_eq!(entry1.p1(), None);
724 749 assert_eq!(entry1.p2(), None);
725 750 let p1_entry = entry1.p1_entry().unwrap();
726 751 assert!(p1_entry.is_none());
727 752 let p2_entry = entry1.p2_entry().unwrap();
728 753 assert!(p2_entry.is_none());
729 754
730 755 let entry2 = revlog.get_entry(2).ok().unwrap();
731 756 assert_eq!(entry2.revision(), 2);
732 757 assert_eq!(*entry2.node(), node2);
733 758 assert!(entry2.has_p1());
734 759 assert_eq!(entry2.p1(), Some(0));
735 760 assert_eq!(entry2.p2(), Some(1));
736 761 let p1_entry = entry2.p1_entry().unwrap();
737 762 assert!(p1_entry.is_some());
738 763 assert_eq!(p1_entry.unwrap().revision(), 0);
739 764 let p2_entry = entry2.p2_entry().unwrap();
740 765 assert!(p2_entry.is_some());
741 766 assert_eq!(p2_entry.unwrap().revision(), 1);
742 767 }
768
769 #[test]
770 fn test_nodemap() {
771 let temp = tempfile::tempdir().unwrap();
772 let vfs = Vfs { base: temp.path() };
773
774 // building a revlog with a forced Node starting with zeros
775 // This is a corruption, but it does not preclude using the nodemap
776 // if we don't try and access the data
777 let node0 = Node::from_hex("00d2a3912a0b24502043eae84ee4b279c18b90dd")
778 .unwrap();
779 let node1 = Node::from_hex("b004912a8510032a0350a74daa2803dadfb00e12")
780 .unwrap();
781 let entry0_bytes = IndexEntryBuilder::new()
782 .is_first(true)
783 .with_version(1)
784 .with_inline(true)
785 .with_offset(INDEX_ENTRY_SIZE)
786 .with_node(node0)
787 .build();
788 let entry1_bytes = IndexEntryBuilder::new()
789 .with_offset(INDEX_ENTRY_SIZE)
790 .with_node(node1)
791 .build();
792 let contents = vec![entry0_bytes, entry1_bytes]
793 .into_iter()
794 .flatten()
795 .collect_vec();
796 std::fs::write(temp.path().join("foo.i"), contents).unwrap();
797 let revlog = Revlog::open(&vfs, "foo.i", None, false).unwrap();
798
799 // accessing the data shows the corruption
800 revlog.get_entry(0).unwrap().data().unwrap_err();
801
802 assert_eq!(revlog.rev_from_node(NULL_NODE.into()).unwrap(), -1);
803 assert_eq!(revlog.rev_from_node(node0.into()).unwrap(), 0);
804 assert_eq!(revlog.rev_from_node(node1.into()).unwrap(), 1);
805 assert_eq!(
806 revlog
807 .rev_from_node(NodePrefix::from_hex("000").unwrap())
808 .unwrap(),
809 -1
810 );
811 assert_eq!(
812 revlog
813 .rev_from_node(NodePrefix::from_hex("b00").unwrap())
814 .unwrap(),
815 1
816 );
817 // RevlogError does not implement PartialEq
818 // (ultimately because io::Error does not)
819 match revlog
820 .rev_from_node(NodePrefix::from_hex("00").unwrap())
821 .expect_err("Expected to give AmbiguousPrefix error")
822 {
823 RevlogError::AmbiguousPrefix => (),
824 e => {
825 panic!("Got another error than AmbiguousPrefix: {:?}", e);
743 826 }
827 };
828 }
829 }
@@ -1,1804 +1,1807 b''
1 1 #
2 2 # This is the mercurial setup script.
3 3 #
4 4 # 'python setup.py install', or
5 5 # 'python setup.py --help' for more options
6 6 import os
7 7
8 8 # Mercurial can't work on 3.6.0 or 3.6.1 due to a bug in % formatting
9 9 # in bytestrings.
10 10 supportedpy = ','.join(
11 11 [
12 12 '>=3.6.2',
13 13 ]
14 14 )
15 15
16 16 import sys, platform
17 17 import sysconfig
18 18
19 19
20 20 def sysstr(s):
21 21 return s.decode('latin-1')
22 22
23 23
24 24 def eprint(*args, **kwargs):
25 25 kwargs['file'] = sys.stderr
26 26 print(*args, **kwargs)
27 27
28 28
29 29 import ssl
30 30
31 31 # ssl.HAS_TLSv1* are preferred to check support but they were added in Python
32 32 # 3.7. Prior to CPython commit 6e8cda91d92da72800d891b2fc2073ecbc134d98
33 33 # (backported to the 3.7 branch), ssl.PROTOCOL_TLSv1_1 / ssl.PROTOCOL_TLSv1_2
34 34 # were defined only if compiled against a OpenSSL version with TLS 1.1 / 1.2
35 35 # support. At the mentioned commit, they were unconditionally defined.
36 36 _notset = object()
37 37 has_tlsv1_1 = getattr(ssl, 'HAS_TLSv1_1', _notset)
38 38 if has_tlsv1_1 is _notset:
39 39 has_tlsv1_1 = getattr(ssl, 'PROTOCOL_TLSv1_1', _notset) is not _notset
40 40 has_tlsv1_2 = getattr(ssl, 'HAS_TLSv1_2', _notset)
41 41 if has_tlsv1_2 is _notset:
42 42 has_tlsv1_2 = getattr(ssl, 'PROTOCOL_TLSv1_2', _notset) is not _notset
43 43 if not (has_tlsv1_1 or has_tlsv1_2):
44 44 error = """
45 45 The `ssl` module does not advertise support for TLS 1.1 or TLS 1.2.
46 46 Please make sure that your Python installation was compiled against an OpenSSL
47 47 version enabling these features (likely this requires the OpenSSL version to
48 48 be at least 1.0.1).
49 49 """
50 50 print(error, file=sys.stderr)
51 51 sys.exit(1)
52 52
53 53 DYLIB_SUFFIX = sysconfig.get_config_vars()['EXT_SUFFIX']
54 54
55 55 # Solaris Python packaging brain damage
56 56 try:
57 57 import hashlib
58 58
59 59 sha = hashlib.sha1()
60 60 except ImportError:
61 61 try:
62 62 import sha
63 63
64 64 sha.sha # silence unused import warning
65 65 except ImportError:
66 66 raise SystemExit(
67 67 "Couldn't import standard hashlib (incomplete Python install)."
68 68 )
69 69
70 70 try:
71 71 import zlib
72 72
73 73 zlib.compressobj # silence unused import warning
74 74 except ImportError:
75 75 raise SystemExit(
76 76 "Couldn't import standard zlib (incomplete Python install)."
77 77 )
78 78
79 79 # The base IronPython distribution (as of 2.7.1) doesn't support bz2
80 80 isironpython = False
81 81 try:
82 82 isironpython = (
83 83 platform.python_implementation().lower().find("ironpython") != -1
84 84 )
85 85 except AttributeError:
86 86 pass
87 87
88 88 if isironpython:
89 89 sys.stderr.write("warning: IronPython detected (no bz2 support)\n")
90 90 else:
91 91 try:
92 92 import bz2
93 93
94 94 bz2.BZ2Compressor # silence unused import warning
95 95 except ImportError:
96 96 raise SystemExit(
97 97 "Couldn't import standard bz2 (incomplete Python install)."
98 98 )
99 99
100 100 ispypy = "PyPy" in sys.version
101 101
102 102 import ctypes
103 103 import stat, subprocess, time
104 104 import re
105 105 import shutil
106 106 import tempfile
107 107
108 108 # We have issues with setuptools on some platforms and builders. Until
109 109 # those are resolved, setuptools is opt-in except for platforms where
110 110 # we don't have issues.
111 111 issetuptools = os.name == 'nt' or 'FORCE_SETUPTOOLS' in os.environ
112 112 if issetuptools:
113 113 from setuptools import setup
114 114 else:
115 try:
115 116 from distutils.core import setup
117 except ModuleNotFoundError:
118 from setuptools import setup
116 119 from distutils.ccompiler import new_compiler
117 120 from distutils.core import Command, Extension
118 121 from distutils.dist import Distribution
119 122 from distutils.command.build import build
120 123 from distutils.command.build_ext import build_ext
121 124 from distutils.command.build_py import build_py
122 125 from distutils.command.build_scripts import build_scripts
123 126 from distutils.command.install import install
124 127 from distutils.command.install_lib import install_lib
125 128 from distutils.command.install_scripts import install_scripts
126 129 from distutils import log
127 130 from distutils.spawn import spawn, find_executable
128 131 from distutils import file_util
129 132 from distutils.errors import (
130 133 CCompilerError,
131 134 DistutilsError,
132 135 DistutilsExecError,
133 136 )
134 137 from distutils.sysconfig import get_python_inc
135 138
136 139
137 140 def write_if_changed(path, content):
138 141 """Write content to a file iff the content hasn't changed."""
139 142 if os.path.exists(path):
140 143 with open(path, 'rb') as fh:
141 144 current = fh.read()
142 145 else:
143 146 current = b''
144 147
145 148 if current != content:
146 149 with open(path, 'wb') as fh:
147 150 fh.write(content)
148 151
149 152
150 153 scripts = ['hg']
151 154 if os.name == 'nt':
152 155 # We remove hg.bat if we are able to build hg.exe.
153 156 scripts.append('contrib/win32/hg.bat')
154 157
155 158
156 159 def cancompile(cc, code):
157 160 tmpdir = tempfile.mkdtemp(prefix='hg-install-')
158 161 devnull = oldstderr = None
159 162 try:
160 163 fname = os.path.join(tmpdir, 'testcomp.c')
161 164 f = open(fname, 'w')
162 165 f.write(code)
163 166 f.close()
164 167 # Redirect stderr to /dev/null to hide any error messages
165 168 # from the compiler.
166 169 # This will have to be changed if we ever have to check
167 170 # for a function on Windows.
168 171 devnull = open('/dev/null', 'w')
169 172 oldstderr = os.dup(sys.stderr.fileno())
170 173 os.dup2(devnull.fileno(), sys.stderr.fileno())
171 174 objects = cc.compile([fname], output_dir=tmpdir)
172 175 cc.link_executable(objects, os.path.join(tmpdir, "a.out"))
173 176 return True
174 177 except Exception:
175 178 return False
176 179 finally:
177 180 if oldstderr is not None:
178 181 os.dup2(oldstderr, sys.stderr.fileno())
179 182 if devnull is not None:
180 183 devnull.close()
181 184 shutil.rmtree(tmpdir)
182 185
183 186
184 187 # simplified version of distutils.ccompiler.CCompiler.has_function
185 188 # that actually removes its temporary files.
186 189 def hasfunction(cc, funcname):
187 190 code = 'int main(void) { %s(); }\n' % funcname
188 191 return cancompile(cc, code)
189 192
190 193
191 194 def hasheader(cc, headername):
192 195 code = '#include <%s>\nint main(void) { return 0; }\n' % headername
193 196 return cancompile(cc, code)
194 197
195 198
196 199 # py2exe needs to be installed to work
197 200 try:
198 201 import py2exe
199 202
200 203 py2exe.patch_distutils()
201 204 py2exeloaded = True
202 205 # import py2exe's patched Distribution class
203 206 from distutils.core import Distribution
204 207 except ImportError:
205 208 py2exeloaded = False
206 209
207 210
208 211 def runcmd(cmd, env, cwd=None):
209 212 p = subprocess.Popen(
210 213 cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, cwd=cwd
211 214 )
212 215 out, err = p.communicate()
213 216 return p.returncode, out, err
214 217
215 218
216 219 class hgcommand:
217 220 def __init__(self, cmd, env):
218 221 self.cmd = cmd
219 222 self.env = env
220 223
221 224 def run(self, args):
222 225 cmd = self.cmd + args
223 226 returncode, out, err = runcmd(cmd, self.env)
224 227 err = filterhgerr(err)
225 228 if err:
226 229 print("stderr from '%s':" % (' '.join(cmd)), file=sys.stderr)
227 230 print(err, file=sys.stderr)
228 231 if returncode != 0:
229 232 return b''
230 233 return out
231 234
232 235
233 236 def filterhgerr(err):
234 237 # If root is executing setup.py, but the repository is owned by
235 238 # another user (as in "sudo python setup.py install") we will get
236 239 # trust warnings since the .hg/hgrc file is untrusted. That is
237 240 # fine, we don't want to load it anyway. Python may warn about
238 241 # a missing __init__.py in mercurial/locale, we also ignore that.
239 242 err = [
240 243 e
241 244 for e in err.splitlines()
242 245 if (
243 246 not e.startswith(b'not trusting file')
244 247 and not e.startswith(b'warning: Not importing')
245 248 and not e.startswith(b'obsolete feature not enabled')
246 249 and not e.startswith(b'*** failed to import extension')
247 250 and not e.startswith(b'devel-warn:')
248 251 and not (
249 252 e.startswith(b'(third party extension')
250 253 and e.endswith(b'or newer of Mercurial; disabling)')
251 254 )
252 255 )
253 256 ]
254 257 return b'\n'.join(b' ' + e for e in err)
255 258
256 259
257 260 def findhg():
258 261 """Try to figure out how we should invoke hg for examining the local
259 262 repository contents.
260 263
261 264 Returns an hgcommand object."""
262 265 # By default, prefer the "hg" command in the user's path. This was
263 266 # presumably the hg command that the user used to create this repository.
264 267 #
265 268 # This repository may require extensions or other settings that would not
266 269 # be enabled by running the hg script directly from this local repository.
267 270 hgenv = os.environ.copy()
268 271 # Use HGPLAIN to disable hgrc settings that would change output formatting,
269 272 # and disable localization for the same reasons.
270 273 hgenv['HGPLAIN'] = '1'
271 274 hgenv['LANGUAGE'] = 'C'
272 275 hgcmd = ['hg']
273 276 # Run a simple "hg log" command just to see if using hg from the user's
274 277 # path works and can successfully interact with this repository. Windows
275 278 # gives precedence to hg.exe in the current directory, so fall back to the
276 279 # python invocation of local hg, where pythonXY.dll can always be found.
277 280 check_cmd = ['log', '-r.', '-Ttest']
278 281 if os.name != 'nt' or not os.path.exists("hg.exe"):
279 282 try:
280 283 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
281 284 except EnvironmentError:
282 285 retcode = -1
283 286 if retcode == 0 and not filterhgerr(err):
284 287 return hgcommand(hgcmd, hgenv)
285 288
286 289 # Fall back to trying the local hg installation.
287 290 hgenv = localhgenv()
288 291 hgcmd = [sys.executable, 'hg']
289 292 try:
290 293 retcode, out, err = runcmd(hgcmd + check_cmd, hgenv)
291 294 except EnvironmentError:
292 295 retcode = -1
293 296 if retcode == 0 and not filterhgerr(err):
294 297 return hgcommand(hgcmd, hgenv)
295 298
296 299 eprint("/!\\")
297 300 eprint(r"/!\ Unable to find a working hg binary")
298 301 eprint(r"/!\ Version cannot be extract from the repository")
299 302 eprint(r"/!\ Re-run the setup once a first version is built")
300 303 return None
301 304
302 305
303 306 def localhgenv():
304 307 """Get an environment dictionary to use for invoking or importing
305 308 mercurial from the local repository."""
306 309 # Execute hg out of this directory with a custom environment which takes
307 310 # care to not use any hgrc files and do no localization.
308 311 env = {
309 312 'HGMODULEPOLICY': 'py',
310 313 'HGRCPATH': '',
311 314 'LANGUAGE': 'C',
312 315 'PATH': '',
313 316 } # make pypi modules that use os.environ['PATH'] happy
314 317 if 'LD_LIBRARY_PATH' in os.environ:
315 318 env['LD_LIBRARY_PATH'] = os.environ['LD_LIBRARY_PATH']
316 319 if 'SystemRoot' in os.environ:
317 320 # SystemRoot is required by Windows to load various DLLs. See:
318 321 # https://bugs.python.org/issue13524#msg148850
319 322 env['SystemRoot'] = os.environ['SystemRoot']
320 323 return env
321 324
322 325
323 326 version = ''
324 327
325 328
326 329 def _try_get_version():
327 330 hg = findhg()
328 331 if hg is None:
329 332 return ''
330 333 hgid = None
331 334 numerictags = []
332 335 cmd = ['log', '-r', '.', '--template', '{tags}\n']
333 336 pieces = sysstr(hg.run(cmd)).split()
334 337 numerictags = [t for t in pieces if t[0:1].isdigit()]
335 338 hgid = sysstr(hg.run(['id', '-i'])).strip()
336 339 if hgid.count('+') == 2:
337 340 hgid = hgid.replace("+", ".", 1)
338 341 if not hgid:
339 342 eprint("/!\\")
340 343 eprint(r"/!\ Unable to determine hg version from local repository")
341 344 eprint(r"/!\ Failed to retrieve current revision tags")
342 345 return ''
343 346 if numerictags: # tag(s) found
344 347 version = numerictags[-1]
345 348 if hgid.endswith('+'): # propagate the dirty status to the tag
346 349 version += '+'
347 350 else: # no tag found on the checked out revision
348 351 ltagcmd = ['log', '--rev', 'wdir()', '--template', '{latesttag}']
349 352 ltag = sysstr(hg.run(ltagcmd))
350 353 if not ltag:
351 354 eprint("/!\\")
352 355 eprint(r"/!\ Unable to determine hg version from local repository")
353 356 eprint(
354 357 r"/!\ Failed to retrieve current revision distance to lated tag"
355 358 )
356 359 return ''
357 360 changessincecmd = [
358 361 'log',
359 362 '-T',
360 363 'x\n',
361 364 '-r',
362 365 "only(parents(),'%s')" % ltag,
363 366 ]
364 367 changessince = len(hg.run(changessincecmd).splitlines())
365 368 version = '%s+hg%s.%s' % (ltag, changessince, hgid)
366 369 if version.endswith('+'):
367 370 version = version[:-1] + 'local' + time.strftime('%Y%m%d')
368 371 return version
369 372
370 373
371 374 if os.path.isdir('.hg'):
372 375 version = _try_get_version()
373 376 elif os.path.exists('.hg_archival.txt'):
374 377 kw = dict(
375 378 [[t.strip() for t in l.split(':', 1)] for l in open('.hg_archival.txt')]
376 379 )
377 380 if 'tag' in kw:
378 381 version = kw['tag']
379 382 elif 'latesttag' in kw:
380 383 if 'changessincelatesttag' in kw:
381 384 version = (
382 385 '%(latesttag)s+hg%(changessincelatesttag)s.%(node).12s' % kw
383 386 )
384 387 else:
385 388 version = '%(latesttag)s+hg%(latesttagdistance)s.%(node).12s' % kw
386 389 else:
387 390 version = '0+hg' + kw.get('node', '')[:12]
388 391 elif os.path.exists('mercurial/__version__.py'):
389 392 with open('mercurial/__version__.py') as f:
390 393 data = f.read()
391 394 version = re.search('version = b"(.*)"', data).group(1)
392 395 if not version:
393 396 if os.environ.get("MERCURIAL_SETUP_MAKE_LOCAL") == "1":
394 397 version = "0.0+0"
395 398 eprint("/!\\")
396 399 eprint(r"/!\ Using '0.0+0' as the default version")
397 400 eprint(r"/!\ Re-run make local once that first version is built")
398 401 eprint("/!\\")
399 402 else:
400 403 eprint("/!\\")
401 404 eprint(r"/!\ Could not determine the Mercurial version")
402 405 eprint(r"/!\ You need to build a local version first")
403 406 eprint(r"/!\ Run `make local` and try again")
404 407 eprint("/!\\")
405 408 msg = "Run `make local` first to get a working local version"
406 409 raise SystemExit(msg)
407 410
408 411 versionb = version
409 412 if not isinstance(versionb, bytes):
410 413 versionb = versionb.encode('ascii')
411 414
412 415 write_if_changed(
413 416 'mercurial/__version__.py',
414 417 b''.join(
415 418 [
416 419 b'# this file is autogenerated by setup.py\n'
417 420 b'version = b"%s"\n' % versionb,
418 421 ]
419 422 ),
420 423 )
421 424
422 425
423 426 class hgbuild(build):
424 427 # Insert hgbuildmo first so that files in mercurial/locale/ are found
425 428 # when build_py is run next.
426 429 sub_commands = [('build_mo', None)] + build.sub_commands
427 430
428 431
429 432 class hgbuildmo(build):
430 433
431 434 description = "build translations (.mo files)"
432 435
433 436 def run(self):
434 437 if not find_executable('msgfmt'):
435 438 self.warn(
436 439 "could not find msgfmt executable, no translations "
437 440 "will be built"
438 441 )
439 442 return
440 443
441 444 podir = 'i18n'
442 445 if not os.path.isdir(podir):
443 446 self.warn("could not find %s/ directory" % podir)
444 447 return
445 448
446 449 join = os.path.join
447 450 for po in os.listdir(podir):
448 451 if not po.endswith('.po'):
449 452 continue
450 453 pofile = join(podir, po)
451 454 modir = join('locale', po[:-3], 'LC_MESSAGES')
452 455 mofile = join(modir, 'hg.mo')
453 456 mobuildfile = join('mercurial', mofile)
454 457 cmd = ['msgfmt', '-v', '-o', mobuildfile, pofile]
455 458 if sys.platform != 'sunos5':
456 459 # msgfmt on Solaris does not know about -c
457 460 cmd.append('-c')
458 461 self.mkpath(join('mercurial', modir))
459 462 self.make_file([pofile], mobuildfile, spawn, (cmd,))
460 463
461 464
462 465 class hgdist(Distribution):
463 466 pure = False
464 467 rust = False
465 468 no_rust = False
466 469 cffi = ispypy
467 470
468 471 global_options = Distribution.global_options + [
469 472 ('pure', None, "use pure (slow) Python code instead of C extensions"),
470 473 ('rust', None, "use Rust extensions additionally to C extensions"),
471 474 (
472 475 'no-rust',
473 476 None,
474 477 "do not use Rust extensions additionally to C extensions",
475 478 ),
476 479 ]
477 480
478 481 negative_opt = Distribution.negative_opt.copy()
479 482 boolean_options = ['pure', 'rust', 'no-rust']
480 483 negative_opt['no-rust'] = 'rust'
481 484
482 485 def _set_command_options(self, command_obj, option_dict=None):
483 486 # Not all distutils versions in the wild have boolean_options.
484 487 # This should be cleaned up when we're Python 3 only.
485 488 command_obj.boolean_options = (
486 489 getattr(command_obj, 'boolean_options', []) + self.boolean_options
487 490 )
488 491 return Distribution._set_command_options(
489 492 self, command_obj, option_dict=option_dict
490 493 )
491 494
492 495 def parse_command_line(self):
493 496 ret = Distribution.parse_command_line(self)
494 497 if not (self.rust or self.no_rust):
495 498 hgrustext = os.environ.get('HGWITHRUSTEXT')
496 499 # TODO record it for proper rebuild upon changes
497 500 # (see mercurial/__modulepolicy__.py)
498 501 if hgrustext != 'cpython' and hgrustext is not None:
499 502 if hgrustext:
500 503 msg = 'unknown HGWITHRUSTEXT value: %s' % hgrustext
501 504 print(msg, file=sys.stderr)
502 505 hgrustext = None
503 506 self.rust = hgrustext is not None
504 507 self.no_rust = not self.rust
505 508 return ret
506 509
507 510 def has_ext_modules(self):
508 511 # self.ext_modules is emptied in hgbuildpy.finalize_options which is
509 512 # too late for some cases
510 513 return not self.pure and Distribution.has_ext_modules(self)
511 514
512 515
513 516 # This is ugly as a one-liner. So use a variable.
514 517 buildextnegops = dict(getattr(build_ext, 'negative_options', {}))
515 518 buildextnegops['no-zstd'] = 'zstd'
516 519 buildextnegops['no-rust'] = 'rust'
517 520
518 521
519 522 class hgbuildext(build_ext):
520 523 user_options = build_ext.user_options + [
521 524 ('zstd', None, 'compile zstd bindings [default]'),
522 525 ('no-zstd', None, 'do not compile zstd bindings'),
523 526 (
524 527 'rust',
525 528 None,
526 529 'compile Rust extensions if they are in use '
527 530 '(requires Cargo) [default]',
528 531 ),
529 532 ('no-rust', None, 'do not compile Rust extensions'),
530 533 ]
531 534
532 535 boolean_options = build_ext.boolean_options + ['zstd', 'rust']
533 536 negative_opt = buildextnegops
534 537
535 538 def initialize_options(self):
536 539 self.zstd = True
537 540 self.rust = True
538 541
539 542 return build_ext.initialize_options(self)
540 543
541 544 def finalize_options(self):
542 545 # Unless overridden by the end user, build extensions in parallel.
543 546 # Only influences behavior on Python 3.5+.
544 547 if getattr(self, 'parallel', None) is None:
545 548 self.parallel = True
546 549
547 550 return build_ext.finalize_options(self)
548 551
549 552 def build_extensions(self):
550 553 ruststandalones = [
551 554 e for e in self.extensions if isinstance(e, RustStandaloneExtension)
552 555 ]
553 556 self.extensions = [
554 557 e for e in self.extensions if e not in ruststandalones
555 558 ]
556 559 # Filter out zstd if disabled via argument.
557 560 if not self.zstd:
558 561 self.extensions = [
559 562 e for e in self.extensions if e.name != 'mercurial.zstd'
560 563 ]
561 564
562 565 # Build Rust standalone extensions if it'll be used
563 566 # and its build is not explicitly disabled (for external build
564 567 # as Linux distributions would do)
565 568 if self.distribution.rust and self.rust:
566 569 if not sys.platform.startswith('linux'):
567 570 self.warn(
568 571 "rust extensions have only been tested on Linux "
569 572 "and may not behave correctly on other platforms"
570 573 )
571 574
572 575 for rustext in ruststandalones:
573 576 rustext.build('' if self.inplace else self.build_lib)
574 577
575 578 return build_ext.build_extensions(self)
576 579
577 580 def build_extension(self, ext):
578 581 if (
579 582 self.distribution.rust
580 583 and self.rust
581 584 and isinstance(ext, RustExtension)
582 585 ):
583 586 ext.rustbuild()
584 587 try:
585 588 build_ext.build_extension(self, ext)
586 589 except CCompilerError:
587 590 if not getattr(ext, 'optional', False):
588 591 raise
589 592 log.warn(
590 593 "Failed to build optional extension '%s' (skipping)", ext.name
591 594 )
592 595
593 596
594 597 class hgbuildscripts(build_scripts):
595 598 def run(self):
596 599 if os.name != 'nt' or self.distribution.pure:
597 600 return build_scripts.run(self)
598 601
599 602 exebuilt = False
600 603 try:
601 604 self.run_command('build_hgexe')
602 605 exebuilt = True
603 606 except (DistutilsError, CCompilerError):
604 607 log.warn('failed to build optional hg.exe')
605 608
606 609 if exebuilt:
607 610 # Copying hg.exe to the scripts build directory ensures it is
608 611 # installed by the install_scripts command.
609 612 hgexecommand = self.get_finalized_command('build_hgexe')
610 613 dest = os.path.join(self.build_dir, 'hg.exe')
611 614 self.mkpath(self.build_dir)
612 615 self.copy_file(hgexecommand.hgexepath, dest)
613 616
614 617 # Remove hg.bat because it is redundant with hg.exe.
615 618 self.scripts.remove('contrib/win32/hg.bat')
616 619
617 620 return build_scripts.run(self)
618 621
619 622
620 623 class hgbuildpy(build_py):
621 624 def finalize_options(self):
622 625 build_py.finalize_options(self)
623 626
624 627 if self.distribution.pure:
625 628 self.distribution.ext_modules = []
626 629 elif self.distribution.cffi:
627 630 from mercurial.cffi import (
628 631 bdiffbuild,
629 632 mpatchbuild,
630 633 )
631 634
632 635 exts = [
633 636 mpatchbuild.ffi.distutils_extension(),
634 637 bdiffbuild.ffi.distutils_extension(),
635 638 ]
636 639 # cffi modules go here
637 640 if sys.platform == 'darwin':
638 641 from mercurial.cffi import osutilbuild
639 642
640 643 exts.append(osutilbuild.ffi.distutils_extension())
641 644 self.distribution.ext_modules = exts
642 645 else:
643 646 h = os.path.join(get_python_inc(), 'Python.h')
644 647 if not os.path.exists(h):
645 648 raise SystemExit(
646 649 'Python headers are required to build '
647 650 'Mercurial but weren\'t found in %s' % h
648 651 )
649 652
650 653 def run(self):
651 654 basepath = os.path.join(self.build_lib, 'mercurial')
652 655 self.mkpath(basepath)
653 656
654 657 rust = self.distribution.rust
655 658 if self.distribution.pure:
656 659 modulepolicy = 'py'
657 660 elif self.build_lib == '.':
658 661 # in-place build should run without rebuilding and Rust extensions
659 662 modulepolicy = 'rust+c-allow' if rust else 'allow'
660 663 else:
661 664 modulepolicy = 'rust+c' if rust else 'c'
662 665
663 666 content = b''.join(
664 667 [
665 668 b'# this file is autogenerated by setup.py\n',
666 669 b'modulepolicy = b"%s"\n' % modulepolicy.encode('ascii'),
667 670 ]
668 671 )
669 672 write_if_changed(os.path.join(basepath, '__modulepolicy__.py'), content)
670 673
671 674 build_py.run(self)
672 675
673 676
674 677 class buildhgextindex(Command):
675 678 description = 'generate prebuilt index of hgext (for frozen package)'
676 679 user_options = []
677 680 _indexfilename = 'hgext/__index__.py'
678 681
679 682 def initialize_options(self):
680 683 pass
681 684
682 685 def finalize_options(self):
683 686 pass
684 687
685 688 def run(self):
686 689 if os.path.exists(self._indexfilename):
687 690 with open(self._indexfilename, 'w') as f:
688 691 f.write('# empty\n')
689 692
690 693 # here no extension enabled, disabled() lists up everything
691 694 code = (
692 695 'import pprint; from mercurial import extensions; '
693 696 'ext = extensions.disabled();'
694 697 'ext.pop("__index__", None);'
695 698 'pprint.pprint(ext)'
696 699 )
697 700 returncode, out, err = runcmd(
698 701 [sys.executable, '-c', code], localhgenv()
699 702 )
700 703 if err or returncode != 0:
701 704 raise DistutilsExecError(err)
702 705
703 706 with open(self._indexfilename, 'wb') as f:
704 707 f.write(b'# this file is autogenerated by setup.py\n')
705 708 f.write(b'docs = ')
706 709 f.write(out)
707 710
708 711
709 712 class buildhgexe(build_ext):
710 713 description = 'compile hg.exe from mercurial/exewrapper.c'
711 714
712 715 LONG_PATHS_MANIFEST = """\
713 716 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
714 717 <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
715 718 <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
716 719 <security>
717 720 <requestedPrivileges>
718 721 <requestedExecutionLevel
719 722 level="asInvoker"
720 723 uiAccess="false"
721 724 />
722 725 </requestedPrivileges>
723 726 </security>
724 727 </trustInfo>
725 728 <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
726 729 <application>
727 730 <!-- Windows Vista -->
728 731 <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
729 732 <!-- Windows 7 -->
730 733 <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
731 734 <!-- Windows 8 -->
732 735 <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
733 736 <!-- Windows 8.1 -->
734 737 <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
735 738 <!-- Windows 10 and Windows 11 -->
736 739 <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
737 740 </application>
738 741 </compatibility>
739 742 <application xmlns="urn:schemas-microsoft-com:asm.v3">
740 743 <windowsSettings
741 744 xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
742 745 <ws2:longPathAware>true</ws2:longPathAware>
743 746 </windowsSettings>
744 747 </application>
745 748 <dependency>
746 749 <dependentAssembly>
747 750 <assemblyIdentity type="win32"
748 751 name="Microsoft.Windows.Common-Controls"
749 752 version="6.0.0.0"
750 753 processorArchitecture="*"
751 754 publicKeyToken="6595b64144ccf1df"
752 755 language="*" />
753 756 </dependentAssembly>
754 757 </dependency>
755 758 </assembly>
756 759 """
757 760
758 761 def initialize_options(self):
759 762 build_ext.initialize_options(self)
760 763
761 764 def build_extensions(self):
762 765 if os.name != 'nt':
763 766 return
764 767 if isinstance(self.compiler, HackedMingw32CCompiler):
765 768 self.compiler.compiler_so = self.compiler.compiler # no -mdll
766 769 self.compiler.dll_libraries = [] # no -lmsrvc90
767 770
768 771 pythonlib = None
769 772
770 773 dirname = os.path.dirname(self.get_ext_fullpath('dummy'))
771 774 self.hgtarget = os.path.join(dirname, 'hg')
772 775
773 776 if getattr(sys, 'dllhandle', None):
774 777 # Different Python installs can have different Python library
775 778 # names. e.g. the official CPython distribution uses pythonXY.dll
776 779 # and MinGW uses libpythonX.Y.dll.
777 780 _kernel32 = ctypes.windll.kernel32
778 781 _kernel32.GetModuleFileNameA.argtypes = [
779 782 ctypes.c_void_p,
780 783 ctypes.c_void_p,
781 784 ctypes.c_ulong,
782 785 ]
783 786 _kernel32.GetModuleFileNameA.restype = ctypes.c_ulong
784 787 size = 1000
785 788 buf = ctypes.create_string_buffer(size + 1)
786 789 filelen = _kernel32.GetModuleFileNameA(
787 790 sys.dllhandle, ctypes.byref(buf), size
788 791 )
789 792
790 793 if filelen > 0 and filelen != size:
791 794 dllbasename = os.path.basename(buf.value)
792 795 if not dllbasename.lower().endswith(b'.dll'):
793 796 raise SystemExit(
794 797 'Python DLL does not end with .dll: %s' % dllbasename
795 798 )
796 799 pythonlib = dllbasename[:-4]
797 800
798 801 # Copy the pythonXY.dll next to the binary so that it runs
799 802 # without tampering with PATH.
800 803 dest = os.path.join(
801 804 os.path.dirname(self.hgtarget),
802 805 os.fsdecode(dllbasename),
803 806 )
804 807
805 808 if not os.path.exists(dest):
806 809 shutil.copy(buf.value, dest)
807 810
808 811 # Also overwrite python3.dll so that hgext.git is usable.
809 812 # TODO: also handle the MSYS flavor
810 813 python_x = os.path.join(
811 814 os.path.dirname(os.fsdecode(buf.value)),
812 815 "python3.dll",
813 816 )
814 817
815 818 if os.path.exists(python_x):
816 819 dest = os.path.join(
817 820 os.path.dirname(self.hgtarget),
818 821 os.path.basename(python_x),
819 822 )
820 823
821 824 shutil.copy(python_x, dest)
822 825
823 826 if not pythonlib:
824 827 log.warn(
825 828 'could not determine Python DLL filename; assuming pythonXY'
826 829 )
827 830
828 831 hv = sys.hexversion
829 832 pythonlib = b'python%d%d' % (hv >> 24, (hv >> 16) & 0xFF)
830 833
831 834 log.info('using %s as Python library name' % pythonlib)
832 835 with open('mercurial/hgpythonlib.h', 'wb') as f:
833 836 f.write(b'/* this file is autogenerated by setup.py */\n')
834 837 f.write(b'#define HGPYTHONLIB "%s"\n' % pythonlib)
835 838
836 839 objects = self.compiler.compile(
837 840 ['mercurial/exewrapper.c'],
838 841 output_dir=self.build_temp,
839 842 macros=[('_UNICODE', None), ('UNICODE', None)],
840 843 )
841 844 self.compiler.link_executable(
842 845 objects, self.hgtarget, libraries=[], output_dir=self.build_temp
843 846 )
844 847
845 848 self.addlongpathsmanifest()
846 849
847 850 def addlongpathsmanifest(self):
848 851 """Add manifest pieces so that hg.exe understands long paths
849 852
850 853 Why resource #1 should be used for .exe manifests? I don't know and
851 854 wasn't able to find an explanation for mortals. But it seems to work.
852 855 """
853 856 exefname = self.compiler.executable_filename(self.hgtarget)
854 857 fdauto, manfname = tempfile.mkstemp(suffix='.hg.exe.manifest')
855 858 os.close(fdauto)
856 859 with open(manfname, 'w', encoding="UTF-8") as f:
857 860 f.write(self.LONG_PATHS_MANIFEST)
858 861 log.info("long paths manifest is written to '%s'" % manfname)
859 862 outputresource = '-outputresource:%s;#1' % exefname
860 863 log.info("running mt.exe to update hg.exe's manifest in-place")
861 864
862 865 self.spawn(
863 866 [
864 867 self.compiler.mt,
865 868 '-nologo',
866 869 '-manifest',
867 870 manfname,
868 871 outputresource,
869 872 ]
870 873 )
871 874 log.info("done updating hg.exe's manifest")
872 875 os.remove(manfname)
873 876
874 877 @property
875 878 def hgexepath(self):
876 879 dir = os.path.dirname(self.get_ext_fullpath('dummy'))
877 880 return os.path.join(self.build_temp, dir, 'hg.exe')
878 881
879 882
880 883 class hgbuilddoc(Command):
881 884 description = 'build documentation'
882 885 user_options = [
883 886 ('man', None, 'generate man pages'),
884 887 ('html', None, 'generate html pages'),
885 888 ]
886 889
887 890 def initialize_options(self):
888 891 self.man = None
889 892 self.html = None
890 893
891 894 def finalize_options(self):
892 895 # If --man or --html are set, only generate what we're told to.
893 896 # Otherwise generate everything.
894 897 have_subset = self.man is not None or self.html is not None
895 898
896 899 if have_subset:
897 900 self.man = True if self.man else False
898 901 self.html = True if self.html else False
899 902 else:
900 903 self.man = True
901 904 self.html = True
902 905
903 906 def run(self):
904 907 def normalizecrlf(p):
905 908 with open(p, 'rb') as fh:
906 909 orig = fh.read()
907 910
908 911 if b'\r\n' not in orig:
909 912 return
910 913
911 914 log.info('normalizing %s to LF line endings' % p)
912 915 with open(p, 'wb') as fh:
913 916 fh.write(orig.replace(b'\r\n', b'\n'))
914 917
915 918 def gentxt(root):
916 919 txt = 'doc/%s.txt' % root
917 920 log.info('generating %s' % txt)
918 921 res, out, err = runcmd(
919 922 [sys.executable, 'gendoc.py', root], os.environ, cwd='doc'
920 923 )
921 924 if res:
922 925 raise SystemExit(
923 926 'error running gendoc.py: %s'
924 927 % '\n'.join([sysstr(out), sysstr(err)])
925 928 )
926 929
927 930 with open(txt, 'wb') as fh:
928 931 fh.write(out)
929 932
930 933 def gengendoc(root):
931 934 gendoc = 'doc/%s.gendoc.txt' % root
932 935
933 936 log.info('generating %s' % gendoc)
934 937 res, out, err = runcmd(
935 938 [sys.executable, 'gendoc.py', '%s.gendoc' % root],
936 939 os.environ,
937 940 cwd='doc',
938 941 )
939 942 if res:
940 943 raise SystemExit(
941 944 'error running gendoc: %s'
942 945 % '\n'.join([sysstr(out), sysstr(err)])
943 946 )
944 947
945 948 with open(gendoc, 'wb') as fh:
946 949 fh.write(out)
947 950
948 951 def genman(root):
949 952 log.info('generating doc/%s' % root)
950 953 res, out, err = runcmd(
951 954 [
952 955 sys.executable,
953 956 'runrst',
954 957 'hgmanpage',
955 958 '--halt',
956 959 'warning',
957 960 '--strip-elements-with-class',
958 961 'htmlonly',
959 962 '%s.txt' % root,
960 963 root,
961 964 ],
962 965 os.environ,
963 966 cwd='doc',
964 967 )
965 968 if res:
966 969 raise SystemExit(
967 970 'error running runrst: %s'
968 971 % '\n'.join([sysstr(out), sysstr(err)])
969 972 )
970 973
971 974 normalizecrlf('doc/%s' % root)
972 975
973 976 def genhtml(root):
974 977 log.info('generating doc/%s.html' % root)
975 978 res, out, err = runcmd(
976 979 [
977 980 sys.executable,
978 981 'runrst',
979 982 'html',
980 983 '--halt',
981 984 'warning',
982 985 '--link-stylesheet',
983 986 '--stylesheet-path',
984 987 'style.css',
985 988 '%s.txt' % root,
986 989 '%s.html' % root,
987 990 ],
988 991 os.environ,
989 992 cwd='doc',
990 993 )
991 994 if res:
992 995 raise SystemExit(
993 996 'error running runrst: %s'
994 997 % '\n'.join([sysstr(out), sysstr(err)])
995 998 )
996 999
997 1000 normalizecrlf('doc/%s.html' % root)
998 1001
999 1002 # This logic is duplicated in doc/Makefile.
1000 1003 sources = {
1001 1004 f
1002 1005 for f in os.listdir('mercurial/helptext')
1003 1006 if re.search(r'[0-9]\.txt$', f)
1004 1007 }
1005 1008
1006 1009 # common.txt is a one-off.
1007 1010 gentxt('common')
1008 1011
1009 1012 for source in sorted(sources):
1010 1013 assert source[-4:] == '.txt'
1011 1014 root = source[:-4]
1012 1015
1013 1016 gentxt(root)
1014 1017 gengendoc(root)
1015 1018
1016 1019 if self.man:
1017 1020 genman(root)
1018 1021 if self.html:
1019 1022 genhtml(root)
1020 1023
1021 1024
1022 1025 class hginstall(install):
1023 1026
1024 1027 user_options = install.user_options + [
1025 1028 (
1026 1029 'old-and-unmanageable',
1027 1030 None,
1028 1031 'noop, present for eggless setuptools compat',
1029 1032 ),
1030 1033 (
1031 1034 'single-version-externally-managed',
1032 1035 None,
1033 1036 'noop, present for eggless setuptools compat',
1034 1037 ),
1035 1038 ]
1036 1039
1037 1040 sub_commands = install.sub_commands + [
1038 1041 ('install_completion', lambda self: True)
1039 1042 ]
1040 1043
1041 1044 # Also helps setuptools not be sad while we refuse to create eggs.
1042 1045 single_version_externally_managed = True
1043 1046
1044 1047 def get_sub_commands(self):
1045 1048 # Screen out egg related commands to prevent egg generation. But allow
1046 1049 # mercurial.egg-info generation, since that is part of modern
1047 1050 # packaging.
1048 1051 excl = {'bdist_egg'}
1049 1052 return filter(lambda x: x not in excl, install.get_sub_commands(self))
1050 1053
1051 1054
1052 1055 class hginstalllib(install_lib):
1053 1056 """
1054 1057 This is a specialization of install_lib that replaces the copy_file used
1055 1058 there so that it supports setting the mode of files after copying them,
1056 1059 instead of just preserving the mode that the files originally had. If your
1057 1060 system has a umask of something like 027, preserving the permissions when
1058 1061 copying will lead to a broken install.
1059 1062
1060 1063 Note that just passing keep_permissions=False to copy_file would be
1061 1064 insufficient, as it might still be applying a umask.
1062 1065 """
1063 1066
1064 1067 def run(self):
1065 1068 realcopyfile = file_util.copy_file
1066 1069
1067 1070 def copyfileandsetmode(*args, **kwargs):
1068 1071 src, dst = args[0], args[1]
1069 1072 dst, copied = realcopyfile(*args, **kwargs)
1070 1073 if copied:
1071 1074 st = os.stat(src)
1072 1075 # Persist executable bit (apply it to group and other if user
1073 1076 # has it)
1074 1077 if st[stat.ST_MODE] & stat.S_IXUSR:
1075 1078 setmode = int('0755', 8)
1076 1079 else:
1077 1080 setmode = int('0644', 8)
1078 1081 m = stat.S_IMODE(st[stat.ST_MODE])
1079 1082 m = (m & ~int('0777', 8)) | setmode
1080 1083 os.chmod(dst, m)
1081 1084
1082 1085 file_util.copy_file = copyfileandsetmode
1083 1086 try:
1084 1087 install_lib.run(self)
1085 1088 finally:
1086 1089 file_util.copy_file = realcopyfile
1087 1090
1088 1091
1089 1092 class hginstallscripts(install_scripts):
1090 1093 """
1091 1094 This is a specialization of install_scripts that replaces the @LIBDIR@ with
1092 1095 the configured directory for modules. If possible, the path is made relative
1093 1096 to the directory for scripts.
1094 1097 """
1095 1098
1096 1099 def initialize_options(self):
1097 1100 install_scripts.initialize_options(self)
1098 1101
1099 1102 self.install_lib = None
1100 1103
1101 1104 def finalize_options(self):
1102 1105 install_scripts.finalize_options(self)
1103 1106 self.set_undefined_options('install', ('install_lib', 'install_lib'))
1104 1107
1105 1108 def run(self):
1106 1109 install_scripts.run(self)
1107 1110
1108 1111 # It only makes sense to replace @LIBDIR@ with the install path if
1109 1112 # the install path is known. For wheels, the logic below calculates
1110 1113 # the libdir to be "../..". This is because the internal layout of a
1111 1114 # wheel archive looks like:
1112 1115 #
1113 1116 # mercurial-3.6.1.data/scripts/hg
1114 1117 # mercurial/__init__.py
1115 1118 #
1116 1119 # When installing wheels, the subdirectories of the "<pkg>.data"
1117 1120 # directory are translated to system local paths and files therein
1118 1121 # are copied in place. The mercurial/* files are installed into the
1119 1122 # site-packages directory. However, the site-packages directory
1120 1123 # isn't known until wheel install time. This means we have no clue
1121 1124 # at wheel generation time what the installed site-packages directory
1122 1125 # will be. And, wheels don't appear to provide the ability to register
1123 1126 # custom code to run during wheel installation. This all means that
1124 1127 # we can't reliably set the libdir in wheels: the default behavior
1125 1128 # of looking in sys.path must do.
1126 1129
1127 1130 if (
1128 1131 os.path.splitdrive(self.install_dir)[0]
1129 1132 != os.path.splitdrive(self.install_lib)[0]
1130 1133 ):
1131 1134 # can't make relative paths from one drive to another, so use an
1132 1135 # absolute path instead
1133 1136 libdir = self.install_lib
1134 1137 else:
1135 1138 libdir = os.path.relpath(self.install_lib, self.install_dir)
1136 1139
1137 1140 for outfile in self.outfiles:
1138 1141 with open(outfile, 'rb') as fp:
1139 1142 data = fp.read()
1140 1143
1141 1144 # skip binary files
1142 1145 if b'\0' in data:
1143 1146 continue
1144 1147
1145 1148 # During local installs, the shebang will be rewritten to the final
1146 1149 # install path. During wheel packaging, the shebang has a special
1147 1150 # value.
1148 1151 if data.startswith(b'#!python'):
1149 1152 log.info(
1150 1153 'not rewriting @LIBDIR@ in %s because install path '
1151 1154 'not known' % outfile
1152 1155 )
1153 1156 continue
1154 1157
1155 1158 data = data.replace(b'@LIBDIR@', libdir.encode('unicode_escape'))
1156 1159 with open(outfile, 'wb') as fp:
1157 1160 fp.write(data)
1158 1161
1159 1162
1160 1163 class hginstallcompletion(Command):
1161 1164 description = 'Install shell completion'
1162 1165
1163 1166 def initialize_options(self):
1164 1167 self.install_dir = None
1165 1168 self.outputs = []
1166 1169
1167 1170 def finalize_options(self):
1168 1171 self.set_undefined_options(
1169 1172 'install_data', ('install_dir', 'install_dir')
1170 1173 )
1171 1174
1172 1175 def get_outputs(self):
1173 1176 return self.outputs
1174 1177
1175 1178 def run(self):
1176 1179 for src, dir_path, dest in (
1177 1180 (
1178 1181 'bash_completion',
1179 1182 ('share', 'bash-completion', 'completions'),
1180 1183 'hg',
1181 1184 ),
1182 1185 ('zsh_completion', ('share', 'zsh', 'site-functions'), '_hg'),
1183 1186 ):
1184 1187 dir = os.path.join(self.install_dir, *dir_path)
1185 1188 self.mkpath(dir)
1186 1189
1187 1190 dest = os.path.join(dir, dest)
1188 1191 self.outputs.append(dest)
1189 1192 self.copy_file(os.path.join('contrib', src), dest)
1190 1193
1191 1194
1192 1195 # virtualenv installs custom distutils/__init__.py and
1193 1196 # distutils/distutils.cfg files which essentially proxy back to the
1194 1197 # "real" distutils in the main Python install. The presence of this
1195 1198 # directory causes py2exe to pick up the "hacked" distutils package
1196 1199 # from the virtualenv and "import distutils" will fail from the py2exe
1197 1200 # build because the "real" distutils files can't be located.
1198 1201 #
1199 1202 # We work around this by monkeypatching the py2exe code finding Python
1200 1203 # modules to replace the found virtualenv distutils modules with the
1201 1204 # original versions via filesystem scanning. This is a bit hacky. But
1202 1205 # it allows us to use virtualenvs for py2exe packaging, which is more
1203 1206 # deterministic and reproducible.
1204 1207 #
1205 1208 # It's worth noting that the common StackOverflow suggestions for this
1206 1209 # problem involve copying the original distutils files into the
1207 1210 # virtualenv or into the staging directory after setup() is invoked.
1208 1211 # The former is very brittle and can easily break setup(). Our hacking
1209 1212 # of the found modules routine has a similar result as copying the files
1210 1213 # manually. But it makes fewer assumptions about how py2exe works and
1211 1214 # is less brittle.
1212 1215
1213 1216 # This only catches virtualenvs made with virtualenv (as opposed to
1214 1217 # venv, which is likely what Python 3 uses).
1215 1218 py2exehacked = py2exeloaded and getattr(sys, 'real_prefix', None) is not None
1216 1219
1217 1220 if py2exehacked:
1218 1221 from distutils.command.py2exe import py2exe as buildpy2exe
1219 1222 from py2exe.mf import Module as py2exemodule
1220 1223
1221 1224 class hgbuildpy2exe(buildpy2exe):
1222 1225 def find_needed_modules(self, mf, files, modules):
1223 1226 res = buildpy2exe.find_needed_modules(self, mf, files, modules)
1224 1227
1225 1228 # Replace virtualenv's distutils modules with the real ones.
1226 1229 modules = {}
1227 1230 for k, v in res.modules.items():
1228 1231 if k != 'distutils' and not k.startswith('distutils.'):
1229 1232 modules[k] = v
1230 1233
1231 1234 res.modules = modules
1232 1235
1233 1236 import opcode
1234 1237
1235 1238 distutilsreal = os.path.join(
1236 1239 os.path.dirname(opcode.__file__), 'distutils'
1237 1240 )
1238 1241
1239 1242 for root, dirs, files in os.walk(distutilsreal):
1240 1243 for f in sorted(files):
1241 1244 if not f.endswith('.py'):
1242 1245 continue
1243 1246
1244 1247 full = os.path.join(root, f)
1245 1248
1246 1249 parents = ['distutils']
1247 1250
1248 1251 if root != distutilsreal:
1249 1252 rel = os.path.relpath(root, distutilsreal)
1250 1253 parents.extend(p for p in rel.split(os.sep))
1251 1254
1252 1255 modname = '%s.%s' % ('.'.join(parents), f[:-3])
1253 1256
1254 1257 if modname.startswith('distutils.tests.'):
1255 1258 continue
1256 1259
1257 1260 if modname.endswith('.__init__'):
1258 1261 modname = modname[: -len('.__init__')]
1259 1262 path = os.path.dirname(full)
1260 1263 else:
1261 1264 path = None
1262 1265
1263 1266 res.modules[modname] = py2exemodule(
1264 1267 modname, full, path=path
1265 1268 )
1266 1269
1267 1270 if 'distutils' not in res.modules:
1268 1271 raise SystemExit('could not find distutils modules')
1269 1272
1270 1273 return res
1271 1274
1272 1275
1273 1276 cmdclass = {
1274 1277 'build': hgbuild,
1275 1278 'build_doc': hgbuilddoc,
1276 1279 'build_mo': hgbuildmo,
1277 1280 'build_ext': hgbuildext,
1278 1281 'build_py': hgbuildpy,
1279 1282 'build_scripts': hgbuildscripts,
1280 1283 'build_hgextindex': buildhgextindex,
1281 1284 'install': hginstall,
1282 1285 'install_completion': hginstallcompletion,
1283 1286 'install_lib': hginstalllib,
1284 1287 'install_scripts': hginstallscripts,
1285 1288 'build_hgexe': buildhgexe,
1286 1289 }
1287 1290
1288 1291 if py2exehacked:
1289 1292 cmdclass['py2exe'] = hgbuildpy2exe
1290 1293
1291 1294 packages = [
1292 1295 'mercurial',
1293 1296 'mercurial.cext',
1294 1297 'mercurial.cffi',
1295 1298 'mercurial.defaultrc',
1296 1299 'mercurial.dirstateutils',
1297 1300 'mercurial.helptext',
1298 1301 'mercurial.helptext.internals',
1299 1302 'mercurial.hgweb',
1300 1303 'mercurial.interfaces',
1301 1304 'mercurial.pure',
1302 1305 'mercurial.stabletailgraph',
1303 1306 'mercurial.templates',
1304 1307 'mercurial.thirdparty',
1305 1308 'mercurial.thirdparty.attr',
1306 1309 'mercurial.thirdparty.zope',
1307 1310 'mercurial.thirdparty.zope.interface',
1308 1311 'mercurial.upgrade_utils',
1309 1312 'mercurial.utils',
1310 1313 'mercurial.revlogutils',
1311 1314 'mercurial.testing',
1312 1315 'hgext',
1313 1316 'hgext.convert',
1314 1317 'hgext.fsmonitor',
1315 1318 'hgext.fastannotate',
1316 1319 'hgext.fsmonitor.pywatchman',
1317 1320 'hgext.git',
1318 1321 'hgext.highlight',
1319 1322 'hgext.hooklib',
1320 1323 'hgext.infinitepush',
1321 1324 'hgext.largefiles',
1322 1325 'hgext.lfs',
1323 1326 'hgext.narrow',
1324 1327 'hgext.remotefilelog',
1325 1328 'hgext.zeroconf',
1326 1329 'hgext3rd',
1327 1330 'hgdemandimport',
1328 1331 ]
1329 1332
1330 1333 for name in os.listdir(os.path.join('mercurial', 'templates')):
1331 1334 if name != '__pycache__' and os.path.isdir(
1332 1335 os.path.join('mercurial', 'templates', name)
1333 1336 ):
1334 1337 packages.append('mercurial.templates.%s' % name)
1335 1338
1336 1339 if 'HG_PY2EXE_EXTRA_INSTALL_PACKAGES' in os.environ:
1337 1340 # py2exe can't cope with namespace packages very well, so we have to
1338 1341 # install any hgext3rd.* extensions that we want in the final py2exe
1339 1342 # image here. This is gross, but you gotta do what you gotta do.
1340 1343 packages.extend(os.environ['HG_PY2EXE_EXTRA_INSTALL_PACKAGES'].split(' '))
1341 1344
1342 1345 common_depends = [
1343 1346 'mercurial/bitmanipulation.h',
1344 1347 'mercurial/compat.h',
1345 1348 'mercurial/cext/util.h',
1346 1349 ]
1347 1350 common_include_dirs = ['mercurial']
1348 1351
1349 1352 common_cflags = []
1350 1353
1351 1354 # MSVC 2008 still needs declarations at the top of the scope, but Python 3.9
1352 1355 # makes declarations not at the top of a scope in the headers.
1353 1356 if os.name != 'nt' and sys.version_info[1] < 9:
1354 1357 common_cflags = ['-Werror=declaration-after-statement']
1355 1358
1356 1359 osutil_cflags = []
1357 1360 osutil_ldflags = []
1358 1361
1359 1362 # platform specific macros
1360 1363 for plat, func in [('bsd', 'setproctitle')]:
1361 1364 if re.search(plat, sys.platform) and hasfunction(new_compiler(), func):
1362 1365 osutil_cflags.append('-DHAVE_%s' % func.upper())
1363 1366
1364 1367 for plat, macro, code in [
1365 1368 (
1366 1369 'bsd|darwin',
1367 1370 'BSD_STATFS',
1368 1371 '''
1369 1372 #include <sys/param.h>
1370 1373 #include <sys/mount.h>
1371 1374 int main() { struct statfs s; return sizeof(s.f_fstypename); }
1372 1375 ''',
1373 1376 ),
1374 1377 (
1375 1378 'linux',
1376 1379 'LINUX_STATFS',
1377 1380 '''
1378 1381 #include <linux/magic.h>
1379 1382 #include <sys/vfs.h>
1380 1383 int main() { struct statfs s; return sizeof(s.f_type); }
1381 1384 ''',
1382 1385 ),
1383 1386 ]:
1384 1387 if re.search(plat, sys.platform) and cancompile(new_compiler(), code):
1385 1388 osutil_cflags.append('-DHAVE_%s' % macro)
1386 1389
1387 1390 if sys.platform == 'darwin':
1388 1391 osutil_ldflags += ['-framework', 'ApplicationServices']
1389 1392
1390 1393 if sys.platform == 'sunos5':
1391 1394 osutil_ldflags += ['-lsocket']
1392 1395
1393 1396 xdiff_srcs = [
1394 1397 'mercurial/thirdparty/xdiff/xdiffi.c',
1395 1398 'mercurial/thirdparty/xdiff/xprepare.c',
1396 1399 'mercurial/thirdparty/xdiff/xutils.c',
1397 1400 ]
1398 1401
1399 1402 xdiff_headers = [
1400 1403 'mercurial/thirdparty/xdiff/xdiff.h',
1401 1404 'mercurial/thirdparty/xdiff/xdiffi.h',
1402 1405 'mercurial/thirdparty/xdiff/xinclude.h',
1403 1406 'mercurial/thirdparty/xdiff/xmacros.h',
1404 1407 'mercurial/thirdparty/xdiff/xprepare.h',
1405 1408 'mercurial/thirdparty/xdiff/xtypes.h',
1406 1409 'mercurial/thirdparty/xdiff/xutils.h',
1407 1410 ]
1408 1411
1409 1412
1410 1413 class RustCompilationError(CCompilerError):
1411 1414 """Exception class for Rust compilation errors."""
1412 1415
1413 1416
1414 1417 class RustExtension(Extension):
1415 1418 """Base classes for concrete Rust Extension classes."""
1416 1419
1417 1420 rusttargetdir = os.path.join('rust', 'target', 'release')
1418 1421
1419 1422 def __init__(self, mpath, sources, rustlibname, subcrate, **kw):
1420 1423 Extension.__init__(self, mpath, sources, **kw)
1421 1424 srcdir = self.rustsrcdir = os.path.join('rust', subcrate)
1422 1425
1423 1426 # adding Rust source and control files to depends so that the extension
1424 1427 # gets rebuilt if they've changed
1425 1428 self.depends.append(os.path.join(srcdir, 'Cargo.toml'))
1426 1429 cargo_lock = os.path.join(srcdir, 'Cargo.lock')
1427 1430 if os.path.exists(cargo_lock):
1428 1431 self.depends.append(cargo_lock)
1429 1432 for dirpath, subdir, fnames in os.walk(os.path.join(srcdir, 'src')):
1430 1433 self.depends.extend(
1431 1434 os.path.join(dirpath, fname)
1432 1435 for fname in fnames
1433 1436 if os.path.splitext(fname)[1] == '.rs'
1434 1437 )
1435 1438
1436 1439 @staticmethod
1437 1440 def rustdylibsuffix():
1438 1441 """Return the suffix for shared libraries produced by rustc.
1439 1442
1440 1443 See also: https://doc.rust-lang.org/reference/linkage.html
1441 1444 """
1442 1445 if sys.platform == 'darwin':
1443 1446 return '.dylib'
1444 1447 elif os.name == 'nt':
1445 1448 return '.dll'
1446 1449 else:
1447 1450 return '.so'
1448 1451
1449 1452 def rustbuild(self):
1450 1453 env = os.environ.copy()
1451 1454 if 'HGTEST_RESTOREENV' in env:
1452 1455 # Mercurial tests change HOME to a temporary directory,
1453 1456 # but, if installed with rustup, the Rust toolchain needs
1454 1457 # HOME to be correct (otherwise the 'no default toolchain'
1455 1458 # error message is issued and the build fails).
1456 1459 # This happens currently with test-hghave.t, which does
1457 1460 # invoke this build.
1458 1461
1459 1462 # Unix only fix (os.path.expanduser not really reliable if
1460 1463 # HOME is shadowed like this)
1461 1464 import pwd
1462 1465
1463 1466 env['HOME'] = pwd.getpwuid(os.getuid()).pw_dir
1464 1467
1465 1468 cargocmd = ['cargo', 'rustc', '--release']
1466 1469
1467 1470 rust_features = env.get("HG_RUST_FEATURES")
1468 1471 if rust_features:
1469 1472 cargocmd.extend(('--features', rust_features))
1470 1473
1471 1474 cargocmd.append('--')
1472 1475 if sys.platform == 'darwin':
1473 1476 cargocmd.extend(
1474 1477 ("-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup")
1475 1478 )
1476 1479 try:
1477 1480 subprocess.check_call(cargocmd, env=env, cwd=self.rustsrcdir)
1478 1481 except FileNotFoundError:
1479 1482 raise RustCompilationError("Cargo not found")
1480 1483 except PermissionError:
1481 1484 raise RustCompilationError(
1482 1485 "Cargo found, but permission to execute it is denied"
1483 1486 )
1484 1487 except subprocess.CalledProcessError:
1485 1488 raise RustCompilationError(
1486 1489 "Cargo failed. Working directory: %r, "
1487 1490 "command: %r, environment: %r"
1488 1491 % (self.rustsrcdir, cargocmd, env)
1489 1492 )
1490 1493
1491 1494
1492 1495 class RustStandaloneExtension(RustExtension):
1493 1496 def __init__(self, pydottedname, rustcrate, dylibname, **kw):
1494 1497 RustExtension.__init__(
1495 1498 self, pydottedname, [], dylibname, rustcrate, **kw
1496 1499 )
1497 1500 self.dylibname = dylibname
1498 1501
1499 1502 def build(self, target_dir):
1500 1503 self.rustbuild()
1501 1504 target = [target_dir]
1502 1505 target.extend(self.name.split('.'))
1503 1506 target[-1] += DYLIB_SUFFIX
1504 1507 target = os.path.join(*target)
1505 1508 os.makedirs(os.path.dirname(target), exist_ok=True)
1506 1509 shutil.copy2(
1507 1510 os.path.join(
1508 1511 self.rusttargetdir, self.dylibname + self.rustdylibsuffix()
1509 1512 ),
1510 1513 target,
1511 1514 )
1512 1515
1513 1516
1514 1517 extmodules = [
1515 1518 Extension(
1516 1519 'mercurial.cext.base85',
1517 1520 ['mercurial/cext/base85.c'],
1518 1521 include_dirs=common_include_dirs,
1519 1522 extra_compile_args=common_cflags,
1520 1523 depends=common_depends,
1521 1524 ),
1522 1525 Extension(
1523 1526 'mercurial.cext.bdiff',
1524 1527 ['mercurial/bdiff.c', 'mercurial/cext/bdiff.c'] + xdiff_srcs,
1525 1528 include_dirs=common_include_dirs,
1526 1529 extra_compile_args=common_cflags,
1527 1530 depends=common_depends + ['mercurial/bdiff.h'] + xdiff_headers,
1528 1531 ),
1529 1532 Extension(
1530 1533 'mercurial.cext.mpatch',
1531 1534 ['mercurial/mpatch.c', 'mercurial/cext/mpatch.c'],
1532 1535 include_dirs=common_include_dirs,
1533 1536 extra_compile_args=common_cflags,
1534 1537 depends=common_depends,
1535 1538 ),
1536 1539 Extension(
1537 1540 'mercurial.cext.parsers',
1538 1541 [
1539 1542 'mercurial/cext/charencode.c',
1540 1543 'mercurial/cext/dirs.c',
1541 1544 'mercurial/cext/manifest.c',
1542 1545 'mercurial/cext/parsers.c',
1543 1546 'mercurial/cext/pathencode.c',
1544 1547 'mercurial/cext/revlog.c',
1545 1548 ],
1546 1549 include_dirs=common_include_dirs,
1547 1550 extra_compile_args=common_cflags,
1548 1551 depends=common_depends
1549 1552 + [
1550 1553 'mercurial/cext/charencode.h',
1551 1554 'mercurial/cext/revlog.h',
1552 1555 ],
1553 1556 ),
1554 1557 Extension(
1555 1558 'mercurial.cext.osutil',
1556 1559 ['mercurial/cext/osutil.c'],
1557 1560 include_dirs=common_include_dirs,
1558 1561 extra_compile_args=common_cflags + osutil_cflags,
1559 1562 extra_link_args=osutil_ldflags,
1560 1563 depends=common_depends,
1561 1564 ),
1562 1565 Extension(
1563 1566 'mercurial.thirdparty.zope.interface._zope_interface_coptimizations',
1564 1567 [
1565 1568 'mercurial/thirdparty/zope/interface/_zope_interface_coptimizations.c',
1566 1569 ],
1567 1570 extra_compile_args=common_cflags,
1568 1571 ),
1569 1572 Extension(
1570 1573 'mercurial.thirdparty.sha1dc',
1571 1574 [
1572 1575 'mercurial/thirdparty/sha1dc/cext.c',
1573 1576 'mercurial/thirdparty/sha1dc/lib/sha1.c',
1574 1577 'mercurial/thirdparty/sha1dc/lib/ubc_check.c',
1575 1578 ],
1576 1579 extra_compile_args=common_cflags,
1577 1580 ),
1578 1581 Extension(
1579 1582 'hgext.fsmonitor.pywatchman.bser',
1580 1583 ['hgext/fsmonitor/pywatchman/bser.c'],
1581 1584 extra_compile_args=common_cflags,
1582 1585 ),
1583 1586 RustStandaloneExtension(
1584 1587 'mercurial.rustext',
1585 1588 'hg-cpython',
1586 1589 'librusthg',
1587 1590 ),
1588 1591 ]
1589 1592
1590 1593
1591 1594 sys.path.insert(0, 'contrib/python-zstandard')
1592 1595 import setup_zstd
1593 1596
1594 1597 zstd = setup_zstd.get_c_extension(
1595 1598 name='mercurial.zstd', root=os.path.abspath(os.path.dirname(__file__))
1596 1599 )
1597 1600 zstd.extra_compile_args += common_cflags
1598 1601 extmodules.append(zstd)
1599 1602
1600 1603 try:
1601 1604 from distutils import cygwinccompiler
1602 1605
1603 1606 # the -mno-cygwin option has been deprecated for years
1604 1607 mingw32compilerclass = cygwinccompiler.Mingw32CCompiler
1605 1608
1606 1609 class HackedMingw32CCompiler(cygwinccompiler.Mingw32CCompiler):
1607 1610 def __init__(self, *args, **kwargs):
1608 1611 mingw32compilerclass.__init__(self, *args, **kwargs)
1609 1612 for i in 'compiler compiler_so linker_exe linker_so'.split():
1610 1613 try:
1611 1614 getattr(self, i).remove('-mno-cygwin')
1612 1615 except ValueError:
1613 1616 pass
1614 1617
1615 1618 cygwinccompiler.Mingw32CCompiler = HackedMingw32CCompiler
1616 1619 except ImportError:
1617 1620 # the cygwinccompiler package is not available on some Python
1618 1621 # distributions like the ones from the optware project for Synology
1619 1622 # DiskStation boxes
1620 1623 class HackedMingw32CCompiler:
1621 1624 pass
1622 1625
1623 1626
1624 1627 if os.name == 'nt':
1625 1628 # Allow compiler/linker flags to be added to Visual Studio builds. Passing
1626 1629 # extra_link_args to distutils.extensions.Extension() doesn't have any
1627 1630 # effect.
1628 1631 from distutils import msvccompiler
1629 1632
1630 1633 msvccompilerclass = msvccompiler.MSVCCompiler
1631 1634
1632 1635 class HackedMSVCCompiler(msvccompiler.MSVCCompiler):
1633 1636 def initialize(self):
1634 1637 msvccompilerclass.initialize(self)
1635 1638 # "warning LNK4197: export 'func' specified multiple times"
1636 1639 self.ldflags_shared.append('/ignore:4197')
1637 1640 self.ldflags_shared_debug.append('/ignore:4197')
1638 1641
1639 1642 msvccompiler.MSVCCompiler = HackedMSVCCompiler
1640 1643
1641 1644 packagedata = {
1642 1645 'mercurial': [
1643 1646 'locale/*/LC_MESSAGES/hg.mo',
1644 1647 'dummycert.pem',
1645 1648 ],
1646 1649 'mercurial.defaultrc': [
1647 1650 '*.rc',
1648 1651 ],
1649 1652 'mercurial.helptext': [
1650 1653 '*.txt',
1651 1654 ],
1652 1655 'mercurial.helptext.internals': [
1653 1656 '*.txt',
1654 1657 ],
1655 1658 'mercurial.thirdparty.attr': [
1656 1659 '*.pyi',
1657 1660 'py.typed',
1658 1661 ],
1659 1662 }
1660 1663
1661 1664
1662 1665 def ordinarypath(p):
1663 1666 return p and p[0] != '.' and p[-1] != '~'
1664 1667
1665 1668
1666 1669 for root in ('templates',):
1667 1670 for curdir, dirs, files in os.walk(os.path.join('mercurial', root)):
1668 1671 packagename = curdir.replace(os.sep, '.')
1669 1672 packagedata[packagename] = list(filter(ordinarypath, files))
1670 1673
1671 1674 datafiles = []
1672 1675
1673 1676 # distutils expects version to be str/unicode. Converting it to
1674 1677 # unicode on Python 2 still works because it won't contain any
1675 1678 # non-ascii bytes and will be implicitly converted back to bytes
1676 1679 # when operated on.
1677 1680 assert isinstance(version, str)
1678 1681 setupversion = version
1679 1682
1680 1683 extra = {}
1681 1684
1682 1685 py2exepackages = [
1683 1686 'hgdemandimport',
1684 1687 'hgext3rd',
1685 1688 'hgext',
1686 1689 'email',
1687 1690 # implicitly imported per module policy
1688 1691 # (cffi wouldn't be used as a frozen exe)
1689 1692 'mercurial.cext',
1690 1693 #'mercurial.cffi',
1691 1694 'mercurial.pure',
1692 1695 ]
1693 1696
1694 1697 py2exe_includes = []
1695 1698
1696 1699 py2exeexcludes = []
1697 1700 py2exedllexcludes = ['crypt32.dll']
1698 1701
1699 1702 if issetuptools:
1700 1703 extra['python_requires'] = supportedpy
1701 1704
1702 1705 if py2exeloaded:
1703 1706 extra['console'] = [
1704 1707 {
1705 1708 'script': 'hg',
1706 1709 'copyright': 'Copyright (C) 2005-2023 Olivia Mackall and others',
1707 1710 'product_version': version,
1708 1711 }
1709 1712 ]
1710 1713 # Sub command of 'build' because 'py2exe' does not handle sub_commands.
1711 1714 # Need to override hgbuild because it has a private copy of
1712 1715 # build.sub_commands.
1713 1716 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1714 1717 # put dlls in sub directory so that they won't pollute PATH
1715 1718 extra['zipfile'] = 'lib/library.zip'
1716 1719
1717 1720 # We allow some configuration to be supplemented via environment
1718 1721 # variables. This is better than setup.cfg files because it allows
1719 1722 # supplementing configs instead of replacing them.
1720 1723 extrapackages = os.environ.get('HG_PY2EXE_EXTRA_PACKAGES')
1721 1724 if extrapackages:
1722 1725 py2exepackages.extend(extrapackages.split(' '))
1723 1726
1724 1727 extra_includes = os.environ.get('HG_PY2EXE_EXTRA_INCLUDES')
1725 1728 if extra_includes:
1726 1729 py2exe_includes.extend(extra_includes.split(' '))
1727 1730
1728 1731 excludes = os.environ.get('HG_PY2EXE_EXTRA_EXCLUDES')
1729 1732 if excludes:
1730 1733 py2exeexcludes.extend(excludes.split(' '))
1731 1734
1732 1735 dllexcludes = os.environ.get('HG_PY2EXE_EXTRA_DLL_EXCLUDES')
1733 1736 if dllexcludes:
1734 1737 py2exedllexcludes.extend(dllexcludes.split(' '))
1735 1738
1736 1739 if os.environ.get('PYOXIDIZER'):
1737 1740 hgbuild.sub_commands.insert(0, ('build_hgextindex', None))
1738 1741
1739 1742 if os.name == 'nt':
1740 1743 # Windows binary file versions for exe/dll files must have the
1741 1744 # form W.X.Y.Z, where W,X,Y,Z are numbers in the range 0..65535
1742 1745 setupversion = setupversion.split(r'+', 1)[0]
1743 1746
1744 1747 setup(
1745 1748 name='mercurial',
1746 1749 version=setupversion,
1747 1750 author='Olivia Mackall and many others',
1748 1751 author_email='mercurial@mercurial-scm.org',
1749 1752 url='https://mercurial-scm.org/',
1750 1753 download_url='https://mercurial-scm.org/release/',
1751 1754 description=(
1752 1755 'Fast scalable distributed SCM (revision control, version '
1753 1756 'control) system'
1754 1757 ),
1755 1758 long_description=(
1756 1759 'Mercurial is a distributed SCM tool written in Python.'
1757 1760 ' It is used by a number of large projects that require'
1758 1761 ' fast, reliable distributed revision control, such as '
1759 1762 'Mozilla.'
1760 1763 ),
1761 1764 license='GNU GPLv2 or any later version',
1762 1765 classifiers=[
1763 1766 'Development Status :: 6 - Mature',
1764 1767 'Environment :: Console',
1765 1768 'Intended Audience :: Developers',
1766 1769 'Intended Audience :: System Administrators',
1767 1770 'License :: OSI Approved :: GNU General Public License (GPL)',
1768 1771 'Natural Language :: Danish',
1769 1772 'Natural Language :: English',
1770 1773 'Natural Language :: German',
1771 1774 'Natural Language :: Italian',
1772 1775 'Natural Language :: Japanese',
1773 1776 'Natural Language :: Portuguese (Brazilian)',
1774 1777 'Operating System :: Microsoft :: Windows',
1775 1778 'Operating System :: OS Independent',
1776 1779 'Operating System :: POSIX',
1777 1780 'Programming Language :: C',
1778 1781 'Programming Language :: Python',
1779 1782 'Topic :: Software Development :: Version Control',
1780 1783 ],
1781 1784 scripts=scripts,
1782 1785 packages=packages,
1783 1786 ext_modules=extmodules,
1784 1787 data_files=datafiles,
1785 1788 package_data=packagedata,
1786 1789 cmdclass=cmdclass,
1787 1790 distclass=hgdist,
1788 1791 options={
1789 1792 'py2exe': {
1790 1793 'bundle_files': 3,
1791 1794 'dll_excludes': py2exedllexcludes,
1792 1795 'includes': py2exe_includes,
1793 1796 'excludes': py2exeexcludes,
1794 1797 'packages': py2exepackages,
1795 1798 },
1796 1799 'bdist_mpkg': {
1797 1800 'zipdist': False,
1798 1801 'license': 'COPYING',
1799 1802 'readme': 'contrib/packaging/macosx/Readme.html',
1800 1803 'welcome': 'contrib/packaging/macosx/Welcome.html',
1801 1804 },
1802 1805 },
1803 1806 **extra
1804 1807 )
@@ -1,120 +1,154 b''
1 1 #!/usr/bin/env python
2 2
3 3 """dummy SMTP server for use in tests"""
4 4
5 5
6 import asyncore
7 6 import optparse
8 import smtpd
7 import os
8 import socket
9 9 import ssl
10 10 import sys
11 import traceback
12 11
13 12 from mercurial import (
14 13 pycompat,
15 14 server,
16 15 sslutil,
17 16 ui as uimod,
18 17 )
19 18
20 19
20 if os.environ.get('HGIPV6', '0') == '1':
21 family = socket.AF_INET6
22 else:
23 family = socket.AF_INET
24
25
21 26 def log(msg):
22 27 sys.stdout.write(msg)
23 28 sys.stdout.flush()
24 29
25 30
26 class dummysmtpserver(smtpd.SMTPServer):
27 def __init__(self, localaddr):
28 smtpd.SMTPServer.__init__(self, localaddr, remoteaddr=None)
31 def mocksmtpserversession(conn, addr):
32 conn.send(b'220 smtp.example.com ESMTP\r\n')
33
34 line = conn.recv(1024)
35 if not line.lower().startswith(b'ehlo '):
36 log('no hello: %s\n' % line)
37 return
38
39 conn.send(b'250 Hello\r\n')
29 40
30 def process_message(self, peer, mailfrom, rcpttos, data, **kwargs):
31 log('%s from=%s to=%s\n' % (peer[0], mailfrom, ', '.join(rcpttos)))
41 line = conn.recv(1024)
42 if not line.lower().startswith(b'mail from:'):
43 log('no mail from: %s\n' % line)
44 return
45 mailfrom = line[10:].decode().rstrip()
46 if mailfrom.startswith('<') and mailfrom.endswith('>'):
47 mailfrom = mailfrom[1:-1]
48
49 conn.send(b'250 Ok\r\n')
32 50
33 def handle_error(self):
34 # On Windows, a bad SSL connection sometimes generates a WSAECONNRESET.
35 # The default handler will shutdown this server, and then both the
36 # current connection and subsequent ones fail on the client side with
37 # "No connection could be made because the target machine actively
38 # refused it". If we eat the error, then the client properly aborts in
39 # the expected way, and the server is available for subsequent requests.
40 traceback.print_exc()
51 rcpttos = []
52 while True:
53 line = conn.recv(1024)
54 if not line.lower().startswith(b'rcpt to:'):
55 break
56 rcptto = line[8:].decode().rstrip()
57 if rcptto.startswith('<') and rcptto.endswith('>'):
58 rcptto = rcptto[1:-1]
59 rcpttos.append(rcptto)
60
61 conn.send(b'250 Ok\r\n')
62
63 if not line.lower().strip() == b'data':
64 log('no rcpt to or data: %s' % line)
65
66 conn.send(b'354 Go ahead\r\n')
67
68 data = b''
69 while True:
70 line = conn.recv(1024)
71 if not line:
72 log('connection closed before end of data')
73 break
74 data += line
75 if data.endswith(b'\r\n.\r\n'):
76 data = data[:-5]
77 break
78
79 conn.send(b'250 Ok\r\n')
80
81 log(
82 '%s from=%s to=%s\n%s\n'
83 % (addr[0], mailfrom, ', '.join(rcpttos), data.decode())
84 )
41 85
42 86
43 class dummysmtpsecureserver(dummysmtpserver):
44 def __init__(self, localaddr, certfile):
45 dummysmtpserver.__init__(self, localaddr)
46 self._certfile = certfile
47
48 def handle_accept(self):
49 pair = self.accept()
50 if not pair:
51 return
52 conn, addr = pair
87 def run(host, port, certificate):
53 88 ui = uimod.ui.load()
89 with socket.socket(family, socket.SOCK_STREAM) as s:
90 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
91 s.bind((host, port))
92 # log('listening at %s:%d\n' % (host, port))
93 s.listen(1)
54 94 try:
55 # wrap_socket() would block, but we don't care
56 conn = sslutil.wrapserversocket(conn, ui, certfile=self._certfile)
57 except ssl.SSLError:
58 log('%s ssl error\n' % addr[0])
95 while True:
96 conn, addr = s.accept()
97 if certificate:
98 try:
99 conn = sslutil.wrapserversocket(
100 conn, ui, certfile=certificate
101 )
102 except ssl.SSLError as e:
103 log('%s ssl error: %s\n' % (addr[0], e))
59 104 conn.close()
60 return
61 smtpd.SMTPChannel(self, conn, addr)
62
63
64 def run():
65 try:
66 asyncore.loop()
105 continue
106 log("connection from %s:%s\n" % addr)
107 mocksmtpserversession(conn, addr)
108 conn.close()
67 109 except KeyboardInterrupt:
68 110 pass
69 111
70 112
71 113 def _encodestrsonly(v):
72 114 if isinstance(v, type(u'')):
73 115 return v.encode('ascii')
74 116 return v
75 117
76 118
77 119 def bytesvars(obj):
78 120 unidict = vars(obj)
79 121 bd = {k.encode('ascii'): _encodestrsonly(v) for k, v in unidict.items()}
80 122 if bd[b'daemon_postexec'] is not None:
81 123 bd[b'daemon_postexec'] = [
82 124 _encodestrsonly(v) for v in bd[b'daemon_postexec']
83 125 ]
84 126 return bd
85 127
86 128
87 129 def main():
88 130 op = optparse.OptionParser()
89 131 op.add_option('-d', '--daemon', action='store_true')
90 132 op.add_option('--daemon-postexec', action='append')
91 133 op.add_option('-p', '--port', type=int, default=8025)
92 134 op.add_option('-a', '--address', default='localhost')
93 135 op.add_option('--pid-file', metavar='FILE')
94 136 op.add_option('--tls', choices=['none', 'smtps'], default='none')
95 137 op.add_option('--certificate', metavar='FILE')
138 op.add_option('--logfile', metavar='FILE')
96 139
97 140 opts, args = op.parse_args()
98 if opts.tls == 'smtps' and not opts.certificate:
99 op.error('--certificate must be specified')
100
101 addr = (opts.address, opts.port)
102
103 def init():
104 if opts.tls == 'none':
105 dummysmtpserver(addr)
106 else:
107 dummysmtpsecureserver(addr, opts.certificate)
108 log('listening at %s:%d\n' % addr)
141 if (opts.tls == 'smtps') != bool(opts.certificate):
142 op.error('--certificate must be specified with --tls=smtps')
109 143
110 144 server.runservice(
111 145 bytesvars(opts),
112 initfn=init,
113 runfn=run,
146 runfn=lambda: run(opts.address, opts.port, opts.certificate),
114 147 runargs=[pycompat.sysexecutable, pycompat.fsencode(__file__)]
115 148 + pycompat.sysargv[1:],
149 logfile=opts.logfile,
116 150 )
117 151
118 152
119 153 if __name__ == '__main__':
120 154 main()
@@ -1,73 +1,73 b''
1 1 CACHEDIR=$PWD/hgcache
2 2 cat >> $HGRCPATH <<EOF
3 3 [remotefilelog]
4 4 cachepath=$CACHEDIR
5 5 debug=True
6 6 [extensions]
7 7 remotefilelog=
8 8 rebase=
9 9 strip=
10 10 [server]
11 11 preferuncompressed=True
12 12 [experimental]
13 13 changegroup3=True
14 14 [rebase]
15 15 singletransaction=True
16 16 EOF
17 17
18 18 hgcloneshallow() {
19 19 local name
20 20 local dest
21 21 orig=$1
22 22 shift
23 23 dest=$1
24 24 shift
25 25 hg clone --shallow --config remotefilelog.reponame=master $orig $dest $@
26 26 cat >> $dest/.hg/hgrc <<EOF
27 27 [remotefilelog]
28 28 reponame=master
29 29 [phases]
30 30 publish=False
31 31 EOF
32 32 }
33 33
34 34 hgcloneshallowlfs() {
35 35 local name
36 36 local dest
37 37 local lfsdir
38 38 orig=$1
39 39 shift
40 40 dest=$1
41 41 shift
42 42 lfsdir=$1
43 43 shift
44 44 hg clone --shallow --config "extensions.lfs=" --config "lfs.url=$lfsdir" --config remotefilelog.reponame=master $orig $dest $@
45 45 cat >> $dest/.hg/hgrc <<EOF
46 46 [extensions]
47 47 lfs=
48 48 [lfs]
49 49 url=$lfsdir
50 50 [remotefilelog]
51 51 reponame=master
52 52 [phases]
53 53 publish=False
54 54 EOF
55 55 }
56 56
57 57 clearcache() {
58 58 rm -rf $CACHEDIR/*
59 59 }
60 60
61 61 mkcommit() {
62 62 echo "$1" > "$1"
63 63 hg add "$1"
64 64 hg ci -m "$1"
65 65 }
66 66
67 67 ls_l() {
68 68 "$PYTHON" $TESTDIR/ls-l.py "$@"
69 69 }
70 70
71 71 identifyrflcaps() {
72 xargs -n 1 echo | egrep '(remotefilelog|getflogheads|getfile)' | sort
72 xargs -n 1 echo | grep -E '(remotefilelog|getflogheads|getfile)' | sort
73 73 }
@@ -1,628 +1,628 b''
1 1 #require serve
2 2
3 3 $ hg init test
4 4 $ cd test
5 5 $ echo foo>foo
6 6 $ hg commit -Am 1 -d '1 0'
7 7 adding foo
8 8 $ echo bar>bar
9 9 $ hg commit -Am 2 -d '2 0'
10 10 adding bar
11 11 $ mkdir baz
12 12 $ echo bletch>baz/bletch
13 13 $ hg commit -Am 3 -d '1000000000 0'
14 14 adding baz/bletch
15 15 $ hg init subrepo
16 16 $ touch subrepo/sub
17 17 $ hg -q -R subrepo ci -Am "init subrepo"
18 18 $ echo "subrepo = subrepo" > .hgsub
19 19 $ hg add .hgsub
20 20 $ hg ci -m "add subrepo"
21 21
22 22 $ cat >> $HGRCPATH <<EOF
23 23 > [extensions]
24 24 > share =
25 25 > EOF
26 26
27 27 hg subrepos are shared when the parent repo is shared
28 28
29 29 $ cd ..
30 30 $ hg share test shared1
31 31 updating working directory
32 32 sharing subrepo subrepo from $TESTTMP/test/subrepo
33 33 5 files updated, 0 files merged, 0 files removed, 0 files unresolved
34 34 $ cat shared1/subrepo/.hg/sharedpath
35 35 $TESTTMP/test/subrepo/.hg (no-eol)
36 36
37 37 hg subrepos are shared into existence on demand if the parent was shared
38 38
39 39 $ hg clone -qr 1 test clone1
40 40 $ hg share clone1 share2
41 41 updating working directory
42 42 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
43 43 $ hg -R clone1 -q pull
44 44 $ hg -R share2 update tip
45 45 sharing subrepo subrepo from $TESTTMP/test/subrepo
46 46 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
47 47 $ cat share2/subrepo/.hg/sharedpath
48 48 $TESTTMP/test/subrepo/.hg (no-eol)
49 49 $ echo 'mod' > share2/subrepo/sub
50 50 $ hg -R share2 ci -Sqm 'subrepo mod'
51 51 $ hg -R clone1 update -C tip
52 52 cloning subrepo subrepo from $TESTTMP/test/subrepo
53 53 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
54 $ find share2 | egrep 'sharedpath|00.+\.i' | sort
54 $ find share2 | grep -E 'sharedpath|00.+\.i' | sort
55 55 share2/.hg/sharedpath
56 56 share2/subrepo/.hg/sharedpath
57 57 $ hg -R share2 unshare
58 58 unsharing subrepo 'subrepo'
59 $ find share2 | egrep 'sharedpath|00.+\.i' | sort
59 $ find share2 | grep -E 'sharedpath|00.+\.i' | sort
60 60 share2/.hg/00changelog.i
61 61 share2/.hg/sharedpath.old
62 62 share2/.hg/store/00changelog.i
63 63 share2/.hg/store/00manifest.i
64 64 share2/subrepo/.hg/00changelog.i
65 65 share2/subrepo/.hg/sharedpath.old
66 66 share2/subrepo/.hg/store/00changelog.i
67 67 share2/subrepo/.hg/store/00manifest.i
68 68 $ hg -R share2/subrepo log -r tip -T compact
69 69 1[tip] 559dcc9bfa65 1970-01-01 00:00 +0000 test
70 70 subrepo mod
71 71
72 72 $ rm -rf clone1
73 73
74 74 $ hg clone -qr 1 test clone1
75 75 $ hg share clone1 shared3
76 76 updating working directory
77 77 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 78 $ hg -R clone1 -q pull
79 79 $ hg -R shared3 archive --config ui.archivemeta=False -r tip -S archive
80 80 sharing subrepo subrepo from $TESTTMP/test/subrepo
81 81 $ cat shared3/subrepo/.hg/sharedpath
82 82 $TESTTMP/test/subrepo/.hg (no-eol)
83 83 $ diff -r archive test
84 84 Only in test: .hg
85 85 Common subdirectories: archive/baz and test/baz (?)
86 86 Common subdirectories: archive/subrepo and test/subrepo (?)
87 87 Only in test/subrepo: .hg
88 88 [1]
89 89 $ rm -rf archive
90 90
91 91 $ cd test
92 92 $ echo "[web]" >> .hg/hgrc
93 93 $ echo "name = test-archive" >> .hg/hgrc
94 94 $ echo "archivesubrepos = True" >> .hg/hgrc
95 95 $ cp .hg/hgrc .hg/hgrc-base
96 96 > test_archtype() {
97 97 > echo "allow-archive = $1" >> .hg/hgrc
98 98 > test_archtype_run "$@"
99 99 > }
100 100 > test_archtype_deprecated() {
101 101 > echo "allow$1 = True" >> .hg/hgrc
102 102 > test_archtype_run "$@"
103 103 > }
104 104 > test_archtype_run() {
105 105 > hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log \
106 106 > --config extensions.blackbox= --config blackbox.track=develwarn
107 107 > cat hg.pid >> $DAEMON_PIDS
108 108 > echo % $1 allowed should give 200
109 109 > get-with-headers.py --bodyfile body localhost:$HGPORT "archive/tip.$2" -
110 110 > f --size --sha1 body
111 111 > echo % $3 and $4 disallowed should both give 403
112 112 > get-with-headers.py --bodyfile body localhost:$HGPORT "archive/tip.$3" -
113 113 > f --size --sha1 body
114 114 > get-with-headers.py --bodyfile body localhost:$HGPORT "archive/tip.$4" -
115 115 > f --size --sha1 body
116 116 > killdaemons.py
117 117 > cat errors.log
118 118 > hg blackbox --config extensions.blackbox= --config blackbox.track=
119 119 > cp .hg/hgrc-base .hg/hgrc
120 120 > }
121 121
122 122 check http return codes
123 123
124 124 $ test_archtype gz tar.gz tar.bz2 zip
125 125 % gz allowed should give 200
126 126 200 Script output follows
127 127 content-disposition: attachment; filename=test-archive-1701ef1f1510.tar.gz
128 128 content-type: application/x-gzip
129 129 date: $HTTP_DATE$
130 130 etag: W/"*" (glob)
131 131 server: testing stub value
132 132 transfer-encoding: chunked
133 133
134 134 body: size=408, sha1=8fa06531bddecc365a9f5edb0f88b65974bfe505 (no-py38 !)
135 135 body: size=506, sha1=70926a04cb8887d0bcccf5380488100a10222def (py38 no-py39 !)
136 136 body: size=505, sha1=eb823c293bedff0df4070b854e2c5cbb06d6ec62 (py39 !)
137 137 % tar.bz2 and zip disallowed should both give 403
138 138 403 Archive type not allowed: bz2
139 139 content-type: text/html; charset=ascii
140 140 date: $HTTP_DATE$
141 141 etag: W/"*" (glob)
142 142 server: testing stub value
143 143 transfer-encoding: chunked
144 144
145 145 body: size=1451, sha1=4c5cf0f574446c44feb7f88f4e0e2a56bd92c352
146 146 403 Archive type not allowed: zip
147 147 content-type: text/html; charset=ascii
148 148 date: $HTTP_DATE$
149 149 etag: W/"*" (glob)
150 150 server: testing stub value
151 151 transfer-encoding: chunked
152 152
153 153 body: size=1451, sha1=cbfa5574b337348bfd0564cc534474d002e7d6c7
154 154 $ test_archtype bz2 tar.bz2 zip tar.gz
155 155 % bz2 allowed should give 200
156 156 200 Script output follows
157 157 content-disposition: attachment; filename=test-archive-1701ef1f1510.tar.bz2
158 158 content-type: application/x-bzip2
159 159 date: $HTTP_DATE$
160 160 etag: W/"*" (glob)
161 161 server: testing stub value
162 162 transfer-encoding: chunked
163 163
164 164 body: size=426, sha1=8d87f5aba6e14f1bfea6c232985982c278b2fb0b (no-py38 !)
165 165 body: size=506, sha1=1bd1f8e8d3701704bd4385038bd9c09b81c77f4e (py38 no-py39 !)
166 166 body: size=503, sha1=2d8ce8bb3816603b9683a1804a5a02c11224cb01 (py39 !)
167 167 % zip and tar.gz disallowed should both give 403
168 168 403 Archive type not allowed: zip
169 169 content-type: text/html; charset=ascii
170 170 date: $HTTP_DATE$
171 171 etag: W/"*" (glob)
172 172 server: testing stub value
173 173 transfer-encoding: chunked
174 174
175 175 body: size=1451, sha1=cbfa5574b337348bfd0564cc534474d002e7d6c7
176 176 403 Archive type not allowed: gz
177 177 content-type: text/html; charset=ascii
178 178 date: $HTTP_DATE$
179 179 etag: W/"*" (glob)
180 180 server: testing stub value
181 181 transfer-encoding: chunked
182 182
183 183 body: size=1450, sha1=71f0b12d59f85fdcfe8ff493e2dc66863f2f7734
184 184 $ test_archtype zip zip tar.gz tar.bz2
185 185 % zip allowed should give 200
186 186 200 Script output follows
187 187 content-disposition: attachment; filename=test-archive-1701ef1f1510.zip
188 188 content-type: application/zip
189 189 date: $HTTP_DATE$
190 190 etag: W/"*" (glob)
191 191 server: testing stub value
192 192 transfer-encoding: chunked
193 193
194 194 body: size=(1377|1461|1489), sha1=(677b14d3d048778d5eb5552c14a67e6192068650|be6d3983aa13dfe930361b2569291cdedd02b537|1897e496871aa89ad685a92b936f5fa0d008b9e8) (re)
195 195 % tar.gz and tar.bz2 disallowed should both give 403
196 196 403 Archive type not allowed: gz
197 197 content-type: text/html; charset=ascii
198 198 date: $HTTP_DATE$
199 199 etag: W/"*" (glob)
200 200 server: testing stub value
201 201 transfer-encoding: chunked
202 202
203 203 body: size=1450, sha1=71f0b12d59f85fdcfe8ff493e2dc66863f2f7734
204 204 403 Archive type not allowed: bz2
205 205 content-type: text/html; charset=ascii
206 206 date: $HTTP_DATE$
207 207 etag: W/"*" (glob)
208 208 server: testing stub value
209 209 transfer-encoding: chunked
210 210
211 211 body: size=1451, sha1=4c5cf0f574446c44feb7f88f4e0e2a56bd92c352
212 212
213 213 check http return codes (with deprecated option)
214 214
215 215 $ test_archtype_deprecated gz tar.gz tar.bz2 zip
216 216 % gz allowed should give 200
217 217 200 Script output follows
218 218 content-disposition: attachment; filename=test-archive-1701ef1f1510.tar.gz
219 219 content-type: application/x-gzip
220 220 date: $HTTP_DATE$
221 221 etag: W/"*" (glob)
222 222 server: testing stub value
223 223 transfer-encoding: chunked
224 224
225 225 body: size=408, sha1=8fa06531bddecc365a9f5edb0f88b65974bfe505 (no-py38 !)
226 226 body: size=506, sha1=70926a04cb8887d0bcccf5380488100a10222def (py38 no-py39 !)
227 227 body: size=505, sha1=eb823c293bedff0df4070b854e2c5cbb06d6ec62 (py39 !)
228 228 % tar.bz2 and zip disallowed should both give 403
229 229 403 Archive type not allowed: bz2
230 230 content-type: text/html; charset=ascii
231 231 date: $HTTP_DATE$
232 232 etag: W/"*" (glob)
233 233 server: testing stub value
234 234 transfer-encoding: chunked
235 235
236 236 body: size=1451, sha1=4c5cf0f574446c44feb7f88f4e0e2a56bd92c352
237 237 403 Archive type not allowed: zip
238 238 content-type: text/html; charset=ascii
239 239 date: $HTTP_DATE$
240 240 etag: W/"*" (glob)
241 241 server: testing stub value
242 242 transfer-encoding: chunked
243 243
244 244 body: size=1451, sha1=cbfa5574b337348bfd0564cc534474d002e7d6c7
245 245 $ test_archtype_deprecated bz2 tar.bz2 zip tar.gz
246 246 % bz2 allowed should give 200
247 247 200 Script output follows
248 248 content-disposition: attachment; filename=test-archive-1701ef1f1510.tar.bz2
249 249 content-type: application/x-bzip2
250 250 date: $HTTP_DATE$
251 251 etag: W/"*" (glob)
252 252 server: testing stub value
253 253 transfer-encoding: chunked
254 254
255 255 body: size=426, sha1=8d87f5aba6e14f1bfea6c232985982c278b2fb0b (no-py38 !)
256 256 body: size=506, sha1=1bd1f8e8d3701704bd4385038bd9c09b81c77f4e (py38 no-py39 !)
257 257 body: size=503, sha1=2d8ce8bb3816603b9683a1804a5a02c11224cb01 (py39 !)
258 258 % zip and tar.gz disallowed should both give 403
259 259 403 Archive type not allowed: zip
260 260 content-type: text/html; charset=ascii
261 261 date: $HTTP_DATE$
262 262 etag: W/"*" (glob)
263 263 server: testing stub value
264 264 transfer-encoding: chunked
265 265
266 266 body: size=1451, sha1=cbfa5574b337348bfd0564cc534474d002e7d6c7
267 267 403 Archive type not allowed: gz
268 268 content-type: text/html; charset=ascii
269 269 date: $HTTP_DATE$
270 270 etag: W/"*" (glob)
271 271 server: testing stub value
272 272 transfer-encoding: chunked
273 273
274 274 body: size=1450, sha1=71f0b12d59f85fdcfe8ff493e2dc66863f2f7734
275 275 $ test_archtype_deprecated zip zip tar.gz tar.bz2
276 276 % zip allowed should give 200
277 277 200 Script output follows
278 278 content-disposition: attachment; filename=test-archive-1701ef1f1510.zip
279 279 content-type: application/zip
280 280 date: $HTTP_DATE$
281 281 etag: W/"*" (glob)
282 282 server: testing stub value
283 283 transfer-encoding: chunked
284 284
285 285 body: size=(1377|1461|1489), sha1=(677b14d3d048778d5eb5552c14a67e6192068650|be6d3983aa13dfe930361b2569291cdedd02b537|1897e496871aa89ad685a92b936f5fa0d008b9e8) (re)
286 286 % tar.gz and tar.bz2 disallowed should both give 403
287 287 403 Archive type not allowed: gz
288 288 content-type: text/html; charset=ascii
289 289 date: $HTTP_DATE$
290 290 etag: W/"*" (glob)
291 291 server: testing stub value
292 292 transfer-encoding: chunked
293 293
294 294 body: size=1450, sha1=71f0b12d59f85fdcfe8ff493e2dc66863f2f7734
295 295 403 Archive type not allowed: bz2
296 296 content-type: text/html; charset=ascii
297 297 date: $HTTP_DATE$
298 298 etag: W/"*" (glob)
299 299 server: testing stub value
300 300 transfer-encoding: chunked
301 301
302 302 body: size=1451, sha1=4c5cf0f574446c44feb7f88f4e0e2a56bd92c352
303 303
304 304 $ echo "allow-archive = gz bz2 zip" >> .hg/hgrc
305 305 $ hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log
306 306 $ cat hg.pid >> $DAEMON_PIDS
307 307
308 308 check archive links' order
309 309
310 310 $ get-with-headers.py localhost:$HGPORT "?revcount=1" | grep '/archive/tip.'
311 311 <a href="/archive/tip.zip">zip</a>
312 312 <a href="/archive/tip.tar.gz">gz</a>
313 313 <a href="/archive/tip.tar.bz2">bz2</a>
314 314
315 315 invalid arch type should give 404
316 316
317 317 $ get-with-headers.py localhost:$HGPORT "archive/tip.invalid" | head -n 1
318 318 404 Unsupported archive type: None
319 319
320 320 $ TIP=`hg id -v | cut -f1 -d' '`
321 321 $ QTIP=`hg id -q`
322 322 $ cat > getarchive.py <<EOF
323 323 > import os
324 324 > import sys
325 325 > from mercurial import (
326 326 > util,
327 327 > )
328 328 > try:
329 329 > # Set stdout to binary mode for win32 platforms
330 330 > import msvcrt
331 331 > msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
332 332 > except ImportError:
333 333 > pass
334 334 > if len(sys.argv) <= 3:
335 335 > node, archive = sys.argv[1:]
336 336 > requeststr = 'cmd=archive&node=%s&type=%s' % (node, archive)
337 337 > else:
338 338 > node, archive, file = sys.argv[1:]
339 339 > requeststr = 'cmd=archive&node=%s&type=%s&file=%s' % (node, archive, file)
340 340 > try:
341 341 > stdout = sys.stdout.buffer
342 342 > except AttributeError:
343 343 > stdout = sys.stdout
344 344 > try:
345 345 > f = util.urlreq.urlopen('http://$LOCALIP:%s/?%s'
346 346 > % (os.environ['HGPORT'], requeststr))
347 347 > stdout.write(f.read())
348 348 > except util.urlerr.httperror as e:
349 349 > sys.stderr.write(str(e) + '\n')
350 350 > EOF
351 351 $ "$PYTHON" getarchive.py "$TIP" gz | gunzip | tar tf - 2>/dev/null
352 352 test-archive-1701ef1f1510/.hg_archival.txt
353 353 test-archive-1701ef1f1510/.hgsub
354 354 test-archive-1701ef1f1510/.hgsubstate
355 355 test-archive-1701ef1f1510/bar
356 356 test-archive-1701ef1f1510/baz/bletch
357 357 test-archive-1701ef1f1510/foo
358 358 test-archive-1701ef1f1510/subrepo/sub
359 359 $ "$PYTHON" getarchive.py "$TIP" bz2 | bunzip2 | tar tf - 2>/dev/null
360 360 test-archive-1701ef1f1510/.hg_archival.txt
361 361 test-archive-1701ef1f1510/.hgsub
362 362 test-archive-1701ef1f1510/.hgsubstate
363 363 test-archive-1701ef1f1510/bar
364 364 test-archive-1701ef1f1510/baz/bletch
365 365 test-archive-1701ef1f1510/foo
366 366 test-archive-1701ef1f1510/subrepo/sub
367 367 $ "$PYTHON" getarchive.py "$TIP" zip > archive.zip
368 368 $ unzip -t archive.zip
369 369 Archive: archive.zip
370 370 testing: test-archive-1701ef1f1510/.hg_archival.txt*OK (glob)
371 371 testing: test-archive-1701ef1f1510/.hgsub*OK (glob)
372 372 testing: test-archive-1701ef1f1510/.hgsubstate*OK (glob)
373 373 testing: test-archive-1701ef1f1510/bar*OK (glob)
374 374 testing: test-archive-1701ef1f1510/baz/bletch*OK (glob)
375 375 testing: test-archive-1701ef1f1510/foo*OK (glob)
376 376 testing: test-archive-1701ef1f1510/subrepo/sub*OK (glob)
377 377 No errors detected in compressed data of archive.zip.
378 378
379 379 test that we can download single directories and files
380 380
381 381 $ "$PYTHON" getarchive.py "$TIP" gz baz | gunzip | tar tf - 2>/dev/null
382 382 test-archive-1701ef1f1510/baz/bletch
383 383 $ "$PYTHON" getarchive.py "$TIP" gz foo | gunzip | tar tf - 2>/dev/null
384 384 test-archive-1701ef1f1510/foo
385 385
386 386 test that we detect file patterns that match no files
387 387
388 388 $ "$PYTHON" getarchive.py "$TIP" gz foobar
389 389 HTTP Error 404: file(s) not found: foobar
390 390
391 391 test that we reject unsafe patterns
392 392
393 393 $ "$PYTHON" getarchive.py "$TIP" gz relre:baz
394 394 HTTP Error 404: file(s) not found: relre:baz
395 395
396 396 $ killdaemons.py
397 397
398 398 $ hg archive -t tar test.tar
399 399 $ tar tf test.tar
400 400 test/.hg_archival.txt
401 401 test/.hgsub
402 402 test/.hgsubstate
403 403 test/bar
404 404 test/baz/bletch
405 405 test/foo
406 406
407 407 $ hg archive --debug -t tbz2 -X baz test.tar.bz2 --config progress.debug=true
408 408 archiving: 0/4 files (0.00%)
409 409 archiving: .hgsub 1/4 files (25.00%)
410 410 archiving: .hgsubstate 2/4 files (50.00%)
411 411 archiving: bar 3/4 files (75.00%)
412 412 archiving: foo 4/4 files (100.00%)
413 413 $ bunzip2 -dc test.tar.bz2 | tar tf - 2>/dev/null
414 414 test/.hg_archival.txt
415 415 test/.hgsub
416 416 test/.hgsubstate
417 417 test/bar
418 418 test/foo
419 419
420 420 $ hg archive -t tgz -p %b-%h test-%h.tar.gz
421 421 $ gzip -dc test-$QTIP.tar.gz | tar tf - 2>/dev/null
422 422 test-1701ef1f1510/.hg_archival.txt
423 423 test-1701ef1f1510/.hgsub
424 424 test-1701ef1f1510/.hgsubstate
425 425 test-1701ef1f1510/bar
426 426 test-1701ef1f1510/baz/bletch
427 427 test-1701ef1f1510/foo
428 428
429 429 $ hg archive autodetected_test.tar
430 430 $ tar tf autodetected_test.tar
431 431 autodetected_test/.hg_archival.txt
432 432 autodetected_test/.hgsub
433 433 autodetected_test/.hgsubstate
434 434 autodetected_test/bar
435 435 autodetected_test/baz/bletch
436 436 autodetected_test/foo
437 437
438 438 The '-t' should override autodetection
439 439
440 440 $ hg archive -t tar autodetect_override_test.zip
441 441 $ tar tf autodetect_override_test.zip
442 442 autodetect_override_test.zip/.hg_archival.txt
443 443 autodetect_override_test.zip/.hgsub
444 444 autodetect_override_test.zip/.hgsubstate
445 445 autodetect_override_test.zip/bar
446 446 autodetect_override_test.zip/baz/bletch
447 447 autodetect_override_test.zip/foo
448 448
449 449 $ for ext in tar tar.gz tgz tar.bz2 tbz2 zip; do
450 450 > hg archive auto_test.$ext
451 451 > if [ -d auto_test.$ext ]; then
452 452 > echo "extension $ext was not autodetected."
453 453 > fi
454 454 > done
455 455
456 456 $ cat > md5comp.py <<EOF
457 457 > import hashlib
458 458 > import sys
459 459 > f1, f2 = sys.argv[1:3]
460 460 > h1 = hashlib.md5(open(f1, 'rb').read()).hexdigest()
461 461 > h2 = hashlib.md5(open(f2, 'rb').read()).hexdigest()
462 462 > print(h1 == h2 or "md5 differ: " + repr((h1, h2)))
463 463 > EOF
464 464
465 465 archive name is stored in the archive, so create similar archives and
466 466 rename them afterwards.
467 467
468 468 $ hg archive -t tgz tip.tar.gz
469 469 $ mv tip.tar.gz tip1.tar.gz
470 470 $ sleep 1
471 471 $ hg archive -t tgz tip.tar.gz
472 472 $ mv tip.tar.gz tip2.tar.gz
473 473 $ "$PYTHON" md5comp.py tip1.tar.gz tip2.tar.gz
474 474 True
475 475
476 476 $ hg archive -t zip -p /illegal test.zip
477 477 abort: archive prefix contains illegal components
478 478 [255]
479 479 $ hg archive -t zip -p very/../bad test.zip
480 480
481 481 $ hg archive --config ui.archivemeta=false -t zip -r 2 test.zip
482 482 $ unzip -t test.zip
483 483 Archive: test.zip
484 484 testing: test/bar*OK (glob)
485 485 testing: test/baz/bletch*OK (glob)
486 486 testing: test/foo*OK (glob)
487 487 No errors detected in compressed data of test.zip.
488 488
489 489 $ hg archive -t tar - | tar tf - 2>/dev/null
490 490 test-1701ef1f1510/.hg_archival.txt
491 491 test-1701ef1f1510/.hgsub
492 492 test-1701ef1f1510/.hgsubstate
493 493 test-1701ef1f1510/bar
494 494 test-1701ef1f1510/baz/bletch
495 495 test-1701ef1f1510/foo
496 496
497 497 $ hg archive -r 0 -t tar rev-%r.tar
498 498 $ [ -f rev-0.tar ]
499 499
500 500 test .hg_archival.txt
501 501
502 502 $ hg archive ../test-tags
503 503 $ cat ../test-tags/.hg_archival.txt
504 504 repo: daa7f7c60e0a224faa4ff77ca41b2760562af264
505 505 node: 1701ef1f151069b8747038e93b5186bb43a47504
506 506 branch: default
507 507 latesttag: null
508 508 latesttagdistance: 4
509 509 changessincelatesttag: 4
510 510 $ hg tag -r 2 mytag
511 511 $ hg tag -r 2 anothertag
512 512 $ hg archive -r 2 ../test-lasttag
513 513 $ cat ../test-lasttag/.hg_archival.txt
514 514 repo: daa7f7c60e0a224faa4ff77ca41b2760562af264
515 515 node: 2c0277f05ed49d1c8328fb9ba92fba7a5ebcb33e
516 516 branch: default
517 517 tag: anothertag
518 518 tag: mytag
519 519
520 520 $ hg archive -t bogus test.bogus
521 521 abort: unknown archive type 'bogus'
522 522 [255]
523 523
524 524 enable progress extension:
525 525
526 526 $ cp $HGRCPATH $HGRCPATH.no-progress
527 527 $ cat >> $HGRCPATH <<EOF
528 528 > [progress]
529 529 > assume-tty = 1
530 530 > format = topic bar number
531 531 > delay = 0
532 532 > refresh = 0
533 533 > width = 60
534 534 > EOF
535 535
536 536 $ hg archive ../with-progress
537 537 \r (no-eol) (esc)
538 538 archiving [ ] 0/6\r (no-eol) (esc)
539 539 archiving [======> ] 1/6\r (no-eol) (esc)
540 540 archiving [=============> ] 2/6\r (no-eol) (esc)
541 541 archiving [====================> ] 3/6\r (no-eol) (esc)
542 542 archiving [===========================> ] 4/6\r (no-eol) (esc)
543 543 archiving [==================================> ] 5/6\r (no-eol) (esc)
544 544 archiving [==========================================>] 6/6\r (no-eol) (esc)
545 545 \r (no-eol) (esc)
546 546
547 547 cleanup after progress extension test:
548 548
549 549 $ cp $HGRCPATH.no-progress $HGRCPATH
550 550
551 551 server errors
552 552
553 553 $ cat errors.log
554 554
555 555 empty repo
556 556
557 557 $ hg init ../empty
558 558 $ cd ../empty
559 559 $ hg archive ../test-empty
560 560 abort: no working directory: please specify a revision
561 561 [10]
562 562
563 563 old file -- date clamped to 1980
564 564
565 565 $ touch -t 197501010000 old
566 566 $ hg add old
567 567 $ hg commit -m old
568 568 $ hg archive ../old.zip
569 $ unzip -l ../old.zip | grep -v -- ----- | egrep -v files$
569 $ unzip -l ../old.zip | grep -v -- ----- | grep -E -v files$
570 570 Archive: ../old.zip
571 571 \s*Length.* (re)
572 572 *172*80*00:00*old/.hg_archival.txt (glob)
573 573 *0*80*00:00*old/old (glob)
574 574
575 575 test xz support only available in Python 3.4
576 576
577 577 #if lzma
578 578 $ hg archive ../archive.txz
579 579 $ which xz >/dev/null && xz -l ../archive.txz | head -n1 || true
580 580 Strms Blocks Compressed Uncompressed Ratio Check Filename (xz !)
581 581 $ rm -f ../archive.txz
582 582 #endif
583 583 #if no-lzma
584 584 $ hg archive ../archive.txz
585 585 abort: lzma module is not available
586 586 [255]
587 587 #endif
588 588
589 589 show an error when a provided pattern matches no files
590 590
591 591 $ hg archive -I file_that_does_not_exist.foo ../empty.zip
592 592 abort: no files match the archive pattern
593 593 [255]
594 594
595 595 $ hg archive -X * ../empty.zip
596 596 abort: no files match the archive pattern
597 597 [255]
598 598
599 599 $ cd ..
600 600
601 601 issue3600: check whether "hg archive" can create archive files which
602 602 are extracted with expected timestamp, even though TZ is not
603 603 configured as GMT.
604 604
605 605 $ mkdir issue3600
606 606 $ cd issue3600
607 607
608 608 $ hg init repo
609 609 $ echo a > repo/a
610 610 $ hg -R repo add repo/a
611 611 $ hg -R repo commit -m '#0' -d '456789012 21600'
612 612 $ cat > show_mtime.py <<EOF
613 613 > import os
614 614 > import sys
615 615 > print(int(os.stat(sys.argv[1]).st_mtime))
616 616 > EOF
617 617
618 618 $ hg -R repo archive --prefix tar-extracted archive.tar
619 619 $ (TZ=UTC-3; export TZ; tar xf archive.tar)
620 620 $ "$PYTHON" show_mtime.py tar-extracted/a
621 621 456789012
622 622
623 623 $ hg -R repo archive --prefix zip-extracted archive.zip
624 624 $ (TZ=UTC-3; export TZ; unzip -q archive.zip)
625 625 $ "$PYTHON" show_mtime.py zip-extracted/a
626 626 456789012
627 627
628 628 $ cd ..
@@ -1,154 +1,154 b''
1 1 #require no-chg
2 2 $ filterlog () {
3 3 > sed -e 's!^[0-9/]* [0-9:]* ([0-9]*)>!YYYY/MM/DD HH:MM:SS (PID)>!'
4 4 > }
5 5
6 6 ensure that failing ui.atexit handlers report sensibly
7 7
8 8 $ cat > $TESTTMP/bailatexit.py <<EOF
9 9 > from mercurial import util
10 10 > def bail():
11 11 > raise RuntimeError('ui.atexit handler exception')
12 12 >
13 13 > def extsetup(ui):
14 14 > ui.atexit(bail)
15 15 > EOF
16 16 $ hg -q --config extensions.bailatexit=$TESTTMP/bailatexit.py \
17 17 > help help
18 18 hg help [-eck] [-s PLATFORM] [TOPIC]
19 19
20 20 show help for a given topic or a help overview
21 21 error in exit handlers:
22 22 Traceback (most recent call last):
23 23 File "*/mercurial/dispatch.py", line *, in _runexithandlers (glob) (no-pyoxidizer !)
24 24 File "mercurial.dispatch", line *, in _runexithandlers (glob) (pyoxidizer !)
25 25 func(*args, **kwargs)
26 26 File "$TESTTMP/bailatexit.py", line *, in bail (glob)
27 27 raise RuntimeError('ui.atexit handler exception')
28 28 RuntimeError: ui.atexit handler exception
29 29 [255]
30 30
31 31 $ rm $TESTTMP/bailatexit.py
32 32
33 33 another bad extension
34 34
35 35 $ echo 'raise Exception("bit bucket overflow")' > badext.py
36 36 $ abspathexc=`pwd`/badext.py
37 37
38 38 $ cat >baddocext.py <<EOF
39 39 > """
40 40 > baddocext is bad
41 41 > """
42 42 > EOF
43 43 $ abspathdoc=`pwd`/baddocext.py
44 44
45 45 $ cat <<EOF >> $HGRCPATH
46 46 > [extensions]
47 47 > gpg =
48 48 > hgext.gpg =
49 49 > badext = $abspathexc
50 50 > baddocext = $abspathdoc
51 51 > badext2 =
52 52 > EOF
53 53
54 54 $ hg -q help help 2>&1 |grep extension
55 55 *** failed to import extension "badext" from $TESTTMP/badext.py: bit bucket overflow
56 56 *** failed to import extension "badext2": No module named 'badext2'
57 57
58 58 show traceback
59 59
60 $ hg -q help help --traceback 2>&1 | egrep ' extension|^Exception|Traceback|ImportError|ModuleNotFound'
60 $ hg -q help help --traceback 2>&1 | grep -E ' extension|^Exception|Traceback|ImportError|ModuleNotFound'
61 61 *** failed to import extension "badext" from $TESTTMP/badext.py: bit bucket overflow
62 62 Traceback (most recent call last):
63 63 Exception: bit bucket overflow
64 64 *** failed to import extension "badext2": No module named 'badext2'
65 65 Traceback (most recent call last):
66 66 ModuleNotFoundError: No module named 'hgext.badext2'
67 67 Traceback (most recent call last):
68 68 ModuleNotFoundError: No module named 'hgext3rd.badext2'
69 69 Traceback (most recent call last):
70 70 ModuleNotFoundError: No module named 'badext2'
71 71
72 72 names of extensions failed to load can be accessed via extensions.notloaded()
73 73
74 74 $ cat <<EOF > showbadexts.py
75 75 > from mercurial import commands, extensions, registrar
76 76 > cmdtable = {}
77 77 > command = registrar.command(cmdtable)
78 78 > @command(b'showbadexts', norepo=True)
79 79 > def showbadexts(ui, *pats, **opts):
80 80 > ui.write(b'BADEXTS: %s\n' % b' '.join(sorted(extensions.notloaded())))
81 81 > EOF
82 82 $ hg --config extensions.badexts=showbadexts.py showbadexts 2>&1 | grep '^BADEXTS'
83 83 BADEXTS: badext badext2
84 84
85 85 #if no-extraextensions
86 86 show traceback for ImportError of hgext.name if devel.debug.extensions is set
87 87
88 88 $ (hg help help --traceback --debug --config devel.debug.extensions=yes 2>&1) \
89 89 > | grep -v '^ ' \
90 90 > | filterlog \
91 > | egrep 'extension..[^p]|^Exception|Traceback|ImportError|^YYYY|not import|ModuleNotFound'
91 > | grep -E 'extension..[^p]|^Exception|Traceback|ImportError|^YYYY|not import|ModuleNotFound'
92 92 YYYY/MM/DD HH:MM:SS (PID)> loading extensions
93 93 YYYY/MM/DD HH:MM:SS (PID)> - processing 5 entries
94 94 YYYY/MM/DD HH:MM:SS (PID)> - loading extension: gpg
95 95 YYYY/MM/DD HH:MM:SS (PID)> > gpg extension loaded in * (glob)
96 96 YYYY/MM/DD HH:MM:SS (PID)> - validating extension tables: gpg
97 97 YYYY/MM/DD HH:MM:SS (PID)> - invoking registered callbacks: gpg
98 98 YYYY/MM/DD HH:MM:SS (PID)> > callbacks completed in * (glob)
99 99 YYYY/MM/DD HH:MM:SS (PID)> - loading extension: badext
100 100 *** failed to import extension "badext" from $TESTTMP/badext.py: bit bucket overflow
101 101 Traceback (most recent call last):
102 102 Exception: bit bucket overflow
103 103 YYYY/MM/DD HH:MM:SS (PID)> - loading extension: baddocext
104 104 YYYY/MM/DD HH:MM:SS (PID)> > baddocext extension loaded in * (glob)
105 105 YYYY/MM/DD HH:MM:SS (PID)> - validating extension tables: baddocext
106 106 YYYY/MM/DD HH:MM:SS (PID)> - invoking registered callbacks: baddocext
107 107 YYYY/MM/DD HH:MM:SS (PID)> > callbacks completed in * (glob)
108 108 YYYY/MM/DD HH:MM:SS (PID)> - loading extension: badext2
109 109 YYYY/MM/DD HH:MM:SS (PID)> - could not import hgext.badext2 (No module named *badext2*): trying hgext3rd.badext2 (glob)
110 110 Traceback (most recent call last):
111 111 ModuleNotFoundError: No module named 'hgext.badext2'
112 112 YYYY/MM/DD HH:MM:SS (PID)> - could not import hgext3rd.badext2 (No module named *badext2*): trying badext2 (glob)
113 113 Traceback (most recent call last):
114 114 ModuleNotFoundError: No module named 'hgext.badext2'
115 115 Traceback (most recent call last):
116 116 ModuleNotFoundError: No module named 'hgext3rd.badext2'
117 117 *** failed to import extension "badext2": No module named 'badext2'
118 118 Traceback (most recent call last):
119 119 ModuleNotFoundError: No module named 'hgext.badext2'
120 120 Traceback (most recent call last):
121 121 ModuleNotFoundError: No module named 'hgext3rd.badext2'
122 122 Traceback (most recent call last):
123 123 ModuleNotFoundError: No module named 'badext2'
124 124 YYYY/MM/DD HH:MM:SS (PID)> > loaded 2 extensions, total time * (glob)
125 125 YYYY/MM/DD HH:MM:SS (PID)> - loading configtable attributes
126 126 YYYY/MM/DD HH:MM:SS (PID)> - executing uisetup hooks
127 127 YYYY/MM/DD HH:MM:SS (PID)> - running uisetup for gpg
128 128 YYYY/MM/DD HH:MM:SS (PID)> > uisetup for gpg took * (glob)
129 129 YYYY/MM/DD HH:MM:SS (PID)> - running uisetup for baddocext
130 130 YYYY/MM/DD HH:MM:SS (PID)> > uisetup for baddocext took * (glob)
131 131 YYYY/MM/DD HH:MM:SS (PID)> > all uisetup took * (glob)
132 132 YYYY/MM/DD HH:MM:SS (PID)> - executing extsetup hooks
133 133 YYYY/MM/DD HH:MM:SS (PID)> - running extsetup for gpg
134 134 YYYY/MM/DD HH:MM:SS (PID)> > extsetup for gpg took * (glob)
135 135 YYYY/MM/DD HH:MM:SS (PID)> - running extsetup for baddocext
136 136 YYYY/MM/DD HH:MM:SS (PID)> > extsetup for baddocext took * (glob)
137 137 YYYY/MM/DD HH:MM:SS (PID)> > all extsetup took * (glob)
138 138 YYYY/MM/DD HH:MM:SS (PID)> - executing remaining aftercallbacks
139 139 YYYY/MM/DD HH:MM:SS (PID)> > remaining aftercallbacks completed in * (glob)
140 140 YYYY/MM/DD HH:MM:SS (PID)> - loading extension registration objects
141 141 YYYY/MM/DD HH:MM:SS (PID)> > extension registration object loading took * (glob)
142 142 YYYY/MM/DD HH:MM:SS (PID)> > extension baddocext take a total of * to load (glob)
143 143 YYYY/MM/DD HH:MM:SS (PID)> > extension gpg take a total of * to load (glob)
144 144 YYYY/MM/DD HH:MM:SS (PID)> extension loading complete
145 145 #endif
146 146
147 147 confirm that there's no crash when an extension's documentation is bad
148 148
149 149 $ hg help --keyword baddocext
150 150 *** failed to import extension "badext" from $TESTTMP/badext.py: bit bucket overflow
151 151 *** failed to import extension "badext2": No module named 'badext2'
152 152 Topics:
153 153
154 154 extensions Using Additional Features
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now