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

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

@@ -1,255 +1,256 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,271 +1,272 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 265 bb42988c7e156931b0ff1e93732b98173ebbcb7f 6.5rc0
266 266 3ffc7209bbae5804a53084c9dc2d41139e88c867 6.5
267 267 787af4e0e8b787e1b77a8059926b123730a4cd01 6.5.1
268 268 5a8b5420103937fca97c584c5162178eed828ada 6.5.2
269 269 c083d9776cb2fb6056715b2988d1ea48055f3162 6.5.3
270 270 27055614b68538576fb0439007009acf93fe0a49 6.6rc0
271 271 26c57e7a0890b96e2c473b394de380d6753c9230 6.6
272 71bd09bebbe36a09569cbfb388f371433360056b 6.6.1
@@ -1,4638 +1,4637 b''
1 1 # perf.py - performance test routines
2 2 '''helper extension to measure performance
3 3
4 4 Configurations
5 5 ==============
6 6
7 7 ``perf``
8 8 --------
9 9
10 10 ``all-timing``
11 11 When set, additional statistics will be reported for each benchmark: best,
12 12 worst, median average. If not set only the best timing is reported
13 13 (default: off).
14 14
15 15 ``presleep``
16 16 number of second to wait before any group of runs (default: 1)
17 17
18 18 ``pre-run``
19 19 number of run to perform before starting measurement.
20 20
21 21 ``profile-benchmark``
22 22 Enable profiling for the benchmarked section.
23 23 (The first iteration is benchmarked)
24 24
25 25 ``run-limits``
26 26 Control the number of runs each benchmark will perform. The option value
27 27 should be a list of `<time>-<numberofrun>` pairs. After each run the
28 28 conditions are considered in order with the following logic:
29 29
30 30 If benchmark has been running for <time> seconds, and we have performed
31 31 <numberofrun> iterations, stop the benchmark,
32 32
33 33 The default value is: `3.0-100, 10.0-3`
34 34
35 35 ``stub``
36 36 When set, benchmarks will only be run once, useful for testing
37 37 (default: off)
38 38 '''
39 39
40 40 # "historical portability" policy of perf.py:
41 41 #
42 42 # We have to do:
43 43 # - make perf.py "loadable" with as wide Mercurial version as possible
44 44 # This doesn't mean that perf commands work correctly with that Mercurial.
45 45 # BTW, perf.py itself has been available since 1.1 (or eb240755386d).
46 46 # - make historical perf command work correctly with as wide Mercurial
47 47 # version as possible
48 48 #
49 49 # We have to do, if possible with reasonable cost:
50 50 # - make recent perf command for historical feature work correctly
51 51 # with early Mercurial
52 52 #
53 53 # We don't have to do:
54 54 # - make perf command for recent feature work correctly with early
55 55 # Mercurial
56 56
57 57 import contextlib
58 58 import functools
59 59 import gc
60 60 import os
61 61 import random
62 62 import shutil
63 63 import struct
64 64 import sys
65 65 import tempfile
66 66 import threading
67 67 import time
68 68
69 69 import mercurial.revlog
70 70 from mercurial import (
71 71 changegroup,
72 72 cmdutil,
73 73 commands,
74 74 copies,
75 75 error,
76 76 extensions,
77 77 hg,
78 78 mdiff,
79 79 merge,
80 80 util,
81 81 )
82 82
83 83 # for "historical portability":
84 84 # try to import modules separately (in dict order), and ignore
85 85 # failure, because these aren't available with early Mercurial
86 86 try:
87 87 from mercurial import branchmap # since 2.5 (or bcee63733aad)
88 88 except ImportError:
89 89 pass
90 90 try:
91 91 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
92 92 except ImportError:
93 93 pass
94 94 try:
95 95 from mercurial import registrar # since 3.7 (or 37d50250b696)
96 96
97 97 dir(registrar) # forcibly load it
98 98 except ImportError:
99 99 registrar = None
100 100 try:
101 101 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
102 102 except ImportError:
103 103 pass
104 104 try:
105 105 from mercurial.utils import repoviewutil # since 5.0
106 106 except ImportError:
107 107 repoviewutil = None
108 108 try:
109 109 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
110 110 except ImportError:
111 111 pass
112 112 try:
113 113 from mercurial import setdiscovery # since 1.9 (or cb98fed52495)
114 114 except ImportError:
115 115 pass
116 116
117 117 try:
118 118 from mercurial import profiling
119 119 except ImportError:
120 120 profiling = None
121 121
122 122 try:
123 123 from mercurial.revlogutils import constants as revlog_constants
124 124
125 125 perf_rl_kind = (revlog_constants.KIND_OTHER, b'created-by-perf')
126 126
127 127 def revlog(opener, *args, **kwargs):
128 128 return mercurial.revlog.revlog(opener, perf_rl_kind, *args, **kwargs)
129 129
130 130
131 131 except (ImportError, AttributeError):
132 132 perf_rl_kind = None
133 133
134 134 def revlog(opener, *args, **kwargs):
135 135 return mercurial.revlog.revlog(opener, *args, **kwargs)
136 136
137 137
138 138 def identity(a):
139 139 return a
140 140
141 141
142 142 try:
143 143 from mercurial import pycompat
144 144
145 145 getargspec = pycompat.getargspec # added to module after 4.5
146 146 _byteskwargs = pycompat.byteskwargs # since 4.1 (or fbc3f73dc802)
147 147 _sysstr = pycompat.sysstr # since 4.0 (or 2219f4f82ede)
148 148 _bytestr = pycompat.bytestr # since 4.2 (or b70407bd84d5)
149 149 _xrange = pycompat.xrange # since 4.8 (or 7eba8f83129b)
150 150 fsencode = pycompat.fsencode # since 3.9 (or f4a5e0e86a7e)
151 151 if pycompat.ispy3:
152 152 _maxint = sys.maxsize # per py3 docs for replacing maxint
153 153 else:
154 154 _maxint = sys.maxint
155 155 except (NameError, ImportError, AttributeError):
156 156 import inspect
157 157
158 158 getargspec = inspect.getargspec
159 159 _byteskwargs = identity
160 160 _bytestr = str
161 161 fsencode = identity # no py3 support
162 162 _maxint = sys.maxint # no py3 support
163 163 _sysstr = lambda x: x # no py3 support
164 164 _xrange = xrange
165 165
166 166 try:
167 167 # 4.7+
168 168 queue = pycompat.queue.Queue
169 169 except (NameError, AttributeError, ImportError):
170 170 # <4.7.
171 171 try:
172 172 queue = pycompat.queue
173 173 except (NameError, AttributeError, ImportError):
174 174 import Queue as queue
175 175
176 176 try:
177 177 from mercurial import logcmdutil
178 178
179 179 makelogtemplater = logcmdutil.maketemplater
180 180 except (AttributeError, ImportError):
181 181 try:
182 182 makelogtemplater = cmdutil.makelogtemplater
183 183 except (AttributeError, ImportError):
184 184 makelogtemplater = None
185 185
186 186 # for "historical portability":
187 187 # define util.safehasattr forcibly, because util.safehasattr has been
188 188 # available since 1.9.3 (or 94b200a11cf7)
189 189 _undefined = object()
190 190
191 191
192 192 def safehasattr(thing, attr):
193 193 return getattr(thing, _sysstr(attr), _undefined) is not _undefined
194 194
195 195
196 196 setattr(util, 'safehasattr', safehasattr)
197 197
198 198 # for "historical portability":
199 199 # define util.timer forcibly, because util.timer has been available
200 200 # since ae5d60bb70c9
201 201 if safehasattr(time, 'perf_counter'):
202 202 util.timer = time.perf_counter
203 203 elif os.name == b'nt':
204 204 util.timer = time.clock
205 205 else:
206 206 util.timer = time.time
207 207
208 208 # for "historical portability":
209 209 # use locally defined empty option list, if formatteropts isn't
210 210 # available, because commands.formatteropts has been available since
211 211 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
212 212 # available since 2.2 (or ae5f92e154d3)
213 213 formatteropts = getattr(
214 214 cmdutil, "formatteropts", getattr(commands, "formatteropts", [])
215 215 )
216 216
217 217 # for "historical portability":
218 218 # use locally defined option list, if debugrevlogopts isn't available,
219 219 # because commands.debugrevlogopts has been available since 3.7 (or
220 220 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
221 221 # since 1.9 (or a79fea6b3e77).
222 222 revlogopts = getattr(
223 223 cmdutil,
224 224 "debugrevlogopts",
225 225 getattr(
226 226 commands,
227 227 "debugrevlogopts",
228 228 [
229 229 (b'c', b'changelog', False, b'open changelog'),
230 230 (b'm', b'manifest', False, b'open manifest'),
231 231 (b'', b'dir', False, b'open directory manifest'),
232 232 ],
233 233 ),
234 234 )
235 235
236 236 cmdtable = {}
237 237
238 238
239 239 # for "historical portability":
240 240 # define parsealiases locally, because cmdutil.parsealiases has been
241 241 # available since 1.5 (or 6252852b4332)
242 242 def parsealiases(cmd):
243 243 return cmd.split(b"|")
244 244
245 245
246 246 if safehasattr(registrar, 'command'):
247 247 command = registrar.command(cmdtable)
248 248 elif safehasattr(cmdutil, 'command'):
249 249 command = cmdutil.command(cmdtable)
250 250 if 'norepo' not in getargspec(command).args:
251 251 # for "historical portability":
252 252 # wrap original cmdutil.command, because "norepo" option has
253 253 # been available since 3.1 (or 75a96326cecb)
254 254 _command = command
255 255
256 256 def command(name, options=(), synopsis=None, norepo=False):
257 257 if norepo:
258 258 commands.norepo += b' %s' % b' '.join(parsealiases(name))
259 259 return _command(name, list(options), synopsis)
260 260
261 261
262 262 else:
263 263 # for "historical portability":
264 264 # define "@command" annotation locally, because cmdutil.command
265 265 # has been available since 1.9 (or 2daa5179e73f)
266 266 def command(name, options=(), synopsis=None, norepo=False):
267 267 def decorator(func):
268 268 if synopsis:
269 269 cmdtable[name] = func, list(options), synopsis
270 270 else:
271 271 cmdtable[name] = func, list(options)
272 272 if norepo:
273 273 commands.norepo += b' %s' % b' '.join(parsealiases(name))
274 274 return func
275 275
276 276 return decorator
277 277
278 278
279 279 try:
280 280 import mercurial.registrar
281 281 import mercurial.configitems
282 282
283 283 configtable = {}
284 284 configitem = mercurial.registrar.configitem(configtable)
285 285 configitem(
286 286 b'perf',
287 287 b'presleep',
288 288 default=mercurial.configitems.dynamicdefault,
289 289 experimental=True,
290 290 )
291 291 configitem(
292 292 b'perf',
293 293 b'stub',
294 294 default=mercurial.configitems.dynamicdefault,
295 295 experimental=True,
296 296 )
297 297 configitem(
298 298 b'perf',
299 299 b'parentscount',
300 300 default=mercurial.configitems.dynamicdefault,
301 301 experimental=True,
302 302 )
303 303 configitem(
304 304 b'perf',
305 305 b'all-timing',
306 306 default=mercurial.configitems.dynamicdefault,
307 307 experimental=True,
308 308 )
309 309 configitem(
310 310 b'perf',
311 311 b'pre-run',
312 312 default=mercurial.configitems.dynamicdefault,
313 313 )
314 314 configitem(
315 315 b'perf',
316 316 b'profile-benchmark',
317 317 default=mercurial.configitems.dynamicdefault,
318 318 )
319 319 configitem(
320 320 b'perf',
321 321 b'run-limits',
322 322 default=mercurial.configitems.dynamicdefault,
323 323 experimental=True,
324 324 )
325 325 except (ImportError, AttributeError):
326 326 pass
327 327 except TypeError:
328 328 # compatibility fix for a11fd395e83f
329 329 # hg version: 5.2
330 330 configitem(
331 331 b'perf',
332 332 b'presleep',
333 333 default=mercurial.configitems.dynamicdefault,
334 334 )
335 335 configitem(
336 336 b'perf',
337 337 b'stub',
338 338 default=mercurial.configitems.dynamicdefault,
339 339 )
340 340 configitem(
341 341 b'perf',
342 342 b'parentscount',
343 343 default=mercurial.configitems.dynamicdefault,
344 344 )
345 345 configitem(
346 346 b'perf',
347 347 b'all-timing',
348 348 default=mercurial.configitems.dynamicdefault,
349 349 )
350 350 configitem(
351 351 b'perf',
352 352 b'pre-run',
353 353 default=mercurial.configitems.dynamicdefault,
354 354 )
355 355 configitem(
356 356 b'perf',
357 357 b'profile-benchmark',
358 358 default=mercurial.configitems.dynamicdefault,
359 359 )
360 360 configitem(
361 361 b'perf',
362 362 b'run-limits',
363 363 default=mercurial.configitems.dynamicdefault,
364 364 )
365 365
366 366
367 367 def getlen(ui):
368 368 if ui.configbool(b"perf", b"stub", False):
369 369 return lambda x: 1
370 370 return len
371 371
372 372
373 373 class noop:
374 374 """dummy context manager"""
375 375
376 376 def __enter__(self):
377 377 pass
378 378
379 379 def __exit__(self, *args):
380 380 pass
381 381
382 382
383 383 NOOPCTX = noop()
384 384
385 385
386 386 def gettimer(ui, opts=None):
387 387 """return a timer function and formatter: (timer, formatter)
388 388
389 389 This function exists to gather the creation of formatter in a single
390 390 place instead of duplicating it in all performance commands."""
391 391
392 392 # enforce an idle period before execution to counteract power management
393 393 # experimental config: perf.presleep
394 394 time.sleep(getint(ui, b"perf", b"presleep", 1))
395 395
396 396 if opts is None:
397 397 opts = {}
398 398 # redirect all to stderr unless buffer api is in use
399 399 if not ui._buffers:
400 400 ui = ui.copy()
401 401 uifout = safeattrsetter(ui, b'fout', ignoremissing=True)
402 402 if uifout:
403 403 # for "historical portability":
404 404 # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d)
405 405 uifout.set(ui.ferr)
406 406
407 407 # get a formatter
408 408 uiformatter = getattr(ui, 'formatter', None)
409 409 if uiformatter:
410 410 fm = uiformatter(b'perf', opts)
411 411 else:
412 412 # for "historical portability":
413 413 # define formatter locally, because ui.formatter has been
414 414 # available since 2.2 (or ae5f92e154d3)
415 415 from mercurial import node
416 416
417 417 class defaultformatter:
418 418 """Minimized composition of baseformatter and plainformatter"""
419 419
420 420 def __init__(self, ui, topic, opts):
421 421 self._ui = ui
422 422 if ui.debugflag:
423 423 self.hexfunc = node.hex
424 424 else:
425 425 self.hexfunc = node.short
426 426
427 427 def __nonzero__(self):
428 428 return False
429 429
430 430 __bool__ = __nonzero__
431 431
432 432 def startitem(self):
433 433 pass
434 434
435 435 def data(self, **data):
436 436 pass
437 437
438 438 def write(self, fields, deftext, *fielddata, **opts):
439 439 self._ui.write(deftext % fielddata, **opts)
440 440
441 441 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
442 442 if cond:
443 443 self._ui.write(deftext % fielddata, **opts)
444 444
445 445 def plain(self, text, **opts):
446 446 self._ui.write(text, **opts)
447 447
448 448 def end(self):
449 449 pass
450 450
451 451 fm = defaultformatter(ui, b'perf', opts)
452 452
453 453 # stub function, runs code only once instead of in a loop
454 454 # experimental config: perf.stub
455 455 if ui.configbool(b"perf", b"stub", False):
456 456 return functools.partial(stub_timer, fm), fm
457 457
458 458 # experimental config: perf.all-timing
459 459 displayall = ui.configbool(b"perf", b"all-timing", True)
460 460
461 461 # experimental config: perf.run-limits
462 462 limitspec = ui.configlist(b"perf", b"run-limits", [])
463 463 limits = []
464 464 for item in limitspec:
465 465 parts = item.split(b'-', 1)
466 466 if len(parts) < 2:
467 467 ui.warn((b'malformatted run limit entry, missing "-": %s\n' % item))
468 468 continue
469 469 try:
470 470 time_limit = float(_sysstr(parts[0]))
471 471 except ValueError as e:
472 472 ui.warn(
473 473 (
474 474 b'malformatted run limit entry, %s: %s\n'
475 475 % (_bytestr(e), item)
476 476 )
477 477 )
478 478 continue
479 479 try:
480 480 run_limit = int(_sysstr(parts[1]))
481 481 except ValueError as e:
482 482 ui.warn(
483 483 (
484 484 b'malformatted run limit entry, %s: %s\n'
485 485 % (_bytestr(e), item)
486 486 )
487 487 )
488 488 continue
489 489 limits.append((time_limit, run_limit))
490 490 if not limits:
491 491 limits = DEFAULTLIMITS
492 492
493 493 profiler = None
494 494 if profiling is not None:
495 495 if ui.configbool(b"perf", b"profile-benchmark", False):
496 496 profiler = profiling.profile(ui)
497 497
498 498 prerun = getint(ui, b"perf", b"pre-run", 0)
499 499 t = functools.partial(
500 500 _timer,
501 501 fm,
502 502 displayall=displayall,
503 503 limits=limits,
504 504 prerun=prerun,
505 505 profiler=profiler,
506 506 )
507 507 return t, fm
508 508
509 509
510 510 def stub_timer(fm, func, setup=None, title=None):
511 511 if setup is not None:
512 512 setup()
513 513 func()
514 514
515 515
516 516 @contextlib.contextmanager
517 517 def timeone():
518 518 r = []
519 519 ostart = os.times()
520 520 cstart = util.timer()
521 521 yield r
522 522 cstop = util.timer()
523 523 ostop = os.times()
524 524 a, b = ostart, ostop
525 525 r.append((cstop - cstart, b[0] - a[0], b[1] - a[1]))
526 526
527 527
528 528 # list of stop condition (elapsed time, minimal run count)
529 529 DEFAULTLIMITS = (
530 530 (3.0, 100),
531 531 (10.0, 3),
532 532 )
533 533
534 534
535 535 @contextlib.contextmanager
536 536 def noop_context():
537 537 yield
538 538
539 539
540 540 def _timer(
541 541 fm,
542 542 func,
543 543 setup=None,
544 544 context=noop_context,
545 545 title=None,
546 546 displayall=False,
547 547 limits=DEFAULTLIMITS,
548 548 prerun=0,
549 549 profiler=None,
550 550 ):
551 551 gc.collect()
552 552 results = []
553 553 begin = util.timer()
554 554 count = 0
555 555 if profiler is None:
556 556 profiler = NOOPCTX
557 557 for i in range(prerun):
558 558 if setup is not None:
559 559 setup()
560 560 with context():
561 561 func()
562 562 keepgoing = True
563 563 while keepgoing:
564 564 if setup is not None:
565 565 setup()
566 566 with context():
567 567 with profiler:
568 568 with timeone() as item:
569 569 r = func()
570 570 profiler = NOOPCTX
571 571 count += 1
572 572 results.append(item[0])
573 573 cstop = util.timer()
574 574 # Look for a stop condition.
575 575 elapsed = cstop - begin
576 576 for t, mincount in limits:
577 577 if elapsed >= t and count >= mincount:
578 578 keepgoing = False
579 579 break
580 580
581 581 formatone(fm, results, title=title, result=r, displayall=displayall)
582 582
583 583
584 584 def formatone(fm, timings, title=None, result=None, displayall=False):
585 585 count = len(timings)
586 586
587 587 fm.startitem()
588 588
589 589 if title:
590 590 fm.write(b'title', b'! %s\n', title)
591 591 if result:
592 592 fm.write(b'result', b'! result: %s\n', result)
593 593
594 594 def display(role, entry):
595 595 prefix = b''
596 596 if role != b'best':
597 597 prefix = b'%s.' % role
598 598 fm.plain(b'!')
599 599 fm.write(prefix + b'wall', b' wall %f', entry[0])
600 600 fm.write(prefix + b'comb', b' comb %f', entry[1] + entry[2])
601 601 fm.write(prefix + b'user', b' user %f', entry[1])
602 602 fm.write(prefix + b'sys', b' sys %f', entry[2])
603 603 fm.write(prefix + b'count', b' (%s of %%d)' % role, count)
604 604 fm.plain(b'\n')
605 605
606 606 timings.sort()
607 607 min_val = timings[0]
608 608 display(b'best', min_val)
609 609 if displayall:
610 610 max_val = timings[-1]
611 611 display(b'max', max_val)
612 612 avg = tuple([sum(x) / count for x in zip(*timings)])
613 613 display(b'avg', avg)
614 614 median = timings[len(timings) // 2]
615 615 display(b'median', median)
616 616
617 617
618 618 # utilities for historical portability
619 619
620 620
621 621 def getint(ui, section, name, default):
622 622 # for "historical portability":
623 623 # ui.configint has been available since 1.9 (or fa2b596db182)
624 624 v = ui.config(section, name, None)
625 625 if v is None:
626 626 return default
627 627 try:
628 628 return int(v)
629 629 except ValueError:
630 630 raise error.ConfigError(
631 631 b"%s.%s is not an integer ('%s')" % (section, name, v)
632 632 )
633 633
634 634
635 635 def safeattrsetter(obj, name, ignoremissing=False):
636 636 """Ensure that 'obj' has 'name' attribute before subsequent setattr
637 637
638 638 This function is aborted, if 'obj' doesn't have 'name' attribute
639 639 at runtime. This avoids overlooking removal of an attribute, which
640 640 breaks assumption of performance measurement, in the future.
641 641
642 642 This function returns the object to (1) assign a new value, and
643 643 (2) restore an original value to the attribute.
644 644
645 645 If 'ignoremissing' is true, missing 'name' attribute doesn't cause
646 646 abortion, and this function returns None. This is useful to
647 647 examine an attribute, which isn't ensured in all Mercurial
648 648 versions.
649 649 """
650 650 if not util.safehasattr(obj, name):
651 651 if ignoremissing:
652 652 return None
653 653 raise error.Abort(
654 654 (
655 655 b"missing attribute %s of %s might break assumption"
656 656 b" of performance measurement"
657 657 )
658 658 % (name, obj)
659 659 )
660 660
661 661 origvalue = getattr(obj, _sysstr(name))
662 662
663 663 class attrutil:
664 664 def set(self, newvalue):
665 665 setattr(obj, _sysstr(name), newvalue)
666 666
667 667 def restore(self):
668 668 setattr(obj, _sysstr(name), origvalue)
669 669
670 670 return attrutil()
671 671
672 672
673 673 # utilities to examine each internal API changes
674 674
675 675
676 676 def getbranchmapsubsettable():
677 677 # for "historical portability":
678 678 # subsettable is defined in:
679 679 # - branchmap since 2.9 (or 175c6fd8cacc)
680 680 # - repoview since 2.5 (or 59a9f18d4587)
681 681 # - repoviewutil since 5.0
682 682 for mod in (branchmap, repoview, repoviewutil):
683 683 subsettable = getattr(mod, 'subsettable', None)
684 684 if subsettable:
685 685 return subsettable
686 686
687 687 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
688 688 # branchmap and repoview modules exist, but subsettable attribute
689 689 # doesn't)
690 690 raise error.Abort(
691 691 b"perfbranchmap not available with this Mercurial",
692 692 hint=b"use 2.5 or later",
693 693 )
694 694
695 695
696 696 def getsvfs(repo):
697 697 """Return appropriate object to access files under .hg/store"""
698 698 # for "historical portability":
699 699 # repo.svfs has been available since 2.3 (or 7034365089bf)
700 700 svfs = getattr(repo, 'svfs', None)
701 701 if svfs:
702 702 return svfs
703 703 else:
704 704 return getattr(repo, 'sopener')
705 705
706 706
707 707 def getvfs(repo):
708 708 """Return appropriate object to access files under .hg"""
709 709 # for "historical portability":
710 710 # repo.vfs has been available since 2.3 (or 7034365089bf)
711 711 vfs = getattr(repo, 'vfs', None)
712 712 if vfs:
713 713 return vfs
714 714 else:
715 715 return getattr(repo, 'opener')
716 716
717 717
718 718 def repocleartagscachefunc(repo):
719 719 """Return the function to clear tags cache according to repo internal API"""
720 720 if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525)
721 721 # in this case, setattr(repo, '_tagscache', None) or so isn't
722 722 # correct way to clear tags cache, because existing code paths
723 723 # expect _tagscache to be a structured object.
724 724 def clearcache():
725 725 # _tagscache has been filteredpropertycache since 2.5 (or
726 726 # 98c867ac1330), and delattr() can't work in such case
727 727 if '_tagscache' in vars(repo):
728 728 del repo.__dict__['_tagscache']
729 729
730 730 return clearcache
731 731
732 732 repotags = safeattrsetter(repo, b'_tags', ignoremissing=True)
733 733 if repotags: # since 1.4 (or 5614a628d173)
734 734 return lambda: repotags.set(None)
735 735
736 736 repotagscache = safeattrsetter(repo, b'tagscache', ignoremissing=True)
737 737 if repotagscache: # since 0.6 (or d7df759d0e97)
738 738 return lambda: repotagscache.set(None)
739 739
740 740 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
741 741 # this point, but it isn't so problematic, because:
742 742 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
743 743 # in perftags() causes failure soon
744 744 # - perf.py itself has been available since 1.1 (or eb240755386d)
745 745 raise error.Abort(b"tags API of this hg command is unknown")
746 746
747 747
748 748 # utilities to clear cache
749 749
750 750
751 751 def clearfilecache(obj, attrname):
752 752 unfiltered = getattr(obj, 'unfiltered', None)
753 753 if unfiltered is not None:
754 754 obj = obj.unfiltered()
755 755 if attrname in vars(obj):
756 756 delattr(obj, attrname)
757 757 obj._filecache.pop(attrname, None)
758 758
759 759
760 760 def clearchangelog(repo):
761 761 if repo is not repo.unfiltered():
762 762 object.__setattr__(repo, '_clcachekey', None)
763 763 object.__setattr__(repo, '_clcache', None)
764 764 clearfilecache(repo.unfiltered(), 'changelog')
765 765
766 766
767 767 # perf commands
768 768
769 769
770 770 @command(b'perf::walk|perfwalk', formatteropts)
771 771 def perfwalk(ui, repo, *pats, **opts):
772 772 opts = _byteskwargs(opts)
773 773 timer, fm = gettimer(ui, opts)
774 774 m = scmutil.match(repo[None], pats, {})
775 775 timer(
776 776 lambda: len(
777 777 list(
778 778 repo.dirstate.walk(m, subrepos=[], unknown=True, ignored=False)
779 779 )
780 780 )
781 781 )
782 782 fm.end()
783 783
784 784
785 785 @command(b'perf::annotate|perfannotate', formatteropts)
786 786 def perfannotate(ui, repo, f, **opts):
787 787 opts = _byteskwargs(opts)
788 788 timer, fm = gettimer(ui, opts)
789 789 fc = repo[b'.'][f]
790 790 timer(lambda: len(fc.annotate(True)))
791 791 fm.end()
792 792
793 793
794 794 @command(
795 795 b'perf::status|perfstatus',
796 796 [
797 797 (b'u', b'unknown', False, b'ask status to look for unknown files'),
798 798 (b'', b'dirstate', False, b'benchmark the internal dirstate call'),
799 799 ]
800 800 + formatteropts,
801 801 )
802 802 def perfstatus(ui, repo, **opts):
803 803 """benchmark the performance of a single status call
804 804
805 805 The repository data are preserved between each call.
806 806
807 807 By default, only the status of the tracked file are requested. If
808 808 `--unknown` is passed, the "unknown" files are also tracked.
809 809 """
810 810 opts = _byteskwargs(opts)
811 811 # m = match.always(repo.root, repo.getcwd())
812 812 # timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
813 813 # False))))
814 814 timer, fm = gettimer(ui, opts)
815 815 if opts[b'dirstate']:
816 816 dirstate = repo.dirstate
817 817 m = scmutil.matchall(repo)
818 818 unknown = opts[b'unknown']
819 819
820 820 def status_dirstate():
821 821 s = dirstate.status(
822 822 m, subrepos=[], ignored=False, clean=False, unknown=unknown
823 823 )
824 824 sum(map(bool, s))
825 825
826 826 if util.safehasattr(dirstate, 'running_status'):
827 827 with dirstate.running_status(repo):
828 828 timer(status_dirstate)
829 829 dirstate.invalidate()
830 830 else:
831 831 timer(status_dirstate)
832 832 else:
833 833 timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown']))))
834 834 fm.end()
835 835
836 836
837 837 @command(b'perf::addremove|perfaddremove', formatteropts)
838 838 def perfaddremove(ui, repo, **opts):
839 839 opts = _byteskwargs(opts)
840 840 timer, fm = gettimer(ui, opts)
841 841 try:
842 842 oldquiet = repo.ui.quiet
843 843 repo.ui.quiet = True
844 844 matcher = scmutil.match(repo[None])
845 845 opts[b'dry_run'] = True
846 846 if 'uipathfn' in getargspec(scmutil.addremove).args:
847 847 uipathfn = scmutil.getuipathfn(repo)
848 848 timer(lambda: scmutil.addremove(repo, matcher, b"", uipathfn, opts))
849 849 else:
850 850 timer(lambda: scmutil.addremove(repo, matcher, b"", opts))
851 851 finally:
852 852 repo.ui.quiet = oldquiet
853 853 fm.end()
854 854
855 855
856 856 def clearcaches(cl):
857 857 # behave somewhat consistently across internal API changes
858 858 if util.safehasattr(cl, b'clearcaches'):
859 859 cl.clearcaches()
860 860 elif util.safehasattr(cl, b'_nodecache'):
861 861 # <= hg-5.2
862 862 from mercurial.node import nullid, nullrev
863 863
864 864 cl._nodecache = {nullid: nullrev}
865 865 cl._nodepos = None
866 866
867 867
868 868 @command(b'perf::heads|perfheads', formatteropts)
869 869 def perfheads(ui, repo, **opts):
870 870 """benchmark the computation of a changelog heads"""
871 871 opts = _byteskwargs(opts)
872 872 timer, fm = gettimer(ui, opts)
873 873 cl = repo.changelog
874 874
875 875 def s():
876 876 clearcaches(cl)
877 877
878 878 def d():
879 879 len(cl.headrevs())
880 880
881 881 timer(d, setup=s)
882 882 fm.end()
883 883
884 884
885 885 def _default_clear_on_disk_tags_cache(repo):
886 886 from mercurial import tags
887 887
888 888 repo.cachevfs.tryunlink(tags._filename(repo))
889 889
890 890
891 891 def _default_clear_on_disk_tags_fnodes_cache(repo):
892 892 from mercurial import tags
893 893
894 894 repo.cachevfs.tryunlink(tags._fnodescachefile)
895 895
896 896
897 897 def _default_forget_fnodes(repo, revs):
898 898 """function used by the perf extension to prune some entries from the
899 899 fnodes cache"""
900 900 from mercurial import tags
901 901
902 902 missing_1 = b'\xff' * 4
903 903 missing_2 = b'\xff' * 20
904 904 cache = tags.hgtagsfnodescache(repo.unfiltered())
905 905 for r in revs:
906 906 cache._writeentry(r * tags._fnodesrecsize, missing_1, missing_2)
907 907 cache.write()
908 908
909 909
910 910 @command(
911 911 b'perf::tags|perftags',
912 912 formatteropts
913 913 + [
914 914 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
915 915 (
916 916 b'',
917 917 b'clear-on-disk-cache',
918 918 False,
919 919 b'clear on disk tags cache (DESTRUCTIVE)',
920 920 ),
921 921 (
922 922 b'',
923 923 b'clear-fnode-cache-all',
924 924 False,
925 925 b'clear on disk file node cache (DESTRUCTIVE),',
926 926 ),
927 927 (
928 928 b'',
929 929 b'clear-fnode-cache-rev',
930 930 [],
931 931 b'clear on disk file node cache (DESTRUCTIVE),',
932 932 b'REVS',
933 933 ),
934 934 (
935 935 b'',
936 936 b'update-last',
937 937 b'',
938 938 b'simulate an update over the last N revisions (DESTRUCTIVE),',
939 939 b'N',
940 940 ),
941 941 ],
942 942 )
943 943 def perftags(ui, repo, **opts):
944 944 """Benchmark tags retrieval in various situation
945 945
946 946 The option marked as (DESTRUCTIVE) will alter the on-disk cache, possibly
947 947 altering performance after the command was run. However, it does not
948 948 destroy any stored data.
949 949 """
950 950 from mercurial import tags
951 951
952 952 opts = _byteskwargs(opts)
953 953 timer, fm = gettimer(ui, opts)
954 954 repocleartagscache = repocleartagscachefunc(repo)
955 955 clearrevlogs = opts[b'clear_revlogs']
956 956 clear_disk = opts[b'clear_on_disk_cache']
957 957 clear_fnode = opts[b'clear_fnode_cache_all']
958 958
959 959 clear_fnode_revs = opts[b'clear_fnode_cache_rev']
960 960 update_last_str = opts[b'update_last']
961 961 update_last = None
962 962 if update_last_str:
963 963 try:
964 964 update_last = int(update_last_str)
965 965 except ValueError:
966 966 msg = b'could not parse value for update-last: "%s"'
967 967 msg %= update_last_str
968 968 hint = b'value should be an integer'
969 969 raise error.Abort(msg, hint=hint)
970 970
971 971 clear_disk_fn = getattr(
972 972 tags,
973 973 "clear_cache_on_disk",
974 974 _default_clear_on_disk_tags_cache,
975 975 )
976 clear_fnodes_fn = getattr(
977 tags,
978 "clear_cache_fnodes",
979 _default_clear_on_disk_tags_fnodes_cache,
980 )
976 if getattr(tags, 'clear_cache_fnodes_is_working', False):
977 clear_fnodes_fn = tags.clear_cache_fnodes
978 else:
979 clear_fnodes_fn = _default_clear_on_disk_tags_fnodes_cache
981 980 clear_fnodes_rev_fn = getattr(
982 981 tags,
983 982 "forget_fnodes",
984 983 _default_forget_fnodes,
985 984 )
986 985
987 986 clear_revs = []
988 987 if clear_fnode_revs:
989 clear_revs.extends(scmutil.revrange(repo, clear_fnode_revs))
988 clear_revs.extend(scmutil.revrange(repo, clear_fnode_revs))
990 989
991 990 if update_last:
992 991 revset = b'last(all(), %d)' % update_last
993 992 last_revs = repo.unfiltered().revs(revset)
994 993 clear_revs.extend(last_revs)
995 994
996 995 from mercurial import repoview
997 996
998 997 rev_filter = {(b'experimental', b'extra-filter-revs'): revset}
999 998 with repo.ui.configoverride(rev_filter, source=b"perf"):
1000 999 filter_id = repoview.extrafilter(repo.ui)
1001 1000
1002 1001 filter_name = b'%s%%%s' % (repo.filtername, filter_id)
1003 1002 pre_repo = repo.filtered(filter_name)
1004 1003 pre_repo.tags() # warm the cache
1005 1004 old_tags_path = repo.cachevfs.join(tags._filename(pre_repo))
1006 1005 new_tags_path = repo.cachevfs.join(tags._filename(repo))
1007 1006
1008 1007 clear_revs = sorted(set(clear_revs))
1009 1008
1010 1009 def s():
1011 1010 if update_last:
1012 1011 util.copyfile(old_tags_path, new_tags_path)
1013 1012 if clearrevlogs:
1014 1013 clearchangelog(repo)
1015 1014 clearfilecache(repo.unfiltered(), 'manifest')
1016 1015 if clear_disk:
1017 1016 clear_disk_fn(repo)
1018 1017 if clear_fnode:
1019 1018 clear_fnodes_fn(repo)
1020 1019 elif clear_revs:
1021 1020 clear_fnodes_rev_fn(repo, clear_revs)
1022 1021 repocleartagscache()
1023 1022
1024 1023 def t():
1025 1024 len(repo.tags())
1026 1025
1027 1026 timer(t, setup=s)
1028 1027 fm.end()
1029 1028
1030 1029
1031 1030 @command(b'perf::ancestors|perfancestors', formatteropts)
1032 1031 def perfancestors(ui, repo, **opts):
1033 1032 opts = _byteskwargs(opts)
1034 1033 timer, fm = gettimer(ui, opts)
1035 1034 heads = repo.changelog.headrevs()
1036 1035
1037 1036 def d():
1038 1037 for a in repo.changelog.ancestors(heads):
1039 1038 pass
1040 1039
1041 1040 timer(d)
1042 1041 fm.end()
1043 1042
1044 1043
1045 1044 @command(b'perf::ancestorset|perfancestorset', formatteropts)
1046 1045 def perfancestorset(ui, repo, revset, **opts):
1047 1046 opts = _byteskwargs(opts)
1048 1047 timer, fm = gettimer(ui, opts)
1049 1048 revs = repo.revs(revset)
1050 1049 heads = repo.changelog.headrevs()
1051 1050
1052 1051 def d():
1053 1052 s = repo.changelog.ancestors(heads)
1054 1053 for rev in revs:
1055 1054 rev in s
1056 1055
1057 1056 timer(d)
1058 1057 fm.end()
1059 1058
1060 1059
1061 1060 @command(
1062 1061 b'perf::delta-find',
1063 1062 revlogopts + formatteropts,
1064 1063 b'-c|-m|FILE REV',
1065 1064 )
1066 1065 def perf_delta_find(ui, repo, arg_1, arg_2=None, **opts):
1067 1066 """benchmark the process of finding a valid delta for a revlog revision
1068 1067
1069 1068 When a revlog receives a new revision (e.g. from a commit, or from an
1070 1069 incoming bundle), it searches for a suitable delta-base to produce a delta.
1071 1070 This perf command measures how much time we spend in this process. It
1072 1071 operates on an already stored revision.
1073 1072
1074 1073 See `hg help debug-delta-find` for another related command.
1075 1074 """
1076 1075 from mercurial import revlogutils
1077 1076 import mercurial.revlogutils.deltas as deltautil
1078 1077
1079 1078 opts = _byteskwargs(opts)
1080 1079 if arg_2 is None:
1081 1080 file_ = None
1082 1081 rev = arg_1
1083 1082 else:
1084 1083 file_ = arg_1
1085 1084 rev = arg_2
1086 1085
1087 1086 repo = repo.unfiltered()
1088 1087
1089 1088 timer, fm = gettimer(ui, opts)
1090 1089
1091 1090 rev = int(rev)
1092 1091
1093 1092 revlog = cmdutil.openrevlog(repo, b'perf::delta-find', file_, opts)
1094 1093
1095 1094 deltacomputer = deltautil.deltacomputer(revlog)
1096 1095
1097 1096 node = revlog.node(rev)
1098 1097 p1r, p2r = revlog.parentrevs(rev)
1099 1098 p1 = revlog.node(p1r)
1100 1099 p2 = revlog.node(p2r)
1101 1100 full_text = revlog.revision(rev)
1102 1101 textlen = len(full_text)
1103 1102 cachedelta = None
1104 1103 flags = revlog.flags(rev)
1105 1104
1106 1105 revinfo = revlogutils.revisioninfo(
1107 1106 node,
1108 1107 p1,
1109 1108 p2,
1110 1109 [full_text], # btext
1111 1110 textlen,
1112 1111 cachedelta,
1113 1112 flags,
1114 1113 )
1115 1114
1116 1115 # Note: we should probably purge the potential caches (like the full
1117 1116 # manifest cache) between runs.
1118 1117 def find_one():
1119 1118 with revlog._datafp() as fh:
1120 1119 deltacomputer.finddeltainfo(revinfo, fh, target_rev=rev)
1121 1120
1122 1121 timer(find_one)
1123 1122 fm.end()
1124 1123
1125 1124
1126 1125 @command(b'perf::discovery|perfdiscovery', formatteropts, b'PATH')
1127 1126 def perfdiscovery(ui, repo, path, **opts):
1128 1127 """benchmark discovery between local repo and the peer at given path"""
1129 1128 repos = [repo, None]
1130 1129 timer, fm = gettimer(ui, opts)
1131 1130
1132 1131 try:
1133 1132 from mercurial.utils.urlutil import get_unique_pull_path_obj
1134 1133
1135 1134 path = get_unique_pull_path_obj(b'perfdiscovery', ui, path)
1136 1135 except ImportError:
1137 1136 try:
1138 1137 from mercurial.utils.urlutil import get_unique_pull_path
1139 1138
1140 1139 path = get_unique_pull_path(b'perfdiscovery', repo, ui, path)[0]
1141 1140 except ImportError:
1142 1141 path = ui.expandpath(path)
1143 1142
1144 1143 def s():
1145 1144 repos[1] = hg.peer(ui, opts, path)
1146 1145
1147 1146 def d():
1148 1147 setdiscovery.findcommonheads(ui, *repos)
1149 1148
1150 1149 timer(d, setup=s)
1151 1150 fm.end()
1152 1151
1153 1152
1154 1153 @command(
1155 1154 b'perf::bookmarks|perfbookmarks',
1156 1155 formatteropts
1157 1156 + [
1158 1157 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
1159 1158 ],
1160 1159 )
1161 1160 def perfbookmarks(ui, repo, **opts):
1162 1161 """benchmark parsing bookmarks from disk to memory"""
1163 1162 opts = _byteskwargs(opts)
1164 1163 timer, fm = gettimer(ui, opts)
1165 1164
1166 1165 clearrevlogs = opts[b'clear_revlogs']
1167 1166
1168 1167 def s():
1169 1168 if clearrevlogs:
1170 1169 clearchangelog(repo)
1171 1170 clearfilecache(repo, b'_bookmarks')
1172 1171
1173 1172 def d():
1174 1173 repo._bookmarks
1175 1174
1176 1175 timer(d, setup=s)
1177 1176 fm.end()
1178 1177
1179 1178
1180 1179 @command(
1181 1180 b'perf::bundle',
1182 1181 [
1183 1182 (
1184 1183 b'r',
1185 1184 b'rev',
1186 1185 [],
1187 1186 b'changesets to bundle',
1188 1187 b'REV',
1189 1188 ),
1190 1189 (
1191 1190 b't',
1192 1191 b'type',
1193 1192 b'none',
1194 1193 b'bundlespec to use (see `hg help bundlespec`)',
1195 1194 b'TYPE',
1196 1195 ),
1197 1196 ]
1198 1197 + formatteropts,
1199 1198 b'REVS',
1200 1199 )
1201 1200 def perfbundle(ui, repo, *revs, **opts):
1202 1201 """benchmark the creation of a bundle from a repository
1203 1202
1204 1203 For now, this only supports "none" compression.
1205 1204 """
1206 1205 try:
1207 1206 from mercurial import bundlecaches
1208 1207
1209 1208 parsebundlespec = bundlecaches.parsebundlespec
1210 1209 except ImportError:
1211 1210 from mercurial import exchange
1212 1211
1213 1212 parsebundlespec = exchange.parsebundlespec
1214 1213
1215 1214 from mercurial import discovery
1216 1215 from mercurial import bundle2
1217 1216
1218 1217 opts = _byteskwargs(opts)
1219 1218 timer, fm = gettimer(ui, opts)
1220 1219
1221 1220 cl = repo.changelog
1222 1221 revs = list(revs)
1223 1222 revs.extend(opts.get(b'rev', ()))
1224 1223 revs = scmutil.revrange(repo, revs)
1225 1224 if not revs:
1226 1225 raise error.Abort(b"not revision specified")
1227 1226 # make it a consistent set (ie: without topological gaps)
1228 1227 old_len = len(revs)
1229 1228 revs = list(repo.revs(b"%ld::%ld", revs, revs))
1230 1229 if old_len != len(revs):
1231 1230 new_count = len(revs) - old_len
1232 1231 msg = b"add %d new revisions to make it a consistent set\n"
1233 1232 ui.write_err(msg % new_count)
1234 1233
1235 1234 targets = [cl.node(r) for r in repo.revs(b"heads(::%ld)", revs)]
1236 1235 bases = [cl.node(r) for r in repo.revs(b"heads(::%ld - %ld)", revs, revs)]
1237 1236 outgoing = discovery.outgoing(repo, bases, targets)
1238 1237
1239 1238 bundle_spec = opts.get(b'type')
1240 1239
1241 1240 bundle_spec = parsebundlespec(repo, bundle_spec, strict=False)
1242 1241
1243 1242 cgversion = bundle_spec.params.get(b"cg.version")
1244 1243 if cgversion is None:
1245 1244 if bundle_spec.version == b'v1':
1246 1245 cgversion = b'01'
1247 1246 if bundle_spec.version == b'v2':
1248 1247 cgversion = b'02'
1249 1248 if cgversion not in changegroup.supportedoutgoingversions(repo):
1250 1249 err = b"repository does not support bundle version %s"
1251 1250 raise error.Abort(err % cgversion)
1252 1251
1253 1252 if cgversion == b'01': # bundle1
1254 1253 bversion = b'HG10' + bundle_spec.wirecompression
1255 1254 bcompression = None
1256 1255 elif cgversion in (b'02', b'03'):
1257 1256 bversion = b'HG20'
1258 1257 bcompression = bundle_spec.wirecompression
1259 1258 else:
1260 1259 err = b'perf::bundle: unexpected changegroup version %s'
1261 1260 raise error.ProgrammingError(err % cgversion)
1262 1261
1263 1262 if bcompression is None:
1264 1263 bcompression = b'UN'
1265 1264
1266 1265 if bcompression != b'UN':
1267 1266 err = b'perf::bundle: compression currently unsupported: %s'
1268 1267 raise error.ProgrammingError(err % bcompression)
1269 1268
1270 1269 def do_bundle():
1271 1270 bundle2.writenewbundle(
1272 1271 ui,
1273 1272 repo,
1274 1273 b'perf::bundle',
1275 1274 os.devnull,
1276 1275 bversion,
1277 1276 outgoing,
1278 1277 bundle_spec.params,
1279 1278 )
1280 1279
1281 1280 timer(do_bundle)
1282 1281 fm.end()
1283 1282
1284 1283
1285 1284 @command(b'perf::bundleread|perfbundleread', formatteropts, b'BUNDLE')
1286 1285 def perfbundleread(ui, repo, bundlepath, **opts):
1287 1286 """Benchmark reading of bundle files.
1288 1287
1289 1288 This command is meant to isolate the I/O part of bundle reading as
1290 1289 much as possible.
1291 1290 """
1292 1291 from mercurial import (
1293 1292 bundle2,
1294 1293 exchange,
1295 1294 streamclone,
1296 1295 )
1297 1296
1298 1297 opts = _byteskwargs(opts)
1299 1298
1300 1299 def makebench(fn):
1301 1300 def run():
1302 1301 with open(bundlepath, b'rb') as fh:
1303 1302 bundle = exchange.readbundle(ui, fh, bundlepath)
1304 1303 fn(bundle)
1305 1304
1306 1305 return run
1307 1306
1308 1307 def makereadnbytes(size):
1309 1308 def run():
1310 1309 with open(bundlepath, b'rb') as fh:
1311 1310 bundle = exchange.readbundle(ui, fh, bundlepath)
1312 1311 while bundle.read(size):
1313 1312 pass
1314 1313
1315 1314 return run
1316 1315
1317 1316 def makestdioread(size):
1318 1317 def run():
1319 1318 with open(bundlepath, b'rb') as fh:
1320 1319 while fh.read(size):
1321 1320 pass
1322 1321
1323 1322 return run
1324 1323
1325 1324 # bundle1
1326 1325
1327 1326 def deltaiter(bundle):
1328 1327 for delta in bundle.deltaiter():
1329 1328 pass
1330 1329
1331 1330 def iterchunks(bundle):
1332 1331 for chunk in bundle.getchunks():
1333 1332 pass
1334 1333
1335 1334 # bundle2
1336 1335
1337 1336 def forwardchunks(bundle):
1338 1337 for chunk in bundle._forwardchunks():
1339 1338 pass
1340 1339
1341 1340 def iterparts(bundle):
1342 1341 for part in bundle.iterparts():
1343 1342 pass
1344 1343
1345 1344 def iterpartsseekable(bundle):
1346 1345 for part in bundle.iterparts(seekable=True):
1347 1346 pass
1348 1347
1349 1348 def seek(bundle):
1350 1349 for part in bundle.iterparts(seekable=True):
1351 1350 part.seek(0, os.SEEK_END)
1352 1351
1353 1352 def makepartreadnbytes(size):
1354 1353 def run():
1355 1354 with open(bundlepath, b'rb') as fh:
1356 1355 bundle = exchange.readbundle(ui, fh, bundlepath)
1357 1356 for part in bundle.iterparts():
1358 1357 while part.read(size):
1359 1358 pass
1360 1359
1361 1360 return run
1362 1361
1363 1362 benches = [
1364 1363 (makestdioread(8192), b'read(8k)'),
1365 1364 (makestdioread(16384), b'read(16k)'),
1366 1365 (makestdioread(32768), b'read(32k)'),
1367 1366 (makestdioread(131072), b'read(128k)'),
1368 1367 ]
1369 1368
1370 1369 with open(bundlepath, b'rb') as fh:
1371 1370 bundle = exchange.readbundle(ui, fh, bundlepath)
1372 1371
1373 1372 if isinstance(bundle, changegroup.cg1unpacker):
1374 1373 benches.extend(
1375 1374 [
1376 1375 (makebench(deltaiter), b'cg1 deltaiter()'),
1377 1376 (makebench(iterchunks), b'cg1 getchunks()'),
1378 1377 (makereadnbytes(8192), b'cg1 read(8k)'),
1379 1378 (makereadnbytes(16384), b'cg1 read(16k)'),
1380 1379 (makereadnbytes(32768), b'cg1 read(32k)'),
1381 1380 (makereadnbytes(131072), b'cg1 read(128k)'),
1382 1381 ]
1383 1382 )
1384 1383 elif isinstance(bundle, bundle2.unbundle20):
1385 1384 benches.extend(
1386 1385 [
1387 1386 (makebench(forwardchunks), b'bundle2 forwardchunks()'),
1388 1387 (makebench(iterparts), b'bundle2 iterparts()'),
1389 1388 (
1390 1389 makebench(iterpartsseekable),
1391 1390 b'bundle2 iterparts() seekable',
1392 1391 ),
1393 1392 (makebench(seek), b'bundle2 part seek()'),
1394 1393 (makepartreadnbytes(8192), b'bundle2 part read(8k)'),
1395 1394 (makepartreadnbytes(16384), b'bundle2 part read(16k)'),
1396 1395 (makepartreadnbytes(32768), b'bundle2 part read(32k)'),
1397 1396 (makepartreadnbytes(131072), b'bundle2 part read(128k)'),
1398 1397 ]
1399 1398 )
1400 1399 elif isinstance(bundle, streamclone.streamcloneapplier):
1401 1400 raise error.Abort(b'stream clone bundles not supported')
1402 1401 else:
1403 1402 raise error.Abort(b'unhandled bundle type: %s' % type(bundle))
1404 1403
1405 1404 for fn, title in benches:
1406 1405 timer, fm = gettimer(ui, opts)
1407 1406 timer(fn, title=title)
1408 1407 fm.end()
1409 1408
1410 1409
1411 1410 @command(
1412 1411 b'perf::changegroupchangelog|perfchangegroupchangelog',
1413 1412 formatteropts
1414 1413 + [
1415 1414 (b'', b'cgversion', b'02', b'changegroup version'),
1416 1415 (b'r', b'rev', b'', b'revisions to add to changegroup'),
1417 1416 ],
1418 1417 )
1419 1418 def perfchangegroupchangelog(ui, repo, cgversion=b'02', rev=None, **opts):
1420 1419 """Benchmark producing a changelog group for a changegroup.
1421 1420
1422 1421 This measures the time spent processing the changelog during a
1423 1422 bundle operation. This occurs during `hg bundle` and on a server
1424 1423 processing a `getbundle` wire protocol request (handles clones
1425 1424 and pull requests).
1426 1425
1427 1426 By default, all revisions are added to the changegroup.
1428 1427 """
1429 1428 opts = _byteskwargs(opts)
1430 1429 cl = repo.changelog
1431 1430 nodes = [cl.lookup(r) for r in repo.revs(rev or b'all()')]
1432 1431 bundler = changegroup.getbundler(cgversion, repo)
1433 1432
1434 1433 def d():
1435 1434 state, chunks = bundler._generatechangelog(cl, nodes)
1436 1435 for chunk in chunks:
1437 1436 pass
1438 1437
1439 1438 timer, fm = gettimer(ui, opts)
1440 1439
1441 1440 # Terminal printing can interfere with timing. So disable it.
1442 1441 with ui.configoverride({(b'progress', b'disable'): True}):
1443 1442 timer(d)
1444 1443
1445 1444 fm.end()
1446 1445
1447 1446
1448 1447 @command(b'perf::dirs|perfdirs', formatteropts)
1449 1448 def perfdirs(ui, repo, **opts):
1450 1449 opts = _byteskwargs(opts)
1451 1450 timer, fm = gettimer(ui, opts)
1452 1451 dirstate = repo.dirstate
1453 1452 b'a' in dirstate
1454 1453
1455 1454 def d():
1456 1455 dirstate.hasdir(b'a')
1457 1456 try:
1458 1457 del dirstate._map._dirs
1459 1458 except AttributeError:
1460 1459 pass
1461 1460
1462 1461 timer(d)
1463 1462 fm.end()
1464 1463
1465 1464
1466 1465 @command(
1467 1466 b'perf::dirstate|perfdirstate',
1468 1467 [
1469 1468 (
1470 1469 b'',
1471 1470 b'iteration',
1472 1471 None,
1473 1472 b'benchmark a full iteration for the dirstate',
1474 1473 ),
1475 1474 (
1476 1475 b'',
1477 1476 b'contains',
1478 1477 None,
1479 1478 b'benchmark a large amount of `nf in dirstate` calls',
1480 1479 ),
1481 1480 ]
1482 1481 + formatteropts,
1483 1482 )
1484 1483 def perfdirstate(ui, repo, **opts):
1485 1484 """benchmap the time of various distate operations
1486 1485
1487 1486 By default benchmark the time necessary to load a dirstate from scratch.
1488 1487 The dirstate is loaded to the point were a "contains" request can be
1489 1488 answered.
1490 1489 """
1491 1490 opts = _byteskwargs(opts)
1492 1491 timer, fm = gettimer(ui, opts)
1493 1492 b"a" in repo.dirstate
1494 1493
1495 1494 if opts[b'iteration'] and opts[b'contains']:
1496 1495 msg = b'only specify one of --iteration or --contains'
1497 1496 raise error.Abort(msg)
1498 1497
1499 1498 if opts[b'iteration']:
1500 1499 setup = None
1501 1500 dirstate = repo.dirstate
1502 1501
1503 1502 def d():
1504 1503 for f in dirstate:
1505 1504 pass
1506 1505
1507 1506 elif opts[b'contains']:
1508 1507 setup = None
1509 1508 dirstate = repo.dirstate
1510 1509 allfiles = list(dirstate)
1511 1510 # also add file path that will be "missing" from the dirstate
1512 1511 allfiles.extend([f[::-1] for f in allfiles])
1513 1512
1514 1513 def d():
1515 1514 for f in allfiles:
1516 1515 f in dirstate
1517 1516
1518 1517 else:
1519 1518
1520 1519 def setup():
1521 1520 repo.dirstate.invalidate()
1522 1521
1523 1522 def d():
1524 1523 b"a" in repo.dirstate
1525 1524
1526 1525 timer(d, setup=setup)
1527 1526 fm.end()
1528 1527
1529 1528
1530 1529 @command(b'perf::dirstatedirs|perfdirstatedirs', formatteropts)
1531 1530 def perfdirstatedirs(ui, repo, **opts):
1532 1531 """benchmap a 'dirstate.hasdir' call from an empty `dirs` cache"""
1533 1532 opts = _byteskwargs(opts)
1534 1533 timer, fm = gettimer(ui, opts)
1535 1534 repo.dirstate.hasdir(b"a")
1536 1535
1537 1536 def setup():
1538 1537 try:
1539 1538 del repo.dirstate._map._dirs
1540 1539 except AttributeError:
1541 1540 pass
1542 1541
1543 1542 def d():
1544 1543 repo.dirstate.hasdir(b"a")
1545 1544
1546 1545 timer(d, setup=setup)
1547 1546 fm.end()
1548 1547
1549 1548
1550 1549 @command(b'perf::dirstatefoldmap|perfdirstatefoldmap', formatteropts)
1551 1550 def perfdirstatefoldmap(ui, repo, **opts):
1552 1551 """benchmap a `dirstate._map.filefoldmap.get()` request
1553 1552
1554 1553 The dirstate filefoldmap cache is dropped between every request.
1555 1554 """
1556 1555 opts = _byteskwargs(opts)
1557 1556 timer, fm = gettimer(ui, opts)
1558 1557 dirstate = repo.dirstate
1559 1558 dirstate._map.filefoldmap.get(b'a')
1560 1559
1561 1560 def setup():
1562 1561 del dirstate._map.filefoldmap
1563 1562
1564 1563 def d():
1565 1564 dirstate._map.filefoldmap.get(b'a')
1566 1565
1567 1566 timer(d, setup=setup)
1568 1567 fm.end()
1569 1568
1570 1569
1571 1570 @command(b'perf::dirfoldmap|perfdirfoldmap', formatteropts)
1572 1571 def perfdirfoldmap(ui, repo, **opts):
1573 1572 """benchmap a `dirstate._map.dirfoldmap.get()` request
1574 1573
1575 1574 The dirstate dirfoldmap cache is dropped between every request.
1576 1575 """
1577 1576 opts = _byteskwargs(opts)
1578 1577 timer, fm = gettimer(ui, opts)
1579 1578 dirstate = repo.dirstate
1580 1579 dirstate._map.dirfoldmap.get(b'a')
1581 1580
1582 1581 def setup():
1583 1582 del dirstate._map.dirfoldmap
1584 1583 try:
1585 1584 del dirstate._map._dirs
1586 1585 except AttributeError:
1587 1586 pass
1588 1587
1589 1588 def d():
1590 1589 dirstate._map.dirfoldmap.get(b'a')
1591 1590
1592 1591 timer(d, setup=setup)
1593 1592 fm.end()
1594 1593
1595 1594
1596 1595 @command(b'perf::dirstatewrite|perfdirstatewrite', formatteropts)
1597 1596 def perfdirstatewrite(ui, repo, **opts):
1598 1597 """benchmap the time it take to write a dirstate on disk"""
1599 1598 opts = _byteskwargs(opts)
1600 1599 timer, fm = gettimer(ui, opts)
1601 1600 ds = repo.dirstate
1602 1601 b"a" in ds
1603 1602
1604 1603 def setup():
1605 1604 ds._dirty = True
1606 1605
1607 1606 def d():
1608 1607 ds.write(repo.currenttransaction())
1609 1608
1610 1609 with repo.wlock():
1611 1610 timer(d, setup=setup)
1612 1611 fm.end()
1613 1612
1614 1613
1615 1614 def _getmergerevs(repo, opts):
1616 1615 """parse command argument to return rev involved in merge
1617 1616
1618 1617 input: options dictionnary with `rev`, `from` and `bse`
1619 1618 output: (localctx, otherctx, basectx)
1620 1619 """
1621 1620 if opts[b'from']:
1622 1621 fromrev = scmutil.revsingle(repo, opts[b'from'])
1623 1622 wctx = repo[fromrev]
1624 1623 else:
1625 1624 wctx = repo[None]
1626 1625 # we don't want working dir files to be stat'd in the benchmark, so
1627 1626 # prime that cache
1628 1627 wctx.dirty()
1629 1628 rctx = scmutil.revsingle(repo, opts[b'rev'], opts[b'rev'])
1630 1629 if opts[b'base']:
1631 1630 fromrev = scmutil.revsingle(repo, opts[b'base'])
1632 1631 ancestor = repo[fromrev]
1633 1632 else:
1634 1633 ancestor = wctx.ancestor(rctx)
1635 1634 return (wctx, rctx, ancestor)
1636 1635
1637 1636
1638 1637 @command(
1639 1638 b'perf::mergecalculate|perfmergecalculate',
1640 1639 [
1641 1640 (b'r', b'rev', b'.', b'rev to merge against'),
1642 1641 (b'', b'from', b'', b'rev to merge from'),
1643 1642 (b'', b'base', b'', b'the revision to use as base'),
1644 1643 ]
1645 1644 + formatteropts,
1646 1645 )
1647 1646 def perfmergecalculate(ui, repo, **opts):
1648 1647 opts = _byteskwargs(opts)
1649 1648 timer, fm = gettimer(ui, opts)
1650 1649
1651 1650 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1652 1651
1653 1652 def d():
1654 1653 # acceptremote is True because we don't want prompts in the middle of
1655 1654 # our benchmark
1656 1655 merge.calculateupdates(
1657 1656 repo,
1658 1657 wctx,
1659 1658 rctx,
1660 1659 [ancestor],
1661 1660 branchmerge=False,
1662 1661 force=False,
1663 1662 acceptremote=True,
1664 1663 followcopies=True,
1665 1664 )
1666 1665
1667 1666 timer(d)
1668 1667 fm.end()
1669 1668
1670 1669
1671 1670 @command(
1672 1671 b'perf::mergecopies|perfmergecopies',
1673 1672 [
1674 1673 (b'r', b'rev', b'.', b'rev to merge against'),
1675 1674 (b'', b'from', b'', b'rev to merge from'),
1676 1675 (b'', b'base', b'', b'the revision to use as base'),
1677 1676 ]
1678 1677 + formatteropts,
1679 1678 )
1680 1679 def perfmergecopies(ui, repo, **opts):
1681 1680 """measure runtime of `copies.mergecopies`"""
1682 1681 opts = _byteskwargs(opts)
1683 1682 timer, fm = gettimer(ui, opts)
1684 1683 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1685 1684
1686 1685 def d():
1687 1686 # acceptremote is True because we don't want prompts in the middle of
1688 1687 # our benchmark
1689 1688 copies.mergecopies(repo, wctx, rctx, ancestor)
1690 1689
1691 1690 timer(d)
1692 1691 fm.end()
1693 1692
1694 1693
1695 1694 @command(b'perf::pathcopies|perfpathcopies', [], b"REV REV")
1696 1695 def perfpathcopies(ui, repo, rev1, rev2, **opts):
1697 1696 """benchmark the copy tracing logic"""
1698 1697 opts = _byteskwargs(opts)
1699 1698 timer, fm = gettimer(ui, opts)
1700 1699 ctx1 = scmutil.revsingle(repo, rev1, rev1)
1701 1700 ctx2 = scmutil.revsingle(repo, rev2, rev2)
1702 1701
1703 1702 def d():
1704 1703 copies.pathcopies(ctx1, ctx2)
1705 1704
1706 1705 timer(d)
1707 1706 fm.end()
1708 1707
1709 1708
1710 1709 @command(
1711 1710 b'perf::phases|perfphases',
1712 1711 [
1713 1712 (b'', b'full', False, b'include file reading time too'),
1714 1713 ],
1715 1714 b"",
1716 1715 )
1717 1716 def perfphases(ui, repo, **opts):
1718 1717 """benchmark phasesets computation"""
1719 1718 opts = _byteskwargs(opts)
1720 1719 timer, fm = gettimer(ui, opts)
1721 1720 _phases = repo._phasecache
1722 1721 full = opts.get(b'full')
1723 1722
1724 1723 def d():
1725 1724 phases = _phases
1726 1725 if full:
1727 1726 clearfilecache(repo, b'_phasecache')
1728 1727 phases = repo._phasecache
1729 1728 phases.invalidate()
1730 1729 phases.loadphaserevs(repo)
1731 1730
1732 1731 timer(d)
1733 1732 fm.end()
1734 1733
1735 1734
1736 1735 @command(b'perf::phasesremote|perfphasesremote', [], b"[DEST]")
1737 1736 def perfphasesremote(ui, repo, dest=None, **opts):
1738 1737 """benchmark time needed to analyse phases of the remote server"""
1739 1738 from mercurial.node import bin
1740 1739 from mercurial import (
1741 1740 exchange,
1742 1741 hg,
1743 1742 phases,
1744 1743 )
1745 1744
1746 1745 opts = _byteskwargs(opts)
1747 1746 timer, fm = gettimer(ui, opts)
1748 1747
1749 1748 path = ui.getpath(dest, default=(b'default-push', b'default'))
1750 1749 if not path:
1751 1750 raise error.Abort(
1752 1751 b'default repository not configured!',
1753 1752 hint=b"see 'hg help config.paths'",
1754 1753 )
1755 1754 if util.safehasattr(path, 'main_path'):
1756 1755 path = path.get_push_variant()
1757 1756 dest = path.loc
1758 1757 else:
1759 1758 dest = path.pushloc or path.loc
1760 1759 ui.statusnoi18n(b'analysing phase of %s\n' % util.hidepassword(dest))
1761 1760 other = hg.peer(repo, opts, dest)
1762 1761
1763 1762 # easier to perform discovery through the operation
1764 1763 op = exchange.pushoperation(repo, other)
1765 1764 exchange._pushdiscoverychangeset(op)
1766 1765
1767 1766 remotesubset = op.fallbackheads
1768 1767
1769 1768 with other.commandexecutor() as e:
1770 1769 remotephases = e.callcommand(
1771 1770 b'listkeys', {b'namespace': b'phases'}
1772 1771 ).result()
1773 1772 del other
1774 1773 publishing = remotephases.get(b'publishing', False)
1775 1774 if publishing:
1776 1775 ui.statusnoi18n(b'publishing: yes\n')
1777 1776 else:
1778 1777 ui.statusnoi18n(b'publishing: no\n')
1779 1778
1780 1779 has_node = getattr(repo.changelog.index, 'has_node', None)
1781 1780 if has_node is None:
1782 1781 has_node = repo.changelog.nodemap.__contains__
1783 1782 nonpublishroots = 0
1784 1783 for nhex, phase in remotephases.iteritems():
1785 1784 if nhex == b'publishing': # ignore data related to publish option
1786 1785 continue
1787 1786 node = bin(nhex)
1788 1787 if has_node(node) and int(phase):
1789 1788 nonpublishroots += 1
1790 1789 ui.statusnoi18n(b'number of roots: %d\n' % len(remotephases))
1791 1790 ui.statusnoi18n(b'number of known non public roots: %d\n' % nonpublishroots)
1792 1791
1793 1792 def d():
1794 1793 phases.remotephasessummary(repo, remotesubset, remotephases)
1795 1794
1796 1795 timer(d)
1797 1796 fm.end()
1798 1797
1799 1798
1800 1799 @command(
1801 1800 b'perf::manifest|perfmanifest',
1802 1801 [
1803 1802 (b'm', b'manifest-rev', False, b'Look up a manifest node revision'),
1804 1803 (b'', b'clear-disk', False, b'clear on-disk caches too'),
1805 1804 ]
1806 1805 + formatteropts,
1807 1806 b'REV|NODE',
1808 1807 )
1809 1808 def perfmanifest(ui, repo, rev, manifest_rev=False, clear_disk=False, **opts):
1810 1809 """benchmark the time to read a manifest from disk and return a usable
1811 1810 dict-like object
1812 1811
1813 1812 Manifest caches are cleared before retrieval."""
1814 1813 opts = _byteskwargs(opts)
1815 1814 timer, fm = gettimer(ui, opts)
1816 1815 if not manifest_rev:
1817 1816 ctx = scmutil.revsingle(repo, rev, rev)
1818 1817 t = ctx.manifestnode()
1819 1818 else:
1820 1819 from mercurial.node import bin
1821 1820
1822 1821 if len(rev) == 40:
1823 1822 t = bin(rev)
1824 1823 else:
1825 1824 try:
1826 1825 rev = int(rev)
1827 1826
1828 1827 if util.safehasattr(repo.manifestlog, b'getstorage'):
1829 1828 t = repo.manifestlog.getstorage(b'').node(rev)
1830 1829 else:
1831 1830 t = repo.manifestlog._revlog.lookup(rev)
1832 1831 except ValueError:
1833 1832 raise error.Abort(
1834 1833 b'manifest revision must be integer or full node'
1835 1834 )
1836 1835
1837 1836 def d():
1838 1837 repo.manifestlog.clearcaches(clear_persisted_data=clear_disk)
1839 1838 repo.manifestlog[t].read()
1840 1839
1841 1840 timer(d)
1842 1841 fm.end()
1843 1842
1844 1843
1845 1844 @command(b'perf::changeset|perfchangeset', formatteropts)
1846 1845 def perfchangeset(ui, repo, rev, **opts):
1847 1846 opts = _byteskwargs(opts)
1848 1847 timer, fm = gettimer(ui, opts)
1849 1848 n = scmutil.revsingle(repo, rev).node()
1850 1849
1851 1850 def d():
1852 1851 repo.changelog.read(n)
1853 1852 # repo.changelog._cache = None
1854 1853
1855 1854 timer(d)
1856 1855 fm.end()
1857 1856
1858 1857
1859 1858 @command(b'perf::ignore|perfignore', formatteropts)
1860 1859 def perfignore(ui, repo, **opts):
1861 1860 """benchmark operation related to computing ignore"""
1862 1861 opts = _byteskwargs(opts)
1863 1862 timer, fm = gettimer(ui, opts)
1864 1863 dirstate = repo.dirstate
1865 1864
1866 1865 def setupone():
1867 1866 dirstate.invalidate()
1868 1867 clearfilecache(dirstate, b'_ignore')
1869 1868
1870 1869 def runone():
1871 1870 dirstate._ignore
1872 1871
1873 1872 timer(runone, setup=setupone, title=b"load")
1874 1873 fm.end()
1875 1874
1876 1875
1877 1876 @command(
1878 1877 b'perf::index|perfindex',
1879 1878 [
1880 1879 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1881 1880 (b'', b'no-lookup', None, b'do not revision lookup post creation'),
1882 1881 ]
1883 1882 + formatteropts,
1884 1883 )
1885 1884 def perfindex(ui, repo, **opts):
1886 1885 """benchmark index creation time followed by a lookup
1887 1886
1888 1887 The default is to look `tip` up. Depending on the index implementation,
1889 1888 the revision looked up can matters. For example, an implementation
1890 1889 scanning the index will have a faster lookup time for `--rev tip` than for
1891 1890 `--rev 0`. The number of looked up revisions and their order can also
1892 1891 matters.
1893 1892
1894 1893 Example of useful set to test:
1895 1894
1896 1895 * tip
1897 1896 * 0
1898 1897 * -10:
1899 1898 * :10
1900 1899 * -10: + :10
1901 1900 * :10: + -10:
1902 1901 * -10000:
1903 1902 * -10000: + 0
1904 1903
1905 1904 It is not currently possible to check for lookup of a missing node. For
1906 1905 deeper lookup benchmarking, checkout the `perfnodemap` command."""
1907 1906 import mercurial.revlog
1908 1907
1909 1908 opts = _byteskwargs(opts)
1910 1909 timer, fm = gettimer(ui, opts)
1911 1910 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1912 1911 if opts[b'no_lookup']:
1913 1912 if opts['rev']:
1914 1913 raise error.Abort('--no-lookup and --rev are mutually exclusive')
1915 1914 nodes = []
1916 1915 elif not opts[b'rev']:
1917 1916 nodes = [repo[b"tip"].node()]
1918 1917 else:
1919 1918 revs = scmutil.revrange(repo, opts[b'rev'])
1920 1919 cl = repo.changelog
1921 1920 nodes = [cl.node(r) for r in revs]
1922 1921
1923 1922 unfi = repo.unfiltered()
1924 1923 # find the filecache func directly
1925 1924 # This avoid polluting the benchmark with the filecache logic
1926 1925 makecl = unfi.__class__.changelog.func
1927 1926
1928 1927 def setup():
1929 1928 # probably not necessary, but for good measure
1930 1929 clearchangelog(unfi)
1931 1930
1932 1931 def d():
1933 1932 cl = makecl(unfi)
1934 1933 for n in nodes:
1935 1934 cl.rev(n)
1936 1935
1937 1936 timer(d, setup=setup)
1938 1937 fm.end()
1939 1938
1940 1939
1941 1940 @command(
1942 1941 b'perf::nodemap|perfnodemap',
1943 1942 [
1944 1943 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1945 1944 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
1946 1945 ]
1947 1946 + formatteropts,
1948 1947 )
1949 1948 def perfnodemap(ui, repo, **opts):
1950 1949 """benchmark the time necessary to look up revision from a cold nodemap
1951 1950
1952 1951 Depending on the implementation, the amount and order of revision we look
1953 1952 up can varies. Example of useful set to test:
1954 1953 * tip
1955 1954 * 0
1956 1955 * -10:
1957 1956 * :10
1958 1957 * -10: + :10
1959 1958 * :10: + -10:
1960 1959 * -10000:
1961 1960 * -10000: + 0
1962 1961
1963 1962 The command currently focus on valid binary lookup. Benchmarking for
1964 1963 hexlookup, prefix lookup and missing lookup would also be valuable.
1965 1964 """
1966 1965 import mercurial.revlog
1967 1966
1968 1967 opts = _byteskwargs(opts)
1969 1968 timer, fm = gettimer(ui, opts)
1970 1969 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1971 1970
1972 1971 unfi = repo.unfiltered()
1973 1972 clearcaches = opts[b'clear_caches']
1974 1973 # find the filecache func directly
1975 1974 # This avoid polluting the benchmark with the filecache logic
1976 1975 makecl = unfi.__class__.changelog.func
1977 1976 if not opts[b'rev']:
1978 1977 raise error.Abort(b'use --rev to specify revisions to look up')
1979 1978 revs = scmutil.revrange(repo, opts[b'rev'])
1980 1979 cl = repo.changelog
1981 1980 nodes = [cl.node(r) for r in revs]
1982 1981
1983 1982 # use a list to pass reference to a nodemap from one closure to the next
1984 1983 nodeget = [None]
1985 1984
1986 1985 def setnodeget():
1987 1986 # probably not necessary, but for good measure
1988 1987 clearchangelog(unfi)
1989 1988 cl = makecl(unfi)
1990 1989 if util.safehasattr(cl.index, 'get_rev'):
1991 1990 nodeget[0] = cl.index.get_rev
1992 1991 else:
1993 1992 nodeget[0] = cl.nodemap.get
1994 1993
1995 1994 def d():
1996 1995 get = nodeget[0]
1997 1996 for n in nodes:
1998 1997 get(n)
1999 1998
2000 1999 setup = None
2001 2000 if clearcaches:
2002 2001
2003 2002 def setup():
2004 2003 setnodeget()
2005 2004
2006 2005 else:
2007 2006 setnodeget()
2008 2007 d() # prewarm the data structure
2009 2008 timer(d, setup=setup)
2010 2009 fm.end()
2011 2010
2012 2011
2013 2012 @command(b'perf::startup|perfstartup', formatteropts)
2014 2013 def perfstartup(ui, repo, **opts):
2015 2014 opts = _byteskwargs(opts)
2016 2015 timer, fm = gettimer(ui, opts)
2017 2016
2018 2017 def d():
2019 2018 if os.name != 'nt':
2020 2019 os.system(
2021 2020 b"HGRCPATH= %s version -q > /dev/null" % fsencode(sys.argv[0])
2022 2021 )
2023 2022 else:
2024 2023 os.environ['HGRCPATH'] = r' '
2025 2024 os.system("%s version -q > NUL" % sys.argv[0])
2026 2025
2027 2026 timer(d)
2028 2027 fm.end()
2029 2028
2030 2029
2031 2030 def _find_stream_generator(version):
2032 2031 """find the proper generator function for this stream version"""
2033 2032 import mercurial.streamclone
2034 2033
2035 2034 available = {}
2036 2035
2037 2036 # try to fetch a v1 generator
2038 2037 generatev1 = getattr(mercurial.streamclone, "generatev1", None)
2039 2038 if generatev1 is not None:
2040 2039
2041 2040 def generate(repo):
2042 2041 entries, bytes, data = generatev2(repo, None, None, True)
2043 2042 return data
2044 2043
2045 2044 available[b'v1'] = generatev1
2046 2045 # try to fetch a v2 generator
2047 2046 generatev2 = getattr(mercurial.streamclone, "generatev2", None)
2048 2047 if generatev2 is not None:
2049 2048
2050 2049 def generate(repo):
2051 2050 entries, bytes, data = generatev2(repo, None, None, True)
2052 2051 return data
2053 2052
2054 2053 available[b'v2'] = generate
2055 2054 # try to fetch a v3 generator
2056 2055 generatev3 = getattr(mercurial.streamclone, "generatev3", None)
2057 2056 if generatev3 is not None:
2058 2057
2059 2058 def generate(repo):
2060 2059 entries, bytes, data = generatev3(repo, None, None, True)
2061 2060 return data
2062 2061
2063 2062 available[b'v3-exp'] = generate
2064 2063
2065 2064 # resolve the request
2066 2065 if version == b"latest":
2067 2066 # latest is the highest non experimental version
2068 2067 latest_key = max(v for v in available if b'-exp' not in v)
2069 2068 return available[latest_key]
2070 2069 elif version in available:
2071 2070 return available[version]
2072 2071 else:
2073 2072 msg = b"unkown or unavailable version: %s"
2074 2073 msg %= version
2075 2074 hint = b"available versions: %s"
2076 2075 hint %= b', '.join(sorted(available))
2077 2076 raise error.Abort(msg, hint=hint)
2078 2077
2079 2078
2080 2079 @command(
2081 2080 b'perf::stream-locked-section',
2082 2081 [
2083 2082 (
2084 2083 b'',
2085 2084 b'stream-version',
2086 2085 b'latest',
2087 2086 b'stream version to use ("v1", "v2", "v3" or "latest", (the default))',
2088 2087 ),
2089 2088 ]
2090 2089 + formatteropts,
2091 2090 )
2092 2091 def perf_stream_clone_scan(ui, repo, stream_version, **opts):
2093 2092 """benchmark the initial, repo-locked, section of a stream-clone"""
2094 2093
2095 2094 opts = _byteskwargs(opts)
2096 2095 timer, fm = gettimer(ui, opts)
2097 2096
2098 2097 # deletion of the generator may trigger some cleanup that we do not want to
2099 2098 # measure
2100 2099 result_holder = [None]
2101 2100
2102 2101 def setupone():
2103 2102 result_holder[0] = None
2104 2103
2105 2104 generate = _find_stream_generator(stream_version)
2106 2105
2107 2106 def runone():
2108 2107 # the lock is held for the duration the initialisation
2109 2108 result_holder[0] = generate(repo)
2110 2109
2111 2110 timer(runone, setup=setupone, title=b"load")
2112 2111 fm.end()
2113 2112
2114 2113
2115 2114 @command(
2116 2115 b'perf::stream-generate',
2117 2116 [
2118 2117 (
2119 2118 b'',
2120 2119 b'stream-version',
2121 2120 b'latest',
2122 2121 b'stream version to us ("v1", "v2" or "latest", (the default))',
2123 2122 ),
2124 2123 ]
2125 2124 + formatteropts,
2126 2125 )
2127 2126 def perf_stream_clone_generate(ui, repo, stream_version, **opts):
2128 2127 """benchmark the full generation of a stream clone"""
2129 2128
2130 2129 opts = _byteskwargs(opts)
2131 2130 timer, fm = gettimer(ui, opts)
2132 2131
2133 2132 # deletion of the generator may trigger some cleanup that we do not want to
2134 2133 # measure
2135 2134
2136 2135 generate = _find_stream_generator(stream_version)
2137 2136
2138 2137 def runone():
2139 2138 # the lock is held for the duration the initialisation
2140 2139 for chunk in generate(repo):
2141 2140 pass
2142 2141
2143 2142 timer(runone, title=b"generate")
2144 2143 fm.end()
2145 2144
2146 2145
2147 2146 @command(
2148 2147 b'perf::stream-consume',
2149 2148 formatteropts,
2150 2149 )
2151 2150 def perf_stream_clone_consume(ui, repo, filename, **opts):
2152 2151 """benchmark the full application of a stream clone
2153 2152
2154 2153 This include the creation of the repository
2155 2154 """
2156 2155 # try except to appease check code
2157 2156 msg = b"mercurial too old, missing necessary module: %s"
2158 2157 try:
2159 2158 from mercurial import bundle2
2160 2159 except ImportError as exc:
2161 2160 msg %= _bytestr(exc)
2162 2161 raise error.Abort(msg)
2163 2162 try:
2164 2163 from mercurial import exchange
2165 2164 except ImportError as exc:
2166 2165 msg %= _bytestr(exc)
2167 2166 raise error.Abort(msg)
2168 2167 try:
2169 2168 from mercurial import hg
2170 2169 except ImportError as exc:
2171 2170 msg %= _bytestr(exc)
2172 2171 raise error.Abort(msg)
2173 2172 try:
2174 2173 from mercurial import localrepo
2175 2174 except ImportError as exc:
2176 2175 msg %= _bytestr(exc)
2177 2176 raise error.Abort(msg)
2178 2177
2179 2178 opts = _byteskwargs(opts)
2180 2179 timer, fm = gettimer(ui, opts)
2181 2180
2182 2181 # deletion of the generator may trigger some cleanup that we do not want to
2183 2182 # measure
2184 2183 if not (os.path.isfile(filename) and os.access(filename, os.R_OK)):
2185 2184 raise error.Abort("not a readable file: %s" % filename)
2186 2185
2187 2186 run_variables = [None, None]
2188 2187
2189 2188 @contextlib.contextmanager
2190 2189 def context():
2191 2190 with open(filename, mode='rb') as bundle:
2192 2191 with tempfile.TemporaryDirectory() as tmp_dir:
2193 2192 tmp_dir = fsencode(tmp_dir)
2194 2193 run_variables[0] = bundle
2195 2194 run_variables[1] = tmp_dir
2196 2195 yield
2197 2196 run_variables[0] = None
2198 2197 run_variables[1] = None
2199 2198
2200 2199 def runone():
2201 2200 bundle = run_variables[0]
2202 2201 tmp_dir = run_variables[1]
2203 2202 # only pass ui when no srcrepo
2204 2203 localrepo.createrepository(
2205 2204 repo.ui, tmp_dir, requirements=repo.requirements
2206 2205 )
2207 2206 target = hg.repository(repo.ui, tmp_dir)
2208 2207 gen = exchange.readbundle(target.ui, bundle, bundle.name)
2209 2208 # stream v1
2210 2209 if util.safehasattr(gen, 'apply'):
2211 2210 gen.apply(target)
2212 2211 else:
2213 2212 with target.transaction(b"perf::stream-consume") as tr:
2214 2213 bundle2.applybundle(
2215 2214 target,
2216 2215 gen,
2217 2216 tr,
2218 2217 source=b'unbundle',
2219 2218 url=filename,
2220 2219 )
2221 2220
2222 2221 timer(runone, context=context, title=b"consume")
2223 2222 fm.end()
2224 2223
2225 2224
2226 2225 @command(b'perf::parents|perfparents', formatteropts)
2227 2226 def perfparents(ui, repo, **opts):
2228 2227 """benchmark the time necessary to fetch one changeset's parents.
2229 2228
2230 2229 The fetch is done using the `node identifier`, traversing all object layers
2231 2230 from the repository object. The first N revisions will be used for this
2232 2231 benchmark. N is controlled by the ``perf.parentscount`` config option
2233 2232 (default: 1000).
2234 2233 """
2235 2234 opts = _byteskwargs(opts)
2236 2235 timer, fm = gettimer(ui, opts)
2237 2236 # control the number of commits perfparents iterates over
2238 2237 # experimental config: perf.parentscount
2239 2238 count = getint(ui, b"perf", b"parentscount", 1000)
2240 2239 if len(repo.changelog) < count:
2241 2240 raise error.Abort(b"repo needs %d commits for this test" % count)
2242 2241 repo = repo.unfiltered()
2243 2242 nl = [repo.changelog.node(i) for i in _xrange(count)]
2244 2243
2245 2244 def d():
2246 2245 for n in nl:
2247 2246 repo.changelog.parents(n)
2248 2247
2249 2248 timer(d)
2250 2249 fm.end()
2251 2250
2252 2251
2253 2252 @command(b'perf::ctxfiles|perfctxfiles', formatteropts)
2254 2253 def perfctxfiles(ui, repo, x, **opts):
2255 2254 opts = _byteskwargs(opts)
2256 2255 x = int(x)
2257 2256 timer, fm = gettimer(ui, opts)
2258 2257
2259 2258 def d():
2260 2259 len(repo[x].files())
2261 2260
2262 2261 timer(d)
2263 2262 fm.end()
2264 2263
2265 2264
2266 2265 @command(b'perf::rawfiles|perfrawfiles', formatteropts)
2267 2266 def perfrawfiles(ui, repo, x, **opts):
2268 2267 opts = _byteskwargs(opts)
2269 2268 x = int(x)
2270 2269 timer, fm = gettimer(ui, opts)
2271 2270 cl = repo.changelog
2272 2271
2273 2272 def d():
2274 2273 len(cl.read(x)[3])
2275 2274
2276 2275 timer(d)
2277 2276 fm.end()
2278 2277
2279 2278
2280 2279 @command(b'perf::lookup|perflookup', formatteropts)
2281 2280 def perflookup(ui, repo, rev, **opts):
2282 2281 opts = _byteskwargs(opts)
2283 2282 timer, fm = gettimer(ui, opts)
2284 2283 timer(lambda: len(repo.lookup(rev)))
2285 2284 fm.end()
2286 2285
2287 2286
2288 2287 @command(
2289 2288 b'perf::linelogedits|perflinelogedits',
2290 2289 [
2291 2290 (b'n', b'edits', 10000, b'number of edits'),
2292 2291 (b'', b'max-hunk-lines', 10, b'max lines in a hunk'),
2293 2292 ],
2294 2293 norepo=True,
2295 2294 )
2296 2295 def perflinelogedits(ui, **opts):
2297 2296 from mercurial import linelog
2298 2297
2299 2298 opts = _byteskwargs(opts)
2300 2299
2301 2300 edits = opts[b'edits']
2302 2301 maxhunklines = opts[b'max_hunk_lines']
2303 2302
2304 2303 maxb1 = 100000
2305 2304 random.seed(0)
2306 2305 randint = random.randint
2307 2306 currentlines = 0
2308 2307 arglist = []
2309 2308 for rev in _xrange(edits):
2310 2309 a1 = randint(0, currentlines)
2311 2310 a2 = randint(a1, min(currentlines, a1 + maxhunklines))
2312 2311 b1 = randint(0, maxb1)
2313 2312 b2 = randint(b1, b1 + maxhunklines)
2314 2313 currentlines += (b2 - b1) - (a2 - a1)
2315 2314 arglist.append((rev, a1, a2, b1, b2))
2316 2315
2317 2316 def d():
2318 2317 ll = linelog.linelog()
2319 2318 for args in arglist:
2320 2319 ll.replacelines(*args)
2321 2320
2322 2321 timer, fm = gettimer(ui, opts)
2323 2322 timer(d)
2324 2323 fm.end()
2325 2324
2326 2325
2327 2326 @command(b'perf::revrange|perfrevrange', formatteropts)
2328 2327 def perfrevrange(ui, repo, *specs, **opts):
2329 2328 opts = _byteskwargs(opts)
2330 2329 timer, fm = gettimer(ui, opts)
2331 2330 revrange = scmutil.revrange
2332 2331 timer(lambda: len(revrange(repo, specs)))
2333 2332 fm.end()
2334 2333
2335 2334
2336 2335 @command(b'perf::nodelookup|perfnodelookup', formatteropts)
2337 2336 def perfnodelookup(ui, repo, rev, **opts):
2338 2337 opts = _byteskwargs(opts)
2339 2338 timer, fm = gettimer(ui, opts)
2340 2339 import mercurial.revlog
2341 2340
2342 2341 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
2343 2342 n = scmutil.revsingle(repo, rev).node()
2344 2343
2345 2344 try:
2346 2345 cl = revlog(getsvfs(repo), radix=b"00changelog")
2347 2346 except TypeError:
2348 2347 cl = revlog(getsvfs(repo), indexfile=b"00changelog.i")
2349 2348
2350 2349 def d():
2351 2350 cl.rev(n)
2352 2351 clearcaches(cl)
2353 2352
2354 2353 timer(d)
2355 2354 fm.end()
2356 2355
2357 2356
2358 2357 @command(
2359 2358 b'perf::log|perflog',
2360 2359 [(b'', b'rename', False, b'ask log to follow renames')] + formatteropts,
2361 2360 )
2362 2361 def perflog(ui, repo, rev=None, **opts):
2363 2362 opts = _byteskwargs(opts)
2364 2363 if rev is None:
2365 2364 rev = []
2366 2365 timer, fm = gettimer(ui, opts)
2367 2366 ui.pushbuffer()
2368 2367 timer(
2369 2368 lambda: commands.log(
2370 2369 ui, repo, rev=rev, date=b'', user=b'', copies=opts.get(b'rename')
2371 2370 )
2372 2371 )
2373 2372 ui.popbuffer()
2374 2373 fm.end()
2375 2374
2376 2375
2377 2376 @command(b'perf::moonwalk|perfmoonwalk', formatteropts)
2378 2377 def perfmoonwalk(ui, repo, **opts):
2379 2378 """benchmark walking the changelog backwards
2380 2379
2381 2380 This also loads the changelog data for each revision in the changelog.
2382 2381 """
2383 2382 opts = _byteskwargs(opts)
2384 2383 timer, fm = gettimer(ui, opts)
2385 2384
2386 2385 def moonwalk():
2387 2386 for i in repo.changelog.revs(start=(len(repo) - 1), stop=-1):
2388 2387 ctx = repo[i]
2389 2388 ctx.branch() # read changelog data (in addition to the index)
2390 2389
2391 2390 timer(moonwalk)
2392 2391 fm.end()
2393 2392
2394 2393
2395 2394 @command(
2396 2395 b'perf::templating|perftemplating',
2397 2396 [
2398 2397 (b'r', b'rev', [], b'revisions to run the template on'),
2399 2398 ]
2400 2399 + formatteropts,
2401 2400 )
2402 2401 def perftemplating(ui, repo, testedtemplate=None, **opts):
2403 2402 """test the rendering time of a given template"""
2404 2403 if makelogtemplater is None:
2405 2404 raise error.Abort(
2406 2405 b"perftemplating not available with this Mercurial",
2407 2406 hint=b"use 4.3 or later",
2408 2407 )
2409 2408
2410 2409 opts = _byteskwargs(opts)
2411 2410
2412 2411 nullui = ui.copy()
2413 2412 nullui.fout = open(os.devnull, 'wb')
2414 2413 nullui.disablepager()
2415 2414 revs = opts.get(b'rev')
2416 2415 if not revs:
2417 2416 revs = [b'all()']
2418 2417 revs = list(scmutil.revrange(repo, revs))
2419 2418
2420 2419 defaulttemplate = (
2421 2420 b'{date|shortdate} [{rev}:{node|short}]'
2422 2421 b' {author|person}: {desc|firstline}\n'
2423 2422 )
2424 2423 if testedtemplate is None:
2425 2424 testedtemplate = defaulttemplate
2426 2425 displayer = makelogtemplater(nullui, repo, testedtemplate)
2427 2426
2428 2427 def format():
2429 2428 for r in revs:
2430 2429 ctx = repo[r]
2431 2430 displayer.show(ctx)
2432 2431 displayer.flush(ctx)
2433 2432
2434 2433 timer, fm = gettimer(ui, opts)
2435 2434 timer(format)
2436 2435 fm.end()
2437 2436
2438 2437
2439 2438 def _displaystats(ui, opts, entries, data):
2440 2439 # use a second formatter because the data are quite different, not sure
2441 2440 # how it flies with the templater.
2442 2441 fm = ui.formatter(b'perf-stats', opts)
2443 2442 for key, title in entries:
2444 2443 values = data[key]
2445 2444 nbvalues = len(data)
2446 2445 values.sort()
2447 2446 stats = {
2448 2447 'key': key,
2449 2448 'title': title,
2450 2449 'nbitems': len(values),
2451 2450 'min': values[0][0],
2452 2451 '10%': values[(nbvalues * 10) // 100][0],
2453 2452 '25%': values[(nbvalues * 25) // 100][0],
2454 2453 '50%': values[(nbvalues * 50) // 100][0],
2455 2454 '75%': values[(nbvalues * 75) // 100][0],
2456 2455 '80%': values[(nbvalues * 80) // 100][0],
2457 2456 '85%': values[(nbvalues * 85) // 100][0],
2458 2457 '90%': values[(nbvalues * 90) // 100][0],
2459 2458 '95%': values[(nbvalues * 95) // 100][0],
2460 2459 '99%': values[(nbvalues * 99) // 100][0],
2461 2460 'max': values[-1][0],
2462 2461 }
2463 2462 fm.startitem()
2464 2463 fm.data(**stats)
2465 2464 # make node pretty for the human output
2466 2465 fm.plain('### %s (%d items)\n' % (title, len(values)))
2467 2466 lines = [
2468 2467 'min',
2469 2468 '10%',
2470 2469 '25%',
2471 2470 '50%',
2472 2471 '75%',
2473 2472 '80%',
2474 2473 '85%',
2475 2474 '90%',
2476 2475 '95%',
2477 2476 '99%',
2478 2477 'max',
2479 2478 ]
2480 2479 for l in lines:
2481 2480 fm.plain('%s: %s\n' % (l, stats[l]))
2482 2481 fm.end()
2483 2482
2484 2483
2485 2484 @command(
2486 2485 b'perf::helper-mergecopies|perfhelper-mergecopies',
2487 2486 formatteropts
2488 2487 + [
2489 2488 (b'r', b'revs', [], b'restrict search to these revisions'),
2490 2489 (b'', b'timing', False, b'provides extra data (costly)'),
2491 2490 (b'', b'stats', False, b'provides statistic about the measured data'),
2492 2491 ],
2493 2492 )
2494 2493 def perfhelpermergecopies(ui, repo, revs=[], **opts):
2495 2494 """find statistics about potential parameters for `perfmergecopies`
2496 2495
2497 2496 This command find (base, p1, p2) triplet relevant for copytracing
2498 2497 benchmarking in the context of a merge. It reports values for some of the
2499 2498 parameters that impact merge copy tracing time during merge.
2500 2499
2501 2500 If `--timing` is set, rename detection is run and the associated timing
2502 2501 will be reported. The extra details come at the cost of slower command
2503 2502 execution.
2504 2503
2505 2504 Since rename detection is only run once, other factors might easily
2506 2505 affect the precision of the timing. However it should give a good
2507 2506 approximation of which revision triplets are very costly.
2508 2507 """
2509 2508 opts = _byteskwargs(opts)
2510 2509 fm = ui.formatter(b'perf', opts)
2511 2510 dotiming = opts[b'timing']
2512 2511 dostats = opts[b'stats']
2513 2512
2514 2513 output_template = [
2515 2514 ("base", "%(base)12s"),
2516 2515 ("p1", "%(p1.node)12s"),
2517 2516 ("p2", "%(p2.node)12s"),
2518 2517 ("p1.nb-revs", "%(p1.nbrevs)12d"),
2519 2518 ("p1.nb-files", "%(p1.nbmissingfiles)12d"),
2520 2519 ("p1.renames", "%(p1.renamedfiles)12d"),
2521 2520 ("p1.time", "%(p1.time)12.3f"),
2522 2521 ("p2.nb-revs", "%(p2.nbrevs)12d"),
2523 2522 ("p2.nb-files", "%(p2.nbmissingfiles)12d"),
2524 2523 ("p2.renames", "%(p2.renamedfiles)12d"),
2525 2524 ("p2.time", "%(p2.time)12.3f"),
2526 2525 ("renames", "%(nbrenamedfiles)12d"),
2527 2526 ("total.time", "%(time)12.3f"),
2528 2527 ]
2529 2528 if not dotiming:
2530 2529 output_template = [
2531 2530 i
2532 2531 for i in output_template
2533 2532 if not ('time' in i[0] or 'renames' in i[0])
2534 2533 ]
2535 2534 header_names = [h for (h, v) in output_template]
2536 2535 output = ' '.join([v for (h, v) in output_template]) + '\n'
2537 2536 header = ' '.join(['%12s'] * len(header_names)) + '\n'
2538 2537 fm.plain(header % tuple(header_names))
2539 2538
2540 2539 if not revs:
2541 2540 revs = ['all()']
2542 2541 revs = scmutil.revrange(repo, revs)
2543 2542
2544 2543 if dostats:
2545 2544 alldata = {
2546 2545 'nbrevs': [],
2547 2546 'nbmissingfiles': [],
2548 2547 }
2549 2548 if dotiming:
2550 2549 alldata['parentnbrenames'] = []
2551 2550 alldata['totalnbrenames'] = []
2552 2551 alldata['parenttime'] = []
2553 2552 alldata['totaltime'] = []
2554 2553
2555 2554 roi = repo.revs('merge() and %ld', revs)
2556 2555 for r in roi:
2557 2556 ctx = repo[r]
2558 2557 p1 = ctx.p1()
2559 2558 p2 = ctx.p2()
2560 2559 bases = repo.changelog._commonancestorsheads(p1.rev(), p2.rev())
2561 2560 for b in bases:
2562 2561 b = repo[b]
2563 2562 p1missing = copies._computeforwardmissing(b, p1)
2564 2563 p2missing = copies._computeforwardmissing(b, p2)
2565 2564 data = {
2566 2565 b'base': b.hex(),
2567 2566 b'p1.node': p1.hex(),
2568 2567 b'p1.nbrevs': len(repo.revs('only(%d, %d)', p1.rev(), b.rev())),
2569 2568 b'p1.nbmissingfiles': len(p1missing),
2570 2569 b'p2.node': p2.hex(),
2571 2570 b'p2.nbrevs': len(repo.revs('only(%d, %d)', p2.rev(), b.rev())),
2572 2571 b'p2.nbmissingfiles': len(p2missing),
2573 2572 }
2574 2573 if dostats:
2575 2574 if p1missing:
2576 2575 alldata['nbrevs'].append(
2577 2576 (data['p1.nbrevs'], b.hex(), p1.hex())
2578 2577 )
2579 2578 alldata['nbmissingfiles'].append(
2580 2579 (data['p1.nbmissingfiles'], b.hex(), p1.hex())
2581 2580 )
2582 2581 if p2missing:
2583 2582 alldata['nbrevs'].append(
2584 2583 (data['p2.nbrevs'], b.hex(), p2.hex())
2585 2584 )
2586 2585 alldata['nbmissingfiles'].append(
2587 2586 (data['p2.nbmissingfiles'], b.hex(), p2.hex())
2588 2587 )
2589 2588 if dotiming:
2590 2589 begin = util.timer()
2591 2590 mergedata = copies.mergecopies(repo, p1, p2, b)
2592 2591 end = util.timer()
2593 2592 # not very stable timing since we did only one run
2594 2593 data['time'] = end - begin
2595 2594 # mergedata contains five dicts: "copy", "movewithdir",
2596 2595 # "diverge", "renamedelete" and "dirmove".
2597 2596 # The first 4 are about renamed file so lets count that.
2598 2597 renames = len(mergedata[0])
2599 2598 renames += len(mergedata[1])
2600 2599 renames += len(mergedata[2])
2601 2600 renames += len(mergedata[3])
2602 2601 data['nbrenamedfiles'] = renames
2603 2602 begin = util.timer()
2604 2603 p1renames = copies.pathcopies(b, p1)
2605 2604 end = util.timer()
2606 2605 data['p1.time'] = end - begin
2607 2606 begin = util.timer()
2608 2607 p2renames = copies.pathcopies(b, p2)
2609 2608 end = util.timer()
2610 2609 data['p2.time'] = end - begin
2611 2610 data['p1.renamedfiles'] = len(p1renames)
2612 2611 data['p2.renamedfiles'] = len(p2renames)
2613 2612
2614 2613 if dostats:
2615 2614 if p1missing:
2616 2615 alldata['parentnbrenames'].append(
2617 2616 (data['p1.renamedfiles'], b.hex(), p1.hex())
2618 2617 )
2619 2618 alldata['parenttime'].append(
2620 2619 (data['p1.time'], b.hex(), p1.hex())
2621 2620 )
2622 2621 if p2missing:
2623 2622 alldata['parentnbrenames'].append(
2624 2623 (data['p2.renamedfiles'], b.hex(), p2.hex())
2625 2624 )
2626 2625 alldata['parenttime'].append(
2627 2626 (data['p2.time'], b.hex(), p2.hex())
2628 2627 )
2629 2628 if p1missing or p2missing:
2630 2629 alldata['totalnbrenames'].append(
2631 2630 (
2632 2631 data['nbrenamedfiles'],
2633 2632 b.hex(),
2634 2633 p1.hex(),
2635 2634 p2.hex(),
2636 2635 )
2637 2636 )
2638 2637 alldata['totaltime'].append(
2639 2638 (data['time'], b.hex(), p1.hex(), p2.hex())
2640 2639 )
2641 2640 fm.startitem()
2642 2641 fm.data(**data)
2643 2642 # make node pretty for the human output
2644 2643 out = data.copy()
2645 2644 out['base'] = fm.hexfunc(b.node())
2646 2645 out['p1.node'] = fm.hexfunc(p1.node())
2647 2646 out['p2.node'] = fm.hexfunc(p2.node())
2648 2647 fm.plain(output % out)
2649 2648
2650 2649 fm.end()
2651 2650 if dostats:
2652 2651 # use a second formatter because the data are quite different, not sure
2653 2652 # how it flies with the templater.
2654 2653 entries = [
2655 2654 ('nbrevs', 'number of revision covered'),
2656 2655 ('nbmissingfiles', 'number of missing files at head'),
2657 2656 ]
2658 2657 if dotiming:
2659 2658 entries.append(
2660 2659 ('parentnbrenames', 'rename from one parent to base')
2661 2660 )
2662 2661 entries.append(('totalnbrenames', 'total number of renames'))
2663 2662 entries.append(('parenttime', 'time for one parent'))
2664 2663 entries.append(('totaltime', 'time for both parents'))
2665 2664 _displaystats(ui, opts, entries, alldata)
2666 2665
2667 2666
2668 2667 @command(
2669 2668 b'perf::helper-pathcopies|perfhelper-pathcopies',
2670 2669 formatteropts
2671 2670 + [
2672 2671 (b'r', b'revs', [], b'restrict search to these revisions'),
2673 2672 (b'', b'timing', False, b'provides extra data (costly)'),
2674 2673 (b'', b'stats', False, b'provides statistic about the measured data'),
2675 2674 ],
2676 2675 )
2677 2676 def perfhelperpathcopies(ui, repo, revs=[], **opts):
2678 2677 """find statistic about potential parameters for the `perftracecopies`
2679 2678
2680 2679 This command find source-destination pair relevant for copytracing testing.
2681 2680 It report value for some of the parameters that impact copy tracing time.
2682 2681
2683 2682 If `--timing` is set, rename detection is run and the associated timing
2684 2683 will be reported. The extra details comes at the cost of a slower command
2685 2684 execution.
2686 2685
2687 2686 Since the rename detection is only run once, other factors might easily
2688 2687 affect the precision of the timing. However it should give a good
2689 2688 approximation of which revision pairs are very costly.
2690 2689 """
2691 2690 opts = _byteskwargs(opts)
2692 2691 fm = ui.formatter(b'perf', opts)
2693 2692 dotiming = opts[b'timing']
2694 2693 dostats = opts[b'stats']
2695 2694
2696 2695 if dotiming:
2697 2696 header = '%12s %12s %12s %12s %12s %12s\n'
2698 2697 output = (
2699 2698 "%(source)12s %(destination)12s "
2700 2699 "%(nbrevs)12d %(nbmissingfiles)12d "
2701 2700 "%(nbrenamedfiles)12d %(time)18.5f\n"
2702 2701 )
2703 2702 header_names = (
2704 2703 "source",
2705 2704 "destination",
2706 2705 "nb-revs",
2707 2706 "nb-files",
2708 2707 "nb-renames",
2709 2708 "time",
2710 2709 )
2711 2710 fm.plain(header % header_names)
2712 2711 else:
2713 2712 header = '%12s %12s %12s %12s\n'
2714 2713 output = (
2715 2714 "%(source)12s %(destination)12s "
2716 2715 "%(nbrevs)12d %(nbmissingfiles)12d\n"
2717 2716 )
2718 2717 fm.plain(header % ("source", "destination", "nb-revs", "nb-files"))
2719 2718
2720 2719 if not revs:
2721 2720 revs = ['all()']
2722 2721 revs = scmutil.revrange(repo, revs)
2723 2722
2724 2723 if dostats:
2725 2724 alldata = {
2726 2725 'nbrevs': [],
2727 2726 'nbmissingfiles': [],
2728 2727 }
2729 2728 if dotiming:
2730 2729 alldata['nbrenames'] = []
2731 2730 alldata['time'] = []
2732 2731
2733 2732 roi = repo.revs('merge() and %ld', revs)
2734 2733 for r in roi:
2735 2734 ctx = repo[r]
2736 2735 p1 = ctx.p1().rev()
2737 2736 p2 = ctx.p2().rev()
2738 2737 bases = repo.changelog._commonancestorsheads(p1, p2)
2739 2738 for p in (p1, p2):
2740 2739 for b in bases:
2741 2740 base = repo[b]
2742 2741 parent = repo[p]
2743 2742 missing = copies._computeforwardmissing(base, parent)
2744 2743 if not missing:
2745 2744 continue
2746 2745 data = {
2747 2746 b'source': base.hex(),
2748 2747 b'destination': parent.hex(),
2749 2748 b'nbrevs': len(repo.revs('only(%d, %d)', p, b)),
2750 2749 b'nbmissingfiles': len(missing),
2751 2750 }
2752 2751 if dostats:
2753 2752 alldata['nbrevs'].append(
2754 2753 (
2755 2754 data['nbrevs'],
2756 2755 base.hex(),
2757 2756 parent.hex(),
2758 2757 )
2759 2758 )
2760 2759 alldata['nbmissingfiles'].append(
2761 2760 (
2762 2761 data['nbmissingfiles'],
2763 2762 base.hex(),
2764 2763 parent.hex(),
2765 2764 )
2766 2765 )
2767 2766 if dotiming:
2768 2767 begin = util.timer()
2769 2768 renames = copies.pathcopies(base, parent)
2770 2769 end = util.timer()
2771 2770 # not very stable timing since we did only one run
2772 2771 data['time'] = end - begin
2773 2772 data['nbrenamedfiles'] = len(renames)
2774 2773 if dostats:
2775 2774 alldata['time'].append(
2776 2775 (
2777 2776 data['time'],
2778 2777 base.hex(),
2779 2778 parent.hex(),
2780 2779 )
2781 2780 )
2782 2781 alldata['nbrenames'].append(
2783 2782 (
2784 2783 data['nbrenamedfiles'],
2785 2784 base.hex(),
2786 2785 parent.hex(),
2787 2786 )
2788 2787 )
2789 2788 fm.startitem()
2790 2789 fm.data(**data)
2791 2790 out = data.copy()
2792 2791 out['source'] = fm.hexfunc(base.node())
2793 2792 out['destination'] = fm.hexfunc(parent.node())
2794 2793 fm.plain(output % out)
2795 2794
2796 2795 fm.end()
2797 2796 if dostats:
2798 2797 entries = [
2799 2798 ('nbrevs', 'number of revision covered'),
2800 2799 ('nbmissingfiles', 'number of missing files at head'),
2801 2800 ]
2802 2801 if dotiming:
2803 2802 entries.append(('nbrenames', 'renamed files'))
2804 2803 entries.append(('time', 'time'))
2805 2804 _displaystats(ui, opts, entries, alldata)
2806 2805
2807 2806
2808 2807 @command(b'perf::cca|perfcca', formatteropts)
2809 2808 def perfcca(ui, repo, **opts):
2810 2809 opts = _byteskwargs(opts)
2811 2810 timer, fm = gettimer(ui, opts)
2812 2811 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
2813 2812 fm.end()
2814 2813
2815 2814
2816 2815 @command(b'perf::fncacheload|perffncacheload', formatteropts)
2817 2816 def perffncacheload(ui, repo, **opts):
2818 2817 opts = _byteskwargs(opts)
2819 2818 timer, fm = gettimer(ui, opts)
2820 2819 s = repo.store
2821 2820
2822 2821 def d():
2823 2822 s.fncache._load()
2824 2823
2825 2824 timer(d)
2826 2825 fm.end()
2827 2826
2828 2827
2829 2828 @command(b'perf::fncachewrite|perffncachewrite', formatteropts)
2830 2829 def perffncachewrite(ui, repo, **opts):
2831 2830 opts = _byteskwargs(opts)
2832 2831 timer, fm = gettimer(ui, opts)
2833 2832 s = repo.store
2834 2833 lock = repo.lock()
2835 2834 s.fncache._load()
2836 2835 tr = repo.transaction(b'perffncachewrite')
2837 2836 tr.addbackup(b'fncache')
2838 2837
2839 2838 def d():
2840 2839 s.fncache._dirty = True
2841 2840 s.fncache.write(tr)
2842 2841
2843 2842 timer(d)
2844 2843 tr.close()
2845 2844 lock.release()
2846 2845 fm.end()
2847 2846
2848 2847
2849 2848 @command(b'perf::fncacheencode|perffncacheencode', formatteropts)
2850 2849 def perffncacheencode(ui, repo, **opts):
2851 2850 opts = _byteskwargs(opts)
2852 2851 timer, fm = gettimer(ui, opts)
2853 2852 s = repo.store
2854 2853 s.fncache._load()
2855 2854
2856 2855 def d():
2857 2856 for p in s.fncache.entries:
2858 2857 s.encode(p)
2859 2858
2860 2859 timer(d)
2861 2860 fm.end()
2862 2861
2863 2862
2864 2863 def _bdiffworker(q, blocks, xdiff, ready, done):
2865 2864 while not done.is_set():
2866 2865 pair = q.get()
2867 2866 while pair is not None:
2868 2867 if xdiff:
2869 2868 mdiff.bdiff.xdiffblocks(*pair)
2870 2869 elif blocks:
2871 2870 mdiff.bdiff.blocks(*pair)
2872 2871 else:
2873 2872 mdiff.textdiff(*pair)
2874 2873 q.task_done()
2875 2874 pair = q.get()
2876 2875 q.task_done() # for the None one
2877 2876 with ready:
2878 2877 ready.wait()
2879 2878
2880 2879
2881 2880 def _manifestrevision(repo, mnode):
2882 2881 ml = repo.manifestlog
2883 2882
2884 2883 if util.safehasattr(ml, b'getstorage'):
2885 2884 store = ml.getstorage(b'')
2886 2885 else:
2887 2886 store = ml._revlog
2888 2887
2889 2888 return store.revision(mnode)
2890 2889
2891 2890
2892 2891 @command(
2893 2892 b'perf::bdiff|perfbdiff',
2894 2893 revlogopts
2895 2894 + formatteropts
2896 2895 + [
2897 2896 (
2898 2897 b'',
2899 2898 b'count',
2900 2899 1,
2901 2900 b'number of revisions to test (when using --startrev)',
2902 2901 ),
2903 2902 (b'', b'alldata', False, b'test bdiffs for all associated revisions'),
2904 2903 (b'', b'threads', 0, b'number of thread to use (disable with 0)'),
2905 2904 (b'', b'blocks', False, b'test computing diffs into blocks'),
2906 2905 (b'', b'xdiff', False, b'use xdiff algorithm'),
2907 2906 ],
2908 2907 b'-c|-m|FILE REV',
2909 2908 )
2910 2909 def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts):
2911 2910 """benchmark a bdiff between revisions
2912 2911
2913 2912 By default, benchmark a bdiff between its delta parent and itself.
2914 2913
2915 2914 With ``--count``, benchmark bdiffs between delta parents and self for N
2916 2915 revisions starting at the specified revision.
2917 2916
2918 2917 With ``--alldata``, assume the requested revision is a changeset and
2919 2918 measure bdiffs for all changes related to that changeset (manifest
2920 2919 and filelogs).
2921 2920 """
2922 2921 opts = _byteskwargs(opts)
2923 2922
2924 2923 if opts[b'xdiff'] and not opts[b'blocks']:
2925 2924 raise error.CommandError(b'perfbdiff', b'--xdiff requires --blocks')
2926 2925
2927 2926 if opts[b'alldata']:
2928 2927 opts[b'changelog'] = True
2929 2928
2930 2929 if opts.get(b'changelog') or opts.get(b'manifest'):
2931 2930 file_, rev = None, file_
2932 2931 elif rev is None:
2933 2932 raise error.CommandError(b'perfbdiff', b'invalid arguments')
2934 2933
2935 2934 blocks = opts[b'blocks']
2936 2935 xdiff = opts[b'xdiff']
2937 2936 textpairs = []
2938 2937
2939 2938 r = cmdutil.openrevlog(repo, b'perfbdiff', file_, opts)
2940 2939
2941 2940 startrev = r.rev(r.lookup(rev))
2942 2941 for rev in range(startrev, min(startrev + count, len(r) - 1)):
2943 2942 if opts[b'alldata']:
2944 2943 # Load revisions associated with changeset.
2945 2944 ctx = repo[rev]
2946 2945 mtext = _manifestrevision(repo, ctx.manifestnode())
2947 2946 for pctx in ctx.parents():
2948 2947 pman = _manifestrevision(repo, pctx.manifestnode())
2949 2948 textpairs.append((pman, mtext))
2950 2949
2951 2950 # Load filelog revisions by iterating manifest delta.
2952 2951 man = ctx.manifest()
2953 2952 pman = ctx.p1().manifest()
2954 2953 for filename, change in pman.diff(man).items():
2955 2954 fctx = repo.file(filename)
2956 2955 f1 = fctx.revision(change[0][0] or -1)
2957 2956 f2 = fctx.revision(change[1][0] or -1)
2958 2957 textpairs.append((f1, f2))
2959 2958 else:
2960 2959 dp = r.deltaparent(rev)
2961 2960 textpairs.append((r.revision(dp), r.revision(rev)))
2962 2961
2963 2962 withthreads = threads > 0
2964 2963 if not withthreads:
2965 2964
2966 2965 def d():
2967 2966 for pair in textpairs:
2968 2967 if xdiff:
2969 2968 mdiff.bdiff.xdiffblocks(*pair)
2970 2969 elif blocks:
2971 2970 mdiff.bdiff.blocks(*pair)
2972 2971 else:
2973 2972 mdiff.textdiff(*pair)
2974 2973
2975 2974 else:
2976 2975 q = queue()
2977 2976 for i in _xrange(threads):
2978 2977 q.put(None)
2979 2978 ready = threading.Condition()
2980 2979 done = threading.Event()
2981 2980 for i in _xrange(threads):
2982 2981 threading.Thread(
2983 2982 target=_bdiffworker, args=(q, blocks, xdiff, ready, done)
2984 2983 ).start()
2985 2984 q.join()
2986 2985
2987 2986 def d():
2988 2987 for pair in textpairs:
2989 2988 q.put(pair)
2990 2989 for i in _xrange(threads):
2991 2990 q.put(None)
2992 2991 with ready:
2993 2992 ready.notify_all()
2994 2993 q.join()
2995 2994
2996 2995 timer, fm = gettimer(ui, opts)
2997 2996 timer(d)
2998 2997 fm.end()
2999 2998
3000 2999 if withthreads:
3001 3000 done.set()
3002 3001 for i in _xrange(threads):
3003 3002 q.put(None)
3004 3003 with ready:
3005 3004 ready.notify_all()
3006 3005
3007 3006
3008 3007 @command(
3009 3008 b'perf::unbundle',
3010 3009 formatteropts,
3011 3010 b'BUNDLE_FILE',
3012 3011 )
3013 3012 def perf_unbundle(ui, repo, fname, **opts):
3014 3013 """benchmark application of a bundle in a repository.
3015 3014
3016 3015 This does not include the final transaction processing"""
3017 3016
3018 3017 from mercurial import exchange
3019 3018 from mercurial import bundle2
3020 3019 from mercurial import transaction
3021 3020
3022 3021 opts = _byteskwargs(opts)
3023 3022
3024 3023 ### some compatibility hotfix
3025 3024 #
3026 3025 # the data attribute is dropped in 63edc384d3b7 a changeset introducing a
3027 3026 # critical regression that break transaction rollback for files that are
3028 3027 # de-inlined.
3029 3028 method = transaction.transaction._addentry
3030 3029 pre_63edc384d3b7 = "data" in getargspec(method).args
3031 3030 # the `detailed_exit_code` attribute is introduced in 33c0c25d0b0f
3032 3031 # a changeset that is a close descendant of 18415fc918a1, the changeset
3033 3032 # that conclude the fix run for the bug introduced in 63edc384d3b7.
3034 3033 args = getargspec(error.Abort.__init__).args
3035 3034 post_18415fc918a1 = "detailed_exit_code" in args
3036 3035
3037 3036 old_max_inline = None
3038 3037 try:
3039 3038 if not (pre_63edc384d3b7 or post_18415fc918a1):
3040 3039 # disable inlining
3041 3040 old_max_inline = mercurial.revlog._maxinline
3042 3041 # large enough to never happen
3043 3042 mercurial.revlog._maxinline = 2 ** 50
3044 3043
3045 3044 with repo.lock():
3046 3045 bundle = [None, None]
3047 3046 orig_quiet = repo.ui.quiet
3048 3047 try:
3049 3048 repo.ui.quiet = True
3050 3049 with open(fname, mode="rb") as f:
3051 3050
3052 3051 def noop_report(*args, **kwargs):
3053 3052 pass
3054 3053
3055 3054 def setup():
3056 3055 gen, tr = bundle
3057 3056 if tr is not None:
3058 3057 tr.abort()
3059 3058 bundle[:] = [None, None]
3060 3059 f.seek(0)
3061 3060 bundle[0] = exchange.readbundle(ui, f, fname)
3062 3061 bundle[1] = repo.transaction(b'perf::unbundle')
3063 3062 # silence the transaction
3064 3063 bundle[1]._report = noop_report
3065 3064
3066 3065 def apply():
3067 3066 gen, tr = bundle
3068 3067 bundle2.applybundle(
3069 3068 repo,
3070 3069 gen,
3071 3070 tr,
3072 3071 source=b'perf::unbundle',
3073 3072 url=fname,
3074 3073 )
3075 3074
3076 3075 timer, fm = gettimer(ui, opts)
3077 3076 timer(apply, setup=setup)
3078 3077 fm.end()
3079 3078 finally:
3080 3079 repo.ui.quiet == orig_quiet
3081 3080 gen, tr = bundle
3082 3081 if tr is not None:
3083 3082 tr.abort()
3084 3083 finally:
3085 3084 if old_max_inline is not None:
3086 3085 mercurial.revlog._maxinline = old_max_inline
3087 3086
3088 3087
3089 3088 @command(
3090 3089 b'perf::unidiff|perfunidiff',
3091 3090 revlogopts
3092 3091 + formatteropts
3093 3092 + [
3094 3093 (
3095 3094 b'',
3096 3095 b'count',
3097 3096 1,
3098 3097 b'number of revisions to test (when using --startrev)',
3099 3098 ),
3100 3099 (b'', b'alldata', False, b'test unidiffs for all associated revisions'),
3101 3100 ],
3102 3101 b'-c|-m|FILE REV',
3103 3102 )
3104 3103 def perfunidiff(ui, repo, file_, rev=None, count=None, **opts):
3105 3104 """benchmark a unified diff between revisions
3106 3105
3107 3106 This doesn't include any copy tracing - it's just a unified diff
3108 3107 of the texts.
3109 3108
3110 3109 By default, benchmark a diff between its delta parent and itself.
3111 3110
3112 3111 With ``--count``, benchmark diffs between delta parents and self for N
3113 3112 revisions starting at the specified revision.
3114 3113
3115 3114 With ``--alldata``, assume the requested revision is a changeset and
3116 3115 measure diffs for all changes related to that changeset (manifest
3117 3116 and filelogs).
3118 3117 """
3119 3118 opts = _byteskwargs(opts)
3120 3119 if opts[b'alldata']:
3121 3120 opts[b'changelog'] = True
3122 3121
3123 3122 if opts.get(b'changelog') or opts.get(b'manifest'):
3124 3123 file_, rev = None, file_
3125 3124 elif rev is None:
3126 3125 raise error.CommandError(b'perfunidiff', b'invalid arguments')
3127 3126
3128 3127 textpairs = []
3129 3128
3130 3129 r = cmdutil.openrevlog(repo, b'perfunidiff', file_, opts)
3131 3130
3132 3131 startrev = r.rev(r.lookup(rev))
3133 3132 for rev in range(startrev, min(startrev + count, len(r) - 1)):
3134 3133 if opts[b'alldata']:
3135 3134 # Load revisions associated with changeset.
3136 3135 ctx = repo[rev]
3137 3136 mtext = _manifestrevision(repo, ctx.manifestnode())
3138 3137 for pctx in ctx.parents():
3139 3138 pman = _manifestrevision(repo, pctx.manifestnode())
3140 3139 textpairs.append((pman, mtext))
3141 3140
3142 3141 # Load filelog revisions by iterating manifest delta.
3143 3142 man = ctx.manifest()
3144 3143 pman = ctx.p1().manifest()
3145 3144 for filename, change in pman.diff(man).items():
3146 3145 fctx = repo.file(filename)
3147 3146 f1 = fctx.revision(change[0][0] or -1)
3148 3147 f2 = fctx.revision(change[1][0] or -1)
3149 3148 textpairs.append((f1, f2))
3150 3149 else:
3151 3150 dp = r.deltaparent(rev)
3152 3151 textpairs.append((r.revision(dp), r.revision(rev)))
3153 3152
3154 3153 def d():
3155 3154 for left, right in textpairs:
3156 3155 # The date strings don't matter, so we pass empty strings.
3157 3156 headerlines, hunks = mdiff.unidiff(
3158 3157 left, b'', right, b'', b'left', b'right', binary=False
3159 3158 )
3160 3159 # consume iterators in roughly the way patch.py does
3161 3160 b'\n'.join(headerlines)
3162 3161 b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
3163 3162
3164 3163 timer, fm = gettimer(ui, opts)
3165 3164 timer(d)
3166 3165 fm.end()
3167 3166
3168 3167
3169 3168 @command(b'perf::diffwd|perfdiffwd', formatteropts)
3170 3169 def perfdiffwd(ui, repo, **opts):
3171 3170 """Profile diff of working directory changes"""
3172 3171 opts = _byteskwargs(opts)
3173 3172 timer, fm = gettimer(ui, opts)
3174 3173 options = {
3175 3174 'w': 'ignore_all_space',
3176 3175 'b': 'ignore_space_change',
3177 3176 'B': 'ignore_blank_lines',
3178 3177 }
3179 3178
3180 3179 for diffopt in ('', 'w', 'b', 'B', 'wB'):
3181 3180 opts = {options[c]: b'1' for c in diffopt}
3182 3181
3183 3182 def d():
3184 3183 ui.pushbuffer()
3185 3184 commands.diff(ui, repo, **opts)
3186 3185 ui.popbuffer()
3187 3186
3188 3187 diffopt = diffopt.encode('ascii')
3189 3188 title = b'diffopts: %s' % (diffopt and (b'-' + diffopt) or b'none')
3190 3189 timer(d, title=title)
3191 3190 fm.end()
3192 3191
3193 3192
3194 3193 @command(
3195 3194 b'perf::revlogindex|perfrevlogindex',
3196 3195 revlogopts + formatteropts,
3197 3196 b'-c|-m|FILE',
3198 3197 )
3199 3198 def perfrevlogindex(ui, repo, file_=None, **opts):
3200 3199 """Benchmark operations against a revlog index.
3201 3200
3202 3201 This tests constructing a revlog instance, reading index data,
3203 3202 parsing index data, and performing various operations related to
3204 3203 index data.
3205 3204 """
3206 3205
3207 3206 opts = _byteskwargs(opts)
3208 3207
3209 3208 rl = cmdutil.openrevlog(repo, b'perfrevlogindex', file_, opts)
3210 3209
3211 3210 opener = getattr(rl, 'opener') # trick linter
3212 3211 # compat with hg <= 5.8
3213 3212 radix = getattr(rl, 'radix', None)
3214 3213 indexfile = getattr(rl, '_indexfile', None)
3215 3214 if indexfile is None:
3216 3215 # compatibility with <= hg-5.8
3217 3216 indexfile = getattr(rl, 'indexfile')
3218 3217 data = opener.read(indexfile)
3219 3218
3220 3219 header = struct.unpack(b'>I', data[0:4])[0]
3221 3220 version = header & 0xFFFF
3222 3221 if version == 1:
3223 3222 inline = header & (1 << 16)
3224 3223 else:
3225 3224 raise error.Abort(b'unsupported revlog version: %d' % version)
3226 3225
3227 3226 parse_index_v1 = getattr(mercurial.revlog, 'parse_index_v1', None)
3228 3227 if parse_index_v1 is None:
3229 3228 parse_index_v1 = mercurial.revlog.revlogio().parseindex
3230 3229
3231 3230 rllen = len(rl)
3232 3231
3233 3232 node0 = rl.node(0)
3234 3233 node25 = rl.node(rllen // 4)
3235 3234 node50 = rl.node(rllen // 2)
3236 3235 node75 = rl.node(rllen // 4 * 3)
3237 3236 node100 = rl.node(rllen - 1)
3238 3237
3239 3238 allrevs = range(rllen)
3240 3239 allrevsrev = list(reversed(allrevs))
3241 3240 allnodes = [rl.node(rev) for rev in range(rllen)]
3242 3241 allnodesrev = list(reversed(allnodes))
3243 3242
3244 3243 def constructor():
3245 3244 if radix is not None:
3246 3245 revlog(opener, radix=radix)
3247 3246 else:
3248 3247 # hg <= 5.8
3249 3248 revlog(opener, indexfile=indexfile)
3250 3249
3251 3250 def read():
3252 3251 with opener(indexfile) as fh:
3253 3252 fh.read()
3254 3253
3255 3254 def parseindex():
3256 3255 parse_index_v1(data, inline)
3257 3256
3258 3257 def getentry(revornode):
3259 3258 index = parse_index_v1(data, inline)[0]
3260 3259 index[revornode]
3261 3260
3262 3261 def getentries(revs, count=1):
3263 3262 index = parse_index_v1(data, inline)[0]
3264 3263
3265 3264 for i in range(count):
3266 3265 for rev in revs:
3267 3266 index[rev]
3268 3267
3269 3268 def resolvenode(node):
3270 3269 index = parse_index_v1(data, inline)[0]
3271 3270 rev = getattr(index, 'rev', None)
3272 3271 if rev is None:
3273 3272 nodemap = getattr(parse_index_v1(data, inline)[0], 'nodemap', None)
3274 3273 # This only works for the C code.
3275 3274 if nodemap is None:
3276 3275 return
3277 3276 rev = nodemap.__getitem__
3278 3277
3279 3278 try:
3280 3279 rev(node)
3281 3280 except error.RevlogError:
3282 3281 pass
3283 3282
3284 3283 def resolvenodes(nodes, count=1):
3285 3284 index = parse_index_v1(data, inline)[0]
3286 3285 rev = getattr(index, 'rev', None)
3287 3286 if rev is None:
3288 3287 nodemap = getattr(parse_index_v1(data, inline)[0], 'nodemap', None)
3289 3288 # This only works for the C code.
3290 3289 if nodemap is None:
3291 3290 return
3292 3291 rev = nodemap.__getitem__
3293 3292
3294 3293 for i in range(count):
3295 3294 for node in nodes:
3296 3295 try:
3297 3296 rev(node)
3298 3297 except error.RevlogError:
3299 3298 pass
3300 3299
3301 3300 benches = [
3302 3301 (constructor, b'revlog constructor'),
3303 3302 (read, b'read'),
3304 3303 (parseindex, b'create index object'),
3305 3304 (lambda: getentry(0), b'retrieve index entry for rev 0'),
3306 3305 (lambda: resolvenode(b'a' * 20), b'look up missing node'),
3307 3306 (lambda: resolvenode(node0), b'look up node at rev 0'),
3308 3307 (lambda: resolvenode(node25), b'look up node at 1/4 len'),
3309 3308 (lambda: resolvenode(node50), b'look up node at 1/2 len'),
3310 3309 (lambda: resolvenode(node75), b'look up node at 3/4 len'),
3311 3310 (lambda: resolvenode(node100), b'look up node at tip'),
3312 3311 # 2x variation is to measure caching impact.
3313 3312 (lambda: resolvenodes(allnodes), b'look up all nodes (forward)'),
3314 3313 (lambda: resolvenodes(allnodes, 2), b'look up all nodes 2x (forward)'),
3315 3314 (lambda: resolvenodes(allnodesrev), b'look up all nodes (reverse)'),
3316 3315 (
3317 3316 lambda: resolvenodes(allnodesrev, 2),
3318 3317 b'look up all nodes 2x (reverse)',
3319 3318 ),
3320 3319 (lambda: getentries(allrevs), b'retrieve all index entries (forward)'),
3321 3320 (
3322 3321 lambda: getentries(allrevs, 2),
3323 3322 b'retrieve all index entries 2x (forward)',
3324 3323 ),
3325 3324 (
3326 3325 lambda: getentries(allrevsrev),
3327 3326 b'retrieve all index entries (reverse)',
3328 3327 ),
3329 3328 (
3330 3329 lambda: getentries(allrevsrev, 2),
3331 3330 b'retrieve all index entries 2x (reverse)',
3332 3331 ),
3333 3332 ]
3334 3333
3335 3334 for fn, title in benches:
3336 3335 timer, fm = gettimer(ui, opts)
3337 3336 timer(fn, title=title)
3338 3337 fm.end()
3339 3338
3340 3339
3341 3340 @command(
3342 3341 b'perf::revlogrevisions|perfrevlogrevisions',
3343 3342 revlogopts
3344 3343 + formatteropts
3345 3344 + [
3346 3345 (b'd', b'dist', 100, b'distance between the revisions'),
3347 3346 (b's', b'startrev', 0, b'revision to start reading at'),
3348 3347 (b'', b'reverse', False, b'read in reverse'),
3349 3348 ],
3350 3349 b'-c|-m|FILE',
3351 3350 )
3352 3351 def perfrevlogrevisions(
3353 3352 ui, repo, file_=None, startrev=0, reverse=False, **opts
3354 3353 ):
3355 3354 """Benchmark reading a series of revisions from a revlog.
3356 3355
3357 3356 By default, we read every ``-d/--dist`` revision from 0 to tip of
3358 3357 the specified revlog.
3359 3358
3360 3359 The start revision can be defined via ``-s/--startrev``.
3361 3360 """
3362 3361 opts = _byteskwargs(opts)
3363 3362
3364 3363 rl = cmdutil.openrevlog(repo, b'perfrevlogrevisions', file_, opts)
3365 3364 rllen = getlen(ui)(rl)
3366 3365
3367 3366 if startrev < 0:
3368 3367 startrev = rllen + startrev
3369 3368
3370 3369 def d():
3371 3370 rl.clearcaches()
3372 3371
3373 3372 beginrev = startrev
3374 3373 endrev = rllen
3375 3374 dist = opts[b'dist']
3376 3375
3377 3376 if reverse:
3378 3377 beginrev, endrev = endrev - 1, beginrev - 1
3379 3378 dist = -1 * dist
3380 3379
3381 3380 for x in _xrange(beginrev, endrev, dist):
3382 3381 # Old revisions don't support passing int.
3383 3382 n = rl.node(x)
3384 3383 rl.revision(n)
3385 3384
3386 3385 timer, fm = gettimer(ui, opts)
3387 3386 timer(d)
3388 3387 fm.end()
3389 3388
3390 3389
3391 3390 @command(
3392 3391 b'perf::revlogwrite|perfrevlogwrite',
3393 3392 revlogopts
3394 3393 + formatteropts
3395 3394 + [
3396 3395 (b's', b'startrev', 1000, b'revision to start writing at'),
3397 3396 (b'', b'stoprev', -1, b'last revision to write'),
3398 3397 (b'', b'count', 3, b'number of passes to perform'),
3399 3398 (b'', b'details', False, b'print timing for every revisions tested'),
3400 3399 (b'', b'source', b'full', b'the kind of data feed in the revlog'),
3401 3400 (b'', b'lazydeltabase', True, b'try the provided delta first'),
3402 3401 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
3403 3402 ],
3404 3403 b'-c|-m|FILE',
3405 3404 )
3406 3405 def perfrevlogwrite(ui, repo, file_=None, startrev=1000, stoprev=-1, **opts):
3407 3406 """Benchmark writing a series of revisions to a revlog.
3408 3407
3409 3408 Possible source values are:
3410 3409 * `full`: add from a full text (default).
3411 3410 * `parent-1`: add from a delta to the first parent
3412 3411 * `parent-2`: add from a delta to the second parent if it exists
3413 3412 (use a delta from the first parent otherwise)
3414 3413 * `parent-smallest`: add from the smallest delta (either p1 or p2)
3415 3414 * `storage`: add from the existing precomputed deltas
3416 3415
3417 3416 Note: This performance command measures performance in a custom way. As a
3418 3417 result some of the global configuration of the 'perf' command does not
3419 3418 apply to it:
3420 3419
3421 3420 * ``pre-run``: disabled
3422 3421
3423 3422 * ``profile-benchmark``: disabled
3424 3423
3425 3424 * ``run-limits``: disabled use --count instead
3426 3425 """
3427 3426 opts = _byteskwargs(opts)
3428 3427
3429 3428 rl = cmdutil.openrevlog(repo, b'perfrevlogwrite', file_, opts)
3430 3429 rllen = getlen(ui)(rl)
3431 3430 if startrev < 0:
3432 3431 startrev = rllen + startrev
3433 3432 if stoprev < 0:
3434 3433 stoprev = rllen + stoprev
3435 3434
3436 3435 lazydeltabase = opts['lazydeltabase']
3437 3436 source = opts['source']
3438 3437 clearcaches = opts['clear_caches']
3439 3438 validsource = (
3440 3439 b'full',
3441 3440 b'parent-1',
3442 3441 b'parent-2',
3443 3442 b'parent-smallest',
3444 3443 b'storage',
3445 3444 )
3446 3445 if source not in validsource:
3447 3446 raise error.Abort('invalid source type: %s' % source)
3448 3447
3449 3448 ### actually gather results
3450 3449 count = opts['count']
3451 3450 if count <= 0:
3452 3451 raise error.Abort('invalide run count: %d' % count)
3453 3452 allresults = []
3454 3453 for c in range(count):
3455 3454 timing = _timeonewrite(
3456 3455 ui,
3457 3456 rl,
3458 3457 source,
3459 3458 startrev,
3460 3459 stoprev,
3461 3460 c + 1,
3462 3461 lazydeltabase=lazydeltabase,
3463 3462 clearcaches=clearcaches,
3464 3463 )
3465 3464 allresults.append(timing)
3466 3465
3467 3466 ### consolidate the results in a single list
3468 3467 results = []
3469 3468 for idx, (rev, t) in enumerate(allresults[0]):
3470 3469 ts = [t]
3471 3470 for other in allresults[1:]:
3472 3471 orev, ot = other[idx]
3473 3472 assert orev == rev
3474 3473 ts.append(ot)
3475 3474 results.append((rev, ts))
3476 3475 resultcount = len(results)
3477 3476
3478 3477 ### Compute and display relevant statistics
3479 3478
3480 3479 # get a formatter
3481 3480 fm = ui.formatter(b'perf', opts)
3482 3481 displayall = ui.configbool(b"perf", b"all-timing", True)
3483 3482
3484 3483 # print individual details if requested
3485 3484 if opts['details']:
3486 3485 for idx, item in enumerate(results, 1):
3487 3486 rev, data = item
3488 3487 title = 'revisions #%d of %d, rev %d' % (idx, resultcount, rev)
3489 3488 formatone(fm, data, title=title, displayall=displayall)
3490 3489
3491 3490 # sorts results by median time
3492 3491 results.sort(key=lambda x: sorted(x[1])[len(x[1]) // 2])
3493 3492 # list of (name, index) to display)
3494 3493 relevants = [
3495 3494 ("min", 0),
3496 3495 ("10%", resultcount * 10 // 100),
3497 3496 ("25%", resultcount * 25 // 100),
3498 3497 ("50%", resultcount * 70 // 100),
3499 3498 ("75%", resultcount * 75 // 100),
3500 3499 ("90%", resultcount * 90 // 100),
3501 3500 ("95%", resultcount * 95 // 100),
3502 3501 ("99%", resultcount * 99 // 100),
3503 3502 ("99.9%", resultcount * 999 // 1000),
3504 3503 ("99.99%", resultcount * 9999 // 10000),
3505 3504 ("99.999%", resultcount * 99999 // 100000),
3506 3505 ("max", -1),
3507 3506 ]
3508 3507 if not ui.quiet:
3509 3508 for name, idx in relevants:
3510 3509 data = results[idx]
3511 3510 title = '%s of %d, rev %d' % (name, resultcount, data[0])
3512 3511 formatone(fm, data[1], title=title, displayall=displayall)
3513 3512
3514 3513 # XXX summing that many float will not be very precise, we ignore this fact
3515 3514 # for now
3516 3515 totaltime = []
3517 3516 for item in allresults:
3518 3517 totaltime.append(
3519 3518 (
3520 3519 sum(x[1][0] for x in item),
3521 3520 sum(x[1][1] for x in item),
3522 3521 sum(x[1][2] for x in item),
3523 3522 )
3524 3523 )
3525 3524 formatone(
3526 3525 fm,
3527 3526 totaltime,
3528 3527 title="total time (%d revs)" % resultcount,
3529 3528 displayall=displayall,
3530 3529 )
3531 3530 fm.end()
3532 3531
3533 3532
3534 3533 class _faketr:
3535 3534 def add(s, x, y, z=None):
3536 3535 return None
3537 3536
3538 3537
3539 3538 def _timeonewrite(
3540 3539 ui,
3541 3540 orig,
3542 3541 source,
3543 3542 startrev,
3544 3543 stoprev,
3545 3544 runidx=None,
3546 3545 lazydeltabase=True,
3547 3546 clearcaches=True,
3548 3547 ):
3549 3548 timings = []
3550 3549 tr = _faketr()
3551 3550 with _temprevlog(ui, orig, startrev) as dest:
3552 3551 if hasattr(dest, "delta_config"):
3553 3552 dest.delta_config.lazy_delta_base = lazydeltabase
3554 3553 else:
3555 3554 dest._lazydeltabase = lazydeltabase
3556 3555 revs = list(orig.revs(startrev, stoprev))
3557 3556 total = len(revs)
3558 3557 topic = 'adding'
3559 3558 if runidx is not None:
3560 3559 topic += ' (run #%d)' % runidx
3561 3560 # Support both old and new progress API
3562 3561 if util.safehasattr(ui, 'makeprogress'):
3563 3562 progress = ui.makeprogress(topic, unit='revs', total=total)
3564 3563
3565 3564 def updateprogress(pos):
3566 3565 progress.update(pos)
3567 3566
3568 3567 def completeprogress():
3569 3568 progress.complete()
3570 3569
3571 3570 else:
3572 3571
3573 3572 def updateprogress(pos):
3574 3573 ui.progress(topic, pos, unit='revs', total=total)
3575 3574
3576 3575 def completeprogress():
3577 3576 ui.progress(topic, None, unit='revs', total=total)
3578 3577
3579 3578 for idx, rev in enumerate(revs):
3580 3579 updateprogress(idx)
3581 3580 addargs, addkwargs = _getrevisionseed(orig, rev, tr, source)
3582 3581 if clearcaches:
3583 3582 dest.index.clearcaches()
3584 3583 dest.clearcaches()
3585 3584 with timeone() as r:
3586 3585 dest.addrawrevision(*addargs, **addkwargs)
3587 3586 timings.append((rev, r[0]))
3588 3587 updateprogress(total)
3589 3588 completeprogress()
3590 3589 return timings
3591 3590
3592 3591
3593 3592 def _getrevisionseed(orig, rev, tr, source):
3594 3593 from mercurial.node import nullid
3595 3594
3596 3595 linkrev = orig.linkrev(rev)
3597 3596 node = orig.node(rev)
3598 3597 p1, p2 = orig.parents(node)
3599 3598 flags = orig.flags(rev)
3600 3599 cachedelta = None
3601 3600 text = None
3602 3601
3603 3602 if source == b'full':
3604 3603 text = orig.revision(rev)
3605 3604 elif source == b'parent-1':
3606 3605 baserev = orig.rev(p1)
3607 3606 cachedelta = (baserev, orig.revdiff(p1, rev))
3608 3607 elif source == b'parent-2':
3609 3608 parent = p2
3610 3609 if p2 == nullid:
3611 3610 parent = p1
3612 3611 baserev = orig.rev(parent)
3613 3612 cachedelta = (baserev, orig.revdiff(parent, rev))
3614 3613 elif source == b'parent-smallest':
3615 3614 p1diff = orig.revdiff(p1, rev)
3616 3615 parent = p1
3617 3616 diff = p1diff
3618 3617 if p2 != nullid:
3619 3618 p2diff = orig.revdiff(p2, rev)
3620 3619 if len(p1diff) > len(p2diff):
3621 3620 parent = p2
3622 3621 diff = p2diff
3623 3622 baserev = orig.rev(parent)
3624 3623 cachedelta = (baserev, diff)
3625 3624 elif source == b'storage':
3626 3625 baserev = orig.deltaparent(rev)
3627 3626 cachedelta = (baserev, orig.revdiff(orig.node(baserev), rev))
3628 3627
3629 3628 return (
3630 3629 (text, tr, linkrev, p1, p2),
3631 3630 {'node': node, 'flags': flags, 'cachedelta': cachedelta},
3632 3631 )
3633 3632
3634 3633
3635 3634 @contextlib.contextmanager
3636 3635 def _temprevlog(ui, orig, truncaterev):
3637 3636 from mercurial import vfs as vfsmod
3638 3637
3639 3638 if orig._inline:
3640 3639 raise error.Abort('not supporting inline revlog (yet)')
3641 3640 revlogkwargs = {}
3642 3641 k = 'upperboundcomp'
3643 3642 if util.safehasattr(orig, k):
3644 3643 revlogkwargs[k] = getattr(orig, k)
3645 3644
3646 3645 indexfile = getattr(orig, '_indexfile', None)
3647 3646 if indexfile is None:
3648 3647 # compatibility with <= hg-5.8
3649 3648 indexfile = getattr(orig, 'indexfile')
3650 3649 origindexpath = orig.opener.join(indexfile)
3651 3650
3652 3651 datafile = getattr(orig, '_datafile', getattr(orig, 'datafile'))
3653 3652 origdatapath = orig.opener.join(datafile)
3654 3653 radix = b'revlog'
3655 3654 indexname = b'revlog.i'
3656 3655 dataname = b'revlog.d'
3657 3656
3658 3657 tmpdir = tempfile.mkdtemp(prefix='tmp-hgperf-')
3659 3658 try:
3660 3659 # copy the data file in a temporary directory
3661 3660 ui.debug('copying data in %s\n' % tmpdir)
3662 3661 destindexpath = os.path.join(tmpdir, 'revlog.i')
3663 3662 destdatapath = os.path.join(tmpdir, 'revlog.d')
3664 3663 shutil.copyfile(origindexpath, destindexpath)
3665 3664 shutil.copyfile(origdatapath, destdatapath)
3666 3665
3667 3666 # remove the data we want to add again
3668 3667 ui.debug('truncating data to be rewritten\n')
3669 3668 with open(destindexpath, 'ab') as index:
3670 3669 index.seek(0)
3671 3670 index.truncate(truncaterev * orig._io.size)
3672 3671 with open(destdatapath, 'ab') as data:
3673 3672 data.seek(0)
3674 3673 data.truncate(orig.start(truncaterev))
3675 3674
3676 3675 # instantiate a new revlog from the temporary copy
3677 3676 ui.debug('truncating adding to be rewritten\n')
3678 3677 vfs = vfsmod.vfs(tmpdir)
3679 3678 vfs.options = getattr(orig.opener, 'options', None)
3680 3679
3681 3680 try:
3682 3681 dest = revlog(vfs, radix=radix, **revlogkwargs)
3683 3682 except TypeError:
3684 3683 dest = revlog(
3685 3684 vfs, indexfile=indexname, datafile=dataname, **revlogkwargs
3686 3685 )
3687 3686 if dest._inline:
3688 3687 raise error.Abort('not supporting inline revlog (yet)')
3689 3688 # make sure internals are initialized
3690 3689 dest.revision(len(dest) - 1)
3691 3690 yield dest
3692 3691 del dest, vfs
3693 3692 finally:
3694 3693 shutil.rmtree(tmpdir, True)
3695 3694
3696 3695
3697 3696 @command(
3698 3697 b'perf::revlogchunks|perfrevlogchunks',
3699 3698 revlogopts
3700 3699 + formatteropts
3701 3700 + [
3702 3701 (b'e', b'engines', b'', b'compression engines to use'),
3703 3702 (b's', b'startrev', 0, b'revision to start at'),
3704 3703 ],
3705 3704 b'-c|-m|FILE',
3706 3705 )
3707 3706 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
3708 3707 """Benchmark operations on revlog chunks.
3709 3708
3710 3709 Logically, each revlog is a collection of fulltext revisions. However,
3711 3710 stored within each revlog are "chunks" of possibly compressed data. This
3712 3711 data needs to be read and decompressed or compressed and written.
3713 3712
3714 3713 This command measures the time it takes to read+decompress and recompress
3715 3714 chunks in a revlog. It effectively isolates I/O and compression performance.
3716 3715 For measurements of higher-level operations like resolving revisions,
3717 3716 see ``perfrevlogrevisions`` and ``perfrevlogrevision``.
3718 3717 """
3719 3718 opts = _byteskwargs(opts)
3720 3719
3721 3720 rl = cmdutil.openrevlog(repo, b'perfrevlogchunks', file_, opts)
3722 3721
3723 3722 # - _chunkraw was renamed to _getsegmentforrevs
3724 3723 # - _getsegmentforrevs was moved on the inner object
3725 3724 try:
3726 3725 segmentforrevs = rl._inner.get_segment_for_revs
3727 3726 except AttributeError:
3728 3727 try:
3729 3728 segmentforrevs = rl._getsegmentforrevs
3730 3729 except AttributeError:
3731 3730 segmentforrevs = rl._chunkraw
3732 3731
3733 3732 # Verify engines argument.
3734 3733 if engines:
3735 3734 engines = {e.strip() for e in engines.split(b',')}
3736 3735 for engine in engines:
3737 3736 try:
3738 3737 util.compressionengines[engine]
3739 3738 except KeyError:
3740 3739 raise error.Abort(b'unknown compression engine: %s' % engine)
3741 3740 else:
3742 3741 engines = []
3743 3742 for e in util.compengines:
3744 3743 engine = util.compengines[e]
3745 3744 try:
3746 3745 if engine.available():
3747 3746 engine.revlogcompressor().compress(b'dummy')
3748 3747 engines.append(e)
3749 3748 except NotImplementedError:
3750 3749 pass
3751 3750
3752 3751 revs = list(rl.revs(startrev, len(rl) - 1))
3753 3752
3754 3753 @contextlib.contextmanager
3755 3754 def reading(rl):
3756 3755 if getattr(rl, 'reading', None) is not None:
3757 3756 with rl.reading():
3758 3757 yield None
3759 3758 elif rl._inline:
3760 3759 indexfile = getattr(rl, '_indexfile', None)
3761 3760 if indexfile is None:
3762 3761 # compatibility with <= hg-5.8
3763 3762 indexfile = getattr(rl, 'indexfile')
3764 3763 yield getsvfs(repo)(indexfile)
3765 3764 else:
3766 3765 datafile = getattr(rl, 'datafile', getattr(rl, 'datafile'))
3767 3766 yield getsvfs(repo)(datafile)
3768 3767
3769 3768 if getattr(rl, 'reading', None) is not None:
3770 3769
3771 3770 @contextlib.contextmanager
3772 3771 def lazy_reading(rl):
3773 3772 with rl.reading():
3774 3773 yield
3775 3774
3776 3775 else:
3777 3776
3778 3777 @contextlib.contextmanager
3779 3778 def lazy_reading(rl):
3780 3779 yield
3781 3780
3782 3781 def doread():
3783 3782 rl.clearcaches()
3784 3783 for rev in revs:
3785 3784 with lazy_reading(rl):
3786 3785 segmentforrevs(rev, rev)
3787 3786
3788 3787 def doreadcachedfh():
3789 3788 rl.clearcaches()
3790 3789 with reading(rl) as fh:
3791 3790 if fh is not None:
3792 3791 for rev in revs:
3793 3792 segmentforrevs(rev, rev, df=fh)
3794 3793 else:
3795 3794 for rev in revs:
3796 3795 segmentforrevs(rev, rev)
3797 3796
3798 3797 def doreadbatch():
3799 3798 rl.clearcaches()
3800 3799 with lazy_reading(rl):
3801 3800 segmentforrevs(revs[0], revs[-1])
3802 3801
3803 3802 def doreadbatchcachedfh():
3804 3803 rl.clearcaches()
3805 3804 with reading(rl) as fh:
3806 3805 if fh is not None:
3807 3806 segmentforrevs(revs[0], revs[-1], df=fh)
3808 3807 else:
3809 3808 segmentforrevs(revs[0], revs[-1])
3810 3809
3811 3810 def dochunk():
3812 3811 rl.clearcaches()
3813 3812 # chunk used to be available directly on the revlog
3814 3813 _chunk = getattr(rl, '_inner', rl)._chunk
3815 3814 with reading(rl) as fh:
3816 3815 if fh is not None:
3817 3816 for rev in revs:
3818 3817 _chunk(rev, df=fh)
3819 3818 else:
3820 3819 for rev in revs:
3821 3820 _chunk(rev)
3822 3821
3823 3822 chunks = [None]
3824 3823
3825 3824 def dochunkbatch():
3826 3825 rl.clearcaches()
3827 3826 _chunks = getattr(rl, '_inner', rl)._chunks
3828 3827 with reading(rl) as fh:
3829 3828 if fh is not None:
3830 3829 # Save chunks as a side-effect.
3831 3830 chunks[0] = _chunks(revs, df=fh)
3832 3831 else:
3833 3832 # Save chunks as a side-effect.
3834 3833 chunks[0] = _chunks(revs)
3835 3834
3836 3835 def docompress(compressor):
3837 3836 rl.clearcaches()
3838 3837
3839 3838 compressor_holder = getattr(rl, '_inner', rl)
3840 3839
3841 3840 try:
3842 3841 # Swap in the requested compression engine.
3843 3842 oldcompressor = compressor_holder._compressor
3844 3843 compressor_holder._compressor = compressor
3845 3844 for chunk in chunks[0]:
3846 3845 rl.compress(chunk)
3847 3846 finally:
3848 3847 compressor_holder._compressor = oldcompressor
3849 3848
3850 3849 benches = [
3851 3850 (lambda: doread(), b'read'),
3852 3851 (lambda: doreadcachedfh(), b'read w/ reused fd'),
3853 3852 (lambda: doreadbatch(), b'read batch'),
3854 3853 (lambda: doreadbatchcachedfh(), b'read batch w/ reused fd'),
3855 3854 (lambda: dochunk(), b'chunk'),
3856 3855 (lambda: dochunkbatch(), b'chunk batch'),
3857 3856 ]
3858 3857
3859 3858 for engine in sorted(engines):
3860 3859 compressor = util.compengines[engine].revlogcompressor()
3861 3860 benches.append(
3862 3861 (
3863 3862 functools.partial(docompress, compressor),
3864 3863 b'compress w/ %s' % engine,
3865 3864 )
3866 3865 )
3867 3866
3868 3867 for fn, title in benches:
3869 3868 timer, fm = gettimer(ui, opts)
3870 3869 timer(fn, title=title)
3871 3870 fm.end()
3872 3871
3873 3872
3874 3873 @command(
3875 3874 b'perf::revlogrevision|perfrevlogrevision',
3876 3875 revlogopts
3877 3876 + formatteropts
3878 3877 + [(b'', b'cache', False, b'use caches instead of clearing')],
3879 3878 b'-c|-m|FILE REV',
3880 3879 )
3881 3880 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
3882 3881 """Benchmark obtaining a revlog revision.
3883 3882
3884 3883 Obtaining a revlog revision consists of roughly the following steps:
3885 3884
3886 3885 1. Compute the delta chain
3887 3886 2. Slice the delta chain if applicable
3888 3887 3. Obtain the raw chunks for that delta chain
3889 3888 4. Decompress each raw chunk
3890 3889 5. Apply binary patches to obtain fulltext
3891 3890 6. Verify hash of fulltext
3892 3891
3893 3892 This command measures the time spent in each of these phases.
3894 3893 """
3895 3894 opts = _byteskwargs(opts)
3896 3895
3897 3896 if opts.get(b'changelog') or opts.get(b'manifest'):
3898 3897 file_, rev = None, file_
3899 3898 elif rev is None:
3900 3899 raise error.CommandError(b'perfrevlogrevision', b'invalid arguments')
3901 3900
3902 3901 r = cmdutil.openrevlog(repo, b'perfrevlogrevision', file_, opts)
3903 3902
3904 3903 # _chunkraw was renamed to _getsegmentforrevs.
3905 3904 try:
3906 3905 segmentforrevs = r._inner.get_segment_for_revs
3907 3906 except AttributeError:
3908 3907 try:
3909 3908 segmentforrevs = r._getsegmentforrevs
3910 3909 except AttributeError:
3911 3910 segmentforrevs = r._chunkraw
3912 3911
3913 3912 node = r.lookup(rev)
3914 3913 rev = r.rev(node)
3915 3914
3916 3915 if getattr(r, 'reading', None) is not None:
3917 3916
3918 3917 @contextlib.contextmanager
3919 3918 def lazy_reading(r):
3920 3919 with r.reading():
3921 3920 yield
3922 3921
3923 3922 else:
3924 3923
3925 3924 @contextlib.contextmanager
3926 3925 def lazy_reading(r):
3927 3926 yield
3928 3927
3929 3928 def getrawchunks(data, chain):
3930 3929 start = r.start
3931 3930 length = r.length
3932 3931 inline = r._inline
3933 3932 try:
3934 3933 iosize = r.index.entry_size
3935 3934 except AttributeError:
3936 3935 iosize = r._io.size
3937 3936 buffer = util.buffer
3938 3937
3939 3938 chunks = []
3940 3939 ladd = chunks.append
3941 3940 for idx, item in enumerate(chain):
3942 3941 offset = start(item[0])
3943 3942 bits = data[idx]
3944 3943 for rev in item:
3945 3944 chunkstart = start(rev)
3946 3945 if inline:
3947 3946 chunkstart += (rev + 1) * iosize
3948 3947 chunklength = length(rev)
3949 3948 ladd(buffer(bits, chunkstart - offset, chunklength))
3950 3949
3951 3950 return chunks
3952 3951
3953 3952 def dodeltachain(rev):
3954 3953 if not cache:
3955 3954 r.clearcaches()
3956 3955 r._deltachain(rev)
3957 3956
3958 3957 def doread(chain):
3959 3958 if not cache:
3960 3959 r.clearcaches()
3961 3960 for item in slicedchain:
3962 3961 with lazy_reading(r):
3963 3962 segmentforrevs(item[0], item[-1])
3964 3963
3965 3964 def doslice(r, chain, size):
3966 3965 for s in slicechunk(r, chain, targetsize=size):
3967 3966 pass
3968 3967
3969 3968 def dorawchunks(data, chain):
3970 3969 if not cache:
3971 3970 r.clearcaches()
3972 3971 getrawchunks(data, chain)
3973 3972
3974 3973 def dodecompress(chunks):
3975 3974 decomp = r.decompress
3976 3975 for chunk in chunks:
3977 3976 decomp(chunk)
3978 3977
3979 3978 def dopatch(text, bins):
3980 3979 if not cache:
3981 3980 r.clearcaches()
3982 3981 mdiff.patches(text, bins)
3983 3982
3984 3983 def dohash(text):
3985 3984 if not cache:
3986 3985 r.clearcaches()
3987 3986 r.checkhash(text, node, rev=rev)
3988 3987
3989 3988 def dorevision():
3990 3989 if not cache:
3991 3990 r.clearcaches()
3992 3991 r.revision(node)
3993 3992
3994 3993 try:
3995 3994 from mercurial.revlogutils.deltas import slicechunk
3996 3995 except ImportError:
3997 3996 slicechunk = getattr(revlog, '_slicechunk', None)
3998 3997
3999 3998 size = r.length(rev)
4000 3999 chain = r._deltachain(rev)[0]
4001 4000
4002 4001 with_sparse_read = False
4003 4002 if hasattr(r, 'data_config'):
4004 4003 with_sparse_read = r.data_config.with_sparse_read
4005 4004 elif hasattr(r, '_withsparseread'):
4006 4005 with_sparse_read = r._withsparseread
4007 4006 if with_sparse_read:
4008 4007 slicedchain = (chain,)
4009 4008 else:
4010 4009 slicedchain = tuple(slicechunk(r, chain, targetsize=size))
4011 4010 data = [segmentforrevs(seg[0], seg[-1])[1] for seg in slicedchain]
4012 4011 rawchunks = getrawchunks(data, slicedchain)
4013 4012 bins = r._inner._chunks(chain)
4014 4013 text = bytes(bins[0])
4015 4014 bins = bins[1:]
4016 4015 text = mdiff.patches(text, bins)
4017 4016
4018 4017 benches = [
4019 4018 (lambda: dorevision(), b'full'),
4020 4019 (lambda: dodeltachain(rev), b'deltachain'),
4021 4020 (lambda: doread(chain), b'read'),
4022 4021 ]
4023 4022
4024 4023 if with_sparse_read:
4025 4024 slicing = (lambda: doslice(r, chain, size), b'slice-sparse-chain')
4026 4025 benches.append(slicing)
4027 4026
4028 4027 benches.extend(
4029 4028 [
4030 4029 (lambda: dorawchunks(data, slicedchain), b'rawchunks'),
4031 4030 (lambda: dodecompress(rawchunks), b'decompress'),
4032 4031 (lambda: dopatch(text, bins), b'patch'),
4033 4032 (lambda: dohash(text), b'hash'),
4034 4033 ]
4035 4034 )
4036 4035
4037 4036 timer, fm = gettimer(ui, opts)
4038 4037 for fn, title in benches:
4039 4038 timer(fn, title=title)
4040 4039 fm.end()
4041 4040
4042 4041
4043 4042 @command(
4044 4043 b'perf::revset|perfrevset',
4045 4044 [
4046 4045 (b'C', b'clear', False, b'clear volatile cache between each call.'),
4047 4046 (b'', b'contexts', False, b'obtain changectx for each revision'),
4048 4047 ]
4049 4048 + formatteropts,
4050 4049 b"REVSET",
4051 4050 )
4052 4051 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
4053 4052 """benchmark the execution time of a revset
4054 4053
4055 4054 Use the --clean option if need to evaluate the impact of build volatile
4056 4055 revisions set cache on the revset execution. Volatile cache hold filtered
4057 4056 and obsolete related cache."""
4058 4057 opts = _byteskwargs(opts)
4059 4058
4060 4059 timer, fm = gettimer(ui, opts)
4061 4060
4062 4061 def d():
4063 4062 if clear:
4064 4063 repo.invalidatevolatilesets()
4065 4064 if contexts:
4066 4065 for ctx in repo.set(expr):
4067 4066 pass
4068 4067 else:
4069 4068 for r in repo.revs(expr):
4070 4069 pass
4071 4070
4072 4071 timer(d)
4073 4072 fm.end()
4074 4073
4075 4074
4076 4075 @command(
4077 4076 b'perf::volatilesets|perfvolatilesets',
4078 4077 [
4079 4078 (b'', b'clear-obsstore', False, b'drop obsstore between each call.'),
4080 4079 ]
4081 4080 + formatteropts,
4082 4081 )
4083 4082 def perfvolatilesets(ui, repo, *names, **opts):
4084 4083 """benchmark the computation of various volatile set
4085 4084
4086 4085 Volatile set computes element related to filtering and obsolescence."""
4087 4086 opts = _byteskwargs(opts)
4088 4087 timer, fm = gettimer(ui, opts)
4089 4088 repo = repo.unfiltered()
4090 4089
4091 4090 def getobs(name):
4092 4091 def d():
4093 4092 repo.invalidatevolatilesets()
4094 4093 if opts[b'clear_obsstore']:
4095 4094 clearfilecache(repo, b'obsstore')
4096 4095 obsolete.getrevs(repo, name)
4097 4096
4098 4097 return d
4099 4098
4100 4099 allobs = sorted(obsolete.cachefuncs)
4101 4100 if names:
4102 4101 allobs = [n for n in allobs if n in names]
4103 4102
4104 4103 for name in allobs:
4105 4104 timer(getobs(name), title=name)
4106 4105
4107 4106 def getfiltered(name):
4108 4107 def d():
4109 4108 repo.invalidatevolatilesets()
4110 4109 if opts[b'clear_obsstore']:
4111 4110 clearfilecache(repo, b'obsstore')
4112 4111 repoview.filterrevs(repo, name)
4113 4112
4114 4113 return d
4115 4114
4116 4115 allfilter = sorted(repoview.filtertable)
4117 4116 if names:
4118 4117 allfilter = [n for n in allfilter if n in names]
4119 4118
4120 4119 for name in allfilter:
4121 4120 timer(getfiltered(name), title=name)
4122 4121 fm.end()
4123 4122
4124 4123
4125 4124 @command(
4126 4125 b'perf::branchmap|perfbranchmap',
4127 4126 [
4128 4127 (b'f', b'full', False, b'Includes build time of subset'),
4129 4128 (
4130 4129 b'',
4131 4130 b'clear-revbranch',
4132 4131 False,
4133 4132 b'purge the revbranch cache between computation',
4134 4133 ),
4135 4134 ]
4136 4135 + formatteropts,
4137 4136 )
4138 4137 def perfbranchmap(ui, repo, *filternames, **opts):
4139 4138 """benchmark the update of a branchmap
4140 4139
4141 4140 This benchmarks the full repo.branchmap() call with read and write disabled
4142 4141 """
4143 4142 opts = _byteskwargs(opts)
4144 4143 full = opts.get(b"full", False)
4145 4144 clear_revbranch = opts.get(b"clear_revbranch", False)
4146 4145 timer, fm = gettimer(ui, opts)
4147 4146
4148 4147 def getbranchmap(filtername):
4149 4148 """generate a benchmark function for the filtername"""
4150 4149 if filtername is None:
4151 4150 view = repo
4152 4151 else:
4153 4152 view = repo.filtered(filtername)
4154 4153 if util.safehasattr(view._branchcaches, '_per_filter'):
4155 4154 filtered = view._branchcaches._per_filter
4156 4155 else:
4157 4156 # older versions
4158 4157 filtered = view._branchcaches
4159 4158
4160 4159 def d():
4161 4160 if clear_revbranch:
4162 4161 repo.revbranchcache()._clear()
4163 4162 if full:
4164 4163 view._branchcaches.clear()
4165 4164 else:
4166 4165 filtered.pop(filtername, None)
4167 4166 view.branchmap()
4168 4167
4169 4168 return d
4170 4169
4171 4170 # add filter in smaller subset to bigger subset
4172 4171 possiblefilters = set(repoview.filtertable)
4173 4172 if filternames:
4174 4173 possiblefilters &= set(filternames)
4175 4174 subsettable = getbranchmapsubsettable()
4176 4175 allfilters = []
4177 4176 while possiblefilters:
4178 4177 for name in possiblefilters:
4179 4178 subset = subsettable.get(name)
4180 4179 if subset not in possiblefilters:
4181 4180 break
4182 4181 else:
4183 4182 assert False, b'subset cycle %s!' % possiblefilters
4184 4183 allfilters.append(name)
4185 4184 possiblefilters.remove(name)
4186 4185
4187 4186 # warm the cache
4188 4187 if not full:
4189 4188 for name in allfilters:
4190 4189 repo.filtered(name).branchmap()
4191 4190 if not filternames or b'unfiltered' in filternames:
4192 4191 # add unfiltered
4193 4192 allfilters.append(None)
4194 4193
4195 4194 if util.safehasattr(branchmap.branchcache, 'fromfile'):
4196 4195 branchcacheread = safeattrsetter(branchmap.branchcache, b'fromfile')
4197 4196 branchcacheread.set(classmethod(lambda *args: None))
4198 4197 else:
4199 4198 # older versions
4200 4199 branchcacheread = safeattrsetter(branchmap, b'read')
4201 4200 branchcacheread.set(lambda *args: None)
4202 4201 branchcachewrite = safeattrsetter(branchmap.branchcache, b'write')
4203 4202 branchcachewrite.set(lambda *args: None)
4204 4203 try:
4205 4204 for name in allfilters:
4206 4205 printname = name
4207 4206 if name is None:
4208 4207 printname = b'unfiltered'
4209 4208 timer(getbranchmap(name), title=printname)
4210 4209 finally:
4211 4210 branchcacheread.restore()
4212 4211 branchcachewrite.restore()
4213 4212 fm.end()
4214 4213
4215 4214
4216 4215 @command(
4217 4216 b'perf::branchmapupdate|perfbranchmapupdate',
4218 4217 [
4219 4218 (b'', b'base', [], b'subset of revision to start from'),
4220 4219 (b'', b'target', [], b'subset of revision to end with'),
4221 4220 (b'', b'clear-caches', False, b'clear cache between each runs'),
4222 4221 ]
4223 4222 + formatteropts,
4224 4223 )
4225 4224 def perfbranchmapupdate(ui, repo, base=(), target=(), **opts):
4226 4225 """benchmark branchmap update from for <base> revs to <target> revs
4227 4226
4228 4227 If `--clear-caches` is passed, the following items will be reset before
4229 4228 each update:
4230 4229 * the changelog instance and associated indexes
4231 4230 * the rev-branch-cache instance
4232 4231
4233 4232 Examples:
4234 4233
4235 4234 # update for the one last revision
4236 4235 $ hg perfbranchmapupdate --base 'not tip' --target 'tip'
4237 4236
4238 4237 $ update for change coming with a new branch
4239 4238 $ hg perfbranchmapupdate --base 'stable' --target 'default'
4240 4239 """
4241 4240 from mercurial import branchmap
4242 4241 from mercurial import repoview
4243 4242
4244 4243 opts = _byteskwargs(opts)
4245 4244 timer, fm = gettimer(ui, opts)
4246 4245 clearcaches = opts[b'clear_caches']
4247 4246 unfi = repo.unfiltered()
4248 4247 x = [None] # used to pass data between closure
4249 4248
4250 4249 # we use a `list` here to avoid possible side effect from smartset
4251 4250 baserevs = list(scmutil.revrange(repo, base))
4252 4251 targetrevs = list(scmutil.revrange(repo, target))
4253 4252 if not baserevs:
4254 4253 raise error.Abort(b'no revisions selected for --base')
4255 4254 if not targetrevs:
4256 4255 raise error.Abort(b'no revisions selected for --target')
4257 4256
4258 4257 # make sure the target branchmap also contains the one in the base
4259 4258 targetrevs = list(set(baserevs) | set(targetrevs))
4260 4259 targetrevs.sort()
4261 4260
4262 4261 cl = repo.changelog
4263 4262 allbaserevs = list(cl.ancestors(baserevs, inclusive=True))
4264 4263 allbaserevs.sort()
4265 4264 alltargetrevs = frozenset(cl.ancestors(targetrevs, inclusive=True))
4266 4265
4267 4266 newrevs = list(alltargetrevs.difference(allbaserevs))
4268 4267 newrevs.sort()
4269 4268
4270 4269 allrevs = frozenset(unfi.changelog.revs())
4271 4270 basefilterrevs = frozenset(allrevs.difference(allbaserevs))
4272 4271 targetfilterrevs = frozenset(allrevs.difference(alltargetrevs))
4273 4272
4274 4273 def basefilter(repo, visibilityexceptions=None):
4275 4274 return basefilterrevs
4276 4275
4277 4276 def targetfilter(repo, visibilityexceptions=None):
4278 4277 return targetfilterrevs
4279 4278
4280 4279 msg = b'benchmark of branchmap with %d revisions with %d new ones\n'
4281 4280 ui.status(msg % (len(allbaserevs), len(newrevs)))
4282 4281 if targetfilterrevs:
4283 4282 msg = b'(%d revisions still filtered)\n'
4284 4283 ui.status(msg % len(targetfilterrevs))
4285 4284
4286 4285 try:
4287 4286 repoview.filtertable[b'__perf_branchmap_update_base'] = basefilter
4288 4287 repoview.filtertable[b'__perf_branchmap_update_target'] = targetfilter
4289 4288
4290 4289 baserepo = repo.filtered(b'__perf_branchmap_update_base')
4291 4290 targetrepo = repo.filtered(b'__perf_branchmap_update_target')
4292 4291
4293 4292 # try to find an existing branchmap to reuse
4294 4293 subsettable = getbranchmapsubsettable()
4295 4294 candidatefilter = subsettable.get(None)
4296 4295 while candidatefilter is not None:
4297 4296 candidatebm = repo.filtered(candidatefilter).branchmap()
4298 4297 if candidatebm.validfor(baserepo):
4299 4298 filtered = repoview.filterrevs(repo, candidatefilter)
4300 4299 missing = [r for r in allbaserevs if r in filtered]
4301 4300 base = candidatebm.copy()
4302 4301 base.update(baserepo, missing)
4303 4302 break
4304 4303 candidatefilter = subsettable.get(candidatefilter)
4305 4304 else:
4306 4305 # no suitable subset where found
4307 4306 base = branchmap.branchcache()
4308 4307 base.update(baserepo, allbaserevs)
4309 4308
4310 4309 def setup():
4311 4310 x[0] = base.copy()
4312 4311 if clearcaches:
4313 4312 unfi._revbranchcache = None
4314 4313 clearchangelog(repo)
4315 4314
4316 4315 def bench():
4317 4316 x[0].update(targetrepo, newrevs)
4318 4317
4319 4318 timer(bench, setup=setup)
4320 4319 fm.end()
4321 4320 finally:
4322 4321 repoview.filtertable.pop(b'__perf_branchmap_update_base', None)
4323 4322 repoview.filtertable.pop(b'__perf_branchmap_update_target', None)
4324 4323
4325 4324
4326 4325 @command(
4327 4326 b'perf::branchmapload|perfbranchmapload',
4328 4327 [
4329 4328 (b'f', b'filter', b'', b'Specify repoview filter'),
4330 4329 (b'', b'list', False, b'List brachmap filter caches'),
4331 4330 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
4332 4331 ]
4333 4332 + formatteropts,
4334 4333 )
4335 4334 def perfbranchmapload(ui, repo, filter=b'', list=False, **opts):
4336 4335 """benchmark reading the branchmap"""
4337 4336 opts = _byteskwargs(opts)
4338 4337 clearrevlogs = opts[b'clear_revlogs']
4339 4338
4340 4339 if list:
4341 4340 for name, kind, st in repo.cachevfs.readdir(stat=True):
4342 4341 if name.startswith(b'branch2'):
4343 4342 filtername = name.partition(b'-')[2] or b'unfiltered'
4344 4343 ui.status(
4345 4344 b'%s - %s\n' % (filtername, util.bytecount(st.st_size))
4346 4345 )
4347 4346 return
4348 4347 if not filter:
4349 4348 filter = None
4350 4349 subsettable = getbranchmapsubsettable()
4351 4350 if filter is None:
4352 4351 repo = repo.unfiltered()
4353 4352 else:
4354 4353 repo = repoview.repoview(repo, filter)
4355 4354
4356 4355 repo.branchmap() # make sure we have a relevant, up to date branchmap
4357 4356
4358 4357 try:
4359 4358 fromfile = branchmap.branchcache.fromfile
4360 4359 except AttributeError:
4361 4360 # older versions
4362 4361 fromfile = branchmap.read
4363 4362
4364 4363 currentfilter = filter
4365 4364 # try once without timer, the filter may not be cached
4366 4365 while fromfile(repo) is None:
4367 4366 currentfilter = subsettable.get(currentfilter)
4368 4367 if currentfilter is None:
4369 4368 raise error.Abort(
4370 4369 b'No branchmap cached for %s repo' % (filter or b'unfiltered')
4371 4370 )
4372 4371 repo = repo.filtered(currentfilter)
4373 4372 timer, fm = gettimer(ui, opts)
4374 4373
4375 4374 def setup():
4376 4375 if clearrevlogs:
4377 4376 clearchangelog(repo)
4378 4377
4379 4378 def bench():
4380 4379 fromfile(repo)
4381 4380
4382 4381 timer(bench, setup=setup)
4383 4382 fm.end()
4384 4383
4385 4384
4386 4385 @command(b'perf::loadmarkers|perfloadmarkers')
4387 4386 def perfloadmarkers(ui, repo):
4388 4387 """benchmark the time to parse the on-disk markers for a repo
4389 4388
4390 4389 Result is the number of markers in the repo."""
4391 4390 timer, fm = gettimer(ui)
4392 4391 svfs = getsvfs(repo)
4393 4392 timer(lambda: len(obsolete.obsstore(repo, svfs)))
4394 4393 fm.end()
4395 4394
4396 4395
4397 4396 @command(
4398 4397 b'perf::lrucachedict|perflrucachedict',
4399 4398 formatteropts
4400 4399 + [
4401 4400 (b'', b'costlimit', 0, b'maximum total cost of items in cache'),
4402 4401 (b'', b'mincost', 0, b'smallest cost of items in cache'),
4403 4402 (b'', b'maxcost', 100, b'maximum cost of items in cache'),
4404 4403 (b'', b'size', 4, b'size of cache'),
4405 4404 (b'', b'gets', 10000, b'number of key lookups'),
4406 4405 (b'', b'sets', 10000, b'number of key sets'),
4407 4406 (b'', b'mixed', 10000, b'number of mixed mode operations'),
4408 4407 (
4409 4408 b'',
4410 4409 b'mixedgetfreq',
4411 4410 50,
4412 4411 b'frequency of get vs set ops in mixed mode',
4413 4412 ),
4414 4413 ],
4415 4414 norepo=True,
4416 4415 )
4417 4416 def perflrucache(
4418 4417 ui,
4419 4418 mincost=0,
4420 4419 maxcost=100,
4421 4420 costlimit=0,
4422 4421 size=4,
4423 4422 gets=10000,
4424 4423 sets=10000,
4425 4424 mixed=10000,
4426 4425 mixedgetfreq=50,
4427 4426 **opts
4428 4427 ):
4429 4428 opts = _byteskwargs(opts)
4430 4429
4431 4430 def doinit():
4432 4431 for i in _xrange(10000):
4433 4432 util.lrucachedict(size)
4434 4433
4435 4434 costrange = list(range(mincost, maxcost + 1))
4436 4435
4437 4436 values = []
4438 4437 for i in _xrange(size):
4439 4438 values.append(random.randint(0, _maxint))
4440 4439
4441 4440 # Get mode fills the cache and tests raw lookup performance with no
4442 4441 # eviction.
4443 4442 getseq = []
4444 4443 for i in _xrange(gets):
4445 4444 getseq.append(random.choice(values))
4446 4445
4447 4446 def dogets():
4448 4447 d = util.lrucachedict(size)
4449 4448 for v in values:
4450 4449 d[v] = v
4451 4450 for key in getseq:
4452 4451 value = d[key]
4453 4452 value # silence pyflakes warning
4454 4453
4455 4454 def dogetscost():
4456 4455 d = util.lrucachedict(size, maxcost=costlimit)
4457 4456 for i, v in enumerate(values):
4458 4457 d.insert(v, v, cost=costs[i])
4459 4458 for key in getseq:
4460 4459 try:
4461 4460 value = d[key]
4462 4461 value # silence pyflakes warning
4463 4462 except KeyError:
4464 4463 pass
4465 4464
4466 4465 # Set mode tests insertion speed with cache eviction.
4467 4466 setseq = []
4468 4467 costs = []
4469 4468 for i in _xrange(sets):
4470 4469 setseq.append(random.randint(0, _maxint))
4471 4470 costs.append(random.choice(costrange))
4472 4471
4473 4472 def doinserts():
4474 4473 d = util.lrucachedict(size)
4475 4474 for v in setseq:
4476 4475 d.insert(v, v)
4477 4476
4478 4477 def doinsertscost():
4479 4478 d = util.lrucachedict(size, maxcost=costlimit)
4480 4479 for i, v in enumerate(setseq):
4481 4480 d.insert(v, v, cost=costs[i])
4482 4481
4483 4482 def dosets():
4484 4483 d = util.lrucachedict(size)
4485 4484 for v in setseq:
4486 4485 d[v] = v
4487 4486
4488 4487 # Mixed mode randomly performs gets and sets with eviction.
4489 4488 mixedops = []
4490 4489 for i in _xrange(mixed):
4491 4490 r = random.randint(0, 100)
4492 4491 if r < mixedgetfreq:
4493 4492 op = 0
4494 4493 else:
4495 4494 op = 1
4496 4495
4497 4496 mixedops.append(
4498 4497 (op, random.randint(0, size * 2), random.choice(costrange))
4499 4498 )
4500 4499
4501 4500 def domixed():
4502 4501 d = util.lrucachedict(size)
4503 4502
4504 4503 for op, v, cost in mixedops:
4505 4504 if op == 0:
4506 4505 try:
4507 4506 d[v]
4508 4507 except KeyError:
4509 4508 pass
4510 4509 else:
4511 4510 d[v] = v
4512 4511
4513 4512 def domixedcost():
4514 4513 d = util.lrucachedict(size, maxcost=costlimit)
4515 4514
4516 4515 for op, v, cost in mixedops:
4517 4516 if op == 0:
4518 4517 try:
4519 4518 d[v]
4520 4519 except KeyError:
4521 4520 pass
4522 4521 else:
4523 4522 d.insert(v, v, cost=cost)
4524 4523
4525 4524 benches = [
4526 4525 (doinit, b'init'),
4527 4526 ]
4528 4527
4529 4528 if costlimit:
4530 4529 benches.extend(
4531 4530 [
4532 4531 (dogetscost, b'gets w/ cost limit'),
4533 4532 (doinsertscost, b'inserts w/ cost limit'),
4534 4533 (domixedcost, b'mixed w/ cost limit'),
4535 4534 ]
4536 4535 )
4537 4536 else:
4538 4537 benches.extend(
4539 4538 [
4540 4539 (dogets, b'gets'),
4541 4540 (doinserts, b'inserts'),
4542 4541 (dosets, b'sets'),
4543 4542 (domixed, b'mixed'),
4544 4543 ]
4545 4544 )
4546 4545
4547 4546 for fn, title in benches:
4548 4547 timer, fm = gettimer(ui, opts)
4549 4548 timer(fn, title=title)
4550 4549 fm.end()
4551 4550
4552 4551
4553 4552 @command(
4554 4553 b'perf::write|perfwrite',
4555 4554 formatteropts
4556 4555 + [
4557 4556 (b'', b'write-method', b'write', b'ui write method'),
4558 4557 (b'', b'nlines', 100, b'number of lines'),
4559 4558 (b'', b'nitems', 100, b'number of items (per line)'),
4560 4559 (b'', b'item', b'x', b'item that is written'),
4561 4560 (b'', b'batch-line', None, b'pass whole line to write method at once'),
4562 4561 (b'', b'flush-line', None, b'flush after each line'),
4563 4562 ],
4564 4563 )
4565 4564 def perfwrite(ui, repo, **opts):
4566 4565 """microbenchmark ui.write (and others)"""
4567 4566 opts = _byteskwargs(opts)
4568 4567
4569 4568 write = getattr(ui, _sysstr(opts[b'write_method']))
4570 4569 nlines = int(opts[b'nlines'])
4571 4570 nitems = int(opts[b'nitems'])
4572 4571 item = opts[b'item']
4573 4572 batch_line = opts.get(b'batch_line')
4574 4573 flush_line = opts.get(b'flush_line')
4575 4574
4576 4575 if batch_line:
4577 4576 line = item * nitems + b'\n'
4578 4577
4579 4578 def benchmark():
4580 4579 for i in pycompat.xrange(nlines):
4581 4580 if batch_line:
4582 4581 write(line)
4583 4582 else:
4584 4583 for i in pycompat.xrange(nitems):
4585 4584 write(item)
4586 4585 write(b'\n')
4587 4586 if flush_line:
4588 4587 ui.flush()
4589 4588 ui.flush()
4590 4589
4591 4590 timer, fm = gettimer(ui, opts)
4592 4591 timer(benchmark)
4593 4592 fm.end()
4594 4593
4595 4594
4596 4595 def uisetup(ui):
4597 4596 if util.safehasattr(cmdutil, b'openrevlog') and not util.safehasattr(
4598 4597 commands, b'debugrevlogopts'
4599 4598 ):
4600 4599 # for "historical portability":
4601 4600 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
4602 4601 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
4603 4602 # openrevlog() should cause failure, because it has been
4604 4603 # available since 3.5 (or 49c583ca48c4).
4605 4604 def openrevlog(orig, repo, cmd, file_, opts):
4606 4605 if opts.get(b'dir') and not util.safehasattr(repo, b'dirlog'):
4607 4606 raise error.Abort(
4608 4607 b"This version doesn't support --dir option",
4609 4608 hint=b"use 3.5 or later",
4610 4609 )
4611 4610 return orig(repo, cmd, file_, opts)
4612 4611
4613 4612 name = _sysstr(b'openrevlog')
4614 4613 extensions.wrapfunction(cmdutil, name, openrevlog)
4615 4614
4616 4615
4617 4616 @command(
4618 4617 b'perf::progress|perfprogress',
4619 4618 formatteropts
4620 4619 + [
4621 4620 (b'', b'topic', b'topic', b'topic for progress messages'),
4622 4621 (b'c', b'total', 1000000, b'total value we are progressing to'),
4623 4622 ],
4624 4623 norepo=True,
4625 4624 )
4626 4625 def perfprogress(ui, topic=None, total=None, **opts):
4627 4626 """printing of progress bars"""
4628 4627 opts = _byteskwargs(opts)
4629 4628
4630 4629 timer, fm = gettimer(ui, opts)
4631 4630
4632 4631 def doprogress():
4633 4632 with ui.makeprogress(topic, total=total) as progress:
4634 4633 for i in _xrange(total):
4635 4634 progress.increment()
4636 4635
4637 4636 timer(doprogress)
4638 4637 fm.end()
@@ -1,2400 +1,2402 b''
1 1 # phabricator.py - simple Phabricator integration
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 """simple Phabricator integration (EXPERIMENTAL)
8 8
9 9 This extension provides a ``phabsend`` command which sends a stack of
10 10 changesets to Phabricator, and a ``phabread`` command which prints a stack of
11 11 revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
12 12 to update statuses in batch.
13 13
14 14 A "phabstatus" view for :hg:`show` is also provided; it displays status
15 15 information of Phabricator differentials associated with unfinished
16 16 changesets.
17 17
18 18 By default, Phabricator requires ``Test Plan`` which might prevent some
19 19 changeset from being sent. The requirement could be disabled by changing
20 20 ``differential.require-test-plan-field`` config server side.
21 21
22 22 Config::
23 23
24 24 [phabricator]
25 25 # Phabricator URL
26 26 url = https://phab.example.com/
27 27
28 28 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
29 29 # callsign is "FOO".
30 30 callsign = FOO
31 31
32 32 # curl command to use. If not set (default), use builtin HTTP library to
33 33 # communicate. If set, use the specified curl command. This could be useful
34 34 # if you need to specify advanced options that is not easily supported by
35 35 # the internal library.
36 36 curlcmd = curl --connect-timeout 2 --retry 3 --silent
37 37
38 38 # retry failed command N time (default 0). Useful when using the extension
39 39 # over flakly connection.
40 40 #
41 41 # We wait `retry.interval` between each retry, in seconds.
42 42 # (default 1 second).
43 43 retry = 3
44 44 retry.interval = 10
45 45
46 46 # the retry option can combine well with the http.timeout one.
47 47 #
48 48 # For example to give up on http request after 20 seconds:
49 49 [http]
50 50 timeout=20
51 51
52 52 [auth]
53 53 example.schemes = https
54 54 example.prefix = phab.example.com
55 55
56 56 # API token. Get it from https://$HOST/conduit/login/
57 57 example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
58 58 """
59 59
60 60
61 61 import base64
62 62 import contextlib
63 63 import hashlib
64 64 import io
65 65 import itertools
66 66 import json
67 67 import mimetypes
68 68 import operator
69 69 import re
70 70 import time
71 71
72 72 from mercurial.node import bin, short
73 73 from mercurial.i18n import _
74 74 from mercurial.thirdparty import attr
75 75 from mercurial import (
76 76 cmdutil,
77 77 context,
78 78 copies,
79 79 encoding,
80 80 error,
81 81 exthelper,
82 82 graphmod,
83 83 httpconnection as httpconnectionmod,
84 84 localrepo,
85 85 logcmdutil,
86 86 match,
87 87 mdiff,
88 88 obsutil,
89 89 parser,
90 90 patch,
91 91 phases,
92 92 pycompat,
93 93 rewriteutil,
94 94 scmutil,
95 95 smartset,
96 96 tags,
97 97 templatefilters,
98 98 templateutil,
99 99 url as urlmod,
100 100 util,
101 101 )
102 102 from mercurial.utils import (
103 103 procutil,
104 104 stringutil,
105 105 urlutil,
106 106 )
107 107 from . import show
108 108
109 109
110 110 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
111 111 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
112 112 # be specifying the version(s) of Mercurial they are tested with, or
113 113 # leave the attribute unspecified.
114 114 testedwith = b'ships-with-hg-core'
115 115
116 116 eh = exthelper.exthelper()
117 117
118 118 cmdtable = eh.cmdtable
119 119 command = eh.command
120 120 configtable = eh.configtable
121 121 templatekeyword = eh.templatekeyword
122 122 uisetup = eh.finaluisetup
123 123
124 124 # developer config: phabricator.batchsize
125 125 eh.configitem(
126 126 b'phabricator',
127 127 b'batchsize',
128 128 default=12,
129 129 )
130 130 eh.configitem(
131 131 b'phabricator',
132 132 b'callsign',
133 133 default=None,
134 134 )
135 135 eh.configitem(
136 136 b'phabricator',
137 137 b'curlcmd',
138 138 default=None,
139 139 )
140 140 # developer config: phabricator.debug
141 141 eh.configitem(
142 142 b'phabricator',
143 143 b'debug',
144 144 default=False,
145 145 )
146 146 # developer config: phabricator.repophid
147 147 eh.configitem(
148 148 b'phabricator',
149 149 b'repophid',
150 150 default=None,
151 151 )
152 152 eh.configitem(
153 153 b'phabricator',
154 154 b'retry',
155 155 default=0,
156 156 )
157 157 eh.configitem(
158 158 b'phabricator',
159 159 b'retry.interval',
160 160 default=1,
161 161 )
162 162 eh.configitem(
163 163 b'phabricator',
164 164 b'url',
165 165 default=None,
166 166 )
167 167 eh.configitem(
168 168 b'phabsend',
169 169 b'confirm',
170 170 default=False,
171 171 )
172 172 eh.configitem(
173 173 b'phabimport',
174 174 b'secret',
175 175 default=False,
176 176 )
177 177 eh.configitem(
178 178 b'phabimport',
179 179 b'obsolete',
180 180 default=False,
181 181 )
182 182
183 183 colortable = {
184 184 b'phabricator.action.created': b'green',
185 185 b'phabricator.action.skipped': b'magenta',
186 186 b'phabricator.action.updated': b'magenta',
187 187 b'phabricator.drev': b'bold',
188 188 b'phabricator.status.abandoned': b'magenta dim',
189 189 b'phabricator.status.accepted': b'green bold',
190 190 b'phabricator.status.closed': b'green',
191 191 b'phabricator.status.needsreview': b'yellow',
192 192 b'phabricator.status.needsrevision': b'red',
193 193 b'phabricator.status.changesplanned': b'red',
194 194 }
195 195
196 196 _VCR_FLAGS = [
197 197 (
198 198 b'',
199 199 b'test-vcr',
200 200 b'',
201 201 _(
202 202 b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
203 203 b', otherwise will mock all http requests using the specified vcr file.'
204 204 b' (ADVANCED)'
205 205 ),
206 206 ),
207 207 ]
208 208
209 209
210 210 @eh.wrapfunction(localrepo, "loadhgrc")
211 211 def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements, *args, **opts):
212 212 """Load ``.arcconfig`` content into a ui instance on repository open."""
213 213 result = False
214 214 arcconfig = {}
215 215
216 216 try:
217 217 # json.loads only accepts bytes from 3.6+
218 218 rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
219 219 # json.loads only returns unicode strings
220 220 arcconfig = pycompat.rapply(
221 221 lambda x: encoding.unitolocal(x) if isinstance(x, str) else x,
222 222 pycompat.json_loads(rawparams),
223 223 )
224 224
225 225 result = True
226 226 except ValueError:
227 227 ui.warn(_(b"invalid JSON in %s\n") % wdirvfs.join(b".arcconfig"))
228 228 except IOError:
229 229 pass
230 230
231 231 cfg = util.sortdict()
232 232
233 233 if b"repository.callsign" in arcconfig:
234 234 cfg[(b"phabricator", b"callsign")] = arcconfig[b"repository.callsign"]
235 235
236 236 if b"phabricator.uri" in arcconfig:
237 237 cfg[(b"phabricator", b"url")] = arcconfig[b"phabricator.uri"]
238 238
239 239 if cfg:
240 240 ui.applyconfig(cfg, source=wdirvfs.join(b".arcconfig"))
241 241
242 242 return (
243 243 orig(ui, wdirvfs, hgvfs, requirements, *args, **opts) or result
244 244 ) # Load .hg/hgrc
245 245
246 246
247 247 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
248 248 fullflags = flags + _VCR_FLAGS
249 249
250 250 def hgmatcher(r1, r2):
251 251 if r1.uri != r2.uri or r1.method != r2.method:
252 252 return False
253 253 r1params = util.urlreq.parseqs(r1.body)
254 254 r2params = util.urlreq.parseqs(r2.body)
255 255 for key in r1params:
256 256 if key not in r2params:
257 257 return False
258 258 value = r1params[key][0]
259 259 # we want to compare json payloads without worrying about ordering
260 260 if value.startswith(b'{') and value.endswith(b'}'):
261 261 r1json = pycompat.json_loads(value)
262 262 r2json = pycompat.json_loads(r2params[key][0])
263 263 if r1json != r2json:
264 264 return False
265 265 elif r2params[key][0] != value:
266 266 return False
267 267 return True
268 268
269 269 def sanitiserequest(request):
270 270 request.body = re.sub(
271 271 br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body
272 272 )
273 273 return request
274 274
275 275 def sanitiseresponse(response):
276 276 if 'set-cookie' in response['headers']:
277 277 del response['headers']['set-cookie']
278 278 return response
279 279
280 280 def decorate(fn):
281 281 def inner(*args, **kwargs):
282 282 vcr = kwargs.pop('test_vcr')
283 283 if vcr:
284 284 cassette = pycompat.fsdecode(vcr)
285 285 import hgdemandimport
286 286
287 287 with hgdemandimport.deactivated():
288 288 # pytype: disable=import-error
289 289 import vcr as vcrmod
290 290 import vcr.stubs as stubs
291 291
292 292 # pytype: enable=import-error
293 293
294 294 vcr = vcrmod.VCR(
295 295 serializer='json',
296 296 before_record_request=sanitiserequest,
297 297 before_record_response=sanitiseresponse,
298 298 custom_patches=[
299 299 (
300 300 urlmod,
301 301 'httpconnection',
302 302 stubs.VCRHTTPConnection,
303 303 ),
304 304 (
305 305 urlmod,
306 306 'httpsconnection',
307 307 stubs.VCRHTTPSConnection,
308 308 ),
309 309 ],
310 310 )
311 311 vcr.register_matcher('hgmatcher', hgmatcher)
312 312 with vcr.use_cassette(cassette, match_on=['hgmatcher']):
313 313 return fn(*args, **kwargs)
314 314 return fn(*args, **kwargs)
315 315
316 316 cmd = util.checksignature(inner, depth=2)
317 317 cmd.__name__ = fn.__name__
318 318 cmd.__doc__ = fn.__doc__
319 319
320 320 return command(
321 321 name,
322 322 fullflags,
323 323 spec,
324 324 helpcategory=helpcategory,
325 325 optionalrepo=optionalrepo,
326 326 )(cmd)
327 327
328 328 return decorate
329 329
330 330
331 331 def _debug(ui, *msg, **opts):
332 332 """write debug output for Phabricator if ``phabricator.debug`` is set
333 333
334 334 Specifically, this avoids dumping Conduit and HTTP auth chatter that is
335 335 printed with the --debug argument.
336 336 """
337 337 if ui.configbool(b"phabricator", b"debug"):
338 338 flag = ui.debugflag
339 339 try:
340 340 ui.debugflag = True
341 341 ui.write(*msg, **opts)
342 342 finally:
343 343 ui.debugflag = flag
344 344
345 345
346 346 def urlencodenested(params):
347 347 """like urlencode, but works with nested parameters.
348 348
349 349 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
350 350 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
351 351 urlencode. Note: the encoding is consistent with PHP's http_build_query.
352 352 """
353 353 flatparams = util.sortdict()
354 354
355 355 def process(prefix: bytes, obj):
356 356 if isinstance(obj, bool):
357 357 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
358 358 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
359 359 # .items() will only be called for a dict type
360 360 # pytype: disable=attribute-error
361 361 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
362 362 # pytype: enable=attribute-error
363 363 if items is None:
364 364 flatparams[prefix] = obj
365 365 else:
366 366 for k, v in items(obj):
367 367 if prefix:
368 368 process(b'%s[%s]' % (prefix, k), v)
369 369 else:
370 370 process(k, v)
371 371
372 372 process(b'', params)
373 373 return urlutil.urlreq.urlencode(flatparams)
374 374
375 375
376 376 def readurltoken(ui):
377 377 """return conduit url, token and make sure they exist
378 378
379 379 Currently read from [auth] config section. In the future, it might
380 380 make sense to read from .arcconfig and .arcrc as well.
381 381 """
382 382 url = ui.config(b'phabricator', b'url')
383 383 if not url:
384 384 raise error.Abort(
385 385 _(b'config %s.%s is required') % (b'phabricator', b'url')
386 386 )
387 387
388 388 res = httpconnectionmod.readauthforuri(ui, url, urlutil.url(url).user)
389 389 token = None
390 390
391 391 if res:
392 392 group, auth = res
393 393
394 394 ui.debug(b"using auth.%s.* for authentication\n" % group)
395 395
396 396 token = auth.get(b'phabtoken')
397 397
398 398 if not token:
399 399 raise error.Abort(
400 400 _(b'Can\'t find conduit token associated to %s') % (url,)
401 401 )
402 402
403 403 return url, token
404 404
405 405
406 406 def callconduit(ui, name, params):
407 407 """call Conduit API, params is a dict. return json.loads result, or None"""
408 408 host, token = readurltoken(ui)
409 409 url, authinfo = urlutil.url(b'/'.join([host, b'api', name])).authinfo()
410 410 ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
411 411 params = params.copy()
412 412 params[b'__conduit__'] = {
413 413 b'token': token,
414 414 }
415 415 rawdata = {
416 416 b'params': templatefilters.json(params),
417 417 b'output': b'json',
418 418 b'__conduit__': 1,
419 419 }
420 420 data = urlencodenested(rawdata)
421 421 curlcmd = ui.config(b'phabricator', b'curlcmd')
422 422 if curlcmd:
423 423 sin, sout = procutil.popen2(
424 424 b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))
425 425 )
426 426 sin.write(data)
427 427 sin.close()
428 428 body = sout.read()
429 429 else:
430 430 urlopener = urlmod.opener(ui, authinfo)
431 431 request = util.urlreq.request(pycompat.strurl(url), data=data)
432 432 max_try = ui.configint(b'phabricator', b'retry') + 1
433 433 timeout = ui.configwith(float, b'http', b'timeout')
434 434 for try_count in range(max_try):
435 435 try:
436 436 with contextlib.closing(
437 437 urlopener.open(request, timeout=timeout)
438 438 ) as rsp:
439 439 body = rsp.read()
440 440 break
441 441 except util.urlerr.urlerror as err:
442 442 if try_count == max_try - 1:
443 443 raise
444 444 ui.debug(
445 445 b'Conduit Request failed (try %d/%d): %r\n'
446 446 % (try_count + 1, max_try, err)
447 447 )
448 448 # failing request might come from overloaded server
449 449 retry_interval = ui.configint(b'phabricator', b'retry.interval')
450 450 time.sleep(retry_interval)
451 451 ui.debug(b'Conduit Response: %s\n' % body)
452 452 parsed = pycompat.rapply(
453 453 lambda x: encoding.unitolocal(x) if isinstance(x, str) else x,
454 454 # json.loads only accepts bytes from py3.6+
455 455 pycompat.json_loads(encoding.unifromlocal(body)),
456 456 )
457 457 if parsed.get(b'error_code'):
458 458 msg = _(b'Conduit Error (%s): %s') % (
459 459 parsed[b'error_code'],
460 460 parsed[b'error_info'],
461 461 )
462 462 raise error.Abort(msg)
463 463 return parsed[b'result']
464 464
465 465
466 466 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
467 467 def debugcallconduit(ui, repo, name):
468 468 """call Conduit API
469 469
470 470 Call parameters are read from stdin as a JSON blob. Result will be written
471 471 to stdout as a JSON blob.
472 472 """
473 473 # json.loads only accepts bytes from 3.6+
474 474 rawparams = encoding.unifromlocal(ui.fin.read())
475 475 # json.loads only returns unicode strings
476 476 params = pycompat.rapply(
477 477 lambda x: encoding.unitolocal(x) if isinstance(x, str) else x,
478 478 pycompat.json_loads(rawparams),
479 479 )
480 480 # json.dumps only accepts unicode strings
481 481 result = pycompat.rapply(
482 482 lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,
483 483 callconduit(ui, name, params),
484 484 )
485 485 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
486 486 ui.write(b'%s\n' % encoding.unitolocal(s))
487 487
488 488
489 489 def getrepophid(repo):
490 490 """given callsign, return repository PHID or None"""
491 491 # developer config: phabricator.repophid
492 492 repophid = repo.ui.config(b'phabricator', b'repophid')
493 493 if repophid:
494 494 return repophid
495 495 callsign = repo.ui.config(b'phabricator', b'callsign')
496 496 if not callsign:
497 497 return None
498 498 query = callconduit(
499 499 repo.ui,
500 500 b'diffusion.repository.search',
501 501 {b'constraints': {b'callsigns': [callsign]}},
502 502 )
503 503 if len(query[b'data']) == 0:
504 504 return None
505 505 repophid = query[b'data'][0][b'phid']
506 506 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
507 507 return repophid
508 508
509 509
510 510 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
511 511 _differentialrevisiondescre = re.compile(
512 512 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M
513 513 )
514 514
515 515
516 516 def getoldnodedrevmap(repo, nodelist):
517 517 """find previous nodes that has been sent to Phabricator
518 518
519 519 return {node: (oldnode, Differential diff, Differential Revision ID)}
520 520 for node in nodelist with known previous sent versions, or associated
521 521 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
522 522 be ``None``.
523 523
524 524 Examines commit messages like "Differential Revision:" to get the
525 525 association information.
526 526
527 527 If such commit message line is not found, examines all precursors and their
528 528 tags. Tags with format like "D1234" are considered a match and the node
529 529 with that tag, and the number after "D" (ex. 1234) will be returned.
530 530
531 531 The ``old node``, if not None, is guaranteed to be the last diff of
532 532 corresponding Differential Revision, and exist in the repo.
533 533 """
534 534 unfi = repo.unfiltered()
535 535 has_node = unfi.changelog.index.has_node
536 536
537 537 result = {} # {node: (oldnode?, lastdiff?, drev)}
538 538 # ordered for test stability when printing new -> old mapping below
539 539 toconfirm = util.sortdict() # {node: (force, {precnode}, drev)}
540 540 for node in nodelist:
541 541 ctx = unfi[node]
542 542 # For tags like "D123", put them into "toconfirm" to verify later
543 543 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
544 544 for n in precnodes:
545 545 if has_node(n):
546 546 for tag in unfi.nodetags(n):
547 547 m = _differentialrevisiontagre.match(tag)
548 548 if m:
549 549 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
550 550 break
551 551 else:
552 552 continue # move to next predecessor
553 553 break # found a tag, stop
554 554 else:
555 555 # Check commit message
556 556 m = _differentialrevisiondescre.search(ctx.description())
557 557 if m:
558 558 toconfirm[node] = (1, set(precnodes), int(m.group('id')))
559 559
560 560 # Double check if tags are genuine by collecting all old nodes from
561 561 # Phabricator, and expect precursors overlap with it.
562 562 if toconfirm:
563 563 drevs = [drev for force, precs, drev in toconfirm.values()]
564 564 alldiffs = callconduit(
565 565 unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}
566 566 )
567 567
568 568 def getnodes(d, precset):
569 569 # Ignore other nodes that were combined into the Differential
570 570 # that aren't predecessors of the current local node.
571 571 return [n for n in getlocalcommits(d) if n in precset]
572 572
573 573 for newnode, (force, precset, drev) in toconfirm.items():
574 574 diffs = [
575 575 d for d in alldiffs.values() if int(d[b'revisionID']) == drev
576 576 ]
577 577
578 578 # local predecessors known by Phabricator
579 579 phprecset = {n for d in diffs for n in getnodes(d, precset)}
580 580
581 581 # Ignore if precursors (Phabricator and local repo) do not overlap,
582 582 # and force is not set (when commit message says nothing)
583 583 if not force and not phprecset:
584 584 tagname = b'D%d' % drev
585 585 tags.tag(
586 586 repo,
587 587 tagname,
588 588 repo.nullid,
589 589 message=None,
590 590 user=None,
591 591 date=None,
592 592 local=True,
593 593 )
594 594 unfi.ui.warn(
595 595 _(
596 596 b'D%d: local tag removed - does not match '
597 597 b'Differential history\n'
598 598 )
599 599 % drev
600 600 )
601 601 continue
602 602
603 603 # Find the last node using Phabricator metadata, and make sure it
604 604 # exists in the repo
605 605 oldnode = lastdiff = None
606 606 if diffs:
607 607 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
608 608 oldnodes = getnodes(lastdiff, precset)
609 609
610 610 _debug(
611 611 unfi.ui,
612 612 b"%s mapped to old nodes %s\n"
613 613 % (
614 614 short(newnode),
615 615 stringutil.pprint([short(n) for n in sorted(oldnodes)]),
616 616 ),
617 617 )
618 618
619 619 # If this commit was the result of `hg fold` after submission,
620 620 # and now resubmitted with --fold, the easiest thing to do is
621 621 # to leave the node clear. This only results in creating a new
622 622 # diff for the _same_ Differential Revision if this commit is
623 623 # the first or last in the selected range. If we picked a node
624 624 # from the list instead, it would have to be the lowest if at
625 625 # the beginning of the --fold range, or the highest at the end.
626 626 # Otherwise, one or more of the nodes wouldn't be considered in
627 627 # the diff, and the Differential wouldn't be properly updated.
628 628 # If this commit is the result of `hg split` in the same
629 629 # scenario, there is a single oldnode here (and multiple
630 630 # newnodes mapped to it). That makes it the same as the normal
631 631 # case, as the edges of the newnode range cleanly maps to one
632 632 # oldnode each.
633 633 if len(oldnodes) == 1:
634 634 oldnode = oldnodes[0]
635 635 if oldnode and not has_node(oldnode):
636 636 oldnode = None
637 637
638 638 result[newnode] = (oldnode, lastdiff, drev)
639 639
640 640 return result
641 641
642 642
643 643 def getdrevmap(repo, revs):
644 644 """Return a dict mapping each rev in `revs` to their Differential Revision
645 645 ID or None.
646 646 """
647 647 result = {}
648 648 for rev in revs:
649 649 result[rev] = None
650 650 ctx = repo[rev]
651 651 # Check commit message
652 652 m = _differentialrevisiondescre.search(ctx.description())
653 653 if m:
654 654 result[rev] = int(m.group('id'))
655 655 continue
656 656 # Check tags
657 657 for tag in repo.nodetags(ctx.node()):
658 658 m = _differentialrevisiontagre.match(tag)
659 659 if m:
660 660 result[rev] = int(m.group(1))
661 661 break
662 662
663 663 return result
664 664
665 665
666 666 def getdiff(basectx, ctx, diffopts):
667 667 """plain-text diff without header (user, commit message, etc)"""
668 668 output = util.stringio()
669 669 for chunk, _label in patch.diffui(
670 670 ctx.repo(), basectx.p1().node(), ctx.node(), None, opts=diffopts
671 671 ):
672 672 output.write(chunk)
673 673 return output.getvalue()
674 674
675 675
676 676 class DiffChangeType:
677 677 ADD = 1
678 678 CHANGE = 2
679 679 DELETE = 3
680 680 MOVE_AWAY = 4
681 681 COPY_AWAY = 5
682 682 MOVE_HERE = 6
683 683 COPY_HERE = 7
684 684 MULTICOPY = 8
685 685
686 686
687 687 class DiffFileType:
688 688 TEXT = 1
689 689 IMAGE = 2
690 690 BINARY = 3
691 691
692 692
693 693 @attr.s
694 694 class phabhunk(dict):
695 695 """Represents a Differential hunk, which is owned by a Differential change"""
696 696
697 697 oldOffset = attr.ib(default=0) # camelcase-required
698 698 oldLength = attr.ib(default=0) # camelcase-required
699 699 newOffset = attr.ib(default=0) # camelcase-required
700 700 newLength = attr.ib(default=0) # camelcase-required
701 701 corpus = attr.ib(default='')
702 702 # These get added to the phabchange's equivalents
703 703 addLines = attr.ib(default=0) # camelcase-required
704 704 delLines = attr.ib(default=0) # camelcase-required
705 705
706 706
707 707 @attr.s
708 708 class phabchange:
709 709 """Represents a Differential change, owns Differential hunks and owned by a
710 710 Differential diff. Each one represents one file in a diff.
711 711 """
712 712
713 713 currentPath = attr.ib(default=None) # camelcase-required
714 714 oldPath = attr.ib(default=None) # camelcase-required
715 715 awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required
716 716 metadata = attr.ib(default=attr.Factory(dict))
717 717 oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
718 718 newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
719 719 type = attr.ib(default=DiffChangeType.CHANGE)
720 720 fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required
721 721 commitHash = attr.ib(default=None) # camelcase-required
722 722 addLines = attr.ib(default=0) # camelcase-required
723 723 delLines = attr.ib(default=0) # camelcase-required
724 724 hunks = attr.ib(default=attr.Factory(list))
725 725
726 726 def copynewmetadatatoold(self):
727 727 for key in list(self.metadata.keys()):
728 728 newkey = key.replace(b'new:', b'old:')
729 729 self.metadata[newkey] = self.metadata[key]
730 730
731 731 def addoldmode(self, value):
732 732 self.oldProperties[b'unix:filemode'] = value
733 733
734 734 def addnewmode(self, value):
735 735 self.newProperties[b'unix:filemode'] = value
736 736
737 737 def addhunk(self, hunk):
738 738 if not isinstance(hunk, phabhunk):
739 739 raise error.Abort(b'phabchange.addhunk only takes phabhunks')
740 740 self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))
741 741 # It's useful to include these stats since the Phab web UI shows them,
742 742 # and uses them to estimate how large a change a Revision is. Also used
743 743 # in email subjects for the [+++--] bit.
744 744 self.addLines += hunk.addLines
745 745 self.delLines += hunk.delLines
746 746
747 747
748 748 @attr.s
749 749 class phabdiff:
750 750 """Represents a Differential diff, owns Differential changes. Corresponds
751 751 to a commit.
752 752 """
753 753
754 754 # Doesn't seem to be any reason to send this (output of uname -n)
755 755 sourceMachine = attr.ib(default=b'') # camelcase-required
756 756 sourcePath = attr.ib(default=b'/') # camelcase-required
757 757 sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required
758 758 sourceControlPath = attr.ib(default=b'/') # camelcase-required
759 759 sourceControlSystem = attr.ib(default=b'hg') # camelcase-required
760 760 branch = attr.ib(default=b'default')
761 761 bookmark = attr.ib(default=None)
762 762 creationMethod = attr.ib(default=b'phabsend') # camelcase-required
763 763 lintStatus = attr.ib(default=b'none') # camelcase-required
764 764 unitStatus = attr.ib(default=b'none') # camelcase-required
765 765 changes = attr.ib(default=attr.Factory(dict))
766 766 repositoryPHID = attr.ib(default=None) # camelcase-required
767 767
768 768 def addchange(self, change):
769 769 if not isinstance(change, phabchange):
770 770 raise error.Abort(b'phabdiff.addchange only takes phabchanges')
771 771 self.changes[change.currentPath] = pycompat.byteskwargs(
772 772 attr.asdict(change)
773 773 )
774 774
775 775
776 776 def maketext(pchange, basectx, ctx, fname):
777 777 """populate the phabchange for a text file"""
778 778 repo = ctx.repo()
779 779 fmatcher = match.exact([fname])
780 780 diffopts = mdiff.diffopts(git=True, context=32767)
781 781 _pfctx, _fctx, header, fhunks = next(
782 782 patch.diffhunks(repo, basectx.p1(), ctx, fmatcher, opts=diffopts)
783 783 )
784 784
785 785 for fhunk in fhunks:
786 786 (oldOffset, oldLength, newOffset, newLength), lines = fhunk
787 787 corpus = b''.join(lines[1:])
788 788 shunk = list(header)
789 789 shunk.extend(lines)
790 790 _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(
791 791 patch.diffstatdata(util.iterlines(shunk))
792 792 )
793 793 pchange.addhunk(
794 794 phabhunk(
795 795 oldOffset,
796 796 oldLength,
797 797 newOffset,
798 798 newLength,
799 799 corpus,
800 800 addLines,
801 801 delLines,
802 802 )
803 803 )
804 804
805 805
806 806 def uploadchunks(fctx, fphid):
807 807 """upload large binary files as separate chunks.
808 808 Phab requests chunking over 8MiB, and splits into 4MiB chunks
809 809 """
810 810 ui = fctx.repo().ui
811 811 chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})
812 812 with ui.makeprogress(
813 813 _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)
814 814 ) as progress:
815 815 for chunk in chunks:
816 816 progress.increment()
817 817 if chunk[b'complete']:
818 818 continue
819 819 bstart = int(chunk[b'byteStart'])
820 820 bend = int(chunk[b'byteEnd'])
821 821 callconduit(
822 822 ui,
823 823 b'file.uploadchunk',
824 824 {
825 825 b'filePHID': fphid,
826 826 b'byteStart': bstart,
827 827 b'data': base64.b64encode(fctx.data()[bstart:bend]),
828 828 b'dataEncoding': b'base64',
829 829 },
830 830 )
831 831
832 832
833 833 def uploadfile(fctx):
834 834 """upload binary files to Phabricator"""
835 835 repo = fctx.repo()
836 836 ui = repo.ui
837 837 fname = fctx.path()
838 838 size = fctx.size()
839 839 fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())
840 840
841 841 # an allocate call is required first to see if an upload is even required
842 842 # (Phab might already have it) and to determine if chunking is needed
843 843 allocateparams = {
844 844 b'name': fname,
845 845 b'contentLength': size,
846 846 b'contentHash': fhash,
847 847 }
848 848 filealloc = callconduit(ui, b'file.allocate', allocateparams)
849 849 fphid = filealloc[b'filePHID']
850 850
851 851 if filealloc[b'upload']:
852 852 ui.write(_(b'uploading %s\n') % bytes(fctx))
853 853 if not fphid:
854 854 uploadparams = {
855 855 b'name': fname,
856 856 b'data_base64': base64.b64encode(fctx.data()),
857 857 }
858 858 fphid = callconduit(ui, b'file.upload', uploadparams)
859 859 else:
860 860 uploadchunks(fctx, fphid)
861 861 else:
862 862 ui.debug(b'server already has %s\n' % bytes(fctx))
863 863
864 864 if not fphid:
865 865 raise error.Abort(b'Upload of %s failed.' % bytes(fctx))
866 866
867 867 return fphid
868 868
869 869
870 870 def addoldbinary(pchange, oldfctx, fctx):
871 871 """add the metadata for the previous version of a binary file to the
872 872 phabchange for the new version
873 873
874 874 ``oldfctx`` is the previous version of the file; ``fctx`` is the new
875 875 version of the file, or None if the file is being removed.
876 876 """
877 877 if not fctx or fctx.cmp(oldfctx):
878 878 # Files differ, add the old one
879 879 pchange.metadata[b'old:file:size'] = oldfctx.size()
880 880 mimeguess, _enc = mimetypes.guess_type(
881 881 encoding.unifromlocal(oldfctx.path())
882 882 )
883 883 if mimeguess:
884 884 pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(
885 885 mimeguess
886 886 )
887 887 fphid = uploadfile(oldfctx)
888 888 pchange.metadata[b'old:binary-phid'] = fphid
889 889 else:
890 890 # If it's left as IMAGE/BINARY web UI might try to display it
891 891 pchange.fileType = DiffFileType.TEXT
892 892 pchange.copynewmetadatatoold()
893 893
894 894
895 895 def makebinary(pchange, fctx):
896 896 """populate the phabchange for a binary file"""
897 897 pchange.fileType = DiffFileType.BINARY
898 898 fphid = uploadfile(fctx)
899 899 pchange.metadata[b'new:binary-phid'] = fphid
900 900 pchange.metadata[b'new:file:size'] = fctx.size()
901 901 mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))
902 902 if mimeguess:
903 903 mimeguess = pycompat.bytestr(mimeguess)
904 904 pchange.metadata[b'new:file:mime-type'] = mimeguess
905 905 if mimeguess.startswith(b'image/'):
906 906 pchange.fileType = DiffFileType.IMAGE
907 907
908 908
909 909 # Copied from mercurial/patch.py
910 910 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
911 911
912 912
913 913 def notutf8(fctx):
914 914 """detect non-UTF-8 text files since Phabricator requires them to be marked
915 915 as binary
916 916 """
917 917 try:
918 918 fctx.data().decode('utf-8')
919 919 return False
920 920 except UnicodeDecodeError:
921 921 fctx.repo().ui.write(
922 922 _(b'file %s detected as non-UTF-8, marked as binary\n')
923 923 % fctx.path()
924 924 )
925 925 return True
926 926
927 927
928 928 def addremoved(pdiff, basectx, ctx, removed):
929 929 """add removed files to the phabdiff. Shouldn't include moves"""
930 930 for fname in removed:
931 931 pchange = phabchange(
932 932 currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE
933 933 )
934 934 oldfctx = basectx.p1()[fname]
935 935 pchange.addoldmode(gitmode[oldfctx.flags()])
936 936 if not (oldfctx.isbinary() or notutf8(oldfctx)):
937 937 maketext(pchange, basectx, ctx, fname)
938 938
939 939 pdiff.addchange(pchange)
940 940
941 941
942 942 def addmodified(pdiff, basectx, ctx, modified):
943 943 """add modified files to the phabdiff"""
944 944 for fname in modified:
945 945 fctx = ctx[fname]
946 946 oldfctx = basectx.p1()[fname]
947 947 pchange = phabchange(currentPath=fname, oldPath=fname)
948 948 filemode = gitmode[fctx.flags()]
949 949 originalmode = gitmode[oldfctx.flags()]
950 950 if filemode != originalmode:
951 951 pchange.addoldmode(originalmode)
952 952 pchange.addnewmode(filemode)
953 953
954 954 if (
955 955 fctx.isbinary()
956 956 or notutf8(fctx)
957 957 or oldfctx.isbinary()
958 958 or notutf8(oldfctx)
959 959 ):
960 960 makebinary(pchange, fctx)
961 961 addoldbinary(pchange, oldfctx, fctx)
962 962 else:
963 963 maketext(pchange, basectx, ctx, fname)
964 964
965 965 pdiff.addchange(pchange)
966 966
967 967
968 968 def addadded(pdiff, basectx, ctx, added, removed):
969 969 """add file adds to the phabdiff, both new files and copies/moves"""
970 970 # Keep track of files that've been recorded as moved/copied, so if there are
971 971 # additional copies we can mark them (moves get removed from removed)
972 972 copiedchanges = {}
973 973 movedchanges = {}
974 974
975 975 copy = {}
976 976 if basectx != ctx:
977 977 copy = copies.pathcopies(basectx.p1(), ctx)
978 978
979 979 for fname in added:
980 980 fctx = ctx[fname]
981 981 oldfctx = None
982 982 pchange = phabchange(currentPath=fname)
983 983
984 984 filemode = gitmode[fctx.flags()]
985 985
986 986 if copy:
987 987 originalfname = copy.get(fname, fname)
988 988 else:
989 989 originalfname = fname
990 990 if fctx.renamed():
991 991 originalfname = fctx.renamed()[0]
992 992
993 993 renamed = fname != originalfname
994 994
995 995 if renamed:
996 996 oldfctx = basectx.p1()[originalfname]
997 997 originalmode = gitmode[oldfctx.flags()]
998 998 pchange.oldPath = originalfname
999 999
1000 1000 if originalfname in removed:
1001 1001 origpchange = phabchange(
1002 1002 currentPath=originalfname,
1003 1003 oldPath=originalfname,
1004 1004 type=DiffChangeType.MOVE_AWAY,
1005 1005 awayPaths=[fname],
1006 1006 )
1007 1007 movedchanges[originalfname] = origpchange
1008 1008 removed.remove(originalfname)
1009 1009 pchange.type = DiffChangeType.MOVE_HERE
1010 1010 elif originalfname in movedchanges:
1011 1011 movedchanges[originalfname].type = DiffChangeType.MULTICOPY
1012 1012 movedchanges[originalfname].awayPaths.append(fname)
1013 1013 pchange.type = DiffChangeType.COPY_HERE
1014 1014 else: # pure copy
1015 1015 if originalfname not in copiedchanges:
1016 1016 origpchange = phabchange(
1017 1017 currentPath=originalfname, type=DiffChangeType.COPY_AWAY
1018 1018 )
1019 1019 copiedchanges[originalfname] = origpchange
1020 1020 else:
1021 1021 origpchange = copiedchanges[originalfname]
1022 1022 origpchange.awayPaths.append(fname)
1023 1023 pchange.type = DiffChangeType.COPY_HERE
1024 1024
1025 1025 if filemode != originalmode:
1026 1026 pchange.addoldmode(originalmode)
1027 1027 pchange.addnewmode(filemode)
1028 1028 else: # Brand-new file
1029 1029 pchange.addnewmode(gitmode[fctx.flags()])
1030 1030 pchange.type = DiffChangeType.ADD
1031 1031
1032 1032 if (
1033 1033 fctx.isbinary()
1034 1034 or notutf8(fctx)
1035 1035 or (oldfctx and (oldfctx.isbinary() or notutf8(oldfctx)))
1036 1036 ):
1037 1037 makebinary(pchange, fctx)
1038 1038 if renamed:
1039 1039 addoldbinary(pchange, oldfctx, fctx)
1040 1040 else:
1041 1041 maketext(pchange, basectx, ctx, fname)
1042 1042
1043 1043 pdiff.addchange(pchange)
1044 1044
1045 1045 for _path, copiedchange in copiedchanges.items():
1046 1046 pdiff.addchange(copiedchange)
1047 1047 for _path, movedchange in movedchanges.items():
1048 1048 pdiff.addchange(movedchange)
1049 1049
1050 1050
1051 1051 def creatediff(basectx, ctx):
1052 1052 """create a Differential Diff"""
1053 1053 repo = ctx.repo()
1054 1054 repophid = getrepophid(repo)
1055 1055 # Create a "Differential Diff" via "differential.creatediff" API
1056 1056 pdiff = phabdiff(
1057 1057 sourceControlBaseRevision=b'%s' % basectx.p1().hex(),
1058 1058 branch=b'%s' % ctx.branch(),
1059 1059 )
1060 1060 modified, added, removed, _d, _u, _i, _c = basectx.p1().status(ctx)
1061 1061 # addadded will remove moved files from removed, so addremoved won't get
1062 1062 # them
1063 1063 addadded(pdiff, basectx, ctx, added, removed)
1064 1064 addmodified(pdiff, basectx, ctx, modified)
1065 1065 addremoved(pdiff, basectx, ctx, removed)
1066 1066 if repophid:
1067 1067 pdiff.repositoryPHID = repophid
1068 1068 diff = callconduit(
1069 1069 repo.ui,
1070 1070 b'differential.creatediff',
1071 1071 pycompat.byteskwargs(attr.asdict(pdiff)),
1072 1072 )
1073 1073 if not diff:
1074 1074 if basectx != ctx:
1075 1075 msg = _(b'cannot create diff for %s::%s') % (basectx, ctx)
1076 1076 else:
1077 1077 msg = _(b'cannot create diff for %s') % ctx
1078 1078 raise error.Abort(msg)
1079 1079 return diff
1080 1080
1081 1081
1082 1082 def writediffproperties(ctxs, diff):
1083 1083 """write metadata to diff so patches could be applied losslessly
1084 1084
1085 1085 ``ctxs`` is the list of commits that created the diff, in ascending order.
1086 1086 The list is generally a single commit, but may be several when using
1087 1087 ``phabsend --fold``.
1088 1088 """
1089 1089 # creatediff returns with a diffid but query returns with an id
1090 1090 diffid = diff.get(b'diffid', diff.get(b'id'))
1091 1091 basectx = ctxs[0]
1092 1092 tipctx = ctxs[-1]
1093 1093
1094 1094 params = {
1095 1095 b'diff_id': diffid,
1096 1096 b'name': b'hg:meta',
1097 1097 b'data': templatefilters.json(
1098 1098 {
1099 1099 b'user': tipctx.user(),
1100 1100 b'date': b'%d %d' % tipctx.date(),
1101 1101 b'branch': tipctx.branch(),
1102 1102 b'node': tipctx.hex(),
1103 1103 b'parent': basectx.p1().hex(),
1104 1104 }
1105 1105 ),
1106 1106 }
1107 1107 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1108 1108
1109 1109 commits = {}
1110 1110 for ctx in ctxs:
1111 1111 commits[ctx.hex()] = {
1112 1112 b'author': stringutil.person(ctx.user()),
1113 1113 b'authorEmail': stringutil.email(ctx.user()),
1114 1114 b'time': int(ctx.date()[0]),
1115 1115 b'commit': ctx.hex(),
1116 1116 b'parents': [ctx.p1().hex()],
1117 1117 b'branch': ctx.branch(),
1118 1118 }
1119 1119 params = {
1120 1120 b'diff_id': diffid,
1121 1121 b'name': b'local:commits',
1122 1122 b'data': templatefilters.json(commits),
1123 1123 }
1124 1124 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1125 1125
1126 1126
1127 1127 def createdifferentialrevision(
1128 1128 ctxs,
1129 1129 revid=None,
1130 1130 parentrevphid=None,
1131 1131 oldbasenode=None,
1132 1132 oldnode=None,
1133 1133 olddiff=None,
1134 1134 actions=None,
1135 1135 comment=None,
1136 1136 ):
1137 1137 """create or update a Differential Revision
1138 1138
1139 1139 If revid is None, create a new Differential Revision, otherwise update
1140 1140 revid. If parentrevphid is not None, set it as a dependency.
1141 1141
1142 1142 If there is a single commit for the new Differential Revision, ``ctxs`` will
1143 1143 be a list of that single context. Otherwise, it is a list that covers the
1144 1144 range of changes for the differential, where ``ctxs[0]`` is the first change
1145 1145 to include and ``ctxs[-1]`` is the last.
1146 1146
1147 1147 If oldnode is not None, check if the patch content (without commit message
1148 1148 and metadata) has changed before creating another diff. For a Revision with
1149 1149 a single commit, ``oldbasenode`` and ``oldnode`` have the same value. For a
1150 1150 Revision covering multiple commits, ``oldbasenode`` corresponds to
1151 1151 ``ctxs[0]`` the previous time this Revision was posted, and ``oldnode``
1152 1152 corresponds to ``ctxs[-1]``.
1153 1153
1154 1154 If actions is not None, they will be appended to the transaction.
1155 1155 """
1156 1156 ctx = ctxs[-1]
1157 1157 basectx = ctxs[0]
1158 1158
1159 1159 repo = ctx.repo()
1160 1160 if oldnode:
1161 1161 diffopts = mdiff.diffopts(git=True, context=32767)
1162 1162 unfi = repo.unfiltered()
1163 1163 oldctx = unfi[oldnode]
1164 1164 oldbasectx = unfi[oldbasenode]
1165 1165 neednewdiff = getdiff(basectx, ctx, diffopts) != getdiff(
1166 1166 oldbasectx, oldctx, diffopts
1167 1167 )
1168 1168 else:
1169 1169 neednewdiff = True
1170 1170
1171 1171 transactions = []
1172 1172 if neednewdiff:
1173 1173 diff = creatediff(basectx, ctx)
1174 1174 transactions.append({b'type': b'update', b'value': diff[b'phid']})
1175 1175 if comment:
1176 1176 transactions.append({b'type': b'comment', b'value': comment})
1177 1177 else:
1178 1178 # Even if we don't need to upload a new diff because the patch content
1179 1179 # does not change. We might still need to update its metadata so
1180 1180 # pushers could know the correct node metadata.
1181 1181 assert olddiff
1182 1182 diff = olddiff
1183 1183 writediffproperties(ctxs, diff)
1184 1184
1185 1185 # Set the parent Revision every time, so commit re-ordering is picked-up
1186 1186 if parentrevphid:
1187 1187 transactions.append(
1188 1188 {b'type': b'parents.set', b'value': [parentrevphid]}
1189 1189 )
1190 1190
1191 1191 if actions:
1192 1192 transactions += actions
1193 1193
1194 1194 # When folding multiple local commits into a single review, arcanist will
1195 1195 # take the summary line of the first commit as the title, and then
1196 1196 # concatenate the rest of the remaining messages (including each of their
1197 1197 # first lines) to the rest of the first commit message (each separated by
1198 1198 # an empty line), and use that as the summary field. Do the same here.
1199 1199 # For commits with only a one line message, there is no summary field, as
1200 1200 # this gets assigned to the title.
1201 1201 fields = util.sortdict() # sorted for stable wire protocol in tests
1202 1202
1203 1203 for i, _ctx in enumerate(ctxs):
1204 1204 # Parse commit message and update related fields.
1205 1205 desc = _ctx.description()
1206 1206 info = callconduit(
1207 1207 repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
1208 1208 )
1209 1209
1210 1210 for k in [b'title', b'summary', b'testPlan']:
1211 1211 v = info[b'fields'].get(k)
1212 1212 if not v:
1213 1213 continue
1214 1214
1215 1215 if i == 0:
1216 1216 # Title, summary and test plan (if present) are taken verbatim
1217 1217 # for the first commit.
1218 1218 fields[k] = v.rstrip()
1219 1219 continue
1220 1220 elif k == b'title':
1221 1221 # Add subsequent titles (i.e. the first line of the commit
1222 1222 # message) back to the summary.
1223 1223 k = b'summary'
1224 1224
1225 1225 # Append any current field to the existing composite field
1226 1226 fields[k] = b'\n\n'.join(filter(None, [fields.get(k), v.rstrip()]))
1227 1227
1228 1228 for k, v in fields.items():
1229 1229 transactions.append({b'type': k, b'value': v})
1230 1230
1231 1231 params = {b'transactions': transactions}
1232 1232 if revid is not None:
1233 1233 # Update an existing Differential Revision
1234 1234 params[b'objectIdentifier'] = revid
1235 1235
1236 1236 revision = callconduit(repo.ui, b'differential.revision.edit', params)
1237 1237 if not revision:
1238 1238 if len(ctxs) == 1:
1239 1239 msg = _(b'cannot create revision for %s') % ctx
1240 1240 else:
1241 1241 msg = _(b'cannot create revision for %s::%s') % (basectx, ctx)
1242 1242 raise error.Abort(msg)
1243 1243
1244 1244 return revision, diff
1245 1245
1246 1246
1247 1247 def userphids(ui, names):
1248 1248 """convert user names to PHIDs"""
1249 1249 names = [name.lower() for name in names]
1250 1250 query = {b'constraints': {b'usernames': names}}
1251 1251 result = callconduit(ui, b'user.search', query)
1252 1252 # username not found is not an error of the API. So check if we have missed
1253 1253 # some names here.
1254 1254 data = result[b'data']
1255 1255 resolved = {entry[b'fields'][b'username'].lower() for entry in data}
1256 1256 unresolved = set(names) - resolved
1257 1257 if unresolved:
1258 1258 raise error.Abort(
1259 1259 _(b'unknown username: %s') % b' '.join(sorted(unresolved))
1260 1260 )
1261 1261 return [entry[b'phid'] for entry in data]
1262 1262
1263 1263
1264 1264 def _print_phabsend_action(ui, ctx, newrevid, action):
1265 1265 """print the ``action`` that occurred when posting ``ctx`` for review
1266 1266
1267 1267 This is a utility function for the sending phase of ``phabsend``, which
1268 1268 makes it easier to show a status for all local commits with `--fold``.
1269 1269 """
1270 1270 actiondesc = ui.label(
1271 1271 {
1272 1272 b'created': _(b'created'),
1273 1273 b'skipped': _(b'skipped'),
1274 1274 b'updated': _(b'updated'),
1275 1275 }[action],
1276 1276 b'phabricator.action.%s' % action,
1277 1277 )
1278 1278 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
1279 1279 summary = cmdutil.format_changeset_summary(ui, ctx, b'phabsend')
1280 1280 ui.write(_(b'%s - %s - %s\n') % (drevdesc, actiondesc, summary))
1281 1281
1282 1282
1283 1283 def _amend_diff_properties(unfi, drevid, newnodes, diff):
1284 1284 """update the local commit list for the ``diff`` associated with ``drevid``
1285 1285
1286 1286 This is a utility function for the amend phase of ``phabsend``, which
1287 1287 converts failures to warning messages.
1288 1288 """
1289 1289 _debug(
1290 1290 unfi.ui,
1291 1291 b"new commits: %s\n" % stringutil.pprint([short(n) for n in newnodes]),
1292 1292 )
1293 1293
1294 1294 try:
1295 1295 writediffproperties([unfi[newnode] for newnode in newnodes], diff)
1296 1296 except util.urlerr.urlerror:
1297 1297 # If it fails just warn and keep going, otherwise the DREV
1298 1298 # associations will be lost
1299 1299 unfi.ui.warnnoi18n(b'Failed to update metadata for D%d\n' % drevid)
1300 1300
1301 1301
1302 1302 @vcrcommand(
1303 1303 b'phabsend',
1304 1304 [
1305 1305 (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
1306 1306 (b'', b'amend', True, _(b'update commit messages')),
1307 1307 (b'', b'reviewer', [], _(b'specify reviewers')),
1308 1308 (b'', b'blocker', [], _(b'specify blocking reviewers')),
1309 1309 (
1310 1310 b'm',
1311 1311 b'comment',
1312 1312 b'',
1313 1313 _(b'add a comment to Revisions with new/updated Diffs'),
1314 1314 ),
1315 1315 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
1316 1316 (b'', b'fold', False, _(b'combine the revisions into one review')),
1317 1317 ],
1318 1318 _(b'REV [OPTIONS]'),
1319 1319 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1320 1320 )
1321 1321 def phabsend(ui, repo, *revs, **opts):
1322 1322 """upload changesets to Phabricator
1323 1323
1324 1324 If there are multiple revisions specified, they will be send as a stack
1325 1325 with a linear dependencies relationship using the order specified by the
1326 1326 revset.
1327 1327
1328 1328 For the first time uploading changesets, local tags will be created to
1329 1329 maintain the association. After the first time, phabsend will check
1330 1330 obsstore and tags information so it can figure out whether to update an
1331 1331 existing Differential Revision, or create a new one.
1332 1332
1333 1333 If --amend is set, update commit messages so they have the
1334 1334 ``Differential Revision`` URL, remove related tags. This is similar to what
1335 1335 arcanist will do, and is more desired in author-push workflows. Otherwise,
1336 1336 use local tags to record the ``Differential Revision`` association.
1337 1337
1338 1338 The --confirm option lets you confirm changesets before sending them. You
1339 1339 can also add following to your configuration file to make it default
1340 1340 behaviour::
1341 1341
1342 1342 [phabsend]
1343 1343 confirm = true
1344 1344
1345 1345 By default, a separate review will be created for each commit that is
1346 1346 selected, and will have the same parent/child relationship in Phabricator.
1347 1347 If ``--fold`` is set, multiple commits are rolled up into a single review
1348 1348 as if diffed from the parent of the first revision to the last. The commit
1349 1349 messages are concatenated in the summary field on Phabricator.
1350 1350
1351 1351 phabsend will check obsstore and the above association to decide whether to
1352 1352 update an existing Differential Revision, or create a new one.
1353 1353 """
1354 1354 opts = pycompat.byteskwargs(opts)
1355 1355 revs = list(revs) + opts.get(b'rev', [])
1356 1356 revs = logcmdutil.revrange(repo, revs)
1357 1357 revs.sort() # ascending order to preserve topological parent/child in phab
1358 1358
1359 1359 if not revs:
1360 1360 raise error.Abort(_(b'phabsend requires at least one changeset'))
1361 1361 if opts.get(b'amend'):
1362 1362 cmdutil.checkunfinished(repo)
1363 1363
1364 1364 ctxs = [repo[rev] for rev in revs]
1365 1365
1366 1366 if any(c for c in ctxs if c.obsolete()):
1367 1367 raise error.Abort(_(b"obsolete commits cannot be posted for review"))
1368 1368
1369 1369 # Ensure the local commits are an unbroken range. The semantics of the
1370 1370 # --fold option implies this, and the auto restacking of orphans requires
1371 1371 # it. Otherwise A+C in A->B->C will cause B to be orphaned, and C' to
1372 1372 # get A' as a parent.
1373 1373 def _fail_nonlinear_revs(revs, revtype):
1374 1374 badnodes = [repo[r].node() for r in revs]
1375 1375 raise error.Abort(
1376 1376 _(b"cannot phabsend multiple %s revisions: %s")
1377 1377 % (revtype, scmutil.nodesummaries(repo, badnodes)),
1378 1378 hint=_(b"the revisions must form a linear chain"),
1379 1379 )
1380 1380
1381 1381 heads = repo.revs(b'heads(%ld)', revs)
1382 1382 if len(heads) > 1:
1383 1383 _fail_nonlinear_revs(heads, b"head")
1384 1384
1385 1385 roots = repo.revs(b'roots(%ld)', revs)
1386 1386 if len(roots) > 1:
1387 1387 _fail_nonlinear_revs(roots, b"root")
1388 1388
1389 1389 fold = opts.get(b'fold')
1390 1390 if fold:
1391 1391 if len(revs) == 1:
1392 1392 # TODO: just switch to --no-fold instead?
1393 1393 raise error.Abort(_(b"cannot fold a single revision"))
1394 1394
1395 1395 # There's no clear way to manage multiple commits with a Dxxx tag, so
1396 1396 # require the amend option. (We could append "_nnn", but then it
1397 1397 # becomes jumbled if earlier commits are added to an update.) It should
1398 1398 # lock the repo and ensure that the range is editable, but that would
1399 1399 # make the code pretty convoluted. The default behavior of `arc` is to
1400 1400 # create a new review anyway.
1401 1401 if not opts.get(b"amend"):
1402 1402 raise error.Abort(_(b"cannot fold with --no-amend"))
1403 1403
1404 1404 # It might be possible to bucketize the revisions by the DREV value, and
1405 1405 # iterate over those groups when posting, and then again when amending.
1406 1406 # But for simplicity, require all selected revisions to be for the same
1407 1407 # DREV (if present). Adding local revisions to an existing DREV is
1408 1408 # acceptable.
1409 1409 drevmatchers = [
1410 1410 _differentialrevisiondescre.search(ctx.description())
1411 1411 for ctx in ctxs
1412 1412 ]
1413 1413 if len({m.group('url') for m in drevmatchers if m}) > 1:
1414 1414 raise error.Abort(
1415 1415 _(b"cannot fold revisions with different DREV values")
1416 1416 )
1417 1417
1418 1418 # {newnode: (oldnode, olddiff, olddrev}
1419 1419 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
1420 1420
1421 1421 confirm = ui.configbool(b'phabsend', b'confirm')
1422 1422 confirm |= bool(opts.get(b'confirm'))
1423 1423 if confirm:
1424 1424 confirmed = _confirmbeforesend(repo, revs, oldmap)
1425 1425 if not confirmed:
1426 1426 raise error.Abort(_(b'phabsend cancelled'))
1427 1427
1428 1428 actions = []
1429 1429 reviewers = opts.get(b'reviewer', [])
1430 1430 blockers = opts.get(b'blocker', [])
1431 1431 phids = []
1432 1432 if reviewers:
1433 1433 phids.extend(userphids(repo.ui, reviewers))
1434 1434 if blockers:
1435 1435 phids.extend(
1436 1436 map(
1437 1437 lambda phid: b'blocking(%s)' % phid,
1438 1438 userphids(repo.ui, blockers),
1439 1439 )
1440 1440 )
1441 1441 if phids:
1442 1442 actions.append({b'type': b'reviewers.add', b'value': phids})
1443 1443
1444 1444 drevids = [] # [int]
1445 1445 diffmap = {} # {newnode: diff}
1446 1446
1447 1447 # Send patches one by one so we know their Differential Revision PHIDs and
1448 1448 # can provide dependency relationship
1449 1449 lastrevphid = None
1450 1450 for ctx in ctxs:
1451 1451 if fold:
1452 1452 ui.debug(b'sending rev %d::%d\n' % (ctx.rev(), ctxs[-1].rev()))
1453 1453 else:
1454 1454 ui.debug(b'sending rev %d\n' % ctx.rev())
1455 1455
1456 1456 # Get Differential Revision ID
1457 1457 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
1458 1458 oldbasenode, oldbasediff, oldbaserevid = oldnode, olddiff, revid
1459 1459
1460 1460 if fold:
1461 1461 oldbasenode, oldbasediff, oldbaserevid = oldmap.get(
1462 1462 ctxs[-1].node(), (None, None, None)
1463 1463 )
1464 1464
1465 1465 if oldnode != ctx.node() or opts.get(b'amend'):
1466 1466 # Create or update Differential Revision
1467 1467 revision, diff = createdifferentialrevision(
1468 1468 ctxs if fold else [ctx],
1469 1469 revid,
1470 1470 lastrevphid,
1471 1471 oldbasenode,
1472 1472 oldnode,
1473 1473 olddiff,
1474 1474 actions,
1475 1475 opts.get(b'comment'),
1476 1476 )
1477 1477
1478 1478 if fold:
1479 1479 for ctx in ctxs:
1480 1480 diffmap[ctx.node()] = diff
1481 1481 else:
1482 1482 diffmap[ctx.node()] = diff
1483 1483
1484 1484 newrevid = int(revision[b'object'][b'id'])
1485 1485 newrevphid = revision[b'object'][b'phid']
1486 1486 if revid:
1487 1487 action = b'updated'
1488 1488 else:
1489 1489 action = b'created'
1490 1490
1491 1491 # Create a local tag to note the association, if commit message
1492 1492 # does not have it already
1493 1493 if not fold:
1494 1494 m = _differentialrevisiondescre.search(ctx.description())
1495 1495 if not m or int(m.group('id')) != newrevid:
1496 1496 tagname = b'D%d' % newrevid
1497 1497 tags.tag(
1498 1498 repo,
1499 1499 tagname,
1500 1500 ctx.node(),
1501 1501 message=None,
1502 1502 user=None,
1503 1503 date=None,
1504 1504 local=True,
1505 1505 )
1506 1506 else:
1507 1507 # Nothing changed. But still set "newrevphid" so the next revision
1508 1508 # could depend on this one and "newrevid" for the summary line.
1509 1509 newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']
1510 1510 newrevid = revid
1511 1511 action = b'skipped'
1512 1512
1513 1513 drevids.append(newrevid)
1514 1514 lastrevphid = newrevphid
1515 1515
1516 1516 if fold:
1517 1517 for c in ctxs:
1518 1518 if oldmap.get(c.node(), (None, None, None))[2]:
1519 1519 action = b'updated'
1520 1520 else:
1521 1521 action = b'created'
1522 1522 _print_phabsend_action(ui, c, newrevid, action)
1523 1523 break
1524 1524
1525 1525 _print_phabsend_action(ui, ctx, newrevid, action)
1526 1526
1527 1527 # Update commit messages and remove tags
1528 1528 if opts.get(b'amend'):
1529 1529 unfi = repo.unfiltered()
1530 1530 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
1531 1531 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
1532 1532 # Eagerly evaluate commits to restabilize before creating new
1533 1533 # commits. The selected revisions are excluded because they are
1534 1534 # automatically restacked as part of the submission process.
1535 1535 restack = [
1536 1536 c
1537 1537 for c in repo.set(
1538 1538 b"(%ld::) - (%ld) - unstable() - obsolete() - public()",
1539 1539 revs,
1540 1540 revs,
1541 1541 )
1542 1542 ]
1543 1543 wnode = unfi[b'.'].node()
1544 1544 mapping = {} # {oldnode: [newnode]}
1545 1545 newnodes = []
1546 1546
1547 1547 drevid = drevids[0]
1548 1548
1549 1549 for i, rev in enumerate(revs):
1550 1550 old = unfi[rev]
1551 1551 if not fold:
1552 1552 drevid = drevids[i]
1553 1553 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
1554 1554
1555 1555 newdesc = get_amended_desc(drev, old, fold)
1556 1556 # Make sure commit message contain "Differential Revision"
1557 1557 if (
1558 1558 old.description() != newdesc
1559 1559 or old.p1().node() in mapping
1560 1560 or old.p2().node() in mapping
1561 1561 ):
1562 1562 if old.phase() == phases.public:
1563 1563 ui.warn(
1564 1564 _(b"warning: not updating public commit %s\n")
1565 1565 % scmutil.formatchangeid(old)
1566 1566 )
1567 1567 continue
1568 1568 parents = [
1569 1569 mapping.get(old.p1().node(), (old.p1(),))[0],
1570 1570 mapping.get(old.p2().node(), (old.p2(),))[0],
1571 1571 ]
1572 1572 newdesc = rewriteutil.update_hash_refs(
1573 1573 repo,
1574 1574 newdesc,
1575 1575 mapping,
1576 1576 )
1577 1577 new = context.metadataonlyctx(
1578 1578 repo,
1579 1579 old,
1580 1580 parents=parents,
1581 1581 text=newdesc,
1582 1582 user=old.user(),
1583 1583 date=old.date(),
1584 1584 extra=old.extra(),
1585 1585 )
1586 1586
1587 1587 newnode = new.commit()
1588 1588
1589 1589 mapping[old.node()] = [newnode]
1590 1590
1591 1591 if fold:
1592 1592 # Defer updating the (single) Diff until all nodes are
1593 1593 # collected. No tags were created, so none need to be
1594 1594 # removed.
1595 1595 newnodes.append(newnode)
1596 1596 continue
1597 1597
1598 1598 _amend_diff_properties(
1599 1599 unfi, drevid, [newnode], diffmap[old.node()]
1600 1600 )
1601 1601
1602 1602 # Remove local tags since it's no longer necessary
1603 1603 tagname = b'D%d' % drevid
1604 1604 if tagname in repo.tags():
1605 1605 tags.tag(
1606 1606 repo,
1607 1607 tagname,
1608 1608 repo.nullid,
1609 1609 message=None,
1610 1610 user=None,
1611 1611 date=None,
1612 1612 local=True,
1613 1613 )
1614 1614 elif fold:
1615 1615 # When folding multiple commits into one review with
1616 1616 # --fold, track even the commits that weren't amended, so
1617 1617 # that their association isn't lost if the properties are
1618 1618 # rewritten below.
1619 1619 newnodes.append(old.node())
1620 1620
1621 1621 # If the submitted commits are public, no amend takes place so
1622 1622 # there are no newnodes and therefore no diff update to do.
1623 1623 if fold and newnodes:
1624 1624 diff = diffmap[old.node()]
1625 1625
1626 1626 # The diff object in diffmap doesn't have the local commits
1627 1627 # because that could be returned from differential.creatediff,
1628 1628 # not differential.querydiffs. So use the queried diff (if
1629 1629 # present), or force the amend (a new revision is being posted.)
1630 1630 if not olddiff or set(newnodes) != getlocalcommits(olddiff):
1631 1631 _debug(ui, b"updating local commit list for D%d\n" % drevid)
1632 1632 _amend_diff_properties(unfi, drevid, newnodes, diff)
1633 1633 else:
1634 1634 _debug(
1635 1635 ui,
1636 1636 b"local commit list for D%d is already up-to-date\n"
1637 1637 % drevid,
1638 1638 )
1639 1639 elif fold:
1640 1640 _debug(ui, b"no newnodes to update\n")
1641 1641
1642 1642 # Restack any children of first-time submissions that were orphaned
1643 1643 # in the process. The ctx won't report that it is an orphan until
1644 1644 # the cleanup takes place below.
1645 1645 for old in restack:
1646 1646 parents = [
1647 1647 mapping.get(old.p1().node(), (old.p1(),))[0],
1648 1648 mapping.get(old.p2().node(), (old.p2(),))[0],
1649 1649 ]
1650 1650 new = context.metadataonlyctx(
1651 1651 repo,
1652 1652 old,
1653 1653 parents=parents,
1654 1654 text=rewriteutil.update_hash_refs(
1655 1655 repo, old.description(), mapping
1656 1656 ),
1657 1657 user=old.user(),
1658 1658 date=old.date(),
1659 1659 extra=old.extra(),
1660 1660 )
1661 1661
1662 1662 newnode = new.commit()
1663 1663
1664 1664 # Don't obsolete unselected descendants of nodes that have not
1665 1665 # been changed in this transaction- that results in an error.
1666 1666 if newnode != old.node():
1667 1667 mapping[old.node()] = [newnode]
1668 1668 _debug(
1669 1669 ui,
1670 1670 b"restabilizing %s as %s\n"
1671 1671 % (short(old.node()), short(newnode)),
1672 1672 )
1673 1673 else:
1674 1674 _debug(
1675 1675 ui,
1676 1676 b"not restabilizing unchanged %s\n" % short(old.node()),
1677 1677 )
1678 1678
1679 1679 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
1680 1680 if wnode in mapping:
1681 1681 unfi.setparents(mapping[wnode][0])
1682 1682
1683 1683
1684 1684 # Map from "hg:meta" keys to header understood by "hg import". The order is
1685 1685 # consistent with "hg export" output.
1686 1686 _metanamemap = util.sortdict(
1687 1687 [
1688 1688 (b'user', b'User'),
1689 1689 (b'date', b'Date'),
1690 1690 (b'branch', b'Branch'),
1691 1691 (b'node', b'Node ID'),
1692 1692 (b'parent', b'Parent '),
1693 1693 ]
1694 1694 )
1695 1695
1696 1696
1697 1697 def _confirmbeforesend(repo, revs, oldmap):
1698 1698 url, token = readurltoken(repo.ui)
1699 1699 ui = repo.ui
1700 1700 for rev in revs:
1701 1701 ctx = repo[rev]
1702 1702 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
1703 1703 if drevid:
1704 1704 drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')
1705 1705 else:
1706 1706 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
1707 1707
1708 1708 ui.write(
1709 1709 _(b'%s - %s\n')
1710 1710 % (
1711 1711 drevdesc,
1712 1712 cmdutil.format_changeset_summary(ui, ctx, b'phabsend'),
1713 1713 )
1714 1714 )
1715 1715
1716 1716 if ui.promptchoice(
1717 1717 _(b'Send the above changes to %s (Y/n)?$$ &Yes $$ &No') % url
1718 1718 ):
1719 1719 return False
1720 1720
1721 1721 return True
1722 1722
1723 1723
1724 1724 _knownstatusnames = {
1725 1725 b'accepted',
1726 1726 b'needsreview',
1727 1727 b'needsrevision',
1728 1728 b'closed',
1729 1729 b'abandoned',
1730 1730 b'changesplanned',
1731 1731 }
1732 1732
1733 1733
1734 1734 def _getstatusname(drev):
1735 1735 """get normalized status name from a Differential Revision"""
1736 1736 return drev[b'statusName'].replace(b' ', b'').lower()
1737 1737
1738 1738
1739 1739 # Small language to specify differential revisions. Support symbols: (), :X,
1740 1740 # +, and -.
1741 1741
1742 1742 _elements = {
1743 1743 # token-type: binding-strength, primary, prefix, infix, suffix
1744 1744 b'(': (12, None, (b'group', 1, b')'), None, None),
1745 1745 b':': (8, None, (b'ancestors', 8), None, None),
1746 1746 b'&': (5, None, None, (b'and_', 5), None),
1747 1747 b'+': (4, None, None, (b'add', 4), None),
1748 1748 b'-': (4, None, None, (b'sub', 4), None),
1749 1749 b')': (0, None, None, None, None),
1750 1750 b'symbol': (0, b'symbol', None, None, None),
1751 1751 b'end': (0, None, None, None, None),
1752 1752 }
1753 1753
1754 1754
1755 1755 def _tokenize(text):
1756 1756 view = memoryview(text) # zero-copy slice
1757 1757 special = b'():+-& '
1758 1758 pos = 0
1759 1759 length = len(text)
1760 1760 while pos < length:
1761 1761 symbol = b''.join(
1762 1762 itertools.takewhile(
1763 1763 lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])
1764 1764 )
1765 1765 )
1766 1766 if symbol:
1767 1767 yield (b'symbol', symbol, pos)
1768 1768 pos += len(symbol)
1769 1769 else: # special char, ignore space
1770 1770 if text[pos : pos + 1] != b' ':
1771 1771 yield (text[pos : pos + 1], None, pos)
1772 1772 pos += 1
1773 1773 yield (b'end', None, pos)
1774 1774
1775 1775
1776 1776 def _parse(text):
1777 1777 tree, pos = parser.parser(_elements).parse(_tokenize(text))
1778 1778 if pos != len(text):
1779 1779 raise error.ParseError(b'invalid token', pos)
1780 1780 return tree
1781 1781
1782 1782
1783 1783 def _parsedrev(symbol):
1784 1784 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
1785 1785 if symbol.startswith(b'D') and symbol[1:].isdigit():
1786 1786 return int(symbol[1:])
1787 1787 if symbol.isdigit():
1788 1788 return int(symbol)
1789 1789
1790 1790
1791 1791 def _prefetchdrevs(tree):
1792 1792 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
1793 1793 drevs = set()
1794 1794 ancestordrevs = set()
1795 1795 op = tree[0]
1796 1796 if op == b'symbol':
1797 1797 r = _parsedrev(tree[1])
1798 1798 if r:
1799 1799 drevs.add(r)
1800 1800 elif op == b'ancestors':
1801 1801 r, a = _prefetchdrevs(tree[1])
1802 1802 drevs.update(r)
1803 1803 ancestordrevs.update(r)
1804 1804 ancestordrevs.update(a)
1805 1805 else:
1806 1806 for t in tree[1:]:
1807 1807 r, a = _prefetchdrevs(t)
1808 1808 drevs.update(r)
1809 1809 ancestordrevs.update(a)
1810 1810 return drevs, ancestordrevs
1811 1811
1812 1812
1813 1813 def querydrev(ui, spec):
1814 1814 """return a list of "Differential Revision" dicts
1815 1815
1816 1816 spec is a string using a simple query language, see docstring in phabread
1817 1817 for details.
1818 1818
1819 1819 A "Differential Revision dict" looks like:
1820 1820
1821 1821 {
1822 1822 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
1823 1823 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
1824 1824 "auxiliary": {
1825 1825 "phabricator:depends-on": [
1826 1826 "PHID-DREV-gbapp366kutjebt7agcd"
1827 1827 ]
1828 1828 "phabricator:projects": [],
1829 1829 },
1830 1830 "branch": "default",
1831 1831 "ccs": [],
1832 1832 "commits": [],
1833 1833 "dateCreated": "1499181406",
1834 1834 "dateModified": "1499182103",
1835 1835 "diffs": [
1836 1836 "3",
1837 1837 "4",
1838 1838 ],
1839 1839 "hashes": [],
1840 1840 "id": "2",
1841 1841 "lineCount": "2",
1842 1842 "phid": "PHID-DREV-672qvysjcczopag46qty",
1843 1843 "properties": {},
1844 1844 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
1845 1845 "reviewers": [],
1846 1846 "sourcePath": null
1847 1847 "status": "0",
1848 1848 "statusName": "Needs Review",
1849 1849 "summary": "",
1850 1850 "testPlan": "",
1851 1851 "title": "example",
1852 1852 "uri": "https://phab.example.com/D2",
1853 1853 }
1854 1854 """
1855 1855 # TODO: replace differential.query and differential.querydiffs with
1856 1856 # differential.diff.search because the former (and their output) are
1857 1857 # frozen, and planned to be deprecated and removed.
1858 1858
1859 1859 def fetch(params):
1860 1860 """params -> single drev or None"""
1861 1861 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
1862 1862 if key in prefetched:
1863 1863 return prefetched[key]
1864 1864 drevs = callconduit(ui, b'differential.query', params)
1865 1865 # Fill prefetched with the result
1866 1866 for drev in drevs:
1867 1867 prefetched[drev[b'phid']] = drev
1868 1868 prefetched[int(drev[b'id'])] = drev
1869 1869 if key not in prefetched:
1870 1870 raise error.Abort(
1871 1871 _(b'cannot get Differential Revision %r') % params
1872 1872 )
1873 1873 return prefetched[key]
1874 1874
1875 1875 def getstack(topdrevids):
1876 1876 """given a top, get a stack from the bottom, [id] -> [id]"""
1877 1877 visited = set()
1878 1878 result = []
1879 1879 queue = [{b'ids': [i]} for i in topdrevids]
1880 1880 while queue:
1881 1881 params = queue.pop()
1882 1882 drev = fetch(params)
1883 1883 if drev[b'id'] in visited:
1884 1884 continue
1885 1885 visited.add(drev[b'id'])
1886 1886 result.append(int(drev[b'id']))
1887 1887 auxiliary = drev.get(b'auxiliary', {})
1888 1888 depends = auxiliary.get(b'phabricator:depends-on', [])
1889 1889 for phid in depends:
1890 1890 queue.append({b'phids': [phid]})
1891 1891 result.reverse()
1892 1892 return smartset.baseset(result)
1893 1893
1894 1894 # Initialize prefetch cache
1895 1895 prefetched = {} # {id or phid: drev}
1896 1896
1897 1897 tree = _parse(spec)
1898 1898 drevs, ancestordrevs = _prefetchdrevs(tree)
1899 1899
1900 1900 # developer config: phabricator.batchsize
1901 1901 batchsize = ui.configint(b'phabricator', b'batchsize')
1902 1902
1903 1903 # Prefetch Differential Revisions in batch
1904 1904 tofetch = set(drevs)
1905 1905 for r in ancestordrevs:
1906 1906 tofetch.update(range(max(1, r - batchsize), r + 1))
1907 1907 if drevs:
1908 1908 fetch({b'ids': list(tofetch)})
1909 1909 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
1910 1910
1911 1911 # Walk through the tree, return smartsets
1912 1912 def walk(tree):
1913 1913 op = tree[0]
1914 1914 if op == b'symbol':
1915 1915 drev = _parsedrev(tree[1])
1916 1916 if drev:
1917 1917 return smartset.baseset([drev])
1918 1918 elif tree[1] in _knownstatusnames:
1919 1919 drevs = [
1920 1920 r
1921 1921 for r in validids
1922 1922 if _getstatusname(prefetched[r]) == tree[1]
1923 1923 ]
1924 1924 return smartset.baseset(drevs)
1925 1925 else:
1926 1926 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
1927 1927 elif op in {b'and_', b'add', b'sub'}:
1928 1928 assert len(tree) == 3
1929 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
1929 return getattr(operator, pycompat.sysstr(op))(
1930 walk(tree[1]), walk(tree[2])
1931 )
1930 1932 elif op == b'group':
1931 1933 return walk(tree[1])
1932 1934 elif op == b'ancestors':
1933 1935 return getstack(walk(tree[1]))
1934 1936 else:
1935 1937 raise error.ProgrammingError(b'illegal tree: %r' % tree)
1936 1938
1937 1939 return [prefetched[r] for r in walk(tree)]
1938 1940
1939 1941
1940 1942 def getdescfromdrev(drev):
1941 1943 """get description (commit message) from "Differential Revision"
1942 1944
1943 1945 This is similar to differential.getcommitmessage API. But we only care
1944 1946 about limited fields: title, summary, test plan, and URL.
1945 1947 """
1946 1948 title = drev[b'title']
1947 1949 summary = drev[b'summary'].rstrip()
1948 1950 testplan = drev[b'testPlan'].rstrip()
1949 1951 if testplan:
1950 1952 testplan = b'Test Plan:\n%s' % testplan
1951 1953 uri = b'Differential Revision: %s' % drev[b'uri']
1952 1954 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
1953 1955
1954 1956
1955 1957 def get_amended_desc(drev, ctx, folded):
1956 1958 """similar to ``getdescfromdrev``, but supports a folded series of commits
1957 1959
1958 1960 This is used when determining if an individual commit needs to have its
1959 1961 message amended after posting it for review. The determination is made for
1960 1962 each individual commit, even when they were folded into one review.
1961 1963 """
1962 1964 if not folded:
1963 1965 return getdescfromdrev(drev)
1964 1966
1965 1967 uri = b'Differential Revision: %s' % drev[b'uri']
1966 1968
1967 1969 # Since the commit messages were combined when posting multiple commits
1968 1970 # with --fold, the fields can't be read from Phabricator here, or *all*
1969 1971 # affected local revisions will end up with the same commit message after
1970 1972 # the URI is amended in. Append in the DREV line, or update it if it
1971 1973 # exists. At worst, this means commit message or test plan updates on
1972 1974 # Phabricator aren't propagated back to the repository, but that seems
1973 1975 # reasonable for the case where local commits are effectively combined
1974 1976 # in Phabricator.
1975 1977 m = _differentialrevisiondescre.search(ctx.description())
1976 1978 if not m:
1977 1979 return b'\n\n'.join([ctx.description(), uri])
1978 1980
1979 1981 return _differentialrevisiondescre.sub(uri, ctx.description())
1980 1982
1981 1983
1982 1984 def getlocalcommits(diff):
1983 1985 """get the set of local commits from a diff object
1984 1986
1985 1987 See ``getdiffmeta()`` for an example diff object.
1986 1988 """
1987 1989 props = diff.get(b'properties') or {}
1988 1990 commits = props.get(b'local:commits') or {}
1989 1991 if len(commits) > 1:
1990 1992 return {bin(c) for c in commits.keys()}
1991 1993
1992 1994 # Storing the diff metadata predates storing `local:commits`, so continue
1993 1995 # to use that in the --no-fold case.
1994 1996 return {bin(getdiffmeta(diff).get(b'node', b'')) or None}
1995 1997
1996 1998
1997 1999 def getdiffmeta(diff):
1998 2000 """get commit metadata (date, node, user, p1) from a diff object
1999 2001
2000 2002 The metadata could be "hg:meta", sent by phabsend, like:
2001 2003
2002 2004 "properties": {
2003 2005 "hg:meta": {
2004 2006 "branch": "default",
2005 2007 "date": "1499571514 25200",
2006 2008 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
2007 2009 "user": "Foo Bar <foo@example.com>",
2008 2010 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
2009 2011 }
2010 2012 }
2011 2013
2012 2014 Or converted from "local:commits", sent by "arc", like:
2013 2015
2014 2016 "properties": {
2015 2017 "local:commits": {
2016 2018 "98c08acae292b2faf60a279b4189beb6cff1414d": {
2017 2019 "author": "Foo Bar",
2018 2020 "authorEmail": "foo@example.com"
2019 2021 "branch": "default",
2020 2022 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
2021 2023 "local": "1000",
2022 2024 "message": "...",
2023 2025 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
2024 2026 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
2025 2027 "summary": "...",
2026 2028 "tag": "",
2027 2029 "time": 1499546314,
2028 2030 }
2029 2031 }
2030 2032 }
2031 2033
2032 2034 Note: metadata extracted from "local:commits" will lose time zone
2033 2035 information.
2034 2036 """
2035 2037 props = diff.get(b'properties') or {}
2036 2038 meta = props.get(b'hg:meta')
2037 2039 if not meta:
2038 2040 if props.get(b'local:commits'):
2039 2041 commit = sorted(props[b'local:commits'].values())[0]
2040 2042 meta = {}
2041 2043 if b'author' in commit and b'authorEmail' in commit:
2042 2044 meta[b'user'] = b'%s <%s>' % (
2043 2045 commit[b'author'],
2044 2046 commit[b'authorEmail'],
2045 2047 )
2046 2048 if b'time' in commit:
2047 2049 meta[b'date'] = b'%d 0' % int(commit[b'time'])
2048 2050 if b'branch' in commit:
2049 2051 meta[b'branch'] = commit[b'branch']
2050 2052 node = commit.get(b'commit', commit.get(b'rev'))
2051 2053 if node:
2052 2054 meta[b'node'] = node
2053 2055 if len(commit.get(b'parents', ())) >= 1:
2054 2056 meta[b'parent'] = commit[b'parents'][0]
2055 2057 else:
2056 2058 meta = {}
2057 2059 if b'date' not in meta and b'dateCreated' in diff:
2058 2060 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
2059 2061 if b'branch' not in meta and diff.get(b'branch'):
2060 2062 meta[b'branch'] = diff[b'branch']
2061 2063 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
2062 2064 meta[b'parent'] = diff[b'sourceControlBaseRevision']
2063 2065 return meta
2064 2066
2065 2067
2066 2068 def _getdrevs(ui, stack, specs):
2067 2069 """convert user supplied DREVSPECs into "Differential Revision" dicts
2068 2070
2069 2071 See ``hg help phabread`` for how to specify each DREVSPEC.
2070 2072 """
2071 2073 if len(specs) > 0:
2072 2074
2073 2075 def _formatspec(s):
2074 2076 if stack:
2075 2077 s = b':(%s)' % s
2076 2078 return b'(%s)' % s
2077 2079
2078 2080 spec = b'+'.join(pycompat.maplist(_formatspec, specs))
2079 2081
2080 2082 drevs = querydrev(ui, spec)
2081 2083 if drevs:
2082 2084 return drevs
2083 2085
2084 2086 raise error.Abort(_(b"empty DREVSPEC set"))
2085 2087
2086 2088
2087 2089 def readpatch(ui, drevs, write):
2088 2090 """generate plain-text patch readable by 'hg import'
2089 2091
2090 2092 write takes a list of (DREV, bytes), where DREV is the differential number
2091 2093 (as bytes, without the "D" prefix) and the bytes are the text of a patch
2092 2094 to be imported. drevs is what "querydrev" returns, results of
2093 2095 "differential.query".
2094 2096 """
2095 2097 # Prefetch hg:meta property for all diffs
2096 2098 diffids = sorted({max(int(v) for v in drev[b'diffs']) for drev in drevs})
2097 2099 diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})
2098 2100
2099 2101 patches = []
2100 2102
2101 2103 # Generate patch for each drev
2102 2104 for drev in drevs:
2103 2105 ui.note(_(b'reading D%s\n') % drev[b'id'])
2104 2106
2105 2107 diffid = max(int(v) for v in drev[b'diffs'])
2106 2108 body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})
2107 2109 desc = getdescfromdrev(drev)
2108 2110 header = b'# HG changeset patch\n'
2109 2111
2110 2112 # Try to preserve metadata from hg:meta property. Write hg patch
2111 2113 # headers that can be read by the "import" command. See patchheadermap
2112 2114 # and extract in mercurial/patch.py for supported headers.
2113 2115 meta = getdiffmeta(diffs[b'%d' % diffid])
2114 2116 for k in _metanamemap.keys():
2115 2117 if k in meta:
2116 2118 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
2117 2119
2118 2120 content = b'%s%s\n%s' % (header, desc, body)
2119 2121 patches.append((drev[b'id'], content))
2120 2122
2121 2123 # Write patches to the supplied callback
2122 2124 write(patches)
2123 2125
2124 2126
2125 2127 @vcrcommand(
2126 2128 b'phabread',
2127 2129 [(b'', b'stack', False, _(b'read dependencies'))],
2128 2130 _(b'DREVSPEC... [OPTIONS]'),
2129 2131 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2130 2132 optionalrepo=True,
2131 2133 )
2132 2134 def phabread(ui, repo, *specs, **opts):
2133 2135 """print patches from Phabricator suitable for importing
2134 2136
2135 2137 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
2136 2138 the number ``123``. It could also have common operators like ``+``, ``-``,
2137 2139 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
2138 2140 select a stack. If multiple DREVSPEC values are given, the result is the
2139 2141 union of each individually evaluated value. No attempt is currently made
2140 2142 to reorder the values to run from parent to child.
2141 2143
2142 2144 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
2143 2145 could be used to filter patches by status. For performance reason, they
2144 2146 only represent a subset of non-status selections and cannot be used alone.
2145 2147
2146 2148 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
2147 2149 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
2148 2150 stack up to D9.
2149 2151
2150 2152 If --stack is given, follow dependencies information and read all patches.
2151 2153 It is equivalent to the ``:`` operator.
2152 2154 """
2153 2155 opts = pycompat.byteskwargs(opts)
2154 2156 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2155 2157
2156 2158 def _write(patches):
2157 2159 for drev, content in patches:
2158 2160 ui.write(content)
2159 2161
2160 2162 readpatch(ui, drevs, _write)
2161 2163
2162 2164
2163 2165 @vcrcommand(
2164 2166 b'phabimport',
2165 2167 [(b'', b'stack', False, _(b'import dependencies as well'))],
2166 2168 _(b'DREVSPEC... [OPTIONS]'),
2167 2169 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2168 2170 )
2169 2171 def phabimport(ui, repo, *specs, **opts):
2170 2172 """import patches from Phabricator for the specified Differential Revisions
2171 2173
2172 2174 The patches are read and applied starting at the parent of the working
2173 2175 directory.
2174 2176
2175 2177 See ``hg help phabread`` for how to specify DREVSPEC.
2176 2178 """
2177 2179 opts = pycompat.byteskwargs(opts)
2178 2180
2179 2181 # --bypass avoids losing exec and symlink bits when importing on Windows,
2180 2182 # and allows importing with a dirty wdir. It also aborts instead of leaving
2181 2183 # rejects.
2182 2184 opts[b'bypass'] = True
2183 2185
2184 2186 # Mandatory default values, synced with commands.import
2185 2187 opts[b'strip'] = 1
2186 2188 opts[b'prefix'] = b''
2187 2189 # Evolve 9.3.0 assumes this key is present in cmdutil.tryimportone()
2188 2190 opts[b'obsolete'] = False
2189 2191
2190 2192 if ui.configbool(b'phabimport', b'secret'):
2191 2193 opts[b'secret'] = True
2192 2194 if ui.configbool(b'phabimport', b'obsolete'):
2193 2195 opts[b'obsolete'] = True # Handled by evolve wrapping tryimportone()
2194 2196
2195 2197 def _write(patches):
2196 2198 parents = repo[None].parents()
2197 2199
2198 2200 with repo.wlock(), repo.lock(), repo.transaction(b'phabimport'):
2199 2201 for drev, contents in patches:
2200 2202 ui.status(_(b'applying patch from D%s\n') % drev)
2201 2203
2202 2204 with patch.extract(ui, io.BytesIO(contents)) as patchdata:
2203 2205 msg, node, rej = cmdutil.tryimportone(
2204 2206 ui,
2205 2207 repo,
2206 2208 patchdata,
2207 2209 parents,
2208 2210 opts,
2209 2211 [],
2210 2212 None, # Never update wdir to another revision
2211 2213 )
2212 2214
2213 2215 if not node:
2214 2216 raise error.Abort(_(b'D%s: no diffs found') % drev)
2215 2217
2216 2218 ui.note(msg + b'\n')
2217 2219 parents = [repo[node]]
2218 2220
2219 2221 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2220 2222
2221 2223 readpatch(repo.ui, drevs, _write)
2222 2224
2223 2225
2224 2226 @vcrcommand(
2225 2227 b'phabupdate',
2226 2228 [
2227 2229 (b'', b'accept', False, _(b'accept revisions')),
2228 2230 (b'', b'reject', False, _(b'reject revisions')),
2229 2231 (b'', b'request-review', False, _(b'request review on revisions')),
2230 2232 (b'', b'abandon', False, _(b'abandon revisions')),
2231 2233 (b'', b'reclaim', False, _(b'reclaim revisions')),
2232 2234 (b'', b'close', False, _(b'close revisions')),
2233 2235 (b'', b'reopen', False, _(b'reopen revisions')),
2234 2236 (b'', b'plan-changes', False, _(b'plan changes for revisions')),
2235 2237 (b'', b'resign', False, _(b'resign as a reviewer from revisions')),
2236 2238 (b'', b'commandeer', False, _(b'commandeer revisions')),
2237 2239 (b'm', b'comment', b'', _(b'comment on the last revision')),
2238 2240 (b'r', b'rev', b'', _(b'local revision to update'), _(b'REV')),
2239 2241 ],
2240 2242 _(b'[DREVSPEC...| -r REV...] [OPTIONS]'),
2241 2243 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2242 2244 optionalrepo=True,
2243 2245 )
2244 2246 def phabupdate(ui, repo, *specs, **opts):
2245 2247 """update Differential Revision in batch
2246 2248
2247 2249 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
2248 2250 """
2249 2251 opts = pycompat.byteskwargs(opts)
2250 2252 transactions = [
2251 2253 b'abandon',
2252 2254 b'accept',
2253 2255 b'close',
2254 2256 b'commandeer',
2255 2257 b'plan-changes',
2256 2258 b'reclaim',
2257 2259 b'reject',
2258 2260 b'reopen',
2259 2261 b'request-review',
2260 2262 b'resign',
2261 2263 ]
2262 2264 flags = [n for n in transactions if opts.get(n.replace(b'-', b'_'))]
2263 2265 if len(flags) > 1:
2264 2266 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
2265 2267
2266 2268 actions = []
2267 2269 for f in flags:
2268 2270 actions.append({b'type': f, b'value': True})
2269 2271
2270 2272 revs = opts.get(b'rev')
2271 2273 if revs:
2272 2274 if not repo:
2273 2275 raise error.InputError(_(b'--rev requires a repository'))
2274 2276
2275 2277 if specs:
2276 2278 raise error.InputError(_(b'cannot specify both DREVSPEC and --rev'))
2277 2279
2278 2280 drevmap = getdrevmap(repo, logcmdutil.revrange(repo, [revs]))
2279 2281 specs = []
2280 2282 unknown = []
2281 2283 for r, d in drevmap.items():
2282 2284 if d is None:
2283 2285 unknown.append(repo[r])
2284 2286 else:
2285 2287 specs.append(b'D%d' % d)
2286 2288 if unknown:
2287 2289 raise error.InputError(
2288 2290 _(b'selected revisions without a Differential: %s')
2289 2291 % scmutil.nodesummaries(repo, unknown)
2290 2292 )
2291 2293
2292 2294 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2293 2295 for i, drev in enumerate(drevs):
2294 2296 if i + 1 == len(drevs) and opts.get(b'comment'):
2295 2297 actions.append({b'type': b'comment', b'value': opts[b'comment']})
2296 2298 if actions:
2297 2299 params = {
2298 2300 b'objectIdentifier': drev[b'phid'],
2299 2301 b'transactions': actions,
2300 2302 }
2301 2303 callconduit(ui, b'differential.revision.edit', params)
2302 2304
2303 2305
2304 2306 @eh.templatekeyword(b'phabreview', requires={b'ctx'})
2305 2307 def template_review(context, mapping):
2306 2308 """:phabreview: Object describing the review for this changeset.
2307 2309 Has attributes `url` and `id`.
2308 2310 """
2309 2311 ctx = context.resource(mapping, b'ctx')
2310 2312 m = _differentialrevisiondescre.search(ctx.description())
2311 2313 if m:
2312 2314 return templateutil.hybriddict(
2313 2315 {
2314 2316 b'url': m.group('url'),
2315 2317 b'id': b"D%s" % m.group('id'),
2316 2318 }
2317 2319 )
2318 2320 else:
2319 2321 tags = ctx.repo().nodetags(ctx.node())
2320 2322 for t in tags:
2321 2323 if _differentialrevisiontagre.match(t):
2322 2324 url = ctx.repo().ui.config(b'phabricator', b'url')
2323 2325 if not url.endswith(b'/'):
2324 2326 url += b'/'
2325 2327 url += t
2326 2328
2327 2329 return templateutil.hybriddict(
2328 2330 {
2329 2331 b'url': url,
2330 2332 b'id': t,
2331 2333 }
2332 2334 )
2333 2335 return None
2334 2336
2335 2337
2336 2338 @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})
2337 2339 def template_status(context, mapping):
2338 2340 """:phabstatus: String. Status of Phabricator differential."""
2339 2341 ctx = context.resource(mapping, b'ctx')
2340 2342 repo = context.resource(mapping, b'repo')
2341 2343 ui = context.resource(mapping, b'ui')
2342 2344
2343 2345 rev = ctx.rev()
2344 2346 try:
2345 2347 drevid = getdrevmap(repo, [rev])[rev]
2346 2348 except KeyError:
2347 2349 return None
2348 2350 drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})
2349 2351 for drev in drevs:
2350 2352 if int(drev[b'id']) == drevid:
2351 2353 return templateutil.hybriddict(
2352 2354 {
2353 2355 b'url': drev[b'uri'],
2354 2356 b'status': drev[b'statusName'],
2355 2357 }
2356 2358 )
2357 2359 return None
2358 2360
2359 2361
2360 2362 @show.showview(b'phabstatus', csettopic=b'work')
2361 2363 def phabstatusshowview(ui, repo, displayer):
2362 2364 """Phabricator differiential status"""
2363 2365 revs = repo.revs('sort(_underway(), topo)')
2364 2366 drevmap = getdrevmap(repo, revs)
2365 2367 unknownrevs, drevids, revsbydrevid = [], set(), {}
2366 2368 for rev, drevid in drevmap.items():
2367 2369 if drevid is not None:
2368 2370 drevids.add(drevid)
2369 2371 revsbydrevid.setdefault(drevid, set()).add(rev)
2370 2372 else:
2371 2373 unknownrevs.append(rev)
2372 2374
2373 2375 drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})
2374 2376 drevsbyrev = {}
2375 2377 for drev in drevs:
2376 2378 for rev in revsbydrevid[int(drev[b'id'])]:
2377 2379 drevsbyrev[rev] = drev
2378 2380
2379 2381 def phabstatus(ctx):
2380 2382 drev = drevsbyrev[ctx.rev()]
2381 2383 status = ui.label(
2382 2384 b'%(statusName)s' % drev,
2383 2385 b'phabricator.status.%s' % _getstatusname(drev),
2384 2386 )
2385 2387 ui.write(b"\n%s %s\n" % (drev[b'uri'], status))
2386 2388
2387 2389 revs -= smartset.baseset(unknownrevs)
2388 2390 revdag = graphmod.dagwalker(repo, revs)
2389 2391
2390 2392 ui.setconfig(b'experimental', b'graphshorten', True)
2391 2393 displayer._exthook = phabstatus
2392 2394 nodelen = show.longestshortest(repo, revs)
2393 2395 logcmdutil.displaygraph(
2394 2396 ui,
2395 2397 repo,
2396 2398 revdag,
2397 2399 displayer,
2398 2400 graphmod.asciiedges,
2399 2401 props={b'nodelen': nodelen},
2400 2402 )
@@ -1,1890 +1,1890 b''
1 1 """ Multicast DNS Service Discovery for Python, v0.12
2 2 Copyright (C) 2003, Paul Scott-Murphy
3 3
4 4 This module provides a framework for the use of DNS Service Discovery
5 5 using IP multicast. It has been tested against the JRendezvous
6 6 implementation from <a href="http://strangeberry.com">StrangeBerry</a>,
7 7 and against the mDNSResponder from Mac OS X 10.3.8.
8 8
9 9 This library is free software; you can redistribute it and/or
10 10 modify it under the terms of the GNU Lesser General Public
11 11 License as published by the Free Software Foundation; either
12 12 version 2.1 of the License, or (at your option) any later version.
13 13
14 14 This library is distributed in the hope that it will be useful,
15 15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17 17 Lesser General Public License for more details.
18 18
19 19 You should have received a copy of the GNU Lesser General Public
20 20 License along with this library; if not, see
21 21 <http://www.gnu.org/licenses/>.
22 22
23 23 """
24 24
25 25 """0.12 update - allow selection of binding interface
26 26 typo fix - Thanks A. M. Kuchlingi
27 27 removed all use of word 'Rendezvous' - this is an API change"""
28 28
29 29 """0.11 update - correction to comments for addListener method
30 30 support for new record types seen from OS X
31 31 - IPv6 address
32 32 - hostinfo
33 33 ignore unknown DNS record types
34 34 fixes to name decoding
35 35 works alongside other processes using port 5353 (e.g. Mac OS X)
36 36 tested against Mac OS X 10.3.2's mDNSResponder
37 37 corrections to removal of list entries for service browser"""
38 38
39 39 """0.10 update - Jonathon Paisley contributed these corrections:
40 40 always multicast replies, even when query is unicast
41 41 correct a pointer encoding problem
42 42 can now write records in any order
43 43 traceback shown on failure
44 44 better TXT record parsing
45 45 server is now separate from name
46 46 can cancel a service browser
47 47
48 48 modified some unit tests to accommodate these changes"""
49 49
50 50 """0.09 update - remove all records on service unregistration
51 51 fix DOS security problem with readName"""
52 52
53 53 """0.08 update - changed licensing to LGPL"""
54 54
55 55 """0.07 update - faster shutdown on engine
56 56 pointer encoding of outgoing names
57 57 ServiceBrowser now works
58 58 new unit tests"""
59 59
60 60 """0.06 update - small improvements with unit tests
61 61 added defined exception types
62 62 new style objects
63 63 fixed hostname/interface problem
64 64 fixed socket timeout problem
65 65 fixed addServiceListener() typo bug
66 66 using select() for socket reads
67 67 tested on Debian unstable with Python 2.2.2"""
68 68
69 69 """0.05 update - ensure case insensitivity on domain names
70 70 support for unicast DNS queries"""
71 71
72 72 """0.04 update - added some unit tests
73 73 added __ne__ adjuncts where required
74 74 ensure names end in '.local.'
75 75 timeout on receiving socket for clean shutdown"""
76 76
77 77 __author__ = b"Paul Scott-Murphy"
78 78 __email__ = b"paul at scott dash murphy dot com"
79 79 __version__ = b"0.12"
80 80
81 81 import errno
82 82 import itertools
83 83 import select
84 84 import socket
85 85 import struct
86 86 import threading
87 87 import time
88 88 import traceback
89 89
90 90 from mercurial import pycompat
91 91
92 92 __all__ = [b"Zeroconf", b"ServiceInfo", b"ServiceBrowser"]
93 93
94 94 # hook for threads
95 95
96 96 globals()[b'_GLOBAL_DONE'] = 0
97 97
98 98 # Some timing constants
99 99
100 100 _UNREGISTER_TIME = 125
101 101 _CHECK_TIME = 175
102 102 _REGISTER_TIME = 225
103 103 _LISTENER_TIME = 200
104 104 _BROWSER_TIME = 500
105 105
106 106 # Some DNS constants
107 107
108 108 _MDNS_ADDR = r'224.0.0.251'
109 109 _MDNS_PORT = 5353
110 110 _DNS_PORT = 53
111 111 _DNS_TTL = 60 * 60 # one hour default TTL
112 112
113 113 _MAX_MSG_TYPICAL = 1460 # unused
114 114 _MAX_MSG_ABSOLUTE = 8972
115 115
116 116 _FLAGS_QR_MASK = 0x8000 # query response mask
117 117 _FLAGS_QR_QUERY = 0x0000 # query
118 118 _FLAGS_QR_RESPONSE = 0x8000 # response
119 119
120 120 _FLAGS_AA = 0x0400 # Authoritative answer
121 121 _FLAGS_TC = 0x0200 # Truncated
122 122 _FLAGS_RD = 0x0100 # Recursion desired
123 123 _FLAGS_RA = 0x8000 # Recursion available
124 124
125 125 _FLAGS_Z = 0x0040 # Zero
126 126 _FLAGS_AD = 0x0020 # Authentic data
127 127 _FLAGS_CD = 0x0010 # Checking disabled
128 128
129 129 _CLASS_IN = 1
130 130 _CLASS_CS = 2
131 131 _CLASS_CH = 3
132 132 _CLASS_HS = 4
133 133 _CLASS_NONE = 254
134 134 _CLASS_ANY = 255
135 135 _CLASS_MASK = 0x7FFF
136 136 _CLASS_UNIQUE = 0x8000
137 137
138 138 _TYPE_A = 1
139 139 _TYPE_NS = 2
140 140 _TYPE_MD = 3
141 141 _TYPE_MF = 4
142 142 _TYPE_CNAME = 5
143 143 _TYPE_SOA = 6
144 144 _TYPE_MB = 7
145 145 _TYPE_MG = 8
146 146 _TYPE_MR = 9
147 147 _TYPE_NULL = 10
148 148 _TYPE_WKS = 11
149 149 _TYPE_PTR = 12
150 150 _TYPE_HINFO = 13
151 151 _TYPE_MINFO = 14
152 152 _TYPE_MX = 15
153 153 _TYPE_TXT = 16
154 154 _TYPE_AAAA = 28
155 155 _TYPE_SRV = 33
156 156 _TYPE_ANY = 255
157 157
158 158 # Mapping constants to names
159 159
160 160 _CLASSES = {
161 161 _CLASS_IN: b"in",
162 162 _CLASS_CS: b"cs",
163 163 _CLASS_CH: b"ch",
164 164 _CLASS_HS: b"hs",
165 165 _CLASS_NONE: b"none",
166 166 _CLASS_ANY: b"any",
167 167 }
168 168
169 169 _TYPES = {
170 170 _TYPE_A: b"a",
171 171 _TYPE_NS: b"ns",
172 172 _TYPE_MD: b"md",
173 173 _TYPE_MF: b"mf",
174 174 _TYPE_CNAME: b"cname",
175 175 _TYPE_SOA: b"soa",
176 176 _TYPE_MB: b"mb",
177 177 _TYPE_MG: b"mg",
178 178 _TYPE_MR: b"mr",
179 179 _TYPE_NULL: b"null",
180 180 _TYPE_WKS: b"wks",
181 181 _TYPE_PTR: b"ptr",
182 182 _TYPE_HINFO: b"hinfo",
183 183 _TYPE_MINFO: b"minfo",
184 184 _TYPE_MX: b"mx",
185 185 _TYPE_TXT: b"txt",
186 186 _TYPE_AAAA: b"quada",
187 187 _TYPE_SRV: b"srv",
188 188 _TYPE_ANY: b"any",
189 189 }
190 190
191 191 # utility functions
192 192
193 193
194 194 def currentTimeMillis():
195 195 """Current system time in milliseconds"""
196 196 return time.time() * 1000
197 197
198 198
199 199 # Exceptions
200 200
201 201
202 202 class NonLocalNameException(Exception):
203 203 pass
204 204
205 205
206 206 class NonUniqueNameException(Exception):
207 207 pass
208 208
209 209
210 210 class NamePartTooLongException(Exception):
211 211 pass
212 212
213 213
214 214 class AbstractMethodException(Exception):
215 215 pass
216 216
217 217
218 218 class BadTypeInNameException(Exception):
219 219 pass
220 220
221 221
222 222 class BadDomainName(Exception):
223 223 def __init__(self, pos):
224 224 Exception.__init__(self, b"at position %s" % pos)
225 225
226 226
227 227 class BadDomainNameCircular(BadDomainName):
228 228 pass
229 229
230 230
231 231 # implementation classes
232 232
233 233
234 234 class DNSEntry:
235 235 """A DNS entry"""
236 236
237 237 def __init__(self, name, type, clazz):
238 238 self.key = name.lower()
239 239 self.name = name
240 240 self.type = type
241 241 self.clazz = clazz & _CLASS_MASK
242 242 self.unique = (clazz & _CLASS_UNIQUE) != 0
243 243
244 244 def __eq__(self, other):
245 245 """Equality test on name, type, and class"""
246 246 if isinstance(other, DNSEntry):
247 247 return (
248 248 self.name == other.name
249 249 and self.type == other.type
250 250 and self.clazz == other.clazz
251 251 )
252 252 return 0
253 253
254 254 def __ne__(self, other):
255 255 """Non-equality test"""
256 256 return not self.__eq__(other)
257 257
258 258 def getClazz(self, clazz):
259 259 """Class accessor"""
260 260 try:
261 261 return _CLASSES[clazz]
262 262 except KeyError:
263 263 return b"?(%s)" % clazz
264 264
265 265 def getType(self, type):
266 266 """Type accessor"""
267 267 try:
268 268 return _TYPES[type]
269 269 except KeyError:
270 270 return b"?(%s)" % type
271 271
272 272 def toString(self, hdr, other):
273 273 """String representation with additional information"""
274 274 result = b"%s[%s,%s" % (
275 275 hdr,
276 276 self.getType(self.type),
277 277 self.getClazz(self.clazz),
278 278 )
279 279 if self.unique:
280 280 result += b"-unique,"
281 281 else:
282 282 result += b","
283 283 result += self.name
284 284 if other is not None:
285 285 result += b",%s]" % other
286 286 else:
287 287 result += b"]"
288 288 return result
289 289
290 290
291 291 class DNSQuestion(DNSEntry):
292 292 """A DNS question entry"""
293 293
294 294 def __init__(self, name, type, clazz):
295 295 if isinstance(name, str):
296 296 name = name.encode('ascii')
297 297 if not name.endswith(b".local."):
298 298 raise NonLocalNameException(name)
299 299 DNSEntry.__init__(self, name, type, clazz)
300 300
301 301 def answeredBy(self, rec):
302 302 """Returns true if the question is answered by the record"""
303 303 return (
304 304 self.clazz == rec.clazz
305 305 and (self.type == rec.type or self.type == _TYPE_ANY)
306 306 and self.name == rec.name
307 307 )
308 308
309 309 def __repr__(self):
310 310 """String representation"""
311 311 return DNSEntry.toString(self, b"question", None)
312 312
313 313
314 314 class DNSRecord(DNSEntry):
315 315 """A DNS record - like a DNS entry, but has a TTL"""
316 316
317 317 def __init__(self, name, type, clazz, ttl):
318 318 DNSEntry.__init__(self, name, type, clazz)
319 319 self.ttl = ttl
320 320 self.created = currentTimeMillis()
321 321
322 322 def __eq__(self, other):
323 323 """Tests equality as per DNSRecord"""
324 324 if isinstance(other, DNSRecord):
325 325 return DNSEntry.__eq__(self, other)
326 326 return 0
327 327
328 328 def suppressedBy(self, msg):
329 329 """Returns true if any answer in a message can suffice for the
330 330 information held in this record."""
331 331 for record in msg.answers:
332 332 if self.suppressedByAnswer(record):
333 333 return 1
334 334 return 0
335 335
336 336 def suppressedByAnswer(self, other):
337 337 """Returns true if another record has same name, type and class,
338 338 and if its TTL is at least half of this record's."""
339 339 if self == other and other.ttl > (self.ttl / 2):
340 340 return 1
341 341 return 0
342 342
343 343 def getExpirationTime(self, percent):
344 344 """Returns the time at which this record will have expired
345 345 by a certain percentage."""
346 346 return self.created + (percent * self.ttl * 10)
347 347
348 348 def getRemainingTTL(self, now):
349 349 """Returns the remaining TTL in seconds."""
350 350 return max(0, (self.getExpirationTime(100) - now) / 1000)
351 351
352 352 def isExpired(self, now):
353 353 """Returns true if this record has expired."""
354 354 return self.getExpirationTime(100) <= now
355 355
356 356 def isStale(self, now):
357 357 """Returns true if this record is at least half way expired."""
358 358 return self.getExpirationTime(50) <= now
359 359
360 360 def resetTTL(self, other):
361 361 """Sets this record's TTL and created time to that of
362 362 another record."""
363 363 self.created = other.created
364 364 self.ttl = other.ttl
365 365
366 366 def write(self, out):
367 367 """Abstract method"""
368 368 raise AbstractMethodException
369 369
370 370 def toString(self, other):
371 371 """String representation with additional information"""
372 372 arg = b"%s/%s,%s" % (
373 373 self.ttl,
374 374 self.getRemainingTTL(currentTimeMillis()),
375 375 other,
376 376 )
377 377 return DNSEntry.toString(self, b"record", arg)
378 378
379 379
380 380 class DNSAddress(DNSRecord):
381 381 """A DNS address record"""
382 382
383 383 def __init__(self, name, type, clazz, ttl, address):
384 384 DNSRecord.__init__(self, name, type, clazz, ttl)
385 385 self.address = address
386 386
387 387 def write(self, out):
388 388 """Used in constructing an outgoing packet"""
389 389 out.writeString(self.address, len(self.address))
390 390
391 391 def __eq__(self, other):
392 392 """Tests equality on address"""
393 393 if isinstance(other, DNSAddress):
394 394 return self.address == other.address
395 395 return 0
396 396
397 397 def __repr__(self):
398 398 """String representation"""
399 399 try:
400 400 return socket.inet_ntoa(self.address)
401 401 except Exception:
402 402 return self.address
403 403
404 404
405 405 class DNSHinfo(DNSRecord):
406 406 """A DNS host information record"""
407 407
408 408 def __init__(self, name, type, clazz, ttl, cpu, os):
409 409 DNSRecord.__init__(self, name, type, clazz, ttl)
410 410 self.cpu = cpu
411 411 self.os = os
412 412
413 413 def write(self, out):
414 414 """Used in constructing an outgoing packet"""
415 415 out.writeString(self.cpu, len(self.cpu))
416 416 out.writeString(self.os, len(self.os))
417 417
418 418 def __eq__(self, other):
419 419 """Tests equality on cpu and os"""
420 420 if isinstance(other, DNSHinfo):
421 421 return self.cpu == other.cpu and self.os == other.os
422 422 return 0
423 423
424 424 def __repr__(self):
425 425 """String representation"""
426 426 return self.cpu + b" " + self.os
427 427
428 428
429 429 class DNSPointer(DNSRecord):
430 430 """A DNS pointer record"""
431 431
432 432 def __init__(self, name, type, clazz, ttl, alias):
433 433 DNSRecord.__init__(self, name, type, clazz, ttl)
434 434 self.alias = alias
435 435
436 436 def write(self, out):
437 437 """Used in constructing an outgoing packet"""
438 438 out.writeName(self.alias)
439 439
440 440 def __eq__(self, other):
441 441 """Tests equality on alias"""
442 442 if isinstance(other, DNSPointer):
443 443 return self.alias == other.alias
444 444 return 0
445 445
446 446 def __repr__(self):
447 447 """String representation"""
448 448 return self.toString(self.alias)
449 449
450 450
451 451 class DNSText(DNSRecord):
452 452 """A DNS text record"""
453 453
454 454 def __init__(self, name, type, clazz, ttl, text):
455 455 DNSRecord.__init__(self, name, type, clazz, ttl)
456 456 self.text = text
457 457
458 458 def write(self, out):
459 459 """Used in constructing an outgoing packet"""
460 460 out.writeString(self.text, len(self.text))
461 461
462 462 def __eq__(self, other):
463 463 """Tests equality on text"""
464 464 if isinstance(other, DNSText):
465 465 return self.text == other.text
466 466 return 0
467 467
468 468 def __repr__(self):
469 469 """String representation"""
470 470 if len(self.text) > 10:
471 471 return self.toString(self.text[:7] + b"...")
472 472 else:
473 473 return self.toString(self.text)
474 474
475 475
476 476 class DNSService(DNSRecord):
477 477 """A DNS service record"""
478 478
479 479 def __init__(self, name, type, clazz, ttl, priority, weight, port, server):
480 480 DNSRecord.__init__(self, name, type, clazz, ttl)
481 481 self.priority = priority
482 482 self.weight = weight
483 483 self.port = port
484 484 self.server = server
485 485
486 486 def write(self, out):
487 487 """Used in constructing an outgoing packet"""
488 488 out.writeShort(self.priority)
489 489 out.writeShort(self.weight)
490 490 out.writeShort(self.port)
491 491 out.writeName(self.server)
492 492
493 493 def __eq__(self, other):
494 494 """Tests equality on priority, weight, port and server"""
495 495 if isinstance(other, DNSService):
496 496 return (
497 497 self.priority == other.priority
498 498 and self.weight == other.weight
499 499 and self.port == other.port
500 500 and self.server == other.server
501 501 )
502 502 return 0
503 503
504 504 def __repr__(self):
505 505 """String representation"""
506 506 return self.toString(b"%s:%s" % (self.server, self.port))
507 507
508 508
509 509 class DNSIncoming:
510 510 """Object representation of an incoming DNS packet"""
511 511
512 512 def __init__(self, data):
513 513 """Constructor from string holding bytes of packet"""
514 514 self.offset = 0
515 515 self.data = data
516 516 self.questions = []
517 517 self.answers = []
518 518 self.numquestions = 0
519 519 self.numanswers = 0
520 520 self.numauthorities = 0
521 521 self.numadditionals = 0
522 522
523 523 self.readHeader()
524 524 self.readQuestions()
525 525 self.readOthers()
526 526
527 527 def readHeader(self):
528 528 """Reads header portion of packet"""
529 529 format = b'!HHHHHH'
530 530 length = struct.calcsize(format)
531 531 info = struct.unpack(
532 532 format, self.data[self.offset : self.offset + length]
533 533 )
534 534 self.offset += length
535 535
536 536 self.id = info[0]
537 537 self.flags = info[1]
538 538 self.numquestions = info[2]
539 539 self.numanswers = info[3]
540 540 self.numauthorities = info[4]
541 541 self.numadditionals = info[5]
542 542
543 543 def readQuestions(self):
544 544 """Reads questions section of packet"""
545 545 format = b'!HH'
546 546 length = struct.calcsize(format)
547 547 for i in range(0, self.numquestions):
548 548 name = self.readName()
549 549 info = struct.unpack(
550 550 format, self.data[self.offset : self.offset + length]
551 551 )
552 552 self.offset += length
553 553
554 554 try:
555 555 question = DNSQuestion(name, info[0], info[1])
556 556 self.questions.append(question)
557 557 except NonLocalNameException:
558 558 pass
559 559
560 560 def readInt(self):
561 561 """Reads an integer from the packet"""
562 562 format = b'!I'
563 563 length = struct.calcsize(format)
564 564 info = struct.unpack(
565 565 format, self.data[self.offset : self.offset + length]
566 566 )
567 567 self.offset += length
568 568 return info[0]
569 569
570 570 def readCharacterString(self):
571 571 """Reads a character string from the packet"""
572 572 length = ord(self.data[self.offset])
573 573 self.offset += 1
574 574 return self.readString(length)
575 575
576 576 def readString(self, len):
577 577 """Reads a string of a given length from the packet"""
578 578 format = b'!%ds' % len
579 579 length = struct.calcsize(format)
580 580 info = struct.unpack(
581 581 format, self.data[self.offset : self.offset + length]
582 582 )
583 583 self.offset += length
584 584 return info[0]
585 585
586 586 def readUnsignedShort(self):
587 587 """Reads an unsigned short from the packet"""
588 588 format = b'!H'
589 589 length = struct.calcsize(format)
590 590 info = struct.unpack(
591 591 format, self.data[self.offset : self.offset + length]
592 592 )
593 593 self.offset += length
594 594 return info[0]
595 595
596 596 def readOthers(self):
597 597 """Reads answers, authorities and additionals section of the packet"""
598 598 format = b'!HHiH'
599 599 length = struct.calcsize(format)
600 600 n = self.numanswers + self.numauthorities + self.numadditionals
601 601 for i in range(0, n):
602 602 domain = self.readName()
603 603 info = struct.unpack(
604 604 format, self.data[self.offset : self.offset + length]
605 605 )
606 606 self.offset += length
607 607
608 608 rec = None
609 609 if info[0] == _TYPE_A:
610 610 rec = DNSAddress(
611 611 domain, info[0], info[1], info[2], self.readString(4)
612 612 )
613 613 elif info[0] == _TYPE_CNAME or info[0] == _TYPE_PTR:
614 614 rec = DNSPointer(
615 615 domain, info[0], info[1], info[2], self.readName()
616 616 )
617 617 elif info[0] == _TYPE_TXT:
618 618 rec = DNSText(
619 619 domain, info[0], info[1], info[2], self.readString(info[3])
620 620 )
621 621 elif info[0] == _TYPE_SRV:
622 622 rec = DNSService(
623 623 domain,
624 624 info[0],
625 625 info[1],
626 626 info[2],
627 627 self.readUnsignedShort(),
628 628 self.readUnsignedShort(),
629 629 self.readUnsignedShort(),
630 630 self.readName(),
631 631 )
632 632 elif info[0] == _TYPE_HINFO:
633 633 rec = DNSHinfo(
634 634 domain,
635 635 info[0],
636 636 info[1],
637 637 info[2],
638 638 self.readCharacterString(),
639 639 self.readCharacterString(),
640 640 )
641 641 elif info[0] == _TYPE_AAAA:
642 642 rec = DNSAddress(
643 643 domain, info[0], info[1], info[2], self.readString(16)
644 644 )
645 645 else:
646 646 # Try to ignore types we don't know about
647 647 # this may mean the rest of the name is
648 648 # unable to be parsed, and may show errors
649 649 # so this is left for debugging. New types
650 650 # encountered need to be parsed properly.
651 651 #
652 652 # print "UNKNOWN TYPE = " + str(info[0])
653 653 # raise BadTypeInNameException
654 654 self.offset += info[3]
655 655
656 656 if rec is not None:
657 657 self.answers.append(rec)
658 658
659 659 def isQuery(self):
660 660 """Returns true if this is a query"""
661 661 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_QUERY
662 662
663 663 def isResponse(self):
664 664 """Returns true if this is a response"""
665 665 return (self.flags & _FLAGS_QR_MASK) == _FLAGS_QR_RESPONSE
666 666
667 667 def readUTF(self, offset, len):
668 668 """Reads a UTF-8 string of a given length from the packet"""
669 669 return self.data[offset : offset + len].decode('utf-8')
670 670
671 671 def readName(self):
672 672 """Reads a domain name from the packet"""
673 673 result = r''
674 674 off = self.offset
675 675 next = -1
676 676 first = off
677 677
678 678 while True:
679 679 len = ord(self.data[off : off + 1])
680 680 off += 1
681 681 if len == 0:
682 682 break
683 683 t = len & 0xC0
684 684 if t == 0x00:
685 685 result = ''.join((result, self.readUTF(off, len) + '.'))
686 686 off += len
687 687 elif t == 0xC0:
688 688 if next < 0:
689 689 next = off + 1
690 690 off = ((len & 0x3F) << 8) | ord(self.data[off : off + 1])
691 691 if off >= first:
692 692 raise BadDomainNameCircular(off)
693 693 first = off
694 694 else:
695 695 raise BadDomainName(off)
696 696
697 697 if next >= 0:
698 698 self.offset = next
699 699 else:
700 700 self.offset = off
701 701
702 702 return result
703 703
704 704
705 705 class DNSOutgoing:
706 706 """Object representation of an outgoing packet"""
707 707
708 708 def __init__(self, flags, multicast=1):
709 709 self.finished = 0
710 710 self.id = 0
711 711 self.multicast = multicast
712 712 self.flags = flags
713 713 self.names = {}
714 714 self.data = []
715 715 self.size = 12
716 716
717 717 self.questions = []
718 718 self.answers = []
719 719 self.authorities = []
720 720 self.additionals = []
721 721
722 722 def addQuestion(self, record):
723 723 """Adds a question"""
724 724 self.questions.append(record)
725 725
726 726 def addAnswer(self, inp, record):
727 727 """Adds an answer"""
728 728 if not record.suppressedBy(inp):
729 729 self.addAnswerAtTime(record, 0)
730 730
731 731 def addAnswerAtTime(self, record, now):
732 732 """Adds an answer if if does not expire by a certain time"""
733 733 if record is not None:
734 734 if now == 0 or not record.isExpired(now):
735 735 self.answers.append((record, now))
736 736
737 737 def addAuthoritativeAnswer(self, record):
738 738 """Adds an authoritative answer"""
739 739 self.authorities.append(record)
740 740
741 741 def addAdditionalAnswer(self, record):
742 742 """Adds an additional answer"""
743 743 self.additionals.append(record)
744 744
745 745 def writeByte(self, value):
746 746 """Writes a single byte to the packet"""
747 747 format = b'!c'
748 748 self.data.append(struct.pack(format, chr(value)))
749 749 self.size += 1
750 750
751 751 def insertShort(self, index, value):
752 752 """Inserts an unsigned short in a certain position in the packet"""
753 753 format = b'!H'
754 754 self.data.insert(index, struct.pack(format, value))
755 755 self.size += 2
756 756
757 757 def writeShort(self, value):
758 758 """Writes an unsigned short to the packet"""
759 759 format = b'!H'
760 760 self.data.append(struct.pack(format, value))
761 761 self.size += 2
762 762
763 763 def writeInt(self, value):
764 764 """Writes an unsigned integer to the packet"""
765 765 format = b'!I'
766 766 self.data.append(struct.pack(format, int(value)))
767 767 self.size += 4
768 768
769 769 def writeString(self, value, length):
770 770 """Writes a string to the packet"""
771 771 format = '!' + str(length) + 's'
772 772 self.data.append(struct.pack(format, value))
773 773 self.size += length
774 774
775 775 def writeUTF(self, s):
776 776 """Writes a UTF-8 string of a given length to the packet"""
777 777 utfstr = s.encode('utf-8')
778 778 length = len(utfstr)
779 779 if length > 64:
780 780 raise NamePartTooLongException
781 781 self.writeByte(length)
782 782 self.writeString(utfstr, length)
783 783
784 784 def writeName(self, name):
785 785 """Writes a domain name to the packet"""
786 786
787 787 try:
788 788 # Find existing instance of this name in packet
789 789 #
790 790 index = self.names[name]
791 791 except KeyError:
792 792 # No record of this name already, so write it
793 793 # out as normal, recording the location of the name
794 794 # for future pointers to it.
795 795 #
796 796 self.names[name] = self.size
797 797 parts = name.split(b'.')
798 798 if parts[-1] == b'':
799 799 parts = parts[:-1]
800 800 for part in parts:
801 801 self.writeUTF(part)
802 802 self.writeByte(0)
803 803 return
804 804
805 805 # An index was found, so write a pointer to it
806 806 #
807 807 self.writeByte((index >> 8) | 0xC0)
808 808 self.writeByte(index)
809 809
810 810 def writeQuestion(self, question):
811 811 """Writes a question to the packet"""
812 812 self.writeName(question.name)
813 813 self.writeShort(question.type)
814 814 self.writeShort(question.clazz)
815 815
816 816 def writeRecord(self, record, now):
817 817 """Writes a record (answer, authoritative answer, additional) to
818 818 the packet"""
819 819 self.writeName(record.name)
820 820 self.writeShort(record.type)
821 821 if record.unique and self.multicast:
822 822 self.writeShort(record.clazz | _CLASS_UNIQUE)
823 823 else:
824 824 self.writeShort(record.clazz)
825 825 if now == 0:
826 826 self.writeInt(record.ttl)
827 827 else:
828 828 self.writeInt(record.getRemainingTTL(now))
829 829 index = len(self.data)
830 830 # Adjust size for the short we will write before this record
831 831 #
832 832 self.size += 2
833 833 record.write(self)
834 834 self.size -= 2
835 835
836 836 length = len(b''.join(self.data[index:]))
837 837 self.insertShort(index, length) # Here is the short we adjusted for
838 838
839 839 def packet(self):
840 840 """Returns a string containing the packet's bytes
841 841
842 842 No further parts should be added to the packet once this
843 843 is done."""
844 844 if not self.finished:
845 845 self.finished = 1
846 846 for question in self.questions:
847 847 self.writeQuestion(question)
848 848 for answer, time_ in self.answers:
849 849 self.writeRecord(answer, time_)
850 850 for authority in self.authorities:
851 851 self.writeRecord(authority, 0)
852 852 for additional in self.additionals:
853 853 self.writeRecord(additional, 0)
854 854
855 855 self.insertShort(0, len(self.additionals))
856 856 self.insertShort(0, len(self.authorities))
857 857 self.insertShort(0, len(self.answers))
858 858 self.insertShort(0, len(self.questions))
859 859 self.insertShort(0, self.flags)
860 860 if self.multicast:
861 861 self.insertShort(0, 0)
862 862 else:
863 863 self.insertShort(0, self.id)
864 864 return b''.join(self.data)
865 865
866 866
867 867 class DNSCache:
868 868 """A cache of DNS entries"""
869 869
870 870 def __init__(self):
871 871 self.cache = {}
872 872
873 873 def add(self, entry):
874 874 """Adds an entry"""
875 875 try:
876 876 list = self.cache[entry.key]
877 877 except KeyError:
878 878 list = self.cache[entry.key] = []
879 879 list.append(entry)
880 880
881 881 def remove(self, entry):
882 882 """Removes an entry"""
883 883 try:
884 884 list = self.cache[entry.key]
885 885 list.remove(entry)
886 886 except KeyError:
887 887 pass
888 888
889 889 def get(self, entry):
890 890 """Gets an entry by key. Will return None if there is no
891 891 matching entry."""
892 892 try:
893 893 list = self.cache[entry.key]
894 894 return list[list.index(entry)]
895 895 except (KeyError, ValueError):
896 896 return None
897 897
898 898 def getByDetails(self, name, type, clazz):
899 899 """Gets an entry by details. Will return None if there is
900 900 no matching entry."""
901 901 entry = DNSEntry(name, type, clazz)
902 902 return self.get(entry)
903 903
904 904 def entriesWithName(self, name):
905 905 """Returns a list of entries whose key matches the name."""
906 906 try:
907 907 return self.cache[name]
908 908 except KeyError:
909 909 return []
910 910
911 911 def entries(self):
912 912 """Returns a list of all entries"""
913 913 try:
914 914 return list(itertools.chain.from_iterable(self.cache.values()))
915 915 except Exception:
916 916 return []
917 917
918 918
919 919 class Engine(threading.Thread):
920 920 """An engine wraps read access to sockets, allowing objects that
921 921 need to receive data from sockets to be called back when the
922 922 sockets are ready.
923 923
924 924 A reader needs a handle_read() method, which is called when the socket
925 925 it is interested in is ready for reading.
926 926
927 927 Writers are not implemented here, because we only send short
928 928 packets.
929 929 """
930 930
931 931 def __init__(self, zeroconf):
932 932 threading.Thread.__init__(self)
933 933 self.zeroconf = zeroconf
934 934 self.readers = {} # maps socket to reader
935 935 self.timeout = 5
936 936 self.condition = threading.Condition()
937 937 self.start()
938 938
939 939 def run(self):
940 940 while not globals()[b'_GLOBAL_DONE']:
941 941 rs = self.getReaders()
942 942 if len(rs) == 0:
943 943 # No sockets to manage, but we wait for the timeout
944 944 # or addition of a socket
945 945 #
946 946 self.condition.acquire()
947 947 self.condition.wait(self.timeout)
948 948 self.condition.release()
949 949 else:
950 950 try:
951 951 rr, wr, er = select.select(rs, [], [], self.timeout)
952 952 for sock in rr:
953 953 try:
954 954 self.readers[sock].handle_read()
955 955 except Exception:
956 956 if not globals()[b'_GLOBAL_DONE']:
957 957 traceback.print_exc()
958 958 except Exception:
959 959 pass
960 960
961 961 def getReaders(self):
962 962 self.condition.acquire()
963 963 result = self.readers.keys()
964 964 self.condition.release()
965 965 return result
966 966
967 967 def addReader(self, reader, socket):
968 968 self.condition.acquire()
969 969 self.readers[socket] = reader
970 970 self.condition.notify()
971 971 self.condition.release()
972 972
973 973 def delReader(self, socket):
974 974 self.condition.acquire()
975 975 del self.readers[socket]
976 976 self.condition.notify()
977 977 self.condition.release()
978 978
979 979 def notify(self):
980 980 self.condition.acquire()
981 981 self.condition.notify()
982 982 self.condition.release()
983 983
984 984
985 985 class Listener:
986 986 """A Listener is used by this module to listen on the multicast
987 987 group to which DNS messages are sent, allowing the implementation
988 988 to cache information as it arrives.
989 989
990 990 It requires registration with an Engine object in order to have
991 991 the read() method called when a socket is available for reading."""
992 992
993 993 def __init__(self, zeroconf):
994 994 self.zeroconf = zeroconf
995 995 self.zeroconf.engine.addReader(self, self.zeroconf.socket)
996 996
997 997 def handle_read(self):
998 998 sock = self.zeroconf.socket
999 999 try:
1000 1000 data, (addr, port) = sock.recvfrom(_MAX_MSG_ABSOLUTE)
1001 1001 except socket.error as e:
1002 1002 if e.errno == errno.EBADF:
1003 1003 # some other thread may close the socket
1004 1004 return
1005 1005 else:
1006 1006 raise
1007 1007 self.data = data
1008 1008 msg = DNSIncoming(data)
1009 1009 if msg.isQuery():
1010 1010 # Always multicast responses
1011 1011 #
1012 1012 if port == _MDNS_PORT:
1013 1013 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
1014 1014 # If it's not a multicast query, reply via unicast
1015 1015 # and multicast
1016 1016 #
1017 1017 elif port == _DNS_PORT:
1018 1018 self.zeroconf.handleQuery(msg, addr, port)
1019 1019 self.zeroconf.handleQuery(msg, _MDNS_ADDR, _MDNS_PORT)
1020 1020 else:
1021 1021 self.zeroconf.handleResponse(msg)
1022 1022
1023 1023
1024 1024 class Reaper(threading.Thread):
1025 1025 """A Reaper is used by this module to remove cache entries that
1026 1026 have expired."""
1027 1027
1028 1028 def __init__(self, zeroconf):
1029 1029 threading.Thread.__init__(self)
1030 1030 self.zeroconf = zeroconf
1031 1031 self.start()
1032 1032
1033 1033 def run(self):
1034 1034 while True:
1035 1035 self.zeroconf.wait(10 * 1000)
1036 1036 if globals()[b'_GLOBAL_DONE']:
1037 1037 return
1038 1038 now = currentTimeMillis()
1039 1039 for record in self.zeroconf.cache.entries():
1040 1040 if record.isExpired(now):
1041 1041 self.zeroconf.updateRecord(now, record)
1042 1042 self.zeroconf.cache.remove(record)
1043 1043
1044 1044
1045 1045 class ServiceBrowser(threading.Thread):
1046 1046 """Used to browse for a service of a specific type.
1047 1047
1048 1048 The listener object will have its addService() and
1049 1049 removeService() methods called when this browser
1050 1050 discovers changes in the services availability."""
1051 1051
1052 1052 def __init__(self, zeroconf, type, listener):
1053 1053 """Creates a browser for a specific type"""
1054 1054 threading.Thread.__init__(self)
1055 1055 self.zeroconf = zeroconf
1056 1056 self.type = type
1057 1057 self.listener = listener
1058 1058 self.services = {}
1059 1059 self.nexttime = currentTimeMillis()
1060 1060 self.delay = _BROWSER_TIME
1061 1061 self.list = []
1062 1062
1063 1063 self.done = 0
1064 1064
1065 1065 self.zeroconf.addListener(
1066 1066 self, DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN)
1067 1067 )
1068 1068 self.start()
1069 1069
1070 1070 def updateRecord(self, zeroconf, now, record):
1071 1071 """Callback invoked by Zeroconf when new information arrives.
1072 1072
1073 1073 Updates information required by browser in the Zeroconf cache."""
1074 1074 if record.type == _TYPE_PTR and record.name == self.type:
1075 1075 expired = record.isExpired(now)
1076 1076 try:
1077 1077 oldrecord = self.services[record.alias.lower()]
1078 1078 if not expired:
1079 1079 oldrecord.resetTTL(record)
1080 1080 else:
1081 1081 del self.services[record.alias.lower()]
1082 1082 callback = lambda x: self.listener.removeService(
1083 1083 x, self.type, record.alias
1084 1084 )
1085 1085 self.list.append(callback)
1086 1086 return
1087 1087 except Exception:
1088 1088 if not expired:
1089 1089 self.services[record.alias.lower()] = record
1090 1090 callback = lambda x: self.listener.addService(
1091 1091 x, self.type, record.alias
1092 1092 )
1093 1093 self.list.append(callback)
1094 1094
1095 1095 expires = record.getExpirationTime(75)
1096 1096 if expires < self.nexttime:
1097 1097 self.nexttime = expires
1098 1098
1099 1099 def cancel(self):
1100 1100 self.done = 1
1101 1101 self.zeroconf.notifyAll()
1102 1102
1103 1103 def run(self):
1104 1104 while True:
1105 1105 event = None
1106 1106 now = currentTimeMillis()
1107 1107 if len(self.list) == 0 and self.nexttime > now:
1108 1108 self.zeroconf.wait(self.nexttime - now)
1109 1109 if globals()[b'_GLOBAL_DONE'] or self.done:
1110 1110 return
1111 1111 now = currentTimeMillis()
1112 1112
1113 1113 if self.nexttime <= now:
1114 1114 out = DNSOutgoing(_FLAGS_QR_QUERY)
1115 1115 out.addQuestion(DNSQuestion(self.type, _TYPE_PTR, _CLASS_IN))
1116 1116 for record in self.services.values():
1117 1117 if not record.isExpired(now):
1118 1118 out.addAnswerAtTime(record, now)
1119 1119 self.zeroconf.send(out)
1120 1120 self.nexttime = now + self.delay
1121 1121 self.delay = min(20 * 1000, self.delay * 2)
1122 1122
1123 1123 if len(self.list) > 0:
1124 1124 event = self.list.pop(0)
1125 1125
1126 1126 if event is not None:
1127 1127 event(self.zeroconf)
1128 1128
1129 1129
1130 1130 class ServiceInfo:
1131 1131 """Service information"""
1132 1132
1133 1133 def __init__(
1134 1134 self,
1135 1135 type,
1136 1136 name,
1137 1137 address=None,
1138 1138 port=None,
1139 1139 weight=0,
1140 1140 priority=0,
1141 1141 properties=None,
1142 1142 server=None,
1143 1143 ):
1144 1144 """Create a service description.
1145 1145
1146 1146 type: fully qualified service type name
1147 1147 name: fully qualified service name
1148 1148 address: IP address as unsigned short, network byte order
1149 1149 port: port that the service runs on
1150 1150 weight: weight of the service
1151 1151 priority: priority of the service
1152 1152 properties: dictionary of properties (or a string holding the bytes for
1153 1153 the text field)
1154 1154 server: fully qualified name for service host (defaults to name)"""
1155 1155
1156 1156 if not name.endswith(type):
1157 1157 raise BadTypeInNameException
1158 1158 self.type = type
1159 1159 self.name = name
1160 1160 self.address = address
1161 1161 self.port = port
1162 1162 self.weight = weight
1163 1163 self.priority = priority
1164 1164 if server:
1165 1165 self.server = server
1166 1166 else:
1167 1167 self.server = name
1168 1168 self.setProperties(properties)
1169 1169
1170 1170 def setProperties(self, properties):
1171 1171 """Sets properties and text of this info from a dictionary"""
1172 1172 if isinstance(properties, dict):
1173 1173 self.properties = properties
1174 1174 list = []
1175 1175 result = b''
1176 1176 for key in properties:
1177 1177 value = properties[key]
1178 1178 if value is None:
1179 1179 suffix = b''
1180 1180 elif isinstance(value, str):
1181 1181 suffix = value
1182 1182 elif isinstance(value, int):
1183 1183 if value:
1184 1184 suffix = b'true'
1185 1185 else:
1186 1186 suffix = b'false'
1187 1187 else:
1188 1188 suffix = b''
1189 1189 list.append(b'='.join((key, suffix)))
1190 1190 for item in list:
1191 1191 result = b''.join(
1192 1192 (
1193 1193 result,
1194 1194 struct.pack(b'!c', pycompat.bytechr(len(item))),
1195 1195 item,
1196 1196 )
1197 1197 )
1198 1198 self.text = result
1199 1199 else:
1200 1200 self.text = properties
1201 1201
1202 1202 def setText(self, text):
1203 1203 """Sets properties and text given a text field"""
1204 1204 self.text = text
1205 1205 try:
1206 1206 result = {}
1207 1207 end = len(text)
1208 1208 index = 0
1209 1209 strs = []
1210 1210 while index < end:
1211 1211 length = ord(text[index])
1212 1212 index += 1
1213 1213 strs.append(text[index : index + length])
1214 1214 index += length
1215 1215
1216 1216 for s in strs:
1217 1217 eindex = s.find(b'=')
1218 1218 if eindex == -1:
1219 1219 # No equals sign at all
1220 1220 key = s
1221 1221 value = 0
1222 1222 else:
1223 1223 key = s[:eindex]
1224 1224 value = s[eindex + 1 :]
1225 1225 if value == b'true':
1226 1226 value = 1
1227 1227 elif value == b'false' or not value:
1228 1228 value = 0
1229 1229
1230 1230 # Only update non-existent properties
1231 1231 if key and result.get(key) is None:
1232 1232 result[key] = value
1233 1233
1234 1234 self.properties = result
1235 1235 except Exception:
1236 1236 traceback.print_exc()
1237 1237 self.properties = None
1238 1238
1239 1239 def getType(self):
1240 1240 """Type accessor"""
1241 1241 return self.type
1242 1242
1243 1243 def getName(self):
1244 1244 """Name accessor"""
1245 1245 if self.type is not None and self.name.endswith(b"." + self.type):
1246 1246 return self.name[: len(self.name) - len(self.type) - 1]
1247 1247 return self.name
1248 1248
1249 1249 def getAddress(self):
1250 1250 """Address accessor"""
1251 1251 return self.address
1252 1252
1253 1253 def getPort(self):
1254 1254 """Port accessor"""
1255 1255 return self.port
1256 1256
1257 1257 def getPriority(self):
1258 1258 """Priority accessor"""
1259 1259 return self.priority
1260 1260
1261 1261 def getWeight(self):
1262 1262 """Weight accessor"""
1263 1263 return self.weight
1264 1264
1265 1265 def getProperties(self):
1266 1266 """Properties accessor"""
1267 1267 return self.properties
1268 1268
1269 1269 def getText(self):
1270 1270 """Text accessor"""
1271 1271 return self.text
1272 1272
1273 1273 def getServer(self):
1274 1274 """Server accessor"""
1275 1275 return self.server
1276 1276
1277 1277 def updateRecord(self, zeroconf, now, record):
1278 1278 """Updates service information from a DNS record"""
1279 1279 if record is not None and not record.isExpired(now):
1280 1280 if record.type == _TYPE_A:
1281 1281 # if record.name == self.name:
1282 1282 if record.name == self.server:
1283 1283 self.address = record.address
1284 1284 elif record.type == _TYPE_SRV:
1285 1285 if record.name == self.name:
1286 1286 self.server = record.server
1287 1287 self.port = record.port
1288 1288 self.weight = record.weight
1289 1289 self.priority = record.priority
1290 1290 # self.address = None
1291 1291 self.updateRecord(
1292 1292 zeroconf,
1293 1293 now,
1294 1294 zeroconf.cache.getByDetails(
1295 1295 self.server, _TYPE_A, _CLASS_IN
1296 1296 ),
1297 1297 )
1298 1298 elif record.type == _TYPE_TXT:
1299 1299 if record.name == self.name:
1300 1300 self.setText(record.text)
1301 1301
1302 1302 def request(self, zeroconf, timeout):
1303 1303 """Returns true if the service could be discovered on the
1304 1304 network, and updates this object with details discovered.
1305 1305 """
1306 1306 now = currentTimeMillis()
1307 1307 delay = _LISTENER_TIME
1308 1308 next = now + delay
1309 1309 last = now + timeout
1310 1310 try:
1311 1311 zeroconf.addListener(
1312 1312 self, DNSQuestion(self.name, _TYPE_ANY, _CLASS_IN)
1313 1313 )
1314 1314 while (
1315 1315 self.server is None or self.address is None or self.text is None
1316 1316 ):
1317 1317 if last <= now:
1318 1318 return 0
1319 1319 if next <= now:
1320 1320 out = DNSOutgoing(_FLAGS_QR_QUERY)
1321 1321 out.addQuestion(
1322 1322 DNSQuestion(self.name, _TYPE_SRV, _CLASS_IN)
1323 1323 )
1324 1324 out.addAnswerAtTime(
1325 1325 zeroconf.cache.getByDetails(
1326 1326 self.name, _TYPE_SRV, _CLASS_IN
1327 1327 ),
1328 1328 now,
1329 1329 )
1330 1330 out.addQuestion(
1331 1331 DNSQuestion(self.name, _TYPE_TXT, _CLASS_IN)
1332 1332 )
1333 1333 out.addAnswerAtTime(
1334 1334 zeroconf.cache.getByDetails(
1335 1335 self.name, _TYPE_TXT, _CLASS_IN
1336 1336 ),
1337 1337 now,
1338 1338 )
1339 1339 if self.server is not None:
1340 1340 out.addQuestion(
1341 1341 DNSQuestion(self.server, _TYPE_A, _CLASS_IN)
1342 1342 )
1343 1343 out.addAnswerAtTime(
1344 1344 zeroconf.cache.getByDetails(
1345 1345 self.server, _TYPE_A, _CLASS_IN
1346 1346 ),
1347 1347 now,
1348 1348 )
1349 1349 zeroconf.send(out)
1350 1350 next = now + delay
1351 1351 delay = delay * 2
1352 1352
1353 1353 zeroconf.wait(min(next, last) - now)
1354 1354 now = currentTimeMillis()
1355 1355 result = 1
1356 1356 finally:
1357 1357 zeroconf.removeListener(self)
1358 1358
1359 1359 return result
1360 1360
1361 1361 def __eq__(self, other):
1362 1362 """Tests equality of service name"""
1363 1363 if isinstance(other, ServiceInfo):
1364 1364 return other.name == self.name
1365 1365 return 0
1366 1366
1367 1367 def __ne__(self, other):
1368 1368 """Non-equality test"""
1369 1369 return not self.__eq__(other)
1370 1370
1371 1371 def __repr__(self):
1372 1372 """String representation"""
1373 1373 result = b"service[%s,%s:%s," % (
1374 1374 self.name,
1375 1375 socket.inet_ntoa(self.getAddress()),
1376 1376 self.port,
1377 1377 )
1378 1378 if self.text is None:
1379 1379 result += b"None"
1380 1380 else:
1381 1381 if len(self.text) < 20:
1382 1382 result += self.text
1383 1383 else:
1384 1384 result += self.text[:17] + b"..."
1385 1385 result += b"]"
1386 1386 return result
1387 1387
1388 1388
1389 1389 class Zeroconf:
1390 1390 """Implementation of Zeroconf Multicast DNS Service Discovery
1391 1391
1392 1392 Supports registration, unregistration, queries and browsing.
1393 1393 """
1394 1394
1395 1395 def __init__(self, bindaddress=None):
1396 1396 """Creates an instance of the Zeroconf class, establishing
1397 1397 multicast communications, listening and reaping threads."""
1398 1398 globals()[b'_GLOBAL_DONE'] = 0
1399 1399 if bindaddress is None:
1400 1400 self.intf = socket.gethostbyname(socket.gethostname())
1401 1401 else:
1402 1402 self.intf = bindaddress
1403 1403 self.group = (b'', _MDNS_PORT)
1404 1404 self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1405 1405 try:
1406 1406 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1407 1407 self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
1408 1408 except Exception:
1409 1409 # SO_REUSEADDR should be equivalent to SO_REUSEPORT for
1410 1410 # multicast UDP sockets (p 731, "TCP/IP Illustrated,
1411 1411 # Volume 2"), but some BSD-derived systems require
1412 1412 # SO_REUSEPORT to be specified explicitly. Also, not all
1413 1413 # versions of Python have SO_REUSEPORT available. So
1414 1414 # if you're on a BSD-based system, and haven't upgraded
1415 1415 # to Python 2.3 yet, you may find this library doesn't
1416 1416 # work as expected.
1417 1417 #
1418 1418 pass
1419 1419 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_TTL, b"\xff")
1420 1420 self.socket.setsockopt(socket.SOL_IP, socket.IP_MULTICAST_LOOP, b"\x01")
1421 1421 try:
1422 1422 self.socket.bind(self.group)
1423 1423 except Exception:
1424 1424 # Some versions of linux raise an exception even though
1425 1425 # SO_REUSEADDR and SO_REUSEPORT have been set, so ignore it
1426 1426 pass
1427 1427 self.socket.setsockopt(
1428 1428 socket.SOL_IP,
1429 1429 socket.IP_ADD_MEMBERSHIP,
1430 1430 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'),
1431 1431 )
1432 1432
1433 1433 self.listeners = []
1434 1434 self.browsers = []
1435 1435 self.services = {}
1436 1436 self.servicetypes = {}
1437 1437
1438 1438 self.cache = DNSCache()
1439 1439
1440 1440 self.condition = threading.Condition()
1441 1441
1442 1442 self.engine = Engine(self)
1443 1443 self.listener = Listener(self)
1444 1444 self.reaper = Reaper(self)
1445 1445
1446 1446 def isLoopback(self):
1447 1447 return self.intf.startswith(b"127.0.0.1")
1448 1448
1449 1449 def isLinklocal(self):
1450 1450 return self.intf.startswith(b"169.254.")
1451 1451
1452 1452 def wait(self, timeout):
1453 1453 """Calling thread waits for a given number of milliseconds or
1454 1454 until notified."""
1455 1455 self.condition.acquire()
1456 1456 self.condition.wait(timeout / 1000)
1457 1457 self.condition.release()
1458 1458
1459 1459 def notifyAll(self):
1460 1460 """Notifies all waiting threads"""
1461 1461 self.condition.acquire()
1462 1462 self.condition.notify_all()
1463 1463 self.condition.release()
1464 1464
1465 1465 def getServiceInfo(self, type, name, timeout=3000):
1466 1466 """Returns network's service information for a particular
1467 1467 name and type, or None if no service matches by the timeout,
1468 1468 which defaults to 3 seconds."""
1469 1469 info = ServiceInfo(type, name)
1470 1470 if info.request(self, timeout):
1471 1471 return info
1472 1472 return None
1473 1473
1474 1474 def addServiceListener(self, type, listener):
1475 1475 """Adds a listener for a particular service type. This object
1476 1476 will then have its updateRecord method called when information
1477 1477 arrives for that type."""
1478 1478 self.removeServiceListener(listener)
1479 1479 self.browsers.append(ServiceBrowser(self, type, listener))
1480 1480
1481 1481 def removeServiceListener(self, listener):
1482 1482 """Removes a listener from the set that is currently listening."""
1483 1483 for browser in self.browsers:
1484 1484 if browser.listener == listener:
1485 1485 browser.cancel()
1486 1486 del browser
1487 1487
1488 1488 def registerService(self, info, ttl=_DNS_TTL):
1489 1489 """Registers service information to the network with a default TTL
1490 1490 of 60 seconds. Zeroconf will then respond to requests for
1491 1491 information for that service. The name of the service may be
1492 1492 changed if needed to make it unique on the network."""
1493 1493 self.checkService(info)
1494 1494 self.services[info.name.lower()] = info
1495 1495 if info.type in self.servicetypes:
1496 1496 self.servicetypes[info.type] += 1
1497 1497 else:
1498 1498 self.servicetypes[info.type] = 1
1499 1499 now = currentTimeMillis()
1500 1500 nexttime = now
1501 1501 i = 0
1502 1502 while i < 3:
1503 1503 if now < nexttime:
1504 1504 self.wait(nexttime - now)
1505 1505 now = currentTimeMillis()
1506 1506 continue
1507 1507 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1508 1508 out.addAnswerAtTime(
1509 1509 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, ttl, info.name), 0
1510 1510 )
1511 1511 out.addAnswerAtTime(
1512 1512 DNSService(
1513 1513 info.name,
1514 1514 _TYPE_SRV,
1515 1515 _CLASS_IN,
1516 1516 ttl,
1517 1517 info.priority,
1518 1518 info.weight,
1519 1519 info.port,
1520 1520 info.server,
1521 1521 ),
1522 1522 0,
1523 1523 )
1524 1524 out.addAnswerAtTime(
1525 1525 DNSText(info.name, _TYPE_TXT, _CLASS_IN, ttl, info.text), 0
1526 1526 )
1527 1527 if info.address:
1528 1528 out.addAnswerAtTime(
1529 1529 DNSAddress(
1530 1530 info.server, _TYPE_A, _CLASS_IN, ttl, info.address
1531 1531 ),
1532 1532 0,
1533 1533 )
1534 1534 self.send(out)
1535 1535 i += 1
1536 1536 nexttime += _REGISTER_TIME
1537 1537
1538 1538 def unregisterService(self, info):
1539 1539 """Unregister a service."""
1540 1540 try:
1541 1541 del self.services[info.name.lower()]
1542 1542 if self.servicetypes[info.type] > 1:
1543 1543 self.servicetypes[info.type] -= 1
1544 1544 else:
1545 1545 del self.servicetypes[info.type]
1546 1546 except KeyError:
1547 1547 pass
1548 1548 now = currentTimeMillis()
1549 1549 nexttime = now
1550 1550 i = 0
1551 1551 while i < 3:
1552 1552 if now < nexttime:
1553 1553 self.wait(nexttime - now)
1554 1554 now = currentTimeMillis()
1555 1555 continue
1556 1556 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1557 1557 out.addAnswerAtTime(
1558 1558 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, 0, info.name), 0
1559 1559 )
1560 1560 out.addAnswerAtTime(
1561 1561 DNSService(
1562 1562 info.name,
1563 1563 _TYPE_SRV,
1564 1564 _CLASS_IN,
1565 1565 0,
1566 1566 info.priority,
1567 1567 info.weight,
1568 1568 info.port,
1569 1569 info.name,
1570 1570 ),
1571 1571 0,
1572 1572 )
1573 1573 out.addAnswerAtTime(
1574 1574 DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text), 0
1575 1575 )
1576 1576 if info.address:
1577 1577 out.addAnswerAtTime(
1578 1578 DNSAddress(
1579 1579 info.server, _TYPE_A, _CLASS_IN, 0, info.address
1580 1580 ),
1581 1581 0,
1582 1582 )
1583 1583 self.send(out)
1584 1584 i += 1
1585 1585 nexttime += _UNREGISTER_TIME
1586 1586
1587 1587 def unregisterAllServices(self):
1588 1588 """Unregister all registered services."""
1589 1589 if len(self.services) > 0:
1590 1590 now = currentTimeMillis()
1591 1591 nexttime = now
1592 1592 i = 0
1593 1593 while i < 3:
1594 1594 if now < nexttime:
1595 1595 self.wait(nexttime - now)
1596 1596 now = currentTimeMillis()
1597 1597 continue
1598 1598 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1599 1599 for info in self.services.values():
1600 1600 out.addAnswerAtTime(
1601 1601 DNSPointer(
1602 1602 info.type, _TYPE_PTR, _CLASS_IN, 0, info.name
1603 1603 ),
1604 1604 0,
1605 1605 )
1606 1606 out.addAnswerAtTime(
1607 1607 DNSService(
1608 1608 info.name,
1609 1609 _TYPE_SRV,
1610 1610 _CLASS_IN,
1611 1611 0,
1612 1612 info.priority,
1613 1613 info.weight,
1614 1614 info.port,
1615 1615 info.server,
1616 1616 ),
1617 1617 0,
1618 1618 )
1619 1619 out.addAnswerAtTime(
1620 1620 DNSText(info.name, _TYPE_TXT, _CLASS_IN, 0, info.text),
1621 1621 0,
1622 1622 )
1623 1623 if info.address:
1624 1624 out.addAnswerAtTime(
1625 1625 DNSAddress(
1626 1626 info.server, _TYPE_A, _CLASS_IN, 0, info.address
1627 1627 ),
1628 1628 0,
1629 1629 )
1630 1630 self.send(out)
1631 1631 i += 1
1632 1632 nexttime += _UNREGISTER_TIME
1633 1633
1634 1634 def checkService(self, info):
1635 1635 """Checks the network for a unique service name, modifying the
1636 1636 ServiceInfo passed in if it is not unique."""
1637 1637 now = currentTimeMillis()
1638 1638 nexttime = now
1639 1639 i = 0
1640 1640 while i < 3:
1641 1641 for record in self.cache.entriesWithName(info.type):
1642 1642 if (
1643 1643 record.type == _TYPE_PTR
1644 1644 and not record.isExpired(now)
1645 1645 and record.alias == info.name
1646 1646 ):
1647 1647 if info.name.find(b'.') < 0:
1648 1648 info.name = b"%s.[%s:%d].%s" % (
1649 1649 info.name,
1650 1650 info.address,
1651 1651 info.port,
1652 1652 info.type,
1653 1653 )
1654 1654 self.checkService(info)
1655 1655 return
1656 1656 raise NonUniqueNameException
1657 1657 if now < nexttime:
1658 1658 self.wait(nexttime - now)
1659 1659 now = currentTimeMillis()
1660 1660 continue
1661 1661 out = DNSOutgoing(_FLAGS_QR_QUERY | _FLAGS_AA)
1662 1662 self.debug = out
1663 1663 out.addQuestion(DNSQuestion(info.type, _TYPE_PTR, _CLASS_IN))
1664 1664 out.addAuthoritativeAnswer(
1665 1665 DNSPointer(info.type, _TYPE_PTR, _CLASS_IN, _DNS_TTL, info.name)
1666 1666 )
1667 1667 self.send(out)
1668 1668 i += 1
1669 1669 nexttime += _CHECK_TIME
1670 1670
1671 1671 def addListener(self, listener, question):
1672 1672 """Adds a listener for a given question. The listener will have
1673 1673 its updateRecord method called when information is available to
1674 1674 answer the question."""
1675 1675 now = currentTimeMillis()
1676 1676 self.listeners.append(listener)
1677 1677 if question is not None:
1678 1678 for record in self.cache.entriesWithName(question.name):
1679 1679 if question.answeredBy(record) and not record.isExpired(now):
1680 1680 listener.updateRecord(self, now, record)
1681 1681 self.notifyAll()
1682 1682
1683 1683 def removeListener(self, listener):
1684 1684 """Removes a listener."""
1685 1685 try:
1686 1686 self.listeners.remove(listener)
1687 1687 self.notifyAll()
1688 1688 except Exception:
1689 1689 pass
1690 1690
1691 1691 def updateRecord(self, now, rec):
1692 1692 """Used to notify listeners of new information that has updated
1693 1693 a record."""
1694 1694 for listener in self.listeners:
1695 1695 listener.updateRecord(self, now, rec)
1696 1696 self.notifyAll()
1697 1697
1698 1698 def handleResponse(self, msg):
1699 1699 """Deal with incoming response packets. All answers
1700 1700 are held in the cache, and listeners are notified."""
1701 1701 now = currentTimeMillis()
1702 1702 for record in msg.answers:
1703 1703 expired = record.isExpired(now)
1704 1704 if record in self.cache.entries():
1705 1705 if expired:
1706 1706 self.cache.remove(record)
1707 1707 else:
1708 1708 entry = self.cache.get(record)
1709 1709 if entry is not None:
1710 1710 entry.resetTTL(record)
1711 1711 record = entry
1712 1712 else:
1713 1713 self.cache.add(record)
1714 1714
1715 1715 self.updateRecord(now, record)
1716 1716
1717 1717 def handleQuery(self, msg, addr, port):
1718 1718 """Deal with incoming query packets. Provides a response if
1719 1719 possible."""
1720 1720 out = None
1721 1721
1722 1722 # Support unicast client responses
1723 1723 #
1724 1724 if port != _MDNS_PORT:
1725 1725 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA, 0)
1726 1726 for question in msg.questions:
1727 1727 out.addQuestion(question)
1728 1728
1729 1729 for question in msg.questions:
1730 1730 if question.type == _TYPE_PTR:
1731 1731 if question.name == b"_services._dns-sd._udp.local.":
1732 1732 for stype in self.servicetypes.keys():
1733 1733 if out is None:
1734 1734 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1735 1735 out.addAnswer(
1736 1736 msg,
1737 1737 DNSPointer(
1738 1738 b"_services._dns-sd._udp.local.",
1739 1739 _TYPE_PTR,
1740 1740 _CLASS_IN,
1741 1741 _DNS_TTL,
1742 1742 stype,
1743 1743 ),
1744 1744 )
1745 1745 for service in self.services.values():
1746 1746 if question.name == service.type:
1747 1747 if out is None:
1748 1748 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1749 1749 out.addAnswer(
1750 1750 msg,
1751 1751 DNSPointer(
1752 1752 service.type,
1753 1753 _TYPE_PTR,
1754 1754 _CLASS_IN,
1755 1755 _DNS_TTL,
1756 1756 service.name,
1757 1757 ),
1758 1758 )
1759 1759 else:
1760 1760 try:
1761 1761 if out is None:
1762 1762 out = DNSOutgoing(_FLAGS_QR_RESPONSE | _FLAGS_AA)
1763 1763
1764 1764 # Answer A record queries for any service addresses we know
1765 1765 if question.type == _TYPE_A or question.type == _TYPE_ANY:
1766 1766 for service in self.services.values():
1767 1767 if service.server == question.name.lower():
1768 1768 out.addAnswer(
1769 1769 msg,
1770 1770 DNSAddress(
1771 1771 question.name,
1772 1772 _TYPE_A,
1773 1773 _CLASS_IN | _CLASS_UNIQUE,
1774 1774 _DNS_TTL,
1775 1775 service.address,
1776 1776 ),
1777 1777 )
1778 1778
1779 1779 service = self.services.get(question.name.lower(), None)
1780 1780 if not service:
1781 1781 continue
1782 1782
1783 1783 if question.type == _TYPE_SRV or question.type == _TYPE_ANY:
1784 1784 out.addAnswer(
1785 1785 msg,
1786 1786 DNSService(
1787 1787 question.name,
1788 1788 _TYPE_SRV,
1789 1789 _CLASS_IN | _CLASS_UNIQUE,
1790 1790 _DNS_TTL,
1791 1791 service.priority,
1792 1792 service.weight,
1793 1793 service.port,
1794 1794 service.server,
1795 1795 ),
1796 1796 )
1797 1797 if question.type == _TYPE_TXT or question.type == _TYPE_ANY:
1798 1798 out.addAnswer(
1799 1799 msg,
1800 1800 DNSText(
1801 1801 question.name,
1802 1802 _TYPE_TXT,
1803 1803 _CLASS_IN | _CLASS_UNIQUE,
1804 1804 _DNS_TTL,
1805 1805 service.text,
1806 1806 ),
1807 1807 )
1808 1808 if question.type == _TYPE_SRV:
1809 1809 out.addAdditionalAnswer(
1810 1810 DNSAddress(
1811 1811 service.server,
1812 1812 _TYPE_A,
1813 1813 _CLASS_IN | _CLASS_UNIQUE,
1814 1814 _DNS_TTL,
1815 1815 service.address,
1816 1816 )
1817 1817 )
1818 1818 except Exception:
1819 1819 traceback.print_exc()
1820 1820
1821 1821 if out is not None and out.answers:
1822 1822 out.id = msg.id
1823 1823 self.send(out, addr, port)
1824 1824
1825 1825 def send(self, out, addr=_MDNS_ADDR, port=_MDNS_PORT):
1826 1826 """Sends an outgoing packet."""
1827 1827 # This is a quick test to see if we can parse the packets we generate
1828 1828 # temp = DNSIncoming(out.packet())
1829 1829 try:
1830 1830 self.socket.sendto(out.packet(), 0, (addr, port))
1831 1831 except Exception:
1832 1832 # Ignore this, it may be a temporary loss of network connection
1833 1833 pass
1834 1834
1835 1835 def close(self):
1836 1836 """Ends the background threads, and prevent this instance from
1837 1837 servicing further queries."""
1838 1838 if globals()[b'_GLOBAL_DONE'] == 0:
1839 1839 globals()[b'_GLOBAL_DONE'] = 1
1840 1840 self.notifyAll()
1841 1841 self.engine.notify()
1842 1842 self.unregisterAllServices()
1843 1843 self.socket.setsockopt(
1844 1844 socket.SOL_IP,
1845 1845 socket.IP_DROP_MEMBERSHIP,
1846 1846 socket.inet_aton(_MDNS_ADDR) + socket.inet_aton('0.0.0.0'),
1847 1847 )
1848 1848 self.socket.close()
1849 1849
1850 1850
1851 1851 # Test a few module features, including service registration, service
1852 1852 # query (for Zoe), and service unregistration.
1853 1853
1854 1854 if __name__ == '__main__':
1855 1855 print(b"Multicast DNS Service Discovery for Python, version", __version__)
1856 1856 r = Zeroconf()
1857 1857 print(b"1. Testing registration of a service...")
1858 1858 desc = {b'version': b'0.10', b'a': b'test value', b'b': b'another value'}
1859 1859 info = ServiceInfo(
1860 1860 b"_http._tcp.local.",
1861 1861 b"My Service Name._http._tcp.local.",
1862 socket.inet_aton(b"127.0.0.1"),
1862 socket.inet_aton("127.0.0.1"),
1863 1863 1234,
1864 1864 0,
1865 1865 0,
1866 1866 desc,
1867 1867 )
1868 1868 print(b" Registering service...")
1869 1869 r.registerService(info)
1870 1870 print(b" Registration done.")
1871 1871 print(b"2. Testing query of service information...")
1872 1872 print(
1873 1873 b" Getting ZOE service:",
1874 1874 str(r.getServiceInfo(b"_http._tcp.local.", b"ZOE._http._tcp.local.")),
1875 1875 )
1876 1876 print(b" Query done.")
1877 1877 print(b"3. Testing query of own service...")
1878 1878 print(
1879 1879 b" Getting self:",
1880 1880 str(
1881 1881 r.getServiceInfo(
1882 1882 b"_http._tcp.local.", b"My Service Name._http._tcp.local."
1883 1883 )
1884 1884 ),
1885 1885 )
1886 1886 print(b" Query done.")
1887 1887 print(b"4. Testing unregister of service information...")
1888 1888 r.unregisterService(info)
1889 1889 print(b" Unregister done.")
1890 1890 r.close()
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